Quincy Hsieh commited on
Commit
dddc53b
Β·
1 Parent(s): 53cab5b

Update README for data sync

Browse files
Files changed (1) hide show
  1. README.md +155 -52
README.md CHANGED
@@ -36,8 +36,8 @@ This application demonstrates how to build a production-ready RAG system within
36
 
37
  | Requirement | Solution |
38
  |---|---|
39
- | LLM API calls | Hugging Face Inference API (`InferenceClient`) |
40
- | Text β†’ Embeddings | `sentence-transformers/all-MiniLM-L6-v2` (runs locally) |
41
  | Vector Store | ChromaDB (persistent, runs in-process) |
42
  | API Endpoint | FastAPI with `POST /query` |
43
  | UI | Gradio Blocks (chat + document ingestion) |
@@ -58,9 +58,9 @@ This application demonstrates how to build a production-ready RAG system within
58
  β”‚ β”‚ β–² β”‚
59
  β”‚ β–Ό β”‚ β”‚
60
  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
61
- β”‚ β”‚ HF Inference β”‚ β”‚ Sentence β”‚ β”‚
62
- β”‚ β”‚ API (LLM) β”‚ β”‚ Transformers β”‚ β”‚
63
- β”‚ β”‚ Qwen2.5-72B β”‚ β”‚ (Embeddings) β”‚ β”‚
64
  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
65
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
66
  ```
@@ -89,15 +89,16 @@ chunks = splitter.split_text(document_text)
89
 
90
  Convert text chunks into dense vector representations that capture semantic meaning.
91
 
92
- 1. **Load model** β€” We use `sentence-transformers/all-MiniLM-L6-v2`, a lightweight model that produces 384-dimensional vectors
93
  2. **Encode text** β€” Each chunk is transformed into a fixed-size vector where semantically similar texts are closer together in vector space
94
 
95
  ```python
96
- from sentence_transformers import SentenceTransformer
97
 
98
- model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
99
- embeddings = model.encode(["chunk 1 text", "chunk 2 text"])
100
- # Result: numpy array of shape (2, 384)
 
101
  ```
102
 
103
  ### Step 3: Store in Vector Database (ChromaDB)
@@ -128,14 +129,14 @@ collection.add(
128
 
129
  When a user asks a question, find the most relevant context.
130
 
131
- 1. **Embed the query** β€” Use the same embedding model to convert the question to a vector
132
  2. **Similarity search** β€” Find the top-K nearest vectors in ChromaDB (cosine similarity)
133
  3. **Return context** β€” Extract the original text chunks for the closest matches
134
 
135
  ```python
136
- query_embedding = model.encode(["What is the Eiffel Tower?"])
137
  results = collection.query(
138
- query_embeddings=query_embedding.tolist(),
139
  n_results=3,
140
  )
141
  ```
@@ -145,7 +146,7 @@ results = collection.query(
145
  Combine retrieved context with the user's question and generate an answer.
146
 
147
  1. **Build prompt** β€” Load the template from [`prompts/rag_prompt.txt`](prompts/rag_prompt.txt), inject retrieved context and the user's question
148
- 2. **Call LLM API** β€” Send the prompt to the Hugging Face Inference API (`chat_completion`, Qwen2.5-72B-Instruct)
149
  3. **Return response** β€” The LLM generates an answer grounded in the provided context
150
 
151
  The prompt template (`prompts/rag_prompt.txt`):
@@ -168,11 +169,16 @@ RAG_PROMPT_TEMPLATE = Path("prompts/rag_prompt.txt").read_text(encoding="utf-8")
168
 
169
  # At query time:
170
  prompt = RAG_PROMPT_TEMPLATE.format(context=context_text, question=user_query)
171
- response = llm_client.chat_completion(
172
- messages=[{"role": "user", "content": prompt}],
173
- max_tokens=512,
174
- )
175
- answer = response.choices[0].message.content
 
 
 
 
 
176
  ```
177
 
178
  > **Tip:** Edit `prompts/rag_prompt.txt` to tune the model's behaviour (tone, language, output format) without touching application code.
@@ -250,8 +256,8 @@ System health check.
250
  {
251
  "status": "healthy",
252
  "documents_in_store": 42,
253
- "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
254
- "llm_model": "HuggingFaceH4/zephyr-7b-beta"
255
  }
256
  ```
257
 
@@ -295,11 +301,11 @@ Once your virtual environment is active, install dependencies and run the app:
295
  # Install dependencies
296
  pip install -r requirements.txt
297
 
298
- # Set your HF token
299
  # macOS / Linux
300
- export HF_TOKEN="hf_your_token_here"
301
  # Windows (PowerShell)
302
- $env:HF_TOKEN = "hf_your_token_here"
303
 
304
  # Run the application
305
  python app.py
@@ -311,7 +317,7 @@ python app.py
311
  ### Deploy to Hugging Face Spaces
312
 
313
  1. Create a new Space on huggingface.co (select Gradio SDK)
314
- 2. Add `HF_TOKEN` as a Space Secret (Settings β†’ Secrets)
315
  3. Push this code to the Space repository
316
  4. The Space automatically installs dependencies and starts the app
317
 
@@ -336,10 +342,32 @@ curl -X POST http://localhost:7860/ingest \
336
 
337
  ## Adding Binary Files to the HF Space
338
 
339
- Large or binary files (PDFs, pre-built ChromaDB databases, datasets, model weights) are stored in a **Hugging Face bucket** and mounted into the Space container at `/data`. The `hf sync` command keeps your local folder in sync with the bucket.
340
 
341
  > **Note:** Git LFS is not supported for Hugging Face Spaces persistent storage. Use the bucket + `hf sync` workflow described here instead.
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  ### Prerequisites
344
 
345
  ```bash
@@ -359,28 +387,24 @@ hf auth login
359
  3. Under **"Attach storage"**, select the existing bucket **`millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage`** (or create a new one)
360
  4. Save β€” Hugging Face will mount the bucket at `/data` inside the Space container
361
 
362
- ### Step 2 β€” Upload Files from Your Local Machine
363
 
364
  Place all persistent files under a local `./data` folder. The expected structure is:
365
 
366
  ```
367
  ./data/
368
- β”œβ”€β”€ chroma_db/ # Pre-built ChromaDB vector store
369
  β”‚ └── chroma.sqlite3
370
- β”œβ”€β”€ sample_documents/ # Text/PDF files ingested on startup
371
- β”‚ β”œβ”€β”€ eiffel_tower.txt
372
- β”‚ └── gustave_eiffel.txt
373
- └── DataSet/ # Training/test datasets (PDFs, CSVs, etc.)
374
- β”œβ”€β”€ Automobile/
375
- β”œβ”€β”€ Climatique/
376
  └── ...
377
  ```
378
 
379
- Then push:
380
-
381
  ```bash
382
- # Sync the entire ./data folder to the bucket
383
- hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage
384
 
385
  # Sync only a specific sub-folder (e.g., the vector store)
386
  hf sync ./data/chroma_db hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage/chroma_db
@@ -390,7 +414,9 @@ hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-
390
  --include "*.pdf" --include "*.sqlite3"
391
  ```
392
 
393
- `hf sync` is **incremental** β€” it computes checksums and only uploads files that have changed.
 
 
394
 
395
  ### Step 3 β€” Download the Bucket to Another Machine
396
 
@@ -400,11 +426,33 @@ To pull the latest bucket contents back to a local `./data` folder (e.g., on a n
400
  hf sync hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage ./data
401
  ```
402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  ### Accessing Files in the Space Application
404
 
405
- Once the bucket is mounted, files appear under `/data` inside the Space container regardless of their path in the bucket.
406
 
407
- The application resolves this automatically via a single `DATA_DIR` constant in `app.py`:
408
 
409
  ```python
410
  from pathlib import Path
@@ -436,11 +484,61 @@ hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-
436
 
437
  ## Configuration
438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  | Variable | Default | Description |
440
  |---|---|---|
441
- | `HF_TOKEN` | (Space Secret) | Hugging Face API token for Inference API |
442
- | `EMBEDDING_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Model for text embeddings |
443
- | `LLM_MODEL_NAME` | `Qwen/Qwen2.5-72B-Instruct` | LLM for answer generation |
 
 
 
444
  | `DATA_DIR` | `/data` (HF Spaces) or `./data` (local) | Root directory for all persistent data |
445
  | `CHROMA_PERSIST_DIR` | `DATA_DIR/chroma_db` | ChromaDB storage path |
446
  | `SAMPLE_DOCS_DIR` | `DATA_DIR/sample_documents` | Documents ingested on startup |
@@ -467,12 +565,12 @@ ChromaDB is ideal for Hugging Face Spaces because:
467
  - Uses **HNSW index** for fast approximate nearest neighbor search
468
  - Zero configuration required
469
 
470
- ### Why Sentence Transformers?
471
 
472
- - Runs **locally** within the Space (no API cost for embeddings)
473
- - `all-MiniLM-L6-v2` is small (~80MB) but effective
474
- - Produces **384-dimensional** vectors (good balance of quality vs. storage)
475
- - Trained on 1B+ sentence pairs for semantic similarity
476
 
477
  ### Evaluation Integration
478
 
@@ -489,9 +587,14 @@ The `/query` endpoint is designed to be called by external RAG evaluation framew
489
 
490
  If you see `capture() takes 1 positional argument but 3 were given` in the logs, it is a version incompatibility between ChromaDB and the `posthog` library. Telemetry is disabled at startup (`anonymized_telemetry=False`) so this error should no longer appear. If it persists after updating dependencies, pin `posthog<3.0` in `requirements.txt`.
491
 
492
- ### LLM 404 β€” Model Not Found
 
 
493
 
494
- If the `/query` endpoint logs a `404 Not Found` for the Inference API URL, the configured model has been removed from the free serverless tier. Update `LLM_MODEL_NAME` in `app.py` to an available model. The app now uses `chat_completion` (`/v1/chat/completions`) rather than the legacy `text_generation` endpoint, so any model listed under **Inference Providers** on huggingface.co will work. The prompt in `prompts/rag_prompt.txt` is model-agnostic (no special tokens).
 
 
 
495
 
496
  ### SSL Certificate Verification Error
497
 
@@ -516,7 +619,7 @@ export REQUESTS_CA_BUNDLE=/path/to/corporate-ca.crt
516
  $env:REQUESTS_CA_BUNDLE = "C:\path\to\corporate-ca.crt"
517
  ```
518
 
519
- Set the variable **before** running `python app.py`. Both `huggingface_hub` and `sentence-transformers` use the `requests` library, which reads `REQUESTS_CA_BUNDLE` automatically β€” no code change is needed.
520
 
521
  > **How to export your corporate CA certificate:**
522
  > Open the failing URL (`https://huggingface.co`) in your browser, click the padlock icon β†’ View Certificate β†’ export the root CA as a `.crt` / `.pem` file, then point `REQUESTS_CA_BUNDLE` to that file.
 
36
 
37
  | Requirement | Solution |
38
  |---|---|
39
+ | LLM API calls | Azure OpenAI (`gpt-5` via REST) |
40
+ | Text β†’ Embeddings | Azure OpenAI (`text-embedding-3-small` via REST) |
41
  | Vector Store | ChromaDB (persistent, runs in-process) |
42
  | API Endpoint | FastAPI with `POST /query` |
43
  | UI | Gradio Blocks (chat + document ingestion) |
 
58
  β”‚ β”‚ β–² β”‚
59
  β”‚ β–Ό β”‚ β”‚
60
  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
61
+ β”‚ β”‚ Azure OpenAI β”‚ β”‚ Azure OpenAI β”‚ β”‚
62
+ β”‚ β”‚ GPT-5 (LLM) β”‚ β”‚ text-embedding β”‚ β”‚
63
+ β”‚ β”‚ β”‚ β”‚ -3-small β”‚ β”‚
64
  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
65
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
66
  ```
 
89
 
90
  Convert text chunks into dense vector representations that capture semantic meaning.
91
 
92
+ 1. **Call Azure OpenAI** β€” We use the `text-embedding-3-small` model via the Azure OpenAI embeddings endpoint
93
  2. **Encode text** β€” Each chunk is transformed into a fixed-size vector where semantically similar texts are closer together in vector space
94
 
95
  ```python
96
+ import requests as http_requests
97
 
98
+ headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
99
+ payload = {"input": ["chunk 1 text", "chunk 2 text"], "model": "text-embedding-3-small"}
100
+ resp = http_requests.post(EMBEDDING_ENDPOINT_URL, headers=headers, json=payload)
101
+ embeddings = [item["embedding"] for item in resp.json()["data"]]
102
  ```
103
 
104
  ### Step 3: Store in Vector Database (ChromaDB)
 
129
 
130
  When a user asks a question, find the most relevant context.
131
 
132
+ 1. **Embed the query** β€” Use the same Azure OpenAI embedding model to convert the question to a vector
133
  2. **Similarity search** β€” Find the top-K nearest vectors in ChromaDB (cosine similarity)
134
  3. **Return context** β€” Extract the original text chunks for the closest matches
135
 
136
  ```python
137
+ query_embedding = generate_embeddings(["What is the Eiffel Tower?"])[0]
138
  results = collection.query(
139
+ query_embeddings=[query_embedding],
140
  n_results=3,
141
  )
142
  ```
 
146
  Combine retrieved context with the user's question and generate an answer.
147
 
148
  1. **Build prompt** β€” Load the template from [`prompts/rag_prompt.txt`](prompts/rag_prompt.txt), inject retrieved context and the user's question
149
+ 2. **Call Azure OpenAI** β€” Send the prompt to the Azure OpenAI chat/completions endpoint (`gpt-5`)
150
  3. **Return response** β€” The LLM generates an answer grounded in the provided context
151
 
152
  The prompt template (`prompts/rag_prompt.txt`):
 
169
 
170
  # At query time:
171
  prompt = RAG_PROMPT_TEMPLATE.format(context=context_text, question=user_query)
172
+ headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
173
+ payload = {
174
+ "model": "gpt-5",
175
+ "messages": [{"role": "user", "content": prompt}],
176
+ "max_tokens": 512,
177
+ "temperature": 0.7,
178
+ "top_p": 0.95,
179
+ }
180
+ resp = requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload)
181
+ answer = resp.json()["choices"][0]["message"]["content"]
182
  ```
183
 
184
  > **Tip:** Edit `prompts/rag_prompt.txt` to tune the model's behaviour (tone, language, output format) without touching application code.
 
256
  {
257
  "status": "healthy",
258
  "documents_in_store": 42,
259
+ "embedding_model": "text-embedding-3-small",
260
+ "llm_model": "gpt-5"
261
  }
262
  ```
263
 
 
301
  # Install dependencies
302
  pip install -r requirements.txt
303
 
304
+ # Set your Azure API key
305
  # macOS / Linux
306
+ export AZURE_API_KEY="your_azure_api_key_here"
307
  # Windows (PowerShell)
308
+ $env:AZURE_API_KEY = "your_azure_api_key_here"
309
 
310
  # Run the application
311
  python app.py
 
317
  ### Deploy to Hugging Face Spaces
318
 
319
  1. Create a new Space on huggingface.co (select Gradio SDK)
320
+ 2. Add `AZURE_API_KEY` as a Space Secret (Settings β†’ Secrets)
321
  3. Push this code to the Space repository
322
  4. The Space automatically installs dependencies and starts the app
323
 
 
342
 
343
  ## Adding Binary Files to the HF Space
344
 
345
+ Large or binary files (PDFs, pre-built ChromaDB databases, datasets, model weights) are stored in a **Hugging Face bucket** and mounted into the Space container. The `hf sync` command keeps your local folder in sync with the bucket.
346
 
347
  > **Note:** Git LFS is not supported for Hugging Face Spaces persistent storage. Use the bucket + `hf sync` workflow described here instead.
348
 
349
+ ### The `/data` Shared Folder
350
+
351
+ Each Hugging Face Space has a **persistent storage bucket** that is automatically mounted at `/data` inside the running container. This folder is the single shared location where the application reads and writes all persistent files β€” the ChromaDB vector store, ingested documents, and any binary assets.
352
+
353
+ | Path in container | Purpose |
354
+ |---|---|
355
+ | `/data/chroma_db/` | ChromaDB vector store (survives Space restarts) |
356
+ | `/data/sample_documents/` | Text/PDF files auto-ingested on startup |
357
+ | `/data/DataSet/` | Training and evaluation datasets |
358
+
359
+ **Default bucket naming convention:** A Space at
360
+ `https://huggingface.co/spaces/<org>/<space-name>`
361
+ gets a default storage bucket at
362
+ `https://huggingface.co/buckets/<org>/<space-name>-storage`
363
+
364
+ For this project:
365
+ - Space: `https://huggingface.co/spaces/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026`
366
+ - Bucket: `https://huggingface.co/buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage`
367
+
368
+ The `hf://` URI for use with the CLI is:
369
+ `hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage`
370
+
371
  ### Prerequisites
372
 
373
  ```bash
 
387
  3. Under **"Attach storage"**, select the existing bucket **`millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage`** (or create a new one)
388
  4. Save β€” Hugging Face will mount the bucket at `/data` inside the Space container
389
 
390
+ ### Step 2 β€” Sync Your Local `./data` with the Space Bucket
391
 
392
  Place all persistent files under a local `./data` folder. The expected structure is:
393
 
394
  ```
395
  ./data/
396
+ β”œβ”€β”€ chroma_db/ # Binary files and ChromaDB vector store
397
  β”‚ └── chroma.sqlite3
398
+ ./train_data/ # Training/test datasets (PDFs, CSVs, etc.)
399
+ β”œβ”€β”€ Automobile - Train/
400
+ β”œβ”€β”€ Climatique-Train/
 
 
 
401
  └── ...
402
  ```
403
 
404
+ **Upload (local β†’ bucket):**
 
405
  ```bash
406
+ # Mirror ./data to the bucket β€” removes remote files deleted locally
407
+ hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage --delete
408
 
409
  # Sync only a specific sub-folder (e.g., the vector store)
410
  hf sync ./data/chroma_db hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage/chroma_db
 
414
  --include "*.pdf" --include "*.sqlite3"
415
  ```
416
 
417
+ > **`--delete` flag:** Without it, `hf sync` only uploads new/changed files and never removes anything from the remote. Add `--delete` to make the bucket an exact mirror of your local `./data`.
418
+
419
+ `hf sync` is **incremental** β€” it computes checksums and only transfers files that have changed.
420
 
421
  ### Step 3 β€” Download the Bucket to Another Machine
422
 
 
426
  hf sync hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage ./data
427
  ```
428
 
429
+ ### Step 4 β€” Sync Hackathon Training Data
430
+
431
+ The shared training dataset for this hackathon is stored in a separate read-only bucket. Sync it to a local `./train_data` folder (it will be mounted at `/train_data` inside the Space):
432
+
433
+ ```bash
434
+ # Download all training data locally
435
+ hf sync hf://buckets/millimanfrance/Hackathon2026TrainData ./train_data
436
+
437
+ # Download only a specific category
438
+ hf sync hf://buckets/millimanfrance/Hackathon2026TrainData/Automobile ./train_data/Automobile
439
+ ```
440
+
441
+ > **Note:** `Hackathon2026TrainData` is a shared read-only bucket. Do not attempt to push to it.
442
+
443
+ To make this data available inside your Space, push it to your Space's own bucket under a `train_data/` sub-folder:
444
+
445
+ ```bash
446
+ hf sync ./train_data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage/train_data
447
+ ```
448
+
449
+ It will then be accessible inside the container at `/data/train_data/`.
450
+
451
  ### Accessing Files in the Space Application
452
 
453
+ Once the bucket is mounted, files appear under `/data` inside the Space container.
454
 
455
+ The application resolves the data root automatically via a single `DATA_DIR` constant in `app.py`:
456
 
457
  ```python
458
  from pathlib import Path
 
484
 
485
  ## Configuration
486
 
487
+ ### Step 1 β€” Edit `config.json`
488
+
489
+ Open [`config.json`](config.json) at the project root and replace the placeholders with your Azure OpenAI resource details:
490
+
491
+ ```json
492
+ {
493
+ "embedding": {
494
+ "endpoint_url": "https://<your-resource>.openai.azure.com/openai/deployments/<your-embedding-deployment>/embeddings?api-version=2024-12-01-preview",
495
+ "model": "text-embedding-3-small"
496
+ },
497
+ "llm": {
498
+ "endpoint_url": "https://<your-resource>.openai.azure.com/openai/deployments/<your-deployment>/chat/completions?api-version=2024-12-01-preview",
499
+ "model": "gpt-5",
500
+ "max_tokens": 512,
501
+ "temperature": 0.7,
502
+ "top_p": 0.95
503
+ }
504
+ }
505
+ ```
506
+
507
+ | Field | What to put |
508
+ |---|---|
509
+ | `<your-resource>` | Your Azure OpenAI resource name (from the Azure Portal) |
510
+ | `<your-embedding-deployment>` | The deployment name for your embedding model |
511
+ | `<your-deployment>` | The deployment name for your LLM |
512
+ | `model` | Must match the model name used when creating the deployment |
513
+
514
+ > **Note:** The `endpoint_url` for embeddings ends in `/embeddings` and the one for the LLM ends in `/chat/completions`. Keep those suffixes intact.
515
+
516
+ ### Step 2 β€” Set the API Key
517
+
518
+ The API key is **not** stored in `config.json`. Set it as an environment variable:
519
+
520
+ ```bash
521
+ # macOS / Linux
522
+ export AZURE_API_KEY="your_azure_api_key_here"
523
+
524
+ # Windows (PowerShell)
525
+ $env:AZURE_API_KEY = "your_azure_api_key_here"
526
+ ```
527
+
528
+ When deploying to Hugging Face Spaces, add it as a **Space Secret** (Settings β†’ Secrets β†’ New secret β†’ name it `AZURE_API_KEY`).
529
+
530
+ ### Tunable Parameters
531
+
532
+ The following constants in `app.py` can also be adjusted directly:
533
+
534
  | Variable | Default | Description |
535
  |---|---|---|
536
+ | `AZURE_API_KEY` | (env var / Space Secret) | Azure OpenAI API key (shared by LLM and embedding endpoints) |
537
+ | `EMBEDDING_MODEL_NAME` | `text-embedding-3-small` | Azure OpenAI embedding model |
538
+ | `LLM_MODEL_NAME` | `gpt-5` | Azure OpenAI LLM for answer generation |
539
+ | `LLM_MAX_TOKENS` | `512` | Maximum tokens in the LLM response |
540
+ | `LLM_TEMPERATURE` | `0.7` | Sampling temperature for the LLM |
541
+ | `LLM_TOP_P` | `0.95` | Top-p (nucleus) sampling for the LLM |
542
  | `DATA_DIR` | `/data` (HF Spaces) or `./data` (local) | Root directory for all persistent data |
543
  | `CHROMA_PERSIST_DIR` | `DATA_DIR/chroma_db` | ChromaDB storage path |
544
  | `SAMPLE_DOCS_DIR` | `DATA_DIR/sample_documents` | Documents ingested on startup |
 
565
  - Uses **HNSW index** for fast approximate nearest neighbor search
566
  - Zero configuration required
567
 
568
+ ### Why Azure OpenAI Embeddings?
569
 
570
+ - **High-quality embeddings** β€” `text-embedding-3-small` produces state-of-the-art embeddings with excellent semantic understanding
571
+ - **No local model loading** β€” Eliminates GPU/memory requirements on the Space
572
+ - **Scalable** β€” Handles large batch embedding requests via the API
573
+ - **Consistent** β€” Same model used across environments (dev, staging, production)
574
 
575
  ### Evaluation Integration
576
 
 
587
 
588
  If you see `capture() takes 1 positional argument but 3 were given` in the logs, it is a version incompatibility between ChromaDB and the `posthog` library. Telemetry is disabled at startup (`anonymized_telemetry=False`) so this error should no longer appear. If it persists after updating dependencies, pin `posthog<3.0` in `requirements.txt`.
589
 
590
+ ### LLM / Embedding API Errors
591
+
592
+ If the `/query` endpoint logs a `503 Service Unavailable` or `401 Unauthorized`:
593
 
594
+ 1. Verify `AZURE_API_KEY` is set correctly as an environment variable or Space Secret
595
+ 2. Check that the endpoint URLs in `config.json` match your Azure OpenAI deployment
596
+ 3. Ensure your Azure OpenAI deployment is active and the model name matches the deployment name
597
+ 4. The app calls the Azure OpenAI-compatible `chat/completions` and `embeddings` endpoints directly via REST
598
 
599
  ### SSL Certificate Verification Error
600
 
 
619
  $env:REQUESTS_CA_BUNDLE = "C:\path\to\corporate-ca.crt"
620
  ```
621
 
622
+ Set the variable **before** running `python app.py`. The `requests` library (used for Azure OpenAI API calls) reads `REQUESTS_CA_BUNDLE` automatically β€” no code change is needed.
623
 
624
  > **How to export your corporate CA certificate:**
625
  > Open the failing URL (`https://huggingface.co`) in your browser, click the padlock icon β†’ View Certificate β†’ export the root CA as a `.crt` / `.pem` file, then point `REQUESTS_CA_BUNDLE` to that file.