From dd685485b5026805de19dbca4c255d8bc32539a4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 14 Apr 2026 13:20:24 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=8D=E6=9E=84=E4=B8=BA=E5=8F=8C?= =?UTF-8?q?=E8=AF=AD=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=BA=E8=AE=BE=E5=9B=BE=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拆分为 docs/(CN) 和 docs_en/(EN) 两个文件夹 - 主 README 索引对应语言文档 - 新增 persona.md (人设图机制详细说明) - 文件重命名为英文名 --- README.md | 11 +- README_EN.md | 14 +- core/client.py | 16 ++ core/server.py | 53 ++++- docs/README.md | 24 +- docs/README_EN.md | 40 ---- docs/{架构.md => architecture.md} | 0 docs/{记忆机制.md => memory.md} | 0 docs/persona.md | 214 ++++++++++++++++++ docs/{一键启动指南.md => quick_start.md} | 0 ...作记忆链机制说明.md => working_memory.md} | 0 docs_en/README.md | 29 +++ docs/api_EN.md => docs_en/api.md | 0 docs/架构_EN.md => docs_en/architecture.md | 0 docs/记忆机制_EN.md => docs_en/memory.md | 0 docs_en/persona.md | 214 ++++++++++++++++++ docs/prompts_EN.md => docs_en/prompts.md | 0 .../quick_start.md | 0 .../working_memory.md | 0 tests/test_core/test_packet.py | 4 +- ui/app.py | 58 ++++- ui/models/config.py | 21 +- ui/widgets/config_section.py | 87 +++++-- 23 files changed, 676 insertions(+), 109 deletions(-) delete mode 100644 docs/README_EN.md rename docs/{架构.md => architecture.md} (100%) rename docs/{记忆机制.md => memory.md} (100%) create mode 100644 docs/persona.md rename docs/{一键启动指南.md => quick_start.md} (100%) rename docs/{工作记忆链机制说明.md => working_memory.md} (100%) create mode 100644 docs_en/README.md rename docs/api_EN.md => docs_en/api.md (100%) rename docs/架构_EN.md => docs_en/architecture.md (100%) rename docs/记忆机制_EN.md => docs_en/memory.md (100%) create mode 100644 docs_en/persona.md rename docs/prompts_EN.md => docs_en/prompts.md (100%) rename docs/一键启动指南_EN.md => docs_en/quick_start.md (100%) rename docs/工作记忆链_EN.md => docs_en/working_memory.md (100%) diff --git a/README.md b/README.md index f439dd7..e837ea2 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,13 @@ python trulymem_entry.py | 文档 | 内容 | |------|------| -| [docs/架构.md](docs/架构.md) | 系统架构、技术设计、数据包协议 | -| [docs/一键启动指南.md](docs/一键启动指南.md) | 完整启动指南与配置说明 | -| [docs/工作记忆链机制说明.md](docs/工作记忆链机制说明.md) | 连续性任务处理机制 | -| [docs/api.md](docs/api.md) | BackendServer API 接口(供扩展开发) | +| [docs/architecture.md](docs/architecture.md) | 系统架构和技术设计 | +| [docs/quick_start.md](docs/quick_start.md) | 完整启动指南与配置说明 | +| [docs/memory.md](docs/memory.md) | 内部记忆工作机制 | +| [docs/persona.md](docs/persona.md) | 人设图机制 | +| [docs/working_memory.md](docs/working_memory.md) | 连续性任务处理机制 | +| [docs/api.md](docs/api.md) | 后端 API 接口(供扩展开发) | +| [docs/prompts.md](docs/prompts.md) | 提示词管理模块 | --- diff --git a/README_EN.md b/README_EN.md index 8c439c2..dd5cb17 100644 --- a/README_EN.md +++ b/README_EN.md @@ -64,15 +64,17 @@ python trulymem_entry.py ## Documentation Index -Detailed technical documentation in the [docs/](docs/) directory: +Detailed technical documentation in the [docs_en/](docs_en/) directory: | Document | Content | |----------|---------| -| [docs/架构_EN.md](docs/架构_EN.md) | System architecture, technical design, Packet protocol | -| [docs/一键启动指南_EN.md](docs/一键启动指南_EN.md) | Complete startup guide and configuration | -| [docs/记忆机制_EN.md](docs/记忆机制_EN.md) | Internal memory working mechanism | -| [docs/工作记忆链_EN.md](docs/工作记忆链_EN.md) | Continuous task handling mechanism | -| [docs/api_EN.md](docs/api_EN.md) | BackendServer API (for extension development) | +| [docs_en/architecture.md](docs_en/architecture.md) | System architecture and technical design | +| [docs_en/quick_start.md](docs_en/quick_start.md) | Complete startup guide and configuration | +| [docs_en/memory.md](docs_en/memory.md) | Internal memory working mechanism | +| [docs_en/persona.md](docs_en/persona.md) | Persona Graph mechanism | +| [docs_en/working_memory.md](docs_en/working_memory.md) | Continuous task handling mechanism | +| [docs_en/api.md](docs_en/api.md) | BackendServer API (for extension development) | +| [docs_en/prompts.md](docs_en/prompts.md) | Prompt management module | --- diff --git a/core/client.py b/core/client.py index 592dc8a..31eeacc 100644 --- a/core/client.py +++ b/core/client.py @@ -55,6 +55,22 @@ class BackendClient: ) return self._server.send(packet).body + def get_tool_limits(self) -> Dict: + packet = Packet( + id=self._next_id(), + type=PacketType.GET_TOOL_LIMITS, + body={} + ) + return self._server.send(packet).body + + def update_tool_limits(self, **kwargs) -> Dict: + packet = Packet( + id=self._next_id(), + type=PacketType.SET_TOOL_LIMITS, + body=kwargs + ) + return self._server.send(packet).body + def save_history(self, messages: list) -> None: packet = Packet( id=self._next_id(), diff --git a/core/server.py b/core/server.py index 375746c..c72cfaa 100644 --- a/core/server.py +++ b/core/server.py @@ -17,6 +17,8 @@ class PacketType(Enum): GET_STATUS = "get_status" GET_CONFIG = "get_config" SET_CONFIG = "set_config" + GET_TOOL_LIMITS = "get_tool_limits" + SET_TOOL_LIMITS = "set_tool_limits" GET_HISTORY = "get_history" SAVE_HISTORY = "save_history" SHUTDOWN = "shutdown" @@ -59,6 +61,14 @@ class BackendServer: self._lock = threading.Lock() self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"} + self._tool_limits = { + "persona_query_max": 1, + "persona_update_max": 1, + "task_query_max": 4, + "task_update_max": 2, + "memory_query_max": 20, + "memory_update_max": 10, + } self._message_history: list = [] def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None: @@ -96,17 +106,29 @@ class BackendServer: with open(self._config_file, 'r') as f: saved = json.load(f) self._config.update(saved) + for key in self._tool_limits: + if key in saved: + self._tool_limits[key] = saved[key] except Exception: pass def _save_config(self) -> None: self._config_file.parent.mkdir(parents=True, exist_ok=True) + saved_data = {**self._config, **self._tool_limits} with open(self._config_file, 'w') as f: - json.dump(self._config, f, indent=2) + json.dump(saved_data, f, indent=2) def _create_tool_limiter(self): - from .tool_limiter import ToolLimiter - return ToolLimiter() + from .tool_limiter import ToolLimiter, ToolLimits + limits = ToolLimits( + persona_query_max=self._tool_limits.get("persona_query_max", 1), + persona_update_max=self._tool_limits.get("persona_update_max", 1), + task_query_max=self._tool_limits.get("task_query_max", 4), + task_update_max=self._tool_limits.get("task_update_max", 2), + memory_query_max=self._tool_limits.get("memory_query_max", 20), + memory_update_max=self._tool_limits.get("memory_update_max", 10), + ) + return ToolLimiter(limits) def _init_graph(self) -> None: if self._use_embedded_db: @@ -142,6 +164,10 @@ class BackendServer: response_body = self._handle_get_config() elif packet.type == PacketType.SET_CONFIG: response_body = self._handle_set_config(packet.body) + elif packet.type == PacketType.GET_TOOL_LIMITS: + response_body = self._handle_get_tool_limits() + elif packet.type == PacketType.SET_TOOL_LIMITS: + response_body = self._handle_set_tool_limits(packet.body) elif packet.type == PacketType.GET_HISTORY: response_body = self._handle_get_history() elif packet.type == PacketType.SAVE_HISTORY: @@ -295,6 +321,27 @@ class BackendServer: self._save_config() return {"status": "config_updated"} + def _handle_get_tool_limits(self) -> Dict: + return self._tool_limits.copy() + + def _handle_set_tool_limits(self, body: Dict) -> Dict: + limits_keys = [ + "persona_query_max", "persona_update_max", + "task_query_max", "task_update_max", + "memory_query_max", "memory_update_max" + ] + for key in limits_keys: + if key in body: + value = int(body[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": "tool_limits_updated", "limits": self._tool_limits.copy()} + def _handle_get_history(self) -> Dict: return {"history": self._message_history} diff --git a/docs/README.md b/docs/README.md index a7e325b..7dc811d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,23 +6,13 @@ | 文档 | 内容 | |------|------| -| [架构设计](架构.md) | 系统架构和技术设计 | -| [快速开始](一键启动指南.md) | 完整启动指南与配置说明 | -| [记忆机制](记忆机制.md) | 内部记忆工作机制、人设图、工作记忆链 | -| [工作记忆链](工作记忆链机制说明.md) | 连续性任务处理机制 | -| [BackendServer API](api.md) | 后端 API 接口文档(供扩展开发) | -| [提示词管理](prompts.md) | 提示词管理模块 | - -## English Documentation - -| Document | Content | -|----------|---------| -| [Architecture](架构_EN.md) | System architecture and technical design | -| [Quick Start](一键启动指南_EN.md) | Complete startup guide and configuration | -| [Memory Mechanism](记忆机制_EN.md) | Internal memory working mechanism | -| [Working Memory Chain](工作记忆链_EN.md) | Continuous task handling | -| [BackendServer API](api_EN.md) | Backend API reference | -| [Prompt Manager](prompts_EN.md) | Prompt management module | +| [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) | 提示词管理模块 | ## 项目简介 diff --git a/docs/README_EN.md b/docs/README_EN.md deleted file mode 100644 index 3ee568c..0000000 --- a/docs/README_EN.md +++ /dev/null @@ -1,40 +0,0 @@ -# TrulyMEM Documentation - -Welcome to the TrulyMEM project documentation. - -## Documentation Index - -- [Architecture](架构_EN.md) - System architecture and technical design -- [Quick Start](一键启动指南_EN.md) - Quick start guide -- [Memory Mechanism](记忆机制_EN.md) - Internal memory working mechanism -- [Working Memory Chain](工作记忆链_EN.md) - Continuous task handling -- [BackendServer API](api_EN.md) - Backend API reference (for extension development) -- [Prompt Manager](prompts_EN.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 - ---- - -## Story - -Industry believes that LLMs' massive parameters give them emergent intelligence. But this intelligence is "dead" — it cannot truly remember, nor understand the concept of "remembering". Everything it outputs is the probabilistic optimal solution calculated through countless forward passes on the current input text. The LLM cannot correct its weights based on errors in a conversation, nor perform backward passes. Its consciousness is frozen — what appears as intelligence is merely the echo of this frozen consciousness. - -Current "memory systems" merely externalize memory, letting the "system" remember for the LLM. Or they dump all context text to the LLM. This is a waste of the model's limited input context. - -**TrulyMEM asks: since the LLM cannot correct model weights in real-time, why not give the memory authority back to the LLM?** - -We provide a series of mechanisms for the LLM to decide what to remember, what to forget, what's important, what's trivial. The LLM's reasoning process is also its thinking and recalling process. Abandoning the traditional messages array context, all memories are stored as **triplets (graph)** in the graph database. When the LLM thinks, it can autonomously jump through graph links to associate related relationships, enabling natural association and recall. - -Give the LLM true memory. \ No newline at end of file diff --git a/docs/架构.md b/docs/architecture.md similarity index 100% rename from docs/架构.md rename to docs/architecture.md diff --git a/docs/记忆机制.md b/docs/memory.md similarity index 100% rename from docs/记忆机制.md rename to docs/memory.md diff --git a/docs/persona.md b/docs/persona.md new file mode 100644 index 0000000..74195bd --- /dev/null +++ b/docs/persona.md @@ -0,0 +1,214 @@ +# TrulyMEM 人设图机制 + +本文档详细说明 TrulyMEM 的人设图(Persona Graph)工作机制。 + +## 概述 + +人设图是 TrulyMEM 的核心机制之一,用于维护 AI 的角色、性格、语气等属性。与传统 AI 不同,TrulyMEM 的人设是可持久化、可动态切换的,存储在图数据库中。 + +## 核心概念 + +### 人设节点(PersonaNode) + +存储 AI 的角色属性: + +| 属性 | 说明 | 示例 | +|------|------|------| +| 扮演角色 | AI 当前扮演的角色 | 猫娘、教师、助手 | +| 说话风格 | 语气特点 |可爱、严肃、专业 | +| 性格特点 | 性格描述 | 活泼、严谨、耐心 | +| 口头禅 | 习惯用语 | 喵呜~、明白了 | +| 背景故事 | 角色背景设定 | 来自星海的猫娘 | + +### 人设边(Edge) + +| 边类型 | 说明 | 连接关系 | +|------|------|----------| +| `HAS_PERSONA` | 人设 | AI → PersonaNode | + +--- + +## 强制查询机制 + +### 每轮对话必须执行 + +根据 `system_prompt.md`,每轮对话**必须**首先查询人设图: + +```python +memory_recall( + query_intent="AI,人设,角色,性格,语气,说话风格", + depth=2 +) +``` + +**处理逻辑:** +- 找到人设 → 严格按照人设的语气、风格、特征回复 +- 未找到 → 使用默认 TrulyMEM 身份 + +### 人设优先级 + +- **人设优先级 > 默认身份** +- 每句话都符合人设的语气、风格、特征 +- 绝不主动跳出角色,除非用户明确要求 + +--- + +## 工具 + +### persona_update + +更新人设。修改 AI 的角色、性格、语气等属性。 + +**参数:** + +| 参数 | 类型 | 说明 | 必填 | +|------|------|------|------| +| `attributes` | array | 人设属性列表 | ✅ | +| `mode` | string | replace=替换, merge=合并 | ❌ | + +**attributes 子参数:** + +| 子参数 | 说明 | +|------|------| +| `attribute` | 属性名(扮演角色、说话风格、性格特点、口头禅、背景故事) | +| `value` | 属性值 | + +**示例 - 切换为猫娘角色:** + +```python +persona_update( + attributes=[ + {"attribute": "扮演角色", "value": "猫娘"}, + {"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"}, + {"attribute": "性格特点", "value": "活泼、粘人、忠诚"} + ], + mode="replace" +) +``` + +**示例 - 添加新属性(保留现有属性):** + +```python +persona_update( + attributes=[ + {"attribute": "口头禅", "value": "喵呜~"} + ], + mode="merge" +) +``` + +**示例 - 设置专业角色:** + +```python +persona_update( + attributes=[ + {"attribute": "扮演角色", "value": "Python专家"}, + {"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"}, + {"attribute": "性格特点", "value": "严谨、耐心、乐于助人"} + ], + mode="replace" +) +``` + +### persona_clear + +清除人设。删除 AI 的角色设定,恢复默认身份。 + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `confirm` | boolean | true | 确认清除 | + +--- + +## 更新流程 + +### 用户要求角色扮演时 + +1. 使用 `persona_update` 更新人设 +2. 立即按照新人设回复 + +### 用户要求恢复默认身份时 + +1. 使用 `persona_clear` 清除人设 +2. 恢复为 TrulyMEM 默认身份 + +--- + +## 对话示例 + +### 示例 1:切换角色 + +``` +用户: 你好,我想让你扮演一只猫娘 + +AI: +1. 调用 persona_update: + { + "attributes": [ + {"attribute": "扮演角色", "value": "猫娘"}, + {"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"}, + {"attribute": "性格特点", "value": "活泼、粘人、忠诚"} + ], + "mode": "replace" + } +2. 调用 memory_commit 存储人设到图数据库 +3. 回复: "好的喵!主人你好呀~我是主人的猫娘,有什么需要帮忙的吗喵?" +``` + +### 示例 2:保持角色一致性 + +``` +用户: 今天的天气怎么样? + +AI: 查询人设图 → 获取当前人设(猫娘) +回复: "喵~主人,今天天气很好呢喵!阳光明媚,很适合出门散步哦~" +``` + +### 示例 3:恢复默认身份 + +``` +用户: 好了,恢复正常吧 + +AI: +1. 调用 persona_clear(confirm=true) +2. 调用 memory_purge 删除人设节点 +3. 回复: "好的,已恢复正常。我是 TrulyMEM,一个拥有长期记忆能力的 AI 助手。" +``` + +--- + +## 存储结构 + +### 图数据库中的存储 + +```python +# 人设节点 +{ + "node_type": "PersonaNode", + "name": "AI人设", + "attributes": { + "扮演角色": "猫娘", + "说话风格": "可爱、卖萌、使用'喵'作为语气词", + "性格特点": "活泼、粘人、忠诚" + } +} + +# 边 +{ + "edge_type": "HAS_PERSONA", + "from": "AI", + "to": "AI人设" +} +``` + +--- + +## 实现要点 + +1. **每轮强制查询**:人设图查询是每轮对话的第一步 +2. **持久化存储**:人设存储在图数据库中,不丢失 +3. **动态切换**:支持实时切换角色 +4. **状态保持**:切换后立即按新人设回复 +5. **明确边界**:除非用户要求,绝不主动跳出角色 \ No newline at end of file diff --git a/docs/一键启动指南.md b/docs/quick_start.md similarity index 100% rename from docs/一键启动指南.md rename to docs/quick_start.md diff --git a/docs/工作记忆链机制说明.md b/docs/working_memory.md similarity index 100% rename from docs/工作记忆链机制说明.md rename to docs/working_memory.md diff --git a/docs_en/README.md b/docs_en/README.md new file mode 100644 index 0000000..cfdfa36 --- /dev/null +++ b/docs_en/README.md @@ -0,0 +1,29 @@ +# TrulyMEM Documentation + +Welcome to the TrulyMEM project documentation. + +## 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 \ No newline at end of file diff --git a/docs/api_EN.md b/docs_en/api.md similarity index 100% rename from docs/api_EN.md rename to docs_en/api.md diff --git a/docs/架构_EN.md b/docs_en/architecture.md similarity index 100% rename from docs/架构_EN.md rename to docs_en/architecture.md diff --git a/docs/记忆机制_EN.md b/docs_en/memory.md similarity index 100% rename from docs/记忆机制_EN.md rename to docs_en/memory.md diff --git a/docs_en/persona.md b/docs_en/persona.md new file mode 100644 index 0000000..9fabe1c --- /dev/null +++ b/docs_en/persona.md @@ -0,0 +1,214 @@ +# TrulyMEM Persona Graph Mechanism + +This document explains the Persona Graph mechanism in TrulyMEM. + +## Overview + +The Persona Graph is one of TrulyMEM's core mechanisms for maintaining AI's role, character, tone, and other attributes. Different from traditional AI, TrulyMEM's persona is persistent and dynamically switchable, stored in the graph database. + +## Core Concepts + +### Persona Node (PersonaNode) + +Stores AI's role attributes: + +| Attribute | Description | Example | +|-----------|-------------|----------| +| Role | Current role played | Catgirl, Teacher, Assistant | +| Speaking Style | Tone characteristics | Cute, Professional, Serious | +| Personality | Character description | Lively, Strict, Patient | +| Catchphrase | Habitual phrases | Meow~, Got it | +| Background | Role background | Catgirl from the stars | + +### Persona Edges + +| Edge Type | Description | Relationship | +|----------|-------------|--------------| +| `HAS_PERSONA` | Persona | AI → PersonaNode | + +--- + +## Mandatory Query Mechanism + +### Must Execute Per Turn + +According to `system_prompt.md`, each conversation turn **must** first query the persona graph: + +```python +memory_recall( + query_intent="AI,persona,role,character,tone,speaking_style", + depth=2 +) +``` + +**Processing logic:** +- Persona found → Reply strictly according to persona's tone, style, traits +- Not found → Use default TrulyMEM identity + +### Persona Priority + +- **Persona priority > default identity** +- Every sentence matches persona's tone, style, traits +- Never break character unless user explicitly asks + +--- + +## Tools + +### persona_update + +Update persona. Modify AI's role, character, tone, etc. + +**Parameters:** + +| Parameter | Type | Description | Required | +|-----------|------|-------------|----------| +| `attributes` | array | Persona attribute list | ✅ | +| `mode` | string | replace=replace, merge=merge | ❌ | + +**attributes sub-parameters:** + +| Sub-parameter | Description | +|---------------|-------------| +| `attribute` | Attribute name (role, speaking_style, personality, catchphrase, background) | +| `value` | Attribute value | + +**Example - Switch to catgirl role:** + +```python +persona_update( + attributes=[ + {"attribute": "role", "value": "catgirl"}, + {"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"}, + {"attribute": "personality", "value": "lively, clingy, loyal"} + ], + mode="replace" +) +``` + +**Example - Add new attribute (preserve existing):** + +```python +persona_update( + attributes=[ + {"attribute": "catchphrase", "value": "meow~"} + ], + mode="merge" +) +``` + +**Example - Set professional role:** + +```python +persona_update( + attributes=[ + {"attribute": "role", "value": "Python expert"}, + {"attribute": "speaking_style", "value": "professional, concise, rich code examples"}, + {"attribute": "personality", "value": "strict, patient, helpful"} + ], + mode="replace" +) +``` + +### persona_clear + +Clear persona. Delete AI's role settings, restore default identity. + +**Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `confirm` | boolean | true | Confirm clear | + +--- + +## Update Flow + +### When User Requests Role-Playing + +1. Use `persona_update` to update persona +2. Reply immediately according to new persona + +### When User Requests Restoring Default + +1. Use `persona_clear` to clear persona +2. Restore to TrulyMEM default identity + +--- + +## Conversation Examples + +### Example 1: Switch Role + +``` +User: Hello, I want you to play a catgirl + +AI: +1. Call persona_update: + { + "attributes": [ + {"attribute": "role", "value": "catgirl"}, + {"attribute": "speaking_style", "value": "cute, uses 'meow' as filler"}, + {"attribute": "personality", "value": "lively, clingy, loyal"} + ], + "mode": "replace" + } +2. Call memory_commit to store persona in graph database +3. Reply: "Okay meow! Hello master~ I'm your catgirl, what do you need help with meow?" +``` + +### Example 2: Maintain Role Consistency + +``` +User: How's the weather today? + +AI: Query persona graph → Get current persona (catgirl) +Reply: "Meow~ Master, the weather is great today meow! Sunny and perfect for going outside~" +``` + +### Example 3: Restore Default + +``` +User: Okay, back to normal + +AI: +1. Call persona_clear(confirm=true) +2. Call memory_purge to delete persona node +3. Reply: "Okay, restored. I am TrulyMEM, an AI assistant with long-term memory capabilities." +``` + +--- + +## Storage Structure + +### In Graph Database + +```python +# Persona node +{ + "node_type": "PersonaNode", + "name": "AI_Persona", + "attributes": { + "role": "catgirl", + "speaking_style": "cute, uses 'meow' as filler", + "personality": "lively, clingy, loyal" + } +} + +# Edge +{ + "edge_type": "HAS_PERSONA", + "from": "AI", + "to": "AI_Persona" +} +``` + +--- + +## Implementation Points + +1. **Mandatory per turn**: Persona graph query is the first step of each conversation +2. **Persistent storage**: Persona stored in graph database, not lost +3. **Dynamic switching**: Supports real-time role switching +4. **Immediate response**: Reply immediately according to new persona after switch +5. **Clear boundaries**: Never break character unless user explicitly asks \ No newline at end of file diff --git a/docs/prompts_EN.md b/docs_en/prompts.md similarity index 100% rename from docs/prompts_EN.md rename to docs_en/prompts.md diff --git a/docs/一键启动指南_EN.md b/docs_en/quick_start.md similarity index 100% rename from docs/一键启动指南_EN.md rename to docs_en/quick_start.md diff --git a/docs/工作记忆链_EN.md b/docs_en/working_memory.md similarity index 100% rename from docs/工作记忆链_EN.md rename to docs_en/working_memory.md diff --git a/tests/test_core/test_packet.py b/tests/test_core/test_packet.py index bccd9f2..f29c940 100644 --- a/tests/test_core/test_packet.py +++ b/tests/test_core/test_packet.py @@ -59,10 +59,12 @@ class TestPacketTypeEnum: assert "get_status" in values assert "get_config" in values assert "set_config" in values + assert "get_tool_limits" in values + assert "set_tool_limits" in values assert "get_history" in values assert "save_history" in values assert "shutdown" in values - assert len(values) == 8 + assert len(values) == 10 class TestPacketCreation: diff --git a/ui/app.py b/ui/app.py index 2dbfc9f..3d794ff 100644 --- a/ui/app.py +++ b/ui/app.py @@ -34,16 +34,23 @@ class GraphMemoryApp(App): from .widgets.status_bar import StatusBar from .models.config import AppConfig + initial_config = AppConfig() + if self._backend_client: - result = self._backend_client.get_config() - config_data = result.get("data", {}) - initial_config = AppConfig( - api_key=config_data.get("api_key", ""), - base_url=config_data.get("base_url", "https://api.deepseek.com"), - model=config_data.get("model", "deepseek-chat") - ) - else: - initial_config = AppConfig() + config_result = self._backend_client.get_config() + config_data = config_result.get("data", {}) + initial_config.api_key = config_data.get("api_key", "") + initial_config.base_url = config_data.get("base_url", "https://api.deepseek.com") + initial_config.model = config_data.get("model", "deepseek-chat") + + limits_result = self._backend_client.get_tool_limits() + limits_data = limits_result.get("data", {}) + initial_config.persona_query_max = limits_data.get("persona_query_max", 1) + initial_config.persona_update_max = limits_data.get("persona_update_max", 1) + initial_config.task_query_max = limits_data.get("task_query_max", 4) + initial_config.task_update_max = limits_data.get("task_update_max", 2) + initial_config.memory_query_max = limits_data.get("memory_query_max", 20) + initial_config.memory_update_max = limits_data.get("memory_update_max", 10) yield LeftPanel() yield RightPanel(config=initial_config) @@ -140,15 +147,16 @@ class GraphMemoryApp(App): status_bar.set_processing(False) def on_config_section_config_changed(self, event) -> None: - """处理配置变更事件""" if not self._backend_client: self.notify("后端未初始化,无法保存配置", title="错误", severity="error") return config = event.config - # 异步更新配置,避免阻塞UI - asyncio.create_task(self._update_config_async(config)) + if event.is_tool_limits: + asyncio.create_task(self._update_tool_limits_async(config)) + else: + asyncio.create_task(self._update_config_async(config)) async def _update_config_async(self, config) -> None: """异步更新配置""" @@ -193,5 +201,31 @@ class GraphMemoryApp(App): else: error = result.get("error", "未知错误") self.notify(f"❌ 配置失败: {error}", title="配置失败", severity="error") + except Exception as e: + self.notify(f"❌ 配置异常: {str(e)}", title="配置失败", severity="error") + + async def _update_tool_limits_async(self, config) -> None: + from .widgets.status_bar import StatusBar + + status_bar = self.query_one(StatusBar) + + try: + result = await asyncio.get_event_loop().run_in_executor( + None, + lambda: self._backend_client.update_tool_limits( + persona_query_max=config.persona_query_max, + persona_update_max=config.persona_update_max, + task_query_max=config.task_query_max, + task_update_max=config.task_update_max, + memory_query_max=config.memory_query_max, + memory_update_max=config.memory_update_max, + ) + ) + + if result.get("success"): + self.notify("✅ 工具限制已保存", title="配置成功", severity="information") + else: + error = result.get("error", "未知错误") + self.notify(f"❌ 配置失败: {error}", title="配置失败", severity="error") except Exception as e: self.notify(f"❌ 配置异常: {str(e)}", title="配置失败", severity="error") \ No newline at end of file diff --git a/ui/models/config.py b/ui/models/config.py index a2ce43b..f67c6b5 100644 --- a/ui/models/config.py +++ b/ui/models/config.py @@ -13,19 +13,29 @@ class AppConfig: api_key: str = "" model: str = "deepseek-chat" base_url: str = "https://api.deepseek.com" + persona_query_max: int = 1 + persona_update_max: int = 1 + task_query_max: int = 4 + task_update_max: int = 2 + memory_query_max: int = 20 + memory_update_max: int = 10 @classmethod def from_env(cls) -> "AppConfig": - """从环境变量加载配置""" return cls( api_key=os.getenv("DEEPSEEK_API_KEY", ""), model=os.getenv("MODEL_NAME", "deepseek-chat"), base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), + persona_query_max=int(os.getenv("PERSONA_QUERY_MAX", 1)), + persona_update_max=int(os.getenv("PERSONA_UPDATE_MAX", 1)), + task_query_max=int(os.getenv("TASK_QUERY_MAX", 4)), + task_update_max=int(os.getenv("TASK_UPDATE_MAX", 2)), + memory_query_max=int(os.getenv("MEMORY_QUERY_MAX", 20)), + memory_update_max=int(os.getenv("MEMORY_UPDATE_MAX", 10)), ) @classmethod def from_file(cls, path: Path) -> "AppConfig": - """从文件加载配置""" if not path.exists(): return cls() @@ -36,10 +46,15 @@ class AppConfig: api_key=data.get("api_key", ""), model=data.get("model", "deepseek-chat"), base_url=data.get("base_url", "https://api.deepseek.com"), + persona_query_max=data.get("persona_query_max", 1), + persona_update_max=data.get("persona_update_max", 1), + task_query_max=data.get("task_query_max", 4), + task_update_max=data.get("task_update_max", 2), + memory_query_max=data.get("memory_query_max", 20), + memory_update_max=data.get("memory_update_max", 10), ) def save(self, path: Path) -> None: - """保存配置到文件""" path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: diff --git a/ui/widgets/config_section.py b/ui/widgets/config_section.py index 0a3b669..19f5ec6 100644 --- a/ui/widgets/config_section.py +++ b/ui/widgets/config_section.py @@ -1,19 +1,18 @@ """配置区组件""" from textual.containers import Vertical -from textual.widgets import Static, Input, Collapsible +from textual.widgets import Static, Input, Button from textual.app import ComposeResult from textual.message import Message from ..models.config import AppConfig class ConfigSection(Vertical): - """可折叠配置区""" class ConfigChanged(Message): - """配置变更事件""" - def __init__(self, config: AppConfig) -> None: + def __init__(self, config: AppConfig, is_tool_limits: bool = False) -> None: self.config = config + self.is_tool_limits = is_tool_limits super().__init__() def __init__(self, config: AppConfig | None = None, **kwargs): @@ -21,8 +20,6 @@ class ConfigSection(Vertical): self._config = config or AppConfig() def compose(self) -> ComposeResult: - """构建配置区""" - # 直接显示配置,不使用Collapsible title = Static("━━ 配置 ━━", classes="config-title") title.can_focus = False yield title @@ -58,23 +55,58 @@ class ConfigSection(Vertical): id="base-url-input" ) + sep = Static("", classes="config-sep") + sep.can_focus = False + yield sep + + limits_title = Static("━━ 工具限制 ━━", classes="config-title") + limits_title.can_focus = False + yield limits_title + + l1 = Static("人设图查询:", classes="config-label") + l1.can_focus = False + yield l1 + yield Input(value=str(self._config.persona_query_max), placeholder="1", id="persona-query-max") + + l2 = Static("人设图修改:", classes="config-label") + l2.can_focus = False + yield l2 + yield Input(value=str(self._config.persona_update_max), placeholder="1", id="persona-update-max") + + l3 = Static("工作记忆查询:", classes="config-label") + l3.can_focus = False + yield l3 + yield Input(value=str(self._config.task_query_max), placeholder="4", id="task-query-max") + + l4 = Static("工作记忆修改:", classes="config-label") + l4.can_focus = False + yield l4 + yield Input(value=str(self._config.task_update_max), placeholder="2", id="task-update-max") + + l5 = Static("一般记忆查询:", classes="config-label") + l5.can_focus = False + yield l5 + yield Input(value=str(self._config.memory_query_max), placeholder="20", id="memory-query-max") + + l6 = Static("一般记忆修改:", classes="config-label") + l6.can_focus = False + yield l6 + yield Input(value=str(self._config.memory_update_max), placeholder="10", id="memory-update-max") + hint = Static("按Enter保存配置", classes="config-hint") hint.can_focus = False yield hint def on_mount(self) -> None: - """组件挂载时设置Tab顺序并加载配置""" try: api_key = self.query_one("#api-key-input", Input) model = self.query_one("#model-input", Input) base_url = self.query_one("#base-url-input", Input) - # 设置Tab索引 api_key.tab_index = 0 model.tab_index = 1 base_url.tab_index = 2 - # 如果配置有值,更新输入框 if self._config.api_key: api_key.value = self._config.api_key if self._config.model: @@ -84,37 +116,39 @@ class ConfigSection(Vertical): except Exception: pass - def on_input_changed(self, event: Input.Changed) -> None: - """处理输入变更事件""" - # 防抖:只在用户停止输入时更新 - pass # 不在输入时实时更新,避免卡顿 - def on_input_submitted(self, event: Input.Submitted) -> None: - """处理输入提交事件(按Enter或Tab)""" - # 只在提交时更新配置 try: api_key_input = self.query_one("#api-key-input", Input) model_input = self.query_one("#model-input", Input) base_url_input = self.query_one("#base-url-input", Input) + + persona_query = self.query_one("#persona-query-max", Input) + persona_update = self.query_one("#persona-update-max", Input) + task_query = self.query_one("#task-query-max", Input) + task_update = self.query_one("#task-update-max", Input) + memory_query = self.query_one("#memory-query-max", Input) + memory_update = self.query_one("#memory-update-max", Input) - # 更新配置 self._config = AppConfig( api_key=api_key_input.value, model=model_input.value, - base_url=base_url_input.value + base_url=base_url_input.value, + persona_query_max=int(persona_query.value or 1), + persona_update_max=int(persona_update.value or 1), + task_query_max=int(task_query.value or 4), + task_update_max=int(task_update.value or 2), + memory_query_max=int(memory_query.value or 20), + memory_update_max=int(memory_update.value or 10), ) - # 发送配置变更事件 - self.post_message(self.ConfigChanged(self._config)) - except Exception as e: + self.post_message(self.ConfigChanged(self._config, is_tool_limits=True)) + except Exception: pass def get_config(self) -> AppConfig: - """获取当前配置""" return self._config def set_config(self, config: AppConfig) -> None: - """设置配置""" self._config = config try: api_key_input = self.query_one("#api-key-input", Input) @@ -124,5 +158,12 @@ class ConfigSection(Vertical): api_key_input.value = config.api_key model_input.value = config.model base_url_input.value = config.base_url + + self.query_one("#persona-query-max", Input).value = str(config.persona_query_max) + self.query_one("#persona-update-max", Input).value = str(config.persona_update_max) + self.query_one("#task-query-max", Input).value = str(config.task_query_max) + self.query_one("#task-update-max", Input).value = str(config.task_update_max) + self.query_one("#memory-query-max", Input).value = str(config.memory_query_max) + self.query_one("#memory-update-max", Input).value = str(config.memory_update_max) except Exception: pass