axiom-coding-agent-setup 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.agents/skills/agent-browser/SKILL.md +55 -0
  2. package/.agents/skills/developing-with-streamlit/SKILL.md +220 -0
  3. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-chat-ui/SKILL.md +195 -0
  4. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/SKILL.md +233 -0
  5. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/packaged-components.md +212 -0
  6. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/state-sync.md +149 -0
  7. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/theme-css-variables.md +243 -0
  8. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/troubleshooting.md +121 -0
  9. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-dashboards/SKILL.md +147 -0
  10. package/.agents/skills/developing-with-streamlit/skills/building-streamlit-multipage-apps/SKILL.md +218 -0
  11. package/.agents/skills/developing-with-streamlit/skills/choosing-streamlit-selection-widgets/SKILL.md +139 -0
  12. package/.agents/skills/developing-with-streamlit/skills/connecting-streamlit-to-snowflake/SKILL.md +188 -0
  13. package/.agents/skills/developing-with-streamlit/skills/creating-streamlit-themes/SKILL.md +486 -0
  14. package/.agents/skills/developing-with-streamlit/skills/displaying-streamlit-data/SKILL.md +199 -0
  15. package/.agents/skills/developing-with-streamlit/skills/improving-streamlit-design/SKILL.md +191 -0
  16. package/.agents/skills/developing-with-streamlit/skills/optimizing-streamlit-performance/SKILL.md +323 -0
  17. package/.agents/skills/developing-with-streamlit/skills/organizing-streamlit-code/SKILL.md +91 -0
  18. package/.agents/skills/developing-with-streamlit/skills/setting-up-streamlit-environment/SKILL.md +128 -0
  19. package/.agents/skills/developing-with-streamlit/skills/using-streamlit-cli/SKILL.md +165 -0
  20. package/.agents/skills/developing-with-streamlit/skills/using-streamlit-custom-components/SKILL.md +170 -0
  21. package/.agents/skills/developing-with-streamlit/skills/using-streamlit-layouts/SKILL.md +229 -0
  22. package/.agents/skills/developing-with-streamlit/skills/using-streamlit-markdown/SKILL.md +207 -0
  23. package/.agents/skills/developing-with-streamlit/skills/using-streamlit-session-state/SKILL.md +144 -0
  24. package/.agents/skills/developing-with-streamlit/templates/.gitattributes +3 -0
  25. package/.agents/skills/developing-with-streamlit/templates/apps/README.md +188 -0
  26. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-companies/pyproject.toml +12 -0
  27. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-companies/streamlit_app.py +365 -0
  28. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute/pyproject.toml +12 -0
  29. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute/streamlit_app.py +461 -0
  30. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute-snowflake/.streamlit/secrets.toml.example +20 -0
  31. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute-snowflake/pyproject.toml +11 -0
  32. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute-snowflake/snowflake.yml +18 -0
  33. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-compute-snowflake/streamlit_app.py +527 -0
  34. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-feature-usage/pyproject.toml +12 -0
  35. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-feature-usage/streamlit_app.py +307 -0
  36. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics/pyproject.toml +12 -0
  37. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics/streamlit_app.py +426 -0
  38. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics-snowflake/.streamlit/secrets.toml.example +20 -0
  39. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics-snowflake/pyproject.toml +11 -0
  40. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics-snowflake/snowflake.yml +18 -0
  41. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-metrics-snowflake/streamlit_app.py +463 -0
  42. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-seattle-weather/pyproject.toml +12 -0
  43. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-seattle-weather/streamlit_app.py +252 -0
  44. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers/pyproject.toml +12 -0
  45. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers/streamlit_app.py +342 -0
  46. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers-snowflake/.streamlit/secrets.toml.example +20 -0
  47. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers-snowflake/pyproject.toml +11 -0
  48. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers-snowflake/snowflake.yml +18 -0
  49. package/.agents/skills/developing-with-streamlit/templates/apps/dashboard-stock-peers-snowflake/streamlit_app.py +384 -0
  50. package/.agents/skills/developing-with-streamlit/templates/themes/README.md +128 -0
  51. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/dracula.toml +39 -0
  52. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/github.toml +37 -0
  53. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/minimal.toml +39 -0
  54. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/nord.toml +39 -0
  55. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/snowflake.toml +42 -0
  56. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/solarized-light.toml +38 -0
  57. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/spotify.toml +34 -0
  58. package/.agents/skills/developing-with-streamlit/templates/themes/_configs/stripe.toml +35 -0
  59. package/.agents/skills/developing-with-streamlit/templates/themes/_shared/streamlit_app.py +336 -0
  60. package/.agents/skills/developing-with-streamlit/templates/themes/_templates/pyproject.toml.tmpl +12 -0
  61. package/.agents/skills/developing-with-streamlit/templates/themes/dracula/.streamlit/config.toml +39 -0
  62. package/.agents/skills/developing-with-streamlit/templates/themes/dracula/pyproject.toml +10 -0
  63. package/.agents/skills/developing-with-streamlit/templates/themes/dracula/streamlit_app.py +337 -0
  64. package/.agents/skills/developing-with-streamlit/templates/themes/github/.streamlit/config.toml +37 -0
  65. package/.agents/skills/developing-with-streamlit/templates/themes/github/pyproject.toml +10 -0
  66. package/.agents/skills/developing-with-streamlit/templates/themes/github/streamlit_app.py +337 -0
  67. package/.agents/skills/developing-with-streamlit/templates/themes/manage.py +332 -0
  68. package/.agents/skills/developing-with-streamlit/templates/themes/minimal/.streamlit/config.toml +39 -0
  69. package/.agents/skills/developing-with-streamlit/templates/themes/minimal/pyproject.toml +10 -0
  70. package/.agents/skills/developing-with-streamlit/templates/themes/minimal/streamlit_app.py +337 -0
  71. package/.agents/skills/developing-with-streamlit/templates/themes/nord/.streamlit/config.toml +39 -0
  72. package/.agents/skills/developing-with-streamlit/templates/themes/nord/pyproject.toml +10 -0
  73. package/.agents/skills/developing-with-streamlit/templates/themes/nord/streamlit_app.py +337 -0
  74. package/.agents/skills/developing-with-streamlit/templates/themes/snowflake/.streamlit/config.toml +42 -0
  75. package/.agents/skills/developing-with-streamlit/templates/themes/snowflake/pyproject.toml +10 -0
  76. package/.agents/skills/developing-with-streamlit/templates/themes/snowflake/streamlit_app.py +337 -0
  77. package/.agents/skills/developing-with-streamlit/templates/themes/solarized-light/.streamlit/config.toml +38 -0
  78. package/.agents/skills/developing-with-streamlit/templates/themes/solarized-light/pyproject.toml +10 -0
  79. package/.agents/skills/developing-with-streamlit/templates/themes/solarized-light/streamlit_app.py +337 -0
  80. package/.agents/skills/developing-with-streamlit/templates/themes/spotify/.streamlit/config.toml +34 -0
  81. package/.agents/skills/developing-with-streamlit/templates/themes/spotify/pyproject.toml +10 -0
  82. package/.agents/skills/developing-with-streamlit/templates/themes/spotify/streamlit_app.py +337 -0
  83. package/.agents/skills/developing-with-streamlit/templates/themes/stripe/.streamlit/config.toml +35 -0
  84. package/.agents/skills/developing-with-streamlit/templates/themes/stripe/pyproject.toml +10 -0
  85. package/.agents/skills/developing-with-streamlit/templates/themes/stripe/streamlit_app.py +337 -0
  86. package/.agents/skills/fastapi/SKILL.md +436 -0
  87. package/.agents/skills/fastapi/references/dependencies.md +142 -0
  88. package/.agents/skills/fastapi/references/other-tools.md +76 -0
  89. package/.agents/skills/fastapi/references/streaming.md +105 -0
  90. package/.agents/skills/frontend-design/LICENSE.txt +177 -0
  91. package/.agents/skills/frontend-design/SKILL.md +42 -0
  92. package/.agents/skills/gradio/SKILL.md +84 -0
  93. package/.agents/skills/gradio/references/api-signatures.md +115 -0
  94. package/.agents/skills/gradio/references/event-listeners.md +130 -0
  95. package/.agents/skills/gradio/references/examples.md +613 -0
  96. package/.agents/skills/project-design/SKILL.md +207 -0
  97. package/.agents/skills/project-design/references/ARCHITECTURE.md +641 -0
  98. package/.agents/skills/project-design/references/PROJECT_PLAN.md +316 -0
  99. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  100. package/.agents/skills/skill-creator/SKILL.md +485 -0
  101. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  102. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  103. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  104. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  105. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  106. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  107. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  108. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  109. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  110. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  111. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  112. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  113. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  114. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  115. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  116. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  117. package/.agents/skills/ui-ux-pro-max/SKILL.md +659 -0
  118. package/.agents/skills/ui-ux-pro-max/data +1 -0
  119. package/.agents/skills/ui-ux-pro-max/scripts +1 -0
  120. package/.env.axiom +8 -0
  121. package/AGENTS.md +2 -1
  122. package/README.md +20 -3
  123. package/bin/cli.js +12 -1
  124. package/opencode.json +7 -4
  125. package/package.json +1 -1
  126. package/plugin/oh-my-openagent.json +198 -0
  127. package/plugin/oh-my-openagent.md +49 -0
  128. package/skills-lock.json +42 -0
  129. package/.agents/skills/n8n-patterns/SKILL.md +0 -272
@@ -0,0 +1,641 @@
1
+ # AdaptiveRAG — Architecture
2
+
3
+ A hybrid Adaptive RAG system. Each query is classified at runtime into one of five execution strategies (`no_retrieval`, `vector_only`, `sql_only`, `hybrid`, `clarify`) and dispatched to the right backend(s). Documents flow through a markdown-first ingestion pipeline; the retrieval layer is hybrid (dense + BM25 + cross-encoder rerank); the SQL layer is read-only with defense in depth.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Goals & Non-Goals](#1-goals--non-goals)
10
+ 2. [Core Principles](#2-core-principles)
11
+ 3. [System Overview](#3-system-overview)
12
+ 4. [Tech Stack](#4-tech-stack)
13
+ 5. [Project Structure](#5-project-structure)
14
+ 6. [Ingestion Pipeline](#6-ingestion-pipeline)
15
+ 7. [Chunking Strategy](#7-chunking-strategy)
16
+ 8. [Retrieval Layer](#8-retrieval-layer)
17
+ 9. [Adaptive Query Router](#9-adaptive-query-router)
18
+ 10. [Tool Layer (Read-Only SQL)](#10-tool-layer-read-only-sql)
19
+ 11. [Synthesis & Citations](#11-synthesis--citations)
20
+ 12. [Caching & Cost Control](#12-caching--cost-control)
21
+ 13. [Observability & Cost Tracking](#13-observability--cost-tracking)
22
+ 14. [Evaluation Framework](#14-evaluation-framework)
23
+ 15. [Configuration](#15-configuration)
24
+ 16. [Implementation Status](#16-implementation-status)
25
+ 17. [Decision Log](#17-decision-log)
26
+ 18. [Future Work](#18-future-work)
27
+
28
+ ---
29
+
30
+ ## 1. Goals & Non-Goals
31
+
32
+ ### Goals
33
+
34
+ - **Adaptive retrieval** — pick the right strategy per-query, not per-file-extension.
35
+ - **Markdown-first ingestion** — convert every input format to markdown so chunks are header-aware.
36
+ - **Hybrid retrieval at the index layer** — dense embeddings + BM25 + reciprocal-rank fusion + cross-encoder reranker.
37
+ - **Grounded answers with citations** — inline `[n]` markers for chunks, `[DB]` for SQL data, parsed back into structured citations for the UI.
38
+ - **Cost-bounded** — content-hash caches for OCR and embeddings; cheap models for routing, frontier models only for synthesis.
39
+ - **Portfolio-presentable** — clean code, working demo, real metrics in Phase 6.
40
+
41
+ ### Non-Goals
42
+
43
+ - Multi-tenant SaaS with RBAC.
44
+ - Real-time CDC / database mirroring into vectors. (See decision log: never embed structured data.)
45
+ - Distributed task queue. `FastAPI BackgroundTasks` is enough until proven otherwise.
46
+ - A constellation of MCP servers. One optional MCP surface that wraps the same tools is enough.
47
+ - Production monitoring stack (Prometheus / Grafana / Jaeger). Langfuse for traces is enough.
48
+
49
+ ---
50
+
51
+ ## 2. Core Principles
52
+
53
+ 1. **If the answer is a sentence, embed it. If the answer is a number, query it.** Free text goes to vectors; structured data stays in SQL.
54
+ 2. **Decide adaptively at query time, not at ingest time.** A PDF can contain prose *and* tables; a SQL row can have a free-text comment. Routing has to see the question, not just the file.
55
+ 3. **Markdown is the universal intermediate format.** Every parser output normalizes to markdown before chunking.
56
+ 4. **Quality of parsing beats quantity of features.** One excellent ingestion path is better than five mediocre ones.
57
+ 5. **Measure before optimizing.** No reranker, no advanced chunking, no MCP — until eval scores justify each addition.
58
+
59
+ ---
60
+
61
+ ## 3. System Overview
62
+
63
+ ```
64
+ ┌──────────────────────────────────────────────────────────────────────┐
65
+ │ INGESTION (offline) │
66
+ │ │
67
+ │ File ─► FileTypeDetector ─► ParserRouter ─► Markdown ─► Chunker │
68
+ │ │ │
69
+ │ ├─ Docling (default) │
70
+ │ ├─ Qwen3-VL (image / scan) │
71
+ │ └─ passthrough (.md, .txt) │
72
+ │ │
73
+ │ Markdown ─► MarkdownHeaderSplitter ─► Embedder ─► Qdrant │
74
+ │ │ │
75
+ │ ├─ dense (text-emb-3) │
76
+ │ └─ sparse (BM25 / IDF) │
77
+ └──────────────────────────────────────────────────────────────────────┘
78
+
79
+ ┌──────────────────────────────────────────────────────────────────────┐
80
+ │ QUERY-TIME (online) │
81
+ │ │
82
+ │ user query │
83
+ │ │ │
84
+ │ ▼ │
85
+ │ ┌──────────────────────┐ │
86
+ │ │ AdaptiveRouter │ cheap LLM classifier │
87
+ │ │ → strategy + intent │ (gpt-4.1-nano default) │
88
+ │ └────────┬─────────────┘ │
89
+ │ │ │
90
+ │ ┌───────┼───────────┬─────────────┬────────────────┐ │
91
+ │ ▼ ▼ ▼ ▼ ▼ │
92
+ │ no_retr. vector sql_only hybrid clarify │
93
+ │ (LLM) ↓ Qdrant ↓ NL→SQL ↓ both (ask user) │
94
+ │ ↓ + rerank ↓ + execute ↓ + merge │
95
+ │ └──────────┴────────────┘ │
96
+ │ │ │
97
+ │ ▼ │
98
+ │ GroundedAnswerer (LLM) │
99
+ │ │ │
100
+ │ ▼ │
101
+ │ answer with [n] / [DB] citations │
102
+ └──────────────────────────────────────────────────────────────────────┘
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 4. Tech Stack
108
+
109
+ | Component | Library | Role |
110
+ |---|---|---|
111
+ | Document parsing | `docling>=2.92` | Born-digital PDFs, DOCX, PPTX, XLSX, HTML, CSV |
112
+ | OCR | `openai>=2.33` against DashScope OpenAI-compat endpoint | Qwen3-VL-Plus for images and scanned PDFs |
113
+ | LLM framework | `langchain>=1.2`, `langchain-core>=1.3` | Message types, prompts, structured output |
114
+ | LLM client | `langchain-openai>=1.2` | Chat synthesis, router, NL→SQL |
115
+ | Embeddings — dense | `langchain-openai` + `text-embedding-3-small` | 1536-dim, with SHA256 disk cache |
116
+ | Embeddings — sparse | `fastembed>=0.4` (`Qdrant/bm25`) | Local BM25 with IDF, no GPU |
117
+ | Vector DB | `qdrant-client>=1.12` + `langchain-qdrant>=1.1` | Hybrid collection (dense + sparse named vectors) |
118
+ | Reranker | `flashrank>=0.2.9` | Pure-ONNX cross-encoder (`ms-marco-MiniLM-L-12-v2`, ~34 MB) |
119
+ | Splitters | `langchain-text-splitters>=1.1` | Header-aware + recursive fallback |
120
+ | PDF inspection | `pypdfium2>=4.30` | Born-digital heuristic + page rendering |
121
+ | Schema validation | `pydantic>=2.9` | Structured router output, structured NL→SQL |
122
+ | SQL | `sqlalchemy>=2.0.36` + `psycopg[binary]>=3.2.3` | Read-only Postgres tool |
123
+ | Retry | `tenacity>=9.x` | Qwen API resilience |
124
+ | UI | `gradio>=6.13` | Tabbed Chat / Ingest / Convert / Admin demo |
125
+ | Tracing | `langfuse>=4.0` | Per-turn spans, token counts, USD cost |
126
+ | Eval | `deepeval>=3.0` | Faithfulness / answer relevancy / contextual relevancy |
127
+ | Env | `python-dotenv>=1.2` | `.env` config loader |
128
+
129
+ ### Explicitly avoided
130
+
131
+ - `celery`, `redis` — not needed; `FastAPI BackgroundTasks` is sufficient.
132
+ - `transformers`, `torch`, `accelerate` — Qwen is API-based and FlashRank uses ONNX. Keeps the install lean and dodges Python-3.14 native-extension instability.
133
+ - `watchdog` — explicit upload via UI/API is fine.
134
+ - CDC tooling (Debezium et al.) — query DB live via tool, never sync.
135
+ - Prometheus / Grafana — Langfuse covers it for v1.
136
+
137
+ ---
138
+
139
+ ## 5. Project Structure
140
+
141
+ ```
142
+ adaptive-rag/
143
+ ├── app.py Gradio entry point
144
+ ├── pyproject.toml
145
+ ├── docker-compose.yml Qdrant + Postgres
146
+ ├── .env.example
147
+ ├── ARCHITECTURE.md (this file)
148
+ ├── README.md
149
+ ├── PROJECT_PLAN.md Phase-by-phase status
150
+ ├── scripts/
151
+ │ ├── init_qdrant.py Create / recreate the Qdrant collection
152
+ │ └── seed_demo_data.py Seed Postgres with demo e-commerce data
153
+
154
+ ├── src/
155
+ │ ├── config/
156
+ │ │ └── settings.py Single source of truth for all tunables
157
+ │ │
158
+ │ ├── core/ Document → markdown
159
+ │ │ ├── file_detector.py Format detection + validation
160
+ │ │ ├── docling_parser.py Docling-backed parser
161
+ │ │ ├── qwen_parser.py Qwen3-VL OCR with retry + cache
162
+ │ │ ├── parser_router.py Picks Docling vs Qwen per file
163
+ │ │ └── converter.py Public conversion API
164
+ │ │
165
+ │ ├── chunking/ Markdown → header-aware chunks
166
+ │ │ ├── markdown_chunker.py Header splitter + recursive fallback
167
+ │ │ └── metadata.py doc_id (SHA256), chunk_uuid (UUID5)
168
+ │ │
169
+ │ ├── indexing/ Chunks → Qdrant
170
+ │ │ ├── embeddings.py Dense (cached) + BM25 sparse
171
+ │ │ ├── qdrant_store.py Hybrid collection + dedup + library
172
+ │ │ └── pipeline.py Convert → chunk → upsert
173
+ │ │
174
+ │ ├── retrieval/ Query → ranked chunks
175
+ │ │ ├── hybrid_search.py HybridRetriever + RetrievalPipeline
176
+ │ │ └── reranker.py FlashRank ONNX cross-encoder
177
+ │ │
178
+ │ ├── routing/ Adaptive router + dispatcher
179
+ │ │ ├── strategies.py Strategy StrEnum + capability sets
180
+ │ │ ├── prompts.py Router system prompt + few-shots
181
+ │ │ ├── adaptive_router.py LLM classifier (structured output)
182
+ │ │ └── dispatcher.py Compose router + retrieval + SQL + synthesis
183
+ │ │
184
+ │ ├── tools/ External tools the dispatcher can call
185
+ │ │ └── sql_tool.py Read-only NL→SQL with safety guards
186
+ │ │
187
+ │ ├── synthesis/ Chunks (+ SQL) → grounded answer
188
+ │ │ └── response.py GroundedAnswerer + Citation parsing
189
+ │ │
190
+ │ ├── observability/ Tracing + cost tracking
191
+ │ │ ├── langfuse_client.py Singleton Langfuse + no-op span() ctx mgr
192
+ │ │ └── cost_tracker.py Pulls daily metrics from Langfuse REST API
193
+ │ │
194
+ │ ├── eval/ Golden set + accuracy / DeepEval runners
195
+ │ │ ├── golden.jsonl Tiny smoke golden set (~one row per strategy)
196
+ │ │ ├── run_routing_eval.py Router-only accuracy gate (CI-friendly)
197
+ │ │ └── run_deepeval.py DeepEval runner + JSON + HTML report
198
+ │ │
199
+ │ ├── cache/ Content-hash caches
200
+ │ │ ├── ocr_cache.py SHA256-keyed disk cache for OCR markdown
201
+ │ │ └── embedding_cache.py SHA256-keyed disk cache for vectors
202
+ │ │
203
+ │ ├── utils/
204
+ │ │ └── pdf_inspector.py Born-digital heuristic + page rendering
205
+ │ │
206
+ │ └── ui/ Gradio interface
207
+ │ ├── main_ui.py Tab composition
208
+ │ ├── chat_ui.py Chat tab (calls AdaptiveDispatcher)
209
+ │ ├── ingest_ui.py Ingest tab (multi-file upload + library)
210
+ │ ├── markdown_converter_ui.py Convert tab (single-document preview)
211
+ │ └── admin_ui.py Admin tab — Langfuse cost dashboard
212
+
213
+ └── docs/
214
+ ├── check_postgres.md DB inspection cheatsheet
215
+ └── check_qdrant.md Vector DB inspection cheatsheet
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 6. Ingestion Pipeline
221
+
222
+ ### Parser routing
223
+
224
+ ```
225
+ file_type ──┐
226
+ ├── .md / .txt ──────────────────► passthrough
227
+
228
+ ├── .pdf ──┬─ "born-digital" ───► Docling (fast, text layer)
229
+ │ └─ "scanned" ────────► Qwen3-VL (vision)
230
+
231
+ ├── .docx / .pptx / .xlsx / .html / .csv ──► Docling
232
+
233
+ └── .png / .jpg / .webp ──► Qwen3-VL (better than Tesseract on layout)
234
+ ```
235
+
236
+ The "born-digital vs scanned" decision for PDFs is a cheap heuristic: render the text layer of the first three pages and treat the file as scanned if the total extracted character count is below a small threshold. Docling has its own internal OCR fallback (EasyOCR / Tesseract); the heuristic lets us skip that path and use Qwen3-VL when accuracy matters.
237
+
238
+ The user can override this routing per-file with a "Force Qwen3-VL OCR for PDFs" toggle in the Convert tab.
239
+
240
+ ### Qwen3-VL OCR
241
+
242
+ Calls the DashScope OpenAI-compatible endpoint. The OCR prompt is intentionally deterministic:
243
+
244
+ > Extract all text from this image into clean GitHub-flavored Markdown. Preserve table structure with pipe syntax. Preserve heading hierarchy. Do not summarize, do not add commentary. If text is illegible, write `[illegible]`.
245
+
246
+ `tenacity` handles transient API failures with exponential backoff. The result is content-hash cached so re-uploading the same file (or re-rendering the same page from a multi-page PDF) never spends a second API call.
247
+
248
+ ### Content-hash everything
249
+
250
+ Three things use SHA256 as a primary key:
251
+
252
+ | Cache | Key | Stored |
253
+ |---|---|---|
254
+ | OCR | `sha256(image_bytes)` | Markdown text on disk |
255
+ | Embeddings | `sha256(model_name + text)` | Raw `float32` vector bytes on disk |
256
+ | Documents | `sha256(file_bytes)[:16]` | `doc_id` for dedup + library listing |
257
+
258
+ Re-ingesting the same file replaces its prior chunks in Qdrant atomically (delete-by-`doc_id` then upsert).
259
+
260
+ ---
261
+
262
+ ## 7. Chunking Strategy
263
+
264
+ Two-pass:
265
+
266
+ 1. **`MarkdownHeaderTextSplitter`** splits by `#`, `##`, `###`. Headers are kept in the chunk content and the header path is also written to chunk metadata.
267
+ 2. **`RecursiveCharacterTextSplitter`** splits any header-section that exceeds `CHUNK_SIZE` (default 1500 chars). Each sub-chunk inherits the parent's header path.
268
+
269
+ ### Per-chunk metadata
270
+
271
+ ```json
272
+ {
273
+ "doc_id": "dc7c3912cd0b003d",
274
+ "source": "data/policy.pdf",
275
+ "filename": "policy.pdf",
276
+ "header_path": "Refund Policy > Eligibility",
277
+ "chunk_index": 7,
278
+ "total_chunks": 23,
279
+ "ingested_at": "2026-05-09T04:52:24+00:00",
280
+ "parser": "docling" // or "qwen3-vl" or "passthrough"
281
+ }
282
+ ```
283
+
284
+ `chunk_uuid` is generated deterministically via `UUID5(doc_id, chunk_index)` so re-upserts are idempotent.
285
+
286
+ No `access_level` / `department` / `tags` in v1 — those go in only when a feature actually consumes them.
287
+
288
+ ---
289
+
290
+ ## 8. Retrieval Layer
291
+
292
+ ### Qdrant collection: hybrid by default
293
+
294
+ ```python
295
+ client.create_collection(
296
+ collection_name="adaptive_rag",
297
+ vectors_config={
298
+ "dense": models.VectorParams(size=1536, distance=models.Distance.COSINE),
299
+ },
300
+ sparse_vectors_config={
301
+ "bm25": models.SparseVectorParams(modifier=models.Modifier.IDF),
302
+ },
303
+ )
304
+ ```
305
+
306
+ Both vectors are populated for every chunk at ingest time; queries hit both.
307
+
308
+ ### Query flow
309
+
310
+ ```
311
+ query
312
+
313
+ ├─► dense embedding (text-embedding-3-small, cached)
314
+ ├─► sparse encoding (FastEmbed BM25, IDF on server)
315
+
316
+
317
+ Qdrant `query_points` with prefetch:
318
+ - prefetch dense (top RETRIEVAL_PREFETCH_K = 25)
319
+ - prefetch sparse (top RETRIEVAL_PREFETCH_K = 25)
320
+ - fusion: server-side RRF
321
+
322
+
323
+ FlashRank cross-encoder rerank
324
+ → top RERANK_TOP_K = 5
325
+
326
+
327
+ Pass to GroundedAnswerer with header_path context
328
+ ```
329
+
330
+ The fusion is server-side RRF (Qdrant native), not client-side merging — single round-trip per query.
331
+
332
+ The reranker uses `ms-marco-MiniLM-L-12-v2` by default (~34 MB ONNX). Lazy first-use download to `CACHE_DIR/flashrank/`. If model loading or scoring fails, the pipeline gracefully falls back to the hybrid-fusion order. Alternatives configurable via `RERANKER_MODEL`:
333
+
334
+ | Model | Size | Notes |
335
+ |---|---|---|
336
+ | `ms-marco-TinyBERT-L-2-v2` | ~4 MB | Fastest |
337
+ | `ms-marco-MiniLM-L-12-v2` | ~34 MB | **Default** — balanced |
338
+ | `ms-marco-MultiBERT-L-12` | ~150 MB | Multilingual |
339
+ | `rank-T5-flan` | ~110 MB | Best quality |
340
+
341
+ Hybrid search typically yields **+5–15% retrieval recall** over pure dense; reranking adds another **+10–20% context precision** on top. Numbers will be re-validated against the golden set in Phase 6.
342
+
343
+ ---
344
+
345
+ ## 9. Adaptive Query Router
346
+
347
+ The router is what makes this *Adaptive RAG* (per Jeong et al., 2024 — query-complexity-aware strategy selection) rather than static dispatch.
348
+
349
+ ### Strategies
350
+
351
+ | Strategy | When | Touches |
352
+ |---|---|---|
353
+ | `no_retrieval` | Greeting, chitchat, generic knowledge, math | LLM only |
354
+ | `vector_only` | Conceptual / "what does our doc say about X" | Qdrant + reranker + LLM with `[n]` citations |
355
+ | `sql_only` | Quantitative / "how many" / "top N" / aggregates | NL→SQL → execute → LLM with `[DB]` citation |
356
+ | `hybrid` | Question needs both narrative AND a number | Vector AND SQL → blended answer |
357
+ | `clarify` | Genuinely ambiguous | One focused follow-up question, no retrieval cost |
358
+
359
+ ### How a decision is made
360
+
361
+ A single LLM call with structured output. The classifier model is intentionally cheap (`gpt-4.1-nano` by default) — classification doesn't need frontier reasoning, and we want this on the hot path of every chat turn.
362
+
363
+ ```python
364
+ class RouterDecision(BaseModel):
365
+ strategy: Strategy # one of the five above
366
+ reasoning: str # one-sentence justification
367
+ vector_query: str | None # optional rephrased search query
368
+ sql_intent: str | None # NL description for the SQL tool
369
+ clarification_question: str | None # only set when strategy == clarify
370
+ ```
371
+
372
+ The router prompt includes:
373
+ 1. Strategy descriptions + few-shot examples (anchors classification).
374
+ 2. The actual chat history (so follow-ups like "what about last quarter?" can resolve to a real referent instead of always falling to `clarify`).
375
+ 3. A one-line summary of available SQL tables — fetched via `inspect()` once at startup. ~80 tokens. Lets the router decide "this is a database question" vs "this is a docs question."
376
+
377
+ If `SQL_DATABASE_URL` is unset, the prompt is told "no SQL backend" and a sanitizer downgrades any leaked `sql_only`/`hybrid` decision to `vector_only`. The router can never pick a strategy it can't fulfill.
378
+
379
+ ### Dispatch
380
+
381
+ `AdaptiveDispatcher.answer(query, history)` is the single entry point the chat UI calls. It:
382
+
383
+ 1. Classifies the query.
384
+ 2. For `clarify` / `no_retrieval`: short-circuits without touching retrieval or SQL.
385
+ 3. For `vector_only` / `hybrid`: runs the retrieval pipeline.
386
+ 4. For `sql_only` / `hybrid`: runs the SQL tool. If the tool fails (DB down, query rejected), records a note and continues with whatever else it has.
387
+ 5. Calls the appropriate `GroundedAnswerer` method.
388
+ 6. Returns an `AdaptiveAnswer` with strategy, decision, citations, executed SQL, and per-stage timings.
389
+
390
+ Backends are initialized lazily on first use so the app starts fast even when SQL or OpenAI aren't configured.
391
+
392
+ ---
393
+
394
+ ## 10. Tool Layer (Read-Only SQL)
395
+
396
+ ### Defense in depth
397
+
398
+ The SQL tool is *not* an agent. It runs once per turn, with five layers of safety:
399
+
400
+ 1. **Dedicated read-only Postgres role.** `seed_demo_data.py` creates `adaptive_rag_ro` with `SELECT`-only grants. The app connects as that role.
401
+ 2. **Statement-level allowlist.** Only `SELECT` and `WITH` (CTE-resolved-to-SELECT) statements are accepted.
402
+ 3. **Forbidden-keyword regex.** Catches `INSERT|UPDATE|DELETE|MERGE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|COPY|VACUUM|...` even if the role grants would already block them.
403
+ 4. **Per-session statement timeout.** Default 5 seconds (`SQL_QUERY_TIMEOUT_SEC`); runaway plans die fast.
404
+ 5. **Implicit row cap.** A `LIMIT N` (default 200, `SQL_ROW_LIMIT`) is appended if the SQL doesn't already cap rows.
405
+
406
+ Plus `SET TRANSACTION READ ONLY` on every connection — belt + suspenders + bungee.
407
+
408
+ ### NL → SQL
409
+
410
+ A second LLM call (`gpt-4.1-mini` by default, `SQL_MODEL`) translates the router's `sql_intent` into a single `SELECT`. Schema is included in the prompt — generated once via SQLAlchemy `inspect()` at startup, formatted as DDL-style table descriptions with column types, primary keys, and foreign keys.
411
+
412
+ Structured output forces a `_SqlOutput { sql: str }` schema, so the LLM can't dump prose around the query.
413
+
414
+ ### Demo dataset
415
+
416
+ `scripts/seed_demo_data.py` builds a deterministic e-commerce schema:
417
+
418
+ ```
419
+ customers (100 rows) — id, name, email, country, signup_date
420
+ products (50 rows) — id, name, category, price, stock
421
+ orders (500 rows) — id, customer_id, status, total, created_at
422
+ order_items (~1300 rows) — id, order_id, product_id, quantity, unit_price
423
+ refunds (~35 rows) — id, order_id, amount, reason, created_at
424
+ ```
425
+
426
+ Idempotent (skips if already populated) with `--recreate` to wipe and reseed. The fixed RNG seed gives the same data every run, so demos and golden-set evals are reproducible.
427
+
428
+ ### Optional MCP surface (Phase 7)
429
+
430
+ The same `SqlTool` and retrieval pipeline can be exposed as an MCP server so Cursor / Claude Desktop can use them without the Gradio UI. This is the *correct* use of MCP — making the same capabilities reusable across LLM clients — not as a microservice framework.
431
+
432
+ ---
433
+
434
+ ## 11. Synthesis & Citations
435
+
436
+ `GroundedAnswerer` has three modes that the dispatcher selects:
437
+
438
+ | Method | Used for | Context shape |
439
+ |---|---|---|
440
+ | `answer_direct` | `no_retrieval` | Just the user query + chat history |
441
+ | `answer` | `vector_only` | Numbered passage block `[1]..[N]` |
442
+ | `answer_with_sql` | `sql_only` / `hybrid` | Passages **plus** a "Database query results" section with the executed SQL and a markdown-rendered preview of rows |
443
+
444
+ ### Citation contract
445
+
446
+ The system prompt requires inline brackets for every claim:
447
+
448
+ - `[1]`, `[2, 3]` — refer to chunk numbers in the passage block.
449
+ - `[DB]` — refers to the SQL results block (only valid when SQL data is present).
450
+
451
+ The chat layer parses the LLM's output back into structured citations:
452
+
453
+ ```python
454
+ @dataclass
455
+ class Citation:
456
+ index: int # 1-based chunk number
457
+ label: str # "policy.pdf > Refunds"
458
+ snippet: str # short preview
459
+ source: str | None
460
+ doc_id: str | None
461
+ rank: int
462
+ rerank_score: float | None
463
+ hybrid_score: float
464
+ ```
465
+
466
+ Only chunks the LLM actually cited end up in the right-hand Sources panel — clean UI by default, with a debug mode that surfaces all retrieved chunks if the LLM cites none. The `cited_db` boolean flag drives a separate "SQL executed" block in the panel.
467
+
468
+ ---
469
+
470
+ ## 12. Caching & Cost Control
471
+
472
+ Three caches on disk, all SHA256-keyed:
473
+
474
+ | Cache | Key | Rationale |
475
+ |---|---|---|
476
+ | OCR | `sha256(image_bytes)` | Qwen3-VL is paid per-call. Re-ingest of the same image must never re-OCR. |
477
+ | Dense embeddings | `sha256(model_name + text)` | OpenAI is paid per-token. Re-chunking with the same content must not re-embed. |
478
+ | Reranker model | FlashRank's own | First-use download (~34 MB), reused thereafter. |
479
+
480
+ Default location is `./.cache/`, override with `CACHE_DIR`.
481
+
482
+ Cost-per-query in the current configuration is dominated by the synthesis LLM (`gpt-4.1-mini`). Routing (`gpt-4.1-nano`) is roughly 1/10th the cost; SQL translation is one extra mini-call only when needed. The live numbers are tracked by Langfuse — see the next section.
483
+
484
+ ---
485
+
486
+ ## 13. Observability & Cost Tracking
487
+
488
+ Tracing is fully optional. When `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` are unset every helper in `src/observability/` is a no-op stub, so the rest of the application code is not branched on "is tracing on?".
489
+
490
+ ### Span tree per chat turn
491
+
492
+ `AdaptiveDispatcher.answer()` opens one parent span and the helpers open children:
493
+
494
+ ```
495
+ chat.turn (input=query, history_len)
496
+ ├── router.classify (input=query; output={strategy, reasoning, vector_query, sql_intent})
497
+ ├── retrieval.hybrid_search (input=query; metadata={fused_count, rerank_ms, prefetch_k, top_k, model})
498
+ ├── tool.sql_execute (input=intent; output={sql, row_count, columns, rows_preview})
499
+ └── synthesis.{direct|grounded} (input=ctx_summary; output={answer, cited_indices, cited_db})
500
+ ```
501
+
502
+ The LangChain `CallbackHandler` is also passed into every `ChatOpenAI.invoke(...)` (router, NL→SQL, both synthesis modes), so each LLM call appears as a `GENERATION` observation with token counts + the model price → USD cost computed server-side by Langfuse. No price table is duplicated client-side.
503
+
504
+ `flush_traces()` is called at the end of each turn so spans appear in the dashboard within a second; we don't rely on the background flush thread to survive a Gradio request boundary.
505
+
506
+ ### Cost tracker
507
+
508
+ `src/observability/cost_tracker.py` is a tiny `httpx`-based reader for Langfuse's `/api/public/metrics/daily` endpoint. It returns a `CostSummary` dataclass with:
509
+
510
+ - Total traces / observations / tokens / USD cost in the window
511
+ - Per-model breakdown (calls, in/out tokens, cost)
512
+ - Per-day breakdown
513
+
514
+ The Admin tab renders the same summary as a Gradio markdown view with a 24h / 7d / 30d window selector. CLI:
515
+
516
+ ```bash
517
+ uv run python -m src.observability.cost_tracker --days 7
518
+ ```
519
+
520
+ ---
521
+
522
+ ## 14. Evaluation Framework
523
+
524
+ Two complementary scripts. Both consume the same golden set and write reports under `src/eval/reports/`.
525
+
526
+ ### Golden set — `src/eval/golden.jsonl`
527
+
528
+ The default file ships as a **token-cheap smoke set**: about five rows (one per strategy). Add more rows to `golden.jsonl` anytime you want deeper coverage — there is no required size.
529
+
530
+ Each row has `id`, `query`, `expected_strategy`, optional `answer_must_contain` / `expected_sql_keywords`, and `notes`.
531
+
532
+ ### Router-only eval — `run_routing_eval.py`
533
+
534
+ Hits the `AdaptiveRouter` against every example without running retrieval / SQL / synthesis. Reports overall accuracy, per-strategy breakdown, latency, and a confusion matrix. Exits non-zero if accuracy drops below `--threshold` (default `0.85`) — drop into CI to catch router-prompt regressions cheaply.
535
+
536
+ ### Full pipeline + DeepEval — `run_deepeval.py`
537
+
538
+ For every example whose `expected_strategy` is `vector_only` or `hybrid`, runs the full dispatcher and feeds the (query, answer, retrieved chunks) triple into DeepEval:
539
+
540
+ - `FaithfulnessMetric` — claims in the answer are grounded in retrieved context
541
+ - `AnswerRelevancyMetric` — the answer addresses the actual question
542
+ - `ContextualRelevancyMetric` — the retrieved chunks were relevant to the query
543
+
544
+ None of these metrics require a hand-written reference answer — they work with just the `input`, `actual_output`, and `retrieval_context`. Adding `expected_output` later unlocks `ContextualPrecisionMetric` and `ContextualRecallMetric`.
545
+
546
+ Outputs:
547
+
548
+ - `src/eval/reports/deepeval_<timestamp>.json` — raw scores per row
549
+ - `src/eval/reports/deepeval_<timestamp>.html` — self-contained summary report
550
+
551
+ ---
552
+
553
+ ## 15. Configuration
554
+
555
+ Every tunable lives in `src/config/settings.py` — a frozen `Settings` dataclass loaded once from `.env` via `python-dotenv`. Some highlights:
556
+
557
+ | Setting | Default | What |
558
+ |---|---|---|
559
+ | `OPENAI_API_KEY` | (required) | Embeddings, router, SQL gen, synthesis |
560
+ | `QWEN_API_KEY` | (required for OCR) | DashScope endpoint |
561
+ | `QDRANT_URL` | `http://localhost:6333` (or unset → embedded mode) | Vector DB |
562
+ | `QDRANT_COLLECTION` | `adaptive_rag` | Collection name |
563
+ | `DENSE_MODEL` | `text-embedding-3-small` | Must match `DENSE_SIZE` |
564
+ | `DENSE_SIZE` | `1536` | Vector dimensions |
565
+ | `SPARSE_MODEL` | `Qdrant/bm25` | FastEmbed BM25 |
566
+ | `CHUNK_SIZE` / `CHUNK_OVERLAP` | `1500` / `200` | Recursive splitter |
567
+ | `RETRIEVAL_PREFETCH_K` | `25` | Candidates fetched before rerank |
568
+ | `RERANK_TOP_K` | `5` | Final chunks shown to the LLM |
569
+ | `RERANKER_MODEL` | `ms-marco-MiniLM-L-12-v2` | FlashRank model |
570
+ | `LLM_MODEL` | `gpt-4.1-mini` | Synthesis |
571
+ | `LLM_TEMPERATURE` | `0.2` | |
572
+ | `ROUTER_MODEL` | `gpt-4.1-nano` | Cheap classifier |
573
+ | `SQL_MODEL` | `gpt-4.1-mini` | NL→SQL translator |
574
+ | `SQL_DATABASE_URL` | (unset) | Leave unset to disable `sql_only` / `hybrid` strategies |
575
+ | `SQL_QUERY_TIMEOUT_SEC` | `5` | Per-query Postgres timeout |
576
+ | `SQL_ROW_LIMIT` | `200` | Implicit `LIMIT N` injection |
577
+ | `LANGFUSE_PUBLIC_KEY` | (unset) | Set both Langfuse keys to enable tracing — app is a no-op tracer when missing |
578
+ | `LANGFUSE_SECRET_KEY` | (unset) | — |
579
+ | `LANGFUSE_HOST` | `https://cloud.langfuse.com` | Override for the US region or self-hosted |
580
+ | `CACHE_DIR` | `./.cache` | OCR + embedding caches |
581
+
582
+ ---
583
+
584
+ ## 16. Implementation Status
585
+
586
+ | Phase | Status | Description |
587
+ |---|---|---|
588
+ | 0. Setup & infra | ✅ | Repo, deps, Docker compose, settings |
589
+ | 1. Docling baseline | ✅ | Document → markdown for native formats |
590
+ | 2. Parser router + Qwen | ✅ | Born-digital vs scanned heuristic, Qwen3-VL OCR with cache |
591
+ | 3. Chunking + indexing | ✅ | Header-aware chunks, hybrid Qdrant collection, dedup |
592
+ | 4. Retrieval + chat | ✅ | Hybrid search + RRF + FlashRank + grounded answers with `[n]` citations |
593
+ | 5. Adaptive router + SQL | ✅ | Five-strategy router, read-only SQL tool, `[DB]` citations |
594
+ | 6. Eval + tracing + polish | ✅ | Langfuse spans, cost dashboard, routing-accuracy gate, DeepEval runner with HTML reports |
595
+ | 7. Stretch | ⏸️ | C-RAG self-reflection, multi-hop, MCP server, web fallback |
596
+
597
+ See `PROJECT_PLAN.md` for the full phase-by-phase task list and acceptance criteria.
598
+
599
+ ---
600
+
601
+ ## 17. Decision Log
602
+
603
+ | Decision | Chosen | Rejected | Reason |
604
+ |---|---|---|---|
605
+ | Routing layer | Query-time LLM classifier | Ingest-time extension matching | A PDF can have prose AND tables; routing must see the question |
606
+ | Document parser | Docling | LangChain native loaders, LlamaParse, Marker | Free, local, table-aware, multi-format |
607
+ | OCR | Qwen3-VL-Plus (API) | GLM-OCR (self-hosted), EasyOCR, Tesseract | No GPU, better quality on complex layouts, cheap per-image |
608
+ | Vector DB | Qdrant | pgvector, Weaviate, Chroma | Best hybrid (dense + sparse) support, server-side RRF |
609
+ | Sparse encoder | BM25 via FastEmbed | SPLADE, no sparse | Free, fast, no GPU |
610
+ | Reranker | FlashRank `ms-marco-MiniLM-L-12-v2` | BGE-reranker-v2-m3 (Torch), Cohere Rerank (paid), ColBERT | Pure ONNX via `onnxruntime` (already pulled by `fastembed`); no Torch / Transformers; sidesteps Python-3.14 native-extension instability we hit with `py-rust-stemmers` |
611
+ | Router LLM | `gpt-4.1-nano` | `gpt-4.1-mini`, `claude-haiku` | Cheapest model that classifies reliably; classification doesn't need frontier reasoning |
612
+ | Synthesis LLM | `gpt-4.1-mini` | `gpt-4.1`, `gpt-4o-mini` | Strong enough to follow citation rules, cheap enough for hot path |
613
+ | Tool protocol | Native Python class with explicit dispatch | OpenAI function calling registry, MCP for everything | We're not an agent loop — the router *picks* a strategy, then we run it. Registry abstraction is dead weight. MCP only when external clients consume. |
614
+ | SQL backend | Postgres in Docker bound to host port 5433 | Default 5432, SQLite, Neon | 5433 sidesteps collisions with host-installed Postgres on 5432; users can swap to Neon by changing `SQL_DATABASE_URL` |
615
+ | Read-only enforcement | Dedicated DB role + statement allowlist + keyword regex + statement_timeout + LIMIT injection + READ ONLY transaction | Just one of those | Defense in depth — any one layer might be misconfigured |
616
+ | Task queue | `BackgroundTasks` | Celery + Redis | YAGNI; add when measured queue depth justifies it |
617
+ | File watching | Manual upload | watchdog filesystem watcher | UI-driven flow is enough; watcher is feature creep |
618
+ | DB sync to vectors | Live SQL tool, query at runtime | CDC (Debezium et al.) | Core principle: never embed structured data |
619
+ | Eval | DeepEval + custom routing-accuracy metric | Ragas (async compat issues on Python 3.14), vibes-based | Portfolio projects without metrics look unfinished; DeepEval has no `nest_asyncio` / `sniffio` issues |
620
+ | Tracing | Langfuse | LangSmith, Prometheus + Grafana + Jaeger | LLM-native, model-/framework-agnostic, MIT-licensed core, generous free tier (50k events/month), self-hosting available — sidesteps the LangSmith vendor lock-in concern |
621
+ | Eval reference answers | Skipped for v1 | Hand-written gold answers per row | DeepEval Faithfulness / Answer Relevancy / Contextual Relevancy don't need them; contextual precision and recall do. Add an `expected_output` field to the golden set when the synthesis prompt is stable enough that recall scores are trustworthy. |
622
+ | Cost table source | Langfuse server-side computation | Maintain price table in repo | Model prices change; Langfuse keeps theirs current. We just read totals back via REST. |
623
+ | Tracing default | Disabled (no-op stubs) | Always-on with warnings | Cleaner OSS UX — works without signup; opt-in by setting two env vars |
624
+
625
+ ---
626
+
627
+ ## 18. Future Work
628
+
629
+ ### Phase 7 — Stretch
630
+
631
+ - **C-RAG self-reflection.** Grade retrieved context for relevance; on low scores, re-route or fall back to web search.
632
+ - **Multi-hop retrieval.** When `clarify` would have been picked, escalate to step-by-step iterative search instead of asking the user.
633
+ - **MCP server.** Expose `search_docs` and `query_sql` so Cursor / Claude Desktop can use the same tools.
634
+ - **Web search fallback.** Tavily / Exa when local context is insufficient.
635
+ - **Streaming responses.** Wire LLM streaming through Gradio for snappier UX.
636
+ - **Multi-collection Qdrant.** Split per-domain (`policies`, `finance`, `technical`) once ingest volume justifies the operational cost.
637
+ - **Image-in-markdown OCR.** Extract images from Docling output, OCR them with Qwen, inline the text into the parent markdown.
638
+
639
+ ---
640
+
641
+ **End of architecture document.**