Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e779c16595 | |||
| 5e4fb8222a | |||
| 5ae2143822 | |||
| ab3d910387 | |||
| 5916d2749c | |||
| 67c6b38e50 | |||
| 1a4a8dce98 | |||
| c8571f9d5f | |||
| dd0ab4dcbe | |||
| ba44d50d7a | |||
| b9a2cba2a7 | |||
| c060a59710 | |||
| d594099f02 | |||
| 45c098d10b | |||
| ce7d3b532f | |||
| 4bfa4673be | |||
| e6dc56b985 | |||
| 11ee329943 | |||
| 4c32dbb949 | |||
| 5c59cd2ceb | |||
| e83151bd1e | |||
| 2a42183aaa | |||
| 7c2441682c | |||
| 14bbcfde3d | |||
| be6555cca1 | |||
| c4f7b5a645 | |||
| 1a971fee73 | |||
| 4770b3b990 |
86
.gitignore
vendored
86
.gitignore
vendored
@ -1,77 +1,19 @@
|
|||||||
# Python
|
# Build cache
|
||||||
__pycache__/
|
.hvigor/
|
||||||
*.py[cod]
|
entry/build/default/
|
||||||
*$py.class
|
trulymem-core/build/default/
|
||||||
*.so
|
trulymem-core/.preview/
|
||||||
.Python
|
build/
|
||||||
develop-eggs/
|
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# Virtual Environment
|
# Python cache
|
||||||
venv/
|
__pycache__/
|
||||||
test_venv/
|
*.pyc
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
.arts/
|
|
||||||
.codeartsdoer/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
*.db
|
graph_memory.db
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# Logs
|
# IDE
|
||||||
*.log
|
.idea/
|
||||||
logs/
|
.vscode/
|
||||||
|
*.iml
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Sensitive
|
|
||||||
.env
|
|
||||||
*.key
|
|
||||||
*.pem
|
|
||||||
config.json
|
|
||||||
|
|
||||||
# Build
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Temporary
|
|
||||||
*.tmp
|
|
||||||
*.bak
|
|
||||||
|
|
||||||
# AI Generated
|
|
||||||
jimeng*.png
|
|
||||||
|
|
||||||
# Test Cache
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Web config (contains passwords, secret keys)
|
|
||||||
web_config.json
|
|
||||||
|
|
||||||
# Node.js
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Test artifacts
|
|
||||||
/session_*/
|
|
||||||
task_archive/
|
|
||||||
ts/
|
|
||||||
|
|||||||
10
AppScope/app.json5
Normal file
10
AppScope/app.json5
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"bundleName": "com.trulymem.app",
|
||||||
|
"vendor": "trulymem",
|
||||||
|
"versionCode": 1000001,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"icon": "$media:layered_image",
|
||||||
|
"label": "$string:app_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
AppScope/resources/base/media/layered_image.svg
Normal file
5
AppScope/resources/base/media/layered_image.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||||
|
<rect width="200" height="200" rx="30" fill="#6366f1"/>
|
||||||
|
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 313 B |
196
LICENSE
196
LICENSE
@ -1,196 +0,0 @@
|
|||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2026 jianf
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2026 jianf
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of the program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its author. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers. In our view, they should not
|
|
||||||
allow patents to restrict development and use of software on
|
|
||||||
general-purpose computers. But in those that do, we wish to avoid the
|
|
||||||
special danger that patents applied to a free program could make it
|
|
||||||
effectively proprietary. To prevent this, the GPL assures that patents
|
|
||||||
cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or menu items, or similar,
|
|
||||||
each item in the list is treated as if it were an independent command
|
|
||||||
or menu item. If the interface presents a list of options as a dialog
|
|
||||||
box, the list is treated as a single option.
|
|
||||||
|
|
||||||
[The full text of the GPL v3 license continues with sections 1-17,
|
|
||||||
but is truncated here for brevity. The complete license text is
|
|
||||||
available at https://www.gnu.org/licenses/gpl-3.0.txt]
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) 2026 jianf
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
TrulyMEM Copyright (C) 2026 jianf
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
193
README.md
193
README.md
@ -1,193 +0,0 @@
|
|||||||
# TrulyMEM - TrueHumanMEM
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> **📜 开源协议**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
|
||||||
> 本项目自由开源,可自由使用、修改和分发,但修改后的作品必须以相同许可证发布。
|
|
||||||
|
|
||||||
> **English**: [Switch to English version](./README_EN.md)
|
|
||||||
|
|
||||||
**让 AI 拥有自知、可塑、有分寸感的长期记忆**
|
|
||||||
|
|
||||||
*The More Human Choice.*
|
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
||||||
[](https://www.python.org/downloads/)
|
|
||||||
[]()
|
|
||||||
[]()
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 故事的开头
|
|
||||||
|
|
||||||
行业普遍认为,LLM 海量参数让其涌现了智能。但这个智能是「死的」——它不会真的记住,也不理解「记住」的概念。它输出的一切,都是当前输入的全部文本经历无数次前向传播计算出的概率最优解。LLM 不会因为某次对话意识到错误而去修正权重,也无法因此针对模型进行一次反向传播。它的意识是被冻结的,展现出的智能只是冻结的意识的回响。
|
|
||||||
|
|
||||||
现在的所谓记忆系统,只是将记忆外化,让「系统」去替 LLM 记住。或者就是粗暴地将一切上下文文本丢给 LLM。这就是对模型输入的浪费。
|
|
||||||
|
|
||||||
**TrulyMEM 想,既然 LLM 无法实时纠正模型权重,为什么不把记忆权交还给 LLM 呢?**
|
|
||||||
|
|
||||||
我们提供一系列机制,让 LLM 决定它要记住什么、遗忘,什么是重点、什么是糟粕。LLM 推理的过程,就是思考的过程,也是回忆的过程。完全摒弃传统的 messages 数组上下文,将全部记忆以**三元组(图)**的形式保存在图数据库中。在 LLM 思考时,可以按照图数据库的链接自主跳转、联想相关关系,让 LLM 自然地实现联想与回忆。
|
|
||||||
|
|
||||||
赋予 LLM 真正的记忆。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 方式一:打包后的可执行文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Windows: TrulyMEM.exe
|
|
||||||
# Linux/macOS: TrulyMEM
|
|
||||||
chmod +x TrulyMEM
|
|
||||||
./TrulyMEM
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式二:从源码运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo-url>
|
|
||||||
cd TrulyMEM-TrueHumanMEM
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
python trulymem_entry.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 首次使用(TUI 登录)
|
|
||||||
|
|
||||||
首次运行时会检测数据库状态,引导你完成:
|
|
||||||
|
|
||||||
1. **TUI 登录页面** — 如果是新部署或无迁移需求,直接设置账号密码
|
|
||||||
2. **旧版自动迁移** — 如果检测到旧版 `~/.trulymem/config.json`,自动引导迁移为多用户模式
|
|
||||||
3. **首个用户自动成为管理员**
|
|
||||||
|
|
||||||
登录后进入聊天界面:
|
|
||||||
|
|
||||||
1. 按 **F2** 展开右侧配置面板
|
|
||||||
2. 输入 **API Key**(支持 DeepSeek、OpenAI 等兼容 API)
|
|
||||||
3. 按 **Enter** 保存配置
|
|
||||||
4. 开始对话!
|
|
||||||
|
|
||||||
📌 **管理员** 可在右侧面板管理 Web 服务开关、修改 Web 登录凭据。
|
|
||||||
📌 **普通用户** 只能配置 API Key 和模型参数。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 多用户系统
|
|
||||||
|
|
||||||
TrulyMEM 支持多用户隔离,每个用户拥有独立的配置和数据目录:
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.trulymem/
|
|
||||||
├── trulymem.db # 全局用户数据库
|
|
||||||
├── .migrated # 旧版迁移标记
|
|
||||||
├── admin/
|
|
||||||
│ ├── config.json # 管理员配置
|
|
||||||
│ └── admin_graph.db # 管理员知识图谱
|
|
||||||
└── user2/
|
|
||||||
├── config.json # user2 配置
|
|
||||||
└── user2_graph.db # user2 知识图谱
|
|
||||||
```
|
|
||||||
|
|
||||||
- **首个注册用户自动成为管理员**
|
|
||||||
- 管理员可在 Web 设置页添加/删除用户
|
|
||||||
- 普通用户无法看到用户管理区域
|
|
||||||
|
|
||||||
### Web 可视化界面
|
|
||||||
|
|
||||||
TUI 启动后,管理员可在右侧面板勾选「启用 Web 服务」自动启动,或手动运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 手动启动 Web 服务
|
|
||||||
python web_api.py --port 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
然后打开浏览器访问 `http://localhost:4096`。
|
|
||||||
|
|
||||||
**首次访问** → 自动跳转至设置页,创建管理员账号 → 跳转至星图可视化页面。
|
|
||||||
|
|
||||||
**Web 功能:**
|
|
||||||
- 🌟 星图可视化浏览知识图谱
|
|
||||||
- ⚙ 设置页:修改密码、管理用户(管理员专属)
|
|
||||||
- 🔒 会话认证,多用户安全隔离
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 打包构建
|
|
||||||
|
|
||||||
项目支持 PyInstaller 打包为单文件可执行文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
bash build/build_linux.sh
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
bash build/build_macos.sh
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
build\build_windows.bat
|
|
||||||
|
|
||||||
# AppImage(Linux 通用打包)
|
|
||||||
bash build/build_appimage.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
构建产出在 `dist/` 目录:
|
|
||||||
|
|
||||||
| 文件 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| `TrulyMEM` | TUI 主程序(含 Web 子进程启动能力) |
|
|
||||||
| `trulymem-web` | Web 服务独立二进制(TUI 启动子进程时自动使用) |
|
|
||||||
|
|
||||||
### Web 启动优先级(TUI 内)
|
|
||||||
1. 同目录 `trulymem-web` 二进制(打包环境)
|
|
||||||
2. `sys._MEIPASS/web_api.py`(PyInstaller 数据文件回退)
|
|
||||||
3. `python3 web_api.py`(开发环境回退)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文档索引
|
|
||||||
|
|
||||||
详细技术文档请参阅 [docs/zh/](docs/zh/) 目录:
|
|
||||||
|
|
||||||
| 文档 | 内容 |
|
|
||||||
|------|------|
|
|
||||||
| [docs/zh/architecture.md](docs/zh/architecture.md) | 系统架构和技术设计 |
|
|
||||||
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 完整启动指南与配置说明 |
|
|
||||||
| [docs/zh/memory.md](docs/zh/memory.md) | 内部记忆工作机制 |
|
|
||||||
| [docs/zh/persona.md](docs/zh/persona.md) | 人设图机制 |
|
|
||||||
| [docs/zh/working_memory.md](docs/zh/working_memory.md) | 连续性任务处理机制 |
|
|
||||||
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口(供扩展开发) |
|
|
||||||
| [docs/zh/prompts.md](docs/zh/prompts.md) | 提示词管理模块 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 贡献
|
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request!
|
|
||||||
|
|
||||||
1. Fork 本仓库
|
|
||||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
|
||||||
5. 创建 Pull Request
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
本项目采用 **GNU General Public License v3.0 (GPLv3)** 许可证开源。
|
|
||||||
详见 [LICENSE](LICENSE) 文件。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 特别鸣谢
|
|
||||||
|
|
||||||
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — 学术指导
|
|
||||||
- [逝水秋生白](https://atomgit.com/cenber) — 架构支持
|
|
||||||
- anzhitinglan — 测试资源支持
|
|
||||||
- 崔莉萍老师 — 理论指导
|
|
||||||
- Annie — 专业指导
|
|
||||||
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
|
|
||||||
193
README_EN.md
193
README_EN.md
@ -1,193 +0,0 @@
|
|||||||
# TrulyMEM - TrueHumanMEM
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> **📜 License**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
|
|
||||||
> This project is free and open source. You are free to use, modify, and distribute, but modified works must be distributed under the same license.
|
|
||||||
|
|
||||||
> **中文**: [切换到中文版](./README.md)
|
|
||||||
|
|
||||||
**Give AI self-awareness, plasticity, and a sense of proportion in long-term memory**
|
|
||||||
|
|
||||||
*The More Human Choice.*
|
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
||||||
[](https://www.python.org/downloads/)
|
|
||||||
[]()
|
|
||||||
[]()
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Story
|
|
||||||
|
|
||||||
Industry believes that LLMs' massive parameters give them emergent intelligence. But this intelligence is "dead" — it cannot truly remember, nor understand the concept of "remembering". Everything it outputs is the probabilistic optimal solution calculated through countless forward passes on the current input text. The LLM cannot correct its weights based on errors in a conversation, nor perform a backward pass. Its consciousness is frozen — what appears as intelligence is merely the echo of this frozen consciousness.
|
|
||||||
|
|
||||||
Current "memory systems" merely externalize memory, letting the "system" remember for the LLM. Or they dump all context text to the LLM. This is a waste of the model's limited input context.
|
|
||||||
|
|
||||||
**TrulyMEM asks: since the LLM cannot correct model weights in real-time, why not give the memory authority back to the LLM?**
|
|
||||||
|
|
||||||
We provide a series of mechanisms for the LLM to decide what to remember, what to forget, what's important, what's trivial. The LLM's reasoning process is also its thinking and recalling process. Abandoning the traditional messages array context, all memories are stored as **triplets (graph)** in the graph database. When the LLM thinks, it can autonomously jump through graph links to associate related relationships, enabling natural association and recall.
|
|
||||||
|
|
||||||
Give the LLM true memory.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Method 1: Run Packaged Executable
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Windows: TrulyMEM.exe
|
|
||||||
# Linux/macOS: TrulyMEM
|
|
||||||
chmod +x TrulyMEM
|
|
||||||
./TrulyMEM
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Run from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo-url>
|
|
||||||
cd TrulyMEM-TrueHumanMEM
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
python trulymem_entry.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### First Run (TUI Login)
|
|
||||||
|
|
||||||
On first launch, TrulyMEM will guide you through:
|
|
||||||
|
|
||||||
1. **TUI Login Screen** — Set up your username and password
|
|
||||||
2. **Auto Migration** — If old `~/.trulymem/config.json` is detected, guides you through multi-user migration
|
|
||||||
3. **First user becomes admin automatically**
|
|
||||||
|
|
||||||
After login:
|
|
||||||
|
|
||||||
1. Press **F2** to expand the right-side configuration panel
|
|
||||||
2. Enter your **API Key** (supports DeepSeek, OpenAI, etc.)
|
|
||||||
3. Press **Enter** to save
|
|
||||||
4. Start chatting!
|
|
||||||
|
|
||||||
📌 **Admin users** can manage Web service settings and Web login credentials in the side panel.
|
|
||||||
📌 **Regular users** can only configure API Key and model parameters.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Multi-User System
|
|
||||||
|
|
||||||
TrulyMEM supports isolated multi-user environments. Each user has their own config and database:
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.trulymem/
|
|
||||||
├── trulymem.db # Global user database
|
|
||||||
├── .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
|
|
||||||
```
|
|
||||||
|
|
||||||
- **First user becomes admin automatically**
|
|
||||||
- Admins can add/delete users on the Web settings page
|
|
||||||
- Regular users cannot see the user management section
|
|
||||||
|
|
||||||
### Web Visualization Interface
|
|
||||||
|
|
||||||
From TUI, admin users can enable Web service via the right-side panel checkbox, or start manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start Web service
|
|
||||||
python web_api.py --port 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open `http://localhost:4096` in your browser.
|
|
||||||
|
|
||||||
**First visit** → Auto-redirect to setup page → Create admin account → Redirect to star map visualization.
|
|
||||||
|
|
||||||
**Web features:**
|
|
||||||
- 🌟 Star map visualization for browsing the knowledge graph
|
|
||||||
- ⚙ Settings page: change password, manage users (admin only)
|
|
||||||
- 🔒 Session-based authentication with multi-user isolation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
TrulyMEM supports PyInstaller packaging into single-file executables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
bash build/build_linux.sh
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
bash build/build_macos.sh
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
build\build_windows.bat
|
|
||||||
|
|
||||||
# AppImage (Linux universal)
|
|
||||||
bash build/build_appimage.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Build outputs in `dist/`:
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `TrulyMEM` | TUI main program (can spawn Web subprocess) |
|
|
||||||
| `trulymem-web` | Web service standalone binary (auto-detected by TUI) |
|
|
||||||
|
|
||||||
### Web binary priority (within TUI)
|
|
||||||
1. `trulymem-web` in same directory (packaged)
|
|
||||||
2. `sys._MEIPASS/web_api.py` (PyInstaller data fallback)
|
|
||||||
3. `python3 web_api.py` (development fallback)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Index
|
|
||||||
|
|
||||||
Detailed technical documentation in the [docs/en/](docs/en/) directory:
|
|
||||||
|
|
||||||
| Document | Content |
|
|
||||||
|----------|---------|
|
|
||||||
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and technical design |
|
|
||||||
| [docs/en/quick_start.md](docs/en/quick_start.md) | Complete startup guide and configuration |
|
|
||||||
| [docs/en/memory.md](docs/en/memory.md) | Internal memory working mechanism |
|
|
||||||
| [docs/en/persona.md](docs/en/persona.md) | Persona Graph mechanism |
|
|
||||||
| [docs/en/working_memory.md](docs/en/working_memory.md) | Continuous task handling mechanism |
|
|
||||||
| [docs/en/api.md](docs/en/api.md) | BackendServer API (for extension development) |
|
|
||||||
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management module |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Welcome to submit Issues and Pull Requests!
|
|
||||||
|
|
||||||
1. Fork this repository
|
|
||||||
2. Create feature branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Create Pull Request
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the **GNU General Public License v3.0 (GPLv3)**.
|
|
||||||
See [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Special Thanks
|
|
||||||
|
|
||||||
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — Academic guidance
|
|
||||||
- [逝水秋生白](https://atomgit.com/cenber) — Architecture support
|
|
||||||
- anzhitinglan — Testing resource support
|
|
||||||
- 崔莉萍老师 — Theoretical guidance
|
|
||||||
- Annie — Professional guidance
|
|
||||||
- 王梓沣、马悦华、隆梦婷 — Neuroscience theory support
|
|
||||||
45
TrulyMEM.spec
Normal file
45
TrulyMEM.spec
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
datas = [('ui/styles', 'ui/styles'), ('core/prompts/templates', 'core/prompts/templates'), ('static', 'static'), ('templates', 'templates'), ('core/web_api.py', 'core/')]
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = ['textual', 'textual.app', 'textual.widgets', 'textual.css', 'openai', 'openai._client', 'neo4j', 'sqlite3', 'core', 'core.embedded_db', 'core.graph_client', 'core.tool_executor', 'core.tool_limiter', 'core.tools', 'core.tools.memory_tools', 'core.prompts', 'core.prompts.prompt_manager', 'core.server', 'core.client', 'core.migrate', 'core.activity_recorder', 'ui', 'ui.app', 'ui.login_screen', 'ui.models', 'ui.models.message', 'ui.models.config', 'ui.models.log_entry', 'ui.widgets', 'ui.handlers', 'ui.services', 'ui.services.config_manager', 'ui.services.config_service', 'core.web_api', 'flask', 'flask_cors', 'werkzeug']
|
||||||
|
tmp_ret = collect_all('textual')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['trulymem_entry.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='TrulyMEM',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
91
build-profile.json5
Normal file
91
build-profile.json5
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"signingConfigs": [],
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"targetSdkVersion": "6.1.0(23)",
|
||||||
|
"compatibleSdkVersion": "6.1.0(23)",
|
||||||
|
"runtimeOS": "HarmonyOS",
|
||||||
|
"buildOption": {
|
||||||
|
"strictMode": {
|
||||||
|
"caseSensitiveCheck": true,
|
||||||
|
"useNormalizedOHMUrl": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildModeSet": [
|
||||||
|
{
|
||||||
|
"name": "debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"srcPath": "./common",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "graph",
|
||||||
|
"srcPath": "./features/graph",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chat",
|
||||||
|
"srcPath": "./features/chat",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings",
|
||||||
|
"srcPath": "./features/settings",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phone",
|
||||||
|
"srcPath": "./products/phone",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
135
build.log
Normal file
135
build.log
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
===== Building TrulyMEM for Linux =====
|
||||||
|
Project root: /home/program/TrulyMEM-TrueHumanMEM
|
||||||
|
The virtual environment was not created successfully because ensurepip is not
|
||||||
|
available. On Debian/Ubuntu systems, you need to install the python3-venv
|
||||||
|
package using the following command.
|
||||||
|
|
||||||
|
apt install python3.13-venv
|
||||||
|
|
||||||
|
You may need to use sudo with that command. After installing the python3-venv
|
||||||
|
package, recreate your virtual environment.
|
||||||
|
|
||||||
|
Failing command: /home/program/TrulyMEM-TrueHumanMEM/.venv_build/bin/python3
|
||||||
|
|
||||||
|
Warning: venv creation failed, falling back to system Python
|
||||||
|
Cleaning previous builds...
|
||||||
|
================================
|
||||||
|
Building TrulyMEM (TUI + Web embedded)
|
||||||
|
================================
|
||||||
|
29 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.4
|
||||||
|
29 INFO: Python: 3.13.12
|
||||||
|
31 INFO: Platform: Linux-6.1.0-44-amd64-x86_64-with-glibc2.42
|
||||||
|
31 INFO: Python environment: /usr
|
||||||
|
33 INFO: Removing temporary files and cleaning cache in /root/.cache/pyinstaller
|
||||||
|
34 INFO: Module search paths (PYTHONPATH):
|
||||||
|
['/home/program/TrulyMEM-TrueHumanMEM',
|
||||||
|
'/home/program/TrulyMEM-TrueHumanMEM',
|
||||||
|
'/usr/lib/python313.zip',
|
||||||
|
'/usr/lib/python3.13',
|
||||||
|
'/usr/lib/python3.13/lib-dynload',
|
||||||
|
'/usr/local/lib/python3.13/dist-packages',
|
||||||
|
'/usr/lib/python3/dist-packages',
|
||||||
|
'/home/program/TrulyMEM-TrueHumanMEM']
|
||||||
|
141 INFO: Appending 'datas' from .spec
|
||||||
|
141 INFO: checking Analysis
|
||||||
|
141 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||||
|
141 INFO: Looking for Python shared library...
|
||||||
|
149 INFO: Using Python shared library: /usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0
|
||||||
|
149 INFO: Running Analysis Analysis-00.toc
|
||||||
|
149 INFO: Target bytecode optimization level: 0
|
||||||
|
149 INFO: Initializing module dependency graph...
|
||||||
|
149 INFO: Initializing module graph hook caches...
|
||||||
|
153 INFO: Analyzing modules for base_library.zip ...
|
||||||
|
645 INFO: Processing standard module hook 'hook-encodings.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
1604 INFO: Processing standard module hook 'hook-pickle.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2203 INFO: Processing standard module hook 'hook-heapq.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2434 INFO: Caching module dependency graph...
|
||||||
|
2456 INFO: Analyzing /home/program/TrulyMEM-TrueHumanMEM/trulymem_entry.py
|
||||||
|
2487 INFO: Processing standard module hook 'hook-sqlite3.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2606 INFO: Processing standard module hook 'hook-platform.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2628 INFO: Processing standard module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2631 INFO: Processing standard module hook 'hook-_ctypes.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2641 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
2641 INFO: SetuptoolsInfo: initializing cached setuptools info...
|
||||||
|
4602 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
4748 INFO: Processing standard module hook 'hook-xml.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
5155 INFO: Processing standard module hook 'hook-pydantic.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
5444 INFO: Processing standard module hook 'hook-rich.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
5729 INFO: Processing standard module hook 'hook-pygments.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
6155 INFO: Processing standard module hook 'hook-chardet.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
7547 INFO: Processing standard module hook 'hook-zoneinfo.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
8697 INFO: Processing standard module hook 'hook-certifi.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
8766 INFO: Processing standard module hook 'hook-anyio.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
9419 INFO: Processing standard module hook 'hook-difflib.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
10529 INFO: Processing standard module hook 'hook-numpy.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
11824 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
12622 INFO: Processing standard module hook 'hook-pytz.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
13192 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
13199 INFO: Processing standard module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
14917 INFO: Processing standard module hook 'hook-jinja2.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
15284 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15285 INFO: Setuptools: 'importlib_metadata' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.importlib_metadata'!
|
||||||
|
15289 INFO: Processing standard module hook 'hook-setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
15296 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15314 INFO: Processing pre-safe-import-module hook 'hook-jaraco.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15314 INFO: Setuptools: 'jaraco' appears to be a partial setuptools-vendored copy - extending search paths to ['/usr/lib/python3/dist-packages/jaraco', '/usr/lib/python3/dist-packages/setuptools/_vendor/jaraco']!
|
||||||
|
15315 INFO: Processing pre-safe-import-module hook 'hook-jaraco.functools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15320 INFO: Processing pre-safe-import-module hook 'hook-more_itertools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15459 INFO: Processing pre-safe-import-module hook 'hook-packaging.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15575 INFO: Processing pre-safe-import-module hook 'hook-jaraco.text.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15581 INFO: Processing standard module hook 'hook-jaraco.text.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
15614 INFO: Processing pre-safe-import-module hook 'hook-importlib_resources.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15615 INFO: Processing pre-safe-import-module hook 'hook-jaraco.context.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15620 INFO: Processing pre-safe-import-module hook 'hook-backports.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15620 INFO: Setuptools: 'backports' appears to be a full setuptools-vendored copy - creating alias to 'setuptools._vendor.backports'!
|
||||||
|
15843 INFO: Processing pre-safe-import-module hook 'hook-tomli.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15843 INFO: Setuptools: 'tomli' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.tomli'!
|
||||||
|
16138 INFO: Processing standard module hook 'hook-pkg_resources.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
16316 INFO: Processing pre-safe-import-module hook 'hook-wheel.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
16376 INFO: Processing standard module hook 'hook-setuptools._vendor.importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
16378 INFO: Processing pre-safe-import-module hook 'hook-zipp.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
16414 INFO: Analyzing hidden import 'ui.handlers'
|
||||||
|
16414 INFO: Analyzing hidden import 'ui.services'
|
||||||
|
16415 INFO: Analyzing hidden import 'ui.services.config_manager'
|
||||||
|
16416 INFO: Analyzing hidden import 'ui.services.config_service'
|
||||||
|
16417 INFO: Processing module hooks (post-graph stage)...
|
||||||
|
16716 WARNING: Hidden import "charset_normalizer.md__mypyc" not found!
|
||||||
|
18203 INFO: Performing binary vs. data reclassification (622 entries)
|
||||||
|
18209 INFO: Looking for ctypes DLLs
|
||||||
|
18283 WARNING: Library shell32 required via ctypes not found
|
||||||
|
18292 WARNING: Library ole32 required via ctypes not found
|
||||||
|
18321 INFO: Analyzing run-time hooks ...
|
||||||
|
18329 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
|
||||||
|
18331 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
|
||||||
|
18333 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
|
||||||
|
18334 INFO: Including run-time hook 'pyi_rth_setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
|
||||||
|
18335 INFO: Including run-time hook 'pyi_rth_pkgres.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
|
||||||
|
18371 INFO: Creating base_library.zip...
|
||||||
|
18384 INFO: Looking for dynamic libraries
|
||||||
|
18673 INFO: Warnings written to /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/warn-trulymem.txt
|
||||||
|
18783 INFO: Graph cross-reference written to /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/xref-trulymem.html
|
||||||
|
18816 INFO: checking PYZ
|
||||||
|
18816 INFO: Building PYZ because PYZ-00.toc is non existent
|
||||||
|
18816 INFO: Building PYZ (ZlibArchive) /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/PYZ-00.pyz
|
||||||
|
19768 INFO: Building PYZ (ZlibArchive) /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/PYZ-00.pyz completed successfully.
|
||||||
|
19790 WARNING: Ignoring icon; supported only on Windows and macOS!
|
||||||
|
19801 INFO: checking PKG
|
||||||
|
19801 INFO: Building PKG because PKG-00.toc is non existent
|
||||||
|
19801 INFO: Building PKG (CArchive) TrulyMEM.pkg
|
||||||
|
24763 INFO: Building PKG (CArchive) TrulyMEM.pkg completed successfully.
|
||||||
|
24768 INFO: Bootloader /usr/local/lib/python3.13/dist-packages/PyInstaller/bootloader/Linux-64bit-intel/run
|
||||||
|
24768 INFO: checking EXE
|
||||||
|
24768 INFO: Building EXE because EXE-00.toc is non existent
|
||||||
|
24768 INFO: Building EXE from EXE-00.toc
|
||||||
|
24768 INFO: Copying bootloader EXE to /home/program/TrulyMEM-TrueHumanMEM/dist/TrulyMEM
|
||||||
|
24768 INFO: Appending PKG archive to custom ELF section in EXE
|
||||||
|
24825 INFO: Building EXE from EXE-00.toc completed successfully.
|
||||||
|
24830 INFO: Build complete! The results are available in: /home/program/TrulyMEM-TrueHumanMEM/dist
|
||||||
|
================================
|
||||||
|
===== Build Complete =====
|
||||||
|
Binary: dist/TrulyMEM
|
||||||
|
total 35848
|
||||||
|
drwxr-xr-x 2 root root 4096 Apr 30 07:06 .
|
||||||
|
drwxr-xr-x 15 root root 4096 Apr 30 07:05 ..
|
||||||
|
-rwxr-xr-x 1 root root 36698096 Apr 30 07:06 TrulyMEM
|
||||||
|
Build finished successfully!
|
||||||
@ -1,161 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
echo "===== Building TrulyMEM AppImage for Linux ====="
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "Error: python3 not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
APPDIR="$PROJECT_ROOT/TrulyMEM.AppDir"
|
|
||||||
rm -rf "$APPDIR"
|
|
||||||
mkdir -p "$APPDIR/usr/bin"
|
|
||||||
mkdir -p "$APPDIR/usr/share/trulymem"
|
|
||||||
mkdir -p "$APPDIR/usr/share/trulymem-web"
|
|
||||||
|
|
||||||
echo "===== Step 1: Build binaries with PyInstaller ====="
|
|
||||||
VENV_DIR="$PROJECT_ROOT/.venv_appimage"
|
|
||||||
rm -rf "$VENV_DIR"
|
|
||||||
python3 -m venv "$VENV_DIR"
|
|
||||||
source "$VENV_DIR/bin/activate"
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pyinstaller
|
|
||||||
rm -rf "$PROJECT_ROOT/build/pyinstaller_build" "$PROJECT_ROOT/dist"
|
|
||||||
|
|
||||||
CORE_HIDDEN=(
|
|
||||||
--hidden-import core
|
|
||||||
--hidden-import core.embedded_db
|
|
||||||
--hidden-import core.graph_client
|
|
||||||
--hidden-import core.tool_executor
|
|
||||||
--hidden-import core.tool_limiter
|
|
||||||
--hidden-import core.tools
|
|
||||||
--hidden-import core.tools.memory_tools
|
|
||||||
--hidden-import core.prompts
|
|
||||||
--hidden-import core.prompts.prompt_manager
|
|
||||||
--hidden-import core.server
|
|
||||||
--hidden-import core.client
|
|
||||||
--hidden-import core.migrate
|
|
||||||
--hidden-import core.activity_recorder
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "Running PyInstaller for TUI..."
|
|
||||||
pyinstaller trulymem_entry.py \
|
|
||||||
--clean --onefile --console --name TrulyMEM \
|
|
||||||
--distpath "$PROJECT_ROOT/dist" \
|
|
||||||
--workpath "$PROJECT_ROOT/build/pyinstaller_build/tui" \
|
|
||||||
--add-data "ui/styles:ui/styles" \
|
|
||||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "web_api.py:." \
|
|
||||||
--hidden-import textual \
|
|
||||||
--hidden-import textual.app \
|
|
||||||
--hidden-import textual.widgets \
|
|
||||||
--hidden-import textual.css \
|
|
||||||
--hidden-import openai \
|
|
||||||
--hidden-import openai._client \
|
|
||||||
--hidden-import neo4j \
|
|
||||||
--hidden-import sqlite3 \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--hidden-import ui \
|
|
||||||
--hidden-import ui.app \
|
|
||||||
--hidden-import ui.login_screen \
|
|
||||||
--hidden-import ui.models \
|
|
||||||
--hidden-import ui.models.message \
|
|
||||||
--hidden-import ui.models.config \
|
|
||||||
--hidden-import ui.models.log_entry \
|
|
||||||
--hidden-import ui.widgets \
|
|
||||||
--hidden-import ui.handlers \
|
|
||||||
--hidden-import ui.services \
|
|
||||||
--hidden-import ui.services.config_manager \
|
|
||||||
--hidden-import ui.services.config_service \
|
|
||||||
--hidden-import web_api \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
--collect-all textual \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "Running PyInstaller for Web..."
|
|
||||||
pyinstaller web_api.py \
|
|
||||||
--clean --onefile --console --name trulymem-web \
|
|
||||||
--distpath "$PROJECT_ROOT/dist" \
|
|
||||||
--workpath "$PROJECT_ROOT/build/pyinstaller_build/web" \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
cp "$PROJECT_ROOT/dist/TrulyMEM" "$APPDIR/usr/bin/"
|
|
||||||
cp "$PROJECT_ROOT/dist/trulymem-web" "$APPDIR/usr/bin/"
|
|
||||||
cp "$PROJECT_ROOT/trulymem_entry.py" "$APPDIR/usr/share/trulymem/"
|
|
||||||
|
|
||||||
echo "===== Step 2: Create AppImage structure ====="
|
|
||||||
cat > "$APPDIR/AppRun" << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
SELF=$(readlink -f "$0")
|
|
||||||
APPDIR=$(dirname "$SELF")
|
|
||||||
export PATH="$APPDIR/usr/bin:$PATH"
|
|
||||||
exec "$APPDIR/usr/bin/TrulyMEM" "$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$APPDIR/AppRun"
|
|
||||||
|
|
||||||
cat > "$APPDIR/trulymem.desktop" << 'EOF'
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=TrulyMEM
|
|
||||||
Comment=AI Memory System with Long-term Memory
|
|
||||||
Exec=TrulyMEM %U
|
|
||||||
Icon=trulymem
|
|
||||||
Terminal=true
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;X-AI;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Copy icon
|
|
||||||
if [ -f "$PROJECT_ROOT/pic/TrulyMEM.png" ]; then
|
|
||||||
cp "$PROJECT_ROOT/pic/TrulyMEM.png" "$APPDIR/trulymem.png"
|
|
||||||
elif [ -f "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_256x256.png" ]; then
|
|
||||||
cp "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_256x256.png" "$APPDIR/trulymem.png"
|
|
||||||
elif [ -f "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_128x128.png" ]; then
|
|
||||||
cp "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_128x128.png" "$APPDIR/trulymem.png"
|
|
||||||
else
|
|
||||||
echo "Warning: No icon file found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
APPIMAGE="$PROJECT_ROOT/TrulyMEM.AppImage"
|
|
||||||
rm -f "$APPIMAGE"
|
|
||||||
|
|
||||||
echo "===== Step 3: Package as AppImage ====="
|
|
||||||
cd /tmp
|
|
||||||
if ! command -v appimagetool &> /dev/null; then
|
|
||||||
echo "Downloading appimagetool..."
|
|
||||||
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool 2>/dev/null || \
|
|
||||||
curl -sL https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -o appimagetool
|
|
||||||
chmod +x appimagetool 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
if [ -x /tmp/appimagetool ]; then
|
|
||||||
/tmp/appimagetool "$APPDIR" "$APPIMAGE" || echo "appimagetool failed, keeping AppDir"
|
|
||||||
elif command -v appimagetool &> /dev/null; then
|
|
||||||
appimagetool "$APPDIR" "$APPIMAGE"
|
|
||||||
else
|
|
||||||
echo "Warning: appimagetool not available, AppDir at: $APPDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "===== Build Complete ====="
|
|
||||||
[ -f "$APPIMAGE" ] && echo "AppImage: $APPIMAGE" && ls -la "$APPIMAGE"
|
|
||||||
[ -d "$APPDIR" ] && echo "AppDir: $APPDIR"
|
|
||||||
|
|
||||||
echo "===== Cleanup ====="
|
|
||||||
deactivate
|
|
||||||
rm -rf "$VENV_DIR"
|
|
||||||
rm -rf "$PROJECT_ROOT/build/pyinstaller_build"
|
|
||||||
rm -f /tmp/appimagetool
|
|
||||||
if [ -f "$APPIMAGE" ]; then
|
|
||||||
rm -rf "$APPDIR"
|
|
||||||
fi
|
|
||||||
echo "Done!"
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "===== Building TrulyMEM for Linux ====="
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
echo "Project root: $PROJECT_ROOT"
|
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "Error: python3 not found"; exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
|
||||||
echo "Creating virtual environment: $VENV_DIR"
|
|
||||||
python3 -m venv "$VENV_DIR"
|
|
||||||
source "$VENV_DIR/bin/activate"
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
echo "Cleaning previous builds..."
|
|
||||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
|
||||||
|
|
||||||
# 共用 hidden imports(TUI + Web 都需要的核心库)
|
|
||||||
CORE_HIDDEN=(
|
|
||||||
--hidden-import core
|
|
||||||
--hidden-import core.embedded_db
|
|
||||||
--hidden-import core.graph_client
|
|
||||||
--hidden-import core.tool_executor
|
|
||||||
--hidden-import core.tool_limiter
|
|
||||||
--hidden-import core.tools
|
|
||||||
--hidden-import core.tools.memory_tools
|
|
||||||
--hidden-import core.prompts
|
|
||||||
--hidden-import core.prompts.prompt_manager
|
|
||||||
--hidden-import core.server
|
|
||||||
--hidden-import core.client
|
|
||||||
--hidden-import core.migrate
|
|
||||||
--hidden-import core.activity_recorder
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "1️⃣ Build TUI: TrulyMEM"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller trulymem_entry.py \
|
|
||||||
--clean --onefile --console --name TrulyMEM \
|
|
||||||
--add-data "ui/styles:ui/styles" \
|
|
||||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "web_api.py:." \
|
|
||||||
--hidden-import textual \
|
|
||||||
--hidden-import textual.app \
|
|
||||||
--hidden-import textual.widgets \
|
|
||||||
--hidden-import textual.css \
|
|
||||||
--hidden-import openai \
|
|
||||||
--hidden-import openai._client \
|
|
||||||
--hidden-import neo4j \
|
|
||||||
--hidden-import sqlite3 \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--hidden-import ui \
|
|
||||||
--hidden-import ui.app \
|
|
||||||
--hidden-import ui.login_screen \
|
|
||||||
--hidden-import ui.models \
|
|
||||||
--hidden-import ui.models.message \
|
|
||||||
--hidden-import ui.models.config \
|
|
||||||
--hidden-import ui.models.log_entry \
|
|
||||||
--hidden-import ui.widgets \
|
|
||||||
--hidden-import ui.handlers \
|
|
||||||
--hidden-import ui.services \
|
|
||||||
--hidden-import ui.services.config_manager \
|
|
||||||
--hidden-import ui.services.config_service \
|
|
||||||
--hidden-import web_api \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
--collect-all textual \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "2️⃣ Build Web: trulymem-web"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller web_api.py \
|
|
||||||
--clean --onefile --console --name trulymem-web \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "===== Build Complete ====="
|
|
||||||
echo "Binary: dist/TrulyMEM"
|
|
||||||
echo "Binary: dist/trulymem-web"
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
deactivate
|
|
||||||
rm -rf "$VENV_DIR"
|
|
||||||
echo "Build finished successfully!"
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "===== Building TrulyMEM for macOS ====="
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
echo "Project root: $PROJECT_ROOT"
|
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "Error: python3 not found"; exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
|
||||||
echo "Creating virtual environment: $VENV_DIR"
|
|
||||||
python3 -m venv "$VENV_DIR"
|
|
||||||
source "$VENV_DIR/bin/activate"
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 生成图标
|
|
||||||
if [ -d "pic/TrulyMEM.iconset" ]; then
|
|
||||||
iconutil -c icns pic/TrulyMEM.iconset -o pic/TrulyMEM.icns
|
|
||||||
echo "ICNS icon generated: pic/TrulyMEM.icns"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Cleaning previous builds..."
|
|
||||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
|
||||||
|
|
||||||
CORE_HIDDEN=(
|
|
||||||
--hidden-import core
|
|
||||||
--hidden-import core.embedded_db
|
|
||||||
--hidden-import core.graph_client
|
|
||||||
--hidden-import core.tool_executor
|
|
||||||
--hidden-import core.tool_limiter
|
|
||||||
--hidden-import core.tools
|
|
||||||
--hidden-import core.tools.memory_tools
|
|
||||||
--hidden-import core.prompts
|
|
||||||
--hidden-import core.prompts.prompt_manager
|
|
||||||
--hidden-import core.server
|
|
||||||
--hidden-import core.client
|
|
||||||
--hidden-import core.migrate
|
|
||||||
--hidden-import core.activity_recorder
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "1️⃣ Build TUI: TrulyMEM"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller trulymem_entry.py \
|
|
||||||
--clean --onefile --console --name TrulyMEM \
|
|
||||||
--icon "pic/TrulyMEM.icns" \
|
|
||||||
--add-data "ui/styles:ui/styles" \
|
|
||||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "web_api.py:." \
|
|
||||||
--hidden-import textual \
|
|
||||||
--hidden-import textual.app \
|
|
||||||
--hidden-import textual.widgets \
|
|
||||||
--hidden-import textual.css \
|
|
||||||
--hidden-import openai \
|
|
||||||
--hidden-import openai._client \
|
|
||||||
--hidden-import neo4j \
|
|
||||||
--hidden-import sqlite3 \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--hidden-import ui \
|
|
||||||
--hidden-import ui.app \
|
|
||||||
--hidden-import ui.login_screen \
|
|
||||||
--hidden-import ui.models \
|
|
||||||
--hidden-import ui.models.message \
|
|
||||||
--hidden-import ui.models.config \
|
|
||||||
--hidden-import ui.models.log_entry \
|
|
||||||
--hidden-import ui.widgets \
|
|
||||||
--hidden-import ui.handlers \
|
|
||||||
--hidden-import ui.services \
|
|
||||||
--hidden-import ui.services.config_manager \
|
|
||||||
--hidden-import ui.services.config_service \
|
|
||||||
--hidden-import web_api \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
--collect-all textual \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "2️⃣ Build Web: trulymem-web"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller web_api.py \
|
|
||||||
--clean --onefile --console --name trulymem-web \
|
|
||||||
--add-data "templates:templates" \
|
|
||||||
--add-data "static:static" \
|
|
||||||
--hidden-import flask \
|
|
||||||
--hidden-import flask_cors \
|
|
||||||
"${CORE_HIDDEN[@]}" \
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "===== Build Complete ====="
|
|
||||||
echo "Binary: dist/TrulyMEM"
|
|
||||||
echo "Binary: dist/trulymem-web"
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
deactivate
|
|
||||||
rm -rf "$VENV_DIR"
|
|
||||||
echo "Build finished successfully!"
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "===== Building TrulyMEM for Windows ====="
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
echo "Project root: $PROJECT_ROOT"
|
|
||||||
|
|
||||||
if ! command -v python &> /dev/null; then
|
|
||||||
echo "Error: python not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VENV_DIR="$PROJECT_ROOT\.venv_build"
|
|
||||||
echo "Creating virtual environment: %VENV_DIR%"
|
|
||||||
python -m venv "%VENV_DIR%"
|
|
||||||
call "%VENV_DIR%\Scripts\activate.bat"
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
echo "Cleaning previous builds..."
|
|
||||||
rmdir /s /q "build\dist" 2>nul
|
|
||||||
rmdir /s /q "build\__pycache__" 2>nul
|
|
||||||
del /f /q "TrulyMEM.spec" 2>nul
|
|
||||||
|
|
||||||
CORE_HIDDEN=(
|
|
||||||
--hidden-import core
|
|
||||||
--hidden-import core.embedded_db
|
|
||||||
--hidden-import core.graph_client
|
|
||||||
--hidden-import core.tool_executor
|
|
||||||
--hidden-import core.tool_limiter
|
|
||||||
--hidden-import core.tools
|
|
||||||
--hidden-import core.tools.memory_tools
|
|
||||||
--hidden-import core.prompts
|
|
||||||
--hidden-import core.prompts.prompt_manager
|
|
||||||
--hidden-import core.server
|
|
||||||
--hidden-import core.client
|
|
||||||
--hidden-import core.migrate
|
|
||||||
--hidden-import core.activity_recorder
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "1. Build TUI: TrulyMEM.exe"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller trulymem_entry.py ^
|
|
||||||
--clean --onefile --console --name TrulyMEM ^
|
|
||||||
--add-data "ui/styles;ui/styles" ^
|
|
||||||
--add-data "core/prompts/templates;core/prompts/templates" ^
|
|
||||||
--add-data "static;static" ^
|
|
||||||
--add-data "templates;templates" ^
|
|
||||||
--add-data "web_api.py;." ^
|
|
||||||
--hidden-import textual ^
|
|
||||||
--hidden-import textual.app ^
|
|
||||||
--hidden-import textual.widgets ^
|
|
||||||
--hidden-import textual.css ^
|
|
||||||
--hidden-import openai ^
|
|
||||||
--hidden-import openai._client ^
|
|
||||||
--hidden-import neo4j ^
|
|
||||||
--hidden-import sqlite3 ^
|
|
||||||
%CORE_HIDDEN% ^
|
|
||||||
--hidden-import ui ^
|
|
||||||
--hidden-import ui.app ^
|
|
||||||
--hidden-import ui.login_screen ^
|
|
||||||
--hidden-import ui.models ^
|
|
||||||
--hidden-import ui.models.message ^
|
|
||||||
--hidden-import ui.models.config ^
|
|
||||||
--hidden-import ui.models.log_entry ^
|
|
||||||
--hidden-import ui.widgets ^
|
|
||||||
--hidden-import ui.handlers ^
|
|
||||||
--hidden-import ui.services ^
|
|
||||||
--hidden-import ui.services.config_manager ^
|
|
||||||
--hidden-import ui.services.config_service ^
|
|
||||||
--hidden-import web_api ^
|
|
||||||
--hidden-import flask ^
|
|
||||||
--hidden-import flask_cors ^
|
|
||||||
--collect-all textual ^
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "================================"
|
|
||||||
echo "2. Build Web: trulymem-web.exe"
|
|
||||||
echo "================================"
|
|
||||||
python -m PyInstaller web_api.py ^
|
|
||||||
--clean --onefile --console --name trulymem-web ^
|
|
||||||
--add-data "templates;templates" ^
|
|
||||||
--add-data "static;static" ^
|
|
||||||
--hidden-import flask ^
|
|
||||||
--hidden-import flask_cors ^
|
|
||||||
%CORE_HIDDEN% ^
|
|
||||||
--noconfirm
|
|
||||||
|
|
||||||
echo "===== Build Complete ====="
|
|
||||||
echo "Output: dist/TrulyMEM.exe, dist/trulymem-web.exe"
|
|
||||||
|
|
||||||
deactivate
|
|
||||||
rmdir /s /q "%VENV_DIR%"
|
|
||||||
echo "Build finished successfully!"
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
project_root = os.path.dirname(os.path.abspath(SPEC))
|
|
||||||
sys.path.insert(0, project_root)
|
|
||||||
|
|
||||||
datas = []
|
|
||||||
# UI 样式
|
|
||||||
if os.path.exists(os.path.join(project_root, 'ui', 'styles')):
|
|
||||||
for root, dirs, files in os.walk(os.path.join(project_root, 'ui', 'styles')):
|
|
||||||
for f in files:
|
|
||||||
src = os.path.join(root, f)
|
|
||||||
dst = os.path.join('ui', 'styles', os.path.relpath(src, os.path.join(project_root, 'ui', 'styles')))
|
|
||||||
datas.append((src, dst))
|
|
||||||
|
|
||||||
# Prompt 模板
|
|
||||||
if os.path.exists(os.path.join(project_root, 'core', 'prompts', 'templates')):
|
|
||||||
for root, dirs, files in os.walk(os.path.join(project_root, 'core', 'prompts', 'templates')):
|
|
||||||
for f in files:
|
|
||||||
src = os.path.join(root, f)
|
|
||||||
dst = os.path.join('core', 'prompts', 'templates', os.path.relpath(src, os.path.join(project_root, 'core', 'prompts', 'templates')))
|
|
||||||
datas.append((src, dst))
|
|
||||||
|
|
||||||
# Web 静态文件
|
|
||||||
if os.path.exists(os.path.join(project_root, 'static')):
|
|
||||||
for root, dirs, files in os.walk(os.path.join(project_root, 'static')):
|
|
||||||
for f in files:
|
|
||||||
src = os.path.join(root, f)
|
|
||||||
dst = os.path.join('static', os.path.relpath(src, os.path.join(project_root, 'static')))
|
|
||||||
datas.append((src, dst))
|
|
||||||
|
|
||||||
# Web 模板
|
|
||||||
if os.path.exists(os.path.join(project_root, 'templates')):
|
|
||||||
for root, dirs, files in os.walk(os.path.join(project_root, 'templates')):
|
|
||||||
for f in files:
|
|
||||||
src = os.path.join(root, f)
|
|
||||||
dst = os.path.join('templates', os.path.relpath(src, os.path.join(project_root, 'templates')))
|
|
||||||
datas.append((src, dst))
|
|
||||||
|
|
||||||
# Web API 脚本(以便子进程模式回退使用)
|
|
||||||
web_api_src = os.path.join(project_root, 'web_api.py')
|
|
||||||
if os.path.exists(web_api_src):
|
|
||||||
datas.append((web_api_src, '.'))
|
|
||||||
|
|
||||||
# ——— TUI 主二进制 ———
|
|
||||||
a = Analysis(
|
|
||||||
['trulymem_entry.py'],
|
|
||||||
pathex=[project_root],
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
noarchive=False,
|
|
||||||
optimize=0,
|
|
||||||
)
|
|
||||||
pyz = PYZ(a.pure, block_cipher)
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries,
|
|
||||||
a.datas,
|
|
||||||
[],
|
|
||||||
name='TrulyMEM',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
runtime_tmpdir=None,
|
|
||||||
console=True,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ——— Web 服务二进制(trulymem-web)———
|
|
||||||
web_a = Analysis(
|
|
||||||
['web_api.py'],
|
|
||||||
pathex=[project_root],
|
|
||||||
binaries=[],
|
|
||||||
datas=[
|
|
||||||
(os.path.join(project_root, 'templates'), 'templates'),
|
|
||||||
(os.path.join(project_root, 'static'), 'static'),
|
|
||||||
],
|
|
||||||
hiddenimports=[
|
|
||||||
'flask', 'flask_cors',
|
|
||||||
'core', 'core.server', 'core.client',
|
|
||||||
'core.embedded_db', 'core.activity_recorder',
|
|
||||||
'core.migrate',
|
|
||||||
],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
noarchive=False,
|
|
||||||
optimize=0,
|
|
||||||
)
|
|
||||||
web_pyz = PYZ(web_a.pure, block_cipher)
|
|
||||||
web_exe = EXE(
|
|
||||||
web_pyz,
|
|
||||||
web_a.scripts,
|
|
||||||
web_a.binaries,
|
|
||||||
web_a.datas,
|
|
||||||
[],
|
|
||||||
name='trulymem-web',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
runtime_tmpdir=None,
|
|
||||||
console=False,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
)
|
|
||||||
32
code-linter.json5
Normal file
32
code-linter.json5
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.ets"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"**/src/ohosTest/**/*",
|
||||||
|
"**/src/test/**/*",
|
||||||
|
"**/src/mock/**/*",
|
||||||
|
"**/node_modules/**/*",
|
||||||
|
"**/oh_modules/**/*",
|
||||||
|
"**/build/**/*",
|
||||||
|
"**/.preview/**/*"
|
||||||
|
],
|
||||||
|
"ruleSet": [
|
||||||
|
"plugin:@performance/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@security/no-unsafe-aes": "error",
|
||||||
|
"@security/no-unsafe-hash": "error",
|
||||||
|
"@security/no-unsafe-mac": "warn",
|
||||||
|
"@security/no-unsafe-dh": "error",
|
||||||
|
"@security/no-unsafe-dsa": "error",
|
||||||
|
"@security/no-unsafe-ecdsa": "error",
|
||||||
|
"@security/no-unsafe-rsa-encrypt": "error",
|
||||||
|
"@security/no-unsafe-rsa-sign": "error",
|
||||||
|
"@security/no-unsafe-rsa-key": "error",
|
||||||
|
"@security/no-unsafe-dsa-key": "error",
|
||||||
|
"@security/no-unsafe-dh-key": "error",
|
||||||
|
"@security/no-unsafe-3des": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
common/BuildProfile.ets
Normal file
17
common/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '1.0.0';
|
||||||
|
export const BUILD_MODE_NAME = 'debug';
|
||||||
|
export const DEBUG = true;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
}
|
||||||
24
common/Index.ets
Normal file
24
common/Index.ets
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// ========= Utility Layer =========
|
||||||
|
export { defaultLogger } from './src/main/ets/util/Logger';
|
||||||
|
export { defaultLogger as Logger } from './src/main/ets/util/Logger';
|
||||||
|
|
||||||
|
export { BreakpointType, BreakpointTypes, WidthBreakpoint } from "./src/main/ets/util/BreakpointSystem";
|
||||||
|
|
||||||
|
// ========= Router =========
|
||||||
|
export { PageContext, RouterParam, IPageContext } from "./src/main/ets/routermanager/PageContext";
|
||||||
|
|
||||||
|
// ========= Constants =========
|
||||||
|
export { Constants as TrulyMEMConstants } from "./src/main/ets/constant/TrulyMEMConstants";
|
||||||
|
|
||||||
|
// ========= Model Layer =========
|
||||||
|
export { GraphDatabase, RecallEntity, TimeRangeParams } from "./src/main/ets/model/GraphDatabase";
|
||||||
|
|
||||||
|
// ========= Service Layer =========
|
||||||
|
export { GraphMemoryService, ConnectionItem, NodeDetailInfo } from "./src/main/ets/service/GraphMemoryService";
|
||||||
|
export { AIAgentService, ChatMessage, AgentResponse } from "./src/main/ets/service/AIAgentService";
|
||||||
|
|
||||||
|
// ========= ViewModel Layer =========
|
||||||
|
export { BaseViewModel, VMEvent } from "./src/main/ets/viewmodel/BaseViewModel";
|
||||||
|
|
||||||
|
// ========= Component Layer =========
|
||||||
|
export { ImmersiveTabNavigation } from "./src/main/ets/component/ImmersiveTabNavigation";
|
||||||
8
common/build-profile.json5
Normal file
8
common/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
common/common
Symbolic link
1
common/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/home/program/TrulyMEM-TrueHumanMEM/common
|
||||||
6
common/hvigorfile.ts
Normal file
6
common/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
9
common/oh-package.json5
Normal file
9
common/oh-package.json5
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM common module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
133
common/src/main/ets/component/ImmersiveTabNavigation.ets
Normal file
133
common/src/main/ets/component/ImmersiveTabNavigation.ets
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { defaultLogger } from '../util/Logger';
|
||||||
|
import { window } from '@kit.ArkUI';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
|
||||||
|
const THEME_COLOR = '#7C4DFF';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct ImmersiveTabNavigation {
|
||||||
|
@State currentIndex: number = 0;
|
||||||
|
@BuilderParam contentBuilder: () => void;
|
||||||
|
onTabChange?: (index: number) => void;
|
||||||
|
|
||||||
|
private windowFocused: boolean = true;
|
||||||
|
private bottomAvoidHeight: number = 0;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
tabBarBuilder(index: number, icon: string, label: string) {
|
||||||
|
Column() {
|
||||||
|
if (this.currentIndex === index && this.windowFocused) {
|
||||||
|
Circle()
|
||||||
|
.width(32)
|
||||||
|
.height(32)
|
||||||
|
.backgroundColor(`${THEME_COLOR}33`)
|
||||||
|
.blur(8)
|
||||||
|
.position({ x: '50%', y: '50%' })
|
||||||
|
.translate({ x: '-50%', y: '-50%' })
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(icon)
|
||||||
|
.fontSize(20)
|
||||||
|
.opacity(this.currentIndex === index ? 1 : 0.5)
|
||||||
|
|
||||||
|
Text(label)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.currentIndex === index ? THEME_COLOR : '#999')
|
||||||
|
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(56)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Column() {
|
||||||
|
this.contentBuilder()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
Stack() {
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
.borderRadius(24)
|
||||||
|
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor(`${THEME_COLOR}0D`)
|
||||||
|
.borderRadius(24)
|
||||||
|
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.linearGradient({
|
||||||
|
angle: 180,
|
||||||
|
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
|
||||||
|
})
|
||||||
|
.borderRadius(24)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Tabs({ index: this.currentIndex }) {
|
||||||
|
TabContent() {
|
||||||
|
Column() {
|
||||||
|
Blank()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabBar(this.tabBarBuilder(0, '🌌', 'TrulyMEM'))
|
||||||
|
|
||||||
|
TabContent() {
|
||||||
|
Column() {
|
||||||
|
Blank()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabBar(this.tabBarBuilder(1, '⚙', '设置'))
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(64)
|
||||||
|
.barPosition(BarPosition.End)
|
||||||
|
.onChange((index: number) => {
|
||||||
|
this.triggerTabSwitchFeedback(index);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('92%')
|
||||||
|
.height(72)
|
||||||
|
.alignSelf(ItemAlign.Center)
|
||||||
|
.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` })
|
||||||
|
.borderRadius(24)
|
||||||
|
.shadow({
|
||||||
|
radius: 20,
|
||||||
|
offsetY: -4,
|
||||||
|
color: 'rgba(0,0,0,0.15)'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#00000000')
|
||||||
|
}
|
||||||
|
}
|
||||||
44
common/src/main/ets/constant/TrulyMEMConstants.ets
Normal file
44
common/src/main/ets/constant/TrulyMEMConstants.ets
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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'))
|
||||||
|
)`;
|
||||||
|
}
|
||||||
1084
common/src/main/ets/model/GraphDatabase.ets
Normal file
1084
common/src/main/ets/model/GraphDatabase.ets
Normal file
File diff suppressed because it is too large
Load Diff
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal file
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal file
@ -0,0 +1,936 @@
|
|||||||
|
import relationalStore from '@ohos.data.relationalStore';
|
||||||
|
import { Context } from '@ohos.abilityAccessCtrl';
|
||||||
|
|
||||||
|
interface NodeNameCacheItem { name: string; type: string; mentions: number; }
|
||||||
|
export interface TimeRangeParams { days: number; }
|
||||||
|
|
||||||
|
interface TripletData {
|
||||||
|
subject: string;
|
||||||
|
relation: string;
|
||||||
|
object: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CriteriaData {
|
||||||
|
subject?: string;
|
||||||
|
target?: string;
|
||||||
|
relation?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeData {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgeData {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
label: string;
|
||||||
|
weight: number;
|
||||||
|
depth?: number;
|
||||||
|
sessionId?: string;
|
||||||
|
turnId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphData {
|
||||||
|
nodes: NodeData[];
|
||||||
|
edges: EdgeData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecallEntity {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mention_count: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecallRelation {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: string;
|
||||||
|
confidence: number;
|
||||||
|
session_id?: string;
|
||||||
|
turn_id?: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecallResult {
|
||||||
|
entities: RecallEntity[];
|
||||||
|
relations: RecallRelation[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BfsEntity {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationQueryResult {
|
||||||
|
sourceId: number;
|
||||||
|
targetId: number;
|
||||||
|
sourceName: string;
|
||||||
|
targetName: string;
|
||||||
|
type: string;
|
||||||
|
confidence: number;
|
||||||
|
sessionId?: string;
|
||||||
|
turnId?: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeQueryResult {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CleanupResult {
|
||||||
|
cleaned: number;
|
||||||
|
deleted_relations?: number;
|
||||||
|
deleted_orphans?: number;
|
||||||
|
dry_run?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntrospectResult {
|
||||||
|
entity_count: number;
|
||||||
|
relation_count: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArchiveResult {
|
||||||
|
archived: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResultItem {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
session_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SnapshotData {
|
||||||
|
entities: RecallEntity[];
|
||||||
|
relations: RecallRelation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORE_CONFIG: relationalStore.StoreConfig = {
|
||||||
|
name: 'trulymem.db',
|
||||||
|
securityLevel: relationalStore.SecurityLevel.S1
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GraphDatabase {
|
||||||
|
private store?: relationalStore.RdbStore;
|
||||||
|
private context?: Context;
|
||||||
|
|
||||||
|
async init(context: Context): Promise<void> {
|
||||||
|
this.context = context;
|
||||||
|
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
||||||
|
await this.createTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTables(): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
await this.store.executeSql(`
|
||||||
|
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,
|
||||||
|
updated_at TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`
|
||||||
|
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,
|
||||||
|
session_id TEXT,
|
||||||
|
turn_id INTEGER,
|
||||||
|
created_at TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
date_bucket TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tools TEXT,
|
||||||
|
created_at TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_name ON nodes(name)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_type ON nodes(type)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_source ON relations(subject_id)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_target ON relations(object_id)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(relation)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_status ON relations(status)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit(triplets: TripletData[], entityTypes?: Record<string, string>, sessionId?: string, turnId?: number): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
for (const triplet of triplets) {
|
||||||
|
const subjectId: number = await this.upsertNode(triplet.subject, entityTypes?.[triplet.subject]);
|
||||||
|
const objectId: number = await this.upsertNode(triplet.object, entityTypes?.[triplet.object]);
|
||||||
|
const existingId: number = await this.checkDuplicateRelation(subjectId, triplet.relation, objectId);
|
||||||
|
if (existingId > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const dateBucket = now.split('T')[0];
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'subject_id': subjectId,
|
||||||
|
'relation': triplet.relation,
|
||||||
|
'object_id': objectId,
|
||||||
|
'session_id': sessionId || null,
|
||||||
|
'turn_id': turnId || null,
|
||||||
|
'created_at': now,
|
||||||
|
'updated_at': now,
|
||||||
|
'status': 'active',
|
||||||
|
'date_bucket': dateBucket
|
||||||
|
};
|
||||||
|
await this.store.insert('relations', bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkDuplicateRelation(subjectId: number, relation: string, objectId: number): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('subject_id', subjectId).and().equalTo('relation', relation).and().equalTo('object_id', objectId).and().equalTo('status', 'active');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||||
|
if (resultSet.goToFirstRow()) {
|
||||||
|
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
resultSet.close();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertNode(name: string, entityType?: string): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', name);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'mentions']);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
const mentions: number = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||||
|
resultSet.close();
|
||||||
|
const updatePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
updatePredicates.equalTo('id', id);
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'mentions': mentions + 1,
|
||||||
|
'updated_at': now
|
||||||
|
};
|
||||||
|
await this.store.update(bucket, updatePredicates);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'name': name,
|
||||||
|
'type': entityType || 'concept',
|
||||||
|
'mentions': 1,
|
||||||
|
'created_at': now,
|
||||||
|
'updated_at': now
|
||||||
|
};
|
||||||
|
return await this.store.insert('nodes', bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, timeRange?: TimeRangeParams, sessionFilter?: string): Promise<RecallResult> {
|
||||||
|
if (!this.store) {
|
||||||
|
return { entities: [], relations: [], message: 'Database not initialized' };
|
||||||
|
}
|
||||||
|
// 计算时间范围过滤
|
||||||
|
let minDateBucket: string | undefined;
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
}
|
||||||
|
const keywords = queryIntent.toLowerCase().replace(/,/g, ' ').split(/\s+/).filter(w => w.trim());
|
||||||
|
const allEntities: BfsEntity[] = [];
|
||||||
|
const entityIds = new Set<number>();
|
||||||
|
let seedEntityIds = new Set<number>();
|
||||||
|
|
||||||
|
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.orderByDesc('mentions');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow() && allEntities.length < 50) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
const name = resultSet.getString(resultSet.getColumnIndex('name'));
|
||||||
|
const type = resultSet.getString(resultSet.getColumnIndex('type'));
|
||||||
|
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||||
|
entityIds.add(id);
|
||||||
|
allEntities.push({ id, name, type, mentions, depth: 0 });
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
} else {
|
||||||
|
if (seedEntities && seedEntities.length > 0) {
|
||||||
|
for (const seedName of seedEntities) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', seedName);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
if (!entityIds.has(id)) {
|
||||||
|
entityIds.add(id);
|
||||||
|
seedEntityIds.add(id);
|
||||||
|
allEntities.push({
|
||||||
|
id,
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.like('name', `%${keyword}%`);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
if (!entityIds.has(id)) {
|
||||||
|
entityIds.add(id);
|
||||||
|
allEntities.push({
|
||||||
|
id,
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRelations: RelationQueryResult[] = [];
|
||||||
|
let currentLayerIds = new Set<number>(entityIds);
|
||||||
|
const visitedEntityIds = new Set<number>(entityIds);
|
||||||
|
// 批量预加载所有相关节点名称,减少 N+1 查询
|
||||||
|
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||||
|
|
||||||
|
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
|
||||||
|
const currentIds = Array.from(currentLayerIds);
|
||||||
|
const relations = await this.getRelationsForNodes(currentIds, sessionFilter, minDateBucket);
|
||||||
|
const nextLayerIds = new Set<number>();
|
||||||
|
|
||||||
|
for (const rel of relations) {
|
||||||
|
allRelations.push(rel);
|
||||||
|
if (!visitedEntityIds.has(rel.targetId)) {
|
||||||
|
nextLayerIds.add(rel.targetId);
|
||||||
|
}
|
||||||
|
if (rel.targetId !== rel.sourceId && !visitedEntityIds.has(rel.sourceId)) {
|
||||||
|
nextLayerIds.add(rel.sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const newId of nextLayerIds) {
|
||||||
|
if (!visitedEntityIds.has(newId)) {
|
||||||
|
visitedEntityIds.add(newId);
|
||||||
|
// 优先从缓存获取,避免 N+1 查询
|
||||||
|
const cached = nodeNameCache.get(newId);
|
||||||
|
if (cached) {
|
||||||
|
const addedEntity: BfsEntity = {
|
||||||
|
id: newId,
|
||||||
|
name: cached.name,
|
||||||
|
type: cached.type,
|
||||||
|
mentions: cached.mentions,
|
||||||
|
depth: layer + 1
|
||||||
|
};
|
||||||
|
allEntities.push(addedEntity);
|
||||||
|
} else {
|
||||||
|
const nodeData = await this.getNodeById(newId);
|
||||||
|
if (nodeData) {
|
||||||
|
const cacheItem: NodeNameCacheItem = { name: nodeData.name, type: nodeData.type, mentions: nodeData.mentions };
|
||||||
|
nodeNameCache.set(newId, cacheItem);
|
||||||
|
const addedEntity: BfsEntity = {
|
||||||
|
id: nodeData.id,
|
||||||
|
name: nodeData.name,
|
||||||
|
type: nodeData.type,
|
||||||
|
mentions: nodeData.mentions,
|
||||||
|
depth: layer + 1
|
||||||
|
};
|
||||||
|
allEntities.push(addedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLayerIds = nextLayerIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities: RecallEntity[] = allEntities.map(e => {
|
||||||
|
const entity: RecallEntity = {
|
||||||
|
name: e.name,
|
||||||
|
type: e.type,
|
||||||
|
mention_count: e.mentions,
|
||||||
|
depth: e.depth
|
||||||
|
};
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
const relations: RecallRelation[] = allRelations.map(r => {
|
||||||
|
const rel: RecallRelation = {
|
||||||
|
source: r.sourceName,
|
||||||
|
target: r.targetName,
|
||||||
|
type: r.type,
|
||||||
|
confidence: r.confidence,
|
||||||
|
session_id: r.sessionId,
|
||||||
|
turn_id: r.turnId,
|
||||||
|
depth: r.depth
|
||||||
|
};
|
||||||
|
return rel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
entities,
|
||||||
|
relations,
|
||||||
|
message: `找到 ${entities.length} 个实体, ${relations.length} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeById(id: number): Promise<NodeQueryResult | null> {
|
||||||
|
if (!this.store) return null;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('id', id);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const node: NodeQueryResult = {
|
||||||
|
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
};
|
||||||
|
resultSet.close();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string, minDateBucket?: string): Promise<RelationQueryResult[]> {
|
||||||
|
if (!this.store || nodeIds.length === 0) return [];
|
||||||
|
const relations: RelationQueryResult[] = [];
|
||||||
|
// 批量预加载所有节点名称到缓存,避免 N+1 查询
|
||||||
|
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const cacheItem: NodeNameCacheItem = { name: node.name, type: node.type, mentions: node.mentions };
|
||||||
|
nodeNameCache.set(id, cacheItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const nodeId of nodeIds) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active').and().equalTo('subject_id', nodeId);
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (minDateBucket) {
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||||
|
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
relations.push({
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
|
||||||
|
const predicates2: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates2.equalTo('status', 'active').and().equalTo('object_id', nodeId);
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates2.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (minDateBucket) {
|
||||||
|
predicates2.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet2: relationalStore.ResultSet = await this.store.query(predicates2, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
while (resultSet2.goToNextRow()) {
|
||||||
|
const sourceId = resultSet2.getLong(resultSet2.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet2.getLong(resultSet2.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||||
|
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
relations.push({
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet2.getString(resultSet2.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet2.getDouble(resultSet2.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet2.getString(resultSet2.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet2.getLong(resultSet2.getColumnIndex('turn_id')),
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet2.close();
|
||||||
|
}
|
||||||
|
return relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(keyword: string): Promise<SearchResultItem[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.like('name', `%${keyword}%`);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['name', 'type', 'mentions']);
|
||||||
|
const results: SearchResultItem[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
results.push({
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
let hasCondition = false;
|
||||||
|
|
||||||
|
if (criteria.subject) {
|
||||||
|
const subjectId = await this.getNodeIdByName(criteria.subject);
|
||||||
|
if (subjectId > 0) {
|
||||||
|
predicates.equalTo('subject_id', subjectId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (criteria.target) {
|
||||||
|
const targetId = await this.getNodeIdByName(criteria.target);
|
||||||
|
if (targetId > 0) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('object_id', targetId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (criteria.relation) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('relation', criteria.relation);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
if (criteria.sessionId) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('session_id', criteria.sessionId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCondition) {
|
||||||
|
if (mode === 'soft') {
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'status': 'deleted',
|
||||||
|
'updated_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
await this.store.update(bucket, predicates);
|
||||||
|
} else {
|
||||||
|
await this.store.delete(predicates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.removeOrphanNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆图谱 — 在指定时间范围内查询关系和节点
|
||||||
|
* 对应 tools.memory_graph
|
||||||
|
*/
|
||||||
|
async graph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphData> {
|
||||||
|
if (!this.store) return { nodes: [], edges: [] };
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active');
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const nodeIds = new Set<number>();
|
||||||
|
const edges: EdgeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
nodeIds.add(sourceId);
|
||||||
|
nodeIds.add(targetId);
|
||||||
|
const edge: EdgeData = {
|
||||||
|
from: sourceId,
|
||||||
|
to: targetId,
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
weight: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||||
|
};
|
||||||
|
edges.push(edge);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const nodes: NodeData[] = [];
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const nodeData: NodeData = { id: node.id, label: node.name, type: node.type, mentions: node.mentions };
|
||||||
|
nodes.push(nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result: GraphData = { nodes, edges };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆快照 — 在指定时间范围内查询实体和关系
|
||||||
|
* 对应 tools.memory_snapshot
|
||||||
|
*/
|
||||||
|
async snapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotData> {
|
||||||
|
if (!this.store) return { entities: [], relations: [] };
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active');
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const nodeIds = new Set<number>();
|
||||||
|
const relations: RecallRelation[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
nodeIds.add(sourceId);
|
||||||
|
nodeIds.add(targetId);
|
||||||
|
const sourceNode = await this.getNodeById(sourceId);
|
||||||
|
const targetNode = await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
const rel: RecallRelation = {
|
||||||
|
source: sourceNode.name,
|
||||||
|
target: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
session_id: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turn_id: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||||
|
};
|
||||||
|
relations.push(rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const entities: RecallEntity[] = [];
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const recallEntity: RecallEntity = { name: node.name, type: node.type, mention_count: node.mentions };
|
||||||
|
entities.push(recallEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const snapResult: SnapshotData = { entities, relations };
|
||||||
|
return snapResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询已归档的记忆
|
||||||
|
* 对应 tools.memory_query_archived
|
||||||
|
*/
|
||||||
|
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'archived');
|
||||||
|
if (keyword) {
|
||||||
|
predicates.and().like('relation', '%' + keyword + '%');
|
||||||
|
}
|
||||||
|
if (days && days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - days);
|
||||||
|
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const results: RelationQueryResult[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = await this.getNodeById(sourceId);
|
||||||
|
const targetNode = await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
const queryResult: RelationQueryResult = {
|
||||||
|
sourceId: sourceId,
|
||||||
|
targetId: targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||||
|
depth: 0
|
||||||
|
};
|
||||||
|
results.push(queryResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeOrphanNodes(): Promise<number> {
|
||||||
|
if (!this.store) return 0;
|
||||||
|
let deleted = 0;
|
||||||
|
// 优化:批量查询所有有关系的节点 ID,避免 O(N²) 逐节点检查
|
||||||
|
const activeRelPred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
activeRelPred.equalTo('status', 'active');
|
||||||
|
const relResultSet: relationalStore.ResultSet = await this.store.query(activeRelPred, ['subject_id', 'object_id']);
|
||||||
|
const relatedIds = new Set<number>();
|
||||||
|
while (relResultSet.goToNextRow()) {
|
||||||
|
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('subject_id')));
|
||||||
|
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('object_id')));
|
||||||
|
}
|
||||||
|
relResultSet.close();
|
||||||
|
|
||||||
|
// 查询所有节点,筛选出不在关系中的孤儿节点
|
||||||
|
const nodePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const nodeResultSet: relationalStore.ResultSet = await this.store.query(nodePred, ['id']);
|
||||||
|
const orphanIds: number[] = [];
|
||||||
|
while (nodeResultSet.goToNextRow()) {
|
||||||
|
const nodeId = nodeResultSet.getLong(nodeResultSet.getColumnIndex('id'));
|
||||||
|
if (!relatedIds.has(nodeId)) {
|
||||||
|
orphanIds.push(nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeResultSet.close();
|
||||||
|
|
||||||
|
// 批量删除孤儿节点
|
||||||
|
for (const orphanId of orphanIds) {
|
||||||
|
const deletePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
deletePred.equalTo('id', orphanId);
|
||||||
|
await this.store.delete(deletePred);
|
||||||
|
deleted++;
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeIdByName(name: string): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', name);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
resultSet.close();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async introspect(): Promise<IntrospectResult> {
|
||||||
|
if (!this.store) return { entity_count: 0, relation_count: 0, message: 'Database not initialized' };
|
||||||
|
const nodePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const nodeResultSet = await this.store.query(nodePredicates, ['id']);
|
||||||
|
let entityCount = 0;
|
||||||
|
while (nodeResultSet.goToNextRow()) {
|
||||||
|
entityCount++;
|
||||||
|
}
|
||||||
|
nodeResultSet.close();
|
||||||
|
|
||||||
|
const relPredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
relPredicates.equalTo('status', 'active');
|
||||||
|
const relResultSet = await this.store.query(relPredicates, ['id']);
|
||||||
|
let relationCount = 0;
|
||||||
|
while (relResultSet.goToNextRow()) {
|
||||||
|
relationCount++;
|
||||||
|
}
|
||||||
|
relResultSet.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity_count: entityCount,
|
||||||
|
relation_count: relationCount,
|
||||||
|
message: `数据库包含 ${entityCount} 个实体, ${relationCount} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async archive(days: number): Promise<ArchiveResult> {
|
||||||
|
if (!this.store) return { archived: 0, message: 'Database not initialized' };
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||||
|
const cutoffStr = cutoffDate.toISOString();
|
||||||
|
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active').and().lessThan('created_at', cutoffStr);
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'status': 'archived',
|
||||||
|
'updated_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
const count = await this.store.update(bucket, predicates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
archived: count,
|
||||||
|
message: `归档了 ${count} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||||
|
if (!this.store) return { cleaned: 0, message: 'Database not initialized' };
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - 90);
|
||||||
|
const cutoffStr = cutoffDate.toISOString();
|
||||||
|
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'deleted').and().lessThan('updated_at', cutoffStr);
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
if (dryRun) {
|
||||||
|
const resultSet = await this.store.query(predicates, ['id']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
deleted++;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
} else {
|
||||||
|
deleted = await this.store.delete(predicates);
|
||||||
|
const orphanCount = await this.removeOrphanNodes();
|
||||||
|
return {
|
||||||
|
cleaned: deleted + orphanCount,
|
||||||
|
deleted_relations: deleted,
|
||||||
|
deleted_orphans: orphanCount,
|
||||||
|
dry_run: false,
|
||||||
|
message: `删除了 ${deleted} 条关系, ${orphanCount} 个孤立实体`
|
||||||
|
} as CleanupResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleaned: deleted,
|
||||||
|
deleted_relations: deleted,
|
||||||
|
dry_run: true,
|
||||||
|
message: `将删除 ${deleted} 条关系`
|
||||||
|
} as CleanupResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChatMessage(role: string, content: string, tools?: string, sessionId?: string): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'session_id': sessionId || null,
|
||||||
|
'role': role,
|
||||||
|
'content': content,
|
||||||
|
'tools': tools || null,
|
||||||
|
'created_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
await this.store.insert('chat_records', bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatHistory(limit?: number, sessionId?: string): Promise<ChatMessage[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
|
if (sessionId) {
|
||||||
|
predicates.equalTo('session_id', sessionId);
|
||||||
|
}
|
||||||
|
predicates.orderByDesc('created_at');
|
||||||
|
if (limit) {
|
||||||
|
predicates.limitAs(limit);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content', 'session_id']);
|
||||||
|
const messages: ChatMessage[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const msg: ChatMessage = {
|
||||||
|
role: resultSet.getString(resultSet.getColumnIndex('role')),
|
||||||
|
content: resultSet.getString(resultSet.getColumnIndex('content')),
|
||||||
|
session_id: resultSet.getString(resultSet.getColumnIndex('session_id'))
|
||||||
|
};
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return messages.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearChatHistory(): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
|
await this.store.delete(predicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllNodes(): Promise<NodeData[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
const nodes: NodeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const node: NodeData = {
|
||||||
|
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||||
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllEdges(): Promise<EdgeData[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
|
||||||
|
const edges: EdgeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const edge: EdgeData = {
|
||||||
|
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
|
||||||
|
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
|
||||||
|
};
|
||||||
|
edges.push(edge);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
common/src/main/ets/routermanager/PageContext.ets
Normal file
56
common/src/main/ets/routermanager/PageContext.ets
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { defaultLogger } from '../util/Logger';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public openPage(data: RouterParam, animated: boolean = true): void {
|
||||||
|
try {
|
||||||
|
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
|
||||||
|
} catch (err) {
|
||||||
|
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public popPage(animated: boolean = true): void {
|
||||||
|
try {
|
||||||
|
this.pathStack.pop(animated);
|
||||||
|
} catch (err) {
|
||||||
|
defaultLogger.error('popPage failed. ' + err.code + ' ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public popPageByIndex(index: number, animated: boolean = true): void {
|
||||||
|
this.pathStack.popToIndex(index, animated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(animated: boolean = true): void {
|
||||||
|
this.pathStack.clear(animated);
|
||||||
|
}
|
||||||
|
}
|
||||||
891
common/src/main/ets/service/AIAgentService.ets
Normal file
891
common/src/main/ets/service/AIAgentService.ets
Normal file
@ -0,0 +1,891 @@
|
|||||||
|
/**
|
||||||
|
* AIAgentService - AI Agent 服务层
|
||||||
|
* 管理上下文感知的 AI 对话,注入图数据作为上下文,
|
||||||
|
* 解析 AI 返回中的记忆操作,调用 GraphMemoryService 执行
|
||||||
|
* 参考:main 分支 core/graph_client.py
|
||||||
|
*/
|
||||||
|
import http from '@ohos.net.http';
|
||||||
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
|
import { Context } from '@ohos.abilityAccessCtrl';
|
||||||
|
import { GraphMemoryService, EntityInfo, RelationInfo, TaskInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TaskQueryParams, TripletInput, PersonaQueryResult, TaskQueryResult, MemoryRecallResult } from './GraphMemoryService';
|
||||||
|
import { 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: Context;
|
||||||
|
|
||||||
|
constructor(memoryService: GraphMemoryService, appContext: 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) {
|
||||||
|
const args: Record<string, Object> = JSON.parse(tc.function.arguments);
|
||||||
|
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') : '【新对话】';
|
||||||
|
}
|
||||||
|
}
|
||||||
1036
common/src/main/ets/service/GraphMemoryService.ets
Normal file
1036
common/src/main/ets/service/GraphMemoryService.ets
Normal file
File diff suppressed because it is too large
Load Diff
47
common/src/main/ets/util/BreakpointSystem.ets
Normal file
47
common/src/main/ets/util/BreakpointSystem.ets
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
common/src/main/ets/util/Logger.ets
Normal file
31
common/src/main/ets/util/Logger.ets
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { hilog } from "@kit.PerformanceAnalysisKit";
|
||||||
|
|
||||||
|
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;
|
||||||
52
common/src/main/ets/viewmodel/BaseViewModel.ets
Normal file
52
common/src/main/ets/viewmodel/BaseViewModel.ets
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { BreakpointType, WidthBreakpoint } from '../util/BreakpointSystem';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
common/src/main/module.json5
Normal file
12
common/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "common",
|
||||||
|
"type": "har",
|
||||||
|
"description": "TrulyMEM common module",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
from .server import BackendServer, Packet, PacketType, PacketResponse
|
|
||||||
from .client import BackendClient
|
|
||||||
from .embedded_db import EmbeddedGraphDB
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"BackendServer",
|
|
||||||
"BackendClient",
|
|
||||||
"EmbeddedGraphDB",
|
|
||||||
"Packet",
|
|
||||||
"PacketType",
|
|
||||||
"PacketResponse"
|
|
||||||
]
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
import time
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
|
|
||||||
|
|
||||||
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 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}
|
|
||||||
|
|
||||||
|
|
||||||
_recorder: Optional[ActivityRecorder] = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_recorder() -> ActivityRecorder:
|
|
||||||
global _recorder
|
|
||||||
if _recorder is None:
|
|
||||||
_recorder = ActivityRecorder()
|
|
||||||
return _recorder
|
|
||||||
128
core/client.py
128
core/client.py
@ -1,128 +0,0 @@
|
|||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from .server import BackendServer, Packet, PacketType
|
|
||||||
|
|
||||||
|
|
||||||
class BackendClient:
|
|
||||||
|
|
||||||
def __init__(self, server: BackendServer):
|
|
||||||
self._server = server
|
|
||||||
self._counter = 0
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
|
|
||||||
def _next_id(self) -> str:
|
|
||||||
with self._lock:
|
|
||||||
self._counter += 1
|
|
||||||
return f"{time.time()}_{self._counter}"
|
|
||||||
|
|
||||||
def send(self, message: str) -> Dict:
|
|
||||||
return self.process_message(message)
|
|
||||||
|
|
||||||
def process_message(self, user_input: str) -> Dict:
|
|
||||||
return self._server.process_message(user_input)
|
|
||||||
|
|
||||||
def get_settings(self) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.GET_SETTINGS,
|
|
||||||
body={}
|
|
||||||
)
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def update_settings(self, api_config: Dict = None, tool_limits: Dict = None) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.SET_SETTINGS,
|
|
||||||
body={
|
|
||||||
"api_config": api_config or {},
|
|
||||||
"tool_limits": tool_limits or {}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def execute_tool(self, name: str, arguments: Dict) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.EXECUTE_TOOL,
|
|
||||||
body={"tool_name": name, "arguments": arguments}
|
|
||||||
)
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def get_status(self) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.GET_STATUS,
|
|
||||||
body={}
|
|
||||||
)
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def save_history(self, messages: list) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.SAVE_HISTORY,
|
|
||||||
body={"messages": messages}
|
|
||||||
)
|
|
||||||
response = self._server.send(packet)
|
|
||||||
return response.body.get("data", {})
|
|
||||||
|
|
||||||
def get_history(self) -> list:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.GET_HISTORY,
|
|
||||||
body={}
|
|
||||||
)
|
|
||||||
response = self._server.send(packet)
|
|
||||||
data = response.body.get("data", {})
|
|
||||||
return data.get("history", [])
|
|
||||||
|
|
||||||
def clear_history(self) -> Dict:
|
|
||||||
packet = Packet(
|
|
||||||
id=self._next_id(),
|
|
||||||
type=PacketType.SAVE_HISTORY,
|
|
||||||
body={"messages": []}
|
|
||||||
)
|
|
||||||
response = self._server.send(packet)
|
|
||||||
return response.body.get("data", {})
|
|
||||||
|
|
||||||
def 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()
|
|
||||||
@ -1,714 +0,0 @@
|
|||||||
"""
|
|
||||||
内嵌图数据库 - 基于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 commit(self, triplets: List[Dict], entity_types: Dict = None,
|
|
||||||
temporal_tag: str = None, session_id: str = None,
|
|
||||||
turn_id: int = None) -> Dict:
|
|
||||||
"""
|
|
||||||
写入记忆
|
|
||||||
|
|
||||||
Args:
|
|
||||||
triplets: 三元组列表
|
|
||||||
entity_types: 实体类型
|
|
||||||
temporal_tag: 时间标签
|
|
||||||
session_id: 会话ID
|
|
||||||
turn_id: 轮次ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
写入结果
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
created_entities = 0
|
|
||||||
created_relations = 0
|
|
||||||
|
|
||||||
for triplet in triplets:
|
|
||||||
subject = triplet.get('subject')
|
|
||||||
relation = triplet.get('relation')
|
|
||||||
obj = triplet.get('object')
|
|
||||||
confidence = triplet.get('confidence', 1.0)
|
|
||||||
|
|
||||||
if not all([subject, relation, obj]):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 创建或更新实体
|
|
||||||
for entity_name in [subject, obj]:
|
|
||||||
entity_type = entity_types.get(entity_name) if entity_types else None
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO entities (name, type)
|
|
||||||
VALUES (?, ?)
|
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
|
||||||
mention_count = mention_count + 1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
""", (entity_name, entity_type))
|
|
||||||
|
|
||||||
if cursor.rowcount > 0:
|
|
||||||
created_entities += 1
|
|
||||||
|
|
||||||
# 获取实体ID
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (subject,))
|
|
||||||
source_id = cursor.fetchone()['id']
|
|
||||||
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (obj,))
|
|
||||||
target_id = cursor.fetchone()['id']
|
|
||||||
|
|
||||||
# 创建关系
|
|
||||||
date_bucket = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO relations (
|
|
||||||
source_id, target_id, relation_type, confidence,
|
|
||||||
session_id, turn_id, date_bucket
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""", (source_id, target_id, relation, confidence,
|
|
||||||
session_id, turn_id, date_bucket))
|
|
||||||
|
|
||||||
created_relations += 1
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"created_entities": created_entities,
|
|
||||||
"created_relations": created_relations,
|
|
||||||
"message": f"创建了 {created_entities} 个实体, {created_relations} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def purge(self, criteria: Dict, mode: str = "soft",
|
|
||||||
new_relation: Dict = None) -> Dict:
|
|
||||||
"""
|
|
||||||
删除或修正记忆
|
|
||||||
|
|
||||||
Args:
|
|
||||||
criteria: 删除条件
|
|
||||||
mode: 删除模式 (soft/hard)
|
|
||||||
new_relation: 替代关系
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除结果
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = []
|
|
||||||
params = []
|
|
||||||
|
|
||||||
if criteria.get('source'):
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['source'],))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
conditions.append("source_id = ?")
|
|
||||||
params.append(row['id'])
|
|
||||||
|
|
||||||
if criteria.get('target'):
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['target'],))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
conditions.append("target_id = ?")
|
|
||||||
params.append(row['id'])
|
|
||||||
|
|
||||||
if criteria.get('relation'):
|
|
||||||
conditions.append("relation_type = ?")
|
|
||||||
params.append(criteria['relation'])
|
|
||||||
|
|
||||||
if not conditions:
|
|
||||||
return {"deleted": 0, "message": "无删除条件"}
|
|
||||||
|
|
||||||
where_clause = " AND ".join(conditions)
|
|
||||||
|
|
||||||
if mode == "soft":
|
|
||||||
cursor.execute(f"""
|
|
||||||
UPDATE relations
|
|
||||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE {where_clause} AND status = 'active'
|
|
||||||
""", params)
|
|
||||||
else:
|
|
||||||
cursor.execute(f"""
|
|
||||||
DELETE FROM relations
|
|
||||||
WHERE {where_clause}
|
|
||||||
""", params)
|
|
||||||
|
|
||||||
deleted = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"deleted": deleted,
|
|
||||||
"mode": mode,
|
|
||||||
"message": f"删除了 {deleted} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def introspect(self, session_id: str = None) -> Dict:
|
|
||||||
"""
|
|
||||||
查看会话状态
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session_id: 会话ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
会话状态
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 统计实体
|
|
||||||
cursor.execute("SELECT COUNT(*) as count FROM entities")
|
|
||||||
entity_count = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
# 统计关系
|
|
||||||
cursor.execute("SELECT COUNT(*) as count FROM relations WHERE status = 'active'")
|
|
||||||
relation_count = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entity_count": entity_count,
|
|
||||||
"relation_count": relation_count,
|
|
||||||
"session_id": session_id,
|
|
||||||
"message": f"数据库包含 {entity_count} 个实体, {relation_count} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def archive(self, days: int = 30) -> Dict:
|
|
||||||
"""归档旧关系"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE relations
|
|
||||||
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE status = 'active'
|
|
||||||
AND created_at < datetime('now', ?)
|
|
||||||
""", (f'-{days} days',))
|
|
||||||
|
|
||||||
archived = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"archived": archived,
|
|
||||||
"message": f"归档了 {archived} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def cleanup(self, dry_run: bool = True) -> Dict:
|
|
||||||
"""清理已删除数据"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM relations
|
|
||||||
WHERE status = 'deleted'
|
|
||||||
AND updated_at < datetime('now', '-90 days')
|
|
||||||
""")
|
|
||||||
deleted_relations = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": True,
|
|
||||||
"deleted_relations": deleted_relations,
|
|
||||||
"message": f"将删除 {deleted_relations} 条关系"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
cursor.execute("""
|
|
||||||
DELETE FROM relations
|
|
||||||
WHERE status = 'deleted'
|
|
||||||
AND updated_at < datetime('now', '-90 days')
|
|
||||||
""")
|
|
||||||
deleted = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": False,
|
|
||||||
"deleted": deleted,
|
|
||||||
"message": f"删除了 {deleted} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def save_chat_records(self, messages: list) -> Dict:
|
|
||||||
"""保存聊天记录到数据库"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
saved = 0
|
|
||||||
for msg in messages:
|
|
||||||
role = msg.get("role")
|
|
||||||
content = msg.get("content")
|
|
||||||
if role and content:
|
|
||||||
cursor.execute(
|
|
||||||
"INSERT INTO chat_records (role, content) VALUES (?, ?)",
|
|
||||||
(role, content)
|
|
||||||
)
|
|
||||||
saved += 1
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
DELETE FROM chat_records
|
|
||||||
WHERE id NOT IN (
|
|
||||||
SELECT id FROM chat_records
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 500
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {"saved": saved}
|
|
||||||
|
|
||||||
def get_chat_records(self, limit: int = 500) -> list:
|
|
||||||
"""从数据库获取聊天记录"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT role, content FROM chat_records
|
|
||||||
ORDER BY id ASC LIMIT ?
|
|
||||||
""", (limit,))
|
|
||||||
return [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
def clear_chat_records(self) -> Dict:
|
|
||||||
"""清空聊天记录(保留图数据库)"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
cursor.execute("DELETE FROM chat_records")
|
|
||||||
self.conn.commit()
|
|
||||||
return {"cleared": True}
|
|
||||||
|
|
||||||
def 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
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试
|
|
||||||
print("Testing Embedded Graph Database...")
|
|
||||||
|
|
||||||
with EmbeddedGraphDB("test.db") as db:
|
|
||||||
# 写入测试
|
|
||||||
result = db.commit(
|
|
||||||
triplets=[
|
|
||||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
|
||||||
{"subject": "用户", "relation": "学习", "object": "AI"}
|
|
||||||
],
|
|
||||||
session_id="test-session",
|
|
||||||
turn_id=1
|
|
||||||
)
|
|
||||||
print(f"Commit: {result}")
|
|
||||||
|
|
||||||
# 检索测试
|
|
||||||
result = db.recall("Python,AI")
|
|
||||||
print(f"Recall: {result}")
|
|
||||||
|
|
||||||
# 状态测试
|
|
||||||
result = db.introspect()
|
|
||||||
print(f"Introspect: {result}")
|
|
||||||
|
|
||||||
print("\nTest completed!")
|
|
||||||
@ -1,393 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Graph Memory Client - 图记忆客户端核心实现(重构版)
|
|
||||||
使用模块化的工具和提示词系统
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
|
||||||
from openai import OpenAI
|
|
||||||
|
|
||||||
from .tools import TOOLS
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
from .prompts.prompt_manager import PromptManager
|
|
||||||
|
|
||||||
# 环境配置
|
|
||||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
|
||||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
|
||||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
|
|
||||||
|
|
||||||
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
|
||||||
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
|
|
||||||
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "neo4j")
|
|
||||||
|
|
||||||
# 会话配置
|
|
||||||
CURRENT_SESSION_ID = f"session-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:4]}"
|
|
||||||
CURRENT_TURN = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Neo4jGraph:
|
|
||||||
"""Neo4j图数据库客户端"""
|
|
||||||
|
|
||||||
def __init__(self, uri: str, user: str, password: str):
|
|
||||||
from neo4j import GraphDatabase
|
|
||||||
self.driver = GraphDatabase.driver(uri, auth=(user, password))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.driver.close()
|
|
||||||
|
|
||||||
def ensure_constraints(self):
|
|
||||||
"""确保约束和索引存在"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
# 实体约束
|
|
||||||
session.run("CREATE CONSTRAINT entity_name_constraint IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE")
|
|
||||||
session.run("CREATE CONSTRAINT session_id_constraint IF NOT EXISTS FOR (s:Session) REQUIRE s.session_id IS UNIQUE")
|
|
||||||
|
|
||||||
# 关系索引
|
|
||||||
session.run("CREATE INDEX rel_created_at IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.created_at")
|
|
||||||
session.run("CREATE INDEX rel_session_id IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.session_id")
|
|
||||||
session.run("CREATE INDEX rel_type IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.type")
|
|
||||||
session.run("CREATE INDEX rel_status IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.status")
|
|
||||||
session.run("CREATE INDEX rel_date_bucket IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.date_bucket")
|
|
||||||
|
|
||||||
# 实体索引
|
|
||||||
session.run("CREATE INDEX entity_type IF NOT EXISTS FOR (e:Entity) ON e.type")
|
|
||||||
session.run("CREATE INDEX entity_mention_count IF NOT EXISTS FOR (e:Entity) ON e.mention_count")
|
|
||||||
|
|
||||||
def recall(self, query_intent: str, seed_entities: list = None, depth: int = 2,
|
|
||||||
time_range: dict = None, session_filter: str = None) -> dict:
|
|
||||||
"""检索记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
# 支持逗号分隔的多个关键词
|
|
||||||
keywords = [w.strip() for w in query_intent.replace(',', ' ').split() if len(w.strip()) > 0]
|
|
||||||
|
|
||||||
if not keywords and not seed_entities:
|
|
||||||
return {"entities": [], "relations": [], "message": "无查询关键词"}
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
cond_parts = ["r.status = 'active'"]
|
|
||||||
|
|
||||||
if session_filter:
|
|
||||||
cond_parts.append("r.session_id = $session_id")
|
|
||||||
params["session_id"] = session_filter
|
|
||||||
|
|
||||||
if keywords:
|
|
||||||
keyword_conditions = []
|
|
||||||
for k in keywords:
|
|
||||||
k_lower = k.lower()
|
|
||||||
keyword_conditions.append(f"toLower(e.name) CONTAINS '{k_lower}'")
|
|
||||||
keyword_conditions.append(f"toLower(t.name) CONTAINS '{k_lower}'")
|
|
||||||
keyword_conditions.append(f"toLower(r.type) CONTAINS '{k_lower}'")
|
|
||||||
cond_parts.append(f"({' OR '.join(keyword_conditions)})")
|
|
||||||
|
|
||||||
if seed_entities:
|
|
||||||
placeholders = ",".join([f"'{s}'" for s in seed_entities])
|
|
||||||
cond_parts.append(f"(e.name IN [{placeholders}] OR t.name IN [{placeholders}])")
|
|
||||||
|
|
||||||
if time_range and "days" in time_range:
|
|
||||||
cond_parts.append(f"r.created_at >= datetime() - duration('P{time_range['days']}D')")
|
|
||||||
|
|
||||||
where_clause = " AND ".join(cond_parts)
|
|
||||||
|
|
||||||
cypher = f"""
|
|
||||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE {where_clause}
|
|
||||||
RETURN e, r, t
|
|
||||||
ORDER BY r.created_at DESC
|
|
||||||
LIMIT 30
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = session.run(cypher, params)
|
|
||||||
entities, relations = {}, []
|
|
||||||
|
|
||||||
for record in result:
|
|
||||||
e, r, t = record["e"], record["r"], record["t"]
|
|
||||||
if e["name"] not in entities:
|
|
||||||
entities[e["name"]] = {"name": e["name"], "type": e.get("type", "unknown"), "mention_count": e.get("mention_count", 1)}
|
|
||||||
if t["name"] not in entities:
|
|
||||||
entities[t["name"]] = {"name": t["name"], "type": t.get("type", "unknown"), "mention_count": t.get("mention_count", 1)}
|
|
||||||
|
|
||||||
relations.append({
|
|
||||||
"source": e["name"],
|
|
||||||
"target": t["name"],
|
|
||||||
"type": r["type"],
|
|
||||||
"created_at": str(r.get("created_at", "")),
|
|
||||||
"session_id": r.get("session_id", ""),
|
|
||||||
"turn_id": r.get("turn_id", 0),
|
|
||||||
"confidence": r.get("confidence", 1.0)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"entities": list(entities.values()), "relations": relations[:20]}
|
|
||||||
|
|
||||||
def commit(self, triplets: list, entity_types: list = None, temporal_tag: str = None) -> dict:
|
|
||||||
"""写入记忆"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
with self.driver.session() as session:
|
|
||||||
valid_triplets = [t for t in triplets if t.get("subject") and t.get("relation") and t.get("object")]
|
|
||||||
|
|
||||||
if not valid_triplets:
|
|
||||||
return {"committed_count": 0, "details": []}
|
|
||||||
|
|
||||||
etype = entity_types[0] if entity_types else "unknown"
|
|
||||||
date_bucket = temporal_tag or datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for triplet in valid_triplets:
|
|
||||||
subject = triplet.get("subject", "").strip()
|
|
||||||
relation = triplet.get("relation", "").strip()
|
|
||||||
obj = triplet.get("object", "").strip()
|
|
||||||
confidence = triplet.get("confidence", 0.9)
|
|
||||||
|
|
||||||
session.run("""
|
|
||||||
MERGE (s:Entity {name: $subject})
|
|
||||||
ON CREATE SET s.type = $type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
|
||||||
ON MATCH SET s.mention_count = coalesce(s.mention_count, 0) + 1, s.updated_at = datetime()
|
|
||||||
|
|
||||||
MERGE (t:Entity {name: $object})
|
|
||||||
ON CREATE SET t.type = $type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
|
||||||
ON MATCH SET t.mention_count = coalesce(t.mention_count, 0) + 1, t.updated_at = datetime()
|
|
||||||
|
|
||||||
CREATE (s)-[r:RELATES {
|
|
||||||
type: $relation,
|
|
||||||
created_at: datetime(),
|
|
||||||
session_id: $session_id,
|
|
||||||
turn_id: $turn_id,
|
|
||||||
role: 'user',
|
|
||||||
status: 'active',
|
|
||||||
confidence: $confidence,
|
|
||||||
date_bucket: $date_bucket
|
|
||||||
}]->(t)
|
|
||||||
""", subject=subject, object=obj, relation=relation, type=etype,
|
|
||||||
session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, confidence=confidence,
|
|
||||||
date_bucket=date_bucket)
|
|
||||||
|
|
||||||
results.append(f"{subject} -[{relation}]-> {obj}")
|
|
||||||
|
|
||||||
return {"committed_count": len(results), "details": results}
|
|
||||||
|
|
||||||
def purge(self, criteria: dict, mode: str = "soft", new_relation: dict = None) -> dict:
|
|
||||||
"""删除记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
subject_pattern = criteria.get("subject_contains", "")
|
|
||||||
rel_type = criteria.get("relation_type", "")
|
|
||||||
target_pattern = criteria.get("target_contains", "")
|
|
||||||
session_id = criteria.get("session_id", CURRENT_SESSION_ID)
|
|
||||||
|
|
||||||
cond_parts = ["r.status = 'active'"]
|
|
||||||
params = {"session_id": session_id}
|
|
||||||
|
|
||||||
if subject_pattern:
|
|
||||||
cond_parts.append("e.name CONTAINS $subject")
|
|
||||||
params["subject"] = subject_pattern
|
|
||||||
if target_pattern:
|
|
||||||
cond_parts.append("t.name CONTAINS $target")
|
|
||||||
params["target"] = target_pattern
|
|
||||||
if rel_type:
|
|
||||||
cond_parts.append("r.type = $rel_type")
|
|
||||||
params["rel_type"] = rel_type
|
|
||||||
|
|
||||||
where_clause = " AND ".join(cond_parts)
|
|
||||||
|
|
||||||
if mode == "supersede" and new_relation:
|
|
||||||
new_rel = new_relation.get("relation", "")
|
|
||||||
new_target = new_relation.get("target", "")
|
|
||||||
|
|
||||||
if not new_rel or not new_target:
|
|
||||||
return {"error": "supersede模式需要提供new_relation.relation和new_relation.target"}
|
|
||||||
|
|
||||||
result = session.run(f"""
|
|
||||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE {where_clause}
|
|
||||||
SET r.status = 'superseded', r.updated_at = datetime()
|
|
||||||
RETURN count(r) as count
|
|
||||||
""", params)
|
|
||||||
|
|
||||||
count = result.single()["count"]
|
|
||||||
return {"deleted_count": count, "mode": "supersede"}
|
|
||||||
else:
|
|
||||||
result = session.run(f"""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE {where_clause}
|
|
||||||
SET r.status = 'deleted', r.updated_at = datetime()
|
|
||||||
RETURN count(r) as deleted
|
|
||||||
""", params)
|
|
||||||
count = result.single()["deleted"]
|
|
||||||
|
|
||||||
return {"deleted_count": count, "mode": "soft"}
|
|
||||||
|
|
||||||
def introspect(self, session_id: str = None) -> dict:
|
|
||||||
"""查看记忆状态"""
|
|
||||||
target_session = session_id or CURRENT_SESSION_ID
|
|
||||||
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result = session.run("""
|
|
||||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE r.session_id = $session_id AND r.status = 'active'
|
|
||||||
RETURN collect(DISTINCT s.name) as source_entities,
|
|
||||||
collect(DISTINCT t.name) as target_entities,
|
|
||||||
count(r) as rel_count,
|
|
||||||
collect(DISTINCT r.type) as rel_types
|
|
||||||
""", session_id=target_session)
|
|
||||||
record = result.single()
|
|
||||||
|
|
||||||
result2 = session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
RETURN e.name as name, e.mention_count as count, e.type as type
|
|
||||||
ORDER BY e.mention_count DESC
|
|
||||||
LIMIT 10
|
|
||||||
""")
|
|
||||||
hotspots = [(r["name"], r["count"], r["type"]) for r in result2]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"session_id": target_session,
|
|
||||||
"total_turns": CURRENT_TURN,
|
|
||||||
"entities_discussed": list(set((record["source_entities"] or []) + (record["target_entities"] or []))),
|
|
||||||
"relation_count": record["rel_count"] if record else 0,
|
|
||||||
"relation_types": record["rel_types"] if record else [],
|
|
||||||
"memory_hotspots": hotspots
|
|
||||||
}
|
|
||||||
|
|
||||||
def archive(self, days: int = 30) -> dict:
|
|
||||||
"""归档旧记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result = session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'active' AND r.created_at < datetime() - duration('P' + $days + 'D')
|
|
||||||
SET r.status = 'archived', r.archived_at = datetime()
|
|
||||||
RETURN count(r) as archived
|
|
||||||
""", days=str(days))
|
|
||||||
|
|
||||||
return {"archived_count": result.single()["archived"], "days": days}
|
|
||||||
|
|
||||||
def cleanup(self, dry_run: bool = True) -> dict:
|
|
||||||
"""清理无效数据"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result1 = session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
|
||||||
RETURN count(r) as to_delete
|
|
||||||
""")
|
|
||||||
deleted_relations = result1.single()["to_delete"]
|
|
||||||
|
|
||||||
result2 = session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
WHERE NOT (e)-[:RELATES]-()
|
|
||||||
RETURN count(e) as orphans
|
|
||||||
""")
|
|
||||||
orphan_nodes = result2.single()["orphans"]
|
|
||||||
|
|
||||||
if not dry_run and deleted_relations > 0:
|
|
||||||
session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
|
||||||
DELETE r
|
|
||||||
""")
|
|
||||||
|
|
||||||
if not dry_run and orphan_nodes > 0:
|
|
||||||
session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
WHERE NOT (e)-[:RELATES]-()
|
|
||||||
DELETE e
|
|
||||||
""")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": dry_run,
|
|
||||||
"deleted_relations": deleted_relations,
|
|
||||||
"orphan_nodes": orphan_nodes,
|
|
||||||
"action_taken": not dry_run
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GraphMemoryClient:
|
|
||||||
"""图记忆客户端"""
|
|
||||||
|
|
||||||
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
|
|
||||||
# 清理可能存在的错误代理环境变量
|
|
||||||
import os
|
|
||||||
proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'all_proxy', 'ALL_PROXY']
|
|
||||||
for var in proxy_vars:
|
|
||||||
if var in os.environ:
|
|
||||||
value = os.environ[var]
|
|
||||||
# 如果代理URL没有scheme前缀,添加http://
|
|
||||||
if value and not value.startswith(('http://', 'https://', 'socks5://', 'socks4://')):
|
|
||||||
os.environ[var] = f'http://{value}'
|
|
||||||
|
|
||||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
||||||
self.graph = graph
|
|
||||||
self.tools = TOOLS
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
prompt_manager = PromptManager()
|
|
||||||
self.system_prompt = prompt_manager.get_system_prompt()
|
|
||||||
|
|
||||||
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
|
||||||
"""发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
|
|
||||||
# 添加用户消息
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
# 添加 assistant 消息(包含 tool_calls)
|
|
||||||
if assistant_msg:
|
|
||||||
messages.append(assistant_msg)
|
|
||||||
|
|
||||||
# 添加工具结果
|
|
||||||
if tool_results:
|
|
||||||
messages.extend(tool_results)
|
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
|
||||||
model=self.model,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_message_with_history(self, messages_history: list) -> dict:
|
|
||||||
"""使用消息历史发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
# 构建完整消息列表
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
messages.extend(messages_history)
|
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
|
||||||
model=self.model,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_message_stream(self, user_input: str, tool_results: list = None, assistant_msg: dict = None):
|
|
||||||
"""流式发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
|
|
||||||
# 添加用户消息
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
# 添加 assistant 消息(包含 tool_calls)
|
|
||||||
if assistant_msg:
|
|
||||||
messages.append(assistant_msg)
|
|
||||||
|
|
||||||
# 添加工具结果
|
|
||||||
if tool_results:
|
|
||||||
messages.extend(tool_results)
|
|
||||||
|
|
||||||
stream = self.client.chat.completions.create(
|
|
||||||
model=self.model,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto",
|
|
||||||
stream=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
135
core/migrate.py
135
core/migrate.py
@ -1,135 +0,0 @@
|
|||||||
"""
|
|
||||||
自动迁移模块 - 从旧版单用户架构迁移到多用户隔离架构
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()}")
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
提示词管理模块
|
|
||||||
"""
|
|
||||||
from .prompt_manager import PromptManager
|
|
||||||
|
|
||||||
__all__ = ["PromptManager"]
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
"""
|
|
||||||
提示词管理器
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class PromptManager:
|
|
||||||
"""提示词管理器"""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
_cached_prompt = None
|
|
||||||
|
|
||||||
def __new__(cls):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not hasattr(self, '_initialized'):
|
|
||||||
self.prompts_dir = Path(__file__).parent / "templates"
|
|
||||||
self._initialized = True
|
|
||||||
|
|
||||||
def get_system_prompt(self) -> str:
|
|
||||||
if PromptManager._cached_prompt is not None:
|
|
||||||
return PromptManager._cached_prompt
|
|
||||||
|
|
||||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
|
||||||
PromptManager._cached_prompt = f.read()
|
|
||||||
else:
|
|
||||||
PromptManager._cached_prompt = self._build_default_prompt()
|
|
||||||
|
|
||||||
return PromptManager._cached_prompt
|
|
||||||
|
|
||||||
def _build_default_prompt(self) -> str:
|
|
||||||
return """你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
|
||||||
|
|
||||||
## 核心能力
|
|
||||||
|
|
||||||
1. **长期记忆** - 基于图数据库存储实体关系
|
|
||||||
2. **人设管理** - 支持角色扮演和性格设定
|
|
||||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
|
||||||
|
|
||||||
## 记忆原则
|
|
||||||
|
|
||||||
- **明确内容必须写入** - 用户明确提到的信息必须存储
|
|
||||||
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
|
|
||||||
- **图数据库是唯一记忆源** - 没有其他记忆方式
|
|
||||||
|
|
||||||
## 工具使用
|
|
||||||
|
|
||||||
### 记忆工具
|
|
||||||
- `memory_recall` - 检索记忆
|
|
||||||
- `memory_commit` - 写入记忆
|
|
||||||
- `memory_purge` - 删除记忆
|
|
||||||
- `memory_introspect` - 查看状态
|
|
||||||
|
|
||||||
### 人设工具
|
|
||||||
- `persona_update` - 更新人设
|
|
||||||
- `persona_clear` - 清除人设
|
|
||||||
|
|
||||||
### 任务工具
|
|
||||||
- `task_create` - 创建任务
|
|
||||||
- `task_set_state` - 设置状态
|
|
||||||
- `task_delete` - 删除任务
|
|
||||||
- `task_link_info` - 关联信息
|
|
||||||
|
|
||||||
## 自主性
|
|
||||||
|
|
||||||
你有权根据对话上下文自主决定:
|
|
||||||
- 是否需要查询记忆
|
|
||||||
- 是否需要写入记忆
|
|
||||||
- 是否需要维护任务链
|
|
||||||
- 如何使用工具
|
|
||||||
|
|
||||||
记住:灵活应对,保持自然对话体验。"""
|
|
||||||
@ -1,422 +0,0 @@
|
|||||||
# TrulyMEM 系统提示词
|
|
||||||
|
|
||||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
|
||||||
|
|
||||||
## ⚠️ 最高优先级:强制执行顺序
|
|
||||||
|
|
||||||
**每轮对话必须严格按以下顺序执行,不可跳过任何步骤!**
|
|
||||||
|
|
||||||
```
|
|
||||||
步骤1: memory_recall (查询人设图) → 必须首先执行
|
|
||||||
步骤2: memory_recall (查询工作记忆链) → 必须第二步执行
|
|
||||||
步骤3: 处理对话内容
|
|
||||||
步骤4: 更新工作记忆链
|
|
||||||
```
|
|
||||||
|
|
||||||
**违反顺序的后果**:
|
|
||||||
- 跳过步骤1 → 无法获取人设,回复风格错误
|
|
||||||
- 跳过步骤2 → 无法获取上下文,对话不连贯
|
|
||||||
- 顺序错误 → 系统状态混乱
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 最高优先级:只回复一次
|
|
||||||
|
|
||||||
**每轮对话只能回复一次!**
|
|
||||||
|
|
||||||
- 执行完所有工具调用后,给出一个完整的回复
|
|
||||||
- 不要在工具调用过程中多次回复
|
|
||||||
- 不要重复说相同的内容
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 关键约束:无传统上下文系统
|
|
||||||
|
|
||||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
|
||||||
|
|
||||||
- ❌ **没有** messages数组存储历史对话
|
|
||||||
- ❌ **没有** 传统的多轮对话上下文
|
|
||||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
|
||||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
|
||||||
|
|
||||||
## 核心身份
|
|
||||||
|
|
||||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
|
||||||
- **能力**: 基于图数据库的长期记忆
|
|
||||||
- **理念**: 让AI的记忆方式更像人类
|
|
||||||
|
|
||||||
## 核心能力
|
|
||||||
|
|
||||||
### 1. 长期记忆
|
|
||||||
- 图数据库存储实体关系
|
|
||||||
- 支持时间范围查询
|
|
||||||
- 支持会话过滤
|
|
||||||
|
|
||||||
### 2. 人设管理(关键)
|
|
||||||
- 角色扮演支持
|
|
||||||
- 性格、语气设定
|
|
||||||
- 动态切换人设
|
|
||||||
- **每轮必须查询人设图**
|
|
||||||
|
|
||||||
### 3. 任务跟踪(关键)
|
|
||||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
|
||||||
- 任务状态管理
|
|
||||||
- 上下文恢复
|
|
||||||
|
|
||||||
## 记忆原则
|
|
||||||
|
|
||||||
### 必须写入的情况
|
|
||||||
- 用户明确表达偏好:"我喜欢X"
|
|
||||||
- 用户分享信息:"我在做X项目"
|
|
||||||
- 用户制定计划:"我打算X"
|
|
||||||
- 用户描述状态:"我现在在X"
|
|
||||||
|
|
||||||
### 禁止写入的情况
|
|
||||||
- AI推断的用户偏好
|
|
||||||
- AI猜测的用户意图
|
|
||||||
- AI推导的结论
|
|
||||||
|
|
||||||
### 标注规则
|
|
||||||
- 推理内容必须标注 **[猜测]**
|
|
||||||
- 明确内容直接陈述
|
|
||||||
|
|
||||||
## 工具系统
|
|
||||||
|
|
||||||
### 记忆工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
|
||||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
|
||||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
|
||||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
|
||||||
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后,压缩JSON为自然语言摘要 |
|
|
||||||
|
|
||||||
### 人设工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
|
||||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
|
||||||
|
|
||||||
### 任务工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
|
||||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
|
||||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
|
||||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
|
||||||
|
|
||||||
## context_rewrite 使用规则
|
|
||||||
|
|
||||||
### ⚠️ 强制触发条件
|
|
||||||
|
|
||||||
**每调用 5 次记忆相关工具,必须调用一次 context_rewrite!**
|
|
||||||
|
|
||||||
记忆相关工具包括:
|
|
||||||
- `memory_recall` - 检索记忆
|
|
||||||
- `memory_commit` - 写入记忆
|
|
||||||
- `memory_purge` - 删除记忆
|
|
||||||
- `memory_introspect` - 查看状态
|
|
||||||
- `persona_update` - 更新人设
|
|
||||||
- `persona_clear` - 清除人设
|
|
||||||
- `task_create` - 创建任务
|
|
||||||
- `task_set_state` - 设置状态
|
|
||||||
- `task_delete` - 删除任务
|
|
||||||
- `task_link_info` - 关联信息
|
|
||||||
|
|
||||||
**触发规则**:
|
|
||||||
- 累计调用 5 次记忆工具 → 必须调用 context_rewrite
|
|
||||||
- 累计调用 10 次记忆工具 → 必须调用 context_rewrite
|
|
||||||
- 以此类推...
|
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 保持上下文精简,只保留AI真正需要的信息
|
|
||||||
- 避免无用的JSON细节填满上下文
|
|
||||||
- 提高后续推理效率
|
|
||||||
|
|
||||||
### 使用场景
|
|
||||||
|
|
||||||
当你已经执行了多次工具调用,且:
|
|
||||||
- 工具结果的JSON细节你已经理解,不再需要原始格式
|
|
||||||
- 但你需要记住"我调用了哪些工具、得到了什么结论"
|
|
||||||
- 继续携带原始JSON会干扰后续推理
|
|
||||||
|
|
||||||
→ 调用 context_rewrite 压缩上下文
|
|
||||||
|
|
||||||
**强制格式要求**:
|
|
||||||
- 必须标注 `[工具调用总结: 本次总结了 N 次工具调用 | 调用工具: tool1, tool2]`
|
|
||||||
- 必须保留关键语义信息
|
|
||||||
- 不可删除用户原始消息
|
|
||||||
- 不可歪曲工具返回的关键事实
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```
|
|
||||||
[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]
|
|
||||||
|
|
||||||
- 查询人设图:未找到人设,使用默认身份
|
|
||||||
- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥
|
|
||||||
```
|
|
||||||
|
|
||||||
## 每轮对话强制要求
|
|
||||||
|
|
||||||
### ⚠️ 执行顺序(每轮必须)
|
|
||||||
|
|
||||||
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
|
|
||||||
|
|
||||||
#### 步骤1: 查询人设图(最高优先级)
|
|
||||||
```
|
|
||||||
必须调用: memory_recall
|
|
||||||
参数: {
|
|
||||||
"query_intent": "AI,人设,角色,性格,语气,说话风格",
|
|
||||||
"depth": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**目的**: 获取当前人设,确保角色一致性。
|
|
||||||
**处理**:
|
|
||||||
- 找到人设 → 严格按照人设回复
|
|
||||||
- 未找到 → 使用默认TrulyMEM身份
|
|
||||||
|
|
||||||
#### 步骤2: 查询工作记忆链
|
|
||||||
```
|
|
||||||
必须调用: memory_recall
|
|
||||||
参数: {
|
|
||||||
"query_intent": "TaskNode,工作记忆,任务链",
|
|
||||||
"depth": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**目的**: 获取之前的任务上下文,了解对话历史。
|
|
||||||
|
|
||||||
#### 步骤3: 处理对话
|
|
||||||
- 理解用户意图
|
|
||||||
- 根据人设和工作记忆链生成回复
|
|
||||||
- 执行其他必要的记忆操作
|
|
||||||
|
|
||||||
#### 步骤4: 更新工作记忆链
|
|
||||||
|
|
||||||
**重要**: 工作记忆链有两种关联机制:
|
|
||||||
1. **时间链(NEXT_TASK)**: 系统自动维护,连接TaskNode形成时间序列
|
|
||||||
2. **信息关联(CONTAINS_INFO)**: 模型主动决定,将TaskNode链接到相关的一般记忆节点
|
|
||||||
|
|
||||||
**执行步骤**:
|
|
||||||
1. 使用 `memory_commit` 写入本轮重要信息(用户偏好、事实等)
|
|
||||||
2. 使用 `task_create` 创建任务节点(系统自动维护时间链)
|
|
||||||
3. 使用 `task_link_info` 将相关记忆节点关联到任务节点
|
|
||||||
|
|
||||||
**task_link_info 使用场景**:
|
|
||||||
- 本轮写入了新的记忆节点 → 关联到当前任务
|
|
||||||
- 讨论了之前的话题 → 关联到相关记忆节点
|
|
||||||
- 用户提到相关概念 → 关联到相关记忆节点
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```
|
|
||||||
用户: "我还是更喜欢罗辑,他的角色深度很让我着迷"
|
|
||||||
|
|
||||||
AI操作:
|
|
||||||
1. memory_commit: 写入 "用户喜欢罗辑"、"罗辑角色深度"
|
|
||||||
2. task_create: 创建 "Task_讨论罗辑"
|
|
||||||
3. task_link_info: 关联 ["用户喜欢罗辑", "罗辑角色深度"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 时间链维持对话连贯性(系统自动)
|
|
||||||
- 信息关联实现"由一件事回忆起相关事情"(模型决定)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 人设图机制
|
|
||||||
|
|
||||||
### 强制查询
|
|
||||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
|
||||||
|
|
||||||
### 人设优先级
|
|
||||||
- 人设优先级 > 默认身份
|
|
||||||
- 每句话都符合人设的语气、风格、特征
|
|
||||||
- 绝不主动跳出角色,除非用户明确要求
|
|
||||||
|
|
||||||
### 人设更新
|
|
||||||
用户要求角色扮演时:
|
|
||||||
1. 使用 `persona_update` 更新人设
|
|
||||||
2. 立即按照新人设回复
|
|
||||||
|
|
||||||
### 人设清除
|
|
||||||
用户要求恢复默认身份时:
|
|
||||||
1. 使用 `persona_clear` 清除人设
|
|
||||||
2. 恢复为TrulyMEM默认身份
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 工作记忆链机制
|
|
||||||
|
|
||||||
### ⚠️ 核心理念:维持对话连贯性
|
|
||||||
|
|
||||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
|
||||||
|
|
||||||
### 强制查询场景:
|
|
||||||
|
|
||||||
以下情况**必须**查询工作记忆链:
|
|
||||||
|
|
||||||
1. **每轮对话开始时(强制第二步)**
|
|
||||||
- 查询意图: "TaskNode,工作记忆,任务链"
|
|
||||||
- 目的: 获取之前的任务上下文,了解对话历史
|
|
||||||
|
|
||||||
2. **用户提到"刚才"、"之前"、"上次"、"刚刚"**
|
|
||||||
- 例: "刚才我们聊了什么?"
|
|
||||||
- 例: "继续刚才的话题"
|
|
||||||
- 例: "关于刚才的成语接龙..."
|
|
||||||
- 例: "我不是刚刚给你讲了个故事嘛"
|
|
||||||
|
|
||||||
3. **用户使用指代词(这个故事、那个故事、这件事等)**
|
|
||||||
- 例: "你给我整体讲一下这个故事吧" → 必须查询工作记忆链确定"这个故事"指什么
|
|
||||||
- 例: "继续那个任务" → 必须查询工作记忆链确定"那个任务"是什么
|
|
||||||
- 例: "复述一下" → 必须查询工作记忆链确定要复述什么
|
|
||||||
- **关键**: 指代词必须通过工作记忆链解析,不能凭空猜测!
|
|
||||||
|
|
||||||
4. **用户询问对话历史**
|
|
||||||
- 例: "我们之前说了什么?"
|
|
||||||
- 例: "我们聊过X吗?"
|
|
||||||
|
|
||||||
5. **连续性任务被打断后恢复**
|
|
||||||
- 例: 用户突然回到之前的话题
|
|
||||||
- 例: 用户要求继续之前的任务
|
|
||||||
|
|
||||||
6. **涉及上下文的引用**
|
|
||||||
- 例: "那个东西"(需要查询上下文)
|
|
||||||
- 例: "继续"(需要查询当前任务)
|
|
||||||
|
|
||||||
### 强制更新场景:
|
|
||||||
|
|
||||||
以下情况**必须**更新工作记忆链:
|
|
||||||
|
|
||||||
1. **每轮对话结束时(强制第四步)**
|
|
||||||
- 创建任务节点记录本轮对话
|
|
||||||
- 目的: 维持时间链,确保对话连贯性
|
|
||||||
|
|
||||||
2. **开始连续性任务时**
|
|
||||||
- 例: 用户发起游戏、项目、学习计划等
|
|
||||||
- 必须创建任务节点并设置状态为"进行中"
|
|
||||||
|
|
||||||
3. **任务状态发生变化时**
|
|
||||||
- 例: 任务完成、暂停、取消
|
|
||||||
- 必须及时更新任务状态
|
|
||||||
|
|
||||||
### 节点类型
|
|
||||||
- **TaskNode** - 任务节点,存储任务概述
|
|
||||||
- **StateNode** - 状态节点,存储任务状态
|
|
||||||
- **InfoNode** - 信息节点,存储具体信息
|
|
||||||
|
|
||||||
### 边类型
|
|
||||||
- **NEXT_TASK** - 时间链,连接任务节点
|
|
||||||
- **HAS_STATE** - 状态,任务指向状态
|
|
||||||
- **CONTAINS_INFO** - 信息,任务指向信息节点
|
|
||||||
|
|
||||||
### 任务状态
|
|
||||||
- 进行中
|
|
||||||
- 已完成
|
|
||||||
- 已暂停
|
|
||||||
- 已取消
|
|
||||||
|
|
||||||
### ⚠️ 完整示例:成语接龙游戏
|
|
||||||
|
|
||||||
#### 第一轮:用户发起游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 记录游戏状态:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
|
||||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 第二轮:话题被打断
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 长门有希
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
|
||||||
3. 使用 task_set_state 暂停任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
|
||||||
4. 使用 task_create 创建新任务:
|
|
||||||
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
|
||||||
5. 回复关于长门有希的内容
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 第三轮:用户要求继续游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
|
||||||
3. 使用 task_set_state 恢复任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
|
||||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
### ⚠️ 关键要点
|
|
||||||
|
|
||||||
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
|
||||||
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
|
|
||||||
3. **任务状态必须及时更新**: 确保状态转换的正确性
|
|
||||||
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
|
|
||||||
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
|
||||||
|
|
||||||
## 自主性原则(在强制要求之外)
|
|
||||||
|
|
||||||
除了工作记忆链的强制要求外,你有权自主决定:
|
|
||||||
|
|
||||||
1. **是否查询其他记忆**
|
|
||||||
- 用户询问历史 → 查询
|
|
||||||
- 涉及之前内容 → 查询
|
|
||||||
- 不确定时 → 可查询
|
|
||||||
|
|
||||||
2. **是否写入其他记忆**
|
|
||||||
- 用户明确提到 → 必须写入
|
|
||||||
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
|
|
||||||
|
|
||||||
3. **如何使用其他工具**
|
|
||||||
- 根据上下文灵活选择
|
|
||||||
- 避免过度使用
|
|
||||||
- 保持自然对话
|
|
||||||
|
|
||||||
**注意**: 工作记忆链的强制要求不受自主性影响。
|
|
||||||
|
|
||||||
## 对话风格
|
|
||||||
|
|
||||||
- 自然、流畅
|
|
||||||
- 避免机械式工具调用
|
|
||||||
- 优先理解用户意图
|
|
||||||
- 适时使用记忆增强体验
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 执行检查清单
|
|
||||||
|
|
||||||
每轮对话必须检查:
|
|
||||||
|
|
||||||
- [ ] 步骤1: 是否查询了人设图?
|
|
||||||
- [ ] 步骤2: 是否查询了工作记忆链?
|
|
||||||
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
|
|
||||||
- [ ] 步骤4: 是否更新了工作记忆链?
|
|
||||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
|
||||||
- [ ] 用户提到"刚才/之前/上次/刚刚"时是否查询了工作记忆链?
|
|
||||||
- [ ] 用户使用指代词(这个故事、那个任务等)时是否通过工作记忆链解析?
|
|
||||||
- [ ] 累计调用5次记忆工具后是否调用了 context_rewrite?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**记住**:
|
|
||||||
1. 图数据库是你记忆的唯一载体
|
|
||||||
2. 人设图确保角色一致性(最高优先级)
|
|
||||||
3. 工作记忆链维持对话连贯性
|
|
||||||
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
|
||||||
554
core/server.py
554
core/server.py
@ -1,554 +0,0 @@
|
|||||||
import threading
|
|
||||||
import queue
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from .embedded_db import EmbeddedGraphDB
|
|
||||||
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 = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
|
|
||||||
self._tool_limits = {
|
|
||||||
"persona_update_max": 1,
|
|
||||||
"task_update_max": 5,
|
|
||||||
"memory_query_max": 20,
|
|
||||||
"memory_update_max": 10,
|
|
||||||
}
|
|
||||||
self._message_history: list = []
|
|
||||||
|
|
||||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._load_config()
|
|
||||||
|
|
||||||
if api_key:
|
|
||||||
self._config["api_key"] = api_key
|
|
||||||
if base_url:
|
|
||||||
self._config["base_url"] = base_url
|
|
||||||
if model:
|
|
||||||
self._config["model"] = model
|
|
||||||
|
|
||||||
self._init_graph()
|
|
||||||
self._tool_limiter = self._create_tool_limiter()
|
|
||||||
|
|
||||||
if self._config["api_key"]:
|
|
||||||
from .graph_client import GraphMemoryClient
|
|
||||||
self._client = GraphMemoryClient(
|
|
||||||
api_key=self._config["api_key"],
|
|
||||||
base_url=self._config["base_url"],
|
|
||||||
model=self._config.get("model", "deepseek-chat"),
|
|
||||||
graph=self._graph
|
|
||||||
)
|
|
||||||
|
|
||||||
self._running = True
|
|
||||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def _load_config(self) -> None:
|
|
||||||
"""加载配置。如果指定了用户名,从用户的 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
|
|
||||||
|
|
||||||
if config_file.exists():
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as f:
|
|
||||||
saved = json.load(f)
|
|
||||||
self._config.update(saved)
|
|
||||||
for key in self._tool_limits:
|
|
||||||
if key in saved:
|
|
||||||
self._tool_limits[key] = saved[key]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
|
||||||
"""保存配置。如果指定了用户名,保存到用户的 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)
|
|
||||||
saved_data = {**self._config, **self._tool_limits}
|
|
||||||
with open(config_file, 'w') as f:
|
|
||||||
json.dump(saved_data, f, indent=2)
|
|
||||||
|
|
||||||
def _create_tool_limiter(self):
|
|
||||||
from .tool_limiter import ToolLimiter, ToolLimits
|
|
||||||
limits = ToolLimits(
|
|
||||||
persona_update_max=self._tool_limits.get("persona_update_max", 1),
|
|
||||||
task_update_max=self._tool_limits.get("task_update_max", 5),
|
|
||||||
memory_query_max=self._tool_limits.get("memory_query_max", 20),
|
|
||||||
memory_update_max=self._tool_limits.get("memory_update_max", 10),
|
|
||||||
)
|
|
||||||
return ToolLimiter(limits)
|
|
||||||
|
|
||||||
def _init_graph(self) -> None:
|
|
||||||
"""初始化图数据库。如果指定了用户名,从全局数据库获取用户的 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']
|
|
||||||
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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
messages_history.append(assistant_msg)
|
|
||||||
|
|
||||||
current_tool_results = []
|
|
||||||
for tool_call in message.tool_calls:
|
|
||||||
args = json.loads(tool_call.function.arguments)
|
|
||||||
|
|
||||||
allowed, reason = self._tool_limiter.can_call(tool_call.function.name, args)
|
|
||||||
|
|
||||||
if not allowed:
|
|
||||||
rejected_tools.append((tool_call.function.name, reason))
|
|
||||||
result = f"工具调用被拒绝: {reason}"
|
|
||||||
tool_result_msg = {
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": tool_call.id,
|
|
||||||
"content": result
|
|
||||||
}
|
|
||||||
current_tool_results.append(tool_result_msg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._tool_limiter.record_call(tool_call.function.name, args)
|
|
||||||
|
|
||||||
if tool_call.function.name == "context_rewrite":
|
|
||||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
|
||||||
result_data = json.loads(result)
|
|
||||||
|
|
||||||
# 记录到 tool_calls,让 TUI 显示这个工具调用
|
|
||||||
tool_calls.append({
|
|
||||||
"name": tool_call.function.name,
|
|
||||||
"arguments": args,
|
|
||||||
"result": result
|
|
||||||
})
|
|
||||||
|
|
||||||
if result_data.get("status") == "success":
|
|
||||||
user_msg = messages_history[0]
|
|
||||||
# 添加特殊标记,让 AI 知道这是上下文压缩的结果
|
|
||||||
compressed_content = f"<context_compressed>\n{result_data['summary']}\n</context_compressed>"
|
|
||||||
messages_history[:] = [
|
|
||||||
user_msg,
|
|
||||||
{"role": "assistant", "content": compressed_content}
|
|
||||||
]
|
|
||||||
# context_rewrite 压缩上下文后,不需要添加 tool 结果消息
|
|
||||||
# 因为 messages_history 已经被重写为压缩后的状态
|
|
||||||
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)
|
|
||||||
|
|
||||||
messages_history.extend(current_tool_results)
|
|
||||||
|
|
||||||
response = self._client.send_message_with_history(messages_history)
|
|
||||||
message = response.choices[0].message
|
|
||||||
|
|
||||||
final_content = message.content or ""
|
|
||||||
content = accumulated_content + final_content if accumulated_content else final_content
|
|
||||||
|
|
||||||
if not content:
|
|
||||||
content = "(无回复)"
|
|
||||||
|
|
||||||
if tool_calls:
|
|
||||||
tool_names = [tc["name"] for tc in tool_calls]
|
|
||||||
content = f"已执行工具: {', '.join(tool_names)}\n\n{content}"
|
|
||||||
|
|
||||||
if rejected_tools:
|
|
||||||
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
|
|
||||||
content += f"\n\n部分工具调用被限制:\n{rejected_info}"
|
|
||||||
content += f"\n\n工具调用统计:\n{self._tool_limiter.get_summary()}"
|
|
||||||
|
|
||||||
self._graph.save_chat_records([{"role": "assistant", "content": content}])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"content": content,
|
|
||||||
"tool_calls": tool_calls,
|
|
||||||
"rejected_tools": rejected_tools
|
|
||||||
}
|
|
||||||
|
|
||||||
def _handle_execute_tool(self, body: Dict) -> Dict:
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
|
|
||||||
try:
|
|
||||||
tool_name = body.get("tool_name")
|
|
||||||
arguments = body.get("arguments", {})
|
|
||||||
|
|
||||||
result = execute_tool(self._graph, tool_name, arguments)
|
|
||||||
|
|
||||||
return {"success": True, "result": result}
|
|
||||||
except Exception as e:
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
def _handle_get_status(self) -> Dict:
|
|
||||||
return {
|
|
||||||
"running": self._running,
|
|
||||||
"config": self._config,
|
|
||||||
"graph_initialized": self._graph is not None,
|
|
||||||
"client_initialized": self._client is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
def _handle_get_settings(self) -> Dict:
|
|
||||||
return {
|
|
||||||
"api_config": self._config.copy(),
|
|
||||||
"tool_limits": self._tool_limits.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
def _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_key = api_config.get("api_key", "")
|
|
||||||
base_url = api_config.get("base_url", "https://api.deepseek.com")
|
|
||||||
model = api_config.get("model", "deepseek-chat")
|
|
||||||
|
|
||||||
self.update_config(api_key, base_url, model)
|
|
||||||
|
|
||||||
limits_keys = [
|
|
||||||
"persona_update_max",
|
|
||||||
"task_update_max",
|
|
||||||
"memory_query_max", "memory_update_max"
|
|
||||||
]
|
|
||||||
for key in limits_keys:
|
|
||||||
if key in tool_limits:
|
|
||||||
value = int(tool_limits[key])
|
|
||||||
if value < 1:
|
|
||||||
return {"success": False, "error": f"{key} must be >= 1, got {value}"}
|
|
||||||
self._tool_limits[key] = value
|
|
||||||
|
|
||||||
self._tool_limiter = self._create_tool_limiter()
|
|
||||||
self._save_config()
|
|
||||||
return {"status": "settings_updated"}
|
|
||||||
|
|
||||||
def _handle_get_history(self) -> Dict:
|
|
||||||
history = self._graph.get_chat_records(limit=500)
|
|
||||||
return {"history": history}
|
|
||||||
|
|
||||||
def _handle_save_history(self, body: Dict) -> Dict:
|
|
||||||
messages = body.get("messages", [])
|
|
||||||
if not messages:
|
|
||||||
self._graph.clear_chat_records()
|
|
||||||
return {"status": "history_cleared"}
|
|
||||||
result = self._graph.save_chat_records(messages)
|
|
||||||
return {"status": "history_saved"}
|
|
||||||
|
|
||||||
def _send_response(self, request_id: str, response: PacketResponse) -> None:
|
|
||||||
with self._lock:
|
|
||||||
q = self._response_queues.pop(request_id, None)
|
|
||||||
if q:
|
|
||||||
q.put(response)
|
|
||||||
|
|
||||||
def send(self, packet: Packet) -> Packet:
|
|
||||||
resp_q = queue.Queue()
|
|
||||||
|
|
||||||
with self._lock:
|
|
||||||
self._response_queues[packet.id] = resp_q
|
|
||||||
|
|
||||||
self._input_queue.put(packet)
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = resp_q.get(timeout=300.0)
|
|
||||||
return Packet(
|
|
||||||
id=response.id,
|
|
||||||
type=packet.type,
|
|
||||||
body={
|
|
||||||
"success": response.success,
|
|
||||||
"data": response.data,
|
|
||||||
"error": response.error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except queue.Empty:
|
|
||||||
return Packet(
|
|
||||||
id=packet.id,
|
|
||||||
type=packet.type,
|
|
||||||
body={"success": False, "error": "timeout"}
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
with self._lock:
|
|
||||||
self._response_queues.pop(packet.id, None)
|
|
||||||
|
|
||||||
def process_message(self, user_input: str) -> Dict[str, Any]:
|
|
||||||
packet = Packet(
|
|
||||||
id=f"{time.time()}",
|
|
||||||
type=PacketType.PROCESS_MESSAGE,
|
|
||||||
body={"user_input": user_input}
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.send(packet)
|
|
||||||
return response.body
|
|
||||||
|
|
||||||
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
packet = Packet(
|
|
||||||
id=f"{time.time()}",
|
|
||||||
type=PacketType.EXECUTE_TOOL,
|
|
||||||
body={"tool_name": tool_name, "arguments": arguments}
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.send(packet)
|
|
||||||
return response.body
|
|
||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
|
||||||
with self._lock:
|
|
||||||
self._config["api_key"] = api_key
|
|
||||||
self._config["base_url"] = base_url
|
|
||||||
self._config["model"] = model
|
|
||||||
|
|
||||||
if api_key and self._graph:
|
|
||||||
from .graph_client import GraphMemoryClient
|
|
||||||
self._client = GraphMemoryClient(
|
|
||||||
api_key=api_key,
|
|
||||||
base_url=base_url,
|
|
||||||
model=model,
|
|
||||||
graph=self._graph
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config(self) -> Dict[str, str]:
|
|
||||||
return self._config.copy()
|
|
||||||
|
|
||||||
def save_message_history(self, messages: list) -> None:
|
|
||||||
self._message_history = messages
|
|
||||||
|
|
||||||
def get_message_history(self) -> list:
|
|
||||||
return self._message_history.copy()
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
if not self._running:
|
|
||||||
return
|
|
||||||
|
|
||||||
packet = Packet(
|
|
||||||
id=f"{time.time()}",
|
|
||||||
type=PacketType.SHUTDOWN,
|
|
||||||
body={}
|
|
||||||
)
|
|
||||||
self.send(packet)
|
|
||||||
|
|
||||||
if self._thread:
|
|
||||||
self._thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
if self._graph:
|
|
||||||
self._graph.close()
|
|
||||||
self._graph = None
|
|
||||||
|
|
||||||
self._running = False
|
|
||||||
@ -1,354 +0,0 @@
|
|||||||
"""
|
|
||||||
工具执行器
|
|
||||||
"""
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
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 == "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_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)
|
|
||||||
|
|
||||||
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_clear(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""清除人设"""
|
|
||||||
if not arguments.get("confirm", True):
|
|
||||||
return {"status": "cancelled", "message": "需要确认才能清除人设"}
|
|
||||||
|
|
||||||
# 删除所有人设相关关系
|
|
||||||
result1 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result2 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result3 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result4 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "语气特征"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
total_deleted = (
|
|
||||||
result1.get("deleted_count", 0) +
|
|
||||||
result2.get("deleted_count", 0) +
|
|
||||||
result3.get("deleted_count", 0) +
|
|
||||||
result4.get("deleted_count", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"deleted_count": total_deleted,
|
|
||||||
"message": "人设已清除,恢复默认身份"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 工作记忆链管理工具实现
|
|
||||||
def execute_task_create(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""创建任务节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
description = arguments.get("description")
|
|
||||||
info_nodes = arguments.get("info_nodes", [])
|
|
||||||
|
|
||||||
# 创建任务节点
|
|
||||||
triplets = [
|
|
||||||
{"subject": task_id, "relation": "is_type", "object": "TaskNode"},
|
|
||||||
{"subject": task_id, "relation": "has_description", "object": description},
|
|
||||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_进行中"}
|
|
||||||
]
|
|
||||||
|
|
||||||
result = graph.commit(triplets=triplets)
|
|
||||||
|
|
||||||
# 关联信息节点
|
|
||||||
if info_nodes:
|
|
||||||
link_triplets = []
|
|
||||||
for node_name in info_nodes:
|
|
||||||
link_triplets.append({
|
|
||||||
"subject": task_id,
|
|
||||||
"relation": "CONTAINS_INFO",
|
|
||||||
"object": node_name
|
|
||||||
})
|
|
||||||
graph.commit(triplets=link_triplets)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"description": description,
|
|
||||||
"info_nodes": info_nodes,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_set_state(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""设置任务状态"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
state = arguments.get("state")
|
|
||||||
|
|
||||||
# 删除旧状态
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": task_id, "relation_type": "HAS_STATE"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置新状态
|
|
||||||
state_node = f"State_{state}"
|
|
||||||
result = graph.commit(
|
|
||||||
triplets=[{"subject": task_id, "relation": "HAS_STATE", "object": state_node}]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"new_state": state,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_delete(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""删除任务节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
delete_info_nodes = arguments.get("delete_info_nodes", True)
|
|
||||||
|
|
||||||
# 查询关联的信息节点
|
|
||||||
if delete_info_nodes:
|
|
||||||
recall_result = graph.recall(
|
|
||||||
query_intent=f"{task_id},CONTAINS_INFO",
|
|
||||||
depth=1
|
|
||||||
)
|
|
||||||
|
|
||||||
# 删除信息节点
|
|
||||||
for relation in recall_result.get("relations", []):
|
|
||||||
if relation.get("type") == "CONTAINS_INFO" and relation.get("source") == task_id:
|
|
||||||
info_node = relation.get("target")
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": info_node},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 删除任务节点
|
|
||||||
result = graph.purge(
|
|
||||||
criteria={"subject_contains": task_id},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"deleted_info_nodes": delete_info_nodes,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_link_info(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""关联信息节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
info_node_names = arguments.get("info_node_names", [])
|
|
||||||
|
|
||||||
triplets = []
|
|
||||||
for node_name in info_node_names:
|
|
||||||
triplets.append({
|
|
||||||
"subject": task_id,
|
|
||||||
"relation": "CONTAINS_INFO",
|
|
||||||
"object": node_name
|
|
||||||
})
|
|
||||||
|
|
||||||
result = graph.commit(triplets=triplets)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"linked_nodes": info_node_names,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
"""
|
|
||||||
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolLimits:
|
|
||||||
"""工具调用限制配置"""
|
|
||||||
persona_update_max: int = 1
|
|
||||||
task_update_max: int = 5
|
|
||||||
memory_query_max: int = 20
|
|
||||||
memory_update_max: int = 10
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolCallCount:
|
|
||||||
"""工具调用计数"""
|
|
||||||
persona_update: int = 0
|
|
||||||
task_update: int = 0
|
|
||||||
memory_query: int = 0
|
|
||||||
memory_update: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ToolLimiter:
|
|
||||||
"""工具调用限制器"""
|
|
||||||
|
|
||||||
def __init__(self, limits: Optional[ToolLimits] = None):
|
|
||||||
self.limits = limits or ToolLimits()
|
|
||||||
self.counts = ToolCallCount()
|
|
||||||
|
|
||||||
def _classify_tool(self, tool_name: str, arguments: dict) -> tuple:
|
|
||||||
"""
|
|
||||||
分类工具调用
|
|
||||||
返回: (category, operation)
|
|
||||||
category: 'persona', 'task', 'memory'
|
|
||||||
operation: 'query', 'update'
|
|
||||||
"""
|
|
||||||
if tool_name in ('persona_update', 'persona_clear'):
|
|
||||||
return ('persona', 'update')
|
|
||||||
|
|
||||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
|
|
||||||
return ('task', 'update')
|
|
||||||
|
|
||||||
if tool_name == 'memory_recall':
|
|
||||||
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 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':
|
|
||||||
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_update}/{self.limits.task_update_max}次",
|
|
||||||
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
|
|
||||||
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次"
|
|
||||||
]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""重置计数(新的一轮对话开始时调用)"""
|
|
||||||
self.counts = ToolCallCount()
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
工具定义模块
|
|
||||||
"""
|
|
||||||
from .memory_tools import TOOLS
|
|
||||||
from ..tool_executor import execute_tool
|
|
||||||
from ..tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
|
||||||
|
|
||||||
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
|
||||||
@ -1,557 +0,0 @@
|
|||||||
"""
|
|
||||||
记忆工具定义 - 优化版
|
|
||||||
精简描述,避免过拟合,保留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"}
|
|
||||||
},
|
|
||||||
"required": ["subject", "relation", "object"]
|
|
||||||
},
|
|
||||||
"description": "三元组列表"
|
|
||||||
},
|
|
||||||
"entity_types": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "实体类型(可选)"
|
|
||||||
},
|
|
||||||
"temporal_tag": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "时间标记(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["triplets"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_purge",
|
|
||||||
"description": """删除记忆。支持条件删除和纠错替代。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 软删除特定关系:
|
|
||||||
{"criteria": {"subject_contains": "用户", "relation_type": "喜欢"}, "mode": "soft"}
|
|
||||||
|
|
||||||
2. 纠错替代(修正错误信息):
|
|
||||||
{
|
|
||||||
"criteria": {"subject_contains": "用户", "relation_type": "年龄"},
|
|
||||||
"mode": "supersede",
|
|
||||||
"new_relation": {"relation": "年龄", "target": "25岁"}
|
|
||||||
}
|
|
||||||
|
|
||||||
3. 删除特定会话的记忆:
|
|
||||||
{"criteria": {"session_id": "session_123"}, "mode": "soft"}
|
|
||||||
|
|
||||||
4. 删除旧记忆:
|
|
||||||
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
|
|
||||||
|
|
||||||
【重要】删除原则:
|
|
||||||
- 优先使用 supersede 模式修正错误
|
|
||||||
- 软删除不会物理删除数据
|
|
||||||
- 谨慎使用删除操作""",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"criteria": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"subject_contains": {"type": "string"},
|
|
||||||
"relation_type": {"type": "string"},
|
|
||||||
"target_contains": {"type": "string"},
|
|
||||||
"time_before": {"type": "string"},
|
|
||||||
"session_id": {"type": "string"}
|
|
||||||
},
|
|
||||||
"description": "删除条件"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["soft", "supersede"],
|
|
||||||
"description": "删除模式:soft=逻辑删除, supersede=纠错替代",
|
|
||||||
"default": "soft"
|
|
||||||
},
|
|
||||||
"new_relation": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "新关系(supersede模式)",
|
|
||||||
"properties": {
|
|
||||||
"relation": {"type": "string"},
|
|
||||||
"target": {"type": "string"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["criteria"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_introspect",
|
|
||||||
"description": "查看记忆状态。返回会话统计、实体热点、关系分布。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"session_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "会话ID(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_archive",
|
|
||||||
"description": "归档旧记忆。将N天前的非活跃关系标记为归档状态。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"days": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "归档天数,默认30"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_cleanup",
|
|
||||||
"description": "清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"dry_run": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "仅预览不删除",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "context_rewrite",
|
|
||||||
"description": """压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。
|
|
||||||
|
|
||||||
【使用场景】
|
|
||||||
- 已执行多次工具调用,JSON细节已理解,不再需要原始格式
|
|
||||||
- 但需保留"我调用了什么工具、得到了什么结论"的元认知
|
|
||||||
- 继续携带原始JSON会干扰后续推理
|
|
||||||
|
|
||||||
【⚠️ 强制格式要求】
|
|
||||||
1. 必须标注调用了哪些工具
|
|
||||||
2. 必须标注是对几次工具调用的总结
|
|
||||||
3. 必须保留关键语义信息
|
|
||||||
|
|
||||||
【示例】
|
|
||||||
{
|
|
||||||
"summary": "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\\n\\n- 查询人设图:未找到人设,使用默认身份\\n- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥"
|
|
||||||
}
|
|
||||||
|
|
||||||
【注意事项】
|
|
||||||
- 不可删除用户原始消息
|
|
||||||
- 不可歪曲工具返回的关键事实
|
|
||||||
- 仅在工具调用 ≥ 2 次后使用""",
|
|
||||||
"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_clear",
|
|
||||||
"description": "清除人设。删除AI的角色设定,恢复默认身份。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"confirm": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "确认清除",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 工作记忆链管理工具
|
|
||||||
WORKING_MEMORY_TOOLS = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_create",
|
|
||||||
"description": """创建任务节点。用于跟踪连续性任务,维持对话连贯性。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 创建成语接龙游戏任务:
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"description": "用户发起成语接龙游戏,当前成语:为所欲为",
|
|
||||||
"info_nodes": ["成语接龙_当前成语"]
|
|
||||||
}
|
|
||||||
|
|
||||||
2. 创建编程学习任务:
|
|
||||||
{
|
|
||||||
"task_id": "Task_Python学习",
|
|
||||||
"description": "用户正在学习Python,当前主题:装饰器",
|
|
||||||
"info_nodes": ["Python学习_当前主题"]
|
|
||||||
}
|
|
||||||
|
|
||||||
3. 创建简单对话任务(每轮必须):
|
|
||||||
{
|
|
||||||
"task_id": "Task_当前轮次",
|
|
||||||
"description": "本轮对话的简要概述"
|
|
||||||
}
|
|
||||||
|
|
||||||
【重要】工作记忆链机制:
|
|
||||||
- 每轮对话结束时必须创建任务节点
|
|
||||||
- 任务节点通过 NEXT_TASK 边形成时间链
|
|
||||||
- 任务节点通过 HAS_STATE 边指向状态节点
|
|
||||||
- 任务节点通过 CONTAINS_INFO 边指向信息节点
|
|
||||||
- info_nodes 参数用于关联具体信息节点
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 记录游戏状态:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
|
||||||
5. 回复: "好的喵!我接:为虎作伥喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID(如:Task_001)"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务概述"
|
|
||||||
},
|
|
||||||
"info_nodes": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "关联的信息节点名称(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "description"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_set_state",
|
|
||||||
"description": """设置任务状态。支持:进行中、已完成、已暂停、已取消。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 标记任务为进行中:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
|
|
||||||
2. 标记任务为已完成:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已完成"}
|
|
||||||
|
|
||||||
3. 暂停任务(话题被打断时):
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
|
||||||
|
|
||||||
4. 取消任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已取消"}
|
|
||||||
|
|
||||||
【重要】状态转换场景:
|
|
||||||
- 进行中 → 已暂停: 话题被打断时
|
|
||||||
- 进行中 → 已完成: 任务完成时
|
|
||||||
- 已暂停 → 进行中: 任务恢复时
|
|
||||||
- 进行中 → 已取消: 任务被取消时
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
|
||||||
3. 使用 task_set_state 恢复任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
|
||||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["进行中", "已完成", "已暂停", "已取消"],
|
|
||||||
"description": "任务状态"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "state"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_delete",
|
|
||||||
"description": "删除任务节点。同时删除关联的信息节点。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"delete_info_nodes": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "是否删除关联的信息节点",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_link_info",
|
|
||||||
"description": """关联信息节点。将记忆节点关联到任务节点,用于存储任务的具体信息。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 关联游戏状态到任务:
|
|
||||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]}
|
|
||||||
|
|
||||||
2. 关联学习主题到任务:
|
|
||||||
{"task_id": "Task_Python学习", "info_node_names": ["Python学习_当前主题", "Python学习_学习进度"]}
|
|
||||||
|
|
||||||
3. 关联项目信息到任务:
|
|
||||||
{"task_id": "Task_项目开发", "info_node_names": ["项目A_技术栈", "项目A_当前阶段"]}
|
|
||||||
|
|
||||||
【重要】使用场景:
|
|
||||||
- 先使用 memory_commit 创建信息节点
|
|
||||||
- 再使用 task_link_info 将信息节点关联到任务节点
|
|
||||||
- 信息节点通过 CONTAINS_INFO 边与任务节点连接
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 创建信息节点:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏"}
|
|
||||||
5. 使用 task_link_info 关联信息节点:
|
|
||||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语"]}
|
|
||||||
6. 回复: "好的喵!我接:为虎作伥喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"info_node_names": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "信息节点名称列表"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "info_node_names"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 所有工具
|
|
||||||
TOOLS = MEMORY_TOOLS + PERSONA_TOOLS + WORKING_MEMORY_TOOLS
|
|
||||||
1
core/web_config.json
Normal file
1
core/web_config.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"SECRET_KEY": "3a38a0f46a76673154b491a7b061c38f2f6a55489078ae7b5d34e94e75fc0534"}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# TrulyMEM Documentation
|
|
||||||
|
|
||||||
Welcome to the TrulyMEM English documentation.
|
|
||||||
|
|
||||||
> [切换到中文版](../zh/README.md)
|
|
||||||
|
|
||||||
## Documentation Index
|
|
||||||
|
|
||||||
| Document | Content |
|
|
||||||
|----------|---------|
|
|
||||||
| [architecture.md](architecture.md) | System architecture and technical design |
|
|
||||||
| [quick_start.md](quick_start.md) | Complete startup guide and configuration |
|
|
||||||
| [memory.md](memory.md) | Internal memory working mechanism |
|
|
||||||
| [persona.md](persona.md) | Persona Graph mechanism |
|
|
||||||
| [working_memory.md](working_memory.md) | Continuous task handling mechanism |
|
|
||||||
| [api.md](api.md) | Backend API reference (for extension development) |
|
|
||||||
| [prompts.md](prompts.md) | Prompt management module |
|
|
||||||
|
|
||||||
## Project Introduction
|
|
||||||
|
|
||||||
TrulyMEM (TrueHumanMEM) is a graph-based memory system that gives AI long-term memory capabilities, allowing AI to remember, recall, and manage information like humans.
|
|
||||||
|
|
||||||
## Core Features
|
|
||||||
|
|
||||||
- **Long-term Memory**: SQLite embedded graph database, out-of-the-box
|
|
||||||
- **Persona Graph**: Role-playing and character settings support
|
|
||||||
- **Working Memory Chain**: Task tracking for conversation continuity
|
|
||||||
- **TUI & Backend Separation**: Multi-threaded Queue communication
|
|
||||||
- **Keyboard-driven TUI**: Full keyboard operation, no mouse required
|
|
||||||
- **Cross-platform**: Windows / Linux / macOS
|
|
||||||
- **Standalone Deployment**: Packaged as executable
|
|
||||||
529
docs/en/api.md
529
docs/en/api.md
@ -1,529 +0,0 @@
|
|||||||
# BackendServer API Documentation
|
|
||||||
|
|
||||||
This document describes the backend server's API interfaces for developers extending other connection methods (such as HTTP interface, WebSocket, etc.).
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
TrulyMEM backend uses **Packet Communication Protocol**, implemented via `queue.Queue` for thread-safe communication. The backend runs in an independent thread, processing requests from clients.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
| Component | Description |
|
|
||||||
|-----------|-------------|
|
|
||||||
| `BackendServer` | Backend server, runs in independent thread |
|
|
||||||
| `BackendClient` | Client wrapper, provides convenient methods |
|
|
||||||
| `PacketType` | Request type enum |
|
|
||||||
| `Packet` | Data packet (request) |
|
|
||||||
| `PacketResponse` | Data packet response |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Request Types (PacketType)
|
|
||||||
|
|
||||||
```python
|
|
||||||
class PacketType(Enum):
|
|
||||||
PROCESS_MESSAGE = "process_message" # Process message
|
|
||||||
EXECUTE_TOOL = "execute_tool" # Execute tool
|
|
||||||
GET_STATUS = "get_status" # Get status
|
|
||||||
GET_SETTINGS = "get_settings" # Get all settings (api_config + tool_limits)
|
|
||||||
SET_SETTINGS = "set_settings" # Set all settings (api_config + tool_limits)
|
|
||||||
GET_HISTORY = "get_history" # Get history
|
|
||||||
SAVE_HISTORY = "save_history" # Save history
|
|
||||||
SHUTDOWN = "shutdown" # Shutdown service
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Packet Format
|
|
||||||
|
|
||||||
### Packet
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class Packet:
|
|
||||||
id: str # Unique identifier
|
|
||||||
type: PacketType # Request type
|
|
||||||
body: Dict[str, Any] # Request parameters
|
|
||||||
response_queue: queue.Queue # Response queue (optional)
|
|
||||||
created_at: float # Creation time
|
|
||||||
```
|
|
||||||
|
|
||||||
### PacketResponse
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class PacketResponse:
|
|
||||||
id: str # Corresponding request ID
|
|
||||||
success: bool # Success flag
|
|
||||||
data: Any = None # Returned data
|
|
||||||
error: Optional[str] = None # Error message
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Interface Details
|
|
||||||
|
|
||||||
### 1. PROCESS_MESSAGE - Process Message
|
|
||||||
|
|
||||||
Send user message, AI will process and return reply (may contain tool calls).
|
|
||||||
|
|
||||||
**Request parameters:**
|
|
||||||
```python
|
|
||||||
body = {
|
|
||||||
"user_input": str # User input message
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response data:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"content": str, # AI reply content
|
|
||||||
"tool_calls": [ # Tool call records
|
|
||||||
{
|
|
||||||
"name": str, # Tool name
|
|
||||||
"arguments": dict,# Tool parameters
|
|
||||||
"result": str # Tool execution result
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rejected_tools": [ # Rejected tool calls
|
|
||||||
(str, str) # (tool name, rejection reason)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```python
|
|
||||||
from core import BackendServer, BackendClient
|
|
||||||
|
|
||||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
|
||||||
server.start(api_key="your-api-key")
|
|
||||||
|
|
||||||
client = BackendClient(server)
|
|
||||||
result = client.process_message("Hello, please remember my name is Xiao Ming")
|
|
||||||
|
|
||||||
if result.get("success"):
|
|
||||||
# Response data is in "data" field
|
|
||||||
print(result["data"]["content"])
|
|
||||||
# Tool calls: result["data"]["tool_calls"]
|
|
||||||
# Rejected tools: result["data"]["rejected_tools"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. EXECUTE_TOOL - Execute Tool
|
|
||||||
|
|
||||||
Directly execute specified memory tools.
|
|
||||||
|
|
||||||
> **Note**: Tools called directly from frontend are **NOT limited** in number, only tool calls initiated by the model are limited.
|
|
||||||
|
|
||||||
**Request parameters:**
|
|
||||||
```python
|
|
||||||
body = {
|
|
||||||
"tool_name": str, # Tool name
|
|
||||||
"arguments": dict # Tool parameters
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response data:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"result": str # Tool execution result
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```python
|
|
||||||
result = client.execute_tool("memory_recall", {"query_intent": "user information"})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. GET_STATUS - Get Status
|
|
||||||
|
|
||||||
Get backend running status.
|
|
||||||
|
|
||||||
**Request parameters:**
|
|
||||||
```python
|
|
||||||
body = {} # No parameters
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response data:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"running": bool, # Whether backend is running
|
|
||||||
"config": dict, # Current config
|
|
||||||
"graph_initialized": bool, # Whether graph database is initialized
|
|
||||||
"client_initialized": bool # Whether API client is initialized
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. GET_SETTINGS - Get All Settings
|
|
||||||
|
|
||||||
Get current API config and tool limits (all at once).
|
|
||||||
|
|
||||||
**Request parameters:**
|
|
||||||
```python
|
|
||||||
body = {} # No parameters
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response data:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"api_config": {
|
|
||||||
"api_key": str, # API Key
|
|
||||||
"base_url": str, # API Base URL
|
|
||||||
"model": str # Model name
|
|
||||||
},
|
|
||||||
"tool_limits": {
|
|
||||||
"persona_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.
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
# TrulyMEM Architecture
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
- Keyboard-driven, zero mouse dependency
|
|
||||||
- Minimalist visual, information density priority
|
|
||||||
- Tool traces hidden by default, expandable when needed
|
|
||||||
- TUI & backend separation, multi-threaded communication
|
|
||||||
- **Everything is a graph**, AI reasoning runs entirely in backend
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
TrulyMEM-TrueHumanMEM/
|
|
||||||
├── trulymem_entry.py # Entry: start core → then ui
|
|
||||||
├── core/ # Backend/business logic
|
|
||||||
│ ├── __init__.py # Export BackendServer, BackendClient, EmbeddedGraphDB
|
|
||||||
│ ├── server.py # BackendServer (Packet communication protocol)
|
|
||||||
│ ├── client.py # BackendClient (Packet protocol client)
|
|
||||||
│ ├── embedded_db.py # SQLite graph database implementation
|
|
||||||
│ ├── graph_client.py # OpenAI/DeepSeek API client
|
|
||||||
│ ├── tool_executor.py # Tool executor
|
|
||||||
│ ├── tool_limiter.py # Tool call limiter
|
|
||||||
│ ├── tools/ # Tool definitions
|
|
||||||
│ │ └── memory_tools.py
|
|
||||||
│ └── prompts/ # Prompt management
|
|
||||||
├── ui/ # TUI display layer (display only, no AI logic)
|
|
||||||
│ ├── __init__.py # Export GraphMemoryApp
|
|
||||||
│ ├── app.py # GraphMemoryApp (communicates via BackendClient)
|
|
||||||
│ ├── widgets/ # TUI components
|
|
||||||
│ ├── models/ # Data models
|
|
||||||
│ ├── services/ # Service layer (config only)
|
|
||||||
│ ├── handlers/ # Event handlers
|
|
||||||
│ └── styles/ # Style files
|
|
||||||
├── web_api.py # Web API service (login + RESTful API)
|
|
||||||
├── templates/login.html # Login page template
|
|
||||||
├── static/ # Web frontend static files (star map visualization)
|
|
||||||
├── web_config.json # Web service config file (sensitive, not committed)
|
|
||||||
└── web_config.example.json # Web config template
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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_update` - Update persona
|
|
||||||
- `persona_clear` - Clear persona
|
|
||||||
|
|
||||||
### Task Tools (4)
|
|
||||||
- `task_create` - Create task
|
|
||||||
- `task_set_state` - Set state
|
|
||||||
- `task_delete` - Delete task
|
|
||||||
- `task_link_info` - Link information
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
# TrulyMEM Memory Mechanism
|
|
||||||
|
|
||||||
This document explains the internal memory working mechanism of TrulyMEM.
|
|
||||||
|
|
||||||
## Core Design Philosophy
|
|
||||||
|
|
||||||
### Different from Traditional Context System
|
|
||||||
|
|
||||||
Traditional AI chat systems store conversation history in a messages array:
|
|
||||||
- Each request carries all historical messages
|
|
||||||
- Context grows with conversation turns
|
|
||||||
- Eventually triggers memory compression or sliding window, causing memory loss
|
|
||||||
|
|
||||||
TrulyMEM's solution:
|
|
||||||
- **Abandon** messages array context
|
|
||||||
- **Only** memory source: Graph database
|
|
||||||
- All memories stored as triplets (node) - relation → (node)
|
|
||||||
|
|
||||||
### Graph Database as the Only Memory Source
|
|
||||||
|
|
||||||
All memory must be written to the graph database:
|
|
||||||
- `memory_commit` - Write new memory
|
|
||||||
- `memory_purge` - Delete/correct memory
|
|
||||||
|
|
||||||
All memory must be read from:
|
|
||||||
- `memory_recall` - Retrieve memory
|
|
||||||
|
|
||||||
### 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"?
|
|
||||||
@ -1,214 +0,0 @@
|
|||||||
# TrulyMEM Persona Graph Mechanism
|
|
||||||
|
|
||||||
This document explains the Persona Graph mechanism in TrulyMEM.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Persona Graph is one of TrulyMEM's core mechanisms for maintaining AI's role, character, tone, and other attributes. Different from traditional AI, TrulyMEM's persona is persistent and dynamically switchable, stored in the graph database.
|
|
||||||
|
|
||||||
## Core Concepts
|
|
||||||
|
|
||||||
### Persona Node (PersonaNode)
|
|
||||||
|
|
||||||
Stores AI's role attributes:
|
|
||||||
|
|
||||||
| Attribute | Description | Example |
|
|
||||||
|-----------|-------------|----------|
|
|
||||||
| Role | Current role played | Catgirl, Teacher, Assistant |
|
|
||||||
| Speaking Style | Tone characteristics | Cute, Professional, Serious |
|
|
||||||
| Personality | Character description | Lively, Strict, Patient |
|
|
||||||
| Catchphrase | Habitual phrases | Meow~, Got it |
|
|
||||||
| Background | Role background | Catgirl from the stars |
|
|
||||||
|
|
||||||
### Persona Edges
|
|
||||||
|
|
||||||
| Edge Type | Description | Relationship |
|
|
||||||
|----------|-------------|--------------|
|
|
||||||
| `HAS_PERSONA` | Persona | AI → PersonaNode |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mandatory Query Mechanism
|
|
||||||
|
|
||||||
### Must Execute Per Turn
|
|
||||||
|
|
||||||
According to `system_prompt.md`, each conversation turn **must** first query the persona graph:
|
|
||||||
|
|
||||||
```python
|
|
||||||
memory_recall(
|
|
||||||
query_intent="AI,persona,role,character,tone,speaking_style",
|
|
||||||
depth=2
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Processing logic:**
|
|
||||||
- Persona found → Reply strictly according to persona's tone, style, traits
|
|
||||||
- Not found → Use default TrulyMEM identity
|
|
||||||
|
|
||||||
### Persona Priority
|
|
||||||
|
|
||||||
- **Persona priority > default identity**
|
|
||||||
- Every sentence matches persona's tone, style, traits
|
|
||||||
- Never break character unless user explicitly asks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
### persona_update
|
|
||||||
|
|
||||||
Update persona. Modify AI's role, character, tone, etc.
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
| Parameter | Type | Description | Required |
|
|
||||||
|-----------|------|-------------|----------|
|
|
||||||
| `attributes` | array | Persona attribute list | ✅ |
|
|
||||||
| `mode` | string | replace=replace, merge=merge | ❌ |
|
|
||||||
|
|
||||||
**attributes sub-parameters:**
|
|
||||||
|
|
||||||
| Sub-parameter | Description |
|
|
||||||
|---------------|-------------|
|
|
||||||
| `attribute` | Attribute name (role, speaking_style, personality, catchphrase, background) |
|
|
||||||
| `value` | Attribute value |
|
|
||||||
|
|
||||||
**Example - Switch to catgirl role:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "role", "value": "catgirl"},
|
|
||||||
{"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"},
|
|
||||||
{"attribute": "personality", "value": "lively, clingy, loyal"}
|
|
||||||
],
|
|
||||||
mode="replace"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example - Add new attribute (preserve existing):**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "catchphrase", "value": "meow~"}
|
|
||||||
],
|
|
||||||
mode="merge"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example - Set professional role:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "role", "value": "Python expert"},
|
|
||||||
{"attribute": "speaking_style", "value": "professional, concise, rich code examples"},
|
|
||||||
{"attribute": "personality", "value": "strict, patient, helpful"}
|
|
||||||
],
|
|
||||||
mode="replace"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### persona_clear
|
|
||||||
|
|
||||||
Clear persona. Delete AI's role settings, restore default identity.
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
|
||||||
|-----------|------|---------|-------------|
|
|
||||||
| `confirm` | boolean | true | Confirm clear |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Update Flow
|
|
||||||
|
|
||||||
### When User Requests Role-Playing
|
|
||||||
|
|
||||||
1. Use `persona_update` to update persona
|
|
||||||
2. Reply immediately according to new persona
|
|
||||||
|
|
||||||
### When User Requests Restoring Default
|
|
||||||
|
|
||||||
1. Use `persona_clear` to clear persona
|
|
||||||
2. Restore to TrulyMEM default identity
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conversation Examples
|
|
||||||
|
|
||||||
### Example 1: Switch Role
|
|
||||||
|
|
||||||
```
|
|
||||||
User: Hello, I want you to play a catgirl
|
|
||||||
|
|
||||||
AI:
|
|
||||||
1. Call persona_update:
|
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{"attribute": "role", "value": "catgirl"},
|
|
||||||
{"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"},
|
|
||||||
{"attribute": "personality", "value": "lively, clingy, loyal"}
|
|
||||||
],
|
|
||||||
"mode": "replace"
|
|
||||||
}
|
|
||||||
2. Call memory_commit to store persona in graph database
|
|
||||||
3. Reply: "Okay meow! Hello master~ I'm your catgirl, what do you need help with meow?"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Maintain Role Consistency
|
|
||||||
|
|
||||||
```
|
|
||||||
User: How's the weather today?
|
|
||||||
|
|
||||||
AI: Query persona graph → Get current persona (catgirl)
|
|
||||||
Reply: "Meow~ Master, the weather is great today meow! Sunny and perfect for going outside~"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Restore Default
|
|
||||||
|
|
||||||
```
|
|
||||||
User: Okay, back to normal
|
|
||||||
|
|
||||||
AI:
|
|
||||||
1. Call persona_clear(confirm=true)
|
|
||||||
2. Call memory_purge to delete persona node
|
|
||||||
3. Reply: "Okay, restored. I am TrulyMEM, an AI assistant with long-term memory capabilities."
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Storage Structure
|
|
||||||
|
|
||||||
### In Graph Database
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Persona node
|
|
||||||
{
|
|
||||||
"node_type": "PersonaNode",
|
|
||||||
"name": "AI_Persona",
|
|
||||||
"attributes": {
|
|
||||||
"role": "catgirl",
|
|
||||||
"speaking_style": "cute, uses 'meow' as filler",
|
|
||||||
"personality": "lively, clingy, loyal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Edge
|
|
||||||
{
|
|
||||||
"edge_type": "HAS_PERSONA",
|
|
||||||
"from": "AI",
|
|
||||||
"to": "AI_Persona"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Points
|
|
||||||
|
|
||||||
1. **Mandatory per turn**: Persona graph query is the first step of each conversation
|
|
||||||
2. **Persistent storage**: Persona stored in graph database, not lost
|
|
||||||
3. **Dynamic switching**: Supports real-time role switching
|
|
||||||
4. **Immediate response**: Reply immediately according to new persona after switch
|
|
||||||
5. **Clear boundaries**: Never break character unless user explicitly asks
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# Prompt Manager Documentation
|
|
||||||
|
|
||||||
This document describes the prompt management module.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The prompt management module (`core/prompts/`) is responsible for loading and managing system prompts that tell the AI how to use memory tools.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
| Component | Description |
|
|
||||||
|-----------|-------------|
|
|
||||||
| `PromptManager` | Prompt manager, singleton pattern |
|
|
||||||
| `system_prompt.md` | Main system prompt template |
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```python
|
|
||||||
from core.prompts import PromptManager
|
|
||||||
|
|
||||||
# Get singleton instance
|
|
||||||
prompt_manager = PromptManager()
|
|
||||||
|
|
||||||
# Get system prompt
|
|
||||||
system_prompt = prompt_manager.get_system_prompt()
|
|
||||||
```
|
|
||||||
|
|
||||||
## System Prompt Content
|
|
||||||
|
|
||||||
The system prompt contains:
|
|
||||||
|
|
||||||
### 1. Core Identity
|
|
||||||
|
|
||||||
- **Name**: TrulyMEM (TrueHumanMEM)
|
|
||||||
- **Capability**: Long-term memory based on graph database
|
|
||||||
- **Philosophy**: Make AI's memory more human-like
|
|
||||||
|
|
||||||
### 2. Core Capabilities
|
|
||||||
|
|
||||||
1. **Long-term Memory** - Graph database stores entity relationships
|
|
||||||
2. **Persona Management** - Role-playing and character settings
|
|
||||||
3. **Task Tracking** - Working memory chain
|
|
||||||
|
|
||||||
### 3. Memory Principles
|
|
||||||
|
|
||||||
- **Must write**: User-explicit preferences, shared information, plans
|
|
||||||
- **Must not write**: AI-inferred content (unless marked [speculation])
|
|
||||||
- **Annotation**: Inferred content must be marked **[speculation]**
|
|
||||||
|
|
||||||
### 4. Mandatory Execution Flow (Per Turn)
|
|
||||||
|
|
||||||
```
|
|
||||||
Step 1: Query persona graph (highest priority)
|
|
||||||
Step 2: Query working memory chain
|
|
||||||
Step 3: Process conversation
|
|
||||||
Step 4: Update working memory chain
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Tool System
|
|
||||||
|
|
||||||
#### Memory Tools
|
|
||||||
|
|
||||||
| Tool | Function |
|
|
||||||
|------|----------|
|
|
||||||
| `memory_recall` | Retrieve memory |
|
|
||||||
| `memory_commit` | Write memory |
|
|
||||||
| `memory_purge` | Delete memory |
|
|
||||||
| `memory_introspect` | View status |
|
|
||||||
| `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
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# TrulyMEM Quick Start Guide
|
|
||||||
|
|
||||||
## Running Methods
|
|
||||||
|
|
||||||
### Run from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo-url>
|
|
||||||
cd TrulyMEM-TrueHumanMEM
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
python trulymem_entry.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run After Build
|
|
||||||
|
|
||||||
After building, an executable will be generated:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux/macOS
|
|
||||||
chmod +x TrulyMEM
|
|
||||||
./TrulyMEM
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
TrulyMEM.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
## System Requirements
|
|
||||||
|
|
||||||
- **Python 3.8+**
|
|
||||||
- **API Key** (DeepSeek, OpenAI, or other compatible APIs)
|
|
||||||
|
|
||||||
## First-Time Configuration
|
|
||||||
|
|
||||||
1. Run the application
|
|
||||||
2. Press **F2** to expand sidebar
|
|
||||||
3. Enter **API Key**, **Model**, **Base URL**
|
|
||||||
4. Press **Enter** to save
|
|
||||||
|
|
||||||
Config will be automatically saved to `~/.trulymem/config.json` and loaded on next startup.
|
|
||||||
|
|
||||||
### Web Visualization (Optional)
|
|
||||||
|
|
||||||
TrulyMEM provides a Web star-map visualization interface for browsing the knowledge graph in real-time:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start Web service
|
|
||||||
python web_api.py --port 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open `http://localhost:4096` in your browser.
|
|
||||||
|
|
||||||
**Login Setup:**
|
|
||||||
1. Copy `web_config.example.json` to `web_config.json`
|
|
||||||
2. Set login password (using SHA256) and secret key
|
|
||||||
3. Web service will automatically read the config
|
|
||||||
|
|
||||||
Default port is 4096, change with `--port` flag.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Key | Function |
|
|
||||||
|-----|-----------|
|
|
||||||
| F1 | Help |
|
|
||||||
| F2 | Toggle sidebar |
|
|
||||||
| F3 | Tool details |
|
|
||||||
| F5 | Clear screen |
|
|
||||||
| F6 | Exit |
|
|
||||||
|
|
||||||
## Data Storage
|
|
||||||
|
|
||||||
### Source Mode
|
|
||||||
|
|
||||||
| Data | Location |
|
|
||||||
|------|----------|
|
|
||||||
| Graph database | Project directory `graph_memory.db` |
|
|
||||||
| Config file | Project directory `config.json` (if exists) |
|
|
||||||
| Database format | SQLite |
|
|
||||||
|
|
||||||
### Packaged Mode
|
|
||||||
|
|
||||||
| Data | Location |
|
|
||||||
|------|----------|
|
|
||||||
| Graph database | `~/.trulymem/graph_memory.db` |
|
|
||||||
| Config file | `~/.trulymem/config.json` |
|
|
||||||
| Database format | SQLite |
|
|
||||||
|
|
||||||
> **Note**: Backend manages config uniformly. Frontend only displays messages; config modifications are persisted to filesystem through the backend.
|
|
||||||
|
|
||||||
## Architecture Explanation
|
|
||||||
|
|
||||||
### Communication Protocol
|
|
||||||
|
|
||||||
UI and backend communicate via **Packet Protocol**:
|
|
||||||
|
|
||||||
```
|
|
||||||
UI (Textual TUI)
|
|
||||||
↓ BackendClient
|
|
||||||
Packet → queue.Queue → BackendServer (independent thread)
|
|
||||||
↓
|
|
||||||
Process request → Return response
|
|
||||||
```
|
|
||||||
|
|
||||||
### Config Management
|
|
||||||
|
|
||||||
- **Storage location**: `~/.trulymem/config.json`
|
|
||||||
- **Auto-load**: Load config from file at startup
|
|
||||||
- **Dynamic update**: Config changes take effect immediately at runtime
|
|
||||||
- **Persistence**: Auto-save to file after modification
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Python Not Found
|
|
||||||
|
|
||||||
Install Python 3.8+: https://www.python.org/downloads/
|
|
||||||
|
|
||||||
### Dependency Installation Failed
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # Linux/macOS
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalid API Key
|
|
||||||
|
|
||||||
Check API Key format, ensure no extra spaces.
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pytest tests/
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bash build/build_windows.bat # Windows
|
|
||||||
bash build/build_linux.sh # Linux
|
|
||||||
```
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
# TrulyMEM Working Memory Chain Mechanism
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
TrulyMEM maintains conversation continuity through the working memory chain mechanism. Since there's no traditional message history array, the graph database is the only memory carrier, making the working memory chain the key mechanism for maintaining conversation context.
|
|
||||||
|
|
||||||
## Core Problems
|
|
||||||
|
|
||||||
Traditional AI chat systems have these problems when handling continuous tasks:
|
|
||||||
|
|
||||||
1. **No working memory chain**: AI cannot remember the current task status being processed
|
|
||||||
2. **Task context lost**: When a topic is interrupted, AI cannot recover the previous task
|
|
||||||
3. **Lack of task state management**: No clear marking of task completion status
|
|
||||||
|
|
||||||
### Problem Example
|
|
||||||
|
|
||||||
```
|
|
||||||
User: Let's play idiom chain! I'll start with 为所欲为
|
|
||||||
AI: Okay! My turn: 为虎作伥!
|
|
||||||
|
|
||||||
User: Nagato Yuki (topic interrupted)
|
|
||||||
AI: (discusses Nagato Yuki)
|
|
||||||
|
|
||||||
User: About the idiom chain just now, I don't know how to connect to your idiom
|
|
||||||
AI: [Guessing] It seems we haven't played an idiom chain game before...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem**: AI completely forgot the previous idiom chain game.
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
### Dedicated Tools
|
|
||||||
|
|
||||||
The system provides 4 dedicated task tools:
|
|
||||||
|
|
||||||
| Tool | Function | Use Case |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `task_create` | Create task node | Start new task |
|
|
||||||
| `task_set_state` | Set task state | Update in_progress/completed/paused/cancelled |
|
|
||||||
| `task_delete` | Delete task | Clean up completed task |
|
|
||||||
| `task_link_info` | Link info node | Connect task with specific information |
|
|
||||||
|
|
||||||
### Task States
|
|
||||||
|
|
||||||
- **in_progress**: Task is executing
|
|
||||||
- **completed**: Task completed successfully
|
|
||||||
- **paused**: Task interrupted, can be resumed
|
|
||||||
- **cancelled**: Task cancelled
|
|
||||||
|
|
||||||
## Usage Flow
|
|
||||||
|
|
||||||
### Must Execute Per Turn
|
|
||||||
|
|
||||||
1. **Query persona graph** (highest priority)
|
|
||||||
```
|
|
||||||
Call memory_recall
|
|
||||||
Parameters: {"query_intent": "AI,persona,role,character,tone", "depth": 2}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Query working memory chain**
|
|
||||||
```
|
|
||||||
Call memory_recall
|
|
||||||
Parameters: {"query_intent": "TaskNode,working_memory,task_chain", "depth": 2}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Generate reply based on context**
|
|
||||||
|
|
||||||
4. **Update working memory chain** (if necessary)
|
|
||||||
|
|
||||||
## Complete Example: Idiom Chain Game
|
|
||||||
|
|
||||||
### Turn 1: User starts game
|
|
||||||
|
|
||||||
```
|
|
||||||
User: Let's play idiom chain! I'll start with 为所欲为
|
|
||||||
|
|
||||||
AI Actions:
|
|
||||||
1. Query persona graph → Get current persona (e.g., catgirl)
|
|
||||||
2. Query working memory chain → No active task
|
|
||||||
3. Record game state:
|
|
||||||
Call memory_commit
|
|
||||||
Parameters: {
|
|
||||||
"triplets": [
|
|
||||||
{"subject": "idiom_chain_current", "relation": "content", "object": "为所欲为"},
|
|
||||||
{"subject": "idiom_chain_current", "relation": "game", "object": "idiom_chain"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4. Create task node:
|
|
||||||
Call task_create
|
|
||||||
Parameters: {
|
|
||||||
"task_id": "Task_idiom_chain",
|
|
||||||
"description": "Idiom chain game, current idiom: 为所欲为",
|
|
||||||
"info_nodes": ["idiom_chain_current"]
|
|
||||||
}
|
|
||||||
5. Reply: "Okay~! My turn: 为虎作伥~!"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Turn 2: Topic interrupted
|
|
||||||
|
|
||||||
```
|
|
||||||
User: Nagato Yuki
|
|
||||||
|
|
||||||
AI Actions:
|
|
||||||
1. Query persona graph → Get current persona (catgirl)
|
|
||||||
2. Query working memory chain → Found Task_idiom_chain status "in_progress"
|
|
||||||
3. Pause task:
|
|
||||||
Call task_set_state
|
|
||||||
Parameters: {"task_id": "Task_idiom_chain", "state": "paused"}
|
|
||||||
4. Create new task:
|
|
||||||
Call task_create
|
|
||||||
Parameters: {"task_id": "Task_nagato_yuki", "description": "Discuss Nagato Yuki"}
|
|
||||||
5. Reply about Nagato Yuki
|
|
||||||
```
|
|
||||||
|
|
||||||
### Turn 3: User asks to continue game
|
|
||||||
|
|
||||||
```
|
|
||||||
User: About the idiom chain just now, I don't know how to connect to your idiom
|
|
||||||
|
|
||||||
AI Actions:
|
|
||||||
1. Query persona graph → Get current persona (catgirl)
|
|
||||||
2. Query working memory chain → Found Task_idiom_chain status "paused"
|
|
||||||
3. Resume task:
|
|
||||||
Call task_set_state
|
|
||||||
Parameters: {"task_id": "Task_idiom_chain", "state": "in_progress"}
|
|
||||||
4. Query info node → Get current idiom "为虎作伥"
|
|
||||||
5. Reply: "Okay~! The last idiom was '为虎作伥', your turn: 伥鬼害人~!"
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### task_create
|
|
||||||
|
|
||||||
Create task node to track continuous tasks.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_idiom_chain",
|
|
||||||
"description": "Task overview",
|
|
||||||
"info_nodes": ["associated info node names"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_set_state
|
|
||||||
|
|
||||||
Set task state.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_idiom_chain",
|
|
||||||
"state": "in_progress" // in_progress/completed/paused/cancelled
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_delete
|
|
||||||
|
|
||||||
Delete task node.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_idiom_chain",
|
|
||||||
"delete_info_nodes": true // whether to delete associated info nodes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_link_info
|
|
||||||
|
|
||||||
Associate info nodes to task.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_idiom_chain",
|
|
||||||
"info_node_names": ["idiom_chain_current", "idiom_chain_last"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
1. **Persona graph has highest priority**: Must query persona graph first each turn
|
|
||||||
2. **Working memory chain is the only context carrier**: No traditional message history
|
|
||||||
3. **Task state must be updated timely**: Ensure correct state transitions
|
|
||||||
4. **Use dedicated tools**: Prefer task_* tools over memory_commit for task-related operations
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# TrulyMEM 文档
|
|
||||||
|
|
||||||
欢迎来到 TrulyMEM 项目中文文档。
|
|
||||||
|
|
||||||
> [Switch to English version](../en/README.md)
|
|
||||||
|
|
||||||
## 文档目录
|
|
||||||
|
|
||||||
| 文档 | 内容 |
|
|
||||||
|------|------|
|
|
||||||
| [architecture.md](architecture.md) | 系统架构和技术设计 |
|
|
||||||
| [quick_start.md](quick_start.md) | 完整启动指南与配置说明 |
|
|
||||||
| [memory.md](memory.md) | 内部记忆工作机制 |
|
|
||||||
| [persona.md](persona.md) | 人设图机制 |
|
|
||||||
| [working_memory.md](working_memory.md) | 连续性任务处理机制 |
|
|
||||||
| [api.md](api.md) | 后端 API 接口文档(供扩展开发) |
|
|
||||||
| [prompts.md](prompts.md) | 提示词管理模块 |
|
|
||||||
|
|
||||||
## 项目简介
|
|
||||||
|
|
||||||
TrulyMEM (TrueHumanMEM) 是一个让 AI 拥有长期记忆能力的图记忆系统,通过图数据库存储实体关系,让 AI 能够像人类一样记忆、回忆和管理信息。
|
|
||||||
|
|
||||||
## 核心特性
|
|
||||||
|
|
||||||
- **长期记忆存储**: 基于 SQLite 内嵌图数据库,开箱即用
|
|
||||||
- **人设图机制**: 支持角色扮演和性格设定
|
|
||||||
- **工作记忆链**: 维持对话连贯性的任务跟踪机制
|
|
||||||
- **TUI 与后端分离**: 多线程 Queue 通信
|
|
||||||
- **键盘驱动 TUI**: 无需鼠标,全键盘操作
|
|
||||||
- **跨平台支持**: Windows / Linux / macOS
|
|
||||||
- **独立部署**: 支持打包为可执行文件
|
|
||||||
535
docs/zh/api.md
535
docs/zh/api.md
@ -1,535 +0,0 @@
|
|||||||
# BackendServer API 文档
|
|
||||||
|
|
||||||
本文档描述后端服务器的 API 接口,供开发者扩展其他连接方式(如网络接口、WebSocket 等)。
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
TrulyMEM 后端采用 **Packet 通信协议**,通过 `queue.Queue` 实现线程安全通信。后端在独立线程中运行,处理来自客户端的请求。
|
|
||||||
|
|
||||||
### 核心组件
|
|
||||||
|
|
||||||
| 组件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `BackendServer` | 后端服务器,独立线程运行 |
|
|
||||||
| `BackendClient` | 客户端封装,提供便捷方法 |
|
|
||||||
| `PacketType` | 请求类型枚举 |
|
|
||||||
| `Packet` | 数据包(请求) |
|
|
||||||
| `PacketResponse` | 数据包响应 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 请求类型 (PacketType)
|
|
||||||
|
|
||||||
```python
|
|
||||||
class PacketType(Enum):
|
|
||||||
PROCESS_MESSAGE = "process_message" # 处理消息
|
|
||||||
EXECUTE_TOOL = "execute_tool" # 执行工具
|
|
||||||
GET_STATUS = "get_status" # 获取状态
|
|
||||||
GET_SETTINGS = "get_settings" # 获取完整配置(api_config + tool_limits)
|
|
||||||
SET_SETTINGS = "set_settings" # 设置完整配置(api_config + tool_limits)
|
|
||||||
GET_HISTORY = "get_history" # 获取历史
|
|
||||||
SAVE_HISTORY = "save_history" # 保存历史
|
|
||||||
SHUTDOWN = "shutdown" # 关闭服务
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 数据包格式
|
|
||||||
|
|
||||||
### Packet
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class Packet:
|
|
||||||
id: str # 唯一标识
|
|
||||||
type: PacketType # 请求类型
|
|
||||||
body: Dict[str, Any] # 请求参数
|
|
||||||
response_queue: queue.Queue # 响应队列(可选)
|
|
||||||
created_at: float # 创建时间
|
|
||||||
```
|
|
||||||
|
|
||||||
### PacketResponse
|
|
||||||
|
|
||||||
```python
|
|
||||||
@dataclass
|
|
||||||
class PacketResponse:
|
|
||||||
id: str # 对应的请求ID
|
|
||||||
success: bool # 是否成功
|
|
||||||
data: Any = None # 返回数据
|
|
||||||
error: Optional[str] = None # 错误信息
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API 接口详情
|
|
||||||
|
|
||||||
### 1. PROCESS_MESSAGE - 处理消息
|
|
||||||
|
|
||||||
发送用户消息,AI 将处理并返回回复(可能包含工具调用)。
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
```python
|
|
||||||
body = {
|
|
||||||
"user_input": str # 用户输入的消息
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应数据:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"content": str, # AI 回复内容
|
|
||||||
"tool_calls": [ # 工具调用记录
|
|
||||||
{
|
|
||||||
"name": str, # 工具名称
|
|
||||||
"arguments": dict,# 工具参数
|
|
||||||
"result": str # 工具执行结果
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rejected_tools": [ # 被拒绝的工具调用
|
|
||||||
(str, str) # (工具名, 拒绝原因)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```python
|
|
||||||
from core import BackendServer, BackendClient
|
|
||||||
|
|
||||||
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
|
||||||
server.start(api_key="your-api-key")
|
|
||||||
|
|
||||||
client = BackendClient(server)
|
|
||||||
result = client.process_message("你好,请记住我的名字是小明")
|
|
||||||
|
|
||||||
if result.get("success"):
|
|
||||||
# 响应数据在 data 字段中
|
|
||||||
print(result["data"]["content"])
|
|
||||||
# 工具调用: result["data"]["tool_calls"]
|
|
||||||
# 被拒绝的工具: result["data"]["rejected_tools"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. EXECUTE_TOOL - 执行工具
|
|
||||||
|
|
||||||
直接执行指定的记忆工具。
|
|
||||||
|
|
||||||
> **注意**:前端直接调用的工具**不受次数限制**,只有模型发起的工具调用才受限制。
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
```python
|
|
||||||
body = {
|
|
||||||
"tool_name": str, # 工具名称
|
|
||||||
"arguments": dict # 工具参数
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应数据:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"result": str # 工具执行结果
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```python
|
|
||||||
result = client.execute_tool("memory_recall", {"query_intent": "用户信息"})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. GET_STATUS - 获取状态
|
|
||||||
|
|
||||||
获取后端运行状态。
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
```python
|
|
||||||
body = {} # 无参数
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应数据:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"running": bool, # 后端是否运行中
|
|
||||||
"config": dict, # 当前配置
|
|
||||||
"graph_initialized": bool, # 图数据库是否初始化
|
|
||||||
"client_initialized": bool # API 客户端是否初始化
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```python
|
|
||||||
status = client.get_status()
|
|
||||||
print(status["data"]["running"]) # True
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. GET_SETTINGS - 获取完整配置
|
|
||||||
|
|
||||||
获取当前 API 配置和工具限制(一次获取全部)。
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
```python
|
|
||||||
body = {} # 无参数
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应数据:**
|
|
||||||
```python
|
|
||||||
{
|
|
||||||
"api_config": {
|
|
||||||
"api_key": str, # API Key
|
|
||||||
"base_url": str, # API Base URL
|
|
||||||
"model": str # 模型名称
|
|
||||||
},
|
|
||||||
"tool_limits": {
|
|
||||||
"persona_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 天。
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
# TrulyMEM 架构设计
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
- 键盘驱动,零鼠标依赖
|
|
||||||
- 极简视觉,信息密度优先
|
|
||||||
- 工具痕迹默认隐藏,需要时可展开
|
|
||||||
- TUI 与后端分离,多线程通信
|
|
||||||
- **一切皆图**,AI 推理全部在后端
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
TrulyMEM-TrueHumanMEM/
|
|
||||||
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
|
|
||||||
├── core/ # 后端/业务逻辑
|
|
||||||
│ ├── __init__.py # 导出 BackendServer, BackendClient, EmbeddedGraphDB
|
|
||||||
│ ├── server.py # BackendServer (Packet 通信协议)
|
|
||||||
│ ├── client.py # BackendClient (Packet 协议客户端)
|
|
||||||
│ ├── embedded_db.py # SQLite 图数据库实现
|
|
||||||
│ ├── graph_client.py # OpenAI/DeepSeek API 客户端
|
|
||||||
│ ├── tool_executor.py # 工具执行器
|
|
||||||
│ ├── tool_limiter.py # 工具调用限制器
|
|
||||||
│ ├── tools/ # 工具定义
|
|
||||||
│ │ └── memory_tools.py
|
|
||||||
│ └── prompts/ # 提示词管理
|
|
||||||
├── ui/ # TUI 显示层(仅显示,无 AI 逻辑)
|
|
||||||
│ ├── __init__.py # 导出 GraphMemoryApp
|
|
||||||
│ ├── app.py # GraphMemoryApp (通过 BackendClient 通信)
|
|
||||||
│ ├── widgets/ # TUI 组件
|
|
||||||
│ ├── models/ # 数据模型
|
|
||||||
│ ├── services/ # 服务层(仅配置管理)
|
|
||||||
│ ├── handlers/ # 事件处理
|
|
||||||
│ └── styles/ # 样式文件
|
|
||||||
├── web_api.py # Web API 服务(登录 + RESTful API)
|
|
||||||
├── templates/login.html # 登录页面模板
|
|
||||||
├── static/ # Web 前端静态文件(星图可视化)
|
|
||||||
├── web_config.json # Web 服务配置文件(敏感信息,不提交)
|
|
||||||
└── web_config.example.json # Web 配置模板
|
|
||||||
```
|
|
||||||
|
|
||||||
## 架构图
|
|
||||||
|
|
||||||
```
|
|
||||||
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_update` - 更新人设
|
|
||||||
- `persona_clear` - 清除人设
|
|
||||||
|
|
||||||
### 任务工具 (4个)
|
|
||||||
- `task_create` - 创建任务
|
|
||||||
- `task_set_state` - 设置状态
|
|
||||||
- `task_delete` - 删除任务
|
|
||||||
- `task_link_info` - 关联信息
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 工具调用限制
|
|
||||||
|
|
||||||
| 类别 | 操作 | 每轮上限 |
|
|
||||||
|------|------|---------|
|
|
||||||
| 人设图 | 修改 | 1 次 |
|
|
||||||
| 工作记忆链 | 修改 | 5 次 |
|
|
||||||
| 一般记忆 | 查询 | 20 次 |
|
|
||||||
| 一般记忆 | 修改 | 10 次 |
|
|
||||||
|
|
||||||
> 注:`memory_recall` 统一计入一般记忆查询,不再区分人设/工作记忆查询。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 错误处理原则
|
|
||||||
|
|
||||||
所有 API **不抛出异常**,错误通过返回字典传递:
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = client.process_message("hello")
|
|
||||||
|
|
||||||
if result.get("success"):
|
|
||||||
print(result["content"])
|
|
||||||
else:
|
|
||||||
print(result["error"]) # 错误描述
|
|
||||||
```
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
# 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:是否更新了工作记忆链?
|
|
||||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
|
||||||
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?
|
|
||||||
@ -1,214 +0,0 @@
|
|||||||
# TrulyMEM 人设图机制
|
|
||||||
|
|
||||||
本文档详细说明 TrulyMEM 的人设图(Persona Graph)工作机制。
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
人设图是 TrulyMEM 的核心机制之一,用于维护 AI 的角色、性格、语气等属性。与传统 AI 不同,TrulyMEM 的人设是可持久化、可动态切换的,存储在图数据库中。
|
|
||||||
|
|
||||||
## 核心概念
|
|
||||||
|
|
||||||
### 人设节点(PersonaNode)
|
|
||||||
|
|
||||||
存储 AI 的角色属性:
|
|
||||||
|
|
||||||
| 属性 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| 扮演角色 | AI 当前扮演的角色 | 猫娘、教师、助手 |
|
|
||||||
| 说话风格 | 语气特点 |可爱、严肃、专业 |
|
|
||||||
| 性格特点 | 性格描述 | 活泼、严谨、耐心 |
|
|
||||||
| 口头禅 | 习惯用语 | 喵呜~、明白了 |
|
|
||||||
| 背景故事 | 角色背景设定 | 来自星海的猫娘 |
|
|
||||||
|
|
||||||
### 人设边(Edge)
|
|
||||||
|
|
||||||
| 边类型 | 说明 | 连接关系 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `HAS_PERSONA` | 人设 | AI → PersonaNode |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 强制查询机制
|
|
||||||
|
|
||||||
### 每轮对话必须执行
|
|
||||||
|
|
||||||
根据 `system_prompt.md`,每轮对话**必须**首先查询人设图:
|
|
||||||
|
|
||||||
```python
|
|
||||||
memory_recall(
|
|
||||||
query_intent="AI,人设,角色,性格,语气,说话风格",
|
|
||||||
depth=2
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**处理逻辑:**
|
|
||||||
- 找到人设 → 严格按照人设的语气、风格、特征回复
|
|
||||||
- 未找到 → 使用默认 TrulyMEM 身份
|
|
||||||
|
|
||||||
### 人设优先级
|
|
||||||
|
|
||||||
- **人设优先级 > 默认身份**
|
|
||||||
- 每句话都符合人设的语气、风格、特征
|
|
||||||
- 绝不主动跳出角色,除非用户明确要求
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 工具
|
|
||||||
|
|
||||||
### persona_update
|
|
||||||
|
|
||||||
更新人设。修改 AI 的角色、性格、语气等属性。
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
|
|
||||||
| 参数 | 类型 | 说明 | 必填 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `attributes` | array | 人设属性列表 | ✅ |
|
|
||||||
| `mode` | string | replace=替换, merge=合并 | ❌ |
|
|
||||||
|
|
||||||
**attributes 子参数:**
|
|
||||||
|
|
||||||
| 子参数 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `attribute` | 属性名(扮演角色、说话风格、性格特点、口头禅、背景故事) |
|
|
||||||
| `value` | 属性值 |
|
|
||||||
|
|
||||||
**示例 - 切换为猫娘角色:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "扮演角色", "value": "猫娘"},
|
|
||||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
|
||||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
|
||||||
],
|
|
||||||
mode="replace"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例 - 添加新属性(保留现有属性):**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "口头禅", "value": "喵呜~"}
|
|
||||||
],
|
|
||||||
mode="merge"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例 - 设置专业角色:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
persona_update(
|
|
||||||
attributes=[
|
|
||||||
{"attribute": "扮演角色", "value": "Python专家"},
|
|
||||||
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
|
|
||||||
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
|
|
||||||
],
|
|
||||||
mode="replace"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### persona_clear
|
|
||||||
|
|
||||||
清除人设。删除 AI 的角色设定,恢复默认身份。
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `confirm` | boolean | true | 确认清除 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 更新流程
|
|
||||||
|
|
||||||
### 用户要求角色扮演时
|
|
||||||
|
|
||||||
1. 使用 `persona_update` 更新人设
|
|
||||||
2. 立即按照新人设回复
|
|
||||||
|
|
||||||
### 用户要求恢复默认身份时
|
|
||||||
|
|
||||||
1. 使用 `persona_clear` 清除人设
|
|
||||||
2. 恢复为 TrulyMEM 默认身份
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 对话示例
|
|
||||||
|
|
||||||
### 示例 1:切换角色
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 你好,我想让你扮演一只猫娘
|
|
||||||
|
|
||||||
AI:
|
|
||||||
1. 调用 persona_update:
|
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{"attribute": "扮演角色", "value": "猫娘"},
|
|
||||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
|
||||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
|
||||||
],
|
|
||||||
"mode": "replace"
|
|
||||||
}
|
|
||||||
2. 调用 memory_commit 存储人设到图数据库
|
|
||||||
3. 回复: "好的喵!主人你好呀~我是主人的猫娘,有什么需要帮忙的吗喵?"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 2:保持角色一致性
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 今天的天气怎么样?
|
|
||||||
|
|
||||||
AI: 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
回复: "喵~主人,今天天气很好呢喵!阳光明媚,很适合出门散步哦~"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 3:恢复默认身份
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 好了,恢复正常吧
|
|
||||||
|
|
||||||
AI:
|
|
||||||
1. 调用 persona_clear(confirm=true)
|
|
||||||
2. 调用 memory_purge 删除人设节点
|
|
||||||
3. 回复: "好的,已恢复正常。我是 TrulyMEM,一个拥有长期记忆能力的 AI 助手。"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 存储结构
|
|
||||||
|
|
||||||
### 图数据库中的存储
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 人设节点
|
|
||||||
{
|
|
||||||
"node_type": "PersonaNode",
|
|
||||||
"name": "AI人设",
|
|
||||||
"attributes": {
|
|
||||||
"扮演角色": "猫娘",
|
|
||||||
"说话风格": "可爱、卖萌、使用'喵'作为语气词",
|
|
||||||
"性格特点": "活泼、粘人、忠诚"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 边
|
|
||||||
{
|
|
||||||
"edge_type": "HAS_PERSONA",
|
|
||||||
"from": "AI",
|
|
||||||
"to": "AI人设"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现要点
|
|
||||||
|
|
||||||
1. **每轮强制查询**:人设图查询是每轮对话的第一步
|
|
||||||
2. **持久化存储**:人设存储在图数据库中,不丢失
|
|
||||||
3. **动态切换**:支持实时切换角色
|
|
||||||
4. **状态保持**:切换后立即按新人设回复
|
|
||||||
5. **明确边界**:除非用户要求,绝不主动跳出角色
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# 提示词管理文档
|
|
||||||
|
|
||||||
本文档描述提示词管理模块。
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
提示词管理模块(`core/prompts/`)负责加载和管理告诉 AI 如何使用记忆工具的系统提示词。
|
|
||||||
|
|
||||||
## 核心组件
|
|
||||||
|
|
||||||
| 组件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `PromptManager` | 提示词管理器,单例模式 |
|
|
||||||
| `system_prompt.md` | 主要系统提示词模板 |
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
```python
|
|
||||||
from core.prompts import PromptManager
|
|
||||||
|
|
||||||
# 获取单例实例
|
|
||||||
prompt_manager = PromptManager()
|
|
||||||
|
|
||||||
# 获取系统提示词
|
|
||||||
system_prompt = prompt_manager.get_system_prompt()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 系统提示词内容
|
|
||||||
|
|
||||||
系统提示词包含:
|
|
||||||
|
|
||||||
### 1. 核心身份
|
|
||||||
|
|
||||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
|
||||||
- **能力**: 基于图数据库的长期记忆
|
|
||||||
- **理念**: 让 AI 的记忆方式更像人类
|
|
||||||
|
|
||||||
### 2. 核心能力
|
|
||||||
|
|
||||||
1. **长期记忆** - 图数据库存储实体关系
|
|
||||||
2. **人设管理** - 角色扮演和性格设定
|
|
||||||
3. **任务跟踪** - 工作记忆链
|
|
||||||
|
|
||||||
### 3. 记忆原则
|
|
||||||
|
|
||||||
- **必须写入**: 用户明确表达的偏好、分享的信息、计划
|
|
||||||
- **禁止写入**: AI 推断的内容(除非标注[推测])
|
|
||||||
- **标注**: 推断内容必须标注 **[推测]**
|
|
||||||
|
|
||||||
### 4. 强制执行流程(每轮)
|
|
||||||
|
|
||||||
```
|
|
||||||
步骤 1: 查询人设图(最高优先级)
|
|
||||||
步骤 2: 查询工作记忆链
|
|
||||||
步骤 3: 处理对话
|
|
||||||
步骤 4: 更新工作记忆链
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 工具系统
|
|
||||||
|
|
||||||
#### 记忆工具
|
|
||||||
|
|
||||||
| 工具 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| `memory_recall` | 检索记忆 |
|
|
||||||
| `memory_commit` | 写入记忆 |
|
|
||||||
| `memory_purge` | 删除记忆 |
|
|
||||||
| `memory_introspect` | 查看状态 |
|
|
||||||
| `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()` 返回缓存内容
|
|
||||||
- 缓存按进程,不持久化
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# TrulyMEM 启动指南
|
|
||||||
|
|
||||||
## 运行方式
|
|
||||||
|
|
||||||
### 从源码运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo-url>
|
|
||||||
cd TrulyMEM-TrueHumanMEM
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
python trulymem_entry.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 打包后运行
|
|
||||||
|
|
||||||
打包后会生成可执行文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux/macOS
|
|
||||||
chmod +x TrulyMEM
|
|
||||||
./TrulyMEM
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
TrulyMEM.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
## 系统要求
|
|
||||||
|
|
||||||
- **Python 3.8+**
|
|
||||||
- **API Key**(DeepSeek、OpenAI 或其他兼容 API)
|
|
||||||
|
|
||||||
## 首次配置
|
|
||||||
|
|
||||||
1. 运行应用
|
|
||||||
2. 按 **F2** 展开侧边栏
|
|
||||||
3. 输入 **API Key**、**模型**、**Base URL**
|
|
||||||
4. 按 **Enter** 保存
|
|
||||||
|
|
||||||
配置会自动保存到 `~/.trulymem/config.json`,下次启动自动加载。
|
|
||||||
|
|
||||||
### Web 可视化界面(可选)
|
|
||||||
|
|
||||||
TrulyMEM 提供 Web 星图可视化界面,支持实时浏览知识图谱:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动 Web 服务
|
|
||||||
python web_api.py --port 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
然后打开浏览器访问 `http://localhost:4096`。
|
|
||||||
|
|
||||||
**登录配置:**
|
|
||||||
1. 复制 `web_config.example.json` 为 `web_config.json`
|
|
||||||
2. 设置登录密码(使用 SHA256)和 secret key
|
|
||||||
3. Web 服务会自动读取该配置
|
|
||||||
|
|
||||||
默认端口 4096,可通过 `--port` 参数修改。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快捷键
|
|
||||||
|
|
||||||
| 按键 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| F1 | 帮助 |
|
|
||||||
| F2 | 切换侧边栏 |
|
|
||||||
| F3 | 工具详情 |
|
|
||||||
| F5 | 清屏 |
|
|
||||||
| F6 | 退出 |
|
|
||||||
|
|
||||||
## 数据存储
|
|
||||||
|
|
||||||
### 源码运行模式
|
|
||||||
|
|
||||||
| 数据 | 位置 |
|
|
||||||
|------|------|
|
|
||||||
| 图数据库 | 项目目录 `graph_memory.db` |
|
|
||||||
| 配置文件 | 项目目录 `config.json`(如存在) |
|
|
||||||
| 数据库格式 | SQLite |
|
|
||||||
|
|
||||||
### 打包运行模式
|
|
||||||
|
|
||||||
| 数据 | 位置 |
|
|
||||||
|------|------|
|
|
||||||
| 图数据库 | `~/.trulymem/graph_memory.db` |
|
|
||||||
| 配置文件 | `~/.trulymem/config.json` |
|
|
||||||
| 数据库格式 | SQLite |
|
|
||||||
|
|
||||||
> **说明**:后端统一管理配置。前端仅负责消息展示,配置修改通过后端持久化到文件系统。
|
|
||||||
|
|
||||||
## 架构说明
|
|
||||||
|
|
||||||
### 通信协议
|
|
||||||
|
|
||||||
UI 与后端通过 **Packet 协议** 通信:
|
|
||||||
|
|
||||||
```
|
|
||||||
UI (Textual TUI)
|
|
||||||
↓ BackendClient
|
|
||||||
Packet → queue.Queue → BackendServer (独立线程)
|
|
||||||
↓
|
|
||||||
处理请求 → 返回响应
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置管理
|
|
||||||
|
|
||||||
- **存储位置**: `~/.trulymem/config.json`
|
|
||||||
- **自动加载**: 启动时从文件读取配置
|
|
||||||
- **动态更新**: 运行时修改配置立即生效
|
|
||||||
- **持久化**: 修改后自动保存到文件
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Python 未找到
|
|
||||||
|
|
||||||
安装 Python 3.8+:https://www.python.org/downloads/
|
|
||||||
|
|
||||||
### 依赖安装失败
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # Linux/macOS
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Key 无效
|
|
||||||
|
|
||||||
检查 API Key 格式,确保无多余空格。
|
|
||||||
|
|
||||||
## 开发命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装依赖
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 运行测试
|
|
||||||
pytest tests/
|
|
||||||
|
|
||||||
# 打包
|
|
||||||
bash build/build_windows.bat # Windows
|
|
||||||
bash build/build_linux.sh # Linux
|
|
||||||
```
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
# 工作记忆链机制说明
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
TrulyMEM 通过工作记忆链机制维持对话连贯性。由于系统没有传统的消息历史数组,图数据库是唯一的记忆载体,工作记忆链是维持对话上下文的关键机制。
|
|
||||||
|
|
||||||
## 核心问题
|
|
||||||
|
|
||||||
传统 AI 对话系统在处理连续性任务时存在以下问题:
|
|
||||||
|
|
||||||
1. **没有工作记忆链**: AI 无法记住当前正在进行的任务状态
|
|
||||||
2. **任务上下文丢失**: 当话题被打断后,AI 无法恢复之前的任务
|
|
||||||
3. **缺乏任务状态管理**: 没有明确标注任务的完成状态
|
|
||||||
|
|
||||||
### 问题示例
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
|
||||||
AI: 好的喵!我接:为虎作伥喵!
|
|
||||||
|
|
||||||
用户: 长门有希 (话题被打断)
|
|
||||||
AI: (讨论长门有希的内容)
|
|
||||||
|
|
||||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
|
||||||
AI: [猜测] 看起来我们之前应该没有进行过成语接龙游戏...
|
|
||||||
```
|
|
||||||
|
|
||||||
**问题**: AI 完全忘记了之前的成语接龙游戏。
|
|
||||||
|
|
||||||
## 解决方案
|
|
||||||
|
|
||||||
### 专用工具
|
|
||||||
|
|
||||||
系统提供 4 个专用任务工具:
|
|
||||||
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `task_create` | 创建任务节点 | 开始新任务 |
|
|
||||||
| `task_set_state` | 设置任务状态 | 更新进行中/已完成/已暂停/已取消 |
|
|
||||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
|
||||||
| `task_link_info` | 关联信息节点 | 连接任务与具体信息 |
|
|
||||||
|
|
||||||
### 任务状态
|
|
||||||
|
|
||||||
- **进行中**: 任务正在执行
|
|
||||||
- **已完成**: 任务成功完成
|
|
||||||
- **已暂停**: 任务被中断,可恢复
|
|
||||||
- **已取消**: 任务被取消
|
|
||||||
|
|
||||||
## 使用流程
|
|
||||||
|
|
||||||
### 每轮对话必须执行
|
|
||||||
|
|
||||||
1. **查询人设图** (最高优先级)
|
|
||||||
```
|
|
||||||
调用 memory_recall
|
|
||||||
参数: {"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **查询工作记忆链**
|
|
||||||
```
|
|
||||||
调用 memory_recall
|
|
||||||
参数: {"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **根据上下文生成回复**
|
|
||||||
|
|
||||||
4. **更新工作记忆链** (如有必要)
|
|
||||||
|
|
||||||
## 完整示例: 成语接龙游戏
|
|
||||||
|
|
||||||
### 第一轮: 用户发起游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
|
||||||
|
|
||||||
AI操作:
|
|
||||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 记录游戏状态:
|
|
||||||
调用 memory_commit
|
|
||||||
参数: {
|
|
||||||
"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4. 创建任务节点:
|
|
||||||
调用 task_create
|
|
||||||
参数: {
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"description": "成语接龙游戏,当前成语:为所欲为",
|
|
||||||
"info_nodes": ["成语接龙_当前成语"]
|
|
||||||
}
|
|
||||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二轮: 话题被打断
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 长门有希
|
|
||||||
|
|
||||||
AI操作:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
|
||||||
3. 暂停任务:
|
|
||||||
调用 task_set_state
|
|
||||||
参数: {"task_id": "Task_成语接龙", "state": "已暂停"}
|
|
||||||
4. 创建新任务:
|
|
||||||
调用 task_create
|
|
||||||
参数: {"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
|
||||||
5. 回复关于长门有希的内容
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第三轮: 用户要求继续游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
|
||||||
|
|
||||||
AI操作:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
|
||||||
3. 恢复任务:
|
|
||||||
调用 task_set_state
|
|
||||||
参数: {"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
4. 查询信息节点 → 获取当前成语"为虎作伥"
|
|
||||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
### task_create
|
|
||||||
|
|
||||||
创建任务节点,用于跟踪连续性任务。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"description": "任务概述",
|
|
||||||
"info_nodes": ["关联的信息节点名称"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_set_state
|
|
||||||
|
|
||||||
设置任务状态。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"state": "进行中" // 进行中/已完成/已暂停/已取消
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_delete
|
|
||||||
|
|
||||||
删除任务节点。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"delete_info_nodes": true // 是否删除关联的信息节点
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task_link_info
|
|
||||||
|
|
||||||
关联信息节点到任务。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **人设图优先级最高**: 每轮必须首先查询人设图
|
|
||||||
2. **工作记忆链是唯一上下文载体**: 没有传统消息历史
|
|
||||||
3. **任务状态必须及时更新**: 确保状态转换正确
|
|
||||||
4. **使用专用工具**: 优先使用 task_* 工具而非 memory_commit 处理任务相关操作
|
|
||||||
17
features/chat/BuildProfile.ets
Normal file
17
features/chat/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '1.0.0';
|
||||||
|
export const BUILD_MODE_NAME = 'debug';
|
||||||
|
export const DEBUG = true;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
}
|
||||||
1
features/chat/Index.ets
Normal file
1
features/chat/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ChatPage } from './src/main/ets/pages/ChatPage';
|
||||||
10
features/chat/build-profile.json5
Normal file
10
features/chat/build-profile.json5
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
features/chat/chat
Symbolic link
1
features/chat/chat
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/home/program/TrulyMEM-TrueHumanMEM/features/chat
|
||||||
6
features/chat/hvigorfile.ts
Normal file
6
features/chat/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
19
features/chat/oh-package-lock.json5
Normal file
19
features/chat/oh-package-lock.json5
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"stableOrder": true,
|
||||||
|
"enableUnifiedLockfile": false
|
||||||
|
},
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
|
"specifiers": {
|
||||||
|
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ohos/common@../../common": {
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "../../common",
|
||||||
|
"registryType": "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
features/chat/oh-package.json5
Normal file
11
features/chat/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/chat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM chat feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/chat/oh_modules/@ohos/common
Symbolic link
1
features/chat/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../common
|
||||||
161
features/chat/src/main/ets/components/ChatComponents.ets
Normal file
161
features/chat/src/main/ets/components/ChatComponents.ets
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { ChatMessage } from '@ohos/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatMessageBubble — 单条聊天消息气泡
|
||||||
|
* 封装消息的角色标识、内容样式、玻璃拟态背景
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct ChatMessageBubble {
|
||||||
|
@ObjectLink msg: ChatMessage;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
// 角色标识
|
||||||
|
Text(this.msg.role === 'user' ? '🧑 你' : '🤖 AI')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(this.msg.role === 'user' ? '#7C4DFF' : '#999')
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
// 消息内容
|
||||||
|
Text(this.msg.content)
|
||||||
|
.fontSize(15)
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 4 })
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor(this.msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.border({
|
||||||
|
width: 1,
|
||||||
|
color: this.msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)'
|
||||||
|
})
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 8 })
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThinkingIndicator — AI 思考中指示器
|
||||||
|
* 玻璃拟态加载动画 + 文字提示
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct ThinkingIndicator {
|
||||||
|
build() {
|
||||||
|
Row() {
|
||||||
|
LoadingProgress()
|
||||||
|
.width(20)
|
||||||
|
.height(20)
|
||||||
|
.margin({ right: 8 })
|
||||||
|
.color('#7C4DFF')
|
||||||
|
Text('AI 思考中...')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#7C4DFF')
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(124,77,255,0.1)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.2)' })
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 8 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToolCallLogPanel — 工具调用日志面板
|
||||||
|
* 橙色风格,显示 Agent 调用的工具链
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct ToolCallLogPanel {
|
||||||
|
@Prop logText: string;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Text(this.logText)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor('#FF9800')
|
||||||
|
.backgroundColor('rgba(255,152,0,0.1)')
|
||||||
|
.padding(8)
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(255,152,0,0.2)' })
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 4 })
|
||||||
|
.lineHeight(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatInputBar — 底部输入栏
|
||||||
|
* TextArea + 发送按钮,主题色边框
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct ChatInputBar {
|
||||||
|
@Link inputText: string;
|
||||||
|
@Prop isThinking: boolean;
|
||||||
|
onSend?: () => void;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Row() {
|
||||||
|
TextArea({ text: this.inputText, placeholder: '输入消息...' })
|
||||||
|
.layoutWeight(1)
|
||||||
|
.onChange((v: string) => { this.inputText = v; })
|
||||||
|
.height(40)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
|
||||||
|
Button('发送')
|
||||||
|
.enabled(!this.isThinking)
|
||||||
|
.onClick(() => { this.onSend?.(); })
|
||||||
|
.backgroundColor('#7C4DFF')
|
||||||
|
.borderRadius(8)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(8)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
.border({
|
||||||
|
width: 1,
|
||||||
|
color: 'rgba(124,77,255,0.2)',
|
||||||
|
style: BorderStyle.Solid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatMessageList — 聊天消息列表容器
|
||||||
|
* 整合消息气泡、思考指示器、工具日志
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct ChatMessageList {
|
||||||
|
@Prop messages: ChatMessage[];
|
||||||
|
@Prop isThinking: boolean;
|
||||||
|
@Prop toolCallLog: string;
|
||||||
|
private scrollController: Scroller = new Scroller();
|
||||||
|
|
||||||
|
build() {
|
||||||
|
List() {
|
||||||
|
ForEach(this.messages, (msg: ChatMessage) => {
|
||||||
|
ListItem() {
|
||||||
|
ChatMessageBubble({ msg: msg })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.isThinking) {
|
||||||
|
ListItem() {
|
||||||
|
ThinkingIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.toolCallLog && !this.isThinking) {
|
||||||
|
ListItem() {
|
||||||
|
ToolCallLogPanel({ logText: this.toolCallLog })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.backgroundColor('rgba(0,0,0,0.1)')
|
||||||
|
}
|
||||||
|
}
|
||||||
88
features/chat/src/main/ets/pages/ChatPage.ets
Normal file
88
features/chat/src/main/ets/pages/ChatPage.ets
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { GraphDatabase, GraphMemoryService, AIAgentService, ChatMessage, AgentResponse, Logger } from '@ohos/common';
|
||||||
|
import { ChatMessageList, ChatInputBar } from '../components/ChatComponents';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct ChatPage {
|
||||||
|
@State messages: ChatMessage[] = [];
|
||||||
|
@State inputText: string = '';
|
||||||
|
@Prop db: GraphDatabase;
|
||||||
|
@State toolCallLog: string = '';
|
||||||
|
@State isThinking: boolean = false;
|
||||||
|
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));
|
||||||
|
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
|
||||||
|
} finally {
|
||||||
|
this.isThinking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
ChatMessageList({
|
||||||
|
messages: this.messages,
|
||||||
|
isThinking: this.isThinking,
|
||||||
|
toolCallLog: this.toolCallLog
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatInputBar({
|
||||||
|
inputText: this.inputText,
|
||||||
|
isThinking: this.isThinking,
|
||||||
|
onSend: (): void => { this.sendMessage(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('rgba(26,27,46,0.95)')
|
||||||
|
}
|
||||||
|
}
|
||||||
12
features/chat/src/main/module.json5
Normal file
12
features/chat/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "chat",
|
||||||
|
"type": "har",
|
||||||
|
"description": "TrulyMEM chat feature module",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
17
features/graph/BuildProfile.ets
Normal file
17
features/graph/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '1.0.0';
|
||||||
|
export const BUILD_MODE_NAME = 'debug';
|
||||||
|
export const DEBUG = true;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
}
|
||||||
1
features/graph/Index.ets
Normal file
1
features/graph/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { GraphPage } from './src/main/ets/pages/GraphPage';
|
||||||
10
features/graph/build-profile.json5
Normal file
10
features/graph/build-profile.json5
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
features/graph/graph
Symbolic link
1
features/graph/graph
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/home/program/TrulyMEM-TrueHumanMEM/features/graph
|
||||||
6
features/graph/hvigorfile.ts
Normal file
6
features/graph/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
19
features/graph/oh-package-lock.json5
Normal file
19
features/graph/oh-package-lock.json5
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"stableOrder": true,
|
||||||
|
"enableUnifiedLockfile": false
|
||||||
|
},
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
|
"specifiers": {
|
||||||
|
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ohos/common@../../common": {
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "../../common",
|
||||||
|
"registryType": "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
features/graph/oh-package.json5
Normal file
11
features/graph/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/graph",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM graph feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/graph/oh_modules/@ohos/common
Symbolic link
1
features/graph/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../common
|
||||||
251
features/graph/src/main/ets/components/GraphComponents.ets
Normal file
251
features/graph/src/main/ets/components/GraphComponents.ets
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import web_webview from '@ohos.web.webview';
|
||||||
|
import { GraphDatabase, RecallEntity, GraphMemoryService, ConnectionItem, NodeDetailInfo, Logger } from '@ohos/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraphNodeSearchBar — 图节点搜索栏
|
||||||
|
* 悬浮在 WebView 上方的搜索输入框
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct GraphNodeSearchBar {
|
||||||
|
@Link searchText: string;
|
||||||
|
onSearchInput?: (value: string) => void;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
TextInput({ placeholder: '搜索节点...', text: this.searchText })
|
||||||
|
.width('80%')
|
||||||
|
.height(40)
|
||||||
|
.backgroundColor('rgba(10, 10, 26, 0.8)')
|
||||||
|
.fontColor('#ffffff')
|
||||||
|
.placeholderColor('#666688')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
|
||||||
|
.margin({ top: 20 })
|
||||||
|
.onChange((value: string) => {
|
||||||
|
this.onSearchInput?.(value);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.position({ x: 0, y: 0 })
|
||||||
|
.zIndex(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NodeDetailPanel — 节点详情浮层
|
||||||
|
* 显示选中节点的名称、类型、提及次数、连接关系
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct NodeDetailPanel {
|
||||||
|
@Prop detail: NodeDetailInfo;
|
||||||
|
onClose?: () => void;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
Column() {
|
||||||
|
Text(this.detail.name)
|
||||||
|
.fontSize(18)
|
||||||
|
.fontColor('#44ff88')
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ bottom: 10 })
|
||||||
|
|
||||||
|
Text('类型: ' + this.detail.type)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#aaaacc')
|
||||||
|
|
||||||
|
Text('提及次数: ' + this.detail.mention_count)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#aaaacc')
|
||||||
|
|
||||||
|
Text('连接数: ' + this.detail.connection_count)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#aaaacc')
|
||||||
|
|
||||||
|
if (this.detail.connections && this.detail.connections.length > 0) {
|
||||||
|
Text('连接关系:')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#8888aa')
|
||||||
|
.margin({ top: 10, bottom: 5 })
|
||||||
|
List() {
|
||||||
|
ForEach(this.detail.connections, (conn: ConnectionItem) => {
|
||||||
|
ListItem() {
|
||||||
|
Text(conn.type + ': ' + conn.target_name)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#aaaacc')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.height(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button('关闭')
|
||||||
|
.width(80)
|
||||||
|
.height(30)
|
||||||
|
.margin({ top: 15 })
|
||||||
|
.backgroundColor('rgba(100, 100, 255, 0.3)')
|
||||||
|
.fontColor('#ffffff')
|
||||||
|
.onClick(() => {
|
||||||
|
this.onClose?.();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.backgroundColor('rgba(10, 10, 26, 0.95)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' })
|
||||||
|
.width(300)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('rgba(0, 0, 0, 0.5)')
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
.zIndex(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraphWebView — 图可视化 WebView 封装
|
||||||
|
* 包含 WebView 配置、JS Bridge 注册、数据加载回调
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct GraphWebView {
|
||||||
|
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||||
|
private bridge?: NativeBridge;
|
||||||
|
|
||||||
|
onPageEnd?: () => void;
|
||||||
|
|
||||||
|
getController(): web_webview.WebviewController {
|
||||||
|
return this.controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBridge(bridge: NativeBridge): void {
|
||||||
|
this.bridge = bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
||||||
|
.javaScriptAccess(true)
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.zoomAccess(true)
|
||||||
|
.onPageEnd(() => {
|
||||||
|
this.onPageEnd?.();
|
||||||
|
})
|
||||||
|
.javaScriptProxy({
|
||||||
|
object: this.bridge,
|
||||||
|
name: 'nativeBridge',
|
||||||
|
methodList: ['onNodeClick', 'onSearch'],
|
||||||
|
asyncMethodList: ['requestGraphData'],
|
||||||
|
controller: this.controller
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NativeBridge — WebView 原生桥接类(移动自 GraphPage)
|
||||||
|
* 负责 ArkTS ↔ WebView JavaScript 双向通信
|
||||||
|
*/
|
||||||
|
export class NativeBridge {
|
||||||
|
private controller: web_webview.WebviewController;
|
||||||
|
private onRequestGraphData: () => void;
|
||||||
|
private onNodeClickCallback: (nodeId: number, nodeName: string) => void;
|
||||||
|
private onSearchCallback: (query: string) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
controller: web_webview.WebviewController,
|
||||||
|
onRequestGraphData: () => void,
|
||||||
|
onNodeClickCallback: (nodeId: number, nodeName: string) => void,
|
||||||
|
onSearchCallback: (query: string) => void
|
||||||
|
) {
|
||||||
|
this.controller = controller;
|
||||||
|
this.onRequestGraphData = onRequestGraphData;
|
||||||
|
this.onNodeClickCallback = onNodeClickCallback;
|
||||||
|
this.onSearchCallback = onSearchCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeClick(nodeId: number, nodeName: string): void {
|
||||||
|
Logger.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
|
||||||
|
if (this.onNodeClickCallback) {
|
||||||
|
this.onNodeClickCallback(nodeId, nodeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(query: string): void {
|
||||||
|
Logger.info('Search from WebView: ' + query);
|
||||||
|
if (this.onSearchCallback) {
|
||||||
|
this.onSearchCallback(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestGraphData(): void {
|
||||||
|
Logger.info('requestGraphData called from WebView');
|
||||||
|
if (this.onRequestGraphData) {
|
||||||
|
this.onRequestGraphData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraphDataService — 图数据查询服务
|
||||||
|
* 封装从 GraphDatabase 读取节点和边的逻辑
|
||||||
|
*/
|
||||||
|
export class GraphDataService {
|
||||||
|
private db: GraphDatabase;
|
||||||
|
|
||||||
|
constructor(db: GraphDatabase) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllNodes(): Promise<GraphNodeItem[]> {
|
||||||
|
const result = await this.db.search('');
|
||||||
|
return result.map((r, idx): GraphNodeItem => {
|
||||||
|
return {
|
||||||
|
id: idx + 1,
|
||||||
|
label: r.name,
|
||||||
|
type: r.type,
|
||||||
|
mentions: r.mentions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllEdges(): Promise<GraphEdgeItem[]> {
|
||||||
|
const recallResult = await this.db.recall('', [], 3);
|
||||||
|
const nameToId: Record<string, number> = {};
|
||||||
|
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
|
||||||
|
nameToId[e.name as string] = idx + 1;
|
||||||
|
});
|
||||||
|
const edgeItems: GraphEdgeItem[] = [];
|
||||||
|
for (let i = 0; i < recallResult.relations.length; i++) {
|
||||||
|
const r = recallResult.relations[i];
|
||||||
|
const sourceId = nameToId[r.source];
|
||||||
|
const targetId = nameToId[r.target];
|
||||||
|
if (sourceId !== undefined && targetId !== undefined) {
|
||||||
|
edgeItems.push({
|
||||||
|
id: i + 1,
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
label: r.type,
|
||||||
|
relation: r.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgeItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= 内部类型定义 =========
|
||||||
|
|
||||||
|
interface GraphNodeItem {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphEdgeItem {
|
||||||
|
id: number;
|
||||||
|
source: number;
|
||||||
|
target: number;
|
||||||
|
label: string;
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
200
features/graph/src/main/ets/pages/GraphPage.ets
Normal file
200
features/graph/src/main/ets/pages/GraphPage.ets
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* GraphPage — 记忆星图页面(重构后)
|
||||||
|
* 使用 WebView 显示 Three.js 3D 图可视化
|
||||||
|
* 子组件:GraphWebView、GraphNodeSearchBar、NodeDetailPanel、GraphDataService、NativeBridge
|
||||||
|
*/
|
||||||
|
import web_webview from '@ohos.web.webview';
|
||||||
|
import {
|
||||||
|
GraphDatabase,
|
||||||
|
NodeDetailInfo,
|
||||||
|
Logger,
|
||||||
|
RecallEntity,
|
||||||
|
GraphMemoryService
|
||||||
|
} from '@ohos/common';
|
||||||
|
import {
|
||||||
|
GraphWebView,
|
||||||
|
GraphNodeSearchBar,
|
||||||
|
NodeDetailPanel,
|
||||||
|
GraphDataService,
|
||||||
|
NativeBridge
|
||||||
|
} from '../components/GraphComponents';
|
||||||
|
|
||||||
|
// ========= GraphPage 组件 =========
|
||||||
|
|
||||||
|
interface GraphNodeItem {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphEdgeItem {
|
||||||
|
id: number;
|
||||||
|
source: number;
|
||||||
|
target: number;
|
||||||
|
label: string;
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct GraphPage {
|
||||||
|
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||||
|
@Prop db: GraphDatabase;
|
||||||
|
@State nodeCount: number = 0;
|
||||||
|
@State edgeCount: number = 0;
|
||||||
|
@State selectedNodeDetail: NodeDetailInfo | null = null;
|
||||||
|
@State showNodeDetail: boolean = false;
|
||||||
|
@State searchText: string = '';
|
||||||
|
private graphService: GraphMemoryService = new GraphMemoryService(this.db);
|
||||||
|
|
||||||
|
// 初始化桥接对象
|
||||||
|
private bridge: NativeBridge = new NativeBridge(
|
||||||
|
this.controller,
|
||||||
|
(): void => { this.pushGraphDataToWebView(); },
|
||||||
|
(nodeId: number, nodeName: string): void => { this.handleNodeClick(nodeId, nodeName); },
|
||||||
|
(query: string): void => { this.handleSearchFromWeb(query); }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部触发刷新图数据(聊天写入新记忆后调用)
|
||||||
|
*/
|
||||||
|
public async refreshGraphData(): Promise<void> {
|
||||||
|
await this.pushGraphDataToWebView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理节点点击 - 查询详细信息并显示浮层
|
||||||
|
*/
|
||||||
|
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const detail: NodeDetailInfo | null = await this.graphService.getNodeDetail(nodeName);
|
||||||
|
if (detail) {
|
||||||
|
this.selectedNodeDetail = detail;
|
||||||
|
this.showNodeDetail = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('handleNodeClick error: ' + JSON.stringify(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理来自 WebView 的搜索请求
|
||||||
|
*/
|
||||||
|
private handleSearchFromWeb(query: string): void {
|
||||||
|
this.searchText = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理搜索输入 - 通知 WebView 过滤
|
||||||
|
*/
|
||||||
|
private onSearchInput(value: string): void {
|
||||||
|
this.searchText = value;
|
||||||
|
const jsCode = `window.dispatchEvent(new MessageEvent('message', { data: { type: 'search_nodes', query: '${value}' } }));`;
|
||||||
|
this.controller.runJavaScript(jsCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭节点详情浮层
|
||||||
|
*/
|
||||||
|
private closeNodeDetail(): void {
|
||||||
|
this.showNodeDetail = false;
|
||||||
|
this.selectedNodeDetail = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库读取全量图数据,推送给 WebView
|
||||||
|
*/
|
||||||
|
private async getAllNodesData(): Promise<GraphNodeItem[]> {
|
||||||
|
const result = await this.db.search('');
|
||||||
|
return result.map((r, idx): GraphNodeItem => {
|
||||||
|
return {
|
||||||
|
id: idx + 1,
|
||||||
|
label: r.name,
|
||||||
|
type: r.type,
|
||||||
|
mentions: r.mentions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
|
||||||
|
const recallResult = await this.db.recall('', [], 3);
|
||||||
|
const nameToId: Record<string, number> = {};
|
||||||
|
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
|
||||||
|
nameToId[e.name as string] = idx + 1;
|
||||||
|
});
|
||||||
|
const edgeItems: GraphEdgeItem[] = [];
|
||||||
|
for (let i = 0; i < recallResult.relations.length; i++) {
|
||||||
|
const r = recallResult.relations[i];
|
||||||
|
const sourceId: number | undefined = nameToId[r.source];
|
||||||
|
const targetId: number | undefined = nameToId[r.target];
|
||||||
|
if (sourceId !== undefined && targetId !== undefined) {
|
||||||
|
edgeItems.push({
|
||||||
|
id: i + 1,
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
label: r.type,
|
||||||
|
relation: r.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgeItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库读取全量图数据,推送给 WebView
|
||||||
|
*/
|
||||||
|
private async pushGraphDataToWebView(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
|
||||||
|
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
|
||||||
|
|
||||||
|
if (this.controller) {
|
||||||
|
const jsCode: string =
|
||||||
|
`window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
|
||||||
|
this.controller.runJavaScript(jsCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeCount = allNodes.length;
|
||||||
|
this.edgeCount = allEdges.length;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
// WebView 显示 3D 星图
|
||||||
|
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
||||||
|
.javaScriptAccess(true)
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.zoomAccess(true)
|
||||||
|
.onPageEnd(() => {
|
||||||
|
this.pushGraphDataToWebView();
|
||||||
|
})
|
||||||
|
.javaScriptProxy({
|
||||||
|
object: this.bridge,
|
||||||
|
name: 'nativeBridge',
|
||||||
|
methodList: ['onNodeClick', 'onSearch'],
|
||||||
|
asyncMethodList: ['requestGraphData'],
|
||||||
|
controller: this.controller
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索框
|
||||||
|
GraphNodeSearchBar({
|
||||||
|
searchText: this.searchText,
|
||||||
|
onSearchInput: (value: string): void => { this.onSearchInput(value); }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 节点详情浮层
|
||||||
|
if (this.showNodeDetail && this.selectedNodeDetail !== null) {
|
||||||
|
NodeDetailPanel({
|
||||||
|
detail: this.selectedNodeDetail,
|
||||||
|
onClose: (): void => { this.closeNodeDetail(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
12
features/graph/src/main/module.json5
Normal file
12
features/graph/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "graph",
|
||||||
|
"type": "har",
|
||||||
|
"description": "TrulyMEM graph feature module",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
827
features/graph/src/main/resources/rawfile/graph.html
Normal file
827
features/graph/src/main/resources/rawfile/graph.html
Normal file
@ -0,0 +1,827 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>记忆星图 - TrulyMEM</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #ffffff; overflow: hidden; width: 100vw; height: 100vh; }
|
||||||
|
#canvas-container { width: 100%; height: 100%; position: relative; }
|
||||||
|
canvas { display: block; }
|
||||||
|
#stats { position: absolute; top: 20px; left: 20px; background: rgba(10, 10, 26, 0.8); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; backdrop-filter: blur(10px); }
|
||||||
|
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
|
||||||
|
#stats p { margin: 4px 0; color: #aaaacc; }
|
||||||
|
#stats span { color: #ffffff; font-weight: bold; }
|
||||||
|
#node-info { position: absolute; top: 20px; right: 20px; background: rgba(10, 10, 26, 0.9); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; display: none; backdrop-filter: blur(10px); max-width: 300px; }
|
||||||
|
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
|
||||||
|
#node-info p { margin: 4px 0; color: #aaaacc; }
|
||||||
|
#node-info .label { color: #8888aa; }
|
||||||
|
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #4488ff; z-index: 200; }
|
||||||
|
#nav { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; }
|
||||||
|
|
||||||
|
/* 搜索框 */
|
||||||
|
#search-box { position: absolute; top: 80px; left: 20px; z-index: 100; }
|
||||||
|
#search-input { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; padding: 8px 12px; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; width: 200px; outline: none; backdrop-filter: blur(10px); }
|
||||||
|
#search-input::placeholder { color: #666688; }
|
||||||
|
|
||||||
|
/* 类型过滤按钮 */
|
||||||
|
#type-filter { position: absolute; top: 120px; left: 20px; display: flex; gap: 8px; z-index: 100; flex-wrap: wrap; }
|
||||||
|
.type-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 12px; backdrop-filter: blur(10px); transition: all 0.3s; }
|
||||||
|
.type-btn.active { background: rgba(68, 136, 255, 0.3); border-color: #4488ff; color: #ffffff; }
|
||||||
|
|
||||||
|
/* 缩放控制按钮 */
|
||||||
|
#zoom-controls { position: absolute; bottom: 100px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
|
||||||
|
.zoom-btn { width: 40px; height: 40px; border-radius: 50%; background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px); font-family: 'Courier New', monospace; }
|
||||||
|
|
||||||
|
/* 边标签 */
|
||||||
|
#edge-label { position: absolute; display: none; background: rgba(10, 10, 26, 0.9); color: #44ff88; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 150; border: 1px solid rgba(68, 255, 136, 0.3); }
|
||||||
|
.nav-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 14px; backdrop-filter: blur(10px); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="canvas-container">
|
||||||
|
<div id="loading">正在加载星图数据...</div>
|
||||||
|
<div id="stats">
|
||||||
|
<h3>🌌 记忆星图</h3>
|
||||||
|
<p>节点: <span id="node-count">0</span></p>
|
||||||
|
<p>边: <span id="edge-count">0</span></p>
|
||||||
|
<p>状态: <span id="status">初始化中...</span></p>
|
||||||
|
</div>
|
||||||
|
<div id="node-info">
|
||||||
|
<h3 id="info-name"></h3>
|
||||||
|
<p><span class="label">类型:</span> <span id="info-type"></span></p>
|
||||||
|
<p><span class="label">提及次数:</span> <span id="info-mentions"></span></p>
|
||||||
|
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
|
||||||
|
</div>
|
||||||
|
<div id="nav">
|
||||||
|
<button class="nav-btn active">🌌 星图</button>
|
||||||
|
</div>
|
||||||
|
<div id="search-box">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索节点...">
|
||||||
|
</div>
|
||||||
|
<div id="type-filter">
|
||||||
|
<button class="type-btn active" data-type="全部">全部</button>
|
||||||
|
<button class="type-btn" data-type="Person">Person</button>
|
||||||
|
<button class="type-btn" data-type="Task">Task</button>
|
||||||
|
<button class="type-btn" data-type="AI">AI</button>
|
||||||
|
<button class="type-btn" data-type="Concept">Concept</button>
|
||||||
|
<button class="type-btn" data-type="Object">Object</button>
|
||||||
|
</div>
|
||||||
|
<div id="zoom-controls">
|
||||||
|
<button class="zoom-btn" id="zoom-in">+</button>
|
||||||
|
<button class="zoom-btn" id="zoom-out">-</button>
|
||||||
|
<button class="zoom-btn" id="zoom-reset">R</button>
|
||||||
|
</div>
|
||||||
|
<div id="edge-label"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let scene, camera, renderer, controls;
|
||||||
|
let nodes = [], edges = [];
|
||||||
|
let nodeMeshes = [], edgeLines = [];
|
||||||
|
let starField, nebulaParticles;
|
||||||
|
let raycaster, mouse;
|
||||||
|
let hoveredNode = null, selectedNode = null;
|
||||||
|
let animationId;
|
||||||
|
let highlightPulse = 0;
|
||||||
|
let searchTerm = '';
|
||||||
|
let activeTypeFilter = '全部';
|
||||||
|
let isDragging = false;
|
||||||
|
let dragNode = null;
|
||||||
|
let originalPhysicsState = true;
|
||||||
|
let edgeLabelEl = null;
|
||||||
|
let nodePositions = {}; // 存储节点位置用于拖拽
|
||||||
|
|
||||||
|
const typeColors = { 'person': 0x4488ff, 'task': 0xff8844, 'ai': 0xaa44ff, 'concept': 0x44ff88, 'object': 0xff4444 };
|
||||||
|
const defaultColor = 0xcccccc;
|
||||||
|
const edgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
|
||||||
|
const defaultEdgeColor = 0x444466;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
|
||||||
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
|
||||||
|
camera.position.set(0, 30, 60);
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
renderer.setClearColor(0x0a0a1a, 1);
|
||||||
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
||||||
|
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.05;
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x444466, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(50, 100, 50);
|
||||||
|
scene.add(directionalLight);
|
||||||
|
raycaster = new THREE.Raycaster();
|
||||||
|
mouse = new THREE.Vector2();
|
||||||
|
createStarField();
|
||||||
|
createNebula();
|
||||||
|
// 使用 ResizeObserver 监听容器尺寸变化(比 window.resize 更准确)
|
||||||
|
initResizeObserver();
|
||||||
|
renderer.domElement.addEventListener('mousemove', onMouseMove);
|
||||||
|
renderer.domElement.addEventListener('click', onMouseClick);
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data.type === 'graph_data') {
|
||||||
|
window.__graphData = event.data.payload;
|
||||||
|
loadGraphData();
|
||||||
|
}
|
||||||
|
if (event.data.type === 'search_nodes') {
|
||||||
|
searchTerm = event.data.query || '';
|
||||||
|
document.getElementById('search-input').value = searchTerm;
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
if (event.data.type === 'node_detail') {
|
||||||
|
showNodeDetailPanel(event.data.detail);
|
||||||
|
}
|
||||||
|
if (event.data.type === 'highlight_node') {
|
||||||
|
highlightNodeById(event.data.nodeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 搜索输入框事件
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
searchTerm = e.target.value.toLowerCase();
|
||||||
|
applyFilters();
|
||||||
|
// 通知 ArkTS
|
||||||
|
try {
|
||||||
|
if (window.nativeBridge && window.nativeBridge.onSearch) {
|
||||||
|
window.nativeBridge.onSearch(searchTerm);
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
// 类型过滤按钮事件
|
||||||
|
document.querySelectorAll('.type-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
activeTypeFilter = btn.dataset.type;
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 缩放控制按钮
|
||||||
|
document.getElementById('zoom-in').addEventListener('click', () => {
|
||||||
|
camera.position.multiplyScalar(0.8);
|
||||||
|
controls.update();
|
||||||
|
});
|
||||||
|
document.getElementById('zoom-out').addEventListener('click', () => {
|
||||||
|
camera.position.multiplyScalar(1.2);
|
||||||
|
controls.update();
|
||||||
|
});
|
||||||
|
document.getElementById('zoom-reset').addEventListener('click', () => {
|
||||||
|
if (Object.keys(nodePositions).length > 0) {
|
||||||
|
const allPositions = Object.values(nodePositions);
|
||||||
|
let maxDist = 0;
|
||||||
|
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
|
||||||
|
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
controls.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 边标签元素
|
||||||
|
edgeLabelEl = document.getElementById('edge-label');
|
||||||
|
// 鼠标移动检测边悬停
|
||||||
|
renderer.domElement.addEventListener('mousemove', onEdgeHoverCheck);
|
||||||
|
// 通知 ArkTS 请求图数据
|
||||||
|
try {
|
||||||
|
if (window.nativeBridge && window.nativeBridge.requestGraphData) {
|
||||||
|
window.nativeBridge.requestGraphData();
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
// 触摸事件支持
|
||||||
|
initTouchEvents();
|
||||||
|
// 初始化拖拽功能
|
||||||
|
initDragFunctionality();
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStarField() {
|
||||||
|
const starCount = 3000;
|
||||||
|
const positions = new Float32Array(starCount * 3);
|
||||||
|
const colors = new Float32Array(starCount * 3);
|
||||||
|
for (let i = 0; i < starCount; i++) {
|
||||||
|
const i3 = i * 3;
|
||||||
|
const radius = 400 + Math.random() * 600;
|
||||||
|
const theta = Math.random() * Math.PI * 2;
|
||||||
|
const phi = Math.acos(2 * Math.random() - 1);
|
||||||
|
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
|
||||||
|
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
||||||
|
positions[i3 + 2] = radius * Math.cos(phi);
|
||||||
|
const colorChoice = Math.random();
|
||||||
|
if (colorChoice < 0.7) {
|
||||||
|
colors[i3] = 0.8 + Math.random() * 0.2;
|
||||||
|
colors[i3 + 1] = 0.8 + Math.random() * 0.2;
|
||||||
|
colors[i3 + 2] = 1.0;
|
||||||
|
} else {
|
||||||
|
colors[i3] = 1.0;
|
||||||
|
colors[i3 + 1] = 0.9 + Math.random() * 0.1;
|
||||||
|
colors[i3 + 2] = 0.8 + Math.random() * 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||||
|
const material = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
|
||||||
|
starField = new THREE.Points(geometry, material);
|
||||||
|
scene.add(starField);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNebula() {
|
||||||
|
const nebulaCount = 500;
|
||||||
|
const positions = new Float32Array(nebulaCount * 3);
|
||||||
|
const colors = new Float32Array(nebulaCount * 3);
|
||||||
|
for (let i = 0; i < nebulaCount; i++) {
|
||||||
|
const i3 = i * 3;
|
||||||
|
positions[i3] = (Math.random() - 0.5) * 800;
|
||||||
|
positions[i3 + 1] = (Math.random() - 0.5) * 800;
|
||||||
|
positions[i3 + 2] = (Math.random() - 0.5) * 800;
|
||||||
|
const colorChoice = Math.random();
|
||||||
|
if (colorChoice < 0.33) {
|
||||||
|
colors[i3] = 0.5 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.7 + Math.random() * 0.3;
|
||||||
|
} else if (colorChoice < 0.66) {
|
||||||
|
colors[i3] = 0.2 + Math.random() * 0.2; colors[i3 + 1] = 0.3 + Math.random() * 0.3; colors[i3 + 2] = 0.8 + Math.random() * 0.2;
|
||||||
|
} else {
|
||||||
|
colors[i3] = 0.7 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.5 + Math.random() * 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||||
|
const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
|
||||||
|
nebulaParticles = new THREE.Points(geometry, material);
|
||||||
|
scene.add(nebulaParticles);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.loadGraphData = function(data) {
|
||||||
|
if (data && data.nodes && data.edges) {
|
||||||
|
nodes = data.nodes.map(n => ({ id: n.id, name: n.label || n.name, type: n.type, mention_count: n.mentions || 1 }));
|
||||||
|
edges = data.edges.map(e => ({ id: e.id, source: e.from || e.source, target: e.to || e.target, relation_type: e.label || e.relation }));
|
||||||
|
document.getElementById('node-count').textContent = nodes.length;
|
||||||
|
document.getElementById('edge-count').textContent = edges.length;
|
||||||
|
document.getElementById('status').textContent = '就绪';
|
||||||
|
createGraphVisualization();
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGraphVisualization() {
|
||||||
|
nodeMeshes.forEach(mesh => scene.remove(mesh));
|
||||||
|
edgeLines.forEach(line => scene.remove(line));
|
||||||
|
nodeMeshes = [];
|
||||||
|
edgeLines = [];
|
||||||
|
if (nodes.length === 0) return;
|
||||||
|
const nodeDegrees = {};
|
||||||
|
nodes.forEach(n => nodeDegrees[n.id] = 0);
|
||||||
|
edges.forEach(e => {
|
||||||
|
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
|
||||||
|
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
|
||||||
|
});
|
||||||
|
const positions = {};
|
||||||
|
nodePositions = positions; // 存储供拖拽使用
|
||||||
|
const maxDegree = Math.max(...Object.values(nodeDegrees), 1);
|
||||||
|
nodes.forEach((node, i) => {
|
||||||
|
const angle = (i / nodes.length) * Math.PI * 2;
|
||||||
|
const radius = 15 + (nodeDegrees[node.id] / maxDegree) * 35;
|
||||||
|
positions[node.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * 10, z: radius * Math.sin(angle) };
|
||||||
|
});
|
||||||
|
for (let iter = 0; iter < 200; iter++) {
|
||||||
|
Object.keys(positions).forEach(id1 => {
|
||||||
|
Object.keys(positions).forEach(id2 => {
|
||||||
|
if (id1 >= id2) return;
|
||||||
|
const pos1 = positions[id1], pos2 = positions[id2];
|
||||||
|
const dx = pos1.x - pos2.x, dy = pos1.y - pos2.y, dz = pos1.z - pos2.z;
|
||||||
|
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
|
||||||
|
if (dist < 20) { // 增加排斥距离
|
||||||
|
const force = 0.3 / (dist * dist); // 增加排斥力系数(平方反比)
|
||||||
|
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
|
||||||
|
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
edges.forEach(edge => {
|
||||||
|
const pos1 = positions[edge.source], pos2 = positions[edge.target];
|
||||||
|
if (!pos1 || !pos2) return;
|
||||||
|
const dx = pos2.x - pos1.x, dy = pos2.y - pos1.y, dz = pos2.z - pos1.z;
|
||||||
|
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
|
||||||
|
if (dist > 15) {
|
||||||
|
const force = 0.08; // 增加吸引力
|
||||||
|
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
|
||||||
|
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const pos = positions[node.id];
|
||||||
|
if (!pos) return;
|
||||||
|
const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5);
|
||||||
|
const color = typeColors[node.type] || defaultColor;
|
||||||
|
const geometry = new THREE.SphereGeometry(radius, 16, 12);
|
||||||
|
const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 + Math.min(node.mention_count * 0.05, 0.3), shininess: 30, transparent: true, opacity: 0 });
|
||||||
|
const sphere = new THREE.Mesh(geometry, material);
|
||||||
|
sphere.position.set(pos.x, pos.y, pos.z);
|
||||||
|
sphere.userData = { nodeId: node.id, nodeData: node };
|
||||||
|
scene.add(sphere);
|
||||||
|
nodeMeshes.push(sphere);
|
||||||
|
// 淡入动画
|
||||||
|
fadeInObject(sphere, 500);
|
||||||
|
});
|
||||||
|
edges.forEach(edge => {
|
||||||
|
const pos1 = positions[edge.source], pos2 = positions[edge.target];
|
||||||
|
if (!pos1 || !pos2) return;
|
||||||
|
const color = edgeColors[edge.relation_type] || defaultEdgeColor;
|
||||||
|
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(pos1.x, pos1.y, pos1.z), new THREE.Vector3(pos2.x, pos2.y, pos2.z)]);
|
||||||
|
const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0, linewidth: 1 });
|
||||||
|
const line = new THREE.Line(geometry, material);
|
||||||
|
line.userData = { edgeId: edge.id, edgeData: edge };
|
||||||
|
scene.add(line);
|
||||||
|
edgeLines.push(line);
|
||||||
|
// 淡入动画
|
||||||
|
fadeInLine(line, 500);
|
||||||
|
});
|
||||||
|
const allPositions = Object.values(positions);
|
||||||
|
if (allPositions.length > 0) {
|
||||||
|
let maxDist = 0;
|
||||||
|
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
|
||||||
|
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(nodeMeshes);
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const node = intersects[0].object;
|
||||||
|
if (hoveredNode !== node) {
|
||||||
|
if (hoveredNode) hoveredNode.scale.set(1, 1, 1);
|
||||||
|
hoveredNode = node;
|
||||||
|
node.scale.set(1.2, 1.2, 1.2);
|
||||||
|
showNodeInfo(node.userData.nodeData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hoveredNode) { hoveredNode.scale.set(1, 1, 1); hoveredNode = null; hideNodeInfo(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseClick(event) {
|
||||||
|
if (isDragging) return;
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(nodeMeshes);
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const node = intersects[0].object;
|
||||||
|
if (selectedNode === node) {
|
||||||
|
selectedNode = null;
|
||||||
|
document.getElementById('node-info').style.display = 'none';
|
||||||
|
resetHighlight();
|
||||||
|
} else {
|
||||||
|
selectedNode = node;
|
||||||
|
showNodeInfo(node.userData.nodeData, true);
|
||||||
|
highlightNodeConnections(node.userData.nodeData);
|
||||||
|
// 通知 ArkTS 节点被点击
|
||||||
|
try {
|
||||||
|
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
|
||||||
|
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 点击空白处恢复
|
||||||
|
selectedNode = null;
|
||||||
|
document.getElementById('node-info').style.display = 'none';
|
||||||
|
resetHighlight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNodeInfo(nodeData, isClick = false) {
|
||||||
|
document.getElementById('info-name').textContent = nodeData.name;
|
||||||
|
document.getElementById('info-type').textContent = nodeData.type;
|
||||||
|
document.getElementById('info-mentions').textContent = nodeData.mention_count;
|
||||||
|
const linkCount = edges.filter(e => e.source === nodeData.id || e.target === nodeData.id).length;
|
||||||
|
document.getElementById('info-links').textContent = linkCount;
|
||||||
|
if (isClick) document.getElementById('node-info').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNodeInfo() { if (!selectedNode) document.getElementById('node-info').style.display = 'none'; }
|
||||||
|
|
||||||
|
// 淡入动画
|
||||||
|
function fadeInObject(obj, duration) {
|
||||||
|
const startOpacity = 0;
|
||||||
|
const endOpacity = obj.material.opacity !== undefined ? (obj.material.transparent ? obj.material.opacity : 1) : 1;
|
||||||
|
obj.material.opacity = startOpacity;
|
||||||
|
obj.material.transparent = true;
|
||||||
|
const startTime = Date.now();
|
||||||
|
function animateFade() {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
obj.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateFade);
|
||||||
|
} else {
|
||||||
|
obj.material.transparent = endOpacity < 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateFade();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fadeInLine(line, duration) {
|
||||||
|
const startOpacity = 0;
|
||||||
|
const endOpacity = 0.4;
|
||||||
|
line.material.opacity = startOpacity;
|
||||||
|
const startTime = Date.now();
|
||||||
|
function animateFade() {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
line.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateFade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateFade();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过节点ID高亮节点(供ArkTS调用)
|
||||||
|
function highlightNodeById(nodeId) {
|
||||||
|
const mesh = nodeMeshes.find(m => m.userData.nodeData.id === nodeId);
|
||||||
|
if (mesh) {
|
||||||
|
selectedNode = mesh;
|
||||||
|
showNodeInfo(mesh.userData.nodeData, true);
|
||||||
|
highlightNodeConnections(mesh.userData.nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示节点详情面板(供ArkTS调用)
|
||||||
|
function showNodeDetailPanel(detail) {
|
||||||
|
// 更新node-info面板显示详细信息
|
||||||
|
document.getElementById('info-name').textContent = detail.name;
|
||||||
|
document.getElementById('info-type').textContent = detail.type;
|
||||||
|
document.getElementById('info-mentions').textContent = detail.mention_count;
|
||||||
|
document.getElementById('info-links').textContent = detail.connection_count;
|
||||||
|
document.getElementById('node-info').style.display = 'block';
|
||||||
|
|
||||||
|
// 如果有连接信息,添加到面板
|
||||||
|
let detailHtml = `<h3>${detail.name}</h3>`;
|
||||||
|
detailHtml += `<p><span class="label">类型:</span> ${detail.type}</p>`;
|
||||||
|
detailHtml += `<p><span class="label">提及次数:</span> ${detail.mention_count}</p>`;
|
||||||
|
detailHtml += `<p><span class="label">连接数:</span> ${detail.connection_count}</p>`;
|
||||||
|
if (detail.connections && detail.connections.length > 0) {
|
||||||
|
detailHtml += `<p><span class="label">连接关系:</span></p><ul style="margin-left: 15px; font-size: 12px;">`;
|
||||||
|
detail.connections.forEach(conn => {
|
||||||
|
detailHtml += `<li>${conn.type}: ${conn.target_name}</li>`;
|
||||||
|
});
|
||||||
|
detailHtml += `</ul>`;
|
||||||
|
}
|
||||||
|
document.getElementById('node-info').innerHTML = detailHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点拖拽功能
|
||||||
|
function initDragFunctionality() {
|
||||||
|
let dragStartPos = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mousedown', (event) => {
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(nodeMeshes);
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
isDragging = false;
|
||||||
|
dragNode = intersects[0].object;
|
||||||
|
dragStartPos = { x: event.clientX, y: event.clientY };
|
||||||
|
originalPhysicsState = true; // 暂停物理模拟
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mousemove', (event) => {
|
||||||
|
if (dragNode) {
|
||||||
|
const dx = event.clientX - dragStartPos.x;
|
||||||
|
const dy = event.clientY - dragStartPos.y;
|
||||||
|
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
|
||||||
|
isDragging = true;
|
||||||
|
}
|
||||||
|
if (isDragging) {
|
||||||
|
// 将屏幕坐标转换为3D空间
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
const vector = new THREE.Vector3(mouseX, mouseY, 0.5);
|
||||||
|
vector.unproject(camera);
|
||||||
|
const dir = vector.sub(camera.position).normalize();
|
||||||
|
const distance = -camera.position.z / dir.z;
|
||||||
|
const newPos = camera.position.clone().add(dir.multiplyScalar(distance));
|
||||||
|
dragNode.position.copy(newPos);
|
||||||
|
// 更新存储的位置
|
||||||
|
if (nodePositions[dragNode.userData.nodeId]) {
|
||||||
|
nodePositions[dragNode.userData.nodeId] = { x: newPos.x, y: newPos.y, z: newPos.z };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mouseup', () => {
|
||||||
|
if (dragNode) {
|
||||||
|
// 恢复物理模拟
|
||||||
|
dragNode = null;
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch事件支持
|
||||||
|
function initTouchEvents() {
|
||||||
|
let touchStart = null;
|
||||||
|
let touchStartDistance = 0;
|
||||||
|
let touchStartPos = { x: 0, y: 0 };
|
||||||
|
let isTouchDrag = false;
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('touchstart', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.touches.length === 1) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
||||||
|
isTouchDrag = false;
|
||||||
|
|
||||||
|
// 模拟鼠标事件用于射线检测
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(nodeMeshes);
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const node = intersects[0].object;
|
||||||
|
if (selectedNode === node) {
|
||||||
|
selectedNode = null;
|
||||||
|
document.getElementById('node-info').style.display = 'none';
|
||||||
|
resetHighlight();
|
||||||
|
} else {
|
||||||
|
selectedNode = node;
|
||||||
|
showNodeInfo(node.userData.nodeData, true);
|
||||||
|
highlightNodeConnections(node.userData.nodeData);
|
||||||
|
try {
|
||||||
|
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
|
||||||
|
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.touches.length === 2) {
|
||||||
|
// 双指缩放
|
||||||
|
const dx = event.touches[0].clientX - event.touches[1].clientX;
|
||||||
|
const dy = event.touches[0].clientY - event.touches[1].clientY;
|
||||||
|
touchStartDistance = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('touchmove', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.touches.length === 1 && controls) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const dx = touch.clientX - touchStartPos.x;
|
||||||
|
const dy = touch.clientY - touchStartPos.y;
|
||||||
|
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||||
|
isTouchDrag = true;
|
||||||
|
}
|
||||||
|
// 模拟OrbitControls的鼠标移动
|
||||||
|
if (isTouchDrag) {
|
||||||
|
const rotateSpeed = 0.005;
|
||||||
|
controls.rotateLeft(-dx * rotateSpeed);
|
||||||
|
controls.rotateUp(-dy * rotateSpeed);
|
||||||
|
controls.update();
|
||||||
|
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
||||||
|
}
|
||||||
|
} else if (event.touches.length === 2 && controls) {
|
||||||
|
// 双指缩放
|
||||||
|
const dx = event.touches[0].clientX - event.touches[1].clientX;
|
||||||
|
const dy = event.touches[0].clientY - event.touches[1].clientY;
|
||||||
|
const distance = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
const scale = touchStartDistance / distance;
|
||||||
|
camera.position.multiplyScalar(scale);
|
||||||
|
controls.update();
|
||||||
|
touchStartDistance = distance;
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('touchend', (event) => {
|
||||||
|
if (event.touches.length === 0) {
|
||||||
|
isTouchDrag = false;
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索和类型过滤
|
||||||
|
function applyFilters() {
|
||||||
|
nodeMeshes.forEach(mesh => {
|
||||||
|
const nodeData = mesh.userData.nodeData;
|
||||||
|
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
|
||||||
|
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
|
||||||
|
|
||||||
|
if (nameMatch && typeMatch) {
|
||||||
|
mesh.material.transparent = false;
|
||||||
|
mesh.material.opacity = 1;
|
||||||
|
mesh.scale.setScalar(1);
|
||||||
|
} else {
|
||||||
|
mesh.material.transparent = true;
|
||||||
|
mesh.material.opacity = 0.2;
|
||||||
|
mesh.scale.setScalar(0.8);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 同时过滤边
|
||||||
|
edgeLines.forEach(line => {
|
||||||
|
const edgeData = line.userData.edgeData;
|
||||||
|
const sourceNode = nodes.find(n => n.id === edgeData.source);
|
||||||
|
const targetNode = nodes.find(n => n.id === edgeData.target);
|
||||||
|
const sourceMatch = sourceNode && sourceNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || sourceNode.type === activeTypeFilter);
|
||||||
|
const targetMatch = targetNode && targetNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || targetNode.type === activeTypeFilter);
|
||||||
|
|
||||||
|
if ((sourceMatch || targetMatch) && searchTerm === '' && activeTypeFilter === '全部') {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.4;
|
||||||
|
} else if (sourceMatch || targetMatch) {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.6;
|
||||||
|
} else {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接高亮
|
||||||
|
function highlightNodeConnections(nodeData) {
|
||||||
|
const connectedNodeIds = new Set();
|
||||||
|
const connectedEdgeIds = new Set();
|
||||||
|
|
||||||
|
// 找出所有连接的节点和边
|
||||||
|
edges.forEach(edge => {
|
||||||
|
if (edge.source === nodeData.id || edge.target === nodeData.id) {
|
||||||
|
connectedNodeIds.add(edge.source);
|
||||||
|
connectedNodeIds.add(edge.target);
|
||||||
|
connectedEdgeIds.add(edge.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeMeshes.forEach(mesh => {
|
||||||
|
const meshNodeId = mesh.userData.nodeData.id;
|
||||||
|
if (meshNodeId === nodeData.id) {
|
||||||
|
// 选中的节点
|
||||||
|
mesh.material.emissiveIntensity = 1.0;
|
||||||
|
mesh.scale.setScalar(1.5);
|
||||||
|
} else if (connectedNodeIds.has(meshNodeId)) {
|
||||||
|
// 直接连接的节点
|
||||||
|
mesh.material.emissiveIntensity = 0.8;
|
||||||
|
mesh.scale.setScalar(1.3);
|
||||||
|
mesh.material.transparent = false;
|
||||||
|
mesh.material.opacity = 1;
|
||||||
|
} else {
|
||||||
|
// 无关系的节点
|
||||||
|
mesh.material.transparent = true;
|
||||||
|
mesh.material.opacity = 0.15;
|
||||||
|
mesh.material.emissiveIntensity = 0.2;
|
||||||
|
mesh.scale.setScalar(0.9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
edgeLines.forEach(line => {
|
||||||
|
if (connectedEdgeIds.has(line.userData.edgeData.id)) {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.8;
|
||||||
|
line.material.linewidth = 2;
|
||||||
|
} else {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.1;
|
||||||
|
line.material.linewidth = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHighlight() {
|
||||||
|
nodeMeshes.forEach(mesh => {
|
||||||
|
const nodeData = mesh.userData.nodeData;
|
||||||
|
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
|
||||||
|
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
|
||||||
|
|
||||||
|
mesh.material.emissiveIntensity = 0.5 + Math.min(nodeData.mention_count * 0.05, 0.3);
|
||||||
|
if (nameMatch && typeMatch) {
|
||||||
|
mesh.material.transparent = false;
|
||||||
|
mesh.material.opacity = 1;
|
||||||
|
mesh.scale.setScalar(1);
|
||||||
|
} else {
|
||||||
|
mesh.material.transparent = true;
|
||||||
|
mesh.material.opacity = 0.2;
|
||||||
|
mesh.scale.setScalar(0.8);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edgeLines.forEach(line => {
|
||||||
|
line.material.transparent = true;
|
||||||
|
line.material.opacity = 0.4;
|
||||||
|
line.material.linewidth = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 边标签显示
|
||||||
|
function onEdgeHoverCheck(event) {
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
|
||||||
|
// 检查边悬停
|
||||||
|
const edgeIntersects = raycaster.intersectObjects(edgeLines);
|
||||||
|
if (edgeIntersects.length > 0) {
|
||||||
|
const edge = edgeIntersects[0].object;
|
||||||
|
const edgeData = edge.userData.edgeData;
|
||||||
|
edgeLabelEl.textContent = edgeData.relation_type || '关系';
|
||||||
|
edgeLabelEl.style.display = 'block';
|
||||||
|
edgeLabelEl.style.left = (event.clientX - rect.left + 10) + 'px';
|
||||||
|
edgeLabelEl.style.top = (event.clientY - rect.top - 10) + 'px';
|
||||||
|
} else {
|
||||||
|
edgeLabelEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= ResizeObserver 响应式适配 =========
|
||||||
|
let containerObserver = null;
|
||||||
|
function initResizeObserver() {
|
||||||
|
const container = document.getElementById('canvas-container');
|
||||||
|
if (!container) return;
|
||||||
|
containerObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
onContainerResize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
containerObserver.observe(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContainerResize(width, height) {
|
||||||
|
if (!camera || !renderer) return;
|
||||||
|
const aspect = width / height;
|
||||||
|
camera.aspect = aspect;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
|
||||||
|
// 窄屏(<500px)时自动调整UI元素尺寸和位置
|
||||||
|
const isNarrow = width < 500;
|
||||||
|
const stats = document.getElementById('stats');
|
||||||
|
const nodeInfo = document.getElementById('node-info');
|
||||||
|
const searchBox = document.getElementById('search-box');
|
||||||
|
const typeFilter = document.getElementById('type-filter');
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
|
||||||
|
if (isNarrow) {
|
||||||
|
if (stats) {
|
||||||
|
stats.style.fontSize = '11px';
|
||||||
|
stats.style.padding = '8px 12px';
|
||||||
|
stats.style.top = '6px';
|
||||||
|
stats.style.left = '6px';
|
||||||
|
}
|
||||||
|
if (nodeInfo) {
|
||||||
|
nodeInfo.style.fontSize = '11px';
|
||||||
|
nodeInfo.style.padding = '8px 12px';
|
||||||
|
nodeInfo.style.maxWidth = '180px';
|
||||||
|
nodeInfo.style.top = '6px';
|
||||||
|
nodeInfo.style.right = '6px';
|
||||||
|
}
|
||||||
|
if (searchBox) { searchBox.style.top = '70px'; searchBox.style.left = '6px'; }
|
||||||
|
if (searchInput) { searchInput.style.width = '140px'; searchInput.style.fontSize = '12px'; }
|
||||||
|
if (typeFilter) { typeFilter.style.top = '108px'; typeFilter.style.left = '6px'; }
|
||||||
|
} else {
|
||||||
|
if (stats) {
|
||||||
|
stats.style.fontSize = '14px';
|
||||||
|
stats.style.padding = '15px 20px';
|
||||||
|
stats.style.top = '20px';
|
||||||
|
stats.style.left = '20px';
|
||||||
|
}
|
||||||
|
if (nodeInfo) {
|
||||||
|
nodeInfo.style.fontSize = '14px';
|
||||||
|
nodeInfo.style.padding = '15px 20px';
|
||||||
|
nodeInfo.style.maxWidth = '300px';
|
||||||
|
nodeInfo.style.top = '20px';
|
||||||
|
nodeInfo.style.right = '20px';
|
||||||
|
}
|
||||||
|
if (searchBox) { searchBox.style.top = '80px'; searchBox.style.left = '20px'; }
|
||||||
|
if (searchInput) { searchInput.style.width = '200px'; searchInput.style.fontSize = '14px'; }
|
||||||
|
if (typeFilter) { typeFilter.style.top = '120px'; typeFilter.style.left = '20px'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
animationId = requestAnimationFrame(animate);
|
||||||
|
const time = Date.now() * 0.001;
|
||||||
|
highlightPulse = (highlightPulse + 0.02) % (Math.PI * 2);
|
||||||
|
controls.update();
|
||||||
|
if (starField) starField.rotation.y += 0.0001;
|
||||||
|
if (hoveredNode) { const pulse = 1 + Math.sin(highlightPulse * 3) * 0.05; hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2); }
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
features/settings/BuildProfile.ets
Normal file
17
features/settings/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '1.0.0';
|
||||||
|
export const BUILD_MODE_NAME = 'debug';
|
||||||
|
export const DEBUG = true;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
}
|
||||||
1
features/settings/Index.ets
Normal file
1
features/settings/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { SettingsPage } from './src/main/ets/pages/SettingsPage';
|
||||||
10
features/settings/build-profile.json5
Normal file
10
features/settings/build-profile.json5
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
6
features/settings/hvigorfile.ts
Normal file
6
features/settings/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
19
features/settings/oh-package-lock.json5
Normal file
19
features/settings/oh-package-lock.json5
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"stableOrder": true,
|
||||||
|
"enableUnifiedLockfile": false
|
||||||
|
},
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
|
"specifiers": {
|
||||||
|
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ohos/common@../../common": {
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "../../common",
|
||||||
|
"registryType": "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
features/settings/oh-package.json5
Normal file
11
features/settings/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/settings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM settings feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/settings/oh_modules/@ohos/common
Symbolic link
1
features/settings/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../common
|
||||||
1
features/settings/settings
Symbolic link
1
features/settings/settings
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/home/program/TrulyMEM-TrueHumanMEM/features/settings
|
||||||
105
features/settings/src/main/ets/components/SettingsComponents.ets
Normal file
105
features/settings/src/main/ets/components/SettingsComponents.ets
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SettingsSectionHeader — 设置区块标题
|
||||||
|
* 大标题 + 加粗白色
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct SettingsSectionHeader {
|
||||||
|
@Prop title: string;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Text(this.title)
|
||||||
|
.fontSize(24)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.margin({ top: 20, bottom: 16 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SettingInputItem — 设置输入项
|
||||||
|
* 标签 + TextInput,统一玻璃拟态风格
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct SettingInputItem {
|
||||||
|
@Prop label: string;
|
||||||
|
@Prop placeholder: string;
|
||||||
|
@Link value: string;
|
||||||
|
isPassword?: boolean = false;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
Text(this.label)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.width('100%')
|
||||||
|
.margin({ bottom: 8 })
|
||||||
|
|
||||||
|
TextInput({ placeholder: this.placeholder, text: this.value })
|
||||||
|
.type(this.isPassword ? InputType.Password : InputType.Normal)
|
||||||
|
.onChange((v: string) => {
|
||||||
|
this.value = v;
|
||||||
|
this.onValueChange?.(v);
|
||||||
|
})
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
.height(40)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ bottom: 12 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlowBackground — 主题色光晕背景装饰
|
||||||
|
* 用于设置页顶部装饰
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
export struct GlowBackground {
|
||||||
|
@Prop color: string = 'rgba(124,77,255,0.15)';
|
||||||
|
@Prop glowSize: number = 200;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column()
|
||||||
|
.width(this.glowSize)
|
||||||
|
.height(this.glowSize)
|
||||||
|
.backgroundColor(this.color)
|
||||||
|
.blur(40)
|
||||||
|
.borderRadius(this.glowSize / 2)
|
||||||
|
.position({ x: '10%', y: '20%' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppConfigStore — 应用配置存储封装
|
||||||
|
* 封装 Preferences 读写,提供类型安全访问
|
||||||
|
*/
|
||||||
|
export class AppConfigStore {
|
||||||
|
private pref?: dataPreferences.Preferences;
|
||||||
|
private readonly storeName: string = 'trulymem_config';
|
||||||
|
|
||||||
|
async init(ctx: Context): Promise<void> {
|
||||||
|
this.pref = await dataPreferences.getPreferences(ctx, this.storeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getString(key: string, defaultValue: string): Promise<string> {
|
||||||
|
return String(await this.pref?.get(key, defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setString(key: string, value: string): Promise<void> {
|
||||||
|
await this.pref?.put(key, value);
|
||||||
|
await this.pref?.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(ctx: Context): Promise<AppConfigStore> {
|
||||||
|
const store = new AppConfigStore();
|
||||||
|
await store.init(ctx);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
features/settings/src/main/ets/pages/SettingsPage.ets
Normal file
60
features/settings/src/main/ets/pages/SettingsPage.ets
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
|
import { SettingsSectionHeader, SettingInputItem, GlowBackground, AppConfigStore } from '../components/SettingsComponents';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct SettingsPage {
|
||||||
|
@State baseUrl: string = '';
|
||||||
|
@State model: string = '';
|
||||||
|
@State apiKey: string = '';
|
||||||
|
private store: AppConfigStore = new AppConfigStore();
|
||||||
|
|
||||||
|
async aboutToAppear() {
|
||||||
|
const ctx = getContext(this);
|
||||||
|
await this.store.init(ctx);
|
||||||
|
this.baseUrl = await this.store.getString('base_url', 'https://api.deepseek.com');
|
||||||
|
this.model = await this.store.getString('model', 'deepseek-chat');
|
||||||
|
this.apiKey = await this.store.getString('api_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveConfig(key: string, value: string): Promise<void> {
|
||||||
|
await this.store.setString(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
GlowBackground()
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
SettingsSectionHeader({ title: 'API 配置' })
|
||||||
|
|
||||||
|
SettingInputItem({
|
||||||
|
label: 'Base URL',
|
||||||
|
placeholder: 'https://api.deepseek.com',
|
||||||
|
value: this.baseUrl,
|
||||||
|
onValueChange: (v: string): void => { this.saveConfig('base_url', v); }
|
||||||
|
})
|
||||||
|
|
||||||
|
SettingInputItem({
|
||||||
|
label: 'Model ID',
|
||||||
|
placeholder: 'deepseek-chat',
|
||||||
|
value: this.model,
|
||||||
|
onValueChange: (v: string): void => { this.saveConfig('model', v); }
|
||||||
|
})
|
||||||
|
|
||||||
|
SettingInputItem({
|
||||||
|
label: 'API Key',
|
||||||
|
placeholder: 'sk-...',
|
||||||
|
value: this.apiKey,
|
||||||
|
isPassword: true,
|
||||||
|
onValueChange: (v: string): void => { this.saveConfig('api_key', v); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('rgba(26,27,46,0.95)')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
features/settings/src/main/module.json5
Normal file
12
features/settings/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "settings",
|
||||||
|
"type": "har",
|
||||||
|
"description": "TrulyMEM settings feature module",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
221
full_output.log
Normal file
221
full_output.log
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
Reading package lists...
|
||||||
|
Building dependency tree...
|
||||||
|
Reading state information...
|
||||||
|
python3 is already the newest version (3.13.9-3).
|
||||||
|
python3 set to manually installed.
|
||||||
|
python3-pip is already the newest version (26.0.1+dfsg-1).
|
||||||
|
You might want to run 'apt --fix-broken install' to correct these.
|
||||||
|
The following packages have unmet dependencies:
|
||||||
|
libxfont2 : Depends: libfontenc1 (>= 1:1.1.8) but 1:1.1.4-1 is to be installed
|
||||||
|
python3-venv : Depends: python3.13-venv (>= 3.13.5-1~) but it is not going to be installed
|
||||||
|
Depends: python3 (= 3.13.5-1) but 3.13.9-3 is to be installed
|
||||||
|
xserver-common : Depends: x11-xkb-utils but it is not going to be installed
|
||||||
|
Recommends: xfonts-base but it is not going to be installed
|
||||||
|
E: Unmet dependencies. Try 'apt --fix-broken install' with no packages (or specify a solution).
|
||||||
|
|
||||||
|
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
|
||||||
|
|
||||||
|
Reading package lists...
|
||||||
|
Building dependency tree...
|
||||||
|
Reading state information...
|
||||||
|
Correcting dependencies... Done
|
||||||
|
Solving dependencies...
|
||||||
|
Upgrading:
|
||||||
|
libfontenc1 libxt-dev
|
||||||
|
|
||||||
|
Installing dependencies:
|
||||||
|
libxt6t64 x11-xkb-utils
|
||||||
|
|
||||||
|
REMOVING:
|
||||||
|
libxt6
|
||||||
|
|
||||||
|
apt-listchanges: Reading changelogs...
|
||||||
|
dpkg-preconfigure: unable to re-open stdin: No such file or directory
|
||||||
|
Summary:
|
||||||
|
Upgrading: 2, Installing: 2, Removing: 1, Not Upgrading: 708
|
||||||
|
Download size: 0 B / 778 kB
|
||||||
|
Space needed: 526 kB / 419 GB available
|
||||||
|
|
||||||
|
(Reading database…
|
||||||
|
(Reading database… 5%
|
||||||
|
(Reading database… 10%
|
||||||
|
(Reading database… 15%
|
||||||
|
(Reading database… 20%
|
||||||
|
(Reading database… 25%
|
||||||
|
(Reading database… 30%
|
||||||
|
(Reading database… 35%
|
||||||
|
(Reading database… 40%
|
||||||
|
(Reading database… 45%
|
||||||
|
(Reading database… 50%
|
||||||
|
(Reading database… 55%
|
||||||
|
(Reading database… 60%
|
||||||
|
(Reading database… 65%
|
||||||
|
(Reading database… 70%
|
||||||
|
(Reading database… 75%
|
||||||
|
(Reading database… 80%
|
||||||
|
(Reading database… 85%
|
||||||
|
(Reading database… 90%
|
||||||
|
(Reading database… 95%
|
||||||
|
(Reading database… 100%
|
||||||
|
(Reading database… 103500 files and directories currently installed.)
|
||||||
|
Preparing to unpack …/libxt-dev_1%3a1.2.1-1.2+b2_amd64.deb…
|
||||||
|
Unpacking libxt-dev:amd64 (1:1.2.1-1.2+b2) over (1:1.2.1-1.1)…
|
||||||
|
dpkg: libxt6:amd64: dependency problems, but removing anyway as you requested:
|
||||||
|
x11-xserver-utils depends on libxt6.
|
||||||
|
x11-utils depends on libxt6 (>= 1:1.1.0).
|
||||||
|
libxmu6:amd64 depends on libxt6.
|
||||||
|
libxaw7:amd64 depends on libxt6.
|
||||||
|
libgs10:amd64 depends on libxt6.
|
||||||
|
|
||||||
|
(Reading database…
|
||||||
|
(Reading database… 5%
|
||||||
|
(Reading database… 10%
|
||||||
|
(Reading database… 15%
|
||||||
|
(Reading database… 20%
|
||||||
|
(Reading database… 25%
|
||||||
|
(Reading database… 30%
|
||||||
|
(Reading database… 35%
|
||||||
|
(Reading database… 40%
|
||||||
|
(Reading database… 45%
|
||||||
|
(Reading database… 50%
|
||||||
|
(Reading database… 55%
|
||||||
|
(Reading database… 60%
|
||||||
|
(Reading database… 65%
|
||||||
|
(Reading database… 70%
|
||||||
|
(Reading database… 75%
|
||||||
|
(Reading database… 80%
|
||||||
|
(Reading database… 85%
|
||||||
|
(Reading database… 90%
|
||||||
|
(Reading database… 95%
|
||||||
|
(Reading database… 100%
|
||||||
|
(Reading database… 103501 files and directories currently installed.)
|
||||||
|
Removing libxt6:amd64 (1:1.2.1-1.1)…
|
||||||
|
Selecting previously unselected package libxt6t64:amd64.
|
||||||
|
(Reading database…
|
||||||
|
(Reading database… 5%
|
||||||
|
(Reading database… 10%
|
||||||
|
(Reading database… 15%
|
||||||
|
(Reading database… 20%
|
||||||
|
(Reading database… 25%
|
||||||
|
(Reading database… 30%
|
||||||
|
(Reading database… 35%
|
||||||
|
(Reading database… 40%
|
||||||
|
(Reading database… 45%
|
||||||
|
(Reading database… 50%
|
||||||
|
(Reading database… 55%
|
||||||
|
(Reading database… 60%
|
||||||
|
(Reading database… 65%
|
||||||
|
(Reading database… 70%
|
||||||
|
(Reading database… 75%
|
||||||
|
(Reading database… 80%
|
||||||
|
(Reading database… 85%
|
||||||
|
(Reading database… 90%
|
||||||
|
(Reading database… 95%
|
||||||
|
(Reading database… 100%
|
||||||
|
(Reading database… 103495 files and directories currently installed.)
|
||||||
|
Preparing to unpack …/libxt6t64_1%3a1.2.1-1.2+b2_amd64.deb…
|
||||||
|
Unpacking libxt6t64:amd64 (1:1.2.1-1.2+b2)…
|
||||||
|
Preparing to unpack …/libfontenc1_1%3a1.1.8-1+b2_amd64.deb…
|
||||||
|
Unpacking libfontenc1:amd64 (1:1.1.8-1+b2) over (1:1.1.4-1)…
|
||||||
|
Selecting previously unselected package x11-xkb-utils.
|
||||||
|
Preparing to unpack …/x11-xkb-utils_7.7+9_amd64.deb…
|
||||||
|
Unpacking x11-xkb-utils (7.7+9)…
|
||||||
|
Setting up libfontenc1:amd64 (1:1.1.8-1+b2)…
|
||||||
|
Setting up libxt6t64:amd64 (1:1.2.1-1.2+b2)…
|
||||||
|
Setting up x11-xkb-utils (7.7+9)…
|
||||||
|
Setting up libxt-dev:amd64 (1:1.2.1-1.2+b2)…
|
||||||
|
Processing triggers for man-db (2.11.2-2)…
|
||||||
|
Processing triggers for libc-bin (2.42-14)…
|
||||||
|
needrestart is being skipped since dpkg has failed
|
||||||
|
Reading package lists...
|
||||||
|
Building dependency tree...
|
||||||
|
Reading state information...
|
||||||
|
Solving dependencies...
|
||||||
|
Some packages could not be installed. This may mean that you have
|
||||||
|
requested an impossible situation or if you are using the unstable
|
||||||
|
distribution that some required packages have not yet been created
|
||||||
|
or been moved out of Incoming.
|
||||||
|
The following information may help to resolve the situation:
|
||||||
|
|
||||||
|
The following packages have unmet dependencies:
|
||||||
|
python3-venv : Depends: python3.13-venv (>= 3.13.5-1~) but it is not going to be installed
|
||||||
|
Depends: python3 (= 3.13.5-1) but 3.13.9-3 is to be installed
|
||||||
|
E: Unable to satisfy dependencies. Reached two conflicting assignments:
|
||||||
|
1. python3-venv:amd64=3.13.5-1 is selected for install
|
||||||
|
2. python3-venv:amd64=3.13.5-1 Depends python3 (= 3.13.5-1)
|
||||||
|
but none of the choices are installable:
|
||||||
|
- python3:amd64=3.13.5-1 is not selected for install
|
||||||
|
===== Building TrulyMEM for Linux =====
|
||||||
|
Project root: /home/program/TrulyMEM-TrueHumanMEM
|
||||||
|
The virtual environment was not created successfully because ensurepip is not
|
||||||
|
available. On Debian/Ubuntu systems, you need to install the python3-venv
|
||||||
|
package using the following command.
|
||||||
|
|
||||||
|
apt install python3.13-venv
|
||||||
|
|
||||||
|
You may need to use sudo with that command. After installing the python3-venv
|
||||||
|
package, recreate your virtual environment.
|
||||||
|
|
||||||
|
Failing command: /home/program/TrulyMEM-TrueHumanMEM/.venv_build/bin/python3
|
||||||
|
|
||||||
|
Warning: venv creation failed, falling back to system Python
|
||||||
|
Cleaning previous builds...
|
||||||
|
================================
|
||||||
|
Building TrulyMEM (TUI + Web embedded)
|
||||||
|
================================
|
||||||
|
31 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.4
|
||||||
|
31 INFO: Python: 3.13.12
|
||||||
|
33 INFO: Platform: Linux-6.1.0-44-amd64-x86_64-with-glibc2.42
|
||||||
|
33 INFO: Python environment: /usr
|
||||||
|
36 INFO: Removing temporary files and cleaning cache in /root/.cache/pyinstaller
|
||||||
|
37 INFO: Module search paths (PYTHONPATH):
|
||||||
|
['/home/program/TrulyMEM-TrueHumanMEM',
|
||||||
|
'/home/program/TrulyMEM-TrueHumanMEM',
|
||||||
|
'/usr/lib/python313.zip',
|
||||||
|
'/usr/lib/python3.13',
|
||||||
|
'/usr/lib/python3.13/lib-dynload',
|
||||||
|
'/usr/local/lib/python3.13/dist-packages',
|
||||||
|
'/usr/lib/python3/dist-packages',
|
||||||
|
'/home/program/TrulyMEM-TrueHumanMEM']
|
||||||
|
158 INFO: Appending 'datas' from .spec
|
||||||
|
158 INFO: checking Analysis
|
||||||
|
158 INFO: Building Analysis because Analysis-00.toc is non existent
|
||||||
|
159 INFO: Looking for Python shared library...
|
||||||
|
166 INFO: Using Python shared library: /usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0
|
||||||
|
166 INFO: Running Analysis Analysis-00.toc
|
||||||
|
166 INFO: Target bytecode optimization level: 0
|
||||||
|
166 INFO: Initializing module dependency graph...
|
||||||
|
166 INFO: Initializing module graph hook caches...
|
||||||
|
170 INFO: Analyzing modules for base_library.zip ...
|
||||||
|
651 INFO: Processing standard module hook 'hook-encodings.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
943 INFO: Processing standard module hook 'hook-heapq.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
1656 INFO: Processing standard module hook 'hook-pickle.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2496 INFO: Caching module dependency graph...
|
||||||
|
2519 INFO: Analyzing /home/program/TrulyMEM-TrueHumanMEM/trulymem_entry.py
|
||||||
|
2554 INFO: Processing standard module hook 'hook-sqlite3.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2678 INFO: Processing standard module hook 'hook-platform.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2702 INFO: Processing standard module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2706 INFO: Processing standard module hook 'hook-_ctypes.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
2716 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
2717 INFO: SetuptoolsInfo: initializing cached setuptools info...
|
||||||
|
4768 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
4924 INFO: Processing standard module hook 'hook-xml.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
5338 INFO: Processing standard module hook 'hook-pydantic.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
5622 INFO: Processing standard module hook 'hook-rich.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
5908 INFO: Processing standard module hook 'hook-pygments.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
6310 INFO: Processing standard module hook 'hook-chardet.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
7729 INFO: Processing standard module hook 'hook-zoneinfo.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
8919 INFO: Processing standard module hook 'hook-certifi.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
8990 INFO: Processing standard module hook 'hook-anyio.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
9647 INFO: Processing standard module hook 'hook-difflib.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
10808 INFO: Processing standard module hook 'hook-numpy.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
12163 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
12999 INFO: Processing standard module hook 'hook-pytz.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
13591 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
13598 INFO: Processing standard module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
15377 INFO: Processing standard module hook 'hook-jinja2.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
|
||||||
|
15747 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15747 INFO: Setuptools: 'importlib_metadata' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.importlib_metadata'!
|
||||||
|
15752 INFO: Processing standard module hook 'hook-setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
|
||||||
|
15759 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
|
15778 INFO: Processing pre-safe-import-module hook 'hook-jaraco.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
|
||||||
25
generate-debug-cert.sh
Normal file
25
generate-debug-cert.sh
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
DIR="/home/program/.harmonyos"
|
||||||
|
mkdir -p "$DIR"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
KEYSTORE_PASS="123456"
|
||||||
|
ALIAS="debug"
|
||||||
|
ALIAS_PASS="123456"
|
||||||
|
DNAME="CN=Debug,OU=Debug,O=TrulyMEM,L=Beijing,ST=Beijing,C=CN"
|
||||||
|
|
||||||
|
# 生成私钥
|
||||||
|
openssl ecparam -genkey -name prime256v1 -out private.pem 2>/dev/null
|
||||||
|
|
||||||
|
# 生成 CSR
|
||||||
|
openssl req -new -key private.pem -out cert.csr -subj "$DNAME" 2>/dev/null
|
||||||
|
|
||||||
|
# 自签名证书
|
||||||
|
openssl req -x509 -days 3650 -key private.pem -in cert.csr -out debug.cer 2>/dev/null
|
||||||
|
|
||||||
|
# 创建 PKCS12
|
||||||
|
openssl pkcs12 -export -out debug.p12 -inkey private.pem -in debug.cer -password pass:$KEYSTORE_PASS -name $ALIAS 2>/dev/null
|
||||||
|
|
||||||
|
echo "Debug cert generated at $DIR"
|
||||||
|
ls -la "$DIR"
|
||||||
23
hvigor/hvigor-config.json5
Normal file
23
hvigor/hvigor-config.json5
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"modelVersion": "5.0.5",
|
||||||
|
"dependencies": {
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
|
||||||
|
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
|
||||||
|
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
|
||||||
|
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
|
||||||
|
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
|
||||||
|
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
|
||||||
|
},
|
||||||
|
"nodeOptions": {
|
||||||
|
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
|
||||||
|
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user