Download src/api/v1/help.py from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 3.35 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F7/src/api/v1/help.py
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog@refs/pr/7/src/api/v1/help.py
-
curl -L -o help.py https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F7/src/api/v1/help.py
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 | |
| 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 | |