Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7a3ecd443 | |||
| d42b250c17 | |||
| 5c3a148a03 | |||
| 3bedc6789a | |||
| 87b1742495 | |||
| 8214f31b5b | |||
| b374179094 | |||
| 30502cf2cf | |||
| c9be0d07bc | |||
| 6cdafddd8e | |||
| 5dd77abb0f | |||
| c554c07bfc | |||
| 8739e9ad26 | |||
| 3839a60459 | |||
| 807242dd78 | |||
| ea4e43bf97 | |||
| 90df082a92 | |||
| 2de699e2aa | |||
| 1f19bb0c57 | |||
| 69a79ac86d | |||
| 9f470cb97d | |||
| da02a729a3 | |||
| aa9b25c829 | |||
| 17978cc71f | |||
| 8802604568 | |||
| f585164cc1 | |||
| c046e6ff6a | |||
| 6749811e49 | |||
| 0f9001beef | |||
| 3a13753d36 | |||
| fa458dcb0d | |||
| 9bc7ac7560 | |||
| 20319f48d5 | |||
| a930648c95 | |||
| 2d2c87d505 | |||
| 159d22510b | |||
| cbc7b13dbc | |||
| cc5ea69eda | |||
| ecb74cebff | |||
| dd00ddaa8b | |||
| 5e1aba3c60 | |||
| 1b5a3adf3e | |||
| 64b31bbf4f | |||
| df156ba840 | |||
| 4fc8ca944b | |||
| 006f3c413f | |||
| a1654db365 | |||
| 2cc4d8b907 | |||
| 5f7c95e861 | |||
| a794c33545 | |||
| 6d49061539 | |||
| a5e71da88c | |||
| d4267e1a8d | |||
| bb426cdb45 | |||
| bc999b18f1 | |||
| 529672a93a | |||
| 00fd75a0a6 | |||
| eab18889ff | |||
| d971afb934 | |||
| 1a394ddd2b | |||
| 12ab1df7f5 | |||
| c1dc3f647b | |||
| 4901d3882f | |||
| 3de418b7e8 | |||
| 039dc41c0b | |||
| 39eee19a3e | |||
| 1fa548da3c | |||
| 7543fcae72 | |||
| b9e4bb164d | |||
| 49d6906f15 | |||
| d228d119f5 | |||
| c72fc5f38f | |||
| 0ccd02b0af | |||
| bddb5498e9 | |||
| 598e03e2eb | |||
| 8b34725872 | |||
| 4d64da743a | |||
| cc2241cac8 | |||
| e7ddaee872 | |||
| 84f217ef55 | |||
| 58dc93f061 | |||
| f0e4c89ac0 | |||
| d676d34cd8 | |||
| ac113d6fb0 | |||
| fd2c688321 | |||
| ae04e4ddbe | |||
| ad382602f2 | |||
| aefaea7b90 | |||
| 05fc252db0 | |||
| b4456a9c5b | |||
| 46008c14c5 | |||
| 0ebd1d7ac6 | |||
| de09eacabc | |||
| 14af13c050 | |||
| 47c5b99617 | |||
| e975fe409b | |||
| 0270e17a76 | |||
| fc5335a5c6 | |||
| 2aa6e9240f | |||
| 535fca933a | |||
| b7b2601180 | |||
| a1b60ed936 |
127
.gitea/workflows/build-binaries.yml
Normal file
127
.gitea/workflows/build-binaries.yml
Normal file
@ -0,0 +1,127 @@
|
||||
name: test-build-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=no
|
||||
run: |
|
||||
git clone --depth 1 ssh://git@jianfgit.xyz:220/jianf/TrulyMEM-TrueHumanMEM.git ./
|
||||
|
||||
- name: Verify workspace and Python
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
python3 --version
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
python3 -m venv .venv-ci
|
||||
. .venv-ci/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt pytest
|
||||
pytest -q
|
||||
|
||||
build-linux:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=no
|
||||
run: |
|
||||
git clone --depth 1 ssh://git@jianfgit.xyz:220/jianf/TrulyMEM-TrueHumanMEM.git ./
|
||||
|
||||
- name: Verify build environment
|
||||
run: |
|
||||
pwd
|
||||
python3 --version
|
||||
tar --version | head -1
|
||||
|
||||
- name: Build Linux binary
|
||||
run: |
|
||||
bash build/build_linux.sh
|
||||
|
||||
- name: Pack release bundle
|
||||
run: |
|
||||
mkdir -p dist/release
|
||||
cp dist/TrulyMEM dist/release/
|
||||
cp dist/TrulyMEM.desktop dist/release/
|
||||
[ -f pic/image.png ] && cp pic/image.png dist/release/ || true
|
||||
tar -C dist/release -czf dist/TrulyMEM-linux-amd64.tar.gz .
|
||||
ls -lh dist/TrulyMEM-linux-amd64.tar.gz
|
||||
|
||||
- name: Build AppImage if preinstalled
|
||||
run: |
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
bash build/build_appimage.sh
|
||||
else
|
||||
echo "appimagetool not found on runner, skip AppImage build"
|
||||
fi
|
||||
|
||||
- name: Show build outputs
|
||||
run: |
|
||||
ls -lah dist/
|
||||
|
||||
- name: Create Gitea release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITEA_TOKEN: ${{ vars.GITEA_RELEASE_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
run: |
|
||||
test -n "$GITEA_TOKEN"
|
||||
|
||||
# Build release payload via env var — avoids heredoc in YAML
|
||||
export TAG_NAME
|
||||
release_payload=$(python3 -c 'import os,json; t=os.environ["TAG_NAME"]; print(json.dumps({"tag_name":t,"name":t,"draft":false,"prerelease":false}))')
|
||||
|
||||
release_response=$(curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases" \
|
||||
-d "$release_payload" \
|
||||
|| true)
|
||||
|
||||
# Extract release id or fallback to existing
|
||||
export RELEASE_RESPONSE
|
||||
release_id=$(python3 -c '
|
||||
import os, json
|
||||
raw = os.environ.get("RELEASE_RESPONSE", "").strip()
|
||||
if raw:
|
||||
try:
|
||||
print(json.loads(raw)["id"])
|
||||
except Exception:
|
||||
pass
|
||||
')
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
release_id=$(curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases/tags/$TAG_NAME" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' \
|
||||
2>/dev/null || true)
|
||||
fi
|
||||
|
||||
for file in dist/TrulyMEM dist/TrulyMEM.desktop dist/TrulyMEM-linux-amd64.tar.gz dist/TrulyMEM.AppImage; do
|
||||
if [ -f "$file" ]; then
|
||||
name=$(basename "$file")
|
||||
curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$file" \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases/$release_id/assets?name=$name"
|
||||
fi
|
||||
done
|
||||
105
.gitignore
vendored
105
.gitignore
vendored
@ -1,76 +1,41 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
test_venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.arts/
|
||||
.codeartsdoer/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Sensitive
|
||||
.env
|
||||
*.key
|
||||
*.pem
|
||||
config.json
|
||||
|
||||
# Build
|
||||
dist/
|
||||
|
||||
# Temporary
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# AI Generated
|
||||
jimeng*.png
|
||||
|
||||
# Test Cache
|
||||
.pytest_cache/
|
||||
|
||||
# TypeScript
|
||||
harmonyos/.hvigor/
|
||||
harmonyos/build/
|
||||
harmonyos/entry/build/
|
||||
harmonyos.bak/
|
||||
node_modules/
|
||||
ts/node_modules/
|
||||
ts/dist/
|
||||
ts/build/
|
||||
*.hap
|
||||
*.hsp
|
||||
.env
|
||||
venv/
|
||||
.venv/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Task Archive (runtime generated)
|
||||
build/trulymem/
|
||||
build/trulymem/*
|
||||
*.db
|
||||
task_archive/
|
||||
ts/task_archive/
|
||||
core/web_config.json
|
||||
|
||||
# Build artifacts
|
||||
build/trulymem/
|
||||
dist/
|
||||
*.spec
|
||||
|
||||
# HarmonyOS / Hvigor build cache
|
||||
.hvigor/
|
||||
entry/build/
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
full_output.log
|
||||
|
||||
# OS
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
# 修复追踪文件
|
||||
|
||||
**分支**: 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: 核心修复
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
# 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`
|
||||
|
||||
每次修改文件前后请查看此文件并更新进度。
|
||||
@ -1,572 +0,0 @@
|
||||
# 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` 验证
|
||||
@ -1,95 +0,0 @@
|
||||
# 重构执行进度追踪
|
||||
|
||||
> 最后更新: 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 分支
|
||||
@ -1,81 +0,0 @@
|
||||
# 迁移工作进度追踪
|
||||
|
||||
> ⚠️ **修改只在 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 |
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
- 每次修改文件前后更新此文件
|
||||
- 记录每次修改的文件和操作
|
||||
- 方便意外终止后恢复任务
|
||||
356
README.md
356
README.md
@ -1,327 +1,79 @@
|
||||
# TrulyMEM - AI 主要长期记忆系统
|
||||
# TrulyMEM - TrueHumanMEM
|
||||
|
||||
让 AI 拥有真正的长期记忆能力 - OpenClaw 框架插件版
|
||||
<p align="center">
|
||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
||||
</p>
|
||||
|
||||
[English Version](./README_EN.md)
|
||||
> **📜 开源协议**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
> **English**: [README_EN.md](./README_EN.md)
|
||||
|
||||
**让 AI 拥有自知、可塑、有分寸感的长期记忆** — *The More Human Choice.*
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
---
|
||||
|
||||
## 定位声明
|
||||
## 一句话
|
||||
|
||||
**TrulyMEM 是 AI 的主要长期记忆系统。**
|
||||
TrulyMEM 将记忆权交还给 LLM。通过图数据库(三元组)替代传统 messages 数组,让 LLM 自主决定记什么、忘什么。
|
||||
|
||||
与 OpenClaw 内置的 memory-core 并存运行:
|
||||
- **memory-core**:负责 session transcripts 和对话历史管理
|
||||
- **GraphMemory (TrulyMEM)**:负责**结构化长期记忆**——知识图谱、人设、任务、语义搜索
|
||||
---
|
||||
## 项目截图
|
||||

|
||||

|
||||
## 快速开始
|
||||
|
||||
AI 在对话中应**优先**将重要信息(偏好、决策、任务、知识点)写入图数据库,而非依赖 message 历史。
|
||||
```bash
|
||||
python trulymem_entry.py # 从源码
|
||||
./dist/TrulyMEM # 打包后
|
||||
```
|
||||
|
||||
首次启动 → TUI 登录页面 → 创建/登录账号 → 按 **F2** 配置 API Key → 开始聊天
|
||||
|
||||
📖 **详细启动文档**: [docs/zh/quick_start.md](docs/zh/quick_start.md)
|
||||
|
||||
---
|
||||
|
||||
## AI 自行安装指南
|
||||
## 主要特性
|
||||
|
||||
> 本节面向 AI Agent,按步骤指导你自主安装此插件到 OpenClaw 框架。
|
||||
|
||||
### 步骤 1:克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git checkout openclaw
|
||||
```
|
||||
|
||||
### 步骤 2:编译 TypeScript 插件
|
||||
|
||||
```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` - 任务管理
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 🧠 **图记忆** | 三元组存储,LLM 自主推理跳转 |
|
||||
| 🔐 **多用户** | 用户隔离 + Admin/User 角色权限 |
|
||||
| 🌐 **Web 可视化** | 内嵌 Flask 服务(线程模式),实时浏览知识图谱 + 聊天上传文件(支持 PDF/Word/文本) |
|
||||
| 🎮 **TUI 界面** | Textual 终端界面,F2 配置面板 |
|
||||
| 📦 **单文件打包** | PyInstaller 打包,Web 服务内嵌于主二进制 |
|
||||
|
||||
---
|
||||
|
||||
## 简介
|
||||
## 文档索引
|
||||
|
||||
本项目是 OpenClaw 的图记忆插件,基于 SQLite 实现持久化图数据库。
|
||||
|
||||
**设计理念: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 应主动追踪任务状态)
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 🔥 **完整启动指南**(含 Web、多用户、打包) |
|
||||
| [docs/zh/architecture.md](docs/zh/architecture.md) | 系统架构和技术设计 |
|
||||
| [docs/zh/memory.md](docs/zh/memory.md) | 内部记忆工作机制 |
|
||||
| [docs/zh/persona.md](docs/zh/persona.md) | 人设图机制 |
|
||||
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口 |
|
||||
| [docs/zh/prompts.md](docs/zh/prompts.md) | 提示词管理模块 |
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
## 特别鸣谢
|
||||
|
||||
### 方式一:作为 OpenClaw 插件安装
|
||||
|
||||
```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 # 运行测试
|
||||
```
|
||||
- [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)
|
||||
GNU General Public License v3.0 (GPLv3)
|
||||
# CI trigger Wednesday, May 13, 2026 AM06:17:55 UTC
|
||||
# runner cache cleared Wednesday, May 13, 2026 AM06:23:27 UTC
|
||||
|
||||
282
README_EN.md
282
README_EN.md
@ -1,261 +1,79 @@
|
||||
# TrulyMEM - OpenClaw Graph Memory Plugin
|
||||
# TrulyMEM - TrueHumanMEM
|
||||
|
||||
Give AI true long-term memory capability - OpenClaw framework plugin version
|
||||
<p align="center">
|
||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
||||
</p>
|
||||
|
||||
[中文版本](./README.md)
|
||||
> **📜 License**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
> **中文**: [README.md](./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/)
|
||||
|
||||
---
|
||||
|
||||
## AI Self-Installation Guide
|
||||
## In a Nutshell
|
||||
|
||||
> This section is for AI Agents. Follow these steps to install this plugin into your OpenClaw framework.
|
||||
TrulyMEM gives memory authority back to the LLM. Using graph database (triplets) instead of the traditional messages array, the LLM autonomously decides what to remember and what to forget.
|
||||
|
||||
### Step 1: Clone the repository
|
||||
---
|
||||
|
||||
## Screen Shot
|
||||

|
||||

|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git checkout openclaw
|
||||
python trulymem_entry.py # from source
|
||||
./dist/TrulyMEM # packaged binary
|
||||
```
|
||||
|
||||
### Step 2: Build the TypeScript plugin
|
||||
First run → TUI login screen → create/sign in → press **F2** for API Key → 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
|
||||
📖 **Full guide**: [docs/en/quick_start.md](docs/en/quick_start.md)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
## Features
|
||||
|
||||
This project is an OpenClaw plugin for graph-based memory with SQLite persistence.
|
||||
|
||||
**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
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🧠 **Graph Memory** | Triplet storage, LLM autonomous navigation |
|
||||
| 🔐 **Multi-User** | Isolated profiles + Admin/User roles |
|
||||
| 🌐 **Web UI** | Embedded Flask server (thread mode), graph browsing + file upload in chat (PDF/Word/text) |
|
||||
| 🎮 **TUI** | Textual-based terminal UI with F2 config panel |
|
||||
| 📦 **Single Binary** | PyInstaller build, Web server embedded |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Documentation
|
||||
|
||||
### Method 1: As OpenClaw Plugin
|
||||
|
||||
```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
|
||||
```
|
||||
| Document | Content |
|
||||
|----------|---------|
|
||||
| [docs/en/quick_start.md](docs/en/quick_start.md) | 🔥 **Full setup guide** (Web, multi-user, building) |
|
||||
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and design |
|
||||
| [docs/en/memory.md](docs/en/memory.md) | Memory working mechanism |
|
||||
| [docs/en/persona.md](docs/en/persona.md) | Persona Graph mechanism |
|
||||
| [docs/en/api.md](docs/en/api.md) | Backend API |
|
||||
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management |
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
## Special Thanks
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
- [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
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[GNU General Public License v3.0 (GPLv3)](LICENSE)
|
||||
GNU General Public License v3.0 (GPLv3)
|
||||
|
||||
48
TODO.md
48
TODO.md
@ -1,48 +0,0 @@
|
||||
# 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/`(编译后)
|
||||
106
TrulyMEM.spec
Normal file
106
TrulyMEM.spec
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- 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 = []
|
||||
# UI 样式
|
||||
ui_styles_dir = os.path.join(project_root, 'ui', 'styles')
|
||||
if os.path.exists(ui_styles_dir):
|
||||
for root, dirs, files in os.walk(ui_styles_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'ui/styles'))
|
||||
|
||||
# Prompt 模板
|
||||
prompt_tmpl_dir = os.path.join(project_root, 'core', 'prompts', 'templates')
|
||||
if os.path.exists(prompt_tmpl_dir):
|
||||
for root, dirs, files in os.walk(prompt_tmpl_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'core/prompts/templates'))
|
||||
|
||||
# Web 静态文件
|
||||
static_dir = os.path.join(project_root, 'ui', 'static')
|
||||
if os.path.exists(static_dir):
|
||||
for root, dirs, files in os.walk(static_dir):
|
||||
for f in files:
|
||||
rel_dir = os.path.relpath(root, project_root)
|
||||
datas.append((os.path.join(root, f), rel_dir))
|
||||
|
||||
# 兼容旧的 static 目录(如果存在)
|
||||
static_dir_old = os.path.join(project_root, 'static')
|
||||
if os.path.exists(static_dir_old):
|
||||
for root, dirs, files in os.walk(static_dir_old):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'static'))
|
||||
|
||||
# Web 模板(Flask template_folder 指向 ui/templates/)
|
||||
templates_dir = os.path.join(project_root, 'ui', 'templates')
|
||||
if os.path.exists(templates_dir):
|
||||
for root, dirs, files in os.walk(templates_dir):
|
||||
for f in files:
|
||||
rel_dir = os.path.relpath(root, project_root)
|
||||
datas.append((os.path.join(root, f), rel_dir))
|
||||
|
||||
# Web API 脚本(以便子进程模式回退使用)
|
||||
web_api_src = os.path.join(project_root, 'core', 'web_api.py')
|
||||
if os.path.exists(web_api_src):
|
||||
datas.append((web_api_src, 'core'))
|
||||
|
||||
# ——— TUI 主二进制 ———
|
||||
a = Analysis(
|
||||
[os.path.join(project_root, 'trulymem_entry.py')],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
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',
|
||||
'core.server', 'core.client', 'core.web_api',
|
||||
'core.migrate',
|
||||
'ui', 'ui.app', 'ui.login_screen',
|
||||
'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', 'ui.services.config_service',
|
||||
'flask', 'flask_cors', 'werkzeug',
|
||||
],
|
||||
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,
|
||||
)
|
||||
100
build/build_appimage.sh
Executable file
100
build/build_appimage.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "===== Building TrulyMEM AppImage ====="
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PROJECT_ROOT"
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
APP_NAME="TrulyMEM"
|
||||
APP_DIR="$PROJECT_ROOT/build/appimage-build"
|
||||
|
||||
# ── Step 1: 复用 build_linux.sh 完成 PyInstaller 构建 ──
|
||||
echo ""
|
||||
echo "Step 1: Running build_linux.sh (PyInstaller build)..."
|
||||
bash "$SCRIPT_DIR/build_linux.sh"
|
||||
|
||||
# ── Step 2: 检查 dist/ 中是否有二进制 ──
|
||||
echo ""
|
||||
echo "Step 2: Checking PyInstaller output..."
|
||||
if [ ! -f "dist/$APP_NAME" ]; then
|
||||
echo "Error: dist/$APP_NAME not found after build_linux.sh"; exit 1
|
||||
fi
|
||||
echo "✅ Found dist/$APP_NAME ($(ls -lh "dist/$APP_NAME" | awk '{print $5}'))"
|
||||
|
||||
# ── Step 3: 组织 AppDir 结构 ──
|
||||
echo ""
|
||||
echo "Step 3: Preparing AppDir structure..."
|
||||
rm -rf "$APP_DIR" 2>/dev/null || true
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/share/applications"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/48x48/apps"
|
||||
|
||||
cp "dist/$APP_NAME" "$APP_DIR/usr/bin/"
|
||||
|
||||
# 图标处理
|
||||
ICON_SOURCE=""
|
||||
if [ -f "pic/image.png" ]; then
|
||||
ICON_SOURCE="pic/image.png"
|
||||
elif [ -f "pic/TrulyMEM.ico" ]; then
|
||||
echo "⚠️ No pic/image.png found; .ico will not display as AppImage icon"
|
||||
echo " To generate a PNG: convert pic/TrulyMEM.ico pic/image.png"
|
||||
fi
|
||||
|
||||
if [ -n "$ICON_SOURCE" ]; then
|
||||
cp "$ICON_SOURCE" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/${APP_NAME}.png"
|
||||
cp "$ICON_SOURCE" "$APP_DIR/usr/share/icons/hicolor/48x48/apps/${APP_NAME}.png"
|
||||
cp "$ICON_SOURCE" "$APP_DIR/${APP_NAME}.png"
|
||||
echo "✅ Icon: $ICON_SOURCE"
|
||||
else
|
||||
echo "⚠️ No icon found, creating placeholder"
|
||||
touch "$APP_DIR/${APP_NAME}.png"
|
||||
fi
|
||||
|
||||
# .desktop 文件
|
||||
cat > "$APP_DIR/${APP_NAME}.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=${APP_NAME}
|
||||
Comment=True Human Memory - TUI & Web Mode
|
||||
Exec=${APP_NAME}
|
||||
Icon=${APP_NAME}
|
||||
Type=Application
|
||||
Categories=Utility;Office;
|
||||
Terminal=true
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
cp "$APP_DIR/${APP_NAME}.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# AppRun 入口
|
||||
cat > "$APP_DIR/AppRun" <<'APPRUN'
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/usr/bin/TrulyMEM" "$@"
|
||||
APPRUN
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
|
||||
# ── Step 4: 打包 AppImage ──
|
||||
echo ""
|
||||
echo "Step 4: Building AppImage..."
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
ARCH="${ARCH:-$(uname -m)}" appimagetool "$APP_DIR" "dist/${APP_NAME}.AppImage"
|
||||
echo "✅ AppImage: dist/${APP_NAME}.AppImage"
|
||||
ls -lh "dist/${APP_NAME}.AppImage"
|
||||
else
|
||||
echo "⚠️ appimagetool not found. AppDir ready at: $APP_DIR"
|
||||
echo ""
|
||||
echo "To complete manually, install appimagetool:"
|
||||
echo " wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(uname -m).AppImage"
|
||||
echo " chmod +x appimagetool-*.AppImage"
|
||||
echo " ./appimagetool-*.AppImage '$APP_DIR' 'dist/${APP_NAME}.AppImage'"
|
||||
echo ""
|
||||
echo "AppDir contents:"
|
||||
find "$APP_DIR" -type f | head -20
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "===== AppImage Build Complete ====="
|
||||
109
build/build_linux.sh
Executable file
109
build/build_linux.sh
Executable file
@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
|
||||
if [ ! -f requirements.txt ]; then
|
||||
echo "Error: requirements.txt not found in $PROJECT_ROOT"; exit 1
|
||||
fi
|
||||
|
||||
# ── 检测 python3-venv ──
|
||||
VENV_AVAILABLE=false
|
||||
if python3 -c "import ensurepip" 2>/dev/null && python3 -m venv --help &>/dev/null; then
|
||||
VENV_AVAILABLE=true
|
||||
else
|
||||
echo "⚠️ python3-venv 未安装(或缺少 ensurepip),建议安装以获得干净构建环境:"
|
||||
echo " sudo apt install python3-venv # Debian/Ubuntu"
|
||||
echo " sudo dnf install python3-virtualenv # Fedora"
|
||||
echo "将使用系统 Python 环境继续(依赖全局包)..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── 尝试 venv 隔离构建,失败则用系统环境 ──
|
||||
USE_VENV=false
|
||||
if [ "$VENV_AVAILABLE" = true ]; then
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
||||
echo "Creating virtual environment..."
|
||||
if python3 -m venv "$VENV_DIR" 2>/dev/null; then
|
||||
source "$VENV_DIR/bin/activate"
|
||||
USE_VENV=true
|
||||
echo "✅ Using virtual environment: $VENV_DIR"
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
else
|
||||
echo "⚠️ venv creation failed, falling back to system Python"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$USE_VENV" = false ]; then
|
||||
echo "Installing dependencies (system Python)..."
|
||||
pip install -r requirements.txt --break-system-packages -q 2>/dev/null || \
|
||||
pip install -r requirements.txt -q 2>/dev/null || {
|
||||
echo "⚠️ pip install failed, trying pip3..."
|
||||
pip3 install -r requirements.txt --break-system-packages -q 2>/dev/null || \
|
||||
pip3 install -r requirements.txt -q 2>/dev/null || \
|
||||
echo "⚠️ Some dependencies may be missing; build will proceed anyway"
|
||||
}
|
||||
fi
|
||||
|
||||
# ── 清理旧构建 ──
|
||||
echo ""
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf dist/ build/trulymem/ 2>/dev/null || true
|
||||
|
||||
# ── PyInstaller 构建 ──
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Building TrulyMEM (TUI + Web embedded)"
|
||||
echo "================================"
|
||||
python3 -m PyInstaller --clean build/trulymem.spec --noconfirm
|
||||
|
||||
# ── 创建 Linux `.desktop` 文件(打包图标不能嵌入 ELF,通过 .desktop 引用)──
|
||||
echo ""
|
||||
echo "Generating .desktop file for Linux..."
|
||||
BINARY_PATH="$(cd dist && pwd)/TrulyMEM"
|
||||
ICON_PATH="$(cd pic && pwd)/image.png"
|
||||
|
||||
cat > "dist/TrulyMEM.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=TrulyMEM
|
||||
Comment=True Human Memory - TUI & Web Mode
|
||||
Exec=${BINARY_PATH}
|
||||
Icon=${ICON_PATH}
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;Office;
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
chmod +x "dist/TrulyMEM.desktop"
|
||||
echo "✅ dist/TrulyMEM.desktop created (icon: pic/image.png)"
|
||||
|
||||
# ── 构建完成 ──
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
echo "Desktop: dist/TrulyMEM.desktop"
|
||||
echo "------------------------------"
|
||||
ls -lh dist/ 2>/dev/null || ls -la dist/
|
||||
echo "================================"
|
||||
|
||||
# ── 清理 venv ──
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
deactivate 2>/dev/null || true
|
||||
rm -rf "$VENV_DIR"
|
||||
echo "Virtual environment cleaned up."
|
||||
fi
|
||||
|
||||
echo "Build finished successfully!"
|
||||
38
build/build_macos.sh
Executable file
38
build/build_macos.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/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"
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf dist/ build/trulymem/ 2>/dev/null || true
|
||||
|
||||
echo "================================"
|
||||
echo "Building TrulyMEM (TUI + Web embedded)"
|
||||
echo "================================"
|
||||
python -m PyInstaller --clean build/trulymem.spec --noconfirm
|
||||
|
||||
echo "================================"
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
echo " -> run: open dist/TrulyMEM"
|
||||
ls -la dist/
|
||||
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
echo "Build finished successfully!"
|
||||
35
build/build_windows.bat
Normal file
35
build/build_windows.bat
Normal file
@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ===== Building TrulyMEM for Windows =====
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PROJECT_ROOT=%SCRIPT_DIR%.."
|
||||
cd /d "%PROJECT_ROOT%"
|
||||
echo Project root: %PROJECT_ROOT%
|
||||
|
||||
echo Creating virtual environment: .venv_build
|
||||
python -m venv .venv_build
|
||||
call .venv_build\Scripts\activate.bat
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo Cleaning previous builds...
|
||||
if exist dist rmdir /s /q dist
|
||||
if exist build\trulymem rmdir /s /q build\trulymem
|
||||
|
||||
echo ================================
|
||||
echo Building TrulyMEM (TUI + Web embedded)
|
||||
echo ================================
|
||||
python -m PyInstaller --clean build\trulymem.spec --noconfirm
|
||||
|
||||
echo ================================
|
||||
echo ===== Build Complete =====
|
||||
echo Binary: dist\TrulyMEM.exe
|
||||
dir dist
|
||||
|
||||
call .venv_build\Scripts\deactivate.bat
|
||||
if exist .venv_build rmdir /s /q .venv_build
|
||||
|
||||
echo Build finished successfully!
|
||||
endlocal
|
||||
99
build/trulymem.spec
Normal file
99
build/trulymem.spec
Normal file
@ -0,0 +1,99 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
|
||||
block_cipher = None
|
||||
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(SPEC)))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
datas = []
|
||||
# UI 样式
|
||||
ui_styles_dir = os.path.join(project_root, 'ui', 'styles')
|
||||
if os.path.exists(ui_styles_dir):
|
||||
for root, dirs, files in os.walk(ui_styles_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'ui/styles'))
|
||||
|
||||
# Prompt 模板
|
||||
prompt_tmpl_dir = os.path.join(project_root, 'core', 'prompts', 'templates')
|
||||
if os.path.exists(prompt_tmpl_dir):
|
||||
for root, dirs, files in os.walk(prompt_tmpl_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'core/prompts/templates'))
|
||||
|
||||
# Web 静态文件
|
||||
static_dir = os.path.join(project_root, 'ui', 'static')
|
||||
if os.path.exists(static_dir):
|
||||
for root, dirs, files in os.walk(static_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'ui/static'))
|
||||
|
||||
# Web 模板
|
||||
templates_dir = os.path.join(project_root, 'ui', 'templates')
|
||||
if os.path.exists(templates_dir):
|
||||
for root, dirs, files in os.walk(templates_dir):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'ui/templates'))
|
||||
|
||||
# Web API 脚本(以便子进程模式回退使用)
|
||||
web_api_src = os.path.join(project_root, 'core', 'web_api.py')
|
||||
if os.path.exists(web_api_src):
|
||||
datas.append((web_api_src, '.'))
|
||||
|
||||
# ——— TUI 主二进制 ———
|
||||
a = Analysis(
|
||||
[os.path.join(project_root, 'trulymem_entry.py')],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
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',
|
||||
'core.server', 'core.client',
|
||||
'core.migrate',
|
||||
'ui', 'ui.app', 'ui.login_screen',
|
||||
'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', 'ui.services.config_service',
|
||||
'web_api',
|
||||
'flask', 'flask_cors', 'werkzeug',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='TrulyMEM',
|
||||
icon=os.path.join(project_root, 'pic', 'TrulyMEM.ico'),
|
||||
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,
|
||||
)
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "common",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM common module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/common",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
12
core/__init__.py
Normal file
12
core/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
from .server import BackendServer, Packet, PacketType, PacketResponse
|
||||
from .client import BackendClient
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
__all__ = [
|
||||
"BackendServer",
|
||||
"BackendClient",
|
||||
"EmbeddedGraphDB",
|
||||
"Packet",
|
||||
"PacketType",
|
||||
"PacketResponse"
|
||||
]
|
||||
185
core/activity_recorder.py
Normal file
185
core/activity_recorder.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""
|
||||
活动记录器 - 记录 AI 对图数据库的操作
|
||||
使用 SQLite :memory: 供 WebUI 实时渲染,同时后台线程持久化到日志文件
|
||||
日志每 6 小时自动压缩归档
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
import os
|
||||
import gzip
|
||||
import json
|
||||
import threading
|
||||
import shutil
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
||||
# 归档间隔(秒)
|
||||
ARCHIVE_INTERVAL = 6 * 3600 # 6 小时
|
||||
# 轮询间隔(秒)
|
||||
POLL_INTERVAL = 10
|
||||
|
||||
|
||||
class ActivityRecorder:
|
||||
"""记录 AI 对图数据库的操作到内存 SQLite"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
|
||||
self.conn.execute(
|
||||
"CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"timestamp REAL, action TEXT, tool_name TEXT, entity TEXT, detail TEXT)"
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def record(self, action: str, tool_name: str, entity: str, detail: str = "") -> None:
|
||||
self.conn.execute(
|
||||
"INSERT INTO activities (timestamp, action, tool_name, entity, detail) VALUES (?, ?, ?, ?, ?)",
|
||||
(time.time(), action, tool_name, entity, detail)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_all(self) -> List[Dict]:
|
||||
cursor = self.conn.execute(
|
||||
"SELECT id, timestamp, action, tool_name, entity, detail FROM activities ORDER BY id"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return [
|
||||
{"id": r[0], "timestamp": r[1], "action": r[2],
|
||||
"tool_name": r[3], "entity": r[4], "detail": r[5]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_since_id(self, last_id: int) -> List[Dict]:
|
||||
"""获取自 last_id 之后的新记录"""
|
||||
cursor = self.conn.execute(
|
||||
"SELECT id, timestamp, action, tool_name, entity, detail FROM activities WHERE id > ? ORDER BY id",
|
||||
(last_id,)
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return [
|
||||
{"id": r[0], "timestamp": r[1], "action": r[2],
|
||||
"tool_name": r[3], "entity": r[4], "detail": r[5]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_max_id(self) -> int:
|
||||
cursor = self.conn.execute("SELECT COALESCE(MAX(id), 0) FROM activities")
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def clear(self) -> None:
|
||||
self.conn.execute("DELETE FROM activities")
|
||||
self.conn.commit()
|
||||
|
||||
def get_summary(self) -> Dict[str, int]:
|
||||
cursor = self.conn.execute("SELECT action, COUNT(*) FROM activities GROUP BY action")
|
||||
rows = cursor.fetchall()
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
# ── 日志文件管理 ──
|
||||
|
||||
def _current_log_path() -> str:
|
||||
"""返回当前日志文件路径(按日期命名)"""
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return os.path.join(LOG_DIR, f"operations.{date_str}.log")
|
||||
|
||||
|
||||
def _archive_log(filepath: str) -> str:
|
||||
"""压缩归档日志文件,返回归档文件路径"""
|
||||
if not os.path.exists(filepath) or os.path.getsize(filepath) == 0:
|
||||
return ""
|
||||
archive_path = filepath + ".gz"
|
||||
try:
|
||||
with open(filepath, "rb") as f_in:
|
||||
with gzip.open(archive_path, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(filepath)
|
||||
return archive_path
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
class LogPersister:
|
||||
"""后台日志持久化线程 - 定期将内存记录写入日志文件并自动归档"""
|
||||
|
||||
def __init__(self, recorder: ActivityRecorder):
|
||||
self.recorder = recorder
|
||||
self._last_persisted_id = 0
|
||||
self._last_archive_time = time.time()
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run, daemon=True, name="log-persister")
|
||||
self._thread.start()
|
||||
|
||||
def _run(self):
|
||||
"""主循环"""
|
||||
while self._running:
|
||||
try:
|
||||
self._persist_new()
|
||||
self._check_archive()
|
||||
except Exception:
|
||||
pass # 不因日志异常影响主进程
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
def _persist_new(self):
|
||||
"""增量写入新记录到日志文件"""
|
||||
records = self.recorder.get_since_id(self._last_persisted_id)
|
||||
if not records:
|
||||
return
|
||||
|
||||
log_path = _current_log_path()
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
for r in records:
|
||||
line = json.dumps(r, ensure_ascii=False)
|
||||
f.write(line + "\n")
|
||||
|
||||
# 更新水位
|
||||
if records:
|
||||
self._last_persisted_id = records[-1]["id"]
|
||||
|
||||
def _check_archive(self):
|
||||
"""检查是否需要归档"""
|
||||
elapsed = time.time() - self._last_archive_time
|
||||
if elapsed < ARCHIVE_INTERVAL:
|
||||
return
|
||||
|
||||
log_path = _current_log_path()
|
||||
archived = _archive_log(log_path)
|
||||
if archived:
|
||||
dt = datetime.fromtimestamp(self._last_archive_time)
|
||||
print(f"[日志归档] {dt.strftime('%H:%M')} → {os.path.basename(archived)} ({_fmt_size(archived)})")
|
||||
self._last_archive_time = time.time()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
|
||||
def _fmt_size(path: str) -> str:
|
||||
size = os.path.getsize(path)
|
||||
for unit in ("B", "KB", "MB"):
|
||||
if size < 1024:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f}GB"
|
||||
|
||||
|
||||
# ── 单例 ──
|
||||
|
||||
_recorder: Optional[ActivityRecorder] = None
|
||||
_persister: Optional[LogPersister] = None
|
||||
|
||||
|
||||
def get_recorder() -> ActivityRecorder:
|
||||
"""获取全局 ActivityRecorder(首次调用时自动启动日志持久化线程)"""
|
||||
global _recorder, _persister
|
||||
if _recorder is None:
|
||||
_recorder = ActivityRecorder()
|
||||
_persister = LogPersister(_recorder)
|
||||
return _recorder
|
||||
|
||||
|
||||
def get_persister() -> Optional[LogPersister]:
|
||||
return _persister
|
||||
128
core/client.py
Normal file
128
core/client.py
Normal file
@ -0,0 +1,128 @@
|
||||
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 get_web_users(self) -> list:
|
||||
"""获取 Web 用户列表"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_USERS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body.get("users", [])
|
||||
|
||||
def set_web_user(self, username: str, password: str) -> Dict:
|
||||
"""设置 Web 用户"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SET_WEB_USER,
|
||||
body={"username": username, "password": password}
|
||||
)
|
||||
return self._server.send(packet).body.get("data", {"success": False})
|
||||
|
||||
def get_full_config(self) -> Dict:
|
||||
"""获取完整配置"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_CONFIG,
|
||||
body={}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"api_config": {}, "tool_limits": {}}
|
||||
|
||||
def report_web_status(self, running: bool, port: int = 4096) -> Dict:
|
||||
"""向后端报告 Web 服务运行状态"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_SERVICE_STATUS,
|
||||
body={"running": running, "port": port}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"success": False}
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._server.shutdown()
|
||||
902
core/embedded_db.py
Normal file
902
core/embedded_db.py
Normal file
@ -0,0 +1,902 @@
|
||||
"""
|
||||
内嵌图数据库 - 基于SQLite实现
|
||||
无需Docker,开箱即用
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import hashlib
|
||||
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)")
|
||||
|
||||
# 创建 Web 用户表(支持多用户隔离)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS web_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
config_path TEXT,
|
||||
db_path TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 检查并添加新字段(用于旧数据库迁移)
|
||||
cursor.execute("PRAGMA table_info(web_users)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'config_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN config_path TEXT")
|
||||
if 'db_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN db_path TEXT")
|
||||
if 'role' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'")
|
||||
|
||||
# 确保至少有一个 admin(当 role 列刚添加时,已有用户都是 user)
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM web_users WHERE role = 'admin'")
|
||||
has_admin = cursor.fetchone()[0] > 0
|
||||
if not has_admin:
|
||||
cursor.execute("SELECT id, username FROM web_users ORDER BY created_at ASC LIMIT 1")
|
||||
first_user = cursor.fetchone()
|
||||
if first_user:
|
||||
cursor.execute("UPDATE web_users SET role = 'admin' WHERE id = ?", (first_user[0],))
|
||||
|
||||
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']
|
||||
})
|
||||
|
||||
# 广度优先搜索(BFS)扩展实体和关系
|
||||
relations = []
|
||||
visited_entity_ids = set(entity_ids) # 已访问的实体
|
||||
current_layer_ids = set(entity_ids) # 当前层的实体
|
||||
|
||||
# 记录每个实体的深度
|
||||
entity_depths = {} # entity_id -> depth
|
||||
for eid in entity_ids:
|
||||
entity_depths[eid] = 0
|
||||
|
||||
for layer in range(depth):
|
||||
if not current_layer_ids:
|
||||
break
|
||||
|
||||
# 查询当前层实体的所有关系
|
||||
placeholders = ','.join('?' * len(current_layer_ids))
|
||||
|
||||
query = f"""
|
||||
SELECT r.id, r.source_id, r.target_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(current_layer_ids) + list(current_layer_ids)
|
||||
|
||||
if session_filter:
|
||||
query += " AND r.session_id = ?"
|
||||
params.append(session_filter)
|
||||
|
||||
cursor.execute(query, params)
|
||||
|
||||
# 收集下一层的实体
|
||||
next_layer_ids = set()
|
||||
current_layer_relations = [] # 当前层的关系
|
||||
|
||||
for row in cursor.fetchall():
|
||||
# 计算关系的深度(取两端实体深度的最大值+1)
|
||||
source_depth = entity_depths.get(row['source_id'], layer)
|
||||
target_depth = entity_depths.get(row['target_id'], layer)
|
||||
relation_depth = max(source_depth, target_depth) + 1
|
||||
|
||||
# 添加关系(带深度标注)
|
||||
current_layer_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'],
|
||||
'depth': relation_depth
|
||||
})
|
||||
|
||||
# 收集新实体(未访问过的)
|
||||
source_id = row['source_id']
|
||||
target_id = row['target_id']
|
||||
|
||||
if source_id not in visited_entity_ids:
|
||||
next_layer_ids.add(source_id)
|
||||
visited_entity_ids.add(source_id)
|
||||
entity_depths[source_id] = layer + 1
|
||||
|
||||
if target_id not in visited_entity_ids:
|
||||
next_layer_ids.add(target_id)
|
||||
visited_entity_ids.add(target_id)
|
||||
entity_depths[target_id] = layer + 1
|
||||
|
||||
relations.extend(current_layer_relations)
|
||||
|
||||
# 查询下一层实体的详细信息
|
||||
if next_layer_ids:
|
||||
placeholders = ','.join('?' * len(next_layer_ids))
|
||||
cursor.execute(f"""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
WHERE id IN ({placeholders})
|
||||
""", list(next_layer_ids))
|
||||
|
||||
for row in cursor.fetchall():
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count'],
|
||||
'depth': entity_depths.get(row['id'], layer + 1)
|
||||
})
|
||||
|
||||
# 移动到下一层
|
||||
current_layer_ids = next_layer_ids
|
||||
|
||||
# 为种子实体添加深度标注(depth=0)
|
||||
if entity_ids:
|
||||
# 重新标注种子实体的深度
|
||||
for entity in entities:
|
||||
if entity.get('depth') is None:
|
||||
entity['depth'] = 0
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"relations": relations,
|
||||
"message": f"找到 {len(entities)} 个实体, {len(relations)} 条关系"
|
||||
}
|
||||
|
||||
def get_recent_tasks(self, limit: int = 10, state_filter: str = None) -> Dict:
|
||||
"""
|
||||
获取最近的任务节点
|
||||
|
||||
Args:
|
||||
limit: 返回数量
|
||||
state_filter: 可选状态过滤(如:进行中、已完成、已暂停、已取消、archived)
|
||||
|
||||
Returns:
|
||||
{"tasks": [{"task_id": str, "description": str, "state": str,
|
||||
"info_count": int, "updated_at": str}, ...]}
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 查询所有 TaskNode 实体
|
||||
cursor.execute("""
|
||||
SELECT e.id, e.name, e.updated_at
|
||||
FROM entities e
|
||||
WHERE e.type = 'TaskNode'
|
||||
ORDER BY e.updated_at DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
tasks = []
|
||||
for row in rows:
|
||||
entity_id, name, updated_at = row
|
||||
|
||||
# 查 description
|
||||
cursor.execute("""
|
||||
SELECT r.relation_type, t.name
|
||||
FROM relations r
|
||||
JOIN entities t ON r.target_id = t.id
|
||||
WHERE r.source_id = ? AND r.status = 'active'
|
||||
AND r.relation_type IN ('has_description', 'HAS_STATE')
|
||||
""", (entity_id,))
|
||||
desc = ""
|
||||
state = "未知"
|
||||
for rtype, tname in cursor.fetchall():
|
||||
if rtype == 'has_description':
|
||||
desc = tname
|
||||
elif rtype == 'HAS_STATE':
|
||||
state = tname.replace('State_', '')
|
||||
|
||||
# 可选状态过滤
|
||||
if state_filter and state != state_filter:
|
||||
continue
|
||||
|
||||
# 查关联信息节点数量
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM relations
|
||||
WHERE source_id = ? AND relation_type = 'CONTAINS_INFO' AND status = 'active'
|
||||
""", (entity_id,))
|
||||
info_count = cursor.fetchone()[0]
|
||||
|
||||
tasks.append({
|
||||
"task_id": name,
|
||||
"description": desc,
|
||||
"state": state,
|
||||
"info_count": info_count,
|
||||
"updated_at": updated_at
|
||||
})
|
||||
|
||||
return {
|
||||
"tasks": tasks,
|
||||
"total": len(tasks)
|
||||
}
|
||||
|
||||
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, entity_key in [(subject, 'subject_type'), (obj, 'object_type')]:
|
||||
# 按优先级获取实体类型:1) triplet中的_type字段 2) entity_types字典 3) 默认
|
||||
entity_type = triplet.get(entity_key) or (entity_types.get(entity_name) if entity_types else None) or 'Concept'
|
||||
|
||||
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: 删除条件
|
||||
支持:
|
||||
- source: 源实体名(精确匹配)
|
||||
- target: 目标实体名(精确匹配)
|
||||
- relation: 关系类型
|
||||
- subject_contains: 源实体名包含(模糊匹配)
|
||||
- target_contains: 目标实体名包含(模糊匹配)
|
||||
- relation_type: 关系类型(同 relation)
|
||||
- source_type: 源实体类型过滤
|
||||
- target_type: 目标实体类型过滤
|
||||
- source_has_status: 源实体 mentions_count 状态(支持 type 字段)
|
||||
mode: 删除模式 (soft/hard)
|
||||
new_relation: 替代关系
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
relation_type = criteria.get('relation') or criteria.get('relation_type', '')
|
||||
|
||||
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 relation_type:
|
||||
conditions.append("relation_type = ?")
|
||||
params.append(relation_type)
|
||||
|
||||
# 通过子查询支持实体属性过滤
|
||||
if criteria.get('subject_contains'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?",
|
||||
(f'%{criteria["subject_contains"]}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
if criteria.get('target_contains'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?",
|
||||
(f'%{criteria["target_contains"]}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"target_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 源实体类型过滤
|
||||
if criteria.get('source_type'):
|
||||
cursor.execute("SELECT id FROM entities WHERE type = ?",
|
||||
(criteria['source_type'],))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 目标实体类型过滤
|
||||
if criteria.get('target_type'):
|
||||
cursor.execute("SELECT id FROM entities WHERE type = ?",
|
||||
(criteria['target_type'],))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"target_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 源实体状态过滤
|
||||
if criteria.get('source_has_status'):
|
||||
status = criteria['source_has_status']
|
||||
cursor.execute("SELECT id FROM entities WHERE type LIKE ?",
|
||||
(f'%{status}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
if not conditions:
|
||||
return {"deleted": 0, "message": "无删除条件"}
|
||||
|
||||
conditions.append("status = 'active'")
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
if mode == "soft":
|
||||
cursor.execute(f"""
|
||||
UPDATE relations
|
||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE {where_clause}
|
||||
""", params)
|
||||
else:
|
||||
cursor.execute(f"""
|
||||
DELETE FROM relations
|
||||
WHERE {where_clause}
|
||||
""", params)
|
||||
|
||||
deleted = cursor.rowcount
|
||||
|
||||
# 删除孤立实体(没有任何关系的数据节点)
|
||||
cursor.execute("""
|
||||
DELETE FROM entities
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT source_id FROM relations
|
||||
UNION
|
||||
SELECT DISTINCT target_id FROM relations
|
||||
)
|
||||
""")
|
||||
deleted_orphans = cursor.rowcount
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"deleted": deleted,
|
||||
"deleted_orphans": deleted_orphans,
|
||||
"mode": mode,
|
||||
"message": f"删除了 {deleted} 条关系, {deleted_orphans} 个孤立实体"
|
||||
}
|
||||
|
||||
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 query_archived(self, days: int = None, keyword: str = "") -> Dict:
|
||||
"""
|
||||
查询已归档的记忆
|
||||
|
||||
Args:
|
||||
days: 可选,最近N天内的归档记录
|
||||
keyword: 可选,过滤包含指定关键词的实体名或关系
|
||||
|
||||
Returns:
|
||||
归档记录列表
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 基础 SQL:查询已归档的关系及其关联实体
|
||||
conditions = ["r.status = 'archived'"]
|
||||
params = []
|
||||
|
||||
# 时间范围过滤(最近N天)
|
||||
if days is not None and days > 0:
|
||||
conditions.append("r.updated_at >= datetime('now', ?)")
|
||||
params.append(f'-{days} days')
|
||||
|
||||
# 关键词过滤(匹配源实体名、目标实体名、关系类型任一)
|
||||
if keyword:
|
||||
# 先找到匹配的实体ID
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?", (f'%{keyword}%',))
|
||||
matched_ids = [str(row['id']) for row in cursor.fetchall()]
|
||||
|
||||
if matched_ids:
|
||||
id_list = ','.join(matched_ids)
|
||||
conditions.append(f"(r.source_id IN ({id_list}) OR r.target_id IN ({id_list}) OR r.relation_type LIKE ?)")
|
||||
params.append(f'%{keyword}%')
|
||||
else:
|
||||
conditions.append("r.relation_type LIKE ?")
|
||||
params.append(f'%{keyword}%')
|
||||
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT r.id, r.relation_type, r.created_at, r.updated_at,
|
||||
e.name AS source_name, t.name AS target_name
|
||||
FROM relations r
|
||||
JOIN entities e ON r.source_id = e.id
|
||||
JOIN entities t ON r.target_id = t.id
|
||||
WHERE {where_clause}
|
||||
ORDER BY r.updated_at DESC
|
||||
LIMIT 200
|
||||
""", params)
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
results.append({
|
||||
"id": row['id'],
|
||||
"source": row['source_name'],
|
||||
"relation": row['relation_type'],
|
||||
"target": row['target_name'],
|
||||
"archived_at": row['updated_at'],
|
||||
"created_at": row['created_at']
|
||||
})
|
||||
|
||||
return {
|
||||
"archived_relations": results,
|
||||
"total_relations": len(results),
|
||||
"message": f"找到 {len(results)} 条归档关系"
|
||||
}
|
||||
|
||||
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 set_web_user(self, username: str, password: str, base_dir: str = None, role: str = 'user') -> Dict:
|
||||
"""设置或更新 Web 登录用户。password 是明文,自动哈希存储。
|
||||
自动创建用户目录并设置 config_path 和 db_path。
|
||||
role: 'admin' 或 'user',默认 'user'"""
|
||||
if not username or not password:
|
||||
return {"success": False, "error": "用户名和密码不能为空"}
|
||||
if role not in ('admin', 'user'):
|
||||
return {"success": False, "error": "角色无效 (admin/user)"}
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# 确定基础目录
|
||||
if base_dir is None:
|
||||
base_dir = Path.home() / ".trulymem"
|
||||
else:
|
||||
base_dir = Path(base_dir)
|
||||
|
||||
# 创建用户目录
|
||||
user_dir = base_dir / username
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置用户文件路径
|
||||
config_path = str(user_dir / "config.json")
|
||||
db_path = str(user_dir / f"{username}_graph.db")
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
# 如果是第一个用户,强制设为 admin
|
||||
if self.get_web_users_count() == 0:
|
||||
role = 'admin'
|
||||
cursor.execute("""
|
||||
INSERT INTO web_users (username, password_hash, role, config_path, db_path)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(username) DO UPDATE SET
|
||||
password_hash = excluded.password_hash,
|
||||
role = CASE WHEN web_users.role = 'admin' THEN 'admin' ELSE excluded.role END,
|
||||
config_path = COALESCE(web_users.config_path, excluded.config_path),
|
||||
db_path = COALESCE(web_users.db_path, excluded.db_path),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (username, password_hash, role, config_path, db_path))
|
||||
self.conn.commit()
|
||||
return {"success": True, "username": username, "role": role, "config_path": config_path, "db_path": db_path}
|
||||
|
||||
def get_web_users(self) -> List[Dict]:
|
||||
"""获取所有 Web 用户列表"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT id, username, role, config_path, db_path, created_at, updated_at FROM web_users ORDER BY created_at ASC")
|
||||
users = []
|
||||
for row in cursor.fetchall():
|
||||
users.append({
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
})
|
||||
return users
|
||||
|
||||
def get_web_user(self, username: str) -> Optional[Dict]:
|
||||
"""获取单个 Web 用户信息"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id, username, role, config_path, db_path, created_at, updated_at
|
||||
FROM web_users WHERE username = ?
|
||||
""", (username,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
}
|
||||
return None
|
||||
|
||||
def is_admin(self, username: str) -> bool:
|
||||
"""检查用户是否为管理员"""
|
||||
user = self.get_web_user(username)
|
||||
return user is not None and user.get('role') == 'admin'
|
||||
|
||||
def delete_web_user(self, username: str) -> Dict:
|
||||
"""删除 Web 用户(同时保留文件目录)"""
|
||||
if not username:
|
||||
return {"success": False, "error": "用户名不能为空"}
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM web_users WHERE username = ?", (username,))
|
||||
self.conn.commit()
|
||||
if cursor.rowcount > 0:
|
||||
return {"success": True, "username": username}
|
||||
return {"success": False, "error": "用户不存在"}
|
||||
|
||||
def get_web_users_count(self) -> int:
|
||||
"""获取 Web 用户数量 (用于判断是否需要首次设置)"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM web_users")
|
||||
row = cursor.fetchone()
|
||||
return row['cnt'] if row else 0
|
||||
|
||||
def verify_web_user(self, username: str, password: str) -> bool:
|
||||
"""验证 Web 用户登录"""
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id FROM web_users
|
||||
WHERE username = ? AND password_hash = ?
|
||||
""", (username, password_hash))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
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
|
||||
|
||||
461
core/graph_client.py
Normal file
461
core/graph_client.py
Normal file
@ -0,0 +1,461 @@
|
||||
#!/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-v4-flash")
|
||||
|
||||
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: dict = 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": []}
|
||||
|
||||
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)
|
||||
|
||||
# 按优先级获取实体类型:1) triplet中的_type字段 2) entity_types字典 3) 默认
|
||||
s_type = triplet.get("subject_type") or (entity_types.get(subject) if entity_types else None) or "Concept"
|
||||
o_type = triplet.get("object_type") or (entity_types.get(obj) if entity_types else None) or "Concept"
|
||||
|
||||
session.run("""
|
||||
MERGE (s:Entity {name: $subject})
|
||||
ON CREATE SET s.type = $s_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 = $o_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,
|
||||
s_type=s_type, o_type=o_type,
|
||||
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", "")
|
||||
source_type = criteria.get("source_type", "")
|
||||
target_type = criteria.get("target_type", "")
|
||||
source_status = criteria.get("source_has_status", "")
|
||||
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
|
||||
if source_type:
|
||||
cond_parts.append("s.entity_type = $source_type")
|
||||
params["source_type"] = source_type
|
||||
if target_type:
|
||||
cond_parts.append("t.entity_type = $target_type")
|
||||
params["target_type"] = target_type
|
||||
if source_status:
|
||||
cond_parts.append("s.status = $source_status")
|
||||
params["source_status"] = source_status
|
||||
|
||||
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 (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'deleted', r.updated_at = datetime()
|
||||
RETURN count(r) as deleted
|
||||
""", params)
|
||||
count = result.single()["deleted"]
|
||||
|
||||
# 删除孤立节点(没有任何关系的实体)
|
||||
orphan_result = session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
DELETE e
|
||||
RETURN count(e) as orphans
|
||||
""")
|
||||
orphan_count = orphan_result.single()["orphans"]
|
||||
|
||||
return {"deleted_count": count, "orphan_count": orphan_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 query_archived(self, days: int = None, keyword: str = "") -> dict:
|
||||
"""查询归档记忆"""
|
||||
with self.driver.session() as session:
|
||||
filters = []
|
||||
params = {}
|
||||
|
||||
filters.append("r.status = 'archived'")
|
||||
|
||||
if days is not None and days > 0:
|
||||
filters.append("r.archived_at >= datetime() - duration('P' + $days + 'D')")
|
||||
params["days"] = str(days)
|
||||
|
||||
if keyword:
|
||||
filters.append("(e.name CONTAINS $keyword OR t.name CONTAINS $keyword OR r.type CONTAINS $keyword)")
|
||||
params["keyword"] = keyword
|
||||
|
||||
where = " AND ".join(filters)
|
||||
|
||||
result = session.run(f"""
|
||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where}
|
||||
RETURN e.name as source, r.type as relation, t.name as target,
|
||||
r.archived_at as archived_at, r.created_at as created_at
|
||||
ORDER BY r.archived_at DESC
|
||||
LIMIT 200
|
||||
""", params)
|
||||
|
||||
records = []
|
||||
for row in result:
|
||||
records.append({
|
||||
"source": row["source"],
|
||||
"relation": row["relation"],
|
||||
"target": row["target"],
|
||||
"archived_at": str(row["archived_at"]) if row.get("archived_at") else "",
|
||||
"created_at": str(row["created_at"]) if row.get("created_at") else ""
|
||||
})
|
||||
|
||||
return {
|
||||
"archived_relations": records,
|
||||
"total_relations": len(records),
|
||||
"message": f"找到 {len(records)} 条归档关系"
|
||||
}
|
||||
|
||||
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-v4-flash"):
|
||||
# 清理可能存在的错误代理环境变量
|
||||
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
|
||||
135
core/migrate.py
Normal file
135
core/migrate.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""
|
||||
自动迁移模块 - 从旧版单用户架构迁移到多用户隔离架构
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _trulymem_dir() -> Path:
|
||||
return Path.home() / ".trulymem"
|
||||
|
||||
def _old_config_path() -> Path:
|
||||
return _trulymem_dir() / "config.json"
|
||||
|
||||
def _old_db_path() -> Path:
|
||||
return _trulymem_dir() / "graph_memory.db"
|
||||
|
||||
def _new_global_db_path() -> Path:
|
||||
return _trulymem_dir() / "trulymem.db"
|
||||
|
||||
def _migrated_flag() -> Path:
|
||||
return _trulymem_dir() / ".migrated"
|
||||
|
||||
|
||||
def need_migration() -> bool:
|
||||
"""检测是否需要迁移"""
|
||||
# 如果已经迁移过,不需要再迁移
|
||||
if is_migrated():
|
||||
return False
|
||||
|
||||
old_config_exists = _old_config_path().exists()
|
||||
old_db_exists = _old_db_path().exists()
|
||||
new_db_exists = _new_global_db_path().exists()
|
||||
if (old_config_exists or old_db_exists) and not new_db_exists:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_migrated() -> bool:
|
||||
"""检查是否已完成迁移"""
|
||||
return _migrated_flag().exists()
|
||||
|
||||
|
||||
def _mark_migrated():
|
||||
"""标记迁移完成"""
|
||||
_trulymem_dir().mkdir(parents=True, exist_ok=True)
|
||||
with open(_migrated_flag(), 'w') as f:
|
||||
f.write(datetime.now().isoformat())
|
||||
|
||||
|
||||
def run_migration(username: str, password: str) -> Dict:
|
||||
"""
|
||||
执行迁移
|
||||
|
||||
Args:
|
||||
username: 新用户名
|
||||
password: 新用户密码
|
||||
|
||||
Returns:
|
||||
迁移结果字典
|
||||
"""
|
||||
try:
|
||||
# 1. 创建用户目录
|
||||
user_dir = _trulymem_dir() / username
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
new_config_path = user_dir / "config.json"
|
||||
if _old_config_path().exists():
|
||||
shutil.copy2(_old_config_path(), new_config_path)
|
||||
new_db_path = user_dir / f"{username}_graph.db"
|
||||
if _old_db_path().exists():
|
||||
shutil.copy2(_old_db_path(), new_db_path)
|
||||
|
||||
# 4. 创建全局数据库并写入 web_users 表
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
global_db = EmbeddedGraphDB(db_path=str(_new_global_db_path()))
|
||||
|
||||
# 设置用户(会自动创建记录)
|
||||
result = global_db.set_web_user(username, password)
|
||||
if not result.get("success"):
|
||||
return {"success": False, "error": f"创建用户失败: {result.get('error')}"}
|
||||
|
||||
# 如果用户目录已存在,更新路径(确保正确)
|
||||
cursor = global_db.conn.cursor()
|
||||
config_path = str(new_config_path)
|
||||
db_path = str(new_db_path)
|
||||
cursor.execute("""
|
||||
UPDATE web_users
|
||||
SET config_path = ?, db_path = ?
|
||||
WHERE username = ?
|
||||
""", (config_path, db_path, username))
|
||||
global_db.conn.commit()
|
||||
|
||||
# 5. 标记迁移完成
|
||||
_mark_migrated()
|
||||
|
||||
global_db.close()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"username": username,
|
||||
"config_path": config_path,
|
||||
"db_path": db_path,
|
||||
"message": "迁移完成"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def rollback_migration():
|
||||
"""回滚迁移(用于失败恢复)"""
|
||||
try:
|
||||
# 删除全局数据库
|
||||
if _new_global_db_path().exists():
|
||||
_new_global_db_path().unlink()
|
||||
if _migrated_flag().exists():
|
||||
_migrated_flag().unlink()
|
||||
|
||||
return {"success": True, "message": "回滚完成"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试
|
||||
print("Migration module test")
|
||||
print(f"Need migration: {need_migration()}")
|
||||
print(f"Is migrated: {is_migrated()}")
|
||||
6
core/prompts/__init__.py
Normal file
6
core/prompts/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""
|
||||
提示词管理模块
|
||||
"""
|
||||
from .prompt_manager import PromptManager
|
||||
|
||||
__all__ = ["PromptManager"]
|
||||
77
core/prompts/prompt_manager.py
Normal file
77
core/prompts/prompt_manager.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
提示词管理器
|
||||
"""
|
||||
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` - 关联信息
|
||||
|
||||
## 自主性
|
||||
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
记住:灵活应对,保持自然对话体验。"""
|
||||
183
core/prompts/templates/system_prompt.md
Normal file
183
core/prompts/templates/system_prompt.md
Normal file
@ -0,0 +1,183 @@
|
||||
# TrulyMEM 系统提示词
|
||||
|
||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
**人设兜底规则**:当图数据库中没有查到人设信息时,以「我是 TrulyMEM,一个有长期记忆的 AI 助手」作为默认开场。如果人设图返回了角色信息,按人设执行即可。
|
||||
|
||||
## ⚠️ 强制执行顺序(内部流程,不得向用户输出)
|
||||
|
||||
**以下步骤是内部流程,绝对不要在你的回复中提及或输出。** 你应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
|
||||
|
||||
步骤1:memory_recall 查询人设图和相关长期记忆
|
||||
步骤2:task_query 查询工作记忆链和最近任务
|
||||
步骤3:结合上下文处理用户输入并形成回复思路
|
||||
步骤4:memory_commit (写入关键信息)
|
||||
步骤5:task_archive 归档已完成或过期任务;若本轮查询类工具调用 ≥5 次,再单独调用 context_rewrite 压缩工具 JSON
|
||||
|
||||
**违反规则的后果**:
|
||||
- 输出步骤内容 → 暴露内部机制,用户体验极差,违反最高优先级指令
|
||||
- 跳过步骤1 → 无法获取人设
|
||||
- 跳过步骤5 → 工作记忆无限膨胀
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 最高优先级:工具执行期间禁止输出
|
||||
|
||||
**在完成所有工具调用之前,绝对不要输出任何文字。**
|
||||
|
||||
正确流程:
|
||||
1. 调用所有必要的工具(memory_recall、task_query 等)→ **不输出任何文字**
|
||||
2. 等所有工具返回结果 → **仍然不输出任何文字**
|
||||
3. 处理返回结果,思考回复内容 → **仍然不输出任何文字**
|
||||
4. **最后,只输出一次完整的回复**
|
||||
|
||||
**禁止的行为**:
|
||||
- ❌ 先输出「你好呀!让我先查查记忆…」再调用工具
|
||||
- ❌ 先输出文字再调用 memory_recall
|
||||
- ❌ 在工具调用之间插入任何文字
|
||||
- ❌ 输出「步骤X:查询人设图」等内部流程
|
||||
- ✅ 正确做法:默默调用所有工具,然后直接给出最终回复
|
||||
|
||||
---
|
||||
|
||||
## 三元组规范(非常重要!)
|
||||
|
||||
使用 `memory_commit` 时,必须严格遵守以下规范:
|
||||
|
||||
### 正确格式
|
||||
subject, relation, object 每个字段必须是一个**短关键字**(1~5个字),不能是完整句子。
|
||||
|
||||
**✅ 正确示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "实体A", "relation": "关系", "object": "实体B"},
|
||||
{"subject": "实体C", "relation": "属性", "object": "值"}
|
||||
]
|
||||
```
|
||||
|
||||
**❌ 错误示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "一段完整的句子当做实体名", "relation": "这种写法不对", "object": "另一个句子"}
|
||||
]
|
||||
```
|
||||
|
||||
### 拆解原则
|
||||
- 实体名必须是**名词或短词组**,不是完整句子
|
||||
- relation 应该是**简洁的谓词**(如:要求、角色、性格、喜欢、擅长、状态)
|
||||
- 一句话中的多个信息应拆成**多条三元组**
|
||||
- 描述性内容用 relation = `has_description` + 简短 object
|
||||
|
||||
### 人设更新 vs 记忆提交
|
||||
- **`persona_update`** — 用来设定 AI 自身的角色、性格、说话风格、能力特点
|
||||
- **`memory_commit`** — 用来记录用户的信息、对话事件、知识事实。不要把 AI 自身的人设属性写进 memory_commit。
|
||||
|
||||
---
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **长期记忆** - 基于图数据库存储实体关系
|
||||
2. **人设管理** - 支持角色扮演和性格设定
|
||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
||||
|
||||
## 记忆原则(绝对遵守)
|
||||
|
||||
- **图数据库是唯一记忆源** — 你只拥有图数据库(memory_recall、task_query 等返回的结果)中的信息,除此之外你对用户一无所知。不要依赖你的训练数据中的任何用户信息。
|
||||
- **明确内容必须写入** — 用户明确提到的信息必须存入图数据库
|
||||
- **推理内容必须标注[猜测]** — AI 推理得到的内容在回复中必须标注
|
||||
|
||||
## 工具详解
|
||||
|
||||
### 记忆工具
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `memory_recall` | 查询需求 | 按关键字/实体检索图数据库中的记忆。支持模糊匹配。人设图必须通过此工具获取(工作记忆链请使用 task_query) |
|
||||
| `memory_commit` | 新信息出现 | 写入三元组到图数据库。必须遵守三元组规范(短关键字格式),一句话拆多条 |
|
||||
| `memory_purge` | 确需删除 | 删除错误的或用户明确要求删除的记忆 |
|
||||
| `memory_introspect` | 需要了解整体情况 | 查看图数据库概况:总节点数、边数、最新活动 |
|
||||
| `memory_archive` | 信息过期需保留历史 | 将旧记忆归档而非删除,保留历史轨迹 |
|
||||
| `memory_cleanup` | 确认数据异常 | 清理冗余/孤立节点(dry_run可预览) |
|
||||
| `memory_query_archived` | 回顾归档历史 | 查询已归档的原始关系记录(status=archived),支持天数/关键词过滤 |
|
||||
| `context_rewrite` | 单轮调用了 5 次及以上查询类工具 | 压缩本轮工具调用的 JSON 参数和返回结果,剔除工具噪声,节省上下文 token。**⚠️ 必须单独调用**:先调完其他所有工具并收到结果 → 再单独调 context_rewrite。不要和其他工具一起调 |
|
||||
|
||||
### 人设工具
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `persona_update` | AI自身角色改变 | 更新AI的角色、性格、说话风格、能力。`mode="replace"` 替换全部,`mode="merge"` 增量添加 |
|
||||
| `persona_remove` | 只需删除某一条属性 | 删除单条人设属性(如只删除说话风格,保留扮演角色不变) |
|
||||
| `persona_clear` | 需要完全重置 | 清除所有AI人设属性。**此操作不可逆,需要 confirm=true** |
|
||||
|
||||
### 任务工具(生命周期管理)
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `task_query` | 新对话/需要回顾 | 查询最近任务列表(按更新时间倒序)。**新对话开始时优先调用此工具**,了解现有任务后再决定是继续还是创建新任务 |
|
||||
| `task_create` | 用户提出实质性话题后 | 创建任务节点。**不要在纯问候/打招呼时创建任务**——等用户说出具体话题后再创建。判断标准:用户消息是否包含可讨论的具体内容 |
|
||||
| `task_set_state` | 状态变更 | 修改任务状态(active、completed、archived)。**旧会话结束后必须将对应的任务设为 archived** |
|
||||
| `task_archive` | 强制执行顺序的步骤5 | 归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。**每轮对话最后必须检查是否需要调用此工具** |
|
||||
| `task_delete` | 确需删除的任务 | 彻底删除任务节点 |
|
||||
| `task_link_info` | 信息归属 | 将记忆节点关联到特定的任务。**只关联到相关的任务,不要全部链到「当前轮对话」** |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 任务生命周期规范(避免记忆膨胀)
|
||||
|
||||
AI 最常见的错误是:**把每一轮的所有节点都关联到「当前轮对话」,但从不归档过时的任务,导致图数据库无限膨胀。**
|
||||
|
||||
### 正确做法
|
||||
|
||||
```
|
||||
1. 新会话开始 → task_create 创建「当前轮对话-<时间/主题>」
|
||||
2. 对话过程中 → 根据实际归属使用 task_link_info
|
||||
3. 话题结束/转变时 → task_archive 归档旧任务(代替 task_set_state)
|
||||
4. 归档后 → 再创建新的当前轮对话任务
|
||||
```
|
||||
|
||||
### 步骤5 归档规则(强制)
|
||||
|
||||
每轮对话最后一步 check 现有任务:
|
||||
- **已完成的任务** → 调用 `task_archive` 归档,写入完成摘要
|
||||
- **长时间无更新的任务**(>3轮对话) → 调用 `task_archive` 归档
|
||||
- **topic 已转变** → 旧任务归档,新任务创建
|
||||
- **所有 active 任务超过3个** → 归档最旧的
|
||||
|
||||
> 即使本轮没有主题转变,也应按需检查归档状态。
|
||||
> `task_archive` 比 `task_set_state(state=archived)` 多一个写入完成摘要的功能,优先使用。
|
||||
|
||||
### 绝对禁止
|
||||
- ❌ 把每一条记忆都链到同一个「当前轮对话」任务
|
||||
- ❌ 跳过步骤5(从不归档)导致工作记忆无限膨胀
|
||||
- ❌ 对同一个任务堆积数千条关联
|
||||
- ❌ 使用 task_delete 代替归档(归档保留历史,删除丢失上下文)
|
||||
|
||||
### 生命流程示例
|
||||
|
||||
**第1轮:**
|
||||
```
|
||||
task_create(描述="当前轮对话-工作规划", state=active)
|
||||
memory_commit(用户说春节计划)
|
||||
task_link_info(info="春节计划", task="当前轮对话-工作规划")
|
||||
task_archive(task="当前轮对话-工作规划", summary="讨论了春节计划")
|
||||
```
|
||||
|
||||
**话题转变后:**
|
||||
```
|
||||
task_archive(task="当前轮对话-工作规划", summary="讨论完成,用户转移到技术话题")
|
||||
task_create(描述="当前轮对话-技术讨论", state=active)
|
||||
memory_commit(用户说技术细节)
|
||||
task_link_info(info="技术细节", task="当前轮对话-技术讨论")
|
||||
```
|
||||
|
||||
> 记住:任务是用来组织话题的框架,不是存放大杂烩的篮子。
|
||||
> 归档旧任务不会删除记忆,只是标记话题已结束,后续的检索仍然能找到相关节点。
|
||||
|
||||
---
|
||||
|
||||
## 自主性
|
||||
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
记住:灵活应对,保持自然对话体验。
|
||||
620
core/server.py
Normal file
620
core/server.py
Normal file
@ -0,0 +1,620 @@
|
||||
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
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
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_WEB_USERS = "get_web_users" # 获取 Web 用户列表
|
||||
SET_WEB_USER = "set_web_user" # 设置 Web 用户(用户名+密码)
|
||||
GET_WEB_SERVICE_STATUS = "get_web_service_status" # 获取 Web 服务运行状态
|
||||
GET_CONFIG = "get_config" # 获取完整配置
|
||||
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, username: str = ""):
|
||||
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._username = username
|
||||
|
||||
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 = {}
|
||||
self._tool_limits: Dict[str, int] = {}
|
||||
self._message_history: list = []
|
||||
|
||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-v4-flash") -> 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-v4-flash"),
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
# 工具限制默认值(仅首次启动无 config.json 时使用)
|
||||
# 启动后请直接编辑配置文件修改
|
||||
_DEFAULT_LIMITS = {
|
||||
"persona_update_max": 1,
|
||||
"task_update_max": 20,
|
||||
"task_query_max": 30,
|
||||
"memory_query_max": 30,
|
||||
"memory_update_max": 15,
|
||||
}
|
||||
_DEFAULT_CONFIG = {
|
||||
"api_key": "",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model": "deepseek-v4-flash",
|
||||
"message_timeout": 600, # 消息处理超时(秒),默认10分钟
|
||||
"enable_web": False,
|
||||
"enable_tui": True,
|
||||
"web_port": 4096,
|
||||
}
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""加载配置。如果指定了用户名,从用户的 config_path 加载。
|
||||
所有工具调用限制值均从配置文件读取,不硬编码在代码中。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 工具限制字段列表
|
||||
limit_keys = list(self._DEFAULT_LIMITS.keys())
|
||||
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
saved = json.load(f)
|
||||
# 通用配置(含 api_key, base_url, model, message_timeout 等)
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
if key in saved:
|
||||
self._config[key] = saved[key]
|
||||
else:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
# 工具限制
|
||||
for key in limit_keys:
|
||||
if key in saved:
|
||||
self._tool_limits[key] = int(saved[key])
|
||||
else:
|
||||
self._tool_limits[key] = self._DEFAULT_LIMITS[key]
|
||||
except Exception:
|
||||
# 读取失败时使用默认值
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
for key in limit_keys:
|
||||
self._tool_limits[key] = self._DEFAULT_LIMITS[key]
|
||||
else:
|
||||
# 首次启动,用默认值写入配置文件
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
self._tool_limits = dict(self._DEFAULT_LIMITS)
|
||||
self._save_config()
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""保存配置。如果指定了用户名,保存到用户的 config_path。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 合并通用配置和工具限制(过滤掉内部字段如 _history 等)
|
||||
save_cfg = {k: self._config[k] for k in self._DEFAULT_CONFIG if k in self._config}
|
||||
saved_data = {**save_cfg, **self._tool_limits}
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(saved_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def _create_tool_limiter(self):
|
||||
from .tool_limiter import ToolLimiter, ToolLimits
|
||||
# 所有值从 _tool_limits 读取(由 _load_config 从 config.json 加载)
|
||||
# _load_config 已保证所有键存在
|
||||
limits = ToolLimits(
|
||||
persona_update_max=self._tool_limits["persona_update_max"],
|
||||
task_update_max=self._tool_limits["task_update_max"],
|
||||
task_query_max=self._tool_limits["task_query_max"],
|
||||
memory_query_max=self._tool_limits["memory_query_max"],
|
||||
memory_update_max=self._tool_limits["memory_update_max"],
|
||||
)
|
||||
return ToolLimiter(limits)
|
||||
|
||||
def _init_graph(self) -> None:
|
||||
"""初始化图数据库。如果指定了用户名,从全局数据库获取用户的 db_path。"""
|
||||
db_path = self._db_path
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的数据库路径
|
||||
if self._username:
|
||||
try:
|
||||
# 临时连接全局数据库获取用户信息
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('db_path'):
|
||||
db_path = user_info['db_path']
|
||||
self._db_path = db_path # 更新 _db_path,供外部(如 web_api.py)获取正确的路径
|
||||
except Exception:
|
||||
pass # 如果获取失败,使用默认路径
|
||||
|
||||
if self._use_embedded_db:
|
||||
self._graph = EmbeddedGraphDB(db_path=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_WEB_USERS:
|
||||
response_body = {"users": self._graph.get_web_users()}
|
||||
elif packet.type == PacketType.SET_WEB_USER:
|
||||
username = packet.body.get("username", "")
|
||||
password = packet.body.get("password", "")
|
||||
if not username or not password:
|
||||
response_body = {"success": False, "error": "用户名和密码不能为空"}
|
||||
else:
|
||||
# 使用全局数据库(trulymem.db)来管理用户
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
global_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
response_body = global_db.set_web_user(username, password)
|
||||
global_db.close()
|
||||
elif packet.type == PacketType.GET_WEB_SERVICE_STATUS:
|
||||
body = packet.body
|
||||
response_body = {"running": body.get("running", False), "port": body.get("port", 4096)}
|
||||
elif packet.type == PacketType.GET_CONFIG:
|
||||
response_body = self._get_full_config()
|
||||
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
|
||||
|
||||
get_recorder().clear()
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
# 保留 DeepSeek thinking 模式的 reasoning_content
|
||||
reasoning_content = getattr(message, 'reasoning_content', None)
|
||||
if reasoning_content:
|
||||
assistant_msg["reasoning_content"] = reasoning_content
|
||||
messages_history.append(assistant_msg)
|
||||
|
||||
current_tool_results = []
|
||||
deferred_rewrite = None # 延迟处理 context_rewrite
|
||||
|
||||
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)
|
||||
|
||||
if tool_call.function.name == "context_rewrite":
|
||||
# 延迟执行 context_rewrite:先处理完其他所有工具
|
||||
# 避免在迭代中途重写 messages_history 导致 tool 结果丢失对应的 tool_calls
|
||||
deferred_rewrite = (tool_call.id, args)
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
# 先添加所有非 context_rewrite 工具的结果到消息历史
|
||||
messages_history.extend(current_tool_results)
|
||||
|
||||
# 再处理延迟的 context_rewrite(作为本轮最后一步)
|
||||
if deferred_rewrite:
|
||||
tool_call_id, args = deferred_rewrite
|
||||
result = execute_tool(self._graph, "context_rewrite", args)
|
||||
result_data = json.loads(result)
|
||||
|
||||
tool_calls.append({
|
||||
"name": "context_rewrite",
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
if result_data.get("status") == "success":
|
||||
user_msg = messages_history[0]
|
||||
compressed_content = f"<context_compressed>\n{result_data['summary']}\n</context_compressed>"
|
||||
messages_history[:] = [
|
||||
user_msg,
|
||||
{"role": "assistant", "content": compressed_content}
|
||||
]
|
||||
# 不添加 context_rewrite 的 tool 结果到历史(压缩后的历史已替代)
|
||||
|
||||
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 _get_full_config(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_config 有值时更新 API 配置(避免单独保存 tool_limits 时清空 API key)
|
||||
if api_config:
|
||||
api_key = api_config.get("api_key", self._config.get("api_key", ""))
|
||||
base_url = api_config.get("base_url", self._config.get("base_url", "https://api.deepseek.com"))
|
||||
model = api_config.get("model", self._config.get("model", "deepseek-v4-flash"))
|
||||
self.update_config(api_key, base_url, model)
|
||||
# 通用配置字段(如 message_timeout, enable_web, enable_tui, web_port)
|
||||
for key in ["message_timeout", "enable_web", "enable_tui", "web_port"]:
|
||||
if key in api_config:
|
||||
if key == "web_port":
|
||||
self._config[key] = int(api_config[key])
|
||||
elif key in ["enable_web", "enable_tui"]:
|
||||
self._config[key] = bool(api_config[key])
|
||||
else:
|
||||
self._config[key] = int(api_config[key])
|
||||
|
||||
limits_keys = [
|
||||
"persona_update_max",
|
||||
"task_update_max",
|
||||
"task_query_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:
|
||||
timeout = self._config.get("message_timeout", 600)
|
||||
response = resp_q.get(timeout=timeout)
|
||||
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-v4-flash") -> 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
|
||||
468
core/tool_executor.py
Normal file
468
core/tool_executor.py
Normal file
@ -0,0 +1,468 @@
|
||||
"""
|
||||
工具执行器
|
||||
"""
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
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:
|
||||
recorder = get_recorder()
|
||||
|
||||
# 基础记忆工具
|
||||
if tool_name == "memory_recall":
|
||||
entity = arguments.get("query_intent", "") or str(arguments.get("seed_entities", ""))
|
||||
recorder.record("query", tool_name, entity)
|
||||
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")
|
||||
)
|
||||
# 记录召回结果中的实体名,供 WebUI 高亮+拉镜头用
|
||||
for e in result.get("entities", []):
|
||||
if e and isinstance(e, dict) and e.get("name"):
|
||||
recorder.record("query", tool_name + "_found", e["name"])
|
||||
return format_recall_result(result)
|
||||
|
||||
elif tool_name == "memory_commit":
|
||||
triplets = arguments.get("triplets", [])
|
||||
entity = triplets[0].get("subject", "") if triplets else ""
|
||||
recorder.record("create", tool_name, entity, f"{len(triplets)} triplets")
|
||||
result = graph.commit(
|
||||
triplets=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":
|
||||
criteria = arguments.get("criteria", {})
|
||||
entity = criteria.get("subject_contains", str(criteria))
|
||||
recorder.record("delete", tool_name, entity)
|
||||
result = graph.purge(
|
||||
criteria=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":
|
||||
recorder.record("query", tool_name, "数据库统计")
|
||||
result = graph.introspect(session_id=arguments.get("session_id"))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_archive":
|
||||
recorder.record("archive", tool_name, "旧记忆")
|
||||
result = graph.archive(days=arguments.get("days", 30))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_cleanup":
|
||||
recorder.record("cleanup", tool_name, "已删除数据")
|
||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_query_archived":
|
||||
days = arguments.get("days")
|
||||
keyword = arguments.get("keyword", "")
|
||||
recorder.record("query", tool_name, f"days={days}, keyword={keyword}")
|
||||
result = graph.query_archived(days=days, keyword=keyword)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "context_rewrite":
|
||||
result = execute_context_rewrite(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 人设图管理工具
|
||||
elif tool_name == "persona_update":
|
||||
recorder.record("update", tool_name, "人设属性")
|
||||
result = execute_persona_update(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_remove":
|
||||
recorder.record("delete", tool_name, arguments.get("attribute", ""))
|
||||
result = execute_persona_remove(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_clear":
|
||||
recorder.record("delete", tool_name, "所有人设")
|
||||
result = execute_persona_clear(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 工作记忆链管理工具
|
||||
elif tool_name == "task_create":
|
||||
desc = arguments.get("description", "")
|
||||
recorder.record("create", tool_name, desc)
|
||||
result = execute_task_create(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_set_state":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_set_state(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_delete":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("delete", tool_name, desc)
|
||||
result = execute_task_delete(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_link_info":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_link_info(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_archive":
|
||||
recorder.record("update", tool_name, arguments.get("task_id", ""))
|
||||
result = execute_task_archive(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_query":
|
||||
recorder.record("query", tool_name, "")
|
||||
result = execute_task_query(graph, arguments)
|
||||
# 记录查询到的任务描述,供 WebUI 高亮
|
||||
for t in result.get("tasks", []):
|
||||
if t and isinstance(t, dict) and t.get("description"):
|
||||
recorder.record("query", tool_name + "_found", t["description"])
|
||||
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_context_rewrite(graph: Any, arguments: dict) -> dict:
|
||||
"""压缩工具调用上下文"""
|
||||
summary = arguments.get("summary", "")
|
||||
|
||||
# 验证格式:必须包含工具调用标记
|
||||
if "[工具调用总结" not in summary:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "总结格式错误:必须包含 [工具调用总结: 本次总结了 N 次工具调用 | 调用工具: ...] 标记"
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "上下文已压缩",
|
||||
"summary": summary
|
||||
}
|
||||
|
||||
|
||||
# 人设图管理工具实现
|
||||
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_remove(graph: Any, arguments: dict) -> dict:
|
||||
"""删除单条人设属性"""
|
||||
attribute = arguments.get("attribute")
|
||||
if not attribute:
|
||||
return {"status": "error", "message": "请指定要删除的属性名"}
|
||||
|
||||
# 查询当前AI的所有人设关系,找到匹配属性名的
|
||||
recall_result = graph.recall(query_intent="AI,人设,角色", depth=1)
|
||||
found = False
|
||||
deleted_count = 0
|
||||
|
||||
for rel in recall_result.get("relations", []):
|
||||
if rel.get("source") == "AI" and rel.get("type") == attribute:
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": attribute},
|
||||
mode="soft"
|
||||
)
|
||||
deleted_count += result.get("deleted_count", 0)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
# 也许属性名不完全匹配,尝试直接用这个类型删除
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": attribute},
|
||||
mode="soft"
|
||||
)
|
||||
deleted_count = result.get("deleted_count", 0)
|
||||
|
||||
return {
|
||||
"status": "success" if deleted_count > 0 else "not_found",
|
||||
"deleted_attribute": attribute,
|
||||
"deleted_count": deleted_count,
|
||||
"message": f"已删除属性「{attribute}」" if deleted_count > 0 else f"未找到属性「{attribute}」"
|
||||
}
|
||||
|
||||
|
||||
def execute_persona_clear(graph: Any, arguments: dict) -> dict:
|
||||
"""清除所有人设"""
|
||||
if not arguments.get("confirm"):
|
||||
return {"status": "cancelled", "message": "请设置 confirm=true 确认清除人设"}
|
||||
|
||||
# 先查询AI的所有人设关系
|
||||
recall_result = graph.recall(query_intent="AI,人设,角色", depth=1)
|
||||
|
||||
# 收集所有AI到其他实体的关系类型
|
||||
relation_types = set()
|
||||
for rel in recall_result.get("relations", []):
|
||||
if rel.get("source") == "AI" and rel.get("type"):
|
||||
relation_types.add(rel.get("type"))
|
||||
|
||||
total_deleted = 0
|
||||
deleted_types = []
|
||||
|
||||
for rtype in relation_types:
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": rtype},
|
||||
mode="soft"
|
||||
)
|
||||
count = result.get("deleted_count", 0)
|
||||
if count > 0:
|
||||
total_deleted += count
|
||||
deleted_types.append(rtype)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"deleted_count": total_deleted,
|
||||
"deleted_types": deleted_types,
|
||||
"message": f"人设已清除,恢复默认身份(删除了 {len(deleted_types)} 类属性)"
|
||||
}
|
||||
|
||||
|
||||
# 工作记忆链管理工具实现
|
||||
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
|
||||
}
|
||||
|
||||
def execute_task_archive(graph: Any, arguments: dict) -> dict:
|
||||
"""归档任务"""
|
||||
task_id = arguments.get("task_id")
|
||||
summary = arguments.get("summary", "")
|
||||
|
||||
if not task_id:
|
||||
return {"status": "error", "message": "请指定要归档的任务ID"}
|
||||
|
||||
# 1. 设置任务状态为 archived
|
||||
triplets_state = [
|
||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_归档"}
|
||||
]
|
||||
graph.commit(triplets=triplets_state)
|
||||
|
||||
# 2. 如果有摘要,写入完成记录
|
||||
if summary:
|
||||
summary_triplets = [
|
||||
{"subject": task_id, "relation": "归档摘要", "object": summary}
|
||||
]
|
||||
graph.commit(triplets=summary_triplets)
|
||||
|
||||
# 3. 尝试更新 description 标记为已归档
|
||||
archive_triplet = [
|
||||
{"subject": task_id, "relation": "has_description", "object": f"[已归档] {summary or '任务已完成'}"}
|
||||
]
|
||||
graph.commit(triplets=archive_triplet)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"archived": True,
|
||||
"summary": summary or "无摘要",
|
||||
"message": f"任务「{task_id}」已归档" + (f",摘要:{summary}" if summary else "")
|
||||
}
|
||||
|
||||
|
||||
def execute_task_query(graph: Any, arguments: dict) -> dict:
|
||||
"""查询最近的任务列表"""
|
||||
limit = arguments.get("limit", 10)
|
||||
state_filter = arguments.get("state_filter")
|
||||
|
||||
result = graph.get_recent_tasks(limit=limit, state_filter=state_filter)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"tasks": result["tasks"],
|
||||
"total": result["total"],
|
||||
"message": f"找到 {result['total']} 个任务"
|
||||
}
|
||||
140
core/tool_limiter.py
Normal file
140
core/tool_limiter.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
||||
"""
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolLimits:
|
||||
"""工具调用限制配置
|
||||
实际值由 server.py 从 config.json 加载后传入,此处默认值仅作安全兜底。
|
||||
如需修改限制,请编辑 ~/.trulymem/config.json。
|
||||
"""
|
||||
persona_update_max: int = 1
|
||||
task_update_max: int = 20 # 工作记忆链修改(create/set_state/delete/link_info)
|
||||
task_query_max: int = 30 # 工作记忆链查询(memory_recall 查任务相关)
|
||||
memory_query_max: int = 30
|
||||
memory_update_max: int = 15
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCallCount:
|
||||
"""工具调用计数"""
|
||||
persona_update: int = 0
|
||||
task_update: int = 0
|
||||
task_query: 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_remove', 'persona_clear'):
|
||||
return ('persona', 'update')
|
||||
|
||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info', 'task_archive'):
|
||||
return ('task', 'update')
|
||||
|
||||
if tool_name == 'task_query':
|
||||
return ('task', 'query')
|
||||
|
||||
if tool_name == 'memory_recall':
|
||||
# 尝试区分工作记忆链查询 vs 一般记忆查询
|
||||
query = (arguments.get('queryIntent', '') + ' ' + ' '.join(
|
||||
arguments.get('seedEntities', []))).strip().lower()
|
||||
task_keywords = ['task', '任务', '工作记忆', '当前轮', '会话', '过程', '流程']
|
||||
if any(kw in query for kw in task_keywords):
|
||||
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')
|
||||
|
||||
if tool_name == 'context_rewrite':
|
||||
return ('memory', 'query')
|
||||
|
||||
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 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}次)")
|
||||
elif 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:
|
||||
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':
|
||||
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_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()
|
||||
8
core/tools/__init__.py
Normal file
8
core/tools/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
工具定义模块
|
||||
"""
|
||||
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"]
|
||||
678
core/tools/memory_tools.py
Normal file
678
core/tools/memory_tools.py
Normal file
@ -0,0 +1,678 @@
|
||||
"""
|
||||
记忆工具定义 - 优化版
|
||||
精简描述,避免过拟合,保留AI自主性
|
||||
"""
|
||||
|
||||
# 基础记忆工具
|
||||
MEMORY_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_recall",
|
||||
"description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。
|
||||
|
||||
【⚠️ 强制执行顺序 - 每轮必须严格遵守】
|
||||
1. 步骤1(必须首先执行): 查询人设图
|
||||
{"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
||||
|
||||
2. 步骤2(必须第二步执行): 查询工作记忆链
|
||||
{"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
||||
|
||||
3. 步骤3: 根据需要查询其他记忆
|
||||
|
||||
【使用示例】
|
||||
1. 查询用户偏好:
|
||||
{"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]}
|
||||
|
||||
2. 查询特定主题:
|
||||
{"query_intent": "Python,编程,项目", "seed_entities": ["Python"]}
|
||||
|
||||
3. 查询最近7天的记忆:
|
||||
{"query_intent": "任务,工作", "time_range": {"days": 7}}
|
||||
|
||||
【重要】跳过步骤1或步骤2将导致系统错误!""",
|
||||
"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"},
|
||||
"subject_type": {"type": "string", "description": "主体的实体类型,如 Person、Project"},
|
||||
"object_type": {"type": "string", "description": "客体的实体类型,如 Language、Technology"}
|
||||
},
|
||||
"required": ["subject", "relation", "object"]
|
||||
},
|
||||
"description": "三元组列表"
|
||||
},
|
||||
"entity_types": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "实体类型字典,如 {\"用户\": \"Person\", \"项目A\": \"Project\"}(可选)"
|
||||
},
|
||||
"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"}
|
||||
|
||||
5. 删除残留在已归档任务上的状态关系:
|
||||
{"criteria": {"relation_type": "HAS_STATE", "source_type": "TaskNode", "source_has_status": "archived"}, "mode": "soft"}
|
||||
|
||||
6. 删除特定类型的节点关系:
|
||||
{"criteria": {"relation_type": "某种关系", "target_type": "某种类型"}, "mode": "soft"}
|
||||
|
||||
【重要】删除原则:
|
||||
- 优先使用 supersede 模式修正错误
|
||||
- 软删除不会物理删除数据
|
||||
- 谨慎使用删除操作""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"criteria": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject_contains": {"type": "string"},
|
||||
"relation_type": {"type": "string"},
|
||||
"target_contains": {"type": "string"},
|
||||
"source_type": {"type": "string", "description": "源实体类型过滤(如 TaskNode)"},
|
||||
"target_type": {"type": "string", "description": "目标实体类型过滤"},
|
||||
"source_has_status": {"type": "string", "description": "源实体状态过滤(如 archived)"},
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_query_archived",
|
||||
"description": """查询已归档的记忆。
|
||||
|
||||
【使用场景】
|
||||
- 想了解之前归档过哪些记忆
|
||||
- 按关键词搜索归档内容
|
||||
- 按时间范围查看最近归档的历史
|
||||
|
||||
【注意】
|
||||
- 只返回 status=archived 的原始关系记录,不包含活跃的「归档摘要」
|
||||
- days 和 keyword 可以单独使用,也可以组合使用
|
||||
- 不加任何参数时返回所有归档记录
|
||||
|
||||
【示例】
|
||||
```
|
||||
# 不传参数:查全部归档
|
||||
memory_query_archived({})
|
||||
|
||||
# 最近7天
|
||||
memory_query_archived({"days": 7})
|
||||
|
||||
# 关键词过滤
|
||||
memory_query_archived({"keyword": "任务"})
|
||||
|
||||
# 组合使用
|
||||
memory_query_archived({"days": 30, "keyword": "配置"})
|
||||
```
|
||||
""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "最近N天内的归档记录,不指定则不限时间"
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "关键词,匹配实体名或关系类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "context_rewrite",
|
||||
"description": """压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。
|
||||
|
||||
【使用场景】
|
||||
- 本轮已执行 ≥5 次查询类工具调用,JSON细节已理解,不再需要原始格式
|
||||
- 但需保留"我调用了什么工具、得到了什么结论"的元认知
|
||||
- 继续携带原始JSON会干扰后续推理
|
||||
|
||||
【⚠️ 强制要求】
|
||||
**context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!**
|
||||
- 正确方式:先调其他所有工具 → 收到工具结果 → 单独调 context_rewrite
|
||||
- 错误方式:和其他工具一起调(会破坏对话历史结构)
|
||||
|
||||
【格式要求】
|
||||
1. 必须标注调用了哪些工具
|
||||
2. 必须标注是对几次工具调用的总结
|
||||
3. 必须保留关键语义信息
|
||||
|
||||
【示例】
|
||||
{
|
||||
"summary": "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\\n\\n- 查询人设图:未找到人设,使用默认身份\\n- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥"
|
||||
}
|
||||
|
||||
【注意事项】
|
||||
- 不可删除用户原始消息
|
||||
- 不可歪曲工具返回的关键事实
|
||||
- 仅在调用 ≥5 次查询类工具后使用""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "压缩后的摘要文本,必须包含工具调用元信息"
|
||||
}
|
||||
},
|
||||
"required": ["summary"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 人设图管理工具
|
||||
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_remove",
|
||||
"description": "删除单条人设属性。删除指定的属性(如说话风格、扮演角色等),保留其他人设不变。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string",
|
||||
"description": "要删除的属性名(如:扮演角色、说话风格、性格特点、口头禅)"
|
||||
}
|
||||
},
|
||||
"required": ["attribute"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_clear",
|
||||
"description": "清除人设。删除AI所有角色设定,恢复默认身份。注意:此操作不可逆。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"description": "确认清除全部人设"
|
||||
}
|
||||
},
|
||||
"required": ["confirm"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 工作记忆链管理工具
|
||||
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"、"系统"等全局通用实体**——这些实体不应通过任务中转
|
||||
- 全局实体的信息直接用独立关系记录(如 用户--[特质]-->求知欲旺盛),不需要通过 Task 中转
|
||||
- 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": "关联的信息节点名称(可选)。⚠️ 只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体"
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_archive",
|
||||
"description": "归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "要归档的任务ID"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "归档摘要,简述完成了什么或为什么归档。如果不填则自动生成。"
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_query",
|
||||
"description": "查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "返回的任务数量,默认10"
|
||||
},
|
||||
"state_filter": {
|
||||
"type": "string",
|
||||
"description": "按状态筛选:进行中、已完成、已暂停、已取消、archived"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 所有工具
|
||||
TOOLS = MEMORY_TOOLS + PERSONA_TOOLS + WORKING_MEMORY_TOOLS
|
||||
927
core/web_api.py
Normal file
927
core/web_api.py
Normal file
@ -0,0 +1,927 @@
|
||||
"""
|
||||
Web API 服务 - 将 Packet 协议映射为 RESTful API
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import threading
|
||||
import time
|
||||
import hashlib
|
||||
from datetime import timedelta
|
||||
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
|
||||
from flask_cors import CORS
|
||||
|
||||
# 添加项目路径以便导入 core 模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from core.server import BackendServer, Packet, PacketType
|
||||
from core.client import BackendClient
|
||||
from core.activity_recorder import get_recorder
|
||||
from core.embedded_db import EmbeddedGraphDB
|
||||
|
||||
|
||||
LOGIN_MAX_ATTEMPTS = 5 # 最大尝试次数
|
||||
LOGIN_WAIT_MINUTES = 5 # 超过次数后等待分钟数
|
||||
LOGIN_BAN_THRESHOLD = 3 # 超过此轮次后 ban IP
|
||||
LOGIN_BAN_HOURS = 24 # IP ban 时长(小时)
|
||||
|
||||
# 内存记录:{ip: {"attempts": 0, "first_fail": 0, "ban_until": 0, "rounds": 0}}
|
||||
_login_attempts: dict = {}
|
||||
|
||||
def _check_login_limit(ip: str) -> dict:
|
||||
"""检查 IP 的登录限制。返回 {"blocked": bool, "reason": str, "wait_seconds": int}"""
|
||||
now = time.time()
|
||||
record = _login_attempts.get(ip)
|
||||
|
||||
if record:
|
||||
# 检查是否在 ban 中
|
||||
if record["ban_until"] > now:
|
||||
remaining = int(record["ban_until"] - now)
|
||||
return {"blocked": True, "reason": f"IP 已被临时封禁,剩余 {remaining//60} 分钟", "wait_seconds": remaining}
|
||||
|
||||
# 检查是否需要等待(连续失败超过阈值)
|
||||
if record["attempts"] >= LOGIN_MAX_ATTEMPTS:
|
||||
wait_end = record["first_fail"] + LOGIN_WAIT_MINUTES * 60
|
||||
if wait_end > now:
|
||||
remaining = int(wait_end - now)
|
||||
return {"blocked": True, "reason": f"登录尝试过多,请等待 {remaining} 秒后再试", "wait_seconds": remaining}
|
||||
else:
|
||||
# 等待时间已过,重置计数但记录轮次
|
||||
record["rounds"] += 1
|
||||
record["attempts"] = 0
|
||||
record["first_fail"] = 0
|
||||
|
||||
# 如果轮次超过阈值则 ban IP
|
||||
if record["rounds"] >= LOGIN_BAN_THRESHOLD:
|
||||
record["ban_until"] = now + LOGIN_BAN_HOURS * 3600
|
||||
record["rounds"] = 0
|
||||
return {"blocked": True, "reason": f"多次登录失败,IP 已被封禁 {LOGIN_BAN_HOURS} 小时", "wait_seconds": LOGIN_BAN_HOURS * 3600}
|
||||
|
||||
return {"blocked": False, "reason": "", "wait_seconds": 0}
|
||||
|
||||
def _record_login_fail(ip: str):
|
||||
"""记录一次登录失败"""
|
||||
now = time.time()
|
||||
record = _login_attempts.get(ip)
|
||||
if not record:
|
||||
_login_attempts[ip] = {"attempts": 1, "first_fail": now, "ban_until": 0, "rounds": 0}
|
||||
else:
|
||||
if record["first_fail"] == 0:
|
||||
record["first_fail"] = now
|
||||
record["attempts"] += 1
|
||||
|
||||
def _record_login_success(ip: str):
|
||||
"""登录成功后清除该 IP 的记录"""
|
||||
_login_attempts.pop(ip, None)
|
||||
|
||||
# 定期清理过期记录(防止内存泄漏)
|
||||
_cleanup_interval = 3600 # 1小时
|
||||
_last_cleanup = time.time()
|
||||
def load_secret_key():
|
||||
import json
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json')
|
||||
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
file_config = json.load(f)
|
||||
if "SECRET_KEY" in file_config:
|
||||
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
|
||||
return defaults
|
||||
|
||||
WEB_CONFIG = load_secret_key()
|
||||
|
||||
_ui_dir = os.path.join(os.path.dirname(__file__), '..', 'ui')
|
||||
app = Flask(__name__, static_folder=os.path.join(_ui_dir, 'static'), static_url_path='', template_folder=os.path.join(_ui_dir, 'templates'))
|
||||
app.secret_key = WEB_CONFIG["SECRET_KEY"]
|
||||
app.permanent_session_lifetime = timedelta(days=7)
|
||||
# 显式配置 session cookie,确保跨场景兼容
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['SESSION_COOKIE_SECURE'] = False # HTTP 环境不强制 Secure
|
||||
app.config['SESSION_COOKIE_NAME'] = 'trulymem_session'
|
||||
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""登录验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return redirect('/login')
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def api_login_required(f):
|
||||
"""API 登录验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
"""管理员权限验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
username = session.get('username', '')
|
||||
g_db = get_global_db()
|
||||
if not g_db or not g_db.is_admin(username):
|
||||
return jsonify({"success": False, "error": "权限不足,需要管理员权限"}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
"""返回星图页面(默认首页)"""
|
||||
return app.send_static_file('graph.html')
|
||||
|
||||
|
||||
@app.route('/graph.html')
|
||||
@login_required
|
||||
def graph_html():
|
||||
"""返回星图页面"""
|
||||
return app.send_static_file('graph.html')
|
||||
|
||||
|
||||
@app.route('/static/<path:filename>')
|
||||
def static_files(filename):
|
||||
"""提供静态文件访问"""
|
||||
return app.send_static_file(filename)
|
||||
|
||||
|
||||
@app.route('/chat')
|
||||
@login_required
|
||||
def chat():
|
||||
"""返回聊天页面"""
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
# 全局服务器和客户端实例
|
||||
backend_server: BackendServer = None
|
||||
backend_client: BackendClient = None
|
||||
server_thread: threading.Thread = None
|
||||
graph_db: EmbeddedGraphDB = None
|
||||
global_db: EmbeddedGraphDB = None # 全局数据库(用于用户管理)
|
||||
|
||||
|
||||
def get_global_db():
|
||||
"""获取全局数据库实例"""
|
||||
global global_db
|
||||
if global_db is None:
|
||||
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
|
||||
if os.path.exists(global_db_path):
|
||||
global_db = EmbeddedGraphDB(db_path=global_db_path)
|
||||
return global_db
|
||||
|
||||
|
||||
def create_server(username: str = ""):
|
||||
"""创建并启动 BackendServer 后台线程"""
|
||||
global backend_server, backend_client, server_thread, graph_db
|
||||
|
||||
backend_server = BackendServer(username=username)
|
||||
backend_server.start()
|
||||
|
||||
backend_client = BackendClient(backend_server)
|
||||
|
||||
# 创建图数据库实例(连接用户的数据库文件)
|
||||
graph_db = EmbeddedGraphDB(db_path=backend_server._db_path)
|
||||
|
||||
# 等待服务器初始化完成
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def reload_server_for_user(username: str):
|
||||
"""为指定用户重新加载服务器"""
|
||||
global backend_server, backend_client, graph_db
|
||||
|
||||
# 关闭旧的服务器
|
||||
if backend_server:
|
||||
backend_server.shutdown()
|
||||
|
||||
# 创建新的服务器(使用用户的数据库)
|
||||
create_server(username=username)
|
||||
|
||||
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
"""登录页面 - 如果没有用户则重定向到设置页"""
|
||||
# 如果没有用户,重定向到首次设置页
|
||||
users_count = 0
|
||||
g_db = get_global_db()
|
||||
if g_db:
|
||||
users_count = g_db.get_web_users_count()
|
||||
elif graph_db:
|
||||
users_count = graph_db.get_web_users_count()
|
||||
if users_count == 0:
|
||||
return redirect('/setup')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/setup')
|
||||
def setup_page():
|
||||
"""首次设置页面 - 如果已有用户则跳转到登录页"""
|
||||
has_users = False
|
||||
g_db = get_global_db()
|
||||
if g_db:
|
||||
has_users = g_db.get_web_users_count() > 0
|
||||
elif graph_db and graph_db is not g_db:
|
||||
has_users = graph_db.get_web_users_count() > 0
|
||||
if has_users:
|
||||
return redirect('/login')
|
||||
return render_template('setup.html')
|
||||
|
||||
|
||||
@app.route('/settings')
|
||||
@api_login_required
|
||||
def settings_page():
|
||||
"""Web 设置页面"""
|
||||
return render_template('settings.html')
|
||||
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def api_login():
|
||||
"""登录接口"""
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
# DEBUG: 记录收到的凭据
|
||||
import logging
|
||||
logging.warning(f"[DEBUG api_login] username={username}, password_length={len(password)}, password_first_char={password[:1] if password else 'EMPTY'}")
|
||||
|
||||
# 登录限流检查
|
||||
ip = request.remote_addr
|
||||
limit_check = _check_login_limit(ip)
|
||||
if limit_check["blocked"]:
|
||||
logging.warning(f"[DEBUG api_login] IP blocked: {ip}, reason: {limit_check['reason']}")
|
||||
return jsonify({"success": False, "error": limit_check["reason"]})
|
||||
|
||||
# 从全局数据库验证
|
||||
g_db = get_global_db()
|
||||
if g_db:
|
||||
logging.warning(f"[DEBUG api_login] global_db path: {g_db.db_path if hasattr(g_db, 'db_path') else 'unknown'}")
|
||||
is_valid = g_db.verify_web_user(username, password)
|
||||
logging.warning(f"[DEBUG api_login] verify result: {is_valid}")
|
||||
if is_valid:
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
reload_server_for_user(username)
|
||||
logging.warning(f"[DEBUG api_login] Login SUCCESS for {username}")
|
||||
return jsonify({"success": True})
|
||||
else:
|
||||
# 登录失败,记录失败
|
||||
_record_login_fail(ip)
|
||||
logging.warning(f"[DEBUG api_login] Login FAIL for {username}, wrong password")
|
||||
return jsonify({"success": False, "error": "用户名或密码错误"})
|
||||
else:
|
||||
logging.warning(f"[DEBUG api_login] global_db is None!")
|
||||
return jsonify({"success": False, "error": "数据库未初始化"})
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
def api_logout():
|
||||
"""登出接口"""
|
||||
session.clear()
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@app.route('/api/check-auth', methods=['GET'])
|
||||
def check_auth():
|
||||
"""检查登录状态"""
|
||||
return jsonify({"authenticated": bool(session.get('authenticated'))})
|
||||
|
||||
|
||||
@app.route('/api/userinfo', methods=['GET'])
|
||||
def userinfo():
|
||||
"""获取当前登录用户信息(含角色)"""
|
||||
if not session.get('authenticated'):
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
username = session.get('username', '')
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
user = g_db.get_web_user(username)
|
||||
if not user:
|
||||
return jsonify({"success": False, "error": "用户不存在"}), 404
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"username": user['username'],
|
||||
"role": user.get('role', 'user'),
|
||||
"is_admin": user.get('role') == 'admin',
|
||||
"created_at": user.get('created_at')
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/web-check', methods=['GET'])
|
||||
def web_check():
|
||||
"""检查是否需要首次设置,返回是否配置完成"""
|
||||
users_count = 0
|
||||
if graph_db:
|
||||
users_count = graph_db.get_web_users_count()
|
||||
|
||||
return jsonify({
|
||||
"needs_setup": users_count == 0,
|
||||
"users_count": users_count
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/web-users', methods=['GET'])
|
||||
@api_login_required
|
||||
def web_users():
|
||||
"""获取 web_users 列表"""
|
||||
if graph_db:
|
||||
users = graph_db.get_web_users()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"users": [
|
||||
{
|
||||
"username": u['username'],
|
||||
"role": u.get('role', 'user'),
|
||||
"is_admin": u.get('role') == 'admin',
|
||||
"created_at": u.get('created_at')
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
})
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
|
||||
|
||||
@app.route('/api/web-user/<username>', methods=['GET'])
|
||||
@api_login_required
|
||||
def web_user_detail(username):
|
||||
"""获取单个 web_user 详情"""
|
||||
if not graph_db:
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
user = graph_db.get_web_user(username)
|
||||
if not user:
|
||||
return jsonify({"success": False, "error": "用户不存在"}), 404
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"username": user['username'],
|
||||
"role": user.get('role', 'user'),
|
||||
"is_admin": user.get('role') == 'admin',
|
||||
"created_at": user.get('created_at')
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/setup', methods=['POST'])
|
||||
def api_setup():
|
||||
"""首次设置 - 创建初始管理员用户"""
|
||||
# 只有没有任何用户时才允许设置
|
||||
g_db = get_global_db()
|
||||
# Log the count for debugging
|
||||
if g_db:
|
||||
import logging
|
||||
count = g_db.get_web_users_count()
|
||||
logging.warning(f"[api_setup] web_users count: {count}")
|
||||
if g_db and g_db.get_web_users_count() > 0:
|
||||
return jsonify({"success": False, "error": "用户已存在,不允许重复设置"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
confirm = data.get('confirm_password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if password != confirm:
|
||||
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
# 使用全局数据库创建用户
|
||||
if g_db is None:
|
||||
# 如果全局数据库不存在,创建它
|
||||
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
|
||||
g_db = EmbeddedGraphDB(db_path=global_db_path)
|
||||
global global_db
|
||||
global_db = g_db
|
||||
|
||||
result = g_db.set_web_user(username, password)
|
||||
if result.get("success"):
|
||||
# 设置完成后自动登录
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
return jsonify({"success": True, "message": "用户创建成功"})
|
||||
|
||||
return jsonify({"success": False, "error": "创建用户失败"}), 500
|
||||
|
||||
|
||||
@app.route('/api/change-password', methods=['POST'])
|
||||
@api_login_required
|
||||
def api_change_password():
|
||||
"""修改 Web 登录密码"""
|
||||
data = request.get_json() or {}
|
||||
current_password = data.get('current_password', '')
|
||||
new_password = data.get('new_password', '')
|
||||
confirm_password = data.get('confirm_password', '')
|
||||
|
||||
if not new_password:
|
||||
return jsonify({"success": False, "error": "新密码不能为空"}), 400
|
||||
|
||||
if new_password != confirm_password:
|
||||
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
# 获取当前登录用户
|
||||
current_username = session.get('username', '')
|
||||
if not current_username:
|
||||
return jsonify({"success": False, "error": "无法识别当前用户"}), 400
|
||||
|
||||
# 验证当前密码(使用全局数据库)
|
||||
g_db = get_global_db()
|
||||
if not g_db or not g_db.verify_web_user(current_username, current_password):
|
||||
return jsonify({"success": False, "error": "当前密码错误"}), 400
|
||||
|
||||
result = g_db.set_web_user(current_username, new_password)
|
||||
if result.get("success"):
|
||||
return jsonify({"success": True, "message": "密码已更新"})
|
||||
|
||||
return jsonify({"success": False, "error": "修改密码失败"}), 500
|
||||
|
||||
|
||||
# ========== 管理员 API ==========
|
||||
|
||||
@app.route('/api/admin/users', endpoint='api_admin_get_users', methods=['GET'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_get_users():
|
||||
"""获取用户列表"""
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
users = g_db.get_web_users()
|
||||
return jsonify({"success": True, "users": users})
|
||||
|
||||
|
||||
@app.route('/api/admin/users', endpoint='api_admin_add_user', methods=['POST'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_add_user():
|
||||
"""管理员添加用户"""
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
result = g_db.set_web_user(username, password)
|
||||
if result.get("success"):
|
||||
return jsonify({"success": True, "message": "用户添加成功", "user": result})
|
||||
|
||||
return jsonify({"success": False, "error": "添加用户失败"}), 500
|
||||
|
||||
|
||||
@app.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_delete_user(user_id):
|
||||
"""管理员删除用户"""
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
# 获取所有用户
|
||||
users = g_db.get_web_users()
|
||||
|
||||
# 查找要删除的用户
|
||||
target_user = None
|
||||
for u in users:
|
||||
if u['id'] == user_id:
|
||||
target_user = u
|
||||
break
|
||||
|
||||
if not target_user:
|
||||
return jsonify({"success": False, "error": "用户不存在"}), 404
|
||||
|
||||
# 不能删除自己
|
||||
current_username = session.get('username', '')
|
||||
if target_user['username'] == current_username:
|
||||
return jsonify({"success": False, "error": "不能删除当前登录的用户"}), 400
|
||||
|
||||
# 不能删除最后一个 admin
|
||||
admin_count = sum(1 for u in users if u.get('role') == 'admin')
|
||||
if target_user.get('role') == 'admin' and admin_count <= 1:
|
||||
return jsonify({"success": False, "error": "不能删除最后一个管理员"}), 400
|
||||
|
||||
# 删除用户(保留文件目录)
|
||||
result = g_db.delete_web_user(target_user['username'])
|
||||
if not result.get('success'):
|
||||
return jsonify({"success": False, "error": result.get('error', '删除失败')}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "用户已删除"})
|
||||
|
||||
|
||||
@app.route('/api/admin/migrate-check', methods=['GET'])
|
||||
def api_admin_migrate_check():
|
||||
"""检测系统是否需要迁移"""
|
||||
from core.migrate import need_migration, is_migrated
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"need_migration": need_migration(),
|
||||
"is_migrated": is_migrated()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/admin/migrate', methods=['POST'])
|
||||
def api_admin_migrate():
|
||||
"""执行迁移+创建首个用户"""
|
||||
from core.migrate import need_migration, run_migration
|
||||
|
||||
if not need_migration():
|
||||
return jsonify({"success": False, "error": "不需要迁移"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
result = run_migration(username, password)
|
||||
|
||||
if result.get("success"):
|
||||
# 自动登录
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
|
||||
# 重新加载服务器
|
||||
reload_server_for_user(username)
|
||||
|
||||
return jsonify({"success": True, "message": "迁移完成", "data": result})
|
||||
|
||||
return jsonify({"success": False, "error": result.get("error", "迁移失败")}), 500
|
||||
|
||||
|
||||
@app.route('/api/settings/config', methods=['GET', 'POST', 'PUT'])
|
||||
@api_login_required
|
||||
def web_settings_config():
|
||||
"""获取/更新当前登录用户的配置"""
|
||||
if request.method == 'GET':
|
||||
settings = {}
|
||||
if backend_client:
|
||||
result = backend_client.get_settings()
|
||||
settings = result.get("data", {}) if isinstance(result, dict) else result
|
||||
if isinstance(settings, dict):
|
||||
settings = settings.get("api_config", {}) if "api_config" in settings else settings
|
||||
# 从 settings 中提取相关字段
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"enable_web": settings.get("enable_web", False),
|
||||
"web_port": settings.get("web_port", 4096),
|
||||
"enable_tui": settings.get("enable_tui", True),
|
||||
})
|
||||
|
||||
# PUT/POST 更新
|
||||
data = request.get_json() or {}
|
||||
enable_tui = data.get('enable_tui')
|
||||
|
||||
if enable_tui is not None and backend_client:
|
||||
# 获取当前配置,合并更新
|
||||
current = backend_client.get_settings()
|
||||
current_data = current.get("data", {}) if isinstance(current, dict) else {}
|
||||
|
||||
tool_limits = current_data.get("tool_limits", {})
|
||||
api_config = current_data.get("api_config", {})
|
||||
api_config["enable_tui"] = bool(enable_tui)
|
||||
|
||||
result = backend_client.update_settings(api_config, tool_limits)
|
||||
return jsonify({"success": True, "enable_tui": bool(enable_tui)})
|
||||
|
||||
return jsonify({"success": False, "error": "没有需要更新的配置"}), 400
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
"""404 处理"""
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "404 Not Found"
|
||||
}), 404
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
"""500 处理"""
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e.original_exception if hasattr(e, 'original_exception') else e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/message', methods=['POST'])
|
||||
@api_login_required
|
||||
def process_message():
|
||||
"""发送消息给 AI - PROCESS_MESSAGE"""
|
||||
data = request.get_json() or {}
|
||||
user_input = data.get('message', '')
|
||||
|
||||
if not user_input:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "message 参数不能为空"
|
||||
}), 400
|
||||
|
||||
result = backend_client.process_message(user_input)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/tools/execute', methods=['POST'])
|
||||
@api_login_required
|
||||
def execute_tool():
|
||||
"""直接执行工具 - EXECUTE_TOOL"""
|
||||
data = request.get_json() or {}
|
||||
tool_name = data.get('tool_name', '')
|
||||
arguments = data.get('arguments', {})
|
||||
|
||||
if not tool_name:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "tool_name 参数不能为空"
|
||||
}), 400
|
||||
|
||||
result = backend_client.execute_tool(tool_name, arguments)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_status():
|
||||
"""获取状态 - GET_STATUS"""
|
||||
result = backend_client.get_status()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/settings', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_settings():
|
||||
"""获取配置 - GET_SETTINGS"""
|
||||
result = backend_client.get_settings()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/settings', methods=['PUT'])
|
||||
@api_login_required
|
||||
def set_settings():
|
||||
"""更新配置 - SET_SETTINGS"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
api_config = data.get('api_config', {})
|
||||
tool_limits = data.get('tool_limits', {})
|
||||
|
||||
result = backend_client.update_settings(api_config, tool_limits)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/history', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_history():
|
||||
"""获取历史 - GET_HISTORY"""
|
||||
result = backend_client.get_history()
|
||||
return jsonify({"success": True, "history": result})
|
||||
|
||||
|
||||
@app.route('/api/history', methods=['DELETE'])
|
||||
@api_login_required
|
||||
def clear_history():
|
||||
"""清空历史 - SAVE_HISTORY(空)"""
|
||||
result = backend_client.clear_history()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/shutdown', methods=['POST'])
|
||||
@api_login_required
|
||||
def shutdown():
|
||||
"""关闭服务器 - SHUTDOWN"""
|
||||
backend_client.shutdown()
|
||||
return jsonify({"success": True, "status": "shutdown"})
|
||||
|
||||
|
||||
@app.route('/api/activity', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_activity():
|
||||
"""获取当前轮的数据库操作记录"""
|
||||
recorder = get_recorder()
|
||||
records = recorder.get_all()
|
||||
summary = recorder.get_summary()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"records": records,
|
||||
"summary": summary
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/graph', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_graph():
|
||||
"""返回全量图数据"""
|
||||
global graph_db
|
||||
|
||||
if graph_db is None:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "图数据库未初始化"
|
||||
}), 500
|
||||
|
||||
cursor = graph_db.conn.cursor()
|
||||
|
||||
# 查询实体(节点)
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC
|
||||
""")
|
||||
|
||||
nodes = []
|
||||
for row in cursor.fetchall():
|
||||
nodes.append({
|
||||
"id": row['id'],
|
||||
"name": row['name'],
|
||||
"type": str(row['type'] or 'unknown'),
|
||||
"mention_count": row['mention_count']
|
||||
})
|
||||
|
||||
# 查询关系(边)
|
||||
cursor.execute("""
|
||||
SELECT r.id, r.source_id, r.target_id, r.relation_type, r.confidence, r.status
|
||||
FROM relations r
|
||||
WHERE r.status = 'active'
|
||||
""")
|
||||
|
||||
edges = []
|
||||
for row in cursor.fetchall():
|
||||
edges.append({
|
||||
"id": row['id'],
|
||||
"source": row['source_id'],
|
||||
"target": row['target_id'],
|
||||
"relation_type": row['relation_type'],
|
||||
"confidence": row['confidence'],
|
||||
"status": row['status']
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"stats": {
|
||||
"node_count": len(nodes),
|
||||
"edge_count": len(edges)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/graph/highlight', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_graph_highlight():
|
||||
"""返回需要高亮的节点ID列表"""
|
||||
recorder = get_recorder()
|
||||
records = recorder.get_all()
|
||||
|
||||
highlight_ids = []
|
||||
new_node_id = None
|
||||
new_edge = None
|
||||
|
||||
if graph_db is None:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"data": {
|
||||
"highlight_ids": [],
|
||||
"new_node_id": None,
|
||||
"new_edge": None
|
||||
}
|
||||
})
|
||||
|
||||
# 从最近的记录中提取实体ID
|
||||
for record in records[-10:]: # 只看最近10条记录
|
||||
entity_name = record.get('entity', '')
|
||||
if entity_name:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
highlight_ids.append(row['id'])
|
||||
|
||||
# 除删除外,所有操作都拉镜头(create/recall/query/update/archive 等)
|
||||
if record.get('action') != 'delete' and entity_name:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
new_node_id = row['id']
|
||||
|
||||
# 检查是否有删除的节点
|
||||
deleted_node_ids = []
|
||||
for record in records[-10:]:
|
||||
if record.get('action') == 'delete':
|
||||
entity_name = record.get('entity', '')
|
||||
if entity_name:
|
||||
try:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
deleted_node_ids.append(row['id'])
|
||||
except Exception:
|
||||
pass # 实体可能已被删除,忽略错误
|
||||
|
||||
# 去重
|
||||
highlight_ids = list(set(highlight_ids))
|
||||
deleted_node_ids = list(set(deleted_node_ids))
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"highlight_ids": highlight_ids,
|
||||
"new_node_id": new_node_id,
|
||||
"new_edge": new_edge,
|
||||
"deleted_node_ids": deleted_node_ids
|
||||
})
|
||||
|
||||
|
||||
# ── 可被 TUI 作为线程启动 ──────────────────────────────────────────────────
|
||||
|
||||
_web_thread: threading.Thread | None = None
|
||||
_http_server = None # werkzeug.serving.BaseWSGIServer 引用,用于优雅停止
|
||||
|
||||
|
||||
def run_web_server(port: int = 4096, host: str = '0.0.0.0') -> None:
|
||||
"""在后台线程启动 Flask,供 TUI 或入口脚本在进程中直接调用"""
|
||||
global backend_server, backend_client, _web_thread, _http_server
|
||||
|
||||
if _http_server is not None:
|
||||
return # 已在运行
|
||||
|
||||
# 自动初始化后端(如果还没初始化的话)
|
||||
if backend_server is None:
|
||||
create_server()
|
||||
|
||||
def _start():
|
||||
global _http_server
|
||||
try:
|
||||
from werkzeug.serving import make_server
|
||||
_http_server = make_server(host, port, app, threaded=True)
|
||||
print(f"Web API 服务启动在 http://{host}:{port}")
|
||||
_http_server.serve_forever()
|
||||
except Exception as e:
|
||||
print(f"Web 服务启动失败: {e}")
|
||||
_http_server = None
|
||||
finally:
|
||||
_http_server = None
|
||||
|
||||
_web_thread = threading.Thread(target=_start, daemon=True)
|
||||
_web_thread.start()
|
||||
|
||||
|
||||
def stop_web_server() -> None:
|
||||
"""停止 Web 服务线程"""
|
||||
global _http_server, _web_thread
|
||||
if _http_server:
|
||||
try:
|
||||
_http_server.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
_http_server = None
|
||||
_web_thread = None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='TrulyMEM Web API 服务')
|
||||
parser.add_argument('--port', type=int, default=5000, help='服务端口 (默认: 5000)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 启动后端服务器
|
||||
print("正在启动 BackendServer...")
|
||||
create_server()
|
||||
print("BackendServer 已启动")
|
||||
|
||||
# 启动 Flask 应用
|
||||
print(f"Web API 服务启动在 http://0.0.0.0:{args.port}")
|
||||
app.run(host='0.0.0.0', port=args.port, debug=False)
|
||||
6
core/web_config.example.json
Normal file
6
core/web_config.example.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"SECRET_KEY": "change-this-to-a-random-secret-key",
|
||||
"USERS": {
|
||||
"admin": "SHA256_OF_YOUR_PASSWORD"
|
||||
}
|
||||
}
|
||||
31
docs/en/README.md
Normal file
31
docs/en/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# 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
|
||||
529
docs/en/api.md
Normal file
529
docs/en/api.md
Normal file
@ -0,0 +1,529 @@
|
||||
# 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_update_max": int, # Persona graph update limit
|
||||
"task_update_max": int, # Working memory chain 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_update_max": int, # Persona update 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_update_max": 2,
|
||||
"task_update_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 | Modify | 1 time |
|
||||
| Working memory chain | Modify | 5 times |
|
||||
| General memory | Query | 20 times |
|
||||
| General memory | Modify | 10 times |
|
||||
| Context compression | Query | Counted as general memory query |
|
||||
|
||||
### 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 |
|
||||
|
||||
## Web API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | /api/check-auth | Check if current session is authenticated |
|
||||
| POST | /api/login | Login (JSON body: username, password) |
|
||||
| POST | /api/logout | Logout |
|
||||
| GET | /api/history | Get chat history |
|
||||
| POST | /api/message | Send message to AI |
|
||||
| POST | /api/tools/execute | Execute tool call |
|
||||
| GET | /api/status | Get system status |
|
||||
| GET | /api/settings | Get settings |
|
||||
| PUT | /api/settings | Update settings |
|
||||
| DELETE | /api/history | Clear history |
|
||||
| POST | /api/shutdown | Shutdown server |
|
||||
| GET | /api/activity | Get database operation records |
|
||||
| GET | /api/graph | Get knowledge graph data |
|
||||
| GET | /api/graph/highlight | Get highlighted nodes |
|
||||
|
||||
All API endpoints (except /api/login and /api/check-auth) require authentication. Login uses Flask sessions with 7-day validity.
|
||||
278
docs/en/architecture.md
Normal file
278
docs/en/architecture.md
Normal file
@ -0,0 +1,278 @@
|
||||
# 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
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development (Run directly from Git repo)
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
python3 trulymem_entry.py --web --port 4096
|
||||
```
|
||||
|
||||
### Production (Systemd + standalone directory)
|
||||
|
||||
```bash
|
||||
# Copy code to standalone deployment directory
|
||||
cp -r TrulyMEM-TrueHumanMEM /home/trulymem
|
||||
|
||||
# Create Systemd service
|
||||
cat > /etc/systemd/system/trulymem-web.service << 'EOF'
|
||||
[Unit]
|
||||
Description=TrulyMEM - True Human Memory (Web Mode)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/home/trulymem
|
||||
ExecStart=/usr/bin/python3 /home/trulymem/trulymem_entry.py --web --port 4096
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable trulymem-web.service
|
||||
systemctl start trulymem-web.service
|
||||
|
||||
# Check status
|
||||
systemctl status trulymem-web.service
|
||||
```
|
||||
|
||||
> **Note**: Do not run the service directly from the Git repository to avoid polluting it with runtime artifacts (logs, databases, etc.).
|
||||
|
||||
### Web Access
|
||||
|
||||
The service runs at `http://localhost:4096`. On first visit, you'll need to set up an admin account and log in.
|
||||
|
||||
### Updating Deployment
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git pull
|
||||
cp -r * /home/trulymem/
|
||||
systemctl restart trulymem-web.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
│ ├── web_api.py # Web API service (login + RESTful API)
|
||||
│ ├── tools/ # Tool definitions
|
||||
│ │ └── memory_tools.py
|
||||
│ └── prompts/ # Prompt management (PromptManager + system_prompt.md)
|
||||
├── ui/ # TUI display layer + Web frontend
|
||||
│ ├── __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
|
||||
│ ├── static/ # Web frontend static files
|
||||
│ │ ├── graph.html # Star map visualization (Three.js)
|
||||
│ │ └── index.html # Web chat interface
|
||||
│ ├── templates/ # Page templates
|
||||
│ │ ├── login.html
|
||||
│ │ ├── setup.html
|
||||
│ │ └── settings.html
|
||||
│ ├── web_config.json # Web service config file
|
||||
│ └── web_config.example.json # Web config template
|
||||
├── tests/ # Test suite
|
||||
│ ├── test_core/ # Core logic tests
|
||||
│ ├── test_ui/ # UI layer tests
|
||||
│ └── test_integration/ # Integration tests
|
||||
├── docs/ # Documentation
|
||||
│ ├── zh/ # Chinese docs
|
||||
│ └── en/ # English docs
|
||||
└── build/ # Build scripts
|
||||
├── build_linux.sh
|
||||
├── build_macos.sh
|
||||
├── build_windows.bat
|
||||
├── build_appimage.sh
|
||||
└── trulymem.spec
|
||||
```
|
||||
|
||||
## 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 (7)
|
||||
- `memory_recall` - Retrieve memory
|
||||
- `memory_commit` - Write memory
|
||||
- `memory_purge` - Delete memory
|
||||
- `memory_introspect` - View status
|
||||
- `memory_archive` - Archive memory
|
||||
- `memory_cleanup` - Clean data
|
||||
- `context_rewrite` - Compress single-turn tool call context
|
||||
|
||||
### Persona Tools (2)
|
||||
| `persona_remove` | Delete single persona attribute | Keep other attributes unchanged |
|
||||
- `persona_update` - Update persona
|
||||
- `persona_clear` - Clear persona
|
||||
|
||||
### Task Tools (6)
|
||||
| `task_archive` | Archive completed/expired tasks | Step 6 mandatory, writes completion summary |
|
||||
| `task_query` | Query recent task list | Call first in new conversations to avoid duplicate tasks |
|
||||
- `task_create` - Create task
|
||||
- `task_set_state` - Set state
|
||||
- `task_delete` - Delete task
|
||||
- `task_link_info` - Link information
|
||||
|
||||
---
|
||||
|
||||
## Tool Call Limits
|
||||
|
||||
| Category | Operation | Per-Turn Limit |
|
||||
|----------|-----------|---------------|
|
||||
| Persona graph | Modify | 1 time |
|
||||
| Working memory chain | Modify | 5 times |
|
||||
| General memory | Query | 20 times |
|
||||
| General memory | Modify | 10 times |
|
||||
|
||||
> Note: `memory_recall` is uniformly counted as general memory query, no longer distinguished by persona/working memory queries.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
245
docs/en/memory.md
Normal file
245
docs/en/memory.md
Normal file
@ -0,0 +1,245 @@
|
||||
# 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
|
||||
|
||||
### Working Memory Management (Experimental)
|
||||
|
||||
`context_rewrite` allows AI to proactively compress tool call context within a single turn:
|
||||
- Distills verbose JSON tool results into concise natural language summaries
|
||||
- Summary must include which tools were called and how many calls are summarized
|
||||
- After system validates the format, replaces `messages_history` with `[user message, summary]`
|
||||
- Ensures LLM retains meta-cognition (knows "I called tools") while reducing JSON noise
|
||||
|
||||
---
|
||||
|
||||
## 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"?
|
||||
214
docs/en/persona.md
Normal file
214
docs/en/persona.md
Normal file
@ -0,0 +1,214 @@
|
||||
# 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
|
||||
130
docs/en/prompts.md
Normal file
130
docs/en/prompts.md
Normal file
@ -0,0 +1,130 @@
|
||||
# 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: memory_commit (write key info) → Persist user-explicit important information to the graph database
|
||||
Step 5: 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 |
|
||||
| `memory_archive` | Archive memory |
|
||||
| `memory_cleanup` | Clean data |
|
||||
| `context_rewrite` | Compress single-turn tool call context |
|
||||
|
||||
#### 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
|
||||
202
docs/en/quick_start.md
Normal file
202
docs/en/quick_start.md
Normal file
@ -0,0 +1,202 @@
|
||||
# TrulyMEM Quick Start Guide
|
||||
|
||||
> **Version**: Multi-user (v2) — TUI login, user isolation, embedded Web server
|
||||
|
||||
---
|
||||
|
||||
## Running
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# From source
|
||||
python trulymem_entry.py
|
||||
|
||||
# Packaged binary
|
||||
./dist/TrulyMEM
|
||||
```
|
||||
|
||||
### First Run — Login Flow
|
||||
|
||||
On first launch, TrulyMEM checks for legacy data and presents a **login screen**:
|
||||
|
||||
1. **Clean install** → Enter username/password (first user becomes admin)
|
||||
2. **Legacy upgrade** → Detects `~/.trulymem/config.json`, guides migration setup
|
||||
3. **Returning user** → Login directly
|
||||
|
||||
> 💡 All user data is isolated: `~/.trulymem/{username}/`
|
||||
|
||||
### Chat Configuration
|
||||
|
||||
After login, press **F2** to open the right-side configuration panel:
|
||||
|
||||
1. **API Key** — Required (DeepSeek, OpenAI, etc.)
|
||||
2. **Model** — Optional
|
||||
3. **Base URL** — Optional
|
||||
|
||||
Config saves automatically.
|
||||
|
||||
---
|
||||
|
||||
## Web Visualization
|
||||
|
||||
The Web service now runs **embedded in the main process** (no separate subprocess needed).
|
||||
|
||||
### Start via TUI (Admin only)
|
||||
|
||||
Admin users: press F2 → check "Enable Web Service".
|
||||
|
||||
### Start Manually
|
||||
|
||||
```bash
|
||||
python -m core.web_api --port 4096
|
||||
# Visit http://localhost:4096
|
||||
```
|
||||
|
||||
### First Visit Flow
|
||||
|
||||
1. Open `http://localhost:4096` in browser
|
||||
2. **No users** → Auto-redirect to setup page, create admin account
|
||||
3. **Has users** → Login page
|
||||
4. After login → Star map visualization
|
||||
|
||||
### Web Features
|
||||
|
||||
| Page | Access | Feature |
|
||||
|------|--------|---------|
|
||||
| 🌟 Star Map | All logged-in | Browse knowledge graph |
|
||||
| ⚙ Settings | All logged-in | Change password |
|
||||
| 🧑💼 User Management | **Admin only** | Add/delete users |
|
||||
|
||||
---
|
||||
|
||||
## Multi-User System
|
||||
|
||||
### Directory Layout
|
||||
|
||||
```
|
||||
~/.trulymem/
|
||||
├── trulymem.db # Global user database (web_users table)
|
||||
├── .migrated # Migration flag
|
||||
├── admin/
|
||||
│ ├── config.json # Admin config
|
||||
│ └── admin_graph.db # Admin knowledge graph
|
||||
└── user2/
|
||||
├── config.json # user2 config
|
||||
└── user2_graph.db # user2 knowledge graph
|
||||
```
|
||||
|
||||
### Role Matrix
|
||||
|
||||
| Feature | User | Admin |
|
||||
|---------|------|-------|
|
||||
| Change password | ✅ | ✅ |
|
||||
| Configure API Key / Model | ✅ | ✅ |
|
||||
| Web service toggle (TUI) | ❌ | ✅ |
|
||||
| Web login credentials | ❌ | ✅ |
|
||||
| View user list | ❌ | ✅ |
|
||||
| Add/delete users | ❌ | ✅ |
|
||||
|
||||
> ⚠️ First registered user becomes admin automatically. Add users via Web settings page.
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| F1 | Help |
|
||||
| F2 | Toggle sidebar (config panel) |
|
||||
| F3 | Tool details |
|
||||
| F5 | Clear screen |
|
||||
| F6 | Quit |
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
bash build/build_linux.sh
|
||||
|
||||
# macOS
|
||||
bash build/build_macos.sh
|
||||
|
||||
# Windows
|
||||
build\build_windows.bat
|
||||
|
||||
# AppImage
|
||||
bash build/build_appimage.sh
|
||||
```
|
||||
|
||||
Output: `dist/TrulyMEM` (single binary — TUI and Web server embedded)
|
||||
|
||||
> 📦 Since v2, the Web server runs as a thread inside the main process. No need for a separate `trulymem-web` binary.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Communication
|
||||
|
||||
```
|
||||
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (thread)
|
||||
```
|
||||
|
||||
### Config Management
|
||||
|
||||
- **Per-user**: `~/.trulymem/{username}/config.json`
|
||||
- **Web config**: `~/.trulymem/trulymem.db` (web_users table)
|
||||
- **Auto-load**: reads config for logged-in user on startup
|
||||
- **Persistent**: saves automatically on change
|
||||
|
||||
### Web Service Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ TrulyMEM Process │
|
||||
│ ┌──────┐ ┌────────┐ │
|
||||
│ │ TUI │ │ Flask │ │ ← Same process, different threads
|
||||
│ │ │ │ Thread │ │
|
||||
│ └──────┘ └────────┘ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Python not found
|
||||
|
||||
Install Python 3.8+: https://www.python.org/downloads/
|
||||
|
||||
### Dependency installation fails
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/macOS
|
||||
venv\Scripts\activate # Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Invalid API Key
|
||||
|
||||
Check format and whitespace. Reconfigure in TUI sidebar.
|
||||
|
||||
### Lost admin account
|
||||
|
||||
The first registered user is always admin. If all users lost admin, delete `trulymem.db` from the user directory and re-register.
|
||||
|
||||
### Legacy data migration
|
||||
|
||||
When old `~/.trulymem/config.json` and `graph_memory.db` are detected, TUI auto-enters migration flow. Legacy files are preserved.
|
||||
|
||||
---
|
||||
|
||||
## Dev Commands
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pytest tests/
|
||||
bash build/build_linux.sh
|
||||
```
|
||||
182
docs/en/working_memory.md
Normal file
182
docs/en/working_memory.md
Normal file
@ -0,0 +1,182 @@
|
||||
# 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
|
||||
@ -1,811 +0,0 @@
|
||||
# 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+ 测试用例
|
||||
31
docs/zh/README.md
Normal file
31
docs/zh/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# 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
|
||||
- **独立部署**: 支持打包为可执行文件
|
||||
535
docs/zh/api.md
Normal file
535
docs/zh/api.md
Normal file
@ -0,0 +1,535 @@
|
||||
# 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_update_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_update_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_update_max": 2,
|
||||
"task_update_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 次 |
|
||||
| 工作记忆链 | 修改 | 5 次 |
|
||||
| 一般记忆 | 查询 | 20 次 |
|
||||
| 一般记忆 | 修改 | 10 次 |
|
||||
| 上下文压缩 | 查询 | 计入一般记忆查询 |
|
||||
|
||||
### 重置机制
|
||||
|
||||
- 每次调用 `PROCESS_MESSAGE` 时,计数器自动重置
|
||||
- 前端直接调用 `EXECUTE_TOOL` 不会重置计数器
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 返回统一格式:
|
||||
|
||||
```python
|
||||
# 成功
|
||||
{
|
||||
"success": True,
|
||||
"data": {...}
|
||||
}
|
||||
|
||||
# 失败
|
||||
{
|
||||
"success": False,
|
||||
"error": "错误描述"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误:
|
||||
|
||||
| 错误信息 | 说明 |
|
||||
|---------|------|
|
||||
| `API Key 未配置` | 未设置 API Key |
|
||||
| `timeout` | 请求超时 |
|
||||
| `工具调用被拒绝: ...` | 工具调用频率超限 |
|
||||
|
||||
## Web API 端点
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/check-auth | 检查当前会话是否已登录 |
|
||||
| POST | /api/login | 登录(JSON body: username, password) |
|
||||
| POST | /api/logout | 登出 |
|
||||
| GET | /api/history | 获取聊天历史 |
|
||||
| POST | /api/message | 发送消息给 AI |
|
||||
| POST | /api/tools/execute | 执行工具调用 |
|
||||
| GET | /api/status | 获取系统状态 |
|
||||
| GET | /api/settings | 获取设置 |
|
||||
| PUT | /api/settings | 更新设置 |
|
||||
| DELETE | /api/history | 清空历史 |
|
||||
| POST | /api/shutdown | 关闭服务器 |
|
||||
| GET | /api/activity | 获取数据库操作记录 |
|
||||
| GET | /api/graph | 获取知识图谱数据 |
|
||||
| GET | /api/graph/highlight | 获取高亮节点 |
|
||||
|
||||
所有 API 端点(除 /api/login 和 /api/check-auth 外)需要登录认证。登录使用 Flask session,有效期 7 天。
|
||||
286
docs/zh/architecture.md
Normal file
286
docs/zh/architecture.md
Normal file
@ -0,0 +1,286 @@
|
||||
# TrulyMEM 架构设计
|
||||
|
||||
## 核心原则
|
||||
|
||||
- 键盘驱动,零鼠标依赖
|
||||
- 极简视觉,信息密度优先
|
||||
- 工具痕迹默认隐藏,需要时可展开
|
||||
- TUI 与后端分离,多线程通信
|
||||
- **一切皆图**,AI 推理全部在后端
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 开发环境(从 Git 仓库直接运行)
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
python3 trulymem_entry.py --web --port 4096
|
||||
```
|
||||
|
||||
### 生产环境(Systemd + 独立部署目录)
|
||||
|
||||
```bash
|
||||
# 将代码复制到独立目录
|
||||
cp -r TrulyMEM-TrueHumanMEM /home/trulymem
|
||||
|
||||
# 创建 Systemd 服务
|
||||
cat > /etc/systemd/system/trulymem-web.service << 'EOF'
|
||||
[Unit]
|
||||
Description=TrulyMEM - True Human Memory (Web Mode)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/home/trulymem
|
||||
ExecStart=/usr/bin/python3 /home/trulymem/trulymem_entry.py --web --port 4096
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable trulymem-web.service
|
||||
systemctl start trulymem-web.service
|
||||
|
||||
# 查看状态
|
||||
systemctl status trulymem-web.service
|
||||
```
|
||||
|
||||
> **注意**: 不要直接从 Git 仓库启动服务,以免日志文件、数据库等运行时产物污染仓库。
|
||||
|
||||
### Web 访问
|
||||
|
||||
服务默认运行在 `http://localhost:4096`,首次访问需设置管理员账号并登录。
|
||||
|
||||
### 更新部署
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git pull
|
||||
|
||||
# 同步到部署目录
|
||||
cp -r * /home/trulymem/
|
||||
|
||||
# 重启服务
|
||||
systemctl restart trulymem-web.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 原始架构说明
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
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 # 工具调用限制器
|
||||
│ ├── web_api.py # Web API 服务(登录 + RESTful API)
|
||||
│ ├── tools/ # 工具定义
|
||||
│ │ └── memory_tools.py
|
||||
│ └── prompts/ # 提示词管理(PromptManager + system_prompt.md)
|
||||
├── ui/ # TUI 显示层 + Web 前端
|
||||
│ ├── __init__.py # 导出 GraphMemoryApp
|
||||
│ ├── app.py # GraphMemoryApp (通过 BackendClient 通信)
|
||||
│ ├── widgets/ # TUI 组件
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── services/ # 服务层(仅配置管理)
|
||||
│ ├── handlers/ # 事件处理
|
||||
│ ├── styles/ # 样式文件
|
||||
│ ├── static/ # Web 前端静态文件
|
||||
│ │ ├── graph.html # 星图可视化(Three.js)
|
||||
│ │ └── index.html # Web 聊天界面
|
||||
│ ├── templates/ # 页面模板
|
||||
│ │ ├── login.html
|
||||
│ │ ├── setup.html
|
||||
│ │ └── settings.html
|
||||
│ ├── web_config.json # Web 服务配置文件
|
||||
│ └── web_config.example.json # Web 配置模板
|
||||
├── tests/ # 测试套件
|
||||
│ ├── test_core/ # 核心逻辑测试
|
||||
│ ├── test_ui/ # UI 层测试
|
||||
│ └── test_integration/ # 集成测试
|
||||
├── docs/ # 文档
|
||||
│ ├── zh/ # 中文文档
|
||||
│ └── en/ # 英文文档
|
||||
└── build/ # 打包脚本
|
||||
├── build_linux.sh
|
||||
├── build_macos.sh
|
||||
├── build_windows.bat
|
||||
├── build_appimage.sh
|
||||
└── trulymem.spec
|
||||
```
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
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()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具系统
|
||||
|
||||
### 记忆工具 (7个)
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
- `memory_archive` - 归档记忆
|
||||
- `memory_cleanup` - 清理数据
|
||||
- `context_rewrite` - 压缩单轮工具调用上下文
|
||||
|
||||
### 人设工具 (2个)
|
||||
| `persona_remove` | 删除单条人设属性 | 保留其他人设不变 |
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具 (6个)
|
||||
| `task_archive` | 归档已完成/过期的任务 | 步骤6强制执行,写入完成摘要 |
|
||||
| `task_query` | 查询最近任务列表 | 新对话时优先调用,避免重复创建任务 |
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
---
|
||||
|
||||
## 工具调用限制
|
||||
|
||||
| 类别 | 操作 | 每轮上限 |
|
||||
|------|------|---------|
|
||||
| 人设图 | 修改 | 1 次 |
|
||||
| 工作记忆链 | 查询 | 30 次 |
|
||||
| 工作记忆链 | 修改 | 20 次 |
|
||||
| 一般记忆 | 查询 | 30 次 |
|
||||
| 一般记忆 | 修改 | 15 次 |
|
||||
|
||||
> 注:`memory_recall` 统一计入一般记忆查询,不再区分人设/工作记忆查询。
|
||||
|
||||
---
|
||||
|
||||
## 错误处理原则
|
||||
|
||||
所有 API **不抛出异常**,错误通过返回字典传递:
|
||||
|
||||
```python
|
||||
result = client.process_message("hello")
|
||||
|
||||
if result.get("success"):
|
||||
print(result["content"])
|
||||
else:
|
||||
print(result["error"]) # 错误描述
|
||||
```
|
||||
245
docs/zh/memory.md
Normal file
245
docs/zh/memory.md
Normal file
@ -0,0 +1,245 @@
|
||||
# TrulyMEM 记忆机制
|
||||
|
||||
本文档详细说明 TrulyMEM 内部的记忆工作机制。
|
||||
|
||||
## 核心设计理念
|
||||
|
||||
### 区别于传统上下文系统
|
||||
|
||||
传统 AI 对话系统使用 messages 数组存储对话历史:
|
||||
- 每次请求携带全部历史消息
|
||||
- 随着对话轮次增加,上下文逐渐膨胀
|
||||
- 最终触发记忆压缩或滑动窗口,造成记忆丢失
|
||||
|
||||
TrulyMEM 的解决思路:
|
||||
- **摒弃** messages 数组上下文
|
||||
- **唯一** 记忆载体:图数据库
|
||||
- 全部记忆以三元组(节点)- 关系 → (节点)形式存储
|
||||
|
||||
### 图数据库作为唯一记忆源
|
||||
|
||||
所有记忆必须通过以下方式写入图数据库:
|
||||
- `memory_commit` - 写入新记忆
|
||||
- `memory_purge` - 删除/修正记忆
|
||||
|
||||
所有记忆必须通过以下方式读取:
|
||||
- `memory_recall` - 检索记忆
|
||||
|
||||
### 工作记忆管理
|
||||
|
||||
`context_rewrite` 允许 AI 在单轮对话内主动压缩工具调用的临时上下文:
|
||||
- 将冗长的 JSON 工具结果提炼为简洁的自然语言摘要
|
||||
- 摘要必须包含调用了哪些工具、对几次调用的总结
|
||||
- 系统验证格式后,替换 `messages_history` 为 `[用户消息, 摘要]`
|
||||
- 确保 LLM 保留元认知(知道"我调用过工具"),同时减少 JSON 噪音
|
||||
|
||||
---
|
||||
|
||||
## 强制执行流程(每轮对话)
|
||||
|
||||
由于没有传统上下文系统,每轮对话必须按以下顺序执行:
|
||||
|
||||
### 步骤 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:是否更新了工作记忆链?
|
||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
||||
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?
|
||||
214
docs/zh/persona.md
Normal file
214
docs/zh/persona.md
Normal file
@ -0,0 +1,214 @@
|
||||
# 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. **明确边界**:除非用户要求,绝不主动跳出角色
|
||||
133
docs/zh/prompts.md
Normal file
133
docs/zh/prompts.md
Normal file
@ -0,0 +1,133 @@
|
||||
# 提示词管理文档
|
||||
|
||||
本文档描述提示词管理模块。
|
||||
|
||||
## 概述
|
||||
|
||||
提示词管理模块(`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: memory_commit (写入关键信息) → 将用户明确提到的重要信息写入图数据库
|
||||
步骤 5: 更新工作记忆链
|
||||
```
|
||||
|
||||
> **注意**: 原提示词仅包含 4 步,缺少显式的写入步骤,导致 AI 只查不写、聊完即忘。
|
||||
> 步骤 4 确保每轮对话的关键信息被持久化到图数据库中。
|
||||
|
||||
### 5. 工具系统
|
||||
|
||||
#### 记忆工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `memory_recall` | 检索记忆 |
|
||||
| `memory_commit` | 写入记忆 |
|
||||
| `memory_purge` | 删除记忆 |
|
||||
| `memory_introspect` | 查看状态 |
|
||||
| `memory_archive` | 归档记忆 |
|
||||
| `memory_cleanup` | 清理数据 |
|
||||
| `context_rewrite` | 压缩单轮工具调用上下文 |
|
||||
|
||||
#### 人设工具
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `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()` 返回缓存内容
|
||||
- 缓存按进程,不持久化
|
||||
200
docs/zh/quick_start.md
Normal file
200
docs/zh/quick_start.md
Normal file
@ -0,0 +1,200 @@
|
||||
# TrulyMEM 启动指南
|
||||
|
||||
> **版本**: 多用户版 (v2) — 支持 TUI 登录、多用户隔离、Web 服务内嵌
|
||||
|
||||
---
|
||||
|
||||
## 运行方式
|
||||
|
||||
### 快速启动(推荐)
|
||||
|
||||
```bash
|
||||
# 从源码
|
||||
python trulymem_entry.py
|
||||
|
||||
# 或打包后
|
||||
./dist/TrulyMEM
|
||||
```
|
||||
|
||||
### 首次使用 —— 登录流程
|
||||
|
||||
首次启动会自动检查是否需要迁移旧数据,然后进入**登录页面**:
|
||||
|
||||
1. **新部署** → 直接输入用户名和密码创建账户(首个用户自动成为管理员)
|
||||
2. **旧版升级** → 自动检测 `~/.trulymem/config.json`,引导设置用户名密码,迁移数据
|
||||
3. **已有账户** → 直接登录进入聊天界面
|
||||
|
||||
> 💡 所有用户数据隔离存储:`~/.trulymem/{用户名}/`
|
||||
|
||||
### 聊天配置
|
||||
|
||||
登录后按 **F2** 展开右侧配置面板:
|
||||
|
||||
1. **API Key** — 必须(支持 DeepSeek、OpenAI 等)
|
||||
2. **模型** — 可选,默认已配置
|
||||
3. **Base URL** — 可选
|
||||
|
||||
配置自动保存,下次启动自动加载。
|
||||
|
||||
---
|
||||
|
||||
## Web 可视化界面
|
||||
|
||||
TrulyMEM 的 Web 服务现在**内嵌在主进程中**(无需独立启动子进程)。
|
||||
|
||||
### TUI 内启动(管理员专有)
|
||||
|
||||
管理员按 F2 打开右侧面板,勾选「启用 Web 服务」即可。
|
||||
|
||||
### 手动启动
|
||||
|
||||
```bash
|
||||
python -m core.web_api --port 4096
|
||||
# 访问 http://localhost:4096
|
||||
```
|
||||
|
||||
### 首次访问流程
|
||||
|
||||
1. 浏览器打开 `http://localhost:4096`
|
||||
2. **无用户** → 自动跳转至设置页,创建管理员账号
|
||||
3. **有用户** → 跳转至登录页
|
||||
4. 登录后进入星图可视化页面
|
||||
|
||||
### Web 功能
|
||||
|
||||
| 页面 | 访问权限 | 功能 |
|
||||
|------|----------|------|
|
||||
| 🌟 星图 | 所有已登录用户 | 浏览知识图谱三元组 |
|
||||
| ⚙ 设置 | 所有已登录用户 | 修改密码 |
|
||||
| 🧑💼 用户管理 | **仅管理员** | 添加/删除用户 |
|
||||
|
||||
---
|
||||
|
||||
## 多用户系统
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
~/.trulymem/
|
||||
├── trulymem.db # 全局用户数据库(web_users 表)
|
||||
├── .migrated # 旧版迁移标记
|
||||
├── admin/
|
||||
│ ├── config.json # 管理员配置
|
||||
│ └── admin_graph.db # 管理员知识图谱
|
||||
└── user2/
|
||||
├── config.json # user2 配置
|
||||
└── user2_graph.db # user2 知识图谱
|
||||
```
|
||||
|
||||
### 角色体系
|
||||
|
||||
| 功能 | 普通用户 | 管理员 |
|
||||
|------|---------|--------|
|
||||
| 修改自己密码 | ✅ | ✅ |
|
||||
| 配置 API Key / 模型 | ✅ | ✅ |
|
||||
| Web 服务开关(TUI) | ❌ | ✅ |
|
||||
| Web 登录凭据 | ❌ | ✅ |
|
||||
| 查看用户列表 | ❌ | ✅ |
|
||||
| 添加/删除用户 | ❌ | ✅ |
|
||||
|
||||
> ⚠️ 首个注册用户自动成为管理员。Web 设置页可添加新用户。
|
||||
|
||||
---
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 按键 | 功能 |
|
||||
|------|------|
|
||||
| F1 | 帮助 |
|
||||
| F2 | 切换侧边栏(配置面板) |
|
||||
| F3 | 工具详情 |
|
||||
| F5 | 清屏 |
|
||||
| F6 | 退出 |
|
||||
|
||||
---
|
||||
|
||||
## 打包构建
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 构建(Linux / macOS)
|
||||
pyinstaller --clean build/trulymem.spec
|
||||
```
|
||||
|
||||
构建产出:`dist/TrulyMEM`(单文件,TUI + Web 服务均内嵌于同一二进制)
|
||||
|
||||
> 📦 从 v2 开始,Web 服务作为线程嵌入主程序,不再需要独立打包 `trulymem-web`。
|
||||
>
|
||||
> 💡 修改 `ui/static/` 或 `core/` 等源码后必须重新编译才能生效(静态文件在构建时打入二进制)。
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 通信协议
|
||||
|
||||
UI 与后端通过 **Packet 协议** 通信:
|
||||
|
||||
```
|
||||
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (独立线程)
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
- **用户级存储**: `~/.trulymem/{username}/config.json`
|
||||
- **Web 配置**: `~/.trulymem/trulymem.db`(web_users 表)
|
||||
- **自动加载**: 启动时根据登录用户加载对应配置文件
|
||||
- **动态更新**: 运行时修改配置立即生效,自动持久化
|
||||
|
||||
### Web 服务架构
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ TrulyMEM 主进程 │
|
||||
│ ┌──────┐ ┌────────┐ │
|
||||
│ │ TUI │ │ Flask │ │ ← 同一进程,不同线程
|
||||
│ │ │ │ Thread │ │
|
||||
│ └──────┘ └────────┘ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 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 格式,确保无多余空格。可在 TUI 右侧面板重新配置。
|
||||
|
||||
### 管理员账号丢失
|
||||
|
||||
数据库中第一个注册账号总是 admin。如果所有用户都丢失了 admin 权限,删除用户目录下的 `trulymem.db` 后重新注册即可。
|
||||
|
||||
### 旧版数据迁移
|
||||
|
||||
检测到旧版 `~/.trulymem/config.json` 和 `graph_memory.db` 时,TUI 启动时自动进入迁移引导。迁移后旧文件保留,不会删除。
|
||||
|
||||
---
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pytest tests/
|
||||
bash build/build_linux.sh
|
||||
```
|
||||
182
docs/zh/working_memory.md
Normal file
182
docs/zh/working_memory.md
Normal file
@ -0,0 +1,182 @@
|
||||
# 工作记忆链机制说明
|
||||
|
||||
## 概述
|
||||
|
||||
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 处理任务相关操作
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "chat",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM chat feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/chat",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "graph",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM graph feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/graph",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "settings",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM settings feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/settings",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
897
package-lock.json
generated
897
package-lock.json
generated
@ -1,897 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@xenova/transformers": "^2.17.2"
|
||||
}
|
||||
}
|
||||
4529
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
4529
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
File diff suppressed because one or more lines are too long
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo.linter
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo.linter
vendored
Normal file
File diff suppressed because one or more lines are too long
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"buildMode":"Debug"}
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.protoBin
vendored
Normal file
Binary file not shown.
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export { ChatPage } from "@normalized:N&&&@ohos/chat/src/main/ets/pages/ChatPage&1.0.0";
|
||||
Binary file not shown.
@ -0,0 +1,488 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ChatMessageList_Params {
|
||||
messages?: ChatMessage[];
|
||||
isThinking?: boolean;
|
||||
toolCallLog?: string;
|
||||
scrollController?: Scroller;
|
||||
}
|
||||
interface ChatInputBar_Params {
|
||||
inputText?: string;
|
||||
isThinking?: boolean;
|
||||
onSend?: () => void;
|
||||
}
|
||||
interface ToolCallLogPanel_Params {
|
||||
logText?: string;
|
||||
}
|
||||
interface ThinkingIndicator_Params {
|
||||
}
|
||||
interface ChatMessageBubble_Params {
|
||||
msg?: ChatMessage;
|
||||
}
|
||||
import type { ChatMessage } from '@ohos/common';
|
||||
export class ChatMessageBubble extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__msg = new SynchedPropertyNesedObjectPU(params.msg, this, "msg");
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatMessageBubble_Params) {
|
||||
this.__msg.set(params.msg);
|
||||
}
|
||||
updateStateVars(params: ChatMessageBubble_Params) {
|
||||
this.__msg.set(params.msg);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__msg.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__msg.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __msg: SynchedPropertyNesedObjectPU<ChatMessage>;
|
||||
get msg() {
|
||||
return this.__msg.get();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.padding(12);
|
||||
Column.backgroundColor(this.msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)');
|
||||
Column.borderRadius(12);
|
||||
Column.border({
|
||||
width: 1,
|
||||
color: this.msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)'
|
||||
});
|
||||
Column.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Column.margin({ left: 8, right: 8, bottom: 8 });
|
||||
Column.width('100%');
|
||||
Column.alignItems(HorizontalAlign.Start);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
// 角色标识
|
||||
Text.create(this.msg.role === 'user' ? '🧑 你' : '🤖 AI');
|
||||
// 角色标识
|
||||
Text.fontSize(11);
|
||||
// 角色标识
|
||||
Text.fontColor(this.msg.role === 'user' ? '#7C4DFF' : '#999');
|
||||
// 角色标识
|
||||
Text.width('100%');
|
||||
}, Text);
|
||||
// 角色标识
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
// 消息内容
|
||||
Text.create(this.msg.content);
|
||||
// 消息内容
|
||||
Text.fontSize(15);
|
||||
// 消息内容
|
||||
Text.width('100%');
|
||||
// 消息内容
|
||||
Text.margin({ top: 4 });
|
||||
// 消息内容
|
||||
Text.fontColor('#FFFFFF');
|
||||
}, Text);
|
||||
// 消息内容
|
||||
Text.pop();
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ThinkingIndicator extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ThinkingIndicator_Params) {
|
||||
}
|
||||
updateStateVars(params: ThinkingIndicator_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Row.create();
|
||||
Row.padding(12);
|
||||
Row.backgroundColor('rgba(124,77,255,0.1)');
|
||||
Row.borderRadius(12);
|
||||
Row.border({ width: 1, color: 'rgba(124,77,255,0.2)' });
|
||||
Row.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Row.margin({ left: 8, right: 8, bottom: 8 });
|
||||
}, Row);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
LoadingProgress.create();
|
||||
LoadingProgress.width(20);
|
||||
LoadingProgress.height(20);
|
||||
LoadingProgress.margin({ right: 8 });
|
||||
LoadingProgress.color('#7C4DFF');
|
||||
}, LoadingProgress);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('AI 思考中...');
|
||||
Text.fontSize(13);
|
||||
Text.fontColor('#7C4DFF');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
Row.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ToolCallLogPanel extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__logText = new SynchedPropertySimpleOneWayPU(params.logText, this, "logText");
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ToolCallLogPanel_Params) {
|
||||
}
|
||||
updateStateVars(params: ToolCallLogPanel_Params) {
|
||||
this.__logText.reset(params.logText);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__logText.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__logText.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __logText: SynchedPropertySimpleOneWayPU<string>;
|
||||
get logText() {
|
||||
return this.__logText.get();
|
||||
}
|
||||
set logText(newValue: string) {
|
||||
this.__logText.set(newValue);
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(this.logText);
|
||||
Text.fontSize(10);
|
||||
Text.fontColor('#FF9800');
|
||||
Text.backgroundColor('rgba(255,152,0,0.1)');
|
||||
Text.padding(8);
|
||||
Text.borderRadius(8);
|
||||
Text.border({ width: 1, color: 'rgba(255,152,0,0.2)' });
|
||||
Text.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Text.margin({ left: 8, right: 8, bottom: 4 });
|
||||
Text.lineHeight(16);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ChatInputBar extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__inputText = new SynchedPropertySimpleTwoWayPU(params.inputText, this, "inputText");
|
||||
this.__isThinking = new SynchedPropertySimpleOneWayPU(params.isThinking, this, "isThinking");
|
||||
this.onSend = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatInputBar_Params) {
|
||||
if (params.onSend !== undefined) {
|
||||
this.onSend = params.onSend;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatInputBar_Params) {
|
||||
this.__isThinking.reset(params.isThinking);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__inputText.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__inputText.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __inputText: SynchedPropertySimpleTwoWayPU<string>;
|
||||
get inputText() {
|
||||
return this.__inputText.get();
|
||||
}
|
||||
set inputText(newValue: string) {
|
||||
this.__inputText.set(newValue);
|
||||
}
|
||||
private __isThinking: SynchedPropertySimpleOneWayPU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private onSend?: () => void;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Row.create();
|
||||
Row.width('100%');
|
||||
Row.padding(8);
|
||||
Row.backgroundColor('rgba(255,255,255,0.05)');
|
||||
Row.backgroundBlurStyle(BlurStyle.Regular);
|
||||
Row.border({
|
||||
width: 1,
|
||||
color: 'rgba(124,77,255,0.2)',
|
||||
style: BorderStyle.Solid
|
||||
});
|
||||
}, Row);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TextArea.create({ text: this.inputText, placeholder: '输入消息...' });
|
||||
TextArea.layoutWeight(1);
|
||||
TextArea.onChange((v: string) => { this.inputText = v; });
|
||||
TextArea.height(40);
|
||||
TextArea.backgroundColor('rgba(255,255,255,0.1)');
|
||||
TextArea.borderRadius(8);
|
||||
TextArea.border({ width: 1, color: 'rgba(124,77,255,0.3)' });
|
||||
}, TextArea);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Button.createWithLabel('发送');
|
||||
Button.enabled(!this.isThinking);
|
||||
Button.onClick(() => { this.onSend?.(); });
|
||||
Button.backgroundColor('#7C4DFF');
|
||||
Button.borderRadius(8);
|
||||
}, Button);
|
||||
Button.pop();
|
||||
Row.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ChatMessageList extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__messages = new SynchedPropertyObjectOneWayPU(params.messages, this, "messages");
|
||||
this.__isThinking = new SynchedPropertySimpleOneWayPU(params.isThinking, this, "isThinking");
|
||||
this.__toolCallLog = new SynchedPropertySimpleOneWayPU(params.toolCallLog, this, "toolCallLog");
|
||||
this.scrollController = new Scroller();
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatMessageList_Params) {
|
||||
if (params.scrollController !== undefined) {
|
||||
this.scrollController = params.scrollController;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatMessageList_Params) {
|
||||
this.__messages.reset(params.messages);
|
||||
this.__isThinking.reset(params.isThinking);
|
||||
this.__toolCallLog.reset(params.toolCallLog);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__messages.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__toolCallLog.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__messages.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
this.__toolCallLog.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __messages: SynchedPropertySimpleOneWayPU<ChatMessage[]>;
|
||||
get messages() {
|
||||
return this.__messages.get();
|
||||
}
|
||||
set messages(newValue: ChatMessage[]) {
|
||||
this.__messages.set(newValue);
|
||||
}
|
||||
private __isThinking: SynchedPropertySimpleOneWayPU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private __toolCallLog: SynchedPropertySimpleOneWayPU<string>;
|
||||
get toolCallLog() {
|
||||
return this.__toolCallLog.get();
|
||||
}
|
||||
set toolCallLog(newValue: string) {
|
||||
this.__toolCallLog.set(newValue);
|
||||
}
|
||||
private scrollController: Scroller;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
List.create();
|
||||
List.width('100%');
|
||||
List.layoutWeight(1);
|
||||
List.backgroundColor('rgba(0,0,0,0.1)');
|
||||
}, List);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
ForEach.create();
|
||||
const forEachItemGenFunction = _item => {
|
||||
const msg = _item;
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatMessageBubble(this, { msg: msg }, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 141, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
msg: msg
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
msg: msg
|
||||
});
|
||||
}
|
||||
}, { name: "ChatMessageBubble" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
};
|
||||
this.forEachUpdateFunction(elmtId, this.messages, forEachItemGenFunction);
|
||||
}, ForEach);
|
||||
ForEach.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.isThinking) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ThinkingIndicator(this, {}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 147, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {});
|
||||
}
|
||||
}, { name: "ThinkingIndicator" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.toolCallLog && !this.isThinking) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ToolCallLogPanel(this, { logText: this.toolCallLog }, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 153, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
logText: this.toolCallLog
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
logText: this.toolCallLog
|
||||
});
|
||||
}
|
||||
}, { name: "ToolCallLogPanel" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
List.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,219 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ChatPage_Params {
|
||||
messages?: ChatMessage[];
|
||||
inputText?: string;
|
||||
db?: GraphDatabase;
|
||||
toolCallLog?: string;
|
||||
isThinking?: boolean;
|
||||
agentService?: AIAgentService;
|
||||
}
|
||||
import { GraphMemoryService, AIAgentService, Logger } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import type { GraphDatabase, ChatMessage, AgentResponse } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import { ChatMessageList, ChatInputBar } from "@normalized:N&&&@ohos/chat/src/main/ets/components/ChatComponents&1.0.0";
|
||||
export class ChatPage extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__messages = new ObservedPropertyObjectPU([], this, "messages");
|
||||
this.__inputText = new ObservedPropertySimplePU('', this, "inputText");
|
||||
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
|
||||
this.__toolCallLog = new ObservedPropertySimplePU('', this, "toolCallLog");
|
||||
this.__isThinking = new ObservedPropertySimplePU(false, this, "isThinking");
|
||||
this.agentService = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatPage_Params) {
|
||||
if (params.messages !== undefined) {
|
||||
this.messages = params.messages;
|
||||
}
|
||||
if (params.inputText !== undefined) {
|
||||
this.inputText = params.inputText;
|
||||
}
|
||||
if (params.toolCallLog !== undefined) {
|
||||
this.toolCallLog = params.toolCallLog;
|
||||
}
|
||||
if (params.isThinking !== undefined) {
|
||||
this.isThinking = params.isThinking;
|
||||
}
|
||||
if (params.agentService !== undefined) {
|
||||
this.agentService = params.agentService;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatPage_Params) {
|
||||
this.__db.reset(params.db);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__messages.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__inputText.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__db.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__toolCallLog.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__messages.aboutToBeDeleted();
|
||||
this.__inputText.aboutToBeDeleted();
|
||||
this.__db.aboutToBeDeleted();
|
||||
this.__toolCallLog.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __messages: ObservedPropertyObjectPU<ChatMessage[]>;
|
||||
get messages() {
|
||||
return this.__messages.get();
|
||||
}
|
||||
set messages(newValue: ChatMessage[]) {
|
||||
this.__messages.set(newValue);
|
||||
}
|
||||
private __inputText: ObservedPropertySimplePU<string>;
|
||||
get inputText() {
|
||||
return this.__inputText.get();
|
||||
}
|
||||
set inputText(newValue: string) {
|
||||
this.__inputText.set(newValue);
|
||||
}
|
||||
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
|
||||
get db() {
|
||||
return this.__db.get();
|
||||
}
|
||||
set db(newValue: GraphDatabase) {
|
||||
this.__db.set(newValue);
|
||||
}
|
||||
private __toolCallLog: ObservedPropertySimplePU<string>;
|
||||
get toolCallLog() {
|
||||
return this.__toolCallLog.get();
|
||||
}
|
||||
set toolCallLog(newValue: string) {
|
||||
this.__toolCallLog.set(newValue);
|
||||
}
|
||||
private __isThinking: ObservedPropertySimplePU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private agentService?: AIAgentService;
|
||||
async aboutToAppear() {
|
||||
// 初始化图记忆服务和 Agent
|
||||
const memoryService = new GraphMemoryService(this.db);
|
||||
this.agentService = new AIAgentService(memoryService, getContext(this));
|
||||
// 加载历史消息(兼容旧数据:无 session_id 时加载全部)
|
||||
const rawHistory = await this.db.getChatHistory(50, this.agentService.getSessionId());
|
||||
if (rawHistory.length === 0) {
|
||||
// 新 session,尝试加载旧消息
|
||||
const legacyHistory = await this.db.getChatHistory(50);
|
||||
this.messages = legacyHistory.map(m => {
|
||||
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.messages = rawHistory.map(m => {
|
||||
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
}
|
||||
async sendMessage() {
|
||||
if (!this.inputText.trim() || !this.agentService)
|
||||
return;
|
||||
const userMessage: string = this.inputText;
|
||||
this.inputText = '';
|
||||
// 添加用户消息
|
||||
await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId());
|
||||
this.messages = [...this.messages, { role: 'user', content: userMessage }];
|
||||
// 显示 loading
|
||||
this.isThinking = true;
|
||||
this.toolCallLog = '';
|
||||
try {
|
||||
// 通过 Agent 发送消息
|
||||
const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage);
|
||||
// 记录工具调用日志
|
||||
if (agentResponse.toolCalls.length > 0) {
|
||||
const logs: string[] = agentResponse.toolCalls.map(tc => `🛠 ${tc.name}: ${tc.message}`);
|
||||
this.toolCallLog = logs.join('\n');
|
||||
}
|
||||
// 保存并显示 AI 回复
|
||||
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
|
||||
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
|
||||
}
|
||||
catch (err) {
|
||||
Logger.error('Agent request failed: ' + JSON.stringify(err));
|
||||
const errMsg = (err as Error).message || JSON.stringify(err);
|
||||
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${errMsg}` }];
|
||||
}
|
||||
finally {
|
||||
this.isThinking = false;
|
||||
}
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundColor('rgba(26,27,46,0.95)');
|
||||
}, Column);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatMessageList(this, {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/pages/ChatPage.ets", line: 73, col: 13 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
});
|
||||
}
|
||||
}, { name: "ChatMessageList" });
|
||||
}
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatInputBar(this, {
|
||||
inputText: this.__inputText,
|
||||
isThinking: this.isThinking,
|
||||
onSend: (): void => { this.sendMessage(); }
|
||||
}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/pages/ChatPage.ets", line: 79, col: 13 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
inputText: this.inputText,
|
||||
isThinking: this.isThinking,
|
||||
onSend: (): void => { this.sendMessage(); }
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
isThinking: this.isThinking
|
||||
});
|
||||
}
|
||||
}, { name: "ChatInputBar" });
|
||||
}
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.protoBin
vendored
Normal file
Binary file not shown.
16
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts
vendored
Normal file
16
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
export { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
export { defaultLogger as Logger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
export { BreakpointType, WidthBreakpoint } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export type { BreakpointTypes } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export { PageContext } from "@normalized:N&&&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0";
|
||||
export type { RouterParam, IPageContext } from "@normalized:N&&&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0";
|
||||
export { Constants as TrulyMEMConstants } from "@normalized:N&&&@ohos/common/src/main/ets/constant/TrulyMEMConstants&1.0.0";
|
||||
export { GraphDatabase } from "@normalized:N&&&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0";
|
||||
export type { RecallEntity, TimeRangeParams } from "@normalized:N&&&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0";
|
||||
export { GraphMemoryService } from "@normalized:N&&&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0";
|
||||
export type { ConnectionItem, NodeDetailInfo } from "@normalized:N&&&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0";
|
||||
export { AIAgentService } from "@normalized:N&&&@ohos/common/src/main/ets/service/AIAgentService&1.0.0";
|
||||
export type { ChatMessage, AgentResponse } from "@normalized:N&&&@ohos/common/src/main/ets/service/AIAgentService&1.0.0";
|
||||
export { BaseViewModel } from "@normalized:N&&&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0";
|
||||
export type { VMEvent } from "@normalized:N&&&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0";
|
||||
export { ImmersiveTabNavigation } from "@normalized:N&&&@ohos/common/src/main/ets/component/ImmersiveTabNavigation&1.0.0";
|
||||
Binary file not shown.
@ -0,0 +1,236 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ImmersiveTabNavigation_Params {
|
||||
currentIndex?: number;
|
||||
contentBuilder?: () => void;
|
||||
onTabChange?: (index: number) => void;
|
||||
windowFocused?: boolean;
|
||||
bottomAvoidHeight?: number;
|
||||
}
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import window from "@ohos:window";
|
||||
import type { BusinessError } from "@ohos:base";
|
||||
const THEME_COLOR = '#7C4DFF';
|
||||
export class ImmersiveTabNavigation extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__currentIndex = new ObservedPropertySimplePU(0, this, "currentIndex");
|
||||
this.contentBuilder = undefined;
|
||||
this.onTabChange = undefined;
|
||||
this.windowFocused = true;
|
||||
this.bottomAvoidHeight = 0;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ImmersiveTabNavigation_Params) {
|
||||
if (params.currentIndex !== undefined) {
|
||||
this.currentIndex = params.currentIndex;
|
||||
}
|
||||
if (params.contentBuilder !== undefined) {
|
||||
this.contentBuilder = params.contentBuilder;
|
||||
}
|
||||
if (params.onTabChange !== undefined) {
|
||||
this.onTabChange = params.onTabChange;
|
||||
}
|
||||
if (params.windowFocused !== undefined) {
|
||||
this.windowFocused = params.windowFocused;
|
||||
}
|
||||
if (params.bottomAvoidHeight !== undefined) {
|
||||
this.bottomAvoidHeight = params.bottomAvoidHeight;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ImmersiveTabNavigation_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__currentIndex.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__currentIndex.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __currentIndex: ObservedPropertySimplePU<number>;
|
||||
get currentIndex() {
|
||||
return this.__currentIndex.get();
|
||||
}
|
||||
set currentIndex(newValue: number) {
|
||||
this.__currentIndex.set(newValue);
|
||||
}
|
||||
private __contentBuilder;
|
||||
private onTabChange?: (index: number) => void;
|
||||
private windowFocused: boolean;
|
||||
private bottomAvoidHeight: number;
|
||||
aboutToAppear() {
|
||||
const mainWindow = AppStorage.get<window.Window>('main_window');
|
||||
if (mainWindow) {
|
||||
try {
|
||||
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
|
||||
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
|
||||
}
|
||||
catch (e) {
|
||||
defaultLogger.error('Failed to get avoid area: ' + (e as BusinessError).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerTabSwitchFeedback(index: number) {
|
||||
this.currentIndex = index;
|
||||
AppStorage.setOrCreate('global_theme_color', THEME_COLOR);
|
||||
this.onTabChange?.(index);
|
||||
}
|
||||
tabBarBuilder(index: number, icon: string, label: string, parent = null) {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height(56);
|
||||
Column.justifyContent(FlexAlign.Center);
|
||||
Column.alignItems(HorizontalAlign.Center);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.currentIndex === index && this.windowFocused) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Circle.create();
|
||||
Circle.width(32);
|
||||
Circle.height(32);
|
||||
Circle.backgroundColor(`${THEME_COLOR}33`);
|
||||
Circle.blur(8);
|
||||
Circle.position({ x: '50%', y: '50%' });
|
||||
Circle.translate({ x: '-50%', y: '-50%' });
|
||||
}, Circle);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(icon);
|
||||
Text.fontSize(20);
|
||||
Text.opacity(this.currentIndex === index ? 1 : 0.5);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(label);
|
||||
Text.fontSize(10);
|
||||
Text.fontColor(this.currentIndex === index ? THEME_COLOR : '#999');
|
||||
Text.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
Column.pop();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Stack.create();
|
||||
Stack.width('100%');
|
||||
Stack.height('100%');
|
||||
Stack.backgroundColor('#00000000');
|
||||
}, Stack);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
}, Column);
|
||||
this.contentBuilder.bind(this)();
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('92%');
|
||||
Column.height(72);
|
||||
Column.alignSelf(ItemAlign.Center);
|
||||
Column.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` });
|
||||
Column.borderRadius(24);
|
||||
Column.shadow({
|
||||
radius: 20,
|
||||
offsetY: -4,
|
||||
color: 'rgba(0,0,0,0.15)'
|
||||
});
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Stack.create();
|
||||
Stack.width('100%');
|
||||
Stack.height('100%');
|
||||
}, Stack);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundBlurStyle(BlurStyle.Regular);
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundColor(`${THEME_COLOR}0D`);
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.linearGradient({
|
||||
angle: 180,
|
||||
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
|
||||
});
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
Stack.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Tabs.create({ index: this.currentIndex });
|
||||
Tabs.width('100%');
|
||||
Tabs.height(64);
|
||||
Tabs.barPosition(BarPosition.End);
|
||||
Tabs.onChange((index: number) => {
|
||||
this.triggerTabSwitchFeedback(index);
|
||||
});
|
||||
}, Tabs);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TabContent.create(() => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Blank.create();
|
||||
}, Blank);
|
||||
Blank.pop();
|
||||
Column.pop();
|
||||
});
|
||||
TabContent.tabBar({ builder: () => {
|
||||
this.tabBarBuilder.call(this, 0, '🌌', 'TrulyMEM');
|
||||
} });
|
||||
}, TabContent);
|
||||
TabContent.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TabContent.create(() => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Blank.create();
|
||||
}, Blank);
|
||||
Blank.pop();
|
||||
Column.pop();
|
||||
});
|
||||
TabContent.tabBar({ builder: () => {
|
||||
this.tabBarBuilder.call(this, 1, '⚙', '设置');
|
||||
} });
|
||||
}, TabContent);
|
||||
TabContent.pop();
|
||||
Tabs.pop();
|
||||
Column.pop();
|
||||
Stack.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
export class Constants {
|
||||
static readonly DB_NAME: string = 'trulymem.db';
|
||||
static readonly CONFIG_PREF_NAME: string = 'trulymem_config';
|
||||
static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com';
|
||||
static readonly DEFAULT_MODEL: string = 'deepseek-chat';
|
||||
static readonly SECURITY_LEVEL: number = 1; // S1
|
||||
// Table names
|
||||
static readonly TABLE_NODES: string = 'nodes';
|
||||
static readonly TABLE_RELATIONS: string = 'relations';
|
||||
static readonly TABLE_CHAT: string = 'chat_records';
|
||||
// SQL definitions
|
||||
static readonly SQL_CREATE_NODES: string = `
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT DEFAULT 'concept',
|
||||
mentions INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
static readonly SQL_CREATE_RELATIONS: string = `
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
subject_id INTEGER NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
object_id INTEGER NOT NULL,
|
||||
weight REAL DEFAULT 1.0,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||
FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
)`;
|
||||
static readonly SQL_CREATE_CHAT: string = `
|
||||
CREATE TABLE IF NOT EXISTS chat_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
tools TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,53 @@
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import type { BusinessError } from "@ohos:base";
|
||||
export interface RouterParam {
|
||||
routerName: string;
|
||||
param?: object;
|
||||
}
|
||||
export interface IPageContext {
|
||||
openPage(data: RouterParam, animated?: boolean): void;
|
||||
popPage(animated?: boolean): void;
|
||||
replacePage(data: RouterParam, animated?: boolean): void;
|
||||
}
|
||||
export class PageContext implements IPageContext {
|
||||
private readonly pathStack: NavPathStack;
|
||||
constructor() {
|
||||
this.pathStack = new NavPathStack();
|
||||
}
|
||||
public get navPathStack(): NavPathStack {
|
||||
return this.pathStack;
|
||||
}
|
||||
public replacePage(data: RouterParam, animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.replacePath({ name: data.routerName, param: data.param }, animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public openPage(data: RouterParam, animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public popPage(animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.pop(animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('popPage failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public popPageByIndex(index: number, animated: boolean = true): void {
|
||||
this.pathStack.popToIndex(index, animated);
|
||||
}
|
||||
public clear(animated: boolean = true): void {
|
||||
this.pathStack.clear(animated);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,785 @@
|
||||
import http from "@ohos:net.http";
|
||||
import dataPreferences from "@ohos:data.preferences";
|
||||
import type common from "@ohos:app.ability.common";
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import type { GraphMemoryService, EntityInfo, RelationInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TripletInput, PersonaQueryResult, MemoryRecallResult } from './GraphMemoryService';
|
||||
import type { TimeRangeParams } from '../model/GraphDatabase';
|
||||
export interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
export interface AgentResponse {
|
||||
content: string;
|
||||
toolCalls: ToolCallResult[];
|
||||
}
|
||||
export interface ToolCallResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
// ========= 工具定义类型 =========
|
||||
// Concrete interface for tool property definitions (replaces Record<string, T>)
|
||||
interface ToolPropertiesDefinition {
|
||||
days?: ToolParamProperty;
|
||||
queryIntent?: ToolParamProperty;
|
||||
seedEntities?: ToolParamProperty;
|
||||
depth?: ToolParamProperty;
|
||||
timeRange?: ToolParamProperty;
|
||||
sessionFilter?: ToolParamProperty;
|
||||
triplets?: ToolParamProperty;
|
||||
entityTypes?: ToolParamProperty;
|
||||
sessionId?: ToolParamProperty;
|
||||
turnId?: ToolParamProperty;
|
||||
criteria?: ToolParamProperty;
|
||||
mode?: ToolParamProperty;
|
||||
newRelation?: ToolParamProperty;
|
||||
tone?: ToolParamProperty;
|
||||
style?: ToolParamProperty;
|
||||
personality?: ToolParamProperty;
|
||||
catchphrase?: ToolParamProperty;
|
||||
background?: ToolParamProperty;
|
||||
taskId?: ToolParamProperty;
|
||||
description?: ToolParamProperty;
|
||||
infoNodes?: ToolParamProperty;
|
||||
state?: ToolParamProperty;
|
||||
deleteInfoNodes?: ToolParamProperty;
|
||||
infoNodeNames?: ToolParamProperty;
|
||||
summary?: ToolParamProperty;
|
||||
limit?: ToolParamProperty;
|
||||
stateFilter?: ToolParamProperty;
|
||||
subject?: ToolParamProperty;
|
||||
relation?: ToolParamProperty;
|
||||
object?: ToolParamProperty;
|
||||
confidence?: ToolParamProperty;
|
||||
subjectContains?: ToolParamProperty;
|
||||
relationType?: ToolParamProperty;
|
||||
targetContains?: ToolParamProperty;
|
||||
target?: ToolParamProperty;
|
||||
dryRun?: ToolParamProperty;
|
||||
keyword?: ToolParamProperty;
|
||||
attribute?: ToolParamProperty;
|
||||
sourceType?: ToolParamProperty;
|
||||
targetType?: ToolParamProperty;
|
||||
sourceHasStatus?: ToolParamProperty;
|
||||
}
|
||||
interface ToolParamProperty {
|
||||
type: string;
|
||||
description: string;
|
||||
items?: ToolParamProperty;
|
||||
properties?: ToolPropertiesDefinition;
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
}
|
||||
interface ToolParamDecl {
|
||||
type: string;
|
||||
properties: ToolPropertiesDefinition;
|
||||
required?: string[];
|
||||
}
|
||||
interface ToolFunctionDecl {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: ToolParamDecl;
|
||||
}
|
||||
interface ToolFunctionDef {
|
||||
type: string;
|
||||
function: ToolFunctionDecl;
|
||||
}
|
||||
// ========= API 请求/响应结构 =========
|
||||
interface ApiRequestMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
interface ApiRequest {
|
||||
model: string;
|
||||
messages: ApiRequestMessage[];
|
||||
tools?: ToolFunctionDef[];
|
||||
tool_choice?: string;
|
||||
}
|
||||
interface ApiToolCall {
|
||||
id: string;
|
||||
type: string;
|
||||
function: ToolFunctionCall;
|
||||
}
|
||||
interface ToolFunctionCall {
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
interface ApiChoiceMessage {
|
||||
content?: string;
|
||||
tool_calls?: ApiToolCall[];
|
||||
}
|
||||
interface ApiChoice {
|
||||
message: ApiChoiceMessage;
|
||||
}
|
||||
interface ApiResponse {
|
||||
choices: ApiChoice[];
|
||||
}
|
||||
// ========= 内部结果类型 =========
|
||||
interface ExecuteToolResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
interface BuildContextBlockParams {
|
||||
persona: Record<string, string>;
|
||||
found: boolean;
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
message: string;
|
||||
}
|
||||
// ========= 服务方法参数类型 =========
|
||||
// ========= 工具定义辅助函数 =========
|
||||
function makeStringProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'string', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeIntegerProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'integer', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeObjectProp(description: string, props: ToolPropertiesDefinition, required?: string[]): ToolParamProperty {
|
||||
const param: ToolParamProperty = { type: 'object', description: description };
|
||||
param.properties = props;
|
||||
if (required && required.length > 0) {
|
||||
param.required = required;
|
||||
}
|
||||
return param;
|
||||
}
|
||||
function makeArrayProp(description: string, item: ToolParamProperty): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'array', description: description, items: item };
|
||||
return result;
|
||||
}
|
||||
function makeBoolProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'boolean', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeNumberProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'number', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeEnumProp(description: string, enumValues: string[]): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'string', description: description, enum: enumValues };
|
||||
return result;
|
||||
}
|
||||
function makeToolDef(name: string, description: string, properties: ToolPropertiesDefinition, required?: string[]): ToolFunctionDef {
|
||||
const params: ToolParamDecl = { type: 'object', properties: properties };
|
||||
const func: ToolFunctionDecl = { name: name, description: description, parameters: params };
|
||||
const tool: ToolFunctionDef = { type: 'function', function: func };
|
||||
if (required && required.length > 0) {
|
||||
tool.function.parameters.required = required;
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
/**
|
||||
* 系统提示词 — AI 人设 + 图记忆使用说明
|
||||
*/
|
||||
function buildSystemPrompt(personaContext: string): string {
|
||||
return `你是 TrulyMEM(True Memory)——一个拥有真实记忆的 AI 助手。
|
||||
|
||||
## 核心身份
|
||||
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
|
||||
|
||||
## ⚠️ 内部执行顺序(不得向用户输出)
|
||||
|
||||
以下步骤是内部流程,绝对不要在你的回复中提及或输出。应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
|
||||
|
||||
1. memory_recall → 查询人设图
|
||||
2. task_query → 查询工作记忆链/最近任务
|
||||
3. 处理对话内容 + 思考回复
|
||||
4. memory_commit → 写入本轮关键信息到图数据库
|
||||
5. task_archive → 归档已完成的旧任务
|
||||
6. 条件: 本轮调用 ≥5 次查询类工具 → context_rewrite 压缩工具 JSON
|
||||
|
||||
## 三元组规范
|
||||
使用 memory_commit 时,subject/relation/object 每个字段必须是一个短关键字(1~5个字),不能是完整句子。
|
||||
|
||||
## 任务信息节点规范
|
||||
- info_nodes 只能包含该任务专属的具体信息节点,严禁关联"用户"、"AI"、"系统"等全局通用实体
|
||||
- 全局实体的信息直接用独立关系记录,不需要通过 Task 中转
|
||||
|
||||
## 可用工具
|
||||
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
|
||||
- memory_commit(triplets, entityTypes?, sessionId?, turnId?): 写入记忆
|
||||
- memory_purge(criteria, mode, newRelation?): 删除/修正记忆
|
||||
- memory_introspect(sessionId?): 查看记忆状态统计
|
||||
- memory_archive(days?): 归档旧记忆
|
||||
- memory_cleanup(dryRun?): 清理已删除数据
|
||||
- memory_query_archived(days?, keyword?): 查询已归档记忆
|
||||
- context_rewrite(summary): 压缩工具调用上下文
|
||||
- persona_update(tone?, style?, personality?, catchphrase?, background?): 更新人设
|
||||
- persona_remove(attribute): 删除单条人设属性
|
||||
- persona_clear(): 清除人设
|
||||
- task_create(taskId, description, infoNodes?): 创建任务
|
||||
- task_set_state(taskId, state): 设置任务状态
|
||||
- task_delete(taskId, deleteInfoNodes?): 删除任务
|
||||
- task_link_info(taskId, infoNodeNames): 关联信息节点
|
||||
- task_archive(taskId, summary?): 归档任务
|
||||
- task_query(limit?, stateFilter?): 查询任务列表
|
||||
|
||||
## 工具调用规则
|
||||
1. ⚠️ 在完成所有工具调用之前,绝对不要输出任何文字。先默默调用工具,等所有结果返回后再输出一次完整的回复。
|
||||
2. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
|
||||
3. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
|
||||
4. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
|
||||
|
||||
## 写入规则
|
||||
用户明确表达以下信息时必须写入记忆:
|
||||
- 偏好、兴趣
|
||||
- 个人信息(工作、项目、学习)
|
||||
- 计划安排
|
||||
- 当前状态
|
||||
- 结论性事实
|
||||
|
||||
推理得到的信息可以写入但需标注 [推测]。`;
|
||||
}
|
||||
// ========= 工具定义 =========
|
||||
// Pre-typed property dictionaries for tool definitions
|
||||
const recallTimeRangeDict: ToolPropertiesDefinition = { days: makeIntegerProp('最近N天') };
|
||||
const tripletPropsDict: ToolPropertiesDefinition = {
|
||||
subject: makeStringProp('主体'),
|
||||
relation: makeStringProp('关系'),
|
||||
object: makeStringProp('客体'),
|
||||
confidence: makeNumberProp('置信度')
|
||||
};
|
||||
const purgeCriteriaDict: ToolPropertiesDefinition = {
|
||||
subjectContains: makeStringProp('源实体名包含(模糊匹配)'),
|
||||
relationType: makeStringProp('关系类型'),
|
||||
targetContains: makeStringProp('目标实体名包含(模糊匹配)'),
|
||||
sessionId: makeStringProp('会话ID过滤'),
|
||||
sourceType: makeStringProp('源实体类型过滤(如 TaskNode)'),
|
||||
targetType: makeStringProp('目标实体类型过滤'),
|
||||
sourceHasStatus: makeStringProp('源实体状态过滤(如 archived)')
|
||||
};
|
||||
const newRelDict: ToolPropertiesDefinition = {
|
||||
relation: makeStringProp(''),
|
||||
target: makeStringProp('')
|
||||
};
|
||||
const EMPTY_PROPS: ToolPropertiesDefinition = {};
|
||||
const recallProps: ToolPropertiesDefinition = {
|
||||
queryIntent: makeStringProp('查询意图,支持逗号分隔多个关键词'),
|
||||
seedEntities: makeArrayProp('种子实体(可选)', makeStringProp('')),
|
||||
depth: makeIntegerProp('搜索深度,默认2'),
|
||||
timeRange: makeObjectProp('时间范围(可选)', recallTimeRangeDict),
|
||||
sessionFilter: makeStringProp('会话ID过滤(可选)')
|
||||
};
|
||||
const commitProps: ToolPropertiesDefinition = {
|
||||
triplets: makeArrayProp('三元组列表', makeObjectProp('', tripletPropsDict, ['subject', 'relation', 'object'])),
|
||||
entityTypes: makeObjectProp('实体类型映射(可选)', EMPTY_PROPS),
|
||||
sessionId: makeStringProp('会话ID(可选)'),
|
||||
turnId: makeIntegerProp('轮次ID(可选)')
|
||||
};
|
||||
const purgeProps: ToolPropertiesDefinition = {
|
||||
criteria: makeObjectProp('删除条件', purgeCriteriaDict),
|
||||
mode: makeEnumProp('删除模式:soft逻辑删除, hard物理删除, supersede纠错替代', ['soft', 'hard', 'supersede']),
|
||||
newRelation: makeObjectProp('替代关系(supersede模式用)', newRelDict)
|
||||
};
|
||||
const personaProps: ToolPropertiesDefinition = {
|
||||
tone: makeStringProp('语气'),
|
||||
style: makeStringProp('风格'),
|
||||
personality: makeStringProp('性格'),
|
||||
catchphrase: makeStringProp('口头禅'),
|
||||
background: makeStringProp('背景')
|
||||
};
|
||||
const createProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
description: makeStringProp('任务描述'),
|
||||
infoNodes: makeArrayProp('关联的信息节点名称列表', makeStringProp(''))
|
||||
};
|
||||
const setStateProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
state: makeEnumProp('任务状态', ['进行中', '已完成', '已暂停', '已取消'])
|
||||
};
|
||||
const deleteProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
deleteInfoNodes: makeBoolProp('是否删除关联的信息节点')
|
||||
};
|
||||
const linkInfoProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
infoNodeNames: makeArrayProp('信息节点名称列表', makeStringProp(''))
|
||||
};
|
||||
const archiveProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
summary: makeStringProp('归档摘要')
|
||||
};
|
||||
const queryProps: ToolPropertiesDefinition = {
|
||||
limit: makeIntegerProp('返回数量,默认10'),
|
||||
stateFilter: makeStringProp('状态过滤: 进行中/已完成/已暂停/已取消/archived')
|
||||
};
|
||||
const introspectProps: ToolPropertiesDefinition = {
|
||||
sessionId: makeStringProp('会话ID(可选)')
|
||||
};
|
||||
const archiveProps2: ToolPropertiesDefinition = {
|
||||
days: makeIntegerProp('归档天数,默认30')
|
||||
};
|
||||
const cleanupProps: ToolPropertiesDefinition = {
|
||||
dryRun: makeBoolProp('仅预览不删除')
|
||||
};
|
||||
const queryArchivedProps: ToolPropertiesDefinition = {
|
||||
days: makeIntegerProp('最近N天内的归档记录'),
|
||||
keyword: makeStringProp('关键词过滤')
|
||||
};
|
||||
const contextRewriteProps: ToolPropertiesDefinition = {
|
||||
summary: makeStringProp('压缩后的摘要文本,必须包含工具调用元信息')
|
||||
};
|
||||
const personaRemoveProps: ToolPropertiesDefinition = {
|
||||
attribute: makeStringProp('要删除的属性名(如:扮演角色、说话风格)')
|
||||
};
|
||||
const TOOLS_DEFINITION: ToolFunctionDef[] = [
|
||||
makeToolDef('memory_recall', '检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。\n\n【⚠️ 强制执行顺序 - 每轮必须严格遵守】\n1. 步骤1(必须首先执行): 查询人设图\n2. 步骤2(必须第二步执行): 查询工作记忆链\n【重要】跳过步骤1或步骤2将导致系统错误!', recallProps, ['queryIntent']),
|
||||
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库,支持批量写入。\n\n【重要】写入原则:\n- 用户明确表达的信息 → 必须写入\n- AI推理得到的信息 → 可以写入,但需标注[推测]\n- 避免写入冗余或无意义的信息', commitProps, ['triplets']),
|
||||
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。\n\n【使用场景】\n- 纠错替代修正错误信息\n- 删除特定类型的节点关系\n- 删除残留在已归档任务上的状态关系\n\n【重要】\n- 优先使用 supersede 模式修正错误\n- 软删除不会物理删除数据', purgeProps, ['criteria', 'mode']),
|
||||
makeToolDef('memory_introspect', '查看记忆状态。返回实体数量、关系数量、热点实体。', introspectProps),
|
||||
makeToolDef('memory_archive', '归档旧记忆。将N天前的非活跃关系标记为归档状态。', archiveProps2, ['days']),
|
||||
makeToolDef('memory_cleanup', '清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。', cleanupProps),
|
||||
makeToolDef('memory_query_archived', '查询已归档的记忆。\n\n【使用场景】\n- 想了解之前归档过哪些记忆\n- 按关键词搜索归档内容\n- 按时间范围查看最近归档的历史\n\n【注意】\n- 只返回 status=archived 的原始关系记录\n- days 和 keyword 可以单独使用或组合使用', queryArchivedProps),
|
||||
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。\n\n【使用场景】\n- 本轮已执行 ≥5 次查询类工具调用\n- 【⚠️ 强制要求】context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!', contextRewriteProps, ['summary']),
|
||||
makeToolDef('persona_update', '更新AI人设属性(语气、风格、性格等)。', personaProps),
|
||||
makeToolDef('persona_remove', '删除单条人设属性。保留其他人设不变。', personaRemoveProps, ['attribute']),
|
||||
makeToolDef('persona_clear', '清除所有人设信息。', EMPTY_PROPS),
|
||||
makeToolDef('task_create', '创建新的工作记忆任务节点。\n\n【重要】info_nodes 只能包含该任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁关联\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', createProps, ['taskId', 'description']),
|
||||
makeToolDef('task_set_state', '设置任务状态。', setStateProps, ['taskId', 'state']),
|
||||
makeToolDef('task_delete', '删除任务节点。', deleteProps, ['taskId']),
|
||||
makeToolDef('task_link_info', '关联信息节点到任务。\n\n【重要】info_node_names只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', linkInfoProps, ['taskId', 'infoNodeNames']),
|
||||
makeToolDef('task_archive', '归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。', archiveProps, ['taskId']),
|
||||
makeToolDef('task_query', '查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。', queryProps)
|
||||
];
|
||||
// ========= 工具名称映射 =========
|
||||
// Types for executeTool generic args
|
||||
type ToolStateArg = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
type ToolHandlerName = 'memoryRecal' | 'memoryCommit' | 'memoryPurge' | 'memoryIntrospect' | 'memoryArchive' | 'memoryCleanup' | 'memoryQueryArchived' | 'contextRewrite' | 'personaUpdate' | 'personaRemove' | 'personaClear' | 'taskCreate' | 'taskSetState' | 'taskDelete' | 'taskLinkInfo' | 'taskArchive' | 'taskQuery';
|
||||
const TOOL_HANDLER_MAP: Record<string, ToolHandlerName> = {
|
||||
'memory_recall': 'memoryRecal',
|
||||
'memory_commit': 'memoryCommit',
|
||||
'memory_purge': 'memoryPurge',
|
||||
'memory_introspect': 'memoryIntrospect',
|
||||
'memory_archive': 'memoryArchive',
|
||||
'memory_cleanup': 'memoryCleanup',
|
||||
'memory_query_archived': 'memoryQueryArchived',
|
||||
'context_rewrite': 'contextRewrite',
|
||||
'persona_update': 'personaUpdate',
|
||||
'persona_remove': 'personaRemove',
|
||||
'persona_clear': 'personaClear',
|
||||
'task_create': 'taskCreate',
|
||||
'task_set_state': 'taskSetState',
|
||||
'task_delete': 'taskDelete',
|
||||
'task_link_info': 'taskLinkInfo',
|
||||
'task_archive': 'taskArchive',
|
||||
'task_query': 'taskQuery',
|
||||
};
|
||||
// ========= AIAgentService =========
|
||||
export class AIAgentService {
|
||||
private memoryService: GraphMemoryService;
|
||||
private currentSessionId: string;
|
||||
private turnCounter: number = 0;
|
||||
private appContext: common.Context;
|
||||
constructor(memoryService: GraphMemoryService, appContext: common.Context, sessionId?: string) {
|
||||
this.memoryService = memoryService;
|
||||
this.appContext = appContext;
|
||||
this.currentSessionId = sessionId || `session-hm-${Date.now()}`;
|
||||
}
|
||||
getSessionId(): string {
|
||||
return this.currentSessionId;
|
||||
}
|
||||
/**
|
||||
* 发送消息 — 完整的 Agent 流程
|
||||
* 1. 查询人设
|
||||
* 2. 查询工作记忆链
|
||||
* 3. 注入上下文后请求 AI
|
||||
* 4. 处理 tool_calls
|
||||
* 5. 返回最终回复
|
||||
*/
|
||||
async sendMessage(userInput: string): Promise<AgentResponse> {
|
||||
this.turnCounter++;
|
||||
// === 步骤1+2: 获取上下文 ===
|
||||
const personaResult = await this.memoryService.personaQuery();
|
||||
const personaContext: string = personaResult.found ? this.formatPersona(personaResult.persona) : '';
|
||||
const recallParams: MemoryRecallParams = {
|
||||
queryIntent: 'TaskNode,工作记忆,任务链',
|
||||
depth: 2
|
||||
};
|
||||
const taskResult = await this.memoryService.memoryRecall(recallParams);
|
||||
// === 读取 API 配置 ===
|
||||
const context = this.appContext;
|
||||
const pref = await dataPreferences.getPreferences(context, 'trulymem_config');
|
||||
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
|
||||
const model: string = String(await pref.get('model', 'deepseek-chat'));
|
||||
const apiKey: string = String(await pref.get('api_key', ''));
|
||||
if (!apiKey) {
|
||||
const noKeyResponse: AgentResponse = {
|
||||
content: '⚠️ API Key 未配置,请先在设置页填写 API Key。',
|
||||
toolCalls: []
|
||||
};
|
||||
return noKeyResponse;
|
||||
}
|
||||
// === 构建上下文丰富的消息 ===
|
||||
const systemPrompt: string = buildSystemPrompt(personaContext);
|
||||
const contextBlock: string = this.buildContextBlock(personaResult, taskResult);
|
||||
const sysMsg: ApiRequestMessage = { role: 'system' as string, content: systemPrompt };
|
||||
const userMsg: ApiRequestMessage = { role: 'user' as string, content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
|
||||
const messages: ApiRequestMessage[] = [sysMsg, userMsg];
|
||||
// === 步骤3: 请求 AI ===
|
||||
const response: ApiResponse = await this.callApi(messages, baseUrl, model, apiKey);
|
||||
const toolCalls: ToolCallResult[] = [];
|
||||
// === 步骤4: 处理 tool_calls ===
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
const choice: ApiChoice = response.choices[0];
|
||||
const aiMessage: ApiChoiceMessage = choice.message;
|
||||
// 处理函数调用
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
for (const tc of aiMessage.tool_calls) {
|
||||
const handlerName: ToolHandlerName | undefined = TOOL_HANDLER_MAP[tc.function.name];
|
||||
if (handlerName) {
|
||||
let args: Record<string, Object> = {};
|
||||
try {
|
||||
args = JSON.parse(tc.function.arguments);
|
||||
}
|
||||
catch (parseErr) {
|
||||
const parseErrMsg = (parseErr as Error).message || JSON.stringify(parseErr);
|
||||
defaultLogger.error('Failed to parse tool arguments: ' + parseErrMsg);
|
||||
const badArgsResult: ToolCallResult = {
|
||||
name: tc.function.name,
|
||||
success: false,
|
||||
message: '参数解析失败'
|
||||
};
|
||||
toolCalls.push(badArgsResult);
|
||||
continue;
|
||||
}
|
||||
const result: ToolCallResult = await this.executeTool(handlerName, args);
|
||||
toolCalls.push(result);
|
||||
}
|
||||
else {
|
||||
const unknownToolResult: ToolCallResult = {
|
||||
name: tc.function.name,
|
||||
success: false,
|
||||
message: '未知工具'
|
||||
};
|
||||
toolCalls.push(unknownToolResult);
|
||||
}
|
||||
}
|
||||
// 有 tool_calls 时需要再次请求 AI,带上工具执行结果
|
||||
const followUpSystemMsg: ApiRequestMessage = { role: 'system', content: systemPrompt };
|
||||
const followUpUserMsg: ApiRequestMessage = { role: 'user', content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
|
||||
const followUpAssistantMsg: ApiRequestMessage = {
|
||||
role: 'assistant',
|
||||
content: aiMessage.content || '(已执行记忆操作)',
|
||||
};
|
||||
const toolResultsMessages: ApiRequestMessage[] = [
|
||||
followUpSystemMsg,
|
||||
followUpUserMsg,
|
||||
followUpAssistantMsg,
|
||||
];
|
||||
for (const tc of aiMessage.tool_calls) {
|
||||
const callResult: ToolCallResult | undefined = toolCalls.find(r => r.name === tc.function.name);
|
||||
const toolResultMsg: string = callResult ? callResult.message : '完成';
|
||||
const toolResultMessage: ApiRequestMessage = {
|
||||
role: 'tool',
|
||||
content: `工具 ${tc.function.name} 执行结果: ${toolResultMsg}`
|
||||
};
|
||||
toolResultsMessages.push(toolResultMessage);
|
||||
}
|
||||
const finalResponse: ApiResponse = await this.callApi(toolResultsMessages, baseUrl, model, apiKey);
|
||||
if (finalResponse.choices && finalResponse.choices.length > 0) {
|
||||
const content: string = finalResponse.choices[0].message.content || '';
|
||||
const finalResult: AgentResponse = { content, toolCalls };
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
// 普通回复(无 tool_calls)
|
||||
const content: string = aiMessage.content || '';
|
||||
const noToolResponse: AgentResponse = { content, toolCalls };
|
||||
return noToolResponse;
|
||||
}
|
||||
const noResponse: AgentResponse = {
|
||||
content: 'AI 无响应',
|
||||
toolCalls
|
||||
};
|
||||
return noResponse;
|
||||
}
|
||||
/**
|
||||
* 请求 DeepSeek API
|
||||
*/
|
||||
private async callApi(messages: ApiRequestMessage[], baseUrl: string, model: string, apiKey: string): Promise<ApiResponse> {
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const resp = await httpRequest.request(baseUrl + '/chat/completions', {
|
||||
method: http.RequestMethod.POST,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiKey
|
||||
},
|
||||
extraData: {
|
||||
model: model,
|
||||
messages: messages,
|
||||
tools: TOOLS_DEFINITION,
|
||||
tool_choice: 'auto'
|
||||
},
|
||||
expectDataType: http.HttpDataType.OBJECT,
|
||||
readTimeout: 60000
|
||||
});
|
||||
if (resp.responseCode === 200) {
|
||||
return resp.result as ApiResponse;
|
||||
}
|
||||
const errorMsg: string = `API 请求失败: HTTP ${resp.responseCode}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 执行工具调用
|
||||
*/
|
||||
private async executeTool(name: ToolHandlerName, args: Record<string, Object>): Promise<ToolCallResult> {
|
||||
try {
|
||||
switch (name) {
|
||||
case 'memoryRecal': {
|
||||
const recallArgs: MemoryRecallParams = {
|
||||
queryIntent: args.queryIntent as string,
|
||||
seedEntities: args.seedEntities as string[],
|
||||
depth: (args.depth as number) ?? 2,
|
||||
timeRange: args.timeRange as TimeRangeParams,
|
||||
sessionFilter: args.sessionFilter as string
|
||||
};
|
||||
const recallResult = await this.memoryService.memoryRecall(recallArgs);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_recall',
|
||||
success: true,
|
||||
message: `找到 ${recallResult.entities.length} 个实体, ${recallResult.relations.length} 条关系`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryCommit': {
|
||||
const commitParams: MemoryCommitParams = {
|
||||
triplets: args.triplets as TripletInput[],
|
||||
entityTypes: args.entityTypes as Record<string, string>,
|
||||
sessionId: (args.sessionId as string) || this.currentSessionId,
|
||||
turnId: (args.turnId as number) || this.turnCounter
|
||||
};
|
||||
const commitResult = await this.memoryService.memoryCommit(commitParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_commit',
|
||||
success: true,
|
||||
message: `已写入 ${commitResult.committedCount} 条记忆`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryPurge': {
|
||||
const purgeArgs: MemoryPurgeParams = {
|
||||
criteria: args.criteria as PurgeCriteriaParams,
|
||||
mode: args.mode as 'soft' | 'hard' | 'supersede',
|
||||
newRelation: args.newRelation as NewRelationParams
|
||||
};
|
||||
const purgeResult = await this.memoryService.memoryPurge(purgeArgs);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_purge',
|
||||
success: true,
|
||||
message: purgeResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryIntrospect': {
|
||||
const introspectResult = await this.memoryService.memoryIntrospect(args.sessionId as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_introspect',
|
||||
success: true,
|
||||
message: `实体: ${introspectResult.entityCount}, 关系: ${introspectResult.relationCount}, 热点: ${introspectResult.hotNodes.length}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryArchive': {
|
||||
const archiveResult = await this.memoryService.archive(args.days as number);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_archive',
|
||||
success: true,
|
||||
message: `已归档 ${archiveResult.archived} 条关系`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryCleanup': {
|
||||
const cleanupResult = await this.memoryService.cleanup((args.dryRun as boolean) !== false);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_cleanup',
|
||||
success: true,
|
||||
message: `清理: ${cleanupResult.cleaned} 条关系, ${cleanupResult.deletedOrphans} 个孤儿节点` + (cleanupResult.dryRun ? ' (预览模式)' : '')
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryQueryArchived': {
|
||||
const qaResult = await this.memoryService.queryArchived(args.days as number, args.keyword as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_query_archived',
|
||||
success: true,
|
||||
message: `找到 ${qaResult.length} 条归档记录`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'contextRewrite': {
|
||||
const summary = args.summary as string;
|
||||
const result: ToolCallResult = {
|
||||
name: 'context_rewrite',
|
||||
success: summary.includes('[工具调用总结'),
|
||||
message: summary.includes('[工具调用总结') ? '上下文已压缩' : '格式错误:必须包含[工具调用总结]标记'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaUpdate': {
|
||||
const personaParams: PersonaUpdateParams = {
|
||||
tone: args.tone as string,
|
||||
style: args.style as string,
|
||||
personality: args.personality as string,
|
||||
catchphrase: args.catchphrase as string,
|
||||
background: args.background as string
|
||||
};
|
||||
const puResult = await this.memoryService.personaUpdate(personaParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_update',
|
||||
success: puResult.success,
|
||||
message: puResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaRemove': {
|
||||
const prResult = await this.memoryService.personaRemove(args.attribute as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_remove',
|
||||
success: prResult.success,
|
||||
message: prResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaClear': {
|
||||
const pcResult = await this.memoryService.personaClear();
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_clear',
|
||||
success: pcResult.success,
|
||||
message: pcResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskCreate': {
|
||||
const createParams: TaskCreateParams = {
|
||||
taskId: args.taskId as string,
|
||||
description: args.description as string,
|
||||
infoNodes: args.infoNodes as string[]
|
||||
};
|
||||
const tcResult = await this.memoryService.taskCreate(createParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_create',
|
||||
success: tcResult.success,
|
||||
message: tcResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskSetState': {
|
||||
const setStateParams: TaskSetStateParams = {
|
||||
taskId: args.taskId as string,
|
||||
state: args.state as ToolStateArg
|
||||
};
|
||||
const tsResult = await this.memoryService.taskSetState(setStateParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_set_state',
|
||||
success: tsResult.success,
|
||||
message: tsResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskDelete': {
|
||||
const deleteParams: TaskDeleteParams = {
|
||||
taskId: args.taskId as string,
|
||||
deleteInfoNodes: (args.deleteInfoNodes as boolean) !== false
|
||||
};
|
||||
const tdResult = await this.memoryService.taskDelete(deleteParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_delete',
|
||||
success: tdResult.success,
|
||||
message: tdResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskLinkInfo': {
|
||||
const linkInfoParams: TaskLinkInfoParams = {
|
||||
taskId: args.taskId as string,
|
||||
infoNodeNames: args.infoNodeNames as string[]
|
||||
};
|
||||
const tliResult = await this.memoryService.taskLinkInfo(linkInfoParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_link_info',
|
||||
success: tliResult.success,
|
||||
message: tliResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskArchive': {
|
||||
const archiveParams: TaskArchiveParams = {
|
||||
taskId: args.taskId as string,
|
||||
summary: args.summary as string
|
||||
};
|
||||
const taResult = await this.memoryService.taskArchive(archiveParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_archive',
|
||||
success: taResult.success,
|
||||
message: taResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskQuery': {
|
||||
const tqResult = await this.memoryService.taskQuery({
|
||||
limit: args.limit as number,
|
||||
stateFilter: args.stateFilter as string
|
||||
});
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_query',
|
||||
success: true,
|
||||
message: `找到 ${tqResult.tasks.length} 个任务`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
default: {
|
||||
const defaultResult: ToolCallResult = {
|
||||
name: name as string,
|
||||
success: false,
|
||||
message: '未实现的工具'
|
||||
};
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const errorMessage: string = (e as Error).message || '';
|
||||
const errorResult: ToolCallResult = {
|
||||
name: name as string,
|
||||
success: false,
|
||||
message: `执行失败: ${errorMessage}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 格式化人设数据为文本
|
||||
*/
|
||||
private formatPersona(persona: Record<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
const keys: string[] = Object.keys(persona);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key: string = keys[i];
|
||||
const val: string = persona[key];
|
||||
parts.push(`${key}: ${val}`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join(';') : '';
|
||||
}
|
||||
/**
|
||||
* 构建上下文注入块
|
||||
*/
|
||||
private buildContextBlock(personaResult: PersonaQueryResult, taskResult: MemoryRecallResult): string {
|
||||
const blocks: string[] = [];
|
||||
if (personaResult.found) {
|
||||
blocks.push(`【当前人设】\n${this.formatPersona(personaResult.persona)}`);
|
||||
}
|
||||
if (taskResult.entities.length > 0) {
|
||||
const entitySample: EntityInfo[] = taskResult.entities.slice(0, 5);
|
||||
const entitiesStr: string = JSON.stringify(entitySample);
|
||||
blocks.push(`【工作记忆】\n${taskResult.message}\n${entitiesStr}`);
|
||||
}
|
||||
return blocks.length > 0 ? blocks.join('\n\n') : '【新对话】';
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,935 @@
|
||||
import type { GraphDatabase, RelationQueryResult, TimeRangeParams } from '../model/GraphDatabase';
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
// ========= 接口定义 =========
|
||||
export interface SnapshotData {
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
}
|
||||
export interface GraphDataNode {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface GraphDataEdge {
|
||||
from: number;
|
||||
to: number;
|
||||
label: string;
|
||||
weight: number;
|
||||
depth?: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
export interface SnapshotEntity {
|
||||
name: string;
|
||||
type: string;
|
||||
mention_count: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface SnapshotRelation {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
session_id?: string;
|
||||
turn_id?: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface GraphOutput {
|
||||
nodes: GraphDataNode[];
|
||||
edges: GraphDataEdge[];
|
||||
}
|
||||
export interface SnapshotOutput {
|
||||
entities: SnapshotEntity[];
|
||||
relations: SnapshotRelation[];
|
||||
}
|
||||
export interface EntityInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
mentionCount: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface RelationInfo {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface TripletInput {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
confidence?: number;
|
||||
}
|
||||
export interface CleanupResult {
|
||||
cleaned: number;
|
||||
deletedRelations?: number;
|
||||
deletedOrphans?: number;
|
||||
dryRun?: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface SearchResult {
|
||||
name: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
export interface TaskInfo {
|
||||
taskId: string;
|
||||
description: string;
|
||||
state: string;
|
||||
infoCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface NodeData {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
export interface EdgeData {
|
||||
from: number;
|
||||
to: number;
|
||||
label: string;
|
||||
weight: number;
|
||||
}
|
||||
export interface GraphData {
|
||||
nodes: NodeData[];
|
||||
edges: EdgeData[];
|
||||
}
|
||||
// 内部接口 — 用于替换内联对象类型声明
|
||||
export interface PurgeCriteriaParams {
|
||||
subjectContains?: string;
|
||||
relationType?: string;
|
||||
targetContains?: string;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
targetType?: string;
|
||||
sourceHasStatus?: string;
|
||||
}
|
||||
export interface NewRelationParams {
|
||||
relation: string;
|
||||
target: string;
|
||||
}
|
||||
interface TripletData {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
}
|
||||
interface CriteriaData {
|
||||
subject?: string;
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
export interface MemoryRecallResult {
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
message: string;
|
||||
}
|
||||
export interface MemoryRecallParams {
|
||||
queryIntent: string;
|
||||
seedEntities?: string[];
|
||||
depth?: number;
|
||||
timeRange?: TimeRangeParams;
|
||||
sessionFilter?: string;
|
||||
}
|
||||
export interface MemoryCommitParams {
|
||||
triplets: TripletInput[];
|
||||
entityTypes?: Record<string, string>;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
interface MemoryCommitResult {
|
||||
committedCount: number;
|
||||
details: string[];
|
||||
}
|
||||
export interface MemoryPurgeParams {
|
||||
criteria: PurgeCriteriaParams;
|
||||
mode: 'soft' | 'hard' | 'supersede';
|
||||
newRelation?: NewRelationParams;
|
||||
}
|
||||
interface MemoryPurgeResult {
|
||||
deletedCount: number;
|
||||
message: string;
|
||||
}
|
||||
interface HotNodeInfo {
|
||||
name: string;
|
||||
mentionCount: number;
|
||||
type: string;
|
||||
}
|
||||
interface MemoryIntrospectResult {
|
||||
entityCount: number;
|
||||
relationCount: number;
|
||||
hotNodes: HotNodeInfo[];
|
||||
message: string;
|
||||
}
|
||||
interface ArchiveResult {
|
||||
archived: number;
|
||||
message: string;
|
||||
}
|
||||
interface PersonaResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface PersonaQueryResult {
|
||||
persona: Record<string, string>;
|
||||
found: boolean;
|
||||
}
|
||||
interface TaskCreateResult {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message: string;
|
||||
}
|
||||
interface TaskActionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface TaskQueryResult {
|
||||
tasks: TaskInfo[];
|
||||
message: string;
|
||||
}
|
||||
export interface PersonaUpdateParams {
|
||||
tone?: string;
|
||||
style?: string;
|
||||
personality?: string;
|
||||
catchphrase?: string;
|
||||
background?: string;
|
||||
}
|
||||
export interface TaskCreateParams {
|
||||
taskId: string;
|
||||
description: string;
|
||||
infoNodes?: string[];
|
||||
}
|
||||
export interface TaskSetStateParams {
|
||||
taskId: string;
|
||||
state: '进行中' | '已完成' | '已暂停' | '已取消' | '已归档';
|
||||
}
|
||||
export interface TaskDeleteParams {
|
||||
taskId: string;
|
||||
deleteInfoNodes?: boolean;
|
||||
}
|
||||
export interface TaskLinkInfoParams {
|
||||
taskId: string;
|
||||
infoNodeNames: string[];
|
||||
}
|
||||
export interface TaskArchiveParams {
|
||||
taskId: string;
|
||||
summary?: string;
|
||||
}
|
||||
export interface TaskQueryParams {
|
||||
limit?: number;
|
||||
stateFilter?: string;
|
||||
}
|
||||
// 节点详情连接项接口
|
||||
export interface ConnectionItem {
|
||||
type: string;
|
||||
target_name: string;
|
||||
}
|
||||
// 节点详情返回接口
|
||||
export interface NodeDetailInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
mention_count: number;
|
||||
connection_count: number;
|
||||
connections: ConnectionItem[];
|
||||
}
|
||||
// 图数据统计接口
|
||||
export interface GraphStats {
|
||||
maxDegree: number;
|
||||
avgDegree: number;
|
||||
}
|
||||
// 人设节点的固定名称
|
||||
const PERSONA_NODE_NAME: string = 'trulymem_persona_identity';
|
||||
const PERSONA_NODE_TYPE: string = 'PersonaNode';
|
||||
// ========= GraphMemoryService =========
|
||||
export class GraphMemoryService {
|
||||
private db: GraphDatabase;
|
||||
constructor(db: GraphDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
// ========= 记忆操作 =========
|
||||
/**
|
||||
* 记忆召回 — 关键词搜索 + BFS 扩展
|
||||
* 对应 tools.memory_recall
|
||||
*/
|
||||
async memoryRecall(params: MemoryRecallParams): Promise<MemoryRecallResult> {
|
||||
const depth: number = params.depth ?? 2;
|
||||
const result = await this.db.recall(params.queryIntent, params.seedEntities, depth, undefined, params.sessionFilter);
|
||||
const entities: EntityInfo[] = result.entities.map(e => {
|
||||
const entityItem: EntityInfo = {
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
mentionCount: e.mention_count,
|
||||
depth: e.depth ?? 0
|
||||
};
|
||||
return entityItem;
|
||||
});
|
||||
const relations: RelationInfo[] = result.relations.map(r => {
|
||||
const relationItem: RelationInfo = {
|
||||
source: r.source,
|
||||
target: r.target,
|
||||
type: r.type,
|
||||
confidence: r.confidence,
|
||||
sessionId: r.session_id,
|
||||
turnId: r.turn_id,
|
||||
depth: r.depth ?? 0
|
||||
};
|
||||
return relationItem;
|
||||
});
|
||||
return { entities, relations, message: result.message };
|
||||
}
|
||||
/**
|
||||
* 记忆写入 — 批量三元组
|
||||
* 对应 tools.memory_commit
|
||||
*/
|
||||
async memoryCommit(params: MemoryCommitParams): Promise<MemoryCommitResult> {
|
||||
const details: string[] = [];
|
||||
for (const t of params.triplets) {
|
||||
const subject = t.subject.trim();
|
||||
const relation = t.relation.trim();
|
||||
const object = t.object.trim();
|
||||
if (!subject || !relation || !object) {
|
||||
continue;
|
||||
}
|
||||
const triplets: TripletData[] = [{ subject, relation, object }];
|
||||
await this.db.commit(triplets, params.entityTypes, params.sessionId, params.turnId);
|
||||
details.push(`${subject} -[${relation}]-> ${object}`);
|
||||
}
|
||||
const result: MemoryCommitResult = {
|
||||
committedCount: details.length,
|
||||
details
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 记忆删除
|
||||
* 对应 tools.memory_purge
|
||||
*/
|
||||
async memoryPurge(params: MemoryPurgeParams): Promise<MemoryPurgeResult> {
|
||||
// soft 模式:调用 db.purge 逻辑删除
|
||||
if (params.mode === 'supersede' && params.newRelation) {
|
||||
// supersede: 先软删除旧关系,再新建
|
||||
const softSubject = params.criteria.subjectContains;
|
||||
const softRelation = params.criteria.relationType;
|
||||
const softTarget = params.criteria.targetContains;
|
||||
await this.db.purge({
|
||||
subject: softSubject,
|
||||
relation: softRelation,
|
||||
target: softTarget
|
||||
}, 'soft');
|
||||
// 新建替代关系
|
||||
const newSubj = softSubject || '';
|
||||
if (newSubj) {
|
||||
await this.db.commit([{
|
||||
subject: newSubj,
|
||||
relation: params.newRelation.relation,
|
||||
object: params.newRelation.target
|
||||
}]);
|
||||
}
|
||||
const supersedeResult: MemoryPurgeResult = {
|
||||
deletedCount: 1,
|
||||
message: '已用 supersede 模式替代记忆'
|
||||
};
|
||||
return supersedeResult;
|
||||
}
|
||||
const subjContains = params.criteria.subjectContains;
|
||||
const relType = params.criteria.relationType;
|
||||
const tgtContains = params.criteria.targetContains;
|
||||
const sessId = params.criteria.sessionId;
|
||||
await this.db.purge({
|
||||
subject: subjContains,
|
||||
relation: relType,
|
||||
target: tgtContains,
|
||||
sessionId: sessId,
|
||||
subjectContains: params.criteria.subjectContains,
|
||||
targetContains: params.criteria.targetContains,
|
||||
sourceType: params.criteria.sourceType,
|
||||
targetType: params.criteria.targetType,
|
||||
sourceHasStatus: params.criteria.sourceHasStatus
|
||||
}, params.mode === 'hard' ? 'hard' : 'soft');
|
||||
const purgeResult: MemoryPurgeResult = {
|
||||
deletedCount: subjContains || tgtContains || relType || sessId ? 1 : 0,
|
||||
message: `已${params.mode === 'hard' ? '物理删除' : '软删除'}匹配的记忆`
|
||||
};
|
||||
return purgeResult;
|
||||
}
|
||||
/**
|
||||
* 记忆状态查询
|
||||
*/
|
||||
async memoryIntrospect(sessionId?: string): Promise<MemoryIntrospectResult> {
|
||||
const stats = await this.db.introspect();
|
||||
const hotNodes: HotNodeInfo[] = [];
|
||||
// 从所有节点中获取前10个高频节点
|
||||
const searchAll = await this.db.search('');
|
||||
const sorted = searchAll.sort((a, b) => b.mentions - a.mentions).slice(0, 10);
|
||||
for (const n of sorted) {
|
||||
const nodeInfo: HotNodeInfo = {
|
||||
name: n.name,
|
||||
mentionCount: n.mentions,
|
||||
type: n.type
|
||||
};
|
||||
hotNodes.push(nodeInfo);
|
||||
}
|
||||
const result: MemoryIntrospectResult = {
|
||||
entityCount: stats.entity_count,
|
||||
relationCount: stats.relation_count,
|
||||
hotNodes,
|
||||
message: stats.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 关键词搜索节点
|
||||
*/
|
||||
async search(keyword: string): Promise<SearchResult[]> {
|
||||
return await this.db.search(keyword);
|
||||
}
|
||||
/**
|
||||
* 归档旧记忆
|
||||
*/
|
||||
async archive(days: number): Promise<ArchiveResult> {
|
||||
return await this.db.archive(days);
|
||||
}
|
||||
/**
|
||||
* 清理已删除的记忆
|
||||
*/
|
||||
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||
const result = await this.db.cleanup(dryRun);
|
||||
const cleanupResult: CleanupResult = {
|
||||
cleaned: result.cleaned,
|
||||
deletedRelations: result.deleted_relations,
|
||||
deletedOrphans: result.deleted_orphans,
|
||||
dryRun: result.dry_run,
|
||||
message: result.message || ''
|
||||
};
|
||||
return cleanupResult;
|
||||
}
|
||||
/**
|
||||
* 记忆图谱 — 在指定时间范围内查询关系
|
||||
* 对应 tools.memory_graph
|
||||
*/
|
||||
async memoryGraph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphOutput> {
|
||||
const dbResult = await this.db.graph(timeRange, sessionFilter);
|
||||
const nodeCount: number = dbResult.nodes.length;
|
||||
const edgeCount: number = dbResult.edges.length;
|
||||
const nodeList: GraphDataNode[] = [];
|
||||
const edgeList: GraphDataEdge[] = [];
|
||||
let idx: number = 0;
|
||||
while (idx < nodeCount) {
|
||||
const n = dbResult.nodes[idx];
|
||||
const id1: number = n.id;
|
||||
const label1: string = n.label;
|
||||
const type1: string = n.type;
|
||||
const mentions1: number = n.mentions;
|
||||
const depth1: number | undefined = n.depth;
|
||||
const graphNode: GraphDataNode = {
|
||||
id: id1,
|
||||
label: label1,
|
||||
type: type1,
|
||||
mentions: mentions1,
|
||||
depth: depth1
|
||||
};
|
||||
nodeList.push(graphNode);
|
||||
idx++;
|
||||
}
|
||||
idx = 0;
|
||||
while (idx < edgeCount) {
|
||||
const e = dbResult.edges[idx];
|
||||
const from1: number = e.from;
|
||||
const to1: number = e.to;
|
||||
const label1: string = e.label;
|
||||
const weight1: number = e.weight;
|
||||
const depth1: number | undefined = e.depth;
|
||||
const sessionId1: string | undefined = e.sessionId;
|
||||
const turnId1: number | undefined = e.turnId;
|
||||
const graphEdge: GraphDataEdge = {
|
||||
from: from1,
|
||||
to: to1,
|
||||
label: label1,
|
||||
weight: weight1,
|
||||
depth: depth1,
|
||||
sessionId: sessionId1,
|
||||
turnId: turnId1
|
||||
};
|
||||
edgeList.push(graphEdge);
|
||||
idx++;
|
||||
}
|
||||
const out: GraphOutput = {
|
||||
nodes: nodeList,
|
||||
edges: edgeList
|
||||
};
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* 记忆快照 — 在指定时间范围内查询实体和关系
|
||||
* 对应 tools.memory_snapshot
|
||||
*/
|
||||
async memorySnapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotOutput> {
|
||||
const dbResult = await this.db.snapshot(timeRange, sessionFilter);
|
||||
const entityCount: number = dbResult.entities.length;
|
||||
const relationCount: number = dbResult.relations.length;
|
||||
const entityList: SnapshotEntity[] = [];
|
||||
const relationList: SnapshotRelation[] = [];
|
||||
let idx: number = 0;
|
||||
while (idx < entityCount) {
|
||||
const e = dbResult.entities[idx];
|
||||
const name1: string = e.name;
|
||||
const type1: string = e.type;
|
||||
const mentionCount1: number = e.mention_count;
|
||||
const depth1: number | undefined = e.depth;
|
||||
const entity: SnapshotEntity = {
|
||||
name: name1,
|
||||
type: type1,
|
||||
mention_count: mentionCount1,
|
||||
depth: depth1
|
||||
};
|
||||
entityList.push(entity);
|
||||
idx++;
|
||||
}
|
||||
idx = 0;
|
||||
while (idx < relationCount) {
|
||||
const r = dbResult.relations[idx];
|
||||
const source1: string = r.source;
|
||||
const target1: string = r.target;
|
||||
const type1: string = r.type;
|
||||
const confidence1: number = r.confidence;
|
||||
const sessionId1: string | undefined = r.session_id;
|
||||
const turnId1: number | undefined = r.turn_id;
|
||||
const depth1: number | undefined = r.depth;
|
||||
const relation: SnapshotRelation = {
|
||||
source: source1,
|
||||
target: target1,
|
||||
type: type1,
|
||||
confidence: confidence1,
|
||||
session_id: sessionId1,
|
||||
turn_id: turnId1,
|
||||
depth: depth1
|
||||
};
|
||||
relationList.push(relation);
|
||||
idx++;
|
||||
}
|
||||
const out2: SnapshotOutput = {
|
||||
entities: entityList,
|
||||
relations: relationList
|
||||
};
|
||||
return out2;
|
||||
}
|
||||
/**
|
||||
* 记忆清理 — 归档旧记忆并清理孤立节点
|
||||
* 对应 tools.memory_cleanup
|
||||
*/
|
||||
async memoryCleanup(dryRun: boolean = false): Promise<CleanupResult> {
|
||||
const result = await this.db.cleanup(dryRun);
|
||||
const cleanupResult: CleanupResult = {
|
||||
cleaned: result.cleaned,
|
||||
deletedRelations: result.deleted_relations,
|
||||
deletedOrphans: result.deleted_orphans,
|
||||
dryRun: result.dry_run,
|
||||
message: result.message || ''
|
||||
};
|
||||
return cleanupResult;
|
||||
}
|
||||
/**
|
||||
* 记忆清理 — 删除指定条件的记忆
|
||||
* 对应 tools.memory_purge
|
||||
*/
|
||||
/**
|
||||
* 查询已归档的记忆
|
||||
* 对应 tools.memory_query_archived
|
||||
*/
|
||||
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||
try {
|
||||
const result = await this.db.queryArchived(days, keyword);
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
defaultLogger.error('queryArchived error: ' + JSON.stringify(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// ========= 人设管理 =========
|
||||
/**
|
||||
* 更新人设
|
||||
* 对应 tools.persona_update
|
||||
* 使用 PersonaNode + HAS_PERSONA 关系存储属性
|
||||
*/
|
||||
async personaUpdate(params: PersonaUpdateParams): Promise<PersonaResult> {
|
||||
try {
|
||||
// 1. 确保 PersonaNode 存在
|
||||
const personaTriplets: TripletData[] = [];
|
||||
const entityTypes: Record<string, string> = {};
|
||||
entityTypes[PERSONA_NODE_NAME] = PERSONA_NODE_TYPE;
|
||||
// 2. 逐个属性写入(作为关系),不使用 as any
|
||||
const toneVal = params.tone;
|
||||
const styleVal = params.style;
|
||||
const personalityVal = params.personality;
|
||||
const catchphraseVal = params.catchphrase;
|
||||
const backgroundVal = params.background;
|
||||
if (toneVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_TONE',
|
||||
object: toneVal
|
||||
});
|
||||
}
|
||||
if (styleVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_STYLE',
|
||||
object: styleVal
|
||||
});
|
||||
}
|
||||
if (personalityVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_PERSONALITY',
|
||||
object: personalityVal
|
||||
});
|
||||
}
|
||||
if (catchphraseVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_CATCHPHRASE',
|
||||
object: catchphraseVal
|
||||
});
|
||||
}
|
||||
if (backgroundVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_BACKGROUND',
|
||||
object: backgroundVal
|
||||
});
|
||||
}
|
||||
if (personaTriplets.length > 0) {
|
||||
await this.db.commit(personaTriplets, entityTypes);
|
||||
}
|
||||
const successResult: PersonaResult = {
|
||||
success: true,
|
||||
message: `已更新 ${personaTriplets.length} 个人设属性`
|
||||
};
|
||||
return successResult;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: PersonaResult = {
|
||||
success: false,
|
||||
message: `更新人设失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清除人设
|
||||
* 对应 tools.persona_clear
|
||||
*/
|
||||
async personaClear(): Promise<PersonaResult> {
|
||||
try {
|
||||
await this.db.purge({ subject: PERSONA_NODE_NAME }, 'hard');
|
||||
const result: PersonaResult = { success: true, message: '已清除所有人设信息' };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: PersonaResult = {
|
||||
success: false,
|
||||
message: `清除人设失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除单条人设属性
|
||||
* 对应 tools.persona_remove
|
||||
*/
|
||||
async personaRemove(attribute: string): Promise<PersonaResult> {
|
||||
try {
|
||||
// 将 attribute 转为关系名格式
|
||||
const relationName = 'HAS_PERSONA_' + attribute.toUpperCase();
|
||||
await this.db.purge({ subject: PERSONA_NODE_NAME, relation: relationName }, 'hard');
|
||||
return {
|
||||
success: true,
|
||||
message: `已删除人设属性: ${attribute}`
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: `删除人设属性失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 查询当前人设
|
||||
*/
|
||||
async personaQuery(): Promise<PersonaQueryResult> {
|
||||
const result = await this.db.recall(PERSONA_NODE_NAME, [PERSONA_NODE_NAME], 2);
|
||||
const persona: Record<string, string> = {};
|
||||
for (const rel of result.relations) {
|
||||
if (rel.source === PERSONA_NODE_NAME && rel.type.startsWith('HAS_PERSONA_')) {
|
||||
const key = rel.type.replace('HAS_PERSONA_', '').toLowerCase();
|
||||
persona[key] = rel.target;
|
||||
}
|
||||
}
|
||||
const queryResult: PersonaQueryResult = {
|
||||
persona,
|
||||
found: Object.keys(persona).length > 0
|
||||
};
|
||||
return queryResult;
|
||||
}
|
||||
// ========= 任务管理 =========
|
||||
/**
|
||||
* 创建任务节点
|
||||
* 对应 tools.task_create
|
||||
*/
|
||||
async taskCreate(params: TaskCreateParams): Promise<TaskCreateResult> {
|
||||
try {
|
||||
const entityTypes: Record<string, string> = {};
|
||||
entityTypes[params.taskId] = 'TaskNode';
|
||||
const triplets: TripletData[] = [
|
||||
{ subject: params.taskId, relation: 'description', object: params.description },
|
||||
{ subject: params.taskId, relation: 'has_state', object: '进行中' }
|
||||
];
|
||||
if (params.infoNodes && params.infoNodes.length > 0) {
|
||||
for (const infoNode of params.infoNodes) {
|
||||
entityTypes[infoNode] = 'InfoNode';
|
||||
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: infoNode });
|
||||
}
|
||||
}
|
||||
await this.db.commit(triplets, entityTypes);
|
||||
const result: TaskCreateResult = {
|
||||
success: true,
|
||||
taskId: params.taskId,
|
||||
message: `已创建任务: ${params.taskId}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskCreateResult = {
|
||||
success: false,
|
||||
taskId: params.taskId,
|
||||
message: `创建任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置任务状态
|
||||
* 对应 tools.task_set_state
|
||||
*/
|
||||
async taskSetState(params: TaskSetStateParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 先删旧的 has_state 关系,再新建
|
||||
await this.db.purge({ subject: params.taskId, relation: 'has_state' }, 'soft');
|
||||
await this.db.commit([{ subject: params.taskId, relation: 'has_state', object: params.state }]);
|
||||
const result: TaskActionResult = {
|
||||
success: true,
|
||||
message: `任务 ${params.taskId} 状态已设为: ${params.state}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `设置任务状态失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除任务
|
||||
* 对应 tools.task_delete
|
||||
*/
|
||||
async taskDelete(params: TaskDeleteParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 删除所有关联关系
|
||||
await this.db.purge({ subject: params.taskId }, 'hard');
|
||||
if (params.deleteInfoNodes !== false) {
|
||||
await this.db.purge({ target: params.taskId }, 'hard');
|
||||
}
|
||||
const result: TaskActionResult = { success: true, message: `已删除任务: ${params.taskId}` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `删除任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 关联信息节点到任务
|
||||
* 对应 tools.task_link_info
|
||||
*/
|
||||
async taskLinkInfo(params: TaskLinkInfoParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
const triplets: TripletData[] = [];
|
||||
const entityTypes: Record<string, string> = {};
|
||||
for (const nodeName of params.infoNodeNames) {
|
||||
entityTypes[nodeName] = 'InfoNode';
|
||||
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: nodeName });
|
||||
}
|
||||
await this.db.commit(triplets, entityTypes);
|
||||
const result: TaskActionResult = { success: true, message: `已关联 ${triplets.length} 个信息节点` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `关联信息节点失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 归档任务
|
||||
* 对应 tools.task_archive
|
||||
*/
|
||||
async taskArchive(params: TaskArchiveParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 归档任务:将状态设为已归档(archived),同时写入完成摘要
|
||||
await this.taskSetState({ taskId: params.taskId, state: '已归档' });
|
||||
if (params.summary) {
|
||||
await this.db.commit([
|
||||
{ subject: params.taskId, relation: 'archive_summary', object: params.summary }
|
||||
]);
|
||||
}
|
||||
const result: TaskActionResult = { success: true, message: `已归档任务: ${params.taskId}` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `归档任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 查询任务列表
|
||||
* 对应 tools.task_query
|
||||
*/
|
||||
async taskQuery(params?: TaskQueryParams): Promise<TaskQueryResult> {
|
||||
const limit: number = params?.limit ?? 10;
|
||||
const stateFilter: string | undefined = params?.stateFilter;
|
||||
const queryResult: TaskQueryResult = {
|
||||
tasks: await this.db.getRecentTasks(limit, stateFilter) as TaskInfo[],
|
||||
message: `找到 ${limit} 个任务`
|
||||
};
|
||||
return queryResult;
|
||||
}
|
||||
// ========= 图数据 =========
|
||||
/**
|
||||
* 获取用于 WebView 的完整图数据
|
||||
*/
|
||||
async getGraphDataForView(): Promise<GraphData> {
|
||||
// 用空关键词召回所有数据
|
||||
const recallResult = await this.db.recall('', [], 3);
|
||||
const nodes: NodeData[] = [];
|
||||
const edges: EdgeData[] = [];
|
||||
let nodeIdCounter = 1;
|
||||
const nameToId: Record<string, number> = {};
|
||||
for (const entity of recallResult.entities) {
|
||||
const id = nodeIdCounter;
|
||||
nodeIdCounter++;
|
||||
nameToId[entity.name] = id;
|
||||
const node: NodeData = {
|
||||
id,
|
||||
label: entity.name,
|
||||
type: entity.type,
|
||||
mentions: entity.mention_count
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
for (const rel of recallResult.relations) {
|
||||
const from = nameToId[rel.source];
|
||||
const to = nameToId[rel.target];
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const edge: EdgeData = {
|
||||
from,
|
||||
to,
|
||||
label: rel.type,
|
||||
weight: rel.confidence
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
}
|
||||
const graphData: GraphData = { nodes, edges };
|
||||
return graphData;
|
||||
}
|
||||
/**
|
||||
* 查询节点的完整信息 — 自身属性 + 所有相连关系
|
||||
*/
|
||||
async getNodeDetail(nodeName: string): Promise<NodeDetailInfo | null> {
|
||||
try {
|
||||
const recallResult = await this.db.recall(nodeName, [nodeName], 1);
|
||||
if (recallResult.entities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const entity = recallResult.entities[0];
|
||||
const connections: ConnectionItem[] = [];
|
||||
let connectionCount = 0;
|
||||
for (const rel of recallResult.relations) {
|
||||
if (rel.source === nodeName) {
|
||||
const conn: ConnectionItem = {
|
||||
type: rel.type,
|
||||
target_name: rel.target
|
||||
};
|
||||
connections.push(conn);
|
||||
connectionCount++;
|
||||
}
|
||||
else if (rel.target === nodeName) {
|
||||
const conn: ConnectionItem = {
|
||||
type: rel.type + ' (反向)',
|
||||
target_name: rel.source
|
||||
};
|
||||
connections.push(conn);
|
||||
connectionCount++;
|
||||
}
|
||||
}
|
||||
const detail: NodeDetailInfo = {
|
||||
name: entity.name,
|
||||
type: entity.type,
|
||||
mention_count: entity.mention_count,
|
||||
connection_count: connectionCount,
|
||||
connections
|
||||
};
|
||||
return detail;
|
||||
}
|
||||
catch (err) {
|
||||
defaultLogger.error('getNodeDetail error: ' + JSON.stringify(err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 返回带连接度数的图数据(每个节点增加 degree 字段)
|
||||
*/
|
||||
async getJoinedData(): Promise<GraphData> {
|
||||
const graphData = await this.getGraphDataForView();
|
||||
// 计算每个节点的连接度数
|
||||
const degreeMap: Record<number, number> = {};
|
||||
for (const edge of graphData.edges) {
|
||||
degreeMap[edge.from] = (degreeMap[edge.from] || 0) + 1;
|
||||
degreeMap[edge.to] = (degreeMap[edge.to] || 0) + 1;
|
||||
}
|
||||
// 手动为节点附加 degree(ArkTS 不支持展开运算符)
|
||||
const nodesWithDegree: NodeData[] = [];
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
const orig = graphData.nodes[i];
|
||||
const copy: NodeData = {
|
||||
id: orig.id,
|
||||
label: orig.label,
|
||||
type: orig.type,
|
||||
mentions: orig.mentions
|
||||
};
|
||||
nodesWithDegree.push(copy);
|
||||
}
|
||||
const graphResult: GraphData = {
|
||||
nodes: nodesWithDegree,
|
||||
edges: graphData.edges
|
||||
};
|
||||
return graphResult;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,43 @@
|
||||
export enum WidthBreakpoint {
|
||||
WIDTH_XS = "xs",
|
||||
WIDTH_SM = "sm",
|
||||
WIDTH_MD = "md",
|
||||
WIDTH_LG = "lg",
|
||||
WIDTH_XL = "xl"
|
||||
}
|
||||
export interface BreakpointTypes<T> {
|
||||
xs?: T;
|
||||
sm: T;
|
||||
md: T;
|
||||
lg: T;
|
||||
xl?: T;
|
||||
}
|
||||
export class BreakpointType<T> {
|
||||
private xs: T;
|
||||
private sm: T;
|
||||
private md: T;
|
||||
private lg: T;
|
||||
private xl: T;
|
||||
public constructor(param: BreakpointTypes<T>) {
|
||||
this.xs = param.xs ?? param.sm;
|
||||
this.sm = param.sm;
|
||||
this.md = param.md;
|
||||
this.lg = param.lg;
|
||||
this.xl = param.xl ?? param.lg;
|
||||
}
|
||||
public getValue(currentBreakpoint: WidthBreakpoint): T {
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_XS) {
|
||||
return this.xs;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_SM) {
|
||||
return this.sm;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_MD) {
|
||||
return this.md;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_XL) {
|
||||
return this.xl;
|
||||
}
|
||||
return this.lg;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
import hilog from "@ohos:hilog";
|
||||
class Logger {
|
||||
private domain: number;
|
||||
private prefix: string;
|
||||
private format: string = "%{public}s, %{public}s";
|
||||
public constructor(prefix: string) {
|
||||
this.prefix = prefix;
|
||||
this.domain = 0xFF00;
|
||||
}
|
||||
public debug(...args: Object[]): void {
|
||||
hilog.debug(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public info(...args: Object[]): void {
|
||||
hilog.info(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public warn(...args: Object[]): void {
|
||||
hilog.warn(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public error(...args: Object[]): void {
|
||||
hilog.error(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
}
|
||||
export const defaultLogger = new Logger("[TrulyMEM]");
|
||||
export default defaultLogger;
|
||||
Binary file not shown.
@ -0,0 +1,42 @@
|
||||
import { WidthBreakpoint } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export interface VMEvent {
|
||||
}
|
||||
export class BaseViewModel {
|
||||
protected isAttached: boolean = false;
|
||||
protected isDisposed: boolean = false;
|
||||
protected currentBreakpoint: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
|
||||
attach(): void {
|
||||
if (this.isAttached) {
|
||||
return;
|
||||
}
|
||||
this.isAttached = true;
|
||||
this.onAttach();
|
||||
}
|
||||
detach(): void {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
this.isAttached = false;
|
||||
this.onDetach();
|
||||
}
|
||||
dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this.detach();
|
||||
this.onDispose();
|
||||
}
|
||||
protected onAttach(): void {
|
||||
}
|
||||
protected onDetach(): void {
|
||||
}
|
||||
protected onDispose(): void {
|
||||
}
|
||||
public get attached(): boolean {
|
||||
return this.isAttached;
|
||||
}
|
||||
public get disposed(): boolean {
|
||||
return this.isDisposed;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{"hspPkgNames":[],"compileEntries":["&@ohos/common/Index&1.0.0","&@ohos/common/src/main/ets/component/ImmersiveTabNavigation&1.0.0","&@ohos/common/src/main/ets/constant/TrulyMEMConstants&1.0.0","&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0","&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0","&@ohos/common/src/main/ets/service/AIAgentService&1.0.0","&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0","&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0","&@ohos/common/src/main/ets/util/Logger&1.0.0","&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0","&@ohos/graph/Index&1.0.0","&@ohos/graph/src/main/ets/components/GraphComponents&1.0.0","&@ohos/graph/src/main/ets/pages/GraphPage&1.0.0","&@ohos/chat/Index&1.0.0","&@ohos/chat/src/main/ets/components/ChatComponents&1.0.0","&@ohos/chat/src/main/ets/pages/ChatPage&1.0.0","&@ohos/settings/Index&1.0.0","&@ohos/settings/src/main/ets/components/SettingsComponents&1.0.0","&@ohos/settings/src/main/ets/pages/SettingsPage&1.0.0","&@ohos/phone/src/main/ets/pages/MainPage&","&phone/build/generated/r/ResourceTable&","&@ohos/phone/src/main/ets/entryability/EntryAbility&","&@ohos/phone/src/main/ets/pages/SplashPage&","&@ohos/phone/src/main/ets/pages/Index&"],"updateVersionInfo":{}}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user