Files
ST-Bionic-Memory-Ecology/maintenance/task-graph-stats.js
Youzini-afk 96b43c7860 feat: migrate compress/consolidation/reflection graphStats to shared ranking core + prompt node refs
- New maintenance/task-graph-stats.js: shared helper wrapping rankNodesForTaskContext + createPromptNodeReferenceMap
- extractor.js: extract and reflection graphStats now use buildTaskGraphStats
- compressor.js: compression graphStats uses shared helper, excludes current batch from relevant ranking
- consolidator.js: consolidation graphStats uses shared helper, excludes new nodes from relevant ranking
- index.js: passes schema + embeddingConfig into generateReflection
- p0-regressions.mjs: added graphStats assertions for compress/consolidation/reflection (prompt-facing refs, no raw UUIDs)
2026-04-12 15:27:22 +08:00

204 lines
5.8 KiB
JavaScript

import { getActiveNodes } from "../graph/graph.js";
import { createPromptNodeReferenceMap } from "../prompting/prompt-node-references.js";
import { rankNodesForTaskContext } from "../retrieval/shared-ranking.js";
const DEFAULT_TYPE_LABELS = Object.freeze({
event: "事件",
character: "角色",
location: "地点",
rule: "规则",
thread: "主线",
synopsis: "全局概要",
reflection: "反思",
pov_memory: "主观记忆",
});
function createTypeLabelMap(schema = []) {
return new Map(
(Array.isArray(schema) ? schema : [])
.filter((typeDef) => String(typeDef?.id || "").trim())
.map((typeDef) => [
String(typeDef?.id || "").trim(),
String(typeDef?.label || typeDef?.id || "").trim(),
]),
);
}
function resolveTypeLabel(typeId = "", typeLabelMap = new Map()) {
const normalizedTypeId = String(typeId || "").trim();
return (
typeLabelMap.get(normalizedTypeId) ||
DEFAULT_TYPE_LABELS[normalizedTypeId] ||
normalizedTypeId ||
"节点"
);
}
function listGraphTypeCounts(activeNodes = [], schema = [], typeLabelMap = new Map()) {
const safeActiveNodes = Array.isArray(activeNodes) ? activeNodes : [];
if (Array.isArray(schema) && schema.length > 0) {
return schema
.map((typeDef) => {
const typeId = String(typeDef?.id || "").trim();
const count = safeActiveNodes.filter((node) => node?.type === typeId).length;
return {
typeId,
label: resolveTypeLabel(typeId, typeLabelMap),
count,
};
})
.filter((entry) => entry.count > 0);
}
const countMap = new Map();
for (const node of safeActiveNodes) {
const typeId = String(node?.type || "").trim();
if (!typeId) continue;
countMap.set(typeId, (countMap.get(typeId) || 0) + 1);
}
return [...countMap.entries()]
.map(([typeId, count]) => ({
typeId,
label: resolveTypeLabel(typeId, typeLabelMap),
count,
}))
.sort((left, right) => left.typeId.localeCompare(right.typeId));
}
export function buildRelevantNodeReferenceMap(
scoredNodes = [],
schema = [],
{
maxCount = 6,
prefix = "G",
maxLength = 28,
} = {},
) {
const typeLabelMap = createTypeLabelMap(schema);
const relevantNodes = (Array.isArray(scoredNodes) ? scoredNodes : [])
.filter(
(entry) =>
entry?.node &&
!entry.node.archived &&
((Number(entry?.vectorScore) || 0) > 0 ||
(Number(entry?.graphScore) || 0) > 0 ||
(Number(entry?.lexicalScore) || 0) > 0),
)
.slice(0, Math.max(1, maxCount));
return createPromptNodeReferenceMap(relevantNodes, {
prefix,
maxLength,
buildMeta: ({ entry, node }) => ({
typeLabel: resolveTypeLabel(node?.type, typeLabelMap),
score:
Math.round((Number(entry?.weightedScore ?? entry?.finalScore) || 0) * 1000) /
1000,
}),
});
}
export function buildGraphOverview(
graph,
schema = [],
relevantReferenceMap = null,
{
relevantHeading = "与当前任务最相关的既有节点",
} = {},
) {
const activeNodes = graph?.nodes
?.filter((node) => node && !node.archived)
?.sort((left, right) => (left.seq || 0) - (right.seq || 0));
if (!Array.isArray(activeNodes) || activeNodes.length === 0) {
return "";
}
const typeLabelMap = createTypeLabelMap(schema);
const typeCounts = listGraphTypeCounts(activeNodes, schema, typeLabelMap);
const lines = ["### 图谱节点统计"];
for (const entry of typeCounts) {
lines.push(` - ${entry.label}: ${entry.count}`);
}
const references = Array.isArray(relevantReferenceMap?.references)
? relevantReferenceMap.references
: [];
if (references.length > 0) {
lines.push("", `### ${String(relevantHeading || "与当前任务最相关的既有节点").trim() || "与当前任务最相关的既有节点"}`);
for (const reference of references) {
const typeLabel =
String(reference?.meta?.typeLabel || reference?.meta?.type || "节点").trim() ||
"节点";
const label = String(reference?.meta?.label || "—").trim() || "—";
const score = Number(reference?.meta?.score || 0).toFixed(3);
lines.push(` - [${reference.key}|${typeLabel}] ${label} (score=${score})`);
}
}
return lines.join("\n");
}
function normalizeActiveNodes(graph, activeNodes = null) {
if (Array.isArray(activeNodes)) {
return activeNodes.filter((node) => node && !node.archived);
}
return getActiveNodes(graph).filter((node) => node && !node.archived);
}
export async function buildTaskGraphStats({
graph,
schema = [],
userMessage = "",
recentMessages = [],
embeddingConfig,
signal,
activeNodes = null,
rankingOptions = {},
relevantHeading = "与当前任务最相关的既有节点",
maxRelevantNodes = 6,
prefix = "G",
maxLabelLength = 28,
} = {}) {
const normalizedActiveNodes = normalizeActiveNodes(graph, activeNodes);
const normalizedUserMessage = String(userMessage || "").trim();
let ranking = null;
if (graph && normalizedActiveNodes.length > 0 && normalizedUserMessage) {
ranking = await rankNodesForTaskContext({
graph,
userMessage: normalizedUserMessage,
recentMessages,
embeddingConfig,
signal,
options: {
activeNodes: normalizedActiveNodes,
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
...rankingOptions,
},
});
}
const relevantReferenceMap = buildRelevantNodeReferenceMap(
ranking?.scoredNodes,
schema,
{
maxCount: maxRelevantNodes,
prefix,
maxLength: maxLabelLength,
},
);
return {
ranking,
relevantReferenceMap,
graphStats: buildGraphOverview(graph, schema, relevantReferenceMap, {
relevantHeading,
}),
};
}