|
|
""" |
|
|
Module: api.app |
|
|
Description: FastAPI application for Cidadão.AI transparency platform |
|
|
Author: Anderson H. Silva |
|
|
Date: 2025-01-24 |
|
|
License: Proprietary - All rights reserved |
|
|
""" |
|
|
|
|
|
import asyncio |
|
|
from contextlib import asynccontextmanager |
|
|
from typing import Dict, Any |
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
|
|
|
from fastapi.responses import JSONResponse |
|
|
from fastapi.openapi.docs import get_swagger_ui_html |
|
|
from fastapi.openapi.utils import get_openapi |
|
|
|
|
|
from src.core import get_logger, settings |
|
|
from src.core.exceptions import CidadaoAIError, create_error_response |
|
|
from src.core.audit import audit_logger, AuditEventType, AuditSeverity, AuditContext |
|
|
from src.api.routes import investigations, analysis, reports, health, auth, oauth, audit, chat, websocket_chat |
|
|
from src.api.middleware.rate_limiting import RateLimitMiddleware |
|
|
from src.api.middleware.authentication import AuthenticationMiddleware |
|
|
from src.api.middleware.logging_middleware import LoggingMiddleware |
|
|
from src.api.middleware.security import SecurityMiddleware |
|
|
from src.api.middleware.compression import CompressionMiddleware |
|
|
|
|
|
|
|
|
logger = get_logger(__name__) |
|
|
|
|
|
|
|
|
@asynccontextmanager |
|
|
async def lifespan(app: FastAPI): |
|
|
"""Application lifespan manager with enhanced audit logging.""" |
|
|
|
|
|
logger.info("cidadao_ai_api_starting") |
|
|
|
|
|
|
|
|
await audit_logger.log_event( |
|
|
event_type=AuditEventType.SYSTEM_STARTUP, |
|
|
message=f"Cidadão.AI API started (env: {settings.app_env})", |
|
|
severity=AuditSeverity.LOW, |
|
|
details={ |
|
|
"version": "1.0.0", |
|
|
"environment": settings.app_env, |
|
|
"debug": settings.debug, |
|
|
"security_enabled": True |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
yield |
|
|
|
|
|
|
|
|
logger.info("cidadao_ai_api_shutting_down") |
|
|
|
|
|
|
|
|
await audit_logger.log_event( |
|
|
event_type=AuditEventType.SYSTEM_SHUTDOWN, |
|
|
message="Cidadão.AI API shutting down", |
|
|
severity=AuditSeverity.LOW |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="Cidadão.AI API", |
|
|
description=""" |
|
|
**Plataforma de Transparência Pública com IA** |
|
|
|
|
|
API para investigação inteligente de dados públicos brasileiros. |
|
|
|
|
|
## Funcionalidades |
|
|
|
|
|
* **Investigação** - Detecção de anomalias e irregularidades |
|
|
* **Análise** - Padrões e correlações em dados públicos |
|
|
* **Relatórios** - Geração de relatórios em linguagem natural |
|
|
* **Transparência** - Acesso democrático a informações governamentais |
|
|
|
|
|
## Agentes Especializados |
|
|
|
|
|
* **InvestigatorAgent** - Detecção de anomalias com IA explicável |
|
|
* **AnalystAgent** - Análise de padrões e correlações |
|
|
* **ReporterAgent** - Geração de relatórios inteligentes |
|
|
|
|
|
## Fontes de Dados |
|
|
|
|
|
* Portal da Transparência do Governo Federal |
|
|
* Contratos, despesas, licitações e convênios públicos |
|
|
* Dados de servidores e empresas sancionadas |
|
|
""", |
|
|
version="1.0.0", |
|
|
contact={ |
|
|
"name": "Cidadão.AI", |
|
|
"url": "https://github.com/anderson-ufrj/cidadao.ai", |
|
|
"email": "[email protected]", |
|
|
}, |
|
|
license_info={ |
|
|
"name": "Proprietary", |
|
|
"url": "https://github.com/anderson-ufrj/cidadao.ai/blob/main/LICENSE", |
|
|
}, |
|
|
lifespan=lifespan, |
|
|
docs_url=None, |
|
|
redoc_url=None, |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware(SecurityMiddleware) |
|
|
app.add_middleware(LoggingMiddleware) |
|
|
app.add_middleware(RateLimitMiddleware) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CompressionMiddleware, |
|
|
minimum_size=1024, |
|
|
compression_level=6 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=settings.cors_origins, |
|
|
allow_credentials=settings.cors_allow_credentials, |
|
|
allow_methods=settings.cors_allow_methods, |
|
|
allow_headers=settings.cors_allow_headers, |
|
|
expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def custom_openapi(): |
|
|
"""Generate custom OpenAPI schema.""" |
|
|
if app.openapi_schema: |
|
|
return app.openapi_schema |
|
|
|
|
|
openapi_schema = get_openapi( |
|
|
title=app.title, |
|
|
version=app.version, |
|
|
description=app.description, |
|
|
routes=app.routes, |
|
|
) |
|
|
|
|
|
|
|
|
openapi_schema["info"]["x-logo"] = { |
|
|
"url": "https://cidadao.ai/logo.png" |
|
|
} |
|
|
|
|
|
|
|
|
openapi_schema["servers"] = [ |
|
|
{"url": "http://localhost:8000", "description": "Development server"}, |
|
|
{"url": "https://api.cidadao.ai", "description": "Production server"}, |
|
|
] |
|
|
|
|
|
|
|
|
openapi_schema["components"]["securitySchemes"] = { |
|
|
"ApiKeyAuth": { |
|
|
"type": "apiKey", |
|
|
"in": "header", |
|
|
"name": "X-API-Key" |
|
|
}, |
|
|
"BearerAuth": { |
|
|
"type": "http", |
|
|
"scheme": "bearer", |
|
|
"bearerFormat": "JWT" |
|
|
} |
|
|
} |
|
|
|
|
|
app.openapi_schema = openapi_schema |
|
|
return app.openapi_schema |
|
|
|
|
|
|
|
|
app.openapi = custom_openapi |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/docs", include_in_schema=False) |
|
|
async def custom_swagger_ui_html(): |
|
|
"""Custom Swagger UI with branding.""" |
|
|
return get_swagger_ui_html( |
|
|
openapi_url=app.openapi_url, |
|
|
title=f"{app.title} - Documentação", |
|
|
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", |
|
|
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", |
|
|
swagger_favicon_url="https://cidadao.ai/favicon.ico", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
app.include_router( |
|
|
health.router, |
|
|
prefix="/health", |
|
|
tags=["Health Check"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
auth.router, |
|
|
prefix="/auth", |
|
|
tags=["Authentication"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
oauth.router, |
|
|
prefix="/auth/oauth", |
|
|
tags=["OAuth2"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
audit.router, |
|
|
prefix="/audit", |
|
|
tags=["Audit & Security"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
investigations.router, |
|
|
prefix="/api/v1/investigations", |
|
|
tags=["Investigations"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
analysis.router, |
|
|
prefix="/api/v1/analysis", |
|
|
tags=["Analysis"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
reports.router, |
|
|
prefix="/api/v1/reports", |
|
|
tags=["Reports"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
chat.router, |
|
|
prefix="/api/v1/chat", |
|
|
tags=["Chat"] |
|
|
) |
|
|
|
|
|
app.include_router( |
|
|
websocket_chat.router, |
|
|
prefix="/api/v1", |
|
|
tags=["WebSocket"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(CidadaoAIError) |
|
|
async def cidadao_ai_exception_handler(request, exc: CidadaoAIError): |
|
|
"""Handle CidadãoAI custom exceptions.""" |
|
|
logger.error( |
|
|
"api_exception_occurred", |
|
|
error_type=type(exc).__name__, |
|
|
error_message=exc.message, |
|
|
error_details=exc.details, |
|
|
path=request.url.path, |
|
|
method=request.method, |
|
|
) |
|
|
|
|
|
|
|
|
status_code_map = { |
|
|
"ValidationError": 400, |
|
|
"DataNotFoundError": 404, |
|
|
"AuthenticationError": 401, |
|
|
"UnauthorizedError": 403, |
|
|
"RateLimitError": 429, |
|
|
"LLMError": 503, |
|
|
"TransparencyAPIError": 502, |
|
|
"AgentExecutionError": 500, |
|
|
} |
|
|
|
|
|
status_code = status_code_map.get(exc.error_code, 500) |
|
|
error_response = create_error_response(exc, status_code) |
|
|
|
|
|
return JSONResponse( |
|
|
status_code=status_code, |
|
|
content=error_response |
|
|
) |
|
|
|
|
|
|
|
|
@app.exception_handler(HTTPException) |
|
|
async def http_exception_handler(request, exc: HTTPException): |
|
|
"""Enhanced HTTP exception handler with audit logging.""" |
|
|
|
|
|
|
|
|
context = AuditContext( |
|
|
ip_address=request.client.host if request.client else "unknown", |
|
|
user_agent=request.headers.get("user-agent"), |
|
|
host=request.headers.get("host") |
|
|
) |
|
|
|
|
|
|
|
|
if exc.status_code in [401, 403, 429]: |
|
|
await audit_logger.log_event( |
|
|
event_type=AuditEventType.UNAUTHORIZED_ACCESS, |
|
|
message=f"HTTP {exc.status_code}: {exc.detail}", |
|
|
severity=AuditSeverity.MEDIUM if exc.status_code != 429 else AuditSeverity.HIGH, |
|
|
success=False, |
|
|
error_code=str(exc.status_code), |
|
|
error_message=exc.detail, |
|
|
context=context |
|
|
) |
|
|
|
|
|
logger.warning( |
|
|
"http_exception_occurred", |
|
|
status_code=exc.status_code, |
|
|
detail=exc.detail, |
|
|
path=request.url.path, |
|
|
method=request.method, |
|
|
) |
|
|
|
|
|
return JSONResponse( |
|
|
status_code=exc.status_code, |
|
|
content={ |
|
|
"status": "error", |
|
|
"status_code": exc.status_code, |
|
|
"error": { |
|
|
"error": "HTTPException", |
|
|
"message": exc.detail, |
|
|
"details": {} |
|
|
} |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
@app.exception_handler(Exception) |
|
|
async def general_exception_handler(request, exc: Exception): |
|
|
"""Enhanced general exception handler with audit logging.""" |
|
|
|
|
|
|
|
|
context = AuditContext( |
|
|
ip_address=request.client.host if request.client else "unknown", |
|
|
user_agent=request.headers.get("user-agent"), |
|
|
host=request.headers.get("host") |
|
|
) |
|
|
|
|
|
await audit_logger.log_event( |
|
|
event_type=AuditEventType.API_ERROR, |
|
|
message=f"Unhandled exception: {str(exc)}", |
|
|
severity=AuditSeverity.HIGH, |
|
|
success=False, |
|
|
error_message=str(exc), |
|
|
details={"error_type": type(exc).__name__}, |
|
|
context=context |
|
|
) |
|
|
|
|
|
logger.error( |
|
|
"unexpected_exception_occurred", |
|
|
error_type=type(exc).__name__, |
|
|
error_message=str(exc), |
|
|
path=request.url.path, |
|
|
method=request.method, |
|
|
) |
|
|
|
|
|
|
|
|
if settings.app_env == "production": |
|
|
return JSONResponse( |
|
|
status_code=500, |
|
|
content={ |
|
|
"status": "error", |
|
|
"status_code": 500, |
|
|
"error": { |
|
|
"error": "InternalServerError", |
|
|
"message": "An unexpected error occurred", |
|
|
"details": {} |
|
|
} |
|
|
} |
|
|
) |
|
|
else: |
|
|
return JSONResponse( |
|
|
status_code=500, |
|
|
content={ |
|
|
"status": "error", |
|
|
"status_code": 500, |
|
|
"error": { |
|
|
"error": "InternalServerError", |
|
|
"message": f"An unexpected error occurred: {str(exc)}", |
|
|
"details": {"error_type": type(exc).__name__} |
|
|
} |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", include_in_schema=False) |
|
|
async def root(): |
|
|
"""Root endpoint with API information.""" |
|
|
return { |
|
|
"message": "Cidadão.AI - Plataforma de Transparência Pública", |
|
|
"version": "1.0.0", |
|
|
"description": "API para investigação inteligente de dados públicos brasileiros", |
|
|
"documentation": "/docs", |
|
|
"health": "/health", |
|
|
"status": "operational" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/v1/info", tags=["General"]) |
|
|
async def api_info(): |
|
|
"""Get API information and capabilities.""" |
|
|
return { |
|
|
"api": { |
|
|
"name": "Cidadão.AI API", |
|
|
"version": "1.0.0", |
|
|
"description": "Plataforma de transparência pública com IA", |
|
|
}, |
|
|
"agents": { |
|
|
"investigator": { |
|
|
"description": "Detecção de anomalias e irregularidades", |
|
|
"capabilities": [ |
|
|
"Anomalias de preço", |
|
|
"Concentração de fornecedores", |
|
|
"Padrões temporais suspeitos", |
|
|
"Contratos duplicados", |
|
|
"Irregularidades de pagamento" |
|
|
] |
|
|
}, |
|
|
"analyst": { |
|
|
"description": "Análise de padrões e correlações", |
|
|
"capabilities": [ |
|
|
"Tendências de gastos", |
|
|
"Padrões organizacionais", |
|
|
"Comportamento de fornecedores", |
|
|
"Análise sazonal", |
|
|
"Métricas de eficiência" |
|
|
] |
|
|
}, |
|
|
"reporter": { |
|
|
"description": "Geração de relatórios inteligentes", |
|
|
"capabilities": [ |
|
|
"Relatórios executivos", |
|
|
"Análise detalhada", |
|
|
"Múltiplos formatos", |
|
|
"Linguagem natural" |
|
|
] |
|
|
} |
|
|
}, |
|
|
"data_sources": [ |
|
|
"Portal da Transparência", |
|
|
"Contratos públicos", |
|
|
"Despesas governamentais", |
|
|
"Licitações", |
|
|
"Convênios", |
|
|
"Servidores públicos" |
|
|
], |
|
|
"formats": [ |
|
|
"JSON", |
|
|
"Markdown", |
|
|
"HTML", |
|
|
"PDF (planned)" |
|
|
] |
|
|
} |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
|
|
|
uvicorn.run( |
|
|
"src.api.app:app", |
|
|
host=settings.host, |
|
|
port=settings.port, |
|
|
reload=settings.debug, |
|
|
workers=settings.workers if not settings.debug else 1, |
|
|
log_level=settings.log_level.lower(), |
|
|
) |