132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
import { Type } from "typebox";
|
|
import { gatewayRequest, loadConfig } from "../shared";
|
|
|
|
export function registerQmemTree(pi: ExtensionAPI) {
|
|
pi.registerTool({
|
|
name: "qmem_tree",
|
|
label: "Qmem memory hierarchy tree",
|
|
description:
|
|
"Show a topic hierarchy from a root UUID or topic, including child UUIDs for qmem_get.",
|
|
parameters: Type.Object({
|
|
memory_id: Type.Optional(Type.String({ description: "Root record UUID." })),
|
|
topic: Type.Optional(Type.String({ description: "Topic ID or prefix." })),
|
|
}),
|
|
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.topic) {
|
|
return {
|
|
content: [{ type: "text", text: "Fornisci almeno un memory_id o un topic da esplorare." }],
|
|
details: { error: "missing_parameter" },
|
|
};
|
|
}
|
|
onUpdate?.({ content: [{ type: "text", text: "qmem: recupero albero gerarchico..." }] });
|
|
|
|
let rootId = p.memory_id;
|
|
let rootData: any = null;
|
|
|
|
if (rootId) {
|
|
const { ok, status, data } = await gatewayRequest(cfg, "GET", `/v1/memories/${rootId}`, undefined, signal);
|
|
if (!ok) {
|
|
return {
|
|
content: [{ type: "text", text: `Errore ${status} nel recupero del nodo root: ${JSON.stringify(data)}` }],
|
|
details: { error: "not_found", memory_id: rootId },
|
|
};
|
|
}
|
|
rootData = data;
|
|
} else {
|
|
// Cerca il nodo root per topic
|
|
const topicQuery = p.topic.includes("/") ? p.topic : `${p.topic}/ROOT`;
|
|
const { ok, status, data } = await gatewayRequest(
|
|
cfg,
|
|
"POST",
|
|
"/v1/memories:search",
|
|
{ query: topicQuery, topic: topicQuery, top_k: 1, include_superseded: false, min_score: 0.0 },
|
|
signal,
|
|
);
|
|
if (!ok || !data.results || data.results.length === 0) {
|
|
// Fallback: cerca con query generica
|
|
const fallback = await gatewayRequest(
|
|
cfg,
|
|
"POST",
|
|
"/v1/memories:search",
|
|
{ query: p.topic, level: "L1_ROOT", top_k: 1, include_superseded: false, min_score: 0.0 },
|
|
signal,
|
|
);
|
|
if (!fallback.ok || !fallback.data.results || fallback.data.results.length === 0) {
|
|
return {
|
|
content: [{ type: "text", text: `Nessun nodo radice (L1_ROOT) trovato per il topic '${p.topic}'.` }],
|
|
details: { error: "root_not_found" },
|
|
};
|
|
}
|
|
rootData = fallback.data.results[0];
|
|
rootId = rootData.memory_id;
|
|
} else {
|
|
rootData = data.results[0];
|
|
rootId = rootData.memory_id;
|
|
}
|
|
}
|
|
|
|
// Recupera tutti i figli associati a parent_id == rootId.
|
|
// Fallback lineage: se il root è stato superseduto, i figli storici
|
|
// puntano ancora al vecchio UUID (supersedes_id) — li includiamo per
|
|
// garantire la navigabilità dell'albero anche per gli orfani pre-fix.
|
|
const parentIds = [rootId];
|
|
if (rootData.supersedes_id) parentIds.push(rootData.supersedes_id);
|
|
const children: any[] = [];
|
|
const seen = new Set<string>();
|
|
for (const pid of parentIds) {
|
|
const childRes = await gatewayRequest(
|
|
cfg,
|
|
"POST",
|
|
"/v1/memories:search",
|
|
{ query: "*", parent_id: pid, top_k: 20, include_superseded: false, min_score: 0.0 },
|
|
signal,
|
|
);
|
|
for (const c of childRes.ok ? (childRes.data.results ?? []) : []) {
|
|
if (!seen.has(c.memory_id)) {
|
|
seen.add(c.memory_id);
|
|
children.push(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
const out: string[] = [];
|
|
out.push(`🌳 ALBERO GERARCHICO: ${rootData.topic ?? "ROOT"} [${rootData.level ?? "L1_ROOT"}]`);
|
|
out.push(` UUID: ${rootId} (${rootData.project_id ?? "pi-qmem"})`);
|
|
if (rootData.text) {
|
|
const summary = String(rootData.text).split("\n")[0].slice(0, 120);
|
|
out.push(` Descrizione: ${summary}`);
|
|
}
|
|
out.push("");
|
|
|
|
if (children.length === 0) {
|
|
out.push(" (Nessun sotto-nodo figlio L2 associato)");
|
|
} else {
|
|
out.push(` └── Sotto-nodi collegati (${children.length}):`);
|
|
children.forEach((c: any, idx: number) => {
|
|
const isLast = idx === children.length - 1;
|
|
const branch = isLast ? " └──" : " ├──";
|
|
const subTitle = c.topic ? c.topic.split("/").slice(1).join("/") : (c.kind ?? "subtopic");
|
|
const firstLine = String(c.text ?? "").split("\n")[0].slice(0, 100);
|
|
out.push(`${branch} [${c.level ?? "L2"}] ${subTitle} (UUID: ${c.memory_id})`);
|
|
out.push(` ${firstLine}`);
|
|
});
|
|
}
|
|
|
|
return {
|
|
content: [{ type: "text", text: out.join("\n") }],
|
|
details: { root_id: rootId, children_count: children.length },
|
|
};
|
|
},
|
|
});
|
|
|
|
}
|