Spaces:
Configuration error
Configuration error
| import os | |
| import json | |
| import time | |
| import asyncio | |
| import logging | |
| from typing import Optional, List, Dict, Any | |
| from fastapi import APIRouter, HTTPException, Request | |
| from pydantic import BaseModel | |
| import httpx | |
| _logger = logging.getLogger("agente_ai.jq") | |
| router = APIRouter(prefix="/jq", tags=["job_queue"]) | |
| # ── Configurazione ──────────────────────────────────────────────────────────── | |
| # S42: Grid Orchestrator — Cluster 4x4 (A, B, C, D) | |
| # SPACE_ROLE: brain | hands | memory | audit | |
| _SPACE_ROLE = os.getenv("SPACE_ROLE", "unknown") | |
| _JQ_ENABLED = os.getenv("JQ_ENABLED", "0") == "1" | |
| _INTERNAL_TOKEN = os.getenv("INTERNAL_TOKEN", "") | |
| # Redis keys (Upstash) | |
| _K_PENDING = "jq:pending" | |
| _K_WAKE = "jq:wake" | |
| _K_CONSUMER = f"jq:consumer:{_SPACE_ROLE}" | |
| _K_RESULT = lambda tid: f"jq:result:{tid}" | |
| _K_EVENTS = lambda tid: f"jq:events:{tid}" | |
| _K_LOAD = lambda role: f"jq:load:{role}" | |
| class JobPayload(BaseModel): | |
| taskId: str | |
| goal: str | |
| context: Optional[Dict[str, Any]] = None | |
| priority: int = 1 | |
| # ── Helper Redis (via HTTP REST per stabilità mobile/serverless) ────────────── | |
| async def _rcmd(cmd: List[Any]) -> Optional[Dict[str, Any]]: | |
| url = os.getenv("UPSTASH_REDIS_REST_URL") | |
| tok = os.getenv("UPSTASH_REDIS_REST_TOKEN") | |
| if not url or not tok: | |
| return None | |
| try: | |
| async with httpx.AsyncClient() as client: | |
| r = await client.post( | |
| url, | |
| headers={"Authorization": f"Bearer {tok}"}, | |
| json=cmd, | |
| timeout=5.0 | |
| ) | |
| return r.json() | |
| except Exception as e: | |
| _logger.error("[jq] redis error: %s", e) | |
| return None | |
| def _redis_ok() -> bool: | |
| return bool(os.getenv("UPSTASH_REDIS_REST_URL") and os.getenv("UPSTASH_REDIS_REST_TOKEN")) | |
| async def _llen(key: str) -> int: | |
| res = await _rcmd(["LLEN", key]) | |
| return int(res.get("result", 0)) if res else 0 | |
| async def _lrange(key: str, start: int, end: int = -1) -> List[str]: | |
| res = await _rcmd(["LRANGE", key, start, end]) | |
| return res.get("result", []) if res else [] | |
| # ── Core Logic ──────────────────────────────────────────────────────────────── | |
| async def publish_load_metrics(): | |
| """Pubblica il carico corrente su Redis per il bilanciamento.""" | |
| if not _redis_ok(): return | |
| data = { | |
| "role": _SPACE_ROLE, | |
| "ts": int(time.time() * 1000), | |
| "active_tasks": len(asyncio.all_tasks()) - 5, # Stima approssimativa | |
| "cpu": 0, # Placeholder per metrics reali | |
| "mem": 0 | |
| } | |
| await _rcmd(["SET", _K_LOAD(_SPACE_ROLE), json.dumps(data), "EX", "60"]) | |
| async def _load_publisher_loop(): | |
| """Loop periodico per aggiornare lo stato del nodo.""" | |
| while True: | |
| try: | |
| await publish_load_metrics() | |
| except Exception as e: | |
| _logger.debug("[jq] load publisher error: %s", e) | |
| await asyncio.sleep(30) | |
| async def _hands_consumer_loop(): | |
| """Loop consumer per nodi HANDS, MEMORY e AUDIT.""" | |
| _logger.info("[jq] consumer loop avviato per ruolo: %s", _SPACE_ROLE) | |
| while True: | |
| try: | |
| # S429: Tool Success Contract — verifica heartbeat consumer | |
| await _rcmd(["SET", _K_CONSUMER, "1", "EX", "15"]) | |
| # TODO: Implementazione effettiva del prelievo job e esecuzione | |
| # Per ora è un segnaposto per la struttura UltraVSS v7.0 | |
| except Exception as e: | |
| _logger.debug("[jq] consumer loop error: %s", e) | |
| await asyncio.sleep(5) | |
| async def start_job_queue_consumer() -> None: | |
| """Punto di ingresso per main.py _on_startup().""" | |
| if not _redis_ok(): | |
| _logger.warning("[jq] Redis non configurato — job queue disabilitato") | |
| return | |
| asyncio.create_task(_load_publisher_loop()) | |
| # Grid Orchestrator: Consumer specializzati (HANDS, MEMORY, AUDIT) | |
| if _SPACE_ROLE in ("hands", "memory", "audit", "unknown"): | |
| asyncio.create_task(_hands_consumer_loop()) | |
| else: | |
| _logger.info("[jq] SPACE_ROLE=%s — consumer non avviato (solo load publisher)", _SPACE_ROLE) | |
| # ── FastAPI endpoints ────────────────────────────────────────────────────────── | |
| async def jq_status(): | |
| result = { | |
| "space_role": _SPACE_ROLE, | |
| "jq_enabled": _JQ_ENABLED, | |
| "redis_configured": _redis_ok(), | |
| "ts": int(time.time() * 1000), | |
| } | |
| return result | |
| async def jq_load(role: str): | |
| if role not in ("brain", "hands", "memory", "audit"): | |
| raise HTTPException(400, "role non valido") | |
| res = await _rcmd(["GET", _K_LOAD(role)]) | |
| if not res or not res.get("result"): | |
| raise HTTPException(404, f"Metriche {role} non disponibili") | |
| return json.loads(res["result"]) | |
| async def jq_submit(job: JobPayload, request: Request): | |
| if _INTERNAL_TOKEN and request.headers.get("X-Internal-Token") != _INTERNAL_TOKEN: | |
| raise HTTPException(401, "Unauthorized") | |
| # Logica di sottomissione job... | |
| return {"taskId": job.taskId, "status": "queued"} | |