/fix message_id for chat/stream Endpoint
#6
by rhbt6767 - opened
- API_ENDPOINTS_RESTRUCTURE.md +5 -5
- DEV_PLAN.md +7 -5
- main.py +3 -2
- src/api/v1/help.py +6 -8
- src/api/v2/chat.py +8 -10
API_ENDPOINTS_RESTRUCTURE.md
CHANGED
|
@@ -14,7 +14,7 @@ lock them.
|
|
| 14 |
unwired from `main` + Swagger (not deleted).
|
| 15 |
|
| 16 |
**Open coordination questions (need a decision with Harry) — flagged inline as ⚠️:**
|
| 17 |
-
1. **`message_id` origin** — who mints the assistant turn id used to correlate stream ↔ observability? (
|
| 18 |
2. **Deterministic `/help` dispatch** — dedicated endpoint (recommended below) vs router classification.
|
| 19 |
3. **Observability storage** — single JSONB row per message (recommended) vs 3 normalized tables.
|
| 20 |
|
|
@@ -31,15 +31,15 @@ the `done` event now carries the assistant `message_id` for observability correl
|
|
| 31 |
{
|
| 32 |
"user_id": "u_1a2b3c",
|
| 33 |
"analysis_id": "an_42",
|
| 34 |
-
"message_id": "msg_88f1",
|
| 35 |
"message": "What were total sales by region last quarter?"
|
| 36 |
}
|
| 37 |
```
|
| 38 |
|
| 39 |
- `analysis_id` is the analysis-session id (replaces `room_id`). No auth header (handled by Go).
|
| 40 |
-
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
| 43 |
|
| 44 |
**Response:** `text/event-stream`. Events arrive in this order:
|
| 45 |
|
|
|
|
| 14 |
unwired from `main` + Swagger (not deleted).
|
| 15 |
|
| 16 |
**Open coordination questions (need a decision with Harry) — flagged inline as ⚠️:**
|
| 17 |
+
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.)*
|
| 18 |
2. **Deterministic `/help` dispatch** — dedicated endpoint (recommended below) vs router classification.
|
| 19 |
3. **Observability storage** — single JSONB row per message (recommended) vs 3 normalized tables.
|
| 20 |
|
|
|
|
| 31 |
{
|
| 32 |
"user_id": "u_1a2b3c",
|
| 33 |
"analysis_id": "an_42",
|
|
|
|
| 34 |
"message": "What were total sales by region last quarter?"
|
| 35 |
}
|
| 36 |
```
|
| 37 |
|
| 38 |
- `analysis_id` is the analysis-session id (replaces `room_id`). No auth header (handled by Go).
|
| 39 |
+
- `message_id` is **not** a request field. Python always mints the assistant turn id
|
| 40 |
+
(server-authoritative, for FE-security) and returns it on `done`; the FE reads it there and passes
|
| 41 |
+
it to `/api/v1/observability?message_id=...`. Any `message_id` a caller sends is ignored. *(The
|
| 42 |
+
`/observability` endpoint is future work — a later PR; §7 is a forward-looking sketch.)*
|
| 43 |
|
| 44 |
**Response:** `text/event-stream`. Events arrive in this order:
|
| 45 |
|
DEV_PLAN.md
CHANGED
|
@@ -24,9 +24,9 @@ the endpoint contract *before* coding the tools. Status legend: ⬜ not started
|
|
| 24 |
| **P0 — contract** | Draft + send endpoint contract to Harry (chat v2 · tools group · observability) | Rifqi + Sofhia | ✅ | `API_ENDPOINTS_RESTRUCTURE.md` sent 2026-06-30 (before-noon deadline met). Observability section flagged tentative. |
|
| 25 |
| **1 — unwire** | Unwire `users`(login)/`document`/`room`/`db_client`/`data_catalog`/`analysis` from `main` + Swagger | Sofhia | ✅ | **KM-686**, commit `0b2d678`. Commented, not deleted; `chat`/`report`/`tools` kept mounted. Resolves the analysis-CRUD scope Q — whole `analysis` router unwired (Go owns it). |
|
| 26 |
| **2 — v2 + regroup** | Create `src/api/v2/` and move the chat pilot there | Rifqi | ✅ | New `src/api/v2/__init__.py` + `src/api/v2/chat.py` (`POST /api/v2/chat/stream`), mounted in `main.py`. Only chat in v2; v1 `/chat/stream` kept mounted until FE moves over. Routes import-verified. |
|
| 27 |
-
| **2 — v2 + regroup** | Chat: `room_id` → **`analysis_id`** (request field + handler + history) | Rifqi | ✅ | v2 `ChatRequest{user_id, analysis_id, message
|
| 28 |
| **2 — v2 + regroup** | Move report under tools → `/api/v1/tools/report` (+ version routes) | Rifqi | ✅ | report router re-prefixed `/api/v1` → `/api/v1/tools` (all 3 routes move together), tag → `Tools`; old `/api/v1/report` gone. Same functionality, new home. Import-verified. |
|
| 29 |
-
| **2 — v2 + regroup** | Move help under tools → `POST /api/v1/tools/help` (dedicated endpoint) | Sofhia | ✅ | New `src/api/v1/help.py` (SSE: `sources:[]`→`chunk`→`done{message_id}`) + additive `ChatHandler.stream_help()` (reuses HelpAgent+state+readiness, no router). Generative-only (no persist). **Router `help` intent KEPT** — both paths live by design. message_id minted Python-side
|
| 30 |
| **2 — v2 + regroup** | Tools list → `/api/v1/tools/list` | Sofhia | ✅ | Renamed route `GET /api/v1/tools` → `GET /api/v1/tools/list` ([tools.py:133](src/api/v1/tools.py:133)). |
|
| 31 |
| **2 — v2 + regroup** | FE: slash menu = `/help` only; report = right-side button | Mentor (FE) | ⬜ | Coordination note, not Python work. |
|
| 32 |
| **3 — tools + obs** | Finish `help` so it actually **calls** (not just lists) + test | Sofhia | ⬜ | Mentor: help currently only lists tools. Core #2 after chat. |
|
|
@@ -34,14 +34,16 @@ the endpoint contract *before* coding the tools. Status legend: ⬜ not started
|
|
| 34 |
| **3 — tools + obs** | Audit `report_inputs` — covers planning + tool I/O + source? add cols / new store | Rifqi | ⬜ | **Rec:** dedicated provenance store = 1 JSONB row per message (logical 3 sections); keep Langfuse for engineering. |
|
| 35 |
| **3 — tools + obs** | Build `GET /api/v1/observability` (one merged response) | Rifqi | ⬜ | Intent-based source rules (greeting/help = none; retrieve = required). Richness path-dependent (full planning only on slow path). |
|
| 36 |
| **3 — tools + obs** | Keep stream **text-only**; observability is a separate parallel call | Rifqi | ⬜ | Per mentor — don't slow the stream. |
|
| 37 |
-
| **3 — tools + obs** | Resolve `message_id` correlation (stream ↔ observability) with Harry | Rifqi ↔ Harry |
|
| 38 |
| **4 — biz questions** | Get Go folder; confirm `business_questions` in create-analysis (max 5); sync Python | Harry/Mentor → Rifqi | ⬜ | Go currently missing the field ("lagi difixing"). Python already models objective + business_questions. |
|
| 39 |
| **deferred** | Report formats: PPT (preferred) / PDF / infographic on download | — | ⏸️ | MD is fine for the FE preview stage now. |
|
| 40 |
| **deferred** | Charts (Plotly→JSON) + images tables | — | ⏸️ | Carried from §4 #26/#27. |
|
| 41 |
|
| 42 |
**Next up:** Phase 2 Python work is **done** (chat→v2 `analysis_id`; `help`/`report`/`list` regrouped
|
| 43 |
-
under `/api/v1/tools/`).
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
|
| 46 |
---
|
| 47 |
|
|
|
|
| 24 |
| **P0 — contract** | Draft + send endpoint contract to Harry (chat v2 · tools group · observability) | Rifqi + Sofhia | ✅ | `API_ENDPOINTS_RESTRUCTURE.md` sent 2026-06-30 (before-noon deadline met). Observability section flagged tentative. |
|
| 25 |
| **1 — unwire** | Unwire `users`(login)/`document`/`room`/`db_client`/`data_catalog`/`analysis` from `main` + Swagger | Sofhia | ✅ | **KM-686**, commit `0b2d678`. Commented, not deleted; `chat`/`report`/`tools` kept mounted. Resolves the analysis-CRUD scope Q — whole `analysis` router unwired (Go owns it). |
|
| 26 |
| **2 — v2 + regroup** | Create `src/api/v2/` and move the chat pilot there | Rifqi | ✅ | New `src/api/v2/__init__.py` + `src/api/v2/chat.py` (`POST /api/v2/chat/stream`), mounted in `main.py`. Only chat in v2; v1 `/chat/stream` kept mounted until FE moves over. Routes import-verified. |
|
| 27 |
+
| **2 — v2 + regroup** | Chat: `room_id` → **`analysis_id`** (request field + handler + history) | Rifqi | ✅ | v2 `ChatRequest{user_id, analysis_id, message}`; reuses warm `ChatHandler` + v1 cache/history helpers; `done` returns `{message_id}` (always minted Python-side, server-authoritative — open-Q #1 resolved in pr/6). Persistence kept transitionally → still ties to #25 (`analyses_messages`); ruff-clean. |
|
| 28 |
| **2 — v2 + regroup** | Move report under tools → `/api/v1/tools/report` (+ version routes) | Rifqi | ✅ | report router re-prefixed `/api/v1` → `/api/v1/tools` (all 3 routes move together), tag → `Tools`; old `/api/v1/report` gone. Same functionality, new home. Import-verified. |
|
| 29 |
+
| **2 — v2 + regroup** | Move help under tools → `POST /api/v1/tools/help` (dedicated endpoint) | Sofhia | ✅ | New `src/api/v1/help.py` (SSE: `sources:[]`→`chunk`→`done{message_id}`) + additive `ChatHandler.stream_help()` (reuses HelpAgent+state+readiness, no router). Generative-only (no persist). **Router `help` intent KEPT** — both paths live by design. message_id always minted Python-side, server-authoritative (open-Q #1 resolved, pr/6). Import-verified. |
|
| 30 |
| **2 — v2 + regroup** | Tools list → `/api/v1/tools/list` | Sofhia | ✅ | Renamed route `GET /api/v1/tools` → `GET /api/v1/tools/list` ([tools.py:133](src/api/v1/tools.py:133)). |
|
| 31 |
| **2 — v2 + regroup** | FE: slash menu = `/help` only; report = right-side button | Mentor (FE) | ⬜ | Coordination note, not Python work. |
|
| 32 |
| **3 — tools + obs** | Finish `help` so it actually **calls** (not just lists) + test | Sofhia | ⬜ | Mentor: help currently only lists tools. Core #2 after chat. |
|
|
|
|
| 34 |
| **3 — tools + obs** | Audit `report_inputs` — covers planning + tool I/O + source? add cols / new store | Rifqi | ⬜ | **Rec:** dedicated provenance store = 1 JSONB row per message (logical 3 sections); keep Langfuse for engineering. |
|
| 35 |
| **3 — tools + obs** | Build `GET /api/v1/observability` (one merged response) | Rifqi | ⬜ | Intent-based source rules (greeting/help = none; retrieve = required). Richness path-dependent (full planning only on slow path). |
|
| 36 |
| **3 — tools + obs** | Keep stream **text-only**; observability is a separate parallel call | Rifqi | ⬜ | Per mentor — don't slow the stream. |
|
| 37 |
+
| **3 — tools + obs** | Resolve `message_id` correlation (stream ↔ observability) with Harry | Rifqi ↔ Harry | ✅ | **RESOLVED (pr/6):** Python is the **sole minter** — `message_id` dropped from the `/api/v2/chat/stream` + `/api/v1/tools/help` request bodies; always minted server-side (server-authoritative, FE-security) and returned on `done`. Any caller-sent `message_id` is ignored. Contract open-Q #1 closed. |
|
| 38 |
| **4 — biz questions** | Get Go folder; confirm `business_questions` in create-analysis (max 5); sync Python | Harry/Mentor → Rifqi | ⬜ | Go currently missing the field ("lagi difixing"). Python already models objective + business_questions. |
|
| 39 |
| **deferred** | Report formats: PPT (preferred) / PDF / infographic on download | — | ⏸️ | MD is fine for the FE preview stage now. |
|
| 40 |
| **deferred** | Charts (Plotly→JSON) + images tables | — | ⏸️ | Carried from §4 #26/#27. |
|
| 41 |
|
| 42 |
**Next up:** Phase 2 Python work is **done** (chat→v2 `analysis_id`; `help`/`report`/`list` regrouped
|
| 43 |
+
under `/api/v1/tools/`). The `message_id` correlation contract is now settled in **pr/6** (Python sole
|
| 44 |
+
minter, stream-only). The full **Phase 3 observability build** — scratchpad + `GET /api/v1/observability`
|
| 45 |
+
(shape sketched in the contract §7) — is slated for a **later PR**, then **Phase 4** (business
|
| 46 |
+
questions, Go-blocked).
|
| 47 |
|
| 48 |
---
|
| 49 |
|
main.py
CHANGED
|
@@ -16,7 +16,8 @@ from slowapi.errors import RateLimitExceeded
|
|
| 16 |
# from src.api.v1.db_client import router as db_client_router # unwired: Go registers DB client
|
| 17 |
# from src.api.v1.data_catalog import router as data_catalog_router # unwired: Go handles the catalog
|
| 18 |
# from src.api.v1.analysis import router as analysis_router # unwired: Go owns create/update analysis
|
| 19 |
-
from src.api.v1.chat import router as chat_router
|
|
|
|
| 20 |
from src.api.v1.report import router as report_router
|
| 21 |
from src.api.v1.tools import router as tools_router
|
| 22 |
from src.api.v1.help import router as help_router # pr/5 Phase 2: dedicated /tools/help
|
|
@@ -62,7 +63,7 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
| 62 |
# app.include_router(db_client_router) # unwired: Go registers DB client
|
| 63 |
# app.include_router(data_catalog_router) # unwired: Go handles the catalog
|
| 64 |
# app.include_router(analysis_router) # unwired: Go owns create/update analysis
|
| 65 |
-
app.include_router(chat_router)
|
| 66 |
app.include_router(report_router)
|
| 67 |
app.include_router(tools_router)
|
| 68 |
app.include_router(help_router)
|
|
|
|
| 16 |
# from src.api.v1.db_client import router as db_client_router # unwired: Go registers DB client
|
| 17 |
# from src.api.v1.data_catalog import router as data_catalog_router # unwired: Go handles the catalog
|
| 18 |
# from src.api.v1.analysis import router as analysis_router # unwired: Go owns create/update analysis
|
| 19 |
+
# from src.api.v1.chat import router as chat_router # unwired: replaced by /api/v2/chat/stream
|
| 20 |
+
# NOTE: src.api.v1.chat module still imported by v2 chat + /tools/help — keep the file.
|
| 21 |
from src.api.v1.report import router as report_router
|
| 22 |
from src.api.v1.tools import router as tools_router
|
| 23 |
from src.api.v1.help import router as help_router # pr/5 Phase 2: dedicated /tools/help
|
|
|
|
| 63 |
# app.include_router(db_client_router) # unwired: Go registers DB client
|
| 64 |
# app.include_router(data_catalog_router) # unwired: Go handles the catalog
|
| 65 |
# app.include_router(analysis_router) # unwired: Go owns create/update analysis
|
| 66 |
+
# app.include_router(chat_router) # unwired: v2 chat replaces it (drops v1 cache ops routes)
|
| 67 |
app.include_router(report_router)
|
| 68 |
app.include_router(tools_router)
|
| 69 |
app.include_router(help_router)
|
src/api/v1/help.py
CHANGED
|
@@ -8,8 +8,9 @@ resolved in favour of a dedicated endpoint).
|
|
| 8 |
|
| 9 |
Contract: `API_ENDPOINTS_RESTRUCTURE.md` §3. The SSE shape mirrors `/chat/stream`, but
|
| 10 |
help never references documents, so `sources` is always `[]` and there are no `status`
|
| 11 |
-
pings. The `done` event carries the assistant `message_id`
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
Python is generative-only (06-25 direction): this endpoint does NOT persist the turn —
|
| 15 |
Go owns writes to `analyses_messages`. It only generates + streams.
|
|
@@ -17,7 +18,6 @@ Go owns writes to `analyses_messages`. It only generates + streams.
|
|
| 17 |
|
| 18 |
import json
|
| 19 |
import uuid
|
| 20 |
-
from typing import Optional
|
| 21 |
|
| 22 |
from fastapi import APIRouter, Depends, HTTPException
|
| 23 |
from pydantic import BaseModel
|
|
@@ -39,9 +39,6 @@ router = APIRouter(prefix="/api/v1/tools", tags=["Tools"])
|
|
| 39 |
class HelpRequest(BaseModel):
|
| 40 |
user_id: str
|
| 41 |
analysis_id: str
|
| 42 |
-
# ⚠️ open-Q #1: Go may mint the assistant turn id and pass it; if absent, Python
|
| 43 |
-
# mints one and returns it on `done` so the FE can call /observability in parallel.
|
| 44 |
-
message_id: Optional[str] = None
|
| 45 |
|
| 46 |
|
| 47 |
@router.post("/help")
|
|
@@ -54,7 +51,8 @@ async def help_stream(request: HelpRequest, db: AsyncSession = Depends(get_db)):
|
|
| 54 |
2. chunk — text fragments of the guidance
|
| 55 |
3. done — `{"message_id": "..."}` for the observability lookup
|
| 56 |
"""
|
| 57 |
-
|
|
|
|
| 58 |
try:
|
| 59 |
history = await load_history(db, request.analysis_id, limit=10)
|
| 60 |
|
|
@@ -79,4 +77,4 @@ async def help_stream(request: HelpRequest, db: AsyncSession = Depends(get_db)):
|
|
| 79 |
|
| 80 |
except Exception as e:
|
| 81 |
logger.error("Help failed", error=str(e))
|
| 82 |
-
raise HTTPException(status_code=500, detail=f"Help failed: {str(e)}")
|
|
|
|
| 8 |
|
| 9 |
Contract: `API_ENDPOINTS_RESTRUCTURE.md` §3. The SSE shape mirrors `/chat/stream`, but
|
| 10 |
help never references documents, so `sources` is always `[]` and there are no `status`
|
| 11 |
+
pings. The `done` event carries the assistant `message_id` — always minted Python-side,
|
| 12 |
+
never accepted from the caller (server-authoritative; keys the future /observability
|
| 13 |
+
lookup, §7).
|
| 14 |
|
| 15 |
Python is generative-only (06-25 direction): this endpoint does NOT persist the turn —
|
| 16 |
Go owns writes to `analyses_messages`. It only generates + streams.
|
|
|
|
| 18 |
|
| 19 |
import json
|
| 20 |
import uuid
|
|
|
|
| 21 |
|
| 22 |
from fastapi import APIRouter, Depends, HTTPException
|
| 23 |
from pydantic import BaseModel
|
|
|
|
| 39 |
class HelpRequest(BaseModel):
|
| 40 |
user_id: str
|
| 41 |
analysis_id: str
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
@router.post("/help")
|
|
|
|
| 51 |
2. chunk — text fragments of the guidance
|
| 52 |
3. done — `{"message_id": "..."}` for the observability lookup
|
| 53 |
"""
|
| 54 |
+
# Server-authoritative turn id — never accepted from the caller (keys /observability).
|
| 55 |
+
message_id = f"msg_{uuid.uuid4().hex[:12]}"
|
| 56 |
try:
|
| 57 |
history = await load_history(db, request.analysis_id, limit=10)
|
| 58 |
|
|
|
|
| 77 |
|
| 78 |
except Exception as e:
|
| 79 |
logger.error("Help failed", error=str(e))
|
| 80 |
+
raise HTTPException(status_code=500, detail=f"Help failed: {str(e)}") from e
|
src/api/v2/chat.py
CHANGED
|
@@ -5,9 +5,9 @@
|
|
| 5 |
- the request carries an explicit **`analysis_id`** (replacing v1's `room_id`). The
|
| 6 |
two are the same session id today (`analysis_id == room_id`), so the warm,
|
| 7 |
process-shared `ChatHandler` and the v1 cache/history helpers are reused unchanged.
|
| 8 |
-
- the `done` event carries the assistant **`message_id`**
|
| 9 |
-
|
| 10 |
-
|
| 11 |
|
| 12 |
Only chat moves to v2; the tools group + observability stay on `/api/v1` (contract:
|
| 13 |
API_ENDPOINTS_RESTRUCTURE.md §1).
|
|
@@ -49,18 +49,16 @@ logger = get_logger("chat_api_v2")
|
|
| 49 |
router = APIRouter(prefix="/api/v2", tags=["Chat"])
|
| 50 |
|
| 51 |
|
| 52 |
-
def _mint_message_id(
|
| 53 |
-
"""
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
class ChatRequest(BaseModel):
|
| 58 |
user_id: str
|
| 59 |
analysis_id: str
|
| 60 |
message: str
|
| 61 |
-
# ⚠️ open-Q #1: Go may mint + pass the assistant turn id; if absent we mint one and
|
| 62 |
-
# echo it on `done` so the FE can correlate /observability with this answer.
|
| 63 |
-
message_id: str | None = None
|
| 64 |
|
| 65 |
|
| 66 |
@router.post("/chat/stream")
|
|
@@ -76,7 +74,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 76 |
4. done — {"message_id": "..."} for the observability lookup
|
| 77 |
"""
|
| 78 |
analysis_id = request.analysis_id
|
| 79 |
-
message_id = _mint_message_id(
|
| 80 |
redis = await get_redis()
|
| 81 |
cache_key = _chat_cache_key(analysis_id, request.user_id, request.message)
|
| 82 |
|
|
|
|
| 5 |
- the request carries an explicit **`analysis_id`** (replacing v1's `room_id`). The
|
| 6 |
two are the same session id today (`analysis_id == room_id`), so the warm,
|
| 7 |
process-shared `ChatHandler` and the v1 cache/history helpers are reused unchanged.
|
| 8 |
+
- the `done` event carries the assistant **`message_id`**. It is always minted
|
| 9 |
+
Python-side and is **never accepted from the caller** (server-authoritative — it keys
|
| 10 |
+
the future `/observability` lookup; open-Q #1 resolved). The FE reads it off `done`.
|
| 11 |
|
| 12 |
Only chat moves to v2; the tools group + observability stay on `/api/v1` (contract:
|
| 13 |
API_ENDPOINTS_RESTRUCTURE.md §1).
|
|
|
|
| 49 |
router = APIRouter(prefix="/api/v2", tags=["Chat"])
|
| 50 |
|
| 51 |
|
| 52 |
+
def _mint_message_id() -> str:
|
| 53 |
+
"""Mint the assistant turn id. Server-authoritative — never accepted from the caller
|
| 54 |
+
(it keys the future /observability lookup). Returned on `done`; open-Q #1 resolved."""
|
| 55 |
+
return f"msg_{uuid.uuid4().hex[:12]}"
|
| 56 |
|
| 57 |
|
| 58 |
class ChatRequest(BaseModel):
|
| 59 |
user_id: str
|
| 60 |
analysis_id: str
|
| 61 |
message: str
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
@router.post("/chat/stream")
|
|
|
|
| 74 |
4. done — {"message_id": "..."} for the observability lookup
|
| 75 |
"""
|
| 76 |
analysis_id = request.analysis_id
|
| 77 |
+
message_id = _mint_message_id()
|
| 78 |
redis = await get_redis()
|
| 79 |
cache_key = _chat_cache_key(analysis_id, request.user_id, request.message)
|
| 80 |
|