""" FastAPI server for the AI Agent. Exposes REST endpoints for agent interaction with A2UI streaming support. """ import os import json from typing import Optional from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import uvicorn from agent import get_agent, A2UIMessage # ============================================================================ # Configuration # ============================================================================ # Get configuration from environment FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000") API_PORT = int(os.getenv("API_PORT", 8000)) API_HOST = os.getenv("API_HOST", "0.0.0.0") DEBUG = os.getenv("DEBUG", "False").lower() == "true" # ============================================================================ # FastAPI App Setup # ============================================================================ app = FastAPI( title="AI Agent API", description="API for AI Agent with A2UI streaming support", version="1.0.0" ) # ============================================================================ # CORS Configuration (for frontend-backend communication) # ============================================================================ cors_origins = [ "http://localhost:3000", "http://localhost:5173", # Vite default FRONTEND_URL, ] if DEBUG: cors_origins.append("*") app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============================================================================ # Serve Static Frontend Files # ============================================================================ frontend_dist = Path(__file__).parent.parent / "frontend" / "dist" if frontend_dist.exists(): app.mount("/static", StaticFiles(directory=str(frontend_dist), html=False), name="static") static_available = True else: static_available = False # ============================================================================ # Request/Response Models # ============================================================================ class ChatRequest(BaseModel): """Chat request model.""" message: str class HealthResponse(BaseModel): """Health check response.""" status: str message: str # ============================================================================ # API Routes # ============================================================================ @app.get("/health", response_model=HealthResponse) async def health_check(): """ Health check endpoint. Returns: HealthResponse: Status of the API """ return { "status": "healthy", "message": "AI Agent API is running" } @app.post("/chat") async def chat(request: ChatRequest): """ Chat endpoint with A2UI streaming. Processes user message through the AI Agent and streams A2UI events. Args: request: ChatRequest containing user message Returns: StreamingResponse: Server-Sent Events stream of A2UI messages """ if not request.message or not request.message.strip(): raise HTTPException(status_code=400, detail="Message cannot be empty") try: agent = get_agent() def event_generator(): """Generate A2UI events from agent processing.""" try: for a2ui_message in agent.process_message(request.message.strip()): # Convert A2UIMessage to dict event_data = a2ui_message.to_dict() # Format as JSON and send as Server-Sent Event json_data = json.dumps(event_data) yield f"data: {json_data}\n\n" except Exception as e: import traceback error_msg = traceback.format_exc() print(f"ERROR in event_generator: {error_msg}") raise return StreamingResponse( event_generator(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", } ) except Exception as e: import traceback error_msg = traceback.format_exc() print(f"ERROR in /chat: {error_msg}") raise HTTPException( status_code=500, detail=f"Error processing message: {str(e)}" ) @app.delete("/chat/history") async def clear_history(): """ Clear chat history. Returns: dict: Confirmation message """ try: agent = get_agent() agent.clear_history() return { "status": "success", "message": "Chat history cleared" } except Exception as e: raise HTTPException( status_code=500, detail=f"Error clearing history: {str(e)}" ) @app.get("/tools") async def get_tools(): """ Get available tools. Returns: dict: List of available tools """ try: agent = get_agent() return { "tools": agent.available_tools } except Exception as e: import traceback error_msg = traceback.format_exc() print(f"ERROR in /tools: {error_msg}") raise HTTPException( status_code=500, detail=f"Error getting tools: {str(e)}" ) # ============================================================================ # Serve Static Frontend Files (mount at root with lower precedence) # ============================================================================ if frontend_dist.exists(): app.mount("/", StaticFiles(directory=str(frontend_dist), html=True), name="static") static_available = True else: static_available = False # ============================================================================ # Main # ============================================================================ if __name__ == "__main__": uvicorn.run( app, host=API_HOST, port=API_PORT, reload=DEBUG )