Spaces:
Running
Running
| """ | |
| backend/api/kernel.py β AI Kernel (ARCH-K2.1) | |
| Interfaccia unica tra Brain/Executor e tutti i servizi sottostanti. | |
| Il Brain NON importa mai direttamente job_queue, event_bus, session_manager, providers. | |
| Usa SOLO le 4 primitive Kernel: | |
| submitTask(payload, priority, session_id) β TaskResult | |
| chat(messages, model_hint, session_id) β ChatResult | |
| memory(op, session_id, **kwargs) β MemoryResult | |
| publishEvent(topic, payload, ...) β EventResult | |
| Invarianti ADR rispettati: | |
| S1: stateless β nessuno stato locale (stato in Queue + DB) | |
| S4: Brain non conosce l'infrastruttura | |
| S5: ogni Tool sostituibile senza toccare agentLoop | |
| S9: nessun servizio conosce l'impl. interna di un altro | |
| S10: ogni comunicazione Γ¨ asincrona | |
| S20: ogni Provider Γ¨ sostituibile | |
| S21: Brain dipende solo dal Kernel | |
| S27: ogni decisione tracciabile via correlation_id | |
| S30: nessuna API pubblica dipende da provider specifico | |
| HTTP Endpoints (auth: MACHINE): | |
| POST /api/kernel/submit β submitTask() | |
| POST /api/kernel/chat β chat() | |
| POST /api/kernel/memory β memory() | |
| POST /api/kernel/event/publish β publishEvent() | |
| GET /api/kernel/status β diagnostica servizi | |
| Python-importable (uso interno brain/executor): | |
| from api.kernel import kernel | |
| result = await kernel.submit_task(payload={...}) | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| import time | |
| import uuid | |
| from typing import Any, Literal | |
| from fastapi import APIRouter, Depends | |
| from pydantic import BaseModel, Field | |
| from .auth_guard import AuthRole, require_role | |
| _logger = logging.getLogger("api.kernel") | |
| # ββ Router βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| router = APIRouter( | |
| prefix="/api/kernel", | |
| tags=["kernel"], | |
| dependencies=[Depends(require_role(AuthRole.MACHINE))], | |
| ) | |
| # ββ Priority βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TaskPriority = Literal["HIGH", "NORMAL", "LOW", "BACKGROUND"] | |
| # ββ Result models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TaskResult(BaseModel): | |
| task_id: str | |
| correlation_id: str | |
| status: str # "queued" | "error" | |
| queue_backend: str # "redis" | "memory" | "none" | |
| ts: float = Field(default_factory=time.time) | |
| error: str | None = None | |
| class ChatResult(BaseModel): | |
| correlation_id: str | |
| content: str | |
| provider: str | |
| model: str | |
| cached: bool = False | |
| ts: float = Field(default_factory=time.time) | |
| error: str | None = None | |
| class MemoryResult(BaseModel): | |
| correlation_id: str | |
| op: str | |
| data: Any = None | |
| backend: str | |
| ts: float = Field(default_factory=time.time) | |
| error: str | None = None | |
| class EventResult(BaseModel): | |
| event_id: str | |
| correlation_id: str | |
| topic: str | |
| delivered_to: int = 0 | |
| redis_fanout: bool = False | |
| ts: float = Field(default_factory=time.time) | |
| error: str | None = None | |
| # ββ Request bodies βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class SubmitTaskRequest(BaseModel): | |
| payload: dict = Field(..., description="Job payload per il Worker") | |
| priority: TaskPriority = "NORMAL" | |
| session_id: str | None = None | |
| correlation_id: str | None = None | |
| timeout_s: int = 300 | |
| class ChatRequest(BaseModel): | |
| messages: list[dict] = Field(..., description="Array OpenAI-style [{role, content}]") | |
| model_hint: str | None = None # "vision" | "chat" | "embedding" | provider name | |
| session_id: str | None = None | |
| correlation_id: str | None = None | |
| max_tokens: int = 4096 | |
| temperature: float = 0.7 | |
| class MemoryRequest(BaseModel): | |
| op: Literal["read", "write", "search", "compress", "clear"] | |
| session_id: str | None = None | |
| correlation_id: str | None = None | |
| query: str | None = None | |
| limit: int = 10 | |
| layer: Literal["working", "episodic", "semantic", "reflection", "all"] = "all" | |
| content: str | None = None | |
| role: str = "assistant" | |
| metadata: dict = Field(default_factory=dict) | |
| class PublishEventRequest(BaseModel): | |
| topic: str = Field(..., description="Es. task.created, tool.finished") | |
| payload: dict = Field(default_factory=dict) | |
| correlation_id: str | None = None | |
| session_id: str | None = None | |
| source: str = "kernel" | |
| # ββ KernelAPI β Python-importable ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class KernelAPI: | |
| """ | |
| Interfaccia Python del Kernel. Importabile dai moduli Brain/Executor | |
| senza dipendenze dirette ai servizi sottostanti. | |
| Usage: | |
| from api.kernel import kernel | |
| result = await kernel.submit_task(payload={"type": "llm_call", ...}) | |
| """ | |
| # ββ submitTask βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def submit_task( | |
| self, | |
| payload: dict, | |
| priority: TaskPriority = "NORMAL", | |
| session_id: str | None = None, | |
| correlation_id: str | None = None, | |
| timeout_s: int = 300, | |
| ) -> TaskResult: | |
| """ | |
| Accoda un job alla Queue con prioritΓ . | |
| Routing: Redis (JQ_ENABLED=1) β fallback none (stateless, S1). | |
| (S10, S16: asincrono, task_id globale univoco) | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| t_id = str(uuid.uuid4()) | |
| job = { | |
| "task_id": t_id, | |
| "correlation_id": corr, | |
| "session_id": session_id, | |
| "priority": priority, | |
| "timeout_s": timeout_s, | |
| "payload": payload, | |
| "submitted_at": time.time(), | |
| } | |
| queue_backend = "none" | |
| try: | |
| from .job_queue import _rpush, _K_PENDING, _redis_ok | |
| if _redis_ok(): | |
| import json as _json | |
| ok = await _rpush(_K_PENDING, _json.dumps(job), ttl=timeout_s + 60) | |
| queue_backend = "redis" if ok else "none" | |
| except Exception as exc: | |
| _logger.warning("[kernel.submit_task] queue err: %s", exc) | |
| # Fire-and-forget: pubblica task.created sull'Event Bus (S26, S27) | |
| asyncio.create_task(self._emit("task.created", { | |
| "task_id": t_id, | |
| "priority": priority, | |
| "session_id": session_id, | |
| }, corr)) | |
| _logger.info("[kernel] submitTask id=%s priority=%s backend=%s", t_id, priority, queue_backend) | |
| return TaskResult( | |
| task_id=t_id, | |
| correlation_id=corr, | |
| status="queued", | |
| queue_backend=queue_backend, | |
| ) | |
| # ββ chat βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def chat( | |
| self, | |
| messages: list[dict], | |
| model_hint: str | None = None, | |
| session_id: str | None = None, | |
| correlation_id: str | None = None, | |
| max_tokens: int = 4096, | |
| temperature: float = 0.7, | |
| ) -> ChatResult: | |
| """ | |
| Invia chat al provider selezionato. model_hint richiede una capability | |
| senza nominare il provider specifico. (S20, S30: provider-agnostic) | |
| LLM cache applicata automaticamente (S9). | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| cache_key = f"k:{model_hint}:{hash(str(messages))}" | |
| # Cache read (GAP-3-fix: get_cached returns str|None β JSON-parse) | |
| try: | |
| import json as _json | |
| from .llm_cache import get_cached, set_cached | |
| _cached_raw = await get_cached(cache_key) | |
| cached = _json.loads(_cached_raw) if _cached_raw else None | |
| if cached: | |
| _logger.debug("[kernel.chat] cache hit corr=%s", corr) | |
| return ChatResult( | |
| correlation_id=corr, | |
| content=cached.get("content", ""), | |
| provider=cached.get("provider", "cache"), | |
| model=cached.get("model", ""), | |
| cached=True, | |
| ) | |
| except Exception: | |
| cached = None | |
| provider_name = "unknown" | |
| model_name = "unknown" | |
| content = "" | |
| error = None | |
| try: | |
| # ARCH-I4.4: Provider Layer LLM β usa CapabilityRouter per selezione dinamica | |
| from models.provider_router import capability_router as _cap_router | |
| _ai_obj = await _cap_router.get_client_for_capability(model_hint or "default") | |
| # Determina provider/model per logging e ChatResult | |
| if hasattr(_ai_obj, "providers") and _ai_obj.providers: | |
| _p = _ai_obj.providers[0] | |
| provider_name = _p.name | |
| model_name = _p.default_model | |
| else: | |
| provider_name = model_hint or "ai_client" | |
| model_name = "unknown" | |
| # AIClient.chat() restituisce str direttamente (non un completions object) | |
| content = await asyncio.wait_for( | |
| _ai_obj.chat(messages, max_tokens=max_tokens, temperature=temperature), | |
| timeout=60.0, | |
| ) | |
| # Cache write (fail-open: non blocca il caller) (GAP-3-fix) | |
| if cached is None: | |
| try: | |
| import json as _json | |
| from .llm_cache import set_cached | |
| await set_cached(cache_key, _json.dumps({ | |
| "content": content, | |
| "provider": provider_name, | |
| "model": model_name, | |
| })) | |
| except Exception: | |
| pass | |
| except Exception as exc: | |
| error = str(exc) | |
| _logger.warning("[kernel.chat] provider err corr=%s: %s", corr, exc) | |
| asyncio.create_task(self._emit("response.generated", { | |
| "provider": provider_name, | |
| "model": model_name, | |
| "session_id": session_id, | |
| "cached": False, | |
| }, corr)) | |
| return ChatResult( | |
| correlation_id=corr, | |
| content=content, | |
| provider=provider_name, | |
| model=model_name, | |
| cached=False, | |
| error=error, | |
| ) | |
| # ββ memory βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def memory( | |
| self, | |
| op: str, | |
| session_id: str | None = None, | |
| correlation_id: str | None = None, | |
| query: str | None = None, | |
| content: str | None = None, | |
| role: str = "assistant", | |
| layer: str = "all", | |
| limit: int = 10, | |
| metadata: dict | None = None, | |
| ) -> MemoryResult: | |
| """ | |
| Operazioni unificate su tutti i layer di memoria. | |
| Router: working β episodic β semantic β reflection. | |
| (S3: solo Queue e DB contengono stato; S9: interfaccia opaca) | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| meta = metadata or {} | |
| try: | |
| from .state import _get_mem_manager_async as _gmm | |
| mem = await _gmm() | |
| if mem is None: | |
| return MemoryResult( | |
| correlation_id=corr, op=op, backend="none", | |
| error="MemoryManager not initialized", | |
| ) | |
| data = None | |
| backend = layer | |
| if op == "read": | |
| ctx = await asyncio.to_thread(mem.working.get_context) \ | |
| if layer in ("working", "all") else "" | |
| data = {"context": ctx} | |
| backend = "working" | |
| elif op == "write" and content: | |
| await asyncio.to_thread(mem.working.add_entry, role, content, meta) | |
| await asyncio.to_thread(mem.episodic.add, content, meta) | |
| backend = "working+episodic" | |
| asyncio.create_task(self._emit("memory.updated", { | |
| "op": "write", "session_id": session_id, "layer": backend, | |
| }, corr)) | |
| elif op == "search" and query: | |
| results = await asyncio.to_thread(mem.semantic.search, query, limit) | |
| data = {"results": results} | |
| backend = "semantic" | |
| elif op == "compress": | |
| summary = await asyncio.to_thread(mem.working.compress) | |
| data = {"summary": summary} | |
| backend = "working" | |
| elif op == "clear": | |
| await asyncio.to_thread(mem.working.clear) | |
| data = {"cleared": True} | |
| backend = "working" | |
| else: | |
| return MemoryResult( | |
| correlation_id=corr, op=op, backend="none", | |
| error=f"op '{op}' non valida o parametri mancanti", | |
| ) | |
| return MemoryResult(correlation_id=corr, op=op, data=data, backend=backend) | |
| except Exception as exc: | |
| _logger.warning("[kernel.memory] op=%s err: %s", op, exc) | |
| return MemoryResult(correlation_id=corr, op=op, backend="error", error=str(exc)) | |
| # ββ publishEvent βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def publish_event( | |
| self, | |
| topic: str, | |
| payload: dict, | |
| correlation_id: str | None = None, | |
| session_id: str | None = None, | |
| source: str = "kernel", | |
| ) -> EventResult: | |
| """ | |
| Pubblica un evento sull'Event Bus (in-memory + Redis fanout). | |
| (S10, S13, S19, S27: asincrono, idempotente, persistente, tracciabile) | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| event_id = str(uuid.uuid4()) | |
| try: | |
| from .event_bus import publish as _publish_internal # GAP-4-fix | |
| result = await _publish_internal( | |
| topic, | |
| payload, | |
| correlation_id=corr, | |
| session_id=session_id, | |
| source=source, | |
| ) | |
| return EventResult( | |
| event_id=event_id, | |
| correlation_id=corr, | |
| topic=topic, | |
| delivered_to=result.get("delivered_to", 0) if isinstance(result, dict) else 0, | |
| redis_fanout=result.get("redis_fanout", False) if isinstance(result, dict) else False, | |
| ) | |
| except ImportError: | |
| # Fallback: usa il router HTTP interno via publish endpoint | |
| try: | |
| from .event_bus import publish as _bus_pub, BusEvent # type: ignore[attr-defined] | |
| evt = BusEvent( | |
| topic=topic, | |
| payload=payload, | |
| correlation_id=corr, | |
| session_id=session_id, | |
| source=source, | |
| ) | |
| r = await _bus_pub(evt) | |
| return EventResult( | |
| event_id=event_id, | |
| correlation_id=corr, | |
| topic=topic, | |
| delivered_to=getattr(r, "delivered_to", 0), | |
| redis_fanout=getattr(r, "redis_fanout", False), | |
| ) | |
| except Exception as exc2: | |
| _logger.warning("[kernel.publish_event] fallback err topic=%s: %s", topic, exc2) | |
| return EventResult(event_id=event_id, correlation_id=corr, topic=topic, error=str(exc2)) | |
| except Exception as exc: | |
| _logger.warning("[kernel.publish_event] topic=%s err: %s", topic, exc) | |
| return EventResult(event_id=event_id, correlation_id=corr, topic=topic, error=str(exc)) | |
| # ββ internal helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _emit(self, topic: str, payload: dict, correlation_id: str) -> None: | |
| """Fire-and-forget β non blocca mai il caller (S10).""" | |
| try: | |
| await self.publish_event(topic=topic, payload=payload, correlation_id=correlation_id) | |
| except Exception as exc: | |
| _logger.debug("[kernel._emit] topic=%s err=%s", topic, exc) | |
| # ββ resolveCapability ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def resolve_capability( | |
| self, | |
| capability: str, | |
| constraints: dict | None = None, | |
| correlation_id: str | None = None, | |
| ) -> dict: | |
| """ | |
| ARCH-E3.2: Mappa una capability al miglior Worker disponibile. | |
| (S4: Brain non conosce l'infrastruttura, chiede solo capacitΓ ) | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| try: | |
| from .marketplace import resolve_capability as _resolve | |
| res = await _resolve(capability, constraints) | |
| _logger.info("[kernel] resolveCapability cap=%s corr=%s -> %s", capability, corr, res.get("status")) | |
| return res | |
| except Exception as exc: | |
| _logger.warning("[kernel.resolve_capability] err: %s", exc) | |
| return {"status": "error", "message": str(exc)} | |
| # ββ executePlugin ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def execute_plugin( | |
| self, | |
| plugin_id: str, | |
| input_data: Any, | |
| session_id: str | None = None, | |
| correlation_id: str | None = None, | |
| ) -> dict: | |
| """ | |
| ARCH-E3.3: Esegue un plugin sandboxato via Kernel. | |
| (S5: Plugin sostituibili senza toccare agentLoop) | |
| """ | |
| corr = correlation_id or str(uuid.uuid4()) | |
| try: | |
| from .plugins import plugin_manager | |
| res = await plugin_manager.execute(plugin_id, input_data, session_id or "default") | |
| _logger.info("[kernel] executePlugin id=%s corr=%s -> %s", plugin_id, corr, res.get("status")) | |
| return res | |
| except Exception as exc: | |
| _logger.warning("[kernel.execute_plugin] err: %s", exc) | |
| return {"status": "error", "message": str(exc)} | |
| # ββ Singleton per uso interno ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| kernel: KernelAPI = KernelAPI() | |
| # ββ HTTP Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def http_submit_task(req: SubmitTaskRequest) -> TaskResult: | |
| """Brain/Executor usano questo endpoint per delegare lavoro ai Worker (S10, S21).""" | |
| return await kernel.submit_task( | |
| payload=req.payload, | |
| priority=req.priority, | |
| session_id=req.session_id, | |
| correlation_id=req.correlation_id, | |
| timeout_s=req.timeout_s, | |
| ) | |
| async def http_chat(req: ChatRequest) -> ChatResult: | |
| """Chiama il provider selezionato senza esporre quale (S20, S30).""" | |
| return await kernel.chat( | |
| messages=req.messages, | |
| model_hint=req.model_hint, | |
| session_id=req.session_id, | |
| correlation_id=req.correlation_id, | |
| max_tokens=req.max_tokens, | |
| temperature=req.temperature, | |
| ) | |
| async def http_memory(req: MemoryRequest) -> MemoryResult: | |
| """Read/write/search/compress su Working, Episodic, Semantic, Reflection (S9).""" | |
| return await kernel.memory( | |
| op=req.op, | |
| session_id=req.session_id, | |
| correlation_id=req.correlation_id, | |
| query=req.query, | |
| content=req.content, | |
| role=req.role, | |
| layer=req.layer, | |
| limit=req.limit, | |
| metadata=req.metadata, | |
| ) | |
| async def http_publish_event(req: PublishEventRequest) -> EventResult: | |
| """Pubblica evento sull'Event Bus con correlazione (S10, S13, S27).""" | |
| return await kernel.publish_event( | |
| topic=req.topic, | |
| payload=req.payload, | |
| correlation_id=req.correlation_id, | |
| session_id=req.session_id, | |
| source=req.source, | |
| ) | |
| async def http_resolve_capability(capability: str, constraints: dict | None = None) -> dict: | |
| """Brain/Executor usano questo per trovare il miglior worker per una capacitΓ (ARCH-E3.2).""" | |
| return await kernel.resolve_capability(capability, constraints) | |
| async def http_execute_plugin(plugin_id: str, input_data: Any, session_id: str | None = None) -> dict: | |
| """Esegue un plugin tramite il Kernel (ARCH-E3.3).""" | |
| return await kernel.execute_plugin(plugin_id, input_data, session_id) | |
| async def http_kernel_status() -> dict: | |
| """Stato aggregato di tutti i servizi sottostanti (S17, S24).""" | |
| checks: dict[str, Any] = {} | |
| # Job Queue (Redis) | |
| try: | |
| from .job_queue import _redis_ok, _llen, _K_PENDING | |
| redis_up = _redis_ok() | |
| pending = await _llen(_K_PENDING) if redis_up else -1 | |
| checks["job_queue"] = {"redis": redis_up, "pending_jobs": pending} | |
| except Exception as exc: | |
| checks["job_queue"] = {"error": str(exc)} | |
| # Event Bus | |
| try: | |
| from .event_bus import _subscribers | |
| checks["event_bus"] = { | |
| "active_subscriptions": sum(len(s) for s in _subscribers.values()), | |
| "topics": list(_subscribers.keys()), | |
| } | |
| except Exception as exc: | |
| checks["event_bus"] = {"error": str(exc)} | |
| # Session Manager | |
| try: | |
| from .session_manager import _lru | |
| checks["session_manager"] = {"lru_entries": len(_lru)} | |
| except Exception as exc: | |
| checks["session_manager"] = {"error": str(exc)} | |
| # Memory Manager | |
| try: | |
| from .state import _get_mem_manager_async as _gmm | |
| mem = await _gmm() | |
| checks["memory"] = { | |
| "initialized": mem is not None, | |
| "working_entries": len(getattr(getattr(mem, "working", None), "_entries", [])) if mem else 0, | |
| } | |
| except Exception as exc: | |
| checks["memory"] = {"error": str(exc)} | |
| return { | |
| "kernel": "ARCH-K2.1", | |
| "version": "1.0.0", | |
| "ts": time.time(), | |
| "services": checks, | |
| } | |