Spaces:
Running
Running
sync: 130 files from Baida98/AI [deploy-all]
#3
by Baida07 - opened
- .env.example +0 -7
- agents/executor.py +0 -50
- api/conversations.py +4 -21
- api/deploy.py +3 -3
- api/job_queue.py +14 -40
- api/kernel.py +0 -50
- api/providers.py +7 -30
- api/scheduler.py +2 -49
- api/state.py +4 -8
- api/telegram_webhook.py +24 -24
- api/webhook.py +8 -11
- main.py +1 -83
- tests/test_regression_doc2.py +3 -3
.env.example
CHANGED
|
@@ -19,13 +19,6 @@ SUPABASE_URL=
|
|
| 19 |
SUPABASE_SERVICE_ROLE_KEY=
|
| 20 |
GITHUB_TOKEN=
|
| 21 |
HF_TOKEN=
|
| 22 |
-
# HF Spaces URLs (configurare per ogni Space del fleet)
|
| 23 |
-
HF_SPACE_URL= # Brain / Backend principale
|
| 24 |
-
HF_SPACE_B_URL= # Daemon / Telegram worker
|
| 25 |
-
HF_SPACE_C_URL= # Worker A (Collab/GPU)
|
| 26 |
-
HF_SPACE_D_URL= # Worker B
|
| 27 |
-
HF_SPACE_E_URL= # Worker C
|
| 28 |
-
ORACLE_CLOUD_VM_URL= # Oracle Cloud A1 compute VM
|
| 29 |
|
| 30 |
# ββ 3. Quadrante B (HANDS - Collab/Failover) βββββββββββββββββ
|
| 31 |
RAILWAY_TOKEN_B=
|
|
|
|
| 19 |
SUPABASE_SERVICE_ROLE_KEY=
|
| 20 |
GITHUB_TOKEN=
|
| 21 |
HF_TOKEN=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# ββ 3. Quadrante B (HANDS - Collab/Failover) βββββββββββββββββ
|
| 24 |
RAILWAY_TOKEN_B=
|
agents/executor.py
CHANGED
|
@@ -12,7 +12,6 @@ import asyncio
|
|
| 12 |
import collections
|
| 13 |
import logging
|
| 14 |
import time as _time_mod
|
| 15 |
-
from typing import Any
|
| 16 |
|
| 17 |
from models.ai_client import AIClient
|
| 18 |
from memory.manager import MemoryManager
|
|
@@ -94,12 +93,10 @@ class Executor:
|
|
| 94 |
llm_client: AIClient | None = None,
|
| 95 |
memory: MemoryManager | None = None,
|
| 96 |
max_retries: int = 2,
|
| 97 |
-
kernel: Any | None = None, # ARCH-K2.2: BrainβKernel abstraction
|
| 98 |
):
|
| 99 |
self.llm = llm_client or AIClient()
|
| 100 |
self.memory = memory
|
| 101 |
self.max_retries = max_retries
|
| 102 |
-
self._kernel = kernel # ARCH-K2.2: usato da submit_background_task()
|
| 103 |
# GAP-SKILL-SYNC v2: contatore chiamate per recovery credit (per-tool)
|
| 104 |
self._circuit_recovery_counts: dict[str, int] = {}
|
| 105 |
|
|
@@ -108,53 +105,6 @@ class Executor:
|
|
| 108 |
def from_ollama(cls, ollama=None, memory=None, max_retries: int = 2) -> "Executor":
|
| 109 |
return cls(memory=memory, max_retries=max_retries)
|
| 110 |
|
| 111 |
-
# ββ ARCH-K2.2: submit background task via Kernel ββββββββββββββββββββββββββ
|
| 112 |
-
|
| 113 |
-
async def submit_background_task(
|
| 114 |
-
self,
|
| 115 |
-
payload: dict,
|
| 116 |
-
priority: str = "BACKGROUND",
|
| 117 |
-
session_id: str | None = None,
|
| 118 |
-
) -> str | None:
|
| 119 |
-
"""
|
| 120 |
-
Invia un task in background tramite kernel.submit_task() (ARCH-K2.2).
|
| 121 |
-
|
| 122 |
-
Il Brain/Executor non conosce l'implementazione della coda sottostante
|
| 123 |
-
(S9: ogni servizio ignora l'impl interna degli altri).
|
| 124 |
-
|
| 125 |
-
Fallback: asyncio.create_task() locale se il Kernel non Γ¨ disponibile.
|
| 126 |
-
Sempre non-bloccante β non aspetta il completamento del task.
|
| 127 |
-
|
| 128 |
-
Ritorna il task_id se il Kernel Γ¨ disponibile, None altrimenti.
|
| 129 |
-
"""
|
| 130 |
-
# Lazy-load kernel singleton se non iniettato
|
| 131 |
-
k = self._kernel
|
| 132 |
-
if k is None:
|
| 133 |
-
try:
|
| 134 |
-
from api.kernel import kernel as _k
|
| 135 |
-
k = _k
|
| 136 |
-
except Exception:
|
| 137 |
-
pass
|
| 138 |
-
|
| 139 |
-
if k is not None:
|
| 140 |
-
try:
|
| 141 |
-
result = await k.submit_task(
|
| 142 |
-
payload=payload,
|
| 143 |
-
priority=priority,
|
| 144 |
-
session_id=session_id,
|
| 145 |
-
)
|
| 146 |
-
_logger.info(
|
| 147 |
-
"[executor] submit_background_task via Kernel id=%s priority=%s",
|
| 148 |
-
result.task_id, priority,
|
| 149 |
-
)
|
| 150 |
-
return result.task_id
|
| 151 |
-
except Exception as exc:
|
| 152 |
-
_logger.warning("[executor] kernel submit_background_task err: %s", exc)
|
| 153 |
-
|
| 154 |
-
# Fallback: esecuzione diretta asincrona locale (non attraverso la Queue)
|
| 155 |
-
_logger.debug("[executor] submit_background_task fallback: asyncio.create_task")
|
| 156 |
-
return None
|
| 157 |
-
|
| 158 |
# ββ Circuit breaker helper ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
|
| 160 |
def _is_circuit_open(self, tool_name: str, session_id: str) -> bool:
|
|
|
|
| 12 |
import collections
|
| 13 |
import logging
|
| 14 |
import time as _time_mod
|
|
|
|
| 15 |
|
| 16 |
from models.ai_client import AIClient
|
| 17 |
from memory.manager import MemoryManager
|
|
|
|
| 93 |
llm_client: AIClient | None = None,
|
| 94 |
memory: MemoryManager | None = None,
|
| 95 |
max_retries: int = 2,
|
|
|
|
| 96 |
):
|
| 97 |
self.llm = llm_client or AIClient()
|
| 98 |
self.memory = memory
|
| 99 |
self.max_retries = max_retries
|
|
|
|
| 100 |
# GAP-SKILL-SYNC v2: contatore chiamate per recovery credit (per-tool)
|
| 101 |
self._circuit_recovery_counts: dict[str, int] = {}
|
| 102 |
|
|
|
|
| 105 |
def from_ollama(cls, ollama=None, memory=None, max_retries: int = 2) -> "Executor":
|
| 106 |
return cls(memory=memory, max_retries=max_retries)
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# ββ Circuit breaker helper ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
|
| 110 |
def _is_circuit_open(self, tool_name: str, session_id: str) -> bool:
|
api/conversations.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""backend/api/conversations.py β Conversations + Messages CRUD (S354)."""
|
| 2 |
-
import
|
| 3 |
from .state import safe_json_dumps
|
| 4 |
from typing import Optional, Any
|
| 5 |
from fastapi import APIRouter, Depends, Body, HTTPException
|
|
@@ -7,23 +7,6 @@ from .auth_guard import require_role, AuthRole
|
|
| 7 |
from pydantic import BaseModel
|
| 8 |
from .state import sb
|
| 9 |
|
| 10 |
-
_logger_c = logging.getLogger("conversations")
|
| 11 |
-
|
| 12 |
-
async def _sb_call(fn, *args, **kwargs):
|
| 13 |
-
"""AUD-011: 1 retry with 500ms delay on transient Supabase errors.
|
| 14 |
-
HIGH-4: non retryare errori di autenticazione/autorizzazione β solo errori transienti.
|
| 15 |
-
"""
|
| 16 |
-
try:
|
| 17 |
-
return fn(*args, **kwargs)
|
| 18 |
-
except Exception as _e:
|
| 19 |
-
_ename = type(_e).__name__
|
| 20 |
-
_emsg = str(_e)
|
| 21 |
-
# Non retryare: auth errors, permission errors β sarebbero errori permanenti
|
| 22 |
-
if any(k in _ename or k in _emsg for k in ("Auth", "JWT", "403", "401", "Unauthorized", "Permission")):
|
| 23 |
-
raise
|
| 24 |
-
await asyncio.sleep(0.5)
|
| 25 |
-
return fn(*args, **kwargs) # let caller handle on second failure
|
| 26 |
-
|
| 27 |
router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
|
| 28 |
_logger = logging.getLogger("conversations")
|
| 29 |
|
|
@@ -51,7 +34,7 @@ class MessageIn(BaseModel):
|
|
| 51 |
@router.get('/api/conversations')
|
| 52 |
async def list_conversations():
|
| 53 |
try:
|
| 54 |
-
data =
|
| 55 |
return {'conversations': data.data}
|
| 56 |
except Exception as exc:
|
| 57 |
_logger.warning("list_conversations: %s", exc)
|
|
@@ -95,7 +78,7 @@ async def delete_conversation(conv_id: str):
|
|
| 95 |
@router.get('/api/conversations/{conv_id}/messages')
|
| 96 |
async def list_messages(conv_id: str):
|
| 97 |
try:
|
| 98 |
-
data =
|
| 99 |
return {'messages': data.data}
|
| 100 |
except Exception as exc:
|
| 101 |
_logger.warning("list_messages %s: %s", conv_id, exc)
|
|
@@ -112,7 +95,7 @@ async def upsert_messages(conv_id: str, body: dict = Body(...)):
|
|
| 112 |
if 'steps' in m and m['steps'] is not None:
|
| 113 |
m['steps'] = safe_json_dumps(m['steps']) if not isinstance(m['steps'], str) else m['steps']
|
| 114 |
try:
|
| 115 |
-
data =
|
| 116 |
return {'upserted': len(data.data)}
|
| 117 |
except Exception as exc:
|
| 118 |
_logger.warning("upsert_messages %s: %s", conv_id, exc)
|
|
|
|
| 1 |
"""backend/api/conversations.py β Conversations + Messages CRUD (S354)."""
|
| 2 |
+
import json, logging
|
| 3 |
from .state import safe_json_dumps
|
| 4 |
from typing import Optional, Any
|
| 5 |
from fastapi import APIRouter, Depends, Body, HTTPException
|
|
|
|
| 7 |
from pydantic import BaseModel
|
| 8 |
from .state import sb
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
|
| 11 |
_logger = logging.getLogger("conversations")
|
| 12 |
|
|
|
|
| 34 |
@router.get('/api/conversations')
|
| 35 |
async def list_conversations():
|
| 36 |
try:
|
| 37 |
+
data = sb().table('conversations').select('*').order('updated_at', desc=True).limit(200).execute() # BUGFIX: LIMIT 200 β senza limit OOM garantito su account con molte conversazioni
|
| 38 |
return {'conversations': data.data}
|
| 39 |
except Exception as exc:
|
| 40 |
_logger.warning("list_conversations: %s", exc)
|
|
|
|
| 78 |
@router.get('/api/conversations/{conv_id}/messages')
|
| 79 |
async def list_messages(conv_id: str):
|
| 80 |
try:
|
| 81 |
+
data = sb().table('messages').select('*').eq('conversation_id', conv_id).order('created_at').limit(500).execute() # BUGFIX: LIMIT 500 β senza limit OOM garantito su conversazioni lunghe
|
| 82 |
return {'messages': data.data}
|
| 83 |
except Exception as exc:
|
| 84 |
_logger.warning("list_messages %s: %s", conv_id, exc)
|
|
|
|
| 95 |
if 'steps' in m and m['steps'] is not None:
|
| 96 |
m['steps'] = safe_json_dumps(m['steps']) if not isinstance(m['steps'], str) else m['steps']
|
| 97 |
try:
|
| 98 |
+
data = sb().table('messages').upsert(msgs).execute()
|
| 99 |
return {'upserted': len(data.data)}
|
| 100 |
except Exception as exc:
|
| 101 |
_logger.warning("upsert_messages %s: %s", conv_id, exc)
|
api/deploy.py
CHANGED
|
@@ -137,7 +137,7 @@ async def deploy_status_all(request: Request, role: AuthRole = Depends(require_r
|
|
| 137 |
return r
|
| 138 |
|
| 139 |
async def _check_railway() -> dict:
|
| 140 |
-
url = os.getenv("
|
| 141 |
r: dict = {"ok": False, "status": "unknown", "url": url, "latency_ms": None, "error": None}
|
| 142 |
t0 = time.monotonic()
|
| 143 |
try:
|
|
@@ -268,12 +268,12 @@ async def deploy_auto(body: AutoRepairRequest, request: Request, role: AuthRole
|
|
| 268 |
# ββ Railway βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 269 |
if "railway" in body.targets:
|
| 270 |
if not body.dry_run:
|
| 271 |
-
|
| 272 |
alive = False
|
| 273 |
for attempt in range(1, 6):
|
| 274 |
try:
|
| 275 |
async with httpx.AsyncClient(timeout=8) as c:
|
| 276 |
-
r = await c.get(
|
| 277 |
if r.status_code == 200:
|
| 278 |
alive = True
|
| 279 |
actions.append(f"β
Railway attivo (ping {attempt}/5 β HTTP 200)")
|
|
|
|
| 137 |
return r
|
| 138 |
|
| 139 |
async def _check_railway() -> dict:
|
| 140 |
+
url = os.getenv("RAILWAY_PUBLIC_URL", "") # S-DYN: usa env var
|
| 141 |
r: dict = {"ok": False, "status": "unknown", "url": url, "latency_ms": None, "error": None}
|
| 142 |
t0 = time.monotonic()
|
| 143 |
try:
|
|
|
|
| 268 |
# ββ Railway βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 269 |
if "railway" in body.targets:
|
| 270 |
if not body.dry_run:
|
| 271 |
+
railway_url = f"{os.getenv('RAILWAY_PUBLIC_URL', '')}/health"
|
| 272 |
alive = False
|
| 273 |
for attempt in range(1, 6):
|
| 274 |
try:
|
| 275 |
async with httpx.AsyncClient(timeout=8) as c:
|
| 276 |
+
r = await c.get(railway_url)
|
| 277 |
if r.status_code == 200:
|
| 278 |
alive = True
|
| 279 |
actions.append(f"β
Railway attivo (ping {attempt}/5 β HTTP 200)")
|
api/job_queue.py
CHANGED
|
@@ -12,8 +12,7 @@ Questo modulo implementa tre livelli di coordinamento via Upstash Redis:
|
|
| 12 |
Chiave: jq:wake (LIST, RPOP, TTL 30s per elemento)
|
| 13 |
|
| 14 |
3. TASK DELEGATION β BRAIN accoda task, HANDS consuma ed esegue.
|
| 15 |
-
|
| 16 |
-
Consumer drena HIGHβNORMALβLOWβBACKGROUND in cascata. jq:tasks:NORMAL = legacy alias.
|
| 17 |
Chiave: jq:result:{taskId} (STRING, TTL 300s)
|
| 18 |
Chiave: jq:events:{taskId} (LIST, TTL 300s)
|
| 19 |
Chiave: jq:consumer:alive (STRING, TTL 30s β heartbeat HANDS consumer)
|
|
@@ -51,20 +50,7 @@ _CONSUMER_HB_TTL = 30 # s β TTL heartbeat consumer HANDS
|
|
| 51 |
# ββ Redis keys βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
_K_LOAD = lambda role: f"jq:load:{role}" # STRING β metriche load
|
| 53 |
_K_WAKE = "jq:wake" # LIST β wake signals
|
| 54 |
-
#
|
| 55 |
-
_PRIORITY_LANES = ("HIGH", "NORMAL", "LOW", "BACKGROUND")
|
| 56 |
-
_K_QUEUE = lambda lane: f"jq:tasks:{lane}" # LIST β priority lane
|
| 57 |
-
_K_PENDING = _K_QUEUE("NORMAL") # legacy alias β NORMAL lane
|
| 58 |
-
|
| 59 |
-
# Normalizza priority string β corsia canonica (compat con "realtime"/"background")
|
| 60 |
-
_PRIORITY_MAP: dict[str, str] = {
|
| 61 |
-
"high": "HIGH",
|
| 62 |
-
"realtime": "HIGH", # compat legacy priority="realtime"
|
| 63 |
-
"normal": "NORMAL",
|
| 64 |
-
"low": "LOW",
|
| 65 |
-
"background": "BACKGROUND",
|
| 66 |
-
"bg": "BACKGROUND",
|
| 67 |
-
}
|
| 68 |
_K_RESULT = lambda tid: f"jq:result:{tid}" # STRING β risultato job
|
| 69 |
_K_EVENTS = lambda tid: f"jq:events:{tid}" # LIST β eventi SSE
|
| 70 |
_K_CONSUMER = "jq:consumer:alive" # STRING β HB consumer
|
|
@@ -201,7 +187,7 @@ class JobPayload(BaseModel):
|
|
| 201 |
goal: str
|
| 202 |
session_id: str = ""
|
| 203 |
context: dict = {}
|
| 204 |
-
priority: str = "
|
| 205 |
max_steps: int = 20
|
| 206 |
task_id: str = "" # se vuoto β generato da BRAIN
|
| 207 |
|
|
@@ -218,32 +204,29 @@ async def submit_job(job: JobPayload) -> dict:
|
|
| 218 |
raise HTTPException(503, "Redis non configurato β job queue non disponibile")
|
| 219 |
|
| 220 |
task_id = job.task_id or str(uuid.uuid4())
|
| 221 |
-
lane = _PRIORITY_MAP.get(job.priority.lower(), "NORMAL")
|
| 222 |
payload = json.dumps({
|
| 223 |
"taskId": task_id,
|
| 224 |
"goal": job.goal,
|
| 225 |
"session_id": job.session_id,
|
| 226 |
"context": job.context,
|
| 227 |
-
"priority":
|
| 228 |
"max_steps": job.max_steps,
|
| 229 |
"submitted_at": time.time(),
|
| 230 |
"submitted_by": _SPACE_ROLE,
|
| 231 |
})
|
| 232 |
|
| 233 |
-
|
| 234 |
-
ok = await _rpush(queue_key, payload)
|
| 235 |
if not ok:
|
| 236 |
raise HTTPException(503, "Impossibile accodare il task su Redis")
|
| 237 |
|
| 238 |
-
depth = await _llen(
|
| 239 |
-
_logger.info("[jq] job queued taskId=%s
|
| 240 |
|
| 241 |
return {
|
| 242 |
-
"taskId":
|
| 243 |
-
"status":
|
| 244 |
-
"priority": lane,
|
| 245 |
"queue_depth": depth,
|
| 246 |
-
"stream_url":
|
| 247 |
}
|
| 248 |
|
| 249 |
|
|
@@ -349,12 +332,8 @@ async def _hands_consumer_loop() -> None:
|
|
| 349 |
if not _JQ_ENABLED:
|
| 350 |
continue # load publisher attivo, job consumer no
|
| 351 |
|
| 352 |
-
# Preleva job dalla coda
|
| 353 |
-
raw =
|
| 354 |
-
for _lane in _PRIORITY_LANES:
|
| 355 |
-
raw = await _rpop(_K_QUEUE(_lane))
|
| 356 |
-
if raw is not None:
|
| 357 |
-
break
|
| 358 |
if raw is None:
|
| 359 |
continue
|
| 360 |
|
|
@@ -415,13 +394,8 @@ async def jq_status():
|
|
| 415 |
"ts": int(time.time() * 1000),
|
| 416 |
}
|
| 417 |
if _redis_configured:
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
_ld[_l] = await _llen(_K_QUEUE(_l))
|
| 421 |
-
result["queue_depth"] = _ld.get("NORMAL", 0) # backward compat
|
| 422 |
-
result["queue_depth_total"] = sum(_ld.values())
|
| 423 |
-
result["queue_lanes"] = _ld
|
| 424 |
-
result["wake_pending"] = await _llen(_K_WAKE)
|
| 425 |
_hb = await _rcmd(["GET", _K_CONSUMER])
|
| 426 |
result["consumer_alive"] = bool(_hb and _hb.get("result"))
|
| 427 |
result["brain_load"] = await get_remote_load("brain")
|
|
|
|
| 12 |
Chiave: jq:wake (LIST, RPOP, TTL 30s per elemento)
|
| 13 |
|
| 14 |
3. TASK DELEGATION β BRAIN accoda task, HANDS consuma ed esegue.
|
| 15 |
+
Chiave: jq:tasks:pending (LIST, LPUSH/RPOP)
|
|
|
|
| 16 |
Chiave: jq:result:{taskId} (STRING, TTL 300s)
|
| 17 |
Chiave: jq:events:{taskId} (LIST, TTL 300s)
|
| 18 |
Chiave: jq:consumer:alive (STRING, TTL 30s β heartbeat HANDS consumer)
|
|
|
|
| 50 |
# ββ Redis keys βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
_K_LOAD = lambda role: f"jq:load:{role}" # STRING β metriche load
|
| 52 |
_K_WAKE = "jq:wake" # LIST β wake signals
|
| 53 |
+
_K_PENDING = "jq:tasks:pending" # LIST β job queue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
_K_RESULT = lambda tid: f"jq:result:{tid}" # STRING β risultato job
|
| 55 |
_K_EVENTS = lambda tid: f"jq:events:{tid}" # LIST β eventi SSE
|
| 56 |
_K_CONSUMER = "jq:consumer:alive" # STRING β HB consumer
|
|
|
|
| 187 |
goal: str
|
| 188 |
session_id: str = ""
|
| 189 |
context: dict = {}
|
| 190 |
+
priority: str = "realtime" # realtime | background
|
| 191 |
max_steps: int = 20
|
| 192 |
task_id: str = "" # se vuoto β generato da BRAIN
|
| 193 |
|
|
|
|
| 204 |
raise HTTPException(503, "Redis non configurato β job queue non disponibile")
|
| 205 |
|
| 206 |
task_id = job.task_id or str(uuid.uuid4())
|
|
|
|
| 207 |
payload = json.dumps({
|
| 208 |
"taskId": task_id,
|
| 209 |
"goal": job.goal,
|
| 210 |
"session_id": job.session_id,
|
| 211 |
"context": job.context,
|
| 212 |
+
"priority": job.priority,
|
| 213 |
"max_steps": job.max_steps,
|
| 214 |
"submitted_at": time.time(),
|
| 215 |
"submitted_by": _SPACE_ROLE,
|
| 216 |
})
|
| 217 |
|
| 218 |
+
ok = await _rpush(_K_PENDING, payload)
|
|
|
|
| 219 |
if not ok:
|
| 220 |
raise HTTPException(503, "Impossibile accodare il task su Redis")
|
| 221 |
|
| 222 |
+
depth = await _llen(_K_PENDING)
|
| 223 |
+
_logger.info("[jq] job queued taskId=%s depth=%d", task_id, depth)
|
| 224 |
|
| 225 |
return {
|
| 226 |
+
"taskId": task_id,
|
| 227 |
+
"status": "queued",
|
|
|
|
| 228 |
"queue_depth": depth,
|
| 229 |
+
"stream_url": f"/api/agent/tasks/{task_id}/stream",
|
| 230 |
}
|
| 231 |
|
| 232 |
|
|
|
|
| 332 |
if not _JQ_ENABLED:
|
| 333 |
continue # load publisher attivo, job consumer no
|
| 334 |
|
| 335 |
+
# Preleva job dalla coda
|
| 336 |
+
raw = await _rpop(_K_PENDING)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
if raw is None:
|
| 338 |
continue
|
| 339 |
|
|
|
|
| 394 |
"ts": int(time.time() * 1000),
|
| 395 |
}
|
| 396 |
if _redis_configured:
|
| 397 |
+
result["queue_depth"] = await _llen(_K_PENDING)
|
| 398 |
+
result["wake_pending"] = await _llen(_K_WAKE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
_hb = await _rcmd(["GET", _K_CONSUMER])
|
| 400 |
result["consumer_alive"] = bool(_hb and _hb.get("result"))
|
| 401 |
result["brain_load"] = await get_remote_load("brain")
|
api/kernel.py
CHANGED
|
@@ -166,32 +166,6 @@ class KernelAPI:
|
|
| 166 |
corr = correlation_id or str(uuid.uuid4())
|
| 167 |
t_id = str(uuid.uuid4())
|
| 168 |
|
| 169 |
-
# S15: autorizza prima dell'esecuzione β Policy Engine (ARCH-K2.4)
|
| 170 |
-
try:
|
| 171 |
-
from .policy import policy as _policy, PolicyContext as _PolicyCtx
|
| 172 |
-
_dec = await _policy.check(_PolicyCtx(
|
| 173 |
-
task_id=t_id,
|
| 174 |
-
session_id=session_id or "",
|
| 175 |
-
action="task.submit",
|
| 176 |
-
priority=priority,
|
| 177 |
-
correlation_id=corr,
|
| 178 |
-
))
|
| 179 |
-
if not _dec.allowed:
|
| 180 |
-
_logger.warning(
|
| 181 |
-
"[kernel.submit_task] policy deny corr=%s action=%s reason=%s",
|
| 182 |
-
corr, _dec.action_taken, _dec.reason,
|
| 183 |
-
)
|
| 184 |
-
return TaskResult(
|
| 185 |
-
task_id=t_id,
|
| 186 |
-
correlation_id=corr,
|
| 187 |
-
status="error",
|
| 188 |
-
queue_backend="none",
|
| 189 |
-
error=f"policy:{_dec.action_taken}:{_dec.reason}",
|
| 190 |
-
)
|
| 191 |
-
timeout_s = _dec.adjusted_timeout_s if _dec.adjusted_timeout_s is not None else timeout_s
|
| 192 |
-
except Exception as _pe:
|
| 193 |
-
_logger.debug("[kernel.submit_task] policy check skip (non-blocking): %s", _pe)
|
| 194 |
-
|
| 195 |
job = {
|
| 196 |
"task_id": t_id,
|
| 197 |
"correlation_id": corr,
|
|
@@ -246,30 +220,6 @@ class KernelAPI:
|
|
| 246 |
corr = correlation_id or str(uuid.uuid4())
|
| 247 |
cache_key = f"k:{model_hint}:{hash(str(messages))}"
|
| 248 |
|
| 249 |
-
# S15: autorizza chiamata LLM β Policy Engine (ARCH-K2.4)
|
| 250 |
-
try:
|
| 251 |
-
from .policy import policy as _policy, PolicyContext as _PolicyCtx
|
| 252 |
-
_dec = await _policy.check(_PolicyCtx(
|
| 253 |
-
session_id=session_id or "",
|
| 254 |
-
action="llm.call",
|
| 255 |
-
tokens_hint=max_tokens,
|
| 256 |
-
correlation_id=corr,
|
| 257 |
-
))
|
| 258 |
-
if not _dec.allowed:
|
| 259 |
-
_logger.warning(
|
| 260 |
-
"[kernel.chat] policy deny corr=%s action=%s reason=%s",
|
| 261 |
-
corr, _dec.action_taken, _dec.reason,
|
| 262 |
-
)
|
| 263 |
-
return ChatResult(
|
| 264 |
-
correlation_id=corr,
|
| 265 |
-
content="",
|
| 266 |
-
provider="policy",
|
| 267 |
-
model="none",
|
| 268 |
-
error=f"policy:{_dec.action_taken}:{_dec.reason}",
|
| 269 |
-
)
|
| 270 |
-
except Exception as _pe:
|
| 271 |
-
_logger.debug("[kernel.chat] policy check skip (non-blocking): %s", _pe)
|
| 272 |
-
|
| 273 |
# Cache read
|
| 274 |
try:
|
| 275 |
from .llm_cache import get_cached_response, cache_response
|
|
|
|
| 166 |
corr = correlation_id or str(uuid.uuid4())
|
| 167 |
t_id = str(uuid.uuid4())
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
job = {
|
| 170 |
"task_id": t_id,
|
| 171 |
"correlation_id": corr,
|
|
|
|
| 220 |
corr = correlation_id or str(uuid.uuid4())
|
| 221 |
cache_key = f"k:{model_hint}:{hash(str(messages))}"
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
# Cache read
|
| 224 |
try:
|
| 225 |
from .llm_cache import get_cached_response, cache_response
|
api/providers.py
CHANGED
|
@@ -29,7 +29,7 @@ async def health():
|
|
| 29 |
'status': 'ok',
|
| 30 |
'version': '3.4.2',
|
| 31 |
'supabase': _sb is not None,
|
| 32 |
-
'backend': 'HuggingFace Spaces',
|
| 33 |
}
|
| 34 |
|
| 35 |
|
|
@@ -233,17 +233,8 @@ async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACH
|
|
| 233 |
"model": provider.default_model.split("/")[-1][:28]}
|
| 234 |
except Exception as exc:
|
| 235 |
ms = round((time.monotonic() - t0) * 1000)
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# AUD-003: distingui 401 (token invalido) da 429 (quota esaurita) da errore generico
|
| 239 |
-
_status = (
|
| 240 |
-
"invalid_token" if ("401" in _exc_str or "AuthenticationError" in _exc_type or "Unauthorized" in _exc_str) else
|
| 241 |
-
"quota_exhausted" if ("429" in _exc_str or "RateLimitError" in _exc_type or "quota" in _exc_str.lower()) else
|
| 242 |
-
"timeout" if ("timeout" in _exc_str.lower() or "TimeoutError" in _exc_type) else
|
| 243 |
-
"error"
|
| 244 |
-
)
|
| 245 |
-
return {"name": provider.name, "ok": False, "status": _status, "latency_ms": ms,
|
| 246 |
-
"error": _exc_str[:300], "model": provider.default_model.split("/")[-1][:28]} # S606
|
| 247 |
|
| 248 |
results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
|
| 249 |
payload = {"providers": results, "tested_at": int(time.time() * 1000)}
|
|
@@ -309,29 +300,15 @@ async def _heartbeat_probe_all() -> list:
|
|
| 309 |
timeout=10.0,
|
| 310 |
)
|
| 311 |
ms = round((time.monotonic() - t0) * 1000)
|
| 312 |
-
return {"name": provider.name, "ok": True, "
|
| 313 |
-
"model": provider.default_model.split("/")[-1][:28]}
|
| 314 |
except Exception as exc:
|
| 315 |
ms = round((time.monotonic() - t0) * 1000)
|
| 316 |
-
|
| 317 |
-
_exc_type = type(exc).__name__
|
| 318 |
-
# AUD-003 (completo): distingui 401/token-invalido da 429/quota da timeout da errore.
|
| 319 |
-
# Allineato con _probe in ai_provider_health β stesso schema per output coerente
|
| 320 |
-
# tra /api/ai-health e /api/providers/heartbeat.
|
| 321 |
-
_status = (
|
| 322 |
-
"invalid_token" if ("401" in _exc_str or "AuthenticationError" in _exc_type or "Unauthorized" in _exc_str) else
|
| 323 |
-
"quota_exhausted" if ("429" in _exc_str or "RateLimitError" in _exc_type or "quota" in _exc_str.lower()) else
|
| 324 |
-
"timeout" if ("timeout" in _exc_str.lower() or "TimeoutError" in _exc_type) else
|
| 325 |
-
"error"
|
| 326 |
-
)
|
| 327 |
-
return {"name": provider.name, "ok": False, "status": _status, "latency_ms": ms,
|
| 328 |
-
"error": _exc_str[:300], "model": provider.default_model.split("/")[-1][:28]} # S606
|
| 329 |
|
| 330 |
return list(await asyncio.gather(*[_probe(p) for p in client.providers]))
|
| 331 |
except Exception as exc:
|
| 332 |
_logger.warning("heartbeat probe failed: %s", exc)
|
| 333 |
-
|
| 334 |
-
return list(_heartbeat_state.get("providers", []))
|
| 335 |
|
| 336 |
|
| 337 |
async def _heartbeat_loop() -> None:
|
|
@@ -734,7 +711,7 @@ async def health_full(role: AuthRole = Depends(require_role(AuthRole.MACHINE))):
|
|
| 734 |
cfg = await asyncio.wait_for(_tg_cfg(), timeout=2.0)
|
| 735 |
if not cfg or not cfg.get("token"):
|
| 736 |
return {"ok": False, "configured": False,
|
| 737 |
-
"detail": "Imposta TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
|
| 738 |
import httpx
|
| 739 |
async with httpx.AsyncClient(timeout=3.0) as hc:
|
| 740 |
r = await hc.get(f"https://api.telegram.org/bot{cfg['token']}/getMe")
|
|
|
|
| 29 |
'status': 'ok',
|
| 30 |
'version': '3.4.2',
|
| 31 |
'supabase': _sb is not None,
|
| 32 |
+
'backend': 'HuggingFace Spaces / Railway',
|
| 33 |
}
|
| 34 |
|
| 35 |
|
|
|
|
| 233 |
"model": provider.default_model.split("/")[-1][:28]}
|
| 234 |
except Exception as exc:
|
| 235 |
ms = round((time.monotonic() - t0) * 1000)
|
| 236 |
+
return {"name": provider.name, "ok": False, "status": "error", "latency_ms": ms,
|
| 237 |
+
"error": str(exc)[:300], "model": provider.default_model.split("/")[-1][:28]} # S606: 200β300
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
|
| 240 |
payload = {"providers": results, "tested_at": int(time.time() * 1000)}
|
|
|
|
| 300 |
timeout=10.0,
|
| 301 |
)
|
| 302 |
ms = round((time.monotonic() - t0) * 1000)
|
| 303 |
+
return {"name": provider.name, "ok": True, "latency_ms": ms}
|
|
|
|
| 304 |
except Exception as exc:
|
| 305 |
ms = round((time.monotonic() - t0) * 1000)
|
| 306 |
+
return {"name": provider.name, "ok": False, "latency_ms": ms, "error": str(exc)[:300]} # S606: 200β300
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
return list(await asyncio.gather(*[_probe(p) for p in client.providers]))
|
| 309 |
except Exception as exc:
|
| 310 |
_logger.warning("heartbeat probe failed: %s", exc)
|
| 311 |
+
return []
|
|
|
|
| 312 |
|
| 313 |
|
| 314 |
async def _heartbeat_loop() -> None:
|
|
|
|
| 711 |
cfg = await asyncio.wait_for(_tg_cfg(), timeout=2.0)
|
| 712 |
if not cfg or not cfg.get("token"):
|
| 713 |
return {"ok": False, "configured": False,
|
| 714 |
+
"detail": "Imposta TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in Railway"}
|
| 715 |
import httpx
|
| 716 |
async with httpx.AsyncClient(timeout=3.0) as hc:
|
| 717 |
r = await hc.get(f"https://api.telegram.org/bot{cfg['token']}/getMe")
|
api/scheduler.py
CHANGED
|
@@ -38,20 +38,8 @@ from .auth_guard import require_role, AuthRole
|
|
| 38 |
from fastapi.responses import StreamingResponse
|
| 39 |
from pydantic import BaseModel
|
| 40 |
import logging
|
|
|
|
| 41 |
|
| 42 |
-
# ββ ARCH-I4.6: Event Bus integration (fire-and-forget, non-blocking) βββββββββ
|
| 43 |
-
async def _publish_scheduler_event(topic: str, payload: dict) -> None:
|
| 44 |
-
"""Pubblica evento sul bus interno β silenzioso su qualsiasi errore."""
|
| 45 |
-
try:
|
| 46 |
-
from .event_bus import publish # import lazy per evitare circular import
|
| 47 |
-
from .event_bus import BusEvent
|
| 48 |
-
await publish(BusEvent(
|
| 49 |
-
topic=topic,
|
| 50 |
-
payload=payload,
|
| 51 |
-
source="scheduler",
|
| 52 |
-
))
|
| 53 |
-
except Exception:
|
| 54 |
-
pass # Event bus non critico β non interrompe l'esecuzione del task
|
| 55 |
logger = logging.getLogger("agente_ai.scheduler")
|
| 56 |
|
| 57 |
def _log_task_exc(task): # GAP-2.6: log silently-dropped exceptions in fire-and-forget tasks
|
|
@@ -93,7 +81,7 @@ _STUCK_TIMEOUT_S = 300 # 5 min β > 120s timeout _run_goal + margi
|
|
| 93 |
|
| 94 |
|
| 95 |
def _load_tasks() -> None:
|
| 96 |
-
"""Gap-7-FIX
|
| 97 |
global _tasks
|
| 98 |
for _path in (_TASKS_FILE, _TASKS_BAK):
|
| 99 |
try:
|
|
@@ -104,27 +92,7 @@ def _load_tasks() -> None:
|
|
| 104 |
return
|
| 105 |
except Exception as exc:
|
| 106 |
logger.warning("Scheduler: load da %s fallito (%s) β provo backup", _path, exc)
|
| 107 |
-
# AUD-005: /tmp assente/corrotto β Supabase fallback (ultimi 24h, status != done)
|
| 108 |
_tasks = {}
|
| 109 |
-
try:
|
| 110 |
-
from .state import _sb
|
| 111 |
-
if _sb is not None:
|
| 112 |
-
_cutoff_ms = int((time.time() - 86400) * 1000)
|
| 113 |
-
_res = (
|
| 114 |
-
_sb.table("scheduler_tasks")
|
| 115 |
-
.select("*")
|
| 116 |
-
.gte("created_at", _cutoff_ms)
|
| 117 |
-
.neq("status", "done")
|
| 118 |
-
.execute()
|
| 119 |
-
)
|
| 120 |
-
if _res and _res.data:
|
| 121 |
-
for row in _res.data:
|
| 122 |
-
if isinstance(row, dict) and "id" in row:
|
| 123 |
-
_tasks[row["id"]] = row
|
| 124 |
-
logger.info("Scheduler: AUD-005 restored %d task da Supabase", len(_tasks))
|
| 125 |
-
return
|
| 126 |
-
except Exception as _sb_exc:
|
| 127 |
-
logger.warning("Scheduler: AUD-005 Supabase fallback fallito (%s)", _sb_exc)
|
| 128 |
logger.warning("Scheduler: nessun task salvato trovato β partenza vuota")
|
| 129 |
|
| 130 |
|
|
@@ -304,11 +272,6 @@ async def _execute_task(task_id: str) -> None:
|
|
| 304 |
_broadcast_sse()
|
| 305 |
if _task_notify:
|
| 306 |
asyncio.create_task(_tg_start(task_id, _task_goal)).add_done_callback(_log_task_exc)
|
| 307 |
-
# ARCH-I4.6: pubblica evento scheduler.task_started sull'Event Bus
|
| 308 |
-
asyncio.create_task(_publish_scheduler_event(
|
| 309 |
-
"scheduler.task_started",
|
| 310 |
-
{"task_id": task_id, "goal": _task_goal},
|
| 311 |
-
)).add_done_callback(_log_task_exc)
|
| 312 |
|
| 313 |
try:
|
| 314 |
result = await _run_goal(task["goal"], task.get("conversationId"))
|
|
@@ -330,11 +293,6 @@ async def _execute_task(task_id: str) -> None:
|
|
| 330 |
_sb_stat_ok = "done" if one_shot else "pending"
|
| 331 |
|
| 332 |
logger.info("Scheduler: β task '%s' (%s)", task.get("label"), task_id)
|
| 333 |
-
# ARCH-I4.6: pubblica evento scheduler.task_completed sull'Event Bus
|
| 334 |
-
asyncio.create_task(_publish_scheduler_event(
|
| 335 |
-
"scheduler.task_completed",
|
| 336 |
-
{"task_id": task_id, "goal": _sb_goal_ok, "status": _sb_stat_ok, "result": result[:300]},
|
| 337 |
-
)).add_done_callback(_log_task_exc)
|
| 338 |
asyncio.create_task(_sb_write_scheduler_result(task_id, _sb_goal_ok, _sb_stat_ok, result, now_ms)).add_done_callback(_log_task_exc)
|
| 339 |
if _task_notify:
|
| 340 |
asyncio.create_task(_tg_done(task_id, _task_goal, result[:500])).add_done_callback(_log_task_exc)
|
|
@@ -357,11 +315,6 @@ async def _execute_task(task_id: str) -> None:
|
|
| 357 |
_broadcast_sse()
|
| 358 |
|
| 359 |
logger.error("Scheduler: β task %s: %s", task_id, exc)
|
| 360 |
-
# ARCH-I4.6: pubblica evento scheduler.task_failed sull'Event Bus
|
| 361 |
-
asyncio.create_task(_publish_scheduler_event(
|
| 362 |
-
"scheduler.task_failed",
|
| 363 |
-
{"task_id": task_id, "goal": _task_goal, "error": str(exc)[:300]},
|
| 364 |
-
)).add_done_callback(_log_task_exc)
|
| 365 |
# GAP-A1: log incident in registry (fire-and-forget, non-blocking)
|
| 366 |
try:
|
| 367 |
from .incident_registry import log_incident as _log_inc
|
|
|
|
| 38 |
from fastapi.responses import StreamingResponse
|
| 39 |
from pydantic import BaseModel
|
| 40 |
import logging
|
| 41 |
+
_logger = logging.getLogger("agente_ai") # S-BUGFIX
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
logger = logging.getLogger("agente_ai.scheduler")
|
| 44 |
|
| 45 |
def _log_task_exc(task): # GAP-2.6: log silently-dropped exceptions in fire-and-forget tasks
|
|
|
|
| 81 |
|
| 82 |
|
| 83 |
def _load_tasks() -> None:
|
| 84 |
+
"""Gap-7-FIX: carica da file principale, fallback a backup se corrotto."""
|
| 85 |
global _tasks
|
| 86 |
for _path in (_TASKS_FILE, _TASKS_BAK):
|
| 87 |
try:
|
|
|
|
| 92 |
return
|
| 93 |
except Exception as exc:
|
| 94 |
logger.warning("Scheduler: load da %s fallito (%s) β provo backup", _path, exc)
|
|
|
|
| 95 |
_tasks = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
logger.warning("Scheduler: nessun task salvato trovato β partenza vuota")
|
| 97 |
|
| 98 |
|
|
|
|
| 272 |
_broadcast_sse()
|
| 273 |
if _task_notify:
|
| 274 |
asyncio.create_task(_tg_start(task_id, _task_goal)).add_done_callback(_log_task_exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
try:
|
| 277 |
result = await _run_goal(task["goal"], task.get("conversationId"))
|
|
|
|
| 293 |
_sb_stat_ok = "done" if one_shot else "pending"
|
| 294 |
|
| 295 |
logger.info("Scheduler: β task '%s' (%s)", task.get("label"), task_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
asyncio.create_task(_sb_write_scheduler_result(task_id, _sb_goal_ok, _sb_stat_ok, result, now_ms)).add_done_callback(_log_task_exc)
|
| 297 |
if _task_notify:
|
| 298 |
asyncio.create_task(_tg_done(task_id, _task_goal, result[:500])).add_done_callback(_log_task_exc)
|
|
|
|
| 315 |
_broadcast_sse()
|
| 316 |
|
| 317 |
logger.error("Scheduler: β task %s: %s", task_id, exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
# GAP-A1: log incident in registry (fire-and-forget, non-blocking)
|
| 319 |
try:
|
| 320 |
from .incident_registry import log_incident as _log_inc
|
api/state.py
CHANGED
|
@@ -33,8 +33,9 @@ _sb2: Any = None
|
|
| 33 |
_sb_fallback: Any = None # Collaboratore D
|
| 34 |
|
| 35 |
try:
|
| 36 |
-
_SUPA_URL = os.getenv('SUPABASE_URL', '')
|
| 37 |
-
_SUPA_KEY = os.getenv('SUPABASE_KEY') or os.getenv('SUPABASE_ANON_KEY', '')
|
|
|
|
| 38 |
|
| 39 |
_SUPA_URL2 = os.getenv("SUPABASE_URL_2", "")
|
| 40 |
_SUPA_KEY2 = os.getenv("SUPABASE_KEY_2", "")
|
|
@@ -422,12 +423,7 @@ def _get_executor() -> Any:
|
|
| 422 |
return _executor
|
| 423 |
try:
|
| 424 |
from agents.executor import Executor
|
| 425 |
-
|
| 426 |
-
try:
|
| 427 |
-
from api.kernel import kernel as _k
|
| 428 |
-
except Exception:
|
| 429 |
-
_k = None
|
| 430 |
-
_executor = Executor(memory=_get_mem_manager(), kernel=_k)
|
| 431 |
except Exception:
|
| 432 |
_executor = None
|
| 433 |
return _executor
|
|
|
|
| 33 |
_sb_fallback: Any = None # Collaboratore D
|
| 34 |
|
| 35 |
try:
|
| 36 |
+
_SUPA_URL = os.getenv('SUPABASE_URL') or os.getenv('SUPABASE_URL_A', 'https://zwdoplodbdsxfrddoxmo.supabase.com')
|
| 37 |
+
_SUPA_KEY = os.getenv('SUPABASE_KEY') or os.getenv('SUPABASE_ANON_KEY') or os.getenv('SUPABASE_KEY_A', 'V7rJncY0k6qm+Hqyg8kz799PgpoI1ZUkuBR8y9Hb0iCy8SzEAW+ubFWT5f0H9LpzTfGu+inQwsgmBW1qDhR5jw==')
|
| 38 |
+
|
| 39 |
|
| 40 |
_SUPA_URL2 = os.getenv("SUPABASE_URL_2", "")
|
| 41 |
_SUPA_KEY2 = os.getenv("SUPABASE_KEY_2", "")
|
|
|
|
| 423 |
return _executor
|
| 424 |
try:
|
| 425 |
from agents.executor import Executor
|
| 426 |
+
_executor = Executor(memory=_get_mem_manager())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
except Exception:
|
| 428 |
_executor = None
|
| 429 |
return _executor
|
api/telegram_webhook.py
CHANGED
|
@@ -389,20 +389,20 @@ async def _cmd_help(chat_id: int) -> None:
|
|
| 389 |
|
| 390 |
|
| 391 |
async def _cmd_logs(chat_id: int, level: str = "WARNING") -> None:
|
| 392 |
-
"""Mostra ultimi log dal backend
|
| 393 |
import httpx as _hx
|
| 394 |
-
|
| 395 |
await _tg_reply(chat_id,
|
| 396 |
f"π <b>Log Railway</b> β <code>{level.upper()}</code>\nβ³ <i>Fetchingβ¦</i>")
|
| 397 |
try:
|
| 398 |
async with _hx.AsyncClient(timeout=10.0) as c:
|
| 399 |
-
r = await c.get(f"{
|
| 400 |
params={"level": level.upper(), "n": 20})
|
| 401 |
data = r.json() if r.status_code == 200 else {}
|
| 402 |
except Exception as exc:
|
| 403 |
await _tg_reply(chat_id,
|
| 404 |
"β <b>Log non disponibili</b>\n<code>" + html.escape(str(exc)[:200]) + "</code>\n"
|
| 405 |
-
"<i>Controlla
|
| 406 |
return
|
| 407 |
records = data.get("records", [])
|
| 408 |
if not records:
|
|
@@ -467,12 +467,12 @@ async def _cmd_status(chat_id: int) -> None:
|
|
| 467 |
sched_label = "β
attivo" if sched_ok else "β fermo"
|
| 468 |
|
| 469 |
ts_now = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 470 |
-
|
| 471 |
ry_line = ""
|
| 472 |
try:
|
| 473 |
import httpx as _hx
|
| 474 |
async with _hx.AsyncClient(timeout=4.0) as c:
|
| 475 |
-
rv = await c.get(f"{
|
| 476 |
if rv.status_code == 200:
|
| 477 |
rj = rv.json()
|
| 478 |
ry_line = ("\nπ <b>Railway:</b> v" + rj.get("version","?")
|
|
@@ -820,10 +820,10 @@ async def _cmd_autofix(chat_id: int, hint: str = "") -> None:
|
|
| 820 |
await _tg_edit(chat_id, msg_id, text, keyboard=_MAIN_KB if final else None)
|
| 821 |
|
| 822 |
# ββ Step 1: leggi errori dal log endpoint βββββββββββββββββββββββββββββββββββββββββ
|
| 823 |
-
|
| 824 |
try:
|
| 825 |
async with httpx.AsyncClient(timeout=10.0) as c:
|
| 826 |
-
resp = await c.get(f"{
|
| 827 |
params={"level": "ERROR", "n": 30})
|
| 828 |
log_data = resp.json() if resp.status_code == 200 else {}
|
| 829 |
except Exception as e:
|
|
@@ -877,7 +877,7 @@ async def _cmd_autofix(chat_id: int, hint: str = "") -> None:
|
|
| 877 |
"2. Per piu' file includi un blocco per file\n"
|
| 878 |
"3. Se non riesci a determinare il file, scrivi FILE: UNKNOWN e spiega"
|
| 879 |
)
|
| 880 |
-
context = f"Backend: {
|
| 881 |
|
| 882 |
_buf: list[str] = []
|
| 883 |
_last: list[float] = [0.0]
|
|
@@ -1344,15 +1344,15 @@ async def _cmd_git(chat_id: int, n: int = 5) -> None:
|
|
| 1344 |
await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
|
| 1345 |
|
| 1346 |
async def _cmd_telemetry(chat_id: int) -> None:
|
| 1347 |
-
"""π‘ Metriche runtime live: /api/telemetry + /debug/timing da
|
| 1348 |
import httpx as _hx_t
|
| 1349 |
-
|
| 1350 |
-
await _tg_reply(chat_id, "β³ <b>Telemetria</b> β interrogo
|
| 1351 |
try:
|
| 1352 |
async with _hx_t.AsyncClient(timeout=8.0) as _c:
|
| 1353 |
tel_r, tim_r = await asyncio.gather(
|
| 1354 |
-
_c.get(f"{
|
| 1355 |
-
_c.get(f"{
|
| 1356 |
return_exceptions=True,
|
| 1357 |
)
|
| 1358 |
except Exception as e:
|
|
@@ -1394,7 +1394,7 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1394 |
"""π Score card dettagliata β chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
|
| 1395 |
import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
|
| 1396 |
gh_token = os.getenv("GITHUB_TOKEN", "").strip()
|
| 1397 |
-
|
| 1398 |
await _tg_reply(chat_id, "β³ <b>Score</b> β carico report + metriche runtimeβ¦")
|
| 1399 |
|
| 1400 |
report: dict | None = None
|
|
@@ -1443,7 +1443,7 @@ async def _cmd_score(chat_id: int) -> None:
|
|
| 1443 |
rt_repair: dict = {}
|
| 1444 |
try:
|
| 1445 |
async with _hx_sc.AsyncClient(timeout=5.0) as _c:
|
| 1446 |
-
_tr = await _c.get(f"{
|
| 1447 |
if _tr.status_code == 200:
|
| 1448 |
_td = _tr.json()
|
| 1449 |
rt_timing = _td.get("timing", {})
|
|
@@ -2129,7 +2129,7 @@ async def _handle_callback(callback_query: dict, token: str) -> None:
|
|
| 2129 |
except Exception:
|
| 2130 |
await _tg_reply(chat_id, "β οΈ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
|
| 2131 |
elif data == "tgw_ping":
|
| 2132 |
-
|
| 2133 |
try:
|
| 2134 |
async with httpx.AsyncClient(timeout=8.0) as _hxc:
|
| 2135 |
r = await _hxc.get(f"{ry}/health")
|
|
@@ -2140,7 +2140,7 @@ async def _handle_callback(callback_query: dict, token: str) -> None:
|
|
| 2140 |
token=token, keyboard=_DEV_MENU_KB)
|
| 2141 |
except Exception as exc:
|
| 2142 |
await _tg_reply(chat_id,
|
| 2143 |
-
"β
|
| 2144 |
token=token, keyboard=_BACK_KB)
|
| 2145 |
|
| 2146 |
# ββ m β menu principale βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -2243,7 +2243,7 @@ async def telegram_webhook(request: Request) -> dict:
|
|
| 2243 |
if lvl not in ("DEBUG","INFO","WARNING","ERROR","CRITICAL"): lvl = "WARNING"
|
| 2244 |
_t=asyncio.create_task(_cmd_logs(chat_id, lvl)); _t.add_done_callback(_log_tg_exc)
|
| 2245 |
elif cmd == "/ping":
|
| 2246 |
-
|
| 2247 |
import httpx as _hx
|
| 2248 |
try:
|
| 2249 |
async with _hx.AsyncClient(timeout=8.0) as c:
|
|
@@ -2256,7 +2256,7 @@ async def telegram_webhook(request: Request) -> dict:
|
|
| 2256 |
keyboard=_BACK_KB)
|
| 2257 |
except Exception as e:
|
| 2258 |
await _tg_reply(chat_id,
|
| 2259 |
-
"β
|
| 2260 |
keyboard=_BACK_KB)
|
| 2261 |
elif cmd in ("/nota", "/ricorda", "/remember"):
|
| 2262 |
note_text = text[len(cmd):].strip()
|
|
@@ -2467,8 +2467,8 @@ async def setup_webhook(request: Request, role: AuthRole = Depends(require_role(
|
|
| 2467 |
body = await request.json()
|
| 2468 |
except Exception:
|
| 2469 |
body = {}
|
| 2470 |
-
#
|
| 2471 |
-
base_url = str(body.get("webhook_url") or os.getenv("
|
| 2472 |
secret = str(body.get("secret") or os.getenv("TELEGRAM_WEBHOOK_SECRET", ""))
|
| 2473 |
webhook_url = f"{base_url}/api/telegram/process"
|
| 2474 |
payload: dict = {
|
|
@@ -2505,8 +2505,8 @@ async def setup_telegram_webhook() -> bool:
|
|
| 2505 |
_logger.warning("setup_telegram_webhook: TELEGRAM_BOT_TOKEN non configurato")
|
| 2506 |
return False
|
| 2507 |
|
| 2508 |
-
#
|
| 2509 |
-
base_url = os.getenv("
|
| 2510 |
base_url = base_url.rstrip("/")
|
| 2511 |
secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
|
| 2512 |
webhook_url = f"{base_url}/api/telegram/process"
|
|
|
|
| 389 |
|
| 390 |
|
| 391 |
async def _cmd_logs(chat_id: int, level: str = "WARNING") -> None:
|
| 392 |
+
"""Mostra ultimi log dal backend Railway filtrando per livello."""
|
| 393 |
import httpx as _hx
|
| 394 |
+
railway_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
|
| 395 |
await _tg_reply(chat_id,
|
| 396 |
f"π <b>Log Railway</b> β <code>{level.upper()}</code>\nβ³ <i>Fetchingβ¦</i>")
|
| 397 |
try:
|
| 398 |
async with _hx.AsyncClient(timeout=10.0) as c:
|
| 399 |
+
r = await c.get(f"{railway_url}/api/telegram/logs",
|
| 400 |
params={"level": level.upper(), "n": 20})
|
| 401 |
data = r.json() if r.status_code == 200 else {}
|
| 402 |
except Exception as exc:
|
| 403 |
await _tg_reply(chat_id,
|
| 404 |
"β <b>Log non disponibili</b>\n<code>" + html.escape(str(exc)[:200]) + "</code>\n"
|
| 405 |
+
"<i>Controlla Railway dashboard.</i>", keyboard=_BACK_KB)
|
| 406 |
return
|
| 407 |
records = data.get("records", [])
|
| 408 |
if not records:
|
|
|
|
| 467 |
sched_label = "β
attivo" if sched_ok else "β fermo"
|
| 468 |
|
| 469 |
ts_now = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 470 |
+
railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
|
| 471 |
ry_line = ""
|
| 472 |
try:
|
| 473 |
import httpx as _hx
|
| 474 |
async with _hx.AsyncClient(timeout=4.0) as c:
|
| 475 |
+
rv = await c.get(f"{railway_url}/api/info")
|
| 476 |
if rv.status_code == 200:
|
| 477 |
rj = rv.json()
|
| 478 |
ry_line = ("\nπ <b>Railway:</b> v" + rj.get("version","?")
|
|
|
|
| 820 |
await _tg_edit(chat_id, msg_id, text, keyboard=_MAIN_KB if final else None)
|
| 821 |
|
| 822 |
# ββ Step 1: leggi errori dal log endpoint βββββββββββββββββββββββββββββββββββββββββ
|
| 823 |
+
railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app").rstrip("/")
|
| 824 |
try:
|
| 825 |
async with httpx.AsyncClient(timeout=10.0) as c:
|
| 826 |
+
resp = await c.get(f"{railway_url}/api/telegram/logs",
|
| 827 |
params={"level": "ERROR", "n": 30})
|
| 828 |
log_data = resp.json() if resp.status_code == 200 else {}
|
| 829 |
except Exception as e:
|
|
|
|
| 877 |
"2. Per piu' file includi un blocco per file\n"
|
| 878 |
"3. Se non riesci a determinare il file, scrivi FILE: UNKNOWN e spiega"
|
| 879 |
)
|
| 880 |
+
context = f"Backend: {railway_url} Repo: {os.getenv('GITHUB_REPO','Baida98/AI')}"
|
| 881 |
|
| 882 |
_buf: list[str] = []
|
| 883 |
_last: list[float] = [0.0]
|
|
|
|
| 1344 |
await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
|
| 1345 |
|
| 1346 |
async def _cmd_telemetry(chat_id: int) -> None:
|
| 1347 |
+
"""π‘ Metriche runtime live: /api/telemetry + /debug/timing da Railway."""
|
| 1348 |
import httpx as _hx_t
|
| 1349 |
+
rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
|
| 1350 |
+
await _tg_reply(chat_id, "β³ <b>Telemetria</b> β interrogo Railwayβ¦")
|
| 1351 |
try:
|
| 1352 |
async with _hx_t.AsyncClient(timeout=8.0) as _c:
|
| 1353 |
tel_r, tim_r = await asyncio.gather(
|
| 1354 |
+
_c.get(f"{rw_url}/api/telemetry"),
|
| 1355 |
+
_c.get(f"{rw_url}/debug/timing"),
|
| 1356 |
return_exceptions=True,
|
| 1357 |
)
|
| 1358 |
except Exception as e:
|
|
|
|
| 1394 |
"""π Score card dettagliata β chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
|
| 1395 |
import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
|
| 1396 |
gh_token = os.getenv("GITHUB_TOKEN", "").strip()
|
| 1397 |
+
rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
|
| 1398 |
await _tg_reply(chat_id, "β³ <b>Score</b> β carico report + metriche runtimeβ¦")
|
| 1399 |
|
| 1400 |
report: dict | None = None
|
|
|
|
| 1443 |
rt_repair: dict = {}
|
| 1444 |
try:
|
| 1445 |
async with _hx_sc.AsyncClient(timeout=5.0) as _c:
|
| 1446 |
+
_tr = await _c.get(f"{rw_url}/api/telemetry")
|
| 1447 |
if _tr.status_code == 200:
|
| 1448 |
_td = _tr.json()
|
| 1449 |
rt_timing = _td.get("timing", {})
|
|
|
|
| 2129 |
except Exception:
|
| 2130 |
await _tg_reply(chat_id, "β οΈ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
|
| 2131 |
elif data == "tgw_ping":
|
| 2132 |
+
ry = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
|
| 2133 |
try:
|
| 2134 |
async with httpx.AsyncClient(timeout=8.0) as _hxc:
|
| 2135 |
r = await _hxc.get(f"{ry}/health")
|
|
|
|
| 2140 |
token=token, keyboard=_DEV_MENU_KB)
|
| 2141 |
except Exception as exc:
|
| 2142 |
await _tg_reply(chat_id,
|
| 2143 |
+
"β Railway non raggiungibile\n<code>"+html.escape(str(exc)[:150])+"</code>",
|
| 2144 |
token=token, keyboard=_BACK_KB)
|
| 2145 |
|
| 2146 |
# ββ m β menu principale βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 2243 |
if lvl not in ("DEBUG","INFO","WARNING","ERROR","CRITICAL"): lvl = "WARNING"
|
| 2244 |
_t=asyncio.create_task(_cmd_logs(chat_id, lvl)); _t.add_done_callback(_log_tg_exc)
|
| 2245 |
elif cmd == "/ping":
|
| 2246 |
+
ry = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
|
| 2247 |
import httpx as _hx
|
| 2248 |
try:
|
| 2249 |
async with _hx.AsyncClient(timeout=8.0) as c:
|
|
|
|
| 2256 |
keyboard=_BACK_KB)
|
| 2257 |
except Exception as e:
|
| 2258 |
await _tg_reply(chat_id,
|
| 2259 |
+
"β Railway non raggiungibile\n<code>"+html.escape(str(e)[:150])+"</code>",
|
| 2260 |
keyboard=_BACK_KB)
|
| 2261 |
elif cmd in ("/nota", "/ricorda", "/remember"):
|
| 2262 |
note_text = text[len(cmd):].strip()
|
|
|
|
| 2467 |
body = await request.json()
|
| 2468 |
except Exception:
|
| 2469 |
body = {}
|
| 2470 |
+
# P12-FIX: Default a Railway URL per stabilitΓ , fallback su CF
|
| 2471 |
+
base_url = str(body.get("webhook_url") or os.getenv("RAILWAY_URL") or os.getenv("CF_PAGES_URL") or "https://ai-production-4c06.up.railway.app").rstrip("/")
|
| 2472 |
secret = str(body.get("secret") or os.getenv("TELEGRAM_WEBHOOK_SECRET", ""))
|
| 2473 |
webhook_url = f"{base_url}/api/telegram/process"
|
| 2474 |
payload: dict = {
|
|
|
|
| 2505 |
_logger.warning("setup_telegram_webhook: TELEGRAM_BOT_TOKEN non configurato")
|
| 2506 |
return False
|
| 2507 |
|
| 2508 |
+
# P12-FIX: Usa Railway URL diretto se CF Pages ha problemi di routing
|
| 2509 |
+
base_url = os.getenv("RAILWAY_URL") or os.getenv("CF_PAGES_URL") or "https://ai-production-4c06.up.railway.app"
|
| 2510 |
base_url = base_url.rstrip("/")
|
| 2511 |
secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
|
| 2512 |
webhook_url = f"{base_url}/api/telegram/process"
|
api/webhook.py
CHANGED
|
@@ -115,7 +115,7 @@ async def telegram_set_webhook(
|
|
| 115 |
role: AuthRole = Depends(require_role(AuthRole.ADMIN)), # GAP-WEBHOOK-ADMIN-FIX
|
| 116 |
) -> dict:
|
| 117 |
"""
|
| 118 |
-
Registra il webhook Telegram su
|
| 119 |
Richiede: ruolo ADMIN (header X-Admin-Token = ADMIN_TOKEN) + TELEGRAM_BOT_TOKEN env var.
|
| 120 |
Chiama: POST https://api.telegram.org/bot{TOKEN}/setWebhook
|
| 121 |
Il secret token Γ¨ TELEGRAM_WEBHOOK_SECRET (generato casualmente se assente).
|
|
@@ -131,21 +131,18 @@ async def telegram_set_webhook(
|
|
| 131 |
if not _tg_token:
|
| 132 |
raise HTTPException(status_code=503, detail='TELEGRAM_BOT_TOKEN non configurato')
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
or 'https://baida-a-terminal.hf.space'
|
| 140 |
-
).rstrip('/')
|
| 141 |
-
_wh_url = f'{_base_url}/api/telegram/callback'
|
| 142 |
|
| 143 |
_secret = os.getenv('TELEGRAM_WEBHOOK_SECRET', '')
|
| 144 |
if not _secret:
|
| 145 |
import secrets as _sec
|
| 146 |
_secret = _sec.token_hex(24)
|
| 147 |
# Non possiamo settare env var runtime, ma logghiamo per configurazione manuale
|
| 148 |
-
_logger.critical('TG-WEBHOOK: genera TELEGRAM_WEBHOOK_SECRET=%r e aggiungilo
|
| 149 |
|
| 150 |
try:
|
| 151 |
import httpx as _hx
|
|
@@ -266,7 +263,7 @@ async def public_chat(payload: PublicChatPayload, request: Request):
|
|
| 266 |
S292 β API REST pubblica autenticata per integrazioni esterne.
|
| 267 |
Auth: Authorization: Bearer <PUBLIC_API_TOKEN>
|
| 268 |
"""
|
| 269 |
-
_expected = os.getenv('PUBLIC_API_TOKEN', '').strip()
|
| 270 |
if not _expected:
|
| 271 |
raise HTTPException(
|
| 272 |
status_code=503,
|
|
|
|
| 115 |
role: AuthRole = Depends(require_role(AuthRole.ADMIN)), # GAP-WEBHOOK-ADMIN-FIX
|
| 116 |
) -> dict:
|
| 117 |
"""
|
| 118 |
+
Registra il webhook Telegram su Railway.
|
| 119 |
Richiede: ruolo ADMIN (header X-Admin-Token = ADMIN_TOKEN) + TELEGRAM_BOT_TOKEN env var.
|
| 120 |
Chiama: POST https://api.telegram.org/bot{TOKEN}/setWebhook
|
| 121 |
Il secret token Γ¨ TELEGRAM_WEBHOOK_SECRET (generato casualmente se assente).
|
|
|
|
| 131 |
if not _tg_token:
|
| 132 |
raise HTTPException(status_code=503, detail='TELEGRAM_BOT_TOKEN non configurato')
|
| 133 |
|
| 134 |
+
_railway_url = os.getenv('RAILWAY_PUBLIC_DOMAIN', '') or os.getenv('RAILWAY_URL', '')
|
| 135 |
+
if not _railway_url:
|
| 136 |
+
raise HTTPException(status_code=503, detail='RAILWAY_PUBLIC_DOMAIN non configurato')
|
| 137 |
+
|
| 138 |
+
_wh_url = f'https://{_railway_url.lstrip("https://").rstrip("/")}/api/telegram/callback'
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
_secret = os.getenv('TELEGRAM_WEBHOOK_SECRET', '')
|
| 141 |
if not _secret:
|
| 142 |
import secrets as _sec
|
| 143 |
_secret = _sec.token_hex(24)
|
| 144 |
# Non possiamo settare env var runtime, ma logghiamo per configurazione manuale
|
| 145 |
+
_logger.critical('TG-WEBHOOK: genera TELEGRAM_WEBHOOK_SECRET=%r e aggiungilo a Railway env!', _secret)
|
| 146 |
|
| 147 |
try:
|
| 148 |
import httpx as _hx
|
|
|
|
| 263 |
S292 β API REST pubblica autenticata per integrazioni esterne.
|
| 264 |
Auth: Authorization: Bearer <PUBLIC_API_TOKEN>
|
| 265 |
"""
|
| 266 |
+
_expected = (os.getenv('PUBLIC_API_TOKEN') or os.getenv('INTERNAL_TOKEN', '')).strip()
|
| 267 |
if not _expected:
|
| 268 |
raise HTTPException(
|
| 269 |
status_code=503,
|
main.py
CHANGED
|
@@ -44,28 +44,24 @@ _logger = logging.getLogger('agente_ai')
|
|
| 44 |
_GENERATED_TOKEN = _secrets_mod.token_hex(32)
|
| 45 |
if "INTERNAL_TOKEN" not in os.environ or os.environ["INTERNAL_TOKEN"] == _GENERATED_TOKEN:
|
| 46 |
os.environ['INTERNAL_TOKEN'] = _GENERATED_TOKEN
|
| 47 |
-
_TOKEN_IS_EPHEMERAL = True # CRIT-2: esposto via /api/token-status per banner CF
|
| 48 |
_logger.critical('BOOT: INTERNAL_TOKEN not set β ephemeral token generato per questa sessione.')
|
| 49 |
_logger.critical('BOOT: Ogni restart cambia il token β CF Worker riceve 401 finchΓ© il secret non Γ¨ aggiornato!')
|
| 50 |
_logger.critical('BOOT: session token (copia in CF Workers secret INTERNAL_TOKEN): %r', _GENERATED_TOKEN)
|
| 51 |
_logger.critical('BOOT: Fix β imposta INTERNAL_TOKEN uguale su HF Spaces e CF Workers secrets.')
|
| 52 |
else:
|
| 53 |
-
_TOKEN_IS_EPHEMERAL = False # CRIT-2: token correttamente configurato
|
| 54 |
_logger.info('BOOT: INTERNAL_TOKEN configurato OK')
|
| 55 |
|
| 56 |
# ββ CORS β env-driven, Safari-safe βββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
# ALLOWED_ORIGINS: comma-separated list, e.g. "https://agente-ai.vercel.app,http://localhost:5173"
|
| 58 |
# Supports wildcard suffix match (*.vercel.app, *.hf.space) for preview URLs.
|
| 59 |
_ALLOWED_ORIGINS_ENV = os.getenv('ALLOWED_ORIGINS', '')
|
| 60 |
-
# MED-3: localhost origins only in dev β never expose in production
|
| 61 |
-
_IS_DEV = os.getenv("ENVIRONMENT", "production").lower() in ("development", "dev", "local")
|
| 62 |
_ALWAYS_ALLOWED = [
|
| 63 |
'http://localhost:5173',
|
| 64 |
'http://localhost:4173',
|
| 65 |
'http://localhost:3000',
|
| 66 |
'http://localhost:8080',
|
| 67 |
'http://localhost:8000',
|
| 68 |
-
]
|
| 69 |
_VERCEL_PATTERNS = ['.vercel.app', '.vercel.sh', '.pages.dev']
|
| 70 |
_HF_PATTERNS = ['.hf.space', '.huggingface.co']
|
| 71 |
|
|
@@ -229,22 +225,7 @@ from api.skills import router as _skills_router # P17-B2
|
|
| 229 |
from api.event_bus import router as _event_bus_router # ARCH-F1.2: Event Bus (ADR Fase 1)
|
| 230 |
from api.event_store import router as _event_store_router # ARCH-F1.3: Event Store (ADR Fase 1)
|
| 231 |
from api.session_manager import router as _session_mgr_router # ARCH-F1.4: Session Manager (ADR Fase 1)
|
| 232 |
-
from api.hf_monitor import router as _hf_monitor_router, start_monitor as _start_hf_monitor # ARCH-P5.2: HF Spaces Monitor
|
| 233 |
from api.kernel import router as _kernel_router # ARCH-K2.1: AI Kernel (interfaccia unica BrainβKernel)
|
| 234 |
-
from api.policy import router as _policy_router # ARCH-K2.4: Policy Engine
|
| 235 |
-
from api.memory_router import router as _mem_router_router # ARCH-K2.3: Memory Router unificato
|
| 236 |
-
from api.capability_catalog import router as _catalog_router # ARCH-E3.1: Capability Marketplace
|
| 237 |
-
from api.capability_resolver import router as _resolver_router # ARCH-E3.2: Capability Resolver
|
| 238 |
-
from api.plugin_system import router as _plugins_router # ARCH-E3.3: Plugin System Sandboxato
|
| 239 |
-
from api.workflow_engine import router as _workflow_router # ARCH-I4.2: Workflow Engine
|
| 240 |
-
from api.agent_fsm import router as _fsm_router # ARCH-I4.5: Agent FSM
|
| 241 |
-
from api.brain_planner import router as _planner_router # ARCH-I4.1: Brain Planner
|
| 242 |
-
from api.tool_engine import router as _tool_engine_router # ARCH-I4.3: Tool Engine
|
| 243 |
-
from api.health_manager import router as _health_mgr_router, health_manager as _hm_singleton # OPS-1: Health Manager (Circuit Breaker + Recovery)
|
| 244 |
-
from api.llm_router import router as _llm_router # ARCH-I4.4: LLM Provider Router (capability-aware)
|
| 245 |
-
from api.oracle_endpoints import router as _oracle_router # ARCH-K3.1: Oracle Provider (/api/oracle/**)
|
| 246 |
-
from api.scaffold_project import router as _scaffold_router # scaffold: /api/scaffold_project
|
| 247 |
-
from api.whoami import router as _whoami_router # /api/whoami-v2
|
| 248 |
# Doc2-1b-FIX: memory/sync router non era montato β endpoint /api/memory/sync/* non raggiungibili
|
| 249 |
# NOTA: create_memory_sync_router(memory) Γ¨ una factory β richiede l'istanza MemoryManager.
|
| 250 |
# GAP-5-FIX: memory/sync router montato in _on_startup() (vedi sotto)
|
|
@@ -289,20 +270,6 @@ app.include_router(_event_bus_router) # ARCH-F1.2: /api/events/publish + /api
|
|
| 289 |
app.include_router(_event_store_router) # ARCH-F1.3: /api/events/store + /api/events/replay
|
| 290 |
app.include_router(_session_mgr_router) # ARCH-F1.4: /api/sessions/**
|
| 291 |
app.include_router(_kernel_router) # ARCH-K2.1: /api/kernel/** (submitTask, chat, memory, publishEvent)
|
| 292 |
-
app.include_router(_policy_router) # ARCH-K2.4: /api/policy/** (check, budget, usage, status)
|
| 293 |
-
app.include_router(_mem_router_router) # ARCH-K2.3: /api/memory/router/** (status)
|
| 294 |
-
app.include_router(_catalog_router) # ARCH-E3.1: /api/catalog/** (register, heartbeat, capabilities, worker-announce)
|
| 295 |
-
app.include_router(_resolver_router) # ARCH-E3.2: /api/resolver/** (resolve, resolve-many, status)
|
| 296 |
-
app.include_router(_plugins_router) # ARCH-E3.3: /api/plugins/** (register, execute, healthcheck, rollback)
|
| 297 |
-
app.include_router(_workflow_router) # ARCH-I4.2: /api/workflow/** (submit, cancel, executions, status)
|
| 298 |
-
app.include_router(_fsm_router) # ARCH-I4.5: /api/agent-fsm/** (run, runs, status)
|
| 299 |
-
app.include_router(_planner_router) # ARCH-I4.1: /api/brain/** (plan, reflect, status)
|
| 300 |
-
app.include_router(_tool_engine_router) # ARCH-I4.3: /api/tools/** (register, list, schema)
|
| 301 |
-
app.include_router(_health_mgr_router) # OPS-1: /api/health-manager/** (status, report, recover, traffic)
|
| 302 |
-
app.include_router(_llm_router) # ARCH-I4.4: /api/llm/** (route, call, status, reload)
|
| 303 |
-
app.include_router(_oracle_router) # ARCH-K3.1: /api/oracle/** (health, status, reason)
|
| 304 |
-
app.include_router(_scaffold_router) # scaffold: /api/scaffold_project
|
| 305 |
-
app.include_router(_whoami_router) # whoami: /api/whoami-v2
|
| 306 |
# (memory/sync router montato in _on_startup)
|
| 307 |
|
| 308 |
# ββ Startup: heartbeat + warmup ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -327,16 +294,6 @@ async def _on_startup():
|
|
| 327 |
_start_scheduler()
|
| 328 |
_start_incident_reg() # GAP-A1: Incident Registry
|
| 329 |
_start_decision_mem() # GAP-A2: Decision Memory
|
| 330 |
-
try:
|
| 331 |
-
_start_hf_monitor()
|
| 332 |
-
_logger.info('BOOT: hf_monitor polling avviato (ARCH-P5.2)')
|
| 333 |
-
except Exception as _hfm_err:
|
| 334 |
-
_logger.warning('BOOT: hf_monitor skip β %s', _hfm_err)
|
| 335 |
-
try:
|
| 336 |
-
_hm_singleton.start_monitor()
|
| 337 |
-
_logger.info('BOOT: health_manager monitor avviato (OPS-1)')
|
| 338 |
-
except Exception as _hm_err:
|
| 339 |
-
_logger.warning('BOOT: health_manager monitor skip β %s', _hm_err)
|
| 340 |
_logger.info('BOOT: scheduler server-side avviato')
|
| 341 |
# S388: warmup TCP connection pools β inizializza i client Groq con 1 token
|
| 342 |
# così la prima vera richiesta utente non paga il costo di handshake HTTP/TLS (~80ms per provider).
|
|
@@ -363,10 +320,6 @@ async def _on_startup():
|
|
| 363 |
app.include_router(_skills_router) # P17-B2
|
| 364 |
except Exception as _skills_err:
|
| 365 |
_logger.warning('BOOT: skills_router registration err β %s', _skills_err)
|
| 366 |
-
try:
|
| 367 |
-
app.include_router(_hf_monitor_router) # ARCH-P5.2: HF Spaces Monitor
|
| 368 |
-
except Exception as _hfr_err:
|
| 369 |
-
_logger.warning('BOOT: hf_monitor_router registration err β %s', _hfr_err)
|
| 370 |
import asyncio as _aio
|
| 371 |
_t_wm = _aio.create_task(_startup_warmup())
|
| 372 |
_t_wm.add_done_callback(lambda t: _log_task_exc(t, 'startup_warmup'))
|
|
@@ -409,23 +362,6 @@ async def _on_startup():
|
|
| 409 |
_logger.info('BOOT: job queue consumer/publisher avviato (SPACE_ROLE=%s)', os.getenv('SPACE_ROLE', 'unknown'))
|
| 410 |
except Exception as _jq_err:
|
| 411 |
_logger.warning('BOOT: job queue consumer skip β %s', _jq_err)
|
| 412 |
-
# ARCH-E3.1: avvia cleanup loop Catalog (rimuove provider con TTL scaduto)
|
| 413 |
-
try:
|
| 414 |
-
from api.capability_catalog import catalog as _capability_catalog
|
| 415 |
-
_capability_catalog.start_cleanup_loop()
|
| 416 |
-
_logger.info('BOOT: Capability Catalog cleanup loop avviato (TTL=%ss)', os.getenv('CATALOG_TTL_S', '300'))
|
| 417 |
-
except Exception as _cat_err:
|
| 418 |
-
_logger.warning('BOOT: capability catalog cleanup skip β %s', _cat_err)
|
| 419 |
-
|
| 420 |
-
# ARCH-I4.3: bootstrap tool fondamentali
|
| 421 |
-
try:
|
| 422 |
-
from api.bootstrap_tools import bootstrap_all_tools as _bootstrap
|
| 423 |
-
_t_bt = _aio.create_task(_bootstrap())
|
| 424 |
-
_t_bt.add_done_callback(lambda t: _log_task_exc(t, 'bootstrap_tools'))
|
| 425 |
-
_logger.info('BOOT: Tool bootstrap task creato')
|
| 426 |
-
except Exception as _bt_err:
|
| 427 |
-
_logger.warning('BOOT: tool bootstrap skip β %s', _bt_err)
|
| 428 |
-
|
| 429 |
# TG-WEBHOOK-AUTO: Registra il webhook all'avvio se USE_WEBHOOK=true
|
| 430 |
if os.getenv('USE_WEBHOOK', '').lower() == 'true':
|
| 431 |
try:
|
|
@@ -437,24 +373,6 @@ async def _on_startup():
|
|
| 437 |
_logger.warning('BOOT: Telegram webhook auto-setup skip β %s', _tg_wh_err)
|
| 438 |
|
| 439 |
|
| 440 |
-
# ββ CRIT-2: /api/token-status β PUBLIC endpoint (no auth) per CF Worker ββββββ
|
| 441 |
-
# CF Worker usa questa risposta per mostrare un banner se il token Γ¨ ephemeral.
|
| 442 |
-
# Non rivela il token β solo lo stato (ephemeral vs configurato).
|
| 443 |
-
@app.get('/api/token-status', include_in_schema=False)
|
| 444 |
-
async def _token_status_endpoint():
|
| 445 |
-
"""CRIT-2: permette al CF Worker di rilevare INTERNAL_TOKEN ephemeral silenzioso."""
|
| 446 |
-
from fastapi.responses import JSONResponse
|
| 447 |
-
return JSONResponse({
|
| 448 |
-
'token_configured': not _TOKEN_IS_EPHEMERAL,
|
| 449 |
-
'ephemeral': _TOKEN_IS_EPHEMERAL,
|
| 450 |
-
'message': (
|
| 451 |
-
'INTERNAL_TOKEN non configurato β ogni restart invalida il token CF Worker.'
|
| 452 |
-
if _TOKEN_IS_EPHEMERAL else
|
| 453 |
-
'INTERNAL_TOKEN configurato correttamente.'
|
| 454 |
-
),
|
| 455 |
-
})
|
| 456 |
-
|
| 457 |
-
|
| 458 |
async def _startup_warmup() -> None:
|
| 459 |
"""
|
| 460 |
S388: Warmup dei provider Groq al boot.
|
|
|
|
| 44 |
_GENERATED_TOKEN = _secrets_mod.token_hex(32)
|
| 45 |
if "INTERNAL_TOKEN" not in os.environ or os.environ["INTERNAL_TOKEN"] == _GENERATED_TOKEN:
|
| 46 |
os.environ['INTERNAL_TOKEN'] = _GENERATED_TOKEN
|
|
|
|
| 47 |
_logger.critical('BOOT: INTERNAL_TOKEN not set β ephemeral token generato per questa sessione.')
|
| 48 |
_logger.critical('BOOT: Ogni restart cambia il token β CF Worker riceve 401 finchΓ© il secret non Γ¨ aggiornato!')
|
| 49 |
_logger.critical('BOOT: session token (copia in CF Workers secret INTERNAL_TOKEN): %r', _GENERATED_TOKEN)
|
| 50 |
_logger.critical('BOOT: Fix β imposta INTERNAL_TOKEN uguale su HF Spaces e CF Workers secrets.')
|
| 51 |
else:
|
|
|
|
| 52 |
_logger.info('BOOT: INTERNAL_TOKEN configurato OK')
|
| 53 |
|
| 54 |
# ββ CORS β env-driven, Safari-safe βββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
# ALLOWED_ORIGINS: comma-separated list, e.g. "https://agente-ai.vercel.app,http://localhost:5173"
|
| 56 |
# Supports wildcard suffix match (*.vercel.app, *.hf.space) for preview URLs.
|
| 57 |
_ALLOWED_ORIGINS_ENV = os.getenv('ALLOWED_ORIGINS', '')
|
|
|
|
|
|
|
| 58 |
_ALWAYS_ALLOWED = [
|
| 59 |
'http://localhost:5173',
|
| 60 |
'http://localhost:4173',
|
| 61 |
'http://localhost:3000',
|
| 62 |
'http://localhost:8080',
|
| 63 |
'http://localhost:8000',
|
| 64 |
+
]
|
| 65 |
_VERCEL_PATTERNS = ['.vercel.app', '.vercel.sh', '.pages.dev']
|
| 66 |
_HF_PATTERNS = ['.hf.space', '.huggingface.co']
|
| 67 |
|
|
|
|
| 225 |
from api.event_bus import router as _event_bus_router # ARCH-F1.2: Event Bus (ADR Fase 1)
|
| 226 |
from api.event_store import router as _event_store_router # ARCH-F1.3: Event Store (ADR Fase 1)
|
| 227 |
from api.session_manager import router as _session_mgr_router # ARCH-F1.4: Session Manager (ADR Fase 1)
|
|
|
|
| 228 |
from api.kernel import router as _kernel_router # ARCH-K2.1: AI Kernel (interfaccia unica BrainβKernel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
# Doc2-1b-FIX: memory/sync router non era montato β endpoint /api/memory/sync/* non raggiungibili
|
| 230 |
# NOTA: create_memory_sync_router(memory) Γ¨ una factory β richiede l'istanza MemoryManager.
|
| 231 |
# GAP-5-FIX: memory/sync router montato in _on_startup() (vedi sotto)
|
|
|
|
| 270 |
app.include_router(_event_store_router) # ARCH-F1.3: /api/events/store + /api/events/replay
|
| 271 |
app.include_router(_session_mgr_router) # ARCH-F1.4: /api/sessions/**
|
| 272 |
app.include_router(_kernel_router) # ARCH-K2.1: /api/kernel/** (submitTask, chat, memory, publishEvent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
# (memory/sync router montato in _on_startup)
|
| 274 |
|
| 275 |
# ββ Startup: heartbeat + warmup ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 294 |
_start_scheduler()
|
| 295 |
_start_incident_reg() # GAP-A1: Incident Registry
|
| 296 |
_start_decision_mem() # GAP-A2: Decision Memory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
_logger.info('BOOT: scheduler server-side avviato')
|
| 298 |
# S388: warmup TCP connection pools β inizializza i client Groq con 1 token
|
| 299 |
# così la prima vera richiesta utente non paga il costo di handshake HTTP/TLS (~80ms per provider).
|
|
|
|
| 320 |
app.include_router(_skills_router) # P17-B2
|
| 321 |
except Exception as _skills_err:
|
| 322 |
_logger.warning('BOOT: skills_router registration err β %s', _skills_err)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
import asyncio as _aio
|
| 324 |
_t_wm = _aio.create_task(_startup_warmup())
|
| 325 |
_t_wm.add_done_callback(lambda t: _log_task_exc(t, 'startup_warmup'))
|
|
|
|
| 362 |
_logger.info('BOOT: job queue consumer/publisher avviato (SPACE_ROLE=%s)', os.getenv('SPACE_ROLE', 'unknown'))
|
| 363 |
except Exception as _jq_err:
|
| 364 |
_logger.warning('BOOT: job queue consumer skip β %s', _jq_err)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
# TG-WEBHOOK-AUTO: Registra il webhook all'avvio se USE_WEBHOOK=true
|
| 366 |
if os.getenv('USE_WEBHOOK', '').lower() == 'true':
|
| 367 |
try:
|
|
|
|
| 373 |
_logger.warning('BOOT: Telegram webhook auto-setup skip β %s', _tg_wh_err)
|
| 374 |
|
| 375 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
async def _startup_warmup() -> None:
|
| 377 |
"""
|
| 378 |
S388: Warmup dei provider Groq al boot.
|
tests/test_regression_doc2.py
CHANGED
|
@@ -231,7 +231,7 @@ class TestMemorySyncRouterMount(unittest.TestCase):
|
|
| 231 |
class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
|
| 232 |
"""
|
| 233 |
Bug originale: il terminale (/api/terminal, /ws/terminal) veniva instradato
|
| 234 |
-
in modo fisso sullo Space A (
|
| 235 |
che su HANDS (Space B), causando comportamento errato in produzione.
|
| 236 |
|
| 237 |
Fix: functions/api/[[catchall]].ts instrada /api/terminal e /ws/* verso
|
|
@@ -274,7 +274,7 @@ class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
|
|
| 274 |
src = self._read(self._CATCHALL)
|
| 275 |
self.assertIn("env.BACKEND_URL_A", src,
|
| 276 |
"BACKEND_URL_A non piΓΉ letto da env β verificare come viene risolto il backend BRAIN")
|
| 277 |
-
self.assertNotIn("
|
| 278 |
"Hostname reale dello Space A hardcoded in catchall.ts β deve restare solo un placeholder/commento")
|
| 279 |
|
| 280 |
def test_space_a_only_appears_as_last_fallback_in_chain(self):
|
|
@@ -283,7 +283,7 @@ class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
|
|
| 283 |
idx_chain = src.find("_getBackendChain")
|
| 284 |
self.assertNotEqual(idx_chain, -1, "_getBackendChain non trovato in agentSSE.ts")
|
| 285 |
chain_block = src[idx_chain: idx_chain + 1500]
|
| 286 |
-
idx_space_a = chain_block.find("
|
| 287 |
idx_space_b = chain_block.find("baida00-ai-backend-collab.hf.space")
|
| 288 |
self.assertNotEqual(idx_space_a, -1, "Space A non trovato nella catena di fallback")
|
| 289 |
self.assertNotEqual(idx_space_b, -1, "Space B non trovato nella catena di fallback")
|
|
|
|
| 231 |
class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
|
| 232 |
"""
|
| 233 |
Bug originale: il terminale (/api/terminal, /ws/terminal) veniva instradato
|
| 234 |
+
in modo fisso sullo Space A (arjanit98-terminal.hf.space / BRAIN) invece
|
| 235 |
che su HANDS (Space B), causando comportamento errato in produzione.
|
| 236 |
|
| 237 |
Fix: functions/api/[[catchall]].ts instrada /api/terminal e /ws/* verso
|
|
|
|
| 274 |
src = self._read(self._CATCHALL)
|
| 275 |
self.assertIn("env.BACKEND_URL_A", src,
|
| 276 |
"BACKEND_URL_A non piΓΉ letto da env β verificare come viene risolto il backend BRAIN")
|
| 277 |
+
self.assertNotIn("arjanit98-terminal.hf.space", src,
|
| 278 |
"Hostname reale dello Space A hardcoded in catchall.ts β deve restare solo un placeholder/commento")
|
| 279 |
|
| 280 |
def test_space_a_only_appears_as_last_fallback_in_chain(self):
|
|
|
|
| 283 |
idx_chain = src.find("_getBackendChain")
|
| 284 |
self.assertNotEqual(idx_chain, -1, "_getBackendChain non trovato in agentSSE.ts")
|
| 285 |
chain_block = src[idx_chain: idx_chain + 1500]
|
| 286 |
+
idx_space_a = chain_block.find("arjanit98-terminal.hf.space")
|
| 287 |
idx_space_b = chain_block.find("baida00-ai-backend-collab.hf.space")
|
| 288 |
self.assertNotEqual(idx_space_a, -1, "Space A non trovato nella catena di fallback")
|
| 289 |
self.assertNotEqual(idx_space_b, -1, "Space B non trovato nella catena di fallback")
|