Hasta ahora el agente solo hablaba contigo, en tu Mac. La Fase 4 le da una puerta HTTP: cualquiera (con el token correcto) puede mandarle una oferta desde internet y recibir la respuesta. Son dos piezas: dejar el motor reutilizable, y ponerle delante una API con un portero que protege tu API key de OpenAI. Todo el ejemplo real, ejecutado, está al final.
El principio es el mismo que ya usaste en el Artefacto 2 (RAG): un motor, muchas interfaces. El motor no cambia; solo le añadimos una boca nueva.
El CLI se contentaba con un texto. Pero una API que alimenta a una web quiere estructura:
no solo la respuesta, también qué pasos dio el agente, cuántos turnos y si terminó bien. Por eso
refactorizamos el bucle para que devuelva un dict — igual que rag_core.py devolvía un dict en el Art.2.
# agente.py — el bucle (ahora núcleo reutilizable) devuelve estructura
def _correr_bucle(peticion, max_turnos=6, verboso=True) -> dict:
...
if not msg.tool_calls: # terminó
return {"respuesta": msg.content, "pasos": pasos,
"turnos": turno, "completado": True}
...
def agente(peticion, ...) -> str: # envoltorio para el CLI
return _correr_bucle(peticion, ...)["respuesta"]
def procesar_oferta(texto, max_turnos=6) -> dict: # envoltorio para la API
peticion = f"Analiza esta oferta y prepárame la candidatura:\n{texto}"
return _correr_bucle(peticion, max_turnos, verboso=False)
Una sola definición del bucle, dos puertas de salida. El CLI toma ["respuesta"]; la API toma el dict entero.
Ese pasos es la traza que la web pintará como barra de progreso.
Ya montaste una en el Art.2, así que esto es memoria muscular. Una API HTTP es un programa que escucha peticiones por la red y devuelve respuestas. Aquí, con FastAPI, definimos dos rutas y una sonda:
| Ruta | Método | Token | Qué hace |
|---|---|---|---|
/salud | GET | no | ¿está vivo? Para monitorización (Cloud Run la usa) |
/preguntar | POST | sí | recibe {"oferta": "..."} y corre el agente |
/ | GET | no | sirve el frontend (Fase 6) |
La oferta viaja en el cuerpo (body) de un POST como JSON, y el token en una cabecera (header)
llamada X-Token. FastAPI valida la forma del body con un modelo Pydantic — si falta oferta, rechaza solo.
Este es el corazón de la fase. Cada llamada al agente cuesta ~4 peticiones a OpenAI (el bucle),
así que dejarlo abierto sería regalar tu dinero a cualquier bot. El token es un portero:
si no coincide con APP_TOKEN, la respuesta es 401 y —clave— el agente ni se ejecuta.
@app.post("/preguntar")
def preguntar(peticion: Peticion, x_token: str = Header(default="")):
if x_token != APP_TOKEN or not APP_TOKEN: # portero
raise HTTPException(status_code=401, detail="Token inválido o ausente.")
if not peticion.oferta.strip():
raise HTTPException(status_code=400, detail="La oferta está vacía.")
return procesar_oferta(peticion.oferta) # ← solo se llega aquí con token OK
Fail-closed: fíjate en or not APP_TOKEN. Si por un despiste el token
quedara vacío, la puerta se cierra para todos en vez de abrirse para todos. En seguridad, el fallo
por defecto debe ser "denegar", nunca "permitir". Es una decisión de una línea que separa un juguete de algo serio.
Arrancar el servidor por cada petición sería absurdo. FastAPI tiene lifespan: un bloque
que corre una vez al levantar el servicio (para "calentar" el motor: crear el cliente OpenAI, cachear el CV)
y otra al apagarlo. En el Art.2 cargabas ahí el índice del RAG; aquí basta con avisar de que el token está puesto.
@asynccontextmanager
async def lifespan(app):
if not APP_TOKEN:
print("⚠️ APP_TOKEN vacío: la API rechazará TODO (fail-closed).")
print("✅ Agente listo. Esperando peticiones.")
yield # ← aquí el servidor vive y atiende; lo de abajo corre al apagar
El frontend web llega en la Fase 6, pero la API ya deja el hueco: monta la carpeta static/ en /…
pero solo si existe, para no romper hoy que aún no está. Va al final del fichero, para que
/salud y /preguntar tengan prioridad sobre el montaje.
if RUTA_STATIC.is_dir(): # solo si ya hay frontend
app.mount("/", StaticFiles(directory=RUTA_STATIC, html=True), name="static")
Esto es lo que salió al arrancar la API en local (uvicorn api:app --port 8099) y llamarla con
curl. Las cuatro pruebas, tal cual:
/salud sin token → vivoEl campo pasos deja ver el bucle uno a uno — el agente decidió, en este orden:
completado: TrueCuatro turnos, tres herramientas, orden elegido por el modelo. Exactamente la misma danza del CLI, ahora servida por HTTP y devuelta como JSON para que una web la pueda pintar.
El token y la API key son secretos: nunca en el código, nunca en git. Hoy viven en .env
(blindado en .gitignore). Al desplegar en la Fase 5 pasarán a Secret Manager de GCP,
exactamente como en el Art.2. El código no cambia: os.environ los lee igual venga de un fichero o de la nube.
Recordatorio para el deploy: el CV (cv_denys.md) no es un secreto de entorno,
es un fichero — y se horneará dentro de la imagen Docker. Revisaremos .gcloudignore antes del push, como acordamos.
| Concepto | Qué es aquí | Dónde vive fuera |
|---|---|---|
| API REST / HTTP | api.py con FastAPI | el 90% de los backends del mundo hablan así |
| Endpoint | /salud, /preguntar | cada "puerta" con la que interactúa un cliente |
| Header / body | token en X-Token, oferta en JSON | autenticación en cabeceras, datos en el cuerpo: convención universal |
| 401 Unauthorized | token malo o ausente | el código HTTP estándar de "identifícate" |
| Fail-closed | or not APP_TOKEN | principio de seguridad: ante la duda, denegar |
| Pydantic model | class Peticion(BaseModel) | validación de datos de entrada en casi todo Python moderno |
| lifespan / warm-up | preparar el motor 1 vez | cargar modelos/conexiones al arrancar, no por petición |
| Secret management | .env → Secret Manager | higiene obligatoria: credenciales fuera del código |
Para tu CV, en una frase: «Expuse el agente como una API REST con FastAPI, autenticada por token con política fail-closed, reutilizando el motor sin cambios y devolviendo la traza de pasos en JSON para el frontend.» Eso es lenguaje de backend real, no de notebook.
La API vive y protege. El siguiente paso es empaquetarla en Docker y desplegarla en Cloud Run con los secretos en Secret Manager — el mismo camino del Art.2, que ya recorriste una vez. Luego, el frontend (Fase 6) y lucirlo en el portfolio.