mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-15 12:15:32 +00:00
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:
200
ts/tests/runtime/core/graph_memory/memory_service_p2.test.ts
Normal file
200
ts/tests/runtime/core/graph_memory/memory_service_p2.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
155
ts/tests/runtime/core/graph_memory/task_node_store.test.ts
Normal file
155
ts/tests/runtime/core/graph_memory/task_node_store.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user