Skip to content

English · Español

Lab 00 — FastAPI mínimo: 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().

Objetivo

Envolver el agente tutor de gramática de la Fase 32 en un servicio FastAPI. Verificar con curl que enviar {"sentence": "He goed to school"} devuelve una corrección. Establecer la estructura de proyecto para src/miniserve/.

Setup

  • Agente de la Fase 32 (from miniagent import GrammarTutorAgent).
  • uv add fastapi uvicorn pydantic — añadir a las dependencias del proyecto.
  • Un src/miniserve/app.py en blanco.

Tareas

  1. Define los esquemas request/response en 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. Implementa 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. Añade /healthz y /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. Arranca el servidor:
uv run uvicorn miniserve.app:app --host 127.0.0.1 --port 8000 --reload
  1. Roundtrip con curl:
curl -X POST http://127.0.0.1:8000/correct \
     -H "Content-Type: application/json" \
     -d '{"sentence": "He goed to school"}'

Esperado (módulo las frases exactas de la Fase 32):

{
  "corrected": "He went to school",
  "explanation": "The past tense of 'go' is the irregular form 'went'.",
  "was_correct": false
}
  1. Visita /docs en el navegador: FastAPI autogenera la documentación OpenAPI en http://127.0.0.1:8000/docs. Verifica que tus esquemas se muestran correctamente. Prueba la UI "try it out" en el navegador.

  2. Prueba las rutas de error:

  3. sentence vacío: debería dar HTTP 422 (error de validación Pydantic).
  4. sentence ausente: HTTP 422.
  5. Payload sobredimensionado: HTTP 422.
  6. sentence válido, pero el modelo lanza excepción (mockéalo): HTTP 500.

Mediciones

Guarda en experiments/<date>-phase-33-lab-00/:

  • roundtrip.txt — output de los comandos curl.
  • openapi.json — exportado de GET /openapi.json.
  • manifest.json — versión de uvicorn, versión de FastAPI, hash del checkpoint del agente.

Aceptación

  • El servidor arranca sin errores.
  • POST /correct con un payload válido devuelve un CorrectResponse que cumple el esquema.
  • /healthz devuelve 200.
  • /readyz devuelve 200 después de cargar el agente, 503 antes.
  • mypy --strict src/miniserve/ pasa.

Trampas

  • Cargar el agente en tiempo de import del módulo vs en un startup handler. Si la carga es lenta (~5s), el servidor aparece "started" antes de estar realmente listo — /readyz mentiría. Usa el lifespan de FastAPI para fijar _model_ready correctamente. Bonus: usa lifespan en lugar de @app.on_event("startup") (deprecado desde FastAPI 0.95).
  • Compartir el agente entre peticiones. Es un singleton a nivel de módulo — pero si el agente tiene estado mutable (memoria, ver Fase 32), las peticiones concurrentes pueden corromperse entre sí. Confirma que el agente de la Fase 32 es stateless entre llamadas O añade un lock. (Debería ser stateless; la memoria por aprendiz se carga fresca en cada llamada.)
  • Sintaxis Pydantic v1 vs v2. Esta fase usa v2 (Field(...), BaseModel).

Siguiente: 01-sync-vs-async.md