ishaq101's picture
/fix message_id for chat/stream Endpoint (#6)
d8e7745
Raw
History Blame
3.35 kB
"""`help` skill endpoint β€” dedicated, deterministic dispatch (pr/5 Phase 2).
`POST /api/v1/tools/help` streams state-aware next-step guidance over SSE. Unlike v1
β€” where `/help` was reachable only by letting the intent router classify a chat
message β€” this endpoint dispatches Help directly: the slash command IS the intent, so
there is no router round-trip and no misclassification risk (contract open-Q #2,
resolved in favour of a dedicated endpoint).
Contract: `API_ENDPOINTS_RESTRUCTURE.md` Β§3. The SSE shape mirrors `/chat/stream`, but
help never references documents, so `sources` is always `[]` and there are no `status`
pings. The `done` event carries the assistant `message_id` β€” always minted Python-side,
never accepted from the caller (server-authoritative; keys the future /observability
lookup, Β§7).
Python is generative-only (06-25 direction): this endpoint does NOT persist the turn β€”
Go owns writes to `analyses_messages`. It only generates + streams.
"""
import json
import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sse_starlette.sse import EventSourceResponse
# Reuse the warm, process-shared ChatHandler (keeps HelpAgent + Azure clients warm)
# and the same history loader the chat endpoint uses. `load_history` reads by
# `analysis_id` (== room_id today); it moves to `analyses_messages` with DEV_PLAN #25.
from src.api.v1.chat import _chat_handler, load_history
from src.db.postgres.connection import get_db
from src.middlewares.logging import get_logger, log_execution
logger = get_logger("help_api")
router = APIRouter(prefix="/api/v1/tools", tags=["Tools"])
class HelpRequest(BaseModel):
user_id: str
analysis_id: str
@router.post("/help")
@log_execution(logger)
async def help_stream(request: HelpRequest, db: AsyncSession = Depends(get_db)):
"""Stream state-aware next-step guidance (deterministic `/help` dispatch).
SSE event sequence:
1. sources β€” always `[]` (help never references documents)
2. chunk β€” text fragments of the guidance
3. done β€” `{"message_id": "..."}` for the observability lookup
"""
# Server-authoritative turn id β€” never accepted from the caller (keys /observability).
message_id = f"msg_{uuid.uuid4().hex[:12]}"
try:
history = await load_history(db, request.analysis_id, limit=10)
async def stream_response():
async for event in _chat_handler.stream_help(
request.user_id,
request.analysis_id,
history=history,
message=None,
):
if event["event"] == "done":
# Stamp the turn id so the FE can fetch /observability for it.
yield {"event": "done", "data": json.dumps({"message_id": message_id})}
elif event["event"] == "error":
yield event
return
else:
# `sources` ([]) and `chunk` pass through unchanged.
yield event
return EventSourceResponse(stream_response())
except Exception as e:
logger.error("Help failed", error=str(e))
raise HTTPException(status_code=500, detail=f"Help failed: {str(e)}") from e