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:
@ -1,3 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './graph_database';
|
||||
export * from './memory_service';
|
||||
export { MemoryService } from './memory_service';
|
||||
|
||||
@ -1,11 +1,27 @@
|
||||
import { GraphDatabase } from './graph_database';
|
||||
import type { RecallParams, CommitParams, PurgeParams, RecallResult, CommitResult, PurgeResult, MemoryStats } from './types';
|
||||
import { TaskNodeStore } from './task_node_store';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
RecallParams, CommitParams, PurgeParams,
|
||||
RecallResult, CommitResult, PurgeResult, MemoryStats,
|
||||
ContextRewriteParams, ContextRewriteResult,
|
||||
WorkingMemoryChainParams, WorkingMemoryChainResult,
|
||||
TaskNodeCreateParams, TaskNodeChainResult
|
||||
} from './types';
|
||||
|
||||
export class MemoryService {
|
||||
private db: GraphDatabase;
|
||||
private taskStore: TaskNodeStore;
|
||||
private contextArchiveDir: string;
|
||||
|
||||
constructor(db: GraphDatabase) {
|
||||
constructor(db: GraphDatabase, taskStore?: TaskNodeStore, archiveDir?: string) {
|
||||
this.db = db;
|
||||
this.taskStore = taskStore || new TaskNodeStore();
|
||||
this.contextArchiveDir = archiveDir || './context_archive';
|
||||
if (!fs.existsSync(this.contextArchiveDir)) {
|
||||
fs.mkdirSync(this.contextArchiveDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async recall(params: RecallParams): Promise<RecallResult> {
|
||||
@ -32,6 +48,8 @@ export class MemoryService {
|
||||
return this.db.cleanup(dryRun);
|
||||
}
|
||||
|
||||
// ========== Persona ==========
|
||||
|
||||
async updatePersona(params: { attributes: Array<{ attribute: string; value: string }>; mode?: 'merge' | 'replace' }): Promise<{ status: string; updatedAttributes: number }> {
|
||||
const { attributes, mode = 'merge' } = params;
|
||||
|
||||
@ -127,6 +145,228 @@ export class MemoryService {
|
||||
return { status: 'success' };
|
||||
}
|
||||
|
||||
// ========== TaskNode Chain ==========
|
||||
|
||||
async createTaskNode(params: TaskNodeCreateParams): Promise<{ node_id: number; chain_linked: boolean; archived_path: string | undefined }> {
|
||||
const result = await this.taskStore.createTaskNode(params);
|
||||
|
||||
// Also archive raw context if provided
|
||||
let archivedPath: string | undefined = undefined;
|
||||
if (params.raw_context) {
|
||||
archivedPath = path.join(this.contextArchiveDir, `${params.session_id}_turn${params.turn_id}_raw.txt`);
|
||||
fs.writeFileSync(archivedPath, params.raw_context, 'utf-8');
|
||||
}
|
||||
|
||||
return { ...result, archived_path: archivedPath };
|
||||
}
|
||||
|
||||
async getRecentTaskNodes(session_id: string, limit: number = 5): Promise<Array<{ id: number; turn_id: number; summary: string; key_facts: string[]; created_at: string }>> {
|
||||
const nodes = await this.taskStore.getRecentTaskNodes(session_id, limit);
|
||||
return nodes.map(n => ({
|
||||
id: n.id,
|
||||
turn_id: n.turn_id,
|
||||
summary: n.summary,
|
||||
key_facts: JSON.parse(n.key_facts || '[]') as string[],
|
||||
created_at: n.created_at
|
||||
}));
|
||||
}
|
||||
|
||||
async getTaskChain(session_id: string, from_node_id?: number): Promise<TaskNodeChainResult> {
|
||||
return this.taskStore.getTaskChain(session_id, from_node_id);
|
||||
}
|
||||
|
||||
async readArchivedContext(session_id: string, turn_id: number): Promise<string | null> {
|
||||
const archivePath = path.join(this.contextArchiveDir, `${session_id}_turn${turn_id}_raw.txt`);
|
||||
if (fs.existsSync(archivePath)) {
|
||||
return fs.readFileSync(archivePath, 'utf-8');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== Context Rewrite ==========
|
||||
|
||||
async contextRewrite(params: ContextRewriteParams): Promise<ContextRewriteResult> {
|
||||
const { context, maxEntities = 20, summary } = params;
|
||||
|
||||
// 1. 提取关键句子
|
||||
const sentences = context
|
||||
.split(/[。!?\n]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 5 && s.length < 200);
|
||||
|
||||
// 2. 提取实体(使用增强规则)
|
||||
const entityPattern = /(?:我|你|用户|AI|系统|项目|任务|文件|代码|程序|功能|接口|类|方法|变量|数据库|服务器|客户端|前端|后端|API|Web|App|Python|JavaScript|TypeScript|Java|Go|Rust|C\+\+|数据库|图|记忆|插件|工具|技能|记忆|上下文|偏好|习惯|决策|重要|关键|目标|计划|问题|解决|方案|结果|选择|决定|配置|环境|版本|分支|提交|合并|发布|部署|测试|调试|优化|重构|设计|架构|模式|框架|库|包|依赖|构建|编译|运行|执行|输出|输入|错误|异常|警告|日志|监控|性能|安全|权限|认证|授权|缓存|队列|消息|事件|状态|数据|模型|视图|控制器|路由|请求|响应|协议|格式|编码|解析|序列化|反序列化|同步|异步|并行|并发|线程|进程|阻塞|非阻塞|流|管道|过滤|映射|归约|排序|搜索|匹配|替换|分割|合并|压缩|解压|加密|解密|签名|验证|哈希|随机|唯一|索引|主键|外键|约束|事务|回滚|提交|锁|死锁|超时|重试|降级|熔断|限流|负载|均衡|路由|网关|代理|转发|重写|镜像|快照|备份|恢复|复制|分片|分区|集群|节点|拓扑|网络|域名|IP|端口|套接字|连接|会话|Cookie|Token|JWT|OAuth|SSO|LDAP|AD|Kerberos|证书|CA|TLS|SSL|HTTPS|HTTP|TCP|UDP|WebSocket|gRPC|REST|GraphQL|SOAP|XML|JSON|YAML|TOML|INI|CSV|TSV|Markdown|HTML|CSS|Sass|Less|Stylus|PostCSS|Tailwind|Bootstrap|jQuery|React|Vue|Angular|Svelte|Next|Nuxt|Express|Koa|Fastify|Nest|Django|Flask|FastAPI|Tornado|Spring|Laravel|Rails|Sinatra|Phoenix|Lumen|CodeIgniter|Symfony|Zend|Cake|Fuel|Yii|Phalcon|Slim|Mezzio|Laminas|Expressive|Struts|JSF|GWT|Vaadin|Wicket|Play|Akka|Vert|Quarkus|Micronaut|Helidon|Ktor|http4k|Javalin|Spark|Dropwizard|SpringBoot|Micronaut|Quarkus|Helidon|Ktor|http4k|Javalin|Spark|Dropwizard|Guice|Dagger|Spring|CDI|OSGi|EJB|JPA|Hibernate|MyBatis|EclipseLink|OpenJPA|DataNucleus|ObjectDB|Versant|db4o|NeoDatis|Perst|H2|SQLite|MySQL|PostgreSQL|Oracle|SQLServer|DB2|Sybase|Informix|Teradata|Vertica|Greenplum|Redshift|BigQuery|Snowflake|Databricks|SparkSQL|Hive|Impala|Presto|Trino|Drill|Phoenix|HBase|Cassandra|MongoDB|CouchDB|DynamoDB|DocumentDB|Firestore|CosmosDB|Redis|Memcached|Riak|Voldemort|Couchbase|Aerospike|Scylla| Yugabyte|TiDB|Cockroach|Vitess|ProxySQL|MaxScale|PgBouncer|Odyssey| Pgpool|Slony|Bucardo|Londiste|Skytools|WalE|Barman|PgBackRest|PgDump| PgRestore|PgUpgrade|PgAdmin|PgStudio|OmniDB|DBeaver|Navicat|DataGrip| TablePlus|SequelPro|HeidiSQL|MySQLWorkbench|phpMyAdmin|Adminer|SQLBuddy| Chive|TinyTinyRSS|FreshRSS|Miniflux|Stringer|Feedly|Inoreader|NewsBlur| TheOldReader|CommaFeed|BazQux|Feedbin|Feed Wrangler|FeedHQ|FeedReader| Liferea|QuiteRSS|RSSOwl|Thunderbird|Outlook|AppleMail|Spark|Airmail| Newton|Canary|Edison|BlueMail|TypeApp|Nine|K9|FairEmail|Aquamail| ProtonMail|Tutanota|CTemplar|StartMail|Runbox|CounterMail|Hushmail| KolabNow|Mailbox.org|Posteo|Soverin|TheXYZ|ZohoMail|FastMail|GandiMail| Namecheap|Hover|DreamHost|HostGator|Bluehost|GoDaddy|Namecheap|Dynadot| GoogleDomains|CloudflareRegistrar|Route53|DNSimple|Gandi|OVH|Hetzner| Linode|DigitalOcean|Vultr|UpCloud|Scaleway|Exoscale|CherryServers| Packet|Equinix|AWS|Azure|GCP|IBMCloud|OracleCloud|AlibabaCloud|TencentCloud| HuaweiCloud|BaiduCloud|JDCloud|UCloud|QingCloud|ChinaTelecom|ChinaUnicom| ChinaMobile|GreatWall|DrPeng|Broadnet|Wasu|Born|Topway|Guangdong| Guangxi|Hainan|Chongqing|Sichuan|Guizhou|Yunnan|Xizang|Shaanxi| Gansu|Qinghai|Ningxia|Xinjiang|Beijing|Tianjin|Hebei|Shanxi|InnerMongolia| Liaoning|Jilin|Heilongjiang|Shanghai|Jiangsu|Zhejiang|Anhui|Fujian| Jiangxi|Shandong|Henan|Hubei|Hunan|Guangdong|Guangxi|Hainan|Chongqing| Sichuan|Guizhou|Yunnan|Xizang|Shaanxi|Gansu|Qinghai|Ningxia|Xinjiang| HongKong|Macau|Taiwan)/g;
|
||||
const foundEntities = new Set<string>();
|
||||
sentences.forEach(s => {
|
||||
const matches = s.match(entityPattern);
|
||||
if (matches) matches.forEach(m => foundEntities.add(m));
|
||||
});
|
||||
|
||||
if (foundEntities.size < 3) {
|
||||
const words = context.split(/\s+/).filter(w => w.length >= 2 && w.length <= 20);
|
||||
const freq = new Map<string, number>();
|
||||
words.forEach(w => freq.set(w, (freq.get(w) || 0) + 1));
|
||||
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
||||
sorted.slice(0, maxEntities).forEach(([w]) => foundEntities.add(w));
|
||||
}
|
||||
|
||||
const entities = Array.from(foundEntities).slice(0, maxEntities);
|
||||
|
||||
// 3. 生成摘要
|
||||
const keySentences = sentences
|
||||
.filter(s => entities.some(e => s.includes(e)))
|
||||
.slice(0, 5);
|
||||
|
||||
const generatedSummary = summary || keySentences.join(';') || context.slice(0, 200);
|
||||
|
||||
// 4. 生成三元组关系
|
||||
const triplets: Array<{ subject: string; relation: string; object: string; confidence?: number }> = [];
|
||||
|
||||
// 实体共现关系
|
||||
for (let i = 0; i < Math.min(entities.length, 10); i++) {
|
||||
for (let j = i + 1; j < Math.min(entities.length, 10); j++) {
|
||||
const s1 = entities[i];
|
||||
const s2 = entities[j];
|
||||
const coOccur = sentences.some(s => s.includes(s1) && s.includes(s2));
|
||||
if (coOccur) {
|
||||
triplets.push({
|
||||
subject: s1,
|
||||
relation: '关联',
|
||||
object: s2,
|
||||
confidence: 0.7
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测偏好和决策模式
|
||||
const preferencePatterns = [
|
||||
{ pattern: /喜欢|偏好|爱好|倾向|习惯|常用|总是|经常/, relation: '偏好' },
|
||||
{ pattern: /决定|决策|选择|确定|定了|采用|使用|方案/, relation: '决策' },
|
||||
{ pattern: /重要|关键|核心|主要|首要|必须|务必|一定/, relation: '重要性' },
|
||||
{ pattern: /目标|计划|打算|准备|预计|期望|希望|想要/, relation: '意图' },
|
||||
{ pattern: /问题|错误|异常|失败|困难|挑战|障碍|风险/, relation: '问题' },
|
||||
{ pattern: /解决|修复|处理|应对|克服|消除|避免|预防/, relation: '解决方案' }
|
||||
];
|
||||
|
||||
sentences.forEach(sentence => {
|
||||
preferencePatterns.forEach(({ pattern, relation }) => {
|
||||
if (pattern.test(sentence)) {
|
||||
const matchedEntities = entities.filter(e => sentence.includes(e));
|
||||
if (matchedEntities.length > 0) {
|
||||
triplets.push({
|
||||
subject: matchedEntities[0],
|
||||
relation,
|
||||
object: sentence.slice(0, 100),
|
||||
confidence: 0.85
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 创建摘要节点
|
||||
const summaryId = `Summary_${Date.now()}`;
|
||||
triplets.push({
|
||||
subject: summaryId,
|
||||
relation: 'is_type',
|
||||
object: 'ContextSummary',
|
||||
confidence: 1.0
|
||||
});
|
||||
triplets.push({
|
||||
subject: summaryId,
|
||||
relation: 'HAS_CONTENT',
|
||||
object: generatedSummary.slice(0, 500),
|
||||
confidence: 1.0
|
||||
});
|
||||
triplets.push({
|
||||
subject: summaryId,
|
||||
relation: 'SOURCE_TYPE',
|
||||
object: 'context_rewrite',
|
||||
confidence: 1.0
|
||||
});
|
||||
|
||||
// 实体与摘要的关联
|
||||
entities.slice(0, 5).forEach(e => {
|
||||
triplets.push({
|
||||
subject: summaryId,
|
||||
relation: 'MENTIONS',
|
||||
object: e,
|
||||
confidence: 0.8
|
||||
});
|
||||
});
|
||||
|
||||
// 写入记忆图
|
||||
const commitResult = await this.db.commit({ triplets });
|
||||
|
||||
// 同时存入任务节点表
|
||||
const keyFacts = entities.slice(0, 10).map(e => `实体: ${e}`);
|
||||
keyFacts.push(`摘要: ${generatedSummary.slice(0, 100)}`);
|
||||
|
||||
await this.taskStore.createTaskNode({
|
||||
session_id: this.db.getSessionId(),
|
||||
turn_id: Date.now(),
|
||||
summary: generatedSummary,
|
||||
key_facts: keyFacts,
|
||||
raw_context: context
|
||||
});
|
||||
|
||||
return {
|
||||
extractedEntities: entities.length,
|
||||
extractedRelations: commitResult.createdRelations,
|
||||
summary: generatedSummary,
|
||||
compressed: context.length > generatedSummary.length
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Working Memory Chain ==========
|
||||
|
||||
async workingMemoryChain(params: WorkingMemoryChainParams = {}): Promise<WorkingMemoryChainResult> {
|
||||
const { maxDepth = 3, recentOnly = true } = params;
|
||||
|
||||
// 使用 TaskNode 链获取工作记忆
|
||||
const sessionId = this.db.getSessionId();
|
||||
const recentNodes = await this.taskStore.getRecentTaskNodes(sessionId, maxDepth * 3);
|
||||
|
||||
const chain = recentNodes.map(n => ({
|
||||
subject: `Turn_${n.turn_id}`,
|
||||
relation: 'summary',
|
||||
object: n.summary.slice(0, 100),
|
||||
timestamp: n.created_at
|
||||
}));
|
||||
|
||||
// 同时从图数据库获取活跃关系补充
|
||||
const timeFilter = recentOnly ? { days: 1 } : undefined;
|
||||
const graphResult = await this.db.recall({
|
||||
queryIntent: '',
|
||||
seedEntities: [],
|
||||
depth: maxDepth,
|
||||
timeRange: timeFilter,
|
||||
sessionFilter: sessionId
|
||||
});
|
||||
|
||||
const graphChain = graphResult.relations
|
||||
.filter(r => r.status === 'active')
|
||||
.slice(0, 10)
|
||||
.map(r => {
|
||||
const source = graphResult.entities.find(e => e.id === r.sourceId);
|
||||
const target = graphResult.entities.find(e => e.id === r.targetId);
|
||||
return {
|
||||
subject: source?.name || r.sourceId,
|
||||
relation: r.relationType,
|
||||
object: target?.name || r.targetId,
|
||||
timestamp: r.createdAt.toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
chain: [...chain, ...graphChain].slice(0, 20),
|
||||
entityCount: graphResult.entities.length + recentNodes.length
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Utility ==========
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
this.db.setSessionId(sessionId);
|
||||
}
|
||||
|
||||
183
ts/src/runtime/core/graph_memory/task_node_store.ts
Normal file
183
ts/src/runtime/core/graph_memory/task_node_store.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TaskNodeData } from './types';
|
||||
|
||||
export { TaskNodeData as TaskNode };
|
||||
|
||||
export class TaskNodeStore {
|
||||
private db: Database.Database;
|
||||
private archiveDir: string;
|
||||
|
||||
constructor(dbPath?: string, archiveDir?: string) {
|
||||
this.db = new Database(dbPath || 'graph_memory.db');
|
||||
this.archiveDir = archiveDir || './task_archive';
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS task_nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
turn_id INTEGER NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
key_facts TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS task_chains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
from_node_id INTEGER NOT NULL,
|
||||
to_node_id INTEGER NOT NULL,
|
||||
relation_type TEXT DEFAULT 'next',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (from_node_id) REFERENCES task_nodes(id),
|
||||
FOREIGN KEY (to_node_id) REFERENCES task_nodes(id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_task_nodes_session ON task_nodes(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_nodes_turn ON task_nodes(session_id, turn_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_chains_session ON task_chains(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_chains_from ON task_chains(from_node_id);
|
||||
`);
|
||||
|
||||
if (!fs.existsSync(this.archiveDir)) {
|
||||
fs.mkdirSync(this.archiveDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async createTaskNode(params: {
|
||||
session_id: string;
|
||||
turn_id: number;
|
||||
summary: string;
|
||||
key_facts: string[];
|
||||
raw_context?: string | undefined;
|
||||
}): Promise<{ node_id: number; chain_linked: boolean }> {
|
||||
const { session_id, turn_id, summary, key_facts, raw_context } = params;
|
||||
|
||||
const insert = this.db.prepare(`
|
||||
INSERT INTO task_nodes (session_id, turn_id, summary, key_facts)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const result = insert.run(session_id, turn_id, summary, JSON.stringify(key_facts));
|
||||
const nodeId = Number(result.lastInsertRowid);
|
||||
|
||||
// Link to previous node in chain
|
||||
let chainLinked = false;
|
||||
const prevNode = this.db.prepare(`
|
||||
SELECT id FROM task_nodes
|
||||
WHERE session_id = ? AND turn_id < ?
|
||||
ORDER BY turn_id DESC LIMIT 1
|
||||
`).get(session_id, turn_id) as { id: number } | undefined;
|
||||
|
||||
if (prevNode) {
|
||||
this.db.prepare(`
|
||||
INSERT INTO task_chains (session_id, from_node_id, to_node_id, relation_type)
|
||||
VALUES (?, ?, ?, 'next')
|
||||
`).run(session_id, prevNode.id, nodeId);
|
||||
chainLinked = true;
|
||||
}
|
||||
|
||||
// Archive raw context to text file if provided
|
||||
if (raw_context) {
|
||||
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
|
||||
fs.writeFileSync(archivePath, raw_context, 'utf-8');
|
||||
}
|
||||
|
||||
return { node_id: nodeId, chain_linked: chainLinked };
|
||||
}
|
||||
|
||||
async getRecentTaskNodes(session_id: string, limit: number = 5): Promise<TaskNodeData[]> {
|
||||
const rows = this.db.prepare(`
|
||||
SELECT * FROM task_nodes
|
||||
WHERE session_id = ?
|
||||
ORDER BY turn_id DESC
|
||||
LIMIT ?
|
||||
`).all(session_id, limit) as Array<Record<string, unknown>>;
|
||||
|
||||
return rows.map(r => ({
|
||||
id: r.id as number,
|
||||
session_id: r.session_id as string,
|
||||
turn_id: r.turn_id as number,
|
||||
summary: r.summary as string,
|
||||
key_facts: r.key_facts as string,
|
||||
created_at: r.created_at as string
|
||||
})).reverse(); // Return in chronological order
|
||||
}
|
||||
|
||||
async getTaskChain(session_id: string, from_node_id?: number): Promise<{
|
||||
nodes: TaskNodeData[];
|
||||
relations: Array<{ from: number; to: number; type: string }>;
|
||||
}> {
|
||||
let startNode = from_node_id;
|
||||
if (!startNode) {
|
||||
const latest = this.db.prepare(`
|
||||
SELECT id FROM task_nodes WHERE session_id = ? ORDER BY turn_id DESC LIMIT 1
|
||||
`).get(session_id) as { id: number } | undefined;
|
||||
if (!latest) return { nodes: [], relations: [] };
|
||||
startNode = latest.id;
|
||||
}
|
||||
|
||||
// Walk backwards through the chain
|
||||
const nodes: TaskNodeData[] = [];
|
||||
const relations: Array<{ from: number; to: number; type: string }> = [];
|
||||
const visited = new Set<number>();
|
||||
let current = startNode;
|
||||
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
const node = this.db.prepare(`SELECT * FROM task_nodes WHERE id = ?`).get(current) as Record<string, unknown> | undefined;
|
||||
if (node) {
|
||||
nodes.unshift({
|
||||
id: node.id as number,
|
||||
session_id: node.session_id as string,
|
||||
turn_id: node.turn_id as number,
|
||||
summary: node.summary as string,
|
||||
key_facts: node.key_facts as string,
|
||||
created_at: node.created_at as string
|
||||
});
|
||||
}
|
||||
|
||||
const prevChain = this.db.prepare(`
|
||||
SELECT from_node_id, relation_type FROM task_chains WHERE to_node_id = ? AND session_id = ?
|
||||
`).get(current, session_id) as { from_node_id: number; relation_type: string } | undefined;
|
||||
|
||||
if (prevChain) {
|
||||
relations.unshift({
|
||||
from: prevChain.from_node_id,
|
||||
to: current,
|
||||
type: prevChain.relation_type
|
||||
});
|
||||
current = prevChain.from_node_id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, relations };
|
||||
}
|
||||
|
||||
async archiveRawContext(session_id: string, turn_id: number, raw_context: string): Promise<string> {
|
||||
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
|
||||
fs.writeFileSync(archivePath, raw_context, 'utf-8');
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
async readArchivedContext(session_id: string, turn_id: number): Promise<string | null> {
|
||||
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
|
||||
if (fs.existsSync(archivePath)) {
|
||||
return fs.readFileSync(archivePath, 'utf-8');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
@ -123,3 +123,48 @@ export interface TaskLinkInfoParams {
|
||||
task_id: string;
|
||||
info_node: string;
|
||||
}
|
||||
|
||||
export interface ContextRewriteParams {
|
||||
context: string;
|
||||
maxEntities?: number | undefined;
|
||||
summary?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ContextRewriteResult {
|
||||
extractedEntities: number;
|
||||
extractedRelations: number;
|
||||
summary: string;
|
||||
compressed: boolean;
|
||||
}
|
||||
|
||||
export interface WorkingMemoryChainParams {
|
||||
maxDepth?: number | undefined;
|
||||
recentOnly?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface WorkingMemoryChainResult {
|
||||
chain: Array<{ subject: string; relation: string; object: string; timestamp: string }>;
|
||||
entityCount: number;
|
||||
}
|
||||
|
||||
export interface TaskNodeData {
|
||||
id: number;
|
||||
session_id: string;
|
||||
turn_id: number;
|
||||
summary: string;
|
||||
key_facts: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TaskNodeCreateParams {
|
||||
session_id: string;
|
||||
turn_id: number;
|
||||
summary: string;
|
||||
key_facts: string[];
|
||||
raw_context?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TaskNodeChainResult {
|
||||
nodes: TaskNodeData[];
|
||||
relations: Array<{ from: number; to: number; type: string }>;
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ export const GraphMemoryToolSchema = Type.Object({
|
||||
enum: [
|
||||
'recall', 'commit', 'purge', 'introspect', 'archive', 'cleanup',
|
||||
'persona_update', 'persona_clear',
|
||||
'task_create', 'task_set_state', 'task_delete', 'task_link_info'
|
||||
'task_create', 'task_set_state', 'task_delete', 'task_link_info',
|
||||
'context_rewrite', 'working_memory_chain',
|
||||
'task_node_create', 'task_node_get_recent', 'task_node_get_chain'
|
||||
]
|
||||
}),
|
||||
params: Type.Object({
|
||||
@ -57,7 +59,18 @@ export const GraphMemoryToolSchema = Type.Object({
|
||||
info_nodes: Type.Optional(Type.Array(Type.String({ description: '节点' }), { description: '信息节点' })),
|
||||
info_node: Type.Optional(Type.String({ description: '信息节点' })),
|
||||
days: Type.Optional(Type.Number({ description: '归档天数' })),
|
||||
dry_run: Type.Optional(Type.Boolean({ description: '仅预览不删除' }))
|
||||
dry_run: Type.Optional(Type.Boolean({ description: '仅预览不删除' })),
|
||||
context: Type.Optional(Type.String({ description: '要压缩的上下文文本' })),
|
||||
maxEntities: Type.Optional(Type.Number({ description: '最大实体数' })),
|
||||
summary: Type.Optional(Type.String({ description: '自定义摘要' })),
|
||||
recentOnly: Type.Optional(Type.Boolean({ description: '仅最近' })),
|
||||
maxDepth: Type.Optional(Type.Number({ description: '最大深度' })),
|
||||
session_id: Type.Optional(Type.String({ description: '会话ID(TaskNode用)' })),
|
||||
turn_id: Type.Optional(Type.Number({ description: '轮次ID' })),
|
||||
key_facts: Type.Optional(Type.Array(Type.String({ description: '关键事实' }), { description: '关键事实数组' })),
|
||||
raw_context: Type.Optional(Type.String({ description: '原始上下文(存档用)' })),
|
||||
limit: Type.Optional(Type.Number({ description: '限制数量' })),
|
||||
from_node_id: Type.Optional(Type.Number({ description: '起始节点ID' }))
|
||||
}, { description: '操作参数' })
|
||||
});
|
||||
|
||||
@ -77,7 +90,12 @@ const GRAPH_MEMORY_TOOL_DESCRIPTION = `图记忆工具 - 让 AI 拥有真正的
|
||||
- task_create: 创建任务(必需参数: task_id, description; 可选: info_nodes)
|
||||
- task_set_state: 设置任务状态(必需参数: task_id, state)
|
||||
- task_delete: 删除任务(必需参数: task_id)
|
||||
- task_link_info: 关联信息(必需参数: task_id, info_node)`;
|
||||
- task_link_info: 关联信息(必需参数: task_id, info_node)
|
||||
- context_rewrite: 压缩上下文为关键记忆(必需参数: context; 可选: maxEntities, summary)
|
||||
- working_memory_chain: 获取工作记忆链(可选参数: maxDepth, recentOnly)
|
||||
- task_node_create: 创建任务节点并链接到链(必需参数: session_id, turn_id, summary, key_facts; 可选: raw_context)
|
||||
- task_node_get_recent: 获取最近N个任务节点(必需参数: session_id; 可选: limit)
|
||||
- task_node_get_chain: 获取完整任务链(必需参数: session_id; 可选: from_node_id)`;
|
||||
|
||||
// ==================== 参数验证 ====================
|
||||
|
||||
@ -228,6 +246,71 @@ function validateTaskLinkInfoParams(params: Record<string, unknown>): Validation
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateContextRewriteParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.context || typeof params.context !== 'string' || params.context.trim() === '') {
|
||||
errors.push({ field: 'context', message: 'context_rewrite 操作必需提供 context 字符串' });
|
||||
}
|
||||
if (params.maxEntities !== undefined && (typeof params.maxEntities !== 'number' || params.maxEntities < 1 || params.maxEntities > 100)) {
|
||||
errors.push({ field: 'maxEntities', message: 'maxEntities 必须在 1-100 之间' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateWorkingMemoryChainParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (params.maxDepth !== undefined && (typeof params.maxDepth !== 'number' || params.maxDepth < 1 || params.maxDepth > 5)) {
|
||||
errors.push({ field: 'maxDepth', message: 'maxDepth 必须在 1-5 之间' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskNodeCreateParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
|
||||
errors.push({ field: 'session_id', message: 'task_node_create 操作必需提供 session_id 字符串' });
|
||||
}
|
||||
if (params.turn_id === undefined || typeof params.turn_id !== 'number') {
|
||||
errors.push({ field: 'turn_id', message: 'task_node_create 操作必需提供 turn_id 数字' });
|
||||
}
|
||||
if (!params.summary || typeof params.summary !== 'string' || params.summary.trim() === '') {
|
||||
errors.push({ field: 'summary', message: 'task_node_create 操作必需提供 summary 字符串' });
|
||||
}
|
||||
if (!params.key_facts || !Array.isArray(params.key_facts) || params.key_facts.length === 0) {
|
||||
errors.push({ field: 'key_facts', message: 'task_node_create 操作必需提供 key_facts 数组' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskNodeGetRecentParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
|
||||
errors.push({ field: 'session_id', message: 'task_node_get_recent 操作必需提供 session_id 字符串' });
|
||||
}
|
||||
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1 || params.limit > 100)) {
|
||||
errors.push({ field: 'limit', message: 'limit 必须在 1-100 之间' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskNodeGetChainParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
|
||||
errors.push({ field: 'session_id', message: 'task_node_get_chain 操作必需提供 session_id 字符串' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validatePersonaClearParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
@ -249,6 +332,11 @@ function validateParams(action: string, params: Record<string, unknown>): Valida
|
||||
case 'task_set_state': return validateTaskSetStateParams(params);
|
||||
case 'task_delete': return validateTaskDeleteParams(params);
|
||||
case 'task_link_info': return validateTaskLinkInfoParams(params);
|
||||
case 'context_rewrite': return validateContextRewriteParams(params);
|
||||
case 'working_memory_chain': return validateWorkingMemoryChainParams(params);
|
||||
case 'task_node_create': return validateTaskNodeCreateParams(params);
|
||||
case 'task_node_get_recent': return validateTaskNodeGetRecentParams(params);
|
||||
case 'task_node_get_chain': return validateTaskNodeGetChainParams(params);
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
@ -396,6 +484,40 @@ export function createGraphMemoryTool(dbPath?: string, sessionId?: string) {
|
||||
info_node: params.info_node as string
|
||||
});
|
||||
|
||||
case 'context_rewrite':
|
||||
return service.contextRewrite({
|
||||
context: params.context as string,
|
||||
maxEntities: params.maxEntities as number | undefined,
|
||||
summary: params.summary as string | undefined
|
||||
});
|
||||
|
||||
case 'working_memory_chain':
|
||||
return service.workingMemoryChain({
|
||||
maxDepth: params.maxDepth as number | undefined,
|
||||
recentOnly: params.recentOnly as boolean | undefined
|
||||
});
|
||||
|
||||
case 'task_node_create':
|
||||
return service.createTaskNode({
|
||||
session_id: params.session_id as string,
|
||||
turn_id: params.turn_id as number,
|
||||
summary: params.summary as string,
|
||||
key_facts: params.key_facts as string[],
|
||||
raw_context: params.raw_context as string | undefined
|
||||
});
|
||||
|
||||
case 'task_node_get_recent':
|
||||
return service.getRecentTaskNodes(
|
||||
params.session_id as string,
|
||||
params.limit as number | undefined
|
||||
);
|
||||
|
||||
case 'task_node_get_chain':
|
||||
return service.getTaskChain(
|
||||
params.session_id as string,
|
||||
params.from_node_id as number | undefined
|
||||
);
|
||||
|
||||
case 'archive':
|
||||
return service.archive(params.days as number | undefined);
|
||||
|
||||
@ -471,4 +593,4 @@ export function createGraphMemoryTool(dbPath?: string, sessionId?: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export { ToolLimiter } from '../tool_limiter.js';
|
||||
export { ToolLimiter } from '../tool_limiter.js';
|
||||
|
||||
Reference in New Issue
Block a user