Spaces:
Running
Running
File size: 11,360 Bytes
24480a0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | """
backend/api/brain_planner.py β Brain Planner (ARCH-I4.1)
Componente Planner del Brain: reasoning, planning, decomposition, reflection.
Produce WorkflowPlan (lista ordinata di PlanStep) senza sapere come eseguirli.
Il Brain Γ¨ ora separato in:
BrainPlanner β "cosa fare e in che ordine" (questo file)
BrainExecutor β "come eseguire lo stato" (brain_executor.py)
Flusso:
Kernel.submit_task(payload, capability="plan") β BrainPlanner.plan(goal)
β WorkflowPlan(steps=[PlanStep,...])
β BrainExecutor.execute_plan(plan) β [Kernel.submit_task per ogni step]
Invarianti ADR:
S4: Brain non conosce l'infrastruttura
S9: Planner ignora come Executor esegue
S21: Brain dipende solo dal Kernel
S27: ogni piano tracciato via plan_id
"""
from __future__ import annotations
import asyncio
import logging
import time
import uuid
from typing import Any, Literal
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from .auth_guard import AuthRole, require_role
_logger = logging.getLogger("api.brain_planner")
# ββ Resolver guard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
from .capability_resolver import resolver as _resolver, ResolveRequest as _RReq
_RESOLVER_AVAILABLE = True
except Exception:
_resolver = None # type: ignore[assignment]
_RESOLVER_AVAILABLE = False
# ββ Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class PlanStep(BaseModel):
"""Singolo step del piano β capability + payload + dipendenze."""
step_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
capability: str = Field(..., description="Capability richiesta per questo step")
description: str = Field("", description="Descrizione human-readable")
payload: dict[str, Any] = Field(default_factory=dict)
depends_on: list[str] = Field(default_factory=list,
description="step_id degli step da completare prima")
timeout_s: int = Field(60)
retry_max: int = Field(2)
optional: bool = Field(False, description="Se True, fallimento non blocca il piano")
requires_gpu: bool = Field(False)
metadata: dict[str, Any] = Field(default_factory=dict)
class WorkflowPlan(BaseModel):
"""Piano di esecuzione prodotto dal Planner β input per il Workflow Engine."""
plan_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
goal: str = Field(..., description="Obiettivo originale")
strategy: Literal["sequential", "parallel", "dag"] = Field(
"sequential",
description="sequential=step uno alla volta, "
"parallel=tutti in parallelo, "
"dag=dipendenze esplicite tra step")
steps: list[PlanStep] = Field(default_factory=list)
context: dict[str, Any] = Field(default_factory=dict,
description="Contesto condiviso tra gli step")
created_at: float = Field(default_factory=time.time)
metadata: dict[str, Any] = Field(default_factory=dict)
def validate_dag(self) -> list[str]:
"""Verifica che il DAG non abbia cicli. Ritorna errori (lista vuota = ok)."""
step_ids = {s.step_id for s in self.steps}
errors = []
for step in self.steps:
for dep in step.depends_on:
if dep not in step_ids:
errors.append(f"Step {step.step_id}: dipende da {dep} che non esiste nel piano")
return errors
class PlanRequest(BaseModel):
goal: str = Field(..., description="Obiettivo da pianificare")
context: dict[str, Any] = Field(default_factory=dict)
strategy: Literal["sequential", "parallel", "dag"] = "sequential"
max_steps: int = Field(10, description="Numero massimo di step nel piano")
session_id: str | None = None
correlation_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
hints: list[str] = Field(default_factory=list,
description="Capability suggerite dal caller")
class ReflectRequest(BaseModel):
"""Richiesta di reflection su un piano eseguito (per migliorare piani futuri)."""
plan_id: str
outcome: Literal["success", "partial", "failure"]
failed_steps: list[str] = Field(default_factory=list)
notes: str = ""
correlation_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
# ββ BrainPlanner singleton ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class BrainPlanner:
"""
Planner del Brain: data un goal, produce un WorkflowPlan.
Strategia di planning (senza LLM, rule-based per ora):
1. Decomposizione goal β capability necessarie (via Resolver + hints)
2. Ordinamento step (sequenziale per default, DAG se dipendenze esplicite)
3. Validazione piano (no cicli, capabilities disponibili)
4. Reflection store (memorizza piani e outcome per futuro miglioramento)
Nota: la versione LLM-based (reasoning con modello) sarΓ ARCH-I4.1 Phase 2.
Questa implementazione Γ¨ rule-based e serve a definire il contratto.
"""
def __init__(self) -> None:
self._plans: dict[str, WorkflowPlan] = {} # plan_id β plan
self._reflections: list[dict[str, Any]] = [] # storia reflection
self._lock = asyncio.Lock()
# ββ Plan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async def plan(self, req: PlanRequest) -> WorkflowPlan:
"""
Produce un WorkflowPlan dal goal.
Step logic (rule-based):
- Se hints presenti: crea uno step per ogni hint
- Altrimenti: step singolo con capability "llm" (Brain decide runtime)
- Verifica che ogni capability sia risolvibile dal Resolver
"""
capabilities = req.hints if req.hints else ["llm"]
# Verifica capabilities via Resolver
verified_caps = []
for cap in capabilities[:req.max_steps]:
if _RESOLVER_AVAILABLE and _resolver is not None:
if _resolver.can_resolve(cap):
verified_caps.append(cap)
else:
_logger.warning("[planner] capability '%s' non risolvibile β esclusa dal piano", cap)
else:
verified_caps.append(cap) # no resolver: trust hints
if not verified_caps:
verified_caps = ["llm"] # fallback sempre
# Costruisci steps
steps = []
prev_id: str | None = None
for cap in verified_caps:
step = PlanStep(
capability = cap,
description = f"Esegui capability '{cap}' per goal: {req.goal[:80]}",
payload = {"goal": req.goal, **req.context},
depends_on = [prev_id] if (req.strategy == "sequential" and prev_id) else [],
timeout_s = 120,
)
steps.append(step)
prev_id = step.step_id
plan = WorkflowPlan(
goal = req.goal,
strategy = req.strategy,
steps = steps,
context = req.context,
metadata = {"session_id": req.session_id, "correlation_id": req.correlation_id},
)
# Validate DAG
errors = plan.validate_dag()
if errors:
raise ValueError("Piano DAG non valido: " + "; ".join(errors))
async with self._lock:
self._plans[plan.plan_id] = plan
_logger.info("[planner] plan created plan_id=%s goal=%s steps=%d strategy=%s",
plan.plan_id, req.goal[:40], len(steps), req.strategy)
return plan
def get_plan(self, plan_id: str) -> WorkflowPlan | None:
return self._plans.get(plan_id)
# ββ Reflect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async def reflect(self, req: ReflectRequest) -> dict:
"""Registra l'outcome di un piano β base per miglioramento futuro."""
plan = self._plans.get(req.plan_id)
entry = {
"plan_id": req.plan_id,
"goal": plan.goal if plan else "unknown",
"outcome": req.outcome,
"failed_steps": req.failed_steps,
"notes": req.notes,
"ts": time.time(),
}
async with self._lock:
self._reflections.append(entry)
if len(self._reflections) > 500: # cap
self._reflections = self._reflections[-500:]
_logger.info("[planner] reflect plan=%s outcome=%s", req.plan_id, req.outcome)
return {"reflected": True, "plan_id": req.plan_id, "outcome": req.outcome}
def status(self) -> dict:
return {
"total_plans": len(self._plans),
"total_reflections": len(self._reflections),
"resolver_available": _RESOLVER_AVAILABLE,
}
# ββ Singleton ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
planner = BrainPlanner()
# ββ HTTP Router ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
router = APIRouter(
prefix="/api/brain",
tags=["brain-planner"],
dependencies=[Depends(require_role(AuthRole.MACHINE))],
)
@router.post("/plan", summary="Pianifica un goal β WorkflowPlan")
async def route_plan(req: PlanRequest) -> WorkflowPlan:
try:
return await planner.plan(req)
except ValueError as exc:
raise HTTPException(400, str(exc))
@router.get("/plan/{plan_id}", summary="Recupera un piano esistente")
async def route_get_plan(plan_id: str) -> WorkflowPlan:
p = planner.get_plan(plan_id)
if not p:
raise HTTPException(404, f"Piano '{plan_id}' non trovato")
return p
@router.post("/reflect", summary="Reflection su un piano eseguito")
async def route_reflect(req: ReflectRequest) -> dict:
return await planner.reflect(req)
@router.get("/planner/status", summary="Stato del BrainPlanner")
async def route_status() -> dict:
return planner.status()
|