"""POST /v1/lab/feedback — SSE stream of Lab in-flow feedback (REQ-2-007). D-016 envelope with agent=lab. Unknown scenario → 404 before streaming. """ import json from collections.abc import AsyncIterator from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sse_starlette.sse import EventSourceResponse from ..agents.registry import AgentRegistry from ..config import Settings from ..corpus.learner_context import get_learner_context from ..corpus.telemetry import get_lab_scenario from .deps import get_agent_registry, get_provider, get_settings router = APIRouter(prefix="/v1") class LabFeedbackRequest(BaseModel): scenario_id: str = Field(min_length=1) learner_id: str | None = None @router.post("/lab/feedback") async def lab_feedback( body: LabFeedbackRequest, registry: AgentRegistry = Depends(get_agent_registry), settings: Settings = Depends(get_settings), provider=Depends(get_provider), ) -> EventSourceResponse: scenario = get_lab_scenario(body.scenario_id) if scenario is None: raise HTTPException( status_code=404, detail=f"unknown scenario {body.scenario_id!r}" ) agent = registry.get(provider, settings, "lab") learner_context = get_learner_context(body.learner_id) async def event_stream() -> AsyncIterator[dict]: yield {"event": "message", "data": json.dumps({ "type": "meta", "agent": "lab", "scenario_id": body.scenario_id, "model": settings.model, })} first_byte = True try: async for token in agent.stream_feedback(scenario, learner_context): first_byte = False yield {"event": "message", "data": json.dumps({ "type": "delta", "content": token })} yield {"event": "message", "data": json.dumps({ "type": "done", "finish_reason": "stop" })} yield {"event": "message", "data": "[DONE]"} except Exception as exc: code = "provider_unavailable" if first_byte else "provider_error" yield {"event": "message", "data": json.dumps({ "type": "error", "code": code, "message": str(exc) })} # [DONE] from except, not finally — a yield in finally would # re-raise after GeneratorExit on client disconnect. yield {"event": "message", "data": "[DONE]"} return EventSourceResponse( event_stream(), headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, )