"""FastAPI application factory for FusionAGI Dvādaśa API.""" from __future__ import annotations import os import time from collections import defaultdict from contextlib import asynccontextmanager from typing import Any from fusionagi._logger import logger from fusionagi.api.dependencies import SessionStore, default_orchestrator, set_app_state def create_app( adapter: Any = None, cors_origins: list[str] | None = None, ) -> Any: """Create FastAPI app with Dvādaśa routes. Args: adapter: Optional LLMAdapter for head/Witness LLM calls. cors_origins: Optional list of CORS allowed origins. """ try: from fastapi import FastAPI, Request, Response from starlette.middleware.base import BaseHTTPMiddleware except ImportError as e: raise ImportError("Install with: pip install fusionagi[api]") from e # --- Lifespan (replaces deprecated on_event) --- @asynccontextmanager async def lifespan(application: FastAPI): # type: ignore[type-arg] """Startup / shutdown lifecycle.""" adapter_inner = getattr(application.state, "llm_adapter", None) orch, bus = default_orchestrator(adapter_inner) store = SessionStore() set_app_state(orch, bus, store) application.state._dvadasa_ready = True logger.info("FusionAGI Dvādaśa API started") yield logger.info("FusionAGI Dvādaśa API shutdown") app = FastAPI( title="FusionAGI Dvādaśa API", description=( "12-headed multi-agent orchestration API.\n\n" "## Authentication\n" "Set `FUSIONAGI_API_KEY` to require Bearer token auth on all `/v1/` routes.\n\n" "## Rate Limiting\n" "Default: 120 requests/minute per client IP. " "Configure via `FUSIONAGI_RATE_LIMIT` (requests) and " "`FUSIONAGI_RATE_WINDOW` (seconds) env vars." ), version="0.1.0", lifespan=lifespan, ) app.state.llm_adapter = adapter from fusionagi.api.dependencies import set_default_adapter set_default_adapter(adapter) # --- Auth middleware --- api_key = os.environ.get("FUSIONAGI_API_KEY") class AuthMiddleware(BaseHTTPMiddleware): """Bearer token authentication for /v1/ routes.""" async def dispatch(self, request: Request, call_next: Any) -> Response: if api_key and request.url.path.startswith("/v1/"): auth = request.headers.get("authorization", "") if not auth.startswith("Bearer ") or auth[7:].strip() != api_key: return Response( content='{"detail":"Invalid or missing API key"}', status_code=401, media_type="application/json", ) return await call_next(request) # type: ignore[no-any-return] app.add_middleware(AuthMiddleware) # --- Rate limiting middleware --- rate_limit = int(os.environ.get("FUSIONAGI_RATE_LIMIT", "120")) rate_window = float(os.environ.get("FUSIONAGI_RATE_WINDOW", "60")) _buckets: dict[str, list[float]] = defaultdict(list) class RateLimitMiddleware(BaseHTTPMiddleware): """Per-IP sliding window rate limiter.""" async def dispatch(self, request: Request, call_next: Any) -> Response: client_ip = request.client.host if request.client else "unknown" now = time.monotonic() cutoff = now - rate_window _buckets[client_ip] = [t for t in _buckets[client_ip] if t > cutoff] if len(_buckets[client_ip]) >= rate_limit: return Response( content='{"detail":"Rate limit exceeded"}', status_code=429, media_type="application/json", headers={"Retry-After": str(int(rate_window))}, ) _buckets[client_ip].append(now) return await call_next(request) # type: ignore[no-any-return] app.add_middleware(RateLimitMiddleware) # --- Routes --- from fusionagi.api.routes import router as api_router app.include_router(api_router, prefix="/v1", tags=["dvadasa"]) if cors_origins is not None: try: from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_methods=["*"], allow_headers=["*"], ) except ImportError: pass return app # Default app instance for uvicorn/gunicorn app = create_app()