My_App_Datack / README.md
Quincy Hsieh
Fix error Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.
b818d09
|
Raw History Blame
26.3 kB
ο»Ώ---
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. [Step-by-Step Explanation](#step-by-step-explanation)
4. [API Endpoints](#api-endpoints)
5. [Setup & Deployment](#setup--deployment)
6. [Adding Binary Files to the HF Space](#adding-binary-files-to-the-hf-space)
7. [Configuration](#configuration)
8. [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 β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
---
## Step-by-Step Explanation
### Step 1: Document Ingestion & Chunking
Before we can answer questions, we need to prepare our knowledge base.
1. **Load documents** β€” Read text files from `sample_documents/` directory
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.
```python
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document_text)
```
### Step 2: Generate Embeddings
Convert text chunks into dense vector representations that capture semantic meaning.
1. **Call Azure OpenAI** β€” We use the `text-embedding-3-small` model via the Azure OpenAI embeddings endpoint
2. **Encode text** β€” Each chunk is transformed into a fixed-size vector where semantically similar texts are closer together in vector space
```python
import requests as http_requests
headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
payload = {"input": ["chunk 1 text", "chunk 2 text"], "model": "text-embedding-3-small"}
resp = http_requests.post(EMBEDDING_ENDPOINT_URL, headers=headers, json=payload)
embeddings = [item["embedding"] for item in resp.json()["data"]]
```
### Step 3: Store in Vector Database (ChromaDB)
Persist embeddings in a vector store optimized for similarity search.
1. **Initialize ChromaDB** β€” Create a persistent client that stores data on disk (survives Space restarts)
2. **Create collection** β€” A named collection with cosine similarity metric
3. **Add documents** β€” Store embeddings alongside the original text and metadata
```python
import chromadb
client = chromadb.PersistentClient(path="./data/chroma_db")
collection = client.get_or_create_collection(
name="rag_documents",
metadata={"hnsw:space": "cosine"},
)
collection.add(
ids=["doc_0", "doc_1"],
embeddings=embeddings.tolist(),
documents=["chunk 1 text", "chunk 2 text"],
metadatas=[{"source": "file.txt"}, {"source": "file.txt"}],
)
```
### Step 4: Query & Retrieval
When a user asks a question, find the most relevant context.
1. **Embed the query** β€” Use the same Azure OpenAI embedding model to convert the question to a vector
2. **Similarity search** β€” Find the top-K nearest vectors in ChromaDB (cosine similarity)
3. **Return context** β€” Extract the original text chunks for the closest matches
```python
query_embedding = generate_embeddings(["What is the Eiffel Tower?"])[0]
results = collection.query(
query_embeddings=[query_embedding],
n_results=3,
)
```
### Step 5: LLM Generation (Augmented Response)
Combine retrieved context with the user's question and generate an answer.
1. **Build prompt** β€” Load the template from [`prompts/rag_prompt.txt`](prompts/rag_prompt.txt), inject retrieved context and the user's question
2. **Call Azure OpenAI** β€” Send the prompt to the Azure OpenAI chat/completions endpoint (`gpt-5`)
3. **Return response** β€” The LLM generates an answer grounded in the provided context
The prompt template (`prompts/rag_prompt.txt`):
```
You are a helpful assistant. Answer the user's question based ONLY on the provided context.
If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
Always be concise and factual.
Context:
{context}
Question: {question}
```
The template is loaded once at startup and sent as the user message to the chat endpoint:
```python
RAG_PROMPT_TEMPLATE = Path("prompts/rag_prompt.txt").read_text(encoding="utf-8")
# At query time:
prompt = RAG_PROMPT_TEMPLATE.format(context=context_text, question=user_query)
headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
payload = {
"model": "gpt-5",
"messages": [{"role": "user", "content": prompt}],
"max_completion_tokens": 512,
"temperature": 0.7,
"top_p": 0.95,
}
resp = requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload)
answer = resp.json()["choices"][0]["message"]["content"]
```
> **Tip:** Edit `prompts/rag_prompt.txt` to tune the model's behaviour (tone, language, output format) without touching application code.
### Step 6: API Endpoint (`/query`)
The FastAPI endpoint ties everything together for the evaluation system.
```python
@app.post("/query")
async def query_endpoint(request: QueryRequest):
# 1. Retrieve relevant context
# 2. Build augmented prompt
# 3. Generate LLM response
# 4. Return answer + sources
result = rag_query(request.query, top_k=request.top_k)
return JSONResponse(content=result)
```
---
## API Endpoints
### `POST /query`
The primary endpoint for the RAG evaluation system.
**Request:**
```json
{
"query": "What materials is the Eiffel Tower made of?",
"top_k": 3
}
```
**Response:**
```json
{
"answer": "The Eiffel Tower is made of wrought iron (puddled iron)...",
"sources": [
{"source": "eiffel_tower.txt", "score": 0.87},
{"source": "paris_landmarks.txt", "score": 0.72}
],
"query": "What materials is the Eiffel Tower made of?"
}
```
### `POST /ingest`
Add new documents to the knowledge base.
**Request:**
```json
{
"text": "The Eiffel Tower was built in 1889...",
"source": "my_document.txt"
}
```
**Response:**
```json
{
"status": "success",
"chunks_added": 5,
"total_chunks": 42
}
```
### `GET /health`
System health check.
**Response:**
```json
{
"status": "healthy",
"documents_in_store": 42,
"embedding_model": "text-embedding-3-small",
"llm_model": "gpt-5"
}
```
---
## 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
```
> **Tip:** To deactivate the virtual environment when you are done, run `deactivate` (venv) or `conda deactivate` (conda).
### Deploy to Hugging Face Spaces
1. Create a new Space on huggingface.co (select Gradio SDK)
2. Add `AZURE_API_KEY` as a Space Secret (Settings β†’ Secrets)
3. Push this code to the Space repository
4. The Space automatically installs dependencies and starts the app
### 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