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

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 },
};
},
});
}