Spaces:
Sleeping
Sleeping
Riley
feat: Major scraper enhancements - consistent format, better extraction, per-month costs
2ae7490 | """Main FastAPI application for Grant Analyst.""" | |
| import uuid | |
| import hashlib | |
| import json | |
| import time | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.middleware.gzip import GZipMiddleware | |
| from fastapi.responses import JSONResponse, Response | |
| import logging | |
| from src.api import feedback_router, health_router, qa_router, websocket_router, translate_router | |
| from src.logging.logger import get_logger | |
| from src.config.versions import get_all_versions | |
| from src.monitoring import get_metrics_summary, export_prometheus, RequestTimer | |
| from analyzer.config import get_settings | |
| # Initialize settings | |
| settings = get_settings() | |
| # Initialize FastAPI app | |
| app = FastAPI( | |
| title="Grant Analyst API", | |
| description="AI-powered grant eligibility and QA system", | |
| version="1.0.0" | |
| ) | |
| # Initialize logger | |
| logger = get_logger() | |
| # Configure CORS with settings | |
| if settings.ALLOWED_ORIGINS: | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=settings.ALLOWED_ORIGINS, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| logger.info(f"CORS enabled for origins: {settings.ALLOWED_ORIGINS}") | |
| else: | |
| logger.warning("CORS disabled - no ALLOWED_ORIGINS configured") | |
| # Add GZip compression middleware | |
| # Compress responses larger than 1000 bytes | |
| app.add_middleware( | |
| GZipMiddleware, | |
| minimum_size=1000 # Only compress responses >= 1KB | |
| ) | |
| async def startup_event(): | |
| """Initialize on startup.""" | |
| versions = get_all_versions() | |
| print(f"Starting Grant Analyst {versions['deployment_id']}") | |
| # Initialize MongoDB connection | |
| try: | |
| from src.database import get_mongo_client | |
| get_mongo_client() | |
| logger.info("MongoDB connection initialized") | |
| except Exception as e: | |
| logger.warning(f"MongoDB connection initialization warning: {e}") | |
| async def shutdown_event(): | |
| """Cleanup on shutdown.""" | |
| from src.database import close_mongo_connection | |
| close_mongo_connection() | |
| logger.info("Application shutdown complete") | |
| async def add_caching_and_etag(request: Request, call_next): | |
| """ | |
| Add caching headers and ETag support to responses. | |
| Implements: | |
| - Cache-Control headers for GET requests | |
| - ETag generation based on response body | |
| - 304 Not Modified responses for matching ETags | |
| - Request latency tracking | |
| """ | |
| # Generate request ID for logging | |
| request_id = str(uuid.uuid4()) | |
| request.state.request_id = request_id | |
| # Track request latency | |
| start_time = time.perf_counter() | |
| # Process the request | |
| response = await call_next(request) | |
| # Record latency (exclude /metrics endpoint to avoid recursion) | |
| if not request.url.path.startswith("/metrics"): | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| from src.monitoring import record_latency | |
| record_latency(request.url.path, latency_ms) | |
| # Only add caching/ETag for GET requests and successful responses | |
| if request.method == "GET" and response.status_code == 200: | |
| # Skip caching for specific endpoints (e.g., health checks, dynamic data) | |
| skip_cache_paths = ["/health/detailed", "/ws/"] | |
| should_skip = any(path in str(request.url.path) for path in skip_cache_paths) | |
| if not should_skip: | |
| # Read response body to generate ETag | |
| response_body = b"" | |
| async for chunk in response.body_iterator: | |
| response_body += chunk | |
| # Generate ETag from response body hash | |
| etag = hashlib.md5(response_body).hexdigest() | |
| # Check if client sent If-None-Match header | |
| client_etag = request.headers.get("If-None-Match") | |
| if client_etag == f'"{etag}"': | |
| # Content hasn't changed, return 304 Not Modified | |
| return Response( | |
| status_code=304, | |
| headers={ | |
| "ETag": f'"{etag}"', | |
| "Cache-Control": "public, max-age=300", # 5 minutes | |
| } | |
| ) | |
| # Add caching headers to response | |
| response.headers["Cache-Control"] = "public, max-age=300" # 5 minutes | |
| response.headers["ETag"] = f'"{etag}"' | |
| # Return new response with body | |
| return Response( | |
| content=response_body, | |
| status_code=response.status_code, | |
| headers=dict(response.headers), | |
| media_type=response.media_type | |
| ) | |
| return response | |
| # Register routers | |
| app.include_router(health_router) | |
| app.include_router(feedback_router) | |
| app.include_router(qa_router) | |
| app.include_router(websocket_router) | |
| app.include_router(translate_router) | |
| async def root(): | |
| """Root endpoint.""" | |
| versions = get_all_versions() | |
| return { | |
| "message": "Grant Analyst API", | |
| "version": versions["deployment_id"], | |
| "endpoints": { | |
| "health": "/health", | |
| "health_detailed": "/health/detailed", | |
| "feedback": "/feedback", | |
| "qa": "/qa", | |
| "websocket": "/ws/query", | |
| "translate": "/translate", | |
| "metrics": "/metrics", | |
| "metrics_json": "/metrics/json", | |
| "docs": "/docs" | |
| } | |
| } | |
| async def metrics_prometheus(): | |
| """ | |
| Export metrics in Prometheus format. | |
| Returns: | |
| Prometheus-formatted metrics text | |
| """ | |
| from fastapi.responses import PlainTextResponse | |
| prometheus_text = export_prometheus() | |
| return PlainTextResponse(content=prometheus_text, media_type="text/plain; version=0.0.4") | |
| async def metrics_json(): | |
| """ | |
| Get metrics summary as JSON. | |
| Returns: | |
| JSON with all metrics including: | |
| - Latency (P50/P95/P99) per endpoint | |
| - Token usage | |
| - Cache hit rates | |
| - Model distribution | |
| """ | |
| return get_metrics_summary() | |
| async def general_exception_handler(request: Request, exc: Exception): | |
| """Handle general exceptions.""" | |
| request_id = getattr(request.state, "request_id", "unknown") | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "error": str(exc), | |
| "request_id": request_id | |
| } | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "src.main:app", | |
| host="0.0.0.0", | |
| port=8000, | |
| workers=4, | |
| reload=False | |
| ) | |