sync: 141 file da Baida98/AI@35be4001 (2026-08-07 14:06 UTC)

#13
by Baida07 - opened
Files changed (1) hide show
  1. api/state.py +1 -231
api/state.py CHANGED
@@ -54,236 +54,6 @@ try:
54
  _clients.append({"client": c, "id": cfg["id"], "status": "connected"})
55
  _logger.info(f"BOOT: Supabase #{cfg['id']} connected OK")
56
  except Exception as e:
57
- _logger.error(f"BOOT: Supabase #{cfg['id']} init failed: {e}")
58
- except ImportError:
59
- _logger.error("BOOT: Supabase init module failed: create_client not found.")
60
-
61
- def _get_sb() -> Any:
62
- """Ritorna il client Supabase corrente dal pool (round-robin)."""
63
- global _current_client_idx
64
- if not _clients: return None
65
- # S-FIX: Salta i client marcati come "failed" (semplice circuit breaker)
66
- for _ in range(len(_clients)):
67
- entry = _clients[_current_client_idx]
68
- _current_client_idx = (_current_client_idx + 1) % len(_clients)
69
- if entry["status"] != "failed":
70
- return entry["client"]
71
- return _clients[0]["client"] if _clients else None
72
-
73
- _sb = _get_sb()
74
-
75
- # ── SENSITIVE keys set (Z-GAP-4) ──────────────────────────────────────────────
76
- SENSITIVE = {
77
- 'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY',
78
- 'HF_TOKEN', 'HUGGINGFACE_API_KEY', 'GH_TOKEN', 'GITHUB_TOKEN',
79
- 'QDRANT_API_KEY', 'DATABASE_URL', 'SESSION_SECRET', 'SECRET_KEY',
80
- 'RAILWAY_TOKEN', 'SUPABASE_KEY', 'SUPABASE_ANON_KEY',
81
- 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID',
82
- 'CF_API_TOKEN', 'CLOUDFLARE_API_TOKEN', 'CF_ACCOUNT_ID',
83
- 'CF_API_TOKEN_B', 'CF_ACCOUNT_ID_B',
84
- 'CEREBRAS_API_KEY', 'SAMBANOVA_API_KEY',
85
- 'VAULT_KEY', 'INTERNAL_TOKEN', 'DEPLOY_SECRET', 'WEBHOOK_TOKEN',
86
- 'TERMINAL_SECRET', 'EXEC_TOKEN', 'VITE_INTERNAL_TOKEN', 'VITE_TERMINAL_SECRET',
87
- 'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY',
88
- 'GH_PAGES_TOKEN', 'VERCEL_TOKEN',
89
- }
90
-
91
- # ── In-memory stores ──────────────────────────────────────────────────────────
92
- _mem_fallback: dict[str, dict] = {}
93
- _agent_tasks: dict[str, dict] = {}
94
- _run_stream_tasks: dict[str, dict] = {}
95
- _loop_registry: dict[str, dict] = {}
96
- _LOOP_REGISTRY_TTL_S: float = 10 * 60
97
- _task_checkpoints: dict[str, dict] = {}
98
- _CHECKPOINT_TTL_MS = 2 * 60 * 60 * 1000
99
- _CHECKPOINT_MAX = 100
100
- _AGENT_TASK_TTL_MS = 2 * 60 * 60 * 1000
101
- _AGENT_TASK_MAX = 200
102
-
103
- # ── Telemetry & Health ────────────────────────────────────────────────────────
104
- _ai_health_cache: dict = {"data": None, "at": 0.0}
105
- _AI_HEALTH_TTL = 60.0
106
- _heartbeat_state: dict = {
107
- "last_run_at": None,
108
- "next_run_at": None,
109
- "best_provider": None,
110
- "best_latency_ms": None,
111
- "providers": [],
112
- "runs": 0,
113
- }
114
-
115
- # ── Singleton Getters ─────────────────────────────────────────────────────────
116
- def get_supabase() -> Optional[Any]:
117
- """Ritorna il client Supabase primario."""
118
- return _sb
119
-
120
- _mem_manager: Any = None
121
- _mem_manager_inited = False
122
- def _get_mem_manager() -> Any:
123
- global _mem_manager, _mem_manager_inited
124
- if _mem_manager_inited: return _mem_manager
125
- try:
126
- from memory.memory_manager import MemoryManager
127
- try:
128
- _mem_manager = MemoryManager()
129
- _mem_manager_inited = True
130
- except RuntimeError: pass
131
- except Exception: _mem_manager = None
132
- return _mem_manager
133
-
134
- _executor: Any = None
135
- def _get_executor() -> Any:
136
- global _executor
137
- if _executor is not None: return _executor
138
- try:
139
- from agents.executor import Executor
140
- _executor = Executor(memory=_get_mem_manager())
141
- except Exception: _executor = None
142
- return _executor
143
-
144
- _ai_client: Any = None
145
- def _get_ai_client() -> Any:
146
- global _ai_client
147
- if _ai_client is not None: return _ai_client
148
- try:
149
- from models.ai_client import AIClient
150
- _ai_client = AIClient()
151
- except Exception: _ai_client = None
152
- return _ai_client
153
-
154
- _planner: Any = None
155
- def _get_planner() -> Any:
156
- global _planner
157
- if _planner is not None: return _planner
158
- try:
159
- from agents.planner import Planner
160
- _planner = Planner(llm_client=_get_ai_client())
161
- except Exception: _planner = None
162
- return _planner
163
-
164
-
165
- _memory_manager: Any = None
166
- _memory_manager_lock = _asyncio_mod.Lock()
167
-
168
- async def _get_mem_manager_async() -> Any:
169
- """Return the shared initialized MemoryManager for agent runs."""
170
- global _memory_manager
171
- if _memory_manager is not None:
172
- return _memory_manager
173
- async with _memory_manager_lock:
174
- if _memory_manager is not None:
175
- return _memory_manager
176
- from memory.manager import MemoryManager
177
- manager = MemoryManager(sb_client=_get_sb())
178
- await manager.init()
179
- _memory_manager = manager
180
- return _memory_manager
181
-
182
- # ── Prune helpers ─────────────────────────────────────────────────────────────
183
- def _prune_checkpoints() -> None:
184
- now = int(time.time() * 1000)
185
- expired = [k for k, v in list(_task_checkpoints.items()) if now - v.get('savedAt', 0) > _CHECKPOINT_TTL_MS]
186
- for k in expired: _task_checkpoints.pop(k, None)
187
- if len(_task_checkpoints) > _CHECKPOINT_MAX:
188
- oldest = sorted(list(_task_checkpoints.items()), key=lambda x: x[1].get('savedAt', 0))
189
- for k, _ in oldest[:len(_task_checkpoints) - _CHECKPOINT_MAX]: _task_checkpoints.pop(k, None)
190
-
191
- def _prune_agent_tasks() -> None:
192
- now = int(time.time() * 1000)
193
- expired = [k for k, v in list(_agent_tasks.items()) if v.get('status') in ('SUCCESS', 'ERROR', 'CANCELLED') and now - v.get('created_at', 0) > _AGENT_TASK_TTL_MS]
194
- for k in expired: _agent_tasks.pop(k, None)
195
- if len(_agent_tasks) > _AGENT_TASK_MAX:
196
- oldest = sorted(list(_agent_tasks.items()), key=lambda x: x[1].get('created_at', 0))
197
- for k, _ in oldest[:len(_agent_tasks) - _AGENT_TASK_MAX]: _agent_tasks.pop(k, None)
198
-
199
- def _prune_loop_registry() -> None:
200
- now = time.time()
201
- stale = [k for k, v in list(_loop_registry.items()) if v.get('done') and now - v.get('finished_at', 0.0) > _LOOP_REGISTRY_TTL_S]
202
- for k in stale: _loop_registry.pop(k, None)
203
-
204
- # ── Shared Pydantic models ────────────────────────────────────────────────────
205
- class ReasonLoopIn(BaseModel):
206
- goal: str
207
- context: list[dict] = []
208
- max_steps: int = 8
209
- project_context: str = ""
210
- learning_hints: list[str] = []
211
- session_id: Optional[str] = None
212
- negative_constraints: Optional[str] = ""
213
-
214
- @field_validator('goal', mode='before')
215
- @classmethod
216
- def validate_goal(cls, v: object) -> str:
217
- if not isinstance(v, str) or not v.strip(): raise ValueError('goal must be a non-empty string')
218
- return v.strip()
219
-
220
- @field_validator('context', 'learning_hints', mode='before')
221
- @classmethod
222
- def coerce_list(cls, v: object) -> list:
223
- return v if isinstance(v, list) else []
224
-
225
- @field_validator('project_context', mode='before')
226
- @classmethod
227
- def coerce_str(cls, v: object) -> str:
228
- return str(v).strip()[:2000] if v else ""
229
-
230
- class AgentTaskIn(BaseModel):
231
- goal: str
232
- context: list[dict] = []
233
- max_steps: int = 8
234
- taskId: Optional[str] = None
235
- project_context: str = ""
236
- learning_hints: list[str] = []
237
- session_id: Optional[str] = None
238
- resume_from_step: Optional[int] = None
239
- persona: Optional[str] = None
240
- negative_constraints: Optional[str] = ""
241
-
242
- @field_validator('goal', mode='before')
243
- @classmethod
244
- def validate_goal(cls, v: object) -> str:
245
- if not isinstance(v, str) or not v.strip(): raise ValueError('goal must be a non-empty string')
246
- return v.strip()
247
-
248
- @field_validator('context', 'learning_hints', mode='before')
249
- @classmethod
250
- def coerce_list(cls, v: object) -> list:
251
- return v if isinstance(v, list) else []
252
-
253
- @router.get("/health")
254
- async def health_check(request: Request):
255
- """
256
- Z-GAP-3: Healthcheck endpoint per monitoraggio deploy (TMA/HF).
257
- Verifica lo stato del server e la connettività al database.
258
- """
259
- health = {
260
- "status": "ok",
261
- "timestamp": time.time(),
262
- "version": "1.5.5",
263
- "database": "unknown",
264
- "pool_size": len(_clients)
265
- }
266
- try:
267
- if _sb:
268
- # S-FIX: Verifica reale con try-except per gestire errori PostgREST malformati
269
- try:
270
- # S-FIX: select('key') è più leggero di select('count') per health check
271
- res = _sb.table("agent_memory").select("key").limit(1).execute()
272
- health["database"] = "connected"
273
- except Exception as inner_e:
274
- # Se il client corrente fallisce, lo marchiamo per il pool
275
- for entry in _clients:
276
- if entry["client"] == _sb:
277
- entry["status"] = "failed"
278
- break
279
- raise inner_e
280
- else:
281
- health["database"] = "disconnected"
282
- except Exception as e:
283
  health["status"] = "degraded"
284
- # S-FIX: Estrae il messaggio di errore in modo più pulito
285
- err_msg = str(e)
286
- if "JSON could not be generated" in err_msg:
287
- err_msg = "PostgREST JSON error (likely RLS or schema mismatch)"
288
- health["database"] = f"error: {err_msg[:100]}"
289
  return health
 
54
  _clients.append({"client": c, "id": cfg["id"], "status": "connected"})
55
  _logger.info(f"BOOT: Supabase #{cfg['id']} connected OK")
56
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  health["status"] = "degraded"
58
+ health["database"] = f"RAW_ERROR: {str(e)}"
 
 
 
 
59
  return health