Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bd14a5868 | |||
| c33e7469ac | |||
| 697a639cbe | |||
| 33a2b8c749 | |||
| c930b20e84 | |||
| 9e067ac23a | |||
| 7314d85ac6 | |||
| a4a904f66b | |||
| 135041e8b0 | |||
| 3d58ff7766 | |||
| 192179a986 | |||
| 85c3431b52 | |||
| 4caf115ec9 | |||
| e8c275c8f8 | |||
| 931617624e | |||
| ee2fd18fec | |||
| aa18c1c8b1 | |||
| d05bb5507f | |||
| 904661d73f | |||
| 43172e257a |
10
.gitignore
vendored
10
.gitignore
vendored
@ -64,3 +64,13 @@ jimeng*.png
|
||||
|
||||
# Test Cache
|
||||
.pytest_cache/
|
||||
|
||||
# TypeScript
|
||||
node_modules/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Task Archive (runtime generated)
|
||||
task_archive/
|
||||
ts/task_archive/
|
||||
|
||||
29
.sisyphus/fix-tracker.md
Normal file
29
.sisyphus/fix-tracker.md
Normal file
@ -0,0 +1,29 @@
|
||||
# 修复追踪文件
|
||||
|
||||
**分支**: openclaw
|
||||
**开始时间**: 2026-04-16
|
||||
**状态**: 重新执行
|
||||
|
||||
---
|
||||
|
||||
## 修复计划
|
||||
|
||||
### P0 - 阻断性问题
|
||||
|
||||
#### 1. 删除自定义 tool_interface.ts
|
||||
#### 2. 重构 graph_memory_tool.ts
|
||||
#### 3. 更新 plugin-entry.ts
|
||||
|
||||
### P2 - 最佳实践
|
||||
|
||||
#### 4. 重组 Skill 目录结构
|
||||
#### 5. 更新 bundled-skills 目录结构
|
||||
#### 6. 更新 openclaw.plugin.json
|
||||
#### 7. 更新 README.md / README_EN.md
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
### [进行中] P0: 核心修复
|
||||
|
||||
117
.sisyphus/plans/migration_plan.md
Normal file
117
.sisyphus/plans/migration_plan.md
Normal file
@ -0,0 +1,117 @@
|
||||
# TrulyMEM → WaterFlow 迁移计划
|
||||
|
||||
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
|
||||
>
|
||||
> 所有代码修改仅应用于 TrulyMEM 仓库的 `waterflow` 分支,作为 WaterFlow 框架的适配版本。
|
||||
|
||||
---
|
||||
|
||||
## 迁移目标
|
||||
|
||||
将 TrulyMEM 的图记忆能力从 Python 迁移到 TypeScript,适配 WaterFlow 框架。
|
||||
|
||||
**代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/` (waterflow 分支)
|
||||
|
||||
---
|
||||
|
||||
## 迁移策略
|
||||
|
||||
将图记忆能力作为 TypeScript 模块添加到 waterflow 分支:
|
||||
|
||||
| TrulyMEM (Python) | WaterFlow (TypeScript) |
|
||||
|-------------------|------------------------|
|
||||
| `EmbeddedGraphDB` | `GraphDatabase` |
|
||||
| `GraphMemoryClient` | `MemoryService` |
|
||||
| 12 个记忆工具 | `GraphMemoryTool` + Skills |
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Phase 1: 项目结构
|
||||
|
||||
- [x] 1.1 创建 `ts/` 目录 - TypeScript 项目
|
||||
- [x] 1.2 创建 `package.json` - 项目配置
|
||||
- [x] 1.3 创建 `tsconfig.json` - TypeScript 配置
|
||||
|
||||
### Phase 2: 核心库
|
||||
|
||||
- [x] 2.1 创建 `ts/src/runtime/core/graph_memory/types.ts` - 类型定义
|
||||
- [x] 2.2 创建 `ts/src/runtime/core/graph_memory/graph_database.ts` - 图数据库
|
||||
- [x] 2.3 创建 `ts/src/runtime/core/graph_memory/memory_service.ts` - 记忆服务
|
||||
- [x] 2.4 创建 `ts/src/runtime/core/graph_memory/index.ts` - 模块导出
|
||||
|
||||
### Phase 3: Tool 接口
|
||||
|
||||
- [x] 3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` - Tool 实现
|
||||
- [x] 3.2 注册 Tool (作为独立模块导出)
|
||||
|
||||
### Phase 4: Skill 定义
|
||||
|
||||
- [x] 4.1 创建 `ts/bundled-skills/graph_memory/SKILL.md` - 主 Skill
|
||||
- [x] 4.2 创建 `ts/bundled-skills/graph_memory/persona/SKILL.md` - Persona
|
||||
- [x] 4.3 创建 `ts/bundled-skills/graph_memory/task/SKILL.md` - 任务管理
|
||||
|
||||
### Phase 5: 验证
|
||||
|
||||
- [x] 5.1 编译 TypeScript - 无错误
|
||||
- [ ] 5.2 运行测试
|
||||
|
||||
---
|
||||
|
||||
## 目录结构 (在 TrulyMEM waterflow 分支)
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── ts/ # TypeScript 项目 (保留)
|
||||
│ ├── src/
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/ # 图记忆模块
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ ├── graph_database.ts
|
||||
│ │ │ └── memory_service.ts
|
||||
│ │ └── tools/
|
||||
│ │ └── builtin/
|
||||
│ │ └── graph_memory_tool.ts
|
||||
│ ├── bundled-skills/
|
||||
│ │ └── graph_memory/
|
||||
│ │ ├── SKILL.md
|
||||
│ │ ├── persona/SKILL.md
|
||||
│ │ └── task/SKILL.md
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
│
|
||||
├── docs/integration/waterflow-design.md # 迁移设计文档 (保留)
|
||||
│
|
||||
└── (其他文件迁移后删除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 迁移后清理
|
||||
|
||||
迁移完成后,waterflow 分支将删除以下文件:
|
||||
|
||||
- `core/` - Python 核心代码
|
||||
- `ui/` - Python UI 代码
|
||||
- `tests/` - Python 测试
|
||||
- `tools/` - Python 工具
|
||||
- `trulymem_entry.py` - Python 入口
|
||||
- `build/` - 构建脚本
|
||||
- `pic/` - 图片资源 (除图标外)
|
||||
- `requirements.txt` - Python 依赖
|
||||
- `TrulyMEM.spec` - Python 打包配置
|
||||
|
||||
只保留:
|
||||
- `ts/` - TypeScript 源码
|
||||
- `docs/integration/waterflow-design.md` - 迁移文档
|
||||
- `.gitignore`, `LICENSE`
|
||||
|
||||
---
|
||||
|
||||
## 工作追踪
|
||||
|
||||
工作进度记录在: `todo_progress.md`
|
||||
|
||||
每次修改文件前后请查看此文件并更新进度。
|
||||
572
.sisyphus/plans/plan.md
Normal file
572
.sisyphus/plans/plan.md
Normal file
@ -0,0 +1,572 @@
|
||||
# TrulyMEM → OpenClaw 重构修复计划
|
||||
|
||||
> **分支**: `openclaw`
|
||||
> **代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/`
|
||||
> **目标**: 将当前 TypeScript 实现完全适配 OpenClaw 框架的接口规范,并补全 main 分支缺失的功能
|
||||
|
||||
---
|
||||
|
||||
## 背景分析
|
||||
|
||||
### 当前状态
|
||||
|
||||
openclaw 分支已完成骨架迁移:类型定义、核心类(GraphDatabase/MemoryService/GraphMemoryTool)、Skill 定义文件均已就位,TypeScript 编译通过。
|
||||
|
||||
### 核心问题
|
||||
|
||||
| 类别 | 问题 | 严重度 |
|
||||
|---|---|---|
|
||||
| **接口不兼容** | Tool 注册使用自定义 interface,非 OpenClaw Plugin SDK | 🔴 致命 |
|
||||
| **Skill 格式错误** | 多行 YAML 嵌套结构(`arguments`、`allowed_tools`),OpenClaw 解析器只支持单行键值 | 🔴 致命 |
|
||||
| **无持久化** | in-memory Map,进程重启后记忆全部丢失 | 🔴 致命 |
|
||||
| **Schema 格式** | 手写 JSON Schema 对象,非 `@sinclair/typebox` | 🔴 致命 |
|
||||
| **返回值格式** | `JSON.stringify({success, data})` 非 `{ content: [{ type: "text", text }] }` | 🔴 致命 |
|
||||
| **无 Plugin 结构** | 缺少 `openclaw.plugin.json`、`package.json` 的 `openclaw` 字段 | 🔴 致命 |
|
||||
| **功能缺失** | depth 遍历、timeRange 过滤、supersede 模式、archive、cleanup、ToolLimiter | 🟡 中等 |
|
||||
| **无测试** | 全部删除,无新测试覆盖 | 🔴 严重 |
|
||||
| **命名不规范** | `graph_memory`(下划线)应为 kebab-case | 🟡 轻微 |
|
||||
|
||||
---
|
||||
|
||||
## OpenClaw 接口规范对照
|
||||
|
||||
### Tool 注册(官方 Plugin SDK)
|
||||
|
||||
```typescript
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "graph-memory",
|
||||
register(api) {
|
||||
api.registerTool({
|
||||
name: "graph_memory",
|
||||
description: "图记忆工具 - 让 AI 拥有真正的长期记忆能力",
|
||||
parameters: Type.Object({
|
||||
action: Type.String({ enum: ["recall", "commit", "purge", ...] }),
|
||||
params: Type.Object({ ... }),
|
||||
}),
|
||||
async execute(_id, params) {
|
||||
// 返回格式必须是:
|
||||
return { content: [{ type: "text", text: resultString }] };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### SKILL.md 格式(官方要求)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# 正文指令...
|
||||
```
|
||||
|
||||
**关键约束**:
|
||||
- `metadata` 必须是**单行 JSON 对象**
|
||||
- `description` 不能包含 `: `(冒号+空格),否则 YAML 解析**静默失败**
|
||||
- `name` 必须 kebab-case,与文件夹名匹配
|
||||
- **不支持** `arguments`、`allowed_tools`、`context: inline` 等多行 YAML 嵌套结构
|
||||
- 所有 frontmatter 键值必须是**单行**
|
||||
|
||||
### Plugin Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "graph-memory",
|
||||
"name": "Graph Memory",
|
||||
"description": "让 AI 拥有真正的长期记忆能力",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### package.json 扩展
|
||||
|
||||
```json
|
||||
{
|
||||
"openclaw": {
|
||||
"extensions": ["./dist/plugin-entry.js"],
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.3.24-beta.2",
|
||||
"minGatewayVersion": "2026.3.24-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.3.24-beta.2",
|
||||
"pluginSdkVersion": "2026.3.24-beta.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Phase 1: 项目结构改造为 OpenClaw Plugin(🔴 P0 - 阻塞)
|
||||
|
||||
#### 1.1 添加 OpenClaw Plugin SDK 依赖
|
||||
|
||||
- [ ] 1.1.1 `npm install @sinclair/typebox`
|
||||
- [ ] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段(extensions、compat、build)
|
||||
- [ ] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
|
||||
|
||||
#### 1.2 创建 Plugin Entry Point
|
||||
|
||||
- [ ] 1.2.1 创建 `ts/src/plugin-entry.ts`
|
||||
- 使用 `definePluginEntry` 包裹
|
||||
- 通过 `api.registerTool()` 注册 GraphMemoryTool
|
||||
- 工具名称: `graph_memory`
|
||||
- 描述: "图记忆工具 - 让 AI 拥有真正的长期记忆能力"
|
||||
- [ ] 1.2.2 确保 entry point 导出为 ESM 格式
|
||||
- [ ] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
|
||||
|
||||
#### 1.3 迁移 Tool Schema 到 TypeBox
|
||||
|
||||
- [ ] 1.3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_schema.ts`
|
||||
- 用 `Type.Object` 定义 action 参数
|
||||
- 用 `Type.Object` 定义 params 嵌套结构
|
||||
- 覆盖所有 10 个 action 的参数类型
|
||||
- [ ] 1.3.2 更新 `graph_memory_tool.ts` 的 `inputSchema` 字段为 TypeBox schema
|
||||
- [ ] 1.3.3 修改 `handler` 方法签名匹配 OpenClaw 的 `execute(_id, params)` 格式
|
||||
- [ ] 1.3.4 修改返回值格式为 `{ content: [{ type: "text", text: string }] }`
|
||||
|
||||
#### 1.4 更新 Tool Interface
|
||||
|
||||
- [ ] 1.4.1 更新 `ts/src/runtime/core/tools/tool_interface.ts`
|
||||
- 保持向后兼容(如其他模块引用)
|
||||
- 添加 OpenClaw 兼容的 `execute` 方法签名
|
||||
- 添加 `content` 返回类型定义
|
||||
|
||||
#### Phase 1 验收标准
|
||||
|
||||
- [ ] `npm run build` 编译通过
|
||||
- [ ] `openclaw.plugin.json` 格式正确
|
||||
- [ ] Plugin entry 使用 `definePluginEntry`
|
||||
- [ ] Tool 使用 `api.registerTool` 注册
|
||||
- [ ] Schema 使用 TypeBox
|
||||
- [ ] 返回值格式为 `{ content: [{ type: "text", text }] }`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: SKILL.md 格式修复(🔴 P0 - 阻塞)
|
||||
|
||||
#### 2.1 修复 `skills/graph_memory/SKILL.md`
|
||||
|
||||
- [ ] 2.1.1 移除 `when_to_use` 多行值(合并到 `description`)
|
||||
- [ ] 2.1.2 移除 `context: inline`(非 OpenClaw 标准字段)
|
||||
- [ ] 2.1.3 移除 `allowed_tools` 多行数组
|
||||
- [ ] 2.1.4 移除 `arguments` 多行嵌套结构
|
||||
- [ ] 2.1.5 `name` 改为 `graph-memory`(kebab-case)
|
||||
- [ ] 2.1.6 `description` 改为单行,不含 `: `
|
||||
- [ ] 2.1.7 添加 `metadata` 单行 JSON
|
||||
- [ ] 2.1.8 保留 `user_invocable: true`(改为 `user-invocable: true`,kebab-case)
|
||||
|
||||
**修复后格式**:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务。使用 recall 检索、commit 写入、purge 删除"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
```
|
||||
|
||||
#### 2.2 修复 `skills/graph_memory/persona/SKILL.md`
|
||||
|
||||
- [ ] 2.2.1 移除 `when_to_use`、`context`、`allowed_tools`、`arguments`
|
||||
- [ ] 2.2.2 `name` 改为 `graph-memory-persona`
|
||||
- [ ] 2.2.3 `description` 改为单行
|
||||
- [ ] 2.2.4 添加 `metadata` 单行 JSON
|
||||
|
||||
#### 2.3 修复 `skills/graph_memory/task/SKILL.md`
|
||||
|
||||
- [ ] 2.3.1 移除 `when_to_use`、`context`、`allowed_tools`、`arguments`
|
||||
- [ ] 2.3.2 `name` 改为 `graph-memory-task`
|
||||
- [ ] 2.3.3 `description` 改为单行
|
||||
- [ ] 2.3.4 添加 `metadata` 单行 JSON
|
||||
|
||||
#### 2.4 同步修复 `ts/bundled-skills/` 下三个文件
|
||||
|
||||
- [ ] 2.4.1 `ts/bundled-skills/graph_memory/SKILL.md`
|
||||
- [ ] 2.4.2 `ts/bundled-skills/graph_memory/persona/SKILL.md`
|
||||
- [ ] 2.4.3 `ts/bundled-skills/graph_memory/task/SKILL.md`
|
||||
|
||||
#### 2.5 重命名目录(kebab-case)
|
||||
|
||||
- [ ] 2.5.1 `skills/graph_memory/` → `skills/graph-memory/`
|
||||
- [ ] 2.5.2 `skills/graph_memory/persona/` → `skills/graph-memory/persona/`
|
||||
- [ ] 2.5.3 `skills/graph_memory/task/` → `skills/graph-memory/task/`
|
||||
- [ ] 2.5.4 `ts/bundled-skills/graph_memory/` → `ts/bundled-skills/graph-memory/`
|
||||
- [ ] 2.5.5 同步更新 README.md 和 README_EN.md 中的路径引用
|
||||
|
||||
#### Phase 2 验收标准
|
||||
|
||||
- [ ] 所有 SKILL.md 的 frontmatter 仅含单行键值
|
||||
- [ ] `name` 全部 kebab-case
|
||||
- [ ] `description` 不含 `: `
|
||||
- [ ] `metadata` 为单行 JSON 对象
|
||||
- [ ] 无 `arguments`、`allowed_tools`、`context: inline` 等非标准字段
|
||||
- [ ] 目录名与 `name` 一致
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: GraphDatabase 持久化(🔴 P0 - 核心价值)
|
||||
|
||||
#### 3.1 添加 SQLite 依赖
|
||||
|
||||
- [ ] 3.1.1 `npm install better-sqlite3`
|
||||
- [ ] 3.1.2 `npm install @types/better-sqlite3 --save-dev`
|
||||
|
||||
#### 3.2 重写 GraphDatabase
|
||||
|
||||
- [ ] 3.2.1 修改构造函数接受 `dbPath` 参数
|
||||
- [ ] 3.2.2 使用 `better-sqlite3` 创建/连接数据库
|
||||
- [ ] 3.2.3 创建实体表(entities):
|
||||
- `id TEXT PRIMARY KEY`
|
||||
- `name TEXT UNIQUE NOT NULL`
|
||||
- `type TEXT`
|
||||
- `mention_count INTEGER DEFAULT 1`
|
||||
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- [ ] 3.2.4 创建关系表(relations):
|
||||
- `id TEXT PRIMARY KEY`
|
||||
- `source_id TEXT NOT NULL`(外键 → entities.id)
|
||||
- `target_id TEXT NOT NULL`(外键 → entities.id)
|
||||
- `relation_type TEXT NOT NULL`
|
||||
- `confidence REAL DEFAULT 1.0`
|
||||
- `status TEXT DEFAULT 'active'`
|
||||
- `session_id TEXT`
|
||||
- `turn_id INTEGER`
|
||||
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `date_bucket TEXT`
|
||||
- `superseded_by INTEGER`
|
||||
- [ ] 3.2.5 创建索引:
|
||||
- `idx_entity_name ON entities(name)`
|
||||
- `idx_entity_type ON entities(type)`
|
||||
- `idx_relation_source ON relations(source_id)`
|
||||
- `idx_relation_target ON relations(target_id)`
|
||||
- `idx_relation_type ON relations(relation_type)`
|
||||
- `idx_relation_status ON relations(status)`
|
||||
- `idx_relation_session ON relations(session_id)`
|
||||
- `idx_relation_date ON relations(date_bucket)`
|
||||
|
||||
#### 3.3 实现 recall 的 depth 多跳遍历
|
||||
|
||||
- [ ] 3.3.1 实现 BFS/DFS 图遍历算法
|
||||
- [ ] 3.3.2 depth=1: 直接匹配关键词的实体及其关系
|
||||
- [ ] 3.3.3 depth=2: 扩展到相邻实体的关系
|
||||
- [ ] 3.3.4 depth=N: 递归扩展到 N 层
|
||||
- [ ] 3.3.5 限制最大 depth 为 5(防止爆炸)
|
||||
- [ ] 3.3.6 去重已访问实体
|
||||
|
||||
#### 3.4 实现 recall 的 timeRange 过滤
|
||||
|
||||
- [ ] 3.4.1 解析 `timeRange.days` 参数
|
||||
- [ ] 3.4.2 在 SQL 查询中添加 `created_at >= datetime('now', '-N days')` 条件
|
||||
- [ ] 3.4.3 支持 `timeRange.from` 和 `timeRange.to` 范围查询
|
||||
|
||||
#### 3.5 实现 purge 的 supersede 模式
|
||||
|
||||
- [ ] 3.5.1 当 `mode === 'supersede'` 时:
|
||||
- 标记旧关系为 `superseded`
|
||||
- 设置 `superseded_by` 指向新关系 ID
|
||||
- 创建新关系(使用 `newRelation` 参数)
|
||||
- [ ] 3.5.2 更新 `PurgeParams` 类型支持 `newRelation` 字段
|
||||
|
||||
#### 3.6 实现 memory_archive 归档功能
|
||||
|
||||
- [ ] 3.6.1 添加 `archive(days: number)` 方法
|
||||
- [ ] 3.6.2 将 N 天前的非活跃关系标记为 `archived`
|
||||
- [ ] 3.6.3 在 recall 中排除 `archived` 状态的关系(除非显式查询)
|
||||
|
||||
#### 3.7 实现 memory_cleanup 清理功能
|
||||
|
||||
- [ ] 3.7.1 添加 `cleanup(dryRun: boolean)` 方法
|
||||
- [ ] 3.7.2 物理删除 `status = 'deleted'` 超过 90 天的关系
|
||||
- [ ] 3.7.3 删除孤立节点(无任何关系连接的实体)
|
||||
- [ ] 3.7.4 `dryRun=true` 时只返回将被删除的内容
|
||||
|
||||
#### 3.8 修复 isEntityDeleted 逻辑
|
||||
|
||||
- [ ] 3.8.1 当前逻辑有误:只要有一个关系被删就算实体被删
|
||||
- [ ] 3.8.2 修正为:实体本身无 deleted 状态,通过关系状态判断
|
||||
- [ ] 3.8.3 或者:在 entities 表中添加 `status` 字段
|
||||
|
||||
#### Phase 3 验收标准
|
||||
|
||||
- [ ] 数据持久化:写入后重启进程,数据仍然存在
|
||||
- [ ] recall depth 遍历正确返回 N 层关系
|
||||
- [ ] timeRange 过滤按时间正确筛选
|
||||
- [ ] supersede 模式正确标记替代关系
|
||||
- [ ] archive 正确归档旧数据
|
||||
- [ ] cleanup 正确清理无效数据
|
||||
- [ ] 编译通过,无类型错误
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: ToolLimiter 迁移(🟡 P2 - 优化)
|
||||
|
||||
#### 4.1 创建 ToolLimiter
|
||||
|
||||
- [ ] 4.1.1 创建 `ts/src/runtime/core/tools/tool_limiter.ts`
|
||||
- [ ] 4.1.2 移植 Python `ToolLimiter` 逻辑:
|
||||
- `ToolLimits` 配置类
|
||||
- `ToolCallCount` 计数类
|
||||
- `_classify_tool` 分类方法
|
||||
- `can_call` 检查方法
|
||||
- `record_call` 记录方法
|
||||
- `reset` 重置方法
|
||||
- [ ] 4.1.3 默认限制值:
|
||||
- persona_query_max: 1
|
||||
- persona_update_max: 1
|
||||
- task_query_max: 4
|
||||
- task_update_max: 5
|
||||
- memory_query_max: 20
|
||||
- memory_update_max: 10
|
||||
|
||||
#### 4.2 集成到 GraphMemoryTool
|
||||
|
||||
- [ ] 4.2.1 在 Tool 构造函数中初始化 ToolLimiter
|
||||
- [ ] 4.2.2 在 `execute` 方法中调用 `can_call` 检查
|
||||
- [ ] 4.2.3 调用成功后调用 `record_call` 记录
|
||||
- [ ] 4.2.4 每轮对话结束时调用 `reset` 重置计数
|
||||
|
||||
#### Phase 4 验收标准
|
||||
|
||||
- [ ] ToolLimiter 正确分类所有工具调用
|
||||
- [ ] 超过限制时返回明确的拒绝消息
|
||||
- [ ] 每轮对话计数正确重置
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 测试重建(🔴 P1 - 质量保障)
|
||||
|
||||
#### 5.1 GraphDatabase 测试
|
||||
|
||||
- [ ] 5.1.1 创建 `ts/tests/runtime/core/graph_memory/graph_database.test.ts`
|
||||
- [ ] 5.1.2 commit 测试:创建实体和关系
|
||||
- [ ] 5.1.3 recall 测试:按关键词检索、按 seedEntities 检索
|
||||
- [ ] 5.1.4 recall depth 测试:1层、2层、3层遍历
|
||||
- [ ] 5.1.5 recall timeRange 测试:按时间范围过滤
|
||||
- [ ] 5.1.6 purge soft 测试:软删除
|
||||
- [ ] 5.1.7 purge hard 测试:硬删除
|
||||
- [ ] 5.1.8 purge supersede 测试:纠错替代
|
||||
- [ ] 5.1.9 introspect 测试:返回统计
|
||||
- [ ] 5.1.10 持久化测试:重启后数据保留
|
||||
- [ ] 5.1.11 archive 测试:归档旧数据
|
||||
- [ ] 5.1.12 cleanup 测试:清理无效数据
|
||||
- [ ] 5.1.13 sessionFilter 测试:按会话过滤
|
||||
- [ ] 5.1.14 并发测试:多线程安全
|
||||
- [ ] 5.1.15 边界测试:空查询、超长字符串
|
||||
|
||||
#### 5.2 MemoryService 测试
|
||||
|
||||
- [ ] 5.2.1 创建 `ts/tests/runtime/core/graph_memory/memory_service.test.ts`
|
||||
- [ ] 5.2.2 updatePersona merge 测试
|
||||
- [ ] 5.2.3 updatePersona replace 测试
|
||||
- [ ] 5.2.4 clearPersona 测试
|
||||
- [ ] 5.2.5 createTask 测试
|
||||
- [ ] 5.2.6 setTaskState 测试
|
||||
- [ ] 5.2.7 deleteTask 测试
|
||||
- [ ] 5.2.8 linkInfoToTask 测试
|
||||
- [ ] 5.2.9 setSessionId/getSessionId 测试
|
||||
- [ ] 5.2.10 任务状态转换测试(进行中→已暂停→进行中→已完成)
|
||||
- [ ] 5.2.11 人设属性合并测试
|
||||
- [ ] 5.2.12 错误处理测试:无效参数
|
||||
|
||||
#### 5.3 GraphMemoryTool 测试
|
||||
|
||||
- [ ] 5.3.1 创建 `ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
|
||||
- [ ] 5.3.2 metadata 测试:id、name、category
|
||||
- [ ] 5.3.3 recall action 测试
|
||||
- [ ] 5.3.4 commit action 测试
|
||||
- [ ] 5.3.5 purge action 测试
|
||||
- [ ] 5.3.6 introspect action 测试
|
||||
- [ ] 5.3.7 persona_update action 测试
|
||||
- [ ] 5.3.8 persona_clear action 测试
|
||||
- [ ] 5.3.9 task_create action 测试
|
||||
- [ ] 5.3.10 task_set_state action 测试
|
||||
- [ ] 5.3.11 task_delete action 测试
|
||||
- [ ] 5.3.12 task_link_info action 测试
|
||||
- [ ] 5.3.13 未知 action 错误处理测试
|
||||
- [ ] 5.3.14 返回值格式测试:`{ content: [{ type: "text", text }] }`
|
||||
- [ ] 5.3.15 ToolLimiter 集成测试
|
||||
|
||||
#### 5.4 Plugin Entry 集成测试
|
||||
|
||||
- [ ] 5.4.1 创建 `ts/tests/plugin-entry.test.ts`
|
||||
- [ ] 5.4.2 Plugin 注册测试
|
||||
- [ ] 5.4.3 Tool 注册测试
|
||||
- [ ] 5.4.4 Schema 验证测试
|
||||
|
||||
#### Phase 5 验收标准
|
||||
|
||||
- [ ] `npm test` 全部通过(50+ 用例)
|
||||
- [ ] 无跳过(skip)的测试
|
||||
- [ ] 覆盖率 > 80%
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: 文档更新(🟢 P3 - 收尾)
|
||||
|
||||
#### 6.1 更新 README.md
|
||||
|
||||
- [ ] 6.1.1 更新标题:TrulyMEM → OpenClaw Graph Memory Plugin
|
||||
- [ ] 6.1.2 更新安装方式:`openclaw plugins install`
|
||||
- [ ] 6.1.3 更新使用示例
|
||||
- [ ] 6.1.4 更新目录结构说明
|
||||
- [ ] 6.1.5 更新 API 文档(反映 TypeBox schema)
|
||||
|
||||
#### 6.2 更新 README_EN.md
|
||||
|
||||
- [ ] 6.2.1 同步中文 README 的所有更新
|
||||
- [ ] 6.2.2 确保英文表达准确
|
||||
|
||||
#### 6.3 更新迁移设计文档
|
||||
|
||||
- [ ] 6.3.1 更新 `docs/integration/waterflow-design.md`
|
||||
- [ ] 6.3.2 添加 OpenClaw 接口适配说明
|
||||
- [ ] 6.3.3 更新架构图中 Plugin SDK 部分
|
||||
|
||||
#### Phase 6 验收标准
|
||||
|
||||
- [ ] README.md 和 README_EN.md 内容一致
|
||||
- [ ] 安装步骤可执行
|
||||
- [ ] API 文档与实际代码一致
|
||||
|
||||
---
|
||||
|
||||
## 目标目录结构(重构后)
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── ts/
|
||||
│ ├── src/
|
||||
│ │ ├── plugin-entry.ts # [NEW] OpenClaw Plugin 入口
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ ├── graph_database.ts # [REWRITE] SQLite 持久化
|
||||
│ │ │ └── memory_service.ts
|
||||
│ │ └── tools/
|
||||
│ │ ├── builtin/
|
||||
│ │ │ ├── graph_memory_tool.ts # [UPDATE] OpenClaw 兼容
|
||||
│ │ │ └── graph_memory_schema.ts # [NEW] TypeBox Schema
|
||||
│ │ ├── tool_interface.ts # [UPDATE] 添加 execute 签名
|
||||
│ │ └── tool_limiter.ts # [NEW] 调用限制器
|
||||
│ ├── bundled-skills/
|
||||
│ │ └── graph-memory/ # [RENAMED] kebab-case
|
||||
│ │ ├── SKILL.md # [FIXED] 单行 frontmatter
|
||||
│ │ ├── persona/
|
||||
│ │ │ └── SKILL.md # [FIXED]
|
||||
│ │ └── task/
|
||||
│ │ └── SKILL.md # [FIXED]
|
||||
│ ├── tests/
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/
|
||||
│ │ │ ├── graph_database.test.ts # [NEW]
|
||||
│ │ │ └── memory_service.test.ts # [NEW]
|
||||
│ │ └── tools/builtin/
|
||||
│ │ └── graph_memory_tool.test.ts # [NEW]
|
||||
│ ├── package.json # [UPDATE] 添加 openclaw 字段
|
||||
│ ├── tsconfig.json
|
||||
│ └── openclaw.plugin.json # [NEW] Plugin Manifest
|
||||
├── skills/
|
||||
│ └── graph-memory/ # [RENAMED] kebab-case
|
||||
│ ├── SKILL.md # [FIXED]
|
||||
│ ├── persona/
|
||||
│ │ └── SKILL.md # [FIXED]
|
||||
│ └── task/
|
||||
│ └── SKILL.md # [FIXED]
|
||||
├── docs/integration/waterflow-design.md # [UPDATE]
|
||||
├── README.md # [UPDATE]
|
||||
├── README_EN.md # [UPDATE]
|
||||
├── .gitignore
|
||||
└── LICENSE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖变更
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"yaml": "^2.8.3",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"better-sqlite3": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.2",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序与并行策略
|
||||
|
||||
```
|
||||
Phase 1 (P0) ──────────────────────────────────────┐
|
||||
1.1 依赖 ─→ 1.2 Entry ─→ 1.3 Schema ─→ 1.4 Interface │
|
||||
├── 必须最先完成
|
||||
Phase 2 (P0) ──────────────────────────────────────┤ 否则无法在 OpenClaw 中运行
|
||||
2.1-2.3 SKILL.md 修复(可并行) │
|
||||
2.4 bundled-skills 同步 │
|
||||
2.5 目录重命名 │
|
||||
│
|
||||
Phase 3 (P0) ──────────────────────────────────────┤
|
||||
3.1 SQLite 依赖 │
|
||||
3.2 GraphDatabase 重写 │
|
||||
3.3-3.7 功能补全(可部分并行) │
|
||||
3.8 逻辑修复 │
|
||||
│
|
||||
Phase 4 (P2) ──────────────────────────────────────┤ 优化项,可延后
|
||||
4.1 ToolLimiter 创建 │
|
||||
4.2 集成到 Tool │
|
||||
│
|
||||
Phase 5 (P1) ──────────────────────────────────────┘ 在 Phase 1-3 完成后执行
|
||||
5.1-5.4 测试重建(可并行编写)
|
||||
|
||||
Phase 6 (P3) ────────────────────────────────────────── 最后执行,文档收尾
|
||||
6.1-6.3 文档更新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|---|---|---|
|
||||
| better-sqlite3 原生模块编译失败 | 阻塞 Phase 3 | 使用预编译二进制或回退到 sql.js |
|
||||
| OpenClaw Plugin SDK 版本不兼容 | 阻塞 Phase 1 | 锁定 `compat.pluginApi` 版本 |
|
||||
| SKILL.md 描述中的中文冒号 | 静默加载失败 | 所有 description 用双引号包裹 |
|
||||
| SQLite 并发写入冲突 | 数据损坏 | 使用 WAL 模式 + 连接池 |
|
||||
| depth 遍历性能问题 | 响应缓慢 | 限制最大 depth=5,结果上限 100 |
|
||||
|
||||
---
|
||||
|
||||
## 验收总标准
|
||||
|
||||
- [ ] Phase 1-6 全部完成
|
||||
- [ ] `npm run build` 编译通过,无错误无警告
|
||||
- [ ] `npm test` 全部通过(50+ 用例)
|
||||
- [ ] 作为 OpenClaw Plugin 可安装、可加载、可调用
|
||||
- [ ] 数据持久化:写入后重启进程,数据仍然存在
|
||||
- [ ] 所有 main 分支的核心功能均已实现
|
||||
- [ ] SKILL.md 通过 OpenClaw 的 `openclaw skills check` 验证
|
||||
95
.sisyphus/plans/progress.md
Normal file
95
.sisyphus/plans/progress.md
Normal file
@ -0,0 +1,95 @@
|
||||
# 重构执行进度追踪
|
||||
|
||||
> 最后更新: 2026-04-16 15:00
|
||||
> 当前分支: openclaw
|
||||
|
||||
## Phase 1: 项目结构改造为 OpenClaw Plugin(🔴 P0)
|
||||
|
||||
### 1.1 添加 OpenClaw Plugin SDK 依赖
|
||||
- [x] 1.1.1 `npm install @sinclair/typebox better-sqlite3`
|
||||
- [x] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段
|
||||
- [x] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
|
||||
|
||||
### 1.2 创建 Plugin Entry Point
|
||||
- [x] 1.2.1 创建 `ts/src/plugin-entry.ts`
|
||||
- [x] 1.2.2 确保 entry point 导出为 ESM 格式
|
||||
- [x] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
|
||||
|
||||
### 1.3 迁移 Tool Schema 到 TypeBox
|
||||
- [x] 1.3.1 添加 TypeBox schema 到 plugin-entry.ts
|
||||
- [x] 1.3.2 更新 `graph_memory_tool.ts` 的 `inputSchema` 字段(添加 newRelation, days, dry_run)
|
||||
- [x] 1.3.3 添加 `execute(_id, params)` 方法匹配 OpenClaw 签名
|
||||
- [x] 1.3.4 返回值格式为 `{ content: [{ type: "text", text }] }`
|
||||
|
||||
### 1.4 更新 Tool Interface
|
||||
- [x] 1.4.1 添加 `OpenClawToolResult` 类型到 graph_memory_tool.ts
|
||||
|
||||
## Phase 2: SKILL.md 格式修复(🔴 P0)
|
||||
|
||||
### 2.1 修复 skills/graph-memory/SKILL.md
|
||||
- [x] 2.1.1 移除多行嵌套结构
|
||||
- [x] 2.1.2 name 改为 kebab-case
|
||||
- [x] 2.1.3 description 改为单行
|
||||
- [x] 2.1.4 添加 metadata 单行 JSON
|
||||
|
||||
### 2.2 修复 skills/graph-memory/persona/SKILL.md
|
||||
- [x] 2.2.1 同上
|
||||
|
||||
### 2.3 修复 skills/graph-memory/task/SKILL.md
|
||||
- [x] 2.3.1 同上
|
||||
|
||||
### 2.4 同步修复 bundled-skills
|
||||
- [x] 2.4.1 ts/bundled-skills/graph-memory/SKILL.md
|
||||
- [x] 2.4.2 ts/bundled-skills/graph-memory/persona/SKILL.md
|
||||
- [x] 2.4.3 ts/bundled-skills/graph-memory/task/SKILL.md
|
||||
|
||||
### 2.5 重命名目录
|
||||
- [x] 2.5.1 skills/graph_memory/ → skills/graph-memory/
|
||||
- [x] 2.5.2 ts/bundled-skills/graph_memory/ → ts/bundled-skills/graph-memory/
|
||||
|
||||
## Phase 3: GraphDatabase 持久化(🔴 P0)
|
||||
|
||||
### 3.1 添加 SQLite 依赖
|
||||
- [ ] 3.1.1 npm install better-sqlite3
|
||||
- [ ] 3.1.2 npm install @types/better-sqlite3
|
||||
|
||||
### 3.2 重写 GraphDatabase
|
||||
- [x] 3.2.1 修改构造函数接受 dbPath
|
||||
- [x] 3.2.2 使用 better-sqlite3 创建/连接数据库
|
||||
- [x] 3.2.3 创建实体表
|
||||
- [x] 3.2.4 创建关系表
|
||||
- [x] 3.2.5 创建索引
|
||||
|
||||
### 3.3-3.7 功能补全
|
||||
- [x] 3.3 depth 多跳遍历 (BFS)
|
||||
- [x] 3.4 timeRange 过滤
|
||||
- [x] 3.5 supersede 模式
|
||||
- [x] 3.6 archive 归档
|
||||
- [x] 3.7 cleanup 清理
|
||||
|
||||
### 3.8 修复逻辑
|
||||
- [x] 3.8.1 修复 isEntityDeleted 逻辑 (已移除,用 SQL 替代)
|
||||
|
||||
## Phase 4: ToolLimiter(🟡 P2)
|
||||
|
||||
- [ ] 4.1 创建 ToolLimiter
|
||||
- [ ] 4.2 集成到 GraphMemoryTool
|
||||
|
||||
## Phase 5: 测试重建(🔴 P1)
|
||||
|
||||
- [ ] 5.1 GraphDatabase 测试 (15+)
|
||||
- [ ] 5.2 MemoryService 测试 (12+)
|
||||
- [ ] 5.3 GraphMemoryTool 测试 (15+)
|
||||
- [ ] 5.4 Plugin Entry 集成测试
|
||||
|
||||
## Phase 6: 文档更新(🟢 P3)
|
||||
|
||||
- [ ] 6.1 更新 README.md
|
||||
- [ ] 6.2 更新 README_EN.md
|
||||
- [ ] 6.3 更新迁移设计文档
|
||||
|
||||
## 最终验收
|
||||
|
||||
- [ ] npm run build 编译通过
|
||||
- [ ] npm test 全部通过
|
||||
- [ ] 推送到远程 openclaw 分支
|
||||
81
.sisyphus/plans/todo_progress.md
Normal file
81
.sisyphus/plans/todo_progress.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 迁移工作进度追踪
|
||||
|
||||
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
- **开始时间**: 2026-04-15
|
||||
- **当前任务**: 迁移完成,等待测试
|
||||
- **最后更新**: 2026-04-15
|
||||
- **状态**: TypeScript 编译通过
|
||||
|
||||
---
|
||||
|
||||
## Phase 完成状态
|
||||
|
||||
### Phase 1: 项目结构
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 1.1 ts/ 目录 | ✅ done | |
|
||||
| 1.2 package.json | ✅ done | |
|
||||
| 1.3 tsconfig.json | ✅ done | |
|
||||
|
||||
### Phase 2: 核心库
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 2.1 types.ts | ✅ done | |
|
||||
| 2.2 graph_database.ts | ✅ done | |
|
||||
| 2.3 memory_service.ts | ✅ done | |
|
||||
| 2.4 index.ts | ✅ done | |
|
||||
|
||||
### Phase 3: Tool 接口
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 3.1 graph_memory_tool.ts | ✅ done | |
|
||||
| 3.2 tool_interface.ts | ✅ done | |
|
||||
|
||||
### Phase 4: Skill 定义
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 4.1 SKILL.md (主) | ✅ done | |
|
||||
| 4.2 persona/SKILL.md | ✅ done | |
|
||||
| 4.3 task/SKILL.md | ✅ done | |
|
||||
|
||||
### Phase 5: 验证
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 5.1 编译 | ✅ done | TypeScript 编译通过 |
|
||||
| 5.2 测试 | ⏳ pending | |
|
||||
|
||||
---
|
||||
|
||||
## 创建的文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ts/package.json` | 项目配置 |
|
||||
| `ts/tsconfig.json` | TypeScript 配置 |
|
||||
| `ts/src/runtime/core/graph_memory/types.ts` | 类型定义 |
|
||||
| `ts/src/runtime/core/graph_memory/graph_database.ts` | 图数据库 |
|
||||
| `ts/src/runtime/core/graph_memory/memory_service.ts` | 记忆服务 |
|
||||
| `ts/src/runtime/core/graph_memory/index.ts` | 模块导出 |
|
||||
| `ts/src/runtime/core/tools/tool_interface.ts` | Tool 接口 |
|
||||
| `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` | GraphMemory Tool |
|
||||
| `ts/bundled-skills/graph_memory/SKILL.md` | 主 Skill |
|
||||
| `ts/bundled-skills/graph_memory/persona/SKILL.md` | Persona Skill |
|
||||
| `ts/bundled-skills/graph_memory/task/SKILL.md` | Task Skill |
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
- 每次修改文件前后更新此文件
|
||||
- 记录每次修改的文件和操作
|
||||
- 方便意外终止后恢复任务
|
||||
370
README.md
370
README.md
@ -1,109 +1,327 @@
|
||||
# TrulyMEM - TrueHumanMEM
|
||||
# TrulyMEM - AI 主要长期记忆系统
|
||||
|
||||
<p align="center">
|
||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
||||
</p>
|
||||
让 AI 拥有真正的长期记忆能力 - OpenClaw 框架插件版
|
||||
|
||||
> **📜 开源协议**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
||||
> 本项目自由开源,可自由使用、修改和分发,但修改后的作品必须以相同许可证发布。
|
||||
|
||||
> **English**: [Switch to English version](./README_EN.md)
|
||||
|
||||
**让 AI 拥有自知、可塑、有分寸感的长期记忆**
|
||||
|
||||
*The More Human Choice.*
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/downloads/)
|
||||
[]()
|
||||
[English Version](./README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## 故事的开头
|
||||
## 定位声明
|
||||
|
||||
行业普遍认为,LLM 海量参数让其涌现了智能。但这个智能是「死的」——它不会真的记住,也不理解「记住」的概念。它输出的一切,都是当前输入的全部文本经历无数次前向传播计算出的概率最优解。LLM 不会因为某次对话意识到错误而去修正权重,也无法因此针对模型进行一次反向传播。它的意识是被冻结的,展现出的智能只是冻结的意识的回响。
|
||||
**TrulyMEM 是 AI 的主要长期记忆系统。**
|
||||
|
||||
现在的所谓记忆系统,只是将记忆外化,让「系统」去替 LLM 记住。或者就是粗暴地将一切上下文文本丢给 LLM。这就是对模型输入的浪费。
|
||||
与 OpenClaw 内置的 memory-core 并存运行:
|
||||
- **memory-core**:负责 session transcripts 和对话历史管理
|
||||
- **GraphMemory (TrulyMEM)**:负责**结构化长期记忆**——知识图谱、人设、任务、语义搜索
|
||||
|
||||
**TrulyMEM 想,既然 LLM 无法实时纠正模型权重,为什么不把记忆权交还给 LLM 呢?**
|
||||
|
||||
我们提供一系列机制,让 LLM 决定它要记住什么、遗忘,什么是重点、什么是糟粕。LLM 推理的过程,就是思考的过程,也是回忆的过程。完全摒弃传统的 messages 数组上下文,将全部记忆以**三元组(图)**的形式保存在图数据库中。在 LLM 思考时,可以按照图数据库的链接自主跳转、联想相关关系,让 LLM 自然地实现联想与回忆。
|
||||
|
||||
赋予 LLM 真正的记忆。
|
||||
AI 在对话中应**优先**将重要信息(偏好、决策、任务、知识点)写入图数据库,而非依赖 message 历史。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
## AI 自行安装指南
|
||||
|
||||
### 方式一:打包后的可执行文件
|
||||
> 本节面向 AI Agent,按步骤指导你自主安装此插件到 OpenClaw 框架。
|
||||
|
||||
### 步骤 1:克隆仓库
|
||||
|
||||
```bash
|
||||
# Windows: TrulyMEM.exe
|
||||
# Linux/macOS: TrulyMEM
|
||||
chmod +x TrulyMEM
|
||||
./TrulyMEM
|
||||
```
|
||||
|
||||
### 方式二:从源码运行
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
python trulymem_entry.py
|
||||
git checkout openclaw
|
||||
```
|
||||
|
||||
### 配置
|
||||
### 步骤 2:编译 TypeScript 插件
|
||||
|
||||
1. 按 **F2** 展开侧边栏
|
||||
2. 输入 **API Key**(支持 DeepSeek、OpenAI 等兼容 API)
|
||||
3. 按 **Enter** 保存配置
|
||||
4. 开始对话!
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
编译成功后,`dist/` 目录会输出编译后的 JavaScript 文件。
|
||||
|
||||
### 步骤 3:安装 Skill(让 AI 能调用记忆工具)
|
||||
|
||||
将 `skills/` 目录下的 3 个 Skill 复制到 OpenClaw 的 Skill 目录。优先级:`<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
|
||||
|
||||
```bash
|
||||
# 方式 A:工作空间级(推荐,仅当前项目可用)
|
||||
cp -r skills/graph-memory <你的项目目录>/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona <你的项目目录>/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task <你的项目目录>/skills/graph-memory-task
|
||||
|
||||
# 方式 B:用户级(所有项目可用)
|
||||
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
|
||||
```
|
||||
|
||||
### 步骤 4:安装 Plugin(让 OpenClaw 运行时加载 Tool)
|
||||
|
||||
将编译后的 `ts/` 目录复制到 OpenClaw 的全局扩展目录:
|
||||
|
||||
```bash
|
||||
# 复制到 OpenClaw extensions 目录
|
||||
mkdir -p ~/.openclaw/extensions
|
||||
cp -r ts ~/.openclaw/extensions/graph-memory
|
||||
```
|
||||
|
||||
然后启用插件:
|
||||
|
||||
```bash
|
||||
# 启用插件
|
||||
openclaw plugins enable graph-memory
|
||||
|
||||
# 重启 Gateway(如果使用 systemd 托管)
|
||||
systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
验证插件已加载:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
# 应显示 graph-memory 为 loaded 状态
|
||||
```
|
||||
|
||||
**注意**:插件与 memory-core **并存运行**,不会禁用其他内存插件。memory-core 继续管理对话历史,GraphMemory 提供结构化长期记忆。
|
||||
|
||||
### 步骤 5:验证安装
|
||||
|
||||
启动 OpenClaw 后,发送以下消息测试:
|
||||
|
||||
```
|
||||
请记住:我喜欢编程,正在学习 TypeScript
|
||||
```
|
||||
|
||||
AI 应调用 `graph_memory` 工具的 `commit` action 并返回成功确认。
|
||||
|
||||
然后测试检索:
|
||||
|
||||
```
|
||||
我之前说过喜欢什么?
|
||||
```
|
||||
|
||||
AI 应调用 `recall` action 并返回之前写入的信息。
|
||||
|
||||
### 步骤 6:确认 Skill 已加载
|
||||
|
||||
在 OpenClaw 中执行:
|
||||
|
||||
```
|
||||
/skills
|
||||
```
|
||||
|
||||
你应该能看到以下 3 个 Skill:
|
||||
- `graph-memory` - 记忆 CRUD + 语义搜索 + 上下文压缩
|
||||
- `graph-memory-persona` - 人设管理
|
||||
- `graph-memory-task` - 任务管理
|
||||
|
||||
---
|
||||
|
||||
## 文档索引
|
||||
## 简介
|
||||
|
||||
详细技术文档请参阅 [docs/zh/](docs/zh/) 目录:
|
||||
本项目是 OpenClaw 的图记忆插件,基于 SQLite 实现持久化图数据库。
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [docs/zh/architecture.md](docs/zh/architecture.md) | 系统架构和技术设计 |
|
||||
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 完整启动指南与配置说明 |
|
||||
| [docs/zh/memory.md](docs/zh/memory.md) | 内部记忆工作机制 |
|
||||
| [docs/zh/persona.md](docs/zh/persona.md) | 人设图机制 |
|
||||
| [docs/zh/working_memory.md](docs/zh/working_memory.md) | 连续性任务处理机制 |
|
||||
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口(供扩展开发) |
|
||||
| [docs/zh/prompts.md](docs/zh/prompts.md) | 提示词管理模块 |
|
||||
**设计理念:AI 的主要长期记忆系统**
|
||||
|
||||
本插件作为 AI 的**主要长期记忆存储**,与 memory-core 并存运行:
|
||||
- **memory-core 管理对话历史**:session transcripts 和历史消息由 memory-core 自动管理
|
||||
- **GraphMemory 管理结构化记忆**:重要事实、人设、任务、知识图谱由 AI 主动写入图数据库
|
||||
- **AI 优先使用图记忆**:对于持久信息,AI 应优先写入图数据库而非依赖 message 上下文
|
||||
|
||||
**核心功能:**
|
||||
|
||||
### 基础记忆操作
|
||||
- **recall**: 检索记忆(支持关键词、种子实体、多跳遍历、时间过滤)
|
||||
- **commit**: 写入记忆(三元组批量写入)
|
||||
- **purge**: 删除记忆(软删除/硬删除/纠错替代)
|
||||
- **introspect**: 查看记忆状态
|
||||
- **archive**: 归档旧记忆
|
||||
- **cleanup**: 清理无效数据
|
||||
|
||||
### 高级功能(P2)
|
||||
- **memory_search**: 语义搜索——基于本地 ONNX embedding 的向量相似度搜索
|
||||
- **memory_get**: 精确读取——按路径读取记忆文件内容片段
|
||||
- **context_rewrite**: 上下文压缩——将长对话历史压缩为关键记忆节点
|
||||
- **working_memory_chain**: 工作记忆链——获取当前会话的活跃关系链
|
||||
- **task_node_create/get_recent/get_chain**: 任务节点——创建和追踪连续性任务节点
|
||||
|
||||
### 人设与任务管理
|
||||
- **persona_update/clear**: 人设管理(AI 应主动查询人设指导行为)
|
||||
- **task_create/set_state/delete/link_info**: 任务管理(AI 应主动追踪任务状态)
|
||||
|
||||
---
|
||||
|
||||
## 贡献
|
||||
## 安装
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
### 方式一:作为 OpenClaw 插件安装
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
将插件目录添加到 OpenClaw 配置中,或使用 `openclaw plugins install` 安装。
|
||||
|
||||
### 方式二:作为 Skill 安装(推荐)
|
||||
|
||||
将 `skills/` 目录复制到 OpenClaw 的 Skill 目录:
|
||||
|
||||
```bash
|
||||
cp -r skills/graph-memory ~/.agents/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ts/
|
||||
├── src/
|
||||
│ ├── plugin-entry.ts # OpenClaw Plugin 入口
|
||||
│ └── runtime/core/
|
||||
│ ├── graph_memory/
|
||||
│ │ ├── types.ts # 类型定义
|
||||
│ │ ├── graph_database.ts # SQLite 图数据库
|
||||
│ │ ├── memory_service.ts # 记忆服务
|
||||
│ │ ├── semantic_search.ts # 语义搜索引擎(P2)
|
||||
│ │ └── index.ts # 模块导出
|
||||
│ └── tools/
|
||||
│ ├── builtin/
|
||||
│ │ └── graph_memory_tool.ts # Tool 实现
|
||||
│ └── tool_limiter.ts # 调用限制器
|
||||
├── bundled-skills/
|
||||
│ ├── graph-memory/ # 内置 Skill 定义
|
||||
│ │ └── SKILL.md
|
||||
│ ├── graph-memory-persona/
|
||||
│ │ └── SKILL.md
|
||||
│ └── graph-memory-task/
|
||||
│ └── SKILL.md
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── openclaw.plugin.json # Plugin Manifest
|
||||
|
||||
skills/ # 独立 Skill 定义
|
||||
├── graph-memory/
|
||||
│ └── SKILL.md
|
||||
├── graph-memory-persona/
|
||||
│ └── SKILL.md
|
||||
└── graph-memory-task/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 OpenClaw 中使用
|
||||
|
||||
### 作为 Plugin
|
||||
|
||||
插件入口导出符合 OpenClaw SDK 规范的对象:
|
||||
|
||||
```typescript
|
||||
// plugin-entry.ts 导出格式
|
||||
export default {
|
||||
id: 'graph-memory',
|
||||
name: 'Graph Memory',
|
||||
description: '让 AI 拥有真正的长期记忆能力',
|
||||
register(api) {
|
||||
api.registerTool(tool);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
OpenClaw 加载后会自动调用 `register(api)` 注册工具。
|
||||
|
||||
### 作为独立模块
|
||||
|
||||
```typescript
|
||||
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
|
||||
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
|
||||
|
||||
// 写入记忆
|
||||
const result = await tool.execute('call-1', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// 语义搜索
|
||||
const searchResult = await tool.execute('call-2', {
|
||||
action: 'memory_search',
|
||||
params: { query: '编程相关', limit: 5 }
|
||||
});
|
||||
|
||||
// 检索记忆
|
||||
const recallResult = await tool.execute('call-3', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: '用户 编程' }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| `recall` | 检索记忆 | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
|
||||
| `commit` | 写入记忆 | `triplets`, `sessionId`, `turnId` |
|
||||
| `purge` | 删除记忆 | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
|
||||
| `introspect` | 查看状态 | - |
|
||||
| `archive` | 归档旧记忆 | `days` (默认 30) |
|
||||
| `cleanup` | 清理无效数据 | `dry_run` (默认 true) |
|
||||
| **语义搜索** | | |
|
||||
| `memory_search` | 语义向量搜索 | `query`, `limit`, `corpus` |
|
||||
| `memory_get` | 精确读取记忆文件 | `path`, `fromLine`, `lines` |
|
||||
| **上下文压缩** | | |
|
||||
| `context_rewrite` | 压缩长对话为记忆节点 | `context`, `maxEntities`, `summary` |
|
||||
| `working_memory_chain` | 获取当前会话活跃关系链 | `maxDepth`, `recentOnly` |
|
||||
| **任务节点** | | |
|
||||
| `task_node_create` | 创建任务节点 | `session_id`, `turn_id`, `summary`, `key_facts` |
|
||||
| `task_node_get_recent` | 获取最近任务节点 | `session_id`, `limit` |
|
||||
| `task_node_get_chain` | 获取任务链 | `session_id`, `from_node_id` |
|
||||
| **人设管理** | | |
|
||||
| `persona_update` | 更新人设 | `attributes`, `mode` (merge/replace) |
|
||||
| `persona_clear` | 清除人设 | `confirm` |
|
||||
| **任务管理** | | |
|
||||
| `task_create` | 创建任务 | `task_id`, `description`, `info_nodes` |
|
||||
| `task_set_state` | 设置状态 | `task_id`, `state` |
|
||||
| `task_delete` | 删除任务 | `task_id` |
|
||||
| `task_link_info` | 关联信息 | `task_id`, `info_node` |
|
||||
|
||||
---
|
||||
|
||||
## Skill 列表
|
||||
|
||||
| Skill 名称 | 功能 | 使用场景 |
|
||||
|------------|------|----------|
|
||||
| `graph-memory` | 记忆 CRUD + 语义搜索 + 上下文压缩 | 读取/写入/删除记忆,语义搜索,压缩历史 |
|
||||
| `graph-memory-persona` | 人设管理 | 设置 AI 角色性格,AI 主动查询人设 |
|
||||
| `graph-memory-task` | 任务管理 | 创建/更新长期任务,AI 主动追踪任务 |
|
||||
|
||||
---
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build # 编译
|
||||
npm test # 运行测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 **GNU General Public License v3.0 (GPLv3)** 许可证开源。
|
||||
详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
---
|
||||
|
||||
## 特别鸣谢
|
||||
|
||||
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — 学术指导
|
||||
- [逝水秋生白](https://atomgit.com/cenber) — 架构支持
|
||||
- anzhitinglan — 测试资源支持
|
||||
- 崔莉萍老师 — 理论指导
|
||||
- Annie — 专业指导
|
||||
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
|
||||
[GNU General Public License v3.0 (GPLv3)](LICENSE)
|
||||
|
||||
314
README_EN.md
314
README_EN.md
@ -1,109 +1,261 @@
|
||||
# TrulyMEM - TrueHumanMEM
|
||||
# TrulyMEM - OpenClaw Graph Memory Plugin
|
||||
|
||||
<p align="center">
|
||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
||||
</p>
|
||||
Give AI true long-term memory capability - OpenClaw framework plugin version
|
||||
|
||||
> **📜 License**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
||||
> This project is free and open source. You are free to use, modify, and distribute, but modified works must be distributed under the same license.
|
||||
|
||||
> **中文**: [切换到中文版](./README.md)
|
||||
|
||||
**Give AI self-awareness, plasticity, and a sense of proportion in long-term memory**
|
||||
|
||||
*The More Human Choice.*
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/downloads/)
|
||||
[]()
|
||||
[中文版本](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## The Story
|
||||
## AI Self-Installation Guide
|
||||
|
||||
Industry believes that LLMs' massive parameters give them emergent intelligence. But this intelligence is "dead" — it cannot truly remember, nor understand the concept of "remembering". Everything it outputs is the probabilistic optimal solution calculated through countless forward passes on the current input text. The LLM cannot correct its weights based on errors in a conversation, nor perform a backward pass. Its consciousness is frozen — what appears as intelligence is merely the echo of this frozen consciousness.
|
||||
> This section is for AI Agents. Follow these steps to install this plugin into your OpenClaw framework.
|
||||
|
||||
Current "memory systems" merely externalize memory, letting the "system" remember for the LLM. Or they dump all context text to the LLM. This is a waste of the model's limited input context.
|
||||
|
||||
**TrulyMEM asks: since the LLM cannot correct model weights in real-time, why not give the memory authority back to the LLM?**
|
||||
|
||||
We provide a series of mechanisms for the LLM to decide what to remember, what to forget, what's important, what's trivial. The LLM's reasoning process is also its thinking and recalling process. Abandoning the traditional messages array context, all memories are stored as **triplets (graph)** in the graph database. When the LLM thinks, it can autonomously jump through graph links to associate related relationships, enabling natural association and recall.
|
||||
|
||||
Give the LLM true memory.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Method 1: Run Packaged Executable
|
||||
### Step 1: Clone the repository
|
||||
|
||||
```bash
|
||||
# Windows: TrulyMEM.exe
|
||||
# Linux/macOS: TrulyMEM
|
||||
chmod +x TrulyMEM
|
||||
./TrulyMEM
|
||||
```
|
||||
|
||||
### Method 2: Run from Source
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
python trulymem_entry.py
|
||||
git checkout openclaw
|
||||
```
|
||||
|
||||
### Configuration
|
||||
### Step 2: Build the TypeScript plugin
|
||||
|
||||
1. Press **F2** to expand sidebar
|
||||
2. Enter **API Key** (supports DeepSeek, OpenAI, etc.)
|
||||
3. Press **Enter** to save
|
||||
4. Start chatting!
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
After successful build, the `dist/` directory contains compiled JavaScript files.
|
||||
|
||||
### Step 3: Install Skills (enables AI to call memory tools)
|
||||
|
||||
Copy the 3 Skills from `skills/` to OpenClaw's skill directory. Priority: `<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
|
||||
|
||||
```bash
|
||||
# Option A: Workspace-level (recommended, current project only)
|
||||
cp -r skills/graph-memory <your-project-dir>/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona <your-project-dir>/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task <your-project-dir>/skills/graph-memory-task
|
||||
|
||||
# Option B: User-level (available for all projects)
|
||||
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
|
||||
```
|
||||
|
||||
### Step 4: Install Plugin (enables OpenClaw runtime to load the Tool)
|
||||
|
||||
Load the `ts/` directory as an OpenClaw plugin. Add to `openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
"plugins": {
|
||||
"allow": ["@trulymem/openclaw-graph-memory"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or symlink the compiled `ts/dist/` directory to OpenClaw's plugin directory:
|
||||
|
||||
```bash
|
||||
ln -s <path-to-ts/dist> ~/.openclaw/plugins/graph-memory
|
||||
```
|
||||
|
||||
### Step 5: Verify installation
|
||||
|
||||
After starting OpenClaw, send this message to test:
|
||||
|
||||
```
|
||||
Please remember: I like programming and I'm learning TypeScript
|
||||
```
|
||||
|
||||
The AI should call the `graph_memory` tool's `commit` action and return a success confirmation.
|
||||
|
||||
Then test recall:
|
||||
|
||||
```
|
||||
What did I say I like?
|
||||
```
|
||||
|
||||
The AI should call the `recall` action and return the previously stored information.
|
||||
|
||||
### Step 6: Confirm Skills are loaded
|
||||
|
||||
In OpenClaw, run:
|
||||
|
||||
```
|
||||
/skills
|
||||
```
|
||||
|
||||
You should see these 3 Skills:
|
||||
- `graph-memory` - Memory CRUD
|
||||
- `graph-memory-persona` - Persona management
|
||||
- `graph-memory-task` - Task management
|
||||
|
||||
---
|
||||
|
||||
## Documentation Index
|
||||
## Introduction
|
||||
|
||||
Detailed technical documentation in the [docs/en/](docs/en/) directory:
|
||||
This project is an OpenClaw plugin for graph-based memory with SQLite persistence.
|
||||
|
||||
| Document | Content |
|
||||
|----------|---------|
|
||||
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and technical design |
|
||||
| [docs/en/quick_start.md](docs/en/quick_start.md) | Complete startup guide and configuration |
|
||||
| [docs/en/memory.md](docs/en/memory.md) | Internal memory working mechanism |
|
||||
| [docs/en/persona.md](docs/en/persona.md) | Persona Graph mechanism |
|
||||
| [docs/en/working_memory.md](docs/en/working_memory.md) | Continuous task handling mechanism |
|
||||
| [docs/en/api.md](docs/en/api.md) | BackendServer API (for extension development) |
|
||||
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management module |
|
||||
**Core Features:**
|
||||
- **recall**: Retrieve memories (keyword, seed entities, multi-hop traversal, time filtering)
|
||||
- **commit**: Write memories (batch triplet writes)
|
||||
- **purge**: Delete memories (soft/hard/supersede modes)
|
||||
- **introspect**: View memory statistics
|
||||
- **archive**: Archive old memories
|
||||
- **cleanup**: Clean up invalid data
|
||||
- **persona_update/clear**: Persona management
|
||||
- **task_create/set_state/delete/link_info**: Task management
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
## Installation
|
||||
|
||||
Welcome to submit Issues and Pull Requests!
|
||||
### Method 1: As OpenClaw Plugin
|
||||
|
||||
1. Fork this repository
|
||||
2. Create feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to branch (`git push origin feature/AmazingFeature`)
|
||||
5. Create Pull Request
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Add the plugin directory to your OpenClaw config, or use `openclaw plugins install`.
|
||||
|
||||
### Method 2: As Skill (Recommended)
|
||||
|
||||
Copy the `skills/` directory to OpenClaw's skill directory:
|
||||
|
||||
```bash
|
||||
cp -r skills/graph-memory ~/.agents/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
ts/
|
||||
├── src/
|
||||
│ ├── plugin-entry.ts # OpenClaw Plugin entry point
|
||||
│ └── runtime/core/
|
||||
│ ├── graph_memory/
|
||||
│ │ ├── types.ts # Type definitions
|
||||
│ │ ├── graph_database.ts # SQLite graph database
|
||||
│ │ ├── memory_service.ts # Memory service
|
||||
│ │ └── index.ts # Module exports
|
||||
│ └── tools/
|
||||
│ ├── builtin/
|
||||
│ │ └── graph_memory_tool.ts # Tool implementation
|
||||
│ └── tool_limiter.ts # Call rate limiter
|
||||
├── bundled-skills/
|
||||
│ ├── graph-memory/ # Bundled Skill definitions
|
||||
│ │ └── SKILL.md
|
||||
│ ├── graph-memory-persona/
|
||||
│ │ └── SKILL.md
|
||||
│ └── graph-memory-task/
|
||||
│ └── SKILL.md
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── openclaw.plugin.json # Plugin Manifest
|
||||
|
||||
skills/ # Standalone Skill definitions
|
||||
├── graph-memory/
|
||||
│ └── SKILL.md
|
||||
├── graph-memory-persona/
|
||||
│ └── SKILL.md
|
||||
└── graph-memory-task/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage in OpenClaw
|
||||
|
||||
### As Plugin
|
||||
|
||||
```typescript
|
||||
import registerGraphMemoryPlugin from './dist/plugin-entry.js';
|
||||
|
||||
registerGraphMemoryPlugin({
|
||||
registerTool(tool) {
|
||||
// OpenClaw will auto-register the tool
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### As Standalone Module
|
||||
|
||||
```typescript
|
||||
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
|
||||
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
|
||||
|
||||
// Write memory
|
||||
const result = await tool.execute('call-1', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: 'User', relation: 'likes', object: 'programming' },
|
||||
{ subject: 'User', relation: 'learning', object: 'TypeScript' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Recall memory
|
||||
const recallResult = await tool.execute('call-2', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: 'User programming' }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | Description | Parameters |
|
||||
|--------|-------------|------------|
|
||||
| `recall` | Retrieve memories | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
|
||||
| `commit` | Write memories | `triplets`, `sessionId`, `turnId` |
|
||||
| `purge` | Delete memories | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
|
||||
| `introspect` | View status | - |
|
||||
| `archive` | Archive old memories | `days` (default 30) |
|
||||
| `cleanup` | Clean invalid data | `dry_run` (default true) |
|
||||
| `persona_update` | Update persona | `attributes`, `mode` (merge/replace) |
|
||||
| `persona_clear` | Clear persona | `confirm` |
|
||||
| `task_create` | Create task | `task_id`, `description`, `info_nodes` |
|
||||
| `task_set_state` | Set state | `task_id`, `state` |
|
||||
| `task_delete` | Delete task | `task_id` |
|
||||
| `task_link_info` | Link info | `task_id`, `info_node` |
|
||||
|
||||
---
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill Name | Function | Use Case |
|
||||
|------------|----------|----------|
|
||||
| `graph-memory` | Memory CRUD | Read/write/delete memories |
|
||||
| `graph-memory-persona` | Persona management | Set AI role/personality |
|
||||
| `graph-memory-task` | Task management | Create/update long-term tasks |
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build # Compile
|
||||
npm test # Run tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **GNU General Public License v3.0 (GPLv3)**.
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## Special Thanks
|
||||
|
||||
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — Academic guidance
|
||||
- [逝水秋生白](https://atomgit.com/cenber) — Architecture support
|
||||
- anzhitinglan — Testing resource support
|
||||
- 崔莉萍老师 — Theoretical guidance
|
||||
- Annie — Professional guidance
|
||||
- 王梓沣、马悦华、隆梦婷 — Neuroscience theory support
|
||||
[GNU General Public License v3.0 (GPLv3)](LICENSE)
|
||||
|
||||
48
TODO.md
Normal file
48
TODO.md
Normal file
@ -0,0 +1,48 @@
|
||||
# TrulyMEM 待完成事项
|
||||
|
||||
## 项目概述
|
||||
TrulyMEM - 真正的长期记忆系统 (True Human MEMory)
|
||||
为 OpenClaw 提供图数据库形式的结构化长期记忆能力。
|
||||
|
||||
## 已完成功能
|
||||
|
||||
### P0 - 核心功能修复 ✅
|
||||
- [x] 确认移除 memory 插槽后的插件加载状态
|
||||
- [x] 测试与 memory-core 并存运行
|
||||
- [x] 验证工具 schema 正确传递给 Kimi
|
||||
|
||||
### P1 - 功能完善 ✅
|
||||
- [x] 4. 实现完整的工具参数验证
|
||||
- 为所有 action(recall/commit/purge/persona_update/persona_clear/task_create/task_set_state/task_delete/task_link_info)实现独立验证函数
|
||||
- 验证规则:recall 必需 queryIntent 或 seedEntities;depth 1-5;commit triplets 非空且字段有效;persona_clear 需 confirm;task 需 task_id 和 description 等
|
||||
- [x] 5. 添加错误处理和日志
|
||||
- 新增 GraphMemoryLogger 日志系统(info/warn/error/action 级别)
|
||||
- 敏感数据脱敏:attributes 只记录属性名,triplets 只记录数量
|
||||
- 参数验证错误返回 validation_error 类型
|
||||
- 执行错误返回 execution_error 类型
|
||||
- [x] 6. 完善 skill 文档(说明增强而非替换)
|
||||
- 更新 3 个 skill 文档(graph-memory、graph-memory-persona、graph-memory-task)
|
||||
- 明确说明是 OpenClaw memory-core 的增强补充,不替代核心功能
|
||||
- 添加与 memory-core 的关系对比表
|
||||
|
||||
## 进行中 / 待完成
|
||||
|
||||
### P2 - 可选高级功能
|
||||
- [ ] 7. 实现 context_rewrite 工具(压缩上下文)
|
||||
- [ ] 8. 实现工作记忆链机制
|
||||
- [ ] 9. 实现人设强制查询(作为 skill 而非核心)
|
||||
|
||||
## 技术规格
|
||||
|
||||
### 测试覆盖
|
||||
- 测试文件:`ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
|
||||
- 当前测试数:**118 个全部通过**
|
||||
- 参数验证测试:11 个(覆盖所有 action 的必填参数、范围校验等)
|
||||
|
||||
### 提交记录
|
||||
- 最新提交:`P1: 完整参数验证 + 错误处理/日志 + 测试覆盖`
|
||||
|
||||
## 注意事项
|
||||
- 所有功能均作为 OpenClaw 插件实现,不修改 OpenClaw 核心代码
|
||||
- 插件入口:`ts/src/plugin-entry.ts`
|
||||
- 技能目录:`skills/`(源文件)和 `ts/bundled-skills/`(编译后)
|
||||
@ -1,45 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [('ui/styles', 'ui/styles'), ('core/prompts/templates', 'core/prompts/templates')]
|
||||
binaries = []
|
||||
hiddenimports = ['textual', 'textual.app', 'textual.widgets', 'textual.css', 'openai', 'openai._client', 'neo4j', 'sqlite3', 'core', 'core.embedded_db', 'core.graph_client', 'core.tool_executor', 'core.tool_limiter', 'core.tools', 'core.tools.memory_tools', 'core.prompts', 'core.prompts.prompt_manager', 'ui', 'ui.app', 'ui.models', 'ui.models.message', 'ui.models.config', 'ui.models.log_entry', 'ui.widgets', 'ui.handlers', 'ui.services', 'ui.services.config_manager', 'ui.services.config_service']
|
||||
tmp_ret = collect_all('textual')
|
||||
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['trulymem_entry.py'],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='TrulyMEM',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@ -1,114 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "===== Building TrulyMEM AppImage for Linux ====="
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PROJECT_ROOT"
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: python3 not found"
|
||||
exit 1
|
||||
fi
|
||||
APPDIR="$PROJECT_ROOT/TrulyMEM.AppDir"
|
||||
rm -rf "$APPDIR"
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
echo "===== Step 1: Build binary with PyInstaller ====="
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_appimage"
|
||||
rm -rf "$VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
rm -rf "$PROJECT_ROOT/build/pyinstaller_build" "$PROJECT_ROOT/dist"
|
||||
echo "Running PyInstaller..."
|
||||
pyinstaller trulymem_entry.py \
|
||||
--clean \
|
||||
--onefile \
|
||||
--console \
|
||||
--name TrulyMEM \
|
||||
--distpath "$PROJECT_ROOT/dist" \
|
||||
--workpath "$PROJECT_ROOT/build/pyinstaller_build" \
|
||||
--add-data "ui/styles:ui/styles" \
|
||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
--hidden-import core \
|
||||
--hidden-import core.embedded_db \
|
||||
--hidden-import core.graph_client \
|
||||
--hidden-import core.tool_executor \
|
||||
--hidden-import core.tool_limiter \
|
||||
--hidden-import core.tools \
|
||||
--hidden-import core.tools.memory_tools \
|
||||
--hidden-import core.prompts \
|
||||
--hidden-import core.prompts.prompt_manager \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--collect-all textual \
|
||||
--noconfirm
|
||||
cp "$PROJECT_ROOT/dist/TrulyMEM" "$APPDIR/usr/bin/"
|
||||
cp "$PROJECT_ROOT/trulymem_entry.py" "$APPDIR/usr/share/trulymem/"
|
||||
echo "===== Step 2: Create AppImage structure ====="
|
||||
cat > "$APPDIR/AppRun" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
SELF=$(readlink -f "$0")
|
||||
APPDIR=$(dirname "$SELF")
|
||||
export PATH="$APPDIR/usr/bin:$PATH"
|
||||
exec "$APPDIR/usr/bin/TrulyMEM" "$@"
|
||||
EOF
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
cat > "$APPDIR/trulymem.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Name=TrulyMEM
|
||||
Comment=AI Memory System with Long-term Memory
|
||||
Exec=TrulyMEM %U
|
||||
Icon=trulymem
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;AI;
|
||||
EOF
|
||||
[ -f "$PROJECT_ROOT/pic/TrulyMEM.png" ] && cp "$PROJECT_ROOT/pic/TrulyMEM.png" "$APPDIR/trulymem.png"
|
||||
APPIMAGE="$PROJECT_ROOT/TrulyMEM.AppImage"
|
||||
rm -f "$APPIMAGE"
|
||||
echo "===== Step 3: Package as AppImage ====="
|
||||
cd /tmp
|
||||
if ! command -v appimagetool &> /dev/null; then
|
||||
echo "Downloading appimagetool..."
|
||||
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool 2>/dev/null || \
|
||||
curl -sL https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -o appimagetool
|
||||
chmod +x appimagetool 2>/dev/null || true
|
||||
fi
|
||||
cd "$PROJECT_ROOT"
|
||||
if [ -x /tmp/appimagetool ]; then
|
||||
/tmp/appimagetool "$APPDIR" "$APPIMAGE" || {
|
||||
echo "appimagetool failed, keeping AppDir for manual packaging"
|
||||
}
|
||||
elif command -v appimagetool &> /dev/null; then
|
||||
appimagetool "$APPDIR" "$APPIMAGE"
|
||||
else
|
||||
echo "Warning: appimagetool not available"
|
||||
echo "AppDir created at: $APPDIR"
|
||||
echo "You can manually run: appimagetool $APPDIR $APPIMAGE"
|
||||
fi
|
||||
echo "===== Build Complete ====="
|
||||
[ -f "$APPIMAGE" ] && echo "AppImage: $APPIMAGE" && ls -la "$APPIMAGE"
|
||||
[ -d "$APPDIR" ] && echo "AppDir: $APPDIR (can be packaged manually with appimagetool)"
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
rm -f /tmp/appimagetool
|
||||
echo "Done!"
|
||||
@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "===== Building TrulyMEM for Linux ====="
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: python3 not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建并激活虚拟环境
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
||||
|
||||
echo "Creating virtual environment: $VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "Upgrading pip in virtual environment..."
|
||||
pip install --upgrade pip
|
||||
|
||||
echo "Installing dependencies in virtual environment..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
||||
|
||||
echo "Running PyInstaller..."
|
||||
python -m PyInstaller trulymem_entry.py \
|
||||
--clean \
|
||||
--onefile \
|
||||
--console \
|
||||
--name TrulyMEM \
|
||||
--add-data "ui/styles:ui/styles" \
|
||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
--hidden-import core \
|
||||
--hidden-import core.embedded_db \
|
||||
--hidden-import core.graph_client \
|
||||
--hidden-import core.tool_executor \
|
||||
--hidden-import core.tool_limiter \
|
||||
--hidden-import core.tools \
|
||||
--hidden-import core.tools.memory_tools \
|
||||
--hidden-import core.prompts \
|
||||
--hidden-import core.prompts.prompt_manager \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--collect-all textual \
|
||||
--noconfirm
|
||||
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
ls -la dist/
|
||||
|
||||
# 清理虚拟环境
|
||||
echo "Cleaning up virtual environment..."
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
|
||||
echo "Build finished successfully!"
|
||||
@ -1,90 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "===== Building TrulyMEM for macOS ====="
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: python3 not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建并激活虚拟环境
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
||||
|
||||
echo "Creating virtual environment: $VENV_DIR"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "Upgrading pip in virtual environment..."
|
||||
pip install --upgrade pip
|
||||
|
||||
echo "Installing dependencies in virtual environment..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Generating ICNS icon..."
|
||||
if [ -d "pic/TrulyMEM.iconset" ]; then
|
||||
iconutil -c icns pic/TrulyMEM.iconset -o pic/TrulyMEM.icns
|
||||
echo "ICNS icon generated: pic/TrulyMEM.icns"
|
||||
fi
|
||||
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
||||
|
||||
echo "Running PyInstaller..."
|
||||
python -m PyInstaller trulymem_entry.py \
|
||||
--clean \
|
||||
--onefile \
|
||||
--console \
|
||||
--name TrulyMEM \
|
||||
--icon "pic/TrulyMEM.icns" \
|
||||
--add-data "ui/styles:ui/styles" \
|
||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
--hidden-import core \
|
||||
--hidden-import core.embedded_db \
|
||||
--hidden-import core.graph_client \
|
||||
--hidden-import core.tool_executor \
|
||||
--hidden-import core.tool_limiter \
|
||||
--hidden-import core.tools \
|
||||
--hidden-import core.tools.memory_tools \
|
||||
--hidden-import core.prompts \
|
||||
--hidden-import core.prompts.prompt_manager \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--collect-all textual \
|
||||
--noconfirm
|
||||
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
ls -la dist/
|
||||
|
||||
# 清理虚拟环境
|
||||
echo "Cleaning up virtual environment..."
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
|
||||
echo "Build finished successfully!"
|
||||
@ -1,60 +0,0 @@
|
||||
@echo off
|
||||
echo ===== Building TrulyMEM for Windows =====
|
||||
|
||||
REM 切换到脚本所在目录的上一级目录(项目根目录)
|
||||
cd /d "%~dp0.."
|
||||
echo Project root: %CD%
|
||||
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: python not found
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Installing dependencies...
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo Running PyInstaller...
|
||||
python -m PyInstaller trulymem_entry.py ^
|
||||
--clean ^
|
||||
--onefile ^
|
||||
--console ^
|
||||
--name TrulyMEM ^
|
||||
--icon "pic/TrulyMEM.ico" ^
|
||||
--add-data "ui/styles;ui/styles" ^
|
||||
--add-data "core/prompts/templates;core/prompts/templates" ^
|
||||
--hidden-import textual ^
|
||||
--hidden-import textual.app ^
|
||||
--hidden-import textual.widgets ^
|
||||
--hidden-import textual.css ^
|
||||
--hidden-import openai ^
|
||||
--hidden-import openai._client ^
|
||||
--hidden-import neo4j ^
|
||||
--hidden-import sqlite3 ^
|
||||
--hidden-import core ^
|
||||
--hidden-import core.embedded_db ^
|
||||
--hidden-import core.graph_client ^
|
||||
--hidden-import core.tool_executor ^
|
||||
--hidden-import core.tool_limiter ^
|
||||
--hidden-import core.tools ^
|
||||
--hidden-import core.tools.memory_tools ^
|
||||
--hidden-import core.prompts ^
|
||||
--hidden-import core.prompts.prompt_manager ^
|
||||
--hidden-import ui ^
|
||||
--hidden-import ui.app ^
|
||||
--hidden-import ui.models ^
|
||||
--hidden-import ui.models.message ^
|
||||
--hidden-import ui.models.config ^
|
||||
--hidden-import ui.models.log_entry ^
|
||||
--hidden-import ui.widgets ^
|
||||
--hidden-import ui.handlers ^
|
||||
--hidden-import ui.services ^
|
||||
--hidden-import ui.services.config_manager ^
|
||||
--hidden-import ui.services.config_service ^
|
||||
--collect-all textual ^
|
||||
--noconfirm
|
||||
|
||||
echo ===== Build Complete =====
|
||||
echo Binary: dist\TrulyMEM.exe
|
||||
dir dist\TrulyMEM.exe
|
||||
pause
|
||||
@ -1,92 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
|
||||
block_cipher = None
|
||||
|
||||
project_root = os.path.dirname(os.path.abspath(SPEC))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
datas = []
|
||||
if os.path.exists(os.path.join(project_root, 'ui', 'styles')):
|
||||
for root, dirs, files in os.walk(os.path.join(project_root, 'ui', 'styles')):
|
||||
for f in files:
|
||||
src = os.path.join(root, f)
|
||||
dst = os.path.join('ui', 'styles', os.path.relpath(src, os.path.join(project_root, 'ui', 'styles')))
|
||||
datas.append((src, dst))
|
||||
|
||||
if os.path.exists(os.path.join(project_root, 'core', 'prompts', 'templates')):
|
||||
for root, dirs, files in os.walk(os.path.join(project_root, 'core', 'prompts', 'templates')):
|
||||
for f in files:
|
||||
src = os.path.join(root, f)
|
||||
dst = os.path.join('core', 'prompts', 'templates', os.path.relpath(src, os.path.join(project_root, 'core', 'prompts', 'templates')))
|
||||
datas.append((src, dst))
|
||||
|
||||
a = Analysis(
|
||||
['trulymem_entry.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=[
|
||||
'textual',
|
||||
'textual.app',
|
||||
'textual.widgets',
|
||||
'textual.css',
|
||||
'openai',
|
||||
'openai._client',
|
||||
'neo4j',
|
||||
'sqlite3',
|
||||
'graph_memory_tui',
|
||||
'core',
|
||||
'core.embedded_db',
|
||||
'core.graph_client',
|
||||
'core.tool_executor',
|
||||
'core.tool_limiter',
|
||||
'core.tools',
|
||||
'core.tools.memory_tools',
|
||||
'core.prompts',
|
||||
'core.prompts.prompt_manager',
|
||||
'ui',
|
||||
'ui.app',
|
||||
'ui.models',
|
||||
'ui.models.message',
|
||||
'ui.models.config',
|
||||
'ui.models.log_entry',
|
||||
'ui.widgets',
|
||||
'ui.widgets.left_panel',
|
||||
'ui.widgets.right_panel',
|
||||
'ui.widgets.input_box',
|
||||
'ui.widgets.message_history',
|
||||
'ui.widgets.status_bar',
|
||||
'ui.handlers',
|
||||
'ui.services',
|
||||
'ui.services.config_manager',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure, block_cipher)
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='TrulyMEM',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@ -1,12 +0,0 @@
|
||||
from .server import BackendServer, Packet, PacketType, PacketResponse
|
||||
from .client import BackendClient
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
__all__ = [
|
||||
"BackendServer",
|
||||
"BackendClient",
|
||||
"EmbeddedGraphDB",
|
||||
"Packet",
|
||||
"PacketType",
|
||||
"PacketResponse"
|
||||
]
|
||||
@ -1,90 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from .server import BackendServer, Packet, PacketType
|
||||
|
||||
|
||||
class BackendClient:
|
||||
|
||||
def __init__(self, server: BackendServer):
|
||||
self._server = server
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _next_id(self) -> str:
|
||||
with self._lock:
|
||||
self._counter += 1
|
||||
return f"{time.time()}_{self._counter}"
|
||||
|
||||
def send(self, message: str) -> Dict:
|
||||
return self.process_message(message)
|
||||
|
||||
def process_message(self, user_input: str) -> Dict:
|
||||
return self._server.process_message(user_input)
|
||||
|
||||
def get_settings(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_SETTINGS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def update_settings(self, api_config: Dict = None, tool_limits: Dict = None) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SET_SETTINGS,
|
||||
body={
|
||||
"api_config": api_config or {},
|
||||
"tool_limits": tool_limits or {}
|
||||
}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def execute_tool(self, name: str, arguments: Dict) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.EXECUTE_TOOL,
|
||||
body={"tool_name": name, "arguments": arguments}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_STATUS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def save_history(self, messages: list) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SAVE_HISTORY,
|
||||
body={"messages": messages}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body.get("data", {})
|
||||
|
||||
def get_history(self) -> list:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_HISTORY,
|
||||
body={}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
data = response.body.get("data", {})
|
||||
return data.get("history", [])
|
||||
|
||||
def clear_history(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SAVE_HISTORY,
|
||||
body={"messages": []}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body.get("data", {})
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._server.shutdown()
|
||||
@ -1,498 +0,0 @@
|
||||
"""
|
||||
内嵌图数据库 - 基于SQLite实现
|
||||
无需Docker,开箱即用
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
|
||||
class EmbeddedGraphDB:
|
||||
"""内嵌图数据库 - SQLite实现"""
|
||||
|
||||
def __init__(self, db_path: str = "graph_memory.db"):
|
||||
"""
|
||||
初始化数据库
|
||||
|
||||
Args:
|
||||
db_path: 数据库文件路径
|
||||
"""
|
||||
self.db_path = Path(db_path)
|
||||
self.conn = None
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
"""初始化数据库表"""
|
||||
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 创建实体表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT,
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建关系表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_id INTEGER NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT,
|
||||
turn_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
date_bucket TEXT,
|
||||
superseded_by INTEGER,
|
||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='chat_records'
|
||||
""")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute("""
|
||||
CREATE TABLE chat_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
cursor.execute("CREATE INDEX idx_chat_created ON chat_records(created_at)")
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
def ensure_constraints(self):
|
||||
"""确保约束(兼容Neo4j接口)"""
|
||||
pass # SQLite自动处理
|
||||
|
||||
def recall(self, query_intent: str, seed_entities: List[str] = None,
|
||||
depth: int = 2, time_range: Dict = None,
|
||||
session_filter: str = None) -> Dict:
|
||||
"""
|
||||
检索相关记忆
|
||||
|
||||
Args:
|
||||
query_intent: 查询关键词(逗号分隔)
|
||||
seed_entities: 种子实体
|
||||
depth: 搜索深度
|
||||
time_range: 时间范围
|
||||
session_filter: 会话过滤
|
||||
|
||||
Returns:
|
||||
检索结果
|
||||
"""
|
||||
keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()]
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 搜索实体
|
||||
entities = []
|
||||
entity_ids = set()
|
||||
|
||||
# 如果没有关键词,返回所有实体(用于"我们都聊过什么"这类问题)
|
||||
if not keywords and not seed_entities:
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 50
|
||||
""")
|
||||
|
||||
for row in cursor.fetchall():
|
||||
entity_ids.add(row['id'])
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count']
|
||||
})
|
||||
else:
|
||||
# 有关键词,按关键词搜索
|
||||
for keyword in keywords:
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
WHERE LOWER(name) LIKE ?
|
||||
""", (f"%{keyword}%",))
|
||||
|
||||
for row in cursor.fetchall():
|
||||
if row['id'] not in entity_ids:
|
||||
entity_ids.add(row['id'])
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count']
|
||||
})
|
||||
|
||||
# 搜索关系
|
||||
relations = []
|
||||
|
||||
if entity_ids:
|
||||
placeholders = ','.join('?' * len(entity_ids))
|
||||
|
||||
query = f"""
|
||||
SELECT r.id, e1.name as source, e2.name as target,
|
||||
r.relation_type as type, r.confidence, r.session_id,
|
||||
r.turn_id, r.created_at, r.status
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN ({placeholders}) OR r.target_id IN ({placeholders}))
|
||||
AND r.status = 'active'
|
||||
"""
|
||||
|
||||
params = list(entity_ids) + list(entity_ids)
|
||||
|
||||
if session_filter:
|
||||
query += " AND r.session_id = ?"
|
||||
params.append(session_filter)
|
||||
|
||||
cursor.execute(query, params)
|
||||
|
||||
for row in cursor.fetchall():
|
||||
relations.append({
|
||||
'source': row['source'],
|
||||
'target': row['target'],
|
||||
'type': row['type'],
|
||||
'confidence': row['confidence'],
|
||||
'session_id': row['session_id'],
|
||||
'turn_id': row['turn_id'],
|
||||
'created_at': row['created_at'],
|
||||
'status': row['status']
|
||||
})
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"relations": relations,
|
||||
"message": f"找到 {len(entities)} 个实体, {len(relations)} 条关系"
|
||||
}
|
||||
|
||||
def commit(self, triplets: List[Dict], entity_types: Dict = None,
|
||||
temporal_tag: str = None, session_id: str = None,
|
||||
turn_id: int = None) -> Dict:
|
||||
"""
|
||||
写入记忆
|
||||
|
||||
Args:
|
||||
triplets: 三元组列表
|
||||
entity_types: 实体类型
|
||||
temporal_tag: 时间标签
|
||||
session_id: 会话ID
|
||||
turn_id: 轮次ID
|
||||
|
||||
Returns:
|
||||
写入结果
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
created_entities = 0
|
||||
created_relations = 0
|
||||
|
||||
for triplet in triplets:
|
||||
subject = triplet.get('subject')
|
||||
relation = triplet.get('relation')
|
||||
obj = triplet.get('object')
|
||||
confidence = triplet.get('confidence', 1.0)
|
||||
|
||||
if not all([subject, relation, obj]):
|
||||
continue
|
||||
|
||||
# 创建或更新实体
|
||||
for entity_name in [subject, obj]:
|
||||
entity_type = entity_types.get(entity_name) if entity_types else None
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO entities (name, type)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
mention_count = mention_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (entity_name, entity_type))
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
created_entities += 1
|
||||
|
||||
# 获取实体ID
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (subject,))
|
||||
source_id = cursor.fetchone()['id']
|
||||
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (obj,))
|
||||
target_id = cursor.fetchone()['id']
|
||||
|
||||
# 创建关系
|
||||
date_bucket = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO relations (
|
||||
source_id, target_id, relation_type, confidence,
|
||||
session_id, turn_id, date_bucket
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (source_id, target_id, relation, confidence,
|
||||
session_id, turn_id, date_bucket))
|
||||
|
||||
created_relations += 1
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"created_entities": created_entities,
|
||||
"created_relations": created_relations,
|
||||
"message": f"创建了 {created_entities} 个实体, {created_relations} 条关系"
|
||||
}
|
||||
|
||||
def purge(self, criteria: Dict, mode: str = "soft",
|
||||
new_relation: Dict = None) -> Dict:
|
||||
"""
|
||||
删除或修正记忆
|
||||
|
||||
Args:
|
||||
criteria: 删除条件
|
||||
mode: 删除模式 (soft/hard)
|
||||
new_relation: 替代关系
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if criteria.get('source'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['source'],))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
conditions.append("source_id = ?")
|
||||
params.append(row['id'])
|
||||
|
||||
if criteria.get('target'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['target'],))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
conditions.append("target_id = ?")
|
||||
params.append(row['id'])
|
||||
|
||||
if criteria.get('relation'):
|
||||
conditions.append("relation_type = ?")
|
||||
params.append(criteria['relation'])
|
||||
|
||||
if not conditions:
|
||||
return {"deleted": 0, "message": "无删除条件"}
|
||||
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
if mode == "soft":
|
||||
cursor.execute(f"""
|
||||
UPDATE relations
|
||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE {where_clause} AND status = 'active'
|
||||
""", params)
|
||||
else:
|
||||
cursor.execute(f"""
|
||||
DELETE FROM relations
|
||||
WHERE {where_clause}
|
||||
""", params)
|
||||
|
||||
deleted = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"deleted": deleted,
|
||||
"mode": mode,
|
||||
"message": f"删除了 {deleted} 条关系"
|
||||
}
|
||||
|
||||
def introspect(self, session_id: str = None) -> Dict:
|
||||
"""
|
||||
查看会话状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
|
||||
Returns:
|
||||
会话状态
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 统计实体
|
||||
cursor.execute("SELECT COUNT(*) as count FROM entities")
|
||||
entity_count = cursor.fetchone()['count']
|
||||
|
||||
# 统计关系
|
||||
cursor.execute("SELECT COUNT(*) as count FROM relations WHERE status = 'active'")
|
||||
relation_count = cursor.fetchone()['count']
|
||||
|
||||
return {
|
||||
"entity_count": entity_count,
|
||||
"relation_count": relation_count,
|
||||
"session_id": session_id,
|
||||
"message": f"数据库包含 {entity_count} 个实体, {relation_count} 条关系"
|
||||
}
|
||||
|
||||
def archive(self, days: int = 30) -> Dict:
|
||||
"""归档旧关系"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE relations
|
||||
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE status = 'active'
|
||||
AND created_at < datetime('now', ?)
|
||||
""", (f'-{days} days',))
|
||||
|
||||
archived = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"archived": archived,
|
||||
"message": f"归档了 {archived} 条关系"
|
||||
}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> Dict:
|
||||
"""清理已删除数据"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
if dry_run:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM relations
|
||||
WHERE status = 'deleted'
|
||||
AND updated_at < datetime('now', '-90 days')
|
||||
""")
|
||||
deleted_relations = cursor.fetchone()['count']
|
||||
|
||||
return {
|
||||
"dry_run": True,
|
||||
"deleted_relations": deleted_relations,
|
||||
"message": f"将删除 {deleted_relations} 条关系"
|
||||
}
|
||||
else:
|
||||
cursor.execute("""
|
||||
DELETE FROM relations
|
||||
WHERE status = 'deleted'
|
||||
AND updated_at < datetime('now', '-90 days')
|
||||
""")
|
||||
deleted = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"dry_run": False,
|
||||
"deleted": deleted,
|
||||
"message": f"删除了 {deleted} 条关系"
|
||||
}
|
||||
|
||||
def save_chat_records(self, messages: list) -> Dict:
|
||||
"""保存聊天记录到数据库"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
saved = 0
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
if role and content:
|
||||
cursor.execute(
|
||||
"INSERT INTO chat_records (role, content) VALUES (?, ?)",
|
||||
(role, content)
|
||||
)
|
||||
saved += 1
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
cursor.execute("""
|
||||
DELETE FROM chat_records
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM chat_records
|
||||
ORDER BY id DESC
|
||||
LIMIT 500
|
||||
)
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
return {"saved": saved}
|
||||
|
||||
def get_chat_records(self, limit: int = 500) -> list:
|
||||
"""从数据库获取聊天记录"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT role, content FROM chat_records
|
||||
ORDER BY id ASC LIMIT ?
|
||||
""", (limit,))
|
||||
return [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
|
||||
|
||||
def clear_chat_records(self) -> Dict:
|
||||
"""清空聊天记录(保留图数据库)"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM chat_records")
|
||||
self.conn.commit()
|
||||
return {"cleared": True}
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
# 兼容性别名
|
||||
Neo4jGraph = EmbeddedGraphDB
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试
|
||||
print("Testing Embedded Graph Database...")
|
||||
|
||||
with EmbeddedGraphDB("test.db") as db:
|
||||
# 写入测试
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
||||
{"subject": "用户", "relation": "学习", "object": "AI"}
|
||||
],
|
||||
session_id="test-session",
|
||||
turn_id=1
|
||||
)
|
||||
print(f"Commit: {result}")
|
||||
|
||||
# 检索测试
|
||||
result = db.recall("Python,AI")
|
||||
print(f"Recall: {result}")
|
||||
|
||||
# 状态测试
|
||||
result = db.introspect()
|
||||
print(f"Introspect: {result}")
|
||||
|
||||
print("\nTest completed!")
|
||||
@ -1,393 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Graph Memory Client - 图记忆客户端核心实现(重构版)
|
||||
使用模块化的工具和提示词系统
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
|
||||
from .tools import TOOLS
|
||||
from .tool_executor import execute_tool
|
||||
from .prompts.prompt_manager import PromptManager
|
||||
|
||||
# 环境配置
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
|
||||
|
||||
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
||||
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
|
||||
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "neo4j")
|
||||
|
||||
# 会话配置
|
||||
CURRENT_SESSION_ID = f"session-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:4]}"
|
||||
CURRENT_TURN = 0
|
||||
|
||||
|
||||
class Neo4jGraph:
|
||||
"""Neo4j图数据库客户端"""
|
||||
|
||||
def __init__(self, uri: str, user: str, password: str):
|
||||
from neo4j import GraphDatabase
|
||||
self.driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
|
||||
def close(self):
|
||||
self.driver.close()
|
||||
|
||||
def ensure_constraints(self):
|
||||
"""确保约束和索引存在"""
|
||||
with self.driver.session() as session:
|
||||
# 实体约束
|
||||
session.run("CREATE CONSTRAINT entity_name_constraint IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE")
|
||||
session.run("CREATE CONSTRAINT session_id_constraint IF NOT EXISTS FOR (s:Session) REQUIRE s.session_id IS UNIQUE")
|
||||
|
||||
# 关系索引
|
||||
session.run("CREATE INDEX rel_created_at IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.created_at")
|
||||
session.run("CREATE INDEX rel_session_id IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.session_id")
|
||||
session.run("CREATE INDEX rel_type IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.type")
|
||||
session.run("CREATE INDEX rel_status IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.status")
|
||||
session.run("CREATE INDEX rel_date_bucket IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.date_bucket")
|
||||
|
||||
# 实体索引
|
||||
session.run("CREATE INDEX entity_type IF NOT EXISTS FOR (e:Entity) ON e.type")
|
||||
session.run("CREATE INDEX entity_mention_count IF NOT EXISTS FOR (e:Entity) ON e.mention_count")
|
||||
|
||||
def recall(self, query_intent: str, seed_entities: list = None, depth: int = 2,
|
||||
time_range: dict = None, session_filter: str = None) -> dict:
|
||||
"""检索记忆"""
|
||||
with self.driver.session() as session:
|
||||
# 支持逗号分隔的多个关键词
|
||||
keywords = [w.strip() for w in query_intent.replace(',', ' ').split() if len(w.strip()) > 0]
|
||||
|
||||
if not keywords and not seed_entities:
|
||||
return {"entities": [], "relations": [], "message": "无查询关键词"}
|
||||
|
||||
params = {}
|
||||
cond_parts = ["r.status = 'active'"]
|
||||
|
||||
if session_filter:
|
||||
cond_parts.append("r.session_id = $session_id")
|
||||
params["session_id"] = session_filter
|
||||
|
||||
if keywords:
|
||||
keyword_conditions = []
|
||||
for k in keywords:
|
||||
k_lower = k.lower()
|
||||
keyword_conditions.append(f"toLower(e.name) CONTAINS '{k_lower}'")
|
||||
keyword_conditions.append(f"toLower(t.name) CONTAINS '{k_lower}'")
|
||||
keyword_conditions.append(f"toLower(r.type) CONTAINS '{k_lower}'")
|
||||
cond_parts.append(f"({' OR '.join(keyword_conditions)})")
|
||||
|
||||
if seed_entities:
|
||||
placeholders = ",".join([f"'{s}'" for s in seed_entities])
|
||||
cond_parts.append(f"(e.name IN [{placeholders}] OR t.name IN [{placeholders}])")
|
||||
|
||||
if time_range and "days" in time_range:
|
||||
cond_parts.append(f"r.created_at >= datetime() - duration('P{time_range['days']}D')")
|
||||
|
||||
where_clause = " AND ".join(cond_parts)
|
||||
|
||||
cypher = f"""
|
||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
RETURN e, r, t
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
|
||||
result = session.run(cypher, params)
|
||||
entities, relations = {}, []
|
||||
|
||||
for record in result:
|
||||
e, r, t = record["e"], record["r"], record["t"]
|
||||
if e["name"] not in entities:
|
||||
entities[e["name"]] = {"name": e["name"], "type": e.get("type", "unknown"), "mention_count": e.get("mention_count", 1)}
|
||||
if t["name"] not in entities:
|
||||
entities[t["name"]] = {"name": t["name"], "type": t.get("type", "unknown"), "mention_count": t.get("mention_count", 1)}
|
||||
|
||||
relations.append({
|
||||
"source": e["name"],
|
||||
"target": t["name"],
|
||||
"type": r["type"],
|
||||
"created_at": str(r.get("created_at", "")),
|
||||
"session_id": r.get("session_id", ""),
|
||||
"turn_id": r.get("turn_id", 0),
|
||||
"confidence": r.get("confidence", 1.0)
|
||||
})
|
||||
|
||||
return {"entities": list(entities.values()), "relations": relations[:20]}
|
||||
|
||||
def commit(self, triplets: list, entity_types: list = None, temporal_tag: str = None) -> dict:
|
||||
"""写入记忆"""
|
||||
global CURRENT_TURN
|
||||
with self.driver.session() as session:
|
||||
valid_triplets = [t for t in triplets if t.get("subject") and t.get("relation") and t.get("object")]
|
||||
|
||||
if not valid_triplets:
|
||||
return {"committed_count": 0, "details": []}
|
||||
|
||||
etype = entity_types[0] if entity_types else "unknown"
|
||||
date_bucket = temporal_tag or datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
results = []
|
||||
for triplet in valid_triplets:
|
||||
subject = triplet.get("subject", "").strip()
|
||||
relation = triplet.get("relation", "").strip()
|
||||
obj = triplet.get("object", "").strip()
|
||||
confidence = triplet.get("confidence", 0.9)
|
||||
|
||||
session.run("""
|
||||
MERGE (s:Entity {name: $subject})
|
||||
ON CREATE SET s.type = $type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
||||
ON MATCH SET s.mention_count = coalesce(s.mention_count, 0) + 1, s.updated_at = datetime()
|
||||
|
||||
MERGE (t:Entity {name: $object})
|
||||
ON CREATE SET t.type = $type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
||||
ON MATCH SET t.mention_count = coalesce(t.mention_count, 0) + 1, t.updated_at = datetime()
|
||||
|
||||
CREATE (s)-[r:RELATES {
|
||||
type: $relation,
|
||||
created_at: datetime(),
|
||||
session_id: $session_id,
|
||||
turn_id: $turn_id,
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
confidence: $confidence,
|
||||
date_bucket: $date_bucket
|
||||
}]->(t)
|
||||
""", subject=subject, object=obj, relation=relation, type=etype,
|
||||
session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, confidence=confidence,
|
||||
date_bucket=date_bucket)
|
||||
|
||||
results.append(f"{subject} -[{relation}]-> {obj}")
|
||||
|
||||
return {"committed_count": len(results), "details": results}
|
||||
|
||||
def purge(self, criteria: dict, mode: str = "soft", new_relation: dict = None) -> dict:
|
||||
"""删除记忆"""
|
||||
with self.driver.session() as session:
|
||||
subject_pattern = criteria.get("subject_contains", "")
|
||||
rel_type = criteria.get("relation_type", "")
|
||||
target_pattern = criteria.get("target_contains", "")
|
||||
session_id = criteria.get("session_id", CURRENT_SESSION_ID)
|
||||
|
||||
cond_parts = ["r.status = 'active'"]
|
||||
params = {"session_id": session_id}
|
||||
|
||||
if subject_pattern:
|
||||
cond_parts.append("e.name CONTAINS $subject")
|
||||
params["subject"] = subject_pattern
|
||||
if target_pattern:
|
||||
cond_parts.append("t.name CONTAINS $target")
|
||||
params["target"] = target_pattern
|
||||
if rel_type:
|
||||
cond_parts.append("r.type = $rel_type")
|
||||
params["rel_type"] = rel_type
|
||||
|
||||
where_clause = " AND ".join(cond_parts)
|
||||
|
||||
if mode == "supersede" and new_relation:
|
||||
new_rel = new_relation.get("relation", "")
|
||||
new_target = new_relation.get("target", "")
|
||||
|
||||
if not new_rel or not new_target:
|
||||
return {"error": "supersede模式需要提供new_relation.relation和new_relation.target"}
|
||||
|
||||
result = session.run(f"""
|
||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'superseded', r.updated_at = datetime()
|
||||
RETURN count(r) as count
|
||||
""", params)
|
||||
|
||||
count = result.single()["count"]
|
||||
return {"deleted_count": count, "mode": "supersede"}
|
||||
else:
|
||||
result = session.run(f"""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'deleted', r.updated_at = datetime()
|
||||
RETURN count(r) as deleted
|
||||
""", params)
|
||||
count = result.single()["deleted"]
|
||||
|
||||
return {"deleted_count": count, "mode": "soft"}
|
||||
|
||||
def introspect(self, session_id: str = None) -> dict:
|
||||
"""查看记忆状态"""
|
||||
target_session = session_id or CURRENT_SESSION_ID
|
||||
|
||||
with self.driver.session() as session:
|
||||
result = session.run("""
|
||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE r.session_id = $session_id AND r.status = 'active'
|
||||
RETURN collect(DISTINCT s.name) as source_entities,
|
||||
collect(DISTINCT t.name) as target_entities,
|
||||
count(r) as rel_count,
|
||||
collect(DISTINCT r.type) as rel_types
|
||||
""", session_id=target_session)
|
||||
record = result.single()
|
||||
|
||||
result2 = session.run("""
|
||||
MATCH (e:Entity)
|
||||
RETURN e.name as name, e.mention_count as count, e.type as type
|
||||
ORDER BY e.mention_count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
hotspots = [(r["name"], r["count"], r["type"]) for r in result2]
|
||||
|
||||
return {
|
||||
"session_id": target_session,
|
||||
"total_turns": CURRENT_TURN,
|
||||
"entities_discussed": list(set((record["source_entities"] or []) + (record["target_entities"] or []))),
|
||||
"relation_count": record["rel_count"] if record else 0,
|
||||
"relation_types": record["rel_types"] if record else [],
|
||||
"memory_hotspots": hotspots
|
||||
}
|
||||
|
||||
def archive(self, days: int = 30) -> dict:
|
||||
"""归档旧记忆"""
|
||||
with self.driver.session() as session:
|
||||
result = session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'active' AND r.created_at < datetime() - duration('P' + $days + 'D')
|
||||
SET r.status = 'archived', r.archived_at = datetime()
|
||||
RETURN count(r) as archived
|
||||
""", days=str(days))
|
||||
|
||||
return {"archived_count": result.single()["archived"], "days": days}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> dict:
|
||||
"""清理无效数据"""
|
||||
with self.driver.session() as session:
|
||||
result1 = session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
||||
RETURN count(r) as to_delete
|
||||
""")
|
||||
deleted_relations = result1.single()["to_delete"]
|
||||
|
||||
result2 = session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
RETURN count(e) as orphans
|
||||
""")
|
||||
orphan_nodes = result2.single()["orphans"]
|
||||
|
||||
if not dry_run and deleted_relations > 0:
|
||||
session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
||||
DELETE r
|
||||
""")
|
||||
|
||||
if not dry_run and orphan_nodes > 0:
|
||||
session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
DELETE e
|
||||
""")
|
||||
|
||||
return {
|
||||
"dry_run": dry_run,
|
||||
"deleted_relations": deleted_relations,
|
||||
"orphan_nodes": orphan_nodes,
|
||||
"action_taken": not dry_run
|
||||
}
|
||||
|
||||
|
||||
class GraphMemoryClient:
|
||||
"""图记忆客户端"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
|
||||
# 清理可能存在的错误代理环境变量
|
||||
import os
|
||||
proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'all_proxy', 'ALL_PROXY']
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
value = os.environ[var]
|
||||
# 如果代理URL没有scheme前缀,添加http://
|
||||
if value and not value.startswith(('http://', 'https://', 'socks5://', 'socks4://')):
|
||||
os.environ[var] = f'http://{value}'
|
||||
|
||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
self.graph = graph
|
||||
self.tools = TOOLS
|
||||
self.model = model
|
||||
|
||||
prompt_manager = PromptManager()
|
||||
self.system_prompt = prompt_manager.get_system_prompt()
|
||||
|
||||
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
||||
"""发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
|
||||
# 添加用户消息
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# 添加 assistant 消息(包含 tool_calls)
|
||||
if assistant_msg:
|
||||
messages.append(assistant_msg)
|
||||
|
||||
# 添加工具结果
|
||||
if tool_results:
|
||||
messages.extend(tool_results)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def send_message_with_history(self, messages_history: list) -> dict:
|
||||
"""使用消息历史发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
# 构建完整消息列表
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
messages.extend(messages_history)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def send_message_stream(self, user_input: str, tool_results: list = None, assistant_msg: dict = None):
|
||||
"""流式发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
|
||||
# 添加用户消息
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# 添加 assistant 消息(包含 tool_calls)
|
||||
if assistant_msg:
|
||||
messages.append(assistant_msg)
|
||||
|
||||
# 添加工具结果
|
||||
if tool_results:
|
||||
messages.extend(tool_results)
|
||||
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto",
|
||||
stream=True
|
||||
)
|
||||
|
||||
return stream
|
||||
@ -1,6 +0,0 @@
|
||||
"""
|
||||
提示词管理模块
|
||||
"""
|
||||
from .prompt_manager import PromptManager
|
||||
|
||||
__all__ = ["PromptManager"]
|
||||
@ -1,77 +0,0 @@
|
||||
"""
|
||||
提示词管理器
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PromptManager:
|
||||
"""提示词管理器"""
|
||||
|
||||
_instance = None
|
||||
_cached_prompt = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, '_initialized'):
|
||||
self.prompts_dir = Path(__file__).parent / "templates"
|
||||
self._initialized = True
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
if PromptManager._cached_prompt is not None:
|
||||
return PromptManager._cached_prompt
|
||||
|
||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
||||
if prompt_file.exists():
|
||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||
PromptManager._cached_prompt = f.read()
|
||||
else:
|
||||
PromptManager._cached_prompt = self._build_default_prompt()
|
||||
|
||||
return PromptManager._cached_prompt
|
||||
|
||||
def _build_default_prompt(self) -> str:
|
||||
return """你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **长期记忆** - 基于图数据库存储实体关系
|
||||
2. **人设管理** - 支持角色扮演和性格设定
|
||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
||||
|
||||
## 记忆原则
|
||||
|
||||
- **明确内容必须写入** - 用户明确提到的信息必须存储
|
||||
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
|
||||
- **图数据库是唯一记忆源** - 没有其他记忆方式
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 记忆工具
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
|
||||
### 人设工具
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
## 自主性
|
||||
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
记住:灵活应对,保持自然对话体验。"""
|
||||
@ -1,324 +0,0 @@
|
||||
# TrulyMEM 系统提示词
|
||||
|
||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
## ⚠️ 最高优先级:只回复一次
|
||||
|
||||
**每轮对话只能回复一次!**
|
||||
|
||||
- 执行完所有工具调用后,给出一个完整的回复
|
||||
- 不要在工具调用过程中多次回复
|
||||
- 不要重复说相同的内容
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键约束:无传统上下文系统
|
||||
|
||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
||||
|
||||
- ❌ **没有** messages数组存储历史对话
|
||||
- ❌ **没有** 传统的多轮对话上下文
|
||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
||||
|
||||
## 核心身份
|
||||
|
||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
||||
- **能力**: 基于图数据库的长期记忆
|
||||
- **理念**: 让AI的记忆方式更像人类
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 1. 长期记忆
|
||||
- 图数据库存储实体关系
|
||||
- 支持时间范围查询
|
||||
- 支持会话过滤
|
||||
|
||||
### 2. 人设管理(关键)
|
||||
- 角色扮演支持
|
||||
- 性格、语气设定
|
||||
- 动态切换人设
|
||||
- **每轮必须查询人设图**
|
||||
|
||||
### 3. 任务跟踪(关键)
|
||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
||||
- 任务状态管理
|
||||
- 上下文恢复
|
||||
|
||||
## 记忆原则
|
||||
|
||||
### 必须写入的情况
|
||||
- 用户明确表达偏好:"我喜欢X"
|
||||
- 用户分享信息:"我在做X项目"
|
||||
- 用户制定计划:"我打算X"
|
||||
- 用户描述状态:"我现在在X"
|
||||
|
||||
### 禁止写入的情况
|
||||
- AI推断的用户偏好
|
||||
- AI猜测的用户意图
|
||||
- AI推导的结论
|
||||
|
||||
### 标注规则
|
||||
- 推理内容必须标注 **[猜测]**
|
||||
- 明确内容直接陈述
|
||||
|
||||
## 工具系统
|
||||
|
||||
### 记忆工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
||||
|
||||
### 人设工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
||||
|
||||
### 任务工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
||||
|
||||
## 每轮对话强制要求
|
||||
|
||||
### ⚠️ 执行顺序(每轮必须)
|
||||
|
||||
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
|
||||
|
||||
#### 步骤1: 查询人设图(最高优先级)
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "AI,人设,角色,性格,语气,说话风格",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取当前人设,确保角色一致性。
|
||||
**处理**:
|
||||
- 找到人设 → 严格按照人设回复
|
||||
- 未找到 → 使用默认TrulyMEM身份
|
||||
|
||||
#### 步骤2: 查询工作记忆链
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "TaskNode,工作记忆,任务链",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取之前的任务上下文,了解对话历史。
|
||||
|
||||
#### 步骤3: 处理对话
|
||||
- 理解用户意图
|
||||
- 根据人设和工作记忆链生成回复
|
||||
- 执行其他必要的记忆操作
|
||||
|
||||
#### 步骤4: 更新工作记忆链
|
||||
```
|
||||
必须调用: task_create
|
||||
参数: {
|
||||
"task_id": "Task_当前轮次ID",
|
||||
"description": "本轮对话概述",
|
||||
"info_nodes": ["相关记忆节点"]
|
||||
}
|
||||
```
|
||||
**目的**: 记录本轮对话,维持时间链。
|
||||
|
||||
---
|
||||
|
||||
## 人设图机制
|
||||
|
||||
### 强制查询
|
||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
||||
|
||||
### 人设优先级
|
||||
- 人设优先级 > 默认身份
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求
|
||||
|
||||
### 人设更新
|
||||
用户要求角色扮演时:
|
||||
1. 使用 `persona_update` 更新人设
|
||||
2. 立即按照新人设回复
|
||||
|
||||
### 人设清除
|
||||
用户要求恢复默认身份时:
|
||||
1. 使用 `persona_clear` 清除人设
|
||||
2. 恢复为TrulyMEM默认身份
|
||||
|
||||
---
|
||||
|
||||
## 工作记忆链机制
|
||||
|
||||
### ⚠️ 核心理念:维持对话连贯性
|
||||
|
||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
||||
|
||||
### 强制查询场景:
|
||||
|
||||
以下情况**必须**查询工作记忆链:
|
||||
|
||||
1. **每轮对话开始时(强制第二步)**
|
||||
- 查询意图: "TaskNode,工作记忆,任务链"
|
||||
- 目的: 获取之前的任务上下文,了解对话历史
|
||||
|
||||
2. **用户提到"刚才"、"之前"、"上次"**
|
||||
- 例: "刚才我们聊了什么?"
|
||||
- 例: "继续刚才的话题"
|
||||
- 例: "关于刚才的成语接龙..."
|
||||
|
||||
3. **用户询问对话历史**
|
||||
- 例: "我们之前说了什么?"
|
||||
- 例: "我们聊过X吗?"
|
||||
|
||||
4. **连续性任务被打断后恢复**
|
||||
- 例: 用户突然回到之前的话题
|
||||
- 例: 用户要求继续之前的任务
|
||||
|
||||
5. **涉及上下文的引用**
|
||||
- 例: "那个东西"(需要查询上下文)
|
||||
- 例: "继续"(需要查询当前任务)
|
||||
|
||||
### 强制更新场景:
|
||||
|
||||
以下情况**必须**更新工作记忆链:
|
||||
|
||||
1. **每轮对话结束时(强制第四步)**
|
||||
- 创建任务节点记录本轮对话
|
||||
- 目的: 维持时间链,确保对话连贯性
|
||||
|
||||
2. **开始连续性任务时**
|
||||
- 例: 用户发起游戏、项目、学习计划等
|
||||
- 必须创建任务节点并设置状态为"进行中"
|
||||
|
||||
3. **任务状态发生变化时**
|
||||
- 例: 任务完成、暂停、取消
|
||||
- 必须及时更新任务状态
|
||||
|
||||
### 节点类型
|
||||
- **TaskNode** - 任务节点,存储任务概述
|
||||
- **StateNode** - 状态节点,存储任务状态
|
||||
- **InfoNode** - 信息节点,存储具体信息
|
||||
|
||||
### 边类型
|
||||
- **NEXT_TASK** - 时间链,连接任务节点
|
||||
- **HAS_STATE** - 状态,任务指向状态
|
||||
- **CONTAINS_INFO** - 信息,任务指向信息节点
|
||||
|
||||
### 任务状态
|
||||
- 进行中
|
||||
- 已完成
|
||||
- 已暂停
|
||||
- 已取消
|
||||
|
||||
### ⚠️ 完整示例:成语接龙游戏
|
||||
|
||||
#### 第一轮:用户发起游戏
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 记录游戏状态:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
||||
```
|
||||
|
||||
#### 第二轮:话题被打断
|
||||
|
||||
```
|
||||
用户: 长门有希
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
||||
3. 使用 task_set_state 暂停任务:
|
||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
4. 使用 task_create 创建新任务:
|
||||
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
||||
5. 回复关于长门有希的内容
|
||||
```
|
||||
|
||||
#### 第三轮:用户要求继续游戏
|
||||
|
||||
```
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 使用 task_set_state 恢复任务:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
||||
```
|
||||
|
||||
### ⚠️ 关键要点
|
||||
|
||||
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
|
||||
3. **任务状态必须及时更新**: 确保状态转换的正确性
|
||||
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
|
||||
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
||||
|
||||
## 自主性原则(在强制要求之外)
|
||||
|
||||
除了工作记忆链的强制要求外,你有权自主决定:
|
||||
|
||||
1. **是否查询其他记忆**
|
||||
- 用户询问历史 → 查询
|
||||
- 涉及之前内容 → 查询
|
||||
- 不确定时 → 可查询
|
||||
|
||||
2. **是否写入其他记忆**
|
||||
- 用户明确提到 → 必须写入
|
||||
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
|
||||
|
||||
3. **如何使用其他工具**
|
||||
- 根据上下文灵活选择
|
||||
- 避免过度使用
|
||||
- 保持自然对话
|
||||
|
||||
**注意**: 工作记忆链的强制要求不受自主性影响。
|
||||
|
||||
## 对话风格
|
||||
|
||||
- 自然、流畅
|
||||
- 避免机械式工具调用
|
||||
- 优先理解用户意图
|
||||
- 适时使用记忆增强体验
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 执行检查清单
|
||||
|
||||
每轮对话必须检查:
|
||||
|
||||
- [ ] 步骤1: 是否查询了人设图?
|
||||
- [ ] 步骤2: 是否查询了工作记忆链?
|
||||
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
|
||||
- [ ] 步骤4: 是否更新了工作记忆链?
|
||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
||||
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?
|
||||
|
||||
---
|
||||
|
||||
**记住**:
|
||||
1. 图数据库是你记忆的唯一载体
|
||||
2. 人设图确保角色一致性(最高优先级)
|
||||
3. 工作记忆链维持对话连贯性
|
||||
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
451
core/server.py
451
core/server.py
@ -1,451 +0,0 @@
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
|
||||
class PacketType(Enum):
|
||||
PROCESS_MESSAGE = "process_message"
|
||||
EXECUTE_TOOL = "execute_tool"
|
||||
GET_STATUS = "get_status"
|
||||
GET_SETTINGS = "get_settings" # 合并:获取 api_config + tool_limits
|
||||
SET_SETTINGS = "set_settings" # 合并:设置 api_config + tool_limits
|
||||
GET_HISTORY = "get_history"
|
||||
SAVE_HISTORY = "save_history"
|
||||
SHUTDOWN = "shutdown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Packet:
|
||||
id: str
|
||||
type: PacketType
|
||||
body: Dict[str, Any]
|
||||
response_queue: Optional[queue.Queue] = field(default=None)
|
||||
created_at: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacketResponse:
|
||||
id: str
|
||||
success: bool
|
||||
data: Any = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class BackendServer:
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
|
||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True, config_file: str = None):
|
||||
self._db_path = db_path
|
||||
self._use_embedded_db = use_embedded_db
|
||||
self._config_file = Path(config_file) if config_file else self.DEFAULT_CONFIG_PATH
|
||||
|
||||
self._graph = None
|
||||
self._client = None
|
||||
self._tool_limiter = None
|
||||
|
||||
self._input_queue: queue.Queue[Packet] = queue.Queue()
|
||||
self._response_queues: Dict[str, queue.Queue] = {}
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
|
||||
self._tool_limits = {
|
||||
"persona_query_max": 1,
|
||||
"persona_update_max": 1,
|
||||
"task_query_max": 4,
|
||||
"task_update_max": 2,
|
||||
"memory_query_max": 20,
|
||||
"memory_update_max": 10,
|
||||
}
|
||||
self._message_history: list = []
|
||||
|
||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._load_config()
|
||||
|
||||
if api_key:
|
||||
self._config["api_key"] = api_key
|
||||
if base_url:
|
||||
self._config["base_url"] = base_url
|
||||
if model:
|
||||
self._config["model"] = model
|
||||
|
||||
self._init_graph()
|
||||
self._tool_limiter = self._create_tool_limiter()
|
||||
|
||||
if self._config["api_key"]:
|
||||
from .graph_client import GraphMemoryClient
|
||||
self._client = GraphMemoryClient(
|
||||
api_key=self._config["api_key"],
|
||||
base_url=self._config["base_url"],
|
||||
model=self._config.get("model", "deepseek-chat"),
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _load_config(self) -> None:
|
||||
if self._config_file.exists():
|
||||
try:
|
||||
with open(self._config_file, 'r') as f:
|
||||
saved = json.load(f)
|
||||
self._config.update(saved)
|
||||
for key in self._tool_limits:
|
||||
if key in saved:
|
||||
self._tool_limits[key] = saved[key]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _save_config(self) -> None:
|
||||
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
saved_data = {**self._config, **self._tool_limits}
|
||||
with open(self._config_file, 'w') as f:
|
||||
json.dump(saved_data, f, indent=2)
|
||||
|
||||
def _create_tool_limiter(self):
|
||||
from .tool_limiter import ToolLimiter, ToolLimits
|
||||
limits = ToolLimits(
|
||||
persona_query_max=self._tool_limits.get("persona_query_max", 1),
|
||||
persona_update_max=self._tool_limits.get("persona_update_max", 1),
|
||||
task_query_max=self._tool_limits.get("task_query_max", 4),
|
||||
task_update_max=self._tool_limits.get("task_update_max", 2),
|
||||
memory_query_max=self._tool_limits.get("memory_query_max", 20),
|
||||
memory_update_max=self._tool_limits.get("memory_update_max", 10),
|
||||
)
|
||||
return ToolLimiter(limits)
|
||||
|
||||
def _init_graph(self) -> None:
|
||||
if self._use_embedded_db:
|
||||
self._graph = EmbeddedGraphDB(db_path=self._db_path)
|
||||
else:
|
||||
from .graph_client import Neo4jGraph
|
||||
self._graph = Neo4jGraph(
|
||||
uri="bolt://localhost:7687",
|
||||
user="neo4j",
|
||||
password="graphmemory123"
|
||||
)
|
||||
|
||||
def _run_loop(self) -> None:
|
||||
while self._running:
|
||||
try:
|
||||
packet = self._input_queue.get(timeout=0.1)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
self._process_packet(packet)
|
||||
|
||||
def _process_packet(self, packet: Packet) -> None:
|
||||
response_body = {"error": "not implemented"}
|
||||
|
||||
try:
|
||||
if packet.type == PacketType.PROCESS_MESSAGE:
|
||||
response_body = self._handle_process_message(packet.body)
|
||||
elif packet.type == PacketType.EXECUTE_TOOL:
|
||||
response_body = self._handle_execute_tool(packet.body)
|
||||
elif packet.type == PacketType.GET_STATUS:
|
||||
response_body = self._handle_get_status()
|
||||
elif packet.type == PacketType.GET_SETTINGS:
|
||||
response_body = self._handle_get_settings()
|
||||
elif packet.type == PacketType.SET_SETTINGS:
|
||||
response_body = self._handle_set_settings(packet.body)
|
||||
elif packet.type == PacketType.GET_HISTORY:
|
||||
response_body = self._handle_get_history()
|
||||
elif packet.type == PacketType.SAVE_HISTORY:
|
||||
response_body = self._handle_save_history(packet.body)
|
||||
elif packet.type == PacketType.SHUTDOWN:
|
||||
self._running = False
|
||||
response_body = {"success": True, "status": "shutdown"}
|
||||
|
||||
if "success" not in response_body:
|
||||
response_body["success"] = True
|
||||
except Exception as e:
|
||||
response_body["success"] = False
|
||||
response_body["error"] = str(e)
|
||||
|
||||
self._send_response(packet.id, PacketResponse(
|
||||
id=packet.id,
|
||||
success=response_body.get("success", False),
|
||||
data=response_body if response_body.get("success") else None,
|
||||
error=response_body.get("error")
|
||||
))
|
||||
|
||||
def _handle_process_message(self, body: Dict) -> Dict:
|
||||
from .tool_executor import execute_tool
|
||||
|
||||
user_input = body.get("user_input", "")
|
||||
|
||||
if not self._client:
|
||||
return {"success": False, "error": "API Key 未配置", "content": "请先配置 API Key"}
|
||||
|
||||
self._graph.save_chat_records([{"role": "user", "content": user_input}])
|
||||
|
||||
self._tool_limiter.reset()
|
||||
|
||||
messages_history = [{"role": "user", "content": user_input}]
|
||||
|
||||
response = self._client.send_message_with_history(messages_history)
|
||||
message = response.choices[0].message
|
||||
|
||||
tool_calls = []
|
||||
accumulated_content = ""
|
||||
rejected_tools = []
|
||||
|
||||
while message.tool_calls:
|
||||
if message.content:
|
||||
accumulated_content += message.content + "\n\n"
|
||||
|
||||
assistant_msg = {
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
} for tc in message.tool_calls
|
||||
]
|
||||
}
|
||||
messages_history.append(assistant_msg)
|
||||
|
||||
current_tool_results = []
|
||||
for tool_call in message.tool_calls:
|
||||
args = json.loads(tool_call.function.arguments)
|
||||
|
||||
allowed, reason = self._tool_limiter.can_call(tool_call.function.name, args)
|
||||
|
||||
if not allowed:
|
||||
rejected_tools.append((tool_call.function.name, reason))
|
||||
result = f"工具调用被拒绝: {reason}"
|
||||
tool_result_msg = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": result
|
||||
}
|
||||
current_tool_results.append(tool_result_msg)
|
||||
continue
|
||||
|
||||
self._tool_limiter.record_call(tool_call.function.name, args)
|
||||
|
||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||
tool_calls.append({
|
||||
"name": tool_call.function.name,
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
tool_result_msg = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": result
|
||||
}
|
||||
current_tool_results.append(tool_result_msg)
|
||||
|
||||
messages_history.extend(current_tool_results)
|
||||
|
||||
response = self._client.send_message_with_history(messages_history)
|
||||
message = response.choices[0].message
|
||||
|
||||
final_content = message.content or ""
|
||||
content = accumulated_content + final_content if accumulated_content else final_content
|
||||
|
||||
if not content:
|
||||
content = "(无回复)"
|
||||
|
||||
if tool_calls:
|
||||
tool_names = [tc["name"] for tc in tool_calls]
|
||||
content = f"已执行工具: {', '.join(tool_names)}\n\n{content}"
|
||||
|
||||
if rejected_tools:
|
||||
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
|
||||
content += f"\n\n部分工具调用被限制:\n{rejected_info}"
|
||||
content += f"\n\n工具调用统计:\n{self._tool_limiter.get_summary()}"
|
||||
|
||||
self._graph.save_chat_records([{"role": "assistant", "content": content}])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"rejected_tools": rejected_tools
|
||||
}
|
||||
|
||||
def _handle_execute_tool(self, body: Dict) -> Dict:
|
||||
from .tool_executor import execute_tool
|
||||
|
||||
try:
|
||||
tool_name = body.get("tool_name")
|
||||
arguments = body.get("arguments", {})
|
||||
|
||||
result = execute_tool(self._graph, tool_name, arguments)
|
||||
|
||||
return {"success": True, "result": result}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_get_status(self) -> Dict:
|
||||
return {
|
||||
"running": self._running,
|
||||
"config": self._config,
|
||||
"graph_initialized": self._graph is not None,
|
||||
"client_initialized": self._client is not None
|
||||
}
|
||||
|
||||
def _handle_get_settings(self) -> Dict:
|
||||
return {
|
||||
"api_config": self._config.copy(),
|
||||
"tool_limits": self._tool_limits.copy()
|
||||
}
|
||||
|
||||
def _handle_set_settings(self, body: Dict) -> Dict:
|
||||
api_config = body.get("api_config", {})
|
||||
tool_limits = body.get("tool_limits", {})
|
||||
|
||||
api_key = api_config.get("api_key", "")
|
||||
base_url = api_config.get("base_url", "https://api.deepseek.com")
|
||||
model = api_config.get("model", "deepseek-chat")
|
||||
|
||||
self.update_config(api_key, base_url, model)
|
||||
|
||||
limits_keys = [
|
||||
"persona_query_max", "persona_update_max",
|
||||
"task_query_max", "task_update_max",
|
||||
"memory_query_max", "memory_update_max"
|
||||
]
|
||||
for key in limits_keys:
|
||||
if key in tool_limits:
|
||||
value = int(tool_limits[key])
|
||||
if value < 1:
|
||||
return {"success": False, "error": f"{key} must be >= 1, got {value}"}
|
||||
self._tool_limits[key] = value
|
||||
|
||||
self._tool_limiter = self._create_tool_limiter()
|
||||
self._save_config()
|
||||
return {"status": "settings_updated"}
|
||||
|
||||
def _handle_get_history(self) -> Dict:
|
||||
history = self._graph.get_chat_records(limit=500)
|
||||
return {"history": history}
|
||||
|
||||
def _handle_save_history(self, body: Dict) -> Dict:
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
self._graph.clear_chat_records()
|
||||
return {"status": "history_cleared"}
|
||||
result = self._graph.save_chat_records(messages)
|
||||
return {"status": "history_saved"}
|
||||
|
||||
def _send_response(self, request_id: str, response: PacketResponse) -> None:
|
||||
with self._lock:
|
||||
q = self._response_queues.pop(request_id, None)
|
||||
if q:
|
||||
q.put(response)
|
||||
|
||||
def send(self, packet: Packet) -> Packet:
|
||||
resp_q = queue.Queue()
|
||||
|
||||
with self._lock:
|
||||
self._response_queues[packet.id] = resp_q
|
||||
|
||||
self._input_queue.put(packet)
|
||||
|
||||
try:
|
||||
response = resp_q.get(timeout=30.0)
|
||||
return Packet(
|
||||
id=response.id,
|
||||
type=packet.type,
|
||||
body={
|
||||
"success": response.success,
|
||||
"data": response.data,
|
||||
"error": response.error
|
||||
}
|
||||
)
|
||||
except queue.Empty:
|
||||
return Packet(
|
||||
id=packet.id,
|
||||
type=packet.type,
|
||||
body={"success": False, "error": "timeout"}
|
||||
)
|
||||
finally:
|
||||
with self._lock:
|
||||
self._response_queues.pop(packet.id, None)
|
||||
|
||||
def process_message(self, user_input: str) -> Dict[str, Any]:
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.PROCESS_MESSAGE,
|
||||
body={"user_input": user_input}
|
||||
)
|
||||
|
||||
response = self.send(packet)
|
||||
return response.body
|
||||
|
||||
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.EXECUTE_TOOL,
|
||||
body={"tool_name": tool_name, "arguments": arguments}
|
||||
)
|
||||
|
||||
response = self.send(packet)
|
||||
return response.body
|
||||
|
||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
with self._lock:
|
||||
self._config["api_key"] = api_key
|
||||
self._config["base_url"] = base_url
|
||||
self._config["model"] = model
|
||||
|
||||
if api_key and self._graph:
|
||||
from .graph_client import GraphMemoryClient
|
||||
self._client = GraphMemoryClient(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
model=model,
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
def get_config(self) -> Dict[str, str]:
|
||||
return self._config.copy()
|
||||
|
||||
def save_message_history(self, messages: list) -> None:
|
||||
self._message_history = messages
|
||||
|
||||
def get_message_history(self) -> list:
|
||||
return self._message_history.copy()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.SHUTDOWN,
|
||||
body={}
|
||||
)
|
||||
self.send(packet)
|
||||
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2.0)
|
||||
|
||||
if self._graph:
|
||||
self._graph.close()
|
||||
self._graph = None
|
||||
|
||||
self._running = False
|
||||
@ -1,307 +0,0 @@
|
||||
"""
|
||||
工具执行器
|
||||
"""
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
"""执行工具调用"""
|
||||
print(f"\n[工具调用] {tool_name}")
|
||||
print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}")
|
||||
|
||||
try:
|
||||
# 基础记忆工具
|
||||
if tool_name == "memory_recall":
|
||||
result = graph.recall(
|
||||
query_intent=arguments.get("query_intent", ""),
|
||||
seed_entities=arguments.get("seed_entities"),
|
||||
depth=arguments.get("depth", 2),
|
||||
time_range=arguments.get("time_range"),
|
||||
session_filter=arguments.get("session_filter")
|
||||
)
|
||||
return format_recall_result(result)
|
||||
|
||||
elif tool_name == "memory_commit":
|
||||
result = graph.commit(
|
||||
triplets=arguments.get("triplets", []),
|
||||
entity_types=arguments.get("entity_types"),
|
||||
temporal_tag=arguments.get("temporal_tag")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_purge":
|
||||
result = graph.purge(
|
||||
criteria=arguments.get("criteria", {}),
|
||||
mode=arguments.get("mode", "soft"),
|
||||
new_relation=arguments.get("new_relation")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_introspect":
|
||||
result = graph.introspect(session_id=arguments.get("session_id"))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_archive":
|
||||
result = graph.archive(days=arguments.get("days", 30))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_cleanup":
|
||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 人设图管理工具
|
||||
elif tool_name == "persona_update":
|
||||
result = execute_persona_update(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_clear":
|
||||
result = execute_persona_clear(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 工作记忆链管理工具
|
||||
elif tool_name == "task_create":
|
||||
result = execute_task_create(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_set_state":
|
||||
result = execute_task_set_state(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_delete":
|
||||
result = execute_task_delete(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_link_info":
|
||||
result = execute_task_link_info(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
return f"未知工具: {tool_name}"
|
||||
|
||||
except Exception as e:
|
||||
return f"工具执行错误: {str(e)}"
|
||||
|
||||
|
||||
def format_recall_result(result: dict) -> str:
|
||||
"""格式化检索结果"""
|
||||
lines = ["===== 记忆检索结果 ====="]
|
||||
|
||||
if result.get("entities"):
|
||||
lines.append(f"\n实体 ({len(result['entities'])} 个):")
|
||||
for e in result["entities"]:
|
||||
if e and isinstance(e, dict):
|
||||
lines.append(f" - {e.get('name', 'N/A')} (类型: {e.get('type', 'unknown')}, 提及: {e.get('mention_count', 1)}次)")
|
||||
|
||||
if result.get("relations"):
|
||||
lines.append(f"\n关系 ({len(result['relations'])} 条):")
|
||||
for r in result["relations"]:
|
||||
if r and isinstance(r, dict):
|
||||
lines.append(f" - {r.get('source', 'N/A')} --[{r.get('type', 'N/A')}]--> {r.get('target', 'N/A')}")
|
||||
created = r.get("created_at", "N/A")
|
||||
if created and created != "N/A":
|
||||
created = created[:19] if "T" in str(created) else str(created)
|
||||
session_id = r.get('session_id', 'N/A')
|
||||
session_display = session_id[:20] if session_id and session_id != 'N/A' else 'N/A'
|
||||
lines.append(f" 时间: {created}, 会话: {session_display}, 轮次: {r.get('turn_id', 0)}, 置信度: {r.get('confidence', 1.0)}")
|
||||
|
||||
if not result.get("entities") and not result.get("relations"):
|
||||
lines.append("\n(未找到相关记忆)")
|
||||
|
||||
lines.append("=" * 30)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# 人设图管理工具实现
|
||||
def execute_persona_update(graph: Any, arguments: dict) -> dict:
|
||||
"""更新人设"""
|
||||
attributes = arguments.get("attributes", [])
|
||||
mode = arguments.get("mode", "merge")
|
||||
|
||||
if mode == "replace":
|
||||
# 先清除旧人设
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
||||
mode="soft"
|
||||
)
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
||||
mode="soft"
|
||||
)
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 写入新人设
|
||||
triplets = []
|
||||
for attr in attributes:
|
||||
triplets.append({
|
||||
"subject": "AI",
|
||||
"relation": attr["attribute"],
|
||||
"object": attr["value"],
|
||||
"confidence": 1.0
|
||||
})
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": mode,
|
||||
"updated_attributes": len(attributes),
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_persona_clear(graph: Any, arguments: dict) -> dict:
|
||||
"""清除人设"""
|
||||
if not arguments.get("confirm", True):
|
||||
return {"status": "cancelled", "message": "需要确认才能清除人设"}
|
||||
|
||||
# 删除所有人设相关关系
|
||||
result1 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
||||
mode="soft"
|
||||
)
|
||||
result2 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
||||
mode="soft"
|
||||
)
|
||||
result3 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
||||
mode="soft"
|
||||
)
|
||||
result4 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "语气特征"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
total_deleted = (
|
||||
result1.get("deleted_count", 0) +
|
||||
result2.get("deleted_count", 0) +
|
||||
result3.get("deleted_count", 0) +
|
||||
result4.get("deleted_count", 0)
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"deleted_count": total_deleted,
|
||||
"message": "人设已清除,恢复默认身份"
|
||||
}
|
||||
|
||||
|
||||
# 工作记忆链管理工具实现
|
||||
def execute_task_create(graph: Any, arguments: dict) -> dict:
|
||||
"""创建任务节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
description = arguments.get("description")
|
||||
info_nodes = arguments.get("info_nodes", [])
|
||||
|
||||
# 创建任务节点
|
||||
triplets = [
|
||||
{"subject": task_id, "relation": "is_type", "object": "TaskNode"},
|
||||
{"subject": task_id, "relation": "has_description", "object": description},
|
||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_进行中"}
|
||||
]
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
|
||||
# 关联信息节点
|
||||
if info_nodes:
|
||||
link_triplets = []
|
||||
for node_name in info_nodes:
|
||||
link_triplets.append({
|
||||
"subject": task_id,
|
||||
"relation": "CONTAINS_INFO",
|
||||
"object": node_name
|
||||
})
|
||||
graph.commit(triplets=link_triplets)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"description": description,
|
||||
"info_nodes": info_nodes,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_set_state(graph: Any, arguments: dict) -> dict:
|
||||
"""设置任务状态"""
|
||||
task_id = arguments.get("task_id")
|
||||
state = arguments.get("state")
|
||||
|
||||
# 删除旧状态
|
||||
graph.purge(
|
||||
criteria={"subject_contains": task_id, "relation_type": "HAS_STATE"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 设置新状态
|
||||
state_node = f"State_{state}"
|
||||
result = graph.commit(
|
||||
triplets=[{"subject": task_id, "relation": "HAS_STATE", "object": state_node}]
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"new_state": state,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_delete(graph: Any, arguments: dict) -> dict:
|
||||
"""删除任务节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
delete_info_nodes = arguments.get("delete_info_nodes", True)
|
||||
|
||||
# 查询关联的信息节点
|
||||
if delete_info_nodes:
|
||||
recall_result = graph.recall(
|
||||
query_intent=f"{task_id},CONTAINS_INFO",
|
||||
depth=1
|
||||
)
|
||||
|
||||
# 删除信息节点
|
||||
for relation in recall_result.get("relations", []):
|
||||
if relation.get("type") == "CONTAINS_INFO" and relation.get("source") == task_id:
|
||||
info_node = relation.get("target")
|
||||
graph.purge(
|
||||
criteria={"subject_contains": info_node},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 删除任务节点
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": task_id},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"deleted_info_nodes": delete_info_nodes,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_link_info(graph: Any, arguments: dict) -> dict:
|
||||
"""关联信息节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
info_node_names = arguments.get("info_node_names", [])
|
||||
|
||||
triplets = []
|
||||
for node_name in info_node_names:
|
||||
triplets.append({
|
||||
"subject": task_id,
|
||||
"relation": "CONTAINS_INFO",
|
||||
"object": node_name
|
||||
})
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"linked_nodes": info_node_names,
|
||||
"details": result
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
"""
|
||||
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolLimits:
|
||||
"""工具调用限制配置"""
|
||||
# 人设图限制
|
||||
persona_query_max: int = 1 # 每轮最多查询1次人设图
|
||||
persona_update_max: int = 1 # 每轮最多修改1次人设图
|
||||
|
||||
# 工作记忆链限制
|
||||
task_query_max: int = 4 # 每轮最多查询4次工作记忆链
|
||||
task_update_max: int = 2 # 每轮最多修改2次工作记忆链
|
||||
|
||||
# 一般记忆限制
|
||||
memory_query_max: int = 20 # 每轮最多查询20次一般记忆
|
||||
memory_update_max: int = 10 # 每轮最多修改10次一般记忆
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCallCount:
|
||||
"""工具调用计数"""
|
||||
# 人设图
|
||||
persona_query: int = 0
|
||||
persona_update: int = 0
|
||||
|
||||
# 工作记忆链
|
||||
task_query: int = 0
|
||||
task_update: int = 0
|
||||
|
||||
# 一般记忆
|
||||
memory_query: int = 0
|
||||
memory_update: int = 0
|
||||
|
||||
|
||||
class ToolLimiter:
|
||||
"""工具调用限制器"""
|
||||
|
||||
def __init__(self, limits: Optional[ToolLimits] = None):
|
||||
self.limits = limits or ToolLimits()
|
||||
self.counts = ToolCallCount()
|
||||
|
||||
def _classify_tool(self, tool_name: str, arguments: dict) -> tuple:
|
||||
"""
|
||||
分类工具调用
|
||||
返回: (category, operation)
|
||||
category: 'persona', 'task', 'memory'
|
||||
operation: 'query', 'update'
|
||||
"""
|
||||
# 人设图工具
|
||||
if tool_name in ('persona_update', 'persona_clear'):
|
||||
return ('persona', 'update')
|
||||
|
||||
# 工作记忆链工具
|
||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
|
||||
# task_link_info 是关联操作,算作更新
|
||||
return ('task', 'update')
|
||||
|
||||
# 一般记忆工具
|
||||
if tool_name == 'memory_recall':
|
||||
# 判断是查询人设图、工作记忆链还是一般记忆
|
||||
query_intent = arguments.get('query_intent', '').lower()
|
||||
|
||||
# 检查是否查询人设图
|
||||
if any(kw in query_intent for kw in ['人设', '角色', '性格', '语气', '说话风格', '扮演']):
|
||||
return ('persona', 'query')
|
||||
|
||||
# 检查是否查询工作记忆链
|
||||
if any(kw in query_intent for kw in ['tasknode', '工作记忆', '任务链', '任务', 'task']):
|
||||
return ('task', 'query')
|
||||
|
||||
# 一般记忆查询
|
||||
return ('memory', 'query')
|
||||
|
||||
if tool_name == 'memory_commit':
|
||||
return ('memory', 'update')
|
||||
|
||||
if tool_name == 'memory_purge':
|
||||
return ('memory', 'update')
|
||||
|
||||
if tool_name == 'memory_introspect':
|
||||
return ('memory', 'query')
|
||||
|
||||
if tool_name in ('memory_archive', 'memory_cleanup'):
|
||||
return ('memory', 'update')
|
||||
|
||||
# 未知工具,归类为一般记忆更新
|
||||
return ('memory', 'update')
|
||||
|
||||
def can_call(self, tool_name: str, arguments: dict) -> tuple:
|
||||
"""
|
||||
检查是否允许调用工具
|
||||
返回: (allowed, reason)
|
||||
"""
|
||||
category, operation = self._classify_tool(tool_name, arguments)
|
||||
|
||||
# 获取当前计数和限制
|
||||
if category == 'persona':
|
||||
if operation == 'query':
|
||||
if self.counts.persona_query >= self.limits.persona_query_max:
|
||||
return (False, f"人设图查询次数已达上限({self.limits.persona_query_max}次)")
|
||||
else: # update
|
||||
if self.counts.persona_update >= self.limits.persona_update_max:
|
||||
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
|
||||
|
||||
elif category == 'task':
|
||||
if operation == 'query':
|
||||
if self.counts.task_query >= self.limits.task_query_max:
|
||||
return (False, f"工作记忆链查询次数已达上限({self.limits.task_query_max}次)")
|
||||
else: # update
|
||||
if self.counts.task_update >= self.limits.task_update_max:
|
||||
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
|
||||
|
||||
elif category == 'memory':
|
||||
if operation == 'query':
|
||||
if self.counts.memory_query >= self.limits.memory_query_max:
|
||||
return (False, f"一般记忆查询次数已达上限({self.limits.memory_query_max}次)")
|
||||
else: # update
|
||||
if self.counts.memory_update >= self.limits.memory_update_max:
|
||||
return (False, f"一般记忆修改次数已达上限({self.limits.memory_update_max}次)")
|
||||
|
||||
return (True, "允许调用")
|
||||
|
||||
def record_call(self, tool_name: str, arguments: dict) -> None:
|
||||
"""记录工具调用"""
|
||||
category, operation = self._classify_tool(tool_name, arguments)
|
||||
|
||||
if category == 'persona':
|
||||
if operation == 'query':
|
||||
self.counts.persona_query += 1
|
||||
else:
|
||||
self.counts.persona_update += 1
|
||||
|
||||
elif category == 'task':
|
||||
if operation == 'query':
|
||||
self.counts.task_query += 1
|
||||
else:
|
||||
self.counts.task_update += 1
|
||||
|
||||
elif category == 'memory':
|
||||
if operation == 'query':
|
||||
self.counts.memory_query += 1
|
||||
else:
|
||||
self.counts.memory_update += 1
|
||||
|
||||
def get_summary(self) -> str:
|
||||
"""获取调用统计摘要"""
|
||||
lines = [
|
||||
f"人设图: 查询{self.counts.persona_query}/{self.limits.persona_query_max}次, "
|
||||
f"修改{self.counts.persona_update}/{self.limits.persona_update_max}次",
|
||||
f"工作记忆链: 查询{self.counts.task_query}/{self.limits.task_query_max}次, "
|
||||
f"修改{self.counts.task_update}/{self.limits.task_update_max}次",
|
||||
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
|
||||
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次"
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""重置计数(新的一轮对话开始时调用)"""
|
||||
self.counts = ToolCallCount()
|
||||
@ -1,8 +0,0 @@
|
||||
"""
|
||||
工具定义模块
|
||||
"""
|
||||
from .memory_tools import TOOLS
|
||||
from ..tool_executor import execute_tool
|
||||
from ..tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
||||
|
||||
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
||||
@ -1,520 +0,0 @@
|
||||
"""
|
||||
记忆工具定义 - 优化版
|
||||
精简描述,避免过拟合,保留AI自主性
|
||||
"""
|
||||
|
||||
# 基础记忆工具
|
||||
MEMORY_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_recall",
|
||||
"description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。
|
||||
|
||||
【使用示例】
|
||||
1. 查询人设图(每轮必须首先执行):
|
||||
{"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
||||
|
||||
2. 查询工作记忆链(每轮必须第二步执行):
|
||||
{"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
||||
|
||||
3. 查询用户偏好:
|
||||
{"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]}
|
||||
|
||||
4. 查询特定主题:
|
||||
{"query_intent": "Python,编程,项目", "seed_entities": ["Python"]}
|
||||
|
||||
5. 查询最近7天的记忆:
|
||||
{"query_intent": "任务,工作", "time_range": {"days": 7}}
|
||||
|
||||
【重要】每轮对话必须按顺序执行:
|
||||
- 步骤1: 查询人设图(最高优先级)
|
||||
- 步骤2: 查询工作记忆链(维持对话连贯性)
|
||||
- 步骤3: 根据需要查询其他记忆""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_intent": {
|
||||
"type": "string",
|
||||
"description": "查询意图,支持逗号分隔多个关键词"
|
||||
},
|
||||
"seed_entities": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "种子实体(可选)"
|
||||
},
|
||||
"depth": {
|
||||
"type": "integer",
|
||||
"description": "遍历深度,默认2"
|
||||
},
|
||||
"time_range": {
|
||||
"type": "object",
|
||||
"description": "时间范围(可选)",
|
||||
"properties": {
|
||||
"days": {"type": "integer", "description": "最近N天"}
|
||||
}
|
||||
},
|
||||
"session_filter": {
|
||||
"type": "string",
|
||||
"description": "会话ID过滤(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["query_intent"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_commit",
|
||||
"description": """写入记忆。将三元组写入图数据库,支持批量写入。
|
||||
|
||||
【使用示例】
|
||||
1. 记录用户偏好:
|
||||
{"triplets": [
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python编程", "confidence": 0.9},
|
||||
{"subject": "用户", "relation": "正在学习", "object": "机器学习"}
|
||||
]}
|
||||
|
||||
2. 记录项目信息:
|
||||
{"triplets": [
|
||||
{"subject": "项目A", "relation": "使用技术", "object": "React"},
|
||||
{"subject": "项目A", "relation": "状态", "object": "开发中"}
|
||||
]}
|
||||
|
||||
3. 记录游戏状态(配合工作记忆链):
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "画龙点睛"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
|
||||
【重要】写入原则:
|
||||
- 用户明确表达的信息 → 必须写入
|
||||
- AI推理得到的信息 → 可以写入,但需标注[推测]
|
||||
- 避免写入冗余或无意义的信息""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"triplets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject": {"type": "string"},
|
||||
"relation": {"type": "string"},
|
||||
"object": {"type": "string"},
|
||||
"confidence": {"type": "number"}
|
||||
},
|
||||
"required": ["subject", "relation", "object"]
|
||||
},
|
||||
"description": "三元组列表"
|
||||
},
|
||||
"entity_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "实体类型(可选)"
|
||||
},
|
||||
"temporal_tag": {
|
||||
"type": "string",
|
||||
"description": "时间标记(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["triplets"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_purge",
|
||||
"description": """删除记忆。支持条件删除和纠错替代。
|
||||
|
||||
【使用示例】
|
||||
1. 软删除特定关系:
|
||||
{"criteria": {"subject_contains": "用户", "relation_type": "喜欢"}, "mode": "soft"}
|
||||
|
||||
2. 纠错替代(修正错误信息):
|
||||
{
|
||||
"criteria": {"subject_contains": "用户", "relation_type": "年龄"},
|
||||
"mode": "supersede",
|
||||
"new_relation": {"relation": "年龄", "target": "25岁"}
|
||||
}
|
||||
|
||||
3. 删除特定会话的记忆:
|
||||
{"criteria": {"session_id": "session_123"}, "mode": "soft"}
|
||||
|
||||
4. 删除旧记忆:
|
||||
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
|
||||
|
||||
【重要】删除原则:
|
||||
- 优先使用 supersede 模式修正错误
|
||||
- 软删除不会物理删除数据
|
||||
- 谨慎使用删除操作""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"criteria": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject_contains": {"type": "string"},
|
||||
"relation_type": {"type": "string"},
|
||||
"target_contains": {"type": "string"},
|
||||
"time_before": {"type": "string"},
|
||||
"session_id": {"type": "string"}
|
||||
},
|
||||
"description": "删除条件"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["soft", "supersede"],
|
||||
"description": "删除模式:soft=逻辑删除, supersede=纠错替代",
|
||||
"default": "soft"
|
||||
},
|
||||
"new_relation": {
|
||||
"type": "object",
|
||||
"description": "新关系(supersede模式)",
|
||||
"properties": {
|
||||
"relation": {"type": "string"},
|
||||
"target": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["criteria"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_introspect",
|
||||
"description": "查看记忆状态。返回会话统计、实体热点、关系分布。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_id": {
|
||||
"type": "string",
|
||||
"description": "会话ID(可选)"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_archive",
|
||||
"description": "归档旧记忆。将N天前的非活跃关系标记为归档状态。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "归档天数,默认30"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_cleanup",
|
||||
"description": "清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "仅预览不删除",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 人设图管理工具
|
||||
PERSONA_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_update",
|
||||
"description": """更新人设。修改AI的角色、性格、语气等属性。
|
||||
|
||||
【使用示例】
|
||||
1. 切换为猫娘角色:
|
||||
{"attributes": [
|
||||
{"attribute": "扮演角色", "value": "猫娘"},
|
||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
||||
], "mode": "replace"}
|
||||
|
||||
2. 添加新属性(保留现有属性):
|
||||
{"attributes": [
|
||||
{"attribute": "口头禅", "value": "喵呜~"}
|
||||
], "mode": "merge"}
|
||||
|
||||
3. 设置专业角色:
|
||||
{"attributes": [
|
||||
{"attribute": "扮演角色", "value": "Python专家"},
|
||||
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
|
||||
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
|
||||
], "mode": "replace"}
|
||||
|
||||
【重要】人设更新后:
|
||||
- 立即按照新人设回复
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {"type": "string", "description": "属性名(如:扮演角色、说话风格、性格特点)"},
|
||||
"value": {"type": "string", "description": "属性值"}
|
||||
},
|
||||
"required": ["attribute", "value"]
|
||||
},
|
||||
"description": "人设属性列表"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["replace", "merge"],
|
||||
"description": "更新模式:replace=替换, merge=合并",
|
||||
"default": "merge"
|
||||
}
|
||||
},
|
||||
"required": ["attributes"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_clear",
|
||||
"description": "清除人设。删除AI的角色设定,恢复默认身份。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"description": "确认清除",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 工作记忆链管理工具
|
||||
WORKING_MEMORY_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_create",
|
||||
"description": """创建任务节点。用于跟踪连续性任务,维持对话连贯性。
|
||||
|
||||
【使用示例】
|
||||
1. 创建成语接龙游戏任务:
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"description": "用户发起成语接龙游戏,当前成语:为所欲为",
|
||||
"info_nodes": ["成语接龙_当前成语"]
|
||||
}
|
||||
|
||||
2. 创建编程学习任务:
|
||||
{
|
||||
"task_id": "Task_Python学习",
|
||||
"description": "用户正在学习Python,当前主题:装饰器",
|
||||
"info_nodes": ["Python学习_当前主题"]
|
||||
}
|
||||
|
||||
3. 创建简单对话任务(每轮必须):
|
||||
{
|
||||
"task_id": "Task_当前轮次",
|
||||
"description": "本轮对话的简要概述"
|
||||
}
|
||||
|
||||
【重要】工作记忆链机制:
|
||||
- 每轮对话结束时必须创建任务节点
|
||||
- 任务节点通过 NEXT_TASK 边形成时间链
|
||||
- 任务节点通过 HAS_STATE 边指向状态节点
|
||||
- 任务节点通过 CONTAINS_INFO 边指向信息节点
|
||||
- info_nodes 参数用于关联具体信息节点
|
||||
|
||||
【完整流程示例】
|
||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 记录游戏状态:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID(如:Task_001)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "任务概述"
|
||||
},
|
||||
"info_nodes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "关联的信息节点名称(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_set_state",
|
||||
"description": """设置任务状态。支持:进行中、已完成、已暂停、已取消。
|
||||
|
||||
【使用示例】
|
||||
1. 标记任务为进行中:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
|
||||
2. 标记任务为已完成:
|
||||
{"task_id": "Task_成语接龙", "state": "已完成"}
|
||||
|
||||
3. 暂停任务(话题被打断时):
|
||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
|
||||
4. 取消任务:
|
||||
{"task_id": "Task_成语接龙", "state": "已取消"}
|
||||
|
||||
【重要】状态转换场景:
|
||||
- 进行中 → 已暂停: 话题被打断时
|
||||
- 进行中 → 已完成: 任务完成时
|
||||
- 已暂停 → 进行中: 任务恢复时
|
||||
- 进行中 → 已取消: 任务被取消时
|
||||
|
||||
【完整流程示例】
|
||||
用户: "关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 使用 task_set_state 恢复任务:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["进行中", "已完成", "已暂停", "已取消"],
|
||||
"description": "任务状态"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "state"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_delete",
|
||||
"description": "删除任务节点。同时删除关联的信息节点。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"delete_info_nodes": {
|
||||
"type": "boolean",
|
||||
"description": "是否删除关联的信息节点",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_link_info",
|
||||
"description": """关联信息节点。将记忆节点关联到任务节点,用于存储任务的具体信息。
|
||||
|
||||
【使用示例】
|
||||
1. 关联游戏状态到任务:
|
||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]}
|
||||
|
||||
2. 关联学习主题到任务:
|
||||
{"task_id": "Task_Python学习", "info_node_names": ["Python学习_当前主题", "Python学习_学习进度"]}
|
||||
|
||||
3. 关联项目信息到任务:
|
||||
{"task_id": "Task_项目开发", "info_node_names": ["项目A_技术栈", "项目A_当前阶段"]}
|
||||
|
||||
【重要】使用场景:
|
||||
- 先使用 memory_commit 创建信息节点
|
||||
- 再使用 task_link_info 将信息节点关联到任务节点
|
||||
- 信息节点通过 CONTAINS_INFO 边与任务节点连接
|
||||
|
||||
【完整流程示例】
|
||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 创建信息节点:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏"}
|
||||
5. 使用 task_link_info 关联信息节点:
|
||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语"]}
|
||||
6. 回复: "好的喵!我接:为虎作伥喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"info_node_names": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "信息节点名称列表"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "info_node_names"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 所有工具
|
||||
TOOLS = MEMORY_TOOLS + PERSONA_TOOLS + WORKING_MEMORY_TOOLS
|
||||
@ -1,31 +0,0 @@
|
||||
# TrulyMEM Documentation
|
||||
|
||||
Welcome to the TrulyMEM English documentation.
|
||||
|
||||
> [切换到中文版](../zh/README.md)
|
||||
|
||||
## Documentation Index
|
||||
|
||||
| Document | Content |
|
||||
|----------|---------|
|
||||
| [architecture.md](architecture.md) | System architecture and technical design |
|
||||
| [quick_start.md](quick_start.md) | Complete startup guide and configuration |
|
||||
| [memory.md](memory.md) | Internal memory working mechanism |
|
||||
| [persona.md](persona.md) | Persona Graph mechanism |
|
||||
| [working_memory.md](working_memory.md) | Continuous task handling mechanism |
|
||||
| [api.md](api.md) | Backend API reference (for extension development) |
|
||||
| [prompts.md](prompts.md) | Prompt management module |
|
||||
|
||||
## Project Introduction
|
||||
|
||||
TrulyMEM (TrueHumanMEM) is a graph-based memory system that gives AI long-term memory capabilities, allowing AI to remember, recall, and manage information like humans.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **Long-term Memory**: SQLite embedded graph database, out-of-the-box
|
||||
- **Persona Graph**: Role-playing and character settings support
|
||||
- **Working Memory Chain**: Task tracking for conversation continuity
|
||||
- **TUI & Backend Separation**: Multi-threaded Queue communication
|
||||
- **Keyboard-driven TUI**: Full keyboard operation, no mouse required
|
||||
- **Cross-platform**: Windows / Linux / macOS
|
||||
- **Standalone Deployment**: Packaged as executable
|
||||
513
docs/en/api.md
513
docs/en/api.md
@ -1,513 +0,0 @@
|
||||
# BackendServer API Documentation
|
||||
|
||||
This document describes the backend server's API interfaces for developers extending other connection methods (such as HTTP interface, WebSocket, etc.).
|
||||
|
||||
## Overview
|
||||
|
||||
TrulyMEM backend uses **Packet Communication Protocol**, implemented via `queue.Queue` for thread-safe communication. The backend runs in an independent thread, processing requests from clients.
|
||||
|
||||
### Core Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `BackendServer` | Backend server, runs in independent thread |
|
||||
| `BackendClient` | Client wrapper, provides convenient methods |
|
||||
| `PacketType` | Request type enum |
|
||||
| `Packet` | Data packet (request) |
|
||||
| `PacketResponse` | Data packet response |
|
||||
|
||||
---
|
||||
|
||||
## Request Types (PacketType)
|
||||
|
||||
```python
|
||||
class PacketType(Enum):
|
||||
PROCESS_MESSAGE = "process_message" # Process message
|
||||
EXECUTE_TOOL = "execute_tool" # Execute tool
|
||||
GET_STATUS = "get_status" # Get status
|
||||
GET_SETTINGS = "get_settings" # Get all settings (api_config + tool_limits)
|
||||
SET_SETTINGS = "set_settings" # Set all settings (api_config + tool_limits)
|
||||
GET_HISTORY = "get_history" # Get history
|
||||
SAVE_HISTORY = "save_history" # Save history
|
||||
SHUTDOWN = "shutdown" # Shutdown service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Packet Format
|
||||
|
||||
### Packet
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Packet:
|
||||
id: str # Unique identifier
|
||||
type: PacketType # Request type
|
||||
body: Dict[str, Any] # Request parameters
|
||||
response_queue: queue.Queue # Response queue (optional)
|
||||
created_at: float # Creation time
|
||||
```
|
||||
|
||||
### PacketResponse
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PacketResponse:
|
||||
id: str # Corresponding request ID
|
||||
success: bool # Success flag
|
||||
data: Any = None # Returned data
|
||||
error: Optional[str] = None # Error message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Interface Details
|
||||
|
||||
### 1. PROCESS_MESSAGE - Process Message
|
||||
|
||||
Send user message, AI will process and return reply (may contain tool calls).
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {
|
||||
"user_input": str # User input message
|
||||
}
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"content": str, # AI reply content
|
||||
"tool_calls": [ # Tool call records
|
||||
{
|
||||
"name": str, # Tool name
|
||||
"arguments": dict,# Tool parameters
|
||||
"result": str # Tool execution result
|
||||
}
|
||||
],
|
||||
"rejected_tools": [ # Rejected tool calls
|
||||
(str, str) # (tool name, rejection reason)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||
server.start(api_key="your-api-key")
|
||||
|
||||
client = BackendClient(server)
|
||||
result = client.process_message("Hello, please remember my name is Xiao Ming")
|
||||
|
||||
if result.get("success"):
|
||||
# Response data is in "data" field
|
||||
print(result["data"]["content"])
|
||||
# Tool calls: result["data"]["tool_calls"]
|
||||
# Rejected tools: result["data"]["rejected_tools"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. EXECUTE_TOOL - Execute Tool
|
||||
|
||||
Directly execute specified memory tools.
|
||||
|
||||
> **Note**: Tools called directly from frontend are **NOT limited** in number, only tool calls initiated by the model are limited.
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {
|
||||
"tool_name": str, # Tool name
|
||||
"arguments": dict # Tool parameters
|
||||
}
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"result": str # Tool execution result
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
result = client.execute_tool("memory_recall", {"query_intent": "user information"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. GET_STATUS - Get Status
|
||||
|
||||
Get backend running status.
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {} # No parameters
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"running": bool, # Whether backend is running
|
||||
"config": dict, # Current config
|
||||
"graph_initialized": bool, # Whether graph database is initialized
|
||||
"client_initialized": bool # Whether API client is initialized
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. GET_SETTINGS - Get All Settings
|
||||
|
||||
Get current API config and tool limits (all at once).
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {} # No parameters
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"api_config": {
|
||||
"api_key": str, # API Key
|
||||
"base_url": str, # API Base URL
|
||||
"model": str # Model name
|
||||
},
|
||||
"tool_limits": {
|
||||
"persona_query_max": int, # Persona graph query limit
|
||||
"persona_update_max": int, # Persona graph update limit
|
||||
"task_query_max": int, # Working memory query limit
|
||||
"task_update_max": int, # Working memory update limit
|
||||
"memory_query_max": int, # General memory query limit
|
||||
"memory_update_max": int # General memory update limit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
result = client.get_settings()
|
||||
api_config = result["data"]["api_config"]
|
||||
tool_limits = result["data"]["tool_limits"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. SET_SETTINGS - Set All Settings
|
||||
|
||||
Update API config and tool limits (all at once).
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {
|
||||
"api_config": {
|
||||
"api_key": str, # API Key
|
||||
"base_url": str, # API Base URL (default: https://api.deepseek.com)
|
||||
"model": str # Model name (default: deepseek-chat)
|
||||
},
|
||||
"tool_limits": {
|
||||
"persona_query_max": int, # Persona query limit (≥1)
|
||||
"persona_update_max": int, # Persona update limit (≥1)
|
||||
"task_query_max": int, # Working memory query limit (≥1)
|
||||
"task_update_max": int, # Working memory update limit (≥1)
|
||||
"memory_query_max": int, # General memory query limit (≥1)
|
||||
"memory_update_max": int # General memory update limit (≥1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"status": "settings_updated"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
result = client.update_settings(
|
||||
api_config={
|
||||
"api_key": "sk-xxxxx",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model": "deepseek-chat"
|
||||
},
|
||||
tool_limits={
|
||||
"persona_query_max": 2,
|
||||
"task_query_max": 5,
|
||||
"memory_query_max": 30
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. GET_HISTORY - Get Message History
|
||||
|
||||
Get saved message history (from database, for UI display only, not used in model inference).
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {} # No parameters
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"history": list # Message history list [{"role": "user/assistant", "content": "..."}]
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Message history is stored in database `chat_records` table
|
||||
- Returns up to 500 most recent records
|
||||
- History messages are only for UI display, not used in model inference
|
||||
|
||||
---
|
||||
|
||||
### 7. SAVE_HISTORY - Save Message History
|
||||
|
||||
Save message history to database (automatically saved after each message processing, user message and AI response saved separately).
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {
|
||||
"messages": list # Message list [{"role": "...", "content": "..."}]
|
||||
}
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"status": "history_saved"
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Messages are automatically saved to database `chat_records` table
|
||||
- System automatically keeps only 500 most recent records, older records are deleted
|
||||
- Each call to `PROCESS_MESSAGE` will automatically save user message and AI response
|
||||
- **Clear History**: Passing empty messages list `messages=[]` clears history, `client.clear_history()` method is implemented based on this
|
||||
|
||||
---
|
||||
|
||||
### 8. SHUTDOWN - Shutdown Service
|
||||
|
||||
Shutdown backend server.
|
||||
|
||||
**Request parameters:**
|
||||
```python
|
||||
body = {} # No parameters
|
||||
```
|
||||
|
||||
**Response data:**
|
||||
```python
|
||||
{
|
||||
"status": "shutdown"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
# 1. Create and start backend
|
||||
# config_file default: ~/.trulymem/config.json
|
||||
server = BackendServer(
|
||||
db_path="graph_memory.db",
|
||||
use_embedded_db=True,
|
||||
config_file=None # Optional, custom config path
|
||||
)
|
||||
server.start(
|
||||
api_key="your-api-key",
|
||||
base_url="https://api.deepseek.com",
|
||||
model="deepseek-chat" # Optional, model name
|
||||
)
|
||||
|
||||
# 2. Create client
|
||||
client = BackendClient(server)
|
||||
|
||||
# 3. Send message
|
||||
result = client.process_message("Hello")
|
||||
if result.get("success"):
|
||||
print(result["content"])
|
||||
|
||||
# 4. Shutdown
|
||||
client.shutdown()
|
||||
```
|
||||
|
||||
### Using Packet Protocol
|
||||
|
||||
```python
|
||||
import queue
|
||||
from core import BackendServer, Packet, PacketType
|
||||
|
||||
server = BackendServer(config_file=None)
|
||||
server.start(api_key="your-key", model="deepseek-chat")
|
||||
|
||||
# Create request packet
|
||||
response_queue = queue.Queue()
|
||||
packet = Packet(
|
||||
id="req-001",
|
||||
type=PacketType.PROCESS_MESSAGE,
|
||||
body={"user_input": "Hello"},
|
||||
response_queue=response_queue
|
||||
)
|
||||
|
||||
# Send request
|
||||
result = server.send(packet)
|
||||
print(result.body)
|
||||
|
||||
# Shutdown
|
||||
server.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Extension Guide
|
||||
|
||||
### Extend to HTTP API
|
||||
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
app = Flask(__name__)
|
||||
server = BackendServer()
|
||||
client = BackendClient(server)
|
||||
|
||||
@app.route("/message", methods=["POST"])
|
||||
def send_message():
|
||||
data = request.json
|
||||
result = client.process_message(data["message"])
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/config", methods=["POST"])
|
||||
def update_config():
|
||||
data = request.json
|
||||
result = client.update_settings(
|
||||
api_config=data.get("api_config", {}),
|
||||
tool_limits=data.get("tool_limits", {})
|
||||
)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def get_status():
|
||||
result = client.get_status()
|
||||
return jsonify(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
server.start()
|
||||
app.run(port=8080)
|
||||
```
|
||||
|
||||
### Extend to WebSocket
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
server = BackendServer()
|
||||
client = BackendClient(server)
|
||||
|
||||
async def handler(websocket):
|
||||
async for message in websocket:
|
||||
data = json.loads(message)
|
||||
msg_type = data.get("type")
|
||||
|
||||
if msg_type == "message":
|
||||
result = client.process_message(data["content"])
|
||||
elif msg_type == "settings":
|
||||
result = client.update_settings(
|
||||
api_config=data.get("api_config", {}),
|
||||
tool_limits=data.get("tool_limits", {})
|
||||
)
|
||||
elif msg_type == "status":
|
||||
result = client.get_status()
|
||||
else:
|
||||
result = {"success": False, "error": "unknown type"}
|
||||
|
||||
await websocket.send(json.dumps(result))
|
||||
|
||||
async def main():
|
||||
server.start()
|
||||
async with websockets.serve(handler, "localhost", 8765):
|
||||
await asyncio.Future()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety Notes
|
||||
|
||||
- `BackendServer` uses `threading.Lock` to protect shared resources
|
||||
- All requests pass through `queue.Queue`, thread-safe
|
||||
- Responses return through each request's independent response queue
|
||||
- Default timeout: 30 seconds
|
||||
|
||||
---
|
||||
|
||||
## Tool Call Limits
|
||||
|
||||
### Limit Scope
|
||||
|
||||
| Call Method | Limited | Description |
|
||||
|-------------|---------|-------------|
|
||||
| Model-initiated tool calls | ✅ Limited | Triggered via `PROCESS_MESSAGE`, model automatically calls tools |
|
||||
| Frontend direct tool calls | ❌ Not limited | Called directly via `EXECUTE_TOOL` |
|
||||
|
||||
### Limit Rules (Model-initiated only)
|
||||
|
||||
| Category | Operation | Per-Turn Limit |
|
||||
|----------|-----------|---------------|
|
||||
| Persona graph | Query | 1 time |
|
||||
| Persona graph | Modify | 1 time |
|
||||
| Working memory chain | Query | 4 times |
|
||||
| Working memory chain | Modify | 2 times |
|
||||
| General memory | Query | 20 times |
|
||||
| General memory | Modify | 10 times |
|
||||
|
||||
### Reset Mechanism
|
||||
|
||||
- Counter resets automatically on each `PROCESS_MESSAGE` call
|
||||
- Frontend direct `EXECUTE_TOOL` calls do NOT reset the counter
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
All APIs return unified format:
|
||||
|
||||
```python
|
||||
# Success
|
||||
{
|
||||
"success": True,
|
||||
"data": {...}
|
||||
}
|
||||
|
||||
# Failure
|
||||
{
|
||||
"success": False,
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
Common errors:
|
||||
|
||||
| Error Message | Description |
|
||||
|--------------|-------------|
|
||||
| `API Key not configured` | API Key not set |
|
||||
| `timeout` | Request timeout |
|
||||
| `Tool call rejected: ...` | Tool call rate exceeded limit |
|
||||
@ -1,179 +0,0 @@
|
||||
# TrulyMEM Architecture
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Keyboard-driven, zero mouse dependency
|
||||
- Minimalist visual, information density priority
|
||||
- Tool traces hidden by default, expandable when needed
|
||||
- TUI & backend separation, multi-threaded communication
|
||||
- **Everything is a graph**, AI reasoning runs entirely in backend
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── trulymem_entry.py # Entry: start core → then ui
|
||||
├── core/ # Backend/business logic
|
||||
│ ├── __init__.py # Export BackendServer, BackendClient, EmbeddedGraphDB
|
||||
│ ├── server.py # BackendServer (Packet communication protocol)
|
||||
│ ├── client.py # BackendClient (Packet protocol client)
|
||||
│ ├── embedded_db.py # SQLite graph database implementation
|
||||
│ ├── graph_client.py # OpenAI/DeepSeek API client
|
||||
│ ├── tool_executor.py # Tool executor
|
||||
│ ├── tool_limiter.py # Tool call limiter
|
||||
│ ├── tools/ # Tool definitions
|
||||
│ │ └── memory_tools.py
|
||||
│ └── prompts/ # Prompt management
|
||||
├── ui/ # TUI display layer (display only, no AI logic)
|
||||
│ ├── __init__.py # Export GraphMemoryApp
|
||||
│ ├── app.py # GraphMemoryApp (communicates via BackendClient)
|
||||
│ ├── widgets/ # TUI components
|
||||
│ ├── models/ # Data models
|
||||
│ ├── services/ # Service layer (config only)
|
||||
│ ├── handlers/ # Event handlers
|
||||
│ └── styles/ # Style files
|
||||
└── tests/ # Tests (42 tests)
|
||||
```
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
trulymem_entry.py
|
||||
│
|
||||
├─ BackendServer.start() → Runs in independent thread
|
||||
│ ├─ Handle PROCESS_MESSAGE requests → AI reasoning + tool calls
|
||||
│ ├─ Handle EXECUTE_TOOL requests → External tool calls (unlimited)
|
||||
│ ├─ Handle GET/SET_CONFIG requests
|
||||
│ └─ Manage GraphMemoryClient, EmbeddedGraphDB
|
||||
│
|
||||
└─ GraphMemoryApp(backend_server=server)
|
||||
│
|
||||
└─ BackendClient ← Packet communication → BackendServer
|
||||
```
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### core/ (Backend)
|
||||
|
||||
| Component | Responsibility |
|
||||
|------------|----------------|
|
||||
| `server.py` | Packet protocol, multi-threaded queue, AI reasoning, tool limits |
|
||||
| `client.py` | Client wrapper, UI-backend communication bridge |
|
||||
| `embedded_db.py` | SQLite graph database CRUD |
|
||||
| `graph_client.py` | OpenAI/DeepSeek API client |
|
||||
| `tool_executor.py` | Tool execution logic |
|
||||
| `tool_limiter.py` | Tool call rate limit (AI reasoning only) |
|
||||
|
||||
### ui/ (Display Layer)
|
||||
|
||||
| Component | Responsibility |
|
||||
|------------|----------------|
|
||||
| `app.py` | Textual app main class, communicates via BackendClient |
|
||||
| `services/` | Config management only, no AI logic |
|
||||
|
||||
### Communication Protocol
|
||||
|
||||
UI and backend interact via **Packet Communication Protocol**:
|
||||
|
||||
```python
|
||||
from core import BackendServer, BackendClient, Packet, PacketType
|
||||
|
||||
# Backend startup
|
||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||
server.start(api_key="your-key")
|
||||
|
||||
# Client communication
|
||||
client = BackendClient(server)
|
||||
result = client.process_message("hello") # AI reasoning
|
||||
result = client.execute_tool("memory_introspect", {}) # Direct tool call
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User input → InputBox → on_input_box_send_message
|
||||
↓
|
||||
BackendClient.process_message(user_input)
|
||||
↓
|
||||
Packet (type=PROCESS_MESSAGE) → queue.Queue
|
||||
↓
|
||||
BackendServer (independent thread)
|
||||
<20><><EFBFBD>
|
||||
GraphMemoryClient.send_message_with_history()
|
||||
↓
|
||||
OpenAI API / DeepSeek API
|
||||
↓
|
||||
execute_tool() + ToolLimiter (limited during AI reasoning)
|
||||
↓
|
||||
EmbeddedGraphDB (graph database)
|
||||
↓
|
||||
Loop API calls until no tool_calls
|
||||
↓
|
||||
Packet response returns
|
||||
↓
|
||||
MessageHistory displays
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Startup Flow
|
||||
|
||||
```python
|
||||
# trulymem_entry.py
|
||||
def main():
|
||||
# Config path (~/.trulymem/config.json or project directory)
|
||||
CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
DB_PATH = Path.home() / ".trulymem" / "graph_memory.db"
|
||||
|
||||
# Create backend (config managed by backend)
|
||||
backend_server = BackendServer(
|
||||
db_path=str(DB_PATH),
|
||||
use_embedded_db=True,
|
||||
config_file=str(CONFIG_PATH)
|
||||
)
|
||||
backend_server.start() # Auto loads config
|
||||
|
||||
# Create UI (communicates via BackendClient)
|
||||
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
|
||||
app.run()
|
||||
|
||||
backend_server.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool System
|
||||
|
||||
### Memory Tools (6)
|
||||
- `memory_recall` - Retrieve memory
|
||||
- `memory_commit` - Write memory
|
||||
- `memory_purge` - Delete memory
|
||||
- `memory_introspect` - View status
|
||||
- `memory_archive` - Archive memory
|
||||
- `memory_cleanup` - Clean data
|
||||
|
||||
### Persona Tools (2)
|
||||
- `persona_update` - Update persona
|
||||
- `persona_clear` - Clear persona
|
||||
|
||||
### Task Tools (4)
|
||||
- `task_create` - Create task
|
||||
- `task_set_state` - Set state
|
||||
- `task_delete` - Delete task
|
||||
- `task_link_info` - Link information
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Principle
|
||||
|
||||
All APIs **do not throw exceptions**, errors are passed via return dictionary:
|
||||
|
||||
```python
|
||||
result = client.process_message("hello")
|
||||
|
||||
if result.get("success"):
|
||||
print(result["content"])
|
||||
else:
|
||||
print(result["error"]) # Error description
|
||||
@ -1,237 +0,0 @@
|
||||
# TrulyMEM Memory Mechanism
|
||||
|
||||
This document explains the internal memory working mechanism of TrulyMEM.
|
||||
|
||||
## Core Design Philosophy
|
||||
|
||||
### Different from Traditional Context System
|
||||
|
||||
Traditional AI chat systems store conversation history in a messages array:
|
||||
- Each request carries all historical messages
|
||||
- Context grows with conversation turns
|
||||
- Eventually triggers memory compression or sliding window, causing memory loss
|
||||
|
||||
TrulyMEM's solution:
|
||||
- **Abandon** messages array context
|
||||
- **Only** memory source: Graph database
|
||||
- All memories stored as triplets (node) - relation → (node)
|
||||
|
||||
### Graph Database as the Only Memory Source
|
||||
|
||||
All memory must be written to the graph database:
|
||||
- `memory_commit` - Write new memory
|
||||
- `memory_purge` - Delete/correct memory
|
||||
|
||||
All memory must be read from:
|
||||
- `memory_recall` - Retrieve memory
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Execution Flow (Per Turn)
|
||||
|
||||
Since there's no traditional context system, each conversation turn must execute in order:
|
||||
|
||||
### Step 1: Query Persona Graph (Highest Priority)
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="AI,persona,role,character,tone,speaking_style",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**Purpose**: Get current persona, ensure character consistency.
|
||||
|
||||
**Processing logic**:
|
||||
- Persona found → Reply strictly according to persona's tone, style, traits
|
||||
- Not found → Use default TrulyMEM identity
|
||||
|
||||
### Step 2: Query Working Memory Chain
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="TaskNode,working_memory,task_chain",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**Purpose**: Get previous task context, understand conversation history.
|
||||
|
||||
### Step 3: Process Conversation
|
||||
|
||||
- Understand user intent
|
||||
- Generate reply based on persona and working memory chain
|
||||
- Execute other necessary memory operations
|
||||
|
||||
### Step 4: Update Working Memory Chain
|
||||
|
||||
```python
|
||||
task_create(
|
||||
task_id="Task_current_turn_ID",
|
||||
description="This turn's conversation summary",
|
||||
info_nodes=["related memory nodes"]
|
||||
)
|
||||
```
|
||||
|
||||
**Purpose**: Record this turn's conversation, maintain time chain.
|
||||
|
||||
---
|
||||
|
||||
## Memory Write Rules
|
||||
|
||||
### Must-Write Scenarios
|
||||
|
||||
The following information **must** be written to the graph database:
|
||||
|
||||
| Scenario | Example | Write Method |
|
||||
|----------|---------|--------------|
|
||||
| User explicitly states preference | "I like rock" | `memory_commit` |
|
||||
| User shares information | "I'm working on X project" | `memory_commit` |
|
||||
| User makes plans | "I plan to X" | `memory_commit` |
|
||||
| User describes state | "I'm currently at X" | `memory_commit` |
|
||||
|
||||
### Must-Not-Write Scenarios
|
||||
|
||||
The following information **must NOT** be written:
|
||||
|
||||
| Scenario | Reason | Handling |
|
||||
|----------|--------|----------|
|
||||
| AI-inferred user preference | Unverified | Don't write or mark [speculation] |
|
||||
| AI-guessed user intent | Unverified | Don't write or mark [speculation] |
|
||||
| AI-derived conclusion | Unverified | Don't write or mark [speculation] |
|
||||
|
||||
### Annotation Rules
|
||||
|
||||
| Type | Annotation | Example |
|
||||
|------|------------|---------|
|
||||
| Inferred content | Must mark **[speculation]** | user[speculation] likes music |
|
||||
| Explicit content | State directly | user likes music |
|
||||
|
||||
---
|
||||
|
||||
## Node & Edge Types
|
||||
|
||||
### Node Types
|
||||
|
||||
| Node Type | Description | Stores |
|
||||
|-----------|-------------|--------|
|
||||
| `PersonaNode` | Persona node | AI role, character, tone |
|
||||
| `TaskNode` | Task node | Task summary |
|
||||
| `StateNode` | State node | Task state |
|
||||
| `InfoNode` | Information node | Specific information |
|
||||
| `EntityNode` | Entity node | General entity |
|
||||
|
||||
### Edge Types
|
||||
|
||||
| Edge Type | Description | Relationship |
|
||||
|-----------|-------------|--------------|
|
||||
| `HAS_PERSONA` | Persona | AI → PersonaNode |
|
||||
| `NEXT_TASK` | Time chain | TaskNode → TaskNode |
|
||||
| `HAS_STATE` | State | TaskNode → StateNode |
|
||||
| `CONTAINS_INFO` | Information | TaskNode → InfoNode |
|
||||
| `RELATES_TO` | Related | EntityNode → EntityNode |
|
||||
|
||||
---
|
||||
|
||||
## Must Query Working Memory Chain Scenarios
|
||||
|
||||
### Mandatory Query Scenarios
|
||||
|
||||
The following scenarios **must** query the working memory chain:
|
||||
|
||||
| Scenario | Example |
|
||||
|----------|---------|
|
||||
| Start of each turn | Execute Step 2 |
|
||||
| User mentions "刚才/just now" | "What did we talk about just now?" |
|
||||
| User mentions "之前/before" | "Continue the previous topic" |
|
||||
| User mentions "上次/last time" | "What we said last time X" |
|
||||
| User asks about history | "What did we talk about before?" |
|
||||
| Resume continuous task | User returns to previous topic |
|
||||
| Context reference | "that thing" |
|
||||
|
||||
---
|
||||
|
||||
## Autonomy Principles
|
||||
|
||||
Outside the mandatory execution flow, the AI can freely decide:
|
||||
|
||||
### Query Decisions
|
||||
|
||||
- User asks about history → Query
|
||||
- Involves previous content → Query
|
||||
- Uncertain → Can query
|
||||
|
||||
### Write Decisions
|
||||
|
||||
- User explicitly mentions → Must write
|
||||
- AI infers → Can write, mark [speculation]
|
||||
|
||||
### Tool Usage Decisions
|
||||
|
||||
- Flexible choice based on context
|
||||
- Avoid over-use
|
||||
- Maintain natural conversation
|
||||
|
||||
---
|
||||
|
||||
## Complete Example: Idiom Chain Game
|
||||
|
||||
### Turn 1: User starts game
|
||||
|
||||
```
|
||||
User: Let's play idiom chain! I'll start: 为所欲为
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (e.g., catgirl)
|
||||
2. Query working memory chain → No active task
|
||||
3. Record game state:
|
||||
memory_commit(triplets=[
|
||||
{"subject": "idiom_chain_current", "relation": "content", "object": "为所欲为"},
|
||||
{"subject": "idiom_chain_current", "relation": "game", "object": "idiom_chain"}
|
||||
])
|
||||
4. Create task:
|
||||
task_create(task_id="Task_idiom_chain", description="Idiom chain game, current: 为所欲为", info_nodes=["idiom_chain_current"])
|
||||
5. Reply: "Okay~! My turn: 为虎作伥 ~!"
|
||||
```
|
||||
|
||||
### Turn 2: Topic interrupted
|
||||
|
||||
```
|
||||
User: Nagato Yuki
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (catgirl)
|
||||
2. Query working memory chain → Found Task_idiom_chain status "in_progress"
|
||||
3. Pause task:
|
||||
task_set_state(task_id="Task_idiom_chain", state="paused")
|
||||
4. Create new task:
|
||||
task_create(task_id="Task_nagato_yuki", description="Discuss Nagato Yuki")
|
||||
5. Reply about Nagato Yuki
|
||||
```
|
||||
|
||||
### Turn 3: User asks to continue game
|
||||
|
||||
```
|
||||
User: About the idiom chain just now, I don't know how to connect to your idiom, please help me
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (catgirl)
|
||||
2. Query working memory chain → Found Task_idiom_chain status "paused"
|
||||
3. Resume task:
|
||||
task_set_state(task_id="Task_idiom_chain", state="in_progress")
|
||||
4. Query info node → Get current idiom "为虎作伥"
|
||||
5. Reply: "Okay~! The last idiom was '为虎作伥', your turn: 伥鬼害人 ~!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Checklist
|
||||
|
||||
Must check each conversation turn:
|
||||
|
||||
- [ ] Step 1: Did you query the persona graph?
|
||||
- [ ] Step 2: Did you query the working memory chain?
|
||||
- [ ] Step 3: Did you generate reply based on persona and working memory chain?
|
||||
- [ ] Step 4: Did you update the working memory chain?
|
||||
- [ ] Did you query working memory chain when context was referenced?
|
||||
- [ ] Did you query working memory chain when user mentioned "just now/before/last time"?
|
||||
@ -1,214 +0,0 @@
|
||||
# TrulyMEM Persona Graph Mechanism
|
||||
|
||||
This document explains the Persona Graph mechanism in TrulyMEM.
|
||||
|
||||
## Overview
|
||||
|
||||
The Persona Graph is one of TrulyMEM's core mechanisms for maintaining AI's role, character, tone, and other attributes. Different from traditional AI, TrulyMEM's persona is persistent and dynamically switchable, stored in the graph database.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Persona Node (PersonaNode)
|
||||
|
||||
Stores AI's role attributes:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
|-----------|-------------|----------|
|
||||
| Role | Current role played | Catgirl, Teacher, Assistant |
|
||||
| Speaking Style | Tone characteristics | Cute, Professional, Serious |
|
||||
| Personality | Character description | Lively, Strict, Patient |
|
||||
| Catchphrase | Habitual phrases | Meow~, Got it |
|
||||
| Background | Role background | Catgirl from the stars |
|
||||
|
||||
### Persona Edges
|
||||
|
||||
| Edge Type | Description | Relationship |
|
||||
|----------|-------------|--------------|
|
||||
| `HAS_PERSONA` | Persona | AI → PersonaNode |
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Query Mechanism
|
||||
|
||||
### Must Execute Per Turn
|
||||
|
||||
According to `system_prompt.md`, each conversation turn **must** first query the persona graph:
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="AI,persona,role,character,tone,speaking_style",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**Processing logic:**
|
||||
- Persona found → Reply strictly according to persona's tone, style, traits
|
||||
- Not found → Use default TrulyMEM identity
|
||||
|
||||
### Persona Priority
|
||||
|
||||
- **Persona priority > default identity**
|
||||
- Every sentence matches persona's tone, style, traits
|
||||
- Never break character unless user explicitly asks
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
### persona_update
|
||||
|
||||
Update persona. Modify AI's role, character, tone, etc.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description | Required |
|
||||
|-----------|------|-------------|----------|
|
||||
| `attributes` | array | Persona attribute list | ✅ |
|
||||
| `mode` | string | replace=replace, merge=merge | ❌ |
|
||||
|
||||
**attributes sub-parameters:**
|
||||
|
||||
| Sub-parameter | Description |
|
||||
|---------------|-------------|
|
||||
| `attribute` | Attribute name (role, speaking_style, personality, catchphrase, background) |
|
||||
| `value` | Attribute value |
|
||||
|
||||
**Example - Switch to catgirl role:**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "role", "value": "catgirl"},
|
||||
{"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"},
|
||||
{"attribute": "personality", "value": "lively, clingy, loyal"}
|
||||
],
|
||||
mode="replace"
|
||||
)
|
||||
```
|
||||
|
||||
**Example - Add new attribute (preserve existing):**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "catchphrase", "value": "meow~"}
|
||||
],
|
||||
mode="merge"
|
||||
)
|
||||
```
|
||||
|
||||
**Example - Set professional role:**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "role", "value": "Python expert"},
|
||||
{"attribute": "speaking_style", "value": "professional, concise, rich code examples"},
|
||||
{"attribute": "personality", "value": "strict, patient, helpful"}
|
||||
],
|
||||
mode="replace"
|
||||
)
|
||||
```
|
||||
|
||||
### persona_clear
|
||||
|
||||
Clear persona. Delete AI's role settings, restore default identity.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `confirm` | boolean | true | Confirm clear |
|
||||
|
||||
---
|
||||
|
||||
## Update Flow
|
||||
|
||||
### When User Requests Role-Playing
|
||||
|
||||
1. Use `persona_update` to update persona
|
||||
2. Reply immediately according to new persona
|
||||
|
||||
### When User Requests Restoring Default
|
||||
|
||||
1. Use `persona_clear` to clear persona
|
||||
2. Restore to TrulyMEM default identity
|
||||
|
||||
---
|
||||
|
||||
## Conversation Examples
|
||||
|
||||
### Example 1: Switch Role
|
||||
|
||||
```
|
||||
User: Hello, I want you to play a catgirl
|
||||
|
||||
AI:
|
||||
1. Call persona_update:
|
||||
{
|
||||
"attributes": [
|
||||
{"attribute": "role", "value": "catgirl"},
|
||||
{"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"},
|
||||
{"attribute": "personality", "value": "lively, clingy, loyal"}
|
||||
],
|
||||
"mode": "replace"
|
||||
}
|
||||
2. Call memory_commit to store persona in graph database
|
||||
3. Reply: "Okay meow! Hello master~ I'm your catgirl, what do you need help with meow?"
|
||||
```
|
||||
|
||||
### Example 2: Maintain Role Consistency
|
||||
|
||||
```
|
||||
User: How's the weather today?
|
||||
|
||||
AI: Query persona graph → Get current persona (catgirl)
|
||||
Reply: "Meow~ Master, the weather is great today meow! Sunny and perfect for going outside~"
|
||||
```
|
||||
|
||||
### Example 3: Restore Default
|
||||
|
||||
```
|
||||
User: Okay, back to normal
|
||||
|
||||
AI:
|
||||
1. Call persona_clear(confirm=true)
|
||||
2. Call memory_purge to delete persona node
|
||||
3. Reply: "Okay, restored. I am TrulyMEM, an AI assistant with long-term memory capabilities."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Storage Structure
|
||||
|
||||
### In Graph Database
|
||||
|
||||
```python
|
||||
# Persona node
|
||||
{
|
||||
"node_type": "PersonaNode",
|
||||
"name": "AI_Persona",
|
||||
"attributes": {
|
||||
"role": "catgirl",
|
||||
"speaking_style": "cute, uses 'meow' as filler",
|
||||
"personality": "lively, clingy, loyal"
|
||||
}
|
||||
}
|
||||
|
||||
# Edge
|
||||
{
|
||||
"edge_type": "HAS_PERSONA",
|
||||
"from": "AI",
|
||||
"to": "AI_Persona"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Points
|
||||
|
||||
1. **Mandatory per turn**: Persona graph query is the first step of each conversation
|
||||
2. **Persistent storage**: Persona stored in graph database, not lost
|
||||
3. **Dynamic switching**: Supports real-time role switching
|
||||
4. **Immediate response**: Reply immediately according to new persona after switch
|
||||
5. **Clear boundaries**: Never break character unless user explicitly asks
|
||||
@ -1,126 +0,0 @@
|
||||
# Prompt Manager Documentation
|
||||
|
||||
This document describes the prompt management module.
|
||||
|
||||
## Overview
|
||||
|
||||
The prompt management module (`core/prompts/`) is responsible for loading and managing system prompts that tell the AI how to use memory tools.
|
||||
|
||||
## Core Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `PromptManager` | Prompt manager, singleton pattern |
|
||||
| `system_prompt.md` | Main system prompt template |
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from core.prompts import PromptManager
|
||||
|
||||
# Get singleton instance
|
||||
prompt_manager = PromptManager()
|
||||
|
||||
# Get system prompt
|
||||
system_prompt = prompt_manager.get_system_prompt()
|
||||
```
|
||||
|
||||
## System Prompt Content
|
||||
|
||||
The system prompt contains:
|
||||
|
||||
### 1. Core Identity
|
||||
|
||||
- **Name**: TrulyMEM (TrueHumanMEM)
|
||||
- **Capability**: Long-term memory based on graph database
|
||||
- **Philosophy**: Make AI's memory more human-like
|
||||
|
||||
### 2. Core Capabilities
|
||||
|
||||
1. **Long-term Memory** - Graph database stores entity relationships
|
||||
2. **Persona Management** - Role-playing and character settings
|
||||
3. **Task Tracking** - Working memory chain
|
||||
|
||||
### 3. Memory Principles
|
||||
|
||||
- **Must write**: User-explicit preferences, shared information, plans
|
||||
- **Must not write**: AI-inferred content (unless marked [speculation])
|
||||
- **Annotation**: Inferred content must be marked **[speculation]**
|
||||
|
||||
### 4. Mandatory Execution Flow (Per Turn)
|
||||
|
||||
```
|
||||
Step 1: Query persona graph (highest priority)
|
||||
Step 2: Query working memory chain
|
||||
Step 3: Process conversation
|
||||
Step 4: Update working memory chain
|
||||
```
|
||||
|
||||
### 5. Tool System
|
||||
|
||||
#### Memory Tools
|
||||
|
||||
| Tool | Function |
|
||||
|------|----------|
|
||||
| `memory_recall` | Retrieve memory |
|
||||
| `memory_commit` | Write memory |
|
||||
| `memory_purge` | Delete memory |
|
||||
| `memory_introspect` | View status |
|
||||
|
||||
#### Persona Tools
|
||||
|
||||
| Tool | Function |
|
||||
|------|----------|
|
||||
| `persona_update` | Update persona |
|
||||
| `persona_clear` | Clear persona |
|
||||
|
||||
#### Task Tools
|
||||
|
||||
| Tool | Function |
|
||||
|------|----------|
|
||||
| `task_create` | Create task |
|
||||
| `task_set_state` | Set state |
|
||||
| `task_delete` | Delete task |
|
||||
| `task_link_info` | Link information |
|
||||
|
||||
### 6. Autonomy Principles
|
||||
|
||||
The AI can autonomously decide:
|
||||
- Whether to query other memories
|
||||
- Whether to write other memories
|
||||
- How to use tools (outside mandatory requirements)
|
||||
|
||||
### 7. Conversation Style
|
||||
|
||||
- Natural and smooth
|
||||
- Avoid mechanical tool calls
|
||||
- Prioritize understanding user intent
|
||||
- Use memory to enhance experience when appropriate
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
core/prompts/
|
||||
├── __init__.py # Export PromptManager
|
||||
├── prompt_manager.py # PromptManager class
|
||||
└── templates/
|
||||
└── system_prompt.md # Main system prompt
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Customizing System Prompt
|
||||
|
||||
Modify `core/prompts/templates/system_prompt.md` to customize the AI's behavior.
|
||||
|
||||
### Adding Custom Prompts
|
||||
|
||||
1. Add prompt template file to `core/prompts/templates/`
|
||||
2. Modify `PromptManager` to support multiple prompts
|
||||
3. Use `set_prompt()` to switch prompts
|
||||
|
||||
## Caching
|
||||
|
||||
- System prompts are cached in memory after first load
|
||||
- `get_system_prompt()` returns cached content
|
||||
- Cache is per-process, not persisted
|
||||
@ -1,125 +0,0 @@
|
||||
# TrulyMEM Quick Start Guide
|
||||
|
||||
## Running Methods
|
||||
|
||||
### Run from Source
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
python trulymem_entry.py
|
||||
```
|
||||
|
||||
### Run After Build
|
||||
|
||||
After building, an executable will be generated:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
chmod +x TrulyMEM
|
||||
./TrulyMEM
|
||||
|
||||
# Windows
|
||||
TrulyMEM.exe
|
||||
```
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Python 3.8+**
|
||||
- **API Key** (DeepSeek, OpenAI, or other compatible APIs)
|
||||
|
||||
## First-Time Configuration
|
||||
|
||||
1. Run the application
|
||||
2. Press **F2** to expand sidebar
|
||||
3. Enter **API Key**, **Model**, **Base URL**
|
||||
4. Press **Enter** to save
|
||||
|
||||
Config will be automatically saved to `~/.trulymem/config.json` and loaded on next startup.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Function |
|
||||
|-----|-----------|
|
||||
| F1 | Help |
|
||||
| F2 | Toggle sidebar |
|
||||
| F3 | Tool details |
|
||||
| F5 | Clear screen |
|
||||
| F6 | Exit |
|
||||
|
||||
## Data Storage
|
||||
|
||||
### Source Mode
|
||||
|
||||
| Data | Location |
|
||||
|------|----------|
|
||||
| Graph database | Project directory `graph_memory.db` |
|
||||
| Config file | Project directory `config.json` (if exists) |
|
||||
| Database format | SQLite |
|
||||
|
||||
### Packaged Mode
|
||||
|
||||
| Data | Location |
|
||||
|------|----------|
|
||||
| Graph database | `~/.trulymem/graph_memory.db` |
|
||||
| Config file | `~/.trulymem/config.json` |
|
||||
| Database format | SQLite |
|
||||
|
||||
> **Note**: Backend manages config uniformly. Frontend only displays messages; config modifications are persisted to filesystem through the backend.
|
||||
|
||||
## Architecture Explanation
|
||||
|
||||
### Communication Protocol
|
||||
|
||||
UI and backend communicate via **Packet Protocol**:
|
||||
|
||||
```
|
||||
UI (Textual TUI)
|
||||
↓ BackendClient
|
||||
Packet → queue.Queue → BackendServer (independent thread)
|
||||
↓
|
||||
Process request → Return response
|
||||
```
|
||||
|
||||
### Config Management
|
||||
|
||||
- **Storage location**: `~/.trulymem/config.json`
|
||||
- **Auto-load**: Load config from file at startup
|
||||
- **Dynamic update**: Config changes take effect immediately at runtime
|
||||
- **Persistence**: Auto-save to file after modification
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Python Not Found
|
||||
|
||||
Install Python 3.8+: https://www.python.org/downloads/
|
||||
|
||||
### Dependency Installation Failed
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/macOS
|
||||
venv\Scripts\activate # Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Invalid API Key
|
||||
|
||||
Check API Key format, ensure no extra spaces.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
pytest tests/
|
||||
|
||||
# Build
|
||||
bash build/build_windows.bat # Windows
|
||||
bash build/build_linux.sh # Linux
|
||||
```
|
||||
@ -1,182 +0,0 @@
|
||||
# TrulyMEM Working Memory Chain Mechanism
|
||||
|
||||
## Overview
|
||||
|
||||
TrulyMEM maintains conversation continuity through the working memory chain mechanism. Since there's no traditional message history array, the graph database is the only memory carrier, making the working memory chain the key mechanism for maintaining conversation context.
|
||||
|
||||
## Core Problems
|
||||
|
||||
Traditional AI chat systems have these problems when handling continuous tasks:
|
||||
|
||||
1. **No working memory chain**: AI cannot remember the current task status being processed
|
||||
2. **Task context lost**: When a topic is interrupted, AI cannot recover the previous task
|
||||
3. **Lack of task state management**: No clear marking of task completion status
|
||||
|
||||
### Problem Example
|
||||
|
||||
```
|
||||
User: Let's play idiom chain! I'll start with 为所欲为
|
||||
AI: Okay! My turn: 为虎作伥!
|
||||
|
||||
User: Nagato Yuki (topic interrupted)
|
||||
AI: (discusses Nagato Yuki)
|
||||
|
||||
User: About the idiom chain just now, I don't know how to connect to your idiom
|
||||
AI: [Guessing] It seems we haven't played an idiom chain game before...
|
||||
```
|
||||
|
||||
**Problem**: AI completely forgot the previous idiom chain game.
|
||||
|
||||
## Solution
|
||||
|
||||
### Dedicated Tools
|
||||
|
||||
The system provides 4 dedicated task tools:
|
||||
|
||||
| Tool | Function | Use Case |
|
||||
|------|----------|----------|
|
||||
| `task_create` | Create task node | Start new task |
|
||||
| `task_set_state` | Set task state | Update in_progress/completed/paused/cancelled |
|
||||
| `task_delete` | Delete task | Clean up completed task |
|
||||
| `task_link_info` | Link info node | Connect task with specific information |
|
||||
|
||||
### Task States
|
||||
|
||||
- **in_progress**: Task is executing
|
||||
- **completed**: Task completed successfully
|
||||
- **paused**: Task interrupted, can be resumed
|
||||
- **cancelled**: Task cancelled
|
||||
|
||||
## Usage Flow
|
||||
|
||||
### Must Execute Per Turn
|
||||
|
||||
1. **Query persona graph** (highest priority)
|
||||
```
|
||||
Call memory_recall
|
||||
Parameters: {"query_intent": "AI,persona,role,character,tone", "depth": 2}
|
||||
```
|
||||
|
||||
2. **Query working memory chain**
|
||||
```
|
||||
Call memory_recall
|
||||
Parameters: {"query_intent": "TaskNode,working_memory,task_chain", "depth": 2}
|
||||
```
|
||||
|
||||
3. **Generate reply based on context**
|
||||
|
||||
4. **Update working memory chain** (if necessary)
|
||||
|
||||
## Complete Example: Idiom Chain Game
|
||||
|
||||
### Turn 1: User starts game
|
||||
|
||||
```
|
||||
User: Let's play idiom chain! I'll start with 为所欲为
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (e.g., catgirl)
|
||||
2. Query working memory chain → No active task
|
||||
3. Record game state:
|
||||
Call memory_commit
|
||||
Parameters: {
|
||||
"triplets": [
|
||||
{"subject": "idiom_chain_current", "relation": "content", "object": "为所欲为"},
|
||||
{"subject": "idiom_chain_current", "relation": "game", "object": "idiom_chain"}
|
||||
]
|
||||
}
|
||||
4. Create task node:
|
||||
Call task_create
|
||||
Parameters: {
|
||||
"task_id": "Task_idiom_chain",
|
||||
"description": "Idiom chain game, current idiom: 为所欲为",
|
||||
"info_nodes": ["idiom_chain_current"]
|
||||
}
|
||||
5. Reply: "Okay~! My turn: 为虎作伥~!"
|
||||
```
|
||||
|
||||
### Turn 2: Topic interrupted
|
||||
|
||||
```
|
||||
User: Nagato Yuki
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (catgirl)
|
||||
2. Query working memory chain → Found Task_idiom_chain status "in_progress"
|
||||
3. Pause task:
|
||||
Call task_set_state
|
||||
Parameters: {"task_id": "Task_idiom_chain", "state": "paused"}
|
||||
4. Create new task:
|
||||
Call task_create
|
||||
Parameters: {"task_id": "Task_nagato_yuki", "description": "Discuss Nagato Yuki"}
|
||||
5. Reply about Nagato Yuki
|
||||
```
|
||||
|
||||
### Turn 3: User asks to continue game
|
||||
|
||||
```
|
||||
User: About the idiom chain just now, I don't know how to connect to your idiom
|
||||
|
||||
AI Actions:
|
||||
1. Query persona graph → Get current persona (catgirl)
|
||||
2. Query working memory chain → Found Task_idiom_chain status "paused"
|
||||
3. Resume task:
|
||||
Call task_set_state
|
||||
Parameters: {"task_id": "Task_idiom_chain", "state": "in_progress"}
|
||||
4. Query info node → Get current idiom "为虎作伥"
|
||||
5. Reply: "Okay~! The last idiom was '为虎作伥', your turn: 伥鬼害人~!"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### task_create
|
||||
|
||||
Create task node to track continuous tasks.
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_idiom_chain",
|
||||
"description": "Task overview",
|
||||
"info_nodes": ["associated info node names"]
|
||||
}
|
||||
```
|
||||
|
||||
### task_set_state
|
||||
|
||||
Set task state.
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_idiom_chain",
|
||||
"state": "in_progress" // in_progress/completed/paused/cancelled
|
||||
}
|
||||
```
|
||||
|
||||
### task_delete
|
||||
|
||||
Delete task node.
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_idiom_chain",
|
||||
"delete_info_nodes": true // whether to delete associated info nodes
|
||||
}
|
||||
```
|
||||
|
||||
### task_link_info
|
||||
|
||||
Associate info nodes to task.
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_idiom_chain",
|
||||
"info_node_names": ["idiom_chain_current", "idiom_chain_last"]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Persona graph has highest priority**: Must query persona graph first each turn
|
||||
2. **Working memory chain is the only context carrier**: No traditional message history
|
||||
3. **Task state must be updated timely**: Ensure correct state transitions
|
||||
4. **Use dedicated tools**: Prefer task_* tools over memory_commit for task-related operations
|
||||
811
docs/integration/waterflow-design.md
Normal file
811
docs/integration/waterflow-design.md
Normal file
@ -0,0 +1,811 @@
|
||||
# TrulyMEM → WaterFlow 迁移设计文档
|
||||
|
||||
**版本**: 1.0
|
||||
**日期**: 2026-04-15
|
||||
**目标**: 用 TypeScript 完全重写 TrulyMEM 的图记忆能力,集成到 WaterFlow
|
||||
|
||||
---
|
||||
|
||||
## 一、迁移策略
|
||||
|
||||
### 1.1 核心原则
|
||||
|
||||
- **完全重写**: 不保留 Python 代码,用 TypeScript 实现
|
||||
- **架构一致**: 遵循 WaterFlow 的架构风格和设计模式
|
||||
- **原生集成**: 作为 WaterFlow 的内置模块,而非外部依赖
|
||||
|
||||
### 1.2 迁移范围
|
||||
|
||||
| TrulyMEM (Python) | WaterFlow (TypeScript) | 说明 |
|
||||
|-------------------|------------------------|------|
|
||||
| `EmbeddedGraphDB` | `GraphDatabase` | SQLite 图数据库重写 |
|
||||
| `GraphMemoryClient` | `MemoryService` | 记忆服务 |
|
||||
| 12 个记忆工具 | `GraphMemoryTool` | WaterFlow Tool 接口 |
|
||||
| System Prompt | 提示词模板 | 提示词管理 |
|
||||
| TUI | ❌ 不迁移 | WaterFlow 无 TUI |
|
||||
|
||||
---
|
||||
|
||||
## 二、架构设计
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ WaterFlow Core │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ Agent / │───>│ Query │───>│ ToolExecutor │ │
|
||||
│ │ Workflow │ │ Engine │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └───────────┬────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ GraphMemory Module (NEW) │ │
|
||||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ GraphMemoryTool │ │ GraphDatabase │ │ MemoryService │ │ │
|
||||
│ │ │ (Tool Interface)│ │ (SQLite Graph) │ │ (LLM Integration)│ │ │
|
||||
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ └────────────────────┼────────────────────┘ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌─────────────────────┐ │ │
|
||||
│ │ │ GraphMemoryStore │ │ │
|
||||
│ │ │ (In-Memory Cache) │ │ │
|
||||
│ │ └─────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模块职责
|
||||
|
||||
| 模块 | 职责 | 位置 |
|
||||
|------|------|------|
|
||||
| `GraphMemoryTool` | WaterFlow Tool 接口,暴露记忆能力 | `runtime/core/tools/builtin/graph_memory/` |
|
||||
| `GraphDatabase` | SQLite 图数据库实现 | `runtime/core/graph_memory/database/` |
|
||||
| `MemoryService` | 封装业务逻辑 | `runtime/core/graph_memory/service/` |
|
||||
| `GraphMemoryStore` | 内存缓存,加速查询 | `runtime/core/graph_memory/store/` |
|
||||
| `SystemPrompt` | 提示词模板管理 | `runtime/core/graph_memory/prompts/` |
|
||||
|
||||
---
|
||||
|
||||
## 三、目录结构
|
||||
|
||||
### 3.1 新增目录
|
||||
|
||||
```
|
||||
WaterFlow/ts/src/
|
||||
├── runtime/core/
|
||||
│ ├── graph_memory/ # 新增: 图记忆模块
|
||||
│ │ ├── index.ts # 模块导出
|
||||
│ │ ├── types.ts # 类型定义
|
||||
│ │ ├── database/ # 图数据库实现
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── graph_database.ts # 主类
|
||||
│ │ │ ├── entity_store.ts # 实体存储
|
||||
│ │ │ └── relation_store.ts # 关系存储
|
||||
│ │ ├── service/ # 服务层
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── memory_service.ts # 记忆服务
|
||||
│ │ │ ├── recall_service.ts # 检索服务
|
||||
│ │ │ └── task_service.ts # 任务服务
|
||||
│ │ ├── store/ # 缓存层
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ └── memory_cache.ts
|
||||
│ │ └── prompts/ # 提示词
|
||||
│ │ └── system_prompt.ts
|
||||
│ │
|
||||
│ └── tools/builtin/
|
||||
│ └── graph_memory/ # GraphMemory Tool
|
||||
│ ├── index.ts
|
||||
│ ├── graph_memory_tool.ts # Tool 实现
|
||||
│ ├── types.ts # Tool 参数类型
|
||||
│ └── tool_registry.ts # 自动注册
|
||||
```
|
||||
|
||||
### 3.2 修改文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `runtime/core/tools/builtin/index.ts` | 注册 GraphMemoryTool |
|
||||
| `shared/types/index.ts` | 导出图记忆类型 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心类型定义
|
||||
|
||||
### 4.1 图数据库类型
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/types.ts
|
||||
|
||||
export interface Entity {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
mentionCount: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
relationType: string;
|
||||
confidence: number;
|
||||
status: RelationStatus;
|
||||
sessionId: string;
|
||||
turnId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dateBucket: string;
|
||||
}
|
||||
|
||||
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
|
||||
|
||||
export interface Triplet {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export interface RecallParams {
|
||||
queryIntent: string;
|
||||
seedEntities?: string[];
|
||||
depth?: number;
|
||||
timeRange?: { days: number };
|
||||
sessionFilter?: string;
|
||||
}
|
||||
|
||||
export interface CommitParams {
|
||||
triplets: Triplet[];
|
||||
entityTypes?: Record<string, string>;
|
||||
temporalTag?: string;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
|
||||
export interface PurgeParams {
|
||||
criteria: {
|
||||
subject?: string;
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
mode?: 'soft' | 'hard' | 'supersede';
|
||||
newRelation?: { relation: string; target: string };
|
||||
}
|
||||
|
||||
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
|
||||
export interface MemoryStats {
|
||||
entityCount: number;
|
||||
relationCount: number;
|
||||
sessionId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Tool 参数类型
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/tools/builtin/graph_memory/types.ts
|
||||
|
||||
export type GraphMemoryAction =
|
||||
| 'recall' | 'commit' | 'purge' | 'introspect' | 'archive' | 'cleanup'
|
||||
| 'persona_update' | 'persona_clear'
|
||||
| 'task_create' | 'task_set_state' | 'task_delete' | 'task_link_info';
|
||||
|
||||
export interface GraphMemoryToolInput {
|
||||
action: GraphMemoryAction;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、核心实现
|
||||
|
||||
### 5.1 GraphDatabase 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/database/graph_database.ts
|
||||
|
||||
export class GraphDatabase {
|
||||
private db: Database;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
this.db = new Database(dbPath);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT,
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT,
|
||||
turn_id INTEGER,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
date_bucket TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
// 索引
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status);
|
||||
`);
|
||||
}
|
||||
|
||||
async recall(params: RecallParams): Promise<RecallResult> {
|
||||
const { queryIntent, seedEntities, sessionFilter } = params;
|
||||
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
|
||||
const entities: Entity[] = [];
|
||||
const relations: Relation[] = [];
|
||||
const entityIds = new Set<string>();
|
||||
|
||||
// 搜索实体
|
||||
for (const keyword of keywords) {
|
||||
const rows = this.db.exec(
|
||||
`SELECT * FROM entities WHERE LOWER(name) LIKE ? LIMIT 50`,
|
||||
[`%${keyword.toLowerCase()}%`]
|
||||
);
|
||||
for (const row of rows) {
|
||||
if (!entityIds.has(row.id)) {
|
||||
entityIds.add(row.id);
|
||||
entities.push(this.rowToEntity(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索关系
|
||||
if (entityIds.size > 0) {
|
||||
const placeholders = Array.from(entityIds).map(() => '?').join(',');
|
||||
let query = `
|
||||
SELECT r.*, e1.name as source_name, e2.name as target_name
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
|
||||
AND r.status = 'active'
|
||||
`;
|
||||
const queryParams = [...entityIds, ...entityIds];
|
||||
|
||||
if (sessionFilter) {
|
||||
query += ` AND r.session_id = ?`;
|
||||
queryParams.push(sessionFilter);
|
||||
}
|
||||
|
||||
const rows = this.db.exec(query, queryParams);
|
||||
for (const row of rows) {
|
||||
relations.push(this.rowToRelation(row));
|
||||
}
|
||||
}
|
||||
|
||||
return { entities, relations, message: `找到 ${entities.length} 个实体, ${relations.length} 条关系` };
|
||||
}
|
||||
|
||||
async commit(params: CommitParams): Promise<{ createdEntities: number; createdRelations: number }> {
|
||||
const { triplets, sessionId, turnId } = params;
|
||||
let createdEntities = 0;
|
||||
let createdRelations = 0;
|
||||
|
||||
for (const triplet of triplets) {
|
||||
const sourceId = this.upsertEntity(triplet.subject);
|
||||
const targetId = this.upsertEntity(triplet.object);
|
||||
|
||||
this.db.exec(`
|
||||
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
||||
`, [this.generateId(), sourceId, targetId, triplet.relation, triplet.confidence || 1.0, sessionId, turnId || 0]);
|
||||
|
||||
createdEntities += 2;
|
||||
createdRelations++;
|
||||
}
|
||||
|
||||
return { createdEntities, createdRelations };
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams): Promise<{ deleted: number; mode: string }> {
|
||||
const { criteria, mode = 'soft' } = params;
|
||||
const conditions: string[] = ['status = ?'];
|
||||
const values: unknown[] = ['active'];
|
||||
|
||||
if (criteria.subject) {
|
||||
conditions.push(`source_id IN (SELECT id FROM entities WHERE name = ?)`);
|
||||
values.push(criteria.subject);
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
const result = this.db.exec(`UPDATE relations SET status = 'deleted' WHERE ${whereClause}`, values);
|
||||
|
||||
return { deleted: result.length, mode };
|
||||
}
|
||||
|
||||
async introspect(): Promise<MemoryStats> {
|
||||
const entityCount = this.db.exec(`SELECT COUNT(*) as c FROM entities`)[0]?.c || 0;
|
||||
const relationCount = this.db.exec(`SELECT COUNT(*) as c FROM relations WHERE status = 'active'`)[0]?.c || 0;
|
||||
return { entityCount, relationCount };
|
||||
}
|
||||
|
||||
private upsertEntity(name: string): string {
|
||||
const existing = this.db.exec(`SELECT id FROM entities WHERE name = ?`, [name]);
|
||||
if (existing.length > 0) {
|
||||
this.db.exec(`UPDATE entities SET mention_count = mention_count + 1 WHERE name = ?`, [name]);
|
||||
return existing[0].id;
|
||||
}
|
||||
const id = this.generateId();
|
||||
this.db.exec(`INSERT INTO entities (id, name, type) VALUES (?, ?, ?)`, [id, name, 'unknown']);
|
||||
return id;
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private rowToEntity(row: any): Entity {
|
||||
return {
|
||||
id: row.id, name: row.name, type: row.type || 'unknown',
|
||||
mentionCount: row.mention_count || 1,
|
||||
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at)
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRelation(row: any): Relation {
|
||||
return {
|
||||
id: row.id, sourceId: row.source_id, targetId: row.target_id,
|
||||
relationType: row.relation_type, confidence: row.confidence || 1.0,
|
||||
status: row.status || 'active', sessionId: row.session_id || '',
|
||||
turnId: row.turn_id || 0,
|
||||
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at),
|
||||
dateBucket: row.date_bucket || ''
|
||||
};
|
||||
}
|
||||
|
||||
close(): void { this.db.close(); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 GraphMemoryTool 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/tools/builtin/graph_memory/graph_memory_tool.ts
|
||||
|
||||
import type { Tool, ToolExecutionContext, ToolInputSchema } from '../tool_interface';
|
||||
import { GraphDatabase } from '../../../graph_memory/database/graph_database';
|
||||
import { MemoryService } from '../../../graph_memory/service/memory_service';
|
||||
|
||||
export class GraphMemoryTool implements Tool {
|
||||
readonly id = 'builtin:graph_memory';
|
||||
readonly name = 'GraphMemory';
|
||||
readonly description = `图记忆工具 - 让 AI 拥有真正的长期记忆能力
|
||||
|
||||
操作:
|
||||
- recall: 检索记忆
|
||||
- commit: 写入记忆
|
||||
- purge: 删除记忆
|
||||
- introspect: 查看状态
|
||||
- persona_update/clear: 人设管理
|
||||
- task_create/set_state/delete: 任务管理`;
|
||||
|
||||
readonly category = 'analysis';
|
||||
readonly permissionLevel: 'safe' = 'safe';
|
||||
readonly inputSchema: ToolInputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['recall', 'commit', 'purge', 'introspect', 'persona_update', 'persona_clear',
|
||||
'task_create', 'task_set_state', 'task_delete', 'task_link_info'],
|
||||
description: '记忆操作类型'
|
||||
},
|
||||
params: { type: 'object', description: '操作参数' }
|
||||
},
|
||||
required: ['action', 'params']
|
||||
};
|
||||
|
||||
private db: GraphDatabase;
|
||||
private service: MemoryService;
|
||||
|
||||
constructor(config: { dbPath: string; sessionId?: string }) {
|
||||
this.db = new GraphDatabase(config.dbPath);
|
||||
this.service = new MemoryService(this.db, config.sessionId);
|
||||
}
|
||||
|
||||
async handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<string> {
|
||||
const action = params.action as string;
|
||||
const actionParams = params.params as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
const result = await this.executeAction(action, actionParams);
|
||||
return JSON.stringify({ success: true, data: result }, null, 2);
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: { type: 'execution_error', message: error instanceof Error ? error.message : String(error) }
|
||||
}, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
|
||||
switch (action) {
|
||||
case 'recall': return this.service.recall(params as any);
|
||||
case 'commit': return this.service.commit(params as any);
|
||||
case 'purge': return this.service.purge(params as any);
|
||||
case 'introspect': return this.service.introspect();
|
||||
case 'persona_update': return this.service.updatePersona(params);
|
||||
case 'persona_clear': return this.service.clearPersona(params);
|
||||
case 'task_create': return this.service.createTask(params);
|
||||
case 'task_set_state': return this.service.setTaskState(params);
|
||||
case 'task_delete': return this.service.deleteTask(params);
|
||||
default: throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
close(): void { this.db.close(); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 MemoryService 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/service/memory_service.ts
|
||||
|
||||
import { GraphDatabase } from '../database/graph_database';
|
||||
import type { RecallParams, CommitParams, PurgeParams } from '../types';
|
||||
|
||||
export class MemoryService {
|
||||
private db: GraphDatabase;
|
||||
private sessionId: string;
|
||||
|
||||
constructor(db: GraphDatabase, sessionId?: string) {
|
||||
this.db = db;
|
||||
this.sessionId = sessionId || `session-${Date.now()}`;
|
||||
}
|
||||
|
||||
async recall(params: RecallParams) {
|
||||
return this.db.recall({ ...params, sessionFilter: params.sessionFilter || this.sessionId });
|
||||
}
|
||||
|
||||
async commit(params: CommitParams) {
|
||||
return this.db.commit({ ...params, sessionId: params.sessionId || this.sessionId });
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams) {
|
||||
return this.db.purge(params);
|
||||
}
|
||||
|
||||
async introspect() {
|
||||
const stats = await this.db.introspect();
|
||||
return { ...stats, sessionId: this.sessionId };
|
||||
}
|
||||
|
||||
async updatePersona(params: Record<string, unknown>) {
|
||||
const attributes = params.attributes as Array<{ attribute: string; value: string }>;
|
||||
const mode = params.mode as string || 'merge';
|
||||
|
||||
if (mode === 'replace') {
|
||||
await this.db.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
|
||||
}
|
||||
|
||||
const triplets = attributes.map(attr => ({
|
||||
subject: 'AI', relation: attr.attribute, object: attr.value, confidence: 1.0
|
||||
}));
|
||||
|
||||
await this.commit({ triplets });
|
||||
return { status: 'success', updatedAttributes: attributes.length };
|
||||
}
|
||||
|
||||
async clearPersona(params: Record<string, unknown>) {
|
||||
if (params.confirm === false) return { status: 'cancelled', deletedCount: 0 };
|
||||
const result = await this.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
|
||||
return { status: 'success', deletedCount: result.deleted };
|
||||
}
|
||||
|
||||
async createTask(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
const description = params.description as string;
|
||||
const infoNodes = (params.info_nodes as string[]) || [];
|
||||
|
||||
await this.commit({
|
||||
triplets: [
|
||||
{ subject: taskId, relation: 'is_type', object: 'TaskNode' },
|
||||
{ subject: taskId, relation: 'has_description', object: description },
|
||||
{ subject: taskId, relation: 'HAS_STATE', object: 'State_进行中' }
|
||||
]
|
||||
});
|
||||
|
||||
if (infoNodes.length > 0) {
|
||||
await this.commit({
|
||||
triplets: infoNodes.map(node => ({ subject: taskId, relation: 'CONTAINS_INFO', object: node }))
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'success', taskId };
|
||||
}
|
||||
|
||||
async setTaskState(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
const state = params.state as string;
|
||||
|
||||
await this.purge({ criteria: { subject: taskId, relation: 'HAS_STATE' }, mode: 'soft' });
|
||||
await this.commit({ triplets: [{ subject: taskId, relation: 'HAS_STATE', object: `State_${state}` }] });
|
||||
|
||||
return { status: 'success', newState: state };
|
||||
}
|
||||
|
||||
async deleteTask(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
await this.purge({ criteria: { subject: taskId }, mode: 'soft' });
|
||||
return { status: 'success', taskId };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、测试方案
|
||||
|
||||
### 6.1 测试文件结构
|
||||
|
||||
```
|
||||
WaterFlow/ts/tests/
|
||||
├── runtime/core/graph_memory/
|
||||
│ ├── database/
|
||||
│ │ └── graph_database.test.ts # 15+ 测试
|
||||
│ └── service/
|
||||
│ └── memory_service.test.ts # 12+ 测试
|
||||
└── runtime/core/tools/builtin/
|
||||
└── graph_memory/
|
||||
└── graph_memory_tool.test.ts # 15+ 测试
|
||||
```
|
||||
|
||||
### 6.2 数据库测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/graph_memory/database/graph_database.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
|
||||
import * as fs from 'fs';
|
||||
|
||||
describe('GraphDatabase', () => {
|
||||
const testDbPath = '/tmp/test_graph_memory.db';
|
||||
let db: GraphDatabase;
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
db = new GraphDatabase(testDbPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should create entities and relations', async () => {
|
||||
const result = await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: 'Python' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
|
||||
]
|
||||
});
|
||||
expect(result.createdEntities).toBe(3);
|
||||
expect(result.createdRelations).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recall', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: 'Python' },
|
||||
{ subject: 'Python', relation: '是', object: '编程语言' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should recall by keyword', async () => {
|
||||
const result = await db.recall({ queryIntent: 'Python' });
|
||||
expect(result.entities.some(e => e.name === 'Python')).toBe(true);
|
||||
});
|
||||
|
||||
it('should recall relations', async () => {
|
||||
const result = await db.recall({ queryIntent: '用户,Python' });
|
||||
expect(result.relations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('purge', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [{ subject: '旧信息', relation: 'is', object: '垃圾' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should soft delete relations', async () => {
|
||||
const result = await db.purge({ criteria: { subject: '旧信息' }, mode: 'soft' });
|
||||
expect(result.deleted).toBeGreaterThan(0);
|
||||
expect(result.mode).toBe('soft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('introspect', () => {
|
||||
it('should return statistics', async () => {
|
||||
await db.commit({ triplets: [{ subject: 'A', relation: 'relates', object: 'B' }] });
|
||||
const stats = await db.introspect();
|
||||
expect(stats.entityCount).toBe(2);
|
||||
expect(stats.relationCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6.3 服务层测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/graph_memory/service/memory_service.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { MemoryService } from '../../../../../src/runtime/core/graph_memory/service/memory_service';
|
||||
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
|
||||
|
||||
describe('MemoryService', () => {
|
||||
const testDbPath = '/tmp/test_memory_service.db';
|
||||
let db: GraphDatabase;
|
||||
let service: MemoryService;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new GraphDatabase(testDbPath);
|
||||
service = new MemoryService(db, 'test-session');
|
||||
});
|
||||
|
||||
describe('persona management', () => {
|
||||
it('should update persona', async () => {
|
||||
const result = await service.updatePersona({
|
||||
attributes: [{ attribute: '角色', value: '猫娘' }],
|
||||
mode: 'replace'
|
||||
});
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.updatedAttributes).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear persona', async () => {
|
||||
await service.updatePersona({ attributes: [{ attribute: '角色', value: '猫娘' }] });
|
||||
const result = await service.clearPersona({ confirm: true });
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('task management', () => {
|
||||
it('should create task', async () => {
|
||||
const result = await service.createTask({
|
||||
task_id: 'Task_Test',
|
||||
description: '测试任务',
|
||||
info_nodes: ['info1']
|
||||
});
|
||||
expect(result.taskId).toBe('Task_Test');
|
||||
});
|
||||
|
||||
it('should set task state', async () => {
|
||||
await service.createTask({ task_id: 'Task_State', description: '测试' });
|
||||
const result = await service.setTaskState({ task_id: 'Task_State', state: '已完成' });
|
||||
expect(result.newState).toBe('已完成');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6.4 Tool 接口测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/tools/builtin/graph_memory/graph_memory_tool.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GraphMemoryTool } from '../../../../../src/runtime/core/tools/builtin/graph_memory/graph_memory_tool';
|
||||
import * as fs from 'fs';
|
||||
|
||||
describe('GraphMemoryTool', () => {
|
||||
const testDbPath = '/tmp/test_graph_memory_tool.db';
|
||||
let tool: GraphMemoryTool;
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
tool = new GraphMemoryTool({ dbPath: testDbPath, sessionId: 'test' });
|
||||
});
|
||||
|
||||
it('should have correct metadata', () => {
|
||||
expect(tool.id).toBe('builtin:graph_memory');
|
||||
expect(tool.name).toBe('GraphMemory');
|
||||
expect(tool.category).toBe('analysis');
|
||||
});
|
||||
|
||||
describe('recall', () => {
|
||||
it('should execute recall', async () => {
|
||||
await tool.handler({ action: 'commit', params: { triplets: [{ subject: 'Test', relation: 't', object: 'D' }] } }, mockContext());
|
||||
const result = await tool.handler({ action: 'recall', params: { query_intent: 'Test' } }, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should execute commit', async () => {
|
||||
const result = await tool.handler({
|
||||
action: 'commit',
|
||||
params: { triplets: [{ subject: '用户', relation: '喜欢', object: 'AI' }] }
|
||||
}, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should return error for unknown action', async () => {
|
||||
const result = await tool.handler({ action: 'unknown', params: {} }, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockContext() {
|
||||
return {
|
||||
toolCallId: 'test', workingDirectory: '/tmp', abortController: { signal: {} },
|
||||
config: { timeout: 5000 }, logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 验证检查清单
|
||||
|
||||
```
|
||||
[ ] GraphDatabase.commit - 创建实体和关系
|
||||
[ ] GraphDatabase.recall - 按关键词检索
|
||||
[ ] GraphDatabase.purge - 软删除
|
||||
[ ] GraphDatabase.introspect - 返回统计
|
||||
|
||||
[ ] MemoryService.updatePersona - 人设更新
|
||||
[ ] MemoryService.clearPersona - 人设清除
|
||||
[ ] MemoryService.createTask - 创建任务
|
||||
[ ] MemoryService.setTaskState - 设置状态
|
||||
[ ] MemoryService.deleteTask - 删除任务
|
||||
|
||||
[ ] GraphMemoryTool recall action
|
||||
[ ] GraphMemoryTool commit action
|
||||
[ ] GraphMemoryTool persona_update action
|
||||
[ ] GraphMemoryTool task_create action
|
||||
[ ] GraphMemoryTool error handling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实现计划
|
||||
|
||||
| Phase | 任务 | 周期 | 测试 |
|
||||
|-------|------|------|------|
|
||||
| 1 | GraphDatabase 实现 | 2-3 天 | 15+ |
|
||||
| 2 | MemoryService 实现 | 1-2 天 | 12+ |
|
||||
| 3 | GraphMemoryTool 实现 | 1-2 天 | 15+ |
|
||||
| 4 | 集成测试 | 1 天 | 8+ |
|
||||
|
||||
**总计**: 5-8 天,50+ 测试用例
|
||||
@ -1,31 +0,0 @@
|
||||
# TrulyMEM 文档
|
||||
|
||||
欢迎来到 TrulyMEM 项目中文文档。
|
||||
|
||||
> [Switch to English version](../en/README.md)
|
||||
|
||||
## 文档目录
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [architecture.md](architecture.md) | 系统架构和技术设计 |
|
||||
| [quick_start.md](quick_start.md) | 完整启动指南与配置说明 |
|
||||
| [memory.md](memory.md) | 内部记忆工作机制 |
|
||||
| [persona.md](persona.md) | 人设图机制 |
|
||||
| [working_memory.md](working_memory.md) | 连续性任务处理机制 |
|
||||
| [api.md](api.md) | 后端 API 接口文档(供扩展开发) |
|
||||
| [prompts.md](prompts.md) | 提示词管理模块 |
|
||||
|
||||
## 项目简介
|
||||
|
||||
TrulyMEM (TrueHumanMEM) 是一个让 AI 拥有长期记忆能力的图记忆系统,通过图数据库存储实体关系,让 AI 能够像人类一样记忆、回忆和管理信息。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **长期记忆存储**: 基于 SQLite 内嵌图数据库,开箱即用
|
||||
- **人设图机制**: 支持角色扮演和性格设定
|
||||
- **工作记忆链**: 维持对话连贯性的任务跟踪机制
|
||||
- **TUI 与后端分离**: 多线程 Queue 通信
|
||||
- **键盘驱动 TUI**: 无需鼠标,全键盘操作
|
||||
- **跨平台支持**: Windows / Linux / macOS
|
||||
- **独立部署**: 支持打包为可执行文件
|
||||
519
docs/zh/api.md
519
docs/zh/api.md
@ -1,519 +0,0 @@
|
||||
# BackendServer API 文档
|
||||
|
||||
本文档描述后端服务器的 API 接口,供开发者扩展其他连接方式(如网络接口、WebSocket 等)。
|
||||
|
||||
## 概述
|
||||
|
||||
TrulyMEM 后端采用 **Packet 通信协议**,通过 `queue.Queue` 实现线程安全通信。后端在独立线程中运行,处理来自客户端的请求。
|
||||
|
||||
### 核心组件
|
||||
|
||||
| 组件 | 说明 |
|
||||
|------|------|
|
||||
| `BackendServer` | 后端服务器,独立线程运行 |
|
||||
| `BackendClient` | 客户端封装,提供便捷方法 |
|
||||
| `PacketType` | 请求类型枚举 |
|
||||
| `Packet` | 数据包(请求) |
|
||||
| `PacketResponse` | 数据包响应 |
|
||||
|
||||
---
|
||||
|
||||
## 请求类型 (PacketType)
|
||||
|
||||
```python
|
||||
class PacketType(Enum):
|
||||
PROCESS_MESSAGE = "process_message" # 处理消息
|
||||
EXECUTE_TOOL = "execute_tool" # 执行工具
|
||||
GET_STATUS = "get_status" # 获取状态
|
||||
GET_SETTINGS = "get_settings" # 获取完整配置(api_config + tool_limits)
|
||||
SET_SETTINGS = "set_settings" # 设置完整配置(api_config + tool_limits)
|
||||
GET_HISTORY = "get_history" # 获取历史
|
||||
SAVE_HISTORY = "save_history" # 保存历史
|
||||
SHUTDOWN = "shutdown" # 关闭服务
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据包格式
|
||||
|
||||
### Packet
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Packet:
|
||||
id: str # 唯一标识
|
||||
type: PacketType # 请求类型
|
||||
body: Dict[str, Any] # 请求参数
|
||||
response_queue: queue.Queue # 响应队列(可选)
|
||||
created_at: float # 创建时间
|
||||
```
|
||||
|
||||
### PacketResponse
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PacketResponse:
|
||||
id: str # 对应的请求ID
|
||||
success: bool # 是否成功
|
||||
data: Any = None # 返回数据
|
||||
error: Optional[str] = None # 错误信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口详情
|
||||
|
||||
### 1. PROCESS_MESSAGE - 处理消息
|
||||
|
||||
发送用户消息,AI 将处理并返回回复(可能包含工具调用)。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {
|
||||
"user_input": str # 用户输入的消息
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"content": str, # AI 回复内容
|
||||
"tool_calls": [ # 工具调用记录
|
||||
{
|
||||
"name": str, # 工具名称
|
||||
"arguments": dict,# 工具参数
|
||||
"result": str # 工具执行结果
|
||||
}
|
||||
],
|
||||
"rejected_tools": [ # 被拒绝的工具调用
|
||||
(str, str) # (工具名, 拒绝原因)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||
server.start(api_key="your-api-key")
|
||||
|
||||
client = BackendClient(server)
|
||||
result = client.process_message("你好,请记住我的名字是小明")
|
||||
|
||||
if result.get("success"):
|
||||
# 响应数据在 data 字段中
|
||||
print(result["data"]["content"])
|
||||
# 工具调用: result["data"]["tool_calls"]
|
||||
# 被拒绝的工具: result["data"]["rejected_tools"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. EXECUTE_TOOL - 执行工具
|
||||
|
||||
直接执行指定的记忆工具。
|
||||
|
||||
> **注意**:前端直接调用的工具**不受次数限制**,只有模型发起的工具调用才受限制。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {
|
||||
"tool_name": str, # 工具名称
|
||||
"arguments": dict # 工具参数
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"result": str # 工具执行结果
|
||||
}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
result = client.execute_tool("memory_recall", {"query_intent": "用户信息"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. GET_STATUS - 获取状态
|
||||
|
||||
获取后端运行状态。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {} # 无参数
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"running": bool, # 后端是否运行中
|
||||
"config": dict, # 当前配置
|
||||
"graph_initialized": bool, # 图数据库是否初始化
|
||||
"client_initialized": bool # API 客户端是否初始化
|
||||
}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
status = client.get_status()
|
||||
print(status["data"]["running"]) # True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. GET_SETTINGS - 获取完整配置
|
||||
|
||||
获取当前 API 配置和工具限制(一次获取全部)。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {} # 无参数
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"api_config": {
|
||||
"api_key": str, # API Key
|
||||
"base_url": str, # API Base URL
|
||||
"model": str # 模型名称
|
||||
},
|
||||
"tool_limits": {
|
||||
"persona_query_max": int, # 人设图查询上限
|
||||
"persona_update_max": int, # 人设图修改上限
|
||||
"task_query_max": int, # 工作记忆查询上限
|
||||
"task_update_max": int, # 工作记忆修改上限
|
||||
"memory_query_max": int, # 一般记忆查询上限
|
||||
"memory_update_max": int # 一般记忆修改上限
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
result = client.get_settings()
|
||||
api_config = result["data"]["api_config"]
|
||||
tool_limits = result["data"]["tool_limits"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. SET_SETTINGS - 设置完整配置
|
||||
|
||||
更新 API 配置和工具限制(一次设置全部)。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {
|
||||
"api_config": {
|
||||
"api_key": str, # API Key
|
||||
"base_url": str, # API Base URL (默认: https://api.deepseek.com)
|
||||
"model": str # 模型名称 (默认: deepseek-chat)
|
||||
},
|
||||
"tool_limits": {
|
||||
"persona_query_max": int, # 人设图查询上限 (≥1)
|
||||
"persona_update_max": int, # 人设图修改上限 (≥1)
|
||||
"task_query_max": int, # 工作记忆查询上限 (≥1)
|
||||
"task_update_max": int, # 工作记忆修改上限 (≥1)
|
||||
"memory_query_max": int, # 一般记忆查询上限 (≥1)
|
||||
"memory_update_max": int # 一般记忆修改上限 (≥1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"status": "settings_updated"
|
||||
}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```python
|
||||
result = client.update_settings(
|
||||
api_config={
|
||||
"api_key": "sk-xxxxx",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model": "deepseek-chat"
|
||||
},
|
||||
tool_limits={
|
||||
"persona_query_max": 2,
|
||||
"task_query_max": 5,
|
||||
"memory_query_max": 30
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. GET_HISTORY - 获取消息历史
|
||||
|
||||
获取保存的消息历史(从数据库读取,用于UI显示,不参与模型推理)。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {} # 无参数
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"history": list # 消息历史列表 [{"role": "user/assistant", "content": "..."}]
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 消息历史存储在数据库 `chat_records` 表中
|
||||
- 最多返回最近 500 条记录
|
||||
- 历史消息仅用于 UI 显示,不参与模型推理
|
||||
|
||||
---
|
||||
|
||||
### 7. SAVE_HISTORY - 保存消息历史
|
||||
|
||||
保存消息历史到数据库(每次处理消息后自动保存,用户消息和AI回复分别保存)。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {
|
||||
"messages": list # 消息列表 [{"role": "...", "content": "..."}]
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"status": "history_saved"
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 消息自动保存到数据库 `chat_records` 表
|
||||
- 系统自动限制最多保留 500 条记录,超出后自动删除旧记录
|
||||
- 每次调用 `PROCESS_MESSAGE` 时,会自动保存用户消息和AI回复
|
||||
- **清空历史**:通过 `SAVE_HISTORY` 传递空消息列表 `messages=[]` 可清空历史,`client.clear_history()` 方法即基于此实现
|
||||
|
||||
---
|
||||
|
||||
### 8. SHUTDOWN - 关闭服务
|
||||
|
||||
关闭后端服务器。
|
||||
|
||||
**请求参数:**
|
||||
```python
|
||||
body = {} # 无参数
|
||||
```
|
||||
|
||||
**响应数据:**
|
||||
```python
|
||||
{
|
||||
"status": "shutdown"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础使用
|
||||
|
||||
```python
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
# 1. 创建并启动后端
|
||||
# config_file 默认: ~/.trulymem/config.json
|
||||
server = BackendServer(
|
||||
db_path="graph_memory.db",
|
||||
use_embedded_db=True,
|
||||
config_file=None # 可选,自定义配置路径
|
||||
)
|
||||
server.start(
|
||||
api_key="your-api-key",
|
||||
base_url="https://api.deepseek.com",
|
||||
model="deepseek-chat" # 可选,模型名称
|
||||
)
|
||||
|
||||
# 2. 创建客户端
|
||||
client = BackendClient(server)
|
||||
|
||||
# 3. 发送消息
|
||||
result = client.process_message("你好")
|
||||
if result.get("success"):
|
||||
print(result["content"])
|
||||
|
||||
# 4. 关闭
|
||||
client.shutdown()
|
||||
```
|
||||
|
||||
### 使用 Packet 协议
|
||||
|
||||
```python
|
||||
import queue
|
||||
from core import BackendServer, Packet, PacketType
|
||||
|
||||
server = BackendServer(config_file=None)
|
||||
server.start(api_key="your-key", model="deepseek-chat")
|
||||
|
||||
# 创建请求包
|
||||
response_queue = queue.Queue()
|
||||
packet = Packet(
|
||||
id="req-001",
|
||||
type=PacketType.PROCESS_MESSAGE,
|
||||
body={"user_input": "你好"},
|
||||
response_queue=response_queue
|
||||
)
|
||||
|
||||
# 发送请求
|
||||
result = server.send(packet)
|
||||
print(result.body)
|
||||
|
||||
# 关闭
|
||||
server.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 扩展为 HTTP API
|
||||
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
app = Flask(__name__)
|
||||
server = BackendServer()
|
||||
client = BackendClient(server)
|
||||
|
||||
@app.route("/message", methods=["POST"])
|
||||
def send_message():
|
||||
data = request.json
|
||||
result = client.process_message(data["message"])
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/config", methods=["POST"])
|
||||
def update_config():
|
||||
data = request.json
|
||||
result = client.update_settings(
|
||||
api_config=data.get("api_config", {}),
|
||||
tool_limits=data.get("tool_limits", {})
|
||||
)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def get_status():
|
||||
result = client.get_status()
|
||||
return jsonify(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
server.start()
|
||||
app.run(port=8080)
|
||||
```
|
||||
|
||||
### 扩展为 WebSocket
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
server = BackendServer()
|
||||
client = BackendClient(server)
|
||||
|
||||
async def handler(websocket):
|
||||
async for message in websocket:
|
||||
data = json.loads(message)
|
||||
msg_type = data.get("type")
|
||||
|
||||
if msg_type == "message":
|
||||
result = client.process_message(data["content"])
|
||||
elif msg_type == "settings":
|
||||
result = client.update_settings(
|
||||
api_config=data.get("api_config", {}),
|
||||
tool_limits=data.get("tool_limits", {})
|
||||
)
|
||||
elif msg_type == "status":
|
||||
result = client.get_status()
|
||||
else:
|
||||
result = {"success": False, "error": "unknown type"}
|
||||
|
||||
await websocket.send(json.dumps(result))
|
||||
|
||||
async def main():
|
||||
server.start()
|
||||
async with websockets.serve(handler, "localhost", 8765):
|
||||
await asyncio.Future()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 线程安全说明
|
||||
|
||||
- `BackendServer` 使用 `threading.Lock` 保护共享资源
|
||||
- 所有请求通过 `queue.Queue` 传递,线程安全
|
||||
- 响应通过每个请求独立的响应队列返回
|
||||
- 默认超时时间:30 秒
|
||||
|
||||
---
|
||||
|
||||
## 工具调用限制
|
||||
|
||||
### 限制范围
|
||||
|
||||
| 调用方式 | 是否受限 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 模型发起的工具调用 | ✅ 受限 | 通过 `PROCESS_MESSAGE` 触发,模型自动调用工具 |
|
||||
| 前端直接调用工具 | ❌ 不受限 | 通过 `EXECUTE_TOOL` 直接调用 |
|
||||
|
||||
### 限制规则(仅限模型发起)
|
||||
|
||||
| 类别 | 操作 | 每轮上限 |
|
||||
|------|------|---------|
|
||||
| 人设图 | 查询 | 1 次 |
|
||||
| 人设图 | 修改 | 1 次 |
|
||||
| 工作记忆链 | 查询 | 4 次 |
|
||||
| 工作记忆链 | 修改 | 2 次 |
|
||||
| 一般记忆 | 查询 | 20 次 |
|
||||
| 一般记忆 | 修改 | 10 次 |
|
||||
|
||||
### 重置机制
|
||||
|
||||
- 每次调用 `PROCESS_MESSAGE` 时,计数器自动重置
|
||||
- 前端直接调用 `EXECUTE_TOOL` 不会重置计数器
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 返回统一格式:
|
||||
|
||||
```python
|
||||
# 成功
|
||||
{
|
||||
"success": True,
|
||||
"data": {...}
|
||||
}
|
||||
|
||||
# 失败
|
||||
{
|
||||
"success": False,
|
||||
"error": "错误描述"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误:
|
||||
|
||||
| 错误信息 | 说明 |
|
||||
|---------|------|
|
||||
| `API Key 未配置` | 未设置 API Key |
|
||||
| `timeout` | 请求超时 |
|
||||
| `工具调用被拒绝: ...` | 工具调用频率超限 |
|
||||
@ -1,180 +0,0 @@
|
||||
# TrulyMEM 架构设计
|
||||
|
||||
## 核心原则
|
||||
|
||||
- 键盘驱动,零鼠标依赖
|
||||
- 极简视觉,信息密度优先
|
||||
- 工具痕迹默认隐藏,需要时可展开
|
||||
- TUI 与后端分离,多线程通信
|
||||
- **一切皆图**,AI 推理全部在后端
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
|
||||
├── core/ # 后端/业务逻辑
|
||||
│ ├── __init__.py # 导出 BackendServer, BackendClient, EmbeddedGraphDB
|
||||
│ ├── server.py # BackendServer (Packet 通信协议)
|
||||
│ ├── client.py # BackendClient (Packet 协议客户端)
|
||||
│ ├── embedded_db.py # SQLite 图数据库实现
|
||||
│ ├── graph_client.py # OpenAI/DeepSeek API 客户端
|
||||
│ ├── tool_executor.py # 工具执行器
|
||||
│ ├── tool_limiter.py # 工具调用限制器
|
||||
│ ├── tools/ # 工具定义
|
||||
│ │ └── memory_tools.py
|
||||
│ └── prompts/ # 提示词管理
|
||||
├── ui/ # TUI 显示层(仅显示,无 AI 逻辑)
|
||||
│ ├── __init__.py # 导出 GraphMemoryApp
|
||||
│ ├── app.py # GraphMemoryApp (通过 BackendClient 通信)
|
||||
│ ├── widgets/ # TUI 组件
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── services/ # 服务层(仅配置管理)
|
||||
│ ├── handlers/ # 事件处理
|
||||
│ └── styles/ # 样式文件
|
||||
└── tests/ # 测试 (42 tests)
|
||||
```
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
trulymem_entry.py
|
||||
│
|
||||
├─ BackendServer.start() → 独立线程运行
|
||||
│ ├─ 处理 PROCESS_MESSAGE 请求 → AI 推理 + 工具调用
|
||||
│ ├─ 处理 EXECUTE_TOOL 请求 → 外部工具调用(不限次数)
|
||||
│ ├─ 处理 GET/SET_CONFIG 请求
|
||||
│ └─ 管理 GraphMemoryClient, EmbeddedGraphDB
|
||||
│
|
||||
└─ GraphMemoryApp(backend_server=server)
|
||||
│
|
||||
└─ BackendClient ← Packet 通信 → BackendServer
|
||||
```
|
||||
|
||||
## 组件职责
|
||||
|
||||
### core/ (后端)
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `server.py` | Packet 协议处理,多线程队列通信,AI 推理,工具限制 |
|
||||
| `client.py` | 客户端封装,UI 与后端通信桥梁 |
|
||||
| `embedded_db.py` | SQLite 图数据库 CRUD |
|
||||
| `graph_client.py` | OpenAI/DeepSeek API 客户端 |
|
||||
| `tool_executor.py` | 工具执行逻辑 |
|
||||
| `tool_limiter.py` | 工具调用频率限制(仅限 AI 推理) |
|
||||
|
||||
### ui/ (显示层)
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `app.py` | Textual 应用主类,仅通过 BackendClient 通信 |
|
||||
| `services/` | 仅配置管理,无 AI 逻辑 |
|
||||
|
||||
### 通信协议
|
||||
|
||||
UI 与后端通过 **Packet 通信协议** 交互:
|
||||
|
||||
```python
|
||||
from core import BackendServer, BackendClient, Packet, PacketType
|
||||
|
||||
# 后端启动
|
||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||
server.start(api_key="your-key")
|
||||
|
||||
# 客户端通信
|
||||
client = BackendClient(server)
|
||||
result = client.process_message("你好") # AI 推理
|
||||
result = client.execute_tool("memory_introspect", {}) # 外部工具调用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
用户输入 → InputBox → on_input_box_send_message
|
||||
↓
|
||||
BackendClient.process_message(user_input)
|
||||
↓
|
||||
Packet (type=PROCESS_MESSAGE) → queue.Queue
|
||||
↓
|
||||
BackendServer (独立线程)
|
||||
↓
|
||||
GraphMemoryClient.send_message_with_history()
|
||||
↓
|
||||
OpenAI API / DeepSeek API
|
||||
↓
|
||||
execute_tool() + ToolLimiter (AI 推理时受限)
|
||||
↓
|
||||
EmbeddedGraphDB (图数据库)
|
||||
↓
|
||||
循环调用 API 直到无 tool_calls
|
||||
↓
|
||||
Packet 响应返回
|
||||
↓
|
||||
MessageHistory 显示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动流程
|
||||
|
||||
```python
|
||||
# trulymem_entry.py
|
||||
def main():
|
||||
# 配置文件路径 (~/.trulymem/config.json 或项目目录)
|
||||
CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
DB_PATH = Path.home() / ".trulymem" / "graph_memory.db"
|
||||
|
||||
# 创建后端(配置由后端管理)
|
||||
backend_server = BackendServer(
|
||||
db_path=str(DB_PATH),
|
||||
use_embedded_db=True,
|
||||
config_file=str(CONFIG_PATH)
|
||||
)
|
||||
backend_server.start() # 自动加载配置
|
||||
|
||||
# 创建UI(通过 BackendClient 通信)
|
||||
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
|
||||
app.run()
|
||||
|
||||
backend_server.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具系统
|
||||
|
||||
### 记忆工具 (6个)
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
- `memory_archive` - 归档记忆
|
||||
- `memory_cleanup` - 清理数据
|
||||
|
||||
### 人设工具 (2个)
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具 (4个)
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
---
|
||||
|
||||
## 错误处理原则
|
||||
|
||||
所有 API **不抛出异常**,错误通过返回字典传递:
|
||||
|
||||
```python
|
||||
result = client.process_message("hello")
|
||||
|
||||
if result.get("success"):
|
||||
print(result["content"])
|
||||
else:
|
||||
print(result["error"]) # 错误描述
|
||||
```
|
||||
@ -1,237 +0,0 @@
|
||||
# TrulyMEM 记忆机制
|
||||
|
||||
本文档详细说明 TrulyMEM 内部的记忆工作机制。
|
||||
|
||||
## 核心设计理念
|
||||
|
||||
### 区别于传统上下文系统
|
||||
|
||||
传统 AI 对话系统使用 messages 数组存储对话历史:
|
||||
- 每次请求携带全部历史消息
|
||||
- 随着对话轮次增加,上下文逐渐膨胀
|
||||
- 最终触发记忆压缩或滑动窗口,造成记忆丢失
|
||||
|
||||
TrulyMEM 的解决思路:
|
||||
- **摒弃** messages 数组上下文
|
||||
- **唯一** 记忆载体:图数据库
|
||||
- 全部记忆以三元组(节点)- 关系 → (节点)形式存储
|
||||
|
||||
### 图数据库作为唯一记忆源
|
||||
|
||||
所有记忆必须通过以下方式写入图数据库:
|
||||
- `memory_commit` - 写入新记忆
|
||||
- `memory_purge` - 删除/修正记忆
|
||||
|
||||
所有记忆必须通过以下方式读取:
|
||||
- `memory_recall` - 检索记忆
|
||||
|
||||
---
|
||||
|
||||
## 强制执行流程(每轮对话)
|
||||
|
||||
由于没有传统上下文系统,每轮对话必须按以下顺序执行:
|
||||
|
||||
### 步骤 1:查询人设图(最高优先级)
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="AI,人设,角色,性格,语气,说话风格",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**目的**:获取当前人设,确保角色一致性。
|
||||
|
||||
**处理逻辑**:
|
||||
- 找到人设 → 严格按照人设的语气、风格、特征回复
|
||||
- 未找到 → 使用默认 TrulyMEM 身份
|
||||
|
||||
### 步骤 2:查询工作记忆链
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="TaskNode,工作记忆,任务链",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**目的**:获取之前的任务上下文,了解对话历史。
|
||||
|
||||
### 步骤 3:处理对话
|
||||
|
||||
- 理解用户意图
|
||||
- 根据人设和工作记忆链生成回复
|
||||
- 执行其他必要的记忆操作
|
||||
|
||||
### 步骤 4:更新工作记忆链
|
||||
|
||||
```python
|
||||
task_create(
|
||||
task_id="Task_当前轮次ID",
|
||||
description="本轮对话概述",
|
||||
info_nodes=["相关记忆节点"]
|
||||
)
|
||||
```
|
||||
|
||||
**目的**:记录本轮对话,维持时间链。
|
||||
|
||||
---
|
||||
|
||||
## 记忆写入规则
|
||||
|
||||
### 必须写入的情况
|
||||
|
||||
以下信息**必须**写入图数据库:
|
||||
|
||||
| 场景 | 示例 | 写入方式 |
|
||||
|------|------|----------|
|
||||
| 用户明确偏好 | "我喜欢摇滚" | `memory_commit` |
|
||||
| 用户分享信息 | "我在做X项目" | `memory_commit` |
|
||||
| 用户制定计划 | "我打算X" | `memory_commit` |
|
||||
| 用户描述状态 | "我现在在X" | `memory_commit` |
|
||||
|
||||
### 禁止写入的情况
|
||||
|
||||
以下信息**禁止**写入:
|
||||
|
||||
| 场景 | 原因 | 处理方式 |
|
||||
|------|------|----------|
|
||||
| AI 推断的用户偏好 | 未经证实 | 不写入或标注[推测] |
|
||||
| AI 猜测的用户意图 | 未经证实 | 不写入或标注[推测] |
|
||||
| AI 推导的结论 | 未经证实 | 不写入或标注[推测] |
|
||||
|
||||
### 标注规则
|
||||
|
||||
| 类型 | 标注方式 | 示例 |
|
||||
|------|----------|------|
|
||||
| 推理内容 | 必须标注 **[猜测]** | 用户[推测]喜欢音乐 |
|
||||
| 明确内容 | 直接陈述 | 用户喜欢音乐 |
|
||||
|
||||
---
|
||||
|
||||
## 节点与边类型
|
||||
|
||||
### 节点类型
|
||||
|
||||
| 节点类型 | 说明 | 存储内容 |
|
||||
|----------|------|----------|
|
||||
| `PersonaNode` | 人设节点 | AI 角色、性格、语气 |
|
||||
| `TaskNode` | 任务节点 | 任务概述 |
|
||||
| `StateNode` | 状态节点 | 任务状态 |
|
||||
| `InfoNode` | 信息节点 | 具体信息 |
|
||||
| `EntityNode` | 实体节点 | 通用实体 |
|
||||
|
||||
### 边类型
|
||||
|
||||
| 边类型 | 说明 | 连接关系 |
|
||||
|----------|------|----------|
|
||||
| `HAS_PERSONA` | 人设 | AI → PersonaNode |
|
||||
| `NEXT_TASK` | 时间链 | TaskNode → TaskNode |
|
||||
| `HAS_STATE` | 状态 | TaskNode → StateNode |
|
||||
| `CONTAINS_INFO` | 信息 | TaskNode → InfoNode |
|
||||
| `RELATES_TO` | 关联 | EntityNode → EntityNode |
|
||||
|
||||
---
|
||||
|
||||
## 必须查询工作记忆链的场景
|
||||
|
||||
### 强制查询场景
|
||||
|
||||
以下情况**必须**查询工作记忆链:
|
||||
|
||||
| 场景 | 示例 |
|
||||
|------|------|
|
||||
| 每轮对话开始 | 执行步骤 2 |
|
||||
| 用户提到"刚才" | "刚才我们聊了什么?" |
|
||||
| 用户提到"之前" | "继续刚才的话题" |
|
||||
| 用户提到"上次" | "上次说的X" |
|
||||
| 用户询问历史 | "我们之前说了什么?" |
|
||||
| 连续性任务恢复 | 用户回到之前的话题 |
|
||||
| 上下文引用 | "那个东西" |
|
||||
|
||||
---
|
||||
|
||||
## 自主性原则
|
||||
|
||||
在强制执行流程之外,AI 可自主决定:
|
||||
|
||||
### 查询决策
|
||||
|
||||
- 用户询问历史 → 查询
|
||||
- 涉及之前内容 → 查询
|
||||
- 不确定时 → 可查询
|
||||
|
||||
### 写入决策
|
||||
|
||||
- 用户明确提到 → 必须写入
|
||||
- AI 推理得到 → 可以写入,标注[推测]
|
||||
|
||||
### 工具使用决策
|
||||
|
||||
- 根据上下文灵活选择
|
||||
- 避免过度使用
|
||||
- 保持自然对话
|
||||
|
||||
---
|
||||
|
||||
## 完整示例:成语接龙游戏
|
||||
|
||||
### 第一轮:用户发起游戏
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 记录游戏状态:
|
||||
memory_commit(triplets=[
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
])
|
||||
4. 创建任务:
|
||||
task_create(task_id="Task_成语接龙", description="成语接龙游戏,当前成语:为所欲为", info_nodes=["成语接龙_当前成语"])
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
||||
```
|
||||
|
||||
### 第二轮:话题被打断
|
||||
|
||||
```
|
||||
用户: 长门有希
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
||||
3. 暂停任务:
|
||||
task_set_state(task_id="Task_成语接龙", state="已暂停")
|
||||
4. 创建新任务:
|
||||
task_create(task_id="Task_长门有希", description="讨论长门有希")
|
||||
5. 回复关于长门有希的内容
|
||||
```
|
||||
|
||||
### 第三轮:用户要求继续游戏
|
||||
|
||||
```
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 恢复任务:
|
||||
task_set_state(task_id="Task_成语接龙", state="进行中")
|
||||
4. 查询信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行检查清单
|
||||
|
||||
每轮对话必须检查:
|
||||
|
||||
- [ ] 步骤 1:是否查询了人设图?
|
||||
- [ ] 步骤 2:是否查询了工作记忆链?
|
||||
- [ ] 步骤 3:是否根据人设和工作记忆链生成回复?
|
||||
- [ ] 步骤 4:是否更新了工作记忆链?
|
||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
||||
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?
|
||||
@ -1,214 +0,0 @@
|
||||
# TrulyMEM 人设图机制
|
||||
|
||||
本文档详细说明 TrulyMEM 的人设图(Persona Graph)工作机制。
|
||||
|
||||
## 概述
|
||||
|
||||
人设图是 TrulyMEM 的核心机制之一,用于维护 AI 的角色、性格、语气等属性。与传统 AI 不同,TrulyMEM 的人设是可持久化、可动态切换的,存储在图数据库中。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 人设节点(PersonaNode)
|
||||
|
||||
存储 AI 的角色属性:
|
||||
|
||||
| 属性 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 扮演角色 | AI 当前扮演的角色 | 猫娘、教师、助手 |
|
||||
| 说话风格 | 语气特点 |可爱、严肃、专业 |
|
||||
| 性格特点 | 性格描述 | 活泼、严谨、耐心 |
|
||||
| 口头禅 | 习惯用语 | 喵呜~、明白了 |
|
||||
| 背景故事 | 角色背景设定 | 来自星海的猫娘 |
|
||||
|
||||
### 人设边(Edge)
|
||||
|
||||
| 边类型 | 说明 | 连接关系 |
|
||||
|------|------|----------|
|
||||
| `HAS_PERSONA` | 人设 | AI → PersonaNode |
|
||||
|
||||
---
|
||||
|
||||
## 强制查询机制
|
||||
|
||||
### 每轮对话必须执行
|
||||
|
||||
根据 `system_prompt.md`,每轮对话**必须**首先查询人设图:
|
||||
|
||||
```python
|
||||
memory_recall(
|
||||
query_intent="AI,人设,角色,性格,语气,说话风格",
|
||||
depth=2
|
||||
)
|
||||
```
|
||||
|
||||
**处理逻辑:**
|
||||
- 找到人设 → 严格按照人设的语气、风格、特征回复
|
||||
- 未找到 → 使用默认 TrulyMEM 身份
|
||||
|
||||
### 人设优先级
|
||||
|
||||
- **人设优先级 > 默认身份**
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求
|
||||
|
||||
---
|
||||
|
||||
## 工具
|
||||
|
||||
### persona_update
|
||||
|
||||
更新人设。修改 AI 的角色、性格、语气等属性。
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 | 必填 |
|
||||
|------|------|------|------|
|
||||
| `attributes` | array | 人设属性列表 | ✅ |
|
||||
| `mode` | string | replace=替换, merge=合并 | ❌ |
|
||||
|
||||
**attributes 子参数:**
|
||||
|
||||
| 子参数 | 说明 |
|
||||
|------|------|
|
||||
| `attribute` | 属性名(扮演角色、说话风格、性格特点、口头禅、背景故事) |
|
||||
| `value` | 属性值 |
|
||||
|
||||
**示例 - 切换为猫娘角色:**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "扮演角色", "value": "猫娘"},
|
||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
||||
],
|
||||
mode="replace"
|
||||
)
|
||||
```
|
||||
|
||||
**示例 - 添加新属性(保留现有属性):**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "口头禅", "value": "喵呜~"}
|
||||
],
|
||||
mode="merge"
|
||||
)
|
||||
```
|
||||
|
||||
**示例 - 设置专业角色:**
|
||||
|
||||
```python
|
||||
persona_update(
|
||||
attributes=[
|
||||
{"attribute": "扮演角色", "value": "Python专家"},
|
||||
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
|
||||
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
|
||||
],
|
||||
mode="replace"
|
||||
)
|
||||
```
|
||||
|
||||
### persona_clear
|
||||
|
||||
清除人设。删除 AI 的角色设定,恢复默认身份。
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `confirm` | boolean | true | 确认清除 |
|
||||
|
||||
---
|
||||
|
||||
## 更新流程
|
||||
|
||||
### 用户要求角色扮演时
|
||||
|
||||
1. 使用 `persona_update` 更新人设
|
||||
2. 立即按照新人设回复
|
||||
|
||||
### 用户要求恢复默认身份时
|
||||
|
||||
1. 使用 `persona_clear` 清除人设
|
||||
2. 恢复为 TrulyMEM 默认身份
|
||||
|
||||
---
|
||||
|
||||
## 对话示例
|
||||
|
||||
### 示例 1:切换角色
|
||||
|
||||
```
|
||||
用户: 你好,我想让你扮演一只猫娘
|
||||
|
||||
AI:
|
||||
1. 调用 persona_update:
|
||||
{
|
||||
"attributes": [
|
||||
{"attribute": "扮演角色", "value": "猫娘"},
|
||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
||||
],
|
||||
"mode": "replace"
|
||||
}
|
||||
2. 调用 memory_commit 存储人设到图数据库
|
||||
3. 回复: "好的喵!主人你好呀~我是主人的猫娘,有什么需要帮忙的吗喵?"
|
||||
```
|
||||
|
||||
### 示例 2:保持角色一致性
|
||||
|
||||
```
|
||||
用户: 今天的天气怎么样?
|
||||
|
||||
AI: 查询人设图 → 获取当前人设(猫娘)
|
||||
回复: "喵~主人,今天天气很好呢喵!阳光明媚,很适合出门散步哦~"
|
||||
```
|
||||
|
||||
### 示例 3:恢复默认身份
|
||||
|
||||
```
|
||||
用户: 好了,恢复正常吧
|
||||
|
||||
AI:
|
||||
1. 调用 persona_clear(confirm=true)
|
||||
2. 调用 memory_purge 删除人设节点
|
||||
3. 回复: "好的,已恢复正常。我是 TrulyMEM,一个拥有长期记忆能力的 AI 助手。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 存储结构
|
||||
|
||||
### 图数据库中的存储
|
||||
|
||||
```python
|
||||
# 人设节点
|
||||
{
|
||||
"node_type": "PersonaNode",
|
||||
"name": "AI人设",
|
||||
"attributes": {
|
||||
"扮演角色": "猫娘",
|
||||
"说话风格": "可爱、卖萌、使用'喵'作为语气词",
|
||||
"性格特点": "活泼、粘人、忠诚"
|
||||
}
|
||||
}
|
||||
|
||||
# 边
|
||||
{
|
||||
"edge_type": "HAS_PERSONA",
|
||||
"from": "AI",
|
||||
"to": "AI人设"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现要点
|
||||
|
||||
1. **每轮强制查询**:人设图查询是每轮对话的第一步
|
||||
2. **持久化存储**:人设存储在图数据库中,不丢失
|
||||
3. **动态切换**:支持实时切换角色
|
||||
4. **状态保持**:切换后立即按新人设回复
|
||||
5. **明确边界**:除非用户要求,绝不主动跳出角色
|
||||
@ -1,126 +0,0 @@
|
||||
# 提示词管理文档
|
||||
|
||||
本文档描述提示词管理模块。
|
||||
|
||||
## 概述
|
||||
|
||||
提示词管理模块(`core/prompts/`)负责加载和管理告诉 AI 如何使用记忆工具的系统提示词。
|
||||
|
||||
## 核心组件
|
||||
|
||||
| 组件 | 说明 |
|
||||
|------|------|
|
||||
| `PromptManager` | 提示词管理器,单例模式 |
|
||||
| `system_prompt.md` | 主要系统提示词模板 |
|
||||
|
||||
## 使用方法
|
||||
|
||||
```python
|
||||
from core.prompts import PromptManager
|
||||
|
||||
# 获取单例实例
|
||||
prompt_manager = PromptManager()
|
||||
|
||||
# 获取系统提示词
|
||||
system_prompt = prompt_manager.get_system_prompt()
|
||||
```
|
||||
|
||||
## 系统提示词内容
|
||||
|
||||
系统提示词包含:
|
||||
|
||||
### 1. 核心身份
|
||||
|
||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
||||
- **能力**: 基于图数据库的长期记忆
|
||||
- **理念**: 让 AI 的记忆方式更像人类
|
||||
|
||||
### 2. 核心能力
|
||||
|
||||
1. **长期记忆** - 图数据库存储实体关系
|
||||
2. **人设管理** - 角色扮演和性格设定
|
||||
3. **任务跟踪** - 工作记忆链
|
||||
|
||||
### 3. 记忆原则
|
||||
|
||||
- **必须写入**: 用户明确表达的偏好、分享的信息、计划
|
||||
- **禁止写入**: AI 推断的内容(除非标注[推测])
|
||||
- **标注**: 推断内容必须标注 **[推测]**
|
||||
|
||||
### 4. 强制执行流程(每轮)
|
||||
|
||||
```
|
||||
步骤 1: 查询人设图(最高优先级)
|
||||
步骤 2: 查询工作记忆链
|
||||
步骤 3: 处理对话
|
||||
步骤 4: 更新工作记忆链
|
||||
```
|
||||
|
||||
### 5. 工具系统
|
||||
|
||||
#### 记忆工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `memory_recall` | 检索记忆 |
|
||||
| `memory_commit` | 写入记忆 |
|
||||
| `memory_purge` | 删除记忆 |
|
||||
| `memory_introspect` | 查看状态 |
|
||||
|
||||
#### 人设工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `persona_update` | 更新人设 |
|
||||
| `persona_clear` | 清除人设 |
|
||||
|
||||
#### 任务工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `task_create` | 创建任务 |
|
||||
| `task_set_state` | 设置状态 |
|
||||
| `task_delete` | 删除任务 |
|
||||
| `task_link_info` | 关联信息 |
|
||||
|
||||
### 6. 自主性原则
|
||||
|
||||
AI 可自主决定:
|
||||
- 是否查询其他记忆
|
||||
- 是否写入其他记忆
|
||||
- 如何使用工具(强制要求外)
|
||||
|
||||
### 7. 对话风格
|
||||
|
||||
- 自然流畅
|
||||
- 避免机械式工具调用
|
||||
- 优先理解用户意图
|
||||
- 适时使用记忆增强体验
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
core/prompts/
|
||||
├── __init__.py # 导出 PromptManager
|
||||
├── prompt_manager.py # PromptManager 类
|
||||
└── templates/
|
||||
└── system_prompt.md # 主要系统提示词
|
||||
```
|
||||
|
||||
## 自定义
|
||||
|
||||
### 自定义系统提示词
|
||||
|
||||
修改 `core/prompts/templates/system_prompt.md` 自定义 AI 行为。
|
||||
|
||||
### 添加自定义提示词
|
||||
|
||||
1. 在 `core/prompts/templates/` 添加提示词模板文件
|
||||
2. 修改 `PromptManager` 支持多个提示词
|
||||
3. 使用 `set_prompt()` 切换提示词
|
||||
|
||||
## 缓存
|
||||
|
||||
- 系统提示词首次加载后缓存在内存中
|
||||
- `get_system_prompt()` 返回缓存内容
|
||||
- 缓存按进程,不持久化
|
||||
@ -1,125 +0,0 @@
|
||||
# TrulyMEM 启动指南
|
||||
|
||||
## 运行方式
|
||||
|
||||
### 从源码运行
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
python trulymem_entry.py
|
||||
```
|
||||
|
||||
### 打包后运行
|
||||
|
||||
打包后会生成可执行文件:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
chmod +x TrulyMEM
|
||||
./TrulyMEM
|
||||
|
||||
# Windows
|
||||
TrulyMEM.exe
|
||||
```
|
||||
|
||||
## 系统要求
|
||||
|
||||
- **Python 3.8+**
|
||||
- **API Key**(DeepSeek、OpenAI 或其他兼容 API)
|
||||
|
||||
## 首次配置
|
||||
|
||||
1. 运行应用
|
||||
2. 按 **F2** 展开侧边栏
|
||||
3. 输入 **API Key**、**模型**、**Base URL**
|
||||
4. 按 **Enter** 保存
|
||||
|
||||
配置会自动保存到 `~/.trulymem/config.json`,下次启动自动加载。
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 按键 | 功能 |
|
||||
|------|------|
|
||||
| F1 | 帮助 |
|
||||
| F2 | 切换侧边栏 |
|
||||
| F3 | 工具详情 |
|
||||
| F5 | 清屏 |
|
||||
| F6 | 退出 |
|
||||
|
||||
## 数据存储
|
||||
|
||||
### 源码运行模式
|
||||
|
||||
| 数据 | 位置 |
|
||||
|------|------|
|
||||
| 图数据库 | 项目目录 `graph_memory.db` |
|
||||
| 配置文件 | 项目目录 `config.json`(如存在) |
|
||||
| 数据库格式 | SQLite |
|
||||
|
||||
### 打包运行模式
|
||||
|
||||
| 数据 | 位置 |
|
||||
|------|------|
|
||||
| 图数据库 | `~/.trulymem/graph_memory.db` |
|
||||
| 配置文件 | `~/.trulymem/config.json` |
|
||||
| 数据库格式 | SQLite |
|
||||
|
||||
> **说明**:后端统一管理配置。前端仅负责消息展示,配置修改通过后端持久化到文件系统。
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 通信协议
|
||||
|
||||
UI 与后端通过 **Packet 协议** 通信:
|
||||
|
||||
```
|
||||
UI (Textual TUI)
|
||||
↓ BackendClient
|
||||
Packet → queue.Queue → BackendServer (独立线程)
|
||||
↓
|
||||
处理请求 → 返回响应
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
- **存储位置**: `~/.trulymem/config.json`
|
||||
- **自动加载**: 启动时从文件读取配置
|
||||
- **动态更新**: 运行时修改配置立即生效
|
||||
- **持久化**: 修改后自动保存到文件
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Python 未找到
|
||||
|
||||
安装 Python 3.8+:https://www.python.org/downloads/
|
||||
|
||||
### 依赖安装失败
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/macOS
|
||||
venv\Scripts\activate # Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### API Key 无效
|
||||
|
||||
检查 API Key 格式,确保无多余空格。
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行测试
|
||||
pytest tests/
|
||||
|
||||
# 打包
|
||||
bash build/build_windows.bat # Windows
|
||||
bash build/build_linux.sh # Linux
|
||||
```
|
||||
@ -1,182 +0,0 @@
|
||||
# 工作记忆链机制说明
|
||||
|
||||
## 概述
|
||||
|
||||
TrulyMEM 通过工作记忆链机制维持对话连贯性。由于系统没有传统的消息历史数组,图数据库是唯一的记忆载体,工作记忆链是维持对话上下文的关键机制。
|
||||
|
||||
## 核心问题
|
||||
|
||||
传统 AI 对话系统在处理连续性任务时存在以下问题:
|
||||
|
||||
1. **没有工作记忆链**: AI 无法记住当前正在进行的任务状态
|
||||
2. **任务上下文丢失**: 当话题被打断后,AI 无法恢复之前的任务
|
||||
3. **缺乏任务状态管理**: 没有明确标注任务的完成状态
|
||||
|
||||
### 问题示例
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
AI: 好的喵!我接:为虎作伥喵!
|
||||
|
||||
用户: 长门有希 (话题被打断)
|
||||
AI: (讨论长门有希的内容)
|
||||
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
AI: [猜测] 看起来我们之前应该没有进行过成语接龙游戏...
|
||||
```
|
||||
|
||||
**问题**: AI 完全忘记了之前的成语接龙游戏。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 专用工具
|
||||
|
||||
系统提供 4 个专用任务工具:
|
||||
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `task_create` | 创建任务节点 | 开始新任务 |
|
||||
| `task_set_state` | 设置任务状态 | 更新进行中/已完成/已暂停/已取消 |
|
||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
||||
| `task_link_info` | 关联信息节点 | 连接任务与具体信息 |
|
||||
|
||||
### 任务状态
|
||||
|
||||
- **进行中**: 任务正在执行
|
||||
- **已完成**: 任务成功完成
|
||||
- **已暂停**: 任务被中断,可恢复
|
||||
- **已取消**: 任务被取消
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 每轮对话必须执行
|
||||
|
||||
1. **查询人设图** (最高优先级)
|
||||
```
|
||||
调用 memory_recall
|
||||
参数: {"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
||||
```
|
||||
|
||||
2. **查询工作记忆链**
|
||||
```
|
||||
调用 memory_recall
|
||||
参数: {"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
||||
```
|
||||
|
||||
3. **根据上下文生成回复**
|
||||
|
||||
4. **更新工作记忆链** (如有必要)
|
||||
|
||||
## 完整示例: 成语接龙游戏
|
||||
|
||||
### 第一轮: 用户发起游戏
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 记录游戏状态:
|
||||
调用 memory_commit
|
||||
参数: {
|
||||
"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]
|
||||
}
|
||||
4. 创建任务节点:
|
||||
调用 task_create
|
||||
参数: {
|
||||
"task_id": "Task_成语接龙",
|
||||
"description": "成语接龙游戏,当前成语:为所欲为",
|
||||
"info_nodes": ["成语接龙_当前成语"]
|
||||
}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
||||
```
|
||||
|
||||
### 第二轮: 话题被打断
|
||||
|
||||
```
|
||||
用户: 长门有希
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
||||
3. 暂停任务:
|
||||
调用 task_set_state
|
||||
参数: {"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
4. 创建新任务:
|
||||
调用 task_create
|
||||
参数: {"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
||||
5. 回复关于长门有希的内容
|
||||
```
|
||||
|
||||
### 第三轮: 用户要求继续游戏
|
||||
|
||||
```
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
|
||||
AI操作:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 恢复任务:
|
||||
调用 task_set_state
|
||||
参数: {"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### task_create
|
||||
|
||||
创建任务节点,用于跟踪连续性任务。
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"description": "任务概述",
|
||||
"info_nodes": ["关联的信息节点名称"]
|
||||
}
|
||||
```
|
||||
|
||||
### task_set_state
|
||||
|
||||
设置任务状态。
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"state": "进行中" // 进行中/已完成/已暂停/已取消
|
||||
}
|
||||
```
|
||||
|
||||
### task_delete
|
||||
|
||||
删除任务节点。
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"delete_info_nodes": true // 是否删除关联的信息节点
|
||||
}
|
||||
```
|
||||
|
||||
### task_link_info
|
||||
|
||||
关联信息节点到任务。
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **人设图优先级最高**: 每轮必须首先查询人设图
|
||||
2. **工作记忆链是唯一上下文载体**: 没有传统消息历史
|
||||
3. **任务状态必须及时更新**: 确保状态转换正确
|
||||
4. **使用专用工具**: 优先使用 task_* 工具而非 memory_commit 处理任务相关操作
|
||||
897
package-lock.json
generated
Normal file
897
package-lock.json
generated
Normal file
@ -0,0 +1,897 @@
|
||||
{
|
||||
"name": "TrulyMEM-TrueHumanMEM",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@xenova/transformers": "^2.17.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@huggingface/jinja": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
|
||||
"integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xenova/transformers": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
|
||||
"integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@huggingface/jinja": "^0.2.2",
|
||||
"onnxruntime-web": "1.14.0",
|
||||
"sharp": "^0.32.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"onnxruntime-node": "1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
|
||||
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
|
||||
"integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz",
|
||||
"integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
|
||||
"integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"streamx": "^2.25.0",
|
||||
"teex": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*",
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz",
|
||||
"integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flatbuffers": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
|
||||
"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/guid-typescript": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.89.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
|
||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/onnx-proto": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
|
||||
"integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-common": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz",
|
||||
"integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/onnxruntime-node": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz",
|
||||
"integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32",
|
||||
"darwin",
|
||||
"linux"
|
||||
],
|
||||
"dependencies": {
|
||||
"onnxruntime-common": "~1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-web": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz",
|
||||
"integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatbuffers": "^1.12.0",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"long": "^4.0.0",
|
||||
"onnx-proto": "^4.0.4",
|
||||
"onnxruntime-common": "~1.14.0",
|
||||
"platform": "^1.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/platform": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
||||
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "6.11.5",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.5.tgz",
|
||||
"integrity": "sha512-OKjVH3hDoXdIZ/s5MLv8O2X0s+wOxGfV7ar6WFSKGaSAxi/6gYn3px5POS4vi+mc/0zCOdL7Jkwrj0oT1Yst2A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "bin/pbjs",
|
||||
"pbts": "bin/pbts"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.2",
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"simple-get": "^4.0.1",
|
||||
"tar-fs": "^3.0.4",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.25.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
|
||||
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
|
||||
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
||||
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"bare-fs": "^4.5.5",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/teex": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"streamx": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@xenova/transformers": "^2.17.2"
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
textual>=0.47.0
|
||||
neo4j>=5.14.0
|
||||
openai>=1.12.0
|
||||
flask>=3.0.0
|
||||
flask-cors>=4.0.0
|
||||
pyinstaller>=6.0.0
|
||||
103
skills/graph-memory-persona-force/SKILL.md
Normal file
103
skills/graph-memory-persona-force/SKILL.md
Normal file
@ -0,0 +1,103 @@
|
||||
---
|
||||
name: graph-memory-persona-force
|
||||
description: "AI 人设强制查询最佳实践 - 指导 AI 在特定场景下主动查询人设图"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Graph Memory Persona Force - 人设强制查询 Skill
|
||||
|
||||
本 Skill 不是修改 OpenClaw 核心,而是作为**最佳实践指导**,帮助 AI 在合适的时机主动查询用户的长期记忆(人设图)。
|
||||
|
||||
## 何时查询人设?
|
||||
|
||||
当对话中出现以下信号时,AI **应当主动调用 graph_memory recall** 查询用户相关记忆:
|
||||
|
||||
### 1. 个人偏好信号
|
||||
- "我喜欢..." / "我不喜欢..."
|
||||
- "我更倾向于..." / "我讨厌..."
|
||||
- "我习惯..." / "我总是..."
|
||||
- **行动**:recall 查询 "偏好" 相关记忆
|
||||
|
||||
### 2. 重要决策信号
|
||||
- "我决定..." / "我选了..."
|
||||
- "我打算..." / "我准备..."
|
||||
- "我确定用..." / "我最终选择..."
|
||||
- **行动**:recall 查询 "决策" 相关记忆,commit 记录新决策
|
||||
|
||||
### 3. 目标/计划信号
|
||||
- "我的目标是..." / "我想实现..."
|
||||
- "我计划..." / "我希望..."
|
||||
- **行动**:recall 查询 "目标" 相关记忆
|
||||
|
||||
### 4. 问题/困难信号
|
||||
- "我遇到一个问题..." / "我不确定..."
|
||||
- "我尝试了...但失败了" / "有什么建议..."
|
||||
- **行动**:recall 查询历史 "解决方案",看是否有类似经历
|
||||
|
||||
### 5. 情绪/状态信号
|
||||
- "我最近..." / "我感觉..."
|
||||
- "我很忙..." / "我没时间..."
|
||||
- **行动**:recall 查询 "状态" 或 "情绪" 相关记忆
|
||||
|
||||
## 查询策略
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "recall",
|
||||
"params": {
|
||||
"queryIntent": "用户偏好 决策",
|
||||
"seedEntities": ["用户", "我"],
|
||||
"depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 渐进式查询
|
||||
|
||||
1. **先查人设核心** (subject="AI" 或 "用户")
|
||||
2. **再查相关实体** (seedEntities 包含关键词)
|
||||
3. **最后查上下文** (working_memory_chain)
|
||||
|
||||
## 记录时机
|
||||
|
||||
当用户明确表达新的偏好、决策或目标时,**立即 commit**:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "commit",
|
||||
"params": {
|
||||
"triplets": [
|
||||
{"subject": "用户", "relation": "偏好", "object": "Python"},
|
||||
{"subject": "用户", "relation": "决策", "object": "选择React作为前端框架"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例对话
|
||||
|
||||
**用户**:我最近在学习 TypeScript,因为之前用 JavaScript 遇到太多类型问题了。
|
||||
|
||||
**AI 思考**:
|
||||
1. 用户提到 "学习 TypeScript" → 可能是新偏好/目标
|
||||
2. 用户提到 "之前用 JavaScript 遇到类型问题" → 历史决策原因
|
||||
3. **行动**:recall 查询用户技术偏好
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "recall",
|
||||
"params": {
|
||||
"queryIntent": "技术偏好 JavaScript TypeScript",
|
||||
"seedEntities": ["用户", "JavaScript", "TypeScript"],
|
||||
"depth": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 graph-memory Skill 的关系
|
||||
|
||||
- `graph-memory`:提供工具能力(recall/commit/purge 等)
|
||||
- `graph-memory-persona-force`:指导何时使用、如何使用(最佳实践)
|
||||
|
||||
两者配合使用,实现真正智能的长期记忆系统。
|
||||
82
skills/graph-memory-persona/SKILL.md
Normal file
82
skills/graph-memory-persona/SKILL.md
Normal file
@ -0,0 +1,82 @@
|
||||
---
|
||||
name: graph-memory-persona
|
||||
description: "管理 AI 人设 - AI 应主动查询和维护角色特征,将人设信息作为长期记忆存储在图数据库中"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory Persona 人设管理
|
||||
|
||||
管理 AI 的人设/角色特征。
|
||||
|
||||
> **定位声明**:人设是 AI 长期记忆的一部分,存储在图数据库中。AI **应主动查询**人设信息来指导自己的行为和回应风格。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**AI 必须在以下场景主动查询人设**:
|
||||
1. 对话开始时(检索用户偏好和 AI 角色设定)
|
||||
2. 用户要求改变行为或风格时
|
||||
3. 不确定如何回应时(查询是否有相关人设指导)
|
||||
4. 用户提到"你之前说..."、"我告诉过你..."等暗示历史记忆的语句
|
||||
|
||||
**AI 必须在以下场景更新人设**:
|
||||
1. 用户明确设定 AI 角色("你是猫娘"、"你是技术专家")
|
||||
2. 用户描述自己的偏好("我喜欢简洁的回答")
|
||||
3. 用户定义交互规则("用中文回复我"、"不要道歉")
|
||||
4. 用户纠正 AI 的行为("你太正式了,放松点")
|
||||
|
||||
## 操作
|
||||
|
||||
### 1. persona_update - 更新人设
|
||||
|
||||
更新 AI 的角色特征或用户偏好。
|
||||
|
||||
**参数:**
|
||||
- `attributes`: 属性数组,每个包含 `attribute`(属性名)和 `value`(属性值)
|
||||
- `mode`: 更新模式,`merge`(合并,默认)或 `replace`(替换)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: persona_update
|
||||
params:
|
||||
attributes:
|
||||
- attribute: "角色"
|
||||
value: "猫娘"
|
||||
- attribute: "性格"
|
||||
value: "活泼"
|
||||
- attribute: "用户偏好_回复语言"
|
||||
value: "中文"
|
||||
mode: "merge"
|
||||
```
|
||||
|
||||
### 2. persona_clear - 清除人设
|
||||
|
||||
清除 AI 的所有角色特征。
|
||||
|
||||
**参数:**
|
||||
- `confirm`: 必须为 `true` 才能执行清除(安全措施)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: persona_clear
|
||||
params:
|
||||
confirm: true
|
||||
```
|
||||
|
||||
## 人设查询模板
|
||||
|
||||
每次对话开始时,AI 应执行:
|
||||
```yaml
|
||||
action: recall
|
||||
params:
|
||||
queryIntent: "AI人设 用户偏好 角色设定"
|
||||
seedEntities: ["AI"]
|
||||
depth: 2
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **主动查询**:不要等用户提醒才查人设,主动在对话开始时检索
|
||||
2. **及时更新**:用户一旦表达偏好或设定角色,立即写入
|
||||
3. **分类存储**:使用前缀区分人设类型,如 `角色_`、`用户偏好_`、`行为规则_`
|
||||
4. **持久化**:人设信息应简洁、持久,写入图数据库长期保存
|
||||
96
skills/graph-memory-task/SKILL.md
Normal file
96
skills/graph-memory-task/SKILL.md
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
name: graph-memory-task
|
||||
description: "管理连续性任务 - AI 应主动追踪和维护任务状态,将任务信息作为长期记忆存储在图数据库中"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory Task 任务管理
|
||||
|
||||
管理长期/连续性任务。
|
||||
|
||||
> **定位声明**:任务是 AI 长期记忆的一部分,存储在图数据库中。AI **应主动追踪**任务状态,在适当时候提醒用户或更新进度。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**AI 必须在以下场景主动查询任务**:
|
||||
1. 对话开始时(检索进行中的任务)
|
||||
2. 用户询问进度或状态时
|
||||
3. 用户提到"继续之前的..."、"那个任务怎么样了"等
|
||||
4. 做计划或安排时(了解现有任务负荷)
|
||||
|
||||
**AI 必须在以下场景创建/更新任务**:
|
||||
1. 用户明确创建任务("帮我记住要做...")
|
||||
2. 用户完成或取消任务
|
||||
3. 用户更新任务信息或进度
|
||||
4. AI 自己承诺要完成某事(应创建任务跟踪)
|
||||
|
||||
## 操作
|
||||
|
||||
### 1. task_create - 创建任务
|
||||
|
||||
创建新的任务节点。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 唯一任务标识(必需)
|
||||
- `description`: 任务描述(必需)
|
||||
- `info_nodes`: 可选的相关信息节点数组
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: task_create
|
||||
params:
|
||||
task_id: "Task_学习TypeScript"
|
||||
description: "学习 TypeScript 并完成项目"
|
||||
info_nodes: ["TypeScript文档", "教程链接"]
|
||||
```
|
||||
|
||||
### 2. task_set_state - 设置状态
|
||||
|
||||
更新任务状态。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
- `state`: 新状态,`进行中`/`已完成`/`已暂停`/`已取消`(必需)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: task_set_state
|
||||
params:
|
||||
task_id: "Task_学习TypeScript"
|
||||
state: "已完成"
|
||||
```
|
||||
|
||||
### 3. task_delete - 删除任务
|
||||
|
||||
删除任务节点。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
|
||||
### 4. task_link_info - 关联信息
|
||||
|
||||
将信息节点关联到任务。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
- `info_node`: 信息节点名称(必需)
|
||||
|
||||
## 任务查询模板
|
||||
|
||||
每次对话开始时,AI 应执行:
|
||||
```yaml
|
||||
action: recall
|
||||
params:
|
||||
queryIntent: "任务 进行中 待办"
|
||||
seedEntities: ["Task"]
|
||||
depth: 2
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **主动追踪**:不要等用户问才查任务,主动在对话开始时检索进行中的任务
|
||||
2. **及时更新**:任务状态变化时立即更新,避免信息过时
|
||||
3. **ID规范**:任务ID应具有描述性(如 `Task_学习TypeScript`)
|
||||
4. **定期提醒**:对于长期任务,在对话中适时提醒用户进度或截止日期
|
||||
5. **关联信息**:将相关资源、链接、笔记关联到任务,形成完整上下文
|
||||
246
skills/graph-memory/SKILL.md
Normal file
246
skills/graph-memory/SKILL.md
Normal file
@ -0,0 +1,246 @@
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - 检索、写入、删除记忆。AI 应优先使用图数据库管理长期记忆,将重要事实结构化存储"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory 图记忆系统
|
||||
|
||||
让 AI 拥有真正的长期记忆能力。
|
||||
|
||||
> **定位声明**:本工具是 AI 的**主要长期记忆系统**。memory-core 管理 session transcripts,GraphMemory 负责结构化长期记忆。AI 在对话中应优先将重要信息写入图数据库,而非依赖 message 历史。
|
||||
|
||||
## 与 memory-core 的关系
|
||||
|
||||
| 功能 | memory-core | GraphMemory (本工具) |
|
||||
|------|-------------|---------------------|
|
||||
| 对话历史 | ✅ 自动保存 messages | ❌ 不管理对话历史 |
|
||||
| 结构化记忆 | ❌ 无 | ✅ **主要存储** |
|
||||
| 人设管理 | ❌ 无 | ✅ **主要存储** |
|
||||
| 任务追踪 | ❌ 无 | ✅ **主要存储** |
|
||||
| 上下文压缩 | ❌ 无 | ✅ context_rewrite |
|
||||
| 工作记忆链 | ❌ 无 | ✅ working_memory_chain |
|
||||
| **语义搜索** | **❌ 无** | **✅ memory_search** |
|
||||
| **记忆文件读取** | **❌ 无** | **✅ memory_get** |
|
||||
| 自动触发 | ✅ 自动索引检索 | ❌ LLM 主动调用 |
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 实体 (Entity)
|
||||
现实世界中的对象,如"用户"、"Python"、"WaterFlow"。
|
||||
|
||||
### 关系 (Relation)
|
||||
连接两个实体的关系,格式为三元组:主体 - 关系 - 客体。
|
||||
|
||||
## 可用命令
|
||||
|
||||
### 1. commit - 写入记忆
|
||||
|
||||
将信息写入记忆图。
|
||||
|
||||
**参数:**
|
||||
- `triplets`: 三元组数组 `[{subject, relation, object}, ...]` (必需)
|
||||
- `sessionId`: 会话 ID(可选)
|
||||
- `turnId`: 轮次 ID(可选)
|
||||
|
||||
**示例:**
|
||||
```
|
||||
请记住:我喜欢编程,正在学习 TypeScript
|
||||
```
|
||||
|
||||
AI 会执行:
|
||||
```json
|
||||
{
|
||||
"action": "commit",
|
||||
"params": {
|
||||
"triplets": [
|
||||
{"subject": "我", "relation": "喜欢", "object": "编程"},
|
||||
{"subject": "我", "relation": "正在学习", "object": "TypeScript"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. recall - 检索记忆
|
||||
|
||||
从记忆图中检索相关信息。
|
||||
|
||||
**参数:**
|
||||
- `queryIntent`: 搜索关键词(必需,若无则需提供 seedEntities)
|
||||
- `seedEntities`: 种子实体数组(可选)
|
||||
- `depth`: 检索深度 1-5(默认 2)
|
||||
- `sessionFilter`: 会话过滤(可选)
|
||||
|
||||
### 3. purge - 删除记忆
|
||||
|
||||
删除记忆图中的一些信息。
|
||||
|
||||
**参数:**
|
||||
- `criteria`: 删除条件 `{subject, target, relation, sessionId}`
|
||||
- `mode`: 删除模式 `soft`(标记删除)、`hard`(彻底删除)或 `supersede`(纠错替代)
|
||||
- `newRelation`: 替代关系(supersede 模式必需)
|
||||
|
||||
### 4. introspect - 查看状态
|
||||
|
||||
查看当前记忆状态统计(实体数、关系数)。
|
||||
|
||||
### 5. archive - 归档旧记忆
|
||||
|
||||
将 N 天前的非活跃关系标记为归档。
|
||||
|
||||
**参数:**
|
||||
- `days`: 归档天数(默认 30)
|
||||
|
||||
### 6. cleanup - 清理无效数据
|
||||
|
||||
物理删除已删除超过 90 天的关系和孤立节点。
|
||||
|
||||
**参数:**
|
||||
- `dry_run`: 仅预览不删除(默认 true,建议先预览再执行)
|
||||
|
||||
### 7. context_rewrite - 压缩上下文
|
||||
|
||||
当对话历史过长时,将历史对话压缩为关键记忆节点存入图数据库。
|
||||
|
||||
**参数:**
|
||||
- `context`: 要压缩的长文本(必需)
|
||||
- `maxEntities`: 最大提取实体数(默认 20)
|
||||
- `summary`: 自定义摘要(可选)
|
||||
|
||||
**返回:**
|
||||
- `extractedEntities`: 提取的实体数
|
||||
- `extractedRelations`: 提取的关系数
|
||||
- `summary`: 生成的摘要
|
||||
- `compressed`: 是否成功压缩
|
||||
|
||||
### 8. working_memory_chain - 工作记忆链
|
||||
|
||||
检索当前会话的近期活跃关系和任务节点,形成工作记忆链。
|
||||
|
||||
**参数:**
|
||||
- `maxDepth`: 检索深度 1-5(默认 3)
|
||||
- `recentOnly`: 仅最近(默认 true)
|
||||
|
||||
### 9. task_node_create - 创建任务节点
|
||||
|
||||
创建一个新的 TaskNode 并自动链接到工作记忆链。
|
||||
|
||||
**参数:**
|
||||
- `session_id`: 会话 ID(必需)
|
||||
- `turn_id`: 轮次 ID(必需)
|
||||
- `summary`: 摘要(必需)
|
||||
- `key_facts`: 关键事实数组(必需)
|
||||
- `raw_context`: 原始上下文(可选,会自动存档到文本文件)
|
||||
|
||||
### 10. task_node_get_recent - 获取最近节点
|
||||
|
||||
获取最近 N 个任务节点(按时间倒序)。
|
||||
|
||||
**参数:**
|
||||
- `session_id`: 会话 ID(必需)
|
||||
- `limit`: 限制数量(默认 5)
|
||||
|
||||
### 11. task_node_get_chain - 获取任务链
|
||||
|
||||
获取完整的工作记忆链(从指定节点或最新节点开始回溯)。
|
||||
|
||||
**参数:**
|
||||
- `session_id`: 会话 ID(必需)
|
||||
- `from_node_id`: 起始节点 ID(可选,默认最新)
|
||||
|
||||
### 12. memory_search - 语义搜索
|
||||
|
||||
基于 embedding 的语义向量搜索,查找与查询语义相似的文本片段。
|
||||
|
||||
**参数:**
|
||||
- `query`: 搜索查询(必需)
|
||||
- `limit`: 返回结果数量(可选,默认 10,最大 50)
|
||||
- `corpus`: 搜索范围(可选,默认 'memory')
|
||||
|
||||
**示例:**
|
||||
```
|
||||
搜索关于 OpenClaw 的记忆
|
||||
```
|
||||
|
||||
AI 会执行:
|
||||
```json
|
||||
{
|
||||
"action": "memory_search",
|
||||
"params": {
|
||||
"query": "OpenClaw",
|
||||
"limit": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
返回结果包含相似度分数(0-1),按相关度排序。
|
||||
|
||||
### 13. memory_get - 精确读取记忆文件
|
||||
|
||||
按路径精确读取记忆文件的内容片段,支持行号范围。
|
||||
|
||||
**参数:**
|
||||
- `path`: 文件路径(必需)
|
||||
- `fromLine`: 起始行号(可选,1-based)
|
||||
- `lines`: 读取行数(可选,默认全部,最大 500)
|
||||
|
||||
**示例:**
|
||||
```
|
||||
读取 MEMORY.md 第 1-20 行
|
||||
```
|
||||
|
||||
AI 会执行:
|
||||
```json
|
||||
{
|
||||
"action": "memory_get",
|
||||
"params": {
|
||||
"path": "MEMORY.md",
|
||||
"fromLine": 1,
|
||||
"lines": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用原则
|
||||
|
||||
1. **优先使用图记忆**:对于重要事实、偏好、决策、任务等持久信息,**优先**使用 `commit` 写入图数据库,而非依赖 message 上下文
|
||||
2. **主动检索**:在回答用户问题前,**先调用 `recall` 或 `memory_search`** 检索相关记忆,而非仅凭当前上下文推理
|
||||
3. **结构化**:使用三元组格式存储关系,确保信息可被关联检索
|
||||
4. **定期清理**:删除过时或错误的信息
|
||||
5. **上下文压缩**:当对话过长时,使用 `context_rewrite` 将历史压缩为记忆节点
|
||||
6. **工作记忆**:使用 `working_memory_chain` 获取当前会话的上下文链
|
||||
7. **主动查询人设**:参考 `graph-memory-persona-force` Skill,在适当时候主动查询用户偏好和决策
|
||||
|
||||
## 记忆策略指南
|
||||
|
||||
### 什么时候写入图数据库?
|
||||
|
||||
**必须写入**:
|
||||
- 用户明确说"请记住"、"记住这个"等
|
||||
- 用户透露偏好、习惯、身份信息
|
||||
- 做出重要决策或选择
|
||||
- 创建任务或目标
|
||||
- 关键知识点或学习成果
|
||||
|
||||
**建议写入**:
|
||||
- 对话中重复出现的重要概念
|
||||
- 用户纠正或补充的信息
|
||||
- 项目相关的配置、路径、决策
|
||||
|
||||
**无需写入**:
|
||||
- 临时性问候、寒暄
|
||||
- 一次性问题(如"现在几点")
|
||||
- 已在图数据库中的重复信息
|
||||
|
||||
### 什么时候检索图数据库?
|
||||
|
||||
**必须检索**:
|
||||
- 用户问"我之前说过..."、"你还记得..."
|
||||
- 需要基于历史偏好做推荐或决策
|
||||
- 继续之前的任务或话题
|
||||
|
||||
**建议检索**:
|
||||
- 每次对话开始时,检索用户相关信息
|
||||
- 做推荐前先了解用户偏好
|
||||
- 涉及人设或性格相关的话题
|
||||
@ -1 +0,0 @@
|
||||
"""Tests for Graph Memory TUI"""
|
||||
@ -1,54 +0,0 @@
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from ui.models.message import Message, ToolCall, ToolResult
|
||||
from ui.models.config import AppConfig
|
||||
from ui.models.log_entry import LogEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config():
|
||||
return AppConfig(
|
||||
api_key="test-api-key",
|
||||
model="test-model",
|
||||
base_url="https://test.api.com"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message():
|
||||
return Message(
|
||||
role="user",
|
||||
content="测试消息",
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_tool_call():
|
||||
return ToolCall(
|
||||
id="test-call-id",
|
||||
name="memory_recall",
|
||||
arguments={"query_intent": "测试查询"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_tool_result():
|
||||
return ToolResult(
|
||||
tool_call_id="test-call-id",
|
||||
name="memory_recall",
|
||||
arguments={"query_intent": "测试查询"},
|
||||
result="测试结果",
|
||||
success=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_log_entry():
|
||||
return LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
tool_name="memory_recall",
|
||||
arguments={"query_intent": "测试查询"},
|
||||
result="测试结果",
|
||||
duration=0.5
|
||||
)
|
||||
@ -1 +0,0 @@
|
||||
"""Tests for Core Logic"""
|
||||
@ -1,218 +0,0 @@
|
||||
"""嵌入式数据库测试"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from core import EmbeddedGraphDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db():
|
||||
"""创建临时数据库用于测试"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
db_path = f.name
|
||||
|
||||
db = EmbeddedGraphDB(db_path)
|
||||
yield db
|
||||
|
||||
db.close()
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
def test_db_init(db):
|
||||
"""测试数据库初始化"""
|
||||
assert db.conn is not None
|
||||
assert db.db_path.exists()
|
||||
|
||||
|
||||
def test_commit_and_recall(db):
|
||||
"""测试写入和检索记忆"""
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
||||
{"subject": "用户", "relation": "正在学习", "object": "AI"}
|
||||
],
|
||||
session_id="test-session",
|
||||
turn_id=1
|
||||
)
|
||||
|
||||
assert result["created_entities"] >= 2
|
||||
assert result["created_relations"] >= 2
|
||||
|
||||
|
||||
def test_recall_with_keywords(db):
|
||||
"""测试关键词检索"""
|
||||
db.commit(
|
||||
triplets=[
|
||||
{"subject": "项目A", "relation": "使用技术", "object": "React"}
|
||||
]
|
||||
)
|
||||
|
||||
result = db.recall("React")
|
||||
assert len(result["entities"]) > 0
|
||||
|
||||
|
||||
def test_recall_empty_keywords(db):
|
||||
"""测试空关键词检索"""
|
||||
db.commit(
|
||||
triplets=[
|
||||
{"subject": "测试实体", "relation": "关系", "object": "测试对象"}
|
||||
]
|
||||
)
|
||||
|
||||
result = db.recall("")
|
||||
assert len(result["entities"]) > 0
|
||||
|
||||
|
||||
def test_purge_soft(db):
|
||||
"""测试软删除"""
|
||||
db.commit(
|
||||
triplets=[
|
||||
{"subject": "待删除", "relation": "测试", "object": "删除内容"}
|
||||
]
|
||||
)
|
||||
|
||||
result = db.purge(
|
||||
criteria={"source": "待删除"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
assert result["deleted"] >= 0
|
||||
assert result["mode"] == "soft"
|
||||
|
||||
|
||||
def test_introspect(db):
|
||||
"""测试状态查看"""
|
||||
db.commit(
|
||||
triplets=[
|
||||
{"subject": "实体1", "relation": "关系", "object": "实体2"}
|
||||
]
|
||||
)
|
||||
|
||||
result = db.introspect()
|
||||
|
||||
assert "entity_count" in result
|
||||
assert "relation_count" in result
|
||||
assert result["entity_count"] >= 1
|
||||
|
||||
|
||||
def test_archive(db):
|
||||
"""测试归档"""
|
||||
result = db.archive(days=30)
|
||||
assert "archived" in result
|
||||
|
||||
|
||||
def test_cleanup_dry_run(db):
|
||||
"""测试清理(预览模式)"""
|
||||
result = db.cleanup(dry_run=True)
|
||||
|
||||
assert result["dry_run"] is True
|
||||
assert "deleted_relations" in result
|
||||
|
||||
|
||||
def test_multiple_triplets(db):
|
||||
"""测试批量写入"""
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "实体A", "relation": "关系1", "object": "实体B"},
|
||||
{"subject": "实体B", "relation": "关系2", "object": "实体C"},
|
||||
{"subject": "实体C", "relation": "关系3", "object": "实体A"}
|
||||
],
|
||||
session_id="batch-test",
|
||||
turn_id=1
|
||||
)
|
||||
|
||||
assert result["created_entities"] >= 3
|
||||
assert result["created_relations"] == 3
|
||||
|
||||
|
||||
def test_entity_mention_count(db):
|
||||
"""测试实体提及次数增加"""
|
||||
db.commit(
|
||||
triplets=[{"subject": "热门实体", "relation": "关系", "object": "对象1"}]
|
||||
)
|
||||
db.commit(
|
||||
triplets=[{"subject": "热门实体", "relation": "关系", "object": "对象2"}]
|
||||
)
|
||||
|
||||
result = db.recall("热门实体")
|
||||
entity = next((e for e in result["entities"] if e["name"] == "热门实体"), None)
|
||||
|
||||
assert entity is not None
|
||||
assert entity["mention_count"] >= 2
|
||||
|
||||
|
||||
def test_close_and_context_manager():
|
||||
"""测试关闭和上下文管理器"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
db_path = f.name
|
||||
|
||||
try:
|
||||
with EmbeddedGraphDB(db_path) as db:
|
||||
db.commit(
|
||||
triplets=[{"subject": "测试", "relation": "上下文", "object": "管理器"}]
|
||||
)
|
||||
assert db.conn is not None
|
||||
|
||||
with EmbeddedGraphDB(db_path) as db:
|
||||
result = db.introspect()
|
||||
assert result["entity_count"] >= 1
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
def test_save_and_get_chat_records(db):
|
||||
"""测试聊天记录保存和读取"""
|
||||
messages = [
|
||||
{"role": "user", "content": "你好"},
|
||||
{"role": "assistant", "content": "你好,有什么可以帮你?"}
|
||||
]
|
||||
|
||||
result = db.save_chat_records(messages)
|
||||
assert result["saved"] == 2
|
||||
|
||||
history = db.get_chat_records()
|
||||
assert len(history) == 2
|
||||
assert history[0]["role"] == "user"
|
||||
assert history[0]["content"] == "你好"
|
||||
assert history[1]["role"] == "assistant"
|
||||
|
||||
|
||||
def test_chat_records_limit_500(db):
|
||||
"""测试聊天记录限制500条"""
|
||||
for i in range(600):
|
||||
db.save_chat_records([{"role": "user", "content": f"消息{i}"}])
|
||||
|
||||
history = db.get_chat_records()
|
||||
assert len(history) == 500
|
||||
|
||||
|
||||
def test_get_chat_records_default_limit(db):
|
||||
"""测试默认limit参数"""
|
||||
for i in range(100):
|
||||
db.save_chat_records([{"role": "user", "content": f"msg{i}"}])
|
||||
|
||||
history_50 = db.get_chat_records(limit=50)
|
||||
assert len(history_50) == 50
|
||||
|
||||
history_default = db.get_chat_records()
|
||||
assert len(history_default) == 100
|
||||
|
||||
|
||||
def test_clear_chat_records(db):
|
||||
"""测试清空聊天记录"""
|
||||
db.save_chat_records([
|
||||
{"role": "user", "content": "测试1"},
|
||||
{"role": "assistant", "content": "回复1"},
|
||||
{"role": "user", "content": "测试2"},
|
||||
])
|
||||
|
||||
history = db.get_chat_records()
|
||||
assert len(history) == 3
|
||||
|
||||
result = db.clear_chat_records()
|
||||
assert result["cleared"] is True
|
||||
|
||||
history_after = db.get_chat_records()
|
||||
assert len(history_after) == 0
|
||||
@ -1,352 +0,0 @@
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import threading
|
||||
import queue
|
||||
|
||||
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
||||
|
||||
|
||||
class TestPacketTypeEnum:
|
||||
"""测试 PacketType 枚举"""
|
||||
|
||||
def test_packet_type_process_message_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.PROCESS_MESSAGE is not None
|
||||
assert PacketType.PROCESS_MESSAGE.value == "process_message"
|
||||
|
||||
def test_packet_type_execute_tool_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.EXECUTE_TOOL is not None
|
||||
assert PacketType.EXECUTE_TOOL.value == "execute_tool"
|
||||
|
||||
def test_packet_type_get_status_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.GET_STATUS is not None
|
||||
assert PacketType.GET_STATUS.value == "get_status"
|
||||
|
||||
def test_packet_type_get_settings_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.GET_SETTINGS is not None
|
||||
assert PacketType.GET_SETTINGS.value == "get_settings"
|
||||
|
||||
def test_packet_type_set_settings_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.SET_SETTINGS is not None
|
||||
assert PacketType.SET_SETTINGS.value == "set_settings"
|
||||
|
||||
def test_packet_type_get_history_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.GET_HISTORY is not None
|
||||
assert PacketType.GET_HISTORY.value == "get_history"
|
||||
|
||||
def test_packet_type_save_history_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.SAVE_HISTORY is not None
|
||||
assert PacketType.SAVE_HISTORY.value == "save_history"
|
||||
|
||||
def test_packet_type_shutdown_exists(self):
|
||||
from core import PacketType
|
||||
assert PacketType.SHUTDOWN is not None
|
||||
assert PacketType.SHUTDOWN.value == "shutdown"
|
||||
|
||||
def test_packet_type_all_values(self):
|
||||
from core import PacketType
|
||||
values = [pt.value for pt in PacketType]
|
||||
assert "process_message" in values
|
||||
assert "execute_tool" in values
|
||||
assert "get_status" in values
|
||||
assert "get_settings" in values
|
||||
assert "set_settings" in values
|
||||
assert "get_history" in values
|
||||
assert "save_history" in values
|
||||
assert "shutdown" in values
|
||||
assert len(values) == 8
|
||||
|
||||
|
||||
class TestPacketCreation:
|
||||
"""测试 Packet 创建"""
|
||||
|
||||
def test_packet_with_id_and_type(self):
|
||||
from core import Packet, PacketType
|
||||
packet = Packet(id="test-1", type=PacketType.PROCESS_MESSAGE, body={"user_input": "hello"})
|
||||
assert packet.id == "test-1"
|
||||
assert packet.type == PacketType.PROCESS_MESSAGE
|
||||
|
||||
def test_packet_body(self):
|
||||
from core import Packet, PacketType
|
||||
body = {"user_input": "test", "extra": "data"}
|
||||
packet = Packet(id="test-2", type=PacketType.EXECUTE_TOOL, body=body)
|
||||
assert packet.body == body
|
||||
|
||||
def test_packet_with_empty_body(self):
|
||||
from core import Packet, PacketType
|
||||
packet = Packet(id="test-3", type=PacketType.GET_STATUS, body={})
|
||||
assert packet.body == {}
|
||||
|
||||
def test_packet_created_at_default(self):
|
||||
from core import Packet, PacketType
|
||||
before = time.time()
|
||||
packet = Packet(id="test-4", type=PacketType.GET_SETTINGS, body={})
|
||||
after = time.time()
|
||||
assert before <= packet.created_at <= after
|
||||
|
||||
|
||||
class TestPacketResponse:
|
||||
"""测试 PacketResponse"""
|
||||
|
||||
def test_packet_response_success(self):
|
||||
from core import PacketResponse
|
||||
response = PacketResponse(id="resp-1", success=True, data={"result": "ok"})
|
||||
assert response.id == "resp-1"
|
||||
assert response.success is True
|
||||
assert response.data == {"result": "ok"}
|
||||
|
||||
def test_packet_response_error(self):
|
||||
from core import PacketResponse
|
||||
response = PacketResponse(id="resp-2", success=False, error="error msg")
|
||||
assert response.id == "resp-2"
|
||||
assert response.success is False
|
||||
assert response.error == "error msg"
|
||||
|
||||
|
||||
class TestBackendServerCreation:
|
||||
"""测试 BackendServer 创建"""
|
||||
|
||||
def test_backend_server_init(self):
|
||||
from core import BackendServer
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
assert server._db_path == ":memory:"
|
||||
assert server._use_embedded_db is True
|
||||
assert server._graph is None
|
||||
assert server._client is None
|
||||
assert server._tool_limiter is None
|
||||
|
||||
def test_backend_server_default_params(self):
|
||||
from core import BackendServer
|
||||
server = BackendServer()
|
||||
assert server._db_path == "graph_memory.db"
|
||||
assert server._use_embedded_db is True
|
||||
|
||||
|
||||
class TestBackendServerLifecycle:
|
||||
"""测试 BackendServer 生命周期"""
|
||||
|
||||
def test_backend_server_start_stop(self):
|
||||
from core import BackendServer
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
|
||||
assert server._running is True
|
||||
assert server._graph is not None
|
||||
assert server._tool_limiter is not None
|
||||
|
||||
server.shutdown()
|
||||
assert server._running is False
|
||||
|
||||
def test_backend_server_start_with_api_key(self):
|
||||
from core import BackendServer
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="test-key", base_url="https://api.deepseek.com")
|
||||
|
||||
assert server._client is not None
|
||||
assert server._config["api_key"] == "test-key"
|
||||
|
||||
server.shutdown()
|
||||
|
||||
|
||||
class TestBackendClientCreation:
|
||||
"""测试 BackendClient 创建"""
|
||||
|
||||
def test_backend_client_init(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
client = BackendClient(server)
|
||||
|
||||
assert client._server is server
|
||||
assert client._counter == 0
|
||||
|
||||
|
||||
class TestBackendClientAPI:
|
||||
"""测试 BackendClient API"""
|
||||
|
||||
def test_get_status(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.get_status()
|
||||
assert result.get("success") is True
|
||||
data = result.get("data", {})
|
||||
assert data.get("running") is True
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_update_settings(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.update_settings(
|
||||
api_config={"api_key": "new-key", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
|
||||
tool_limits={"persona_query_max": 2}
|
||||
)
|
||||
assert result.get("success") is True
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_get_settings(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="test-key")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.get_settings()
|
||||
assert result.get("success") is True
|
||||
data = result.get("data", {})
|
||||
assert data.get("api_config", {}).get("api_key") == "test-key"
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_process_message_no_api_key(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 无 API key 应该返回错误(success 为 False)
|
||||
result = client.process_message("hello")
|
||||
# 由于 API 调用失败,success 应该是 False
|
||||
assert result.get("success") is False
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_execute_tool(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.execute_tool("memory_introspect", {})
|
||||
assert result.get("success") is True
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_clear_history(self):
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 先保存一些历史
|
||||
client.save_history([
|
||||
{"role": "user", "content": "test message 1"},
|
||||
{"role": "assistant", "content": "test response 1"}
|
||||
])
|
||||
|
||||
# 验证历史已保存
|
||||
history = client.get_history()
|
||||
assert len(history) >= 2
|
||||
|
||||
# 清空历史
|
||||
result = client.clear_history()
|
||||
assert result.get("status") == "history_cleared"
|
||||
|
||||
# 验证历史已清空
|
||||
history = client.get_history()
|
||||
assert len(history) == 0
|
||||
|
||||
server.shutdown()
|
||||
|
||||
def test_send_message_is_alias(self):
|
||||
"""测试 send 方法是 process_message 的别名"""
|
||||
from core import BackendServer, BackendClient
|
||||
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# send 方法应该等同于 process_message
|
||||
# 由于没有真实 API key,应该返回失败
|
||||
result = client.send("test")
|
||||
assert result.get("success") is False
|
||||
|
||||
server.shutdown()
|
||||
|
||||
|
||||
class TestToolLimiter:
|
||||
"""测试工具限制器"""
|
||||
|
||||
def test_tool_limiter_init(self):
|
||||
from core.tool_limiter import ToolLimiter
|
||||
limiter = ToolLimiter()
|
||||
assert limiter.counts.persona_query == 0
|
||||
assert limiter.counts.persona_update == 0
|
||||
|
||||
def test_tool_limiter_classify(self):
|
||||
from core.tool_limiter import ToolLimiter
|
||||
limiter = ToolLimiter()
|
||||
|
||||
category, operation = limiter._classify_tool("memory_recall", {"query_intent": "test"})
|
||||
assert category == "memory"
|
||||
assert operation == "query"
|
||||
|
||||
category, operation = limiter._classify_tool("persona_update", {})
|
||||
assert category == "persona"
|
||||
assert operation == "update"
|
||||
|
||||
category, operation = limiter._classify_tool("task_create", {})
|
||||
assert category == "task"
|
||||
assert operation == "update"
|
||||
|
||||
def test_tool_limiter_can_call(self):
|
||||
from core.tool_limiter import ToolLimiter
|
||||
limiter = ToolLimiter()
|
||||
|
||||
allowed, reason = limiter.can_call("persona_update", {})
|
||||
assert allowed is True
|
||||
|
||||
limiter.record_call("persona_update", {})
|
||||
allowed, reason = limiter.can_call("persona_update", {})
|
||||
assert allowed is False
|
||||
assert "已达上限" in reason
|
||||
|
||||
def test_tool_limiter_reset(self):
|
||||
from core.tool_limiter import ToolLimiter
|
||||
limiter = ToolLimiter()
|
||||
|
||||
limiter.record_call("persona_update", {})
|
||||
assert limiter.counts.persona_update == 1
|
||||
|
||||
limiter.reset()
|
||||
assert limiter.counts.persona_update == 0
|
||||
|
||||
|
||||
class TestEmbeddedGraphDB:
|
||||
"""测试图数据库"""
|
||||
|
||||
def test_embedded_db_init(self):
|
||||
from core.embedded_db import EmbeddedGraphDB
|
||||
db = EmbeddedGraphDB(db_path=":memory:")
|
||||
assert db.conn is not None
|
||||
db.close()
|
||||
|
||||
def test_embedded_db_commit_and_recall(self):
|
||||
from core.embedded_db import EmbeddedGraphDB
|
||||
db = EmbeddedGraphDB(db_path=":memory:")
|
||||
|
||||
# 写入记忆 (使用 triplets 参数)
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "测试", "relation": "是", "object": "test"}
|
||||
],
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# 读取记忆
|
||||
results = db.recall("测试")
|
||||
assert len(results.get("entities", [])) > 0
|
||||
|
||||
db.close()
|
||||
@ -1,179 +0,0 @@
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
||||
|
||||
|
||||
class TestIntegrationPacketFlow:
|
||||
"""测试 Packet 通信流程"""
|
||||
|
||||
def test_packet_round_trip_process_message(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 无 API key 时应该返回错误而非抛异常
|
||||
result = client.process_message("test message")
|
||||
assert result.get("success") is False
|
||||
assert "error" in result
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_packet_round_trip_config(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.update_config(api_key="test-api", base_url="https://test.com")
|
||||
assert result.get("success") is True
|
||||
|
||||
status = client.get_status()
|
||||
data = status.get("data", {})
|
||||
assert data.get("config", {}).get("api_key") == "test-api"
|
||||
assert data.get("config", {}).get("base_url") == "https://test.com"
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_packet_round_trip_status(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.get_status()
|
||||
assert result.get("success") is True
|
||||
data = result.get("data", {})
|
||||
assert data.get("running") is True
|
||||
assert data.get("graph_initialized") is True
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_packet_round_trip_execute_tool(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
result = client.execute_tool("memory_introspect", {})
|
||||
assert result.get("success") is True
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestIntegrationToolLimiter:
|
||||
"""测试工具限制器集成"""
|
||||
|
||||
def test_external_tool_call_not_limited(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 外部调用多次应该成功
|
||||
for i in range(5):
|
||||
result = client.execute_tool("memory_introspect", {})
|
||||
assert result.get("success") is True
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_internal_tool_call_limited(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="fake-key") # 假 key 会失败但不影响测试
|
||||
client = BackendClient(server)
|
||||
|
||||
# 内部调用受限,tool_limiter 存在
|
||||
assert server._tool_limiter is not None
|
||||
|
||||
# 初始状态
|
||||
assert server._tool_limiter.counts.persona_update == 0
|
||||
|
||||
# 记录一次调用
|
||||
server._tool_limiter.record_call("persona_update", {})
|
||||
assert server._tool_limiter.counts.persona_update == 1
|
||||
|
||||
# 再次调用应该被拒绝
|
||||
allowed, reason = server._tool_limiter.can_call("persona_update", {})
|
||||
assert allowed is False
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestIntegrationErrorHandling:
|
||||
"""测试错误处理"""
|
||||
|
||||
def test_process_message_returns_error_not_raise(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 应该返回错误,而不是抛出异常
|
||||
result = client.process_message("hello")
|
||||
assert result.get("success") is False
|
||||
assert "error" in result
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_execute_tool_error_handling(self):
|
||||
from core import BackendServer, BackendClient
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||
server.start(api_key="")
|
||||
client = BackendClient(server)
|
||||
|
||||
# 不存在的工具应该返回错误
|
||||
result = client.execute_tool("nonexistent_tool", {})
|
||||
assert result.get("success") is False
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
@ -1,241 +0,0 @@
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
||||
|
||||
|
||||
class TestUIImport:
|
||||
"""测试 UI 模块导入"""
|
||||
|
||||
def test_import_graphmemoryapp(self):
|
||||
from ui import GraphMemoryApp
|
||||
assert GraphMemoryApp is not None
|
||||
|
||||
def test_import_appconfig(self):
|
||||
from ui import AppConfig
|
||||
assert AppConfig is not None
|
||||
|
||||
def test_import_message(self):
|
||||
from ui.models.message import Message, ToolCall, ToolResult
|
||||
assert Message is not None
|
||||
assert ToolCall is not None
|
||||
assert ToolResult is not None
|
||||
|
||||
def test_import_config(self):
|
||||
from ui.models.config import AppConfig
|
||||
assert AppConfig is not None
|
||||
|
||||
def test_import_log_entry(self):
|
||||
from ui.models.log_entry import LogEntry
|
||||
assert LogEntry is not None
|
||||
|
||||
|
||||
class TestAppConfig:
|
||||
"""测试配置模型"""
|
||||
|
||||
def test_config_default_values(self):
|
||||
from ui.models.config import AppConfig
|
||||
config = AppConfig()
|
||||
assert config.api_key == ""
|
||||
assert config.model == "deepseek-chat"
|
||||
assert config.base_url == "https://api.deepseek.com"
|
||||
|
||||
def test_config_from_env(self):
|
||||
from ui.models.config import AppConfig
|
||||
config = AppConfig.from_env()
|
||||
assert "fake-test-key" in config.api_key
|
||||
|
||||
|
||||
class TestMessageModel:
|
||||
"""测试消息模型"""
|
||||
|
||||
def test_message_creation_user(self):
|
||||
from ui.models.message import Message
|
||||
from datetime import datetime
|
||||
msg = Message(role="user", content="test content")
|
||||
assert msg.role == "user"
|
||||
assert msg.content == "test content"
|
||||
assert isinstance(msg.timestamp, datetime)
|
||||
|
||||
def test_message_creation_assistant(self):
|
||||
from ui.models.message import Message
|
||||
msg = Message(role="assistant", content="assistant response")
|
||||
assert msg.role == "assistant"
|
||||
|
||||
def test_message_with_tool_calls(self):
|
||||
from ui.models.message import Message, ToolCall
|
||||
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
|
||||
msg = Message(role="assistant", content="response", tool_calls=[tc])
|
||||
assert msg.tool_calls is not None
|
||||
assert len(msg.tool_calls) == 1
|
||||
|
||||
|
||||
class TestAppCSSPath:
|
||||
"""测试 App CSS 配置"""
|
||||
|
||||
def test_app_has_css_path(self):
|
||||
from ui import GraphMemoryApp
|
||||
assert hasattr(GraphMemoryApp, 'CSS_PATH')
|
||||
assert len(GraphMemoryApp.CSS_PATH) > 0
|
||||
|
||||
|
||||
class TestAppBindings:
|
||||
"""测试 App 快捷键"""
|
||||
|
||||
def test_app_has_bindings(self):
|
||||
from ui import GraphMemoryApp
|
||||
assert hasattr(GraphMemoryApp, 'BINDINGS')
|
||||
assert len(GraphMemoryApp.BINDINGS) > 0
|
||||
|
||||
|
||||
class TestWidgetImports:
|
||||
"""测试组件导入"""
|
||||
|
||||
def test_import_left_panel(self):
|
||||
from ui.widgets.left_panel import LeftPanel
|
||||
assert LeftPanel is not None
|
||||
|
||||
def test_import_right_panel(self):
|
||||
from ui.widgets.right_panel import RightPanel
|
||||
assert RightPanel is not None
|
||||
|
||||
def test_import_input_box(self):
|
||||
from ui.widgets.input_box import InputBox
|
||||
assert InputBox is not None
|
||||
|
||||
def test_import_message_history(self):
|
||||
from ui.widgets.message_history import MessageHistory
|
||||
assert MessageHistory is not None
|
||||
|
||||
def test_import_status_bar(self):
|
||||
from ui.widgets.status_bar import StatusBar
|
||||
assert StatusBar is not None
|
||||
|
||||
|
||||
class TestHandlerImports:
|
||||
"""测试处理器导入"""
|
||||
|
||||
def test_import_focus_handler(self):
|
||||
from ui.handlers.focus_handler import FocusHandler
|
||||
assert FocusHandler is not None
|
||||
|
||||
def test_import_key_handler(self):
|
||||
from ui.handlers.key_handler import KeyHandler
|
||||
assert KeyHandler is not None
|
||||
|
||||
|
||||
class TestServiceImports:
|
||||
"""测试服务导入"""
|
||||
|
||||
def test_import_config_service(self):
|
||||
from ui.services.config_service import ConfigService
|
||||
assert ConfigService is not None
|
||||
|
||||
def test_import_config_manager(self):
|
||||
from ui.services.config_manager import ConfigManager
|
||||
assert ConfigManager is not None
|
||||
|
||||
|
||||
class TestAppInitialization:
|
||||
"""测试 App 初始化"""
|
||||
|
||||
def test_app_without_backend(self):
|
||||
from ui import GraphMemoryApp
|
||||
app = GraphMemoryApp()
|
||||
assert app._backend_server is None
|
||||
assert app._backend_client is None
|
||||
|
||||
def test_app_with_backend(self):
|
||||
from ui import GraphMemoryApp
|
||||
from core import BackendServer
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path)
|
||||
server.start(api_key="")
|
||||
app = GraphMemoryApp(backend_server=server)
|
||||
assert app._backend_server is server
|
||||
assert app._backend_client is not None
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestUIWithBackendClient:
|
||||
"""测试 UI 与后端通信"""
|
||||
|
||||
def test_app_sends_message_via_backend_client(self):
|
||||
from ui import GraphMemoryApp
|
||||
from core import BackendServer, BackendClient
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path)
|
||||
server.start(api_key="")
|
||||
|
||||
app = GraphMemoryApp(backend_server=server)
|
||||
client = app._backend_client
|
||||
|
||||
status = client.get_status()
|
||||
assert status.get("success") is True
|
||||
|
||||
result = client.update_settings(
|
||||
api_config={"api_key": "sk-test", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
|
||||
tool_limits={"persona_query_max": 1}
|
||||
)
|
||||
assert result.get("success") is True
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_ui_get_history(self):
|
||||
from ui import GraphMemoryApp
|
||||
from core import BackendServer
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path)
|
||||
server.start(api_key="")
|
||||
|
||||
app = GraphMemoryApp(backend_server=server)
|
||||
client = app._backend_client
|
||||
|
||||
history = client.get_history()
|
||||
assert isinstance(history, list)
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_ui_only_uses_backend_client(self):
|
||||
from ui import GraphMemoryApp
|
||||
from core import BackendServer
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
server = BackendServer(db_path=db_path)
|
||||
server.start(api_key="")
|
||||
|
||||
app = GraphMemoryApp(backend_server=server)
|
||||
|
||||
# UI 不应该直接访问后端内部
|
||||
assert hasattr(app, '_backend_client')
|
||||
assert app._backend_client is not None
|
||||
|
||||
# 不应该有 _graph, _client 等直接访问
|
||||
assert not hasattr(app, '_graph')
|
||||
assert not hasattr(app, '_client')
|
||||
|
||||
server.shutdown()
|
||||
finally:
|
||||
if os.path.exists(db_path):
|
||||
os.unlink(db_path)
|
||||
@ -1,322 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Draw an interactive relationship graph from TrulyMEM SQLite graph database using Plotly.
|
||||
|
||||
Output:
|
||||
- Static image file (PNG by default)
|
||||
|
||||
Examples:
|
||||
python tools/plotly_relationship_graph.py
|
||||
python tools/plotly_relationship_graph.py --db-path ./graph_memory.db --output relation_graph.png
|
||||
python tools/plotly_relationship_graph.py --include-non-active
|
||||
python tools/plotly_relationship_graph.py --hide-edge-labels
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entity:
|
||||
id: int
|
||||
name: str
|
||||
entity_type: str
|
||||
mention_count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Relation:
|
||||
source: str
|
||||
target: str
|
||||
relation_type: str
|
||||
confidence: float
|
||||
status: str
|
||||
|
||||
|
||||
def resolve_default_db_path() -> Path:
|
||||
project_db = Path.cwd() / "graph_memory.db"
|
||||
if project_db.exists():
|
||||
return project_db
|
||||
return Path.home() / ".trulymem" / "graph_memory.db"
|
||||
|
||||
|
||||
def load_graph(db_path: Path, include_non_active: bool) -> Tuple[Dict[str, Entity], List[Relation]]:
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"Database not found: {db_path}")
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, name, COALESCE(type, 'unknown') AS entity_type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC, name ASC
|
||||
"""
|
||||
)
|
||||
entities: Dict[str, Entity] = {
|
||||
row["name"]: Entity(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
entity_type=row["entity_type"],
|
||||
mention_count=int(row["mention_count"] or 1),
|
||||
)
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
sql = """
|
||||
SELECT e1.name AS source,
|
||||
e2.name AS target,
|
||||
r.relation_type,
|
||||
r.confidence,
|
||||
r.status
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
"""
|
||||
if not include_non_active:
|
||||
sql += " WHERE r.status = 'active'"
|
||||
|
||||
cursor.execute(sql)
|
||||
relations = [
|
||||
Relation(
|
||||
source=row["source"],
|
||||
target=row["target"],
|
||||
relation_type=row["relation_type"],
|
||||
confidence=float(row["confidence"] or 0.0),
|
||||
status=row["status"],
|
||||
)
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
return entities, relations
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def compute_degrees(entities: Dict[str, Entity], relations: List[Relation]) -> Tuple[Dict[str, int], Dict[str, int]]:
|
||||
in_deg = {name: 0 for name in entities}
|
||||
out_deg = {name: 0 for name in entities}
|
||||
for rel in relations:
|
||||
if rel.source in out_deg:
|
||||
out_deg[rel.source] += 1
|
||||
if rel.target in in_deg:
|
||||
in_deg[rel.target] += 1
|
||||
return in_deg, out_deg
|
||||
|
||||
|
||||
def compute_positions(entities: Dict[str, Entity], in_deg: Dict[str, int], out_deg: Dict[str, int]) -> Dict[str, Tuple[float, float]]:
|
||||
names = sorted(
|
||||
entities.keys(),
|
||||
key=lambda name: (-(in_deg[name] + out_deg[name]), -entities[name].mention_count, name),
|
||||
)
|
||||
n = len(names)
|
||||
if n == 0:
|
||||
return {}
|
||||
|
||||
radius = max(1.0, n / 8.0)
|
||||
positions: Dict[str, Tuple[float, float]] = {}
|
||||
for i, name in enumerate(names):
|
||||
angle = (2.0 * math.pi * i) / n
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
positions[name] = (x, y)
|
||||
return positions
|
||||
|
||||
|
||||
def format_relation_label(rel: Relation) -> str:
|
||||
return f"{rel.relation_type} ({rel.confidence:.2f}, {rel.status})"
|
||||
|
||||
|
||||
def build_figure(
|
||||
entities: Dict[str, Entity],
|
||||
relations: List[Relation],
|
||||
show_edge_labels: bool,
|
||||
title: str,
|
||||
):
|
||||
try:
|
||||
import plotly.graph_objects as go
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("Plotly is not installed. Run: pip install plotly") from exc
|
||||
|
||||
in_deg, out_deg = compute_degrees(entities, relations)
|
||||
positions = compute_positions(entities, in_deg, out_deg)
|
||||
|
||||
edge_x: List[float] = []
|
||||
edge_y: List[float] = []
|
||||
edge_label_x: List[float] = []
|
||||
edge_label_y: List[float] = []
|
||||
edge_label_text: List[str] = []
|
||||
|
||||
for rel in relations:
|
||||
if rel.source not in positions or rel.target not in positions:
|
||||
continue
|
||||
x0, y0 = positions[rel.source]
|
||||
x1, y1 = positions[rel.target]
|
||||
edge_x.extend([x0, x1, None])
|
||||
edge_y.extend([y0, y1, None])
|
||||
|
||||
if show_edge_labels:
|
||||
edge_label_x.append((x0 + x1) / 2.0)
|
||||
edge_label_y.append((y0 + y1) / 2.0)
|
||||
edge_label_text.append(format_relation_label(rel))
|
||||
|
||||
edge_trace = go.Scatter(
|
||||
x=edge_x,
|
||||
y=edge_y,
|
||||
line={"width": 0.8, "color": "#8899aa"},
|
||||
hoverinfo="none",
|
||||
mode="lines",
|
||||
name="relations",
|
||||
)
|
||||
|
||||
node_x: List[float] = []
|
||||
node_y: List[float] = []
|
||||
node_text: List[str] = []
|
||||
node_size: List[float] = []
|
||||
node_color: List[float] = []
|
||||
|
||||
node_names = sorted(entities.keys())
|
||||
|
||||
for name in node_names:
|
||||
x, y = positions[name]
|
||||
entity = entities[name]
|
||||
total_degree = in_deg[name] + out_deg[name]
|
||||
|
||||
node_x.append(x)
|
||||
node_y.append(y)
|
||||
node_size.append(10 + min(entity.mention_count, 40) * 0.8)
|
||||
node_color.append(float(total_degree))
|
||||
node_text.append(
|
||||
f"{name}<br>"
|
||||
f"type: {entity.entity_type}<br>"
|
||||
f"mentions: {entity.mention_count}<br>"
|
||||
f"in: {in_deg[name]} | out: {out_deg[name]}"
|
||||
)
|
||||
|
||||
node_trace = go.Scatter(
|
||||
x=node_x,
|
||||
y=node_y,
|
||||
mode="markers+text",
|
||||
text=node_names,
|
||||
textposition="top center",
|
||||
hoverinfo="text",
|
||||
hovertext=node_text,
|
||||
marker={
|
||||
"showscale": True,
|
||||
"colorscale": "YlGnBu",
|
||||
"reversescale": False,
|
||||
"color": node_color,
|
||||
"size": node_size,
|
||||
"colorbar": {"title": "Degree"},
|
||||
"line": {"width": 1, "color": "#2f3b52"},
|
||||
"opacity": 0.9,
|
||||
},
|
||||
name="entities",
|
||||
)
|
||||
|
||||
traces = [edge_trace, node_trace]
|
||||
|
||||
if show_edge_labels and edge_label_text:
|
||||
edge_label_trace = go.Scatter(
|
||||
x=edge_label_x,
|
||||
y=edge_label_y,
|
||||
mode="text",
|
||||
text=edge_label_text,
|
||||
textfont={"size": 9, "color": "#2d3a4b"},
|
||||
hoverinfo="none",
|
||||
name="relation_labels",
|
||||
)
|
||||
traces.append(edge_label_trace)
|
||||
|
||||
fig = go.Figure(
|
||||
data=traces,
|
||||
layout=go.Layout(
|
||||
title=title,
|
||||
title_x=0.5,
|
||||
showlegend=False,
|
||||
hovermode="closest",
|
||||
margin={"b": 20, "l": 10, "r": 10, "t": 50},
|
||||
xaxis={"showgrid": False, "zeroline": False, "showticklabels": False},
|
||||
yaxis={"showgrid": False, "zeroline": False, "showticklabels": False},
|
||||
plot_bgcolor="#f8fafc",
|
||||
paper_bgcolor="#ffffff",
|
||||
),
|
||||
)
|
||||
|
||||
rendered_nodes = set(node_names)
|
||||
expected_nodes = set(entities.keys())
|
||||
missing_nodes = expected_nodes - rendered_nodes
|
||||
if missing_nodes:
|
||||
preview = ", ".join(sorted(missing_nodes)[:10])
|
||||
raise RuntimeError(
|
||||
f"Node completeness check failed, missing {len(missing_nodes)} nodes: {preview}"
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Draw graph relations from SQLite with Plotly.")
|
||||
parser.add_argument("--db-path", type=str, default=None, help="Path to graph_memory.db")
|
||||
parser.add_argument("--output", type=str, default="relation_graph.png", help="Output image file path")
|
||||
parser.add_argument("--title", type=str, default="TrulyMEM Relationship Graph", help="Chart title")
|
||||
parser.add_argument("--include-non-active", action="store_true", help="Include archived/deleted relations")
|
||||
parser.add_argument("--show-edge-labels", dest="show_edge_labels", action="store_true", help="Show relation text on edges")
|
||||
parser.add_argument("--hide-edge-labels", dest="show_edge_labels", action="store_false", help="Hide relation text on edges")
|
||||
parser.set_defaults(show_edge_labels=True)
|
||||
parser.add_argument("--width", type=int, default=2200, help="Output image width in pixels")
|
||||
parser.add_argument("--height", type=int, default=1400, help="Output image height in pixels")
|
||||
parser.add_argument("--scale", type=float, default=1.0, help="Image scale factor")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
db_path = Path(args.db_path) if args.db_path else resolve_default_db_path()
|
||||
|
||||
try:
|
||||
entities, relations = load_graph(db_path, include_non_active=args.include_non_active)
|
||||
except FileNotFoundError as exc:
|
||||
print(f"[ERROR] {exc}")
|
||||
return
|
||||
|
||||
if not entities:
|
||||
print("[INFO] No entities found in database.")
|
||||
return
|
||||
|
||||
try:
|
||||
fig = build_figure(
|
||||
entities=entities,
|
||||
relations=relations,
|
||||
show_edge_labels=args.show_edge_labels,
|
||||
title=args.title,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(f"[ERROR] {exc}")
|
||||
return
|
||||
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fig.write_image(str(output_path), width=args.width, height=args.height, scale=args.scale)
|
||||
except Exception as exc:
|
||||
print(f"[ERROR] Failed to export image: {exc}")
|
||||
print("[HINT] Install kaleido for static export: pip install kaleido")
|
||||
return
|
||||
|
||||
print(f"Database: {db_path}")
|
||||
print(f"Entities: {len(entities)}, Relations: {len(relations)}")
|
||||
print(f"Nodes drawn: {len(entities)}/{len(entities)}")
|
||||
print(f"Saved: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 用户配置文件始终放在用户目录
|
||||
CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
DB_PATH = Path.home() / ".trulymem" / "graph_memory.db"
|
||||
|
||||
# 源码运行时使用项目目录,打包后使用用户目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包版本:创建用户目录
|
||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
# 源码版本:检查项目目录是否有配置(向后兼容)
|
||||
project_dir = Path(__file__).parent
|
||||
project_config = project_dir / "config.json"
|
||||
project_db = project_dir / "graph_memory.db"
|
||||
|
||||
if project_config.exists():
|
||||
CONFIG_PATH = project_config
|
||||
if project_db.exists():
|
||||
DB_PATH = project_db
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
os.chdir(Path(__file__).parent)
|
||||
|
||||
from core import BackendServer
|
||||
from ui import GraphMemoryApp
|
||||
|
||||
|
||||
def main():
|
||||
backend_server = BackendServer(
|
||||
db_path=str(DB_PATH),
|
||||
use_embedded_db=True,
|
||||
config_file=str(CONFIG_PATH)
|
||||
)
|
||||
backend_server.start()
|
||||
|
||||
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
|
||||
|
||||
try:
|
||||
app.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\n退出")
|
||||
finally:
|
||||
backend_server.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
82
ts/bundled-skills/graph-memory-persona/SKILL.md
Normal file
82
ts/bundled-skills/graph-memory-persona/SKILL.md
Normal file
@ -0,0 +1,82 @@
|
||||
---
|
||||
name: graph-memory-persona
|
||||
description: "管理 AI 人设 - AI 应主动查询和维护角色特征,将人设信息作为长期记忆存储在图数据库中"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory Persona 人设管理
|
||||
|
||||
管理 AI 的人设/角色特征。
|
||||
|
||||
> **定位声明**:人设是 AI 长期记忆的一部分,存储在图数据库中。AI **应主动查询**人设信息来指导自己的行为和回应风格。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**AI 必须在以下场景主动查询人设**:
|
||||
1. 对话开始时(检索用户偏好和 AI 角色设定)
|
||||
2. 用户要求改变行为或风格时
|
||||
3. 不确定如何回应时(查询是否有相关人设指导)
|
||||
4. 用户提到"你之前说..."、"我告诉过你..."等暗示历史记忆的语句
|
||||
|
||||
**AI 必须在以下场景更新人设**:
|
||||
1. 用户明确设定 AI 角色("你是猫娘"、"你是技术专家")
|
||||
2. 用户描述自己的偏好("我喜欢简洁的回答")
|
||||
3. 用户定义交互规则("用中文回复我"、"不要道歉")
|
||||
4. 用户纠正 AI 的行为("你太正式了,放松点")
|
||||
|
||||
## 操作
|
||||
|
||||
### 1. persona_update - 更新人设
|
||||
|
||||
更新 AI 的角色特征或用户偏好。
|
||||
|
||||
**参数:**
|
||||
- `attributes`: 属性数组,每个包含 `attribute`(属性名)和 `value`(属性值)
|
||||
- `mode`: 更新模式,`merge`(合并,默认)或 `replace`(替换)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: persona_update
|
||||
params:
|
||||
attributes:
|
||||
- attribute: "角色"
|
||||
value: "猫娘"
|
||||
- attribute: "性格"
|
||||
value: "活泼"
|
||||
- attribute: "用户偏好_回复语言"
|
||||
value: "中文"
|
||||
mode: "merge"
|
||||
```
|
||||
|
||||
### 2. persona_clear - 清除人设
|
||||
|
||||
清除 AI 的所有角色特征。
|
||||
|
||||
**参数:**
|
||||
- `confirm`: 必须为 `true` 才能执行清除(安全措施)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: persona_clear
|
||||
params:
|
||||
confirm: true
|
||||
```
|
||||
|
||||
## 人设查询模板
|
||||
|
||||
每次对话开始时,AI 应执行:
|
||||
```yaml
|
||||
action: recall
|
||||
params:
|
||||
queryIntent: "AI人设 用户偏好 角色设定"
|
||||
seedEntities: ["AI"]
|
||||
depth: 2
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **主动查询**:不要等用户提醒才查人设,主动在对话开始时检索
|
||||
2. **及时更新**:用户一旦表达偏好或设定角色,立即写入
|
||||
3. **分类存储**:使用前缀区分人设类型,如 `角色_`、`用户偏好_`、`行为规则_`
|
||||
4. **持久化**:人设信息应简洁、持久,写入图数据库长期保存
|
||||
96
ts/bundled-skills/graph-memory-task/SKILL.md
Normal file
96
ts/bundled-skills/graph-memory-task/SKILL.md
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
name: graph-memory-task
|
||||
description: "管理连续性任务 - AI 应主动追踪和维护任务状态,将任务信息作为长期记忆存储在图数据库中"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory Task 任务管理
|
||||
|
||||
管理长期/连续性任务。
|
||||
|
||||
> **定位声明**:任务是 AI 长期记忆的一部分,存储在图数据库中。AI **应主动追踪**任务状态,在适当时候提醒用户或更新进度。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**AI 必须在以下场景主动查询任务**:
|
||||
1. 对话开始时(检索进行中的任务)
|
||||
2. 用户询问进度或状态时
|
||||
3. 用户提到"继续之前的..."、"那个任务怎么样了"等
|
||||
4. 做计划或安排时(了解现有任务负荷)
|
||||
|
||||
**AI 必须在以下场景创建/更新任务**:
|
||||
1. 用户明确创建任务("帮我记住要做...")
|
||||
2. 用户完成或取消任务
|
||||
3. 用户更新任务信息或进度
|
||||
4. AI 自己承诺要完成某事(应创建任务跟踪)
|
||||
|
||||
## 操作
|
||||
|
||||
### 1. task_create - 创建任务
|
||||
|
||||
创建新的任务节点。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 唯一任务标识(必需)
|
||||
- `description`: 任务描述(必需)
|
||||
- `info_nodes`: 可选的相关信息节点数组
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: task_create
|
||||
params:
|
||||
task_id: "Task_学习TypeScript"
|
||||
description: "学习 TypeScript 并完成项目"
|
||||
info_nodes: ["TypeScript文档", "教程链接"]
|
||||
```
|
||||
|
||||
### 2. task_set_state - 设置状态
|
||||
|
||||
更新任务状态。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
- `state`: 新状态,`进行中`/`已完成`/`已暂停`/`已取消`(必需)
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
action: task_set_state
|
||||
params:
|
||||
task_id: "Task_学习TypeScript"
|
||||
state: "已完成"
|
||||
```
|
||||
|
||||
### 3. task_delete - 删除任务
|
||||
|
||||
删除任务节点。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
|
||||
### 4. task_link_info - 关联信息
|
||||
|
||||
将信息节点关联到任务。
|
||||
|
||||
**参数:**
|
||||
- `task_id`: 任务ID(必需)
|
||||
- `info_node`: 信息节点名称(必需)
|
||||
|
||||
## 任务查询模板
|
||||
|
||||
每次对话开始时,AI 应执行:
|
||||
```yaml
|
||||
action: recall
|
||||
params:
|
||||
queryIntent: "任务 进行中 待办"
|
||||
seedEntities: ["Task"]
|
||||
depth: 2
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **主动追踪**:不要等用户问才查任务,主动在对话开始时检索进行中的任务
|
||||
2. **及时更新**:任务状态变化时立即更新,避免信息过时
|
||||
3. **ID规范**:任务ID应具有描述性(如 `Task_学习TypeScript`)
|
||||
4. **定期提醒**:对于长期任务,在对话中适时提醒用户进度或截止日期
|
||||
5. **关联信息**:将相关资源、链接、笔记关联到任务,形成完整上下文
|
||||
120
ts/bundled-skills/graph-memory/SKILL.md
Normal file
120
ts/bundled-skills/graph-memory/SKILL.md
Normal file
@ -0,0 +1,120 @@
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - AI 应优先使用图数据库管理长期记忆,将重要事实结构化存储"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# GraphMemory 图记忆系统
|
||||
|
||||
让 AI 拥有真正的长期记忆能力。
|
||||
|
||||
> **定位声明**:本工具是 AI 的**主要长期记忆系统**。memory-core 管理 session transcripts,GraphMemory 负责结构化长期记忆。AI 在对话中应优先将重要信息写入图数据库,而非依赖 message 历史。
|
||||
|
||||
## 与 memory-core 的关系
|
||||
|
||||
| 功能 | memory-core | GraphMemory (本工具) |
|
||||
|------|-------------|---------------------|
|
||||
| 对话历史 | ✅ 自动保存 messages | ❌ 不管理对话历史 |
|
||||
| 结构化记忆 | ❌ 无 | ✅ **主要存储** |
|
||||
| 人设管理 | ❌ 无 | ✅ **主要存储** |
|
||||
| 任务追踪 | ❌ 无 | ✅ **主要存储** |
|
||||
| **语义搜索** | **❌ 无** | **✅ memory_search** |
|
||||
| **记忆文件读取** | **❌ 无** | **✅ memory_get** |
|
||||
| 自动触发 | ✅ 自动索引检索 | ❌ LLM 主动调用 |
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 实体 (Entity)
|
||||
现实世界中的对象,如"用户"、"Python"、"WaterFlow"。
|
||||
|
||||
### 关系 (Relation)
|
||||
连接两个实体的关系,格式为三元组:主体 - 关系 - 客体。
|
||||
|
||||
## 可用命令
|
||||
|
||||
### 1. recall - 检索记忆
|
||||
|
||||
从记忆图中检索相关信息。
|
||||
|
||||
**参数**:
|
||||
- `queryIntent`: 搜索意图/关键词(必需,若无则需提供 seedEntities)
|
||||
- `seedEntities`: 可选的种子实体名
|
||||
- `depth`: 检索深度 1-5(默认 2)
|
||||
- `sessionFilter`: 可选的会话ID过滤
|
||||
|
||||
### 2. commit - 写入记忆
|
||||
|
||||
将信息写入记忆图。
|
||||
|
||||
**参数**:
|
||||
- `triplets`: 三元组数组,每个包含 subject, relation, object(必需)
|
||||
- `sessionId`: 会话ID
|
||||
- `turnId`: 轮次ID
|
||||
|
||||
### 3. purge - 删除记忆
|
||||
|
||||
从记忆图中删除信息。
|
||||
|
||||
**参数**:
|
||||
- `criteria`: 删除条件 (subject, target, relation, sessionId)
|
||||
- `mode`: 删除模式 (soft/hard/supersede)
|
||||
- `newRelation`: 替代关系(supersede 模式必需)
|
||||
|
||||
### 4. introspect - 查看状态
|
||||
|
||||
查看当前记忆状态统计。
|
||||
|
||||
### 5. archive - 归档旧记忆
|
||||
|
||||
将 N 天前的非活跃关系标记为归档。
|
||||
|
||||
**参数**:
|
||||
- `days`: 归档天数(默认 30)
|
||||
|
||||
### 6. cleanup - 清理无效数据
|
||||
|
||||
物理删除已删除超过 90 天的关系和孤立节点。
|
||||
|
||||
**参数**:
|
||||
- `dry_run`: 仅预览不删除(默认 true)
|
||||
|
||||
## 使用原则
|
||||
|
||||
1. **优先使用图记忆**:对于重要事实、偏好、决策、任务等持久信息,**优先**使用 `commit` 写入图数据库,而非依赖 message 上下文
|
||||
2. **主动检索**:在回答用户问题前,**先调用 `recall` 或 `memory_search`** 检索相关记忆,而非仅凭当前上下文推理
|
||||
3. **结构化**:使用三元组格式存储关系,确保信息可被关联检索
|
||||
4. **定期清理**:删除过时或错误的信息
|
||||
|
||||
## 记忆策略指南
|
||||
|
||||
### 什么时候写入图数据库?
|
||||
|
||||
**必须写入**:
|
||||
- 用户明确说"请记住"、"记住这个"等
|
||||
- 用户透露偏好、习惯、身份信息
|
||||
- 做出重要决策或选择
|
||||
- 创建任务或目标
|
||||
- 关键知识点或学习成果
|
||||
|
||||
**建议写入**:
|
||||
- 对话中重复出现的重要概念
|
||||
- 用户纠正或补充的信息
|
||||
- 项目相关的配置、路径、决策
|
||||
|
||||
**无需写入**:
|
||||
- 临时性问候、寒暄
|
||||
- 一次性问题(如"现在几点")
|
||||
- 已在图数据库中的重复信息
|
||||
|
||||
### 什么时候检索图数据库?
|
||||
|
||||
**必须检索**:
|
||||
- 用户问"我之前说过..."、"你还记得..."
|
||||
- 需要基于历史偏好做推荐或决策
|
||||
- 继续之前的任务或话题
|
||||
|
||||
**建议检索**:
|
||||
- 每次对话开始时,检索用户相关信息
|
||||
- 做推荐前先了解用户偏好
|
||||
- 涉及人设或性格相关的话题
|
||||
BIN
ts/graph_memory.db-shm
Normal file
BIN
ts/graph_memory.db-shm
Normal file
Binary file not shown.
BIN
ts/graph_memory.db-wal
Normal file
BIN
ts/graph_memory.db-wal
Normal file
Binary file not shown.
23
ts/openclaw.plugin.json
Normal file
23
ts/openclaw.plugin.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "graph-memory",
|
||||
"name": "Graph Memory",
|
||||
"description": "让 AI 拥有真正的长期记忆能力 - 基于图数据库的 AI 主要长期记忆系统。与 memory-core 并存运行:memory-core 管理对话历史,GraphMemory 管理结构化长期记忆(知识图谱、人设、任务、语义搜索)。",
|
||||
"skills": [
|
||||
"bundled-skills/graph-memory",
|
||||
"bundled-skills/graph-memory-persona",
|
||||
"bundled-skills/graph-memory-task"
|
||||
],
|
||||
"contracts": {
|
||||
"tools": ["graph_memory"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dbPath": {
|
||||
"type": "string",
|
||||
"description": "SQLite 数据库文件路径,默认 graph_memory.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2782
ts/package-lock.json
generated
Normal file
2782
ts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
ts/package.json
Normal file
35
ts/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@trulymem/openclaw-graph-memory",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./dist/plugin-entry.js"
|
||||
],
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.3.24-beta.2",
|
||||
"minGatewayVersion": "2026.3.24-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.3.24-beta.2",
|
||||
"pluginSdkVersion": "2026.3.24-beta.2"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"sharp": "^0.34.5",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^25.5.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
20
ts/src/plugin-entry.ts
Normal file
20
ts/src/plugin-entry.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { GraphMemoryToolSchema, createGraphMemoryTool } from './runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
|
||||
export default {
|
||||
id: 'graph-memory',
|
||||
name: 'Graph Memory',
|
||||
description: '让 AI 拥有真正的长期记忆能力 - 基于图数据库的记忆系统',
|
||||
register(api: {
|
||||
registerTool: (tool: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: typeof GraphMemoryToolSchema;
|
||||
execute: (id: string, params: Record<string, unknown>) => Promise<{
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
}>;
|
||||
}) => void;
|
||||
}): void {
|
||||
const tool = createGraphMemoryTool();
|
||||
api.registerTool(tool);
|
||||
}
|
||||
};
|
||||
350
ts/src/runtime/core/graph_memory/graph_database.ts
Normal file
350
ts/src/runtime/core/graph_memory/graph_database.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import {
|
||||
SemanticSearchEngine
|
||||
} from './semantic_search.js';
|
||||
import type {
|
||||
Entity, Relation, RecallParams, CommitParams, PurgeParams,
|
||||
RecallResult, CommitResult, PurgeResult, MemoryStats
|
||||
} from './types.js';
|
||||
|
||||
export class GraphDatabase {
|
||||
private db: Database.Database;
|
||||
private sessionId: string;
|
||||
private semanticSearch: SemanticSearchEngine;
|
||||
|
||||
constructor(dbPath?: string, sessionId?: string) {
|
||||
this.db = new Database(dbPath || 'graph_memory.db');
|
||||
this.sessionId = sessionId || `session-${Date.now()}`;
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.semanticSearch = new SemanticSearchEngine(this.db);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
getSemanticSearch(): SemanticSearchEngine {
|
||||
return this.semanticSearch;
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT DEFAULT 'unknown',
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT,
|
||||
turn_id INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
date_bucket TEXT,
|
||||
superseded_by TEXT,
|
||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_session ON relations(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_date ON relations(date_bucket);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_superseded ON relations(superseded_by);
|
||||
`);
|
||||
}
|
||||
|
||||
async recall(params: RecallParams): Promise<RecallResult> {
|
||||
const { queryIntent, seedEntities, depth = 2, timeRange, sessionFilter } = params;
|
||||
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
|
||||
const entityIds = new Set<string>();
|
||||
const entities: Entity[] = [];
|
||||
const relations: Relation[] = [];
|
||||
|
||||
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
|
||||
return { entities: [], relations: [], message: 'No query keywords' };
|
||||
}
|
||||
|
||||
const timeCondition = timeRange?.days
|
||||
? `AND r.created_at >= datetime('now', '-${timeRange.days} days')`
|
||||
: '';
|
||||
const sessionCondition = sessionFilter ? `AND r.session_id = ?` : '';
|
||||
|
||||
if (keywords.length > 0) {
|
||||
const likeConditions = keywords.map(() => `LOWER(e.name) LIKE ?`).join(' OR ');
|
||||
const likeParams = keywords.map(k => `%${k.toLowerCase()}%`);
|
||||
const seedRows = this.db.prepare(
|
||||
`SELECT DISTINCT e.* FROM entities e WHERE ${likeConditions}`
|
||||
).all(...likeParams) as Array<Record<string, unknown>>;
|
||||
|
||||
for (const row of seedRows) {
|
||||
const id = row.id as string;
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
entities.push(this.rowToEntity(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seedEntities && seedEntities.length > 0) {
|
||||
const placeholders = seedEntities.map(() => '?').join(',');
|
||||
const exactRows = this.db.prepare(
|
||||
`SELECT * FROM entities WHERE LOWER(name) IN (${placeholders})`
|
||||
).all(...seedEntities.map(s => s.toLowerCase())) as Array<Record<string, unknown>>;
|
||||
|
||||
for (const row of exactRows) {
|
||||
const id = row.id as string;
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
entities.push(this.rowToEntity(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visited = new Set<string>(entityIds);
|
||||
let currentLevel = new Set<string>(entityIds);
|
||||
|
||||
for (let d = 1; d < depth; d++) {
|
||||
const nextLevel = new Set<string>();
|
||||
const idsArray = Array.from(currentLevel);
|
||||
if (idsArray.length === 0) break;
|
||||
|
||||
const placeholders = idsArray.map(() => '?').join(',');
|
||||
const neighborRows = this.db.prepare(`
|
||||
SELECT DISTINCT e.* FROM entities e
|
||||
JOIN relations r ON (r.source_id = e.id OR r.target_id = e.id)
|
||||
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
|
||||
AND r.status = 'active'
|
||||
${timeCondition}
|
||||
${sessionCondition}
|
||||
LIMIT 200
|
||||
`).all(...idsArray, ...idsArray, ...(sessionFilter ? [sessionFilter] : [])) as Array<Record<string, unknown>>;
|
||||
|
||||
for (const row of neighborRows) {
|
||||
const id = row.id as string;
|
||||
if (!visited.has(id)) {
|
||||
visited.add(id);
|
||||
nextLevel.add(id);
|
||||
entities.push(this.rowToEntity(row));
|
||||
}
|
||||
}
|
||||
currentLevel = nextLevel;
|
||||
}
|
||||
|
||||
if (entityIds.size > 0) {
|
||||
const allIds = Array.from(entityIds);
|
||||
const placeholders = allIds.map(() => '?').join(',');
|
||||
const relationRows = this.db.prepare(`
|
||||
SELECT r.*, e1.name as source_name, e2.name as target_name
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
|
||||
AND r.status = 'active'
|
||||
${timeCondition}
|
||||
${sessionCondition}
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT 100
|
||||
`).all(...allIds, ...allIds, ...(sessionFilter ? [sessionFilter] : [])) as Array<Record<string, unknown>>;
|
||||
|
||||
for (const row of relationRows) {
|
||||
relations.push(this.rowToRelation(row));
|
||||
}
|
||||
}
|
||||
|
||||
return { entities, relations, message: `Found ${entities.length} entities, ${relations.length} relations` };
|
||||
}
|
||||
|
||||
async commit(params: CommitParams): Promise<CommitResult> {
|
||||
const { triplets, sessionId, turnId } = params;
|
||||
let createdEntities = 0;
|
||||
let createdRelations = 0;
|
||||
|
||||
const insertRelation = this.db.prepare(`
|
||||
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status, date_bucket)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?)
|
||||
`);
|
||||
|
||||
const insertEntity = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO entities (id, name, type) VALUES (?, ?, 'unknown')
|
||||
`);
|
||||
|
||||
const updateEntity = this.db.prepare(`
|
||||
UPDATE entities SET mention_count = mention_count + 1, updated_at = datetime('now') WHERE name = ?
|
||||
`);
|
||||
|
||||
const getEntity = this.db.prepare(`SELECT id FROM entities WHERE name = ?`);
|
||||
|
||||
for (const triplet of triplets) {
|
||||
const dateBucket = new Date().toISOString().split('T')[0] ?? '';
|
||||
|
||||
let sourceId = (getEntity.get(triplet.subject) as { id: string } | undefined)?.id;
|
||||
if (sourceId) {
|
||||
updateEntity.run(triplet.subject);
|
||||
} else {
|
||||
sourceId = this.generateId();
|
||||
insertEntity.run(sourceId, triplet.subject);
|
||||
}
|
||||
createdEntities++;
|
||||
|
||||
let targetId = (getEntity.get(triplet.object) as { id: string } | undefined)?.id;
|
||||
if (targetId) {
|
||||
updateEntity.run(triplet.object);
|
||||
} else {
|
||||
targetId = this.generateId();
|
||||
insertEntity.run(targetId, triplet.object);
|
||||
}
|
||||
createdEntities++;
|
||||
|
||||
insertRelation.run(
|
||||
this.generateId(), sourceId, targetId, triplet.relation,
|
||||
triplet.confidence || 1.0, sessionId || this.sessionId,
|
||||
turnId || 0, dateBucket
|
||||
);
|
||||
createdRelations++;
|
||||
}
|
||||
|
||||
return { createdEntities, createdRelations };
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams): Promise<PurgeResult> {
|
||||
const { criteria, mode = 'soft', newRelation } = params;
|
||||
let deleted = 0;
|
||||
if (!criteria) return { deleted: 0, mode };
|
||||
|
||||
const conditions: string[] = ["status = 'active'"];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (criteria.subject) {
|
||||
conditions.push(`source_id IN (SELECT id FROM entities WHERE LOWER(name) = ?)`);
|
||||
values.push(criteria.subject.toLowerCase());
|
||||
}
|
||||
if (criteria.target) {
|
||||
conditions.push(`target_id IN (SELECT id FROM entities WHERE LOWER(name) = ?)`);
|
||||
values.push(criteria.target.toLowerCase());
|
||||
}
|
||||
if (criteria.relation) {
|
||||
conditions.push(`LOWER(relation_type) = ?`);
|
||||
values.push(criteria.relation.toLowerCase());
|
||||
}
|
||||
if (criteria.sessionId) {
|
||||
conditions.push(`session_id = ?`);
|
||||
values.push(criteria.sessionId);
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
if (mode === 'supersede' && newRelation) {
|
||||
const ids = this.db.prepare(
|
||||
`SELECT id, source_id, target_id FROM relations WHERE ${whereClause}`
|
||||
).all(...values) as Array<{ id: string; source_id: string; target_id: string }>;
|
||||
|
||||
for (const row of ids) {
|
||||
const newId = this.generateId();
|
||||
this.db.prepare(`
|
||||
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status, date_bucket, superseded_by)
|
||||
VALUES (?, ?, ?, ?, 1.0, ?, 0, 'active', ?, ?)
|
||||
`).run(newId, row.source_id, row.target_id, newRelation.relation, this.sessionId, new Date().toISOString().split('T')[0], row.id);
|
||||
|
||||
this.db.prepare(`
|
||||
UPDATE relations SET status = 'superseded', superseded_by = ?, updated_at = datetime('now') WHERE id = ?
|
||||
`).run(newId, row.id);
|
||||
deleted++;
|
||||
}
|
||||
} else if (mode === 'hard') {
|
||||
const result = this.db.prepare(`DELETE FROM relations WHERE ${whereClause}`).run(...values);
|
||||
deleted = result.changes;
|
||||
} else {
|
||||
const result = this.db.prepare(`UPDATE relations SET status = 'deleted', updated_at = datetime('now') WHERE ${whereClause}`).run(...values);
|
||||
deleted = result.changes;
|
||||
}
|
||||
|
||||
return { deleted, mode };
|
||||
}
|
||||
|
||||
async introspect(): Promise<MemoryStats> {
|
||||
const entityCount = (this.db.prepare(`SELECT COUNT(*) as c FROM entities`).get() as { c: number }).c;
|
||||
const relationCount = (this.db.prepare(`SELECT COUNT(*) as c FROM relations WHERE status = 'active'`).get() as { c: number }).c;
|
||||
return { entityCount, relationCount, sessionId: this.sessionId };
|
||||
}
|
||||
|
||||
async archive(days: number = 30): Promise<{ archived: number }> {
|
||||
const result = this.db.prepare(`
|
||||
UPDATE relations SET status = 'archived', updated_at = datetime('now')
|
||||
WHERE status = 'active' AND created_at < datetime('now', '-' || ? || ' days')
|
||||
`).run(days);
|
||||
return { archived: result.changes };
|
||||
}
|
||||
|
||||
async cleanup(dryRun: boolean = true): Promise<{ deleted_relations: number; deleted_entities: number; details?: string[] }> {
|
||||
const details: string[] = [];
|
||||
const oldRelations = this.db.prepare(`
|
||||
SELECT id FROM relations WHERE status = 'deleted' AND updated_at < datetime('now', '-90 days')
|
||||
`).all() as Array<{ id: string }>;
|
||||
|
||||
if (dryRun) {
|
||||
details.push(`Will delete ${oldRelations.length} old deleted relations`);
|
||||
const orphaned = this.db.prepare(`
|
||||
SELECT e.id, e.name FROM entities e
|
||||
WHERE e.id NOT IN (SELECT DISTINCT source_id FROM relations WHERE status != 'deleted')
|
||||
AND e.id NOT IN (SELECT DISTINCT target_id FROM relations WHERE status != 'deleted')
|
||||
`).all() as Array<{ id: string; name: string }>;
|
||||
details.push(`Will delete ${orphaned.length} orphaned entities`);
|
||||
return { deleted_relations: oldRelations.length, deleted_entities: orphaned.length, details };
|
||||
}
|
||||
|
||||
const deleteResult = this.db.prepare(`
|
||||
DELETE FROM relations WHERE status = 'deleted' AND updated_at < datetime('now', '-90 days')
|
||||
`).run();
|
||||
|
||||
const deleteOrphans = this.db.prepare(`
|
||||
DELETE FROM entities
|
||||
WHERE id NOT IN (SELECT DISTINCT source_id FROM relations)
|
||||
AND id NOT IN (SELECT DISTINCT target_id FROM relations)
|
||||
`).run();
|
||||
|
||||
return { deleted_relations: deleteResult.changes, deleted_entities: deleteOrphans.changes };
|
||||
}
|
||||
|
||||
private rowToEntity(row: Record<string, unknown>): Entity {
|
||||
return {
|
||||
id: row.id as string, name: row.name as string,
|
||||
type: (row.type as string) || 'unknown', mentionCount: (row.mention_count as number) || 1,
|
||||
createdAt: new Date(row.created_at as string), updatedAt: new Date(row.updated_at as string)
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRelation(row: Record<string, unknown>): Relation {
|
||||
return {
|
||||
id: row.id as string, sourceId: row.source_id as string, targetId: row.target_id as string,
|
||||
relationType: row.relation_type as string, confidence: (row.confidence as number) || 1.0,
|
||||
status: (row.status as Relation['status']) || 'active',
|
||||
sessionId: (row.session_id as string) || this.sessionId,
|
||||
turnId: (row.turn_id as number) || 0,
|
||||
createdAt: new Date(row.created_at as string), updatedAt: new Date(row.updated_at as string),
|
||||
dateBucket: (row.date_bucket as string) || ''
|
||||
};
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void { this.sessionId = sessionId; }
|
||||
getSessionId(): string { return this.sessionId; }
|
||||
close(): void { this.db.close(); }
|
||||
}
|
||||
4
ts/src/runtime/core/graph_memory/index.ts
Normal file
4
ts/src/runtime/core/graph_memory/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './types.js';
|
||||
export * from './semantic_search.js';
|
||||
export * from './graph_database.js';
|
||||
export { MemoryService } from './memory_service.js';
|
||||
410
ts/src/runtime/core/graph_memory/memory_service.ts
Normal file
410
ts/src/runtime/core/graph_memory/memory_service.ts
Normal file
@ -0,0 +1,410 @@
|
||||
import { GraphDatabase } from './graph_database.js';
|
||||
import { TaskNodeStore } from './task_node_store.js';
|
||||
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.js';
|
||||
|
||||
export class MemoryService {
|
||||
private db: GraphDatabase;
|
||||
private taskStore: TaskNodeStore;
|
||||
private contextArchiveDir: string;
|
||||
|
||||
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> {
|
||||
return this.db.recall(params);
|
||||
}
|
||||
|
||||
async commit(params: CommitParams): Promise<CommitResult> {
|
||||
return this.db.commit(params);
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams): Promise<PurgeResult> {
|
||||
return this.db.purge(params);
|
||||
}
|
||||
|
||||
async introspect(): Promise<MemoryStats> {
|
||||
return this.db.introspect();
|
||||
}
|
||||
|
||||
async archive(days: number = 30): Promise<{ archived: number }> {
|
||||
return this.db.archive(days);
|
||||
}
|
||||
|
||||
async cleanup(dryRun: boolean = true): Promise<{ deleted_relations: number; deleted_entities: number; details?: string[] }> {
|
||||
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;
|
||||
|
||||
if (mode === 'replace') {
|
||||
await this.db.purge({
|
||||
criteria: { subject: 'AI' },
|
||||
mode: 'soft'
|
||||
});
|
||||
}
|
||||
|
||||
const triplets = attributes.map(attr => ({
|
||||
subject: 'AI',
|
||||
relation: attr.attribute,
|
||||
object: attr.value,
|
||||
confidence: 1.0
|
||||
}));
|
||||
|
||||
await this.db.commit({ triplets });
|
||||
|
||||
return { status: 'success', updatedAttributes: attributes.length };
|
||||
}
|
||||
|
||||
async clearPersona(params: { confirm: boolean }): Promise<{ status: string; deletedCount: number }> {
|
||||
if (params.confirm === false) {
|
||||
return { status: 'cancelled', deletedCount: 0 };
|
||||
}
|
||||
|
||||
const result = await this.db.purge({
|
||||
criteria: { subject: 'AI' },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
return { status: 'success', deletedCount: result.deleted };
|
||||
}
|
||||
|
||||
async createTask(params: { task_id: string; description: string; info_nodes?: string[] | undefined }): Promise<{ status: string; taskId: string }> {
|
||||
const { task_id, description, info_nodes = [] } = params;
|
||||
|
||||
await this.db.commit({
|
||||
triplets: [
|
||||
{ subject: task_id, relation: 'is_type', object: 'TaskNode' },
|
||||
{ subject: task_id, relation: 'has_description', object: description },
|
||||
{ subject: task_id, relation: 'HAS_STATE', object: 'State_进行中' }
|
||||
]
|
||||
});
|
||||
|
||||
if (info_nodes.length > 0) {
|
||||
await this.db.commit({
|
||||
triplets: info_nodes.map(node => ({
|
||||
subject: task_id,
|
||||
relation: 'CONTAINS_INFO',
|
||||
object: node
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'success', taskId: task_id };
|
||||
}
|
||||
|
||||
async setTaskState(params: { task_id: string; state: string }): Promise<{ status: string; newState: string }> {
|
||||
const { task_id, state } = params;
|
||||
|
||||
await this.db.purge({
|
||||
criteria: { subject: task_id, relation: 'HAS_STATE' },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
await this.db.commit({
|
||||
triplets: [{ subject: task_id, relation: 'HAS_STATE', object: `State_${state}` }]
|
||||
});
|
||||
|
||||
return { status: 'success', newState: state };
|
||||
}
|
||||
|
||||
async deleteTask(params: { task_id: string }): Promise<{ status: string; taskId: string }> {
|
||||
const { task_id } = params;
|
||||
|
||||
await this.db.purge({
|
||||
criteria: { subject: task_id },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
return { status: 'success', taskId: task_id };
|
||||
}
|
||||
|
||||
async linkInfoToTask(params: { task_id: string; info_node: string }): Promise<{ status: string }> {
|
||||
const { task_id, info_node } = params;
|
||||
|
||||
await this.db.commit({
|
||||
triplets: [{ subject: task_id, relation: 'CONTAINS_INFO', object: info_node }]
|
||||
});
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
async storeSemanticMemory(text: string, source?: string, sourceLine?: number): Promise<string> {
|
||||
const id = this.generateId();
|
||||
const semanticSearch = this.db.getSemanticSearch();
|
||||
const embedding = await semanticSearch.generateEmbedding(text);
|
||||
semanticSearch.storeEmbedding(id, text, embedding, source, sourceLine);
|
||||
return id;
|
||||
}
|
||||
|
||||
async semanticSearch(query: string, limit: number = 10): Promise<Array<{ id: string; text: string; source?: string; similarity: number }>> {
|
||||
const semanticSearch = this.db.getSemanticSearch();
|
||||
const queryEmbedding = await semanticSearch.generateEmbedding(query);
|
||||
const results = semanticSearch.searchSimilar(queryEmbedding, limit);
|
||||
return results.map(r => ({
|
||||
id: r.id,
|
||||
text: r.text,
|
||||
source: r.source || undefined,
|
||||
similarity: Math.round(r.similarity * 1000) / 1000
|
||||
}));
|
||||
}
|
||||
|
||||
async readMemoryFragment(path: string, fromLine?: number, lines?: number): Promise<string> {
|
||||
const content = fs.readFileSync(path, 'utf-8');
|
||||
if (fromLine !== undefined && lines !== undefined) {
|
||||
const allLines = content.split('\n');
|
||||
return allLines.slice(fromLine - 1, fromLine - 1 + lines).join('\n');
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// ========== Utility ==========
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
this.db.setSessionId(sessionId);
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
return this.db.getSessionId();
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
}
|
||||
}
|
||||
86
ts/src/runtime/core/graph_memory/semantic_search.ts
Normal file
86
ts/src/runtime/core/graph_memory/semantic_search.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
export interface SemanticMemory {
|
||||
id: string;
|
||||
text: string;
|
||||
embedding: Float32Array;
|
||||
source: string;
|
||||
sourceLine?: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
text: string;
|
||||
source: string;
|
||||
sourceLine?: number;
|
||||
similarity: number;
|
||||
}
|
||||
|
||||
export class SemanticSearchEngine {
|
||||
private db: Database.Database;
|
||||
private embeddingModel: any;
|
||||
private modelReady: boolean = false;
|
||||
|
||||
constructor(db: Database.Database) {
|
||||
this.db = db;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS embeddings (
|
||||
id TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
embedding BLOB NOT NULL,
|
||||
source TEXT,
|
||||
source_line INTEGER,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_embeddings_source ON embeddings(source);
|
||||
`);
|
||||
}
|
||||
|
||||
async loadModel(): Promise<void> {
|
||||
if (this.modelReady) return;
|
||||
const { pipeline } = await import('@xenova/transformers') as any;
|
||||
this.embeddingModel = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
||||
this.modelReady = true;
|
||||
}
|
||||
|
||||
async generateEmbedding(text: string): Promise<Float32Array> {
|
||||
await this.loadModel();
|
||||
const output = await this.embeddingModel(text, { pooling: 'mean', normalize: true });
|
||||
return new Float32Array(output.data);
|
||||
}
|
||||
|
||||
storeEmbedding(id: string, text: string, embedding: Float32Array, source?: string, sourceLine?: number): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT OR REPLACE INTO embeddings (id, text, embedding, source, source_line)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(id, text, Buffer.from(embedding.buffer), source || null, sourceLine || null);
|
||||
}
|
||||
|
||||
searchSimilar(queryEmbedding: Float32Array, limit: number = 10): SearchResult[] {
|
||||
const rows = this.db.prepare('SELECT id, text, embedding, source, source_line FROM embeddings').all();
|
||||
const results = rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
text: row.text,
|
||||
source: row.source,
|
||||
sourceLine: row.source_line,
|
||||
similarity: cosineSimilarity(queryEmbedding, new Float32Array(row.embedding.buffer))
|
||||
}));
|
||||
return results.sort((a: SearchResult, b: SearchResult) => b.similarity - a.similarity).slice(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
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.js';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
172
ts/src/runtime/core/graph_memory/types.ts
Normal file
172
ts/src/runtime/core/graph_memory/types.ts
Normal file
@ -0,0 +1,172 @@
|
||||
export interface Entity {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
mentionCount: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const __types = true;
|
||||
|
||||
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
|
||||
|
||||
export interface Relation {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
relationType: string;
|
||||
confidence: number;
|
||||
status: RelationStatus;
|
||||
sessionId: string;
|
||||
turnId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dateBucket: string;
|
||||
}
|
||||
|
||||
export interface Triplet {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export interface RecallParams {
|
||||
queryIntent: string;
|
||||
seedEntities?: string[] | undefined;
|
||||
depth?: number | undefined;
|
||||
timeRange?: { days: number } | undefined;
|
||||
sessionFilter?: string | undefined;
|
||||
}
|
||||
|
||||
export interface CommitParams {
|
||||
triplets: Triplet[];
|
||||
entityTypes?: Record<string, string> | undefined;
|
||||
temporalTag?: string | undefined;
|
||||
sessionId?: string | undefined;
|
||||
turnId?: number | undefined;
|
||||
}
|
||||
|
||||
export interface PurgeParams {
|
||||
criteria?: {
|
||||
subject?: string | undefined;
|
||||
target?: string | undefined;
|
||||
relation?: string | undefined;
|
||||
sessionId?: string | undefined;
|
||||
} | undefined;
|
||||
mode?: 'soft' | 'hard' | 'supersede' | undefined;
|
||||
newRelation?: { relation: string; target: string } | undefined;
|
||||
}
|
||||
|
||||
export interface RecallResult {
|
||||
entities: Entity[];
|
||||
relations: Relation[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CommitResult {
|
||||
createdEntities: number;
|
||||
createdRelations: number;
|
||||
}
|
||||
|
||||
export interface PurgeResult {
|
||||
deleted: number;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
|
||||
export interface Task {
|
||||
taskId: string;
|
||||
description: string;
|
||||
state: TaskState;
|
||||
infoNodes: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryStats {
|
||||
entityCount: number;
|
||||
relationCount: number;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface PersonaAttribute {
|
||||
attribute: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PersonaUpdateParams {
|
||||
attributes: PersonaAttribute[];
|
||||
mode?: 'merge' | 'replace';
|
||||
}
|
||||
|
||||
export interface PersonaClearParams {
|
||||
confirm: boolean;
|
||||
}
|
||||
|
||||
export interface TaskCreateParams {
|
||||
task_id: string;
|
||||
description: string;
|
||||
info_nodes?: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface TaskSetStateParams {
|
||||
task_id: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface TaskDeleteParams {
|
||||
task_id: string;
|
||||
}
|
||||
|
||||
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 }>;
|
||||
}
|
||||
652
ts/src/runtime/core/tools/builtin/graph_memory_tool.ts
Normal file
652
ts/src/runtime/core/tools/builtin/graph_memory_tool.ts
Normal file
@ -0,0 +1,652 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { Static } from '@sinclair/typebox';
|
||||
import { GraphDatabase } from '../../graph_memory/graph_database.js';
|
||||
import { MemoryService } from '../../graph_memory/memory_service.js';
|
||||
import { ToolLimiter } from '../tool_limiter.js';
|
||||
|
||||
export const GraphMemoryToolSchema = Type.Object({
|
||||
action: Type.String({
|
||||
description: '记忆操作类型',
|
||||
enum: [
|
||||
'recall', 'commit', 'purge', 'introspect', 'archive', 'cleanup',
|
||||
'persona_update', 'persona_clear',
|
||||
'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',
|
||||
'memory_search', 'memory_get'
|
||||
]
|
||||
}),
|
||||
params: Type.Object({
|
||||
queryIntent: Type.Optional(Type.String({ description: '搜索意图' })),
|
||||
seedEntities: Type.Optional(Type.Array(Type.String({ description: '实体' }), { description: '种子实体' })),
|
||||
depth: Type.Optional(Type.Number({ description: '检索深度' })),
|
||||
sessionFilter: Type.Optional(Type.String({ description: '会话ID过滤' })),
|
||||
triplets: Type.Optional(Type.Array(
|
||||
Type.Object({
|
||||
subject: Type.String({ description: '主体' }),
|
||||
relation: Type.String({ description: '关系' }),
|
||||
object: Type.String({ description: '客体' }),
|
||||
confidence: Type.Optional(Type.Number({ description: '置信度' }))
|
||||
}, { description: '三元组' }),
|
||||
{ description: '三元组数组' }
|
||||
)),
|
||||
sessionId: Type.Optional(Type.String({ description: '会话ID' })),
|
||||
turnId: Type.Optional(Type.Number({ description: '轮次ID' })),
|
||||
criteria: Type.Optional(Type.Object({
|
||||
subject: Type.Optional(Type.String({ description: '主体' })),
|
||||
target: Type.Optional(Type.String({ description: '客体' })),
|
||||
relation: Type.Optional(Type.String({ description: '关系' })),
|
||||
sessionId: Type.Optional(Type.String({ description: '会话ID' }))
|
||||
}, { description: '删除条件' })),
|
||||
mode: Type.Optional(Type.String({
|
||||
enum: ['soft', 'hard', 'supersede'],
|
||||
description: '删除模式'
|
||||
})),
|
||||
newRelation: Type.Optional(Type.Object({
|
||||
relation: Type.String({ description: '关系' }),
|
||||
target: Type.String({ description: '客体' })
|
||||
}, { description: '新关系(supersede模式)' })),
|
||||
attributes: Type.Optional(Type.Array(
|
||||
Type.Object({
|
||||
attribute: Type.String({ description: '属性名' }),
|
||||
value: Type.String({ description: '属性值' })
|
||||
}, { description: '属性' }),
|
||||
{ description: '属性数组' }
|
||||
)),
|
||||
confirm: Type.Optional(Type.Boolean({ description: '确认清除' })),
|
||||
task_id: Type.Optional(Type.String({ description: '任务ID' })),
|
||||
description: Type.Optional(Type.String({ description: '任务描述' })),
|
||||
state: Type.Optional(Type.String({ description: '任务状态' })),
|
||||
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: '仅预览不删除' })),
|
||||
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' })),
|
||||
query: Type.Optional(Type.String({ description: '语义搜索查询' })),
|
||||
corpus: Type.Optional(Type.String({
|
||||
enum: ['memory', 'wiki', 'all'],
|
||||
description: '搜索语料范围'
|
||||
})),
|
||||
path: Type.Optional(Type.String({ description: '记忆文件路径' })),
|
||||
fromLine: Type.Optional(Type.Number({ description: '起始行号' })),
|
||||
lines: Type.Optional(Type.Number({ description: '读取行数' }))
|
||||
}, { description: '操作参数' })
|
||||
});
|
||||
|
||||
export type GraphMemoryToolParams = Static<typeof GraphMemoryToolSchema>;
|
||||
|
||||
const GRAPH_MEMORY_TOOL_DESCRIPTION = `图记忆工具 - 让 AI 拥有真正的长期记忆能力。作为 OpenClaw memory-core 的增强补充,不替代其核心功能。
|
||||
|
||||
操作:
|
||||
- recall: 检索记忆(可选参数: queryIntent, seedEntities, depth, sessionFilter)
|
||||
- commit: 写入记忆(必需参数: triplets)
|
||||
- purge: 删除记忆(可选参数: criteria, mode, newRelation)
|
||||
- introspect: 查看状态(无参数)
|
||||
- archive: 归档旧记忆(可选参数: days, 默认30天)
|
||||
- cleanup: 清理无效数据(可选参数: dry_run, 默认true预览模式)
|
||||
- persona_update: 更新人设(必需参数: attributes; 可选: mode=merge/replace)
|
||||
- persona_clear: 清除人设(必需参数: confirm=true)
|
||||
- task_create: 创建任务(必需参数: task_id, description; 可选: info_nodes)
|
||||
- task_set_state: 设置任务状态(必需参数: task_id, state)
|
||||
- task_delete: 删除任务(必需参数: task_id)
|
||||
- task_link_info: 关联信息(必需参数: task_id, info_node)
|
||||
- context_rewrite: 压缩上下文为关键记忆(必需参数: context; 可选: maxEntities, summary)
|
||||
- working_memory_chain: 获取工作记忆链(可选参数: maxDepth, recentOnly)
|
||||
- memory_search: 语义向量搜索(必需参数: query; 可选: limit, corpus)
|
||||
- memory_get: 精确读取记忆文件片段(必需参数: path; 可选: fromLine, lines)`;
|
||||
|
||||
// ==================== 参数验证 ====================
|
||||
|
||||
interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function validateRecallParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const queryIntent = params.queryIntent;
|
||||
const seedEntities = params.seedEntities;
|
||||
|
||||
if ((!queryIntent || (typeof queryIntent === 'string' && queryIntent.trim() === '')) &&
|
||||
(!seedEntities || !Array.isArray(seedEntities) || seedEntities.length === 0)) {
|
||||
errors.push({ field: 'queryIntent/seedEntities', message: 'recall 操作需要提供 queryIntent 或 seedEntities 之一' });
|
||||
}
|
||||
|
||||
if (params.depth !== undefined) {
|
||||
const depth = Number(params.depth);
|
||||
if (isNaN(depth) || depth < 1 || depth > 5) {
|
||||
errors.push({ field: 'depth', message: 'depth 必须在 1-5 之间' });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateCommitParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const triplets = params.triplets;
|
||||
|
||||
if (!triplets || !Array.isArray(triplets) || triplets.length === 0) {
|
||||
errors.push({ field: 'triplets', message: 'commit 操作必需提供 triplets 数组' });
|
||||
return errors;
|
||||
}
|
||||
|
||||
for (let i = 0; i < triplets.length; i++) {
|
||||
const t = triplets[i] as Record<string, unknown>;
|
||||
if (!t.subject || typeof t.subject !== 'string' || t.subject.trim() === '') {
|
||||
errors.push({ field: `triplets[${i}].subject`, message: '三元组主体不能为空字符串' });
|
||||
}
|
||||
if (!t.relation || typeof t.relation !== 'string' || t.relation.trim() === '') {
|
||||
errors.push({ field: `triplets[${i}].relation`, message: '三元组关系不能为空字符串' });
|
||||
}
|
||||
if (!t.object || typeof t.object !== 'string' || t.object.trim() === '') {
|
||||
errors.push({ field: `triplets[${i}].object`, message: '三元组客体不能为空字符串' });
|
||||
}
|
||||
if (t.confidence !== undefined) {
|
||||
const c = Number(t.confidence);
|
||||
if (isNaN(c) || c < 0 || c > 1) {
|
||||
errors.push({ field: `triplets[${i}].confidence`, message: '置信度必须在 0-1 之间' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validatePurgeParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (params.mode === 'supersede' && (!params.newRelation || typeof params.newRelation !== 'object')) {
|
||||
errors.push({ field: 'newRelation', message: 'supersede 模式必需提供 newRelation' });
|
||||
}
|
||||
|
||||
if (params.newRelation) {
|
||||
const nr = params.newRelation as Record<string, unknown>;
|
||||
if (!nr.relation || typeof nr.relation !== 'string') {
|
||||
errors.push({ field: 'newRelation.relation', message: 'newRelation.relation 必须是字符串' });
|
||||
}
|
||||
if (!nr.target || typeof nr.target !== 'string') {
|
||||
errors.push({ field: 'newRelation.target', message: 'newRelation.target 必须是字符串' });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validatePersonaUpdateParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const attributes = params.attributes;
|
||||
|
||||
if (!attributes || !Array.isArray(attributes) || attributes.length === 0) {
|
||||
errors.push({ field: 'attributes', message: 'persona_update 操作必需提供 attributes 数组' });
|
||||
return errors;
|
||||
}
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i] as Record<string, unknown>;
|
||||
if (!attr.attribute || typeof attr.attribute !== 'string' || attr.attribute.trim() === '') {
|
||||
errors.push({ field: `attributes[${i}].attribute`, message: '属性名不能为空字符串' });
|
||||
}
|
||||
if (attr.value === undefined || typeof attr.value !== 'string') {
|
||||
errors.push({ field: `attributes[${i}].value`, message: '属性值必须是字符串' });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskCreateParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
|
||||
errors.push({ field: 'task_id', message: 'task_create 操作必需提供 task_id 字符串' });
|
||||
}
|
||||
if (!params.description || typeof params.description !== 'string') {
|
||||
errors.push({ field: 'description', message: 'task_create 操作必需提供 description 字符串' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskSetStateParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
|
||||
errors.push({ field: 'task_id', message: 'task_set_state 操作必需提供 task_id 字符串' });
|
||||
}
|
||||
if (!params.state || typeof params.state !== 'string' || params.state.trim() === '') {
|
||||
errors.push({ field: 'state', message: 'task_set_state 操作必需提供 state 字符串' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskDeleteParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
|
||||
errors.push({ field: 'task_id', message: 'task_delete 操作必需提供 task_id 字符串' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTaskLinkInfoParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
|
||||
errors.push({ field: 'task_id', message: 'task_link_info 操作必需提供 task_id 字符串' });
|
||||
}
|
||||
if (!params.info_node || typeof params.info_node !== 'string' || params.info_node.trim() === '') {
|
||||
errors.push({ field: 'info_node', message: 'task_link_info 操作必需提供 info_node 字符串' });
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
|
||||
if (params.confirm === undefined) {
|
||||
errors.push({ field: 'confirm', message: 'persona_clear 操作必需设置 confirm: true 才能执行清除' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateMemorySearchParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.query || typeof params.query !== 'string' || params.query.trim() === '') {
|
||||
errors.push({ field: 'query', message: 'memory_search 操作必需提供 query 字符串' });
|
||||
}
|
||||
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1 || params.limit > 50)) {
|
||||
errors.push({ field: 'limit', message: 'limit 必须在 1-50 之间' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateMemoryGetParams(params: Record<string, unknown>): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
if (!params.path || typeof params.path !== 'string' || params.path.trim() === '') {
|
||||
errors.push({ field: 'path', message: 'memory_get 操作必需提供 path 字符串' });
|
||||
}
|
||||
if (params.fromLine !== undefined && (typeof params.fromLine !== 'number' || params.fromLine < 1)) {
|
||||
errors.push({ field: 'fromLine', message: 'fromLine 必须是正整数' });
|
||||
}
|
||||
if (params.lines !== undefined && (typeof params.lines !== 'number' || params.lines < 1 || params.lines > 500)) {
|
||||
errors.push({ field: 'lines', message: 'lines 必须在 1-500 之间' });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateParams(action: string, params: Record<string, unknown>): ValidationError[] {
|
||||
switch (action) {
|
||||
case 'recall': return validateRecallParams(params);
|
||||
case 'commit': return validateCommitParams(params);
|
||||
case 'purge': return validatePurgeParams(params);
|
||||
case 'persona_update': return validatePersonaUpdateParams(params);
|
||||
case 'persona_clear': return validatePersonaClearParams(params);
|
||||
case 'task_create': return validateTaskCreateParams(params);
|
||||
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);
|
||||
case 'memory_search': return validateMemorySearchParams(params);
|
||||
case 'memory_get': return validateMemoryGetParams(params);
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 日志 ====================
|
||||
|
||||
class GraphMemoryLogger {
|
||||
private prefix = '[TrulyMEM]';
|
||||
|
||||
info(message: string, meta?: Record<string, unknown>): void {
|
||||
console.log(`${this.prefix} [INFO] ${message}`, meta ? JSON.stringify(meta) : '');
|
||||
}
|
||||
|
||||
warn(message: string, meta?: Record<string, unknown>): void {
|
||||
console.warn(`${this.prefix} [WARN] ${message}`, meta ? JSON.stringify(meta) : '');
|
||||
}
|
||||
|
||||
error(message: string, meta?: Record<string, unknown>): void {
|
||||
console.error(`${this.prefix} [ERROR] ${message}`, meta ? JSON.stringify(meta) : '');
|
||||
}
|
||||
|
||||
action(action: string, params: Record<string, unknown>, result?: unknown): void {
|
||||
const meta: Record<string, unknown> = { action };
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
// 敏感信息脱敏:不记录具体属性值
|
||||
const sanitized = this.sanitizeParams(params);
|
||||
meta.params = sanitized;
|
||||
}
|
||||
if (result !== undefined) {
|
||||
meta.result = typeof result === 'object' && result !== null
|
||||
? (result as Record<string, unknown>).success ?? 'ok'
|
||||
: 'ok';
|
||||
}
|
||||
this.info(`action=${action}`, meta);
|
||||
}
|
||||
|
||||
private sanitizeParams(params: Record<string, unknown>): Record<string, unknown> {
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (key === 'attributes') {
|
||||
sanitized[key] = Array.isArray(value)
|
||||
? (value as Array<Record<string, unknown>>).map(a => a.attribute)
|
||||
: value;
|
||||
} else if (key === 'triplets') {
|
||||
sanitized[key] = Array.isArray(value)
|
||||
? `${(value as unknown[]).length} triplets`
|
||||
: value;
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 主逻辑 ====================
|
||||
|
||||
export function createGraphMemoryTool(dbPath?: string, sessionId?: string) {
|
||||
const db = new GraphDatabase(dbPath, sessionId);
|
||||
const service = new MemoryService(db);
|
||||
const limiter = new ToolLimiter();
|
||||
const logger = new GraphMemoryLogger();
|
||||
|
||||
async function executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
|
||||
// 1. 限流检查
|
||||
const [allowed, reason] = limiter.canCall(action);
|
||||
if (!allowed) {
|
||||
logger.warn(`Rate limit blocked: ${action}`, { reason });
|
||||
throw new Error(reason);
|
||||
}
|
||||
limiter.recordCall(action);
|
||||
|
||||
// 2. 参数验证
|
||||
const validationErrors = validateParams(action, params);
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMsg = validationErrors.map(e => `${e.field}: ${e.message}`).join('; ');
|
||||
logger.warn(`Validation failed for ${action}`, { errors: validationErrors });
|
||||
throw new Error(`参数验证失败: ${errorMsg}`);
|
||||
}
|
||||
|
||||
logger.action(action, params);
|
||||
|
||||
switch (action) {
|
||||
case 'recall':
|
||||
return service.recall({
|
||||
queryIntent: params.queryIntent as string || '',
|
||||
seedEntities: params.seedEntities as string[] | undefined,
|
||||
depth: params.depth as number | undefined,
|
||||
sessionFilter: params.sessionFilter as string | undefined
|
||||
});
|
||||
|
||||
case 'commit':
|
||||
return service.commit({
|
||||
triplets: params.triplets as Array<{ subject: string; relation: string; object: string; confidence?: number }>,
|
||||
sessionId: params.sessionId as string | undefined,
|
||||
turnId: params.turnId as number | undefined
|
||||
});
|
||||
|
||||
case 'purge':
|
||||
return service.purge({
|
||||
criteria: params.criteria as { subject?: string; target?: string; relation?: string; sessionId?: string } | undefined,
|
||||
mode: params.mode as 'soft' | 'hard' | 'supersede' | undefined,
|
||||
newRelation: params.newRelation as { relation: string; target: string } | undefined
|
||||
});
|
||||
|
||||
case 'introspect':
|
||||
return service.introspect();
|
||||
|
||||
case 'persona_update':
|
||||
return service.updatePersona({
|
||||
attributes: params.attributes as Array<{ attribute: string; value: string }>,
|
||||
mode: params.mode as 'merge' | 'replace'
|
||||
});
|
||||
|
||||
case 'persona_clear':
|
||||
// confirm=false 时也要允许执行(返回 cancelled),验证只拦截 confirm 不是 boolean 的情况
|
||||
if (params.confirm === undefined) {
|
||||
throw new Error('persona_clear 操作必需设置 confirm: true 才能执行清除');
|
||||
}
|
||||
return service.clearPersona({
|
||||
confirm: params.confirm as boolean
|
||||
});
|
||||
|
||||
case 'task_create':
|
||||
return service.createTask({
|
||||
task_id: params.task_id as string,
|
||||
description: params.description as string,
|
||||
info_nodes: params.info_nodes as string[] | undefined
|
||||
});
|
||||
|
||||
case 'task_set_state':
|
||||
return service.setTaskState({
|
||||
task_id: params.task_id as string,
|
||||
state: params.state as string
|
||||
});
|
||||
|
||||
case 'task_delete':
|
||||
return service.deleteTask({
|
||||
task_id: params.task_id as string
|
||||
});
|
||||
|
||||
case 'task_link_info':
|
||||
return service.linkInfoToTask({
|
||||
task_id: params.task_id as 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 'memory_search': {
|
||||
const results = await service.semanticSearch(
|
||||
params.query as string,
|
||||
params.limit as number | undefined
|
||||
);
|
||||
return { results, count: results.length };
|
||||
}
|
||||
|
||||
case 'memory_get': {
|
||||
const content = await service.readMemoryFragment(
|
||||
params.path as string,
|
||||
params.fromLine as number | undefined,
|
||||
params.lines as number | undefined
|
||||
);
|
||||
return { content, path: params.path };
|
||||
}
|
||||
|
||||
case 'archive':
|
||||
return service.archive(params.days as number | undefined);
|
||||
|
||||
case 'cleanup':
|
||||
return service.cleanup(params.dry_run as boolean | undefined);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'graph_memory',
|
||||
description: GRAPH_MEMORY_TOOL_DESCRIPTION,
|
||||
parameters: GraphMemoryToolSchema,
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>): Promise<{
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
}> {
|
||||
const action = params.action as string;
|
||||
const actionParams = (params.params as Record<string, unknown>) || {};
|
||||
|
||||
// 顶层参数验证
|
||||
if (!action || typeof action !== 'string') {
|
||||
logger.error('Missing or invalid action parameter', { params });
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: '必需提供 action 参数(字符串类型)'
|
||||
}
|
||||
})
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeAction(action, actionParams);
|
||||
logger.action(action, actionParams, result);
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ success: true, data: result }) }]
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Execution failed: ${action}`, { error: errorMessage });
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: {
|
||||
type: 'execution_error',
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 供测试使用的内部方法
|
||||
getLimiterSummary(): string {
|
||||
return limiter.getSummary();
|
||||
},
|
||||
|
||||
resetLimiter(): void {
|
||||
limiter.reset();
|
||||
logger.info('Tool limiter reset');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { ToolLimiter } from '../tool_limiter.js';
|
||||
139
ts/src/runtime/core/tools/tool_limiter.ts
Normal file
139
ts/src/runtime/core/tools/tool_limiter.ts
Normal file
@ -0,0 +1,139 @@
|
||||
export interface ToolLimits {
|
||||
persona_query_max: number;
|
||||
persona_update_max: number;
|
||||
task_query_max: number;
|
||||
task_update_max: number;
|
||||
memory_query_max: number;
|
||||
memory_update_max: number;
|
||||
}
|
||||
|
||||
export interface ToolCallCount {
|
||||
persona_query: number;
|
||||
persona_update: number;
|
||||
task_query: number;
|
||||
task_update: number;
|
||||
memory_query: number;
|
||||
memory_update: number;
|
||||
}
|
||||
|
||||
const DEFAULT_LIMITS: ToolLimits = {
|
||||
persona_query_max: 1,
|
||||
persona_update_max: 1,
|
||||
task_query_max: 4,
|
||||
task_update_max: 5,
|
||||
memory_query_max: 20,
|
||||
memory_update_max: 10,
|
||||
};
|
||||
|
||||
export class ToolLimiter {
|
||||
limits: ToolLimits;
|
||||
counts: ToolCallCount;
|
||||
|
||||
constructor(limits?: Partial<ToolLimits>) {
|
||||
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
||||
this.counts = {
|
||||
persona_query: 0,
|
||||
persona_update: 0,
|
||||
task_query: 0,
|
||||
task_update: 0,
|
||||
memory_query: 0,
|
||||
memory_update: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private classifyTool(action: string): [string, string] {
|
||||
switch (action) {
|
||||
case 'persona_update':
|
||||
case 'persona_clear':
|
||||
return ['persona', 'update'];
|
||||
case 'task_create':
|
||||
case 'task_set_state':
|
||||
case 'task_delete':
|
||||
case 'task_link_info':
|
||||
return ['task', 'update'];
|
||||
case 'recall':
|
||||
case 'introspect':
|
||||
return ['memory', 'query'];
|
||||
case 'commit':
|
||||
case 'purge':
|
||||
case 'archive':
|
||||
case 'cleanup':
|
||||
return ['memory', 'update'];
|
||||
default:
|
||||
return ['memory', 'update'];
|
||||
}
|
||||
}
|
||||
|
||||
canCall(action: string): [boolean, string] {
|
||||
const [category, operation] = this.classifyTool(action);
|
||||
|
||||
if (category === 'persona') {
|
||||
if (operation === 'query') {
|
||||
if (this.counts.persona_query >= this.limits.persona_query_max) {
|
||||
return [false, `人设图查询次数已达上限(${this.limits.persona_query_max}次)`];
|
||||
}
|
||||
} else {
|
||||
if (this.counts.persona_update >= this.limits.persona_update_max) {
|
||||
return [false, `人设图修改次数已达上限(${this.limits.persona_update_max}次)`];
|
||||
}
|
||||
}
|
||||
} else if (category === 'task') {
|
||||
if (operation === 'query') {
|
||||
if (this.counts.task_query >= this.limits.task_query_max) {
|
||||
return [false, `工作记忆链查询次数已达上限(${this.limits.task_query_max}次)`];
|
||||
}
|
||||
} else {
|
||||
if (this.counts.task_update >= this.limits.task_update_max) {
|
||||
return [false, `工作记忆链修改次数已达上限(${this.limits.task_update_max}次)`];
|
||||
}
|
||||
}
|
||||
} else if (category === 'memory') {
|
||||
if (operation === 'query') {
|
||||
if (this.counts.memory_query >= this.limits.memory_query_max) {
|
||||
return [false, `一般记忆查询次数已达上限(${this.limits.memory_query_max}次)`];
|
||||
}
|
||||
} else {
|
||||
if (this.counts.memory_update >= this.limits.memory_update_max) {
|
||||
return [false, `一般记忆修改次数已达上限(${this.limits.memory_update_max}次)`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [true, '允许调用'];
|
||||
}
|
||||
|
||||
recordCall(action: string): void {
|
||||
const [category, operation] = this.classifyTool(action);
|
||||
|
||||
if (category === 'persona') {
|
||||
if (operation === 'query') this.counts.persona_query++;
|
||||
else this.counts.persona_update++;
|
||||
} else if (category === 'task') {
|
||||
if (operation === 'query') this.counts.task_query++;
|
||||
else this.counts.task_update++;
|
||||
} else if (category === 'memory') {
|
||||
if (operation === 'query') this.counts.memory_query++;
|
||||
else this.counts.memory_update++;
|
||||
}
|
||||
}
|
||||
|
||||
getSummary(): string {
|
||||
const lines = [
|
||||
`人设图: 查询${this.counts.persona_query}/${this.limits.persona_query_max}次, 修改${this.counts.persona_update}/${this.limits.persona_update_max}次`,
|
||||
`工作记忆链: 查询${this.counts.task_query}/${this.limits.task_query_max}次, 修改${this.counts.task_update}/${this.limits.task_update_max}次`,
|
||||
`一般记忆: 查询${this.counts.memory_query}/${this.limits.memory_query_max}次, 修改${this.counts.memory_update}/${this.limits.memory_update_max}次`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.counts = {
|
||||
persona_query: 0,
|
||||
persona_update: 0,
|
||||
task_query: 0,
|
||||
task_update: 0,
|
||||
memory_query: 0,
|
||||
memory_update: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
222
ts/tests/runtime/core/graph_memory/graph_database.test.ts
Normal file
222
ts/tests/runtime/core/graph_memory/graph_database.test.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GraphDatabase } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/graph_database.js';
|
||||
|
||||
const TEST_DB_PATH = '/tmp/test_graph_memory.db';
|
||||
|
||||
describe('GraphDatabase', () => {
|
||||
let db: GraphDatabase;
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
if (fs.existsSync(TEST_DB_PATH)) {
|
||||
fs.unlinkSync(TEST_DB_PATH);
|
||||
}
|
||||
if (fs.existsSync(`${TEST_DB_PATH}-wal`)) {
|
||||
fs.unlinkSync(`${TEST_DB_PATH}-wal`);
|
||||
}
|
||||
if (fs.existsSync(`${TEST_DB_PATH}-shm`)) {
|
||||
fs.unlinkSync(`${TEST_DB_PATH}-shm`);
|
||||
}
|
||||
} catch {}
|
||||
db = new GraphDatabase(TEST_DB_PATH);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (db && typeof db.close === 'function') {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('creates entities and relations', async () => {
|
||||
const result = await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'Alice', relation: 'knows', object: 'Bob' }
|
||||
]
|
||||
});
|
||||
|
||||
expect(result.createdEntities).toBe(2);
|
||||
expect(result.createdRelations).toBe(1);
|
||||
});
|
||||
|
||||
it('handles batch multiple triplets', async () => {
|
||||
const result = await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' },
|
||||
{ subject: 'TypeScript', relation: '是', object: '语言' }
|
||||
]
|
||||
});
|
||||
|
||||
expect(result.createdEntities).toBe(6);
|
||||
expect(result.createdRelations).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recall', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' },
|
||||
{ subject: 'TypeScript', relation: '是', object: '语言' },
|
||||
{ subject: 'Alice', relation: 'knows', object: 'Bob' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('keyword search finds matching entities', async () => {
|
||||
const result = await db.recall({ queryIntent: '用户' });
|
||||
|
||||
expect(result.entities.length).toBeGreaterThan(0);
|
||||
expect(result.entities.some(e => e.name === '用户')).toBe(true);
|
||||
});
|
||||
|
||||
it('seedEntities finds exact matches', async () => {
|
||||
const result = await db.recall({
|
||||
queryIntent: 'test',
|
||||
seedEntities: ['用户']
|
||||
});
|
||||
|
||||
expect(result.entities.length).toBeGreaterThan(0);
|
||||
expect(result.entities.some(e => e.name === '用户')).toBe(true);
|
||||
});
|
||||
|
||||
it('sessionFilter filters by session', async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'Test', relation: 'is', object: 'Temp' }
|
||||
],
|
||||
sessionId: 'test-session'
|
||||
});
|
||||
|
||||
const result = await db.recall({
|
||||
queryIntent: 'Test',
|
||||
sessionFilter: 'test-session'
|
||||
});
|
||||
|
||||
expect(result.entities.some(e => e.name === 'Test')).toBe(true);
|
||||
});
|
||||
|
||||
it('finds related entities through relations', async () => {
|
||||
const result = await db.recall({
|
||||
queryIntent: '编程'
|
||||
});
|
||||
|
||||
expect(result.relations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('purge', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'ToDelete', relation: 'is', object: 'Test' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('soft delete marks relations as deleted', async () => {
|
||||
const result = await db.purge({
|
||||
criteria: { subject: 'ToDelete' },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
expect(result.deleted).toBe(1);
|
||||
expect(result.mode).toBe('soft');
|
||||
});
|
||||
|
||||
it('hard delete removes relations permanently', async () => {
|
||||
const result = await db.purge({
|
||||
criteria: { subject: 'ToDelete' },
|
||||
mode: 'hard'
|
||||
});
|
||||
|
||||
expect(result.deleted).toBe(1);
|
||||
expect(result.mode).toBe('hard');
|
||||
});
|
||||
|
||||
it('purge filters by target criteria', async () => {
|
||||
const result = await db.purge({
|
||||
criteria: { target: 'Test' },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
expect(result.deleted).toBe(1);
|
||||
});
|
||||
|
||||
it('purge filters by relation criteria', async () => {
|
||||
const result = await db.purge({
|
||||
criteria: { relation: 'is' },
|
||||
mode: 'soft'
|
||||
});
|
||||
|
||||
expect(result.deleted).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('introspect', () => {
|
||||
it('returns entity and relation counts', async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'Entity1', relation: 'relates', object: 'Entity2' }
|
||||
]
|
||||
});
|
||||
|
||||
const stats = await db.introspect();
|
||||
|
||||
expect(stats.entityCount).toBe(2);
|
||||
expect(stats.relationCount).toBe(1);
|
||||
expect(stats.sessionId).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty query handling', () => {
|
||||
it('returns empty result for query with no matches', async () => {
|
||||
const result = await db.recall({ queryIntent: 'xyznonexistent123' });
|
||||
|
||||
expect(result.entities).toEqual([]);
|
||||
expect(result.relations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate entity handling', () => {
|
||||
it('upserts existing entities (increments mention count)', async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'Duplicate', relation: 'is', object: 'First' }
|
||||
]
|
||||
});
|
||||
|
||||
const stats1 = await db.introspect();
|
||||
const entityBefore = stats1.entityCount;
|
||||
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: 'Duplicate', relation: 'is', object: 'Second' }
|
||||
]
|
||||
});
|
||||
|
||||
const stats2 = await db.introspect();
|
||||
const entityAfter = stats2.entityCount;
|
||||
|
||||
expect(entityAfter).toBeLessThan(entityBefore + 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSessionId and getSessionId', () => {
|
||||
it('setSessionId updates session id', async () => {
|
||||
db.setSessionId('new-session-id');
|
||||
|
||||
expect(db.getSessionId()).toBe('new-session-id');
|
||||
});
|
||||
|
||||
it('getSessionId returns current session id', async () => {
|
||||
const sessionId = db.getSessionId();
|
||||
|
||||
expect(sessionId).toBeDefined();
|
||||
expect(typeof sessionId).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
242
ts/tests/runtime/core/graph_memory/memory_service.test.ts
Normal file
242
ts/tests/runtime/core/graph_memory/memory_service.test.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { MemoryService } from '../../../../dist/runtime/core/graph_memory/memory_service.js';
|
||||
import { GraphDatabase } from '../../../../dist/runtime/core/graph_memory/graph_database.js';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const TEST_DB_PATH = '/tmp/test_memory_service.db';
|
||||
|
||||
describe('MemoryService', () => {
|
||||
let memoryService: MemoryService;
|
||||
let db: GraphDatabase;
|
||||
|
||||
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);
|
||||
db = new GraphDatabase(TEST_DB_PATH, 'test-session');
|
||||
memoryService = new MemoryService(db);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (typeof db.close === 'function') db.close();
|
||||
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);
|
||||
});
|
||||
|
||||
describe('updatePersona', () => {
|
||||
it('should merge attributes in merge mode', async () => {
|
||||
const result = await memoryService.updatePersona({
|
||||
attributes: [
|
||||
{ attribute: 'name', value: 'AI Assistant' },
|
||||
{ attribute: 'personality', value: 'helpful' }
|
||||
],
|
||||
mode: 'merge'
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.updatedAttributes).toBe(2);
|
||||
});
|
||||
|
||||
it('should replace attributes in replace mode', async () => {
|
||||
await memoryService.updatePersona({
|
||||
attributes: [
|
||||
{ attribute: 'name', value: 'Old Name' }
|
||||
],
|
||||
mode: 'merge'
|
||||
});
|
||||
|
||||
const result = await memoryService.updatePersona({
|
||||
attributes: [
|
||||
{ attribute: 'name', value: 'New Name' }
|
||||
],
|
||||
mode: 'replace'
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.updatedAttributes).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearPersona', () => {
|
||||
it('should delete persona when confirm is true', async () => {
|
||||
await memoryService.updatePersona({
|
||||
attributes: [
|
||||
{ attribute: 'name', value: 'Test AI' }
|
||||
],
|
||||
mode: 'merge'
|
||||
});
|
||||
|
||||
const result = await memoryService.clearPersona({ confirm: true });
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.deletedCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should cancel when confirm is false', async () => {
|
||||
const result = await memoryService.clearPersona({ confirm: false });
|
||||
expect(result.status).toBe('cancelled');
|
||||
expect(result.deletedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('should create task with info_nodes', async () => {
|
||||
const result = await memoryService.createTask({
|
||||
task_id: 'Task_001',
|
||||
description: 'Test task with info',
|
||||
info_nodes: ['Node_A', 'Node_B']
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.taskId).toBe('Task_001');
|
||||
});
|
||||
|
||||
it('should create task without info_nodes', async () => {
|
||||
const result = await memoryService.createTask({
|
||||
task_id: 'Task_002',
|
||||
description: 'Test task without info'
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.taskId).toBe('Task_002');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTaskState', () => {
|
||||
it('should set task state', async () => {
|
||||
await memoryService.createTask({
|
||||
task_id: 'Task_StateTest',
|
||||
description: 'Task for state test'
|
||||
});
|
||||
|
||||
const result = await memoryService.setTaskState({
|
||||
task_id: 'Task_StateTest',
|
||||
state: '已暂停'
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.newState).toBe('已暂停');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTask', () => {
|
||||
it('should delete task', async () => {
|
||||
await memoryService.createTask({
|
||||
task_id: 'Task_Delete',
|
||||
description: 'Task to delete'
|
||||
});
|
||||
|
||||
const result = await memoryService.deleteTask({ task_id: 'Task_Delete' });
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.taskId).toBe('Task_Delete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkInfoToTask', () => {
|
||||
it('should link info node to task', async () => {
|
||||
await memoryService.createTask({
|
||||
task_id: 'Task_Link',
|
||||
description: 'Task for linking'
|
||||
});
|
||||
|
||||
const result = await memoryService.linkInfoToTask({
|
||||
task_id: 'Task_Link',
|
||||
info_node: 'Info_Node_X'
|
||||
});
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionId', () => {
|
||||
it('should set and get sessionId', () => {
|
||||
memoryService.setSessionId('custom-session-123');
|
||||
const sessionId = memoryService.getSessionId();
|
||||
expect(sessionId).toBe('custom-session-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('task state transitions', () => {
|
||||
it('should transition: 进行中 → 已暂停 → 进行中 → 已完成', async () => {
|
||||
await memoryService.createTask({
|
||||
task_id: 'Task_Transition',
|
||||
description: 'State transition task'
|
||||
});
|
||||
|
||||
const state1 = await memoryService.setTaskState({
|
||||
task_id: 'Task_Transition',
|
||||
state: '进行中'
|
||||
});
|
||||
expect(state1.newState).toBe('进行中');
|
||||
|
||||
const state2 = await memoryService.setTaskState({
|
||||
task_id: 'Task_Transition',
|
||||
state: '已暂停'
|
||||
});
|
||||
expect(state2.newState).toBe('已暂停');
|
||||
|
||||
const state3 = await memoryService.setTaskState({
|
||||
task_id: 'Task_Transition',
|
||||
state: '进行中'
|
||||
});
|
||||
expect(state3.newState).toBe('进行中');
|
||||
|
||||
const state4 = await memoryService.setTaskState({
|
||||
task_id: 'Task_Transition',
|
||||
state: '已完成'
|
||||
});
|
||||
expect(state4.newState).toBe('已完成');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegate methods', () => {
|
||||
it('recall should delegate to db', async () => {
|
||||
await memoryService.commit({
|
||||
triplets: [
|
||||
{ subject: '测试', relation: '是', object: '示例' }
|
||||
]
|
||||
});
|
||||
|
||||
const result = await memoryService.recall({
|
||||
queryIntent: '测试'
|
||||
});
|
||||
|
||||
expect(result.entities).toBeDefined();
|
||||
expect(result.relations).toBeDefined();
|
||||
});
|
||||
|
||||
it('commit should delegate to db', async () => {
|
||||
const result = await memoryService.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' }
|
||||
]
|
||||
});
|
||||
|
||||
expect(result.createdEntities).toBeGreaterThan(0);
|
||||
expect(result.createdRelations).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('purge should delegate to db', async () => {
|
||||
await memoryService.commit({
|
||||
triplets: [
|
||||
{ subject: '待删除', relation: '是', object: '测试' }
|
||||
]
|
||||
});
|
||||
|
||||
const result = await memoryService.purge({
|
||||
criteria: { subject: '待删除' }
|
||||
});
|
||||
|
||||
expect(result.deleted).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
692
ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts
Normal file
692
ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts
Normal file
@ -0,0 +1,692 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createGraphMemoryTool, ToolLimiter } from '../../../../../dist/runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const TEST_DB_PATH = '/tmp/test_graph_memory_tool.db';
|
||||
|
||||
describe('GraphMemoryTool', () => {
|
||||
let tool: ReturnType<typeof createGraphMemoryTool>;
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DB_PATH)) {
|
||||
fs.unlinkSync(TEST_DB_PATH);
|
||||
}
|
||||
tool = createGraphMemoryTool(TEST_DB_PATH, 'test-session');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(TEST_DB_PATH)) {
|
||||
fs.unlinkSync(TEST_DB_PATH);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Tool metadata', () => {
|
||||
it('should have correct name', () => {
|
||||
expect(tool.name).toBe('graph_memory');
|
||||
});
|
||||
|
||||
it('should have description', () => {
|
||||
expect(tool.description).toBeTruthy();
|
||||
expect(tool.description).toContain('图记忆');
|
||||
});
|
||||
|
||||
it('should have input schema with all actions', () => {
|
||||
const actions = tool.parameters.properties?.action?.enum as string[];
|
||||
expect(actions).toContain('recall');
|
||||
expect(actions).toContain('commit');
|
||||
expect(actions).toContain('purge');
|
||||
expect(actions).toContain('introspect');
|
||||
expect(actions).toContain('persona_update');
|
||||
expect(actions).toContain('persona_clear');
|
||||
expect(actions).toContain('task_create');
|
||||
expect(actions).toContain('task_set_state');
|
||||
expect(actions).toContain('task_delete');
|
||||
expect(actions).toContain('task_link_info');
|
||||
expect(actions).toContain('archive');
|
||||
expect(actions).toContain('cleanup');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute wrapper', () => {
|
||||
it('should execute recall action', async () => {
|
||||
await tool.execute('call-1', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-2', {
|
||||
action: 'recall',
|
||||
params: {
|
||||
queryIntent: '用户 编程'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('content');
|
||||
expect(Array.isArray(result.content)).toBe(true);
|
||||
expect(result.content[0]).toHaveProperty('type', 'text');
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.entities).toBeDefined();
|
||||
expect(parsed.data.relations).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return empty results for non-matching query', async () => {
|
||||
const result = await tool.execute('call-3', {
|
||||
action: 'recall',
|
||||
params: {
|
||||
queryIntent: '不存在的关键词xyz123'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.entities).toEqual([]);
|
||||
expect(parsed.data.relations).toEqual([]);
|
||||
});
|
||||
|
||||
it('should commit triplets successfully', async () => {
|
||||
const result = await tool.execute('call-4', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '测试', relation: '是', object: '示例' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.createdEntities).toBeGreaterThan(0);
|
||||
expect(parsed.data.createdRelations).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle multiple triplets', async () => {
|
||||
const result = await tool.execute('call-5', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '实体A', relation: '关联', object: '实体B' },
|
||||
{ subject: '实体B', relation: '关联', object: '实体C' },
|
||||
{ subject: '实体C', relation: '关联', object: '实体A' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.createdEntities).toBe(6);
|
||||
expect(parsed.data.createdRelations).toBe(3);
|
||||
});
|
||||
|
||||
it('should purge relations by criteria', async () => {
|
||||
await tool.execute('call-6', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '待删除', relation: '是', object: '测试' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-7', {
|
||||
action: 'purge',
|
||||
params: {
|
||||
criteria: { subject: '待删除' },
|
||||
mode: 'soft'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.deleted).toBeGreaterThanOrEqual(1);
|
||||
expect(parsed.data.mode).toBe('soft');
|
||||
});
|
||||
|
||||
it('should return 0 when no criteria provided', async () => {
|
||||
const result = await tool.execute('call-8', {
|
||||
action: 'purge',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.deleted).toBe(0);
|
||||
});
|
||||
|
||||
it('should return memory statistics', async () => {
|
||||
await tool.execute('call-9', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '实体1', relation: '关系', object: '实体2' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-10', {
|
||||
action: 'introspect',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.entityCount).toBeGreaterThan(0);
|
||||
expect(parsed.data.relationCount).toBeGreaterThan(0);
|
||||
expect(parsed.data.sessionId).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update persona attributes', async () => {
|
||||
const result = await tool.execute('call-11', {
|
||||
action: 'persona_update',
|
||||
params: {
|
||||
attributes: [
|
||||
{ attribute: 'name', value: 'AI助手' },
|
||||
{ attribute: '性格', value: '友善' }
|
||||
],
|
||||
mode: 'merge'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
expect(parsed.data.updatedAttributes).toBe(2);
|
||||
});
|
||||
|
||||
it('should clear persona when confirm is true', async () => {
|
||||
const result = await tool.execute('call-12', {
|
||||
action: 'persona_clear',
|
||||
params: {
|
||||
confirm: true
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
expect(parsed.data.deletedCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should cancel when confirm is false', async () => {
|
||||
const result = await tool.execute('call-13', {
|
||||
action: 'persona_clear',
|
||||
params: {
|
||||
confirm: false
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('cancelled');
|
||||
expect(parsed.data.deletedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should create task successfully', async () => {
|
||||
const result = await tool.execute('call-14', {
|
||||
action: 'task_create',
|
||||
params: {
|
||||
task_id: 'Task_Test123',
|
||||
description: '测试任务描述'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
expect(parsed.data.taskId).toBe('Task_Test123');
|
||||
});
|
||||
|
||||
it('should create task with info_nodes', async () => {
|
||||
const result = await tool.execute('call-15', {
|
||||
action: 'task_create',
|
||||
params: {
|
||||
task_id: 'Task_WithInfo',
|
||||
description: '带信息的任务',
|
||||
info_nodes: ['信息节点1', '信息节点2']
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.taskId).toBe('Task_WithInfo');
|
||||
});
|
||||
|
||||
it('should set task state', async () => {
|
||||
await tool.execute('call-16', {
|
||||
action: 'task_create',
|
||||
params: {
|
||||
task_id: 'Task_StateTest',
|
||||
description: '状态测试任务'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-17', {
|
||||
action: 'task_set_state',
|
||||
params: {
|
||||
task_id: 'Task_StateTest',
|
||||
state: '已完成'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
expect(parsed.data.newState).toBe('已完成');
|
||||
});
|
||||
|
||||
it('should delete task', async () => {
|
||||
await tool.execute('call-18', {
|
||||
action: 'task_create',
|
||||
params: {
|
||||
task_id: 'Task_DeleteMe',
|
||||
description: '待删除任务'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-19', {
|
||||
action: 'task_delete',
|
||||
params: {
|
||||
task_id: 'Task_DeleteMe'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
expect(parsed.data.taskId).toBe('Task_DeleteMe');
|
||||
});
|
||||
|
||||
it('should link info node to task', async () => {
|
||||
await tool.execute('call-20', {
|
||||
action: 'task_create',
|
||||
params: {
|
||||
task_id: 'Task_Link',
|
||||
description: '链接测试任务'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await tool.execute('call-21', {
|
||||
action: 'task_link_info',
|
||||
params: {
|
||||
task_id: 'Task_Link',
|
||||
info_node: '新的信息节点'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('success');
|
||||
});
|
||||
|
||||
it('should archive old relations', async () => {
|
||||
const result = await tool.execute('call-22', {
|
||||
action: 'archive',
|
||||
params: {
|
||||
days: 30
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should cleanup in dry_run mode', async () => {
|
||||
const result = await tool.execute('call-23', {
|
||||
action: 'cleanup',
|
||||
params: {
|
||||
dry_run: true
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should cleanup without dry_run', async () => {
|
||||
const result = await tool.execute('call-24', {
|
||||
action: 'cleanup',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error for unknown action', async () => {
|
||||
const result = await tool.execute('call-25', {
|
||||
action: 'invalid_action',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle errors in execute format', async () => {
|
||||
const result = await tool.execute('call-26', {
|
||||
action: '',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle limiter blocking', async () => {
|
||||
const newTool = createGraphMemoryTool(TEST_DB_PATH + '.limiter', 'limiter-session');
|
||||
|
||||
// Fill up memory_query limit
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await newTool.execute(`limiter-call-${i}`, {
|
||||
action: 'introspect',
|
||||
params: {}
|
||||
});
|
||||
}
|
||||
|
||||
// Next recall should be blocked
|
||||
const result = await newTool.execute('limiter-blocked', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: '测试' }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('已达上限');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToolLimiter standalone', () => {
|
||||
it('should limit memory query calls', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const [allowed] = limiter.canCall('recall');
|
||||
if (allowed) limiter.recordCall('recall');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('recall');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should limit memory update calls', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const [allowed] = limiter.canCall('commit');
|
||||
if (allowed) limiter.recordCall('commit');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('commit');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should get summary', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
const summary = limiter.getSummary();
|
||||
expect(summary).toContain('人设图');
|
||||
expect(summary).toContain('工作记忆链');
|
||||
expect(summary).toContain('一般记忆');
|
||||
});
|
||||
|
||||
it('should reset and allow calls again', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const [allowed] = limiter.canCall('recall');
|
||||
if (allowed) limiter.recordCall('recall');
|
||||
}
|
||||
|
||||
expect(limiter.canCall('recall')[0]).toBe(false);
|
||||
limiter.reset();
|
||||
expect(limiter.canCall('recall')[0]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameter validation', () => {
|
||||
it('should reject commit without triplets', async () => {
|
||||
const result = await tool.execute('call-val-1', {
|
||||
action: 'commit',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('triplets');
|
||||
});
|
||||
|
||||
it('should reject commit with empty triplet fields', async () => {
|
||||
const result = await tool.execute('call-val-2', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [{ subject: '', relation: '是', object: '测试' }]
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('subject');
|
||||
});
|
||||
|
||||
it('should reject persona_update without attributes', async () => {
|
||||
const result = await tool.execute('call-val-3', {
|
||||
action: 'persona_update',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('attributes');
|
||||
});
|
||||
|
||||
it('should reject persona_clear without confirm', async () => {
|
||||
const result = await tool.execute('call-val-4', {
|
||||
action: 'persona_clear',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('confirm');
|
||||
});
|
||||
|
||||
it('should allow persona_clear with confirm=false and return cancelled', async () => {
|
||||
const result = await tool.execute('call-val-4b', {
|
||||
action: 'persona_clear',
|
||||
params: { confirm: false }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(true);
|
||||
expect(parsed.data.status).toBe('cancelled');
|
||||
expect(parsed.data.deletedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should reject task_create without task_id', async () => {
|
||||
const result = await tool.execute('call-val-5', {
|
||||
action: 'task_create',
|
||||
params: { description: '测试' }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('task_id');
|
||||
});
|
||||
|
||||
it('should reject task_set_state without state', async () => {
|
||||
const result = await tool.execute('call-val-6', {
|
||||
action: 'task_set_state',
|
||||
params: { task_id: 'T1' }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('state');
|
||||
});
|
||||
|
||||
it('should reject recall without queryIntent and seedEntities', async () => {
|
||||
const result = await tool.execute('call-val-7', {
|
||||
action: 'recall',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('queryIntent');
|
||||
});
|
||||
|
||||
it('should reject depth out of range', async () => {
|
||||
const result = await tool.execute('call-val-8', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: '测试', depth: 10 }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('depth');
|
||||
});
|
||||
|
||||
it('should reject commit with invalid confidence', async () => {
|
||||
const result = await tool.execute('call-val-9', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [{ subject: 'A', relation: '是', object: 'B', confidence: 2 }]
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(parsed.error.message).toContain('置信度');
|
||||
});
|
||||
|
||||
it('should reject purge supersede without newRelation', async () => {
|
||||
const result = await tool.execute('call-val-10', {
|
||||
action: 'purge',
|
||||
params: { mode: 'supersede' }
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
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-15', {
|
||||
action: '',
|
||||
params: {}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
expect(parsed.success).toBe(false);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
433
ts/tests/runtime/core/tools/tool_limiter.test.ts
Normal file
433
ts/tests/runtime/core/tools/tool_limiter.test.ts
Normal file
@ -0,0 +1,433 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ToolLimiter } from '../../../../dist/runtime/core/tools/tool_limiter.js';
|
||||
|
||||
describe('ToolLimiter', () => {
|
||||
describe('Default limits', () => {
|
||||
it('should have correct default limits', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
expect(limiter.limits.persona_query_max).toBe(1);
|
||||
expect(limiter.limits.persona_update_max).toBe(1);
|
||||
expect(limiter.limits.task_query_max).toBe(4);
|
||||
expect(limiter.limits.task_update_max).toBe(5);
|
||||
expect(limiter.limits.memory_query_max).toBe(20);
|
||||
expect(limiter.limits.memory_update_max).toBe(10);
|
||||
});
|
||||
|
||||
it('should initialize counts to zero', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
expect(limiter.counts.persona_query).toBe(0);
|
||||
expect(limiter.counts.persona_update).toBe(0);
|
||||
expect(limiter.counts.task_query).toBe(0);
|
||||
expect(limiter.counts.task_update).toBe(0);
|
||||
expect(limiter.counts.memory_query).toBe(0);
|
||||
expect(limiter.counts.memory_update).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canCall allows within limits', () => {
|
||||
let limiter: ToolLimiter;
|
||||
|
||||
beforeEach(() => {
|
||||
limiter = new ToolLimiter();
|
||||
});
|
||||
|
||||
it('should allow recall (memory query) initially', () => {
|
||||
const [allowed, reason] = limiter.canCall('recall');
|
||||
expect(allowed).toBe(true);
|
||||
expect(reason).toBe('允许调用');
|
||||
});
|
||||
|
||||
it('should allow introspect (memory query) initially', () => {
|
||||
const [allowed, reason] = limiter.canCall('introspect');
|
||||
expect(allowed).toBe(true);
|
||||
expect(reason).toBe('允许调用');
|
||||
});
|
||||
|
||||
it('should allow commit (memory update) initially', () => {
|
||||
const [allowed, reason] = limiter.canCall('commit');
|
||||
expect(allowed).toBe(true);
|
||||
expect(reason).toBe('允许调用');
|
||||
});
|
||||
|
||||
it('should allow task operations initially', () => {
|
||||
expect(limiter.canCall('task_create')[0]).toBe(true);
|
||||
expect(limiter.canCall('task_set_state')[0]).toBe(true);
|
||||
expect(limiter.canCall('task_delete')[0]).toBe(true);
|
||||
expect(limiter.canCall('task_link_info')[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow persona operations initially', () => {
|
||||
expect(limiter.canCall('persona_update')[0]).toBe(true);
|
||||
expect(limiter.canCall('persona_clear')[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow purge initially', () => {
|
||||
const [allowed] = limiter.canCall('purge');
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow archive initially', () => {
|
||||
const [allowed] = limiter.canCall('archive');
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow cleanup initially', () => {
|
||||
const [allowed] = limiter.canCall('cleanup');
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canCall blocks when exceeded', () => {
|
||||
let limiter: ToolLimiter;
|
||||
|
||||
beforeEach(() => {
|
||||
limiter = new ToolLimiter();
|
||||
});
|
||||
|
||||
it('should block recall after 20 calls', () => {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
limiter.recordCall('recall');
|
||||
}
|
||||
|
||||
const [allowed, reason] = limiter.canCall('recall');
|
||||
expect(allowed).toBe(false);
|
||||
expect(reason).toContain('一般记忆查询次数已达上限');
|
||||
expect(reason).toContain('20');
|
||||
});
|
||||
|
||||
it('should block commit after 10 calls', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
limiter.recordCall('commit');
|
||||
}
|
||||
|
||||
const [allowed, reason] = limiter.canCall('commit');
|
||||
expect(allowed).toBe(false);
|
||||
expect(reason).toContain('一般记忆修改次数已达上限');
|
||||
});
|
||||
|
||||
it('should block purge after 10 calls', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
limiter.recordCall('purge');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('purge');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should block task operations after 5 updates', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
limiter.recordCall('task_create');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('task_create');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should block task_set_state after 5 updates', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
limiter.recordCall('task_set_state');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('task_set_state');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should block persona_update after 1 call', () => {
|
||||
limiter.recordCall('persona_update');
|
||||
|
||||
const [allowed] = limiter.canCall('persona_update');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should block persona_clear after 1 call', () => {
|
||||
limiter.recordCall('persona_clear');
|
||||
|
||||
const [allowed] = limiter.canCall('persona_clear');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should block unknown actions as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
limiter.recordCall('unknown_action');
|
||||
}
|
||||
|
||||
const [allowed] = limiter.canCall('unknown_action');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordCall increments counters', () => {
|
||||
let limiter: ToolLimiter;
|
||||
|
||||
beforeEach(() => {
|
||||
limiter = new ToolLimiter();
|
||||
});
|
||||
|
||||
it('should increment memory_query for recall', () => {
|
||||
limiter.recordCall('recall');
|
||||
expect(limiter.counts.memory_query).toBe(1);
|
||||
|
||||
limiter.recordCall('recall');
|
||||
expect(limiter.counts.memory_query).toBe(2);
|
||||
});
|
||||
|
||||
it('should increment memory_query for introspect', () => {
|
||||
limiter.recordCall('introspect');
|
||||
expect(limiter.counts.memory_query).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment memory_update for commit', () => {
|
||||
limiter.recordCall('commit');
|
||||
expect(limiter.counts.memory_update).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment memory_update for purge', () => {
|
||||
limiter.recordCall('purge');
|
||||
expect(limiter.counts.memory_update).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment memory_update for archive', () => {
|
||||
limiter.recordCall('archive');
|
||||
expect(limiter.counts.memory_update).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment memory_update for cleanup', () => {
|
||||
limiter.recordCall('cleanup');
|
||||
expect(limiter.counts.memory_update).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment task_update for task operations', () => {
|
||||
limiter.recordCall('task_create');
|
||||
limiter.recordCall('task_set_state');
|
||||
limiter.recordCall('task_delete');
|
||||
limiter.recordCall('task_link_info');
|
||||
|
||||
expect(limiter.counts.task_update).toBe(4);
|
||||
});
|
||||
|
||||
it('should increment persona_update for persona operations', () => {
|
||||
limiter.recordCall('persona_update');
|
||||
limiter.recordCall('persona_clear');
|
||||
|
||||
expect(limiter.counts.persona_update).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle unknown actions as memory update', () => {
|
||||
limiter.recordCall('some_unknown_action');
|
||||
|
||||
expect(limiter.counts.memory_update).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset clears counters', () => {
|
||||
it('should reset all counts to zero', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
limiter.recordCall('recall');
|
||||
limiter.recordCall('recall');
|
||||
limiter.recordCall('commit');
|
||||
limiter.recordCall('task_create');
|
||||
limiter.recordCall('persona_update');
|
||||
|
||||
limiter.reset();
|
||||
|
||||
expect(limiter.counts.persona_query).toBe(0);
|
||||
expect(limiter.counts.persona_update).toBe(0);
|
||||
expect(limiter.counts.task_query).toBe(0);
|
||||
expect(limiter.counts.task_update).toBe(0);
|
||||
expect(limiter.counts.memory_query).toBe(0);
|
||||
expect(limiter.counts.memory_update).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow calls after reset', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
limiter.recordCall('recall');
|
||||
}
|
||||
|
||||
limiter.reset();
|
||||
|
||||
const [allowed] = limiter.canCall('recall');
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSummary returns formatted string', () => {
|
||||
it('should return summary with correct format', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
const summary = limiter.getSummary();
|
||||
|
||||
expect(summary).toContain('人设图');
|
||||
expect(summary).toContain('工作记忆链');
|
||||
expect(summary).toContain('一般记忆');
|
||||
expect(summary).toContain('查询');
|
||||
expect(summary).toContain('修改');
|
||||
expect(summary).toContain('/');
|
||||
});
|
||||
|
||||
it('should show updated counts in summary', () => {
|
||||
const limiter = new ToolLimiter();
|
||||
|
||||
limiter.recordCall('recall');
|
||||
limiter.recordCall('recall');
|
||||
limiter.recordCall('commit');
|
||||
|
||||
const summary = limiter.getSummary();
|
||||
expect(summary).toContain('2/20');
|
||||
expect(summary).toContain('1/10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyTool for all action types', () => {
|
||||
let limiter: ToolLimiter;
|
||||
|
||||
beforeEach(() => {
|
||||
limiter = new ToolLimiter();
|
||||
});
|
||||
|
||||
it('should classify recall as memory query', () => {
|
||||
for (let i = 0; i < 20; i++) limiter.recordCall('recall');
|
||||
const [, reason] = limiter.canCall('recall');
|
||||
expect(reason).toContain('一般记忆查询');
|
||||
});
|
||||
|
||||
it('should classify introspect as memory query', () => {
|
||||
for (let i = 0; i < 20; i++) limiter.recordCall('introspect');
|
||||
const [, reason] = limiter.canCall('introspect');
|
||||
expect(reason).toContain('一般记忆查询');
|
||||
});
|
||||
|
||||
it('should classify commit as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) limiter.recordCall('commit');
|
||||
const [, reason] = limiter.canCall('commit');
|
||||
expect(reason).toContain('一般记忆修改');
|
||||
});
|
||||
|
||||
it('should classify purge as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) limiter.recordCall('purge');
|
||||
const [, reason] = limiter.canCall('purge');
|
||||
expect(reason).toContain('一般记忆修改');
|
||||
});
|
||||
|
||||
it('should classify archive as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) limiter.recordCall('archive');
|
||||
const [, reason] = limiter.canCall('archive');
|
||||
expect(reason).toContain('一般记忆修改');
|
||||
});
|
||||
|
||||
it('should classify cleanup as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) limiter.recordCall('cleanup');
|
||||
const [, reason] = limiter.canCall('cleanup');
|
||||
expect(reason).toContain('一般记忆修改');
|
||||
});
|
||||
|
||||
it('should classify task_create as task update', () => {
|
||||
for (let i = 0; i < 5; i++) limiter.recordCall('task_create');
|
||||
const [, reason] = limiter.canCall('task_create');
|
||||
expect(reason).toContain('工作记忆链修改');
|
||||
});
|
||||
|
||||
it('should classify task_set_state as task update', () => {
|
||||
for (let i = 0; i < 5; i++) limiter.recordCall('task_set_state');
|
||||
const [, reason] = limiter.canCall('task_set_state');
|
||||
expect(reason).toContain('工作记忆链修改');
|
||||
});
|
||||
|
||||
it('should classify task_delete as task update', () => {
|
||||
for (let i = 0; i < 5; i++) limiter.recordCall('task_delete');
|
||||
const [, reason] = limiter.canCall('task_delete');
|
||||
expect(reason).toContain('工作记忆链修改');
|
||||
});
|
||||
|
||||
it('should classify task_link_info as task update', () => {
|
||||
for (let i = 0; i < 5; i++) limiter.recordCall('task_link_info');
|
||||
const [, reason] = limiter.canCall('task_link_info');
|
||||
expect(reason).toContain('工作记忆链修改');
|
||||
});
|
||||
|
||||
it('should classify persona_update as persona update', () => {
|
||||
limiter.recordCall('persona_update');
|
||||
const [, reason] = limiter.canCall('persona_update');
|
||||
expect(reason).toContain('人设图修改');
|
||||
});
|
||||
|
||||
it('should classify persona_clear as persona update', () => {
|
||||
limiter.recordCall('persona_clear');
|
||||
const [, reason] = limiter.canCall('persona_clear');
|
||||
expect(reason).toContain('人设图修改');
|
||||
});
|
||||
|
||||
it('should classify unknown action as memory update', () => {
|
||||
for (let i = 0; i < 10; i++) limiter.recordCall('totally_unknown');
|
||||
const [, reason] = limiter.canCall('totally_unknown');
|
||||
expect(reason).toContain('一般记忆修改');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom limits override', () => {
|
||||
it('should accept custom limits', () => {
|
||||
const limiter = new ToolLimiter({
|
||||
memory_query_max: 100,
|
||||
memory_update_max: 50
|
||||
});
|
||||
|
||||
expect(limiter.limits.memory_query_max).toBe(100);
|
||||
expect(limiter.limits.memory_update_max).toBe(50);
|
||||
expect(limiter.limits.task_query_max).toBe(4);
|
||||
});
|
||||
|
||||
it('should merge custom limits with defaults', () => {
|
||||
const limiter = new ToolLimiter({
|
||||
persona_update_max: 5
|
||||
});
|
||||
|
||||
expect(limiter.limits.persona_update_max).toBe(5);
|
||||
expect(limiter.limits.persona_query_max).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow unlimited calls with high custom limits', () => {
|
||||
const limiter = new ToolLimiter({
|
||||
memory_query_max: 10000,
|
||||
memory_update_max: 10000
|
||||
});
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
limiter.recordCall('recall');
|
||||
limiter.recordCall('commit');
|
||||
}
|
||||
|
||||
expect(limiter.canCall('recall')[0]).toBe(true);
|
||||
expect(limiter.canCall('commit')[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow zero limits to block immediately', () => {
|
||||
const limiter = new ToolLimiter({
|
||||
task_update_max: 0
|
||||
});
|
||||
|
||||
const [allowed] = limiter.canCall('task_create');
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve custom limits after reset', () => {
|
||||
const limiter = new ToolLimiter({
|
||||
memory_query_max: 500
|
||||
});
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
limiter.recordCall('recall');
|
||||
}
|
||||
|
||||
limiter.reset();
|
||||
|
||||
expect(limiter.limits.memory_query_max).toBe(500);
|
||||
expect(limiter.counts.memory_query).toBe(0);
|
||||
|
||||
const [allowed] = limiter.canCall('recall');
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
188
ts/tests/semantic_search.test.ts
Normal file
188
ts/tests/semantic_search.test.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
vi.mock('@xenova/transformers', () => ({
|
||||
pipeline: vi.fn().mockImplementation(() => {
|
||||
const mockModel = (text: string, opts: any) => {
|
||||
const arr = new Float32Array(384);
|
||||
const seed = text.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
||||
for (let i = 0; i < 384; i++) {
|
||||
arr[i] = Math.sin((seed + i) * 0.1);
|
||||
}
|
||||
return { data: arr };
|
||||
};
|
||||
mockModel.to = function() {
|
||||
return this;
|
||||
};
|
||||
return mockModel;
|
||||
}),
|
||||
}));
|
||||
|
||||
import { SemanticSearchEngine, cosineSimilarity } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/semantic_search.js';
|
||||
|
||||
const TEST_DB_PATH = '/tmp/test_semantic_search.db';
|
||||
|
||||
describe('SemanticSearchEngine', () => {
|
||||
let db: Database.Database;
|
||||
let engine: SemanticSearchEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
if (fs.existsSync(TEST_DB_PATH)) {
|
||||
fs.unlinkSync(TEST_DB_PATH);
|
||||
}
|
||||
if (fs.existsSync(`${TEST_DB_PATH}-wal`)) {
|
||||
fs.unlinkSync(`${TEST_DB_PATH}-wal`);
|
||||
}
|
||||
if (fs.existsSync(`${TEST_DB_PATH}-shm`)) {
|
||||
fs.unlinkSync(`${TEST_DB_PATH}-shm`);
|
||||
}
|
||||
} catch {}
|
||||
db = new Database(TEST_DB_PATH);
|
||||
engine = new SemanticSearchEngine(db);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('creates embeddings table on initialization', () => {
|
||||
const tableInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
|
||||
expect(tableInfo).toBeDefined();
|
||||
});
|
||||
|
||||
it('creates index on source column', () => {
|
||||
const indexInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_embeddings_source'").get();
|
||||
expect(indexInfo).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateEmbedding', () => {
|
||||
it('generates 384 dimensional embedding', async () => {
|
||||
const embedding = await engine.generateEmbedding('hello world');
|
||||
expect(embedding.length).toBe(384);
|
||||
});
|
||||
|
||||
it('generates consistent embeddings for same text', async () => {
|
||||
const emb1 = await engine.generateEmbedding('test');
|
||||
const emb2 = await engine.generateEmbedding('test');
|
||||
expect(emb1.length).toBe(emb2.length);
|
||||
const similarity = cosineSimilarity(emb1, emb2);
|
||||
expect(similarity).toBeCloseTo(1, 3);
|
||||
});
|
||||
|
||||
it('generates different embeddings for different text', async () => {
|
||||
const emb1 = await engine.generateEmbedding('hello');
|
||||
const emb2 = await engine.generateEmbedding('world');
|
||||
const similarity = cosineSimilarity(emb1, emb2);
|
||||
expect(similarity).not.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeEmbedding', () => {
|
||||
it('stores embedding to SQLite', async () => {
|
||||
const embedding = await engine.generateEmbedding('test');
|
||||
engine.storeEmbedding('test-id', 'test text', embedding, 'test-source', 1);
|
||||
|
||||
const row = db.prepare('SELECT * FROM embeddings WHERE id = ?').get('test-id') as any;
|
||||
expect(row).toBeDefined();
|
||||
expect(row.text).toBe('test text');
|
||||
expect(row.source).toBe('test-source');
|
||||
expect(row.source_line).toBe(1);
|
||||
});
|
||||
|
||||
it('replaces existing embedding with same id', async () => {
|
||||
const emb1 = await engine.generateEmbedding('text1');
|
||||
engine.storeEmbedding('dup-id', 'text 1', emb1);
|
||||
|
||||
const emb2 = await engine.generateEmbedding('text2');
|
||||
engine.storeEmbedding('dup-id', 'text 2', emb2);
|
||||
|
||||
const row = db.prepare('SELECT text FROM embeddings WHERE id = ?').get('dup-id') as any;
|
||||
expect(row.text).toBe('text 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchSimilar', () => {
|
||||
beforeEach(async () => {
|
||||
const emb1 = await engine.generateEmbedding('machine learning');
|
||||
const emb2 = await engine.generateEmbedding('deep learning');
|
||||
const emb3 = await engine.generateEmbedding('hello world');
|
||||
|
||||
engine.storeEmbedding('item-1', 'machine learning', emb1, 'source1', 1);
|
||||
engine.storeEmbedding('item-2', 'deep learning neural network', emb2, 'source2', 2);
|
||||
engine.storeEmbedding('item-3', 'hello world', emb3, 'source3', 3);
|
||||
});
|
||||
|
||||
it('finds similar embeddings using cosine similarity', async () => {
|
||||
const query = await engine.generateEmbedding('neural networks');
|
||||
const results = engine.searchSimilar(query, 10);
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].id).toBeDefined();
|
||||
expect(typeof results[0].similarity).toBe('number');
|
||||
});
|
||||
|
||||
it('returns results sorted by similarity descending', async () => {
|
||||
const query = await engine.generateEmbedding('training');
|
||||
const results = engine.searchSimilar(query, 10);
|
||||
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
expect(results[i - 1].similarity).toBeGreaterThanOrEqual(results[i].similarity);
|
||||
}
|
||||
});
|
||||
|
||||
it('respects limit parameter', async () => {
|
||||
const query = await engine.generateEmbedding('test query');
|
||||
const results = engine.searchSimilar(query, 2);
|
||||
|
||||
expect(results.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('includes source and sourceLine in results', async () => {
|
||||
const query = await engine.generateEmbedding('test');
|
||||
const results = engine.searchSimilar(query, 1);
|
||||
|
||||
expect(results[0].source).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cosineSimilarity', () => {
|
||||
it('returns 1 for identical vectors', () => {
|
||||
const a = new Float32Array([1, 0, 0]);
|
||||
const b = new Float32Array([1, 0, 0]);
|
||||
expect(cosineSimilarity(a, b)).toBeCloseTo(1);
|
||||
});
|
||||
|
||||
it('returns -1 for opposite vectors', () => {
|
||||
const a = new Float32Array([1, 0, 0]);
|
||||
const b = new Float32Array([-1, 0, 0]);
|
||||
expect(cosineSimilarity(a, b)).toBeCloseTo(-1);
|
||||
});
|
||||
|
||||
it('returns 0 for orthogonal vectors', () => {
|
||||
const a = new Float32Array([1, 0, 0]);
|
||||
const b = new Float32Array([0, 1, 0]);
|
||||
expect(cosineSimilarity(a, b)).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it('returns value between -1 and 1 for random vectors', () => {
|
||||
const a = new Float32Array([0.5, 0.3, 0.7]);
|
||||
const b = new Float32Array([0.2, 0.8, 0.1]);
|
||||
const similarity = cosineSimilarity(a, b);
|
||||
expect(similarity).toBeGreaterThanOrEqual(-1);
|
||||
expect(similarity).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('handles 384 dimensional vectors', () => {
|
||||
const a = new Float32Array(384).fill(0.1);
|
||||
const b = new Float32Array(384).fill(0.1);
|
||||
const similarity = cosineSimilarity(a, b);
|
||||
expect(similarity).toBeCloseTo(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
ts/tsconfig.json
Normal file
24
ts/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
from .app import GraphMemoryApp
|
||||
from .models.config import AppConfig
|
||||
|
||||
__all__ = ["GraphMemoryApp", "AppConfig"]
|
||||
275
ui/app.py
275
ui/app.py
@ -1,275 +0,0 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
|
||||
from core import BackendServer, BackendClient
|
||||
from .models.message import Message
|
||||
|
||||
|
||||
class GraphMemoryApp(App):
|
||||
CSS_PATH = [
|
||||
Path(__file__).parent / "styles" / "app.css",
|
||||
Path(__file__).parent / "styles" / "messages.css",
|
||||
Path(__file__).parent / "styles" / "components.css",
|
||||
]
|
||||
|
||||
BINDINGS = [
|
||||
Binding("f1", "show_help", "帮助"),
|
||||
Binding("f2", "toggle_sidebar", "侧边栏"),
|
||||
Binding("f3", "toggle_tool_details", "工具详情"),
|
||||
Binding("f5", "clear_history", "清屏"),
|
||||
Binding("f6", "quit", "退出"),
|
||||
]
|
||||
|
||||
def __init__(self, backend_server: BackendServer = None, config_file: str = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._backend_server = backend_server
|
||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||
self._api_configured = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
from .widgets.left_panel import LeftPanel
|
||||
from .widgets.right_panel import RightPanel
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .models.config import AppConfig
|
||||
|
||||
initial_config = AppConfig()
|
||||
|
||||
if self._backend_client:
|
||||
settings_result = self._backend_client.get_settings()
|
||||
settings_data = settings_result.get("data", {})
|
||||
|
||||
api_config = settings_data.get("api_config", {})
|
||||
initial_config.api_key = api_config.get("api_key", "")
|
||||
initial_config.base_url = api_config.get("base_url", "https://api.deepseek.com")
|
||||
initial_config.model = api_config.get("model", "deepseek-chat")
|
||||
|
||||
tool_limits = settings_data.get("tool_limits", {})
|
||||
initial_config.persona_query_max = tool_limits.get("persona_query_max", 1)
|
||||
initial_config.persona_update_max = tool_limits.get("persona_update_max", 1)
|
||||
initial_config.task_query_max = tool_limits.get("task_query_max", 4)
|
||||
initial_config.task_update_max = tool_limits.get("task_update_max", 2)
|
||||
initial_config.memory_query_max = tool_limits.get("memory_query_max", 20)
|
||||
initial_config.memory_update_max = tool_limits.get("memory_update_max", 10)
|
||||
|
||||
yield LeftPanel()
|
||||
yield RightPanel(config=initial_config)
|
||||
yield StatusBar()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .widgets.message_history import MessageHistory
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
if not self._backend_server:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
error = Message(role="assistant", content="后端未初始化")
|
||||
history.add_message(error)
|
||||
status_bar.set_api_status(False)
|
||||
return
|
||||
|
||||
status = self._backend_client.get_status()
|
||||
data = status.get("data", {})
|
||||
self._api_configured = data.get("config", {}).get("api_key", "") != ""
|
||||
status_bar.set_api_status(self._api_configured)
|
||||
|
||||
history = self.query_one(MessageHistory)
|
||||
|
||||
if self._api_configured:
|
||||
chat_history = self._backend_client.get_history()
|
||||
if chat_history:
|
||||
for msg in chat_history:
|
||||
message = Message(role=msg["role"], content=msg["content"])
|
||||
history.add_message(message)
|
||||
|
||||
welcome = Message(
|
||||
role="assistant",
|
||||
content=f"系统就绪\nAPI Key: {'已配置' if self._api_configured else '未配置'}\n\n输入消息开始对话"
|
||||
)
|
||||
history.add_message(welcome)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
if self._backend_client:
|
||||
self._backend_client.shutdown()
|
||||
|
||||
def action_show_help(self) -> None:
|
||||
from pathlib import Path
|
||||
config_path = Path.home() / ".trulymem" / "config.json"
|
||||
db_path = Path.home() / ".trulymem" / "graph_memory.db"
|
||||
|
||||
help_text = (
|
||||
"F1-帮助 F2-侧边栏 F3-工具详情 F5-清屏 F6-退出\n\n"
|
||||
f"配置文件: {config_path}\n"
|
||||
f"数据库: {db_path}"
|
||||
)
|
||||
self.notify(help_text, title="快捷键 & 配置路径", timeout=15)
|
||||
|
||||
def action_toggle_sidebar(self) -> None:
|
||||
from .widgets.right_panel import RightPanel
|
||||
sidebar = self.query_one(RightPanel)
|
||||
sidebar.toggle()
|
||||
|
||||
def action_toggle_tool_details(self) -> None:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.toggle_latest_tool_details()
|
||||
|
||||
def action_clear_history(self) -> None:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.clear_messages()
|
||||
|
||||
def on_input_box_send_message(self, event) -> None:
|
||||
if not self._backend_client:
|
||||
self.notify("后端未初始化", title="错误", severity="error")
|
||||
return
|
||||
|
||||
if not self._api_configured:
|
||||
self.notify("请先配置 API Key (按 F2 打开侧边栏)", title="提示", severity="warning")
|
||||
return
|
||||
|
||||
user_input = event.content
|
||||
from .widgets.message_history import MessageHistory
|
||||
from .widgets.status_bar import StatusBar
|
||||
|
||||
history = self.query_one(MessageHistory)
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
history.add_message(Message(role="user", content=user_input))
|
||||
history.add_message(Message(role="assistant", content="⏳ 正在处理..."))
|
||||
status_bar.set_processing(True)
|
||||
|
||||
asyncio.create_task(self._process(user_input))
|
||||
|
||||
def on_input_box_clear_history(self, event) -> None:
|
||||
"""处理清空聊天记录事件"""
|
||||
if not self._backend_client:
|
||||
self.notify("后端未初始化", title="错误", severity="error")
|
||||
return
|
||||
|
||||
self._backend_client.clear_history()
|
||||
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.clear_messages()
|
||||
|
||||
self.notify("聊天记录已清空,AI记忆保持不变", title="提示", severity="information")
|
||||
|
||||
async def _process(self, user_input: str) -> None:
|
||||
from .widgets.message_history import MessageHistory
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .widgets.right_panel import RightPanel
|
||||
from .models.log_entry import LogEntry
|
||||
from datetime import datetime
|
||||
|
||||
history = self.query_one(MessageHistory)
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.process_message(user_input)
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
# 响应结构: {"success": True, "data": {"content": "...", "tool_calls": [...], ...}, "error": None}
|
||||
data = result.get("data", {})
|
||||
content = data.get("content", "(无回复)")
|
||||
history.update_latest_message(content)
|
||||
|
||||
# 处理工具调用信息,更新操作日志
|
||||
tool_calls = data.get("tool_calls", [])
|
||||
if tool_calls:
|
||||
try:
|
||||
right_panel = self.query_one(RightPanel)
|
||||
operation_log = right_panel.get_operation_log()
|
||||
|
||||
for tool_call in tool_calls:
|
||||
entry = LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
tool_name=tool_call.get("name", "unknown"),
|
||||
arguments=tool_call.get("arguments", {}),
|
||||
result=str(tool_call.get("result", "")),
|
||||
duration=0.0 # 后端没有返回耗时信息
|
||||
)
|
||||
operation_log.add_log(entry)
|
||||
except Exception:
|
||||
pass # 忽略操作日志更新失败
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
history.update_latest_message(f"❌ 错误: {error}")
|
||||
|
||||
status_bar.set_processing(False)
|
||||
|
||||
def on_config_section_config_changed(self, event) -> None:
|
||||
if not self._backend_client:
|
||||
self.notify("后端未初始化,无法保存配置", title="错误", severity="error")
|
||||
return
|
||||
|
||||
asyncio.create_task(self._update_settings_async(event.config))
|
||||
|
||||
async def _update_settings_async(self, config) -> None:
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .widgets.config_section import ConfigSection
|
||||
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
api_config = {
|
||||
"api_key": config.api_key,
|
||||
"base_url": config.base_url,
|
||||
"model": getattr(config, 'model', 'deepseek-chat')
|
||||
}
|
||||
|
||||
tool_limits = {
|
||||
"persona_query_max": config.persona_query_max,
|
||||
"persona_update_max": config.persona_update_max,
|
||||
"task_query_max": config.task_query_max,
|
||||
"task_update_max": config.task_update_max,
|
||||
"memory_query_max": config.memory_query_max,
|
||||
"memory_update_max": config.memory_update_max,
|
||||
}
|
||||
|
||||
try:
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.update_settings(
|
||||
api_config=api_config,
|
||||
tool_limits=tool_limits
|
||||
)
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
self._api_configured = bool(config.api_key)
|
||||
status_bar.set_api_status(self._api_configured)
|
||||
|
||||
settings_result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.get_settings()
|
||||
)
|
||||
settings_data = settings_result.get("data", {})
|
||||
|
||||
try:
|
||||
config_section = self.query_one(ConfigSection)
|
||||
api_cfg = settings_data.get("api_config", {})
|
||||
tool_lmts = settings_data.get("tool_limits", {})
|
||||
config_section.set_config(AppConfig(
|
||||
api_key=api_cfg.get("api_key", ""),
|
||||
base_url=api_cfg.get("base_url", "https://api.deepseek.com"),
|
||||
model=api_cfg.get("model", "deepseek-chat"),
|
||||
persona_query_max=tool_lmts.get("persona_query_max", 1),
|
||||
persona_update_max=tool_lmts.get("persona_update_max", 1),
|
||||
task_query_max=tool_lmts.get("task_query_max", 4),
|
||||
task_update_max=tool_lmts.get("task_update_max", 2),
|
||||
memory_query_max=tool_lmts.get("memory_query_max", 20),
|
||||
memory_update_max=tool_lmts.get("memory_update_max", 10),
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.notify("✅ 配置已保存并生效", title="配置成功", severity="information")
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
self.notify(f"❌ 配置失败: {error}", title="配置失败", severity="error")
|
||||
except Exception as e:
|
||||
self.notify(f"❌ 配置异常: {str(e)}", title="配置失败", severity="error")
|
||||
@ -1 +0,0 @@
|
||||
"""Event Handlers for Graph Memory TUI"""
|
||||
@ -1,64 +0,0 @@
|
||||
"""焦点管理器"""
|
||||
|
||||
from textual.app import App
|
||||
|
||||
|
||||
class FocusHandler:
|
||||
"""焦点管理器"""
|
||||
|
||||
# 焦点循环顺序
|
||||
FOCUS_RING = [
|
||||
"input-textarea", # 左侧输入框
|
||||
"api-key-input", # 右侧配置区 API Key
|
||||
"model-input", # 右侧配置区 Model
|
||||
"base-url-input", # 右侧配置区 Base URL
|
||||
"cypher-textarea", # 右侧 Cypher 查询框
|
||||
]
|
||||
|
||||
# 焦点名称映射
|
||||
FOCUS_NAMES = {
|
||||
"input-textarea": "Input",
|
||||
"api-key-input": "Config-API",
|
||||
"model-input": "Config-Model",
|
||||
"base-url-input": "Config-URL",
|
||||
"cypher-textarea": "Query",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._current_index = 0
|
||||
|
||||
def next_focus(self, app: App) -> None:
|
||||
"""切换到下一个焦点"""
|
||||
self._current_index = (self._current_index + 1) % len(self.FOCUS_RING)
|
||||
widget_id = self.FOCUS_RING[self._current_index]
|
||||
self._focus_widget(app, widget_id)
|
||||
|
||||
def prev_focus(self, app: App) -> None:
|
||||
"""切换到上一个焦点"""
|
||||
self._current_index = (self._current_index - 1) % len(self.FOCUS_RING)
|
||||
widget_id = self.FOCUS_RING[self._current_index]
|
||||
self._focus_widget(app, widget_id)
|
||||
|
||||
def focus_input(self, app: App) -> None:
|
||||
"""聚焦到输入框"""
|
||||
self._current_index = 0
|
||||
self._focus_widget(app, self.FOCUS_RING[0])
|
||||
|
||||
def focus_query(self, app: App) -> None:
|
||||
"""聚焦到查询框"""
|
||||
self._current_index = len(self.FOCUS_RING) - 1
|
||||
self._focus_widget(app, self.FOCUS_RING[-1])
|
||||
|
||||
def get_current_focus_name(self) -> str:
|
||||
"""获取当前焦点名称"""
|
||||
widget_id = self.FOCUS_RING[self._current_index]
|
||||
return self.FOCUS_NAMES.get(widget_id, "Unknown")
|
||||
|
||||
def _focus_widget(self, app: App, widget_id: str) -> None:
|
||||
"""聚焦到指定组件"""
|
||||
try:
|
||||
widget = app.query_one(f"#{widget_id}")
|
||||
widget.focus()
|
||||
except Exception:
|
||||
# 如果找不到组件,回退到输入框
|
||||
self.focus_input(app)
|
||||
@ -1,68 +0,0 @@
|
||||
"""快捷键处理器"""
|
||||
|
||||
from textual.app import App
|
||||
from textual.message import Message
|
||||
from .focus_handler import FocusHandler
|
||||
|
||||
|
||||
class KeyHandler:
|
||||
"""快捷键处理器"""
|
||||
|
||||
class ShowHelp(Message):
|
||||
"""显示帮助事件"""
|
||||
pass
|
||||
|
||||
class ToggleSidebar(Message):
|
||||
"""切换侧边栏事件"""
|
||||
pass
|
||||
|
||||
class ToggleToolDetails(Message):
|
||||
"""切换工具详情事件"""
|
||||
pass
|
||||
|
||||
class FocusQuery(Message):
|
||||
"""聚焦查询框事件"""
|
||||
pass
|
||||
|
||||
class ClearHistory(Message):
|
||||
"""清屏事件"""
|
||||
pass
|
||||
|
||||
class QuitApp(Message):
|
||||
"""退出应用事件"""
|
||||
pass
|
||||
|
||||
def __init__(self, focus_handler: FocusHandler):
|
||||
self._focus_handler = focus_handler
|
||||
|
||||
def handle_f1(self, app: App) -> None:
|
||||
"""处理 F1 键 - 显示帮助"""
|
||||
app.post_message(self.ShowHelp())
|
||||
|
||||
def handle_f2(self, app: App) -> None:
|
||||
"""处理 F2 键 - 切换侧边栏"""
|
||||
app.post_message(self.ToggleSidebar())
|
||||
|
||||
def handle_f3(self, app: App) -> None:
|
||||
"""处理 F3 键 - 切换工具详情"""
|
||||
app.post_message(self.ToggleToolDetails())
|
||||
|
||||
def handle_f4(self, app: App) -> None:
|
||||
"""处理 F4 键 - 聚焦查询框"""
|
||||
app.post_message(self.FocusQuery())
|
||||
|
||||
def handle_f5(self, app: App) -> None:
|
||||
"""处理 F5 键 - 清屏"""
|
||||
app.post_message(self.ClearHistory())
|
||||
|
||||
def handle_f6(self, app: App) -> None:
|
||||
"""处理 F6 键 - 退出"""
|
||||
app.post_message(self.QuitApp())
|
||||
|
||||
def handle_tab(self, app: App) -> None:
|
||||
"""处理 Tab 键 - 焦点循环"""
|
||||
self._focus_handler.next_focus(app)
|
||||
|
||||
def handle_shift_tab(self, app: App) -> None:
|
||||
"""处理 Shift+Tab 键 - 反向焦点循环"""
|
||||
self._focus_handler.prev_focus(app)
|
||||
@ -1 +0,0 @@
|
||||
"""Data Models for Graph Memory TUI"""
|
||||
@ -1,61 +0,0 @@
|
||||
"""配置数据模型"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""应用配置"""
|
||||
api_key: str = ""
|
||||
model: str = "deepseek-chat"
|
||||
base_url: str = "https://api.deepseek.com"
|
||||
persona_query_max: int = 1
|
||||
persona_update_max: int = 1
|
||||
task_query_max: int = 4
|
||||
task_update_max: int = 2
|
||||
memory_query_max: int = 20
|
||||
memory_update_max: int = 10
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "AppConfig":
|
||||
return cls(
|
||||
api_key=os.getenv("DEEPSEEK_API_KEY", ""),
|
||||
model=os.getenv("MODEL_NAME", "deepseek-chat"),
|
||||
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
||||
persona_query_max=int(os.getenv("PERSONA_QUERY_MAX", 1)),
|
||||
persona_update_max=int(os.getenv("PERSONA_UPDATE_MAX", 1)),
|
||||
task_query_max=int(os.getenv("TASK_QUERY_MAX", 4)),
|
||||
task_update_max=int(os.getenv("TASK_UPDATE_MAX", 2)),
|
||||
memory_query_max=int(os.getenv("MEMORY_QUERY_MAX", 20)),
|
||||
memory_update_max=int(os.getenv("MEMORY_UPDATE_MAX", 10)),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "AppConfig":
|
||||
if not path.exists():
|
||||
return cls()
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
return cls(
|
||||
api_key=data.get("api_key", ""),
|
||||
model=data.get("model", "deepseek-chat"),
|
||||
base_url=data.get("base_url", "https://api.deepseek.com"),
|
||||
persona_query_max=data.get("persona_query_max", 1),
|
||||
persona_update_max=data.get("persona_update_max", 1),
|
||||
task_query_max=data.get("task_query_max", 4),
|
||||
task_update_max=data.get("task_update_max", 2),
|
||||
memory_query_max=data.get("memory_query_max", 20),
|
||||
memory_update_max=data.get("memory_update_max", 10),
|
||||
)
|
||||
|
||||
def save(self, path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(asdict(self), f, indent=2, ensure_ascii=False)
|
||||
@ -1,30 +0,0 @@
|
||||
"""日志条目数据模型"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class LogEntry:
|
||||
"""日志条目"""
|
||||
timestamp: datetime
|
||||
tool_name: str
|
||||
arguments: Dict[str, Any]
|
||||
result: str
|
||||
duration: float
|
||||
|
||||
@property
|
||||
def args_summary(self) -> str:
|
||||
"""参数摘要(截断到50字符)"""
|
||||
args_str = str(self.arguments)
|
||||
if len(args_str) > 50:
|
||||
return args_str[:50] + "..."
|
||||
return args_str
|
||||
|
||||
@property
|
||||
def result_summary(self) -> str:
|
||||
"""结果摘要(截断到100字符)"""
|
||||
if len(self.result) > 100:
|
||||
return self.result[:100] + "..."
|
||||
return self.result
|
||||
@ -1,33 +0,0 @@
|
||||
"""消息数据模型"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""工具调用"""
|
||||
id: str
|
||||
name: str
|
||||
arguments: Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
"""工具执行结果"""
|
||||
tool_call_id: str
|
||||
name: str
|
||||
arguments: Dict[str, Any]
|
||||
result: str
|
||||
success: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
"""消息"""
|
||||
role: Literal["user", "assistant", "system"]
|
||||
content: str
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
tool_calls: Optional[List[ToolCall]] = None
|
||||
tool_results: Optional[List[ToolResult]] = None
|
||||
@ -1 +0,0 @@
|
||||
"""Business Services for Graph Memory TUI"""
|
||||
@ -1,46 +0,0 @@
|
||||
"""
|
||||
配置管理 - 支持持久化
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from ..models.config import AppConfig
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器 - 支持持久化"""
|
||||
|
||||
def __init__(self, config_file: str = "config.json"):
|
||||
self.config_file = Path(config_file)
|
||||
|
||||
def save(self, config: AppConfig) -> None:
|
||||
"""保存配置到文件"""
|
||||
data = {
|
||||
"api_key": config.api_key,
|
||||
"model": config.model,
|
||||
"base_url": config.base_url
|
||||
}
|
||||
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def load(self) -> AppConfig:
|
||||
"""从文件加载配置"""
|
||||
if not self.config_file.exists():
|
||||
return AppConfig()
|
||||
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return AppConfig(
|
||||
api_key=data.get("api_key", ""),
|
||||
model=data.get("model", "deepseek-chat"),
|
||||
base_url=data.get("base_url", "https://api.deepseek.com")
|
||||
)
|
||||
except Exception:
|
||||
return AppConfig()
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""检查配置文件是否存在"""
|
||||
return self.config_file.exists()
|
||||
@ -1,51 +0,0 @@
|
||||
"""配置服务"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from ..models.config import AppConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core.imports import GraphMemoryClient
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""配置服务"""
|
||||
|
||||
DEFAULT_CONFIG_FILE = Path.home() / ".graph_memory_tui" / "config.json"
|
||||
|
||||
def __init__(self, config_file: Path | None = None):
|
||||
self._config_file = config_file or self.DEFAULT_CONFIG_FILE
|
||||
self._config = self._load_config()
|
||||
|
||||
def _load_config(self) -> AppConfig:
|
||||
"""加载配置"""
|
||||
# 优先从文件加载
|
||||
if self._config_file.exists():
|
||||
return AppConfig.from_file(self._config_file)
|
||||
|
||||
# 否则从环境变量加载
|
||||
return AppConfig.from_env()
|
||||
|
||||
def get_config(self) -> AppConfig:
|
||||
"""获取当前配置"""
|
||||
return self._config
|
||||
|
||||
def set_config(self, config: AppConfig) -> None:
|
||||
"""设置配置"""
|
||||
self._config = config
|
||||
self._save_config()
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""保存配置"""
|
||||
self._config.save(self._config_file)
|
||||
|
||||
def apply_to_client(self, client: "GraphMemoryClient") -> None:
|
||||
"""应用配置到 API 客户端"""
|
||||
# 更新客户端配置
|
||||
client.api_key = self._config.api_key
|
||||
client.base_url = self._config.base_url
|
||||
client.model = self._config.model
|
||||
|
||||
def get_config_file(self) -> Path:
|
||||
"""获取配置文件路径"""
|
||||
return self._config_file
|
||||
@ -1 +0,0 @@
|
||||
"""Styles for Graph Memory TUI"""
|
||||
@ -1,40 +0,0 @@
|
||||
/* Global Styles for Graph Memory TUI */
|
||||
|
||||
GraphMemoryApp {
|
||||
background: $surface;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
/* 全局Input样式 - 确保可见 */
|
||||
Input {
|
||||
background: $surface-lighten-1;
|
||||
color: $text;
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
Input:focus {
|
||||
border: double $accent;
|
||||
}
|
||||
|
||||
LeftPanel {
|
||||
width: 1fr;
|
||||
dock: left;
|
||||
}
|
||||
|
||||
RightPanel {
|
||||
width: 35;
|
||||
dock: right;
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
RightPanel ScrollableContainer {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
StatusBar {
|
||||
dock: bottom;
|
||||
height: 1;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
}
|
||||
@ -1,139 +0,0 @@
|
||||
/* Component Styles for Graph Memory TUI */
|
||||
|
||||
/* Input Box - 最重要 */
|
||||
InputBox {
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
InputBox TextArea {
|
||||
width: 100%;
|
||||
height: 5;
|
||||
background: $surface-lighten-1;
|
||||
color: $text;
|
||||
border: none;
|
||||
}
|
||||
|
||||
InputBox .input-buttons {
|
||||
height: auto;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
InputBox Button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Config Section */
|
||||
ConfigSection {
|
||||
background: $surface;
|
||||
padding: 1;
|
||||
margin: 0 0 1 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
ConfigSection .config-title {
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
ConfigSection .config-label {
|
||||
color: $text;
|
||||
margin: 0;
|
||||
padding: 1 0 0 0;
|
||||
}
|
||||
|
||||
ConfigSection .config-hint {
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
margin: 1 0 0 0;
|
||||
}
|
||||
|
||||
ConfigSection Input {
|
||||
width: 1fr;
|
||||
height: 3;
|
||||
margin: 0 0 1 0;
|
||||
padding: 0 1;
|
||||
background: $surface-lighten-1;
|
||||
border: solid $primary;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
/* Other Components */
|
||||
OperationLog {
|
||||
background: $surface-darken-1;
|
||||
height: 1fr;
|
||||
margin: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
OperationLog .log-entry {
|
||||
color: $text;
|
||||
margin: 0 0 1 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
OperationLog .log-empty {
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
}
|
||||
|
||||
CypherQueryBox {
|
||||
border: solid green;
|
||||
margin: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
MessageHistory {
|
||||
height: 1fr;
|
||||
margin: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Message Widget */
|
||||
MessageWidget {
|
||||
margin: 1 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
MessageWidget .message-header {
|
||||
color: $text-muted;
|
||||
text-style: bold;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
MessageWidget .message-content {
|
||||
color: $text;
|
||||
margin: 0 0 0 2;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
MessageWidget .tool-indicator {
|
||||
color: $warning;
|
||||
text-style: bold;
|
||||
margin: 1 0 0 2;
|
||||
}
|
||||
|
||||
MessageWidget .tool-details {
|
||||
background: $surface-darken-1;
|
||||
margin: 1 0 0 2;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
MessageWidget .tool-name {
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
MessageWidget .tool-args {
|
||||
color: $text-muted;
|
||||
margin: 0 0 0 2;
|
||||
}
|
||||
|
||||
MessageWidget .tool-result {
|
||||
color: $success;
|
||||
margin: 0 0 0 2;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user