AUDIT / src /lib /agent /RouteSelector.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
5.85 kB
/**
* RouteSelector.ts — Adaptive A/B routing between GraphOrchestrator and ReAct (FASE 5)
*
* Selects the best execution path based on:
* 1. Deterministic rules (low complexity → react, high+3tools → graph)
* 2. Historical quality scores per task signature (stored in agentMemory)
* 3. 10% epsilon-greedy exploration to avoid local optima
*
* S344: RouteStats ora traccia qualityScore (0–1 da goalVerifier) invece di binario
* success/failure — il RouteSelector sceglie per qualità reale, non lunghezza testo.
*
* 100% local, no API calls, synchronous selection, async persistence.
*/
import { agentMemory } from "../agentMemory";
import type { PlanLike } from "./GraphOrchestrator";
export type Route = "graph" | "react";
export interface RouteOutcome {
route: Route;
success: boolean;
latencyMs: number;
qualityScore?: number; // S344: 0–1 da goalVerifier (sostituisce heuristica lunghezza)
}
interface RouteStats {
gs: number; // graph successes (binario — backward compat)
gt: number; // graph total
rs: number; // react successes (binario — backward compat)
rt: number; // react total
gqs: number; // graph quality score sum (S344 — goalVerifier scores)
rqs: number; // react quality score sum (S344 — goalVerifier scores)
}
const STORAGE_KEY = "route_selector:v2"; // S344: bump version — nuovo schema stats
const MIN_SAMPLES_FOR_STATS = 2; // FIX-5a: 2 sample già sufficienti su progetti piccoli
const EXPLORE_RATE = 0.12; // FIX-5b: 12% — più esplorazione con pochi task totali
function planSignature(plan: PlanLike): string {
const tools = [...new Set(plan.subtasks.filter(s => s.tool !== "respond").map(s => s.tool))].sort().join(",");
return `${plan.complexity}|${tools}`;
}
/**
* S344: usa qualityScore medio se disponibile (goalVerifier), altrimenti successRate binario.
* qualitySum > 0 → almeno una sessione con goalVerifier attivo → score medio più accurato.
*/
function winRate(success: number, total: number, qualitySum: number): number {
if (total === 0) return 0.5; // prior ottimistico al primo run
if (qualitySum > 0) return qualitySum / total; // goalVerifier score — il più accurato
// FIX-5c: Laplace smoothing — (success+1)/(total+2) evita 0.0/1.0 con 1-2 sample
return (success + 1) / (total + 2);
}
export class RouteSelector {
private stats = new Map<string, RouteStats>();
private loaded = false;
// Lazy-load from agentMemory on first use
async ensureLoaded(): Promise<void> {
if (this.loaded) return;
this.loaded = true;
try {
// Tenta prima la chiave v2 (S344), poi v1 per backward compat
const raw = agentMemory.get(STORAGE_KEY) ?? agentMemory.get("route_selector:v1");
if (raw) {
const obj = JSON.parse(raw) as Record<string, RouteStats>;
for (const [k, v] of Object.entries(obj)) {
// S344: backward compat — old v1 entries non hanno gqs/rqs
this.stats.set(k, { ...v, gqs: v.gqs ?? 0, rqs: v.rqs ?? 0 });
}
}
} catch { /* first run — empty stats */ }
}
private persist(): void {
try {
const obj: Record<string, RouteStats> = {};
for (const [k, v] of this.stats.entries()) {
obj[k] = v;
}
agentMemory.set(STORAGE_KEY, JSON.stringify(obj), "routing");
} catch { /* non-blocking */ }
}
/** Select the route for a given plan. Must call ensureLoaded() first. */
select(plan: PlanLike): Route {
const toolCount = plan.subtasks.filter(s => s.tool !== "respond").length;
// ── Hard rules (deterministic) ──────────────────────────────────────────
if (plan.complexity === "low" || toolCount <= 1) return "react";
if (plan.complexity === "high" && toolCount >= 3) return "graph";
// ── Epsilon-greedy exploration ───────────────────────────────────────────
if (Math.random() < EXPLORE_RATE) {
return Math.random() < 0.5 ? "graph" : "react";
}
// ── Stats-based selection (S344: qualità-pesata) ─────────────────────────
const sig = planSignature(plan);
const s = this.stats.get(sig);
if (!s || (s.gt + s.rt) < MIN_SAMPLES_FOR_STATS) {
// Default for medium complexity: graph if 2+ tools, react otherwise
return toolCount >= 2 ? "graph" : "react";
}
const graphWin = winRate(s.gs, s.gt, s.gqs);
const reactWin = winRate(s.rs, s.rt, s.rqs);
return graphWin >= reactWin ? "graph" : "react";
}
/**
* Record the outcome of a completed execution for future routing decisions.
* S344: qualityScore (da goalVerifier) sostituisce l'heuristica binaria length > 80.
* Fallback: se qualityScore non disponibile (task senza formalGoal), usa 0.8/0.2.
*/
record(plan: PlanLike, outcome: RouteOutcome): void {
const sig = planSignature(plan);
const s = this.stats.get(sig) ?? { gs: 0, gt: 0, rs: 0, rt: 0, gqs: 0, rqs: 0 };
// S344: quality score da usare — goalVerifier se disponibile, fallback binario
const quality = outcome.qualityScore ?? (outcome.success ? 0.8 : 0.2);
if (outcome.route === "graph") {
s.gt++;
if (outcome.success) s.gs++;
s.gqs += quality;
} else {
s.rt++;
if (outcome.success) s.rs++;
s.rqs += quality;
}
this.stats.set(sig, s);
this.persist();
}
/** Debug: return stats for all known signatures */
dump(): Record<string, RouteStats> {
const out: Record<string, RouteStats> = {};
for (const [k, v] of this.stats.entries()) out[k] = v;
return out;
}
}
export const routeSelector = new RouteSelector();