Spaces:
Running
Running
| """ | |
| backend/main.py β Entrypoint principale dell'Agente AI con migrazione automatica. | |
| """ | |
| import os | |
| import sys | |
| import logging | |
| import asyncio | |
| import argparse | |
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from api.version import RUNTIME_VERSION | |
| # Configurazione Logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| datefmt="%H:%M:%S", | |
| ) | |
| _logger = logging.getLogger("agente_ai.main") | |
| app = FastAPI( | |
| title="Agente AI API", | |
| description="Backend per l'orchestrazione di agenti autonomi e tool-use.", | |
| version=RUNTIME_VERSION, | |
| ) | |
| # CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ P17-F1: RLS Fix & Auto-Migration ββββββββββββββββββββββββββββββββββββββββββ | |
| async def _run_auto_migration(): | |
| """Esegue la migrazione SQL per RLS e indici al boot (Z-GAP-1/2/3/4).""" | |
| db_host = os.getenv("SUPABASE_DB_HOST") | |
| db_pass = os.getenv("SUPABASE_DB_PASSWORD") | |
| if not db_host or not db_pass: | |
| _logger.warning("BOOT: Migration skipped β SUPABASE_DB_HOST/PASSWORD non configurati.") | |
| return | |
| # Lista completa dal set SENSITIVE in state.py | |
| sensitive_keys = [ | |
| 'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', | |
| 'HF_TOKEN', 'HUGGINGFACE_API_KEY', 'GH_TOKEN', 'GITHUB_TOKEN', | |
| 'QDRANT_API_KEY', 'DATABASE_URL', 'SESSION_SECRET', 'SECRET_KEY', | |
| 'RAILWAY_TOKEN', 'SUPABASE_KEY', 'SUPABASE_ANON_KEY', | |
| 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', | |
| 'CF_API_TOKEN', 'CLOUDFLARE_API_TOKEN', 'CF_ACCOUNT_ID', | |
| 'CF_API_TOKEN_B', 'CF_ACCOUNT_ID_B', | |
| 'CEREBRAS_API_KEY', 'SAMBANOVA_API_KEY', | |
| 'VAULT_KEY', 'INTERNAL_TOKEN', 'DEPLOY_SECRET', 'WEBHOOK_TOKEN', | |
| 'TERMINAL_SECRET', 'EXEC_TOKEN', 'VITE_INTERNAL_TOKEN', 'VITE_TERMINAL_SECRET', | |
| 'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY', | |
| 'OPENROUTER_PROFILES_JSON', 'GROQ_PROFILES_JSON', 'CEREBRAS_PROFILES_JSON', | |
| 'SAMBANOVA_PROFILES_JSON', 'GEMINI_PROFILES_JSON', 'NVIDIA_PROFILES_JSON', | |
| 'HF_ROUTER_PROFILES_JSON', 'HF_MODEL', | |
| 'GH_PAGES_TOKEN', 'VERCEL_TOKEN' | |
| ] | |
| # SAFETY: sensitive_keys Γ¨ un literal Python hardcoded β nessun input utente, nessun rischio injection. | |
| keys_str = ", ".join(f"'{k}'" for k in sensitive_keys) # noqa: S608 | |
| sql = f""" | |
| -- 1. Indexing per performance | |
| CREATE INDEX IF NOT EXISTS idx_agent_memory_key ON public.agent_memory(key); | |
| CREATE INDEX IF NOT EXISTS idx_agent_memory_task_id ON public.agent_memory(task_id); | |
| -- 2. RLS Enforcement | |
| ALTER TABLE public.agent_memory ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.ai_providers ENABLE ROW LEVEL SECURITY; | |
| -- 3. Policy: Deny Anonymous Access to sensitive keys (Full SENSITIVE set) | |
| DROP POLICY IF EXISTS "Frontend Anon Access" ON public.agent_memory; | |
| CREATE POLICY "Frontend Anon Access" ON public.agent_memory | |
| FOR SELECT | |
| USING ( | |
| auth.role() = 'anon' | |
| AND key NOT IN ({keys_str}) | |
| ); | |
| -- 4. Policy: Full access for service_role | |
| DROP POLICY IF EXISTS "Service Role Full Access" ON public.agent_memory; | |
| CREATE POLICY "Service Role Full Access" ON public.agent_memory | |
| FOR ALL | |
| TO service_role | |
| USING (true) | |
| WITH CHECK (true); | |
| -- 5. Healthcheck function | |
| CREATE OR REPLACE FUNCTION public.health_check() | |
| RETURNS jsonb AS $$ | |
| BEGIN | |
| RETURN jsonb_build_object('status', 'ok', 'timestamp', now()); | |
| END; | |
| $$ LANGUAGE plpgsql SECURITY DEFINER; | |
| """ | |
| try: | |
| import psycopg2 | |
| for port in [6543, 5432]: | |
| try: | |
| conn = psycopg2.connect(f"postgresql://postgres:{db_pass}@{db_host}:{port}/postgres?sslmode=require", connect_timeout=5) | |
| cur = conn.cursor() | |
| cur.execute(sql) | |
| conn.commit() | |
| cur.close() | |
| conn.close() | |
| _logger.info(f"β BOOT: Migrazione RLS completa applicata su porta {port}.") | |
| return | |
| except Exception as e: | |
| _logger.debug(f"BOOT: Fallito tentativo su porta {port}: {e}") | |
| except Exception as e: | |
| _logger.error(f"β BOOT: Errore migrazione: {e}") | |
| def _apply_rls_fix(): | |
| s_url = os.getenv("SUPABASE_URL") | |
| s_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY") | |
| if s_url and s_key: | |
| os.environ["SUPABASE_URL"] = s_url | |
| os.environ["SUPABASE_KEY"] = s_key | |
| _apply_rls_fix() | |
| # ββ Importazione Route ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # S-GAP-FIX: Caricamento robusto dei router per evitare che un import fallito blocchi tutto. | |
| _ROUTER_MAP = { | |
| # ββ GiΓ montati βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| "state": "state", | |
| "research": "research", | |
| "agent_memory": "agent_memory", | |
| "agent": "agent", | |
| "workflows": "workflows", | |
| "exec": "exec", | |
| "vault": "vault", | |
| "browser": "browser", | |
| "deploy": "deploy", | |
| "scheduler": "scheduler", | |
| "blackboard": "blackboard", | |
| "conversations": "conversations", | |
| "benchmark": "benchmark", | |
| "files": "files", | |
| "telegram": "telegram_webhook", | |
| "marketplace": "marketplace", | |
| "plugins": "plugins", | |
| "skills": "skills", | |
| "private_state": "private_state", | |
| "auth": "auth_managed", | |
| "public_status": "public_status", | |
| "me_tasks": "me_tasks", | |
| "admin_state": "admin_state", | |
| # ββ Aggiunti ROUTER-COMPLETE (29 moduli orfani rimontati) βββββββββββββββββ | |
| "agent_checkpoint": "agent_checkpoint", | |
| "agent_telemetry": "agent_telemetry", | |
| "coding": "coding", | |
| "daemon_status": "daemon_status", | |
| "database": "database", | |
| "decision_memory": "decision_memory", | |
| "email": "email", | |
| "event_bus": "event_bus", | |
| "event_store": "event_store", | |
| "gemini_vision": "gemini_vision", | |
| "incident_registry": "incident_registry", | |
| "integrity_manager": "integrity_manager", | |
| "job_queue": "job_queue", | |
| "kernel": "kernel", | |
| "llm_cache": "llm_cache", | |
| "mcp": "mcp", | |
| "memory_router": "memory_router", | |
| "notify_bot": "notify_bot", | |
| "policy": "policy", | |
| "providers": "providers", | |
| "search": "search", | |
| "semantic_cache": "semantic_cache", | |
| "session_manager": "session_manager", | |
| "structured_log": "structured_log", | |
| "telemetry": "telemetry", | |
| "performance": "performance_rum", | |
| "terminal": "terminal", | |
| "vision": "vision", | |
| "web": "web", | |
| "webhook": "webhook", | |
| } | |
| for prefix, module_name in _ROUTER_MAP.items(): | |
| try: | |
| import importlib | |
| module = importlib.import_module(f"api.{module_name}") | |
| if hasattr(module, "router"): | |
| app.include_router(module.router) | |
| _logger.info(f"β Route montata: /api/{prefix} (da api.{module_name})") | |
| else: | |
| _logger.warning(f"β οΈ Modulo api.{module_name} non ha un attributo 'router'") | |
| except ImportError as e: | |
| _logger.error(f"β Errore import rotta {prefix} (api.{module_name}): {e}") | |
| except Exception as e: | |
| _logger.error(f"β Errore montaggio rotta {prefix}: {e}") | |
| # ββ Memory sync protocol βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Γ una factory parametrica, quindi non puΓ² stare in _ROUTER_MAP. Riutilizza il | |
| # singleton lazy di state.py per evitare una seconda istanza di MemoryManager. | |
| try: | |
| from memory.sync import create_memory_sync_router | |
| from api.state import _get_mem_manager | |
| _sync_router = create_memory_sync_router(_get_mem_manager()) | |
| app.include_router(_sync_router) | |
| _logger.info("β Route montata: /api/memory/sync (da memory.sync)") | |
| except Exception as e: | |
| _logger.error(f"β Errore montaggio memory sync router: {e}") | |
| # ββ CLI Task Execution ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def run_cli_task(task_description: str): | |
| _logger.info(f"CLI: Avvio task richiesto: {task_description[:50]}...") | |
| try: | |
| from agents.unified_loop import UnifiedAgentLoop | |
| from models.ai_client import AIClient | |
| llm = AIClient() | |
| agent = UnifiedAgentLoop(llm_client=llm) | |
| result = await agent.run(task_description) | |
| print("\nRESULT:\n", result) | |
| except Exception as e: | |
| _logger.error(f"CLI: Errore: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| sys.exit(1) | |
| # ββ Startup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def startup_event(): | |
| _logger.info("Server starting up...") | |
| try: | |
| from api.startup_migration import apply_rls_fix_sync | |
| apply_rls_fix_sync() | |
| _logger.info("β BOOT: apply_rls_fix_sync() eseguito con successo.") | |
| except Exception as e: | |
| _logger.warning(f"β οΈ BOOT: apply_rls_fix_sync() fallito (non bloccante): {e}") | |
| asyncio.create_task(_run_auto_migration()) | |
| try: | |
| from api.providers import start_heartbeat | |
| start_heartbeat() | |
| _logger.info("β BOOT: provider heartbeat avviato.") | |
| except Exception as e: | |
| _logger.warning(f"β οΈ BOOT: avvio provider heartbeat fallito (non bloccante): {e}") | |
| if not any(arg in sys.argv for arg in ["--task", "-t"]): | |
| try: | |
| from api.job_queue import start_job_queue_consumer | |
| asyncio.create_task(start_job_queue_consumer()) | |
| except Exception: pass | |
| # ββ SPA Hosting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _STATIC_DIR = os.getenv('FRONTEND_DIST', '/app/backend/static') | |
| if os.path.isdir(_STATIC_DIR): | |
| app.mount('/', StaticFiles(directory=_STATIC_DIR, html=True), name='spa') | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Agente AI Backend & CLI") | |
| parser.add_argument("--task", "-t", type=str, help="Esegue un task e termina") | |
| parser.add_argument("--port", "-p", type=int, default=8000, help="Porta server") | |
| args = parser.parse_args() | |
| if args.task: | |
| asyncio.run(run_cli_task(args.task)) | |
| else: | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=args.port) | |