dc00a4f8f5
Wave 5-1 mypy adoption (D-058 relaxed-first): mypy 2.3.1, [tool.mypy] check_untyped_defs + no_implicit_optional + warn_unused_ignores + warn_redundant_casts + warn_unreachable, no overrides (every dep ships py.typed), files = ai_service + scripts/sandbox-agent.py + tests (not excluded). 215 errors → 0: ~140 fixed at source incl. genuine bugs found by the gate (LLMProvider protocol async-generator shape, SessionStore protocol sync/async mismatch, structured_reply genericity, get_args migrations, sandbox-agent Optional guard honesty, proctor test arg-order, dead ExecResult kwarg, 16 sqlmodel col() sites); 34 live suppressions scoped-coded + reasoned. Original 27 type:ignore dispositioned: 12 dead removed, 15 scoped (warn_unused_ignores self-eliminates the rest). Wired: scripts/typecheck.sh + package.json + turbo task + root ai:typecheck — pnpm typecheck now 5 tsc + mypy. Wave 5-2 recharts 2.15.4 → 3.10.1 (D-049/D-059): one breaking class (Tooltip formatter ValueType typing); 2 one-line inference fixes on the 2 chart files; no rollback needed (well inside the G-106 timebox). Deprecated direct dep resolved; uuid@9 gone from the tree. Build 248/248 with chart SSR markers verified in exported employer + dashboard pages. Wave 5-3 deprecated-transitive audit (G-109): 4 lockfile entries remain, all dev-only, none actionable within NFR budget — glob@7.2.3 + inflight + rimraf@3 via Storybook docgen chain (flat-cache@3); glob@10.5.0 via c8→test-exclude (D-052-binding coverage tool — swapping it is out of budget). Python: no EOL packages; zero-warning regime under filterwarnings=error is the machine-checked proof. Counts: 670 py / 44 web / 64 cli, zero warnings, G-104 allowlist still exactly 2. ---ci--- phase: 5 milestone: v0.7 status: execute ---/ci---
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""POST /v1/lab/feedback — SSE stream of Lab in-flow feedback (REQ-3-007).
|
|
|
|
v0.3 re-grounding: LIVE trace digest. Request carries {learner_id, task_id};
|
|
the digest is computed from the learner's real TraceStore events (D-028)
|
|
and handed to the Lab agent. No corpus scenarios. Empty/unknown trace is NOT
|
|
an error — Lab gets a "no telemetry yet" timeline and coaches the baseline.
|
|
D-016 envelope with agent=lab.
|
|
"""
|
|
|
|
import json
|
|
from collections.abc import AsyncIterator
|
|
from typing import cast
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel, Field
|
|
from sse_starlette.sse import EventSourceResponse
|
|
|
|
from ..agents.lab import LabAgent
|
|
from ..agents.registry import AgentRegistry
|
|
from ..config import Settings
|
|
from ..corpus.learner_context import get_learner_context
|
|
from ..grading.features import compute_digest
|
|
from .deps import (
|
|
get_agent_registry,
|
|
get_provider,
|
|
get_settings,
|
|
get_trace_store,
|
|
)
|
|
|
|
router = APIRouter(prefix="/v1")
|
|
|
|
|
|
class LabFeedbackRequest(BaseModel):
|
|
learner_id: str = Field(min_length=1)
|
|
task_id: str = Field(min_length=1)
|
|
|
|
|
|
@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),
|
|
trace_store=Depends(get_trace_store),
|
|
) -> EventSourceResponse:
|
|
# P5: registry.get returns BaseAgent; "lab" is registered as LabAgent
|
|
# (register_builtin_agents) — the cast states that mapping.
|
|
agent = cast(LabAgent, registry.get(provider, settings, "lab"))
|
|
learner_context = get_learner_context(body.learner_id)
|
|
trace = (
|
|
trace_store.get_trace(body.learner_id, body.task_id)
|
|
if body.task_id in trace_store.list_tasks(body.learner_id)
|
|
else []
|
|
)
|
|
digest = compute_digest(trace) if trace else None
|
|
|
|
async def event_stream() -> AsyncIterator[dict]:
|
|
yield {"event": "message", "data": json.dumps({
|
|
"type": "meta",
|
|
"agent": "lab",
|
|
"task_id": body.task_id,
|
|
"model": settings.model,
|
|
})}
|
|
first_byte = True
|
|
try:
|
|
async for token in agent.stream_feedback(digest, 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"},
|
|
)
|