feat(hierarchy): supporto metadati strutturati e gerarchici su Qdrant (parent_id, level, topic, links)

This commit is contained in:
Matteo Benedetto
2026-08-23 13:07:16 +02:00
parent fc11f878f6
commit 369a2c2d9e
4 changed files with 224 additions and 12 deletions
+68 -5
View File
@@ -207,6 +207,23 @@ export default function qmemExtension(pi: ExtensionAPI) {
),
supersedes_id: Type.Optional(Type.String({ description: "UUID del record da supersedere (correzione): il nuovo record diventa la versione attiva, il vecchio resta in archivio marcato superseded." })),
supersede_reason: Type.Optional(Type.String({ description: "Motivo della correzione (visibile in audit e sul vecchio record)." })),
parent_id: Type.Optional(Type.String({ description: "UUID del record genitore per organizzazione gerarchica/subtopic." })),
level: Type.Optional(
Type.Union([Type.Literal("L1_ROOT"), Type.Literal("L2_SUBTOPIC"), Type.Literal("L3_DETAIL")], {
description: "Livello gerarchico (L1_ROOT = indice macro-topic, L2_SUBTOPIC = dettaglio specialistico, L3_DETAIL).",
}),
),
topic: Type.Optional(Type.String({ description: "Topic ID gerarchico (es. ALFA-ROMEO-GT-1300-JUNIOR/SPECS)." })),
links: Type.Optional(
Type.Array(
Type.Object({
target_id: Type.String({ description: "UUID del record target collegato." }),
predicate: Type.Optional(Type.String({ description: "Tipo di relazione (parent_of, part_of, relates_to, supersedes...)." })),
weight: Type.Optional(Type.Number({ description: "Peso della relazione (default: 1.0)." })),
}),
{ description: "Collegamenti relazionali espliciti verso altri record." },
),
),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const cfg = loadConfig();
@@ -245,6 +262,10 @@ export default function qmemExtension(pi: ExtensionAPI) {
expires_at: p.expires_at,
supersedes_id: p.supersedes_id,
supersede_reason: p.supersede_reason,
parent_id: p.parent_id,
level: p.level,
topic: p.topic,
links: p.links,
},
signal,
idemKey,
@@ -261,9 +282,10 @@ export default function qmemExtension(pi: ExtensionAPI) {
dupes.map((d: any) => ` - ${d.memory_id} (score ${d.score}): ${String(d.text).slice(0, 100)}`).join("\n") +
"\nValuta se il nuovo record è davvero necessario o se conviene qmem_correct sul duplicato."
: "";
const extra = `${p.level ? `, level ${p.level}` : ""}${p.topic ? `, topic ${p.topic}` : ""}${p.parent_id ? ` — parent ${p.parent_id}` : ""}`;
return {
content: [{ type: "text", text: `Memoria salvata: ${data.memory_id} (${p.kind ?? "fact"}, scope ${p.scope ?? "agent"})${p.supersedes_id ? ` — supersede ${p.supersedes_id}` : ""}${dupWarning}` }],
details: { memory_id: data.memory_id, created_at: data.created_at, duplicates: dupes.length },
content: [{ type: "text", text: `Memoria salvata: ${data.memory_id} (${p.kind ?? "fact"}, scope ${p.scope ?? "agent"}${extra})${p.supersedes_id ? ` — supersede ${p.supersedes_id}` : ""}${dupWarning}` }],
details: { memory_id: data.memory_id, created_at: data.created_at, duplicates: dupes.length, parent_id: p.parent_id, level: p.level, topic: p.topic },
};
},
});
@@ -318,6 +340,13 @@ export default function qmemExtension(pi: ExtensionAPI) {
"non come similarità. min_score resta applicato al ramo vettoriale (anti-rumore).",
}),
),
parent_id: Type.Optional(Type.String({ description: "Filtra per UUID del record genitore." })),
level: Type.Optional(
Type.Union([Type.Literal("L1_ROOT"), Type.Literal("L2_SUBTOPIC"), Type.Literal("L3_DETAIL")], {
description: "Filtra per livello gerarchico.",
}),
),
topic: Type.Optional(Type.String({ description: "Filtra per topic esatto." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const cfg = loadConfig();
@@ -342,6 +371,9 @@ export default function qmemExtension(pi: ExtensionAPI) {
min_score: p.min_score ?? 0.45,
top_k: p.top_k ?? 5,
hybrid: p.hybrid ?? false,
parent_id: p.parent_id,
level: p.level,
topic: p.topic,
},
signal,
);
@@ -364,8 +396,13 @@ export default function qmemExtension(pi: ExtensionAPI) {
};
}
const lines = results.map(
(r: any, i: number) =>
`${i + 1}. [${r.kind}/${r.scope} score=${r.score}${r.score < 0.6 ? " ⚠️" : ""}${r.confidence ? ` conf=${r.confidence}` : ""}] ${r.text}\n (id: ${r.memory_id}, agente: ${r.agent_id ?? "?"}, creato: ${r.created_at ?? "?"}${r.source ? `, fonte: ${r.source}` : ""}${r.supersedes_id ? `, supersede ${r.supersedes_id}` : ""}${r.superseded_by ? `, ⚠️ superseduto da ${r.superseded_by}` : ""})`,
(r: any, i: number) => {
const lvl = r.level ? ` [${r.level}]` : "";
const top = r.topic ? ` (${r.topic})` : "";
const parent = r.parent_id ? `, parent: ${r.parent_id}` : "";
const links = r.links && r.links.length > 0 ? `, links: ${r.links.length}` : "";
return `${i + 1}. [${r.kind}/${r.scope}${lvl}${top} score=${r.score}${r.score < 0.6 ? " ⚠️" : ""}${r.confidence ? ` conf=${r.confidence}` : ""}] ${r.text}\n (id: ${r.memory_id}${parent}${links}, agente: ${r.agent_id ?? "?"}, creato: ${r.created_at ?? "?"}${r.source ? `, fonte: ${r.source}` : ""}${r.supersedes_id ? `, supersede ${r.supersedes_id}` : ""}${r.superseded_by ? `, ⚠️ superseduto da ${r.superseded_by}` : ""})`;
},
);
return {
content: [{ type: "text", text: lines.join("\n") }],
@@ -410,6 +447,23 @@ export default function qmemExtension(pi: ExtensionAPI) {
}),
),
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();
@@ -496,6 +550,10 @@ export default function qmemExtension(pi: ExtensionAPI) {
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,
@@ -610,10 +668,12 @@ export default function qmemExtension(pi: ExtensionAPI) {
}
const r = data ?? {};
const meta = [
`[${r.kind ?? "?"}/${r.scope ?? "?"}${r.confidence ? ` conf=${r.confidence}` : ""}]`,
`[${r.kind ?? "?"}/${r.scope ?? "?"}${r.level ? ` ${r.level}` : ""}${r.topic ? ` (${r.topic})` : ""}${r.confidence ? ` conf=${r.confidence}` : ""}]`,
`project: ${r.project_id ?? "?"}`,
`agente: ${r.agent_id ?? "?"}, creato: ${r.created_at ?? "?"}${r.source ? `, fonte: ${r.source}` : ""}`,
];
if (r.parent_id) meta.push(`parent: ${r.parent_id}`);
if (r.links && r.links.length > 0) meta.push(`links: ${r.links.length}`);
if (r.supersedes_id) meta.push(`supersede ${r.supersedes_id}`);
if (r.superseded_by) meta.push(`⚠️ SUPERSEDUTO da ${r.superseded_by}`);
if (r.expires_at) meta.push(`scade: ${r.expires_at}`);
@@ -625,6 +685,9 @@ export default function qmemExtension(pi: ExtensionAPI) {
kind: r.kind,
scope: r.scope,
project_id: r.project_id,
parent_id: r.parent_id,
level: r.level,
topic: r.topic,
superseded_by: r.superseded_by ?? null,
},
};