refactor: split gateway and pi-qmem extension into modules
- gateway: separate config, models, state, audit, guardrail, embeddings, store, metrics, cleanup and routes; keep main.py as FastAPI bootstrap - extension: split client/config, six tools, config command and rules; preserve jiti entrypoint and registrations - Dockerfile copies the complete gateway module set - tests: update monkeypatch boundaries for modular config/state
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { gatewayRequest, loadConfig } from "../shared";
|
||||
|
||||
export function registerQmemCorrect(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "qmem_correct",
|
||||
label: "Qmem memory correct",
|
||||
description:
|
||||
"Corregge una memoria falsa o superata: crea un NUOVO record che supersede il vecchio (che resta " +
|
||||
"in archivio marcato superseded, mai eliminato). Passa memory_id se lo conosci (dalla risposta di " +
|
||||
"qmem_search), oppure query per individuare automaticamente il record attivo più rilevante. Il testo " +
|
||||
"corretto sostituisce quello vecchio nella ricerca semantica. Usalo quando hai evidenza verificata che " +
|
||||
"una memoria è falsa: contraddizione con fonte autorevole, conferma dell'utente o esito di un'azione. " +
|
||||
"Con query: rifiuta se lo score del top-1 è sotto la soglia (correctMinScore, default 0.60) per evitare " +
|
||||
"di supersedere il record sbagliato — in quel caso verifica e riprova con memory_id esplicito.",
|
||||
promptGuidelines: [
|
||||
"qmem_correct: correggi solo con evidenza verificata (fonte autorevole, conferma utente, esito di azione) — mai per semplice dubbio o opinione.",
|
||||
"qmem_correct: il vecchio record resta in archivio marcato superseded — mai eliminare (tranne duplicati esatti).",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
memory_id: Type.Optional(Type.String({ description: "UUID del record attivo da supersedere (dalla risposta di qmem_search)." })),
|
||||
query: Type.Optional(Type.String({ description: "Query per trovare il record da correggere (usata solo se memory_id non è fornito)." })),
|
||||
corrected_text: Type.String({ description: "Il testo corretto e verificato che sostituisce quello falso." }),
|
||||
reason: Type.Optional(Type.String({ description: "Motivo della correzione (visibile in audit e sul vecchio record)." })),
|
||||
kind: Type.Optional(
|
||||
Type.Union(
|
||||
[Type.Literal("decision"), Type.Literal("fact"), Type.Literal("episode"), Type.Literal("preference")],
|
||||
{ description: "Tipo del nuovo record (default: eredita dal record superseduto)." },
|
||||
),
|
||||
),
|
||||
project_id: Type.Optional(Type.String({ description: "Progetto del nuovo record (default: eredita dal record superseduto)." })),
|
||||
confidence: Type.Optional(
|
||||
Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")], {
|
||||
description: "Affidabilità del nuovo record (default: eredita dal record superseduto).",
|
||||
}),
|
||||
),
|
||||
agent_id: Type.Optional(Type.String({ description: "Nome dell'agente che corregge (solo provenienza)." })),
|
||||
parent_id: Type.Optional(Type.String({ description: "UUID del record genitore (default: eredita dal record superseduto se presente)." })),
|
||||
level: Type.Optional(
|
||||
Type.Union([Type.Literal("L1_ROOT"), Type.Literal("L2_SUBTOPIC"), Type.Literal("L3_DETAIL")], {
|
||||
description: "Livello gerarchico del nuovo record.",
|
||||
}),
|
||||
),
|
||||
topic: Type.Optional(Type.String({ description: "Topic ID gerarchico del nuovo record." })),
|
||||
links: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object({
|
||||
target_id: Type.String(),
|
||||
predicate: Type.Optional(Type.String()),
|
||||
weight: Type.Optional(Type.Number()),
|
||||
}),
|
||||
{ description: "Collegamenti relazionali espliciti." },
|
||||
),
|
||||
),
|
||||
}),
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.apiKey) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Config mancante: esegui /qmem:config per impostare url e apiKey." }],
|
||||
details: { error: "missing_config" },
|
||||
};
|
||||
}
|
||||
const p = params as any;
|
||||
if (!p.memory_id && !p.query) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Serve memory_id (da qmem_search) oppure query per trovare il record da correggere." }],
|
||||
details: { error: "missing_target" },
|
||||
};
|
||||
}
|
||||
|
||||
let memoryId = p.memory_id;
|
||||
let orig: any = {};
|
||||
if (!memoryId) {
|
||||
onUpdate?.({ content: [{ type: "text", text: `qmem: ricerca del record da correggere ("${p.query}")...` }] });
|
||||
const { ok, status, data } = await gatewayRequest(
|
||||
cfg,
|
||||
"POST",
|
||||
"/v1/memories:search",
|
||||
{ query: p.query, top_k: 1, include_superseded: false },
|
||||
signal,
|
||||
);
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }],
|
||||
details: { error: "gateway_error", status },
|
||||
};
|
||||
}
|
||||
const results = data.results ?? [];
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Nessun record attivo trovato per la query. Nessuna correzione applicata." }],
|
||||
details: { error: "not_found" },
|
||||
};
|
||||
}
|
||||
// Soglia di sicurezza: rifiuta correzioni su record deboli/rumorosi
|
||||
const minScore = cfg.correctMinScore ?? 0.6;
|
||||
if (results[0].score < minScore) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Correzione rifiutata: il record più rilevante ha score ${results[0].score} < soglia ${minScore} (evidenza debole/rumorosa). ` +
|
||||
`Verifica il record con qmem_search (include_superseded=true per la lineage) e riprova con memory_id esplicito, oppure abbassa correctMinScore nella config.`,
|
||||
},
|
||||
],
|
||||
details: { error: "low_score", score: results[0].score, min_score: minScore },
|
||||
};
|
||||
}
|
||||
memoryId = results[0].memory_id;
|
||||
orig = results[0];
|
||||
} else {
|
||||
// memory_id fornito: recupera il record per ereditare kind/scope/project_id
|
||||
const { ok, status, data } = await gatewayRequest(cfg, "GET", `/v1/memories/${memoryId}`, undefined, signal);
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }],
|
||||
details: { error: "gateway_error", status },
|
||||
};
|
||||
}
|
||||
orig = data;
|
||||
}
|
||||
|
||||
onUpdate?.({ content: [{ type: "text", text: `qmem: supersede di ${memoryId}...` }] });
|
||||
// Idempotency: stessa key per tutta l'operazione (e per eventuali retry)
|
||||
const idemKey = crypto.randomUUID();
|
||||
const { ok, status, data } = await gatewayRequest(
|
||||
cfg,
|
||||
"POST",
|
||||
"/v1/memories",
|
||||
{
|
||||
text: p.corrected_text,
|
||||
kind: p.kind ?? orig.kind ?? "fact",
|
||||
agent_id: p.agent_id,
|
||||
project_id: p.project_id ?? orig.project_id,
|
||||
scope: orig.scope ?? "agent",
|
||||
confidence: p.confidence ?? orig.confidence ?? "medium",
|
||||
source: "qmem_correct",
|
||||
supersedes_id: memoryId,
|
||||
supersede_reason: p.reason,
|
||||
parent_id: p.parent_id ?? orig.parent_id,
|
||||
level: p.level ?? orig.level,
|
||||
topic: p.topic ?? orig.topic,
|
||||
links: p.links ?? orig.links,
|
||||
},
|
||||
signal,
|
||||
idemKey,
|
||||
);
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }],
|
||||
details: { error: "gateway_error", status },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Correzione applicata: nuovo record ${data.memory_id} supersede ${memoryId}${p.reason ? ` (motivo: ${p.reason})` : ""}. Il vecchio record resta in archivio marcato superseded.${data.reparented ? ` ${data.reparented} figli ri-parentati al nuovo UUID.` : ""}`,
|
||||
},
|
||||
],
|
||||
details: { new_id: data.memory_id, superseded_id: memoryId, reparented: data.reparented ?? 0 },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user