Zeetay
prep for hf deploy
08b77b5
Raw
History Blame Contribute Delete
3.88 kB
"""FastAPI app for the hosted public demo.
Endpoints:
- GET /health -> liveness probe
- GET /stats -> live counts (runs, golden, flagged) for the frontend strip
- POST /run -> full agent loop (rate limited), returns outputs + scores
Run locally: uvicorn api.main:app --reload
"""
import os
from contextlib import asynccontextmanager
from typing import Any
from dotenv import load_dotenv
load_dotenv() # pick up GROQ_API_KEY / CORS_ORIGINS from .env if present
from fastapi import FastAPI, HTTPException, Request # noqa: E402
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
from pydantic import BaseModel, Field # noqa: E402
from slowapi import Limiter, _rate_limit_exceeded_handler # noqa: E402
from slowapi.errors import RateLimitExceeded # noqa: E402
from slowapi.util import get_remote_address # noqa: E402
from agent.core import Agent # noqa: E402 (after load_dotenv on purpose)
from seed.loader import load_seed_if_empty # noqa: E402
# one shared agent (and Store/Memory) per process
_agent: Agent | None = None
def get_agent() -> Agent:
global _agent
if _agent is None:
_agent = Agent()
return _agent
def _cors_origins() -> list[str]:
"""Origins from CORS_ORIGINS (comma-separated), always plus localhost:3000.
If CORS_ORIGINS is unset, default to allow-all ("*").
"""
raw = os.getenv("CORS_ORIGINS")
if not raw:
return ["*"]
origins = [o.strip() for o in raw.split(",") if o.strip()]
if "http://localhost:3000" not in origins:
origins.append("http://localhost:3000")
return origins
# seed golden + memory on a fresh deployment so the first visitor isn't cold
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
agent = get_agent()
seeded = load_seed_if_empty(agent.store, agent.memory)
if seeded:
print(f"[startup] Seeded {seeded} golden/memory example(s).")
except Exception as exc: # noqa: BLE001 - never block startup on seeding.
print(f"[startup] Seed skipped: {exc}")
yield
limiter = Limiter(key_func=get_remote_address)
app = FastAPI(title="Self-Improving Ad Copy Agent", version="1.0.0", lifespan=lifespan)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins(),
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
class BrandBrief(BaseModel):
brand: str = Field(..., examples=["FitFuel"])
product: str = Field(..., examples=["High-protein meal replacement shake"])
audience: str = Field(..., examples=["Busy professionals aged 25-40"])
tone: str = Field(..., examples=["Energetic and no-nonsense"])
goal: str = Field(..., examples=["Drive trial purchases"])
@app.get("/")
def root() -> dict[str, str]:
return {"status": "ok", "endpoint": "POST /run with a brand brief"}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/stats")
def stats() -> dict[str, int]:
"""Live counts for the frontend stats strip."""
store = get_agent().store
return {
"runs": store.count_runs(),
"golden": store.count_golden(),
"flagged": store.count_flagged(),
}
@app.post("/run")
@limiter.limit("10/minute")
def run(request: Request, brief: BrandBrief) -> dict[str, Any]:
"""Trigger the full agent loop for a brand brief (rate limited per IP)."""
try:
agent = get_agent()
return agent.run(brief.model_dump())
except RuntimeError as exc: # e.g. missing GROQ_API_KEY
raise HTTPException(status_code=500, detail=str(exc)) from exc
except Exception as exc: # noqa: BLE001 - surface generation/judge failures
raise HTTPException(status_code=502, detail=f"Agent run failed: {exc}") from exc