import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware from app.config import settings from app.dependencies.auth import get_current_user from app.models.database import init_db, close_db from app.api import router as api_router, kb_router, analysis_router from app.services.gemini_service import GeminiService from app.services.reference_docs import ReferenceDocsService from app.services.analysis_service import AnalysisService from app.services.knowledge_base_service import KnowledgeBaseService # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) logger = logging.getLogger(__name__) class HealthCheckFilter(logging.Filter): """Filter out health check endpoint logs from uvicorn access log.""" def filter(self, record: logging.LogRecord) -> bool: if "GET /health" in record.getMessage(): return False return True logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter()) @asynccontextmanager async def lifespan(app: FastAPI): """Initialize services on startup and cleanup on shutdown.""" # Validate settings settings.validate() # Initialize database print("Initializing database connection...") db_available = False try: await init_db() print("Database initialized successfully") db_available = True except Exception as e: logger.warning(f"Database initialization failed (may not be available): {e}") print("Warning: Database not available - running in stateless mode") # Initialize services print("Loading reference documents...") reference_docs = ReferenceDocsService(settings.REFERENCE_DOCS_PATH) if db_available: try: from app.models.database import async_session_factory async with async_session_factory() as session: print("Loading specs from database...") await reference_docs.load_specs_from_db(session) except Exception as e: logger.warning(f"Failed to load specs from DB (falling back to files): {e}") doc_summary = reference_docs.get_context_summary() print(f" Brand documents: {len(doc_summary['brand_files'])} files ({doc_summary['brand_context_length']} chars)") print(f" Channel documents: {len(doc_summary['channel_files'])} files ({doc_summary['channel_context_length']} chars)") print("Initializing Gemini service...") gemini_service = GeminiService(settings.GEMINI_API_KEY) print("Initializing analysis service...") analysis_service = AnalysisService(gemini_service, reference_docs) app.state.analysis_service = analysis_service # Initialize Knowledge Base service (requires LlamaParse API key) knowledge_base_service = None if settings.LLAMA_CLOUD_API_KEY: from app.services.llamaparse_service import LlamaParseService print("Initializing LlamaParse service...") llamaparse_service = LlamaParseService(settings.LLAMA_CLOUD_API_KEY, settings.LLAMA_CLOUD_BASE_URL) knowledge_base_service = KnowledgeBaseService(llamaparse_service, gemini_service, reference_docs) app.state.knowledge_base_service = knowledge_base_service print("Knowledge Base pipeline ready!") if db_available: import asyncio as _asyncio from app.models.database import async_session_factory as _session_factory from app.repositories.knowledge_base_repository import KnowledgeBaseRepository as _KBRepo try: async with _session_factory() as _session: _repo = _KBRepo(_session) _stale = await _repo.get_all_stale_active_jobs() if _stale: print(f"Resuming {len(_stale)} interrupted processing job(s)...") for _job in _stale: _kb = await _repo.get_knowledge_base(_job.knowledge_base_id) if _kb: _asyncio.create_task( knowledge_base_service.process_documents( kb_id=_job.knowledge_base_id, job_id=_job.id, agent_key=_kb.agent_key, user_id=_job.triggered_by_id, user_name=_job.triggered_by_name, ) ) print(f" Resumed: {_kb.display_name} (job {_job.id}, was {_job.status})") except Exception as _e: logger.warning(f"Startup job recovery failed: {_e}") else: print("LLAMA_CLOUD_API_KEY not set - Knowledge Base processing pipeline disabled") print("Backend ready!") yield # Cleanup on shutdown print("Shutting down...") await close_db() # Create FastAPI app app = FastAPI( title="ModComms Proof Review API", description="AI-powered proof review backend for Barclays marketing materials", version="1.0.0", lifespan=lifespan, ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS.split(","), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include API routes app.include_router(api_router, prefix="/api") app.include_router(kb_router, prefix="/api") app.include_router(analysis_router, prefix="/api") @app.get("/health") async def health_check(): """Health check endpoint.""" return {"status": "healthy", "service": "modcomms-backend"} @app.get("/info") async def info(user: dict = Depends(get_current_user)): """Get backend information. Requires authentication.""" analysis_service = getattr(app.state, "analysis_service", None) if analysis_service: ref_docs = analysis_service.reference_docs doc_summary = ref_docs.get_context_summary() return { "status": "ready", "user": user.get("name", "Unknown"), "agents": ["Risk & Control Agent", "Brand Agent", "Channel Best Practices Agent", "Channel Tech Specs Agent"], "reference_docs": doc_summary, } return {"status": "initializing", "user": user.get("name", "Unknown")}