Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit ·
4835fb8
1
Parent(s): e0384e1
chore(docs): tidy root markdown — archive stale docs, delete superseded ones
Browse filesArchive (moved to local gitignored docs/_archive/): ARCHITECTURE, AGENT_ARCHITECTURE_CONTEXT_new, PROGRESS, CHECKPOINT_PLAN_2026-06-17, ORCHESTRATOR_REWORK_PLAN. Delete (stale/superseded by REPO_STATUS.md + DEV_PLAN.md): REPO_CONTEXT, PROJECT_SUMMARY, PHASE1_TO_PHASE2_REPORT. Root now keeps README + REPO_STATUS + DEV_PLAN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .gitignore +2 -3
- ARCHITECTURE.md +0 -353
- CHECKPOINT_PLAN_2026-06-17.md +0 -147
- PHASE1_TO_PHASE2_REPORT.md +0 -273
- PROGRESS.md +0 -692
- REPO_CONTEXT.md +0 -494
.gitignore
CHANGED
|
@@ -53,6 +53,5 @@ migratego/
|
|
| 53 |
docs/specs/tabular_parquet_contract.md
|
| 54 |
docs/specs/tabular_parquet.md
|
| 55 |
|
| 56 |
-
# Personal / local working docs (not for the shared repo)
|
| 57 |
-
|
| 58 |
-
PROJECT_SUMMARY.md
|
|
|
|
| 53 |
docs/specs/tabular_parquet_contract.md
|
| 54 |
docs/specs/tabular_parquet.md
|
| 55 |
|
| 56 |
+
# Personal / local working docs (not for the shared repo) — archived out of root
|
| 57 |
+
docs/_archive/
|
|
|
ARCHITECTURE.md
DELETED
|
@@ -1,353 +0,0 @@
|
|
| 1 |
-
# Architecture — Data Eyond Agentic Service
|
| 2 |
-
|
| 3 |
-
**Last updated**: 2026-05-20
|
| 4 |
-
**Status**: Phase 2 catalog path shipped; document ingestion has moved to a separate Go service. The long-term split is **Python = agent/ML layer, Go = data plane**; this document covers the Python side only.
|
| 5 |
-
|
| 6 |
-
---
|
| 7 |
-
|
| 8 |
-
## Product vision (north star)
|
| 9 |
-
|
| 10 |
-
Data Eyond is an *AI data scientist* for business analytics, structured around **CRISP-DM** (Business Understanding → Data Understanding → Data Preparation → Modeling → Evaluation → Deployment). Targets executives doing self-serve deep-dives and data analysts/scientists offloading routine work.
|
| 11 |
-
|
| 12 |
-
Envisioned user flow: **interview agent** captures goal → user connects data sources → asks natural-language question → CRISP-DM-structured analytical response, exportable as a **presentation** or **notebook-style report**.
|
| 13 |
-
|
| 14 |
-
The catalog-driven, IR-based architecture documented below is the *foundation*. The next architectural evolution is an agentic layer (analytical planner, per-stage CRISP-DM agents, evaluator, reporter) that consumes the existing IntentRouter → QueryPlanner → Executor → ChatbotAgent spine as its tool layer. See `REPO_CONTEXT.md` → *Roadmap — agentic evolution* for the target agent topology.
|
| 15 |
-
|
| 16 |
-
---
|
| 17 |
-
|
| 18 |
-
## TL;DR
|
| 19 |
-
|
| 20 |
-
A catalog-driven AI service for data analysis. Users upload documents and register databases or tabular files; they ask natural-language questions and get answers grounded in their data.
|
| 21 |
-
|
| 22 |
-
The architecture has two paths:
|
| 23 |
-
|
| 24 |
-
- **Unstructured** (PDF, DOCX, TXT) — dense similarity over prose chunks (the right primitive for free-form text). **Ingestion is handled by a separate Go service**; this Python service reads embeddings from PGVector at query time.
|
| 25 |
-
- **Structured** (databases, XLSX, CSV, Parquet) — a per-user **data catalog** describes what tables/columns exist; an LLM produces a structured **JSON intermediate representation (IR)** of the user's intent; a deterministic compiler turns the IR into SQL or pandas operations.
|
| 26 |
-
|
| 27 |
-
The LLM produces *intent*, not query syntax. Deterministic code does the rest.
|
| 28 |
-
|
| 29 |
-
---
|
| 30 |
-
|
| 31 |
-
## 1. Why catalog-driven design
|
| 32 |
-
|
| 33 |
-
For a database or spreadsheet, a user's question maps to *known tables and columns* — not to *similar text fragments*. Treating structured data with the same retrieval primitive as prose (chunk + embed + rank top-K) makes the right column survive a probabilistic ranking lottery. Catalog-based **lookup** is the right primitive instead.
|
| 34 |
-
|
| 35 |
-
A central per-user catalog also means:
|
| 36 |
-
|
| 37 |
-
- One place to keep table/column descriptions (AI-generated, refreshed when the source changes).
|
| 38 |
-
- The query planner sees the user's full data landscape in a single prompt.
|
| 39 |
-
- Schema stays stable across user sessions without hitting the source DB on every query.
|
| 40 |
-
- New sources auto-update the catalog without re-embedding chunks.
|
| 41 |
-
|
| 42 |
-
---
|
| 43 |
-
|
| 44 |
-
## 2. Source taxonomy
|
| 45 |
-
|
| 46 |
-
```
|
| 47 |
-
Sources
|
| 48 |
-
├── Unstructured (pdf, docx, txt) → Cu (prose chunks via DocumentRetriever)
|
| 49 |
-
└── Structured
|
| 50 |
-
├── Schema (DB) → Cs (DB tables + columns)
|
| 51 |
-
└── Tabular (xlsx, csv, parquet) → Ct (sheets + columns)
|
| 52 |
-
Cs ∪ Ct = Data Catalog Context
|
| 53 |
-
```
|
| 54 |
-
|
| 55 |
-
- **Cu** = unstructured prose context. Retrieval primitive: dense similarity over chunks.
|
| 56 |
-
- **Cs** = DB schema context (tables, columns, descriptions, sample values).
|
| 57 |
-
- **Ct** = tabular file context (sheets, columns, descriptions, sample values).
|
| 58 |
-
- **Data Catalog Context** = `Cs ∪ Ct`. Passed to the query planner as a single unified view.
|
| 59 |
-
|
| 60 |
-
DB vs tabular is **not** a routing concern — it's a per-source attribute (`source_type`) on each catalog entry. The split only matters at execution time (SQL vs pandas).
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
## 3. Routing model
|
| 65 |
-
|
| 66 |
-
> **Superseded 2026-06-18** — the 3-way `source_hint` below was reworked into a flat **6-intent** handler router (`chat`, `help`, `problem_statement`, `check`, `unstructured_flow`, `structured_flow`). Modality (structured vs unstructured *data*) is now the Planner's job, not the router's. See `ORCHESTRATOR_REWORK_PLAN.md`.
|
| 67 |
-
|
| 68 |
-
```
|
| 69 |
-
source_hint ∈ { chat, unstructured, structured }
|
| 70 |
-
```
|
| 71 |
-
|
| 72 |
-
- `chat` — no search, conversational reply only
|
| 73 |
-
- `unstructured` — DocumentRetriever path (Cu)
|
| 74 |
-
- `structured` — catalog-driven path (Cs ∪ Ct → planner → compiler → executor)
|
| 75 |
-
|
| 76 |
-
The router commits to one path. Cross-source questions ("compare DB sales vs uploaded customer file") are handled inside the structured path because the planner sees both Cs and Ct in one prompt.
|
| 77 |
-
|
| 78 |
-
---
|
| 79 |
-
|
| 80 |
-
## 4. Core architectural decisions
|
| 81 |
-
|
| 82 |
-
### 4.1 Catalog as primary context, not retrieval
|
| 83 |
-
|
| 84 |
-
For most users (≤50 tables), the entire catalog fits in ~3-5k tokens and is passed verbatim to the planner. No vector search, no BM25, no chunk retrieval. The LLM reads the whole catalog and picks the right table.
|
| 85 |
-
|
| 86 |
-
When a user has hundreds of tables, **catalog-level retrieval** (BM25 + table-level vectors with RRF) can be added as a slicer between `CatalogReader` and `Planner`. Deferred until measurably needed.
|
| 87 |
-
|
| 88 |
-
### 4.2 JSON IR over raw SQL
|
| 89 |
-
|
| 90 |
-
The planner LLM emits a structured JSON IR describing query intent — not a SQL string. A deterministic compiler turns the IR into SQL (per dialect) or pandas/polars operations.
|
| 91 |
-
|
| 92 |
-
Benefits:
|
| 93 |
-
|
| 94 |
-
- Validatable with Pydantic before execution
|
| 95 |
-
- Compiler whitelists allowed operations (no DROP, DELETE, etc.)
|
| 96 |
-
- Portable: same IR → SQL (any dialect) / pandas / polars
|
| 97 |
-
- Cheaper tokens, easier to debug, trivially testable without an LLM
|
| 98 |
-
- LLM cannot emit valid-but-wrong SQL syntax
|
| 99 |
-
|
| 100 |
-
### 4.3 Deterministic compiler, not LLM SQL writer
|
| 101 |
-
|
| 102 |
-
The LLM produces *intent* (the IR). All actual query construction is deterministic Python. Compiler bugs are reproducible and fixable. Same IR always produces the same query.
|
| 103 |
-
|
| 104 |
-
### 4.4 Pipeline stage isolation
|
| 105 |
-
|
| 106 |
-
Each stage is its own module with typed input and typed output. No god classes. Stages: `IntentRouter`, `CatalogReader`, `QueryPlanner`, `IRValidator`, `QueryCompiler`, `QueryExecutor`, `ChatbotAgent`. Each is testable in isolation.
|
| 107 |
-
|
| 108 |
-
### 4.5 Minimal LLM surface
|
| 109 |
-
|
| 110 |
-
LLM calls happen in exactly three places (KM-557 removed `CatalogEnricher`; ingestion is now LLM-free — the planner reads column names, stats, and sample rows directly):
|
| 111 |
-
|
| 112 |
-
1. **`IntentRouter`** — once per user message
|
| 113 |
-
2. **`QueryPlanner`** — once per structured query (produces the IR)
|
| 114 |
-
3. **`ChatbotAgent`** — once per answer (formats the response)
|
| 115 |
-
|
| 116 |
-
Compiler and executors are pure code. No LLM in the hot path of query construction.
|
| 117 |
-
|
| 118 |
-
---
|
| 119 |
-
|
| 120 |
-
## 5. End-to-end flow
|
| 121 |
-
|
| 122 |
-
### Ingestion (when user uploads a file or connects a DB)
|
| 123 |
-
|
| 124 |
-
```
|
| 125 |
-
Structured sources (DB connect / XLSX / CSV / Parquet upload) — Python:
|
| 126 |
-
source upload / DB connect
|
| 127 |
-
↓
|
| 128 |
-
introspect schema (DB: information_schema; tabular: file headers + sample rows)
|
| 129 |
-
↓
|
| 130 |
-
validate (Pydantic)
|
| 131 |
-
↓
|
| 132 |
-
write to catalog store (Postgres jsonb in `data_catalog`, keyed by user_id)
|
| 133 |
-
```
|
| 134 |
-
|
| 135 |
-
**Unstructured ingestion (PDF / DOCX / TXT) is handled by a separate Go service**, which writes chunks + embeddings into the `documents` collection in PGVector. The Python service does not own this path — it reads only.
|
| 136 |
-
|
| 137 |
-
### Query (per user message)
|
| 138 |
-
|
| 139 |
-
```
|
| 140 |
-
User message
|
| 141 |
-
↓
|
| 142 |
-
Chat cache check (Redis, 24h TTL)
|
| 143 |
-
↓ miss
|
| 144 |
-
Load chat history
|
| 145 |
-
↓
|
| 146 |
-
IntentRouter LLM → needs_search? source_hint?
|
| 147 |
-
↓
|
| 148 |
-
├── chat → ChatbotAgent → SSE stream
|
| 149 |
-
├── unstructured → DocumentRetriever (raw SQL: pgvector `<=>` cosine or `<+>` manhattan) → answerer
|
| 150 |
-
└── structured →
|
| 151 |
-
CatalogReader (load full Cs ∪ Ct for user)
|
| 152 |
-
↓
|
| 153 |
-
QueryPlanner LLM → JSON IR
|
| 154 |
-
↓
|
| 155 |
-
IRValidator (Pydantic + columns-exist + ops whitelist)
|
| 156 |
-
↓
|
| 157 |
-
QueryCompiler → SQL (schema source) or pandas (tabular source)
|
| 158 |
-
↓
|
| 159 |
-
QueryExecutor (DbExecutor or TabularExecutor)
|
| 160 |
-
↓
|
| 161 |
-
QueryResult
|
| 162 |
-
↓
|
| 163 |
-
ChatbotAgent → SSE stream
|
| 164 |
-
```
|
| 165 |
-
|
| 166 |
-
---
|
| 167 |
-
|
| 168 |
-
## 6. Data catalog
|
| 169 |
-
|
| 170 |
-
### Storage
|
| 171 |
-
|
| 172 |
-
Per-user JSON document, stored as a `jsonb` row in Postgres keyed by `user_id`.
|
| 173 |
-
|
| 174 |
-
### Schema (initial scope)
|
| 175 |
-
|
| 176 |
-
```
|
| 177 |
-
Catalog
|
| 178 |
-
├── user_id, schema_version, generated_at
|
| 179 |
-
└── sources[]
|
| 180 |
-
└── Source
|
| 181 |
-
├── source_id, source_type, name, description, location_ref, updated_at
|
| 182 |
-
└── tables[]
|
| 183 |
-
└── Table
|
| 184 |
-
├── table_id, name, description, row_count
|
| 185 |
-
└── columns[]
|
| 186 |
-
└── Column
|
| 187 |
-
├── column_id, name, data_type, description
|
| 188 |
-
├── nullable
|
| 189 |
-
├── pii_flag
|
| 190 |
-
├── sample_values[]
|
| 191 |
-
└── stats: { min, max, distinct_count } | null
|
| 192 |
-
```
|
| 193 |
-
|
| 194 |
-
### Best-practice fields deferred
|
| 195 |
-
|
| 196 |
-
`description_human`, `synonyms[]`, `tags[]`, `primary_key`, `foreign_keys`, `unit`, `semantic_type`, `example_questions[]`, `schema_hash`, `enrichment_status`. Add when justified by user need.
|
| 197 |
-
|
| 198 |
-
### Stable IDs
|
| 199 |
-
|
| 200 |
-
`source_id`, `table_id`, `column_id` are stable internal references. `name` fields can change (e.g. column rename in source DB) without invalidating cached IRs.
|
| 201 |
-
|
| 202 |
-
### PII handling
|
| 203 |
-
|
| 204 |
-
Columns with `pii_flag: true` have `sample_values: null` — real values never enter LLM prompts. Auto-detected at ingestion via name patterns + value regex.
|
| 205 |
-
|
| 206 |
-
---
|
| 207 |
-
|
| 208 |
-
## 7. JSON IR
|
| 209 |
-
|
| 210 |
-
### Schema (initial scope)
|
| 211 |
-
|
| 212 |
-
```
|
| 213 |
-
QueryIR
|
| 214 |
-
├── ir_version : "1.0"
|
| 215 |
-
├── source_id : str (references catalog)
|
| 216 |
-
├── table_id : str (references catalog)
|
| 217 |
-
├── select[] : SelectItem
|
| 218 |
-
│ ├── { kind: "column", column_id, alias? }
|
| 219 |
-
│ └── { kind: "agg", fn, column_id?, alias? }
|
| 220 |
-
├── filters[] : { column_id, op, value, value_type }
|
| 221 |
-
├── group_by[] : column_id
|
| 222 |
-
├── order_by[] : { column_id | alias, dir }
|
| 223 |
-
└── limit : int | null
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
### Whitelisted operators
|
| 227 |
-
|
| 228 |
-
```
|
| 229 |
-
Filter ops: = != < <= > >= in not_in is_null is_not_null like between
|
| 230 |
-
Agg fns: count count_distinct sum avg min max
|
| 231 |
-
```
|
| 232 |
-
|
| 233 |
-
### Validation rules (enforced before execution)
|
| 234 |
-
|
| 235 |
-
- `source_id` exists in catalog for this user
|
| 236 |
-
- `table_id` belongs to that source
|
| 237 |
-
- Every `column_id` exists in that table
|
| 238 |
-
- Every `agg.fn` and `filter.op` is whitelisted
|
| 239 |
-
- `value_type` consistent with column's `data_type`
|
| 240 |
-
- `limit` positive int, ≤ hard cap (e.g. 10000)
|
| 241 |
-
|
| 242 |
-
If any rule fails → reject IR → re-prompt planner with error context (max 3 retries).
|
| 243 |
-
|
| 244 |
-
### Deferred features
|
| 245 |
-
|
| 246 |
-
`having`, `offset`, boolean tree filters (OR/NOT), `distinct`, joins, window functions. Add as user demand proves the limitation.
|
| 247 |
-
|
| 248 |
-
---
|
| 249 |
-
|
| 250 |
-
## 8. Executors
|
| 251 |
-
|
| 252 |
-
Same input (validated IR), same output (`QueryResult`), different backends.
|
| 253 |
-
|
| 254 |
-
### DbExecutor (schema sources)
|
| 255 |
-
|
| 256 |
-
```
|
| 257 |
-
IR → SqlCompiler → SQL string + params
|
| 258 |
-
↓
|
| 259 |
-
sqlglot validation (SELECT-only, whitelist tables/columns, LIMIT enforced)
|
| 260 |
-
↓
|
| 261 |
-
asyncpg / pymysql in read-only transaction with timeout (30s)
|
| 262 |
-
↓
|
| 263 |
-
QueryResult
|
| 264 |
-
```
|
| 265 |
-
|
| 266 |
-
Identifiers come from catalog (verified at validation time, safe to inline as quoted identifiers). Values are always parameterized — never inlined as strings.
|
| 267 |
-
|
| 268 |
-
### TabularExecutor (tabular sources)
|
| 269 |
-
|
| 270 |
-
```
|
| 271 |
-
IR → PandasCompiler → operation chain
|
| 272 |
-
↓
|
| 273 |
-
choose strategy by file size:
|
| 274 |
-
≤ 100 MB → eager pandas
|
| 275 |
-
100 MB-1 GB → pyarrow with predicate pushdown
|
| 276 |
-
> 1 GB → polars lazy scan
|
| 277 |
-
↓
|
| 278 |
-
execute in asyncio.to_thread (CPU work off the event loop)
|
| 279 |
-
↓
|
| 280 |
-
QueryResult
|
| 281 |
-
```
|
| 282 |
-
|
| 283 |
-
Initially eager pandas is sufficient. Add the others when a real file is too big.
|
| 284 |
-
|
| 285 |
-
### Shared safety guarantees
|
| 286 |
-
|
| 287 |
-
1. IR validated before reaching compiler
|
| 288 |
-
2. Compiler is deterministic (no LLM)
|
| 289 |
-
3. Identifiers from catalog (trusted)
|
| 290 |
-
4. Values parameterized
|
| 291 |
-
5. sqlglot second-line defence for SQL
|
| 292 |
-
6. Read-only at every layer
|
| 293 |
-
7. Timeouts and row caps
|
| 294 |
-
|
| 295 |
-
---
|
| 296 |
-
|
| 297 |
-
## 9. Implementation scope
|
| 298 |
-
|
| 299 |
-
### Initial PR — what ships first
|
| 300 |
-
|
| 301 |
-
| Item | Folder |
|
| 302 |
-
|---|---|
|
| 303 |
-
| Data catalog Pydantic models | `src/catalog/models.py` |
|
| 304 |
-
| Catalog ingestion (introspect → enrich → validate → store) | `src/catalog/`, `src/pipeline/` |
|
| 305 |
-
| `IntentRouter` with 3-way source_hint | `src/agents/` |
|
| 306 |
-
| `CatalogReader` (loads full catalog) | `src/catalog/reader.py` |
|
| 307 |
-
| `QueryPlanner` LLM call | `src/query/planner/` |
|
| 308 |
-
| JSON IR Pydantic models | `src/query/ir/models.py` |
|
| 309 |
-
| IR validator | `src/query/ir/validator.py` |
|
| 310 |
-
|
| 311 |
-
**Output**: a validated JSON IR object. Execution lands in a follow-up PR.
|
| 312 |
-
|
| 313 |
-
### Follow-up PRs
|
| 314 |
-
|
| 315 |
-
| PR | Scope |
|
| 316 |
-
|---|---|
|
| 317 |
-
| 2 | `QueryCompiler` (IR → SQL / pandas) |
|
| 318 |
-
| 3 | `QueryExecutor` split: `DbExecutor` + `TabularExecutor` |
|
| 319 |
-
| 4 | Retry / self-correction loop on execution failure |
|
| 320 |
-
| 5 | Eval harness (golden question→IR→result examples) |
|
| 321 |
-
| 6 | Auto PII tagging in catalog |
|
| 322 |
-
| Later | Joins in IR, schema drift detection, hybrid catalog search |
|
| 323 |
-
|
| 324 |
-
---
|
| 325 |
-
|
| 326 |
-
## 10. Open questions
|
| 327 |
-
|
| 328 |
-
| # | Question | Why it matters |
|
| 329 |
-
|---|---|---|
|
| 330 |
-
| 1 | Catalog storage: JSON file per user vs Postgres `jsonb` row? | Affects ingestion + read performance |
|
| 331 |
-
| 2 | Should the catalog also list unstructured files (with descriptions only)? | Gives router unified view of all user sources |
|
| 332 |
-
| 3 | Catalog refresh trigger: explicit "rebuild" button, on every upload, or background TTL? | Staleness vs latency tradeoff |
|
| 333 |
-
| 4 | Confirm joins are out of initial IR scope? | Limits what user questions can be answered |
|
| 334 |
-
| 5 | PII handling for sample_values: mask, synthesize, or skip? | Affects what gets sent to LLM prompts |
|
| 335 |
-
|
| 336 |
-
---
|
| 337 |
-
|
| 338 |
-
## 11. References
|
| 339 |
-
|
| 340 |
-
- `docs/flowchart.html` — interactive end-to-end diagram (open in browser)
|
| 341 |
-
- `docs/flowchart.mmd` — mermaid source for the diagram
|
| 342 |
-
|
| 343 |
-
---
|
| 344 |
-
|
| 345 |
-
## Glossary
|
| 346 |
-
|
| 347 |
-
- **Cu** — unstructured context (prose chunks)
|
| 348 |
-
- **Cs** — schema context (DB tables/columns from catalog)
|
| 349 |
-
- **Ct** — tabular context (file sheets/columns from catalog)
|
| 350 |
-
- **IR** — intermediate representation (the JSON query shape)
|
| 351 |
-
- **PR** — pull request (a unit of code change)
|
| 352 |
-
- **PII** — personally identifiable information (names, emails, etc.)
|
| 353 |
-
- **ABC** — abstract base class (Python contract for subclasses)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CHECKPOINT_PLAN_2026-06-17.md
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
# Checkpoint Plan — Wednesday, 17 June 2026
|
| 2 |
-
|
| 3 |
-
Working plan for Sofhia & Rifqi based on the checkpoint with mas Harry on **Thursday, 11 June 2026**.
|
| 4 |
-
Goal: everything below is **merged and demo-able before the next sync on Wednesday, 17 June (afternoon)**.
|
| 5 |
-
|
| 6 |
-
**Updated at: Friday, 12 June 2026** (Sofhia + Rifqi)
|
| 7 |
-
|
| 8 |
-
> Source of truth for decisions is the meeting itself. Note: the NotebookLM summary is **stale on two points** — Data Availability Check was *eliminated* as a tool, and Success Metrics was *folded into* the Problem Statement template. Do not build either as a standalone skill.
|
| 9 |
-
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
## 0. Progress (per Fri 12 Jun — Sofhia)
|
| 13 |
-
|
| 14 |
-
Dated snapshot of what landed this session. Live task status (incl. what's left) lives in §2 Ownership — this section only records the deltas + traceability.
|
| 15 |
-
|
| 16 |
-
- ✅ **Tool matrix** built (xlsx, all ~10 tools + status colours) — presentation material ready.
|
| 17 |
-
- ✅ **Registry trimmed to 4 active analytics** (`KM-641`, commit `66e2e4d`): `ACTIVE_ANALYTICS_TOOLS` (descriptive, aggregate, correlation, trend) vs `DEFERRED_ANALYTICS_TOOLS` (comparison, contribution, profile, segment) — specs + compute fns kept, only registry exposure withheld. Tests 206 pass, ruff/mypy clean.
|
| 18 |
-
- ✅ **Planner few-shot synced**: Example A `analyze_contribution` → `analyze_aggregate` (so few-shots don't reference a deferred tool).
|
| 19 |
-
- ✅ **Data-access tools renamed** (`KM-642`, commit `c38c0c2`): `query_structured` → `data_retrieve`, `retrieve_documents` → `knowledge_retrieve` across the tool layer + planner stub/prompt/validator/few-shots. Mechanical, no behavior change.
|
| 20 |
-
- ✅ **`data_check` merge + `knowledge_check`** (`KM-643`, commit `4bd5f1e`): `list_sources` + `describe_source` → one parameterized `data_check` (no arg = list structured sources; `source_id` = schema) + new `knowledge_check` (unstructured). Tests 206 pass.
|
| 21 |
-
- ✅ **Redis Cloud live** (free tier, TTL = 1 h), env vars shared in the group (Rifqi).
|
| 22 |
-
- ✅ **Planner tool list verified** against the trimmed registry — no references to old tool names or deferred analytics anywhere in `src/` (Rifqi).
|
| 23 |
-
- 📌 **Decision:** `tests/` stays gitignored — team decided not to push tests to origin (closes PROGRESS.md R3 as won't-do).
|
| 24 |
-
- 📌 **Ownership:** Rifqi owns `generate_report` development + the `analysis_records` table / real `AnalysisStore` (contract still co-designed with Sofhia).
|
| 25 |
-
- ✅ **R5 cache fix** (Rifqi, `b701e95`): chat cache scoped by `user_id`, TTL 24h→1h.
|
| 26 |
-
- ✅ **AnalysisRecord persistence landed** (Rifqi): `stage` now flows to the record (CRISP-DM grouping for the report) + identity fields (`record_id`/`analysis_id`/`user_id`); `PostgresAnalysisStore` + `analysis_records` table replace `NullAnalysisStore`, wired into `ChatHandler`. Unblocks the `generate_report` renderer and the DoD "record persisted" step. Open: `analysis_id` handoff from Harry's Analysis State.
|
| 27 |
-
- ✅ **Verb-first tool naming** (Sofhia, commit `2d6406d`): the 4 data/knowledge tools renamed to lead with a verb — `data_check`→`check_data`, `knowledge_check`→`check_knowledge`, `data_retrieve`→`retrieve_data`, `knowledge_retrieve`→`retrieve_knowledge` (the `analyze_*` tools already lead with a verb). These verb-first names are now canonical; the tool-set table + §3 below use them. Dated log entries above keep the old names as historical record.
|
| 28 |
-
|
| 29 |
-
---
|
| 30 |
-
|
| 31 |
-
## 1. Locked decisions (from the 2026-06-11 checkpoint)
|
| 32 |
-
|
| 33 |
-
1. **Single chat page.** The separate interview/survey page is killed. Sidebar = Knowledge menu (connect/manage data) + Analysis menu (sessions).
|
| 34 |
-
2. **Data-first hard gate.** Creating a new analysis requires **≥ 1 bound data source** (server-side rejection, no empty sessions). User provides title + optional short description.
|
| 35 |
-
3. **Analysis State lives in the DB.** Per-analysis row: `user_id`, `data_source_ids[]`, `interview_status` (default `not_pass`), `report_status` (default `no_report` → `V1`, `V2`, …). Explicitly **NOT cached, NOT in Redis** — the Orchestrator reads it from Postgres every turn.
|
| 36 |
-
4. **Skills, not agents.** No separate interview agent. The Orchestrator routes per user turn using the Analysis State; an analytical request still executes through the existing Planner → TaskRunner → Assembler spine (static plan, no mid-run LLM).
|
| 37 |
-
5. **Interview = one skill: Problem Statement.** Success metrics become fields inside the PS template (what to increase/decrease + target). Data availability check is handled by the data-first creation gate + PS validation cross-checking fields against the bound catalog — not a separate tool.
|
| 38 |
-
6. **Analytics focus = 4 tools:** descriptive, aggregate, correlation, trend. The other four composites (comparison, contribution, profile, segment) are **deprioritized, not deleted** — keep the code, just don't register them. If "comparison" returns later it should be a proper statistical **test**, not a generic compare.
|
| 39 |
-
7. **`describe_source` merges into the listing tool** — one call returns sources *with* their schema/metadata, fewer tools for the planner.
|
| 40 |
-
8. **Report = on-demand, button-triggered (not a chat skill).** A dedicated "Generate Report" button in the Analysis menu calls a **report API** (not the chat route): trigger generation for a session, list its versions, fetch a version. Renders from accumulated **AnalysisRecords + the Problem Statement** — never from chat history. Each report is a **persisted, versioned artifact**: generation snapshots the record IDs it used and bumps `report_status` to `V<n>`. (Owner: Rifqi, KM-644.)
|
| 41 |
-
9. **Help = deterministic guide.** No LLM: read Analysis State → tell the user the next required step. Callable in any state.
|
| 42 |
-
10. **Redis Cloud free tier, TTL = 1 hour**, env shared in the team group — for retrieval/query caching only, never for state.
|
| 43 |
-
|
| 44 |
-
### Final tool set (~10)
|
| 45 |
-
|
| 46 |
-
| Tool (canonical, verb-first) | Maps to (lineage) | Status |
|
| 47 |
-
|---|---|---|
|
| 48 |
-
| `check_knowledge` | new — list user's documents + metadata | done |
|
| 49 |
-
| `check_data` | `list_sources` + `describe_source` merged (catalog-backed) | done |
|
| 50 |
-
| `retrieve_knowledge` | `retrieve_documents` → `knowledge_retrieve` | done |
|
| 51 |
-
| `retrieve_data` | `query_structured` → `data_retrieve` (tabular: file + DB, both working) | done |
|
| 52 |
-
| `analyze_descriptive` | `src/tools/analytics/descriptive.py` | done |
|
| 53 |
-
| `analyze_aggregate` | `src/tools/analytics/aggregation.py` | done |
|
| 54 |
-
| `analyze_correlation` | `src/tools/analytics/relationship.py` | done |
|
| 55 |
-
| `analyze_trend` | `src/tools/analytics/temporal.py` | done |
|
| 56 |
-
| `problem_statement` | new — interview skill (**Harry**) | Harry |
|
| 57 |
-
| `generate_report` | new — on-demand, versioned | to design |
|
| 58 |
-
| `help` | new — deterministic state guide | to build |
|
| 59 |
-
|
| 60 |
-
(`problem_statement` + `help` live at the orchestrator level; `generate_report` is **button-triggered via a dedicated report API**, not chat-routed (decision #8). The TaskRunner registry holds the 4 analytics + 4 data/knowledge tools. Unregister `analyze_comparison`, `analyze_contribution`, `analyze_profile`, `analyze_segment` from the planner-visible registry — keep the modules.)
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
## 2. Ownership
|
| 65 |
-
|
| 66 |
-
### Sofhia
|
| 67 |
-
- [x] 4 analytics tools: trim registry to 4 active, tests still pass after deprioritizing the other four. (`KM-641`, commit `66e2e4d`)
|
| 68 |
-
- [x] Data/knowledge tools: merge `describe_source` into `data_check`, rename `retrieve_documents` → `knowledge_retrieve`, `query_structured` → `data_retrieve`, build `knowledge_check`. (`KM-642` `c38c0c2`, `KM-643` `4bd5f1e`)
|
| 69 |
-
- [ ] Co-design `generate_report` contract with Rifqi (Rifqi owns development, see §3).
|
| 70 |
-
- [x] Tool matrix (see §4).
|
| 71 |
-
|
| 72 |
-
### Rifqi
|
| 73 |
-
- [x] **Redis Cloud free tier** (~30–50 MB): create instance, set TTL = 1 h, share env vars in the group. (done 12 Jun)
|
| 74 |
-
- [x] **R5 cache fix**: chat cache key scoped by `user_id`, TTL 24h→1h (urgent on shared Redis). (12 Jun, commit `b701e95`)
|
| 75 |
-
- [x] **AnalysisRecord contract gaps closed**: `stage` (CRISP-DM) now flows Task→TaskResult→TaskSummary so the report can group the method appendix; `AnalysisRecord` gained `record_id`/`analysis_id`/`user_id` identity fields. (12 Jun)
|
| 76 |
-
- [x] **`analysis_records` table + real `AnalysisStore`**: `PostgresAnalysisStore` (save + `list_for_analysis`, never-throw) replaces `NullAnalysisStore`; wired into `ChatHandler`, `user_id` stamped at save. Satisfies the DoD "record persisted" step. (12 Jun)
|
| 77 |
-
- [ ] **Own `generate_report` development — KM-644 "Report Generator"** (contract co-designed with Sofhia, see §3). Button-triggered via a dedicated **report API** (trigger / list versions / fetch); reads `analysis_records` + Problem Statement; persists a versioned report artifact, bumps `report_status`. *(record persistence done above; report API + persistence + renderer + contract doc next)*
|
| 78 |
-
- [x] Verify planner tool list matches the trimmed registry (4 analytics + 4 data/knowledge) and few-shots don't reference removed tools. (verified 12 Jun — no stale tool names in `src/`)
|
| 79 |
-
- ⚠️ **Blocked-on-Harry**: `analysis_id` is `NULL` on persisted records until the Analysis State reaches the slow path — need the session-ID handoff so `generate_report` can group records per analysis.
|
| 80 |
-
|
| 81 |
-
### Shared (Sofhia + Rifqi)
|
| 82 |
-
- [ ] `generate_report` design + skeleton: input = AnalysisRecords for the session + Problem Statement from Analysis State; output = versioned artifact; bumps `report_status`. Agree on the contract even if rendering is stubbed for Wednesday. (Development: Rifqi.)
|
| 83 |
-
- [ ] `help` skill: deterministic — read Analysis State, return the next required step. Small, do it together or whoever finishes first.
|
| 84 |
-
- [ ] Tool behavior smoke test end-to-end on an easy case (descriptive/aggregate path), per Harry's ask: "robust tools before agents."
|
| 85 |
-
|
| 86 |
-
### Harry (dependencies — not ours, but we block on them)
|
| 87 |
-
- `problem_statement` skill + PS template (incl. increase/decrease target fields).
|
| 88 |
-
- Analysis State class + DB table, frontend analysis-builder step.
|
| 89 |
-
- Merging our PRs (he auto-merges; he clones from latest after).
|
| 90 |
-
|
| 91 |
-
---
|
| 92 |
-
|
| 93 |
-
## 3. Per-tool behavior contract (how to build each one)
|
| 94 |
-
|
| 95 |
-
Harry's framing: for every tool, define **goal / trigger / input / process / output**, and behave like a Claude-style skill — if a required argument is missing, respond with a polite feedback message asking for it (e.g. table/column name), never guess silently.
|
| 96 |
-
|
| 97 |
-
- **`check_knowledge`** — "what documents do I have?" → list documents with name, type, uploaded-at.
|
| 98 |
-
- **`check_data`** — "what data do I have?" → sources (file + DB) with schema/metadata from the data catalog, created/uploaded timestamps.
|
| 99 |
-
- **`retrieve_knowledge`** — RAG over uploaded documents; returns passages with source attribution.
|
| 100 |
-
- **`retrieve_data`** — query tabular data (file + DB) via QueryIR; output consumable by the `analyze_*` tools.
|
| 101 |
-
- **`analyze_*` (4)** — require valid table/column references; if missing or wrong, return actionable feedback instead of guessing.
|
| 102 |
-
- **`generate_report`** — button-triggered via a dedicated report API (not chat-routed); on-demand only (never auto); post-pass gated; renders from AnalysisRecords + PS; persists a versioned artifact, snapshots record IDs, bumps version. (KM-644, Rifqi.)
|
| 103 |
-
- **`help`** — no LLM; state → next step. Repeating it is fine, that's its job.
|
| 104 |
-
|
| 105 |
-
---
|
| 106 |
-
|
| 107 |
-
## 4. Tool matrix (deliverable for the sync)
|
| 108 |
-
|
| 109 |
-
Harry explicitly asked for a matrix covering every tool. Produce one sheet/markdown table with columns:
|
| 110 |
-
|
| 111 |
-
`tool | goal | trigger (when the orchestrator calls it) | input | process | output | gated by interview_status? | status (done / in progress / planned)`
|
| 112 |
-
|
| 113 |
-
Use the tool set table in §1 as the row list. This doubles as the presentation material on Wednesday.
|
| 114 |
-
|
| 115 |
-
---
|
| 116 |
-
|
| 117 |
-
## 5. Day-by-day
|
| 118 |
-
|
| 119 |
-
| Day | Target |
|
| 120 |
-
|---|---|
|
| 121 |
-
| **Thu 11** | Checkpoint meeting + task split with Harry. |
|
| 122 |
-
| **Fri 12 (today)** | ✅ Registry trimmed to 4 analytics + few-shot synced (Sofhia, KM-641). ✅ Tool matrix built. ⏳ Redis Cloud + env share (Rifqi). |
|
| 123 |
-
| **Mon 15** | Data/knowledge tools done (`data_check` merge, renames, `knowledge_check`). `generate_report` contract agreed. |
|
| 124 |
-
| **Tue 16** | `help` skill done. `generate_report` skeleton wired to AnalysisRecord. Tool matrix drafted. End-to-end smoke test on the easy path. |
|
| 125 |
-
| **Wed 17 (AM)** | Buffer: fix fallout, finalize matrix, rehearse the demo flow. |
|
| 126 |
-
| **Wed 17 (PM)** | **Sync with Harry.** |
|
| 127 |
-
|
| 128 |
-
---
|
| 129 |
-
|
| 130 |
-
## 6. Open questions to confirm with Harry on Wednesday
|
| 131 |
-
|
| 132 |
-
1. **Gate scope.** Proposal: keep the fast path + exploration tools (`check_knowledge`, `check_data`, retrieves, `help`, arguably `descriptive`) available **pre-pass**; gate only the insight tools (correlation, trend, report). Hard-gating everything risks frustrating users who just want to look at their data.
|
| 133 |
-
2. **Who flips `interview_status` to `pass`?** Proposal: a deterministic validator (PS template slots complete + fields cross-checked against the bound catalog) makes the call — the LLM conducts the conversation but never decides the pass. ("Conversational skin, deterministic skeleton.")
|
| 134 |
-
3. **Skills vs spine — one sentence to lock in writing:** *"Skills are registry tools executed by the existing Planner → TaskRunner → Assembler spine; the Analysis State gate is a pre-check in the Orchestrator."* This keeps the new flow and the locked architecture fully compatible.
|
| 135 |
-
4. `generate_report` invocation goes through the same gate (post-pass only) — confirm.
|
| 136 |
-
|
| 137 |
-
---
|
| 138 |
-
|
| 139 |
-
## 7. Definition of done for Wednesday
|
| 140 |
-
|
| 141 |
-
- [ ] All team PRs merged; Harry unblocked on the Analysis State class.
|
| 142 |
-
- [ ] Registry exposes exactly 4 analytics + 4 data/knowledge tools, all passing local tests.
|
| 143 |
-
- [ ] Redis Cloud shared and working locally for all three of us (TTL 1 h).
|
| 144 |
-
- [ ] `help` works against a (possibly stubbed) Analysis State.
|
| 145 |
-
- [ ] `generate_report` contract written; skeleton callable.
|
| 146 |
-
- [ ] Tool matrix ready to present.
|
| 147 |
-
- [ ] One end-to-end happy path runs: create analysis (with data) → blocked pre-pass → interview stub passes → descriptive/aggregate answer → record persisted.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PHASE1_TO_PHASE2_REPORT.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
| 1 |
-
# Phase 1 → Phase 2 Migration Report
|
| 2 |
-
|
| 3 |
-
A walkthrough of what changed between the original retrieval-style backend (Phase 1) and the current catalog-driven backend (Phase 2). Intended as a hand-off for the lead.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## 1. The conceptual change
|
| 8 |
-
|
| 9 |
-
**Phase 1** was a single retrieval-style RAG pipeline. Every question — whether it pointed at a database, a spreadsheet, or a PDF — went through the same primitive: **chunk + embed + top-K** over PGVector. Schema and tabular columns were embedded as chunks and ranked alongside prose. When the question needed SQL, the LLM **wrote the SQL string directly** (via `query_executor`).
|
| 10 |
-
|
| 11 |
-
**Phase 2** splits the system into two paths governed by an LLM router:
|
| 12 |
-
|
| 13 |
-
| Path | Primitive | Why |
|
| 14 |
-
|---|---|---|
|
| 15 |
-
| Unstructured (PDF / DOCX / TXT) | Dense similarity over prose chunks (PGVector) | Right primitive for free text |
|
| 16 |
-
| Structured (DB / CSV / XLSX / Parquet) | **Per-user data catalog** → LLM emits a **JSON IR** of intent → deterministic **compiler** → **executor** (SQL or pandas) | A column lookup shouldn't go through a similarity ranking lottery; the LLM emits intent, never SQL syntax |
|
| 17 |
-
|
| 18 |
-
Three explicit LLM call sites only:
|
| 19 |
-
|
| 20 |
-
1. **Intent router** (classifies the user message into `chat` / `unstructured` / `structured`)
|
| 21 |
-
2. **Query planner** (turns the question + catalog into a Pydantic-validated `QueryIR`)
|
| 22 |
-
3. **Chatbot agent** (formats the final answer, streamed over SSE)
|
| 23 |
-
|
| 24 |
-
Everything else — IR validation, SQL/pandas compilation, execution — is deterministic Python.
|
| 25 |
-
|
| 26 |
-
---
|
| 27 |
-
|
| 28 |
-
## 2. File-by-file changes
|
| 29 |
-
|
| 30 |
-
### 2.1 Deleted (Phase 1 only)
|
| 31 |
-
|
| 32 |
-
| Phase 1 path | Reason it was removed |
|
| 33 |
-
|---|---|
|
| 34 |
-
| `src/rag/base.py`, `src/rag/retriever.py`, `src/rag/router.py` | Replaced by `src/retrieval/` |
|
| 35 |
-
| `src/rag/retrievers/baseline.py`, `schema.py`, `document.py` | Schema retrieval gone (catalog replaces it); document retriever rewritten in `src/retrieval/document.py` |
|
| 36 |
-
| `src/tools/search.py` (whole `tools/` folder) | Only consumer was `rag/router.py` |
|
| 37 |
-
| `src/query/base.py` | Duplicate of `query/executor/base.py` |
|
| 38 |
-
| `src/query/query_executor.py` | Replaced by `src/query/service.py` |
|
| 39 |
-
| `src/query/executors/db_executor.py` | Replaced by `src/query/executor/db.py` |
|
| 40 |
-
| `src/query/executors/tabular.py` | Replaced by `src/query/executor/tabular.py` |
|
| 41 |
-
| `src/agents/chatbot.py` (Phase 1 LangChain chatbot) | Phase 2 `ChatbotAgent` lives at the same path now — see §2.2 |
|
| 42 |
-
| `src/api/v1/knowledge.py` | Fake `/knowledge/rebuild` endpoint, never wired |
|
| 43 |
-
| `src/config/agents/system_prompt.md`, `guardrails_prompt.md` | Replaced by `src/config/prompts/{chatbot_system,guardrails}.md` |
|
| 44 |
-
| `src/models/structured_output.py` (`IntentClassification`) | Replaced by `IntentRouterDecision` Pydantic model inside `agents/orchestration.py` |
|
| 45 |
-
| `src/models/sql_query.py` | LLM no longer emits SQL; IR replaces it |
|
| 46 |
-
| `src/pipeline/orchestrator.py` (empty stub) | Redundant — `StructuredPipeline` takes the introspector at `run()` time |
|
| 47 |
-
|
| 48 |
-
### 2.2 Renamed / moved (same role, new home)
|
| 49 |
-
|
| 50 |
-
| Phase 1 location | Phase 2 location | Notes |
|
| 51 |
-
|---|---|---|
|
| 52 |
-
| `src/agents/chatbot.py` (Phase 1) → deleted, then `src/agents/answer_agent.py` (`AnswerAgent`) → renamed | `src/agents/chatbot.py::ChatbotAgent` | Final answer formation; streams via `astream` |
|
| 53 |
-
| `src/knowledge/parquet_service.py` | `src/storage/parquet.py` | Parquet upload/download helper |
|
| 54 |
-
| `src/pipeline/document_pipeline/document_pipeline.py` (folder) | `src/pipeline/document_pipeline.py` (flat) | Single module |
|
| 55 |
-
| `src/rag/retrievers/document.py` | `src/retrieval/document.py` | `DocumentRetriever` migrated; tabular file types filtered out of results. **Post-report update (mentor commit 61c746f, 2026-05-20):** rewritten to raw SQL (pgvector `<=>` cosine, `<+>` manhattan only) to dodge asyncpg type-mapping issues with the Go-ingested schema. MMR / euclidean / inner_product dropped. |
|
| 56 |
-
| `src/rag/router.py` | `src/retrieval/router.py` | `RetrievalRouter`, Redis-cached, unstructured-only; dead `db: AsyncSession` + `source_hint` params removed |
|
| 57 |
-
| `src/rag/base.py` (`RetrievalResult`, `BaseRetriever`) | `src/retrieval/base.py` | Same dataclass + ABC |
|
| 58 |
-
|
| 59 |
-
> **Heads-up on the intent router**: the Phase 1 file `src/agents/orchestration.py` and its class `OrchestratorAgent` were **kept in place** for Phase 2 — but the body was fully rewritten. The class now emits `IntentRouterDecision(needs_search, source_hint ∈ {chat, unstructured, structured}, rewritten_query)`. The prompt file and test file use the `intent_router` name (`config/prompts/intent_router.md`, `tests/agents/test_intent_router.py`), but **the source module is still `orchestration.py` and the class is still `OrchestratorAgent`**. Existing imports continue to work; only the behavior changed.
|
| 60 |
-
|
| 61 |
-
### 2.3 Added (Phase 2 new)
|
| 62 |
-
|
| 63 |
-
**Catalog subsystem (whole new concept)**
|
| 64 |
-
|
| 65 |
-
| Path | Role |
|
| 66 |
-
|---|---|
|
| 67 |
-
| `src/catalog/models.py` | Pydantic: `Catalog → Source[] → Table[] → Column[]`, `ForeignKey`, `ColumnStats.top_values` |
|
| 68 |
-
| `src/catalog/introspect/base.py` | `BaseIntrospector` ABC |
|
| 69 |
-
| `src/catalog/introspect/database.py` | DB introspector — wraps Phase 1 `db_pipeline/extractor.py` (`get_schema`, `profile_column`, `get_row_count`) |
|
| 70 |
-
| `src/catalog/introspect/tabular.py` | CSV / XLSX / Parquet introspector — one `Table` per XLSX sheet |
|
| 71 |
-
| `src/catalog/render.py` | Renders a `Source` for the planner prompt |
|
| 72 |
-
| `src/catalog/validator.py` | Unique-ID + foreign-key-ref invariants |
|
| 73 |
-
| `src/catalog/store.py` | Postgres `jsonb` upsert keyed by `user_id` (table `data_catalog`) |
|
| 74 |
-
| `src/catalog/reader.py` | Loads + filters catalog by `source_hint` |
|
| 75 |
-
| `src/catalog/pii_detector.py` | Flags PII columns at ingestion → suppresses `sample_values` |
|
| 76 |
-
| `src/security/pii_patterns.py` | Name patterns + value regex used by the detector |
|
| 77 |
-
|
| 78 |
-
**JSON IR + query subsystem**
|
| 79 |
-
|
| 80 |
-
| Path | Role |
|
| 81 |
-
|---|---|
|
| 82 |
-
| `src/query/ir/models.py` | `QueryIR` Pydantic schema |
|
| 83 |
-
| `src/query/ir/operators.py` | `ALLOWED_FILTER_OPS`, `ALLOWED_AGG_FNS`, `LIMIT_HARD_CAP`, `TYPE_COMPATIBILITY` |
|
| 84 |
-
| `src/query/ir/validator.py` | Catalog-aware IR validation (rejects unknown column ids, bad ops, type mismatches, oversize limits) |
|
| 85 |
-
| `src/query/planner/service.py` | `QueryPlannerService.plan(question, catalog, previous_error)` — Azure OpenAI structured output → `QueryIR` |
|
| 86 |
-
| `src/query/planner/prompt.py` | Builds the planner prompt from catalog text |
|
| 87 |
-
| `src/query/compiler/base.py` | Compiler ABC |
|
| 88 |
-
| `src/query/compiler/sql.py` | `SqlCompiler` (Postgres) — all 12 filter ops, params as a dict |
|
| 89 |
-
| `src/query/compiler/pandas.py` | `PandasCompiler` — returns `CompiledPandas(apply, output_columns)` |
|
| 90 |
-
| `src/query/executor/base.py` | `BaseExecutor` + `QueryResult` |
|
| 91 |
-
| `src/query/executor/db.py` | `DbExecutor` — sqlglot SELECT-only guard, RO txn, 30 s `statement_timeout`, 10 k row cap |
|
| 92 |
-
| `src/query/executor/tabular.py` | `TabularExecutor` — Parquet via blob, `asyncio.to_thread`, 10 k cap |
|
| 93 |
-
| `src/query/executor/dispatcher.py` | `ExecutorDispatcher.pick(ir)` — picks by `source.source_type` |
|
| 94 |
-
| `src/query/service.py` | `QueryService.run(user_id, question, catalog)` — plan → validate → retry (max 3) → dispatch → execute |
|
| 95 |
-
|
| 96 |
-
**Agents**
|
| 97 |
-
|
| 98 |
-
| Path | Role |
|
| 99 |
-
|---|---|
|
| 100 |
-
| `src/agents/orchestration.py` | `OrchestratorAgent` — Phase 1 file/class name preserved; Phase 2 body. Emits `IntentRouterDecision` |
|
| 101 |
-
| `src/agents/chatbot.py` | `ChatbotAgent` — formerly `AnswerAgent` in `agents/answer_agent.py`; renamed in Cleanup PR |
|
| 102 |
-
| `src/agents/chat_handler.py` | `ChatHandler.handle(...)` — top-level orchestrator; yields `intent` / `chunk` / `done` / `error` SSE events |
|
| 103 |
-
|
| 104 |
-
**Pipelines & API**
|
| 105 |
-
|
| 106 |
-
| Path | Role |
|
| 107 |
-
|---|---|
|
| 108 |
-
| `src/pipeline/structured_pipeline.py` | DB / tabular ingestion: introspect → merge → validate → upsert |
|
| 109 |
-
| `src/pipeline/triggers.py` | `on_db_registered`, `on_tabular_uploaded`, `on_document_uploaded`, `on_catalog_rebuild_requested` |
|
| 110 |
-
| `src/api/v1/data_catalog.py` | `GET /api/v1/data-catalog/{user_id}` + `POST /api/v1/data-catalog/rebuild` |
|
| 111 |
-
| `src/models/api/catalog.py` | Catalog request/response models |
|
| 112 |
-
| `src/config/prompts/intent_router.md`, `query_planner.md`, `chatbot_system.md`, `guardrails.md` | New prompts. `guardrails.md` is appended to `chatbot_system.md` at load time |
|
| 113 |
-
| `src/db/postgres/models.py` (added `Catalog` SQLAlchemy class) | Stores the per-user jsonb document in `data_catalog` |
|
| 114 |
-
|
| 115 |
-
### 2.4 Rewired API endpoints
|
| 116 |
-
|
| 117 |
-
| Endpoint | Phase 1 wiring | Phase 2 wiring |
|
| 118 |
-
|---|---|---|
|
| 119 |
-
| `POST /api/v1/chat/stream` | Inline in `chat.py`: `OrchestratorAgent` → `retriever` → `query_executor` → `chatbot` | Delegates to `ChatHandler.handle()`. Redis cache, fast intent, history load, and message persistence stay in the endpoint |
|
| 120 |
-
| `POST /api/v1/database-clients/{id}/ingest` | Called `db_pipeline_service.run()` and dual-wrote vectors | Calls **only** `on_db_registered` (catalog build). Failure → HTTP 500 |
|
| 121 |
-
| `POST /api/v1/document/process` | Always pushed to vector store | PDF/DOCX/TXT → `knowledge_processor` (vectors); CSV/XLSX → `on_tabular_uploaded` (catalog only, **no vector embedding**) |
|
| 122 |
-
| `POST /api/v1/document/upload` | Storage + DB row | Same, plus `on_document_uploaded` trigger |
|
| 123 |
-
| `POST /api/v1/data-catalog/rebuild` | — | New: iterates all sources, re-runs per-source trigger |
|
| 124 |
-
| `GET /api/v1/data-catalog/{user_id}` | — | New: returns `list[CatalogIndexEntry]` |
|
| 125 |
-
|
| 126 |
-
### 2.5 Phase 1 files still in production use
|
| 127 |
-
|
| 128 |
-
These were **not rewritten** — Phase 2 imports them directly:
|
| 129 |
-
|
| 130 |
-
- `src/database_client/database_client_service.py`
|
| 131 |
-
- `src/utils/db_credential_encryption.py` (`decrypt_credentials_dict`) — `src/security/credentials.py` is still a stub
|
| 132 |
-
- `src/pipeline/db_pipeline/db_pipeline_service.py` (`engine_scope` context manager — used by both the introspector and `DbExecutor`)
|
| 133 |
-
- `src/pipeline/db_pipeline/extractor.py` (`get_schema`, `profile_column`, `get_row_count`)
|
| 134 |
-
- `src/knowledge/processing_service.py` (PDF / DOCX / TXT extraction + embedding)
|
| 135 |
-
- `src/db/postgres/{connection,init_db,vector_store}.py`, `src/storage/az_blob/`, `src/middlewares/`, `src/security/auth.py`
|
| 136 |
-
|
| 137 |
-
---
|
| 138 |
-
|
| 139 |
-
## 3. End-to-end flow (current state)
|
| 140 |
-
|
| 141 |
-
### 3.1 Ingestion
|
| 142 |
-
|
| 143 |
-
```
|
| 144 |
-
User action Pipeline Storage
|
| 145 |
-
────────────── ──────────────────────────── ─────────────────
|
| 146 |
-
upload PDF/DOCX/TXT → DocumentPipeline → Azure Blob + PGVector
|
| 147 |
-
(extract → chunk → embed) (table: langchain_pg_embedding)
|
| 148 |
-
+ on_document_uploaded + retrieval cache invalidate
|
| 149 |
-
|
| 150 |
-
upload CSV/XLSX → TabularIntrospector → Azure Blob (Parquet)
|
| 151 |
-
(sheets / columns + sample + stats) + data_catalog jsonb row
|
| 152 |
-
→ CatalogValidator → CatalogStore (NO vector store — catalog only)
|
| 153 |
-
via on_tabular_uploaded
|
| 154 |
-
|
| 155 |
-
register DB → DatabaseIntrospector → data_catalog jsonb row
|
| 156 |
-
(information_schema + sample + FKs)
|
| 157 |
-
→ validate → store
|
| 158 |
-
via on_db_registered
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
### 3.2 Query (per user message → SSE stream)
|
| 162 |
-
|
| 163 |
-
```
|
| 164 |
-
POST /api/v1/chat/stream
|
| 165 |
-
│
|
| 166 |
-
├── Redis cache check (24h TTL) — hit returns cached stream
|
| 167 |
-
├── _fast_intent (greetings / goodbyes) — bypass LLM
|
| 168 |
-
├── load history from chat_messages
|
| 169 |
-
│
|
| 170 |
-
└── ChatHandler.handle(message, user_id, history) [src/agents/chat_handler.py]
|
| 171 |
-
│
|
| 172 |
-
├─ OrchestratorAgent.classify() [agents/orchestration.py]
|
| 173 |
-
│ → needs_search, source_hint, rewritten_query
|
| 174 |
-
│
|
| 175 |
-
├── source_hint == "chat"
|
| 176 |
-
│ → ChatbotAgent.astream() → yield chunk events
|
| 177 |
-
│
|
| 178 |
-
├── source_hint == "unstructured"
|
| 179 |
-
│ → RetrievalRouter.retrieve() [retrieval/router.py, Redis-cached]
|
| 180 |
-
│ → DocumentRetriever (raw SQL: pgvector `<=>` cosine or `<+>` manhattan)
|
| 181 |
-
│ → ChatbotAgent.astream(chunks=...)
|
| 182 |
-
│
|
| 183 |
-
└── source_hint == "structured"
|
| 184 |
-
→ CatalogReader.read(user_id, "structured") [catalog/reader.py]
|
| 185 |
-
→ QueryService.run(user_id, question, catalog) [query/service.py]
|
| 186 |
-
│
|
| 187 |
-
├─ QueryPlannerService.plan(...) [query/planner/service.py]
|
| 188 |
-
│ LLM(catalog, question, prev_error?) → QueryIR
|
| 189 |
-
│
|
| 190 |
-
├─ IRValidator.validate(ir, catalog) [query/ir/validator.py]
|
| 191 |
-
│ fail → loop back to planner with error context (max 3)
|
| 192 |
-
│
|
| 193 |
-
├─ ExecutorDispatcher.pick(ir) [query/executor/dispatcher.py]
|
| 194 |
-
│ schema source → DbExecutor
|
| 195 |
-
│ tabular source → TabularExecutor
|
| 196 |
-
│
|
| 197 |
-
├─ DbExecutor.run(ir): [query/executor/db.py]
|
| 198 |
-
│ SqlCompiler → (sql, params)
|
| 199 |
-
│ → sqlglot SELECT-only guard
|
| 200 |
-
│ → engine_scope (Phase 1 utility) in asyncio.to_thread
|
| 201 |
-
│ → RO txn + statement_timeout=30s + 10k cap
|
| 202 |
-
│
|
| 203 |
-
├─ TabularExecutor.run(ir): [query/executor/tabular.py]
|
| 204 |
-
│ resolve Parquet blob path
|
| 205 |
-
│ → download → PandasCompiler.apply(df)
|
| 206 |
-
│ → asyncio.to_thread → 10k cap
|
| 207 |
-
│
|
| 208 |
-
└─ QueryResult { rows, columns, row_count,
|
| 209 |
-
truncated, source_id, error?, elapsed_ms }
|
| 210 |
-
→
|
| 211 |
-
ChatbotAgent.astream(query_result=...)
|
| 212 |
-
→ yield chunk events
|
| 213 |
-
│
|
| 214 |
-
└── final events: done / error
|
| 215 |
-
│
|
| 216 |
-
└── persist user + assistant messages to chat_messages
|
| 217 |
-
└── populate Redis cache
|
| 218 |
-
```
|
| 219 |
-
|
| 220 |
-
**Safety invariants for the structured path** (read-only at every layer):
|
| 221 |
-
|
| 222 |
-
1. IR validated against the catalog before reaching the compiler
|
| 223 |
-
2. Identifiers come from the catalog (trusted; inlined as quoted identifiers)
|
| 224 |
-
3. Values from `IR.filters` are always parameterized
|
| 225 |
-
4. Compiler is deterministic — no LLM in the hot path
|
| 226 |
-
5. sqlglot rejects anything that isn't a pure SELECT
|
| 227 |
-
6. DB connection is read-only with a 30 s `statement_timeout`
|
| 228 |
-
7. Hard 10 000 row cap on both executors; neither raises — errors go in `QueryResult.error`
|
| 229 |
-
|
| 230 |
-
---
|
| 231 |
-
|
| 232 |
-
## 4. Summary table for review
|
| 233 |
-
|
| 234 |
-
| Concern | Phase 1 — where it lived | Phase 2 — where it lives | Change type |
|
| 235 |
-
|---|---|---|---|
|
| 236 |
-
| Intent classification | `agents/orchestration.py::OrchestratorAgent` (free-text intent) | **Same path + same class name** — body rewritten to emit `IntentRouterDecision` | Body rewrite only |
|
| 237 |
-
| Top-level chat orchestration | Inline in `api/v1/chat.py` | `agents/chat_handler.py::ChatHandler` | Extracted to a reusable module |
|
| 238 |
-
| Final answer formation | `agents/chatbot.py` (Phase 1 LangChain) | `agents/chatbot.py::ChatbotAgent` (was `AnswerAgent` in `answer_agent.py` mid-cycle) | Rewritten + renamed |
|
| 239 |
-
| Schema retrieval (DB / tabular) | `rag/retrievers/schema.py` + PGVector chunks | **Removed**. Replaced by catalog (`catalog/store.py` jsonb) loaded verbatim into planner prompt | Whole concept replaced |
|
| 240 |
-
| Doc retrieval (PDF / DOCX / TXT) | `rag/retrievers/document.py`, `rag/router.py` | `retrieval/document.py`, `retrieval/router.py` | Moved; Redis cache restored; tabular files filtered. **Post-report update:** rewritten to raw SQL (cosine / manhattan only); collection renamed `document_embeddings` → `documents` to match the Go ingestion service. |
|
| 241 |
-
| Query writing | `query/query_executor.py` + `models/sql_query.py` (LLM writes SQL) | `query/planner/service.py` (LLM writes IR) + `query/compiler/sql.py` (deterministic) | LLM emits intent, not SQL |
|
| 242 |
-
| DB execution | `query/executors/db_executor.py` | `query/executor/db.py::DbExecutor` | Folder renamed (`executors` → `executor`); sqlglot guard + RO txn + 30 s timeout kept |
|
| 243 |
-
| Tabular execution | `query/executors/tabular.py` | `query/executor/tabular.py::TabularExecutor` | Parquet-only; pandas compiler split out |
|
| 244 |
-
| Executor selection | Hard-coded in `query_executor.py` | `query/executor/dispatcher.py::ExecutorDispatcher` | New; routes by `source.source_type` |
|
| 245 |
-
| Catalog (NEW) | — | `catalog/` (models, introspect/, validator, store, reader, pii_detector, render) | New subsystem |
|
| 246 |
-
| Catalog persistence | (data was embedded in PGVector) | Postgres jsonb table `data_catalog`, keyed by `user_id` | New table |
|
| 247 |
-
| Ingestion triggers | Inline in API endpoints | `pipeline/triggers.py` (`on_db_registered`, `on_tabular_uploaded`, `on_document_uploaded`, `on_catalog_rebuild_requested`) | Centralized event entry points |
|
| 248 |
-
| Structured pipeline | `pipeline/db_pipeline/db_pipeline_service.py` (still present for `engine_scope` + extractor reuse) | `pipeline/structured_pipeline.py` (orchestrator) — reuses Phase 1 extractor | New orchestrator wraps Phase 1 introspection helpers |
|
| 249 |
-
| Document pipeline | `pipeline/document_pipeline/document_pipeline.py` (folder) | `pipeline/document_pipeline.py` (file) | Flattened; CSV / XLSX now skip the vector store |
|
| 250 |
-
| Parquet helper | `knowledge/parquet_service.py` | `storage/parquet.py` | Moved into `storage/` |
|
| 251 |
-
| Prompts | `config/agents/system_prompt.md`, `guardrails_prompt.md` | `config/prompts/{intent_router,query_planner,chatbot_system,guardrails}.md` | Folder renamed; split into four files; guardrails appended to `chatbot_system` at load |
|
| 252 |
-
| PII detection | — | `catalog/pii_detector.py` + `security/pii_patterns.py` | New. Columns flagged `pii_flag=true` get `sample_values: null` so PII never enters prompts |
|
| 253 |
-
| Chat endpoint | `api/v1/chat.py` (does everything inline) | `api/v1/chat.py` (cache + history + persistence) → delegates to `ChatHandler` | Slimmed; SSE event shape is `intent` / `chunk` / `done` / `error` |
|
| 254 |
-
| DB ingest endpoint | `api/v1/db_client.py::ingest` (Phase 1 `db_pipeline_service.run()`) | `api/v1/db_client.py::ingest` (calls `on_db_registered` only) | Phase 1 dual-write removed |
|
| 255 |
-
| Document process endpoint | `api/v1/document.py::process` (always vectorize) | `api/v1/document.py::process` (PDF/DOCX/TXT → vectors; CSV/XLSX → catalog via `on_tabular_uploaded`) | Routing by file type |
|
| 256 |
-
| Catalog management API | — | `api/v1/data_catalog.py` (GET index + POST rebuild) | New |
|
| 257 |
-
|
| 258 |
-
**Bottom line.** Every Phase 1 file under `src/rag/`, `src/tools/`, `src/query/executors/`, `src/query/query_executor.py`, `src/query/base.py`, `src/api/v1/knowledge.py`, and `src/config/agents/` is gone. Phase 1 introspection helpers under `src/pipeline/db_pipeline/` and `src/database_client/` are still imported by Phase 2 — they were not rewritten, just wrapped. The three LLM call sites are now explicit and the SQL-writing one no longer exists; the planner emits a Pydantic-validated `QueryIR` instead.
|
| 259 |
-
|
| 260 |
-
The one filename gotcha to remember: the **intent router** still lives at `src/agents/orchestration.py` as class `OrchestratorAgent` (Phase 1 name kept for import-site compatibility, Phase 2 body). The matching prompt and tests use the `intent_router` name, but the source module does not.
|
| 261 |
-
|
| 262 |
-
---
|
| 263 |
-
|
| 264 |
-
## 5. Addendum — post-report changes (2026-05-20, mentor commit `61c746f`)
|
| 265 |
-
|
| 266 |
-
This report was originally written as a snapshot at Phase 2 completion. The Phase 2 architecture itself hasn't changed, but a few implementation details have shifted as the Go migration progresses. Captured here so the report stays trustworthy:
|
| 267 |
-
|
| 268 |
-
- **Doc ingestion is now a Go service.** PDF/DOCX/TXT chunking + embedding + writes into PGVector are no longer done by Python. The Python service reads only.
|
| 269 |
-
- **PGVector collection renamed:** `document_embeddings` → `documents` (to match the Go service's writes). Touched files: `db/postgres/vector_store.py`, `retrieval/document.py`.
|
| 270 |
-
- **`DocumentRetriever` rewritten to raw SQL.** Uses pgvector operators directly (`<=>` cosine, `<+>` manhattan). The LangChain ORM path couldn't cope with the schema written by the Go service (asyncpg type-mapping issues — id String vs UUID, jsonb_path_match binding quirks). MMR / euclidean / inner_product were dropped as part of the rewrite.
|
| 271 |
-
- **Intent router defaults flipped.** Ambiguous topical/knowledge questions now prefer `unstructured` (was `structured`). Indonesian few-shot examples added to the prompt.
|
| 272 |
-
- **Cache management endpoints added:** `DELETE /api/v1/chat/cache`, `DELETE /api/v1/chat/cache/room/{id}`, `DELETE /api/v1/retrieval/cache/{user_id}`. Redis chat cache now stores `{response, sources}` (was just `response`) so cached replies repopulate `message_sources`.
|
| 273 |
-
- **Direction.** The long-term split is **Python = agent/ML layer, Go = data plane**. More pieces are expected to follow doc ingestion out of Python.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PROGRESS.md
DELETED
|
@@ -1,692 +0,0 @@
|
|
| 1 |
-
# Progress — Phase 2 catalog-driven build
|
| 2 |
-
|
| 3 |
-
Persistent tracker mirroring the 42-item ownership table in `REPO_CONTEXT.md` "Team — division of work". Update as PRs land. Future Claude Code sessions read this to know what's already done.
|
| 4 |
-
|
| 5 |
-
**Last updated**: 2026-06-12 (Redis Cloud live; R3 closed as won't-do; R5 cache fix; AnalysisRecord persistence landed — `PostgresAnalysisStore` + `analysis_records` table)
|
| 6 |
-
**Current open PR**: `pr/3` — active.
|
| 7 |
-
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
## What just shipped (2026-06-12 — AnalysisRecord persistence, Rifqi)
|
| 11 |
-
|
| 12 |
-
Groundwork for `generate_report`. The slow path now persists a real, citable
|
| 13 |
-
record; the report (next) renders from it.
|
| 14 |
-
|
| 15 |
-
- **Contract gaps closed** (`agents/slow_path/schemas.py`): `stage: CrispStage`
|
| 16 |
-
added to `TaskResult` + `TaskSummary` and populated at all 3 `TaskResult` build
|
| 17 |
-
sites in `task_runner.py` + copied in `assembler._build_record` — so the report
|
| 18 |
-
can group its method appendix by CRISP-DM phase. `AnalysisRecord` gained identity:
|
| 19 |
-
`record_id` (auto uuid), `analysis_id`/`user_id` (optional; stamped at persist).
|
| 20 |
-
- **Real store** (`agents/slow_path/store.py`): `PostgresAnalysisStore` —
|
| 21 |
-
`save()` (never-throw, idempotent upsert) + `list_for_analysis()` (oldest-first,
|
| 22 |
-
the report's render order). `NullAnalysisStore` kept (tests / disabled persistence).
|
| 23 |
-
`AnalysisStore` Protocol gained `list_for_analysis`.
|
| 24 |
-
- **Table** (`db/postgres/models.py`): `analysis_records` jsonb table (one row per
|
| 25 |
-
run, indexed by `analysis_id` + `user_id`); registered in `init_db.py`, created by
|
| 26 |
-
`create_all` on startup (no migration — `data_catalog` precedent).
|
| 27 |
-
- **Wired** (`agents/chat_handler.py`): default store flipped to `PostgresAnalysisStore`;
|
| 28 |
-
`user_id` stamped onto the record at the save site (in scope there).
|
| 29 |
-
- **Open**: `analysis_id` is `NULL` until Harry's Analysis State reaches the slow
|
| 30 |
-
path (session-ID handoff needed to group records per analysis).
|
| 31 |
-
|
| 32 |
-
---
|
| 33 |
-
|
| 34 |
-
## Principal architecture review (2026-06-10) — findings + fix tracker
|
| 35 |
-
|
| 36 |
-
A full external review (read the context docs + the slow path, tool layer, query
|
| 37 |
-
spine, catalog plumbing, chat endpoint, config/connection layers) landed. It confirmed
|
| 38 |
-
the DB-latency diagnosis and surfaced several gaps **not previously tracked here**.
|
| 39 |
-
Verified against code before logging. Severity: **critical** / important / nice-to-have.
|
| 40 |
-
|
| 41 |
-
**Runtime / latency (the original problem):**
|
| 42 |
-
- DB connection handling is the anomaly, NOT cold start. `DbExecutor._run_sync`
|
| 43 |
-
(`db.py:192`) → `engine_scope` does `create_engine → connect (TCP+TLS+SCRAM) → 2×SET
|
| 44 |
-
→ dispose` on EVERY query. Measured ~6–8s for 60 rows; a 2nd warm-session query was
|
| 45 |
-
still ~6.6s → per-call handshake, never amortized. `engine_scope`'s connect-once-dispose
|
| 46 |
-
semantics were designed for the ingestion pipeline and wrongly inherited by the query path.
|
| 47 |
-
- `describe_source` ~3.5s is **planner-induced waste**: every few-shot (`examples.py`)
|
| 48 |
-
opens with a `describe_source` task, so the LLM always plans a tool that re-reads from
|
| 49 |
-
the catalog DB the same catalog already rendered into its prompt. Its impl does 2
|
| 50 |
-
sequential full-catalog reads (`data_access.py:127-128`). Total catalog reads/request ~5×.
|
| 51 |
-
- Azure LLM clients rebuilt per request: `ChatHandler(enable_tracing=True)` is constructed
|
| 52 |
-
per request (`chat.py:172`) → fresh Orchestrator/Chatbot → fresh AzureChatOpenAI → fresh
|
| 53 |
-
TLS to Azure each call. Planner/Assembler correctly use module singletons; the other two don't.
|
| 54 |
-
- Tokens (~13k/request) are NORMAL for this design — do not optimize for $.
|
| 55 |
-
- **Reject the scheduled DB-warmer idea**: targets cold start (~1.8s slice) not the per-call
|
| 56 |
-
handshake, keeps serverless user DBs awake 24/7 (their compute bill), and decrypts every
|
| 57 |
-
tenant's creds on a cron (attack surface). Strictly dominated by an engine cache +
|
| 58 |
-
request-scoped pre-connect.
|
| 59 |
-
|
| 60 |
-
**Fix tracker (new):**
|
| 61 |
-
|
| 62 |
-
| # | Fix | Severity | Owner | Status |
|
| 63 |
-
|---|---|---|---|---|
|
| 64 |
-
| R1 | **AuthN/AuthZ** on data endpoints — reject body-supplied `user_id`/`room_id`, derive identity from a verified token. `/chat/stream` has none (`chat.py:40,128`); tenant isolation is client honesty. **CORRECTION to the review:** `security/auth.py` is a STUB (all `NotImplementedError`); the real JWT impl lives in `src/users/users.py` (`encode_jwt`/`decode_jwt`, HS, env-keyed) **but is unused** — `/login` (`api/v1/users.py`) returns the user profile as plain JSON and mints NO token. So R1 is cross-team: (1) `/login` must issue a JWT, (2) frontend must send it as `Bearer`, (3) data endpoints validate it. **Gates the engine-cache work (DB2).** | **critical** | DB/B + frontend | `[ ]` |
|
| 65 |
-
| R2 | **Always compile a LIMIT** — `sql.py` now emits a bound for every query: explicit limit honored (clamped to `MAX_RESULT_ROWS=10000`), unbounded queries get `LIMIT cap+1` so an unbounded SELECT can't stream a whole table into memory. `CompiledSql.row_cap` carries the cap; `DbExecutor` caps + flags truncation from it (dropped its own `_ROW_HARD_CAP`). Tests updated (`test_sql.py`, +3 cases); `S608` restored to `tests/**` ruff ignore (was dropped). | **critical** | DB | `[x]` |
|
| 66 |
-
| R3 | **Commit `tests/` + minimal CI** — `tests/` is gitignored; the 200+ tests cited as done exist only on laptops (already caused rename rot). ~~GitHub origin carries tests; HF Space gets the Docker build.~~ **2026-06-12: team decided tests stay gitignored/local — closed as won't-do.** | **critical (process)** | shared | `[won't do]` |
|
| 67 |
-
| DB1 | **In-memory `describe_source`** (request-scoped `MemoizingCatalogReader`, `reader.py`) + **LLM-client hoist** (shared module-level `ChatHandler` in `chat.py`). Measured live: `describe_source` 3.5s→~2.0s (structured read now served from the planner's cached snapshot; only the unstructured read remains a round-trip), catalog reads/request ~5→~2. External `query_structured` handshake unchanged (DB2's job) so total slow path is ~flat until DB2. Tests: `tests/catalog/test_reader.py`. | important | agent | `[x]` |
|
| 68 |
-
| DB2 | **Keyed engine cache** — `src/database_client/engine.py::UserEngineCache` (process singleton): pooled engines keyed by `client_id + creds-hash` (rotation auto-invalidates), bounded LRU (50) + 600s idle TTL, `pool_pre_ping` + `pool_recycle=300`. `DbExecutor._run_sync` reuses the warm connection instead of `create_engine→connect→dispose` per query (postgres/supabase only; other db_types keep the legacy path — no regression). **Live-measured: warm `query_structured` 6.6–9.4s → ~2.5s** (the residual is the per-call catalog-DB client fetch + pre-ping, not the external handshake). **Finding:** Neon's transaction pooler REJECTS `default_transaction_read_only` as a libpq startup `option` — caught live; moved read-only + statement_timeout to a per-connection `connect` event (best-effort; authoritative read-only is the SELECT-only compiler + sqlglot guard, see R10). Per-request ownership/active check kept. Proceeded ahead of R1 per owner decision (marginal security delta over the existing no-auth state; auth tracked separately). Tests: `tests/database_client/test_engine.py`. First query/process still cold → DB3. | important | DB | `[x]` |
|
| 69 |
-
| DB3 | **Speculative pre-connect** — `DbExecutor.prewarm(catalog, user_id)` warms the pooled engine for schema sources (fire-and-forget at slow-path entry) so the cold first-query handshake overlaps the ~4s Planner call. Best-effort, never raises; gated to the default path (skipped when a coordinator factory is injected). Verified live through `ChatHandler.handle`. | nice-to-have | DB | `[x]` |
|
| 70 |
-
| R4 | **Per-stage progress events** — `SlowPathCoordinator.run` gained an optional `progress` callback; `ChatHandler` bridges it to SSE `status` events (`chat.py` forwards them). Live: stream now shows `Planning…`→`Running N steps…`→`Composing…` (max wire gap ~4.6s, was ~13s of silence) → fixes proxy idle-timeout + UX. **Deferred:** token-streaming the Assembler answer needs splitting it into a streamed prose call + a structured-record call — that doubles the Assembler LLM calls (cost/latency), so it's a separate decision; the answer is still emitted as one chunk after the (fast ~2.5s) Assembler. Test: `test_chat_handler_wiring.py`. | important | agent | `[~]` |
|
| 71 |
-
| R5 | **Response cache**: key on `user_id` + catalog version; invalidate on ingest. Was `chat:{room_id}:{message}`, 24h TTL, no user → cross-user replay + stale answers. **2026-06-12 (Rifqi):** key now `chat:{room_id}:{user_id}:{message}` via `_chat_cache_key()`, TTL 24h→1h (checkpoint decision) — urgent now that Redis is a shared Cloud instance. `DELETE /chat/cache` gained a required `user_id` param (frontend heads-up); room-wide clear pattern unchanged. **Still open:** catalog-version in key / invalidate-on-ingest. | important | B | `[~]` |
|
| 72 |
-
| R6 | **Hard time budget** — wrap `coordinator.run()` in `asyncio.wait_for` (60–90s). `Constraints.time_budget_seconds` is rendered but not enforced. | important | agent | `[ ]` |
|
| 73 |
-
| R7 | **Root-task-failure short-circuit** before the Assembler (templated/fast-path fallback, NOT replanning) — stops paying ~2k tok to narrate an empty RunState. | important | agent | `[ ]` |
|
| 74 |
-
| R8 | **Catalog upsert race** — per-user advisory lock around read-merge-upsert (`store.py`); concurrent uploads can drop a source. | important | DB | `[ ]` |
|
| 75 |
-
| R9 | **`extra="ignore"`** in `settings.py:15` (currently `allow` → typo'd env vars silently swallowed); require Azure keys in prod. | nice-to-have | B | `[ ]` |
|
| 76 |
-
| R10 | **Read-only enforcement is session-state, not a server role.** `REPO_CONTEXT.md` counts "read-only DB credentials" as a defense layer but nothing requests/verifies a read-only role. Either request read-only creds at registration (verify via `SELECT current_setting(...)`) or drop the claim. | important | DB | `[ ]` |
|
| 77 |
-
| R11 | **De-duplicate** `_PLACEHOLDER_RE` (`task_runner.py:31` vs validator) and `_DATA_ACCESS_TOOLS` (invoker vs planner registry) — import one from the other; comments aren't a sync mechanism. **TAB slice done (90e80f9):** canonical `DATA_ACCESS_TOOLS` now lives once in `tools/data_access.py`; `invoker.py` imports it (was a duplicated frozenset synced by comment). **Agent slice done (2026-06-10):** `PLACEHOLDER_RE` single-sourced in `planner/schemas.py` (part of the ToolCall placeholder convention); validator + task_runner import it. `planner/registry.py` keeps local spec *bodies* (stub pending KM-465 #4) but name-checks them against `DATA_ACCESS_TOOLS` in `_data_access_slice()` — upstream rename/add now raises at `default_registry()` instead of drifting silently. Registry output unchanged (same 12 tools, same order). | nice-to-have | agent/tool | `[x]` |
|
| 78 |
-
| R12 | **Doc/process hygiene** — some code docstrings cite internal design specs that are not committed to the repo (design docs are kept out of version control), so the references dangle for anyone but the author; `CLAUDE.md` lists deleted modules (enricher, `pipeline/orchestrator.py`); `main` is 38 commits behind on a dead architecture. | nice-to-have | agent | `[ ]` |
|
| 79 |
-
| R13 | **Pre-existing test failure** (found during R2, NOT caused by it): `tests/query/planner/test_prompt.py::test_render_catalog_with_sources` fails — `query/planner/prompt.py::render_catalog` now renders stable IDs (`src_test_db`) the test asserts are absent. Old query-planner path; confirmed failing on a clean tree. | nice-to-have | DB | `[ ]` |
|
| 80 |
-
| T1 | **`input_schema` is presence-only, not type-checked** — `ToolSpec.input_schema` comment said "validates ToolCall.args", but `TaskRunner._validate_args` only enforces `required` presence; the `properties` types are documentation, never validated at runtime. Clarified the contract in `tools/contracts.py` so nobody assumes type-safety (a wrong-typed arg passes validation, surfaces only inside the compute fn). Doc-only, no behavior change (90e80f9). | nice-to-have | TAB | `[x]` |
|
| 81 |
-
| T2 | **Dead Python embed path?** — `document_pipeline.process()` → `knowledge_processor` → `vector_store.aadd_documents()` still writes PDF/DOCX/TXT embeddings to `langchain_pg_embedding`, contradicting CLAUDE.md's "Go is sole writer, Python reads only". Verified the Go service (`Orchestrator-Agent-Service/internal/documents`) IS a complete ingestion writer to the same tables for all 5 file types (OCR + chunk + embed) → the Python embed branch is very likely redundant. **Blocked on one operational fact:** does the frontend still upload to `/document/process` (Python) or to Go? Park until confirmed — deleting a live ingestion path would break unstructured RAG. The csv/xlsx parquet branch stays regardless (feeds the catalog/tabular path). | nice-to-have | TAB | `[blocked]` |
|
| 82 |
-
|
| 83 |
-
**Slow-path endpoint wiring (2026-06-10):** the Orchestrator→slow-path is now wired
|
| 84 |
-
into the live endpoint behind an **env flag**. `settings.enable_slow_path` (env
|
| 85 |
-
`ENABLE_SLOW_PATH`, default **off**) is passed to the shared `ChatHandler` in
|
| 86 |
-
`api/v1/chat.py`. Flip `ENABLE_SLOW_PATH=true` to route `structured` intents through
|
| 87 |
-
Planner→TaskRunner→Assembler and test end-to-end from `/chat/stream` (status progress
|
| 88 |
-
events + answer stream). Stays opt-in because `BusinessContext` is still the stub;
|
| 89 |
-
fast/unstructured paths unchanged. Verified live via `ChatHandler.handle`.
|
| 90 |
-
|
| 91 |
-
**Architecture verdict:** fundamentally sound (catalog-driven IR + deterministic compiler
|
| 92 |
-
+ static plan is the right call). Debt is transitional duplication (two planners/registries/
|
| 93 |
-
contract modules — documented, owned) and `ChatHandler` drifting toward a god object
|
| 94 |
-
(extract the slow-path composition root + the SSE `_build_sources`/`_normalize_chunks`
|
| 95 |
-
mappers when convenient).
|
| 96 |
-
|
| 97 |
-
---
|
| 98 |
-
|
| 99 |
-
## What just shipped (2026-06-09/10 — tool layer, tracing, slow-path wiring)
|
| 100 |
-
|
| 101 |
-
Big stretch since the slow-path workers landed. The tool layer (teammate-owned) is
|
| 102 |
-
now **complete and real**, the slow path is **wired into `ChatHandler` behind a gate**,
|
| 103 |
-
and the whole chat pipeline is **traced**. Fast path still untouched; live behavior
|
| 104 |
-
unchanged (flags default off).
|
| 105 |
-
|
| 106 |
-
**Tool layer — COMPLETE (teammate, KM-624→630).** `src/tools/` was re-created (the
|
| 107 |
-
2026-05-11 note about deleting it is superseded). Now teammate-owned:
|
| 108 |
-
- `src/tools/analytics/` — the 8 **composite** `analyze_*` computes (descriptive,
|
| 109 |
-
aggregate, comparison, contribution, profile, correlation, segment, trend) +
|
| 110 |
-
prompt-style DESCRIPTIONs (KM-624/625).
|
| 111 |
-
- `src/tools/contracts.py` — canonical `ToolSpec`/`ToolRegistry`/`ToolOutput` (KM-627).
|
| 112 |
-
`agents/planner/contracts.py` now just re-exports them + keeps the `BusinessContext`
|
| 113 |
-
stub (lead's).
|
| 114 |
-
- `src/tools/registry.py::analytics_registry()` (KM-628); `src/tools/invoker.py` +
|
| 115 |
-
`src/tools/data_access.py` — `AnalyticsToolInvoker` (KM-629), `DataAccessToolInvoker`
|
| 116 |
-
+ `CompositeToolInvoker` (KM-630). All never-throw. **Pattern A confirmed** (`analyze_*`
|
| 117 |
-
take a `data` `${t<id>}` placeholder from an upstream `query_structured`).
|
| 118 |
-
- **Verified live E2E (2026-06-09):** real `query_structured` against a user's Neon
|
| 119 |
-
Postgres → `analyze_trend` → Assembler. `analyze_contribution` surfaced a real tool
|
| 120 |
-
bug (Decimal vs float in `decomposition.py`) — degrade-and-continue held; **now fixed
|
| 121 |
-
by the tool owner** (`_coerce_decimals` in `invoker._materialize`, KM-630 / commit
|
| 122 |
-
1195870), so the whole `analyze_*` family is covered in one place. **Directive:** agent
|
| 123 |
-
side does NOT modify `src/tools/` without confirmation.
|
| 124 |
-
|
| 125 |
-
**Planner — realigned to the real tools (KM-626).** `registry.py::default_registry()`
|
| 126 |
-
composes the real `analytics_registry()` + a local stub for the 4 data-access tools.
|
| 127 |
-
Few-shots grown to **A–D**: A `analyze_contribution`, B `analyze_trend`, C mixed
|
| 128 |
-
structured+unstructured (`retrieve_documents`, independent branch), D `analyze_aggregate`.
|
| 129 |
-
`parallelizable_with` **removed** from `Task` (schema/validator/examples/prompt) —
|
| 130 |
-
TaskRunner derives parallelism from `depends_on` alone.
|
| 131 |
-
|
| 132 |
-
**Slow-path wiring — built, GATED OFF (KM-626).** `agents/chat_handler.py` gains a
|
| 133 |
-
`structured→slow` branch behind `ChatHandler(enable_slow_path=False)`: when on it builds
|
| 134 |
-
a per-request `CompositeToolInvoker` (composition root) + `SlowPathCoordinator`, streams
|
| 135 |
-
`chat_answer`, persists the `analysis_record`. Two seams isolate the remaining blockers:
|
| 136 |
-
- `agents/planner/business_context.py::get_business_context(user_id)` — async stub
|
| 137 |
-
`BusinessContext`; TODO(lead) swap for the real read.
|
| 138 |
-
- `agents/slow_path/store.py` — `AnalysisStore` Protocol + `NullAnalysisStore` (logs
|
| 139 |
-
only). Real store = `analysis_records` table in the catalog DB (Neon `dataeyond`) —
|
| 140 |
-
**table not created yet**. `chat_answer` still emitted as one chunk (not token-streamed).
|
| 141 |
-
|
| 142 |
-
**Observability — Langfuse tracing wired (KM-631).** `src/observability/langfuse/
|
| 143 |
-
tracing.py` — `RequestTracer`/`NullTracer`/`TracingToolInvoker` + `_redact`. One trace
|
| 144 |
-
per request groups Orchestrator.classify, Planner.plan (each retry = its own generation),
|
| 145 |
-
Assembler.assemble, Chatbot.astream + tool spans (latency/metadata only). Gated:
|
| 146 |
-
`ChatHandler(enable_tracing=False)`; `api/v1/chat.py` opts in (`=True`). PII policy:
|
| 147 |
-
Orchestrator+Planner unmasked (question + PII-safe summary); Assembler+Chatbot masked
|
| 148 |
-
(see real rows/chunks); tool spans carry name + arg keys + row count only. Zero added
|
| 149 |
-
LLM tokens; verified live to US Cloud.
|
| 150 |
-
|
| 151 |
-
**Live evals green (2026-06-09, real Azure 4o):** `RUN_PLANNER_EVAL=1` and
|
| 152 |
-
`RUN_SLOW_PATH_EVAL=1` both pass — Planner emits valid catalog-consistent `QueryIR` and
|
| 153 |
-
wires Pattern A correctly; self-corrects via retry.
|
| 154 |
-
|
| 155 |
-
**Open follow-ups:** real `BusinessContext` (lead); create `analysis_records` table +
|
| 156 |
-
real `AnalysisStore` (**Rifqi owns, 2026-06-12** — folded into `generate_report` work,
|
| 157 |
-
see `CHECKPOINT_PLAN_2026-06-17.md`); register data-access `ToolSpec`s upstream (`data_access_registry()`)
|
| 158 |
-
or keep the planner stub; 4o → GPT-mini deployment swap; flip `enable_slow_path` on once
|
| 159 |
-
`BusinessContext` is real. NOTE: 3 test files pre-existing broken from rename rot
|
| 160 |
-
(`test_chat_handler.py`, `test_intent_router.py`, `test_answer_agent.py` import the old
|
| 161 |
-
`answer_agent`/`intent_router` module names).
|
| 162 |
-
|
| 163 |
-
---
|
| 164 |
-
|
| 165 |
-
## What just shipped (2026-06-10 — TAB: tool-layer hardening + DRY)
|
| 166 |
-
|
| 167 |
-
Owner-side companion to the agent block above. After the live E2E surfaced real-data
|
| 168 |
-
edge cases, the tool layer got a round of correctness hardening. All in TAB-owned paths
|
| 169 |
-
(`src/tools/`, `src/catalog/`); no agent-side or API change.
|
| 170 |
-
|
| 171 |
-
**JSON-safety across the `analyze_*` family.** Real DB rows carry scalar types that
|
| 172 |
-
don't survive the jsonb / SSE round-trip:
|
| 173 |
-
- `[KM-630] coerce DB Decimal → float` (commit 1195870) — `_coerce_decimals` in
|
| 174 |
-
`invoker._materialize` converts object-columns holding `decimal.Decimal` (asyncpg
|
| 175 |
-
returns NUMERIC as `Decimal`) to `float64` before any compute runs. Fixes the
|
| 176 |
-
`float + Decimal` TypeError in `decomposition.analyze_contribution` **and** the whole
|
| 177 |
-
family in one seam — only touches columns that actually contain a `Decimal`.
|
| 178 |
-
- `[KM-624] non-JSON-safe scalars in mode & top_value` (commit 6981ed3) — normalize
|
| 179 |
-
numpy / non-native scalars so descriptive + top-value outputs serialize cleanly.
|
| 180 |
-
|
| 181 |
-
**Planner↔Tools registry alignment + Timestamp keys** (commit 4bb7623, `fix(tools)`):
|
| 182 |
-
- `registry.py` — `analyze_descriptive.required` corrected `["data"]` → `["data",
|
| 183 |
-
"column_ids"]` to match the compute signature (`column_ids` has no default). Prevents
|
| 184 |
-
the Planner from emitting a call that's missing a required arg. `analyze_profile` stays
|
| 185 |
-
`["data"]` (its `column_ids` defaults to `None`).
|
| 186 |
-
- `aggregation._clean` — group-by over a datetime column produced `pd.Timestamp` group
|
| 187 |
-
keys that aren't JSON-safe; now normalized to `.isoformat()` alongside the existing
|
| 188 |
-
numpy `.item()` branch.
|
| 189 |
-
|
| 190 |
-
**DRY: single `SAMPLE_LIMIT` constant** (commit 6d46ba5, `[NOTICKET] refactor(catalog)`):
|
| 191 |
-
- One source of truth in `catalog/introspect/base.py` (`SAMPLE_LIMIT = 3`, down from 5 —
|
| 192 |
-
token cost: sample values feed the planner prompt). Both introspection paths import it:
|
| 193 |
-
`catalog/introspect/tabular.py` and `pipeline/db_pipeline/extractor.py` (which dropped
|
| 194 |
-
its own local `= 3`). Dependency direction is pipeline→catalog (no circular import).
|
| 195 |
-
Stale test `test_sample_values_capped_at_five` updated to assert the real cap (3).
|
| 196 |
-
|
| 197 |
-
**Audit result:** Planner↔Tools arg alignment swept end-to-end — 7/8 `analyze_*` tools
|
| 198 |
-
already matched; the 1 mismatch (`analyze_descriptive`) is the fix above. Pattern A holds
|
| 199 |
-
across all of them.
|
| 200 |
-
|
| 201 |
-
---
|
| 202 |
-
|
| 203 |
-
## What just shipped (2026-06-08 — KM-626: slow-path agent layer)
|
| 204 |
-
|
| 205 |
-
The rest of the slow path after the Planner (KM-567) — TaskRunner, Assembler, and
|
| 206 |
-
the coordinator. Built and tested against
|
| 207 |
-
mocks; **not yet wired into the live `ChatHandler`** (waits on the tool team's real
|
| 208 |
-
`ToolInvoker` + a real `BusinessContext`). Fast path untouched.
|
| 209 |
-
|
| 210 |
-
**Naming:** "Orchestrator" = the entry dispatcher only (`agents/orchestration.py`).
|
| 211 |
-
The slow-path **workers** live in **`agents/slow_path/`** — deliberately NOT named
|
| 212 |
-
"orchestrator".
|
| 213 |
-
|
| 214 |
-
**Files added** (`src/agents/slow_path/`):
|
| 215 |
-
- `schemas.py` — `TaskResult`, `RunState`; `TaskSummary`, `AnalysisRecord`,
|
| 216 |
-
`AssembledOutput`, `AssemblerNarrative`. Reuses `ToolOutput`.
|
| 217 |
-
- `invoker.py` — `ToolInvoker` Protocol only; the tool team owns the impl (KM-418).
|
| 218 |
-
- `errors.py` — `SlowPathError`, `AssemblerError`.
|
| 219 |
-
- `task_runner.py` — deterministic, 0 LLM: wave-based execution, `${t<id>}` placeholder
|
| 220 |
-
resolution, internal `validate_args`, never-throw invoke, status labeling,
|
| 221 |
-
degrade-and-continue → `RunState`.
|
| 222 |
-
- `assembler.py` + `prompt.py` + `config/prompts/assembler.md` — single LLM call →
|
| 223 |
-
`AssemblerNarrative`; code merges with `RunState` to build the `AnalysisRecord`
|
| 224 |
-
(structured fields copied, never re-authored).
|
| 225 |
-
- `coordinator.py` — `SlowPathCoordinator`: Planner → TaskRunner → Assembler.
|
| 226 |
-
|
| 227 |
-
**Tests added** (`tests/agents/slow_path/`, 12 passing; gitignored): schema round-trips
|
| 228 |
-
+ chat_answer-first; runner happy/placeholder/parallel/degrade/arg-miss; assembler
|
| 229 |
-
narrative-vs-snapshot + question threading; coordinator end-to-end. `ruff` clean;
|
| 230 |
-
tool-agnostic (no `src/tools/*` import).
|
| 231 |
-
|
| 232 |
-
**Open follow-ups (not blockers):** wire `SlowPathCoordinator` into the expanded
|
| 233 |
-
Orchestrator/`ChatHandler` once the real invoker + `BusinessContext` exist; swap the
|
| 234 |
-
test `MockToolInvoker` for the tool team's real one (zero agent change, INV-7); 4o →
|
| 235 |
-
GPT-mini deployment swap.
|
| 236 |
-
|
| 237 |
-
---
|
| 238 |
-
|
| 239 |
-
## What just shipped (2026-06-08 — tool taxonomy + ownership revision)
|
| 240 |
-
|
| 241 |
-
Team decisions after the teammate pushed KM-624 (`src/tools/analytics/`):
|
| 242 |
-
|
| 243 |
-
- **Composite tools, not atomic.** v1 uses **composite "family" tools** (`analyze_*`),
|
| 244 |
-
not the atomic `compute_*` set the earlier draft assumed. One `analyze_*` call does a
|
| 245 |
-
whole analytical job (e.g. `analyze_descriptive` subsumes median/mode/stddev/percentile;
|
| 246 |
-
`analyze_trend` subsumes `date_trunc`). Tool-taxonomy decision recorded.
|
| 247 |
-
- **Tool team owns ALL tools** — compute, data-access (`query_structured`,
|
| 248 |
-
`retrieve_documents`, `list_sources`, `describe_source`), the wrapper/invoker layer
|
| 249 |
-
(KM-418), and **all tool tests**. The agent team owns nothing below the registry contract.
|
| 250 |
-
- **Planner stub realigned to the real tools.** `registry.py` rewritten from the 9 atomic
|
| 251 |
-
entries to **12 composite entries** (4 data-access + 8 `analyze_*`); `examples.py`
|
| 252 |
-
rewritten (Example A → `analyze_contribution`, Example B → `analyze_trend`); `planner.md`
|
| 253 |
-
bullet updated; planner tests updated. 32 passing + 1 gated, `ruff` clean.
|
| 254 |
-
- **Open (tool team's call):** Pattern A (analyze_* take a `${t<id>}` `data` placeholder
|
| 255 |
-
from an upstream `query_structured`) vs Pattern B (self-fetch by `source_id`). Stub
|
| 256 |
-
assumes A; reshaped to match once decided (agent code unaffected, INV-7).
|
| 257 |
-
- **New coupling:** the tool team's `query_structured`/`retrieve_documents` are expected
|
| 258 |
-
to call our existing `QueryService`/`RetrievalRouter`; `query_structured` stays
|
| 259 |
-
inline-`QueryIR` so `IRValidator` still applies. Interface to coordinate.
|
| 260 |
-
|
| 261 |
-
**Next (our scope, all mock-able now):** TaskRunner + Assembler against a `MockToolInvoker`,
|
| 262 |
-
then Orchestrator slow-path wiring. Stubs still to retire on integration: `contracts.py`
|
| 263 |
-
(BusinessContext from lead; ToolSpec/ToolRegistry/ToolOutput from tool team) and `registry.py`
|
| 264 |
-
(real registry from tool team). Infra: swap the 4o stand-in for a GPT-mini deployment.
|
| 265 |
-
|
| 266 |
-
---
|
| 267 |
-
|
| 268 |
-
## What just shipped (2026-06-05 — Phase 3: Planner agent)
|
| 269 |
-
|
| 270 |
-
First slow-path agent (the Planner). A single LLM
|
| 271 |
-
call turns BusinessContext + Catalog + ToolRegistry + question + Constraints into a
|
| 272 |
-
validated, **static** `TaskList` (DAG of fully-specified tool-call chains). No
|
| 273 |
-
replanning (INV-6); tool-agnostic against a registry contract (INV-7). Fast path
|
| 274 |
-
(`agents/orchestration.py`, `agents/chatbot.py`, `query/`) untouched.
|
| 275 |
-
|
| 276 |
-
**Files added** (`src/agents/planner/`):
|
| 277 |
-
- `contracts.py` — **STUB** Pydantic contracts pending reconciliation: `BusinessContext`
|
| 278 |
-
(+KeyTerm/DataTableNote/DataColumnNote, lead's), `ToolSpec`/`ToolRegistry` (tool
|
| 279 |
-
team KM-608), `ToolOutput` envelope.
|
| 280 |
-
- `schemas.py` — `CrispStage`, `ToolCall`, `Task`, `TaskList`. No replan schemas.
|
| 281 |
-
- `inputs.py` — `CatalogSummary` (condensed, PII `sample_values` nulled, `from_catalog`
|
| 282 |
-
builder + `render`) and `Constraints` (max_tasks=5, modeling_allowed=False).
|
| 283 |
-
- `registry.py` — **STUB** v1 P0 registry: query_structured, retrieve_documents,
|
| 284 |
-
list_sources, describe_source, compute_median/stddev/percentile/mode, date_trunc.
|
| 285 |
-
- `errors.py` — `PlannerError`, `PlannerValidationError`.
|
| 286 |
-
- `prompt.py` + `config/prompts/planner.md` — system prompt (INV-1/6/7 + principles) +
|
| 287 |
-
per-call human content (context + catalog + tools + constraints + few-shots + question).
|
| 288 |
-
- `examples.py` — two few-shots (A exploratory revenue-by-category; B descriptive
|
| 289 |
-
monthly-trend-by-region with date_trunc), built from the real `TaskList` schema.
|
| 290 |
-
- `validator.py` — `PlannerValidator` running the 8 checks; reuses the existing
|
| 291 |
-
`IRValidator` for inline `query_structured` IRs.
|
| 292 |
-
- `service.py` — `PlannerService` + `plan_analysis(...)`: chain (mirrors
|
| 293 |
-
`query/planner/service.py`) + validate-and-retry loop (max 3, mirrors `QueryService`).
|
| 294 |
-
|
| 295 |
-
**Tests added** (`tests/agents/planner/`, 30 passing + 1 gated): `test_schemas.py`,
|
| 296 |
-
`test_inputs.py`, `test_validator.py` (one failure per check + happy paths),
|
| 297 |
-
`test_service.py` (`_FakeChain` + retry), `test_golden_questions.py` (live eval gated on
|
| 298 |
-
`RUN_PLANNER_EVAL=1`). `ruff check` clean on planner paths.
|
| 299 |
-
|
| 300 |
-
**Open follow-ups (not blockers):** reconcile `BusinessContext` with the lead and
|
| 301 |
-
`ToolRegistry`/`ToolSpec` + real tools with teammate (KM-608); "GPT mini" currently uses
|
| 302 |
-
the configured 4o deployment (swap `azure_deployment` when a mini deployment exists). Next:
|
| 303 |
-
Orchestrator slow-path expansion + TaskRunner + Assembler.
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
-
|
| 307 |
-
## Legend
|
| 308 |
-
|
| 309 |
-
- `[x]` done and merged
|
| 310 |
-
- `[~]` in progress (open PR or active branch)
|
| 311 |
-
- `[ ]` not started
|
| 312 |
-
- **DB** / **TAB** / **B** — ownership (from REPO_CONTEXT.md)
|
| 313 |
-
|
| 314 |
-
---
|
| 315 |
-
|
| 316 |
-
## PR sequence
|
| 317 |
-
|
| 318 |
-
| PR | Status | Owner(s) | Scope |
|
| 319 |
-
|---|---|---|---|
|
| 320 |
-
| PR1 | `[x]` merged | DB | Contract locks + catalog plumbing + DB introspector + IR validator + tests |
|
| 321 |
-
| PR1-tab | `[x]` shipped | TAB | Tabular introspector + on_tabular_uploaded trigger + 31 unit tests |
|
| 322 |
-
| PR2a | `[x]` merged | DB | CatalogEnricher + StructuredPipeline + on_db_registered trigger + FK extension on Table (enricher later removed in KM-557) |
|
| 323 |
-
| KM-557 | `[x]` shipped | DB | Drop CatalogEnricher entirely (cost cut — planner uses stats + sample rows directly); rename jsonb table `catalogs` → `data_catalog`; add `GET /api/v1/data-catalog/{user_id}` index endpoint for catalog refresher |
|
| 324 |
-
| PR2b | `[x]` shipped | DB-solo (B-review) | IntentRouter + planner prompt + planner LLM service |
|
| 325 |
-
| PR3-DB | `[x]` shipped | DB | SqlCompiler (Postgres) + DbExecutor (sqlglot guard, RO + statement_timeout, asyncio.to_thread) + 36 golden IR→SQL tests |
|
| 326 |
-
| PR3-TAB | `[x]` shipped | TAB | PandasCompiler + TabularExecutor + 43+12 golden IR→DataFrame tests |
|
| 327 |
-
| PR4 | `[x]` | DB-solo (B-review) | ExecutorDispatcher + QueryService + ChatHandler module. **API rewired in Cleanup PR.** |
|
| 328 |
-
| PR5 | `[x]` shipped | DB-solo (B-review) | Retry/self-correction loop on validation failure (lives in QueryService, max 3 attempts, planner re-prompted with prior error) |
|
| 329 |
-
| PR6 | `[~]` scaffold | DB-solo (B-review) | Eval harness scaffold + 3 DB-targeting golden cases. Skipped without `RUN_PLANNER_EVAL=1` env. TAB extends with tabular cases. |
|
| 330 |
-
| PR7 | `[x]` | DB-solo (B-review) | `ChatbotAgent` (renamed from `AnswerAgent`) + chatbot_system + guardrails prompts. `answer_agent.py` → `chatbot.py`, `AnswerAgent` → `ChatbotAgent`. API rewired in Cleanup PR. |
|
| 331 |
-
| Cleanup | `[x]` | B | ChatHandler wired to chat.py; Phase 1 dual-write dropped from /ingest; on_catalog_rebuild_requested + POST /data-catalog/rebuild; dead modules deleted (chatbot Phase 1, orchestrator, query/base, knowledge.py, config/agents/); retrieval cache restored via RetrievalRouter; top_values added to ColumnStats; lifespan migration; knowledge_router removed. |
|
| 332 |
-
|
| 333 |
-
---
|
| 334 |
-
|
| 335 |
-
## All items
|
| 336 |
-
|
| 337 |
-
### Contracts (B — shared)
|
| 338 |
-
|
| 339 |
-
| # | Item | Status | Notes |
|
| 340 |
-
|---|---|---|---|
|
| 341 |
-
| 1 | Catalog Pydantic models (`catalog/models.py`) | `[x]` | PR1 added `location_ref` URI-scheme docstring; PR2a added `ForeignKey` model + `Table.foreign_keys` field |
|
| 342 |
-
| 2 | IR Pydantic models (`query/ir/models.py`) | `[x]` | Pre-existing scaffold |
|
| 343 |
-
| 3 | IR operator whitelists (`query/ir/operators.py`) | `[x]` | PR1 filled `TYPE_COMPATIBILITY` matrix |
|
| 344 |
-
| 4 | PII patterns / regex (`security/pii_patterns.py`) | `[x]` | Pre-existing |
|
| 345 |
-
| — | `data_catalog` Postgres jsonb table (`db/postgres/models.py`) | `[x]` | PR1 added `Catalog` SQLAlchemy class + `init_db.py` import. KM-557 renamed `__tablename__` from `catalogs` → `data_catalog`; created fresh (no migration) |
|
| 346 |
-
| — | `QueryResult` shape (`query/executor/base.py`) | `[x]` | Pre-existing scaffold; `columns: list[str]` added (TAB owner, PR1-tab) — DbExecutor updated to populate it. |
|
| 347 |
-
| — | `Source.location_ref` URI scheme | `[x]` | PR1 documented in `catalog/models.py` docstring |
|
| 348 |
-
|
| 349 |
-
### Ingestion — introspection
|
| 350 |
-
|
| 351 |
-
| # | Item | Owner | Status | Notes |
|
| 352 |
-
|---|---|---|---|---|
|
| 353 |
-
| 5 | DB introspector (`catalog/introspect/database.py`) | DB | `[x]` | PR1 — reuses Phase 1 `database_client_service`, `db_credential_encryption`, `db_pipeline_service.engine_scope`, `extractor.get_schema/profile_column/get_row_count`. PR2a wired FK extraction (was discarded before). |
|
| 354 |
-
| 6 | Tabular introspector (`catalog/introspect/tabular.py`) | TAB | `[x]` | PR1-tab — downloads original blob (CSV/XLSX/Parquet), one Table per sheet (XLSX) or one Table (CSV/Parquet). `source_id = document_id`. `fetch_doc`/`fetch_blob` injectable for unit tests (no Settings). **2026-06-10**: sample cap now imports the shared `SAMPLE_LIMIT` (=3) from `catalog/introspect/base.py` — single source of truth across the tabular + DB introspection paths (commit 6d46ba5). |
|
| 355 |
-
| 7 | `BaseIntrospector` ABC (`catalog/introspect/base.py`) | B | `[x]` | Pre-existing; signature locked |
|
| 356 |
-
|
| 357 |
-
### Ingestion — shared catalog plumbing
|
| 358 |
-
|
| 359 |
-
| # | Item | Owner | Status | Notes |
|
| 360 |
-
|---|---|---|---|---|
|
| 361 |
-
| 8 | ~~Catalog enricher + prompt~~ | B | **REMOVED in KM-557** | Cost optimization — planner reads stats + sample rows + column names directly. `catalog/enricher.py` + `config/prompts/catalog_enricher.md` deleted. `render_source` (the only piece still needed) moved to `src/catalog/render.py`. Tests moved to `tests/catalog/test_render.py`. |
|
| 362 |
-
| 9 | Catalog validator (`catalog/validator.py`) | B | `[x]` | PR1 (DB owner picked up) — uniqueness invariants |
|
| 363 |
-
| 10 | Catalog store — Postgres jsonb (`catalog/store.py`) | B | `[x]` | PR1 (DB owner picked up) — `INSERT ... ON CONFLICT` |
|
| 364 |
-
| 11 | Catalog reader (`catalog/reader.py`) | B | `[x]` | PR1 (DB owner picked up) — filters by source_hint, empty on miss |
|
| 365 |
-
| 12 | PII detector (`catalog/pii_detector.py`) | B | `[x]` | PR1 (DB owner picked up) — name + value matching, bias toward over-flag |
|
| 366 |
-
|
| 367 |
-
### Ingestion — pipelines
|
| 368 |
-
|
| 369 |
-
| # | Item | Owner | Status | Notes |
|
| 370 |
-
|---|---|---|---|---|
|
| 371 |
-
| 13 | Structured pipeline (`pipeline/structured_pipeline.py`) | B | `[x]` | PR2a (DB owner) — Source-type-agnostic: caller supplies the introspector. `default_structured_pipeline()` factory wires production deps lazily so tests can inject mocks without `Settings()` construction. **KM-557**: enrich step removed; pipeline is now `introspect → merge with existing → validate → upsert`. Constructor no longer takes `enricher`. |
|
| 372 |
-
| 14 | Triggers (`pipeline/triggers.py`) | B | `[x]` | PR2a — `on_db_registered` implemented (DB owner). PR1-tab — `on_tabular_uploaded` implemented (TAB owner). **2026-05-11** — `on_document_uploaded` implemented. **2026-05-12** — `on_catalog_rebuild_requested` implemented: iterates all Sources in current catalog, re-runs `on_db_registered` (schema) or `on_tabular_uploaded` (tabular) per source; per-source errors logged but don't abort. |
|
| 373 |
-
| 15 | Ingestion orchestrator (`pipeline/orchestrator.py`) | B | **DELETED** | Redundant stub — `StructuredPipeline` already takes introspector at run() time. Deleted in Cleanup PR. |
|
| 374 |
-
| 16 | Document pipeline (`pipeline/document_pipeline.py`) | TAB | `[x]` | Flattened `pipeline/document_pipeline/document_pipeline.py` (folder) → `pipeline/document_pipeline.py` (file). Updated import in `api/v1/document.py`. |
|
| 375 |
-
|
| 376 |
-
### Query — shared spine
|
| 377 |
-
|
| 378 |
-
| # | Item | Owner | Status | Notes |
|
| 379 |
-
|---|---|---|---|---|
|
| 380 |
-
| 17 | IR validator (`query/ir/validator.py`) | B | `[x]` | PR1 (DB owner) — full rule set; descriptive errors for planner retry |
|
| 381 |
-
| 18 | Planner LLM service (`query/planner/service.py`) | B | `[x]` | PR2b — Azure OpenAI structured output → `QueryIR`. Injectable chain. Supports retry via `previous_error` argument. |
|
| 382 |
-
| 19 | Planner prompt (`query/planner/prompt.py`, `config/prompts/query_planner.md`) | B | `[x]` | PR2b — system prompt with hard constraints + few-shot for DB and tabular sources. `build_planner_prompt(question, catalog, previous_error)` calls `catalog.render.render_source` (renamed from `catalog.enricher.render_source` in KM-557). |
|
| 383 |
-
| 20 | Intent router (`agents/orchestration.py` — class `OrchestratorAgent`; `config/prompts/intent_router.md`) | B | `[x]` | PR2b — single LLM call → `IntentRouterDecision(needs_search, source_hint, rewritten_query)`. Supports conversation history. **NOTE**: source filename + class name were kept from Phase 1 for import-site compatibility; only the body is Phase 2. Prompt file and test file use the `intent_router` name. |
|
| 384 |
-
| 21 | Executor base + `QueryResult` (`query/executor/base.py`) | B | `[x]` | Pre-existing scaffold |
|
| 385 |
-
| 22 | Executor dispatcher (`query/executor/dispatcher.py`) | B | `[x]` | PR4 — picks DbExecutor / TabularExecutor by `source.source_type`. Lazy imports of production executors keep import side-effect-free for tests. Caches per source_type. |
|
| 386 |
-
| 23 | Compiler base ABC (`query/compiler/base.py`) | B | `[x]` | Pre-existing scaffold |
|
| 387 |
-
| 24 | Top-level QueryService (`query/service.py`) | B | `[x]` | PR4+5 — `plan → validate → dispatch → execute → QueryResult`. Retry loop on validation failure (max 3, planner re-prompted with prior error). Catches NotImplementedError from TabularExecutor placeholder gracefully. Never raises. |
|
| 388 |
-
|
| 389 |
-
### Query — DB path
|
| 390 |
-
|
| 391 |
-
| # | Item | Status | Notes |
|
| 392 |
-
|---|---|---|---|
|
| 393 |
-
| 25 | SQL compiler (`query/compiler/sql.py`) | `[x]` | PR3-DB — Postgres dialect (Supabase reuses); deterministic IR → (sql, named-params dict); double-quoted identifiers from catalog; all whitelisted ops (=, !=, <, <=, >, >=, in, not_in, is_null, is_not_null, like, between); alias-aware order_by; `CompiledSql.params: dict[str, Any]` (changed from `list`). MySQL/BigQuery/Snowflake compilers later. |
|
| 394 |
-
| 26 | DB executor (`query/executor/db.py`) | `[x]` | PR3-DB — sync engine via `db_pipeline_service.engine_scope` inside `asyncio.to_thread`. sqlglot SELECT-only / no-DML guard. Postgres-only session settings: `default_transaction_read_only=on` + `statement_timeout=30000`. asyncio.wait_for backstop. Never raises — populates `QueryResult.error`. 10k row hard cap. |
|
| 395 |
-
| 27 | Credential encryption (`security/credentials.py`) | `[ ]` | Stub exists; PR1 reused Phase 1 `utils/db_credential_encryption.py` instead. Move in cleanup PR |
|
| 396 |
-
| 28 | User-DB connection management | `[x]` | PR3-DB reused Phase 1 `db_pipeline_service.engine_scope` (same as PR1 introspector); no new helper needed |
|
| 397 |
-
|
| 398 |
-
### Query — Tabular path
|
| 399 |
-
|
| 400 |
-
| # | Item | Status | Notes |
|
| 401 |
-
|---|---|---|---|
|
| 402 |
-
| 29 | Pandas compiler (`query/compiler/pandas.py`) | `[x]` | PR3-TAB — `CompiledPandas` dataclass; all 12 filter ops; all 6 aggs; group_by via `pd.concat` of Series; alias-aware order_by; `_like_to_regex` (`%`→`.*`, `_`→`.`); pure module-level helpers. (`polars` for large files still deferred — see Planned dependencies.) |
|
| 403 |
-
| 30 | Tabular executor (`query/executor/tabular.py`) | `[x]` | PR3-TAB — `fetch_blob` injectable for tests; blob path: single-table → `{uid}/{did}.parquet`, multi-table → `{uid}/{did}__{table.name}.parquet`; `asyncio.to_thread`; 10k row hard cap; errors → `QueryResult.error`. Dispatcher routes to it by `source_type`. |
|
| 404 |
-
| 31 | Parquet upload/download wrapper | `[x]` | Moved `knowledge/parquet_service.py` → `storage/parquet.py`. Updated 4 import sites: `pipeline/document_pipeline.py`, `knowledge/processing_service.py`, `query/executor/tabular.py`, `query/executors/tabular.py`. |
|
| 405 |
-
|
| 406 |
-
### Agents + chat
|
| 407 |
-
|
| 408 |
-
| # | Item | Status | Notes |
|
| 409 |
-
|---|---|---|---|
|
| 410 |
-
| 32 | Chatbot agent + prompt (`agents/chatbot.py`, `config/prompts/chatbot_system.md`) | `[x]` | PR7-bundle — `ChatbotAgent` (was `AnswerAgent`) streams tokens, accepts `QueryResult` or list[`DocumentChunk`] or neither. **Cleanup PR**: renamed `answer_agent.py` → `chatbot.py`, `AnswerAgent` → `ChatbotAgent`; Phase 1 `agents/chatbot.py` deleted. |
|
| 411 |
-
| 33 | Guardrails prompt (`config/prompts/guardrails.md`) | `[x]` | PR7-bundle — appended to `chatbot_system.md` so guardrails take precedence in conflict. |
|
| 412 |
-
| — | Chat handler / orchestrator (`agents/chat_handler.py`) | `[x]` | PR4-bundle — top-level Phase 2 orchestrator. Routes by `source_hint`: chat → AnswerAgent direct; structured → CatalogReader + QueryService; unstructured → DocumentRetriever placeholder + AnswerAgent. Yields `intent` / `chunk` / `done` / `error` SSE-style events. Phase 1 chat.py NOT touched — cleanup PR rewires the API to call this. **2026-06-09**: gained the gated `structured→slow` branch (`enable_slow_path=False`) + `enable_tracing` (KM-626/631). |
|
| 413 |
-
|
| 414 |
-
### Tools — slow-path "Tools" component (TAB)
|
| 415 |
-
|
| 416 |
-
New scope after the original 42-item table; added as the tool layer landed (KM-608/624–631). All TAB-owned (`src/tools/`), all never-throw.
|
| 417 |
-
|
| 418 |
-
| # | Item | Owner | Status | Notes |
|
| 419 |
-
|---|---|---|---|---|
|
| 420 |
-
| — | Analytics compute fns (`tools/analytics/`) | TAB | `[x]` | KM-608/624/625 — 8 **composite** `analyze_*` fns (descriptive, aggregate, comparison, contribution, profile, correlation, segment, trend) + prompt-style DESCRIPTIONs. Pure pandas, no I/O. JSON-safe outputs (numpy/Decimal/Timestamp normalized — KM-624 + commit 4bb7623). |
|
| 421 |
-
| — | Tool contracts (`tools/contracts.py`) | TAB | `[x]` | KM-627 — canonical `ToolSpec` / `ToolRegistry` / `ToolOutput`. `agents/planner/contracts.py` re-exports them (+ keeps the lead's `BusinessContext` stub). |
|
| 422 |
-
| — | Analytics registry (`tools/registry.py`) | TAB | `[x]` | KM-628 — `analytics_registry()`. `analyze_descriptive.required` = `["data","column_ids"]` (aligned to compute signature, commit 4bb7623). |
|
| 423 |
-
| — | Invoker layer (`tools/invoker.py`) | TAB | `[x]` | KM-629 — `AnalyticsToolInvoker` (Pattern A: `analyze_*` take a `data` `${t<id>}` placeholder from upstream `query_structured`; `_materialize` → DataFrame, `_coerce_decimals` covers the whole family) + `CompositeToolInvoker` (routes data-access vs analytics by name). |
|
| 424 |
-
| — | Data-access tools (`tools/data_access.py`) | TAB | `[x]` | KM-630 — `DataAccessToolInvoker`: `list_sources` / `describe_source` / `query_structured` / `retrieve_documents`. Per-request DI (`user_id` + `CatalogReader`). `query_structured` calls `IRValidator` + `ExecutorDispatcher` (planner skipped — IR pre-built by the agent Planner). **Superseded by KM-642/643** — renamed `data_retrieve`/`knowledge_retrieve` and `list_sources`+`describe_source` merged into `data_check` + new `knowledge_check`; see row below. |
|
| 425 |
-
| — | Tool tests (`tests/unit/tools/`) | TAB | `[x]` | analytics + data-access + invoker tests (gitignored). Incl. regression `test_decimal_columns_coerced_for_analyze_contribution`. |
|
| 426 |
-
| — | Data/knowledge tool taxonomy (`tools/data_access.py`) | TAB | `[x]` | KM-642/643 (commits c38c0c2, 4bd5f1e) — renamed `query_structured`→`data_retrieve`, `retrieve_documents`→`knowledge_retrieve`; merged `list_sources`+`describe_source` → parameterized `data_check` (no arg = list structured sources; `source_id` = that source's schema) + new `knowledge_check` (unstructured/documents). Split mirrors the catalog's structured/unstructured slices. Planner stub/prompt/validator/few-shots synced; `DATA_ACCESS_TOOLS` guard kept in lockstep. Note: dated log entries above (e.g. the 2026-06-09 E2E) keep the old names as historical record. |
|
| 427 |
-
|
| 428 |
-
### API surface
|
| 429 |
-
|
| 430 |
-
| # | Item | Owner | Status | Notes |
|
| 431 |
-
|---|---|---|---|---|
|
| 432 |
-
| 34 | DB client endpoints (`api/v1/db_client.py`) | DB | `[x]` | **Cleanup PR** — `/ingest` now calls only `on_db_registered`. Phase 1 `db_pipeline_service.run()` + `decrypt_credentials_dict` removed. Error from catalog build now raises HTTP 500 (was silent log). Response simplified to `{"status": "success", "client_id": ...}`. |
|
| 433 |
-
| 35 | Document/tabular upload endpoints (`api/v1/document.py`) | TAB | `[x]` | Rewired `/document/process` — after processing CSV/XLSX, calls `on_tabular_uploaded(document_id, user_id)`. Catalog ingestion failure is logged but does not fail the request. **2026-05-11** — CSV/XLSX no longer ingested to vector store (`knowledge_processor` skipped for tabular types in `document_pipeline.py`); they go to catalog only. |
|
| 434 |
-
| 36 | Chat stream endpoint (`api/v1/chat.py`) | B | `[x]` | Rewired `/chat/stream` — replaced `query_executor.execute()` (Phase 1) with `CatalogReader + QueryService` (Phase 2). **Cleanup PR**: fully rewired to `ChatHandler.handle()`. Inline intent routing, retrieval, and answer generation removed. Redis cache, fast intent, history loading, and message persistence remain in chat.py. Sources event emits `[]` (retrieval not yet exposed by ChatHandler). |
|
| 435 |
-
| 37 | Room / users endpoints (`api/v1/room.py`, `api/v1/users.py`) | B | `[ ]` | No catalog work; only touch if auth flow changes |
|
| 436 |
-
| — | Data catalog index endpoint (`api/v1/data_catalog.py`) | DB | `[x]` | **KM-557** — `GET /api/v1/data-catalog/{user_id}` → `list[CatalogIndexEntry]`. **Cleanup PR** — added `POST /api/v1/data-catalog/rebuild?user_id=` → calls `on_catalog_rebuild_requested`; per-source errors logged but don't fail the request. |
|
| 437 |
-
|
| 438 |
-
### Tests + eval
|
| 439 |
-
|
| 440 |
-
| # | Item | Owner | Status | Notes |
|
| 441 |
-
|---|---|---|---|---|
|
| 442 |
-
| 38 | DB compiler golden tests (`tests/query/compiler/test_sql.py`) | DB | `[x]` | PR3-DB — 36 tests across all whitelisted ops, identifier quoting, agg / count_distinct / count(*), order_by alias resolution, parameter sequencing, error paths. Pure-Python, no LLM, no DB. |
|
| 443 |
-
| 39 | Pandas compiler golden tests (`tests/unit/query/compiler/test_pandas_compiler.py`) | TAB | `[x]` | PR3-TAB — 43 tests: all 12 filter ops, all 6 aggs, group_by, order_by, limit, aliases, empty DataFrame, error paths. `test_tabular_executor.py` adds 12 more (blob name resolution + happy path + error paths). |
|
| 444 |
-
| 40 | IR validator tests (`tests/query/ir/test_validator.py`) | B | `[x]` | PR1 — 19 tests, all rules covered |
|
| 445 |
-
| — | PII detector tests (`tests/catalog/test_pii_detector.py`) | B | `[x]` | PR1 — 26 tests (parametrized) |
|
| 446 |
-
| — | Catalog validator tests (`tests/catalog/test_validator.py`) | B | `[x]` | PR1 — 5 tests |
|
| 447 |
-
| — | Catalog render tests (`tests/catalog/test_render.py`) | B | `[x]` | **KM-557** — 5 tests (renamed from `test_enricher.py`; LLM enrichment tests dropped, render-only tests kept). |
|
| 448 |
-
| — | Catalog store integration test (`tests/catalog/test_store.py`) | DB | `[x]` | PR1 — module-level skip without `RUN_INTEGRATION_TESTS=1` |
|
| 449 |
-
| — | DB introspector test | DB | `[ ]` | Deferred to PR2 — needs Postgres testcontainer or fixture infra |
|
| 450 |
-
| — | Tabular introspector test | TAB | `[x]` | PR1-tab — 31 unit tests (CSV/XLSX/Parquet, stats, PII, error paths). No DB/blob I/O — mocks injected via constructor. |
|
| 451 |
-
| 41 | Planner eval (`tests/query/planner/`) | B | `[x]` | PR6-scaffold — `test_golden_questions.py` with 3 DB-targeting cases. TAB added `test_golden_tabular.py` with 4 tabular cases (group_by+sum, top-N+limit, date range filter, XLSX sheet selection). All 4 passed against real Azure OpenAI. Fix shipped alongside: `query/planner/service.py` replaced `("system", text)` tuple with `SystemMessage` — without this, `{...}` in `query_planner.md` was parsed as f-string variables and crashed on every real invocation. |
|
| 452 |
-
| 42 | E2E smoke tests (`tests/e2e/`) | B | `[ ]` | Defer until Phase 2 endpoints are wired (cleanup PR). Component-level orchestration is already covered by `test_chat_handler.py` + `test_service.py`. |
|
| 453 |
-
| — | Golden IR fixtures (`tests/fixtures/golden_irs.json`) | B | `[~]` | PR1 seeded with 5 DB-targeting examples; TAB extends in PR1-tab |
|
| 454 |
-
| — | Shared `sample_catalog` fixture (`tests/conftest.py`) | B | `[x]` | PR1 — DB-shaped; TAB may add tabular sibling |
|
| 455 |
-
|
| 456 |
-
---
|
| 457 |
-
|
| 458 |
-
## What just shipped (2026-05-12 — Cleanup PR)
|
| 459 |
-
|
| 460 |
-
**Phase 1 removal + Phase 2 API rewiring:**
|
| 461 |
-
- `src/api/v1/chat.py` — fully rewired to `ChatHandler.handle()`. Removed inline IntentRouter, retrieval, and ChatbotAgent calls. Redis cache, fast intent, load_history, save_messages stay in chat.py.
|
| 462 |
-
- `src/api/v1/db_client.py` — `/ingest` now calls only `on_db_registered`. Phase 1 `db_pipeline_service.run()` block removed. Catalog build failure now raises HTTP 500.
|
| 463 |
-
- `src/api/v1/data_catalog.py` — added `POST /api/v1/data-catalog/rebuild` endpoint.
|
| 464 |
-
- `src/pipeline/triggers.py` — `on_catalog_rebuild_requested` implemented: iterates catalog sources, re-runs the appropriate trigger per source type, per-source errors logged.
|
| 465 |
-
|
| 466 |
-
**Dead modules deleted:**
|
| 467 |
-
- `src/agents/chatbot.py` (Phase 1 LangChain chatbot)
|
| 468 |
-
- `src/pipeline/orchestrator.py` (empty stub)
|
| 469 |
-
- `src/query/base.py` (old duplicate of `executor/base.py`)
|
| 470 |
-
- `src/api/v1/knowledge.py` (fake `/knowledge/rebuild` endpoint)
|
| 471 |
-
- `src/config/agents/` (folder — prompts only used by deleted Phase 1 chatbot)
|
| 472 |
-
|
| 473 |
-
**Renames:**
|
| 474 |
-
- `src/agents/answer_agent.py` → `src/agents/chatbot.py`; `AnswerAgent` → `ChatbotAgent`; updated all import sites (`chat_handler.py`, `chat.py`)
|
| 475 |
-
|
| 476 |
-
**Fixes + improvements:**
|
| 477 |
-
- `src/agents/chat_handler.py` — `_get_document_retriever()` now returns `RetrievalRouter` (Redis-cached) instead of `DocumentRetriever` directly; retrieval-level cache restored.
|
| 478 |
-
- `src/retrieval/router.py` — removed dead `db: AsyncSession` and `source_hint` parameters + `_UNSTRUCTURED_HINTS` constant from `retrieve()`. Cache key simplified.
|
| 479 |
-
- `src/knowledge/processing_service.py` — removed dead `_build_csv_documents`, `_build_excel_documents`, `_profile_dataframe`, `_to_sheet_document` methods + `pandas` and `upload_parquet` imports.
|
| 480 |
-
- `src/catalog/models.py` — added `top_values: list[Any] | None` to `ColumnStats`.
|
| 481 |
-
- `src/catalog/introspect/tabular.py` — `_to_column` now populates `top_values` for columns with ≤10 distinct values; useful for query planner WHERE clause generation.
|
| 482 |
-
- `main.py` — replaced deprecated `@app.on_event("startup")` with `lifespan` context manager; removed `knowledge_router`.
|
| 483 |
-
|
| 484 |
-
---
|
| 485 |
-
|
| 486 |
-
## What just shipped (KM-557 — DB owner)
|
| 487 |
-
|
| 488 |
-
After lead review of the catalog ingestion cost: dropped LLM enrichment,
|
| 489 |
-
renamed the storage table, and exposed a lightweight index endpoint for
|
| 490 |
-
the upcoming catalog refresher.
|
| 491 |
-
|
| 492 |
-
**Files deleted**:
|
| 493 |
-
- `src/catalog/enricher.py` — entire CatalogEnricher + EnrichmentResponse + apply_descriptions removed
|
| 494 |
-
- `src/config/prompts/catalog_enricher.md` — dead prompt
|
| 495 |
-
- `tests/catalog/test_enricher.py` — replaced by `test_render.py`
|
| 496 |
-
|
| 497 |
-
**Files added**:
|
| 498 |
-
- `src/catalog/render.py` — new home for `render_source` (the only piece of the old enricher still needed; consumed by `query/planner/prompt.py`)
|
| 499 |
-
- `src/api/v1/data_catalog.py` — `GET /api/v1/data-catalog/{user_id}` returns `list[CatalogIndexEntry]`
|
| 500 |
-
- `tests/catalog/test_render.py` — 5 tests (same coverage as the old render block)
|
| 501 |
-
|
| 502 |
-
**Files modified**:
|
| 503 |
-
- `src/db/postgres/models.py` — `__tablename__ = "data_catalog"` (was `"catalogs"`). Class name unchanged
|
| 504 |
-
- `src/pipeline/structured_pipeline.py` — `StructuredPipeline(validator, store)` (was `(enricher, validator, store)`); pipeline is now `introspect → merge → validate → upsert`; `default_structured_pipeline()` no longer constructs an enricher
|
| 505 |
-
- `src/pipeline/triggers.py` — docstrings updated; `on_catalog_rebuild_requested` docstring rewritten for the refresher use case
|
| 506 |
-
- `src/query/planner/prompt.py` — import now `from ...catalog.render import render_source`
|
| 507 |
-
- `src/catalog/introspect/{base,database,tabular}.py` — docstring scrubs (no behavior changes)
|
| 508 |
-
- `src/models/api/catalog.py` — added `CatalogIndexEntry`; simplified `CatalogRebuildResponse` to `sources_rebuilt`
|
| 509 |
-
- `main.py` — registered `data_catalog_router`
|
| 510 |
-
- `src/security/README.md` — one stale wording fix
|
| 511 |
-
|
| 512 |
-
**No migration**: the `data_catalog` table is created from scratch on first `init_db()`. The old `catalogs` table was never deployed against production data, so no rename SQL is needed.
|
| 513 |
-
|
| 514 |
-
**Tests**: all 4 `test_structured_pipeline.py` tests reworked to construct `StructuredPipeline(validator=, store=)` without `enricher`. 5 `test_render.py` tests cover render_source standalone.
|
| 515 |
-
|
| 516 |
-
**Lint**: `ruff check` clean on modified Phase 2 paths.
|
| 517 |
-
|
| 518 |
-
**Open follow-ups left for the lead**:
|
| 519 |
-
- `on_catalog_rebuild_requested` body — the refresher will iterate the index endpoint and call this trigger per source
|
| 520 |
-
- `api/v1/db_client.py` `/ingest` still doesn't call `on_db_registered` — same blocker as before, untouched by KM-557
|
| 521 |
-
|
| 522 |
-
---
|
| 523 |
-
|
| 524 |
-
## What just shipped (2026-05-11 — retrieval migration + bug fixes)
|
| 525 |
-
|
| 526 |
-
**Files implemented / migrated**:
|
| 527 |
-
- `src/retrieval/base.py` — `RetrievalResult` dataclass + `BaseRetriever` ABC (was in `src/rag/base.py`)
|
| 528 |
-
- `src/retrieval/document.py` — full `DocumentRetriever` migrated from `src/rag/retrievers/document.py`; all retrieval methods (MMR/cosine/euclidean/inner_product/manhattan). Tabular file types filtered out from results.
|
| 529 |
-
- `src/retrieval/router.py` — `RetrievalRouter` (Redis-cached, unstructured-only). `invalidate_cache(user_id)` clears all `retrieval:{user_id}:*` keys.
|
| 530 |
-
|
| 531 |
-
**Deleted** (no longer used):
|
| 532 |
-
- `src/rag/` — entire folder (base.py, retriever.py, router.py, retrievers/)
|
| 533 |
-
- `src/tools/` — entire folder (search.py was the only real file; only called by deleted rag/ router)
|
| 534 |
-
|
| 535 |
-
**Bug fixes**:
|
| 536 |
-
- `src/pipeline/document_pipeline.py` — `retrieval_router.invalidate_cache(user_id)` called after `process()` and `delete()`. Redis failure is caught and logged (does not fail the document op).
|
| 537 |
-
- `src/pipeline/document_pipeline.py` — CSV/XLSX now skips `knowledge_processor` (vector store). Tabular files go to catalog only; no duplicate embeddings.
|
| 538 |
-
- `src/pipeline/triggers.py` — `on_document_uploaded` implemented (was `raise NotImplementedError`).
|
| 539 |
-
- `src/agents/chat_handler.py` — `_normalize_chunks` now handles `RetrievalResult` objects. Previously they were silently dropped, causing empty context for unstructured queries through ChatHandler.
|
| 540 |
-
|
| 541 |
-
**Import updates** (all changed from `src.rag.*` → `src.retrieval.*`):
|
| 542 |
-
- `src/api/v1/chat.py`, `src/query/base.py`, `src/query/query_executor.py`, `src/query/executors/db_executor.py`, `src/query/executors/tabular.py`
|
| 543 |
-
|
| 544 |
-
---
|
| 545 |
-
|
| 546 |
-
## What shipped previously (PR2b/4/5/6/7-bundle — DB owner solo, teammate reviews)
|
| 547 |
-
|
| 548 |
-
**Files implemented**:
|
| 549 |
-
- `src/agents/orchestration.py` — `OrchestratorAgent.classify(message, history) → IntentRouterDecision`. Pydantic model for structured output. History-aware query rewriting. Phase 1 filename + class name preserved; body fully rewritten for Phase 2.
|
| 550 |
-
- `src/agents/answer_agent.py` — `AnswerAgent.astream(...)` streams answer tokens; accepts `QueryResult` and/or `list[DocumentChunk]`. Renames to `chatbot.py` in cleanup PR.
|
| 551 |
-
- `src/agents/chat_handler.py` — `ChatHandler.handle(message, user_id, history)` returns `AsyncIterator[dict]` of `intent` / `chunk` / `done` / `error` SSE events. All deps injectable; lazy default builders.
|
| 552 |
-
- `src/query/planner/prompt.py` — `render_catalog(catalog)` + `build_planner_prompt(question, catalog, previous_error)`. Reuses `catalog.enricher.render_source` for consistency across LLM call sites.
|
| 553 |
-
- `src/query/planner/service.py` — `QueryPlannerService.plan(question, catalog, previous_error)` Azure OpenAI structured output → `QueryIR`.
|
| 554 |
-
- `src/query/executor/dispatcher.py` — `ExecutorDispatcher.pick(ir) → BaseExecutor` by `source.source_type`. Lazy executor imports + per-source-type cache.
|
| 555 |
-
- `src/query/service.py` — `QueryService.run(user_id, question, catalog) → QueryResult`. Plan→validate→retry-on-failure (max 3)→dispatch→execute. Catches NotImplementedError from TabularExecutor placeholder gracefully.
|
| 556 |
-
|
| 557 |
-
**Prompts written** (filled in placeholders):
|
| 558 |
-
- `src/config/prompts/intent_router.md`
|
| 559 |
-
- `src/config/prompts/query_planner.md`
|
| 560 |
-
- `src/config/prompts/chatbot_system.md`
|
| 561 |
-
- `src/config/prompts/guardrails.md`
|
| 562 |
-
|
| 563 |
-
**Tests added** (46 new — total now 146 + 2 skipped):
|
| 564 |
-
- `tests/agents/test_intent_router.py` (4)
|
| 565 |
-
- `tests/agents/test_answer_agent.py` (12)
|
| 566 |
-
- `tests/agents/test_chat_handler.py` (6)
|
| 567 |
-
- `tests/query/planner/test_prompt.py` (7)
|
| 568 |
-
- `tests/query/planner/test_service.py` (3)
|
| 569 |
-
- `tests/query/executor/test_dispatcher.py` (5)
|
| 570 |
-
- `tests/query/test_service.py` (8)
|
| 571 |
-
- `tests/query/planner/test_golden_questions.py` (3 — skipped by default; eval harness scaffold)
|
| 572 |
-
|
| 573 |
-
**Lint**: `ruff check` clean on all Phase 2 paths. Phase 1 files have pre-existing E501/S608 issues — out of scope for this PR.
|
| 574 |
-
|
| 575 |
-
**Placeholders / blockers for teammate** (status as of DB owner's commit, before merge):
|
| 576 |
-
- `src/query/executor/tabular.py` (TAB) — DB owner's note: "still raises NotImplementedError". **Post-merge**: TAB shipped this in PR3-TAB; dispatcher now routes to the real `TabularExecutor`. The `NotImplementedError` catch in `QueryService` stays as a safety net.
|
| 577 |
-
- `src/retrieval/document.py` — **implemented** (2026-05-11). Full `DocumentRetriever` migrated from `src/rag/retrievers/document.py`; supports MMR/cosine/euclidean/manhattan/inner_product. `_normalize_chunks` in `chat_handler.py` now handles `RetrievalResult` → `DocumentChunk` conversion correctly.
|
| 578 |
-
- `src/api/v1/chat.py` (Phase 1) — NOT touched. Cleanup PR rewires the SSE endpoint to call `ChatHandler.handle(...)`.
|
| 579 |
-
- `src/api/v1/db_client.py` (Phase 1) — NOT touched. Cleanup PR rewires `/database-clients/{id}/ingest` to call `pipeline.triggers.on_db_registered`.
|
| 580 |
-
|
| 581 |
-
---
|
| 582 |
-
|
| 583 |
-
## What shipped previously (PR3-TAB — TAB owner)
|
| 584 |
-
|
| 585 |
-
**Files implemented**:
|
| 586 |
-
- `src/query/compiler/pandas.py` — `PandasCompiler` + `CompiledPandas(apply, output_columns)` dataclass. Pure helper functions (easier to test in isolation): `_apply_filters` (all 12 ops, `_like_to_regex` for LIKE), `_apply_select` (column pick + rename), `_apply_agg` (scalar + group_by via `pd.concat` of Series → `reset_index`), `_apply_orderby` (alias-aware via `_resolve_order_col`). Closure captures all IR fields explicitly so `apply(df)` is self-contained.
|
| 587 |
-
- `src/query/executor/tabular.py` — `TabularExecutor` with injectable `fetch_blob` (same testability pattern as `TabularIntrospector`). Resolves Parquet blob path from `az_blob://{uid}/{did}` + table: single-table → `{uid}/{did}.parquet`, multi-table → `{uid}/{did}__{table.name}.parquet`. Runs compile → download → `asyncio.to_thread(_load_and_apply)` → 10k hard cap. Never raises; errors populate `QueryResult.error`. Uses `compiled.output_columns` for column labels (safe on empty DataFrame).
|
| 588 |
-
|
| 589 |
-
**Tests added** (55 new — total suite was 86 all passing at PR3-TAB time):
|
| 590 |
-
- `tests/unit/query/compiler/test_pandas_compiler.py` — 43 tests across all 12 filter ops (including `is_null`, `not_in`, `like`, `between`), all 6 agg fns, group_by, order_by asc/desc, limit-after-order, alias round-trip, empty DataFrame, error paths.
|
| 591 |
-
- `tests/unit/query/executor/test_tabular_executor.py` — 12 tests: `_resolve_blob_name` (single/multi-table, bad prefix), happy-path `QueryResult` shape (columns, rows, backend, truncated, source_id), wrong source_type → error, blob fetch failure → error, unknown source → error.
|
| 592 |
-
|
| 593 |
-
**Lint**: `ruff check` clean on both files.
|
| 594 |
-
|
| 595 |
-
---
|
| 596 |
-
|
| 597 |
-
## What shipped previously (PR1-tab — TAB owner)
|
| 598 |
-
|
| 599 |
-
**Files implemented**:
|
| 600 |
-
- `src/catalog/introspect/tabular.py` — `TabularIntrospector` reads original blob (CSV/XLSX/Parquet), profiles each column (dtype, stats, sample values), runs PIIDetector. For XLSX: one `Table` per sheet (`Table.name = sheet_name`); for CSV/Parquet: one `Table` (`Table.name = filename stem`). `fetch_doc`/`fetch_blob` are constructor-injectable for unit tests — no `Settings` or DB required at import time.
|
| 601 |
-
- `src/pipeline/triggers.py` — `on_tabular_uploaded` wired (mirrors `on_db_registered` pattern).
|
| 602 |
-
|
| 603 |
-
**Tests added** (31 new):
|
| 604 |
-
- `tests/unit/catalog/test_introspect_tabular.py` — CSV / XLSX / Parquet shapes, per-column stats, nullable detection, PII name + value matching, sample capping, all error paths. Pure Python, no network I/O.
|
| 605 |
-
|
| 606 |
-
**Executor contract note**: introspector downloads the *original* blob for schema reading. The tabular executor (PR3-TAB) downloads *Parquet* blobs for query execution. For CSV/Parquet sources (single table), the executor must call `parquet_blob_name(uid, did, sheet_name=None)`; for XLSX (multi-table), `parquet_blob_name(uid, did, table.name)`.
|
| 607 |
-
|
| 608 |
-
---
|
| 609 |
-
|
| 610 |
-
## What shipped previously (PR3-DB — DB owner)
|
| 611 |
-
|
| 612 |
-
**Files implemented**:
|
| 613 |
-
- `src/query/compiler/sql.py` — `SqlCompiler` for Postgres dialect; `CompiledSql(sql, params)` dataclass with `params: dict[str, Any]` (changed from `list`); supports all 12 whitelisted filter ops, all 6 aggs, alias-aware order_by; `_qident` escapes embedded double-quotes
|
| 614 |
-
- `src/query/executor/db.py` — `DbExecutor` with sqlglot SELECT-only guard, Postgres session-level read-only + 30s `statement_timeout`, `asyncio.wait_for` backstop, 10k row hard cap; rejects non-`schema` source_type and `dbclient://` URI mismatch; never raises (populates `QueryResult.error`)
|
| 615 |
-
|
| 616 |
-
**Files extended**:
|
| 617 |
-
- `src/query/compiler/pandas.py` — fixed pre-existing UP035 (Callable import)
|
| 618 |
-
- `pyproject.toml` — added `S608` to `tests/**` ruff ignore (false positive: tests assert literal SQL strings)
|
| 619 |
-
|
| 620 |
-
**Tests added** (36 new, all passing — total now 100):
|
| 621 |
-
- `tests/query/compiler/test_sql.py` — every filter op, every agg, count(*), count_distinct, order_by alias vs column, multi-filter AND, identifier quoting escape, error paths
|
| 622 |
-
|
| 623 |
-
**Lint**: `ruff check` clean on Phase 2 paths.
|
| 624 |
-
|
| 625 |
-
**Hand-off note for teammate**: `CompiledSql.params` is now `dict[str, Any]` not `list`. The pandas compiler will follow the same convention (or document its own) — coordinate when PR3-TAB lands.
|
| 626 |
-
|
| 627 |
-
---
|
| 628 |
-
|
| 629 |
-
## What shipped previously (PR2a — DB owner)
|
| 630 |
-
|
| 631 |
-
**Files implemented**:
|
| 632 |
-
- `src/catalog/enricher.py` — Azure OpenAI GPT-4o + structured output (`EnrichmentResponse`), `render_source` (reusable by planner prompt later), `apply_descriptions` merger, injectable `structured_chain` for tests
|
| 633 |
-
- `src/pipeline/structured_pipeline.py` — `StructuredPipeline` orchestrator + `default_structured_pipeline()` factory with lazy production-dep imports
|
| 634 |
-
- `src/pipeline/triggers.py` — `on_db_registered` wired; tabular/document/rebuild stubs preserved with implementation notes
|
| 635 |
-
|
| 636 |
-
**Files extended**:
|
| 637 |
-
- `src/catalog/models.py` — added `ForeignKey` model, `Table.foreign_keys: list[ForeignKey] = []`
|
| 638 |
-
- `src/catalog/introspect/database.py` — `_extract_foreign_keys` populates `Table.foreign_keys` from extractor data
|
| 639 |
-
- `src/config/prompts/catalog_enricher.md` — full system prompt with style rules and one few-shot example
|
| 640 |
-
|
| 641 |
-
**Tests added** (14 new, all passing — total now 64):
|
| 642 |
-
- `tests/catalog/test_enricher.py` — render / apply / end-to-end with fake chain (10 tests)
|
| 643 |
-
- `tests/pipeline/test_structured_pipeline.py` — orchestration with stub deps (4 tests)
|
| 644 |
-
|
| 645 |
-
**Lint**: `ruff check` clean on all Phase 2 paths. Phase 1 files (`pipeline/db_pipeline/`, `pipeline/document_pipeline/`) have pre-existing ruff issues — out of scope for this PR.
|
| 646 |
-
|
| 647 |
-
---
|
| 648 |
-
|
| 649 |
-
## What shipped previously (PR1 — DB owner's first chunk)
|
| 650 |
-
|
| 651 |
-
**Files implemented** (was `NotImplementedError`):
|
| 652 |
-
- `src/catalog/pii_detector.py`, `src/catalog/validator.py`, `src/catalog/store.py`, `src/catalog/reader.py`
|
| 653 |
-
- `src/catalog/introspect/database.py` (FK extraction added in PR2a)
|
| 654 |
-
- `src/query/ir/validator.py`
|
| 655 |
-
|
| 656 |
-
**Files extended**:
|
| 657 |
-
- `src/query/ir/operators.py` — `TYPE_COMPATIBILITY` matrix
|
| 658 |
-
- `src/catalog/models.py` — `location_ref` URI-scheme docstring
|
| 659 |
-
- `src/db/postgres/models.py` — `Catalog` SQLAlchemy table; `init_db.py` imports it
|
| 660 |
-
|
| 661 |
-
**Tests**: 50 unit tests + 1 integration (gated on `RUN_INTEGRATION_TESTS=1`).
|
| 662 |
-
|
| 663 |
-
**Reused Phase 1 utilities** (cleanup deferred):
|
| 664 |
-
- `src/database_client/database_client_service.py:get`
|
| 665 |
-
- `src/utils/db_credential_encryption.py:decrypt_credentials_dict`
|
| 666 |
-
- `src/pipeline/db_pipeline/db_pipeline_service.py:engine_scope`
|
| 667 |
-
- `src/pipeline/db_pipeline/extractor.py:get_schema/profile_column/get_row_count`
|
| 668 |
-
|
| 669 |
-
---
|
| 670 |
-
|
| 671 |
-
## Open contract items (not yet locked)
|
| 672 |
-
|
| 673 |
-
- **Joins in IR** — currently single-table only (ARCHITECTURE.md §7); DB owner accepted the constraint for v1, will revisit in PR3 if it's blocking real queries
|
| 674 |
-
- **`updated_at` on Source vs `generated_at` on Catalog** — Pydantic models have both; introspector sets per-Source; CatalogStore preserves both
|
| 675 |
-
- **Catalog refresh trigger** (open question §3) — default policy is rebuild-on-upload-or-connect; auto-refresh deferred
|
| 676 |
-
- **Unstructured catalog entries** (open question §2) — currently empty filter for `source_hint="unstructured"`; revisit when adding doc descriptions
|
| 677 |
-
- **PII handling for `sample_values`** (open question §5) — currently nulls them out (skip); mask/synthesize deferred
|
| 678 |
-
- **Dialect priority for SQL compiler** — PR3 will land Postgres first, MySQL second; BigQuery/Snowflake/SQL Server later
|
| 679 |
-
|
| 680 |
-
---
|
| 681 |
-
|
| 682 |
-
## How to update this file
|
| 683 |
-
|
| 684 |
-
When a PR lands:
|
| 685 |
-
1. Flip status from `[ ]` or `[~]` to `[x]`
|
| 686 |
-
2. Add a short note (file paths, scope cuts, surprises)
|
| 687 |
-
3. Bump "Last updated" at the top
|
| 688 |
-
4. If a new contract decision lands, move it from "Open contract items" to the relevant inline note
|
| 689 |
-
|
| 690 |
-
When opening a PR:
|
| 691 |
-
1. Flip status to `[~]` and add yourself as the active owner in the PR row
|
| 692 |
-
2. Don't promise items in the PR description that aren't in the table
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
REPO_CONTEXT.md
DELETED
|
@@ -1,494 +0,0 @@
|
|
| 1 |
-
# Repo Context — Agentic Service Data Eyond Catalog
|
| 2 |
-
|
| 3 |
-
Orientation file for future Claude Code sessions. Cross-reference `ARCHITECTURE.md` for the full design rationale and decision log.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## Product vision — Data Eyond, your AI data scientist
|
| 8 |
-
|
| 9 |
-
Data Eyond is positioned as an *AI data scientist* that supports business analytics. It is built around the **CRISP-DM** framework (Business Understanding → Data Understanding → Data Preparation → Modeling → Evaluation → Deployment) — the agent works through data problems the way a real analyst would, not as a one-shot Q&A bot.
|
| 10 |
-
|
| 11 |
-
**Target users:**
|
| 12 |
-
- **Executives** — deep-dive into their own data and extract insight to drive business decisions without needing a data team in the loop.
|
| 13 |
-
- **Data analysts / scientists** — offload routine analysis so they can focus on heavier work.
|
| 14 |
-
|
| 15 |
-
**Envisioned user flow:**
|
| 16 |
-
1. **Discovery interview** — a short conversation with a Data Eyond *interview agent* that draws out goal, business context, and what the user is actually trying to learn (CRISP-DM Business Understanding).
|
| 17 |
-
2. **Connect data** — DB connection or file upload (DB, CSV, XLSX, Parquet, documents).
|
| 18 |
-
3. **Ask Data Eyond** — natural-language analytical question.
|
| 19 |
-
4. **CRISP-DM-structured analytical response** — exportable as a **presentation deliverable** or a **notebook-style report**.
|
| 20 |
-
|
| 21 |
-
North star: less "chatbot over a database", more "junior data scientist that hands back a polished, decision-ready deliverable."
|
| 22 |
-
|
| 23 |
-
The current repo (Phase 2, below) is the *foundation* — IntentRouter → QueryPlanner → Executor → ChatbotAgent gives us a reliable structured-query spine. The next evolution is the agentic layer that turns this into an end-to-end CRISP-DM workflow (see *Roadmap — agentic evolution* further down).
|
| 24 |
-
|
| 25 |
-
---
|
| 26 |
-
|
| 27 |
-
## TL;DR
|
| 28 |
-
|
| 29 |
-
FastAPI multi-agent backend for data analysis. Users upload documents and register databases / tabular files; they ask natural-language questions and get answers grounded in their data, streamed via SSE.
|
| 30 |
-
|
| 31 |
-
The architecture has two paths:
|
| 32 |
-
|
| 33 |
-
- **Unstructured** (PDF, DOCX, TXT) — dense similarity over prose chunks (PGVector).
|
| 34 |
-
- **Structured** (databases, XLSX, CSV, Parquet) — a per-user **data catalog** describes what tables/columns exist; an LLM produces a **JSON IR** of intent; a deterministic Python compiler turns the IR into SQL or pandas; the executor runs it.
|
| 35 |
-
|
| 36 |
-
The LLM produces *intent*, not query syntax. Deterministic code does the rest.
|
| 37 |
-
|
| 38 |
-
The Phase 2 end-to-end flow is **wired and runnable** as of 2026-05-12. See *Implementation status* below for the per-file matrix. `PROGRESS.md` is the authoritative line-by-line tracker; this file is the orientation.
|
| 39 |
-
|
| 40 |
-
---
|
| 41 |
-
|
| 42 |
-
## Stack
|
| 43 |
-
|
| 44 |
-
- Python 3.12, FastAPI 0.115, uvicorn, sse-starlette
|
| 45 |
-
- Async SQLAlchemy 2.0 + asyncpg (Postgres), psycopg3 (PGVector multi-statement workaround)
|
| 46 |
-
- LangChain 0.3 + langchain-postgres (PGVector) + langchain-openai (Azure OpenAI GPT-4o + embeddings)
|
| 47 |
-
- LangGraph 0.2 + langgraph-checkpoint-postgres
|
| 48 |
-
- Redis 5 (response + retrieval cache)
|
| 49 |
-
- Azure Blob Storage (uploads + Parquet)
|
| 50 |
-
- pandas, pyarrow, polars-ready (deferred), sqlglot, pydantic v2, structlog, slowapi, langfuse
|
| 51 |
-
- presidio-analyzer + spaCy `en_core_web_lg` (PII), pytesseract + pdf2image (PDF OCR)
|
| 52 |
-
- DB connectors: psycopg2, pymysql, pymssql, sqlalchemy-bigquery, snowflake-sqlalchemy
|
| 53 |
-
|
| 54 |
-
Run: `uv run --no-sync uvicorn main:app --host 0.0.0.0 --port 7860`. On Windows use `uv run --no-sync python run.py` (sets `WindowsSelectorEventLoopPolicy` for psycopg3 async).
|
| 55 |
-
|
| 56 |
-
---
|
| 57 |
-
|
| 58 |
-
## Top-level layout
|
| 59 |
-
|
| 60 |
-
```
|
| 61 |
-
main.py — FastAPI app + middleware + router wiring + init_db() on startup
|
| 62 |
-
run.py — Windows-safe local entry point
|
| 63 |
-
ARCHITECTURE.md — design intent (source of truth for shape + invariants)
|
| 64 |
-
README.md
|
| 65 |
-
Dockerfile — python:3.12-slim, installs spaCy en_core_web_lg, tesseract, poppler
|
| 66 |
-
pyproject.toml / uv.lock
|
| 67 |
-
scripts/ — backfill scripts (build_initial_catalogs, enrich_all_sources)
|
| 68 |
-
src/ — all application code
|
| 69 |
-
```
|
| 70 |
-
|
| 71 |
-
---
|
| 72 |
-
|
| 73 |
-
## src/ map
|
| 74 |
-
|
| 75 |
-
### Core data shapes (only files with real content)
|
| 76 |
-
|
| 77 |
-
| Path | Role |
|
| 78 |
-
|---|---|
|
| 79 |
-
| `catalog/models.py` | Pydantic: `Catalog → Source[] → Table[] → Column[]` |
|
| 80 |
-
| `query/ir/models.py` | `QueryIR` (select / filters / group_by / order_by / limit) |
|
| 81 |
-
| `query/ir/operators.py` | `ALLOWED_FILTER_OPS`, `ALLOWED_AGG_FNS`, `LIMIT_HARD_CAP=10000` |
|
| 82 |
-
| `security/pii_patterns.py` | name patterns + email/phone regex for PII detection |
|
| 83 |
-
|
| 84 |
-
### Catalog — identity layer for structured sources (Cs ∪ Ct)
|
| 85 |
-
|
| 86 |
-
| Path | Role |
|
| 87 |
-
|---|---|
|
| 88 |
-
| `catalog/introspect/base.py` | `BaseIntrospector.introspect(location_ref) -> Source` |
|
| 89 |
-
| `catalog/introspect/database.py` | `information_schema` + ~100 row sample → draft Source |
|
| 90 |
-
| `catalog/introspect/tabular.py` | Parquet/CSV/XLSX header reader + sample (one Table per sheet for XLSX) |
|
| 91 |
-
| `catalog/render.py` | renders a `Source` as the canonical text block consumed by the planner (KM-557; LLM enrichment removed — planner reads stats + samples directly) |
|
| 92 |
-
| `catalog/validator.py` | invariants beyond Pydantic shape (unique IDs, FK refs) |
|
| 93 |
-
| `catalog/store.py` | persist as Postgres `jsonb` row keyed by user_id (`get/upsert/delete`) — table `data_catalog` |
|
| 94 |
-
| `catalog/reader.py` | load + filter catalog by source_hint (returns full catalog for ≤50 tables) |
|
| 95 |
-
| `catalog/pii_detector.py` | flag PII columns at ingestion → suppresses `sample_values` |
|
| 96 |
-
|
| 97 |
-
### Query — catalog-driven structured path
|
| 98 |
-
|
| 99 |
-
| Path | Role |
|
| 100 |
-
|---|---|
|
| 101 |
-
| `query/service.py` | `QueryService.run(user_id, question, catalog) -> QueryResult` (top-level) |
|
| 102 |
-
| `query/planner/service.py` | LLM call: question + catalog → QueryIR (structured output) |
|
| 103 |
-
| `query/planner/prompt.py` | renders catalog into the planner prompt |
|
| 104 |
-
| `query/ir/validator.py` | catalog-aware IR validation: column_ids exist, ops whitelisted, value_type matches data_type, limit ≤ cap |
|
| 105 |
-
| `query/compiler/base.py` | `BaseCompiler.compile(ir) -> object` |
|
| 106 |
-
| `query/compiler/sql.py` | IR → `(sql, params)`; identifiers from catalog, values parameterized |
|
| 107 |
-
| `query/compiler/pandas.py` | IR → callable that runs against a DataFrame |
|
| 108 |
-
| `query/executor/base.py` | `BaseExecutor.run(ir) -> QueryResult` (uniform across backends) |
|
| 109 |
-
| `query/executor/db.py` | runs compiled SQL via asyncpg/pymysql in read-only txn (sqlglot second-line defence) |
|
| 110 |
-
| `query/executor/tabular.py` | runs pandas/polars chain on a Parquet file (eager pandas → pyarrow pushdown → polars lazy by file size) |
|
| 111 |
-
| `query/executor/dispatcher.py` | picks DB vs Tabular executor based on `source.source_type` of the IR's source |
|
| 112 |
-
|
| 113 |
-
### Retrieval — unstructured path (Cu)
|
| 114 |
-
|
| 115 |
-
| Path | Role |
|
| 116 |
-
|---|---|
|
| 117 |
-
| `retrieval/document.py` | `DocumentRetriever` over PGVector chunks |
|
| 118 |
-
| `retrieval/router.py` | dispatches the `unstructured` route (the `chat` and `structured` routes do not pass through here) |
|
| 119 |
-
|
| 120 |
-
### Agents — the three LLM call sites
|
| 121 |
-
|
| 122 |
-
| Path | Role |
|
| 123 |
-
|---|---|
|
| 124 |
-
| `agents/orchestration.py` | `OrchestratorAgent` — classifies message → `needs_search`, `source_hint ∈ {chat, unstructured, structured}`, `rewritten_query`. Filename + class name kept from Phase 1; body replaced with Phase 2 logic. Output model is `IntentRouterDecision` |
|
| 125 |
-
| `agents/chatbot.py` | `ChatbotAgent` — final answer formation (receives Cu chunks or QueryResult); SSE-streamed via `astream` |
|
| 126 |
-
| `agents/chat_handler.py` | `ChatHandler` — top-level orchestrator; routes to chat / unstructured / structured and yields SSE-style `intent`/`chunk`/`done`/`error` events |
|
| 127 |
-
|
| 128 |
-
(`QueryPlanner` is the third LLM call site, under `query/planner/`. The
|
| 129 |
-
fourth — `CatalogEnricher` — was removed in KM-557; ingestion no longer
|
| 130 |
-
makes any LLM calls.)
|
| 131 |
-
|
| 132 |
-
### Pipelines — ingestion coordinators
|
| 133 |
-
|
| 134 |
-
| Path | Role |
|
| 135 |
-
|---|---|
|
| 136 |
-
| `pipeline/structured_pipeline.py` | DB / tabular: introspect → merge → validate → store (no enrich step since KM-557) |
|
| 137 |
-
| `pipeline/document_pipeline.py` | unstructured: extract → chunk → embed → PGVector. CSV/XLSX skip vector store (catalog only). Invalidates retrieval cache on process/delete. |
|
| 138 |
-
| `pipeline/triggers.py` | event entry points called by API routes: `on_db_registered`, `on_tabular_uploaded`, `on_document_uploaded`, `on_catalog_rebuild_requested` |
|
| 139 |
-
|
| 140 |
-
(`pipeline/orchestrator.py` was deleted in the Cleanup PR — it was a redundant stub; `StructuredPipeline` already takes the introspector at `run()` time.)
|
| 141 |
-
|
| 142 |
-
### Security — cross-cutting
|
| 143 |
-
|
| 144 |
-
| Path | Role |
|
| 145 |
-
|---|---|
|
| 146 |
-
| `security/auth.py` | bcrypt password hash/verify, JWT encode/decode, get_user |
|
| 147 |
-
| `security/credentials.py` | Fernet encrypt/decrypt for stored DB credentials |
|
| 148 |
-
| `security/pii_patterns.py` | (already listed) |
|
| 149 |
-
|
| 150 |
-
### API + infra + config
|
| 151 |
-
|
| 152 |
-
| Path | Role |
|
| 153 |
-
|---|---|
|
| 154 |
-
| `api/v1/*.py` | FastAPI routers — thin endpoints delegating to `pipeline/triggers` and `query/service` |
|
| 155 |
-
| `models/api/{catalog,chat,document}.py` | request/response Pydantic models |
|
| 156 |
-
| `db/postgres/connection.py` | two async engines: `engine` (app) and `_pgvector_engine` (PGVector) |
|
| 157 |
-
| `db/postgres/init_db.py` | startup: creates `vector` extension, all tables, HNSW + GIN indexes |
|
| 158 |
-
| `db/postgres/models.py` | SQLAlchemy app tables (users, rooms, chat messages, …) |
|
| 159 |
-
| `db/postgres/vector_store.py` | shared PGVector instance (collection `documents` — written by Go ingestion service) |
|
| 160 |
-
| `db/redis/connection.py` | async Redis client |
|
| 161 |
-
| `storage/az_blob/az_blob.py` | Azure Blob async wrapper (uploads + Parquet) |
|
| 162 |
-
| `middlewares/{cors,logging,rate_limit}.py` | CORS allow-all (POC), structlog JSON, slowapi |
|
| 163 |
-
| `observability/langfuse/langfuse.py` | trace helper |
|
| 164 |
-
| `config/settings.py` | pydantic-settings; `.env` uses double-underscore aliases |
|
| 165 |
-
| `config/env_constant.py` | env file path constant |
|
| 166 |
-
| `config/prompts/*.md` | prompt templates: `intent_router`, `query_planner`, `chatbot_system`, `guardrails` (KM-557 removed `catalog_enricher`) |
|
| 167 |
-
|
| 168 |
-
---
|
| 169 |
-
|
| 170 |
-
## Core architectural decisions
|
| 171 |
-
|
| 172 |
-
1. **Catalog as primary context, not retrieval.** For ≤50 tables (typical), the entire catalog is rendered into the planner prompt verbatim (~3–5k tokens). No vector search, no BM25, no top-k for structured data. Catalog-level retrieval (BM25 + table-level vectors with RRF) is the *deferred* upgrade for users with hundreds of tables.
|
| 173 |
-
|
| 174 |
-
2. **JSON IR over raw SQL.** The planner LLM emits a Pydantic-validated intent, never a SQL string. The compiler is deterministic Python. Benefits: validatable before execution, dialect-portable (one IR → SQL of any dialect / pandas / polars), cheaper tokens, trivially testable without an LLM, and the LLM literally cannot emit invalid SQL syntax.
|
| 175 |
-
|
| 176 |
-
3. **Deterministic compiler, not LLM SQL writer.** All actual query construction happens in pure code. Compiler bugs are reproducible and fixable. Same IR → same query.
|
| 177 |
-
|
| 178 |
-
4. **Pipeline stage isolation.** Each stage (`IntentRouter`, `CatalogReader`, `QueryPlanner`, `IRValidator`, `QueryCompiler`, `QueryExecutor`, `ChatbotAgent`) is its own module with typed input and typed output. No god classes.
|
| 179 |
-
|
| 180 |
-
5. **Minimal LLM surface.** Only three LLM call sites in the system (KM-557 dropped `CatalogEnricher` — ingestion is now LLM-free; the planner reads stats + sample rows + column names directly):
|
| 181 |
-
- `IntentRouter` — once per user message
|
| 182 |
-
- `QueryPlanner` — once per structured query
|
| 183 |
-
- `ChatbotAgent` — once per answer (formatting)
|
| 184 |
-
|
| 185 |
-
6. **Three-way routing**: `chat` / `unstructured` / `structured`. The router commits to one path. Cross-source questions ("compare DB sales vs uploaded customer file") are handled inside the structured path because the planner sees Cs ∪ Ct in one prompt. **DB vs tabular is not a routing concern** — it's a per-source attribute (`source_type`) that only matters at execution time.
|
| 186 |
-
|
| 187 |
-
7. **Stable IDs.** `source_id`, `table_id`, `column_id` are stable internal references. Renaming a column in the source DB does not invalidate cached IRs.
|
| 188 |
-
|
| 189 |
-
8. **PII suppression at the boundary.** Columns flagged with `pii_flag=true` have `sample_values: null` — real PII never enters LLM prompts. Auto-detected at ingestion via name patterns + value regex (`security/pii_patterns.py`). When in doubt, flag — false positives cost nothing; false negatives leak data.
|
| 190 |
-
|
| 191 |
-
---
|
| 192 |
-
|
| 193 |
-
## End-to-end flows
|
| 194 |
-
|
| 195 |
-
### Ingestion (when user uploads a file or connects a DB)
|
| 196 |
-
|
| 197 |
-
```
|
| 198 |
-
source upload / DB connect
|
| 199 |
-
│
|
| 200 |
-
├── unstructured (pdf/docx/txt)
|
| 201 |
-
│ → DocumentPipeline: extract → chunk → embed → PGVector
|
| 202 |
-
│
|
| 203 |
-
└── structured (DB schema or tabular file)
|
| 204 |
-
→ introspect (information_schema or file headers + sample rows)
|
| 205 |
-
→ CatalogValidator (Pydantic + unique-IDs + FK refs)
|
| 206 |
-
→ CatalogStore.upsert(user_id jsonb row in `data_catalog`)
|
| 207 |
-
```
|
| 208 |
-
|
| 209 |
-
### Query (per user message)
|
| 210 |
-
|
| 211 |
-
```
|
| 212 |
-
user message
|
| 213 |
-
│
|
| 214 |
-
→ Redis cache check (24h TTL) ── miss ─→ continue
|
| 215 |
-
→
|
| 216 |
-
→ IntentRouter LLM → needs_search? source_hint?
|
| 217 |
-
│
|
| 218 |
-
├── chat → ChatbotAgent → SSE stream
|
| 219 |
-
├── unstructured → DocumentRetriever (Cu) → ChatbotAgent → SSE stream
|
| 220 |
-
└── structured →
|
| 221 |
-
CatalogReader.read(user_id, "structured") # full Cs ∪ Ct
|
| 222 |
-
↓
|
| 223 |
-
QueryPlanner LLM(question, catalog) → QueryIR
|
| 224 |
-
↓
|
| 225 |
-
IRValidator.validate(ir, catalog)
|
| 226 |
-
(source_id ∈ catalog, table_id ∈ source, column_ids ∈ table,
|
| 227 |
-
ops/aggs whitelisted, value_type matches data_type, limit ≤ 10000)
|
| 228 |
-
fail → re-prompt planner with error context (max 3 retries)
|
| 229 |
-
↓
|
| 230 |
-
ExecutorDispatcher.pick(ir) # by source.source_type
|
| 231 |
-
├─ DbExecutor → SqlCompiler → sqlglot guard → asyncpg/pymysql
|
| 232 |
-
│ (read-only txn, 30s timeout)
|
| 233 |
-
└─ TabularExecutor → PandasCompiler → eager pandas (≤100 MB)
|
| 234 |
-
or pyarrow pushdown (100 MB–1 GB)
|
| 235 |
-
or polars lazy scan (>1 GB)
|
| 236 |
-
↓
|
| 237 |
-
QueryResult
|
| 238 |
-
↓
|
| 239 |
-
ChatbotAgent → SSE stream
|
| 240 |
-
```
|
| 241 |
-
|
| 242 |
-
---
|
| 243 |
-
|
| 244 |
-
## Catalog schema (per-user `jsonb` row)
|
| 245 |
-
|
| 246 |
-
```
|
| 247 |
-
Catalog
|
| 248 |
-
├── user_id, schema_version, generated_at
|
| 249 |
-
└── sources[]
|
| 250 |
-
└── Source { source_id, source_type, name, description, location_ref, updated_at }
|
| 251 |
-
└── tables[]
|
| 252 |
-
└── Table { table_id, name, description, row_count, foreign_keys[] }
|
| 253 |
-
├── columns[]
|
| 254 |
-
│ └── Column { column_id, name, data_type, description,
|
| 255 |
-
│ nullable, pii_flag, sample_values[]|null, stats|null }
|
| 256 |
-
└── foreign_keys[]
|
| 257 |
-
└── ForeignKey { column_id, target_table_id, target_column_id }
|
| 258 |
-
```
|
| 259 |
-
|
| 260 |
-
`source_type ∈ {schema, tabular, unstructured}`.
|
| 261 |
-
`data_type ∈ {int, decimal, string, datetime, date, bool, json}`.
|
| 262 |
-
`ForeignKey` references are within the SAME `Source` only; cross-source FKs are not modeled.
|
| 263 |
-
|
| 264 |
-
Deferred Column fields (add when justified): `description_human`, `synonyms[]`, `tags[]`, `primary_key`, `unit`, `semantic_type`, `example_questions[]`, `schema_hash`, `enrichment_status`.
|
| 265 |
-
|
| 266 |
-
---
|
| 267 |
-
|
| 268 |
-
## JSON IR schema
|
| 269 |
-
|
| 270 |
-
```jsonc
|
| 271 |
-
{
|
| 272 |
-
"ir_version": "1.0",
|
| 273 |
-
"source_id": "...",
|
| 274 |
-
"table_id": "...",
|
| 275 |
-
"select": [
|
| 276 |
-
{"kind": "column", "column_id": "...", "alias": "..."},
|
| 277 |
-
{"kind": "agg", "fn": "count|count_distinct|sum|avg|min|max",
|
| 278 |
-
"column_id": "...?", "alias": "..."}
|
| 279 |
-
],
|
| 280 |
-
"filters": [
|
| 281 |
-
{"column_id": "...",
|
| 282 |
-
"op": "= | != | < | <= | > | >= | in | not_in | is_null | is_not_null | like | between",
|
| 283 |
-
"value": ...,
|
| 284 |
-
"value_type": "int|decimal|string|datetime|date|bool"}
|
| 285 |
-
],
|
| 286 |
-
"group_by": ["column_id", ...],
|
| 287 |
-
"order_by": [{"column_id": "...", "dir": "asc|desc"}],
|
| 288 |
-
"limit": 100
|
| 289 |
-
}
|
| 290 |
-
```
|
| 291 |
-
|
| 292 |
-
Single-table only in v1. `having`, `offset`, boolean filter trees, `distinct`, joins, window functions are deferred until user demand proves the limitation.
|
| 293 |
-
|
| 294 |
-
---
|
| 295 |
-
|
| 296 |
-
## Implementation status
|
| 297 |
-
|
| 298 |
-
**As of 2026-05-12 — Phase 2 end-to-end flow is wired.** `PROGRESS.md` has the per-PR line-item table; this section is the high-level snapshot. Stub files (`raise NotImplementedError`) are now the exception, not the rule.
|
| 299 |
-
|
| 300 |
-
| Area | Status | Notes |
|
| 301 |
-
|---|---|---|
|
| 302 |
-
| Catalog Pydantic models | ✅ | `catalog/models.py` — incl. `ForeignKey`, `ColumnStats.top_values` |
|
| 303 |
-
| JSON IR Pydantic models | ✅ | `query/ir/models.py` + `operators.py` (TYPE_COMPATIBILITY filled) |
|
| 304 |
-
| Catalog ingestion — DB | ✅ | introspect → validate → upsert. `on_db_registered` wired; `/api/v1/db-clients/{id}/ingest` calls it |
|
| 305 |
-
| Catalog ingestion — tabular | ✅ | CSV/XLSX/Parquet; `on_tabular_uploaded` wired into `/api/v1/document/process`. XLSX → one Table per sheet. CSV/XLSX skip vector store |
|
| 306 |
-
| Catalog ingestion — unstructured | ✅ | `on_document_uploaded` implemented; full DocumentPipeline (extract → chunk → embed → PGVector) |
|
| 307 |
-
| Catalog store / reader / validator / PII detector | ✅ | `data_catalog` jsonb table (renamed from `catalogs` in KM-557) |
|
| 308 |
-
| LLM enrichment | ❌ removed (KM-557) | Cost cut — planner reads `column.stats` + `sample_values` + `top_values` + `column.name` directly. `catalog/render.py` keeps the source-rendering helper |
|
| 309 |
-
| `IntentRouter` (lives as `OrchestratorAgent` in `agents/orchestration.py`) | ✅ | 3-way `source_hint`, history-aware query rewriting. Filename + class name kept from Phase 1; Phase 2 body |
|
| 310 |
-
| `CatalogReader` | ✅ | Loads full catalog; filters by `source_hint` |
|
| 311 |
-
| `QueryPlanner` LLM call | ✅ | Azure OpenAI structured output → `QueryIR`; supports retry with `previous_error` |
|
| 312 |
-
| IR validator | ✅ | Catalog-aware; full rule set; descriptive errors |
|
| 313 |
-
| SQL compiler (Postgres) | ✅ | All 12 filter ops, all 6 aggs, alias-aware order_by, parameterized values, quoted identifiers |
|
| 314 |
-
| DbExecutor | ✅ | sqlglot SELECT-only guard, RO txn, `statement_timeout=30000`, 10k row cap, never raises |
|
| 315 |
-
| Pandas compiler | ✅ | Same op coverage as SQL; pure module-level helpers |
|
| 316 |
-
| TabularExecutor | ✅ | Parquet blob path resolution, `asyncio.to_thread`, 10k cap, never raises |
|
| 317 |
-
| ExecutorDispatcher | ✅ | Routes by `source.source_type`; lazy imports + cache |
|
| 318 |
-
| QueryService | ✅ | plan → validate → retry-on-fail (max 3) → dispatch → execute → `QueryResult` |
|
| 319 |
-
| `ChatbotAgent` + prompt + guardrails | ✅ | Renamed from `AnswerAgent` in Cleanup PR. Guardrails appended to `chatbot_system.md` |
|
| 320 |
-
| `ChatHandler` (top-level chat orchestrator) | ✅ | SSE events: `intent` / `chunk` / `done` / `error` |
|
| 321 |
-
| `DocumentRetriever` + `RetrievalRouter` (Redis-cached) | ✅ | Migrated from `src/rag/` (now deleted). Mentor commit `61c746f` rewrote to raw SQL (pgvector `<=>` cosine, `<+>` manhattan) to dodge asyncpg type-mapping issues with Go-ingested schema. Methods reduced to `cosine | manhattan`. Collection: `documents`. |
|
| 322 |
-
| `/api/v1/chat/stream` | ✅ | Rewired to `ChatHandler`; Redis cache + fast intent + history + message persistence remain in chat.py |
|
| 323 |
-
| `/api/v1/db-clients/{id}/ingest` | ✅ | Calls only `on_db_registered`; Phase 1 dual-write removed |
|
| 324 |
-
| `/api/v1/document/{upload,process,delete}` | ✅ | `/process` triggers `on_tabular_uploaded` for CSV/XLSX |
|
| 325 |
-
| `GET /api/v1/data-catalog/{user_id}` | ✅ | Index endpoint (KM-557) |
|
| 326 |
-
| `POST /api/v1/data-catalog/rebuild` | ✅ | Iterates sources, re-runs per-source trigger |
|
| 327 |
-
| Credential encryption | ⚠️ stub | `security/credentials.py` not migrated; runtime reuses Phase 1 `utils/db_credential_encryption.py` |
|
| 328 |
-
| Tests | ✅ 146+ unit | Compilers (DB 36, Pandas 43), validators, introspectors, agents, chat handler, dispatcher, planner |
|
| 329 |
-
| Planner eval harness | 🟡 scaffold | 3 DB + 4 tabular golden cases. Gated on `RUN_PLANNER_EVAL=1`. Real Azure OpenAI passing |
|
| 330 |
-
| E2E smoke tests | ❌ not started | Component-level orchestration is covered |
|
| 331 |
-
| DB introspector unit test | ❌ deferred | Needs Postgres testcontainer |
|
| 332 |
-
| Sources event in `/chat/stream` | ⚠️ emits `[]` | `ChatHandler` doesn't surface retrieval sources yet; same gap reflected in `save_messages` |
|
| 333 |
-
|
| 334 |
-
**Deferred to later phases**: joins in IR, schema drift detection, hybrid catalog search (BM25 + RRF for 100+ table users), polars lazy scan for >1GB tabular files, MySQL/BigQuery/Snowflake SQL dialects, mask/synthesize PII strategies.
|
| 335 |
-
|
| 336 |
-
---
|
| 337 |
-
|
| 338 |
-
## Team — division of work
|
| 339 |
-
|
| 340 |
-
The service is built by two engineers; many modules are source-type-agnostic and shared.
|
| 341 |
-
|
| 342 |
-
- **DB** owns SQL paths: introspection, SQL compiler, DB executor, credential storage.
|
| 343 |
-
- **TAB** owns tabular paths: CSV/XLSX/Parquet introspection, pandas compiler, tabular executor, blob/Parquet plumbing.
|
| 344 |
-
- **B** = both — shared contracts and source-type-agnostic plumbing. Pair-program or split with explicit hand-off.
|
| 345 |
-
|
| 346 |
-
### Step-by-step ownership
|
| 347 |
-
|
| 348 |
-
| # | Step | File / area | Owner | Notes |
|
| 349 |
-
|---|---|---|---|---|
|
| 350 |
-
| 0 | **Lock contracts before coding** | — | B | See "Decisions to lock" below; block until aligned |
|
| 351 |
-
| 1 | Catalog Pydantic models | `catalog/models.py` | B | Already done; only touch if both agree |
|
| 352 |
-
| 2 | IR Pydantic models | `query/ir/models.py` | B | Already done; joins/window fns require joint sign-off |
|
| 353 |
-
| 3 | IR operator whitelists | `query/ir/operators.py` | B | Already done; both compilers rely on these |
|
| 354 |
-
| 4 | PII patterns / regex | `security/pii_patterns.py` | B | Already done; extend together as gaps appear |
|
| 355 |
-
| **Ingestion — introspection** | | | | |
|
| 356 |
-
| 5 | DB introspector (information_schema, sample, FKs) | `catalog/introspect/database.py` | DB | Use SQLAlchemy `inspect()`; dialect-aware quoting |
|
| 357 |
-
| 6 | Tabular introspector (CSV/XLSX/Parquet headers + sample) | `catalog/introspect/tabular.py` | TAB | Each XLSX sheet → one Table |
|
| 358 |
-
| 7 | `BaseIntrospector` ABC | `catalog/introspect/base.py` | B | Confirm signature returns the same `Source` shape |
|
| 359 |
-
| **Ingestion — shared catalog plumbing** | | | | |
|
| 360 |
-
| 8 | ~~Catalog enricher + prompt~~ | — | **REMOVED in KM-557.** Cost optimization — planner reads stats + sample rows directly. `catalog/render.py` keeps the source-rendering helper. |
|
| 361 |
-
| 9 | Catalog validator | `catalog/validator.py` | B | Type-agnostic |
|
| 362 |
-
| 10 | Catalog store (Postgres jsonb) | `catalog/store.py` | B | Recommend DB (Postgres expertise) |
|
| 363 |
-
| 11 | Catalog reader | `catalog/reader.py` | B | Type-agnostic |
|
| 364 |
-
| 12 | PII detector | `catalog/pii_detector.py` | B | Either; uses `pii_patterns.py` |
|
| 365 |
-
| **Ingestion — pipelines** | | | | |
|
| 366 |
-
| 13 | Structured pipeline (introspect → enrich → validate → store) | `pipeline/structured_pipeline.py` | B | Pair on this — calls both introspectors via dispatcher |
|
| 367 |
-
| 14 | Triggers (`on_db_registered`, `on_tabular_uploaded`) | `pipeline/triggers.py` | B | Each owns their trigger function |
|
| 368 |
-
| 15 | Ingestion orchestrator | `pipeline/orchestrator.py` | B | Routes by source_type; pair |
|
| 369 |
-
| 16 | Document pipeline (PDF/DOCX/TXT) | `pipeline/document_pipeline.py` | TAB | Tabular-adjacent (file uploads) |
|
| 370 |
-
| **Query — shared spine** | | | | |
|
| 371 |
-
| 17 | IR validator (catalog-aware) | `query/ir/validator.py` | B | Recommend DB; both must agree on exact error messages so retry-prompt is consistent |
|
| 372 |
-
| 18 | Planner LLM service | `query/planner/service.py` | B | Type-agnostic |
|
| 373 |
-
| 19 | Planner prompt (catalog → text) | `query/planner/prompt.py`, `config/prompts/query_planner.md` | B | **Pair-program**. Must describe DB tables and tabular files in one consistent format |
|
| 374 |
-
| 20 | Intent router (chat/unstructured/structured) | `agents/orchestration.py` (class `OrchestratorAgent` — Phase 1 filename + class name preserved; Phase 2 body), `config/prompts/intent_router.md` | B | Type-agnostic. The prompt file uses `intent_router.md`, but the source module is still `orchestration.py` |
|
| 375 |
-
| 21 | Executor base + `QueryResult` | `query/executor/base.py` | B | Lock the shape before either implements an executor |
|
| 376 |
-
| 22 | Executor dispatcher | `query/executor/dispatcher.py` | B | Reads `source.source_type` from catalog; pair |
|
| 377 |
-
| 23 | Compiler base ABC | `query/compiler/base.py` | B | Already done |
|
| 378 |
-
| 24 | Top-level QueryService | `query/service.py` | B | Wires planner → validator → compiler → executor; pair |
|
| 379 |
-
| **Query — DB path** | | | | |
|
| 380 |
-
| 25 | SQL compiler (IR → SQL + params, per dialect) | `query/compiler/sql.py` | DB | Identifiers from catalog (quoted), values parameterized |
|
| 381 |
-
| 26 | DB executor (asyncpg/pymysql, sqlglot guard, RO txn, 30s timeout) | `query/executor/db.py` | DB | |
|
| 382 |
-
| 27 | Credential encryption (Fernet) | `security/credentials.py` | DB | Needed for stored user DB creds |
|
| 383 |
-
| 28 | User-DB connection management | helper in pipelines | DB | engine_scope context manager pattern |
|
| 384 |
-
| **Query — Tabular path** | | | | |
|
| 385 |
-
| 29 | Pandas compiler (IR → callable on DataFrame) | `query/compiler/pandas.py` | TAB | Same IR, different backend |
|
| 386 |
-
| 30 | Tabular executor (eager pandas first; pyarrow / polars later) | `query/executor/tabular.py` | TAB | Initial scope: eager pandas only |
|
| 387 |
-
| 31 | Parquet upload/download + Azure Blob wrapper | `storage/az_blob/az_blob.py` (+ helper) | TAB | XLSX sheet → one Parquet per sheet (deterministic blob name) |
|
| 388 |
-
| **Agents + chat** | | | | |
|
| 389 |
-
| 32 | Chatbot agent + prompt | `agents/chatbot.py`, `config/prompts/chatbot_system.md` | B | Receives QueryResult or Cu chunks |
|
| 390 |
-
| 33 | Guardrails prompt | `config/prompts/guardrails.md` | B | |
|
| 391 |
-
| **API surface** | | | | |
|
| 392 |
-
| 34 | DB client endpoints (register/ingest/list/delete) | `api/v1/db_client.py` | DB | |
|
| 393 |
-
| 35 | Document/tabular upload endpoints | `api/v1/document.py` | TAB | |
|
| 394 |
-
| 36 | Chat stream endpoint (SSE) | `api/v1/chat.py` | B | Dispatches both paths; pair |
|
| 395 |
-
| 37 | Room / users endpoints | `api/v1/room.py`, `api/v1/users.py` | B | Whoever has bandwidth |
|
| 396 |
-
| **Tests + eval** | | | | |
|
| 397 |
-
| 38 | DB compiler golden tests (IR → SQL fixtures) | `tests/query/compiler/test_sql.py` | DB | Pure-Python, no LLM |
|
| 398 |
-
| 39 | Pandas compiler golden tests (IR → expected DataFrame) | `tests/query/compiler/test_pandas.py` | TAB | Pure-Python, no LLM |
|
| 399 |
-
| 40 | IR validator tests (catalog × IR error matrix) | `tests/query/ir/test_validator.py` | B | Each contributes test cases for their source type |
|
| 400 |
-
| 41 | Planner eval (golden question → IR examples) | `tests/query/planner/` | B | Each contributes ~10 question→IR examples |
|
| 401 |
-
| 42 | E2E smoke tests | `tests/e2e/` | B | Pair |
|
| 402 |
-
|
| 403 |
-
### Decisions to lock before coding
|
| 404 |
-
|
| 405 |
-
If made unilaterally these create silent contract drift. Lock them in a 30-min sync first.
|
| 406 |
-
|
| 407 |
-
| Decision | Why it matters | Recommended call |
|
| 408 |
-
|---|---|---|
|
| 409 |
-
| `QueryResult` shape (current scaffold: `source_id, backend, rows, row_count, truncated, elapsed_ms, error`) | Both executors return this; chatbot consumes it | Lock as-is unless either side needs more (e.g. `column_types` for formatting) |
|
| 410 |
-
| `Source.location_ref` format (`az_blob://...` vs `dbclient://{id}` etc.) | Dispatcher and executors both parse this | Pick a convention now; document in `catalog/models.py` docstring |
|
| 411 |
-
| Where do user DB credentials live? | DB executor needs creds to run queries; Source has `location_ref` but creds are encrypted separately | Recommend: `location_ref="dbclient://{client_id}"`; executor looks up creds by ID |
|
| 412 |
-
| How does dispatcher pick the executor? | Routes by `source.source_type` — but where does dispatcher get it (catalog reload, or IR carries it)? | Recommend: dispatcher takes `(Catalog, IR)`, looks up source by `IR.source_id` |
|
| 413 |
-
| Joins in v1 IR? | Excluded per ARCHITECTURE.md §7. DB path is most affected — real DB use often needs joins. | Recommend: ship single-table; revisit in PR 2. **DB owner must accept the constraint or push back early** |
|
| 414 |
-
| Planner prompt — render tabular vs DB sources uniformly | If described differently, planner gets confused | Pair-program. Render both as `Table: name (n rows) — Columns: ...` regardless of source_type |
|
| 415 |
-
| Error contract — raise or return `QueryResult.error`? | Both executors must behave the same so chatbot branches consistently | Recommend: never raise from `executor.run()`; populate `QueryResult.error` |
|
| 416 |
-
| PII handling for tabular `sample_values` | DB samples come from `information_schema`; tabular from file reads. Same `pii_flag` rule must apply both sides | Confirm tabular introspector calls `pii_detector` |
|
| 417 |
-
| Catalog refresh trigger (open question §3) | Affects both pipelines symmetrically | Default: rebuild on every upload/connect; defer auto-refresh |
|
| 418 |
-
| `updated_at` semantics — per-Source vs per-Catalog | Affects how each pipeline writes | Recommend: per-Source `updated_at` + Catalog-level `generated_at` |
|
| 419 |
-
| Dialect support scope for v1 | DB compiler must implement at least one dialect well | Recommend: Postgres first (matches app DB); MySQL second |
|
| 420 |
-
| Test-fixture format for golden IRs | Both compilers test against golden IR → expected output | Recommend: shared `tests/fixtures/golden_irs.json`; each side adds expected SQL or DataFrame |
|
| 421 |
-
| Logging conventions | structlog is already in place; both should log the same fields | Quick agreement: log `source_id`, `table_id`, `ir_version`, `elapsed_ms` |
|
| 422 |
-
|
| 423 |
-
### Working rhythm (suggested)
|
| 424 |
-
|
| 425 |
-
1. **Day 1** — 30-min sync to lock the decisions table. PR any contract/docstring changes that fall out.
|
| 426 |
-
2. **Week 1** — both build introspectors + agree on the planner prompt format. PR in parallel; review each other's.
|
| 427 |
-
3. **Week 2** — DB builds SQL compiler + DB executor; TAB builds pandas compiler + tabular executor. Both write golden tests against shared IR fixtures.
|
| 428 |
-
4. **Week 3** — pair on dispatcher, QueryService, and chat endpoint integration. End-to-end smoke test.
|
| 429 |
-
5. **Ongoing** — short daily standup, mostly to flag IR-shape questions and catalog-field additions *before* either side implements against an unconfirmed contract.
|
| 430 |
-
|
| 431 |
-
Biggest risk: **silent contract drift** — one side adds a `QueryResult` field or assumes a new IR op exists, the other ships without it, and integration breaks at the dispatcher. The §0 lock + shared golden-IR fixtures are what prevent that.
|
| 432 |
-
|
| 433 |
-
### Onboarding to Claude Code
|
| 434 |
-
|
| 435 |
-
If you're new to Claude Code, before you start:
|
| 436 |
-
|
| 437 |
-
1. Read `ARCHITECTURE.md` end-to-end (~10 min) — this is the source of truth.
|
| 438 |
-
2. Skim this file (`REPO_CONTEXT.md`) — find your section in the ownership table.
|
| 439 |
-
3. Read your owned files' docstrings — every stub explains its contract.
|
| 440 |
-
4. Open Claude Code in this repo. When you ask Claude to implement a stub:
|
| 441 |
-
- Reference the file path + the contract it should follow
|
| 442 |
-
- Point it at `ARCHITECTURE.md` section if relevant (e.g. §7 for IR validation)
|
| 443 |
-
- Ask it to write the test first (golden IR fixtures), then the implementation
|
| 444 |
-
- Always review the diff — don't auto-accept
|
| 445 |
-
|
| 446 |
-
Useful slash commands while working: `/review` (PR review), `/security-review` (audit pending changes).
|
| 447 |
-
|
| 448 |
-
---
|
| 449 |
-
|
| 450 |
-
## Conventions & gotchas
|
| 451 |
-
|
| 452 |
-
- **Async event loop on Windows**: `run.py` sets `WindowsSelectorEventLoopPolicy` because psycopg3 async needs it. Don't call `uvicorn` directly on Windows.
|
| 453 |
-
- **Two Postgres engines**: `engine` (app tables) and `_pgvector_engine` (asyncpg with `prepared_statement_cache_size=0`) — the latter is required because PGVector emits `advisory_lock + CREATE EXTENSION` as a multi-statement string and asyncpg rejects multi-statement prepared queries. `init_db.py` creates the extension explicitly so `PGVector(create_extension=False)` skips that path.
|
| 454 |
-
- **Read-only at every layer for user DBs**: IR validation + compiler whitelists + sqlglot SELECT-only check + read-only DB credentials + LIMIT enforcement + 30s timeout. Five layers; no single point of failure.
|
| 455 |
-
- **Identifiers vs values**: identifiers (table/column names) come from the catalog and are inlined as quoted identifiers — they were verified at validation time so this is safe. Values from `IR.filters` are *always* parameterized, never inlined as strings.
|
| 456 |
-
- **Credential encryption**: Fernet via `dataeyond__db__credential__key` env var; lives in `security/credentials.py`. Sensitive fields = `{"password", "service_account_json"}`.
|
| 457 |
-
- **Settings env-var aliases**: `.env` uses double-underscore names (`azureai__api_key__4o`); `Settings` exposes them as `azureai_api_key_4o` via `Field(alias=...)`. Mind both forms when adding settings.
|
| 458 |
-
- **Prompts**: `src/config/prompts/*.md` — `intent_router`, `query_planner`, `chatbot_system`, `guardrails` are all written. `chatbot_system` has `guardrails` appended so guardrails take precedence in conflict. `catalog_enricher.md` was deleted in KM-557. `config/agents/` folder deleted in Cleanup PR.
|
| 459 |
-
- **Planner prompt parsing gotcha**: `query/planner/service.py` uses `SystemMessage(content=...)` not `("system", text)`. The tuple form causes LangChain to interpret `{...}` in `query_planner.md` as f-string variables and crash on every real invocation. Don't refactor back to tuples.
|
| 460 |
-
- **Tests**: 146+ unit tests in place. Run with `uv run pytest`. Planner eval gated on `RUN_PLANNER_EVAL=1`; catalog store integration test gated on `RUN_INTEGRATION_TESTS=1`.
|
| 461 |
-
|
| 462 |
-
---
|
| 463 |
-
|
| 464 |
-
## Recommended reading order
|
| 465 |
-
|
| 466 |
-
1. `ARCHITECTURE.md` — design intent (the source of truth)
|
| 467 |
-
2. `src/catalog/models.py` + `src/query/ir/models.py` — the two data shapes everything else moves between
|
| 468 |
-
3. `src/query/ir/operators.py` + `src/security/pii_patterns.py` — the explicit whitelists / patterns
|
| 469 |
-
4. Skim every `__init__.py`-level docstring under `src/catalog/`, `src/query/`, `src/agents/`, `src/pipeline/` — each describes the contract its module enforces
|
| 470 |
-
5. `main.py` + `src/db/postgres/{connection,init_db}.py` — runtime bootstrap
|
| 471 |
-
6. `ARCHITECTURE.md §10` — five open questions that haven't been decided yet
|
| 472 |
-
|
| 473 |
-
---
|
| 474 |
-
|
| 475 |
-
## Open questions
|
| 476 |
-
|
| 477 |
-
Resolved as Phase 2 landed:
|
| 478 |
-
|
| 479 |
-
1. ✅ Catalog storage shape — Postgres `jsonb` row in `data_catalog` table, keyed by `user_id`.
|
| 480 |
-
2. ❌ Unstructured files in catalog — still not modeled; router uses `source_hint` from the LLM instead.
|
| 481 |
-
3. 🟡 Catalog refresh trigger — rebuild-on-upload-or-connect is the default. Explicit endpoint `POST /api/v1/data-catalog/rebuild` exists. Background TTL deferred.
|
| 482 |
-
4. ✅ Joins out of v1 IR — confirmed; single-table only. Revisit when real queries need it.
|
| 483 |
-
5. 🟡 PII `sample_values` — currently nulled out (skip). Mask/synthesize deferred.
|
| 484 |
-
|
| 485 |
-
---
|
| 486 |
-
|
| 487 |
-
## Glossary
|
| 488 |
-
|
| 489 |
-
- **Cu** — unstructured context (prose chunks)
|
| 490 |
-
- **Cs** — schema context (DB tables/columns from catalog)
|
| 491 |
-
- **Ct** — tabular context (file sheets/columns from catalog)
|
| 492 |
-
- **IR** — intermediate representation (the JSON query shape)
|
| 493 |
-
- **PII** — personally identifiable information
|
| 494 |
-
- **ABC** — abstract base class
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|