88a1dab810
---ci--- phase: 7 milestone: v0.2 status: complete requirements: covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012] partial: [] ---/ci--- Milestone v0.2 (ai-tutor-architecture) merged to main. Escalation record (audit remediation, durable): P1 executor delegation failed twice (empty subagent results, zero files created); auto-resolved at full autonomy to inline execution with identical plan fidelity (commit 3271373, reflog-only after phase branch squash-delete).
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""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"},
|
|
)
|