| --- |
| title: Example App Hackathon Gustave Eiffel 2026 |
| emoji: π€ |
| colorFrom: blue |
| colorTo: purple |
| sdk: gradio |
| sdk_version: 4.44.1 |
| python_version: '3.11' |
| app_file: app.py |
| pinned: false |
| license: apache-2.0 |
| --- |
| |
| # πΌ RAG Chat API β Gustave Eiffel Hackathon 2026 |
|
|
| A complete **Retrieval-Augmented Generation (RAG)** system deployed as a Hugging Face Space, with a `/query` API endpoint designed for the RAG evaluation system. |
|
|
| --- |
|
|
| ## Table of Contents |
|
|
| 1. [Overview](#overview) |
| 2. [Architecture](#architecture) |
| 3. [Setup & Deployment](#setup--deployment) |
| 4. [Adding Binary Files to the HF Space](#adding-binary-files-to-the-hf-space) |
| 5. [Configuration](#configuration) |
| 6. [How It Works (Detailed)](#how-it-works-detailed) |
|
|
| --- |
|
|
| ## Overview |
|
|
| This application demonstrates how to build a production-ready RAG system within the Hugging Face ecosystem. It covers: |
|
|
| | Requirement | Solution | |
| |---|---| |
| | LLM API calls | Azure OpenAI (`gpt-5` via REST) | |
| | Text β Embeddings | Azure OpenAI (`text-embedding-3-small` via REST) | |
| | Vector Store | ChromaDB (persistent, runs in-process) | |
| | API Endpoint | FastAPI with `POST /query` | |
| | UI | Gradio Blocks (chat + document ingestion) | |
|
|
| --- |
|
|
| ## Architecture |
|
|
| ``` |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| β Hugging Face Space β |
| β β |
| β ββββββββββββ ββββββββββββββββ βββββββββββββββββ β |
| β β Gradio β β FastAPI β β ChromaDB β β |
| β β UI ββββββΆβ /query ββββββΆβ Vector Store β β |
| β β β β /ingest β β (persistent) β β |
| β ββββββββββββ ββββββββ¬ββββββββ βββββββββββββββββ β |
| β β β² β |
| β βΌ β β |
| β ββββββββββββββββββββ βββββββββββββββββββ β |
| β β Azure OpenAI β β Azure OpenAI β β |
| β β GPT-5 (LLM) β β text-embedding β β |
| β β β β -3-small β β |
| β ββββββββββββββββββββ βββββββββββββββββββ β |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| ``` |
|
|
| --- |
|
|
|
|
| ## Setup & Deployment |
|
|
| ### Prerequisites |
|
|
| - Python 3.11+ |
| - A Hugging Face account with an API token |
|
|
| ### Local Development |
|
|
| ```bash |
| # Clone the repository |
| git clone https://huggingface.co/spaces/YOUR_USERNAME/Example-App-Hackathon-Gustave-Eiffel-2026 |
| cd Example-App-Hackathon-Gustave-Eiffel-2026 |
| ``` |
|
|
| #### Set Up a Python Virtual Environment |
|
|
| Using a virtual environment isolates project dependencies from your global Python installation. |
|
|
| ```bash |
| # Create the virtual environment (Python 3.11+ required) |
| python -m venv ~/.venv/hackathon-eiffel |
| |
| # Activate it |
| # macOS / Linux |
| source ~/.venv/hackathon-eiffel/bin/activate |
| # Windows (PowerShell) |
| ~\.venv\hackathon-eiffel\Scripts\Activate.ps1 |
| # Windows (Command Prompt) |
| ~\.venv\hackathon-eiffel\Scripts\activate.bat |
| |
| ``` |
|
|
| Once your virtual environment is active, install dependencies and run the app: |
|
|
| ```bash |
| # Install dependencies |
| pip install -r requirements.txt |
| |
| # Set your Azure API key |
| # macOS / Linux |
| export AZURE_API_KEY="your_azure_api_key_here" |
| # Windows (PowerShell) |
| $env:AZURE_API_KEY = "your_azure_api_key_here" |
| |
| # Run the application |
| python app.py |
| # Server starts at http://localhost:7860 |
| |
| # To make it work, you first need to create embeddings , you can learn about it from README_RAG.md |
| ``` |
|
|
| > **Tip:** To deactivate the virtual environment when you are done, run `deactivate` (venv) or `conda deactivate` (conda). |
|
|
|
|
|
|
| ### Testing the API |
|
|
| ```bash |
| # Health check |
| curl http://localhost:7860/health |
| |
| # Query the RAG system |
| curl -X POST http://localhost:7860/query \ |
| -H "Content-Type: application/json" \ |
| -d '{"query": "What is the Eiffel Tower?", "top_k": 3}' |
| |
| # Ingest a new document |
| curl -X POST http://localhost:7860/ingest \ |
| -H "Content-Type: application/json" \ |
| -d '{"text": "Your document text here...", "source": "new_doc.txt"}' |
| ``` |
|
|
| --- |
|
|
| ## Adding Binary Files to the HF Space |
|
|
| 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. |
|
|
| > **Note:** Git LFS is not supported for Hugging Face Spaces persistent storage. Use the bucket + `hf sync` workflow described here instead. |
|
|
| ### The `/data` Shared Folder |
|
|
| 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. |
|
|
| | Path in container | Purpose | |
| |---|---| |
| | `/data/chroma_db/` | ChromaDB vector store (survives Space restarts) | |
| | `/data/sample_documents/` | Text/PDF files auto-ingested on startup | |
| | `/data/DataSet/` | Training and evaluation datasets | |
|
|
| **Default bucket naming convention:** A Space at |
| `https://huggingface.co/spaces/<org>/<space-name>` |
| gets a default storage bucket at |
| `https://huggingface.co/buckets/<org>/<space-name>-storage` |
|
|
| For this project: |
| - Space: `https://huggingface.co/spaces/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026` |
| - Bucket: `https://huggingface.co/buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage` |
|
|
| The `hf://` URI for use with the CLI is: |
| `hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage` |
|
|
| ### Prerequisites |
|
|
| ```bash |
| # Install the hf CLI (requires uv) |
| uv tool install "huggingface_hub[cli]" |
| |
| # Authenticate (needs Write access to the bucket) |
| export HF_TOKEN="hf_your_token_here" |
| # or interactively: |
| hf auth login |
| ``` |
|
|
| ### Step 1 β Attach the Storage Bucket to Your Space |
|
|
| 1. Open your Space on huggingface.co |
| 2. Go to **Settings β Persistent Storage** |
| 3. Under **"Attach storage"**, select the existing bucket **`millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage`** (or create a new one) |
| 4. Save β Hugging Face will mount the bucket at `/data` inside the Space container |
|
|
| ### Step 2 β Sync Your Local `./data` with the Space Bucket |
|
|
| Place all persistent files under a local `./data` folder. The expected structure is: |
|
|
| ``` |
| ./data/ |
| βββ chroma_db/ # Binary files and ChromaDB vector store |
| β βββ chroma.sqlite3 |
| ./train_data/ # Training/test datasets (PDFs, CSVs, etc.) |
| βββ Automobile - Train/ |
| βββ Climatique-Train/ |
| βββ ... |
| ``` |
|
|
| **Upload (local β bucket):** |
| ```bash |
| # Mirror ./data to the bucket β removes remote files deleted locally |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage --delete |
| |
| # Sync only a specific sub-folder (e.g., the vector store) |
| hf sync ./data/chroma_db hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage/chroma_db |
| |
| # Sync only specific file types |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage \ |
| --include "*.pdf" --include "*.sqlite3" |
| ``` |
|
|
| > **`--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`. |
|
|
| `hf sync` is **incremental** β it computes checksums and only transfers files that have changed. |
|
|
| ### Step 3 β Download the Bucket to Another Machine |
|
|
| To pull the latest bucket contents back to a local `./data` folder (e.g., on a new dev machine): |
|
|
| ```bash |
| hf sync hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage ./data |
| ``` |
|
|
| ### Step 4 β Sync Hackathon Training Data |
|
|
| 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): |
|
|
| ```bash |
| # Download all training data locally |
| hf sync hf://buckets/millimanfrance/Hackathon2026TrainData ./train_data |
| |
| # Download only a specific category |
| hf sync hf://buckets/millimanfrance/Hackathon2026TrainData/Automobile ./train_data/Automobile |
| ``` |
|
|
| > **Note:** `Hackathon2026TrainData` is a shared read-only bucket. Do not attempt to push to it. |
|
|
| To make this data available inside your Space, push it to your Space's own bucket under a `train_data/` sub-folder: |
|
|
| ```bash |
| hf sync ./train_data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage/train_data |
| ``` |
|
|
| It will then be accessible inside the container at `/data/train_data/`. |
|
|
| ### Accessing Files in the Space Application |
|
|
| Once the bucket is mounted, files appear under `/data` inside the Space container. |
|
|
| The application resolves the data root automatically via a single `DATA_DIR` constant in `app.py`: |
|
|
| ```python |
| from pathlib import Path |
| |
| # /data when running in HF Spaces (bucket mount), ./data for local dev |
| DATA_DIR = Path("/data") if Path("/data").is_dir() else Path("./data") |
| |
| CHROMA_PERSIST_DIR = str(DATA_DIR / "chroma_db") |
| SAMPLE_DOCS_DIR = DATA_DIR / "sample_documents" |
| ``` |
|
|
| All persistent data β the vector store, sample documents, datasets β lives under `DATA_DIR` so a single `hf sync ./data ...` covers everything. |
|
|
| ### Useful `hf sync` Flags |
|
|
| | Flag | Description | |
| |---|---| |
| | `--include "*.pdf"` | Only sync files matching the pattern | |
| | `--exclude "*.tmp"` | Skip files matching the pattern | |
| | `--delete` | Remove files in the destination that no longer exist in the source | |
| | `--dry-run` | Preview what would be transferred without actually doing it | |
|
|
| ```bash |
| # Preview before committing |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage --dry-run |
| ``` |
|
|
| --- |
|
|
| ## Configuration |
|
|
| The application loads model configuration with the following priority: |
|
|
| 1. **`./data/config.json`** β the live runtime config (read at startup) |
| 2. **`./config.json`** (project root) β example/template file used as fallback when no runtime config exists; **never put real credentials here** |
|
|
| ### Step 1 β Create `data/config.json` |
|
|
| Copy the root template to `./data/` and fill in your Azure OpenAI resource details: |
|
|
| ```bash |
| cp config.json data/config.json |
| ``` |
|
|
| Then edit `data/config.json`: |
|
|
| ```json |
| { |
| "embedding": { |
| "endpoint_url": "https://<your-resource>.openai.azure.com/openai/deployments/<your-embedding-deployment>/embeddings?api-version=2024-12-01-preview", |
| "model": "text-embedding-3-small" |
| }, |
| "llm": { |
| "endpoint_url": "https://<your-resource>.openai.azure.com/openai/deployments/<your-deployment>/chat/completions?api-version=2024-12-01-preview", |
| "model": "gpt-5", |
| "max_completion_tokens": 512, |
| "temperature": 0.7, |
| "top_p": 0.95 |
| } |
| } |
| ``` |
|
|
| | Field | What to put | |
| |---|---| |
| | `<your-resource>` | Your Azure OpenAI resource name (from the Azure Portal) | |
| | `<your-embedding-deployment>` | The deployment name for your embedding model | |
| | `<your-deployment>` | The deployment name for your LLM | |
| | `model` | Must match the model name used when creating the deployment | |
|
|
| > **Note:** The `endpoint_url` for embeddings ends in `/embeddings` and the one for the LLM ends in `/chat/completions`. Keep those suffixes intact. |
| |
| ### Step 2 β Set the API Key |
| |
| The API key is **not** stored in `config.json`. Set it as an environment variable: |
| |
| ```bash |
| # macOS / Linux |
| export AZURE_API_KEY="your_azure_api_key_here" |
| |
| # Windows (PowerShell) |
| $env:AZURE_API_KEY = "your_azure_api_key_here" |
| ``` |
| |
| When deploying to Hugging Face Spaces, add it as a **Space Secret** (Settings β Secrets β New secret β name it `AZURE_API_KEY`). |
| |
| ### Step 3 β Push `data/config.json` to the HF Bucket |
| |
| `data/config.json` lives under `./data` so a normal bucket sync includes it automatically: |
| |
| ```bash |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage --delete |
| ``` |
| |
| The Space container will then find it at `/data/config.json` on next restart. |
| |
| > **Do not commit `data/config.json` to Git** β it contains endpoint URLs tied to your Azure resource. Add `data/config.json` to `.gitignore`. The root `config.json` is safe to commit as it only contains placeholder values. |
| |
| ### Tunable Parameters |
| |
| The following constants in `app.py` can also be adjusted directly: |
| |
| | Variable | Default | Description | |
| |---|---|---| |
| | `AZURE_API_KEY` | (env var / Space Secret) | Azure OpenAI API key (shared by LLM and embedding endpoints) | |
| | `EMBEDDING_MODEL_NAME` | `text-embedding-3-small` | Azure OpenAI embedding model | |
| | `LLM_MODEL_NAME` | `gpt-5` | Azure OpenAI LLM for answer generation | |
| | `LLM_MAX_TOKENS` | `512` | Maximum tokens in the LLM response | |
| | `LLM_TEMPERATURE` | `0.7` | Sampling temperature for the LLM | |
| | `LLM_TOP_P` | `0.95` | Top-p (nucleus) sampling for the LLM | |
| | `DATA_DIR` | `/data` (HF Spaces) or `./data` (local) | Root directory for all persistent data | |
| | `CHROMA_PERSIST_DIR` | `DATA_DIR/chroma_db` | ChromaDB storage path | |
| | `SAMPLE_DOCS_DIR` | `DATA_DIR/sample_documents` | Documents ingested on startup | |
| | `CHUNK_SIZE` | `512` | Text chunk size in characters | |
| | `CHUNK_OVERLAP` | `50` | Overlap between chunks | |
| | `TOP_K_RESULTS` | `3` | Number of context chunks to retrieve | |
|
|
| --- |
|
|
| ## How It Works (Detailed) |
|
|
| ### Why RAG? |
|
|
| Large Language Models have a knowledge cutoff and can hallucinate. RAG solves this by: |
| - **Grounding** responses in actual documents |
| - **Updating** knowledge without retraining |
| - **Providing** source attribution for answers |
|
|
| ### Why ChromaDB in HF Spaces? |
|
|
| ChromaDB is ideal for Hugging Face Spaces because: |
| - Runs **in-process** (no external database needed) |
| - Supports **persistent storage** (survives Space restarts) |
| - Uses **HNSW index** for fast approximate nearest neighbor search |
| - Zero configuration required |
|
|
| ### Why Azure OpenAI Embeddings? |
|
|
| - **High-quality embeddings** β `text-embedding-3-small` produces state-of-the-art embeddings with excellent semantic understanding |
| - **No local model loading** β Eliminates GPU/memory requirements on the Space |
| - **Scalable** β Handles large batch embedding requests via the API |
| - **Consistent** β Same model used across environments (dev, staging, production) |
|
|
| ### Evaluation Integration |
|
|
| The `/query` endpoint is designed to be called by external RAG evaluation frameworks. It returns: |
| - The generated `answer` for correctness evaluation |
| - Source `documents` for faithfulness/groundedness checks |
| - Similarity `scores` for retrieval quality assessment |
|
|
| --- |
|
|
| ## Troubleshooting |
|
|
| ### ChromaDB Telemetry Error |
|
|
| 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`. |
|
|
| ### LLM / Embedding API Errors |
|
|
| If the `/query` endpoint logs a `503 Service Unavailable` or `401 Unauthorized`: |
|
|
| 1. Verify `AZURE_API_KEY` is set correctly as an environment variable or Space Secret |
| 2. Check that the endpoint URLs in `config.json` match your Azure OpenAI deployment |
| 3. Ensure your Azure OpenAI deployment is active and the model name matches the deployment name |
| 4. The app calls the Azure OpenAI-compatible `chat/completions` and `embeddings` endpoints directly via REST |
|
|
| ### SSL Certificate Verification Error |
|
|
| If you see an error like the one below when running `python app.py`, your machine is most likely behind a **corporate proxy that performs SSL inspection** and has its own root CA that Python does not trust by default. |
|
|
| ``` |
| ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify |
| failed: unable to get local issuer certificate |
| ``` |
|
|
| **Fix β point `requests` to your corporate CA bundle:** |
|
|
| ```bash |
| # macOS / Linux β use the system bundle (adjust path for your distro) |
| export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu |
| export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt # RHEL/CentOS |
| |
| # Or point to a specific corporate certificate file |
| export REQUESTS_CA_BUNDLE=/path/to/corporate-ca.crt |
| |
| # Windows (PowerShell) |
| $env:REQUESTS_CA_BUNDLE = "C:\path\to\corporate-ca.crt" |
| ``` |
|
|
| 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. |
|
|
| > **How to export your corporate CA certificate:** |
| > 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. |
|
|
| ### `hf sync` 401 Unauthorized Error |
|
|
| If `hf sync` fails with: |
|
|
| ``` |
| Error: Client error '401 Unauthorized' for url |
| 'https://huggingface.co/api/buckets/.../tree?recursive=true' |
| Invalid username or password. |
| ``` |
|
|
| the `hf` CLI has not been authenticated. Log in with your Hugging Face token: |
|
|
| ```bash |
| hf auth login |
| # Paste your token when prompted (needs read + write access to the bucket) |
| ``` |
|
|
| Or set the token as an environment variable so the CLI picks it up automatically without an interactive prompt: |
|
|
| ```bash |
| export HF_TOKEN="hf_your_token_here" |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage |
| ``` |
|
|
| > **Generate a token:** Go to huggingface.co β Settings β Access Tokens β New token. Select **Write** role so the CLI can both read and upload to the bucket. |
|
|
| > **Check current login state:** |
| > ```bash |
| > hf auth whoami |
| > ``` |
|
|
| --- |
|
|
| ### `hf sync` SSL Certificate Verification Error |
|
|
| The `hf` CLI (installed via `uv tool install huggingface_hub`) uses `httpx` internally instead of `requests`, so it ignores `REQUESTS_CA_BUNDLE`. If `hf sync` fails with: |
|
|
| ``` |
| httpcore.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: |
| unable to get local issuer certificate |
| ``` |
|
|
| you must set the certificate bundle through the variables that `httpx` (and the underlying `ssl` module) respects: |
|
|
| ```bash |
| # Option A β point to your corporate CA bundle file (recommended) |
| export SSL_CERT_FILE=/path/to/corporate-ca.crt |
| export REQUESTS_CA_BUNDLE=/path/to/corporate-ca.crt # keep this too for other tools |
| |
| hf sync ./data hf://buckets/millimanfrance/Example-App-Hackathon-Gustave-Eiffel-2026-storage |
| |
| # Option B β append your CA cert to the system bundle and point there |
| cat /path/to/corporate-ca.crt >> /etc/ssl/certs/ca-certificates.crt |
| export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt |
| ``` |
|
|
| `SSL_CERT_FILE` overrides the default CA store for Python's `ssl` module, which `httpx` / `httpcore` uses directly. |
|
|
| **If you need to set these variables permanently** (e.g., in a shared dev environment), add them to your shell profile: |
|
|
| ```bash |
| # ~/.bashrc or ~/.zshrc |
| export SSL_CERT_FILE=/path/to/corporate-ca.crt |
| export REQUESTS_CA_BUNDLE=/path/to/corporate-ca.crt |
| ``` |
|
|
| > **Finding your corporate CA cert on Linux:** |
| > ```bash |
| > # List all trusted CAs and look for your company's entry |
| > awk -v cmd='openssl x509 -noout -subject' '/BEGIN CERT/{close(cmd)}; {print | cmd}' \ |
| > /etc/ssl/certs/ca-certificates.crt | grep -i "your-company-name" |
| > |
| > # Or export the cert directly from the proxy |
| > echo | openssl s_client -connect huggingface.co:443 -showcerts 2>/dev/null \ |
| > | openssl x509 -outform PEM > /tmp/hf-chain.pem |
| > export SSL_CERT_FILE=/tmp/hf-chain.pem |
| > ``` |
| |
| --- |
| |
| ## License |
| |
| Apache 2.0 |
| |
| |