docs: update all references Render → HF Spaces (Stage 12)
Browse files- README.md: deployment row, design decisions, architecture diagram
- docs/architecture.md: component table, key decisions, known limits, future work
- docs/decisions.md: Render decision superseded, HF Spaces decision added
- docs/evolution.md: Stage 12 added, Current State Snapshot updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README.md +6 -5
- docs/architecture.md +12 -10
- docs/decisions.md +4 -3
- docs/evolution.md +34 -5
README.md
CHANGED
|
@@ -88,7 +88,7 @@ If retrieval degrades, you see it before the user does.
|
|
| 88 |
| **Eval** | RAGAS (pre-computed JSON) + LLM-as-Judge per turn |
|
| 89 |
| **Observability** | LangSmith |
|
| 90 |
| **Frontend** | React 19 + Vite + Tailwind CSS v4 |
|
| 91 |
-
| **Deployment** |
|
| 92 |
|
| 93 |
---
|
| 94 |
|
|
@@ -232,10 +232,11 @@ prism/
|
|
| 232 |
|
| 233 |
## Design Decisions Worth Noting
|
| 234 |
|
| 235 |
-
- **Singleton retriever cache per workspace** — without cache, every request rebuilt the Chroma instance (full embedding reload)
|
| 236 |
-
- **CPU-only torch in Dockerfile** — sentence-transformers pulls CUDA torch (~2GB) by default
|
| 237 |
-
- **
|
| 238 |
-
- **
|
|
|
|
| 239 |
- **Web search bypasses chain** — `ConversationalRetrievalChain` condensation step strips prepended Tavily context before LLM sees it. Web path uses direct LLM call with chat history.
|
| 240 |
- **Idempotent ingestion** — chunk IDs are `md5(source + page + text)`. Re-ingesting same doc does not duplicate chunks.
|
| 241 |
|
|
|
|
| 88 |
| **Eval** | RAGAS (pre-computed JSON) + LLM-as-Judge per turn |
|
| 89 |
| **Observability** | LangSmith |
|
| 90 |
| **Frontend** | React 19 + Vite + Tailwind CSS v4 |
|
| 91 |
+
| **Deployment** | HF Spaces Docker (backend, 16GB RAM) + Vercel (frontend) |
|
| 92 |
|
| 93 |
---
|
| 94 |
|
|
|
|
| 232 |
|
| 233 |
## Design Decisions Worth Noting
|
| 234 |
|
| 235 |
+
- **Singleton retriever cache per workspace** — without cache, every request rebuilt the Chroma instance (full embedding reload). Cache invalidated after ingest.
|
| 236 |
+
- **CPU-only torch in Dockerfile** — sentence-transformers pulls CUDA torch (~2GB) by default. Pre-installing CPU torch keeps the image lean.
|
| 237 |
+
- **HF Spaces UID 1000** — HF runs Docker containers as user 1000. Reranker weights baked under `HF_HOME=/app/.cache/huggingface` as user 1000 at build time; `HF_HUB_OFFLINE=1` set after download to block runtime Hub calls.
|
| 238 |
+
- **RAGAS pre-computed locally** — `nest_asyncio` cannot patch `uvloop` (uvicorn's event loop on Linux). Live RAGAS eval always 500s. Run locally, commit JSON, Vercel reads file.
|
| 239 |
+
- **TinyBERT-L-2-v2 reranker** — same ranking quality as MiniLM-L-6-v2 at demo corpus scale; ~17MB vs ~85MB.
|
| 240 |
- **Web search bypasses chain** — `ConversationalRetrievalChain` condensation step strips prepended Tavily context before LLM sees it. Web path uses direct LLM call with chat history.
|
| 241 |
- **Idempotent ingestion** — chunk IDs are `md5(source + page + text)`. Re-ingesting same doc does not duplicate chunks.
|
| 242 |
|
docs/architecture.md
CHANGED
|
@@ -13,8 +13,8 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
|
|
| 13 |
| Vector store | ChromaDB (persistent) | Dense embedding storage and retrieval |
|
| 14 |
| Sparse retrieval | rank_bm25 (BM25Okapi) | Keyword-match retrieval for regulatory text |
|
| 15 |
| Hybrid fusion | Weighted RRF (dense 0.7 + sparse 0.3) | Merge dense + sparse result lists |
|
| 16 |
-
| Reranker | cross-encoder/ms-marco-TinyBERT-L-2-v2 | Re-score top-10 → return top-5 (~17MB
|
| 17 |
-
| Embeddings | Euron API (text-embedding-3-small) | API-based;
|
| 18 |
| LLM | Groq (llama-3.3-70b-versatile) via langchain-groq | Fast open-weight inference; OpenAI-compatible |
|
| 19 |
| Chunking | LangChain ParentDocumentRetriever | Child 200-char indexed, parent 800-char sent to LLM |
|
| 20 |
| Memory | ConversationBufferWindowMemory (k=10) | Last 10 conversation turns |
|
|
@@ -28,7 +28,7 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
|
|
| 28 |
| Document parsing | LlamaParse (primary), pypdf (fallback) | PDF extraction |
|
| 29 |
| Backend | FastAPI + Uvicorn | REST API |
|
| 30 |
| Frontend | React 19 + Vite + Tailwind CSS v4 | Chat / Upload tabs |
|
| 31 |
-
| Deployment |
|
| 32 |
|
| 33 |
## Data flow
|
| 34 |
|
|
@@ -49,24 +49,26 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
|
|
| 49 |
7. Response includes: answer, sources (with scores), retrieval_method
|
| 50 |
|
| 51 |
## Key design decisions
|
| 52 |
-
- **API embeddings over local**:
|
| 53 |
- **ParentDocumentRetriever**: small chunks improve retrieval precision; large parent chunks improve answer faithfulness
|
| 54 |
- **Cross-encoder reranker**: bi-encoder (ChromaDB) is fast but approximate; cross-encoder is slower but more accurate on top-20 pool
|
| 55 |
- **BM25 weight 0.3**: regulatory text has exact keyword matches (section numbers); sparse retrieval catches what dense misses
|
| 56 |
-
- **RAGAS benchmark pre-computed locally**: `nest_asyncio` cannot patch `uvloop` (used by uvicorn on
|
| 57 |
-
- **TinyBERT-L-2-v2 reranker**: MiniLM-L-6-v2 (~85MB)
|
| 58 |
-
- **Singleton vectorstore/retriever cache**: each workspace caches its Chroma vectorstore + HybridRetriever in a module-level dict. Without cache, every chat request created a new Chroma instance (full embedding reload)
|
| 59 |
- **Multi-workspace isolation**: each workspace maps to one ChromaDB collection. Frontend workspace switcher passes `workspace_id` on every request; backend resolves the correct collection before retrieval.
|
| 60 |
-
- **URL size guard**: `url_loader.py` enforces a max content size before embedding URL content, preventing
|
| 61 |
- **Eval dashboard separate site**: eval runs offline, results versioned as JSON. Separates eval tooling from user-facing app; no live eval endpoint on prod backend. Per-message faithfulness badge removed from UI — moved to dedicated dashboard.
|
| 62 |
- **answer_correctness over faithfulness**: faithfulness (LLM judge vs retrieved chunks) is circular — inflates when eval pairs are corpus-aligned. answer_correctness (LLM judge vs ground_truth reference) is an independent signal.
|
|
|
|
| 63 |
|
| 64 |
## Known limitations
|
| 65 |
- InMemoryStore for parent chunks: does not survive server restart (re-ingest required)
|
| 66 |
- BM25 index rebuilt in memory on each startup (not persisted to disk)
|
| 67 |
-
-
|
|
|
|
| 68 |
|
| 69 |
## Future improvements
|
| 70 |
- Persist BM25 index to disk (pickle)
|
| 71 |
-
-
|
| 72 |
- Streaming responses from LLM
|
|
|
|
| 13 |
| Vector store | ChromaDB (persistent) | Dense embedding storage and retrieval |
|
| 14 |
| Sparse retrieval | rank_bm25 (BM25Okapi) | Keyword-match retrieval for regulatory text |
|
| 15 |
| Hybrid fusion | Weighted RRF (dense 0.7 + sparse 0.3) | Merge dense + sparse result lists |
|
| 16 |
+
| Reranker | cross-encoder/ms-marco-TinyBERT-L-2-v2 | Re-score top-10 → return top-5 (~17MB; chosen over MiniLM-L-6-v2 ~85MB for lower memory footprint) |
|
| 17 |
+
| Embeddings | Euron API (text-embedding-3-small) | API-based; Groq has no embeddings endpoint. |
|
| 18 |
| LLM | Groq (llama-3.3-70b-versatile) via langchain-groq | Fast open-weight inference; OpenAI-compatible |
|
| 19 |
| Chunking | LangChain ParentDocumentRetriever | Child 200-char indexed, parent 800-char sent to LLM |
|
| 20 |
| Memory | ConversationBufferWindowMemory (k=10) | Last 10 conversation turns |
|
|
|
|
| 28 |
| Document parsing | LlamaParse (primary), pypdf (fallback) | PDF extraction |
|
| 29 |
| Backend | FastAPI + Uvicorn | REST API |
|
| 30 |
| Frontend | React 19 + Vite + Tailwind CSS v4 | Chat / Upload tabs |
|
| 31 |
+
| Deployment | HF Spaces (Docker backend, 16GB RAM) + Vercel (frontend) + Vercel (eval-dashboard) | Production |
|
| 32 |
|
| 33 |
## Data flow
|
| 34 |
|
|
|
|
| 49 |
7. Response includes: answer, sources (with scores), retrieval_method
|
| 50 |
|
| 51 |
## Key design decisions
|
| 52 |
+
- **API embeddings over local**: Euron API embeddings ~0MB RAM; Groq has no embeddings endpoint so Euron is retained for embeddings.
|
| 53 |
- **ParentDocumentRetriever**: small chunks improve retrieval precision; large parent chunks improve answer faithfulness
|
| 54 |
- **Cross-encoder reranker**: bi-encoder (ChromaDB) is fast but approximate; cross-encoder is slower but more accurate on top-20 pool
|
| 55 |
- **BM25 weight 0.3**: regulatory text has exact keyword matches (section numbers); sparse retrieval catches what dense misses
|
| 56 |
+
- **RAGAS benchmark pre-computed locally**: `nest_asyncio` cannot patch `uvloop` (used by uvicorn on Linux), making live RAGAS eval impossible on prod. Run `scripts/run_ragas_local.py` locally, commit JSON results, Vercel builds dashboard from file.
|
| 57 |
+
- **TinyBERT-L-2-v2 reranker**: MiniLM-L-6-v2 (~85MB) vs TinyBERT-L-2-v2 (~17MB) — same ranking quality at demo corpus scale with lower memory footprint.
|
| 58 |
+
- **Singleton vectorstore/retriever cache**: each workspace caches its Chroma vectorstore + HybridRetriever in a module-level dict. Without cache, every chat request created a new Chroma instance (full embedding reload). Cache is invalidated after ingest.
|
| 59 |
- **Multi-workspace isolation**: each workspace maps to one ChromaDB collection. Frontend workspace switcher passes `workspace_id` on every request; backend resolves the correct collection before retrieval.
|
| 60 |
+
- **URL size guard**: `url_loader.py` enforces a max content size before embedding URL content, preventing memory spikes from large external pages.
|
| 61 |
- **Eval dashboard separate site**: eval runs offline, results versioned as JSON. Separates eval tooling from user-facing app; no live eval endpoint on prod backend. Per-message faithfulness badge removed from UI — moved to dedicated dashboard.
|
| 62 |
- **answer_correctness over faithfulness**: faithfulness (LLM judge vs retrieved chunks) is circular — inflates when eval pairs are corpus-aligned. answer_correctness (LLM judge vs ground_truth reference) is an independent signal.
|
| 63 |
+
- **HF Spaces Docker (UID 1000)**: model weights baked into image under `HF_HOME=/app/.cache/huggingface` as user 1000 at build time; `HF_HUB_OFFLINE=1` set after download to block runtime network calls.
|
| 64 |
|
| 65 |
## Known limitations
|
| 66 |
- InMemoryStore for parent chunks: does not survive server restart (re-ingest required)
|
| 67 |
- BM25 index rebuilt in memory on each startup (not persisted to disk)
|
| 68 |
+
- HF Spaces free tier: ephemeral filesystem — chroma_db lost on cold start (re-upload required)
|
| 69 |
+
- Euron embedding API sequential: ~5s/chunk — 30 chunks = ~150s blocking upload
|
| 70 |
|
| 71 |
## Future improvements
|
| 72 |
- Persist BM25 index to disk (pickle)
|
| 73 |
+
- Move Euron embedding to background task (return 202, poll for ready)
|
| 74 |
- Streaming responses from LLM
|
docs/decisions.md
CHANGED
|
@@ -4,15 +4,16 @@
|
|
| 4 |
|
| 5 |
| Date | Decision | Rationale | Status |
|
| 6 |
|------|----------|-----------|--------|
|
| 7 |
-
| 2026-05 | Euron API for embeddings (not local sentence-transformers) |
|
| 8 |
| 2026-05-24 | Groq (llama-3.3-70b-versatile) for LLM; Euron retained for embeddings | Groq: faster inference, open-weight model. Euron kept for embeddings — Groq has no embeddings endpoint. | Active |
|
| 9 |
-
| 2026-05 | Render (Docker) over Railway for backend | Free tier RAM fit confirmed: embeddings ~150MB + reranker ~17MB = ~230MB total |
|
|
|
|
| 10 |
| 2026-05 | ParentDocumentRetriever (child 200 / parent 800) | Better faithfulness: small chunks retrieved precisely, large chunks give LLM full context | Active |
|
| 11 |
| 2026-05 | BM25 weight 0.3 in RRF fusion | Regulatory text has exact keyword matches; sparse recall is complementary, not dominant | Active |
|
| 12 |
| 2026-05-30 | RAGAS pre-computed locally, dashboard JSON-driven | `nest_asyncio` cannot patch `uvloop` on Render — live eval endpoint always 500s. Run `scripts/run_ragas_local.py` (8B judge model) → commit `ragas_benchmark.json` → Vercel builds dashboard. | Active |
|
| 13 |
| 2026-05-30 | RAGAS judge uses `llama-3.1-8b-instant` not `llama-3.3-70b` | 70B model exhausts Groq free-tier 100k TPD in one eval run. 8B has 500k TPD and is sufficient for statement-level faithfulness checks. Answer generation still uses 70B. | Active |
|
| 14 |
| 2026-05 | LangSmith tracing via env var (no code changes) | LangChain reads LANGCHAIN_TRACING_V2 automatically; zero instrumentation cost | Active |
|
| 15 |
-
| 2026-05-30 | Reranker switched to TinyBERT-L-2-v2 (~17MB) from MiniLM-L-6-v2 (~85MB) |
|
| 16 |
| 2026-05 | Cross-encoder reranker pre-downloaded at Docker build time | Avoids cold-start latency on first request in production | Active |
|
| 17 |
| 2026-05 | Idempotent ingestion via md5(source+page+text) chunk IDs | Re-running ingest does not duplicate chunks in ChromaDB | Active |
|
| 18 |
| 2026-06-14 | Multi-workspace: one ChromaDB collection per workspace | Isolated document sets per workspace; `workspace_id` passed on every request; `list_collections()` normalised for chromadb ≥0.5.4 (returns `list[str]`) and <0.5 (returns `list[Collection]`) | Active |
|
|
|
|
| 4 |
|
| 5 |
| Date | Decision | Rationale | Status |
|
| 6 |
|------|----------|-----------|--------|
|
| 7 |
+
| 2026-05 | Euron API for embeddings (not local sentence-transformers) | Groq has no embeddings endpoint; Euron API keeps RAM footprint near zero vs local model | Active |
|
| 8 |
| 2026-05-24 | Groq (llama-3.3-70b-versatile) for LLM; Euron retained for embeddings | Groq: faster inference, open-weight model. Euron kept for embeddings — Groq has no embeddings endpoint. | Active |
|
| 9 |
+
| 2026-05 | Render (Docker) over Railway for backend | Free tier RAM fit confirmed: embeddings ~150MB + reranker ~17MB = ~230MB total | **Superseded 2026-06-22** |
|
| 10 |
+
| 2026-06-22 | HF Spaces Docker over Render for backend | Render 512MB free tier caused repeated OOM under contextual refresh + concurrent chat. HF Spaces CPU Basic = 16GB RAM free. No memory guards needed. Port 7860, user UID 1000, HF_HOME=/app/.cache/huggingface baked at build time. | Active |
|
| 11 |
| 2026-05 | ParentDocumentRetriever (child 200 / parent 800) | Better faithfulness: small chunks retrieved precisely, large chunks give LLM full context | Active |
|
| 12 |
| 2026-05 | BM25 weight 0.3 in RRF fusion | Regulatory text has exact keyword matches; sparse recall is complementary, not dominant | Active |
|
| 13 |
| 2026-05-30 | RAGAS pre-computed locally, dashboard JSON-driven | `nest_asyncio` cannot patch `uvloop` on Render — live eval endpoint always 500s. Run `scripts/run_ragas_local.py` (8B judge model) → commit `ragas_benchmark.json` → Vercel builds dashboard. | Active |
|
| 14 |
| 2026-05-30 | RAGAS judge uses `llama-3.1-8b-instant` not `llama-3.3-70b` | 70B model exhausts Groq free-tier 100k TPD in one eval run. 8B has 500k TPD and is sufficient for statement-level faithfulness checks. Answer generation still uses 70B. | Active |
|
| 15 |
| 2026-05 | LangSmith tracing via env var (no code changes) | LangChain reads LANGCHAIN_TRACING_V2 automatically; zero instrumentation cost | Active |
|
| 16 |
+
| 2026-05-30 | Reranker switched to TinyBERT-L-2-v2 (~17MB) from MiniLM-L-6-v2 (~85MB) | Lower memory footprint with acceptable ranking quality at demo corpus scale | Active |
|
| 17 |
| 2026-05 | Cross-encoder reranker pre-downloaded at Docker build time | Avoids cold-start latency on first request in production | Active |
|
| 18 |
| 2026-05 | Idempotent ingestion via md5(source+page+text) chunk IDs | Re-running ingest does not duplicate chunks in ChromaDB | Active |
|
| 19 |
| 2026-06-14 | Multi-workspace: one ChromaDB collection per workspace | Isolated document sets per workspace; `workspace_id` passed on every request; `list_collections()` normalised for chromadb ≥0.5.4 (returns `list[str]`) and <0.5 (returns `list[Collection]`) | Active |
|
docs/evolution.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Prism — Project Evolution
|
| 2 |
|
| 3 |
> End-to-end record of what was broken at each stage, what was built to fix it, and what is planned next.
|
| 4 |
-
> Updated as the project evolves. Last updated: 2026-06-
|
| 5 |
|
| 6 |
---
|
| 7 |
|
|
@@ -18,6 +18,7 @@
|
|
| 18 |
9. [Stage 8 — Eval Dashboard + Rigorous Metrics](#stage-8--eval-dashboard--rigorous-metrics-2026-06-17)
|
| 19 |
10. [Stage 9 — Multi-Query Retrieval](#stage-9--multi-query-retrieval-2026-06-19)
|
| 20 |
11. [Stage 10 — Contextual Retrieval (Eval)](#stage-10--contextual-retrieval-eval-2026-06-20)
|
|
|
|
| 21 |
12. [Stage 11 — Contextual Retrieval in Production + Dashboard Polish](#stage-11--contextual-retrieval-in-production--dashboard-polish-2026-06-20)
|
| 22 |
13. [Current State Snapshot](#current-state-snapshot)
|
| 23 |
13. [Roadmap — Retrieval & Answer Quality](#roadmap--retrieval--answer-quality)
|
|
@@ -487,15 +488,43 @@ Eval: Separate eval-dashboard/ static site → https://askprism-eval.ver
|
|
| 487 |
Versioning: MAJOR.MINOR.PATCH — name changes on MAJOR only (v1.x.x=Violet, v2.x.x=Indigo)
|
| 488 |
Frontend: Violet v1.3 badge in sidebar footer. Maintenance banner config-driven (frontend/src/config.js).
|
| 489 |
Workspaces: Per-workspace ChromaDB collection, singleton retriever cache
|
| 490 |
-
Infra:
|
| 491 |
https://askprism.vercel.app/ (frontend) + https://askprism-eval.vercel.app/ (eval)
|
| 492 |
-
|
| 493 |
-
|
|
|
|
| 494 |
Observability: LangSmith traces all LLM + retrieval calls (optional, env var)
|
| 495 |
```
|
| 496 |
|
| 497 |
---
|
| 498 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
## Roadmap — Retrieval & Answer Quality
|
| 500 |
|
| 501 |
### Phase 1 — Quick wins (no infra change, measurable RAGAS lift)
|
|
@@ -528,7 +557,7 @@ Observability: LangSmith traces all LLM + retrieval calls (optional, env var)
|
|
| 528 |
### Phase 3 — UX + trust
|
| 529 |
|
| 530 |
#### Streaming Responses
|
| 531 |
-
- **Problem:** User submits question → 8–15s wait → full answer appears.
|
| 532 |
- **How:** Backend: `chain.astream_events()` → `StreamingResponse` yielding SSE tokens. Frontend: `EventSource` or `fetch` + `ReadableStream` — append tokens as they arrive. Faithfulness scoring runs as background task after full answer assembled.
|
| 533 |
- **Effort:** High — both backend and frontend change. `ConversationalRetrievalChain` supports `astream_events()` in LangChain ≥0.2.
|
| 534 |
- **Impact:** Perceived latency drops from 10s to ~1s. Single biggest UX improvement.
|
|
|
|
| 1 |
# Prism — Project Evolution
|
| 2 |
|
| 3 |
> End-to-end record of what was broken at each stage, what was built to fix it, and what is planned next.
|
| 4 |
+
> Updated as the project evolves. Last updated: 2026-06-22 (Stage 12).
|
| 5 |
|
| 6 |
---
|
| 7 |
|
|
|
|
| 18 |
9. [Stage 8 — Eval Dashboard + Rigorous Metrics](#stage-8--eval-dashboard--rigorous-metrics-2026-06-17)
|
| 19 |
10. [Stage 9 — Multi-Query Retrieval](#stage-9--multi-query-retrieval-2026-06-19)
|
| 20 |
11. [Stage 10 — Contextual Retrieval (Eval)](#stage-10--contextual-retrieval-eval-2026-06-20)
|
| 21 |
+
12. [Stage 12 — HF Spaces Migration](#stage-12--hf-spaces-migration-2026-06-22)
|
| 22 |
12. [Stage 11 — Contextual Retrieval in Production + Dashboard Polish](#stage-11--contextual-retrieval-in-production--dashboard-polish-2026-06-20)
|
| 23 |
13. [Current State Snapshot](#current-state-snapshot)
|
| 24 |
13. [Roadmap — Retrieval & Answer Quality](#roadmap--retrieval--answer-quality)
|
|
|
|
| 488 |
Versioning: MAJOR.MINOR.PATCH — name changes on MAJOR only (v1.x.x=Violet, v2.x.x=Indigo)
|
| 489 |
Frontend: Violet v1.3 badge in sidebar footer. Maintenance banner config-driven (frontend/src/config.js).
|
| 490 |
Workspaces: Per-workspace ChromaDB collection, singleton retriever cache
|
| 491 |
+
Infra: HF Spaces CPU Basic (backend, 16GB RAM, ephemeral FS — re-upload required after cold start) +
|
| 492 |
https://askprism.vercel.app/ (frontend) + https://askprism-eval.vercel.app/ (eval)
|
| 493 |
+
Backend URL: https://benroshan-prism.hf.space
|
| 494 |
+
Known limits: Euron embed ~5s/chunk sequential — 30 chunks = ~150s blocking upload. Next: move embed to background.
|
| 495 |
+
HF Spaces ephemeral FS: chroma_db lost on cold start. Fix: mount HF persistent storage bucket.
|
| 496 |
Observability: LangSmith traces all LLM + retrieval calls (optional, env var)
|
| 497 |
```
|
| 498 |
|
| 499 |
---
|
| 500 |
|
| 501 |
+
## Stage 12 — HF Spaces Migration (2026-06-22)
|
| 502 |
+
|
| 503 |
+
### What was wrong
|
| 504 |
+
Render free tier (512MB RAM) caused repeated OOM crashes under contextual retrieval:
|
| 505 |
+
- Base RSS after upload = 524MB (over the 512MB limit)
|
| 506 |
+
- `gc.collect()` had no effect — ChromaDB HNSW index + torch runtime held by native allocators, not Python heap
|
| 507 |
+
- Contextual refresh (3 async Groq coroutines) + simultaneous chat (Tavily + LLM + CrossEncoder) = peak exceeded 512MB
|
| 508 |
+
- Workarounds (RSS guard skipping contextual retrieval, web search suppression during refresh) negated the +18% recall improvement
|
| 509 |
+
|
| 510 |
+
### What we built
|
| 511 |
+
|
| 512 |
+
| File | Change |
|
| 513 |
+
|------|--------|
|
| 514 |
+
| `Dockerfile` | Port 8000 → 7860 (HF convention). Add `useradd -m -u 1000 user` + `chown -R user /app` (HF runs containers as UID 1000). Set `HF_HOME=/app/.cache/huggingface` BEFORE pre-download so user 1000 owns cached weights. Set `HF_HUB_OFFLINE=1` AFTER download. |
|
| 515 |
+
| `README.md` | Added HF Spaces frontmatter (`sdk: docker`, `app_port: 7860`). Updated deploy instructions. |
|
| 516 |
+
| `server/routes/chat.py` | Removed `is_contextualizing` web search suppression guard (Render-specific). |
|
| 517 |
+
| `server/routes/upload.py` | Removed `RSS > 460MB` contextual retrieval skip guard (Render-specific). |
|
| 518 |
+
| `docs/`, `decisions.md` | Render → HF Spaces across all infra references. |
|
| 519 |
+
|
| 520 |
+
### Key discoveries
|
| 521 |
+
- HF_HUB_OFFLINE must be set AFTER the pre-download RUN step — setting it before blocks the download itself
|
| 522 |
+
- Docker build runs pre-download as root by default; must `USER 1000` first then set `HF_HOME` under `/app` so runtime user 1000 can read the cached weights
|
| 523 |
+
- HF Spaces free CPU Basic: 2 vCPUs, 16GB RAM — resolves all Render OOM issues permanently
|
| 524 |
+
- Contextual retrieval now runs fully in production (was silently skipped by RSS guard on Render)
|
| 525 |
+
|
| 526 |
+
---
|
| 527 |
+
|
| 528 |
## Roadmap — Retrieval & Answer Quality
|
| 529 |
|
| 530 |
### Phase 1 — Quick wins (no infra change, measurable RAGAS lift)
|
|
|
|
| 557 |
### Phase 3 — UX + trust
|
| 558 |
|
| 559 |
#### Streaming Responses
|
| 560 |
+
- **Problem:** User submits question → 8–15s wait → full answer appears. Feels broken even on fast hardware.
|
| 561 |
- **How:** Backend: `chain.astream_events()` → `StreamingResponse` yielding SSE tokens. Frontend: `EventSource` or `fetch` + `ReadableStream` — append tokens as they arrive. Faithfulness scoring runs as background task after full answer assembled.
|
| 562 |
- **Effort:** High — both backend and frontend change. `ConversationalRetrievalChain` supports `astream_events()` in LangChain ≥0.2.
|
| 563 |
- **Impact:** Perceived latency drops from 10s to ~1s. Single biggest UX improvement.
|