Spaces:
Sleeping
Sleeping
| """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 | |
| 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"]) | |
| def root() -> dict[str, str]: | |
| return {"status": "ok", "endpoint": "POST /run with a brand brief"} | |
| def health() -> dict[str, str]: | |
| return {"status": "ok"} | |
| 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(), | |
| } | |
| 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 | |