Rifqi Hafizuddin commited on
Commit Β·
b43ecc7
1
Parent(s): 9e248d9
[KM-691] Make sources trace-authoritative; drop stream sources
Browse filesSources move fully to GET /api/v1/traceability. The SSE sources event
now always emits [] (kept for backward-compat); removed the dead
_build_sources helper and the structured-path source TODO. Updates the
contract + v2 chat docstring accordingly.
- API_CONTRACT_BE_PYTHON.md +3 -3
- src/agents/chat_handler.py +8 -57
- src/api/v2/chat.py +3 -3
- src/traceability/scratchpad.py +2 -1
API_CONTRACT_BE_PYTHON.md
CHANGED
|
@@ -51,7 +51,7 @@ Common event types:
|
|
| 51 |
|
| 52 |
| Event | Data | Meaning |
|
| 53 |
| --- | --- | --- |
|
| 54 |
-
| `sources` | JSON array |
|
| 55 |
| `status` | text | Optional progress update for slower paths. |
|
| 56 |
| `chunk` | text | Answer text fragment. Concatenate chunks in order. |
|
| 57 |
| `done` | JSON object | Terminal success event. Includes `message_id`. |
|
|
@@ -91,7 +91,7 @@ Example structured answer:
|
|
| 91 |
|
| 92 |
```text
|
| 93 |
event: sources
|
| 94 |
-
data: [
|
| 95 |
|
| 96 |
event: status
|
| 97 |
data: Planning analysis...
|
|
@@ -127,7 +127,7 @@ Behavior notes:
|
|
| 127 |
- Greeting and farewell messages may use a fast canned path.
|
| 128 |
- Stateless `chat` intent may use a 1-hour Redis response cache.
|
| 129 |
- The router may classify messages into intents such as `chat`, `help`, `check`, `unstructured_flow`, or `structured_flow`.
|
| 130 |
-
- `sources`
|
| 131 |
- `status` events are optional and should be safe for the frontend to ignore.
|
| 132 |
|
| 133 |
## Tools
|
|
|
|
| 51 |
|
| 52 |
| Event | Data | Meaning |
|
| 53 |
| --- | --- | --- |
|
| 54 |
+
| `sources` | JSON array | Always `[]` β sources moved to `GET /api/v1/traceability` (KM-691). Event kept for backward-compat; read `sources[]` from the traceability call. |
|
| 55 |
| `status` | text | Optional progress update for slower paths. |
|
| 56 |
| `chunk` | text | Answer text fragment. Concatenate chunks in order. |
|
| 57 |
| `done` | JSON object | Terminal success event. Includes `message_id`. |
|
|
|
|
| 91 |
|
| 92 |
```text
|
| 93 |
event: sources
|
| 94 |
+
data: []
|
| 95 |
|
| 96 |
event: status
|
| 97 |
data: Planning analysis...
|
|
|
|
| 127 |
- Greeting and farewell messages may use a fast canned path.
|
| 128 |
- Stateless `chat` intent may use a 1-hour Redis response cache.
|
| 129 |
- The router may classify messages into intents such as `chat`, `help`, `check`, `unstructured_flow`, or `structured_flow`.
|
| 130 |
+
- `sources` in the stream is **always `[]`** (KM-691) β read the real `sources[]` from `GET /api/v1/traceability` after `done`.
|
| 131 |
- `status` events are optional and should be safe for the frontend to ignore.
|
| 132 |
|
| 133 |
## Tools
|
src/agents/chat_handler.py
CHANGED
|
@@ -547,14 +547,11 @@ class ChatHandler:
|
|
| 547 |
# else: chat path β no context
|
| 548 |
|
| 549 |
# ---- 2b. Emit sources ---------------------------------------
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
raw_chunks_count=len(raw_chunks) if raw_chunks else 0,
|
| 556 |
-
)
|
| 557 |
-
yield {"event": "sources", "data": json.dumps(sources)}
|
| 558 |
|
| 559 |
# ---- 3. Stream answer ----------------------------------------
|
| 560 |
# masked: the answer call sees real query rows / doc chunks (possible PII).
|
|
@@ -750,7 +747,9 @@ class ChatHandler:
|
|
| 750 |
yield {"event": "error", "data": f"Analysis failed: {e}"}
|
| 751 |
return
|
| 752 |
|
| 753 |
-
|
|
|
|
|
|
|
| 754 |
yield {"event": "chunk", "data": result.chat_answer}
|
| 755 |
try:
|
| 756 |
# Stamp identity from the request scope: owner + the shared session id
|
|
@@ -798,54 +797,6 @@ class _ScopedCatalogReader:
|
|
| 798 |
return catalog.model_copy(update={"sources": scoped or catalog.sources})
|
| 799 |
|
| 800 |
|
| 801 |
-
def _build_sources(
|
| 802 |
-
intent: str,
|
| 803 |
-
user_id: str,
|
| 804 |
-
query_result: Any,
|
| 805 |
-
raw_chunks: Any,
|
| 806 |
-
) -> list[dict[str, Any]]:
|
| 807 |
-
"""Build the sources payload for the SSE `sources` event.
|
| 808 |
-
|
| 809 |
-
- structured_flow: one entry per executed table (table_name only).
|
| 810 |
-
- unstructured_flow: deduped by (document_id, page_label), Phase 1 shape.
|
| 811 |
-
- chat or error: empty list.
|
| 812 |
-
"""
|
| 813 |
-
if intent == "structured_flow":
|
| 814 |
-
if query_result is None or getattr(query_result, "error", None):
|
| 815 |
-
return []
|
| 816 |
-
table_name = getattr(query_result, "table_name", "") or ""
|
| 817 |
-
if not table_name:
|
| 818 |
-
return []
|
| 819 |
-
return [{
|
| 820 |
-
"document_id": f"{user_id}_{table_name}",
|
| 821 |
-
"filename": table_name,
|
| 822 |
-
"page_label": None,
|
| 823 |
-
}]
|
| 824 |
-
|
| 825 |
-
if intent == "unstructured_flow" and raw_chunks:
|
| 826 |
-
seen: set[tuple[Any, Any]] = set()
|
| 827 |
-
sources: list[dict[str, Any]] = []
|
| 828 |
-
for item in raw_chunks:
|
| 829 |
-
if isinstance(item, RetrievalResult):
|
| 830 |
-
data = item.metadata.get("data", {})
|
| 831 |
-
elif isinstance(item, dict):
|
| 832 |
-
data = item
|
| 833 |
-
else:
|
| 834 |
-
continue
|
| 835 |
-
key = (data.get("document_id"), data.get("page_label"))
|
| 836 |
-
if key in seen or key == (None, None):
|
| 837 |
-
continue
|
| 838 |
-
seen.add(key)
|
| 839 |
-
sources.append({
|
| 840 |
-
"document_id": data.get("document_id"),
|
| 841 |
-
"filename": data.get("filename", "Unknown"),
|
| 842 |
-
"page_label": data.get("page_label", "Unknown"),
|
| 843 |
-
})
|
| 844 |
-
return sources
|
| 845 |
-
|
| 846 |
-
return []
|
| 847 |
-
|
| 848 |
-
|
| 849 |
def _normalize_chunks(raw: Any) -> list[DocumentChunk]:
|
| 850 |
"""Convert whatever the retriever returns into list[DocumentChunk].
|
| 851 |
|
|
|
|
| 547 |
# else: chat path β no context
|
| 548 |
|
| 549 |
# ---- 2b. Emit sources ---------------------------------------
|
| 550 |
+
# Sources moved to traceability (KM-691): the stream stays text-only. The FE
|
| 551 |
+
# reads sources from GET /api/v1/traceability (richer + intent-consistent; the
|
| 552 |
+
# document sources for this turn are captured on the pad above). The empty
|
| 553 |
+
# `sources` event is kept for SSE backward-compat.
|
| 554 |
+
yield {"event": "sources", "data": json.dumps([])}
|
|
|
|
|
|
|
|
|
|
| 555 |
|
| 556 |
# ---- 3. Stream answer ----------------------------------------
|
| 557 |
# masked: the answer call sees real query rows / doc chunks (possible PII).
|
|
|
|
| 747 |
yield {"event": "error", "data": f"Analysis failed: {e}"}
|
| 748 |
return
|
| 749 |
|
| 750 |
+
# Sources live in traceability now (KM-691), derived from the run's
|
| 751 |
+
# retrieve_data calls; the stream stays text-only.
|
| 752 |
+
yield {"event": "sources", "data": json.dumps([])}
|
| 753 |
yield {"event": "chunk", "data": result.chat_answer}
|
| 754 |
try:
|
| 755 |
# Stamp identity from the request scope: owner + the shared session id
|
|
|
|
| 797 |
return catalog.model_copy(update={"sources": scoped or catalog.sources})
|
| 798 |
|
| 799 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
def _normalize_chunks(raw: Any) -> list[DocumentChunk]:
|
| 801 |
"""Convert whatever the retriever returns into list[DocumentChunk].
|
| 802 |
|
src/api/v2/chat.py
CHANGED
|
@@ -93,11 +93,11 @@ async def chat_stream(
|
|
| 93 |
"""Chat endpoint with streaming response (v2 β keyed on `analysis_id`).
|
| 94 |
|
| 95 |
SSE event sequence:
|
| 96 |
-
1. sources β
|
| 97 |
-
|
| 98 |
2. status β slow-path progress pings (optional)
|
| 99 |
3. chunk β text fragments of the answer
|
| 100 |
-
4. done β {"message_id": "..."} for the
|
| 101 |
"""
|
| 102 |
analysis_id = body.analysis_id
|
| 103 |
message_id = _mint_message_id()
|
|
|
|
| 93 |
"""Chat endpoint with streaming response (v2 β keyed on `analysis_id`).
|
| 94 |
|
| 95 |
SSE event sequence:
|
| 96 |
+
1. sources β always `[]` (KM-691): sources moved to GET /api/v1/traceability;
|
| 97 |
+
the stream stays text-only. Event kept for backward-compat.
|
| 98 |
2. status β slow-path progress pings (optional)
|
| 99 |
3. chunk β text fragments of the answer
|
| 100 |
+
4. done β {"message_id": "..."} for the traceability lookup
|
| 101 |
"""
|
| 102 |
analysis_id = body.analysis_id
|
| 103 |
message_id = _mint_message_id()
|
src/traceability/scratchpad.py
CHANGED
|
@@ -148,7 +148,8 @@ class TraceabilityScratchpad:
|
|
| 148 |
|
| 149 |
def add_document_sources(self, raw_chunks: Any, query: str) -> None:
|
| 150 |
"""Dedupe retrieved chunks by (document_id, page_label) into document
|
| 151 |
-
sources
|
|
|
|
| 152 |
for item in raw_chunks or []:
|
| 153 |
if hasattr(item, "metadata"):
|
| 154 |
data = item.metadata.get("data", {})
|
|
|
|
| 148 |
|
| 149 |
def add_document_sources(self, raw_chunks: Any, query: str) -> None:
|
| 150 |
"""Dedupe retrieved chunks by (document_id, page_label) into document
|
| 151 |
+
sources, stamped with the query. Sole source of document provenance now
|
| 152 |
+
that the stream no longer emits sources (KM-691)."""
|
| 153 |
for item in raw_chunks or []:
|
| 154 |
if hasattr(item, "metadata"):
|
| 155 |
data = item.metadata.get("data", {})
|