Quincy Hsieh commited on
Commit
b263309
·
1 Parent(s): 0aebf2c

Add sample code of RAG API

Browse files
Files changed (3) hide show
  1. README.md +330 -6
  2. app.py +471 -4
  3. requirements.txt +14 -0
README.md CHANGED
@@ -1,14 +1,338 @@
1
  ---
2
  title: Example App Hackathon Gustave Eiffel 2026
3
- emoji: 💻
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Example App Hackathon Gustave Eiffel 2026
3
+ emoji: �
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
+ python_version: '3.11'
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  ---
13
 
14
+ # 🗼 RAG Chat API — Gustave Eiffel Hackathon 2026
15
+
16
+ A complete **Retrieval-Augmented Generation (RAG)** system deployed as a Hugging Face Space, with a `/query` API endpoint designed for the RAG evaluation system.
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ 1. [Overview](#overview)
23
+ 2. [Architecture](#architecture)
24
+ 3. [Step-by-Step Explanation](#step-by-step-explanation)
25
+ 4. [API Endpoints](#api-endpoints)
26
+ 5. [Setup & Deployment](#setup--deployment)
27
+ 6. [Configuration](#configuration)
28
+ 7. [How It Works (Detailed)](#how-it-works-detailed)
29
+
30
+ ---
31
+
32
+ ## Overview
33
+
34
+ This application demonstrates how to build a production-ready RAG system within the Hugging Face ecosystem. It covers:
35
+
36
+ | Requirement | Solution |
37
+ |---|---|
38
+ | LLM API calls | Hugging Face Inference API (`InferenceClient`) |
39
+ | Text → Embeddings | `sentence-transformers/all-MiniLM-L6-v2` (runs locally) |
40
+ | Vector Store | ChromaDB (persistent, runs in-process) |
41
+ | API Endpoint | FastAPI with `POST /query` |
42
+ | UI | Gradio Blocks (chat + document ingestion) |
43
+
44
+ ---
45
+
46
+ ## Architecture
47
+
48
+ ```
49
+ ┌─────────────────────────────────────────────────────────────┐
50
+ │ Hugging Face Space │
51
+ │ │
52
+ │ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
53
+ │ │ Gradio │ │ FastAPI │ │ ChromaDB │ │
54
+ │ │ UI │────▶│ /query │────▶│ Vector Store │ │
55
+ │ │ │ │ /ingest │ │ (persistent) │ │
56
+ │ └──────────┘ └──────┬───────┘ └───────────────┘ │
57
+ │ │ ▲ │
58
+ │ ▼ │ │
59
+ │ ┌──────────────────┐ ┌─────────────────┐ │
60
+ │ │ HF Inference │ │ Sentence │ │
61
+ │ │ API (LLM) │ │ Transformers │ │
62
+ │ │ Zephyr-7B │ │ (Embeddings) │ │
63
+ │ └──────────────────┘ └─────────────────┘ │
64
+ └─────────────────────────────────────────────────────────────┘
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Step-by-Step Explanation
70
+
71
+ ### Step 1: Document Ingestion & Chunking
72
+
73
+ Before we can answer questions, we need to prepare our knowledge base.
74
+
75
+ 1. **Load documents** — Read text files from `sample_documents/` directory
76
+ 2. **Chunk text** — Split documents into smaller overlapping chunks (512 tokens, 50 token overlap) using `RecursiveCharacterTextSplitter`. This ensures each chunk fits within the embedding model's context window while maintaining semantic coherence.
77
+
78
+ ```python
79
+ splitter = RecursiveCharacterTextSplitter(
80
+ chunk_size=512,
81
+ chunk_overlap=50,
82
+ separators=["\n\n", "\n", ". ", " ", ""],
83
+ )
84
+ chunks = splitter.split_text(document_text)
85
+ ```
86
+
87
+ ### Step 2: Generate Embeddings
88
+
89
+ Convert text chunks into dense vector representations that capture semantic meaning.
90
+
91
+ 1. **Load model** — We use `sentence-transformers/all-MiniLM-L6-v2`, a lightweight model that produces 384-dimensional vectors
92
+ 2. **Encode text** — Each chunk is transformed into a fixed-size vector where semantically similar texts are closer together in vector space
93
+
94
+ ```python
95
+ from sentence_transformers import SentenceTransformer
96
+
97
+ model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
98
+ embeddings = model.encode(["chunk 1 text", "chunk 2 text"])
99
+ # Result: numpy array of shape (2, 384)
100
+ ```
101
+
102
+ ### Step 3: Store in Vector Database (ChromaDB)
103
+
104
+ Persist embeddings in a vector store optimized for similarity search.
105
+
106
+ 1. **Initialize ChromaDB** — Create a persistent client that stores data on disk (survives Space restarts)
107
+ 2. **Create collection** — A named collection with cosine similarity metric
108
+ 3. **Add documents** — Store embeddings alongside the original text and metadata
109
+
110
+ ```python
111
+ import chromadb
112
+
113
+ client = chromadb.PersistentClient(path="./chroma_db")
114
+ collection = client.get_or_create_collection(
115
+ name="rag_documents",
116
+ metadata={"hnsw:space": "cosine"},
117
+ )
118
+ collection.add(
119
+ ids=["doc_0", "doc_1"],
120
+ embeddings=embeddings.tolist(),
121
+ documents=["chunk 1 text", "chunk 2 text"],
122
+ metadatas=[{"source": "file.txt"}, {"source": "file.txt"}],
123
+ )
124
+ ```
125
+
126
+ ### Step 4: Query & Retrieval
127
+
128
+ When a user asks a question, find the most relevant context.
129
+
130
+ 1. **Embed the query** — Use the same embedding model to convert the question to a vector
131
+ 2. **Similarity search** — Find the top-K nearest vectors in ChromaDB (cosine similarity)
132
+ 3. **Return context** — Extract the original text chunks for the closest matches
133
+
134
+ ```python
135
+ query_embedding = model.encode(["What is the Eiffel Tower?"])
136
+ results = collection.query(
137
+ query_embeddings=query_embedding.tolist(),
138
+ n_results=3,
139
+ )
140
+ ```
141
+
142
+ ### Step 5: LLM Generation (Augmented Response)
143
+
144
+ Combine retrieved context with the user's question and generate an answer.
145
+
146
+ 1. **Build prompt** — Create a structured prompt with system instructions, retrieved context, and the user's question
147
+ 2. **Call LLM API** — Send the prompt to the Hugging Face Inference API (Zephyr-7B)
148
+ 3. **Return response** — The LLM generates an answer grounded in the provided context
149
+
150
+ ```python
151
+ from huggingface_hub import InferenceClient
152
+
153
+ client = InferenceClient(model="HuggingFaceH4/zephyr-7b-beta", token=HF_TOKEN)
154
+ response = client.text_generation(prompt, max_new_tokens=512)
155
+ ```
156
+
157
+ ### Step 6: API Endpoint (`/query`)
158
+
159
+ The FastAPI endpoint ties everything together for the evaluation system.
160
+
161
+ ```python
162
+ @app.post("/query")
163
+ async def query_endpoint(request: QueryRequest):
164
+ # 1. Retrieve relevant context
165
+ # 2. Build augmented prompt
166
+ # 3. Generate LLM response
167
+ # 4. Return answer + sources
168
+ result = rag_query(request.query, top_k=request.top_k)
169
+ return JSONResponse(content=result)
170
+ ```
171
+
172
+ ---
173
+
174
+ ## API Endpoints
175
+
176
+ ### `POST /query`
177
+
178
+ The primary endpoint for the RAG evaluation system.
179
+
180
+ **Request:**
181
+ ```json
182
+ {
183
+ "query": "What materials is the Eiffel Tower made of?",
184
+ "top_k": 3
185
+ }
186
+ ```
187
+
188
+ **Response:**
189
+ ```json
190
+ {
191
+ "answer": "The Eiffel Tower is made of wrought iron (puddled iron)...",
192
+ "sources": [
193
+ {"source": "eiffel_tower.txt", "score": 0.87},
194
+ {"source": "paris_landmarks.txt", "score": 0.72}
195
+ ],
196
+ "query": "What materials is the Eiffel Tower made of?"
197
+ }
198
+ ```
199
+
200
+ ### `POST /ingest`
201
+
202
+ Add new documents to the knowledge base.
203
+
204
+ **Request:**
205
+ ```json
206
+ {
207
+ "text": "The Eiffel Tower was built in 1889...",
208
+ "source": "my_document.txt"
209
+ }
210
+ ```
211
+
212
+ **Response:**
213
+ ```json
214
+ {
215
+ "status": "success",
216
+ "chunks_added": 5,
217
+ "total_chunks": 42
218
+ }
219
+ ```
220
+
221
+ ### `GET /health`
222
+
223
+ System health check.
224
+
225
+ **Response:**
226
+ ```json
227
+ {
228
+ "status": "healthy",
229
+ "documents_in_store": 42,
230
+ "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
231
+ "llm_model": "HuggingFaceH4/zephyr-7b-beta"
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Setup & Deployment
238
+
239
+ ### Prerequisites
240
+
241
+ - Python 3.11+
242
+ - A Hugging Face account with an API token
243
+
244
+ ### Local Development
245
+
246
+ ```bash
247
+ # Clone the repository
248
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/Example-App-Hackathon-Gustave-Eiffel-2026
249
+ cd Example-App-Hackathon-Gustave-Eiffel-2026
250
+
251
+ # Install dependencies
252
+ pip install -r requirements.txt
253
+
254
+ # Set your HF token
255
+ export HF_TOKEN="hf_your_token_here"
256
+
257
+ # Run the application
258
+ python app.py
259
+ # Server starts at http://localhost:7860
260
+ ```
261
+
262
+ ### Deploy to Hugging Face Spaces
263
+
264
+ 1. Create a new Space on huggingface.co (select Gradio SDK)
265
+ 2. Add `HF_TOKEN` as a Space Secret (Settings → Secrets)
266
+ 3. Push this code to the Space repository
267
+ 4. The Space automatically installs dependencies and starts the app
268
+
269
+ ### Testing the API
270
+
271
+ ```bash
272
+ # Health check
273
+ curl http://localhost:7860/health
274
+
275
+ # Query the RAG system
276
+ curl -X POST http://localhost:7860/query \
277
+ -H "Content-Type: application/json" \
278
+ -d '{"query": "What is the Eiffel Tower?", "top_k": 3}'
279
+
280
+ # Ingest a new document
281
+ curl -X POST http://localhost:7860/ingest \
282
+ -H "Content-Type: application/json" \
283
+ -d '{"text": "Your document text here...", "source": "new_doc.txt"}'
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Configuration
289
+
290
+ | Variable | Default | Description |
291
+ |---|---|---|
292
+ | `HF_TOKEN` | (Space Secret) | Hugging Face API token for Inference API |
293
+ | `EMBEDDING_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Model for text embeddings |
294
+ | `LLM_MODEL_NAME` | `HuggingFaceH4/zephyr-7b-beta` | LLM for answer generation |
295
+ | `CHROMA_PERSIST_DIR` | `./chroma_db` | ChromaDB storage path |
296
+ | `CHUNK_SIZE` | `512` | Text chunk size in characters |
297
+ | `CHUNK_OVERLAP` | `50` | Overlap between chunks |
298
+ | `TOP_K_RESULTS` | `3` | Number of context chunks to retrieve |
299
+
300
+ ---
301
+
302
+ ## How It Works (Detailed)
303
+
304
+ ### Why RAG?
305
+
306
+ Large Language Models have a knowledge cutoff and can hallucinate. RAG solves this by:
307
+ - **Grounding** responses in actual documents
308
+ - **Updating** knowledge without retraining
309
+ - **Providing** source attribution for answers
310
+
311
+ ### Why ChromaDB in HF Spaces?
312
+
313
+ ChromaDB is ideal for Hugging Face Spaces because:
314
+ - Runs **in-process** (no external database needed)
315
+ - Supports **persistent storage** (survives Space restarts)
316
+ - Uses **HNSW index** for fast approximate nearest neighbor search
317
+ - Zero configuration required
318
+
319
+ ### Why Sentence Transformers?
320
+
321
+ - Runs **locally** within the Space (no API cost for embeddings)
322
+ - `all-MiniLM-L6-v2` is small (~80MB) but effective
323
+ - Produces **384-dimensional** vectors (good balance of quality vs. storage)
324
+ - Trained on 1B+ sentence pairs for semantic similarity
325
+
326
+ ### Evaluation Integration
327
+
328
+ The `/query` endpoint is designed to be called by external RAG evaluation frameworks. It returns:
329
+ - The generated `answer` for correctness evaluation
330
+ - Source `documents` for faithfulness/groundedness checks
331
+ - Similarity `scores` for retrieval quality assessment
332
+
333
+ ---
334
+
335
+ ## License
336
+
337
+ Apache 2.0
338
+
app.py CHANGED
@@ -1,7 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
1
+ """
2
+ RAG Chat API - Gustave Eiffel Hackathon 2026
3
+ =============================================
4
+
5
+ This application demonstrates a complete Retrieval-Augmented Generation (RAG) system
6
+ deployed as a Hugging Face Space. It includes:
7
+
8
+ 1. /query endpoint - Called by the RAG evaluation system
9
+ 2. LLM API integration via Hugging Face Inference API
10
+ 3. Document embedding using sentence-transformers
11
+ 4. ChromaDB as the vector store (runs locally within HF Spaces)
12
+ 5. End-to-end RAG pipeline: ingest → embed → retrieve → generate
13
+
14
+ Architecture:
15
+ User Query → Embedding → Vector Search → Context Retrieval → LLM Generation → Response
16
+ """
17
+
18
+ import os
19
+ import logging
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
  import gradio as gr
24
+ from fastapi import FastAPI, HTTPException
25
+ from fastapi.responses import JSONResponse
26
+ from pydantic import BaseModel
27
+ import chromadb
28
+ from chromadb.config import Settings
29
+ from sentence_transformers import SentenceTransformer
30
+ from huggingface_hub import InferenceClient
31
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
32
+
33
+ logging.basicConfig(level=logging.INFO)
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Configuration
38
+ # ---------------------------------------------------------------------------
39
+
40
+ EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
41
+ LLM_MODEL_NAME = "HuggingFaceH4/zephyr-7b-beta"
42
+ CHROMA_PERSIST_DIR = "./chroma_db"
43
+ COLLECTION_NAME = "rag_documents"
44
+ CHUNK_SIZE = 512
45
+ CHUNK_OVERLAP = 50
46
+ TOP_K_RESULTS = 3
47
+
48
+ # HF_TOKEN is set as a Space secret for authenticated Inference API calls
49
+ HF_TOKEN = os.environ.get("HF_TOKEN")
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Step 1: Initialize the Embedding Model
53
+ # ---------------------------------------------------------------------------
54
+ # We use sentence-transformers to convert text chunks into dense vector embeddings.
55
+ # The model runs locally inside the Space — no external API call needed.
56
+
57
+ logger.info(f"Loading embedding model: {EMBEDDING_MODEL_NAME}")
58
+ embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME)
59
+ logger.info("Embedding model loaded successfully.")
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Step 2: Initialize the Vector Store (ChromaDB)
63
+ # ---------------------------------------------------------------------------
64
+ # ChromaDB runs in-process with persistent storage. This is ideal for HF Spaces
65
+ # because it requires no external database service.
66
+
67
+ logger.info(f"Initializing ChromaDB at: {CHROMA_PERSIST_DIR}")
68
+ chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIR)
69
+ collection = chroma_client.get_or_create_collection(
70
+ name=COLLECTION_NAME,
71
+ metadata={"hnsw:space": "cosine"},
72
+ )
73
+ logger.info(f"ChromaDB collection '{COLLECTION_NAME}' ready. Documents: {collection.count()}")
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Step 3: Initialize the LLM Client (Hugging Face Inference API)
77
+ # ---------------------------------------------------------------------------
78
+ # We use the Hugging Face Inference API to call a hosted LLM model.
79
+ # This avoids loading large models into the Space's limited memory.
80
+
81
+ llm_client = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN)
82
+ logger.info(f"LLM client initialized for model: {LLM_MODEL_NAME}")
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Helper Functions
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def chunk_text(text: str, source: str = "unknown") -> list[dict]:
90
+ """
91
+ Split a document into smaller chunks for embedding.
92
+
93
+ We use LangChain's RecursiveCharacterTextSplitter which intelligently
94
+ splits on paragraph/sentence boundaries to preserve semantic meaning.
95
+ """
96
+ splitter = RecursiveCharacterTextSplitter(
97
+ chunk_size=CHUNK_SIZE,
98
+ chunk_overlap=CHUNK_OVERLAP,
99
+ separators=["\n\n", "\n", ". ", " ", ""],
100
+ )
101
+ chunks = splitter.split_text(text)
102
+ return [{"text": chunk, "source": source, "chunk_index": i} for i, chunk in enumerate(chunks)]
103
+
104
+
105
+ def generate_embeddings(texts: list[str]) -> list[list[float]]:
106
+ """
107
+ Transform text chunks into vector embeddings using sentence-transformers.
108
+
109
+ This demonstrates Step 2a: How to convert documents into embeddings.
110
+ The model maps each text string to a 384-dimensional dense vector.
111
+ """
112
+ embeddings = embedding_model.encode(texts, show_progress_bar=False)
113
+ return embeddings.tolist()
114
+
115
+
116
+ def add_documents_to_vectorstore(documents: list[dict]) -> int:
117
+ """
118
+ Save document embeddings to the ChromaDB vector store.
119
+
120
+ This demonstrates Step 2c: How to persist embeddings for retrieval.
121
+ Each document gets a unique ID, its embedding vector, the raw text,
122
+ and metadata (source file, chunk index).
123
+ """
124
+ if not documents:
125
+ return 0
126
+
127
+ texts = [doc["text"] for doc in documents]
128
+ embeddings = generate_embeddings(texts)
129
+
130
+ existing_count = collection.count()
131
+ ids = [f"doc_{existing_count + i}" for i in range(len(documents))]
132
+ metadatas = [{"source": doc["source"], "chunk_index": doc["chunk_index"]} for doc in documents]
133
+
134
+ collection.add(
135
+ ids=ids,
136
+ embeddings=embeddings,
137
+ documents=texts,
138
+ metadatas=metadatas,
139
+ )
140
+
141
+ logger.info(f"Added {len(documents)} chunks to vector store. Total: {collection.count()}")
142
+ return len(documents)
143
+
144
+
145
+ def retrieve_relevant_context(query: str, top_k: int = TOP_K_RESULTS) -> list[dict]:
146
+ """
147
+ Retrieve the most relevant document chunks for a given query.
148
+
149
+ This is the "Retrieval" step in RAG:
150
+ 1. Embed the user's query using the same embedding model
151
+ 2. Search the vector store for the nearest neighbors
152
+ 3. Return the top-k most similar chunks as context
153
+ """
154
+ if collection.count() == 0:
155
+ return []
156
+
157
+ query_embedding = generate_embeddings([query])[0]
158
+
159
+ results = collection.query(
160
+ query_embeddings=[query_embedding],
161
+ n_results=min(top_k, collection.count()),
162
+ include=["documents", "metadatas", "distances"],
163
+ )
164
+
165
+ contexts = []
166
+ for i in range(len(results["documents"][0])):
167
+ contexts.append({
168
+ "text": results["documents"][0][i],
169
+ "source": results["metadatas"][0][i]["source"],
170
+ "similarity_score": 1 - results["distances"][0][i],
171
+ })
172
+
173
+ return contexts
174
+
175
+
176
+ def call_llm(prompt: str) -> str:
177
+ """
178
+ Make a call to the LLM via Hugging Face Inference API.
179
+
180
+ This demonstrates Step 2a: How to call a given LLM model API.
181
+ We use the text_generation endpoint with a structured prompt
182
+ that includes the retrieved context and the user's question.
183
+ """
184
+ try:
185
+ response = llm_client.text_generation(
186
+ prompt,
187
+ max_new_tokens=512,
188
+ temperature=0.7,
189
+ top_p=0.95,
190
+ repetition_penalty=1.1,
191
+ do_sample=True,
192
+ )
193
+ return response.strip()
194
+ except Exception as e:
195
+ logger.error(f"LLM API call failed: {e}")
196
+ raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
197
+
198
+
199
+ def build_rag_prompt(query: str, contexts: list[dict]) -> str:
200
+ """
201
+ Construct the RAG prompt by combining retrieved context with the user question.
202
+
203
+ The prompt template instructs the LLM to:
204
+ - Answer based ONLY on the provided context
205
+ - Acknowledge when information is insufficient
206
+ - Cite sources when possible
207
+ """
208
+ context_text = "\n\n".join(
209
+ f"[Source: {ctx['source']}]\n{ctx['text']}" for ctx in contexts
210
+ )
211
+
212
+ prompt = f"""<|system|>
213
+ You are a helpful assistant. Answer the user's question based ONLY on the provided context.
214
+ If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
215
+ Always be concise and factual.</s>
216
+ <|user|>
217
+ Context:
218
+ {context_text}
219
+
220
+ Question: {query}</s>
221
+ <|assistant|>
222
+ """
223
+ return prompt
224
+
225
+
226
+ def rag_query(query: str, top_k: int = TOP_K_RESULTS) -> dict:
227
+ """
228
+ End-to-end RAG pipeline: Query → Retrieve → Generate.
229
+
230
+ This demonstrates Step 2d: How to make a RAG system end-to-end.
231
+
232
+ Pipeline steps:
233
+ 1. Receive user query
234
+ 2. Retrieve relevant context from vector store
235
+ 3. Build augmented prompt with context
236
+ 4. Call LLM to generate answer
237
+ 5. Return answer with source metadata
238
+ """
239
+ # Step 1: Retrieve relevant document chunks
240
+ contexts = retrieve_relevant_context(query, top_k=top_k)
241
+
242
+ if not contexts:
243
+ return {
244
+ "answer": "No documents have been ingested yet. Please upload documents first.",
245
+ "sources": [],
246
+ "query": query,
247
+ }
248
+
249
+ # Step 2: Build the augmented prompt
250
+ prompt = build_rag_prompt(query, contexts)
251
+
252
+ # Step 3: Generate answer from LLM
253
+ answer = call_llm(prompt)
254
+
255
+ # Step 4: Return structured response
256
+ return {
257
+ "answer": answer,
258
+ "sources": [{"source": ctx["source"], "score": ctx["similarity_score"]} for ctx in contexts],
259
+ "query": query,
260
+ }
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Step 4: Ingest Sample Documents on Startup
265
+ # ---------------------------------------------------------------------------
266
+ # Load sample documents so the demo works out of the box.
267
+
268
+ SAMPLE_DOCS_DIR = Path("./sample_documents")
269
+
270
+
271
+ def ingest_sample_documents():
272
+ """Load and embed sample documents into the vector store on first run."""
273
+ if collection.count() > 0:
274
+ logger.info("Vector store already has documents, skipping ingestion.")
275
+ return
276
+
277
+ if not SAMPLE_DOCS_DIR.exists():
278
+ logger.warning("No sample_documents directory found.")
279
+ return
280
+
281
+ for file_path in SAMPLE_DOCS_DIR.glob("*.txt"):
282
+ logger.info(f"Ingesting: {file_path.name}")
283
+ text = file_path.read_text(encoding="utf-8")
284
+ chunks = chunk_text(text, source=file_path.name)
285
+ add_documents_to_vectorstore(chunks)
286
+
287
+ logger.info(f"Sample document ingestion complete. Total chunks: {collection.count()}")
288
+
289
+
290
+ ingest_sample_documents()
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # Step 5: FastAPI Application with /query Endpoint
295
+ # ---------------------------------------------------------------------------
296
+
297
+ app = FastAPI(
298
+ title="RAG Chat API - Gustave Eiffel Hackathon 2026",
299
+ description="A RAG system with /query endpoint for evaluation",
300
+ version="1.0.0",
301
+ )
302
+
303
+
304
+ class QueryRequest(BaseModel):
305
+ """Request schema for the /query endpoint."""
306
+ query: str
307
+ top_k: Optional[int] = TOP_K_RESULTS
308
+
309
+
310
+ class IngestRequest(BaseModel):
311
+ """Request schema for the /ingest endpoint."""
312
+ text: str
313
+ source: str = "user_upload"
314
+
315
+
316
+ @app.post("/query")
317
+ async def query_endpoint(request: QueryRequest):
318
+ """
319
+ RAG Query Endpoint - Called by the evaluation system.
320
+
321
+ Accepts a user query, retrieves relevant context from the vector store,
322
+ and generates an answer using the LLM.
323
+
324
+ Request body:
325
+ - query (str): The user's question
326
+ - top_k (int, optional): Number of context chunks to retrieve (default: 3)
327
+
328
+ Returns:
329
+ - answer (str): The generated answer
330
+ - sources (list): Source documents used for context
331
+ - query (str): The original query
332
+ """
333
+ logger.info(f"Query received: {request.query}")
334
+ result = rag_query(request.query, top_k=request.top_k)
335
+ return JSONResponse(content=result)
336
+
337
+
338
+ @app.post("/ingest")
339
+ async def ingest_endpoint(request: IngestRequest):
340
+ """
341
+ Document Ingestion Endpoint.
342
+
343
+ Accepts raw text, chunks it, generates embeddings, and stores in the vector store.
344
+
345
+ Request body:
346
+ - text (str): The document text to ingest
347
+ - source (str, optional): Source identifier for the document
348
+ """
349
+ chunks = chunk_text(request.text, source=request.source)
350
+ count = add_documents_to_vectorstore(chunks)
351
+ return JSONResponse(content={
352
+ "status": "success",
353
+ "chunks_added": count,
354
+ "total_chunks": collection.count(),
355
+ })
356
+
357
+
358
+ @app.get("/health")
359
+ async def health_check():
360
+ """Health check endpoint for monitoring."""
361
+ return {
362
+ "status": "healthy",
363
+ "documents_in_store": collection.count(),
364
+ "embedding_model": EMBEDDING_MODEL_NAME,
365
+ "llm_model": LLM_MODEL_NAME,
366
+ }
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # Step 6: Gradio UI for Interactive Demo
371
+ # ---------------------------------------------------------------------------
372
+
373
+ def gradio_query(question: str) -> str:
374
+ """Handle queries from the Gradio chat interface."""
375
+ if not question.strip():
376
+ return "Please enter a question."
377
+ result = rag_query(question)
378
+ sources_text = "\n".join(
379
+ f" - {s['source']} (relevance: {s['score']:.2f})" for s in result["sources"]
380
+ )
381
+ return f"{result['answer']}\n\n📚 Sources:\n{sources_text}" if result["sources"] else result["answer"]
382
+
383
+
384
+ def gradio_ingest(text: str, source_name: str) -> str:
385
+ """Handle document ingestion from the Gradio UI."""
386
+ if not text.strip():
387
+ return "Please provide text to ingest."
388
+ chunks = chunk_text(text, source=source_name or "user_upload")
389
+ count = add_documents_to_vectorstore(chunks)
390
+ return f"✅ Ingested {count} chunks. Total documents in store: {collection.count()}"
391
+
392
+
393
+ with gr.Blocks(title="RAG Chat API - Gustave Eiffel Hackathon") as demo:
394
+ gr.Markdown("""
395
+ # 🗼 RAG Chat API - Gustave Eiffel Hackathon 2026
396
+
397
+ This application demonstrates a complete **Retrieval-Augmented Generation (RAG)** system.
398
+
399
+ **API Endpoint:** Use `POST /query` with `{"query": "your question"}` for programmatic access.
400
+
401
+ ---
402
+ """)
403
+
404
+ with gr.Tab("💬 Chat"):
405
+ gr.Markdown("Ask questions about the ingested documents.")
406
+ with gr.Row():
407
+ query_input = gr.Textbox(
408
+ label="Your Question",
409
+ placeholder="e.g., What is the Eiffel Tower made of?",
410
+ lines=2,
411
+ )
412
+ query_button = gr.Button("Ask", variant="primary")
413
+ query_output = gr.Textbox(label="Answer", lines=8, interactive=False)
414
+ query_button.click(fn=gradio_query, inputs=query_input, outputs=query_output)
415
+
416
+ with gr.Tab("📄 Ingest Documents"):
417
+ gr.Markdown("Add new documents to the knowledge base.")
418
+ doc_text = gr.Textbox(
419
+ label="Document Text",
420
+ placeholder="Paste your document text here...",
421
+ lines=10,
422
+ )
423
+ doc_source = gr.Textbox(
424
+ label="Source Name",
425
+ placeholder="e.g., my_document.txt",
426
+ value="user_upload",
427
+ )
428
+ ingest_button = gr.Button("Ingest Document", variant="primary")
429
+ ingest_output = gr.Textbox(label="Status", interactive=False)
430
+ ingest_button.click(fn=gradio_ingest, inputs=[doc_text, doc_source], outputs=ingest_output)
431
+
432
+ with gr.Tab("ℹ️ API Info"):
433
+ gr.Markdown("""
434
+ ## API Endpoints
435
+
436
+ ### POST /query
437
+ ```json
438
+ {
439
+ "query": "What is the Eiffel Tower?",
440
+ "top_k": 3
441
+ }
442
+ ```
443
+
444
+ **Response:**
445
+ ```json
446
+ {
447
+ "answer": "The Eiffel Tower is...",
448
+ "sources": [{"source": "eiffel_tower.txt", "score": 0.85}],
449
+ "query": "What is the Eiffel Tower?"
450
+ }
451
+ ```
452
+
453
+ ### POST /ingest
454
+ ```json
455
+ {
456
+ "text": "Your document text here...",
457
+ "source": "document_name.txt"
458
+ }
459
+ ```
460
+
461
+ ### GET /health
462
+ Returns system health and document count.
463
+ """)
464
+
465
+ # Mount Gradio onto FastAPI so both the UI and API endpoints are served
466
+ app = gr.mount_gradio_app(app, demo, path="/")
467
 
468
+ # ---------------------------------------------------------------------------
469
+ # Entry Point
470
+ # ---------------------------------------------------------------------------
471
 
472
+ if __name__ == "__main__":
473
+ import uvicorn
474
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.0
3
+ gradio==4.44.0
4
+ chromadb==0.5.0
5
+ sentence-transformers==3.0.0
6
+ huggingface-hub==0.25.0
7
+ langchain==0.3.0
8
+ langchain-community==0.3.0
9
+ langchain-huggingface==0.1.0
10
+ langchain-chroma==0.2.0
11
+ langchain-text-splitters==0.3.0
12
+ pypdf==4.3.0
13
+ python-multipart==0.0.9
14
+ pydantic==2.9.0