harryagasi commited on
Commit
e2da2f6
·
1 Parent(s): eb3bd3c

fix: update traceability endpoint, remove retry logic, and fix report stale version

Browse files

- Update GET traceability path from /api/v1/observability to /api/v1/traceability
- Remove 3x retry logic on 404: contract guarantees row is written before done event
- Update 404 error message to "Traceability not available for this answer"
- Remove unused wait helper
- Use message_id from API response (savedUi.messageId) instead of doneMessageId
from agentic stream, now that Golang backend returns message_id in POST/GET responses
- Fix ReportSidebar stale selectedVersion: reset versions and selectedVersion
synchronously on analysis change to prevent cross-analysis 404 report requests
- Update API_CONTRACT_BE_PYTHON.md to reflect endpoint and behavior changes

docs/API_CONTRACT_BE_PYTHON.md CHANGED
@@ -1,6 +1,6 @@
1
  # Backend Agentic Service API Contract
2
 
3
- This document describes the Python agentic backend used by the frontend for AI chat, help/report tools, and observability data shown alongside chat answers.
4
 
5
  Base path examples use relative URLs. Configure the frontend with the deployed Python service base URL.
6
 
@@ -11,14 +11,14 @@ The Python backend owns the generative AI interaction surface:
11
  1. Stream chat answers from the AI agent.
12
  2. Execute tool-style actions for help and report generation.
13
  3. Return report versions and report details.
14
- 4. Return observability/provenance for a completed assistant answer.
15
 
16
  The frontend uses this service during the analysis conversation flow:
17
 
18
  1. User sends a chat message.
19
  2. Frontend calls `POST /api/v2/chat/stream` and renders the streamed answer.
20
  3. When the stream emits `done`, frontend uses the returned `message_id` as the assistant answer correlation id.
21
- 4. Frontend calls `GET /api/v1/observability` for planning, tool calls, and source provenance.
22
  5. Frontend calls `/api/v1/tools/help` for guided help and `/api/v1/tools/report` for report generation.
23
 
24
  ## Endpoint Summary
@@ -31,7 +31,7 @@ The frontend uses this service during the analysis conversation flow:
31
  | `POST` | `/api/v1/tools/report` | Generate and persist a new report version. |
32
  | `GET` | `/api/v1/tools/report/{analysis_id}` | List report versions for an analysis. |
33
  | `GET` | `/api/v1/tools/report/{analysis_id}/{version}` | Retrieve one report version. |
34
- | `GET` | `/api/v1/observability` | Retrieve provenance for one assistant answer. |
35
 
36
  ## Common Concepts
37
 
@@ -39,7 +39,7 @@ The frontend uses this service during the analysis conversation flow:
39
 
40
  - `user_id`: user identifier passed by the frontend.
41
  - `analysis_id`: analysis conversation identifier.
42
- - `message_id`: assistant answer identifier generated by Python and returned in the stream `done` event; used to correlate chat streaming, Golang message persistence, and observability.
43
 
44
  ### Server-Sent Events
45
 
@@ -57,7 +57,7 @@ Common event types:
57
  | `done` | JSON object | Terminal success event. Includes `message_id`. |
58
  | `error` | text | Terminal error event. Stream stops after this. |
59
 
60
- The stream carries answer text only. Planning, tool call details, and full provenance are fetched from `GET /api/v1/observability` after the stream is done.
61
 
62
  ## Chat
63
 
@@ -355,13 +355,13 @@ Response `404`:
355
  }
356
  ```
357
 
358
- ## Observability
359
 
360
- ### `GET /api/v1/observability`
361
 
362
- Returns Responsible AI provenance for one assistant answer.
363
 
364
- The frontend should call this after the chat/help stream emits `done`, using the Python-generated `message_id` from the `done` event. If the row is not ready yet, the frontend may poll until `200` or stop on `404` according to product behavior.
365
 
366
  Query params:
367
 
@@ -373,15 +373,19 @@ Query params:
373
  Example:
374
 
375
  ```text
376
- GET /api/v1/observability?analysis_id=an_42&message_id=msg_88f1
377
  ```
378
 
 
 
379
  Field rules:
380
 
381
- - `planning`: present only when the planner ran; otherwise `null`.
382
- - `thinking`: optional reasoning summary; `null` if unavailable.
383
- - `tool_calls`: every invoked tool with input, output, and status; empty for pure chat or greeting paths.
384
- - `sources`: required for retrieval flows; empty for chat/help paths that do not reference data.
 
 
385
 
386
  Response `200` for `structured_flow`:
387
 
@@ -389,49 +393,58 @@ Response `200` for `structured_flow`:
389
  {
390
  "analysis_id": "an_42",
391
  "message_id": "msg_88f1",
 
392
  "intent": "structured_flow",
393
- "generated_at": "2026-06-30T03:21:09.114Z",
394
  "planning": {
395
  "goal_restated": "Find which regions drive revenue and why Q1 dipped.",
396
- "assumptions": ["'last quarter' = Q1 2026"],
397
  "steps": [
398
  {
399
  "step": 1,
400
  "stage": "data_understanding",
401
- "objective": "Inventory the sales source"
 
 
402
  },
403
  {
404
  "step": 2,
405
  "stage": "modeling",
406
- "objective": "Aggregate revenue by region"
 
 
407
  }
408
  ]
409
  },
410
- "thinking": "The question needs a per-region breakdown plus a cause, so I inventory the source, aggregate revenue by region, then compare quarters.",
411
  "tool_calls": [
412
  {
413
  "order": 1,
 
414
  "name": "check_data",
415
  "input": { "source_hint": "structured" },
416
- "output": { "kind": "table", "summary": "1 source, 1 table, 48,213 rows" },
417
- "status": "success"
 
 
 
 
 
 
418
  },
419
  {
420
  "order": 2,
 
421
  "name": "retrieve_data",
422
- "input": {
423
- "source_id": "src_sales_db",
424
- "table_id": "orders",
425
- "select": ["region", "amount"],
426
- "group_by": ["region"]
427
- },
428
  "output": {
429
  "kind": "table",
430
  "columns": ["region", "total"],
431
  "row_count": 4,
432
  "preview": [["Central", 1210000], ["East", 740000]]
433
  },
434
- "status": "success"
 
435
  }
436
  ],
437
  "sources": [
@@ -441,37 +454,36 @@ Response `200` for `structured_flow`:
441
  "name": "orders",
442
  "query": "SELECT region, SUM(amount) AS total FROM orders GROUP BY region",
443
  "detail": {
444
- "tables": ["orders"],
445
- "row_count": 48213
446
  }
447
  }
448
  ]
449
  }
450
  ```
451
 
 
 
452
  Response `200` for `unstructured_flow`:
453
 
454
  ```json
455
  {
456
  "analysis_id": "an_42",
457
  "message_id": "msg_55",
 
458
  "intent": "unstructured_flow",
459
- "generated_at": "2026-06-30T03:40:02.001Z",
460
  "planning": null,
461
  "thinking": null,
462
  "tool_calls": [
463
  {
464
  "order": 1,
 
465
  "name": "retrieve_knowledge",
466
- "input": {
467
- "query": "technology stack used in this project",
468
- "top_k": 4
469
- },
470
- "output": {
471
- "kind": "documents",
472
- "row_count": 4
473
- },
474
- "status": "success"
475
  }
476
  ],
477
  "sources": [
@@ -488,14 +500,15 @@ Response `200` for `unstructured_flow`:
488
  }
489
  ```
490
 
491
- Response `200` for simple chat or greeting:
492
 
493
  ```json
494
  {
495
  "analysis_id": "an_42",
496
  "message_id": "msg_12",
 
497
  "intent": "chat",
498
- "generated_at": "2026-06-30T03:05:00.000Z",
499
  "planning": null,
500
  "thinking": null,
501
  "tool_calls": [],
@@ -507,13 +520,13 @@ Response `404`:
507
 
508
  ```json
509
  {
510
- "detail": "No observability for message 'msg_88f1' yet."
511
  }
512
  ```
513
 
514
  Frontend rendering guidance:
515
 
516
- - Render observability separately from the streamed answer.
517
  - Default state can be collapsed.
518
  - Show planning, tool calls, and sources as separate sections.
519
- - Treat `planning: null`, `tool_calls: []`, and `sources: []` as valid states.
 
1
  # Backend Agentic Service API Contract
2
 
3
+ This document describes the Python agentic backend used by the frontend for AI chat, help/report tools, and traceability data shown alongside chat answers.
4
 
5
  Base path examples use relative URLs. Configure the frontend with the deployed Python service base URL.
6
 
 
11
  1. Stream chat answers from the AI agent.
12
  2. Execute tool-style actions for help and report generation.
13
  3. Return report versions and report details.
14
+ 4. Return traceability for a completed assistant answer.
15
 
16
  The frontend uses this service during the analysis conversation flow:
17
 
18
  1. User sends a chat message.
19
  2. Frontend calls `POST /api/v2/chat/stream` and renders the streamed answer.
20
  3. When the stream emits `done`, frontend uses the returned `message_id` as the assistant answer correlation id.
21
+ 4. Frontend calls `GET /api/v1/traceability` for planning, tool calls, and source provenance.
22
  5. Frontend calls `/api/v1/tools/help` for guided help and `/api/v1/tools/report` for report generation.
23
 
24
  ## Endpoint Summary
 
31
  | `POST` | `/api/v1/tools/report` | Generate and persist a new report version. |
32
  | `GET` | `/api/v1/tools/report/{analysis_id}` | List report versions for an analysis. |
33
  | `GET` | `/api/v1/tools/report/{analysis_id}/{version}` | Retrieve one report version. |
34
+ | `GET` | `/api/v1/traceability` | Retrieve provenance for one assistant answer. |
35
 
36
  ## Common Concepts
37
 
 
39
 
40
  - `user_id`: user identifier passed by the frontend.
41
  - `analysis_id`: analysis conversation identifier.
42
+ - `message_id`: assistant answer identifier generated by Python and returned in the stream `done` event; used to correlate chat streaming, Golang message persistence, and traceability.
43
 
44
  ### Server-Sent Events
45
 
 
57
  | `done` | JSON object | Terminal success event. Includes `message_id`. |
58
  | `error` | text | Terminal error event. Stream stops after this. |
59
 
60
+ The stream carries answer text only. Planning, tool call details, and full provenance are fetched from `GET /api/v1/traceability` after the stream is done.
61
 
62
  ## Chat
63
 
 
355
  }
356
  ```
357
 
358
+ ## Traceability
359
 
360
+ ### `GET /api/v1/traceability`
361
 
362
+ Returns user-facing provenance for one assistant answer.
363
 
364
+ The frontend should call this after the chat/help stream emits `done`, using the `message_id` from the `done` event. The row is written **before** `done`, so an immediate GET returns `200` (no polling race). A `404` means the id is unknown or the turn errored before completing (error turns never produce a row).
365
 
366
  Query params:
367
 
 
373
  Example:
374
 
375
  ```text
376
+ GET /api/v1/traceability?analysis_id=an_42&message_id=msg_88f1
377
  ```
378
 
379
+ `intent` values the frontend may see: `chat` · `help` · `check` · `unstructured_flow` · `structured_flow` · `out_of_scope` · `blocked` (`blocked` = input-guard or Azure content-filter refusal; `chat` also covers the greeting fast-path and cache replays).
380
+
381
  Field rules:
382
 
383
+ - `planning`: present only when the planner ran (`structured_flow`); otherwise `null`.
384
+ - `thinking`: **always `null` in v1** — our agents are plain chat completions with no native reasoning output, and synthesizing it post-hoc would be unfaithful. The field stays in the payload so it can be populated later without a contract change.
385
+ - `tool_calls`: every invoked tool with `input`, `output`, `status`, `task_id` (nullable), and `error` (nullable); empty for chat / help / greeting / refusal paths.
386
+ - `sources`: required for retrieval flows; empty for chat / help / refusal paths and for `check`.
387
+ - The payload also carries an internal `user_id` (ownership); the frontend may ignore it.
388
+ - Truncation: `preview` ≤ 5 rows; any string inside `input`/`output`/`preview`/`snippet` ≤ 300 chars; rows beyond the preview are dropped (`row_count` is preserved).
389
 
390
  Response `200` for `structured_flow`:
391
 
 
393
  {
394
  "analysis_id": "an_42",
395
  "message_id": "msg_88f1",
396
+ "user_id": "user_7",
397
  "intent": "structured_flow",
398
+ "generated_at": "2026-07-06T03:21:09.114Z",
399
  "planning": {
400
  "goal_restated": "Find which regions drive revenue and why Q1 dipped.",
401
+ "assumptions": [],
402
  "steps": [
403
  {
404
  "step": 1,
405
  "stage": "data_understanding",
406
+ "objective": "Inventory the sales source",
407
+ "status": "success",
408
+ "tools_used": ["check_data"]
409
  },
410
  {
411
  "step": 2,
412
  "stage": "modeling",
413
+ "objective": "Aggregate revenue by region",
414
+ "status": "success",
415
+ "tools_used": ["retrieve_data", "analyze_aggregate"]
416
  }
417
  ]
418
  },
419
+ "thinking": null,
420
  "tool_calls": [
421
  {
422
  "order": 1,
423
+ "task_id": null,
424
  "name": "check_data",
425
  "input": { "source_hint": "structured" },
426
+ "output": {
427
+ "kind": "table",
428
+ "columns": ["source_id", "name", "source_type", "table_count"],
429
+ "row_count": 1,
430
+ "preview": [["src_sales_db", "orders", "schema", 1]]
431
+ },
432
+ "status": "success",
433
+ "error": null
434
  },
435
  {
436
  "order": 2,
437
+ "task_id": null,
438
  "name": "retrieve_data",
439
+ "input": { "ir": { "source_id": "src_sales_db", "table_id": "orders", "select": ["region", "amount"], "group_by": ["region"] } },
 
 
 
 
 
440
  "output": {
441
  "kind": "table",
442
  "columns": ["region", "total"],
443
  "row_count": 4,
444
  "preview": [["Central", 1210000], ["East", 740000]]
445
  },
446
+ "status": "success",
447
+ "error": null
448
  }
449
  ],
450
  "sources": [
 
454
  "name": "orders",
455
  "query": "SELECT region, SUM(amount) AS total FROM orders GROUP BY region",
456
  "detail": {
457
+ "table": "orders",
458
+ "row_count": 4
459
  }
460
  }
461
  ]
462
  }
463
  ```
464
 
465
+ > Note: `retrieve_data`'s real `input` is the compiled query IR under an `ir` key (the planner builds an IR, never raw SQL). The executed SQL/rendered query appears on the corresponding `sources[].query`.
466
+
467
  Response `200` for `unstructured_flow`:
468
 
469
  ```json
470
  {
471
  "analysis_id": "an_42",
472
  "message_id": "msg_55",
473
+ "user_id": "user_7",
474
  "intent": "unstructured_flow",
475
+ "generated_at": "2026-07-06T03:40:02.001Z",
476
  "planning": null,
477
  "thinking": null,
478
  "tool_calls": [
479
  {
480
  "order": 1,
481
+ "task_id": null,
482
  "name": "retrieve_knowledge",
483
+ "input": { "query": "technology stack used in this project" },
484
+ "output": { "kind": "documents", "row_count": 4 },
485
+ "status": "success",
486
+ "error": null
 
 
 
 
 
487
  }
488
  ],
489
  "sources": [
 
500
  }
501
  ```
502
 
503
+ Response `200` for chat / greeting / help / refusals (`out_of_scope`, `blocked`):
504
 
505
  ```json
506
  {
507
  "analysis_id": "an_42",
508
  "message_id": "msg_12",
509
+ "user_id": "user_7",
510
  "intent": "chat",
511
+ "generated_at": "2026-07-06T03:05:00.000Z",
512
  "planning": null,
513
  "thinking": null,
514
  "tool_calls": [],
 
520
 
521
  ```json
522
  {
523
+ "detail": "No traceability for message 'msg_88f1' yet."
524
  }
525
  ```
526
 
527
  Frontend rendering guidance:
528
 
529
+ - Render traceability separately from the streamed answer.
530
  - Default state can be collapsed.
531
  - Show planning, tool calls, and sources as separate sections.
532
+ - Treat `planning: null`, `tool_calls: []`, and `sources: []` as valid states.
src/app/components/analysis/AnalysisShell.tsx CHANGED
@@ -39,7 +39,6 @@ function toUiMessage(message: AnalysisMessage): UiMessage {
39
  };
40
  }
41
 
42
- const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
43
 
44
  function normalizeAssistantContent(content: string) {
45
  return content
@@ -132,25 +131,17 @@ export function AnalysisShell() {
132
  if (!messageId) return;
133
  setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: true, traceabilityError: null } : msg)));
134
 
135
- for (let attempt = 0; attempt < 3; attempt += 1) {
136
- try {
137
- const traceability = await getObservability(analysisId, messageId);
138
- setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceability, traceabilityLoading: false } : msg)));
139
- return;
140
- } catch (err) {
141
- if (err instanceof AgenticApiError && err.status === 404 && attempt < 2) {
142
- await wait(900);
143
- continue;
144
- }
145
- const traceabilityError =
146
- err instanceof AgenticApiError && err.status === 404
147
- ? "Traceability is still being prepared. Try again after the next answer."
148
- : err instanceof Error
149
- ? err.message
150
- : "Failed to load traceability";
151
- setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: false, traceabilityError } : msg)));
152
- return;
153
- }
154
  }
155
  };
156
  useEffect(() => {
 
39
  };
40
  }
41
 
 
42
 
43
  function normalizeAssistantContent(content: string) {
44
  return content
 
131
  if (!messageId) return;
132
  setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: true, traceabilityError: null } : msg)));
133
 
134
+ try {
135
+ const traceability = await getObservability(analysisId, messageId);
136
+ setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceability, traceabilityLoading: false } : msg)));
137
+ } catch (err) {
138
+ const traceabilityError =
139
+ err instanceof AgenticApiError && err.status === 404
140
+ ? "Traceability not available for this answer."
141
+ : err instanceof Error
142
+ ? err.message
143
+ : "Failed to load traceability";
144
+ setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: false, traceabilityError } : msg)));
 
 
 
 
 
 
 
 
145
  }
146
  };
147
  useEffect(() => {
src/app/components/analysis/ReportSidebar.tsx CHANGED
@@ -53,11 +53,9 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
53
  useEffect(() => {
54
  setDetail(null);
55
  setPrecondition(null);
 
 
56
  if (analysis?.id) loadVersions(analysis.id, { silent: true });
57
- else {
58
- setVersions([]);
59
- setSelectedVersion(undefined);
60
- }
61
  }, [analysis?.id]);
62
 
63
  useEffect(() => {
 
53
  useEffect(() => {
54
  setDetail(null);
55
  setPrecondition(null);
56
+ setVersions([]);
57
+ setSelectedVersion(undefined);
58
  if (analysis?.id) loadVersions(analysis.id, { silent: true });
 
 
 
 
59
  }, [analysis?.id]);
60
 
61
  useEffect(() => {
src/services/agenticApi.ts CHANGED
@@ -205,7 +205,7 @@ export const listTools = (): Promise<{ count: number; tools: Tool[] }> => agenti
205
 
206
  export const getObservability = (analysisId: string, messageId: string): Promise<Observability> =>
207
  agenticJson(
208
- `/api/v1/observability?analysis_id=${encodeURIComponent(analysisId)}&message_id=${encodeURIComponent(messageId)}`
209
  );
210
 
211
  export const generateReport = (analysisId: string, userId: string): Promise<ReportDetail> =>
 
205
 
206
  export const getObservability = (analysisId: string, messageId: string): Promise<Observability> =>
207
  agenticJson(
208
+ `/api/v1/traceability?analysis_id=${encodeURIComponent(analysisId)}&message_id=${encodeURIComponent(messageId)}`
209
  );
210
 
211
  export const generateReport = (analysisId: string, userId: string): Promise<ReportDetail> =>