Skip to content

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

  1. 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
  1. 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,
    )
  1. Add /healthz and /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}
  1. Start the server:
uv run uvicorn miniserve.app:app --host 127.0.0.1 --port 8000 --reload
  1. 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
}
  1. Hit /docs in a browser: FastAPI auto-generates OpenAPI docs at http://127.0.0.1:8000/docs. Verify your schemas show up correctly. Try the in-browser "try it out" UI.

  2. Test the error paths:

  3. Empty sentence: should get HTTP 422 (Pydantic validation error).
  4. Missing sentence: HTTP 422.
  5. Oversized payload: HTTP 422.
  6. 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 from GET /openapi.json.
  • manifest.json — uvicorn version, FastAPI version, agent checkpoint hash.

Acceptance

  • The server starts without errors.
  • POST /correct with a valid payload returns a CorrectResponse matching the schema.
  • /healthz returns 200.
  • /readyz returns 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 — /readyz would lie. Use FastAPI's lifespan to set _model_ready correctly. Bonus: use lifespan over @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