File size: 15,391 Bytes
3bacc1d d8e7745 3bacc1d d8e7745 3bacc1d 17e31c6 3bacc1d 17e31c6 3bacc1d 17e31c6 3bacc1d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | # Backend Agentic Service — API Endpoint Docs (endpoint restructure)
**Status:** contract draft for FE/Go integration (2026-06-30). Covers the AI-only surface after the
restructure. Sections marked **TENTATIVE** (observability) may still change — send feedback before we
lock them.
**What changed**
- **Only the chat pilot moves to `/api/v2`.** Everything else stays on `/api/v1`, regrouped under `/tools`.
- **Chat pilot (`/api/v2/chat/stream`) uses `analysis_id`, not `room_id`.**
- **Skills are grouped under `/api/v1/tools`:** `list` / `help` / `report`.
- **New:** `GET /api/v1/observability` — Responsible-AI provenance per chat answer.
- Python is **generative-AI only.** It never creates/updates an analysis, room, document, DB
client, or catalog — Go owns those. Python just receives `analysis_id`. Those v1 routers are
unwired from `main` + Swagger (not deleted).
**Open coordination questions (need a decision with Harry) — flagged inline as ⚠️:**
1. ~~**`message_id` origin** — who mints the assistant turn id used to correlate stream ↔ observability?~~ **RESOLVED (pr/6):** **Python is the sole minter.** It is not a request field; Python always mints it (server-authoritative — keeps the `/observability` correlation key out of client control, for FE-security) and returns it on the `done` event. Go/FE read it off the stream. *(The `/observability` consumer itself is future work — a later PR.)*
2. **Deterministic `/help` dispatch** — dedicated endpoint (recommended below) vs router classification.
3. **Observability storage** — single JSONB row per message (recommended) vs 3 normalized tables.
---
## 1. call_agent — `POST /api/v2/chat/stream`
The only FE→Python call in normal operation. Same as v1 except **`room_id` → `analysis_id`**, and
the `done` event now carries the assistant `message_id` for observability correlation.
**Request body** (`application/json`) — `ChatRequest`:
```json
{
"user_id": "u_1a2b3c",
"analysis_id": "an_42",
"message": "What were total sales by region last quarter?"
}
```
- `analysis_id` is the analysis-session id (replaces `room_id`). No auth header (handled by Go).
- `message_id` is **not** a request field. Python always mints the assistant turn id
(server-authoritative, for FE-security) and returns it on `done`; the FE reads it there and passes
it to `/api/v1/observability?message_id=...`. Any `message_id` a caller sends is ignored. *(The
`/observability` endpoint is future work — a later PR; §7 is a forward-looking sketch.)*
**Response:** `text/event-stream`. Events arrive in this order:
| event | data | notes |
|---|---|---|
| `sources` | JSON array of `{document_id, filename, page_label}` | structured: one per executed table; unstructured: deduped doc/page; chat/help/error: `[]`. |
| `status` | text | slow-path only — progress pings ("Planning…", "Running N steps…"). Safe to surface or ignore. |
| `chunk` | text fragment | concatenate in order to form the answer. |
| `done` | `{"message_id": "..."}` | **v2 change:** was empty; now returns the turn id for the observability lookup. |
| `error` | text | terminal error; stream stops after this. |
The internal `intent` event is consumed inside Python (gates caching) and **not** forwarded.
**Stream carries the answer text ONLY.** Planning / tool calls / sources detail are **not** in the
stream (it would slow it down) — fetch them from `/observability` (§7), called in parallel.
**Example — `structured_flow` answer** (raw SSE; blank line separates events):
```
event: sources
data: [{"document_id":"u_1a2b3c_orders","filename":"orders","page_label":null}]
event: status
data: Planning analysis…
event: status
data: Running 3 steps…
event: chunk
data: Total sales by region last quarter:
event: chunk
data: Central led at $1.21M (38%), East $0.74M, West $0.55M (down 12% QoQ).
event: done
data: {"message_id":"msg_88f1"}
```
**Example — simple chat reply** (no status pings, empty sources):
```
event: sources
data: []
event: chunk
data: I'm your AI data analyst — connect a source or ask a question to get started.
event: done
data: {"message_id":"msg_12"}
```
Behavior unchanged from v1: 1h Redis response-cache on the stateless `chat` intent only;
greeting/farewell fast-path (canned, no LLM); LLM router classifies every message into one of 5
intents (`chat · help · check · unstructured_flow · structured_flow`); messages persist on `done`.
---
## 2. list_skills — `GET /api/v1/tools/list`
Static, deterministic, safe for Go to cache. (Was `GET /api/v1/tools`.)
**Request:** none.
**Response 200** (`ListToolsResponse`):
```json
{
"count": 1,
"tools": [
{ "command": "/help", "name": "help", "type": "skill",
"description": "Show what the assistant can do and guide your next step." }
]
}
```
`CommandResponse = { command, name, type, description }`, `type ∈ {skill, analytics, data_access}`.
Catalog is `/help` only; the `/report`, `analyze_*` / `check_*` / `retrieve_*`, and retired
`/problem-statement` entries are commented out (kept for restorability), not deleted. `/report`
was retired as a slash command (KM-711) — report generation is the right-side button only.
**FE behavior:** the `/` slash menu surfaces **`/help` only**. **Report is a right-side button, not
a slash command** (it fires only when an analysis is finished — saves tokens).
---
## 3. skill: help — `POST /api/v1/tools/help`
⚠️ **Proposed dedicated endpoint** (new in v2). In v1 there was no `/help` endpoint — help was reached
only by letting the LLM router classify a chat message. A dedicated endpoint makes `/help` dispatch
**deterministic** (no risk the router mis-classifies the slash command) and gives it a clean home in
the tools group. State-aware: reads analysis state + history to guide the next step.
> Alternative if we *don't* add this endpoint: FE keeps calling `POST /chat/stream` and trusts the
> router to classify the help intent. We recommend the dedicated endpoint — decision pending (open
> question #2).
**Request body** (`application/json`):
```json
{
"user_id": "u_1a2b3c",
"analysis_id": "an_42"
}
```
**Response:** `text/event-stream` — same SSE shape as chat, with `sources: []` and no `status`
pings (help never references documents). Streams a next-step guidance reply.
```
event: sources
data: []
event: chunk
data: Your goal is set — you can start exploring now. Try a question like "average order value by month", then I can generate a report.
event: done
data: {"message_id":"msg_h7"}
```
---
## 4. skill: report — `POST /api/v1/tools/report`
The "Generate Report" button. Same as v1, moved under `/tools`. Generate, persist, and return a new
report version. Currently renders **Markdown** (FE preview); PPT/PDF/infographic export is future work
(triggered on a download button, not here).
**Query params:** `analysis_id` (required), `user_id` (required). No request body.
```
POST /api/v1/tools/report?analysis_id=an_42&user_id=u_1a2b3c
```
| status | meaning |
|---|---|
| 201 | new version generated → `AnalysisReport` body. |
| 409 | floor not met — no recorded analyses yet for this session, nothing to report. |
| 500 | generation or persistence failed. |
**201 response** (`AnalysisReport`):
```json
{
"report_id": "8f3a2b1c9d4e4f6a8b0c1d2e3f4a5b6c",
"analysis_id": "an_42",
"user_id": "u_1a2b3c",
"version": 2,
"generated_at": "2026-06-30T09:14:33.512Z",
"problem_statement": {
"objective": "Understand which regions drive revenue and why Q1 dipped.",
"business_questions": [
"Which regions contribute most to total revenue?",
"Did any region decline quarter-over-quarter?"
]
},
"record_ids": ["rec_a1", "rec_b2"],
"executive_summary": "Revenue is concentrated in the Central region (38% of total). The West was the only region to contract, down 12% QoQ — the main driver of the Q1 dip.",
"findings": [
{ "text": "Central region contributed 38% of total revenue, the largest share.",
"record_ids": ["rec_a1"], "supporting_data": null },
{ "text": "West region revenue fell 12% quarter-over-quarter.",
"record_ids": ["rec_b2"], "supporting_data": null }
],
"caveats": [
{ "text": "March data for the East region was partially missing (~6% of rows).",
"record_ids": ["rec_b2"] }
],
"open_questions": [
{ "text": "What drove the West region's QoQ decline?", "record_ids": ["rec_b2"] }
],
"data_sources": [
{ "source_id": "src_sales_db", "name": "orders", "source_type": "postgres",
"detail": { "tables": ["orders"], "row_count": 48213,
"columns": ["region", "amount", "ordered_at"] } }
],
"method_steps": [
{ "task_id": "t1", "stage": "data_understanding", "objective": "Inventory the sales source",
"status": "success", "tools_used": ["check_data"] },
{ "task_id": "t2", "stage": "modeling", "objective": "Aggregate revenue by region",
"status": "success", "tools_used": ["analyze_aggregate"] }
],
"rendered_markdown": "# Analysis Report\n\n*Generated 2026-06-30 by u_1a2b3c · 2 analyses · 1 source(s)*\n\n## Objective\nUnderstand which regions drive revenue…\n\n## Key Findings\n1. Central region contributed 38%…"
}
```
**409 response** (floor not met — the demo's most common error):
```json
{ "detail": "Not ready to generate a report — still needs at least one completed analysis." }
```
⚠️ **Precondition:** `AnalysisRecord`s persist only on the slow path, so reports require
`ENABLE_SLOW_PATH=true` on the Python deployment and ≥1 prior `structured_flow` question in the
session. With slow path off, `POST` 409s by design.
---
## 5. report versions — `GET /api/v1/tools/report/{analysis_id}` and `/{analysis_id}/{version}`
List a session's report versions (oldest-first). Returns `[ReportVersionEntry]`; `[]` if none.
```json
[
{ "report_id": "1b2c3d4e…", "version": 1, "generated_at": "2026-06-24T15:02:11Z", "record_count": 1 },
{ "report_id": "8f3a2b1c…", "version": 2, "generated_at": "2026-06-25T09:14:33Z", "record_count": 2 }
]
```
`GET /api/v1/tools/report/{analysis_id}/{version}` → one `AnalysisReport` (same shape as the POST
201 body); 404 if that version doesn't exist:
```json
{ "detail": "No report v3 for analysis 'an_42'." }
```
---
## 6. Unwired in v2 (mounted in v1, OFF in v2)
Commented out of `main` + Swagger, **files kept**. Go owns these; Python is generative-only:
`POST /analysis/create` + analysis CRUD · `room` · `db_client` · `document` · `data_catalog` ·
`users`/login. Re-mounting is a one-line `include_router` if ever needed.
---
## 7. observability — `GET /api/v1/observability` **(NEW · TENTATIVE)**
Responsible-AI provenance for **one chat answer**. Separate endpoint, called **in parallel with the
stream** — never embedded in it. The FE renders it as a collapsed dropdown the user can expand
(planning / tool calls / sources), Claude/Codex-style.
**Design (recommended):** one endpoint returns one merged object, backed by **one JSONB row per
message** written by an accumulating "scratchpad" decorator inside the chat agent and flushed on
`done`. The 3 facets (`planning` / `tool_calls` / `sources`) are **logical sections of the JSON**,
not separate tables — so the shape can evolve without a dedorch migration each time. (Storage is open
question #3.)
**Query params:** `analysis_id` (required), `message_id` (required).
```
GET /api/v1/observability?analysis_id=an_42&message_id=msg_88f1
```
**Timing:** the row is written when the turn finishes, so call this **after** the stream's `done`
event (or poll until 200). "Parallel" = a separate call the FE fires alongside the stream, not data
embedded in the stream.
**Field rules (by intent):**
- `planning` — present **only when the planner ran** (slow path); `null` otherwise.
- `tool_calls` — every tool invoked, with input + output. `[]` for pure chat / greeting / help.
- `sources` — **required for retrieve flows** (`structured_flow`, `unstructured_flow`). **Empty for
greeting / `chat` / `help`** (they don't reference documents).
- `thinking` — optional reasoning text; `null` if none.
**200 response — full `structured_flow` turn** (planner ran → all sections present):
```json
{
"analysis_id": "an_42",
"message_id": "msg_88f1",
"intent": "structured_flow",
"generated_at": "2026-06-30T03:21:09.114Z",
"planning": {
"goal_restated": "Find which regions drive revenue and why Q1 dipped.",
"assumptions": ["'last quarter' = Q1 2026"],
"steps": [
{ "step": 1, "stage": "data_understanding", "objective": "Inventory the sales source" },
{ "step": 2, "stage": "modeling", "objective": "Aggregate revenue by region" }
]
},
"thinking": "The question needs a per-region breakdown plus a cause, so I inventory the source, aggregate revenue by region, then compare quarters.",
"tool_calls": [
{
"order": 1,
"name": "check_data",
"input": { "source_hint": "structured" },
"output": { "kind": "table", "summary": "1 source · 1 table (orders) · 48,213 rows" },
"status": "success"
},
{
"order": 2,
"name": "retrieve_data",
"input": { "source_id": "src_sales_db", "table_id": "orders",
"select": ["region", "amount"], "group_by": ["region"] },
"output": { "kind": "table", "columns": ["region", "total"], "row_count": 4,
"preview": [["Central", 1210000], ["East", 740000]] },
"status": "success"
}
],
"sources": [
{
"type": "database",
"source_id": "src_sales_db",
"name": "orders",
"query": "SELECT region, SUM(amount) AS total FROM orders GROUP BY region",
"detail": { "tables": ["orders"], "row_count": 48213 }
}
]
}
```
**200 response — `unstructured_flow` turn** (no planner; source = document, with the retrieval query):
```json
{
"analysis_id": "an_42",
"message_id": "msg_55",
"intent": "unstructured_flow",
"generated_at": "2026-06-30T03:40:02.001Z",
"planning": null,
"thinking": null,
"tool_calls": [
{ "order": 1, "name": "retrieve_knowledge",
"input": { "query": "technology stack used in this project", "top_k": 4 },
"output": { "kind": "documents", "row_count": 4 }, "status": "success" }
],
"sources": [
{ "type": "document", "document_id": "doc_7", "filename": "tech_handbook.pdf",
"page_label": "12", "query": "technology stack used in this project",
"snippet": "The backend is built on FastAPI with async SQLAlchemy…", "score": 0.83 }
]
}
```
**200 response — simple `chat` / greeting turn** (nothing to trace):
```json
{
"analysis_id": "an_42",
"message_id": "msg_12",
"intent": "chat",
"generated_at": "2026-06-30T03:05:00.000Z",
"planning": null,
"thinking": null,
"tool_calls": [],
"sources": []
}
```
**404** — no provenance for that message yet (turn still running or unknown id):
```json
{ "detail": "No observability for message 'msg_88f1' yet." }
```
> ⚠️ **Richness is path-dependent.** Full `planning` + tool I/O exist only when
> `ENABLE_SLOW_PATH=true`. Fast chat / single-query / help still record `sources` + the single
> tool call but have `planning: null`. This matches the rule "planning only when the planner runs."
|