P2: 高级功能开发 - context_rewrite + TaskNode链 + 人设强制查询Skill

- context_rewrite: 压缩上下文,提取关键三元组,支持偏好/决策/目标检测
- TaskNodeStore: 新表结构 + next链式关系 + 原始文本存档
- MemoryService: 集成TaskNode链操作,workingMemoryChain增强
- GraphMemoryTool: 新增 task_node_create/get_recent/get_chain 3个action
- graph-memory-persona-force Skill: 指导AI主动查询人设图
- 新增测试: task_node_store.test.ts (6测试) + memory_service_p2.test.ts (7测试)
- 全部138个测试通过
This commit is contained in:
root
2026-04-24 15:44:45 +08:00
parent 7314d85ac6
commit 9e067ac23a
11 changed files with 1213 additions and 9 deletions

View File

@ -0,0 +1,200 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GraphDatabase } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/graph_database.js';
import { MemoryService } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/memory_service.js';
import { TaskNodeStore } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/task_node_store.js';
import * as fs from 'fs';
import * as path from 'path';
const TEST_DB_PATH = '/tmp/test_memory_service_p2.db';
const TEST_ARCHIVE_DIR = '/tmp/test_context_archive';
describe('MemoryService P2 Advanced Features', () => {
let memoryService: MemoryService;
let db: GraphDatabase;
let taskStore: TaskNodeStore;
beforeEach(async () => {
// Clean up test files
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
db = new GraphDatabase(TEST_DB_PATH, 'test-session-p2');
taskStore = new TaskNodeStore(TEST_DB_PATH, TEST_ARCHIVE_DIR + '/task');
memoryService = new MemoryService(db, taskStore, TEST_ARCHIVE_DIR);
});
afterEach(() => {
if (typeof db.close === 'function') db.close();
if (typeof taskStore.close === 'function') taskStore.close();
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
});
describe('context_rewrite', () => {
it('should compress context and extract key entities', async () => {
const longContext = `用户说:我喜欢用Python编程。最近在学习TypeScript,因为想做一个全栈项目。
我决定采用React作为前端框架,后端用FastAPI。数据库选择PostgreSQL。
这个决策对我来说很重要,因为我希望能快速迭代。我对性能有较高要求。
目标是三个月内上线第一个版本。`;
const result = await memoryService.contextRewrite({
context: longContext,
maxEntities: 10
});
expect(result.extractedEntities).toBeGreaterThan(0);
expect(result.summary.length).toBeGreaterThan(0);
expect(result.compressed).toBe(true);
});
it('should detect preferences and decisions', async () => {
const context = `用户决定使用React而不是Vue。用户喜欢简洁的代码风格。
用户习惯每天早上检查代码质量。用户选择PostgreSQL作为数据库。`;
const result = await memoryService.contextRewrite({
context,
summary: '用户技术偏好总结'
});
expect(result.extractedRelations).toBeGreaterThan(0);
expect(result.summary).toBe('用户技术偏好总结');
});
});
describe('working_memory_chain', () => {
it('should retrieve working memory from task nodes', async () => {
// First create some task nodes
await memoryService.createTaskNode({
session_id: 'test-session-p2',
turn_id: 1,
summary: '用户询问天气',
key_facts: ['意图: 查询天气', '地点: 北京']
});
await memoryService.createTaskNode({
session_id: 'test-session-p2',
turn_id: 2,
summary: '用户询问交通',
key_facts: ['意图: 查询交通', '地点: 北京']
});
const result = await memoryService.workingMemoryChain({
maxDepth: 2,
recentOnly: true
});
expect(result.chain.length).toBeGreaterThan(0);
expect(result.entityCount).toBeGreaterThan(0);
});
});
describe('task_node chain', () => {
it('should create task nodes and link them in chain', async () => {
const node1 = await memoryService.createTaskNode({
session_id: 'chain-test',
turn_id: 1,
summary: '开始对话',
key_facts: ['fact1', 'fact2'],
raw_context: '用户: 你好\nAI: 你好!有什么可以帮你的?'
});
expect(node1.node_id).toBeDefined();
expect(node1.chain_linked).toBe(false); // First node
const node2 = await memoryService.createTaskNode({
session_id: 'chain-test',
turn_id: 2,
summary: '用户询问编程',
key_facts: ['fact3'],
raw_context: '用户: 我想学编程\nAI: 太好了!你想学什么语言?'
});
expect(node2.node_id).toBeDefined();
expect(node2.chain_linked).toBe(true); // Linked to first node
});
it('should get recent task nodes', async () => {
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 1,
summary: 'Node 1',
key_facts: ['fact1']
});
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 2,
summary: 'Node 2',
key_facts: ['fact2']
});
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 3,
summary: 'Node 3',
key_facts: ['fact3']
});
const recent = await memoryService.getRecentTaskNodes('recent-test', 2);
expect(recent.length).toBe(2);
expect(recent[0].turn_id).toBe(2); // Chronological order
expect(recent[1].turn_id).toBe(3);
});
it('should get full task chain', async () => {
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 1,
summary: 'Start',
key_facts: ['start']
});
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 2,
summary: 'Middle',
key_facts: ['middle']
});
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 3,
summary: 'End',
key_facts: ['end']
});
const chain = await memoryService.getTaskChain('chain-full-test');
expect(chain.nodes.length).toBe(3);
expect(chain.relations.length).toBe(2); // 3 nodes = 2 next relations
expect(chain.relations[0].type).toBe('next');
});
it('should archive and read raw context', async () => {
const rawContext = '这是一个很长的对话记录...包含很多细节...';
await memoryService.createTaskNode({
session_id: 'archive-test',
turn_id: 1,
summary: '对话摘要',
key_facts: ['fact1'],
raw_context: rawContext
});
const readBack = await memoryService.readArchivedContext('archive-test', 1);
expect(readBack).toBe(rawContext);
});
});
});

View File

@ -0,0 +1,155 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TaskNodeStore } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/task_node_store.js';
import * as fs from 'fs';
const TEST_DB_PATH = '/tmp/test_task_node_store.db';
const TEST_ARCHIVE_DIR = '/tmp/test_task_archive';
describe('TaskNodeStore', () => {
let store: TaskNodeStore;
beforeEach(async () => {
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
store = new TaskNodeStore(TEST_DB_PATH, TEST_ARCHIVE_DIR);
});
afterEach(() => {
if (typeof store.close === 'function') store.close();
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
});
describe('createTaskNode', () => {
it('should create a task node', async () => {
const result = await store.createTaskNode({
session_id: 'session-1',
turn_id: 1,
summary: 'Test summary',
key_facts: ['fact1', 'fact2']
});
expect(result.node_id).toBeDefined();
expect(result.chain_linked).toBe(false);
});
it('should link nodes in chain', async () => {
await store.createTaskNode({
session_id: 'chain-session',
turn_id: 1,
summary: 'First',
key_facts: ['fact1']
});
const result = await store.createTaskNode({
session_id: 'chain-session',
turn_id: 2,
summary: 'Second',
key_facts: ['fact2']
});
expect(result.chain_linked).toBe(true);
});
it('should archive raw context', async () => {
const rawContext = 'This is a long context...';
await store.createTaskNode({
session_id: 'archive-session',
turn_id: 1,
summary: 'Archived',
key_facts: ['fact1'],
raw_context: rawContext
});
const readBack = await store.readArchivedContext('archive-session', 1);
expect(readBack).toBe(rawContext);
});
});
describe('getRecentTaskNodes', () => {
it('should return recent nodes in chronological order', async () => {
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 1,
summary: 'One',
key_facts: ['fact1']
});
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 2,
summary: 'Two',
key_facts: ['fact2']
});
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 3,
summary: 'Three',
key_facts: ['fact3']
});
const recent = await store.getRecentTaskNodes('recent-session', 2);
expect(recent.length).toBe(2);
expect(recent[0].turn_id).toBe(2);
expect(recent[1].turn_id).toBe(3);
});
});
describe('getTaskChain', () => {
it('should walk full chain backwards', async () => {
const n1 = await store.createTaskNode({
session_id: 'walk-session',
turn_id: 1,
summary: 'Start',
key_facts: ['start']
});
const n2 = await store.createTaskNode({
session_id: 'walk-session',
turn_id: 2,
summary: 'Middle',
key_facts: ['middle']
});
await store.createTaskNode({
session_id: 'walk-session',
turn_id: 3,
summary: 'End',
key_facts: ['end']
});
const chain = await store.getTaskChain('walk-session');
expect(chain.nodes.length).toBe(3);
expect(chain.relations.length).toBe(2);
});
it('should walk chain from specific node', async () => {
await store.createTaskNode({
session_id: 'from-session',
turn_id: 1,
summary: 'A',
key_facts: ['a']
});
const n2 = await store.createTaskNode({
session_id: 'from-session',
turn_id: 2,
summary: 'B',
key_facts: ['b']
});
const chain = await store.getTaskChain('from-session', n2.node_id);
expect(chain.nodes.length).toBe(2);
});
});
});

View File

@ -575,8 +575,41 @@ describe('GraphMemoryTool', () => {
expect(parsed.error.message).toContain('newRelation');
});
it('should reject context_rewrite without context', async () => {
const result = await tool.execute('call-val-12', {
action: 'context_rewrite',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('context');
});
it('should reject context_rewrite with invalid maxEntities', async () => {
const result = await tool.execute('call-val-13', {
action: 'context_rewrite',
params: { context: '测试', maxEntities: 200 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('maxEntities');
});
it('should reject working_memory_chain with invalid maxDepth', async () => {
const result = await tool.execute('call-val-14', {
action: 'working_memory_chain',
params: { maxDepth: 10 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('maxDepth');
});
it('should reject missing action', async () => {
const result = await tool.execute('call-val-11', {
const result = await tool.execute('call-val-15', {
action: '',
params: {}
});
@ -586,4 +619,74 @@ describe('GraphMemoryTool', () => {
expect(parsed.error.message).toContain('action');
});
});
describe('Context rewrite', () => {
it('should compress context and extract entities', async () => {
const longContext = '我们在开发一个项目。这个项目使用 TypeScript。TypeScript 是 JavaScript 的超集。我们在写代码。代码在仓库里。仓库用 Git 管理。Git 是版本控制系统。';
const result = await tool.execute('call-cr-1', {
action: 'context_rewrite',
params: {
context: longContext,
maxEntities: 10
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.extractedEntities).toBeGreaterThan(0);
expect(parsed.data.summary).toBeTruthy();
expect(parsed.data.compressed).toBe(true);
});
it('should use provided summary', async () => {
const result = await tool.execute('call-cr-2', {
action: 'context_rewrite',
params: {
context: '测试文本',
summary: '用户自定义摘要'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.summary).toBe('用户自定义摘要');
});
});
describe('Working memory chain', () => {
it('should return working memory chain', async () => {
// 先写入一些数据
await tool.execute('call-wmc-1', {
action: 'commit',
params: {
triplets: [
{ subject: '用户', relation: '喜欢', object: 'Python' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
]
}
});
const result = await tool.execute('call-wmc-2', {
action: 'working_memory_chain',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.chain).toBeDefined();
expect(Array.isArray(parsed.data.chain)).toBe(true);
});
it('should respect maxDepth parameter', async () => {
const result = await tool.execute('call-wmc-3', {
action: 'working_memory_chain',
params: { maxDepth: 2 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.entityCount).toBeDefined();
});
});
});