mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Implement scoped memory graph and refresh defaults
This commit is contained in:
@@ -66,6 +66,16 @@ assert.equal(defaultSettings.recallNmfTopics, 15);
|
||||
assert.equal(defaultSettings.recallNmfNoveltyThreshold, 0.4);
|
||||
assert.equal(defaultSettings.recallResidualThreshold, 0.3);
|
||||
assert.equal(defaultSettings.recallResidualTopK, 5);
|
||||
assert.equal(defaultSettings.enableScopedMemory, true);
|
||||
assert.equal(defaultSettings.enablePovMemory, true);
|
||||
assert.equal(defaultSettings.enableRegionScopedObjective, true);
|
||||
assert.equal(defaultSettings.recallCharacterPovWeight, 1.25);
|
||||
assert.equal(defaultSettings.recallUserPovWeight, 1.05);
|
||||
assert.equal(defaultSettings.recallObjectiveCurrentRegionWeight, 1.15);
|
||||
assert.equal(defaultSettings.recallObjectiveAdjacentRegionWeight, 0.9);
|
||||
assert.equal(defaultSettings.recallObjectiveGlobalWeight, 0.75);
|
||||
assert.equal(defaultSettings.injectUserPovMemory, true);
|
||||
assert.equal(defaultSettings.injectObjectiveGlobalMemory, true);
|
||||
assert.equal(defaultSettings.injectDepth, 9999);
|
||||
assert.equal(defaultSettings.enabled, true);
|
||||
assert.equal(defaultSettings.enableReflection, true);
|
||||
|
||||
@@ -5,6 +5,10 @@ import { DEFAULT_NODE_SCHEMA } from "../schema.js";
|
||||
const coreEvent = {
|
||||
id: "event-1",
|
||||
type: "event",
|
||||
scope: {
|
||||
layer: "objective",
|
||||
regionPrimary: "钟楼",
|
||||
},
|
||||
fields: {
|
||||
summary: "艾琳在钟楼发现了地下入口",
|
||||
participants: "艾琳",
|
||||
@@ -14,21 +18,36 @@ const coreEvent = {
|
||||
|
||||
const recalledCharacter = {
|
||||
id: "char-1",
|
||||
type: "character",
|
||||
type: "pov_memory",
|
||||
scope: {
|
||||
layer: "pov",
|
||||
ownerType: "character",
|
||||
ownerId: "艾琳",
|
||||
ownerName: "艾琳",
|
||||
regionPrimary: "钟楼",
|
||||
},
|
||||
fields: {
|
||||
name: "艾琳",
|
||||
state: "警觉并准备进入地下室",
|
||||
goal: "调查钟楼秘密",
|
||||
summary: "艾琳觉得地下室入口说明钟楼里有人长期活动",
|
||||
belief: "这里藏着失踪案线索",
|
||||
emotion: "警觉",
|
||||
attitude: "必须立刻下去查看",
|
||||
},
|
||||
};
|
||||
|
||||
const recalledReflection = {
|
||||
id: "reflection-1",
|
||||
type: "reflection",
|
||||
id: "user-pov-1",
|
||||
type: "pov_memory",
|
||||
scope: {
|
||||
layer: "pov",
|
||||
ownerType: "user",
|
||||
ownerId: "玩家",
|
||||
ownerName: "玩家",
|
||||
},
|
||||
fields: {
|
||||
insight: "地下入口意味着先前的失踪案与钟楼存在长期关联",
|
||||
trigger: "钟楼发现暗门",
|
||||
suggestion: "后续优先追查地下通道与失踪人口名单",
|
||||
summary: "玩家已经把钟楼和失踪案牢牢绑定起来了",
|
||||
belief: "钟楼地下室肯定有更深的秘密",
|
||||
emotion: "紧张",
|
||||
attitude: "希望艾琳谨慎推进",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,23 +55,21 @@ const text = formatInjection(
|
||||
{
|
||||
coreNodes: [coreEvent],
|
||||
recallNodes: [recalledCharacter, recalledReflection],
|
||||
groupedRecallNodes: {
|
||||
state: [recalledCharacter],
|
||||
episodic: [],
|
||||
reflective: [recalledReflection],
|
||||
rule: [],
|
||||
other: [],
|
||||
scopeBuckets: {
|
||||
characterPov: [recalledCharacter],
|
||||
userPov: [recalledReflection],
|
||||
objectiveCurrentRegion: [coreEvent],
|
||||
objectiveGlobal: [],
|
||||
},
|
||||
},
|
||||
DEFAULT_NODE_SCHEMA,
|
||||
);
|
||||
|
||||
assert.match(text, /\[Memory - Core\]/);
|
||||
assert.match(text, /\[Memory - Character POV\]/);
|
||||
assert.match(text, /\[Memory - User POV \/ Not Character Facts\]/);
|
||||
assert.match(text, /不等于角色已知事实/);
|
||||
assert.match(text, /\[Memory - Objective \/ Current Region\]/);
|
||||
assert.match(text, /pov_memory_table:/);
|
||||
assert.match(text, /event_table:/);
|
||||
assert.match(text, /\[Memory - Recalled\]/);
|
||||
assert.match(text, /## 当前状态记忆/);
|
||||
assert.match(text, /## 反思与长期锚点/);
|
||||
assert.match(text, /character_table:/);
|
||||
assert.match(text, /reflection_table:/);
|
||||
|
||||
console.log("injector-format tests passed");
|
||||
|
||||
@@ -92,6 +92,53 @@ const graph = createGraph();
|
||||
const helpers = createGraphHelpers(graph);
|
||||
const retrieve = await loadRetrieve({
|
||||
...helpers,
|
||||
MEMORY_SCOPE_BUCKETS: {
|
||||
CHARACTER_POV: "characterPov",
|
||||
USER_POV: "userPov",
|
||||
OBJECTIVE_CURRENT_REGION: "objectiveCurrentRegion",
|
||||
OBJECTIVE_ADJACENT_REGION: "objectiveAdjacentRegion",
|
||||
OBJECTIVE_GLOBAL: "objectiveGlobal",
|
||||
OTHER_POV: "otherPov",
|
||||
},
|
||||
normalizeMemoryScope(scope = {}) {
|
||||
return {
|
||||
layer: scope.layer === "pov" ? "pov" : "objective",
|
||||
ownerType: scope.ownerType || "",
|
||||
ownerId: scope.ownerId || "",
|
||||
ownerName: scope.ownerName || "",
|
||||
regionPrimary: scope.regionPrimary || "",
|
||||
regionPath: Array.isArray(scope.regionPath) ? scope.regionPath : [],
|
||||
regionSecondary: Array.isArray(scope.regionSecondary)
|
||||
? scope.regionSecondary
|
||||
: [],
|
||||
};
|
||||
},
|
||||
getScopeRegionKey(scope = {}) {
|
||||
return String(scope.regionPrimary || "");
|
||||
},
|
||||
classifyNodeScopeBucket(node, { activeRegion = "" } = {}) {
|
||||
if (node?.scope?.layer === "pov") {
|
||||
return node?.scope?.ownerType === "user"
|
||||
? "userPov"
|
||||
: "characterPov";
|
||||
}
|
||||
if (
|
||||
activeRegion &&
|
||||
String(node?.scope?.regionPrimary || "").trim() === String(activeRegion).trim()
|
||||
) {
|
||||
return "objectiveCurrentRegion";
|
||||
}
|
||||
return "objectiveGlobal";
|
||||
},
|
||||
resolveScopeBucketWeight(bucket, overrides = {}) {
|
||||
return Number(overrides?.[bucket] ?? 1) || 1;
|
||||
},
|
||||
describeMemoryScope(scope = {}) {
|
||||
return `${scope.layer || "objective"}:${scope.ownerType || ""}:${scope.regionPrimary || ""}`;
|
||||
},
|
||||
describeScopeBucket(bucket = "") {
|
||||
return String(bucket || "");
|
||||
},
|
||||
buildTaskPrompt() {
|
||||
return { systemPrompt: "" };
|
||||
},
|
||||
@@ -446,4 +493,67 @@ assert.equal(lexicalResult.meta.retrieval.queryBlendActive, false);
|
||||
assert.equal(lexicalResult.meta.retrieval.lexicalBoostedNodes, 1);
|
||||
assert.equal(lexicalResult.meta.retrieval.lexicalTopHits[0]?.nodeId, "char-1");
|
||||
|
||||
const scopedGraph = {
|
||||
nodes: [
|
||||
{
|
||||
id: "obj-global",
|
||||
type: "event",
|
||||
importance: 8,
|
||||
createdTime: 1,
|
||||
archived: false,
|
||||
fields: { title: "旧王都事件" },
|
||||
seqRange: [1, 1],
|
||||
scope: { layer: "objective", regionPrimary: "旧城区" },
|
||||
},
|
||||
{
|
||||
id: "char-pov",
|
||||
type: "pov_memory",
|
||||
importance: 4,
|
||||
createdTime: 2,
|
||||
archived: false,
|
||||
fields: { summary: "艾琳觉得钟楼入口非常可疑" },
|
||||
seqRange: [2, 2],
|
||||
scope: {
|
||||
layer: "pov",
|
||||
ownerType: "character",
|
||||
ownerId: "艾琳",
|
||||
ownerName: "艾琳",
|
||||
regionPrimary: "钟楼",
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
historyState: {
|
||||
activeRegion: "钟楼",
|
||||
activeCharacterPovOwner: "艾琳",
|
||||
activeUserPovOwner: "玩家",
|
||||
},
|
||||
};
|
||||
const scopedSchema = [
|
||||
{ id: "event", label: "事件", alwaysInject: true },
|
||||
{ id: "pov_memory", label: "主观记忆", alwaysInject: false },
|
||||
];
|
||||
const scopedResult = await retrieve({
|
||||
graph: scopedGraph,
|
||||
userMessage: "钟楼里到底有什么",
|
||||
recentMessages: [],
|
||||
embeddingConfig: {},
|
||||
schema: scopedSchema,
|
||||
options: {
|
||||
topK: 2,
|
||||
maxRecallNodes: 1,
|
||||
enableVectorPrefilter: false,
|
||||
enableGraphDiffusion: false,
|
||||
enableLLMRecall: false,
|
||||
enableDiversitySampling: false,
|
||||
enableScopedMemory: true,
|
||||
activeRegion: "钟楼",
|
||||
activeCharacterPovOwner: "艾琳",
|
||||
},
|
||||
});
|
||||
assert.deepEqual(Array.from(scopedResult.selectedNodeIds), ["char-pov"]);
|
||||
assert.equal(scopedResult.meta.retrieval.activeRegion, "钟楼");
|
||||
assert.ok(Array.isArray(scopedResult.scopeBuckets.characterPov));
|
||||
assert.equal(scopedResult.scopeBuckets.characterPov[0]?.id, "char-pov");
|
||||
|
||||
console.log("retrieval-config tests passed");
|
||||
|
||||
88
tests/scoped-memory.mjs
Normal file
88
tests/scoped-memory.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
addNode,
|
||||
createEmptyGraph,
|
||||
createNode,
|
||||
deserializeGraph,
|
||||
findLatestNode,
|
||||
serializeGraph,
|
||||
} from "../graph.js";
|
||||
|
||||
const graph = createEmptyGraph();
|
||||
const objectiveNode = createNode({
|
||||
type: "character",
|
||||
fields: { name: "艾琳", state: "平静" },
|
||||
seq: 1,
|
||||
});
|
||||
const povNode = createNode({
|
||||
type: "character",
|
||||
fields: { name: "艾琳", state: "怀疑一切" },
|
||||
seq: 2,
|
||||
scope: {
|
||||
layer: "pov",
|
||||
ownerType: "character",
|
||||
ownerId: "艾琳",
|
||||
ownerName: "艾琳",
|
||||
regionPrimary: "钟楼",
|
||||
},
|
||||
});
|
||||
addNode(graph, objectiveNode);
|
||||
addNode(graph, povNode);
|
||||
|
||||
const latestObjective = findLatestNode(
|
||||
graph,
|
||||
"character",
|
||||
"艾琳",
|
||||
"name",
|
||||
{ layer: "objective" },
|
||||
);
|
||||
const latestPov = findLatestNode(
|
||||
graph,
|
||||
"character",
|
||||
"艾琳",
|
||||
"name",
|
||||
{
|
||||
layer: "pov",
|
||||
ownerType: "character",
|
||||
ownerId: "艾琳",
|
||||
ownerName: "艾琳",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(latestObjective?.id, objectiveNode.id);
|
||||
assert.equal(latestPov?.id, povNode.id);
|
||||
|
||||
const legacyGraph = deserializeGraph({
|
||||
version: 5,
|
||||
lastProcessedSeq: 0,
|
||||
nodes: [
|
||||
{
|
||||
id: "legacy-1",
|
||||
type: "event",
|
||||
fields: { title: "旧事件", summary: "旧摘要" },
|
||||
seq: 0,
|
||||
seqRange: [0, 0],
|
||||
archived: false,
|
||||
importance: 5,
|
||||
createdTime: 1,
|
||||
accessCount: 0,
|
||||
lastAccessTime: 1,
|
||||
level: 0,
|
||||
parentId: null,
|
||||
childIds: [],
|
||||
prevId: null,
|
||||
nextId: null,
|
||||
clusters: [],
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
assert.equal(legacyGraph.nodes[0]?.scope?.layer, "objective");
|
||||
assert.equal(legacyGraph.version, 6);
|
||||
|
||||
const restored = deserializeGraph(serializeGraph(graph));
|
||||
assert.equal(restored.nodes.find((node) => node.id === povNode.id)?.scope?.ownerType, "character");
|
||||
assert.equal(restored.nodes.find((node) => node.id === povNode.id)?.scope?.regionPrimary, "钟楼");
|
||||
|
||||
console.log("scoped-memory tests passed");
|
||||
Reference in New Issue
Block a user