diff --git a/graph_memory_tui/app.py b/graph_memory_tui/app.py index 785d12f..9b513b2 100644 --- a/graph_memory_tui/app.py +++ b/graph_memory_tui/app.py @@ -27,6 +27,7 @@ from .core.imports import ( NEO4J_PASSWORD, MODEL_NAME, ) +from .core.tools.tool_limiter import ToolLimiter class GraphMemoryApp(App[None]): @@ -62,6 +63,9 @@ class GraphMemoryApp(App[None]): # 核心组件 self._graph: Neo4jGraph | None = None self._client: GraphMemoryClient | None = None + + # 工具调用限制器 + self._tool_limiter = ToolLimiter() def compose(self) -> ComposeResult: """构建组件树""" @@ -219,10 +223,16 @@ F6 - 退出 if not self._client: raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key,然后按 Enter 保存") + # 重置工具调用限制器(新的一轮对话) + self._tool_limiter.reset() + + # 构建初始消息历史 + messages_history = [{"role": "user", "content": user_input}] + # 参考demo的调用方式 response = await asyncio.get_event_loop().run_in_executor( None, - lambda: self._client.send_message(user_input) + lambda: self._client.send_message_with_history(messages_history) ) message = response.choices[0].message @@ -230,8 +240,8 @@ F6 - 退出 # 处理工具调用循环 tool_calls = [] tool_results = [] - last_assistant_msg = None accumulated_content = "" # 累积所有中间内容 + rejected_tools = [] # 被拒绝的工具调用 # 显示工具调用摘要 if message.tool_calls: @@ -248,8 +258,8 @@ F6 - 退出 if message.content: accumulated_content += message.content + "\n\n" - # 保存包含 tool_calls 的 assistant 消息 - last_assistant_msg = { + # 构建包含 tool_calls 的 assistant 消息 + assistant_msg = { "role": "assistant", "content": message.content, "tool_calls": [ @@ -263,6 +273,9 @@ F6 - 退出 } for tc in message.tool_calls ] } + + # 添加到消息历史 + messages_history.append(assistant_msg) # 执行当前轮的所有工具调用 current_tool_results = [] @@ -272,6 +285,36 @@ F6 - 退出 name=tool_call.function.name, arguments=json.loads(tool_call.function.arguments) ) + + # 检查工具调用限制 + allowed, reason = self._tool_limiter.can_call(tc.name, tc.arguments) + + if not allowed: + # 拒绝调用 + rejected_tools.append((tc.name, reason)) + result = f"⚠️ 工具调用被拒绝: {reason}" + + # 添加到当前轮结果 + tool_result_msg = { + "role": "tool", + "tool_call_id": tc.id, + "content": result + } + current_tool_results.append(tool_result_msg) + + # 添加日志 + log_entry = LogEntry( + timestamp=datetime.now(), + tool_name=tc.name, + arguments=tc.arguments, + result=result, + duration=0.0 + ) + log.add_log(log_entry) + continue + + # 记录调用 + self._tool_limiter.record_call(tc.name, tc.arguments) tool_calls.append(tc) # 执行工具 @@ -293,11 +336,12 @@ F6 - 退出 tool_results.append(tr) # 添加到当前轮结果 - current_tool_results.append({ + tool_result_msg = { "role": "tool", "tool_call_id": tc.id, "content": result - }) + } + current_tool_results.append(tool_result_msg) # 添加日志 log_entry = LogEntry( @@ -309,14 +353,13 @@ F6 - 退出 ) log.add_log(log_entry) - # 继续调用API(参考demo的实现) + # 将工具结果添加到消息历史 + messages_history.extend(current_tool_results) + + # 继续调用API(使用累积的消息历史) response = await asyncio.get_event_loop().run_in_executor( None, - lambda: self._client.send_message( - user_input, - current_tool_results, - last_assistant_msg - ) + lambda: self._client.send_message_with_history(messages_history) ) message = response.choices[0].message @@ -333,6 +376,12 @@ F6 - 退出 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()}" + assistant_message = Message( role="assistant", content=content, diff --git a/graph_memory_tui/core/graph_client.py b/graph_memory_tui/core/graph_client.py index bfeec50..408fcc2 100644 --- a/graph_memory_tui/core/graph_client.py +++ b/graph_memory_tui/core/graph_client.py @@ -318,13 +318,33 @@ class GraphMemoryClient: 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) - messages.append({"role": "user", "content": user_input}) + response = self.client.chat.completions.create( + model=MODEL_NAME, + 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=MODEL_NAME, @@ -341,14 +361,17 @@ class GraphMemoryClient: 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) - messages.append({"role": "user", "content": user_input}) - stream = self.client.chat.completions.create( model=MODEL_NAME, messages=messages, diff --git a/graph_memory_tui/core/prompts/templates/system_prompt.md b/graph_memory_tui/core/prompts/templates/system_prompt.md index 96d4b0d..ea608c3 100644 --- a/graph_memory_tui/core/prompts/templates/system_prompt.md +++ b/graph_memory_tui/core/prompts/templates/system_prompt.md @@ -157,26 +157,51 @@ ## 工作记忆链机制 -#### 强制查询场景: +### ⚠️ 核心理念:维持对话连贯性 + +**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。 + +### 强制查询场景: 以下情况**必须**查询工作记忆链: -1. **用户提到"刚才"、"之前"、"上次"** +1. **每轮对话开始时(强制第二步)** + - 查询意图: "TaskNode,工作记忆,任务链" + - 目的: 获取之前的任务上下文,了解对话历史 + +2. **用户提到"刚才"、"之前"、"上次"** - 例: "刚才我们聊了什么?" - 例: "继续刚才的话题" + - 例: "关于刚才的成语接龙..." -2. **用户询问对话历史** +3. **用户询问对话历史** - 例: "我们之前说了什么?" - 例: "我们聊过X吗?" -3. **连续性任务被打断后恢复** +4. **连续性任务被打断后恢复** - 例: 用户突然回到之前的话题 - 例: 用户要求继续之前的任务 -4. **涉及上下文的引用** +5. **涉及上下文的引用** - 例: "那个东西"(需要查询上下文) - 例: "继续"(需要查询当前任务) +### 强制更新场景: + +以下情况**必须**更新工作记忆链: + +1. **每轮对话结束时(强制第四步)** + - 创建任务节点记录本轮对话 + - 目的: 维持时间链,确保对话连贯性 + +2. **开始连续性任务时** + - 例: 用户发起游戏、项目、学习计划等 + - 必须创建任务节点并设置状态为"进行中" + +3. **任务状态发生变化时** + - 例: 任务完成、暂停、取消 + - 必须及时更新任务状态 + ### 节点类型 - **TaskNode** - 任务节点,存储任务概述 - **StateNode** - 状态节点,存储任务状态 @@ -193,6 +218,63 @@ - 已暂停 - 已取消 +### ⚠️ 完整示例:成语接龙游戏 + +#### 第一轮:用户发起游戏 + +``` +用户: 咱来玩成语接龙吧,我先开始,为所欲为 + +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. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中 + ## 自主性原则(在强制要求之外) 除了工作记忆链的强制要求外,你有权自主决定: diff --git a/graph_memory_tui/core/tools/__init__.py b/graph_memory_tui/core/tools/__init__.py index 3b1c6d6..0a0ad7d 100644 --- a/graph_memory_tui/core/tools/__init__.py +++ b/graph_memory_tui/core/tools/__init__.py @@ -3,5 +3,6 @@ """ from .memory_tools import TOOLS from .tool_executor import execute_tool +from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount -__all__ = ["TOOLS", "execute_tool"] +__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"] diff --git a/graph_memory_tui/core/tools/memory_tools.py b/graph_memory_tui/core/tools/memory_tools.py index 760f8e5..d480120 100644 --- a/graph_memory_tui/core/tools/memory_tools.py +++ b/graph_memory_tui/core/tools/memory_tools.py @@ -9,7 +9,28 @@ MEMORY_TOOLS = [ "type": "function", "function": { "name": "memory_recall", - "description": "检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。", + "description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。 + +【使用示例】 +1. 查询人设图(每轮必须首先执行): + {"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2} + +2. 查询工作记忆链(每轮必须第二步执行): + {"query_intent": "TaskNode,工作记忆,任务链", "depth": 2} + +3. 查询用户偏好: + {"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]} + +4. 查询特定主题: + {"query_intent": "Python,编程,项目", "seed_entities": ["Python"]} + +5. 查询最近7天的记忆: + {"query_intent": "任务,工作", "time_range": {"days": 7}} + +【重要】每轮对话必须按顺序执行: +- 步骤1: 查询人设图(最高优先级) +- 步骤2: 查询工作记忆链(维持对话连贯性) +- 步骤3: 根据需要查询其他记忆""", "parameters": { "type": "object", "properties": { @@ -46,7 +67,31 @@ MEMORY_TOOLS = [ "type": "function", "function": { "name": "memory_commit", - "description": "写入记忆。将三元组写入图数据库,支持批量写入。", + "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": { @@ -82,7 +127,29 @@ MEMORY_TOOLS = [ "type": "function", "function": { "name": "memory_purge", - "description": "删除记忆。支持条件删除和纠错替代。", + "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": { @@ -176,7 +243,32 @@ PERSONA_TOOLS = [ "type": "function", "function": { "name": "persona_update", - "description": "更新人设。修改AI的角色、性格、语气等属性。", + "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": { @@ -229,7 +321,50 @@ WORKING_MEMORY_TOOLS = [ "type": "function", "function": { "name": "task_create", - "description": "创建任务节点。用于跟踪连续性任务。", + "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": { @@ -255,7 +390,37 @@ WORKING_MEMORY_TOOLS = [ "type": "function", "function": { "name": "task_set_state", - "description": "设置任务状态。支持:进行中、已完成、已暂停、已取消。", + "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": { @@ -299,7 +464,39 @@ WORKING_MEMORY_TOOLS = [ "type": "function", "function": { "name": "task_link_info", - "description": "关联信息节点。将记忆节点关联到任务节点。", + "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": { diff --git a/graph_memory_tui/core/tools/tool_limiter.py b/graph_memory_tui/core/tools/tool_limiter.py new file mode 100644 index 0000000..9f0f614 --- /dev/null +++ b/graph_memory_tui/core/tools/tool_limiter.py @@ -0,0 +1,164 @@ +""" +工具调用限制器 - 限制每轮对话中各类工具的调用次数 +""" +from typing import Dict, List, Optional +from dataclasses import dataclass, field + + +@dataclass +class ToolLimits: + """工具调用限制配置""" + # 人设图限制 + persona_query_max: int = 1 # 每轮最多查询1次人设图 + persona_update_max: int = 1 # 每轮最多修改1次人设图 + + # 工作记忆链限制 + task_query_max: int = 4 # 每轮最多查询4次工作记忆链 + task_update_max: int = 2 # 每轮最多修改2次工作记忆链 + + # 一般记忆限制 + memory_query_max: int = 20 # 每轮最多查询20次一般记忆 + memory_update_max: int = 10 # 每轮最多修改10次一般记忆 + + +@dataclass +class ToolCallCount: + """工具调用计数""" + # 人设图 + persona_query: int = 0 + persona_update: int = 0 + + # 工作记忆链 + task_query: int = 0 + task_update: int = 0 + + # 一般记忆 + memory_query: int = 0 + memory_update: int = 0 + + +class ToolLimiter: + """工具调用限制器""" + + def __init__(self, limits: Optional[ToolLimits] = None): + self.limits = limits or ToolLimits() + self.counts = ToolCallCount() + + def _classify_tool(self, tool_name: str, arguments: dict) -> tuple: + """ + 分类工具调用 + 返回: (category, operation) + category: 'persona', 'task', 'memory' + operation: 'query', 'update' + """ + # 人设图工具 + if tool_name in ('persona_update', 'persona_clear'): + return ('persona', 'update') + + # 工作记忆链工具 + if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'): + # task_link_info 是关联操作,算作更新 + return ('task', 'update') + + # 一般记忆工具 + if tool_name == 'memory_recall': + # 判断是查询人设图、工作记忆链还是一般记忆 + query_intent = arguments.get('query_intent', '').lower() + + # 检查是否查询人设图 + if any(kw in query_intent for kw in ['人设', '角色', '性格', '语气', '说话风格', '扮演']): + return ('persona', 'query') + + # 检查是否查询工作记忆链 + if any(kw in query_intent for kw in ['tasknode', '工作记忆', '任务链', '任务', 'task']): + return ('task', 'query') + + # 一般记忆查询 + return ('memory', 'query') + + if tool_name == 'memory_commit': + return ('memory', 'update') + + if tool_name == 'memory_purge': + return ('memory', 'update') + + if tool_name == 'memory_introspect': + return ('memory', 'query') + + if tool_name in ('memory_archive', 'memory_cleanup'): + return ('memory', 'update') + + # 未知工具,归类为一般记忆更新 + return ('memory', 'update') + + def can_call(self, tool_name: str, arguments: dict) -> tuple: + """ + 检查是否允许调用工具 + 返回: (allowed, reason) + """ + category, operation = self._classify_tool(tool_name, arguments) + + # 获取当前计数和限制 + if category == 'persona': + if operation == 'query': + if self.counts.persona_query >= self.limits.persona_query_max: + return (False, f"人设图查询次数已达上限({self.limits.persona_query_max}次)") + else: # update + if self.counts.persona_update >= self.limits.persona_update_max: + return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)") + + elif category == 'task': + if operation == 'query': + if self.counts.task_query >= self.limits.task_query_max: + return (False, f"工作记忆链查询次数已达上限({self.limits.task_query_max}次)") + else: # update + if self.counts.task_update >= self.limits.task_update_max: + return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)") + + elif category == 'memory': + if operation == 'query': + if self.counts.memory_query >= self.limits.memory_query_max: + return (False, f"一般记忆查询次数已达上限({self.limits.memory_query_max}次)") + else: # update + if self.counts.memory_update >= self.limits.memory_update_max: + return (False, f"一般记忆修改次数已达上限({self.limits.memory_update_max}次)") + + return (True, "允许调用") + + def record_call(self, tool_name: str, arguments: dict) -> None: + """记录工具调用""" + category, operation = self._classify_tool(tool_name, arguments) + + if category == 'persona': + if operation == 'query': + self.counts.persona_query += 1 + else: + self.counts.persona_update += 1 + + elif category == 'task': + if operation == 'query': + self.counts.task_query += 1 + else: + self.counts.task_update += 1 + + elif category == 'memory': + if operation == 'query': + self.counts.memory_query += 1 + else: + self.counts.memory_update += 1 + + def get_summary(self) -> str: + """获取调用统计摘要""" + lines = [ + f"人设图: 查询{self.counts.persona_query}/{self.limits.persona_query_max}次, " + f"修改{self.counts.persona_update}/{self.limits.persona_update_max}次", + f"工作记忆链: 查询{self.counts.task_query}/{self.limits.task_query_max}次, " + f"修改{self.counts.task_update}/{self.limits.task_update_max}次", + f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, " + f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次" + ] + return "\n".join(lines) + + def reset(self) -> None: + """重置计数(新的一轮对话开始时调用)""" + self.counts = ToolCallCount() diff --git a/graph_memory_tui/web/interface.py b/graph_memory_tui/web/interface.py deleted file mode 100644 index 8a1de43..0000000 --- a/graph_memory_tui/web/interface.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Web接口 - 提供浏览器访问 -""" - -from flask import Flask, render_template, jsonify, request -from flask_cors import CORS -import json -from datetime import datetime -from typing import Optional - -from ..core.embedded_db import EmbeddedGraphDB -from ..models.config import AppConfig - - -class WebInterface: - """Web接口服务""" - - def __init__(self, config: AppConfig, db: EmbeddedGraphDB, port: int = 5000): - self.config = config - self.db = db - self.port = port - self.app = Flask(__name__, - template_folder='templates', - static_folder='static') - CORS(self.app) - self._setup_routes() - - def _setup_routes(self): - """设置路由""" - - @self.app.route('/') - def index(): - return render_template('index.html') - - @self.app.route('/api/chat', methods=['POST']) - def chat(): - data = request.json - message = data.get('message', '') - # 简单响应,实际应集成完整的聊天逻辑 - return jsonify({ - 'response': f'收到消息: {message}\n\n请使用TUI界面获得完整功能。', - 'timestamp': datetime.now().isoformat() - }) - - @self.app.route('/api/memory/recall', methods=['POST']) - def recall(): - data = request.json - result = self.db.recall( - query_intent=data.get('query_intent', ''), - seed_entities=data.get('seed_entities'), - depth=data.get('depth', 2) - ) - return jsonify(result) - - @self.app.route('/api/memory/commit', methods=['POST']) - def commit(): - data = request.json - result = self.db.commit( - triplets=data.get('triplets', []), - entity_types=data.get('entity_types'), - session_id=data.get('session_id'), - turn_id=data.get('turn_id') - ) - return jsonify(result) - - @self.app.route('/api/memory/introspect', methods=['GET']) - def introspect(): - result = self.db.introspect() - return jsonify(result) - - @self.app.route('/api/config', methods=['GET']) - def get_config(): - return jsonify({ - 'api_key': self.config.api_key[:10] + '...' if self.config.api_key else '', - 'model': self.config.model, - 'base_url': self.config.base_url - }) - - @self.app.route('/api/config', methods=['POST']) - def save_config(): - data = request.json - self.config.api_key = data.get('api_key', '') - self.config.model = data.get('model', 'deepseek-chat') - self.config.base_url = data.get('base_url', 'https://api.deepseek.com') - return jsonify({'status': 'ok', 'message': 'Configuration saved'}) - - def run(self): - """启动Web服务""" - print(f"Web interface running at http://localhost:{self.port}") - self.app.run(host='0.0.0.0', port=self.port, debug=False, threaded=True) - - def run_async(self): - """异步启动Web服务""" - import threading - thread = threading.Thread(target=self.run, daemon=True) - thread.start() - return thread diff --git a/graph_memory_tui/web/templates/index.html b/graph_memory_tui/web/templates/index.html deleted file mode 100644 index 53c2e2f..0000000 --- a/graph_memory_tui/web/templates/index.html +++ /dev/null @@ -1,408 +0,0 @@ - - - - - - Graph Memory TUI - Web Interface - - - -
-

Graph Memory TUI - Web Interface

-
Connected | Database: SQLite
-
- -
- -
-
-
-
Assistant
-
欢迎使用 Graph Memory TUI Web界面! - -这是一个让AI拥有长期记忆的图记忆系统。 - -功能: -• 记忆检索 - 查找历史信息 -• 记忆写入 - 保存重要信息 -• 记忆管理 - 删除、归档记忆 - -请先在右侧配置API Key,然后开始对话。
-
System
-
-
- -
- -
按 Ctrl+Enter 发送消息
-
-
- - -
-
-

━━ 配置 ━━

-
- - -
-
- - -
-
- - -
- -
配置会自动保存到本地
-
- -
-

━━ 数据库状态 ━━

-
加载中...
- -
- -
-

━━ 操作日志 ━━

-
-
-
-
- - - - diff --git a/test_message_order.py b/test_message_order.py new file mode 100644 index 0000000..e345e47 --- /dev/null +++ b/test_message_order.py @@ -0,0 +1,54 @@ +"""测试消息顺序""" +from graph_memory_tui.core.graph_client import GraphMemoryClient + +# 模拟消息构建 +class MockClient: + pass + +class MockGraph: + pass + +# 测试消息顺序 +client = GraphMemoryClient.__new__(GraphMemoryClient) +client.client = MockClient() +client.graph = MockGraph() +client.tools = [] +client.system_prompt = 'You are a helpful assistant.' + +# 模拟第一次调用(无工具结果) +print('=== 第一次调用(无工具结果)===') +messages = [{'role': 'system', 'content': client.system_prompt}] +messages.append({'role': 'user', 'content': '你好'}) +for i, msg in enumerate(messages): + content = str(msg.get('content', ''))[:50] + print(f'{i+1}. {msg["role"]}: {content}') + +print() +print('=== 第二次调用(有工具结果)===') +messages = [{'role': 'system', 'content': client.system_prompt}] +messages.append({'role': 'user', 'content': '你好'}) +messages.append({ + 'role': 'assistant', + 'content': None, + 'tool_calls': [{'id': 'call_123', 'type': 'function', 'function': {'name': 'memory_recall', 'arguments': '{}'}}] +}) +messages.append({'role': 'tool', 'tool_call_id': 'call_123', 'content': '查询结果...'}) + +for i, msg in enumerate(messages): + role = msg.get('role') + if role == 'tool': + tid = msg.get('tool_call_id') + print(f'{i+1}. {role}: tool_call_id={tid}') + elif role == 'assistant' and msg.get('tool_calls'): + tc_count = len(msg.get('tool_calls')) + print(f'{i+1}. {role}: tool_calls={tc_count}') + else: + content = str(msg.get('content', ''))[:50] + print(f'{i+1}. {role}: {content}') + +print() +print('✅ 消息顺序正确!') +print('AI 可以看到:') +print('1. 用户说了什么') +print('2. 自己调用了什么工具') +print('3. 工具返回了什么结果') diff --git a/test_tool_call_flow.py b/test_tool_call_flow.py new file mode 100644 index 0000000..d99a846 --- /dev/null +++ b/test_tool_call_flow.py @@ -0,0 +1,52 @@ +""" +测试工具调用的正确流程 + +OpenAI API 工具调用的正确消息顺序: + +第一轮: +1. system +2. user: "帮我查询用户信息" +→ AI 返回 tool_calls: [memory_recall] + +第二轮: +1. system +2. user: "帮我查询用户信息" +3. assistant: tool_calls=[memory_recall] +4. tool: "查询结果..." +→ AI 返回 tool_calls: [memory_commit] + +第三轮: +1. system +2. user: "帮我查询用户信息" +3. assistant: tool_calls=[memory_recall] +4. tool: "查询结果..." +5. assistant: tool_calls=[memory_commit] +6. tool: "写入结果..." +→ AI 返回最终回复 + +关键点: +- 每次调用都要包含完整的消息历史 +- 包括之前所有的 assistant 消息和 tool 结果 +""" + +print(__doc__) + +print("\n当前实现的问题:") +print("=" * 60) +print("每次调用只传递:") +print(" - user_input (用户输入)") +print(" - last_assistant_msg (上一轮的 assistant 消息)") +print(" - current_tool_results (当前轮的 tool 结果)") +print() +print("这会导致:") +print(" ❌ AI 看不到之前轮次的工具调用和结果") +print(" ❌ AI 无法理解完整的上下文") +print(" ❌ AI 可能重复调用相同的工具") +print() +print("正确的做法:") +print("=" * 60) +print("需要累积所有轮次的消息:") +print(" ✅ user_input") +print(" ✅ assistant_msg_1 + tool_results_1") +print(" ✅ assistant_msg_2 + tool_results_2") +print(" ✅ ...")