Files
pi-qmem/extensions/tools/correct.ts
T

160 lines
5.9 KiB
TypeScript

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:
"Correct a false record by creating a superseding version; the old record remains archived. " +
"Requires verified evidence. Prefer memory_id; query matching requires score >=0.60.",
parameters: Type.Object({
memory_id: Type.Optional(Type.String({ description: "Active record UUID." })),
query: Type.Optional(Type.String({ description: "Lookup query; used only without memory_id." })),
corrected_text: Type.String({ description: "Verified replacement text." }),
reason: Type.Optional(Type.String({ description: "Correction reason." })),
kind: Type.Optional(
Type.Union(
[Type.Literal("decision"), Type.Literal("fact"), Type.Literal("episode"), Type.Literal("preference")],
{ description: "New kind; inherits by default." },
),
),
project_id: Type.Optional(Type.String({ description: "New project; inherits by default." })),
confidence: Type.Optional(
Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")], {
description: "Confidence; inherits by default.",
}),
),
agent_id: Type.Optional(Type.String({ description: "Writer provenance." })),
parent_id: Type.Optional(Type.String({ description: "Parent UUID; inherits by default." })),
level: Type.Optional(
Type.Union([Type.Literal("L1_ROOT"), Type.Literal("L2_SUBTOPIC"), Type.Literal("L3_DETAIL")], {
description: "Hierarchy level.",
}),
),
topic: Type.Optional(Type.String({ description: "Hierarchy topic ID." })),
links: Type.Optional(
Type.Array(
Type.Object({
target_id: Type.String(),
predicate: Type.Optional(Type.String()),
weight: Type.Optional(Type.Number()),
}),
{ description: "Related records." },
),
),
}),
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 },
};
},
});
}