Terminal / api /kernel.py
Baida-A
Initial clean deploy (Reverse Proxy removed)
28a08e7
Raw
History Blame
24.5 kB
"""
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 ─────────────────────────────────────────────────────────────
@router.post("/submit", response_model=TaskResult, summary="submitTask β€” accoda job con prioritΓ ")
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,
)
@router.post("/chat", response_model=ChatResult, summary="chat β€” LLM provider-agnostic")
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,
)
@router.post("/memory", response_model=MemoryResult, summary="memory β€” router unificato su tutti i layer")
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,
)
@router.post("/event/publish", response_model=EventResult, summary="publishEvent β€” Event Bus bridge")
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,
)
@router.post("/resolve", summary="resolveCapability β€” mappa capability a Worker")
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)
@router.post("/plugin/execute", summary="executePlugin β€” esegue plugin sandboxato")
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)
@router.get("/status", summary="diagnostica servizi Kernel")
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,
}