English · Español
Lab 00 — Minimal FastAPI: POST /correct¶
🇪🇸 El primer paso: envolver el agente tutor en un endpoint HTTP. Sin batching, sin async; solo FastAPI + uvicorn + un handler que llama a
agent.correct().
Objective¶
Wrap the Phase 32 grammar-tutor agent in a FastAPI service. Verify with curl that posting {"sentence": "He goed to school"} returns a correction. Establish the project structure for src/miniserve/.
Setup¶
- Phase 32's agent (
from miniagent import GrammarTutorAgent). uv add fastapi uvicorn pydantic— add to project dependencies.- A blank
src/miniserve/app.py.
Tasks¶
- Define request/response schemas in
src/miniserve/schemas.py:
from pydantic import BaseModel, Field
class CorrectRequest(BaseModel):
sentence: str = Field(min_length=1, max_length=500)
learner_id: str | None = Field(default=None, description="optional learner identifier")
class CorrectResponse(BaseModel):
corrected: str
explanation: str
was_correct: bool # true if the input was already grammatical
- Implement
src/miniserve/app.py:
from fastapi import FastAPI
from miniagent import GrammarTutorAgent
from miniserve.schemas import CorrectRequest, CorrectResponse
app = FastAPI(title="Lynx Tutor")
agent = GrammarTutorAgent.load_default() # singleton
@app.post("/correct", response_model=CorrectResponse)
def correct(req: CorrectRequest) -> CorrectResponse:
result = agent.correct(req.sentence, learner_id=req.learner_id)
return CorrectResponse(
corrected=result.text,
explanation=result.why,
was_correct=result.was_correct,
)
- Add
/healthzand/readyz:
@app.get("/healthz")
def healthz() -> dict[str, str]:
return {"status": "ok"}
_model_ready = False # set to True after agent.load_default() completes
@app.get("/readyz")
def readyz() -> dict[str, str | bool]:
return {"ready": _model_ready}
- Start the server:
- Roundtrip with curl:
curl -X POST http://127.0.0.1:8000/correct \
-H "Content-Type: application/json" \
-d '{"sentence": "He goed to school"}'
Expected (modulo Phase 32's exact phrasings):
{
"corrected": "He went to school",
"explanation": "The past tense of 'go' is the irregular form 'went'.",
"was_correct": false
}
-
Hit
/docsin a browser: FastAPI auto-generates OpenAPI docs athttp://127.0.0.1:8000/docs. Verify your schemas show up correctly. Try the in-browser "try it out" UI. -
Test the error paths:
- Empty
sentence: should get HTTP 422 (Pydantic validation error). - Missing
sentence: HTTP 422. - Oversized payload: HTTP 422.
- Valid
sentence, but the model raises (mock this): HTTP 500.
Measurements¶
Save to experiments/<date>-phase-33-lab-00/:
roundtrip.txt— output of the curl commands.openapi.json— exported fromGET /openapi.json.manifest.json— uvicorn version, FastAPI version, agent checkpoint hash.
Acceptance¶
- The server starts without errors.
POST /correctwith a valid payload returns aCorrectResponsematching the schema./healthzreturns 200./readyzreturns 200 after agent load, 503 before.mypy --strict src/miniserve/passes.
Pitfalls¶
- Loading the agent at module import time vs in a startup handler. If load is slow (~5s), the server appears "started" before it's actually ready —
/readyzwould lie. Use FastAPI's lifespan to set_model_readycorrectly. Bonus: uselifespanover@app.on_event("startup")(deprecated since FastAPI 0.95). - Sharing the agent across requests. It's a singleton at module scope — but if the agent has mutable state (memory, see Phase 32), concurrent requests can corrupt each other. Confirm Phase 32's agent is stateless across calls OR add a lock. (It should be stateless; the per-learner memory is loaded fresh each call.)
- Pydantic v1 vs v2 syntax. This phase uses v2 (
Field(...),BaseModel).
Next: 01-sync-vs-async.md