refactor: clean harmonyos branch to only contain HarmonyOS project in harmony/

This commit is contained in:
root
2026-04-28 16:34:34 +08:00
parent e83151bd1e
commit 5c59cd2ceb
68 changed files with 0 additions and 13420 deletions

196
LICENSE
View File

@ -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>.

View File

@ -1,75 +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**: [README_EN.md](./README_EN.md)
**让 AI 拥有自知、可塑、有分寸感的长期记忆***The More Human Choice.*
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
---
## 一句话
TrulyMEM 将记忆权交还给 LLM。通过图数据库三元组替代传统 messages 数组,让 LLM 自主决定记什么、忘什么。
---
## 快速开始
```bash
python trulymem_entry.py # 从源码
./dist/TrulyMEM # 打包后
```
首次启动 → TUI 登录页面 → 创建/登录账号 → 按 **F2** 配置 API Key → 开始聊天
📖 **详细启动文档**: [docs/zh/quick_start.md](docs/zh/quick_start.md)
---
## 主要特性
| 特性 | 说明 |
|------|------|
| 🧠 **图记忆** | 三元组存储LLM 自主推理跳转 |
| 🔐 **多用户** | 用户隔离 + Admin/User 角色权限 |
| 🌐 **Web 可视化** | 内嵌 Flask 服务(线程模式),实时浏览知识图谱 |
| 🎮 **TUI 界面** | Textual 终端界面F2 配置面板 |
| 📦 **单文件打包** | PyInstaller 打包Web 服务内嵌于主二进制 |
---
## 文档索引
| 文档 | 内容 |
|------|------|
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 🔥 **完整启动指南**(含 Web、多用户、打包 |
| [docs/zh/architecture.md](docs/zh/architecture.md) | 系统架构和技术设计 |
| [docs/zh/memory.md](docs/zh/memory.md) | 内部记忆工作机制 |
| [docs/zh/persona.md](docs/zh/persona.md) | 人设图机制 |
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口 |
| [docs/zh/prompts.md](docs/zh/prompts.md) | 提示词管理模块 |
---
## 特别鸣谢
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — 学术指导
- [逝水秋生白](https://atomgit.com/cenber) — 架构支持
- anzhitinglan — 测试资源支持
- 崔莉萍老师 — 理论指导
- Annie — 专业指导
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
---
## 许可证
GNU General Public License v3.0 (GPLv3)

View File

@ -1,75 +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)
> **中文**: [README.md](./README.md)
**Give AI self-awareness, plasticity, and a sense of proportion in long-term memory***The More Human Choice.*
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
---
## In a Nutshell
TrulyMEM gives memory authority back to the LLM. Using graph database (triplets) instead of the traditional messages array, the LLM autonomously decides what to remember and what to forget.
---
## Quick Start
```bash
python trulymem_entry.py # from source
./dist/TrulyMEM # packaged binary
```
First run → TUI login screen → create/sign in → press **F2** for API Key → start chatting
📖 **Full guide**: [docs/en/quick_start.md](docs/en/quick_start.md)
---
## Features
| Feature | Description |
|---------|-------------|
| 🧠 **Graph Memory** | Triplet storage, LLM autonomous navigation |
| 🔐 **Multi-User** | Isolated profiles + Admin/User roles |
| 🌐 **Web UI** | Embedded Flask server (thread mode) for knowledge graph browsing |
| 🎮 **TUI** | Textual-based terminal UI with F2 config panel |
| 📦 **Single Binary** | PyInstaller build, Web server embedded |
---
## Documentation
| Document | Content |
|----------|---------|
| [docs/en/quick_start.md](docs/en/quick_start.md) | 🔥 **Full setup guide** (Web, multi-user, building) |
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and design |
| [docs/en/memory.md](docs/en/memory.md) | Memory working mechanism |
| [docs/en/persona.md](docs/en/persona.md) | Persona Graph mechanism |
| [docs/en/api.md](docs/en/api.md) | Backend API |
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management |
---
## 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
---
## License
GNU General Public License v3.0 (GPLv3)

View File

@ -1,141 +0,0 @@
#!/bin/bash
set -e
echo "===== Building TrulyMEM AppImage ====="
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
APPIMAGE_TOOL="${APPIMAGE_TOOL:-appimagetool}"
if ! command -v "$APPIMAGE_TOOL" &> /dev/null && [ ! -f "$APPIMAGE_TOOL" ]; then
echo "Warning: $APPIMAGE_TOOL not found, will try to create AppDir only"
SKIP_APPIMAGE=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
# ── 打包 ──────────────────────────────────────────────────────────────────
echo "================================"
echo "Building TrulyMEM (TUI + Web embedded)"
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 \
--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 \
--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 \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
# ── 创建 AppDir ───────────────────────────────────────────────────────────
APP_NAME="TrulyMEM"
APP_DIR="$PROJECT_ROOT/build/${APP_NAME}.AppDir"
mkdir -p "$APP_DIR/usr/bin"
cp "dist/TrulyMEM" "$APP_DIR/usr/bin/TrulyMEM"
# Desktop 文件
cat > "$APP_DIR/${APP_NAME}.desktop" << EOF
[Desktop Entry]
Name=TrulyMEM
Comment=TrueHumanMEM - AI Memory System
Exec=TrulyMEM
Icon=${APP_NAME}
Terminal=true
Type=Application
Categories=Utility;AI;
EOF
# 图标
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
# 如果有图标文件就复制,否则创建占位
if [ -f "pic/image.png" ]; then
cp "pic/image.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/${APP_NAME}.png"
cp "pic/image.png" "$APP_DIR/${APP_NAME}.png"
else
# 创建最小占位图标1x1 透明 PNG
echo -n "" > "$APP_DIR/${APP_NAME}.png"
fi
# AppRun
cat > "$APP_DIR/AppRun" << 'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
export PATH="${HERE}/usr/bin:${PATH}"
exec "${HERE}/usr/bin/TrulyMEM" "$@"
APPRUN
chmod +x "$APP_DIR/AppRun"
# ── 构建 AppImage ────────────────────────────────────────────────────────
if [ "$SKIP_APPIMAGE" != "1" ]; then
echo "================================"
echo "Building AppImage"
echo "================================"
# 支持 ARCH 环境变量
ARCH="${ARCH:-$(uname -m)}" "$APPIMAGE_TOOL" "$APP_DIR" "dist/TrulyMEM-${ARCH}.AppImage"
echo "AppImage: dist/TrulyMEM-${ARCH}.AppImage"
else
echo "(Skipped AppImage packaging, AppDir ready at $APP_DIR)"
fi
echo "================================"
echo "===== Build Complete ====="
echo "Binary: dist/TrulyMEM"
ls -la dist/
deactivate
rm -rf "$VENV_DIR"
echo "Build finished successfully!"

View File

@ -1,82 +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
echo "================================"
echo "Building TrulyMEM (TUI + Web embedded)"
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 \
--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 \
--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 \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
echo "================================"
echo "===== Build Complete ====="
echo "Binary: dist/TrulyMEM"
ls -la dist/
deactivate
rm -rf "$VENV_DIR"
echo "Build finished successfully!"

View File

@ -1,82 +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
echo "Cleaning previous builds..."
rm -rf build/dist build/__pycache__ 2>/dev/null || true
echo "================================"
echo "Building TrulyMEM (TUI + Web embedded)"
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 \
--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 \
--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 \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
echo "================================"
echo "===== Build Complete ====="
echo "Binary: dist/TrulyMEM"
ls -la dist/
deactivate
rm -rf "$VENV_DIR"
echo "Build finished successfully!"

View File

@ -1,86 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo ===== Building TrulyMEM for Windows =====
set SCRIPT_DIR=%~dp0
set PROJECT_ROOT=%SCRIPT_DIR%..
cd /d "%PROJECT_ROOT%"
echo Project root: %CD%
where python >nul 2>nul
if %ERRORLEVEL% neq 0 (
echo Error: python not found
exit /b 1
)
set 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...
if exist build\dist rmdir /s /q build\dist
if exist build\__pycache__ rmdir /s /q build\__pycache__
echo ================================
echo Building TrulyMEM ^(TUI + Web embedded^)
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 ^
--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 ^
--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 ^
--hidden-import werkzeug ^
--collect-all textual ^
--noconfirm
echo ================================
echo ===== Build Complete =====
echo Binary: dist\TrulyMEM.exe
dir dist\
call deactivate
if exist "%VENV_DIR%" rmdir /s /q "%VENV_DIR%"
echo Build finished successfully!
endlocal

View File

@ -1,98 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
block_cipher = None
project_root = os.path.dirname(os.path.dirname(os.path.abspath(SPEC)))
sys.path.insert(0, project_root)
datas = []
# UI 样式
ui_styles_dir = os.path.join(project_root, 'ui', 'styles')
if os.path.exists(ui_styles_dir):
for root, dirs, files in os.walk(ui_styles_dir):
for f in files:
datas.append((os.path.join(root, f), 'ui/styles'))
# Prompt 模板
prompt_tmpl_dir = os.path.join(project_root, 'core', 'prompts', 'templates')
if os.path.exists(prompt_tmpl_dir):
for root, dirs, files in os.walk(prompt_tmpl_dir):
for f in files:
datas.append((os.path.join(root, f), 'core/prompts/templates'))
# Web 静态文件
static_dir = os.path.join(project_root, 'static')
if os.path.exists(static_dir):
for root, dirs, files in os.walk(static_dir):
for f in files:
datas.append((os.path.join(root, f), 'static'))
# Web 模板
templates_dir = os.path.join(project_root, 'templates')
if os.path.exists(templates_dir):
for root, dirs, files in os.walk(templates_dir):
for f in files:
datas.append((os.path.join(root, f), 'templates'))
# 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(
[os.path.join(project_root, 'trulymem_entry.py')],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=[
'textual', 'textual.app', 'textual.widgets', 'textual.css',
'openai', 'openai._client',
'neo4j',
'sqlite3',
'core', 'core.embedded_db', 'core.graph_client',
'core.tool_executor', 'core.tool_limiter',
'core.tools', 'core.tools.memory_tools',
'core.prompts', 'core.prompts.prompt_manager',
'core.server', 'core.client',
'core.migrate',
'ui', 'ui.app', 'ui.login_screen',
'ui.models', 'ui.models.message', 'ui.models.config', 'ui.models.log_entry',
'ui.widgets', 'ui.widgets.left_panel', 'ui.widgets.right_panel',
'ui.widgets.input_box', 'ui.widgets.message_history', 'ui.widgets.status_bar',
'ui.handlers',
'ui.services', 'ui.services.config_manager', 'ui.services.config_service',
'web_api',
'flask', 'flask_cors', 'werkzeug',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='TrulyMEM',
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,
)

View File

@ -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"
]

View File

@ -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

View File

@ -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()

View File

@ -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!")

View File

@ -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

View File

@ -1,6 +0,0 @@
"""
提示词管理模块
"""
from .prompt_manager import PromptManager
__all__ = ["PromptManager"]

View File

@ -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` - 关联信息
## 自主性
你有权根据对话上下文自主决定:
- 是否需要查询记忆
- 是否需要写入记忆
- 是否需要维护任务链
- 如何使用工具
记住:灵活应对,保持自然对话体验。"""

View File

@ -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. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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"]

View File

@ -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": "任务IDTask_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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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"?

View File

@ -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

View File

@ -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

View File

@ -1,202 +0,0 @@
# TrulyMEM Quick Start Guide
> **Version**: Multi-user (v2) — TUI login, user isolation, embedded Web server
---
## Running
### Quick Start
```bash
# From source
python trulymem_entry.py
# Packaged binary
./dist/TrulyMEM
```
### First Run — Login Flow
On first launch, TrulyMEM checks for legacy data and presents a **login screen**:
1. **Clean install** → Enter username/password (first user becomes admin)
2. **Legacy upgrade** → Detects `~/.trulymem/config.json`, guides migration setup
3. **Returning user** → Login directly
> 💡 All user data is isolated: `~/.trulymem/{username}/`
### Chat Configuration
After login, press **F2** to open the right-side configuration panel:
1. **API Key** — Required (DeepSeek, OpenAI, etc.)
2. **Model** — Optional
3. **Base URL** — Optional
Config saves automatically.
---
## Web Visualization
The Web service now runs **embedded in the main process** (no separate subprocess needed).
### Start via TUI (Admin only)
Admin users: press F2 → check "Enable Web Service".
### Start Manually
```bash
python web_api.py --port 4096
# Visit http://localhost:4096
```
### First Visit Flow
1. Open `http://localhost:4096` in browser
2. **No users** → Auto-redirect to setup page, create admin account
3. **Has users** → Login page
4. After login → Star map visualization
### Web Features
| Page | Access | Feature |
|------|--------|---------|
| 🌟 Star Map | All logged-in | Browse knowledge graph |
| ⚙ Settings | All logged-in | Change password |
| 🧑‍💼 User Management | **Admin only** | Add/delete users |
---
## Multi-User System
### Directory Layout
```
~/.trulymem/
├── trulymem.db # Global user database (web_users table)
├── .migrated # Migration flag
├── admin/
│ ├── config.json # Admin config
│ └── admin_graph.db # Admin knowledge graph
└── user2/
├── config.json # user2 config
└── user2_graph.db # user2 knowledge graph
```
### Role Matrix
| Feature | User | Admin |
|---------|------|-------|
| Change password | ✅ | ✅ |
| Configure API Key / Model | ✅ | ✅ |
| Web service toggle (TUI) | ❌ | ✅ |
| Web login credentials | ❌ | ✅ |
| View user list | ❌ | ✅ |
| Add/delete users | ❌ | ✅ |
> ⚠️ First registered user becomes admin automatically. Add users via Web settings page.
---
## Keyboard Shortcuts
| Key | Action |
|-----|--------|
| F1 | Help |
| F2 | Toggle sidebar (config panel) |
| F3 | Tool details |
| F5 | Clear screen |
| F6 | Quit |
---
## Building
```bash
# Linux
bash build/build_linux.sh
# macOS
bash build/build_macos.sh
# Windows
build\build_windows.bat
# AppImage
bash build/build_appimage.sh
```
Output: `dist/TrulyMEM` (single binary — TUI and Web server embedded)
> 📦 Since v2, the Web server runs as a thread inside the main process. No need for a separate `trulymem-web` binary.
---
## Architecture
### Communication
```
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (thread)
```
### Config Management
- **Per-user**: `~/.trulymem/{username}/config.json`
- **Web config**: `~/.trulymem/trulymem.db` (web_users table)
- **Auto-load**: reads config for logged-in user on startup
- **Persistent**: saves automatically on change
### Web Service Architecture
```
┌──────────────────────┐
│ TrulyMEM Process │
│ ┌──────┐ ┌────────┐ │
│ │ TUI │ │ Flask │ │ ← Same process, different threads
│ │ │ │ Thread │ │
│ └──────┘ └────────┘ │
└──────────────────────┘
```
---
## FAQ
### Python not found
Install Python 3.8+: https://www.python.org/downloads/
### Dependency installation fails
```bash
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
pip install -r requirements.txt
```
### Invalid API Key
Check format and whitespace. Reconfigure in TUI sidebar.
### Lost admin account
The first registered user is always admin. If all users lost admin, delete `trulymem.db` from the user directory and re-register.
### Legacy data migration
When old `~/.trulymem/config.json` and `graph_memory.db` are detected, TUI auto-enters migration flow. Legacy files are preserved.
---
## Dev Commands
```bash
pip install -r requirements.txt
pytest tests/
bash build/build_linux.sh
```

View File

@ -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

View File

@ -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
- **独立部署**: 支持打包为可执行文件

View File

@ -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 天。

View File

@ -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"]) # 错误描述
```

View File

@ -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是否更新了工作记忆链
- [ ] 涉及上下文引用时是否查询了工作记忆链?
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?

View File

@ -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. **明确边界**:除非用户要求,绝不主动跳出角色

View File

@ -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()` 返回缓存内容
- 缓存按进程,不持久化

View File

@ -1,204 +0,0 @@
# TrulyMEM 启动指南
> **版本**: 多用户版 (v2) — 支持 TUI 登录、多用户隔离、Web 服务内嵌
---
## 运行方式
### 快速启动(推荐)
```bash
# 从源码
python trulymem_entry.py
# 或打包后
./dist/TrulyMEM
```
### 首次使用 —— 登录流程
首次启动会自动检查是否需要迁移旧数据,然后进入**登录页面**
1. **新部署** → 直接输入用户名和密码创建账户(首个用户自动成为管理员)
2. **旧版升级** → 自动检测 `~/.trulymem/config.json`,引导设置用户名密码,迁移数据
3. **已有账户** → 直接登录进入聊天界面
> 💡 所有用户数据隔离存储:`~/.trulymem/{用户名}/`
### 聊天配置
登录后按 **F2** 展开右侧配置面板:
1. **API Key** — 必须(支持 DeepSeek、OpenAI 等)
2. **模型** — 可选,默认已配置
3. **Base URL** — 可选
配置自动保存,下次启动自动加载。
---
## Web 可视化界面
TrulyMEM 的 Web 服务现在**内嵌在主进程中**(无需独立启动子进程)。
### TUI 内启动(管理员专有)
管理员按 F2 打开右侧面板,勾选「启用 Web 服务」即可。
### 手动启动
```bash
python web_api.py --port 4096
# 访问 http://localhost:4096
```
### 首次访问流程
1. 浏览器打开 `http://localhost:4096`
2. **无用户** → 自动跳转至设置页,创建管理员账号
3. **有用户** → 跳转至登录页
4. 登录后进入星图可视化页面
### Web 功能
| 页面 | 访问权限 | 功能 |
|------|----------|------|
| 🌟 星图 | 所有已登录用户 | 浏览知识图谱三元组 |
| ⚙ 设置 | 所有已登录用户 | 修改密码 |
| 🧑‍💼 用户管理 | **仅管理员** | 添加/删除用户 |
---
## 多用户系统
### 目录结构
```
~/.trulymem/
├── trulymem.db # 全局用户数据库web_users 表)
├── .migrated # 旧版迁移标记
├── admin/
│ ├── config.json # 管理员配置
│ └── admin_graph.db # 管理员知识图谱
└── user2/
├── config.json # user2 配置
└── user2_graph.db # user2 知识图谱
```
### 角色体系
| 功能 | 普通用户 | 管理员 |
|------|---------|--------|
| 修改自己密码 | ✅ | ✅ |
| 配置 API Key / 模型 | ✅ | ✅ |
| Web 服务开关TUI | ❌ | ✅ |
| Web 登录凭据 | ❌ | ✅ |
| 查看用户列表 | ❌ | ✅ |
| 添加/删除用户 | ❌ | ✅ |
> ⚠️ 首个注册用户自动成为管理员。Web 设置页可添加新用户。
---
## 快捷键
| 按键 | 功能 |
|------|------|
| F1 | 帮助 |
| F2 | 切换侧边栏(配置面板) |
| F3 | 工具详情 |
| F5 | 清屏 |
| F6 | 退出 |
---
## 打包构建
```bash
# Linux
bash build/build_linux.sh
# macOS
bash build/build_macos.sh
# Windows
build\build_windows.bat
# AppImage
bash build/build_appimage.sh
```
构建产出:`dist/TrulyMEM`单文件TUI + Web 服务均内嵌于同一二进制)
> 📦 从 v2 开始Web 服务作为线程嵌入主程序,不再需要独立打包 `trulymem-web`。
---
## 架构说明
### 通信协议
UI 与后端通过 **Packet 协议** 通信:
```
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (独立线程)
```
### 配置管理
- **用户级存储**: `~/.trulymem/{username}/config.json`
- **Web 配置**: `~/.trulymem/trulymem.db`web_users 表)
- **自动加载**: 启动时根据登录用户加载对应配置文件
- **动态更新**: 运行时修改配置立即生效,自动持久化
### Web 服务架构
```
┌──────────────────────┐
│ TrulyMEM 主进程 │
│ ┌──────┐ ┌────────┐ │
│ │ TUI │ │ Flask │ │ ← 同一进程,不同线程
│ │ │ │ Thread │ │
│ └──────┘ └────────┘ │
└──────────────────────┘
```
---
## 常见问题
### Python 未找到
安装 Python 3.8+https://www.python.org/downloads/
### 依赖安装失败
```bash
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
pip install -r requirements.txt
```
### API Key 无效
检查 API Key 格式,确保无多余空格。可在 TUI 右侧面板重新配置。
### 管理员账号丢失
数据库中第一个注册账号总是 admin。如果所有用户都丢失了 admin 权限,删除用户目录下的 `trulymem.db` 后重新注册即可。
### 旧版数据迁移
检测到旧版 `~/.trulymem/config.json``graph_memory.db`TUI 启动时自动进入迁移引导。迁移后旧文件保留,不会删除。
---
## 开发命令
```bash
pip install -r requirements.txt
pytest tests/
bash build/build_linux.sh
```

View File

@ -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 处理任务相关操作

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,6 +0,0 @@
textual>=0.47.0
neo4j>=5.14.0
openai>=1.12.0
flask>=3.0.0
flask-cors>=4.0.0
pyinstaller>=6.0.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,228 +0,0 @@
<!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;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(2px 2px at 20px 30px, #eee, transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.8), transparent),
radial-gradient(1px 1px at 90px 40px, #fff, transparent),
radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.6), transparent),
radial-gradient(2px 2px at 160px 30px, #ddd, transparent);
background-repeat: repeat;
background-size: 200px 100px;
animation: twinkle 5s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes twinkle {
0% { opacity: 0.5; }
100% { opacity: 1; }
}
.login-container {
position: relative;
z-index: 1;
width: 400px;
padding: 40px;
background: rgba(10, 10, 26, 0.9);
border-radius: 12px;
border: 1px solid rgba(100, 100, 255, 0.3);
box-shadow:
0 0 20px rgba(68, 136, 255, 0.2),
0 0 60px rgba(68, 136, 255, 0.1),
inset 0 0 20px rgba(68, 136, 255, 0.05);
backdrop-filter: blur(10px);
}
.login-title {
text-align: center;
font-size: 28px;
margin-bottom: 10px;
color: #4488ff;
text-shadow: 0 0 10px rgba(68, 136, 255, 0.5);
letter-spacing: 2px;
}
.login-subtitle {
text-align: center;
font-size: 14px;
color: #8888aa;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #aaaacc;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
background: rgba(20, 20, 40, 0.8);
border: 1px solid rgba(100, 100, 255, 0.3);
border-radius: 6px;
color: #ffffff;
font-family: 'Courier New', monospace;
font-size: 14px;
transition: all 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #4488ff;
box-shadow: 0 0 10px rgba(68, 136, 255, 0.3);
}
.form-group input::placeholder {
color: #555577;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, rgba(68, 136, 255, 0.3), rgba(68, 136, 255, 0.1));
border: 1px solid rgba(68, 136, 255, 0.5);
border-radius: 6px;
color: #ffffff;
font-family: 'Courier New', monospace;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
position: relative;
letter-spacing: 1px;
}
.login-btn:hover {
background: linear-gradient(135deg, rgba(68, 136, 255, 0.5), rgba(68, 136, 255, 0.3));
box-shadow: 0 0 15px rgba(68, 136, 255, 0.4);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn .spinner {
display: none;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.login-btn.loading .spinner {
display: block;
}
.login-btn.loading span {
visibility: hidden;
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.error-message {
display: none;
margin-top: 15px;
padding: 12px;
background: rgba(255, 68, 68, 0.15);
border: 1px solid rgba(255, 68, 68, 0.4);
border-radius: 6px;
color: #ff6b6b;
font-size: 13px;
text-align: center;
}
.error-message.show {
display: block;
}
</style>
</head>
<body>
<div class="login-container">
<h1 class="login-title">记忆星图</h1>
<p class="login-subtitle">TrulyMEM - 登录以继续</p>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="login-btn" id="loginBtn">
<span>登 录</span>
<div class="spinner"></div>
</button>
</form>
<div class="error-message" id="errorMsg"></div>
</div>
<script>
const loginForm = document.getElementById('loginForm');
const loginBtn = document.getElementById('loginBtn');
const errorMsg = document.getElementById('errorMsg');
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
errorMsg.classList.remove('show');
loginBtn.classList.add('loading');
loginBtn.disabled = true;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
window.location.href = '/graph.html';
} else {
errorMsg.textContent = data.error || '登录失败';
errorMsg.classList.add('show');
}
} catch (err) {
errorMsg.textContent = '网络错误,请重试';
errorMsg.classList.add('show');
} finally {
loginBtn.classList.remove('loading');
loginBtn.disabled = false;
}
});
</script>
</body>
</html>

View File

@ -1,544 +0,0 @@
<!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;
min-height: 100vh;
display: flex; align-items: center; justify-content: center;
position: relative;
}
body::before {
content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background:
radial-gradient(2px 2px at 20px 30px, #eee, transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.8), transparent),
radial-gradient(1px 1px at 90px 40px, #fff, transparent);
background-repeat: repeat;
background-size: 200px 100px;
animation: twinkle 5s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes twinkle { 0% { opacity: 0.5; } 100% { opacity: 1; } }
.settings-container {
position: relative; z-index: 1; width: 480px; padding: 40px;
background: rgba(10, 10, 26, 0.9);
border-radius: 12px;
border: 1px solid rgba(100, 100, 255, 0.3);
box-shadow: 0 0 20px rgba(68, 136, 255, 0.2), 0 0 60px rgba(68, 136, 255, 0.1), inset 0 0 20px rgba(68, 136, 255, 0.05);
backdrop-filter: blur(10px);
}
.settings-title {
text-align: center; font-size: 26px; margin-bottom: 25px;
color: #4488ff; text-shadow: 0 0 10px rgba(68, 136, 255, 0.5);
letter-spacing: 2px;
}
.section-title {
font-size: 16px; color: #8888cc; margin: 20px 0 15px;
border-bottom: 1px solid rgba(100, 100, 255, 0.2);
padding-bottom: 6px; letter-spacing: 1px;
}
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; color: #aaaacc; font-size: 14px; }
.form-group input {
width: 100%; padding: 10px 14px;
background: rgba(20, 20, 40, 0.8);
border: 1px solid rgba(100, 100, 255, 0.3);
border-radius: 6px; color: #ffffff;
font-family: 'Courier New', monospace; font-size: 14px;
transition: all 0.3s;
}
.form-group input:focus {
outline: none; border-color: #4488ff; box-shadow: 0 0 10px rgba(68, 136, 255, 0.3);
}
.form-group input::placeholder { color: #555577; }
.toggle-row {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 0; border-bottom: 1px solid rgba(100, 100, 255, 0.1);
}
.toggle-label { color: #ccccee; font-size: 14px; }
.toggle-desc { color: #7777aa; font-size: 12px; margin-top: 2px; }
.toggle-switch {
position: relative; width: 48px; height: 26px; cursor: pointer; flex-shrink: 0;
}
.toggle-switch input { display: none; }
.toggle-slider {
position: absolute; inset: 0;
background: rgba(60, 60, 80, 0.8);
border-radius: 13px; transition: all 0.3s;
border: 1px solid rgba(100, 100, 255, 0.2);
}
.toggle-slider::after {
content: ''; position: absolute; width: 20px; height: 20px;
left: 2px; bottom: 2px; background: #6666aa;
border-radius: 50%; transition: all 0.3s;
}
.toggle-switch input:checked + .toggle-slider {
background: rgba(68, 136, 255, 0.4);
border-color: rgba(68, 136, 255, 0.6);
}
.toggle-switch input:checked + .toggle-slider::after {
left: 24px; background: #4488ff;
}
.btn {
width: 100%; padding: 12px;
background: linear-gradient(135deg, rgba(68, 136, 255, 0.3), rgba(68, 136, 255, 0.1));
border: 1px solid rgba(68, 136, 255, 0.5);
border-radius: 6px; color: #ffffff;
font-family: 'Courier New', monospace; font-size: 14px;
cursor: pointer; transition: all 0.3s; margin-top: 8px;
}
.btn:hover {
background: linear-gradient(135deg, rgba(68, 136, 255, 0.5), rgba(68, 136, 255, 0.3));
box-shadow: 0 0 15px rgba(68, 136, 255, 0.4);
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.back-link {
display: block; text-align: center; margin-top: 20px; color: #6666aa;
text-decoration: none; font-size: 13px; transition: color 0.3s;
}
.back-link:hover { color: #4488ff; }
.success-msg, .error-msg {
display: none; margin-top: 12px; padding: 10px;
border-radius: 6px; font-size: 13px; text-align: center;
}
.success-msg { background: rgba(68, 255, 136, 0.15); border: 1px solid rgba(68, 255, 136, 0.4); color: #6bff9b; }
.error-msg { background: rgba(255, 68, 68, 0.15); border: 1px solid rgba(255, 68, 68, 0.4); color: #ff6b6b; }
.success-msg.show, .error-msg.show { display: block; }
.tui-status {
text-align: center; font-size: 12px; color: #666688; margin-top: 5px;
}
/* 用户管理样式 */
.user-section { margin-top: 20px; }
.user-table {
width: 100%; border-collapse: collapse; margin-top: 10px;
font-size: 13px;
}
.user-table th {
text-align: left; padding: 8px; color: #8888cc;
border-bottom: 1px solid rgba(100, 100, 255, 0.2);
}
.user-table td {
padding: 8px; border-bottom: 1px solid rgba(100, 100, 255, 0.1);
color: #ccccee;
}
.btn-small {
padding: 4px 10px; font-size: 12px;
background: rgba(255, 68, 68, 0.2);
border: 1px solid rgba(255, 68, 68, 0.4);
border-radius: 4px; color: #ff6b6b;
cursor: pointer; transition: all 0.3s;
}
.btn-small:hover {
background: rgba(255, 68, 68, 0.4);
}
.btn-small:disabled {
opacity: 0.4; cursor: not-allowed;
}
.btn-add {
margin-top: 10px; padding: 8px 16px;
background: rgba(68, 136, 255, 0.2);
border: 1px solid rgba(68, 136, 255, 0.4);
border-radius: 6px; color: #4488ff;
cursor: pointer; font-size: 13px; transition: all 0.3s;
}
.btn-add:hover {
background: rgba(68, 136, 255, 0.4);
}
/* 弹窗样式 */
.modal-overlay {
display: none; position: fixed; top: 0; left: 0;
width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7);
z-index: 1000; align-items: center; justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal {
background: rgba(10, 10, 26, 0.95);
border: 1px solid rgba(100, 100, 255, 0.3);
border-radius: 12px; padding: 30px; width: 400px;
}
.modal-title {
font-size: 18px; color: #4488ff; margin-bottom: 20px;
text-align: center;
}
.modal .form-group { margin-bottom: 15px; }
.modal .btn {
margin-top: 15px;
}
.modal .btn-cancel {
background: rgba(100, 100, 100, 0.2);
border-color: rgba(100, 100, 100, 0.4);
margin-top: 10px;
}
.modal .btn-cancel:hover {
background: rgba(100, 100, 100, 0.4);
}
.current-user {
color: #4488ff; font-weight: bold;
}
</style>
</head>
<body>
<div class="settings-container">
<h1 class="settings-title">⚙ 设置</h1>
<!-- 修改密码 -->
<div class="section-title">🔑 修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label for="current_password">当前密码</label>
<input type="password" id="current_password" placeholder="输入当前密码" required>
</div>
<div class="form-group">
<label for="new_password">新密码</label>
<input type="password" id="new_password" placeholder="至少 6 位" required minlength="6">
</div>
<div class="form-group">
<label for="confirm_password">确认新密码</label>
<input type="password" id="confirm_password" placeholder="再次输入新密码" required>
</div>
<button type="submit" class="btn" id="changePwdBtn">更 新 密 码</button>
</form>
<!-- TUI 服务控制 -->
<div class="section-title">🖥 TUI 终端服务</div>
<div class="toggle-row">
<div>
<div class="toggle-label">启用终端 TUI 服务</div>
<div class="toggle-desc">控制终端文本界面是否允许连接</div>
</div>
<label class="toggle-switch">
<input type="checkbox" id="enableTuiToggle">
<span class="toggle-slider"></span>
</label>
</div>
<div class="tui-status" id="tuiStatus">状态加载中...</div>
<!-- 保存结果提示 -->
<div class="success-msg" id="successMsg"></div>
<div class="error-msg" id="errorMsg"></div>
<!-- 用户管理(仅管理员可见) -->
<div id="adminSection" style="display:none;">
<div class="section-title">👥 用户管理</div>
<div class="user-section">
<table class="user-table" id="userTable">
<thead>
<tr>
<th>用户名</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- 用户列表将在这里动态生成 -->
</tbody>
</table>
<button class="btn-add" id="addUserBtn">+ 添加用户</button>
</div>
</div>
<a href="/graph.html" class="back-link">← 返回星图</a>
</div>
<!-- 添加用户弹窗 -->
<div class="modal-overlay" id="addUserModal">
<div class="modal">
<div class="modal-title">添加用户</div>
<form id="addUserForm">
<div class="form-group">
<label for="new_username">用户名</label>
<input type="text" id="new_username" placeholder="输入新用户名" required>
</div>
<div class="form-group">
<label for="new_user_password">密码</label>
<input type="password" id="new_user_password" placeholder="至少 6 位" required minlength="6">
</div>
<button type="submit" class="btn" id="confirmAddBtn">添加</button>
<button type="button" class="btn btn-cancel" id="cancelAddBtn">取消</button>
</form>
<div class="success-msg" id="addUserSuccess"></div>
<div class="error-msg" id="addUserError"></div>
</div>
</div>
<script>
let currentUser = '';
let isAdmin = false;
// 初始化:获取用户信息和配置
async function init() {
// 获取当前用户信息(含角色)
try {
const resp = await fetch('/api/userinfo');
const data = await resp.json();
if (data.success) {
currentUser = data.username;
isAdmin = data.is_admin;
// 仅管理员显示用户管理区域
if (isAdmin) {
document.getElementById('adminSection').style.display = 'block';
loadUsers();
}
}
} catch (e) {
console.error('获取用户信息失败:', e);
}
// 加载配置
loadConfig();
}
// 加载当前配置
async function loadConfig() {
try {
const resp = await fetch('/api/settings/config');
const data = await resp.json();
if (data.success) {
document.getElementById('enableTuiToggle').checked = data.enable_tui !== false;
document.getElementById('tuiStatus').textContent =
data.enable_tui ? '✅ TUI 服务已启用' : '⏹️ TUI 服务已禁用';
}
} catch (e) {
document.getElementById('tuiStatus').textContent = '⚠️ 无法加载配置';
}
}
// 加载用户列表
async function loadUsers() {
try {
const resp = await fetch('/api/admin/users');
const data = await resp.json();
if (data.success) {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '';
data.users.forEach(user => {
const tr = document.createElement('tr');
const isCurrentUser = user.username === currentUser;
const roleBadge = user.role === 'admin'
? '<span style="color:#ffaa44;font-weight:bold;">管理员</span>'
: '<span style="color:#8888cc;">用户</span>';
tr.innerHTML = `
<td class="${isCurrentUser ? 'current-user' : ''}">
${user.username} ${isCurrentUser ? '(当前)' : ''}
</td>
<td>${roleBadge}</td>
<td>${new Date(user.created_at).toLocaleString('zh-CN')}</td>
<td>
<button class="btn-small"
onclick="deleteUser(${user.id}, '${user.username}')"
${isCurrentUser ? 'disabled' : ''}>
删除
</button>
</td>
`;
tbody.appendChild(tr);
});
}
} catch (e) {
console.error('加载用户列表失败:', e);
}
}
// 删除用户
async function deleteUser(userId, username) {
if (!confirm(`确定要删除用户 "${username}" 吗?`)) {
return;
}
try {
const resp = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE'
});
const data = await resp.json();
if (data.success) {
showSuccess('用户已删除');
loadUsers();
} else {
showError(data.error || '删除失败');
}
} catch (e) {
showError('网络错误');
}
}
// 显示添加用户弹窗
document.getElementById('addUserBtn').addEventListener('click', () => {
document.getElementById('addUserModal').classList.add('show');
hideAddUserMessages();
});
// 隐藏弹窗
document.getElementById('cancelAddBtn').addEventListener('click', () => {
document.getElementById('addUserModal').classList.remove('show');
document.getElementById('addUserForm').reset();
});
// 添加用户
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('new_username').value.trim();
const password = document.getElementById('new_user_password').value;
if (!username || !password) {
showAddUserError('用户名和密码不能为空');
return;
}
if (password.length < 6) {
showAddUserError('密码长度至少 6 位');
return;
}
const btn = document.getElementById('confirmAddBtn');
btn.disabled = true;
btn.textContent = '添加中...';
hideAddUserMessages();
try {
const resp = await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await resp.json();
if (data.success) {
showAddUserSuccess('✅ 用户添加成功');
setTimeout(() => {
document.getElementById('addUserModal').classList.remove('show');
document.getElementById('addUserForm').reset();
loadUsers();
}, 1500);
} else {
showAddUserError(data.error || '添加失败');
}
} catch (e) {
showAddUserError('网络错误');
} finally {
btn.disabled = false;
btn.textContent = '添加';
}
});
function showAddUserSuccess(msg) {
const el = document.getElementById('addUserSuccess');
el.textContent = msg;
el.classList.add('show');
document.getElementById('addUserError').classList.remove('show');
}
function showAddUserError(msg) {
const el = document.getElementById('addUserError');
el.textContent = msg;
el.classList.add('show');
document.getElementById('addUserSuccess').classList.remove('show');
}
function hideAddUserMessages() {
document.getElementById('addUserSuccess').classList.remove('show');
document.getElementById('addUserError').classList.remove('show');
}
init();
// TUI 开关
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
const enable = this.checked;
document.getElementById('tuiStatus').textContent = '🔄 更新中...';
try {
const resp = await fetch('/api/settings/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enable_tui: enable })
});
const data = await resp.json();
if (data.success) {
document.getElementById('tuiStatus').textContent =
enable ? '✅ TUI 服务已启用' : '⏹️ TUI 服务已禁用';
showSuccess('设置已保存');
} else {
document.getElementById('tuiStatus').textContent = '⚠️ 更新失败';
this.checked = !enable;
}
} catch (e) {
document.getElementById('tuiStatus').textContent = '⚠️ 网络错误';
this.checked = !enable;
}
});
// 修改密码
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const current = document.getElementById('current_password').value;
const newPwd = document.getElementById('new_password').value;
const confirm = document.getElementById('confirm_password').value;
if (newPwd !== confirm) {
showError('两次密码输入不一致');
return;
}
const btn = document.getElementById('changePwdBtn');
btn.disabled = true;
btn.textContent = '更新中...';
hideMessages();
try {
const resp = await fetch('/api/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
current_password: current,
new_password: newPwd,
confirm_password: confirm
})
});
const data = await resp.json();
if (data.success) {
showSuccess('✅ 密码已更新');
document.getElementById('current_password').value = '';
document.getElementById('new_password').value = '';
document.getElementById('confirm_password').value = '';
} else {
showError(data.error || '修改失败');
}
} catch (e) {
showError('网络错误');
} finally {
btn.disabled = false;
btn.textContent = '更 新 密 码';
}
});
function showSuccess(msg) {
const el = document.getElementById('successMsg');
el.textContent = msg;
el.classList.add('show');
document.getElementById('errorMsg').classList.remove('show');
setTimeout(() => el.classList.remove('show'), 3000);
}
function showError(msg) {
const el = document.getElementById('errorMsg');
el.textContent = msg;
el.classList.add('show');
document.getElementById('successMsg').classList.remove('show');
}
function hideMessages() {
document.getElementById('successMsg').classList.remove('show');
document.getElementById('errorMsg').classList.remove('show');
}
</script>
</body>
</html>

View File

@ -1,170 +0,0 @@
<!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;
display: flex; align-items: center; justify-content: center;
position: relative;
}
body::before {
content: '';
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background:
radial-gradient(2px 2px at 20px 30px, #eee, transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.8), transparent),
radial-gradient(1px 1px at 90px 40px, #fff, transparent),
radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.6), transparent),
radial-gradient(2px 2px at 160px 30px, #ddd, transparent);
background-repeat: repeat;
background-size: 200px 100px;
animation: twinkle 5s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes twinkle { 0% { opacity: 0.5; } 100% { opacity: 1; } }
.setup-container {
position: relative; z-index: 1; width: 420px; padding: 40px;
background: rgba(10, 10, 26, 0.9);
border-radius: 12px;
border: 1px solid rgba(100, 100, 255, 0.3);
box-shadow: 0 0 20px rgba(68, 136, 255, 0.2), 0 0 60px rgba(68, 136, 255, 0.1), inset 0 0 20px rgba(68, 136, 255, 0.05);
backdrop-filter: blur(10px);
}
.setup-title {
text-align: center; font-size: 26px; margin-bottom: 8px;
color: #4488ff; text-shadow: 0 0 10px rgba(68, 136, 255, 0.5);
letter-spacing: 2px;
}
.setup-subtitle {
text-align: center; font-size: 14px; color: #8888aa; margin-bottom: 8px;
}
.setup-hint {
text-align: center; font-size: 12px; color: #666688; margin-bottom: 25px;
}
.form-group { margin-bottom: 18px; }
.form-group label { display: block; margin-bottom: 6px; color: #aaaacc; font-size: 14px; }
.form-group input {
width: 100%; padding: 12px 16px;
background: rgba(20, 20, 40, 0.8);
border: 1px solid rgba(100, 100, 255, 0.3);
border-radius: 6px; color: #ffffff;
font-family: 'Courier New', monospace; font-size: 14px;
transition: all 0.3s;
}
.form-group input:focus {
outline: none; border-color: #4488ff; box-shadow: 0 0 10px rgba(68, 136, 255, 0.3);
}
.form-group input::placeholder { color: #555577; }
.setup-btn {
width: 100%; padding: 14px;
background: linear-gradient(135deg, rgba(68, 136, 255, 0.3), rgba(68, 136, 255, 0.1));
border: 1px solid rgba(68, 136, 255, 0.5);
border-radius: 6px; color: #ffffff;
font-family: 'Courier New', monospace; font-size: 16px;
cursor: pointer; transition: all 0.3s;
letter-spacing: 1px; margin-top: 5px;
}
.setup-btn:hover {
background: linear-gradient(135deg, rgba(68, 136, 255, 0.5), rgba(68, 136, 255, 0.3));
box-shadow: 0 0 15px rgba(68, 136, 255, 0.4);
}
.setup-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.setup-btn .spinner {
display: none; width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff;
border-radius: 50%; animation: spin 0.6s linear infinite;
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
}
.setup-btn.loading .spinner { display: block; }
.setup-btn.loading span { visibility: hidden; }
@keyframes spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
.error-message {
display: none; margin-top: 12px; padding: 10px;
background: rgba(255, 68, 68, 0.15);
border: 1px solid rgba(255, 68, 68, 0.4);
border-radius: 6px; color: #ff6b6b; font-size: 13px; text-align: center;
}
.error-message.show { display: block; }
</style>
</head>
<body>
<div class="setup-container">
<h1 class="setup-title">🚀 首次设置</h1>
<p class="setup-subtitle">TrulyMEM Web 管理界面</p>
<p class="setup-hint">创建管理员账户,用于登录 Web 管理界面</p>
<form id="setupForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="设置管理员用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="至少 6 位密码" required minlength="6">
</div>
<div class="form-group">
<label for="confirm">确认密码</label>
<input type="password" id="confirm" name="confirm_password" placeholder="再次输入密码" required>
</div>
<button type="submit" class="setup-btn" id="setupBtn">
<span>创 建 账 户</span>
<div class="spinner"></div>
</button>
</form>
<div class="error-message" id="errorMsg"></div>
</div>
<script>
const setupForm = document.getElementById('setupForm');
const setupBtn = document.getElementById('setupBtn');
const errorMsg = document.getElementById('errorMsg');
setupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const confirm = document.getElementById('confirm').value;
if (password !== confirm) {
errorMsg.textContent = '两次密码输入不一致';
errorMsg.classList.add('show');
return;
}
errorMsg.classList.remove('show');
setupBtn.classList.add('loading');
setupBtn.disabled = true;
try {
const resp = await fetch('/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, confirm_password: confirm })
});
const data = await resp.json();
if (data.success) {
window.location.href = '/graph.html';
} else {
errorMsg.textContent = data.error || '创建失败';
errorMsg.classList.add('show');
}
} catch (err) {
errorMsg.textContent = '网络错误,请重试';
errorMsg.classList.add('show');
} finally {
setupBtn.classList.remove('loading');
setupBtn.disabled = false;
}
});
</script>
</body>
</html>

View File

@ -1 +0,0 @@
"""Tests for Graph Memory TUI"""

View File

@ -1 +0,0 @@
import pytest

View File

@ -1 +0,0 @@
"""Tests for Core Logic"""

View File

@ -1,218 +0,0 @@
"""context_rewrite 全面测试 - 单元 + 集成"""
import pytest
import json
import tempfile
import os
from core.tools.memory_tools import TOOLS, MEMORY_TOOLS
from core.tool_executor import execute_tool, execute_context_rewrite
from core.tool_limiter import ToolLimiter, ToolLimits
from core import EmbeddedGraphDB
# ========== 工具定义测试 ==========
class TestToolDefinition:
def test_context_rewrite_in_tools(self):
tool_names = [t["function"]["name"] for t in TOOLS]
assert "context_rewrite" in tool_names
def test_context_rewrite_in_memory_tools(self):
tool_names = [t["function"]["name"] for t in MEMORY_TOOLS]
assert "context_rewrite" in tool_names
def test_context_rewrite_has_required_params(self):
tool_def = None
for t in MEMORY_TOOLS:
if t["function"]["name"] == "context_rewrite":
tool_def = t
break
assert tool_def is not None
assert "summary" in tool_def["function"]["parameters"]["required"]
def test_context_rewrite_description_not_empty(self):
for t in MEMORY_TOOLS:
if t["function"]["name"] == "context_rewrite":
assert len(t["function"]["description"]) > 100
break
# ========== 执行器测试 ==========
class TestContextRewriteExecutor:
@pytest.fixture
def db(self):
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
db = EmbeddedGraphDB(db_path)
yield db
db.close()
os.unlink(db_path)
def test_valid_summary(self, db):
args = {"summary": "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\n\n- 查询人设图:未找到"}
result = execute_context_rewrite(db, args)
assert result["status"] == "success"
assert result["message"] == "上下文已压缩"
assert "memory_recall" in result["summary"]
def test_missing_marker(self, db):
args = {"summary": "查询人设图:未找到"}
result = execute_context_rewrite(db, args)
assert result["status"] == "error"
assert "必须包含" in result["message"]
def test_empty_summary(self, db):
args = {"summary": ""}
result = execute_context_rewrite(db, args)
assert result["status"] == "error"
def test_marker_only(self, db):
args = {"summary": "[工具调用总结"}
result = execute_context_rewrite(db, args)
assert result["status"] == "success"
def test_via_execute_tool(self, db):
args = {"summary": "[工具调用总结: 本次总结了 1 次工具调用 | 调用工具: memory_recall]\n\n- 查询记忆:找到 3 个实体"}
result_str = execute_tool(db, "context_rewrite", args)
result = json.loads(result_str)
assert result["status"] == "success"
def test_unicode_content(self, db):
args = {"summary": "[工具调用总结: 本次总结了 3 次工具调用 | 调用工具: memory_recall, memory_commit, task_create]\n\n- 查询:找到实体\"用户\"\n- 写入:{\"subject\": \"用户\", \"relation\": \"喜欢\"}\n- 任务Task_测试"}
result = execute_context_rewrite(db, args)
assert result["status"] == "success"
assert "用户" in result["summary"]
def test_newlines_preserved(self, db):
summary = "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\n\n- 查询1结果1\n- 查询2结果2"
args = {"summary": summary}
result = execute_context_rewrite(db, args)
assert result["summary"] == summary
def test_long_summary(self, db):
summary = "[工具调用总结: 本次总结了 5 次工具调用 | 调用工具: memory_recall, memory_recall, memory_commit, task_create, task_set_state]\n\n" + "详细结果\n" * 50
args = {"summary": summary}
result = execute_context_rewrite(db, args)
assert result["status"] == "success"
assert len(result["summary"]) == len(summary)
def test_special_json_chars(self, db):
args = {"summary": '[工具调用总结: 本次总结了 1 次工具调用 | 调用工具: memory_commit]\n\n- 写入:{"subject": "测试", "relation": "包含\"引号"}'}
result = execute_context_rewrite(db, args)
assert result["status"] == "success"
def test_missing_summary_key(self, db):
args = {}
result = execute_context_rewrite(db, args)
assert result["status"] == "error"
# ========== 工具限流器测试 ==========
class TestContextRewriteLimiter:
def test_classified_as_memory_query(self):
limiter = ToolLimiter(ToolLimits(memory_query_max=1))
category, operation = limiter._classify_tool("context_rewrite", {})
assert category == "memory"
assert operation == "query"
def test_counts_toward_memory_query_limit(self):
limiter = ToolLimiter(ToolLimits(memory_query_max=1))
allowed, _ = limiter.can_call("context_rewrite", {})
assert allowed
limiter.record_call("context_rewrite", {})
allowed, reason = limiter.can_call("context_rewrite", {})
assert not allowed
assert "一般记忆查询次数已达上限" in reason
def test_does_not_affect_memory_update(self):
limiter = ToolLimiter(ToolLimits(memory_update_max=1))
limiter.record_call("context_rewrite", {})
allowed, _ = limiter.can_call("memory_commit", {})
assert allowed
def test_reset_clears_count(self):
limiter = ToolLimiter(ToolLimits(memory_query_max=1))
limiter.record_call("context_rewrite", {})
limiter.reset()
allowed, _ = limiter.can_call("context_rewrite", {})
assert allowed
# ========== 集成测试messages_history 压缩流程 ==========
class TestMessagesHistoryCompression:
def test_compression_preserves_user_message(self):
messages_history = [
{"role": "user", "content": "我们之前聊过成语接龙吗?"},
{"role": "assistant", "content": None, "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "memory_recall", "arguments": '{"query_intent": "人设"}'}}]},
{"role": "tool", "tool_call_id": "tc1", "content": "===== 记忆检索结果 =====\n\n(未找到相关记忆)\n=============================="},
{"role": "assistant", "content": None, "tool_calls": [{"id": "tc2", "type": "function", "function": {"name": "memory_recall", "arguments": '{"query_intent": "工作记忆"}'}}]},
{"role": "tool", "tool_call_id": "tc2", "content": "===== 记忆检索结果 =====\n\n实体 (2 个):\n - Task_成语接龙 (类型: unknown, 提及: 1次)\n=============================="},
]
summary = "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\n\n- 查询人设图:未找到人设\n- 查询工作记忆链:发现 Task_成语接龙状态已暂停"
user_msg = messages_history[0]
messages_history[:] = [
user_msg,
{"role": "assistant", "content": summary}
]
assert len(messages_history) == 2
assert messages_history[0]["role"] == "user"
assert messages_history[0]["content"] == "我们之前聊过成语接龙吗?"
assert messages_history[1]["role"] == "assistant"
assert "memory_recall" in messages_history[1]["content"]
assert "成语接龙" in messages_history[1]["content"]
def test_compression_removes_json_noise(self):
messages_history = [
{"role": "user", "content": "查询用户信息"},
{"role": "assistant", "content": None, "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "memory_recall", "arguments": '{"query_intent": "用户"}'}}]},
{"role": "tool", "tool_call_id": "tc1", "content": json.dumps({"entities": [{"name": "用户", "type": "person", "mention_count": 5}], "relations": [{"source": "用户", "target": "Python", "type": "喜欢"}]})},
]
summary = "[工具调用总结: 本次总结了 1 次工具调用 | 调用工具: memory_recall]\n\n- 查询用户找到用户实体提及5次喜欢Python"
user_msg = messages_history[0]
messages_history[:] = [user_msg, {"role": "assistant", "content": summary}]
for msg in messages_history[1:]:
assert "entities" not in msg.get("content", "")
assert "relations" not in msg.get("content", "")
def test_compression_retains_tool_meta_cognition(self):
messages_history = [
{"role": "user", "content": "查询"},
{"role": "assistant", "content": None, "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "memory_recall", "arguments": "{}"}}]},
{"role": "tool", "tool_call_id": "tc1", "content": "结果"},
]
summary = "[工具调用总结: 本次总结了 1 次工具调用 | 调用工具: memory_recall]\n\n- 查询记忆:无结果"
user_msg = messages_history[0]
messages_history[:] = [user_msg, {"role": "assistant", "content": summary}]
content = messages_history[1]["content"]
assert "工具调用总结" in content
assert "memory_recall" in content
assert "1 次" in content
def test_multiple_compressions_in_sequence(self):
messages_history = [
{"role": "user", "content": "多轮查询"},
]
for i in range(3):
messages_history.append({"role": "assistant", "content": None, "tool_calls": [{"id": f"tc{i}", "type": "function", "function": {"name": "memory_recall", "arguments": "{}"}}]})
messages_history.append({"role": "tool", "tool_call_id": f"tc{i}", "content": f"结果{i}"})
summary = f"[工具调用总结: 本次总结了 {i+1} 次工具调用 | 调用工具: memory_recall]\n\n- 第{i+1}轮查询:结果{i}"
user_msg = messages_history[0]
messages_history[:] = [user_msg, {"role": "assistant", "content": summary}]
assert len(messages_history) == 2
assert messages_history[0]["content"] == "多轮查询"
assert "第3轮查询" in messages_history[1]["content"]

View File

@ -1,218 +0,0 @@
"""嵌入式数据库测试"""
import pytest
import tempfile
import os
from core import EmbeddedGraphDB
@pytest.fixture
def db():
"""创建临时数据库用于测试"""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
db = EmbeddedGraphDB(db_path)
yield db
db.close()
os.unlink(db_path)
def test_db_init(db):
"""测试数据库初始化"""
assert db.conn is not None
assert db.db_path.exists()
def test_commit_and_recall(db):
"""测试写入和检索记忆"""
result = db.commit(
triplets=[
{"subject": "用户", "relation": "喜欢", "object": "Python"},
{"subject": "用户", "relation": "正在学习", "object": "AI"}
],
session_id="test-session",
turn_id=1
)
assert result["created_entities"] >= 2
assert result["created_relations"] >= 2
def test_recall_with_keywords(db):
"""测试关键词检索"""
db.commit(
triplets=[
{"subject": "项目A", "relation": "使用技术", "object": "React"}
]
)
result = db.recall("React")
assert len(result["entities"]) > 0
def test_recall_empty_keywords(db):
"""测试空关键词检索"""
db.commit(
triplets=[
{"subject": "测试实体", "relation": "关系", "object": "测试对象"}
]
)
result = db.recall("")
assert len(result["entities"]) > 0
def test_purge_soft(db):
"""测试软删除"""
db.commit(
triplets=[
{"subject": "待删除", "relation": "测试", "object": "删除内容"}
]
)
result = db.purge(
criteria={"source": "待删除"},
mode="soft"
)
assert result["deleted"] >= 0
assert result["mode"] == "soft"
def test_introspect(db):
"""测试状态查看"""
db.commit(
triplets=[
{"subject": "实体1", "relation": "关系", "object": "实体2"}
]
)
result = db.introspect()
assert "entity_count" in result
assert "relation_count" in result
assert result["entity_count"] >= 1
def test_archive(db):
"""测试归档"""
result = db.archive(days=30)
assert "archived" in result
def test_cleanup_dry_run(db):
"""测试清理(预览模式)"""
result = db.cleanup(dry_run=True)
assert result["dry_run"] is True
assert "deleted_relations" in result
def test_multiple_triplets(db):
"""测试批量写入"""
result = db.commit(
triplets=[
{"subject": "实体A", "relation": "关系1", "object": "实体B"},
{"subject": "实体B", "relation": "关系2", "object": "实体C"},
{"subject": "实体C", "relation": "关系3", "object": "实体A"}
],
session_id="batch-test",
turn_id=1
)
assert result["created_entities"] >= 3
assert result["created_relations"] == 3
def test_entity_mention_count(db):
"""测试实体提及次数增加"""
db.commit(
triplets=[{"subject": "热门实体", "relation": "关系", "object": "对象1"}]
)
db.commit(
triplets=[{"subject": "热门实体", "relation": "关系", "object": "对象2"}]
)
result = db.recall("热门实体")
entity = next((e for e in result["entities"] if e["name"] == "热门实体"), None)
assert entity is not None
assert entity["mention_count"] >= 2
def test_close_and_context_manager():
"""测试关闭和上下文管理器"""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
try:
with EmbeddedGraphDB(db_path) as db:
db.commit(
triplets=[{"subject": "测试", "relation": "上下文", "object": "管理器"}]
)
assert db.conn is not None
with EmbeddedGraphDB(db_path) as db:
result = db.introspect()
assert result["entity_count"] >= 1
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_save_and_get_chat_records(db):
"""测试聊天记录保存和读取"""
messages = [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好,有什么可以帮你?"}
]
result = db.save_chat_records(messages)
assert result["saved"] == 2
history = db.get_chat_records()
assert len(history) == 2
assert history[0]["role"] == "user"
assert history[0]["content"] == "你好"
assert history[1]["role"] == "assistant"
def test_chat_records_limit_500(db):
"""测试聊天记录限制500条"""
for i in range(600):
db.save_chat_records([{"role": "user", "content": f"消息{i}"}])
history = db.get_chat_records()
assert len(history) == 500
def test_get_chat_records_default_limit(db):
"""测试默认limit参数"""
for i in range(100):
db.save_chat_records([{"role": "user", "content": f"msg{i}"}])
history_50 = db.get_chat_records(limit=50)
assert len(history_50) == 50
history_default = db.get_chat_records()
assert len(history_default) == 100
def test_clear_chat_records(db):
"""测试清空聊天记录"""
db.save_chat_records([
{"role": "user", "content": "测试1"},
{"role": "assistant", "content": "回复1"},
{"role": "user", "content": "测试2"},
])
history = db.get_chat_records()
assert len(history) == 3
result = db.clear_chat_records()
assert result["cleared"] is True
history_after = db.get_chat_records()
assert len(history_after) == 0

View File

@ -1,244 +0,0 @@
import pytest
import os
import tempfile
import time
import threading
import queue
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestPacketTypeEnum:
@pytest.mark.parametrize("packet_type,expected_value", [
("PROCESS_MESSAGE", "process_message"),
("EXECUTE_TOOL", "execute_tool"),
("GET_STATUS", "get_status"),
("GET_SETTINGS", "get_settings"),
("SET_SETTINGS", "set_settings"),
("GET_HISTORY", "get_history"),
("SAVE_HISTORY", "save_history"),
("SHUTDOWN", "shutdown"),
])
def test_packet_type_exists(self, packet_type, expected_value):
from core import PacketType
pt = getattr(PacketType, packet_type)
assert pt is not None
assert pt.value == expected_value
def test_packet_type_count(self):
from core import PacketType
assert len(list(PacketType)) == 8
class TestPacketCreation:
"""测试 Packet 创建"""
def test_packet_with_id_and_type(self):
from core import Packet, PacketType
packet = Packet(id="test-1", type=PacketType.PROCESS_MESSAGE, body={"user_input": "hello"})
assert packet.id == "test-1"
assert packet.type == PacketType.PROCESS_MESSAGE
def test_packet_body(self):
from core import Packet, PacketType
body = {"user_input": "test", "extra": "data"}
packet = Packet(id="test-2", type=PacketType.EXECUTE_TOOL, body=body)
assert packet.body == body
def test_packet_with_empty_body(self):
from core import Packet, PacketType
packet = Packet(id="test-3", type=PacketType.GET_STATUS, body={})
assert packet.body == {}
def test_packet_created_at_default(self):
from core import Packet, PacketType
before = time.time()
packet = Packet(id="test-4", type=PacketType.GET_SETTINGS, body={})
after = time.time()
assert before <= packet.created_at <= after
class TestPacketResponse:
"""测试 PacketResponse"""
def test_packet_response_success(self):
from core import PacketResponse
response = PacketResponse(id="resp-1", success=True, data={"result": "ok"})
assert response.id == "resp-1"
assert response.success is True
assert response.data == {"result": "ok"}
def test_packet_response_error(self):
from core import PacketResponse
response = PacketResponse(id="resp-2", success=False, error="error msg")
assert response.id == "resp-2"
assert response.success is False
assert response.error == "error msg"
class TestBackendServerCreation:
"""测试 BackendServer 创建"""
def test_backend_server_init(self):
from core import BackendServer
server = BackendServer(db_path=":memory:", use_embedded_db=True)
assert server._db_path == ":memory:"
assert server._use_embedded_db is True
assert server._graph is None
assert server._client is None
assert server._tool_limiter is None
def test_backend_server_default_params(self):
from core import BackendServer
server = BackendServer()
assert server._db_path == "graph_memory.db"
assert server._use_embedded_db is True
class TestBackendServerLifecycle:
"""测试 BackendServer 生命周期"""
def test_backend_server_start_stop(self):
from core import BackendServer
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
assert server._running is True
assert server._graph is not None
assert server._tool_limiter is not None
server.shutdown()
assert server._running is False
def test_backend_server_start_with_api_key(self):
from core import BackendServer
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="test-key", base_url="https://api.deepseek.com")
assert server._client is not None
assert server._config["api_key"] == "test-key"
server.shutdown()
class TestBackendClientCreation:
"""测试 BackendClient 创建"""
def test_backend_client_init(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
client = BackendClient(server)
assert client._server is server
assert client._counter == 0
class TestBackendClientAPI:
"""测试 BackendClient API"""
def test_get_status(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.get_status()
assert result.get("success") is True
data = result.get("data", {})
assert data.get("running") is True
server.shutdown()
def test_update_settings(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.update_settings(
api_config={"api_key": "new-key", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
tool_limits={"persona_update_max": 2}
)
assert result.get("success") is True
server.shutdown()
def test_get_settings(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="test-key")
client = BackendClient(server)
result = client.get_settings()
assert result.get("success") is True
data = result.get("data", {})
assert data.get("api_config", {}).get("api_key") == "test-key"
server.shutdown()
def test_process_message_no_api_key(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
# 无 API key 应该返回错误success 为 False
result = client.process_message("hello")
# 由于 API 调用失败success 应该是 False
assert result.get("success") is False
server.shutdown()
def test_execute_tool(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.execute_tool("memory_introspect", {})
assert result.get("success") is True
server.shutdown()
def test_clear_history(self):
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
# 先保存一些历史
client.save_history([
{"role": "user", "content": "test message 1"},
{"role": "assistant", "content": "test response 1"}
])
# 验证历史已保存
history = client.get_history()
assert len(history) >= 2
# 清空历史
result = client.clear_history()
assert result.get("status") == "history_cleared"
# 验证历史已清空
history = client.get_history()
assert len(history) == 0
server.shutdown()
def test_send_message_is_alias(self):
"""测试 send 方法是 process_message 的别名"""
from core import BackendServer, BackendClient
server = BackendServer(db_path=":memory:", use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
# send 方法应该等同于 process_message
# 由于没有真实 API key应该返回失败
result = client.send("test")
assert result.get("success") is False
server.shutdown()

View File

@ -1 +0,0 @@
"""Tests for integration layer"""

View File

@ -1,164 +0,0 @@
"""Integration tests - Packet flow, tool limiter, and error handling across layers."""
import pytest
import os
import tempfile
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestIntegrationPacketFlow:
"""测试 Packet 通信流程"""
def test_packet_round_trip_process_message(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.process_message("test message")
assert result.get("success") is False
assert "error" in result
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_config(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.update_settings(
api_config={"api_key": "test-api", "base_url": "https://test.com"},
)
assert result.get("success") is True
settings = client.get_settings()
data = settings.get("data", {})
assert data.get("api_config", {}).get("api_key") == "test-api"
assert data.get("api_config", {}).get("base_url") == "https://test.com"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_status(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.get_status()
assert result.get("success") is True
data = result.get("data", {})
assert data.get("running") is True
assert data.get("graph_initialized") is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_execute_tool(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.execute_tool("memory_introspect", {})
assert result.get("success") is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestIntegrationToolLimiter:
"""测试工具限制器集成"""
def test_external_tool_call_not_limited(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
for i in range(5):
result = client.execute_tool("memory_introspect", {})
assert result.get("success") is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_internal_tool_call_limited(self):
from core.tool_limiter import ToolLimiter, ToolLimits
limiter = ToolLimiter(ToolLimits(persona_update_max=1))
assert limiter.counts.persona_update == 0
limiter.record_call("persona_update", {})
assert limiter.counts.persona_update == 1
allowed, reason = limiter.can_call("persona_update", {})
assert allowed is False
assert "已达上限" in reason
class TestIntegrationErrorHandling:
"""测试错误处理"""
def test_process_message_returns_error_not_raise(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.process_message("hello")
assert result.get("success") is False
assert "error" in result
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_execute_tool_error_handling(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.execute_tool("nonexistent_tool", {})
data = result.get("data", {})
assert "未知工具" in data.get("result", "")
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)

View File

@ -1 +0,0 @@
"""Tests for UI layer"""

View File

@ -1,242 +0,0 @@
"""Tests for UI layer - models, widgets, handlers, services, and app initialization."""
import pytest
import os
import tempfile
from pathlib import Path
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestUIImport:
"""测试 UI 模块导入"""
def test_import_graphmemoryapp(self):
from ui import GraphMemoryApp
assert GraphMemoryApp is not None
def test_import_appconfig(self):
from ui import AppConfig
assert AppConfig is not None
def test_import_message(self):
from ui.models.message import Message, ToolCall, ToolResult
assert Message is not None
assert ToolCall is not None
assert ToolResult is not None
def test_import_config(self):
from ui.models.config import AppConfig
assert AppConfig is not None
def test_import_log_entry(self):
from ui.models.log_entry import LogEntry
assert LogEntry is not None
class TestAppConfig:
"""测试配置模型"""
def test_config_default_values(self):
from ui.models.config import AppConfig
config = AppConfig()
assert config.api_key == ""
assert config.model == "deepseek-chat"
assert config.base_url == "https://api.deepseek.com"
def test_config_from_env(self):
from ui.models.config import AppConfig
config = AppConfig.from_env()
assert "fake-test-key" in config.api_key
class TestMessageModel:
"""测试消息模型"""
def test_message_creation_user(self):
from ui.models.message import Message
from datetime import datetime
msg = Message(role="user", content="test content")
assert msg.role == "user"
assert msg.content == "test content"
assert isinstance(msg.timestamp, datetime)
def test_message_creation_assistant(self):
from ui.models.message import Message
msg = Message(role="assistant", content="assistant response")
assert msg.role == "assistant"
def test_message_with_tool_calls(self):
from ui.models.message import Message, ToolCall
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
msg = Message(role="assistant", content="response", tool_calls=[tc])
assert msg.tool_calls is not None
assert len(msg.tool_calls) == 1
class TestAppCSSPath:
"""测试 App CSS 配置"""
def test_app_has_css_path(self):
from ui import GraphMemoryApp
assert hasattr(GraphMemoryApp, 'CSS_PATH')
assert len(GraphMemoryApp.CSS_PATH) > 0
class TestAppBindings:
"""测试 App 快捷键"""
def test_app_has_bindings(self):
from ui import GraphMemoryApp
assert hasattr(GraphMemoryApp, 'BINDINGS')
assert len(GraphMemoryApp.BINDINGS) > 0
class TestWidgetImports:
"""测试组件导入"""
def test_import_left_panel(self):
from ui.widgets.left_panel import LeftPanel
assert LeftPanel is not None
def test_import_right_panel(self):
from ui.widgets.right_panel import RightPanel
assert RightPanel is not None
def test_import_input_box(self):
from ui.widgets.input_box import InputBox
assert InputBox is not None
def test_import_message_history(self):
from ui.widgets.message_history import MessageHistory
assert MessageHistory is not None
def test_import_status_bar(self):
from ui.widgets.status_bar import StatusBar
assert StatusBar is not None
class TestHandlerImports:
"""测试处理器导入"""
def test_import_focus_handler(self):
from ui.handlers.focus_handler import FocusHandler
assert FocusHandler is not None
def test_import_key_handler(self):
from ui.handlers.key_handler import KeyHandler
assert KeyHandler is not None
class TestServiceImports:
"""测试服务导入"""
def test_import_config_service(self):
from ui.services.config_service import ConfigService
assert ConfigService is not None
def test_import_config_manager(self):
from ui.services.config_manager import ConfigManager
assert ConfigManager is not None
class TestAppInitialization:
"""测试 App 初始化"""
def test_app_without_backend(self):
from ui import GraphMemoryApp
app = GraphMemoryApp()
assert app._backend_server is None
assert app._backend_client is None
def test_app_with_backend(self):
from ui import GraphMemoryApp
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
app = GraphMemoryApp(backend_server=server)
assert app._backend_server is server
assert app._backend_client is not None
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestUIWithBackendClient:
"""测试 UI 与后端通信"""
def test_app_sends_message_via_backend_client(self):
from ui import GraphMemoryApp
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
app = GraphMemoryApp(backend_server=server)
client = app._backend_client
status = client.get_status()
assert status.get("success") is True
result = client.update_settings(
api_config={"api_key": "sk-test", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
tool_limits={"persona_update_max": 1}
)
assert result.get("success") is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_ui_get_history(self):
from ui import GraphMemoryApp
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
app = GraphMemoryApp(backend_server=server)
client = app._backend_client
history = client.get_history()
assert isinstance(history, list)
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_ui_only_uses_backend_client(self):
from ui import GraphMemoryApp
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
app = GraphMemoryApp(backend_server=server)
# UI 不应该直接访问后端内部
assert hasattr(app, '_backend_client')
assert app._backend_client is not None
# 不应该有 _graph, _client 等直接访问
assert not hasattr(app, '_graph')
assert not hasattr(app, '_client')
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)

View File

@ -1,322 +0,0 @@
#!/usr/bin/env python3
"""
Draw an interactive relationship graph from TrulyMEM SQLite graph database using Plotly.
Output:
- Static image file (PNG by default)
Examples:
python tools/plotly_relationship_graph.py
python tools/plotly_relationship_graph.py --db-path ./graph_memory.db --output relation_graph.png
python tools/plotly_relationship_graph.py --include-non-active
python tools/plotly_relationship_graph.py --hide-edge-labels
"""
from __future__ import annotations
import argparse
import math
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple
@dataclass
class Entity:
id: int
name: str
entity_type: str
mention_count: int
@dataclass
class Relation:
source: str
target: str
relation_type: str
confidence: float
status: str
def resolve_default_db_path() -> Path:
project_db = Path.cwd() / "graph_memory.db"
if project_db.exists():
return project_db
return Path.home() / ".trulymem" / "graph_memory.db"
def load_graph(db_path: Path, include_non_active: bool) -> Tuple[Dict[str, Entity], List[Relation]]:
if not db_path.exists():
raise FileNotFoundError(f"Database not found: {db_path}")
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT id, name, COALESCE(type, 'unknown') AS entity_type, mention_count
FROM entities
ORDER BY mention_count DESC, name ASC
"""
)
entities: Dict[str, Entity] = {
row["name"]: Entity(
id=row["id"],
name=row["name"],
entity_type=row["entity_type"],
mention_count=int(row["mention_count"] or 1),
)
for row in cursor.fetchall()
}
sql = """
SELECT e1.name AS source,
e2.name AS target,
r.relation_type,
r.confidence,
r.status
FROM relations r
JOIN entities e1 ON r.source_id = e1.id
JOIN entities e2 ON r.target_id = e2.id
"""
if not include_non_active:
sql += " WHERE r.status = 'active'"
cursor.execute(sql)
relations = [
Relation(
source=row["source"],
target=row["target"],
relation_type=row["relation_type"],
confidence=float(row["confidence"] or 0.0),
status=row["status"],
)
for row in cursor.fetchall()
]
return entities, relations
finally:
conn.close()
def compute_degrees(entities: Dict[str, Entity], relations: List[Relation]) -> Tuple[Dict[str, int], Dict[str, int]]:
in_deg = {name: 0 for name in entities}
out_deg = {name: 0 for name in entities}
for rel in relations:
if rel.source in out_deg:
out_deg[rel.source] += 1
if rel.target in in_deg:
in_deg[rel.target] += 1
return in_deg, out_deg
def compute_positions(entities: Dict[str, Entity], in_deg: Dict[str, int], out_deg: Dict[str, int]) -> Dict[str, Tuple[float, float]]:
names = sorted(
entities.keys(),
key=lambda name: (-(in_deg[name] + out_deg[name]), -entities[name].mention_count, name),
)
n = len(names)
if n == 0:
return {}
radius = max(1.0, n / 8.0)
positions: Dict[str, Tuple[float, float]] = {}
for i, name in enumerate(names):
angle = (2.0 * math.pi * i) / n
x = radius * math.cos(angle)
y = radius * math.sin(angle)
positions[name] = (x, y)
return positions
def format_relation_label(rel: Relation) -> str:
return f"{rel.relation_type} ({rel.confidence:.2f}, {rel.status})"
def build_figure(
entities: Dict[str, Entity],
relations: List[Relation],
show_edge_labels: bool,
title: str,
):
try:
import plotly.graph_objects as go
except ImportError as exc:
raise RuntimeError("Plotly is not installed. Run: pip install plotly") from exc
in_deg, out_deg = compute_degrees(entities, relations)
positions = compute_positions(entities, in_deg, out_deg)
edge_x: List[float] = []
edge_y: List[float] = []
edge_label_x: List[float] = []
edge_label_y: List[float] = []
edge_label_text: List[str] = []
for rel in relations:
if rel.source not in positions or rel.target not in positions:
continue
x0, y0 = positions[rel.source]
x1, y1 = positions[rel.target]
edge_x.extend([x0, x1, None])
edge_y.extend([y0, y1, None])
if show_edge_labels:
edge_label_x.append((x0 + x1) / 2.0)
edge_label_y.append((y0 + y1) / 2.0)
edge_label_text.append(format_relation_label(rel))
edge_trace = go.Scatter(
x=edge_x,
y=edge_y,
line={"width": 0.8, "color": "#8899aa"},
hoverinfo="none",
mode="lines",
name="relations",
)
node_x: List[float] = []
node_y: List[float] = []
node_text: List[str] = []
node_size: List[float] = []
node_color: List[float] = []
node_names = sorted(entities.keys())
for name in node_names:
x, y = positions[name]
entity = entities[name]
total_degree = in_deg[name] + out_deg[name]
node_x.append(x)
node_y.append(y)
node_size.append(10 + min(entity.mention_count, 40) * 0.8)
node_color.append(float(total_degree))
node_text.append(
f"{name}<br>"
f"type: {entity.entity_type}<br>"
f"mentions: {entity.mention_count}<br>"
f"in: {in_deg[name]} | out: {out_deg[name]}"
)
node_trace = go.Scatter(
x=node_x,
y=node_y,
mode="markers+text",
text=node_names,
textposition="top center",
hoverinfo="text",
hovertext=node_text,
marker={
"showscale": True,
"colorscale": "YlGnBu",
"reversescale": False,
"color": node_color,
"size": node_size,
"colorbar": {"title": "Degree"},
"line": {"width": 1, "color": "#2f3b52"},
"opacity": 0.9,
},
name="entities",
)
traces = [edge_trace, node_trace]
if show_edge_labels and edge_label_text:
edge_label_trace = go.Scatter(
x=edge_label_x,
y=edge_label_y,
mode="text",
text=edge_label_text,
textfont={"size": 9, "color": "#2d3a4b"},
hoverinfo="none",
name="relation_labels",
)
traces.append(edge_label_trace)
fig = go.Figure(
data=traces,
layout=go.Layout(
title=title,
title_x=0.5,
showlegend=False,
hovermode="closest",
margin={"b": 20, "l": 10, "r": 10, "t": 50},
xaxis={"showgrid": False, "zeroline": False, "showticklabels": False},
yaxis={"showgrid": False, "zeroline": False, "showticklabels": False},
plot_bgcolor="#f8fafc",
paper_bgcolor="#ffffff",
),
)
rendered_nodes = set(node_names)
expected_nodes = set(entities.keys())
missing_nodes = expected_nodes - rendered_nodes
if missing_nodes:
preview = ", ".join(sorted(missing_nodes)[:10])
raise RuntimeError(
f"Node completeness check failed, missing {len(missing_nodes)} nodes: {preview}"
)
return fig
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Draw graph relations from SQLite with Plotly.")
parser.add_argument("--db-path", type=str, default=None, help="Path to graph_memory.db")
parser.add_argument("--output", type=str, default="relation_graph.png", help="Output image file path")
parser.add_argument("--title", type=str, default="TrulyMEM Relationship Graph", help="Chart title")
parser.add_argument("--include-non-active", action="store_true", help="Include archived/deleted relations")
parser.add_argument("--show-edge-labels", dest="show_edge_labels", action="store_true", help="Show relation text on edges")
parser.add_argument("--hide-edge-labels", dest="show_edge_labels", action="store_false", help="Hide relation text on edges")
parser.set_defaults(show_edge_labels=True)
parser.add_argument("--width", type=int, default=2200, help="Output image width in pixels")
parser.add_argument("--height", type=int, default=1400, help="Output image height in pixels")
parser.add_argument("--scale", type=float, default=1.0, help="Image scale factor")
return parser.parse_args()
def main() -> None:
args = parse_args()
db_path = Path(args.db_path) if args.db_path else resolve_default_db_path()
try:
entities, relations = load_graph(db_path, include_non_active=args.include_non_active)
except FileNotFoundError as exc:
print(f"[ERROR] {exc}")
return
if not entities:
print("[INFO] No entities found in database.")
return
try:
fig = build_figure(
entities=entities,
relations=relations,
show_edge_labels=args.show_edge_labels,
title=args.title,
)
except RuntimeError as exc:
print(f"[ERROR] {exc}")
return
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
fig.write_image(str(output_path), width=args.width, height=args.height, scale=args.scale)
except Exception as exc:
print(f"[ERROR] Failed to export image: {exc}")
print("[HINT] Install kaleido for static export: pip install kaleido")
return
print(f"Database: {db_path}")
print(f"Entities: {len(entities)}, Relations: {len(relations)}")
print(f"Nodes drawn: {len(entities)}/{len(entities)}")
print(f"Saved: {output_path}")
if __name__ == "__main__":
main()

View File

@ -1,789 +0,0 @@
"""
Web API 服务 - 将 Packet 协议映射为 RESTful API
"""
import sys
import os
import argparse
import threading
import time
import hashlib
from datetime import timedelta
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
from flask_cors import CORS
# 添加项目路径以便导入 core 模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from core.server import BackendServer, Packet, PacketType
from core.client import BackendClient
from core.activity_recorder import get_recorder
from core.embedded_db import EmbeddedGraphDB
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
def load_secret_key():
import json
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json')
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
file_config = json.load(f)
if "SECRET_KEY" in file_config:
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
return defaults
WEB_CONFIG = load_secret_key()
app = Flask(__name__, static_folder='static', static_url_path='', template_folder='templates')
app.secret_key = WEB_CONFIG["SECRET_KEY"]
app.permanent_session_lifetime = timedelta(days=7)
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
def login_required(f):
"""登录验证装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('authenticated'):
return redirect('/login')
return f(*args, **kwargs)
return decorated_function
def api_login_required(f):
"""API 登录验证装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('authenticated'):
return jsonify({"success": False, "error": "未登录"}), 401
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""管理员权限验证装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
username = session.get('username', '')
g_db = get_global_db()
if not g_db or not g_db.is_admin(username):
return jsonify({"success": False, "error": "权限不足,需要管理员权限"}), 403
return f(*args, **kwargs)
return decorated_function
@app.route('/')
@login_required
def index():
"""返回星图页面(默认首页)"""
return app.send_static_file('graph.html')
@app.route('/graph.html')
@login_required
def graph_html():
"""返回星图页面"""
return app.send_static_file('graph.html')
@app.route('/chat')
@login_required
def chat():
"""返回聊天页面"""
return app.send_static_file('index.html')
# 全局服务器和客户端实例
backend_server: BackendServer = None
backend_client: BackendClient = None
server_thread: threading.Thread = None
graph_db: EmbeddedGraphDB = None
global_db: EmbeddedGraphDB = None # 全局数据库(用于用户管理)
def get_global_db():
"""获取全局数据库实例"""
global global_db
if global_db is None:
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
if os.path.exists(global_db_path):
global_db = EmbeddedGraphDB(db_path=global_db_path)
return global_db
def create_server(username: str = ""):
"""创建并启动 BackendServer 后台线程"""
global backend_server, backend_client, server_thread, graph_db
backend_server = BackendServer(username=username)
backend_server.start()
backend_client = BackendClient(backend_server)
# 创建图数据库实例(连接用户的数据库文件)
graph_db = EmbeddedGraphDB(db_path=backend_server._db_path)
# 等待服务器初始化完成
time.sleep(0.5)
def reload_server_for_user(username: str):
"""为指定用户重新加载服务器"""
global backend_server, backend_client, graph_db
# 关闭旧的服务器
if backend_server:
backend_server.shutdown()
# 创建新的服务器(使用用户的数据库)
create_server(username=username)
@app.route('/login')
def login_page():
"""登录页面 - 如果没有用户则重定向到设置页"""
# 如果没有用户,重定向到首次设置页
users_count = 0
if graph_db:
users_count = graph_db.get_web_users_count()
if users_count == 0:
return redirect('/setup')
return render_template('login.html')
@app.route('/setup')
def setup_page():
"""首次设置页面 - 如果已有用户则跳转到登录页"""
has_users = False
if graph_db:
has_users = graph_db.get_web_users_count() > 0
if has_users:
return redirect('/login')
return render_template('setup.html')
@app.route('/settings')
@api_login_required
def settings_page():
"""Web 设置页面"""
return render_template('settings.html')
@app.route('/api/login', methods=['POST'])
def api_login():
"""登录接口"""
data = request.get_json() or {}
username = data.get('username', '')
password = data.get('password', '')
# 从全局数据库验证
g_db = get_global_db()
if g_db and g_db.verify_web_user(username, password):
session['authenticated'] = True
session['username'] = username # 存储用户名
session.permanent = True
# 重新加载服务器使用该用户的数据库
reload_server_for_user(username)
return jsonify({"success": True})
return jsonify({"success": False, "error": "用户名或密码错误"})
@app.route('/api/logout', methods=['POST'])
def api_logout():
"""登出接口"""
session.clear()
return jsonify({"success": True})
@app.route('/api/check-auth', methods=['GET'])
def check_auth():
"""检查登录状态"""
return jsonify({"authenticated": bool(session.get('authenticated'))})
@app.route('/api/userinfo', methods=['GET'])
def userinfo():
"""获取当前登录用户信息(含角色)"""
if not session.get('authenticated'):
return jsonify({"success": False, "error": "未登录"}), 401
username = session.get('username', '')
g_db = get_global_db()
if not g_db:
return jsonify({"success": False, "error": "数据库未初始化"}), 500
user = g_db.get_web_user(username)
if not user:
return jsonify({"success": False, "error": "用户不存在"}), 404
return jsonify({
"success": True,
"username": user['username'],
"role": user.get('role', 'user'),
"is_admin": user.get('role') == 'admin',
"created_at": user.get('created_at')
})
@app.route('/api/web-check', methods=['GET'])
def web_check():
"""检查是否需要首次设置,返回是否配置完成"""
users_count = 0
if graph_db:
users_count = graph_db.get_web_users_count()
return jsonify({
"needs_setup": users_count == 0,
"users_count": users_count
})
@app.route('/api/setup', methods=['POST'])
def api_setup():
"""首次设置 - 创建初始管理员用户"""
# 只有没有任何用户时才允许设置
g_db = get_global_db()
if g_db and g_db.get_web_users_count() > 0:
return jsonify({"success": False, "error": "用户已存在,不允许重复设置"}), 400
data = request.get_json() or {}
username = data.get('username', '')
password = data.get('password', '')
confirm = data.get('confirm_password', '')
if not username or not password:
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
if password != confirm:
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
if len(password) < 6:
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
# 使用全局数据库创建用户
if g_db is None:
# 如果全局数据库不存在,创建它
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
g_db = EmbeddedGraphDB(db_path=global_db_path)
global global_db
global_db = g_db
result = g_db.set_web_user(username, password)
if result.get("success"):
# 设置完成后自动登录
session['authenticated'] = True
session['username'] = username
session.permanent = True
return jsonify({"success": True, "message": "用户创建成功"})
return jsonify({"success": False, "error": "创建用户失败"}), 500
@app.route('/api/change-password', methods=['POST'])
@api_login_required
def api_change_password():
"""修改 Web 登录密码"""
data = request.get_json() or {}
current_password = data.get('current_password', '')
new_password = data.get('new_password', '')
confirm_password = data.get('confirm_password', '')
if not new_password:
return jsonify({"success": False, "error": "新密码不能为空"}), 400
if new_password != confirm_password:
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
if len(new_password) < 6:
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
# 获取当前登录用户
current_username = session.get('username', '')
if not current_username:
return jsonify({"success": False, "error": "无法识别当前用户"}), 400
# 验证当前密码(使用全局数据库)
g_db = get_global_db()
if not g_db or not g_db.verify_web_user(current_username, current_password):
return jsonify({"success": False, "error": "当前密码错误"}), 400
result = g_db.set_web_user(current_username, new_password)
if result.get("success"):
return jsonify({"success": True, "message": "密码已更新"})
return jsonify({"success": False, "error": "修改密码失败"}), 500
# ========== 管理员 API ==========
@app.route('/api/admin/users', endpoint='api_admin_get_users', methods=['GET'])
@api_login_required
@admin_required
def api_admin_get_users():
"""获取用户列表"""
g_db = get_global_db()
if not g_db:
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
users = g_db.get_web_users()
return jsonify({"success": True, "users": users})
@app.route('/api/admin/users', endpoint='api_admin_add_user', methods=['POST'])
@api_login_required
@admin_required
def api_admin_add_user():
"""管理员添加用户"""
data = request.get_json() or {}
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
if len(password) < 6:
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
g_db = get_global_db()
if not g_db:
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
result = g_db.set_web_user(username, password)
if result.get("success"):
return jsonify({"success": True, "message": "用户添加成功", "user": result})
return jsonify({"success": False, "error": "添加用户失败"}), 500
@app.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
@api_login_required
@admin_required
def api_admin_delete_user(user_id):
"""管理员删除用户"""
g_db = get_global_db()
if not g_db:
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
# 获取所有用户
users = g_db.get_web_users()
# 查找要删除的用户
target_user = None
for u in users:
if u['id'] == user_id:
target_user = u
break
if not target_user:
return jsonify({"success": False, "error": "用户不存在"}), 404
# 不能删除自己
current_username = session.get('username', '')
if target_user['username'] == current_username:
return jsonify({"success": False, "error": "不能删除当前登录的用户"}), 400
# 不能删除最后一个 admin
admin_count = sum(1 for u in users if u.get('role') == 'admin')
if target_user.get('role') == 'admin' and admin_count <= 1:
return jsonify({"success": False, "error": "不能删除最后一个管理员"}), 400
# 删除用户(保留文件目录)
result = g_db.delete_web_user(target_user['username'])
if not result.get('success'):
return jsonify({"success": False, "error": result.get('error', '删除失败')}), 500
return jsonify({"success": True, "message": "用户已删除"})
@app.route('/api/admin/migrate-check', methods=['GET'])
def api_admin_migrate_check():
"""检测系统是否需要迁移"""
from core.migrate import need_migration, is_migrated
return jsonify({
"success": True,
"need_migration": need_migration(),
"is_migrated": is_migrated()
})
@app.route('/api/admin/migrate', methods=['POST'])
def api_admin_migrate():
"""执行迁移+创建首个用户"""
from core.migrate import need_migration, run_migration
if not need_migration():
return jsonify({"success": False, "error": "不需要迁移"}), 400
data = request.get_json() or {}
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
if len(password) < 6:
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
result = run_migration(username, password)
if result.get("success"):
# 自动登录
session['authenticated'] = True
session['username'] = username
session.permanent = True
# 重新加载服务器
reload_server_for_user(username)
return jsonify({"success": True, "message": "迁移完成", "data": result})
return jsonify({"success": False, "error": result.get("error", "迁移失败")}), 500
@app.route('/api/settings/config', methods=['GET', 'POST', 'PUT'])
@api_login_required
def web_settings_config():
"""获取/更新当前登录用户的配置"""
if request.method == 'GET':
settings = {}
if backend_client:
result = backend_client.get_settings()
settings = result.get("data", {}) if isinstance(result, dict) else result
if isinstance(settings, dict):
settings = settings.get("api_config", {}) if "api_config" in settings else settings
# 从 settings 中提取相关字段
return jsonify({
"success": True,
"enable_web": settings.get("enable_web", False),
"web_port": settings.get("web_port", 4096),
"enable_tui": settings.get("enable_tui", True),
})
# PUT/POST 更新
data = request.get_json() or {}
enable_tui = data.get('enable_tui')
if enable_tui is not None and backend_client:
# 获取当前配置,合并更新
current = backend_client.get_settings()
current_data = current.get("data", {}) if isinstance(current, dict) else {}
tool_limits = current_data.get("tool_limits", {})
api_config = current_data.get("api_config", {})
api_config["enable_tui"] = bool(enable_tui)
result = backend_client.update_settings(api_config, tool_limits)
return jsonify({"success": True, "enable_tui": bool(enable_tui)})
return jsonify({"success": False, "error": "没有需要更新的配置"}), 400
@app.errorhandler(404)
def not_found(e):
"""404 处理"""
return jsonify({
"success": False,
"error": "404 Not Found"
}), 404
@app.errorhandler(500)
def server_error(e):
"""500 处理"""
return jsonify({
"success": False,
"error": str(e.original_exception if hasattr(e, 'original_exception') else e)
}), 500
@app.route('/api/message', methods=['POST'])
@api_login_required
def process_message():
"""发送消息给 AI - PROCESS_MESSAGE"""
data = request.get_json() or {}
user_input = data.get('message', '')
if not user_input:
return jsonify({
"success": False,
"error": "message 参数不能为空"
}), 400
result = backend_client.process_message(user_input)
return jsonify(result)
@app.route('/api/tools/execute', methods=['POST'])
@api_login_required
def execute_tool():
"""直接执行工具 - EXECUTE_TOOL"""
data = request.get_json() or {}
tool_name = data.get('tool_name', '')
arguments = data.get('arguments', {})
if not tool_name:
return jsonify({
"success": False,
"error": "tool_name 参数不能为空"
}), 400
result = backend_client.execute_tool(tool_name, arguments)
return jsonify(result)
@app.route('/api/status', methods=['GET'])
@api_login_required
def get_status():
"""获取状态 - GET_STATUS"""
result = backend_client.get_status()
return jsonify(result)
@app.route('/api/settings', methods=['GET'])
@api_login_required
def get_settings():
"""获取配置 - GET_SETTINGS"""
result = backend_client.get_settings()
return jsonify(result)
@app.route('/api/settings', methods=['PUT'])
@api_login_required
def set_settings():
"""更新配置 - SET_SETTINGS"""
data = request.get_json() or {}
api_config = data.get('api_config', {})
tool_limits = data.get('tool_limits', {})
result = backend_client.update_settings(api_config, tool_limits)
return jsonify(result)
@app.route('/api/history', methods=['GET'])
@api_login_required
def get_history():
"""获取历史 - GET_HISTORY"""
result = backend_client.get_history()
return jsonify({"success": True, "history": result})
@app.route('/api/history', methods=['DELETE'])
@api_login_required
def clear_history():
"""清空历史 - SAVE_HISTORY(空)"""
result = backend_client.clear_history()
return jsonify(result)
@app.route('/api/shutdown', methods=['POST'])
@api_login_required
def shutdown():
"""关闭服务器 - SHUTDOWN"""
backend_client.shutdown()
return jsonify({"success": True, "status": "shutdown"})
@app.route('/api/activity', methods=['GET'])
@api_login_required
def get_activity():
"""获取当前轮的数据库操作记录"""
recorder = get_recorder()
records = recorder.get_all()
summary = recorder.get_summary()
return jsonify({
"success": True,
"data": {
"records": records,
"summary": summary
}
})
@app.route('/api/graph', methods=['GET'])
@api_login_required
def get_graph():
"""返回全量图数据"""
global graph_db
if graph_db is None:
return jsonify({
"success": False,
"error": "图数据库未初始化"
}), 500
cursor = graph_db.conn.cursor()
# 查询实体(节点)
cursor.execute("""
SELECT id, name, type, mention_count
FROM entities
ORDER BY mention_count DESC
LIMIT 200
""")
nodes = []
for row in cursor.fetchall():
nodes.append({
"id": row['id'],
"name": row['name'],
"type": str(row['type'] or 'unknown'),
"mention_count": row['mention_count']
})
# 查询关系(边)
cursor.execute("""
SELECT r.id, r.source_id, r.target_id, r.relation_type, r.confidence, r.status
FROM relations r
WHERE r.status = 'active'
""")
edges = []
for row in cursor.fetchall():
edges.append({
"id": row['id'],
"source": row['source_id'],
"target": row['target_id'],
"relation_type": row['relation_type'],
"confidence": row['confidence'],
"status": row['status']
})
return jsonify({
"success": True,
"nodes": nodes,
"edges": edges,
"stats": {
"node_count": len(nodes),
"edge_count": len(edges)
}
})
@app.route('/api/graph/highlight', methods=['GET'])
@api_login_required
def get_graph_highlight():
"""返回需要高亮的节点ID列表"""
recorder = get_recorder()
records = recorder.get_all()
highlight_ids = []
new_node_id = None
new_edge = None
if graph_db is None:
return jsonify({
"success": False,
"data": {
"highlight_ids": [],
"new_node_id": None,
"new_edge": None
}
})
# 从最近的记录中提取实体ID
for record in records[-10:]: # 只看最近10条记录
entity_name = record.get('entity', '')
if entity_name:
cursor = graph_db.conn.cursor()
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
row = cursor.fetchone()
if row:
highlight_ids.append(row['id'])
# 检查是否有新创建的节点
if record.get('action') == 'create' and entity_name:
cursor = graph_db.conn.cursor()
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
row = cursor.fetchone()
if row:
new_node_id = row['id']
# 检查是否有删除的节点
deleted_node_ids = []
for record in records[-10:]:
if record.get('action') == 'delete':
entity_name = record.get('entity', '')
if entity_name:
try:
cursor = graph_db.conn.cursor()
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
row = cursor.fetchone()
if row:
deleted_node_ids.append(row['id'])
except Exception:
pass # 实体可能已被删除,忽略错误
# 去重
highlight_ids = list(set(highlight_ids))
deleted_node_ids = list(set(deleted_node_ids))
return jsonify({
"success": True,
"highlight_ids": highlight_ids,
"new_node_id": new_node_id,
"new_edge": new_edge,
"deleted_node_ids": deleted_node_ids
})
# ── 可被 TUI 作为线程启动 ──────────────────────────────────────────────────
_web_thread: threading.Thread | None = None
_http_server = None # werkzeug.serving.BaseWSGIServer 引用,用于优雅停止
def run_web_server(port: int = 4096, host: str = '0.0.0.0') -> None:
"""在后台线程启动 Flask供 TUI 在进程中直接调用"""
global _web_thread, _http_server
if _http_server is not None:
return # 已在运行
def _start():
global _http_server
try:
from werkzeug.serving import make_server
_http_server = make_server(host, port, app, threaded=True)
print(f"Web API 服务启动在 http://{host}:{port}")
_http_server.serve_forever()
except Exception as e:
print(f"Web 服务启动失败: {e}")
_http_server = None
finally:
_http_server = None
_web_thread = threading.Thread(target=_start, daemon=True)
_web_thread.start()
def stop_web_server() -> None:
"""停止 Web 服务线程"""
global _http_server, _web_thread
if _http_server:
try:
_http_server.shutdown()
except Exception:
pass
_http_server = None
_web_thread = None
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='TrulyMEM Web API 服务')
parser.add_argument('--port', type=int, default=5000, help='服务端口 (默认: 5000)')
args = parser.parse_args()
# 启动后端服务器
print("正在启动 BackendServer...")
create_server()
print("BackendServer 已启动")
# 启动 Flask 应用
print(f"Web API 服务启动在 http://0.0.0.0:{args.port}")
app.run(host='0.0.0.0', port=args.port, debug=False)

View File

@ -1,6 +0,0 @@
{
"SECRET_KEY": "change-this-to-a-random-secret-key",
"USERS": {
"admin": "SHA256_OF_YOUR_PASSWORD"
}
}