Rifqi Hafizuddin commited on
Commit
ade9c0c
Β·
1 Parent(s): 0721bb4

[NOTICKET] Add project repo documentation

Browse files
Files changed (1) hide show
  1. REPO_STATUS.md +303 -0
REPO_STATUS.md ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data Eyond β€” Python Agentic Service: Current Status
2
+
3
+ **Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
4
+ **Scope:** what the code does **right now** (branch `pr/3`, ticket KM-652). Describes current state only β€” no roadmap or to-dos.
5
+ **Snapshot date:** 2026-06-24.
6
+
7
+ > This file is grounded in the source, not the older design docs. Where the two
8
+ > disagree, the code wins β€” see [Β§11 Doc-vs-code](#11-where-the-older-docs-are-stale).
9
+ > `REPO_CONTEXT.md` / `ARCHITECTURE.md` are the original Phase-2 design docs and are
10
+ > stale on the router, joins, and the analysis/report stack.
11
+
12
+ ---
13
+
14
+ ## 1. The product in one paragraph
15
+
16
+ Data Eyond is an **"AI data scientist"** for business analytics, modelled on **CRISP-DM**
17
+ (Business Understanding β†’ Data Understanding β†’ Preparation β†’ Modeling β†’ Evaluation β†’
18
+ Deployment). It targets executives doing self-serve deep-dives and analysts offloading
19
+ routine work. A user defines a goal, connects data (DB or files), asks natural-language
20
+ analytical questions, and gets CRISP-DM-structured answers that can be exported as a
21
+ versioned **report**. The aim is "junior data scientist that hands back a decision-ready
22
+ deliverable," not "chatbot over a database."
23
+
24
+ ---
25
+
26
+ ## 2. Three repos, one hard ownership rule
27
+
28
+ Request flow is **FE β†’ Go β†’ Python**. The FE never calls Python directly except for chat
29
+ streaming.
30
+
31
+ | Repo | Role | We edit? |
32
+ |---|---|---|
33
+ | **Python** β€” `Agentic-Service-Data-Eyond-Catalog` (this repo) | The agentic LLM service: router, gate, skills, slow analytical path, structured query engine, unstructured RAG, report generation, analysis-session state. FastAPI + async SQLAlchemy + LangChain + Azure GPT-4o. | **Yes β€” the only repo we edit.** |
34
+ | **Go** — `Orchestrator-Agent-Service` | Gateway / data plane: interview agent, auth/JWT, rooms, documents (Azure Blob + CSV/XLSX→Parquet + embeddings), database_clients (Fernet creds), catalog ingestion, **all DB migrations**. | Reference only. |
35
+ | **FE** β€” `E2E-Frontend-Data-Eyond` | React/Vite SPA. Talks to Go for everything and to Python only for chat streaming. | Reference only. |
36
+
37
+ Shared infra: **Postgres** (app tables + `data_catalog` jsonb + PGVector `langchain_pg_embedding`), **Azure Blob**, and (Python-only) **Redis**.
38
+
39
+ ---
40
+
41
+ ## 3. Tech stack & how to run
42
+
43
+ - Python 3.12, FastAPI, uvicorn, sse-starlette
44
+ - Async SQLAlchemy 2.0 + asyncpg (Postgres); psycopg3 for the PGVector engine
45
+ - LangChain + langchain-openai (Azure OpenAI GPT-4o) + langchain-postgres (PGVector)
46
+ - Redis (response + retrieval cache), Azure Blob (uploads + Parquet)
47
+ - pandas / pyarrow, sqlglot, pydantic v2, structlog, slowapi, langfuse
48
+ - DB connectors: psycopg2, pymysql, pymssql, sqlalchemy-bigquery, snowflake-sqlalchemy
49
+
50
+ Run (Linux/Docker): `uv run --no-sync uvicorn main:app --host 0.0.0.0 --port 7860`
51
+ Run (Windows): `uv run --no-sync python run.py` (sets `WindowsSelectorEventLoopPolicy` for psycopg3 async β€” don't call uvicorn directly on Windows).
52
+
53
+ Tests live locally and are gitignored. Run with `./.venv/Scripts/python.exe -m pytest`.
54
+
55
+ ---
56
+
57
+ ## 4. Chat request lifecycle
58
+
59
+ Entry: `POST /api/v1/chat/stream` (`src/api/v1/chat.py`) β†’ `ChatHandler.handle(...)`
60
+ (`src/agents/chat_handler.py`). One shared `ChatHandler` per process keeps the Azure clients warm.
61
+
62
+ ```
63
+ POST /chat/stream { user_id, room_id, message }
64
+ β”‚ (analysis_id == room_id β€” one session = one analysis = one chat room)
65
+ β”œβ”€ Redis response-cache check (1h TTL, key chat:{room}:{user}:{message}) ── hit β†’ replay
66
+ β”œβ”€ greeting/farewell short-circuit (_fast_intent, EN+ID) ── hit β†’ canned reply
67
+ β”œβ”€ load last-10 history
68
+ └─ ChatHandler.handle:
69
+ 1. classify β†’ RouterDecision [1 GPT-4o call]
70
+ 2. ensure analysis-state row (get-or-create, idempotent)
71
+ 3. gate (if ENABLE_GATE): structured_flow + not validated β†’ problem_statement
72
+ 4. emit `intent` (internal; gates caching), then dispatch:
73
+ chat β†’ ChatbotAgent β†’ SSE
74
+ help β†’ HelpAgent (state + history + readiness) β†’ SSE
75
+ check β†’ check_data/check_knowledge tool β†’ rendered table [no LLM]
76
+ problem_statement β†’ PS draft + validate β†’ write state
77
+ unstructured_flow β†’ DocumentRetriever (PGVector RAG) β†’ ChatbotAgent β†’ SSE
78
+ structured_flow β†’ CatalogReader β†’ (slow path | QueryService) β†’ SSE
79
+ 5. SSE events: intent (internal), sources, chunk, status, done | error
80
+ ```
81
+
82
+ Only the `chat` intent is cached (stateless). Messages persist on `done`.
83
+
84
+ ---
85
+
86
+ ## 5. Report lifecycle
87
+
88
+ The report is a **dedicated API, not a chat route** (`src/api/v1/report.py`):
89
+
90
+ ```
91
+ POST /report?analysis_id&user_id
92
+ β”œβ”€ load analysis state; enforce the report FLOOR
93
+ β”‚ (validated goal + β‰₯1 substantive analyze_* success) β†’ else 409
94
+ β”œβ”€ ReportGenerator.generate (src/agents/report/generator.py):
95
+ β”‚ read persisted AnalysisRecords (list_for_analysis)
96
+ β”‚ deterministically assemble findings / caveats / open-questions /
97
+ β”‚ data-source appendix / CRISP-DM method appendix (copied verbatim)
98
+ β”‚ ONE LLM call β†’ executive summary only (deterministic fallback on failure)
99
+ β”‚ render markdown
100
+ β”œβ”€ ReportStore.save: advisory-locked version assignment β†’ dedorch `reports`
101
+ └─ write report_id back onto analysis state
102
+
103
+ GET /report/{analysis_id} β†’ list versions (oldest-first)
104
+ GET /report/{analysis_id}/{ver} β†’ fetch one version
105
+ ```
106
+
107
+ Two facts to internalise:
108
+ - **Records only exist on the slow path.** With `ENABLE_SLOW_PATH=false` (the default) no
109
+ records accumulate, so generation 409s β€” by design, not a bug.
110
+ - **dedorch `reports` stores markdown only.** Structured report fields are computed at
111
+ generation, rendered into `rendered_markdown`, and only the markdown is persisted; on
112
+ read-back the structured fields come back empty.
113
+
114
+ ---
115
+
116
+ ## 6. Feature list (what's built)
117
+
118
+ - **6-intent handler router** with history-aware query rewriting (EN/ID).
119
+ - **Deterministic gate** β€” `problem_validated` precondition for analytical queries.
120
+ - **Skills:** `help` (LLM, state-aware next-step guidance), `check` (no-LLM data/document inventory), `problem_statement` (LLM drafter + completeness validator).
121
+ - **Slow analytical path:** Planner β†’ TaskRunner β†’ Assembler (static plan, degrade-and-continue, 3 LLM calls fixed).
122
+ - **Structured query engine:** catalog-driven JSON IR β†’ deterministic SQL/pandas compiler β†’ read-only executor, with **single-level FK joins** (DB sources only).
123
+ - **Unstructured RAG** over PGVector.
124
+ - **Analytics tools:** 4 registered composite `analyze_*` (descriptive, aggregate, correlation, trend) + 4 data-access tools (check_data, check_knowledge, retrieve_data, retrieve_knowledge). Four further composites (comparison, contribution, profile, segment) exist in code but are **not registered** with the Planner.
125
+ - **Versioned report generation** from persisted records.
126
+ - **Analysis sessions:** data-first creation gate (β‰₯1 bound source), per-analysis data-source binding (#10).
127
+ - **Langfuse tracing** (PII-masked), **Redis caching**, **pooled DB engines** + speculative prewarm.
128
+
129
+ ---
130
+
131
+ ## 7. API surface (this repo, all under `/api/v1`)
132
+
133
+ | Endpoint | Purpose | Caller |
134
+ |---|---|---|
135
+ | `POST /chat/stream` | Main chat SSE (router → gate → dispatch) | FE → Go → Python (the only FE→Python call today) |
136
+ | `DELETE /chat/cache` Β· `/chat/cache/room/{id}` Β· `/retrieval/cache/{user_id}` | Cache management | internal / ops |
137
+ | `POST /analysis/create` Β· `GET /analysis` Β· `GET /analysis/{id}` | Analysis-session CRUD (state + room + bindings created atomically) | intended FE β†’ Go |
138
+ | `POST /report` Β· `GET /report/{id}` Β· `GET /report/{id}/{ver}` | Report generate / list / fetch | FE β†’ Go (report button) |
139
+ | `GET /tools` | Slash-command catalog (static, cacheable) | Go caches it for the FE "/" menu |
140
+ | `users` Β· `room` Β· `document` Β· `db_client` Β· `data_catalog` routers | Phase-1 legacy; functionally migrated to Go | mostly dormant |
141
+
142
+ ---
143
+
144
+ ## 8. Data model
145
+
146
+ SQLAlchemy models in `src/db/postgres/models.py`. Created on startup by `init_db()`
147
+ unless `SKIP_INIT_DB=true`.
148
+
149
+ | Table | Shape | Written by | Read by |
150
+ |---|---|---|---|
151
+ | `users`, `rooms`, `chat_messages`, `message_sources` | base app | chat endpoint, Go | chat history |
152
+ | `documents`, `databases` | uploads + DB creds (Fernet-encrypted) | Go ingestion | executor cred resolution |
153
+ | `data_catalog` | per-user jsonb `Catalog` (Source β†’ Table β†’ Column) | Go ingestion / Python pipeline | CatalogReader, planner, tools |
154
+ | `langchain_pg_embedding` | PGVector document chunks | Go ingestion | DocumentRetriever |
155
+ | `analysis_records` | jsonb `AnalysisRecord`, one per slow-path run | slow path | ReportGenerator, report readiness |
156
+ | `analysis` *(dedorch)* | uuid id, `owner_id`, `problem_statement`, `problem_validated`, `report_id` | `/analysis/create`, state store | gate, Help, report |
157
+ | `reports` *(dedorch)* | uuid, `title` + markdown `content` + `version` | ReportStore | report API |
158
+ | `data_sources` *(dedorch)* | per-analysis binding; `reference_id` = catalog source_id | `/analysis/create` | structured-flow scoping, report appendix |
159
+
160
+ **Catalog shape** (the jsonb in `data_catalog`):
161
+ `Catalog β†’ Source[ {source_id, source_type ∈ schema|tabular|unstructured, name, location_ref} β†’ Table[ {table_id, name, row_count, foreign_keys[]} β†’ Column[ {column_id, name, data_type, nullable, pii_flag, sample_values|null, stats} ] ] ]`. PII columns have `sample_values: null` so real values never enter prompts.
162
+
163
+ **QueryIR shape** (`src/query/ir/models.py`):
164
+ `{ source_id, table_id, joins[], select[], filters[], group_by[], order_by[], limit }`.
165
+ Joins are single-level equi-joins to a related table **in the same source**, FK-backed,
166
+ **DB sources only**.
167
+
168
+ ---
169
+
170
+ ## 9. Subsystems (where the code lives)
171
+
172
+ ### Router β€” `src/agents/orchestration.py`
173
+ One GPT-4o structured-output call β†’ `RouterDecision{intent, rewritten_query, confidence}`,
174
+ `intent ∈ {chat, help, problem_statement, check, unstructured_flow, structured_flow}`. It's a
175
+ *handler* classifier: `structured_flow` = slow path, `unstructured_flow` = fast RAG; the
176
+ data-modality mix on the slow path is the Planner's job. Prompt: `src/config/prompts/intent_router.md`.
177
+
178
+ ### Gate β€” `src/agents/gate.py`
179
+ Pure function, no I/O. One rule: `structured_flow` + `not problem_validated` β†’ redirect to
180
+ `problem_statement`; everything else passes. Fails **closed** (not-validated stub) on a missing
181
+ row / read error. `AnalysisState` is the locked contract (id, analysis_title, problem_statement,
182
+ problem_validated, owner_id, report_id, created_at, updated_at). Applied in `chat_handler.handle`
183
+ only when `ENABLE_GATE` and `analysis_id` are present.
184
+
185
+ ### Skills β€” `src/agents/handlers/`
186
+ - `help.py` β€” LLM (streamed). A consistency guard derives the *allowed* actions from state
187
+ (mirrors the gate) and feeds them to the prompt, so Help can't suggest a report when the goal
188
+ isn't validated or there's nothing to report. Consumes a deterministic readiness signal.
189
+ - `check.py` β€” **no LLM.** Keyword cues route to `check_data`, `check_knowledge`, or both
190
+ (helicopter view, concurrent). Renders tool tables to markdown.
191
+ - `problem_statement.py` β€” LLM drafts `{problem_statement, objective, metric, missing[], feedback}`;
192
+ valid iff a statement exists and `missing` is empty (a bare data question stays incomplete). On a
193
+ valid draft, writes `problem_statement` + `problem_validated=True`.
194
+
195
+ ### Slow path β€” `src/agents/slow_path/` + `src/agents/planner/`
196
+ - **Planner** (`planner/service.py`) β€” 1 LLM call β†’ `TaskList` (DAG of tool-call chains). 8-check
197
+ validator with re-prompt retry (max 3). `BusinessContext` is a **stub** (`planner/business_context.py`),
198
+ which is why the slow path stays opt-in.
199
+ - **TaskRunner** (`slow_path/task_runner.py`) β€” deterministic, 0 LLM. Wave-based execution,
200
+ `${t<id>}` placeholder resolution (Pattern A), never-throw invocation, **degrade-and-continue**
201
+ (failed task β†’ dependents skipped, independent branches run). No replanning.
202
+ - **Assembler** (`slow_path/assembler.py`) β€” 1 LLM call authoring only the narrative; code copies
203
+ the structured `results_snapshot` / `tasks_run` from the run state into the `AnalysisRecord`
204
+ (the report's source of truth).
205
+
206
+ Streaming + persistence: `chat_handler._run_slow_path` bridges per-stage progress to SSE `status`
207
+ events, prewarms the DB engine in parallel with planning, emits the answer, then persists the
208
+ record stamped with `user_id` + `analysis_id`.
209
+
210
+ ### Structured query engine β€” `src/query/`
211
+ `QueryService.run` (`query/service.py`): plan β†’ validate β†’ retry(3) β†’ dispatch β†’ execute; **never
212
+ raises** (errors land in `QueryResult.error`). `IRValidator` (`query/ir/validator.py`) checks
213
+ source/table/column existence, op/agg whitelists, type compatibility, limit cap, and **FK-backed
214
+ joins** (DB only). `DbExecutor` (`query/executor/db.py`): SqlCompiler β†’ sqlglot SELECT-only guard β†’
215
+ Fernet-decrypt creds (with owner check) β†’ `asyncio.to_thread` (30s timeout) β†’ pooled engine
216
+ (read-only + statement_timeout) β†’ 10k row cap. Defense-in-depth: IR validation + compiler whitelist
217
+ + sqlglot guard + read-only session + LIMIT/timeout.
218
+
219
+ ### Data-source binding (#10) β€” `src/agents/binding_store.py`
220
+ At `/analysis/create`, chosen `data_source_ids` become `data_sources` rows. On a `structured_flow`
221
+ turn the catalog reader is wrapped so the Planner and the tools' re-reads see the same scoped
222
+ catalog. **Fail-open**: empty/disjoint binding β†’ whole catalog.
223
+
224
+ ### Tool layer β€” `src/tools/data_access.py`, `src/agents/planner/registry.py`
225
+ `DataAccessToolInvoker` implements the never-throw tool seam for the 4 data-access tools.
226
+ `retrieve_data` runs a pre-built IR (validate β†’ dispatch β†’ execute, skipping the planner) and
227
+ coerces `Decimal`β†’`float` β€” the Pattern A handoff the `analyze_*` tools consume. The planner
228
+ registry composes a local data-access spec stub (name-checked against `DATA_ACCESS_TOOLS`) with the
229
+ real `analytics_registry()`.
230
+
231
+ ### Report β€” `src/agents/report/`
232
+ `generator.py` reads records, deterministically assembles structured fields, 1 LLM call for the
233
+ executive summary; `store.py` versions under an advisory lock and persists markdown to dedorch
234
+ `reports`; `readiness.py` defines the **report floor** (validated goal + β‰₯1 successful `analyze_*`)
235
+ shared by the report API and the Help readiness signal so the two can't disagree.
236
+
237
+ ### Observability β€” Langfuse
238
+ The endpoint's `ChatHandler` runs with `enable_tracing=True`. One trace per request groups
239
+ router/planner/assembler/chatbot + tool spans. PII policy: router/planner unmasked (PII-safe
240
+ summaries); assembler/chatbot masked (see real rows); tool spans carry name + arg keys + row counts
241
+ only.
242
+
243
+ ---
244
+
245
+ ## 10. Feature flags
246
+
247
+ | Flag | Where | Default | Effect |
248
+ |---|---|---|---|
249
+ | `ENABLE_SLOW_PATH` | `settings.enable_slow_path` | **off** | Route `structured_flow` through Planner/TaskRunner/Assembler (vs single-query `QueryService`). Records persist only on the slow path β†’ reports require this on. |
250
+ | `ENABLE_GATE` | `settings.enable_gate` | **off** | Apply the `problem_validated` redirect. Off because legacy rooms have no state row. |
251
+ | `SKIP_INIT_DB` | env, `main.py` | off | Skip `create_all` on startup β€” the dedorch cutover switch (Go owns dedorch migrations). |
252
+ | `enable_tracing` | hardcoded `True` in `chat.py` | on (endpoint) | Langfuse tracing. |
253
+
254
+ ---
255
+
256
+ ## 11. Where the older docs are stale
257
+
258
+ Trust the code. The original Phase-2 docs (`ARCHITECTURE.md`, `REPO_CONTEXT.md`) and the Go repo's
259
+ copies disagree with the current code on:
260
+
261
+ | Topic | Old docs | Current code |
262
+ |---|---|---|
263
+ | Router | 3-way `source_hint` (chat/unstructured/structured) | Flat **6-intent** `RouterDecision` |
264
+ | Joins in IR | "single-table only; deferred" | **Single-level FK-backed joins** (DB sources only) |
265
+ | Analysis / report / gate / slow path | "Phase 2 spine only" | All built and present |
266
+ | `analysis_id` | open question | resolved: **`analysis_id == room_id`** |
267
+ | Report source | (newer invariant) "from records, never chat history" | confirmed: generator reads `AnalysisRecord`s |
268
+
269
+ ---
270
+
271
+ ## 12. dedorch migration β€” current state
272
+
273
+ The Python DB is moving from `dataeyond` β†’ **dedorch** (Go owns dedorch migrations; Python is
274
+ consumer-only). Current state:
275
+
276
+ - Base tables already match dedorch.
277
+ - The analysis-family models have been **renamed to dedorch** on `pr/3`: `analysis` (was
278
+ `analysis_states`, uuid ids), `data_sources` (was `analysis_data_sources`), `reports` (was
279
+ `analysis_reports`, flattened to title + markdown content + version).
280
+ - `analysis_records` (the slow-path structured output) has **no dedorch home** β€” it remains a
281
+ Python-owned jsonb table.
282
+ - The connection-string cutover (paired with `SKIP_INIT_DB`) is a coordinated step that has not
283
+ happened yet; Python still creates tables on startup until then.
284
+
285
+ The dedorch migrations themselves live outside the three checked-out repos (Harry owns them), so the
286
+ dedorch table shapes are asserted by the Python model docstrings, not visible in the Go repo here.
287
+
288
+ ---
289
+
290
+ ## 13. Conventions & gotchas
291
+
292
+ - **Two Postgres engines:** app engine + a separate PGVector engine (`prepared_statement_cache_size=0`)
293
+ because PGVector emits multi-statement strings asyncpg rejects.
294
+ - **Identifiers vs values:** identifiers come from the catalog and are inlined as quoted; filter
295
+ values are always parameterized.
296
+ - **Settings aliases:** `.env` uses double-underscore names (`azureai__api_key__4o`); `Settings`
297
+ exposes them as `azureai_api_key_4o`.
298
+ - **Never-throw seams** are pervasive (tool invoker, query service, executors, state/binding reads,
299
+ record persistence, report summary). Failures degrade into soft output rather than raising β€” good
300
+ for UX, but they can mask real breakage (e.g. a binding silently fail-opening to the full catalog).
301
+ - **Prompts** live in `src/config/prompts/*.md`. `chatbot_system.md` has `guardrails.md` appended so
302
+ guardrails win on conflict.
303
+ - **Tests** are gitignored (team decision) β€” run them locally.