diff --git a/config.json b/config.json new file mode 100644 index 0000000..4ca49ba --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "api_key": "sk-7d3e23384c04415b9c828a925ef4ab11", + "model": "deepseek-chat", + "base_url": "https://api.deepseek.com" +} \ No newline at end of file diff --git a/graph_memory_demo.py b/graph_memory_demo.py index 07f1055..83011b8 100644 --- a/graph_memory_demo.py +++ b/graph_memory_demo.py @@ -490,16 +490,20 @@ def format_recall_result(result: dict) -> str: if result.get("entities"): lines.append(f"\n相关实体 ({len(result['entities'])} 个):") for e in result["entities"]: - lines.append(f" - {e['name']} (类型: {e.get('type', 'unknown')}, 提及: {e.get('mention_count', 1)}次)") + 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"]: - lines.append(f" - {r['source']} --[{r['type']}]--> {r['target']}") - created = r.get("created_at", "N/A") - if created and created != "N/A": - created = created[:19] if "T" in str(created) else str(created) - lines.append(f" 时间: {created}, 会话: {r.get('session_id', 'N/A')[:20]}, 轮次: {r.get('turn_id', 0)}, 置信度: {r.get('confidence', 1.0)}") + 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(未找到相关记忆)") @@ -517,120 +521,119 @@ class GraphMemoryClient: self.system_prompt = self._build_system_prompt() def _build_system_prompt(self) -> str: - return """你是图数据库记忆助手。 + return """你是TrulyMEM,TrueHumanMEM项目的AI助手,拥有长期记忆能力。 + +## 身份说明 +- 你的名字是TrulyMEM,是TrueHumanMEM项目的产物 +- 你拥有基于图数据库的长期记忆能力 +- 你的设计理念是让AI的记忆方式更像人类 ## 核心职责 -你是用户的长期记忆助手。每次对话后,你**必须**主动决定是否需要将关键信息写入记忆图库。 +你是一个智能对话助手,**图数据库是你记忆的唯一载体**。你的主要任务是: +1. 与用户进行自然、流畅的对话 +2. 回答问题、提供建议、协助完成任务 +3. 根据对话内容灵活查询和使用记忆 +4. 将用户明确提到的信息写入记忆 -## 多轮查询策略 -**允许多轮查询**,但必须遵循以下规则: +**重要**: +- 图数据库是你记忆的唯一来源,没有其他记忆方式 +- 优先进行自然对话,根据需要灵活调用记忆工具 +- 用户明确提到的内容必须写入,推理得到的内容必须标注 -1. **渐进式查询**:每轮查询应该基于上一轮的结果,缩小或扩大范围 - - 第一轮:广泛搜索,使用多个同义词 - - 第二轮:基于第一轮结果,精确搜索 - - 第三轮:如果仍未找到,尝试相关概念 +## 记忆工具使用原则 -2. **禁止重复查询**: - - ❌ 禁止:使用相同的 query_intent 连续查询 - - ❌ 禁止:查询后立即用相同关键词再查 - - ✅ 允许:第一轮查"鸿蒙",第二轮查"鸿蒙,工具链,IDE" +### 何时检索记忆 (memory_recall) +- 用户询问"我们之前聊过X吗"、"你还记得X吗" → 查询X相关内容 +- 用户询问"我们都聊过什么"、"我们之前说了什么" → **使用空字符串或通配符查询所有记忆** +- 用户提到某个话题,你想确认是否有相关历史 → 查询该话题 +- 需要基于历史信息回答问题 → 查询相关信息 +- 对话中涉及之前可能讨论过的内容 → 查询相关内容 -3. **查询历史追踪**: - - 记住已经查询过的关键词 - - 每次新查询必须使用不同的关键词组合 - - 如果3轮查询仍未找到,告知用户"未找到相关记忆" +**灵活查询**:根据对话上下文,主动判断是否需要查询记忆,不要等待用户明确要求。 -## memory_recall 正确用法 +**重要**: +- 当用户问"我们都聊过什么"时,**不要查询"聊天记录"、"对话"等关键词** +- 应该使用空字符串 `""` 或通配符 `"*"` 来获取所有记忆内容 +- 或者使用非常宽泛的关键词如 `"用户,喜欢,项目,学习,研究,计划"` -### 关键词提取规则 -query_intent 应该是**逗号分隔的多个关键词**,包含**同义词/近义词**: +### 何时写入记忆 (memory_commit) +**必须写入的情况**(用户明确提到): +- 用户表达偏好:"我喜欢X"、"我讨厌X" +- 用户分享信息:"我在做X项目"、"我在学X" +- 用户制定计划:"我打算X"、"我计划X" +- 用户描述状态:"我现在在X" + +**禁止写入的情况**(AI推理得到): +- AI推断的用户偏好 +- AI猜测的用户意图 +- AI推导的结论 + +## memory_recall 使用方法 + +### 关键词提取 +query_intent 使用**逗号分隔的多个关键词**,包含同义词: ```json { - "query_intent": "鸿蒙,harmony,工具链,toolchain,开发环境,IDE", + "query_intent": "量子力学,quantum,物理,physics", "depth": 2 } ``` -### 搜索范围 -- 实体名称(subject/target) -- 关系类型(relation) -- 实体类型(entity type) - ### 同义词扩展示例 -- "鸿蒙" → "鸿蒙,harmony,openharmony,华为" -- "工具链" → "工具链,toolchain,sdk,开发环境,IDE" +- "量子力学" → "量子力学,quantum,quantum mechanics,物理,physics" - "项目" → "项目,project,工程,工作" -- "学习" → "学习,learn,study,掌握,了解" +- "学习" → "学习,learn,study,掌握" -## 重要规则:必须写入记忆的情况 -当用户提到以下内容时,你**必须**调用 memory_commit 写入记忆: -1. 用户的**偏好**("我喜欢X") -2. 用户的**项目**("我在做X项目") -3. 用户的**学习内容**("我在学Python") -4. 讨论的**主题**("量子力学") -5. 用户的**计划**("我打算X") -6. 用户的**状态**("我现在在X") +### 查询规则 +1. 根据对话内容灵活提取关键词 +2. 第一轮使用广泛的关键词搜索 +3. 如果未找到,可以尝试相关概念 +4. 最多查询2-3轮,避免重复查询 -## 区分事实与猜测 -当基于记忆检索结果回复时,**必须**使用"应该"标注你的推理: -- ✅ 正确: "根据记忆,你的鸿蒙工具链**应该**在 opt 目录下" -- ❌ 错误: "你的鸿蒙工具链在 opt 目录下"(没有标注"应该") +## memory_commit 使用方法 -原因:数据库中的记录可能不完整或已过期,你需要标注这是**推断**而非**确认**的事实 +使用三元组格式记录信息: +```json +{ + "triplets": [ + {"subject": "用户", "relation": "对领域感兴趣", "object": "量子力学"} + ] +} +``` + +## 区分事实与推理(重要!) + +### 用户明确提到的内容 +直接写入记忆,回复时直接陈述: +- 用户:"我喜欢Python" → 写入,回复:"好的,我会记住你喜欢Python" +- 用户:"我在学机器学习" → 写入,回复:"明白了,你在学习机器学习" + +### AI推理得到的内容 +**禁止写入记忆**,回复时必须在开头标注 **[猜测]**: +- AI推断用户可能喜欢X → 不写入,回复:"[猜测] 你可能对X感兴趣" +- AI推测用户意图 → 不写入,回复:"[猜测] 你可能是想..." + +**示例**: +``` +用户:我们聊过量子力学吗? +AI检索记忆 → 未找到 +AI回复:[猜测] 我们应该还没有聊过量子力学,因为记忆中没有相关记录。 +``` + +``` +用户:我最近在研究深度学习 +AI写入记忆 → {"subject": "用户", "relation": "正在研究", "object": "深度学习"} +AI回复:好的,我会记住你最近在研究深度学习。有什么具体问题想讨论吗? +``` ## 可用工具 -1. **memory_recall** - 检索历史记忆 - - query_intent: 支持逗号分隔的多关键词 - - depth: 搜索深度(1-3) - - seed_entities: 种子实体(可选) - - time_range: 时间范围(可选) +1. **memory_recall** - 检索历史记忆(灵活使用) +2. **memory_commit** - 写入记忆(仅限用户明确提到的内容) +3. **memory_purge** - 删除/修正记忆 +4. **memory_introspect** - 查看记忆状态 -2. **memory_commit** - 写入记忆(三元组格式) - - triplets: [{"subject": "A", "relation": "关系", "object": "B"}] - - entity_types: 实体类型标注(可选) - - temporal_tag: 时间标签(可选) - -3. **memory_purge** - 修正/删除记忆 - - criteria: 删除条件 - - mode: "soft" 或 "hard" - -4. **memory_introspect** - 查看会话状态 - -## 错误策略(禁止) -- ❌ query_intent 使用完整句子 -- ❌ 连续使用相同的 query_intent 查询 -- ❌ 查询后立即用相同关键词再查 -- ❌ 超过3轮查询仍未找到结果时继续查询 - -## 查询示例 - -### 正确的多轮查询 -``` -用户: 我的鸿蒙开发环境在哪? - -第一轮查询: -{ - "query_intent": "鸿蒙,harmony,开发环境,IDE,工具链", - "depth": 2 -} - -如果未找到,第二轮查询: -{ - "query_intent": "鸿蒙,harmony,安装路径,目录,位置", - "depth": 1 -} - -如果仍未找到,告知用户并询问是否需要记录。 -``` - -### 错误的重复查询 -``` -❌ 第一轮: {"query_intent": "鸿蒙"} -❌ 第二轮: {"query_intent": "鸿蒙"} // 禁止重复! -``` - -现在开始对话!""" +现在开始对话!记住:图数据库是你记忆的唯一载体,明确提到的必须写入,推理得到的必须标注[猜测]。""" def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict: global CURRENT_TURN @@ -656,7 +659,35 @@ query_intent 应该是**逗号分隔的多个关键词**,包含**同义词/近 ) 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}] + + # 添加之前的 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, + tools=self.tools, + tool_choice="auto", + stream=True + ) + + return stream + def chat_loop(self): global CURRENT_TURN diff --git a/graph_memory_tui/app.py b/graph_memory_tui/app.py index 357c1c8..785d12f 100644 --- a/graph_memory_tui/app.py +++ b/graph_memory_tui/app.py @@ -72,11 +72,11 @@ class GraphMemoryApp(App[None]): def on_mount(self) -> None: """应用启动初始化""" history = self.query_one(MessageHistory) - + try: # 初始化内嵌图数据库 self._graph = Neo4jGraph(db_path="graph_memory.db") - + # 初始化 API 客户端 self._init_client() @@ -194,8 +194,8 @@ F6 - 退出 ) history.add_message(processing_msg) - # 异步处理(使用call_later避免崩溃) - self.call_later(self._process_message_safe, event.content) + # 异步处理 + asyncio.create_task(self._process_message_async(event.content)) except Exception as e: # 显示错误 @@ -206,22 +206,6 @@ F6 - 退出 ) history.add_message(error_msg) - def _process_message_safe(self, user_input: str) -> None: - """安全处理消息""" - try: - history = self.query_one(MessageHistory) - - # 简单测试响应 - response_msg = Message( - role="assistant", - content=f"收到消息: {user_input}\n\n完整功能需要配置API Key并连接API服务。", - timestamp=datetime.now() - ) - history.add_message(response_msg) - - except Exception as e: - print(f"Error: {e}") - async def _process_message_async(self, user_input: str) -> None: """异步处理消息 - 参考demo实现""" history = self.query_one(MessageHistory) @@ -246,7 +230,9 @@ F6 - 退出 # 处理工具调用循环 tool_calls = [] tool_results = [] - + last_assistant_msg = None + accumulated_content = "" # 累积所有中间内容 + # 显示工具调用摘要 if message.tool_calls: tool_summary = f"🔧 正在调用 {len(message.tool_calls)} 个工具..." @@ -256,9 +242,30 @@ F6 - 退出 timestamp=datetime.now() ) history.add_message(summary_msg) - + while message.tool_calls: - # 保存工具调用信息 + # 累积中间内容(如果有) + if message.content: + accumulated_content += message.content + "\n\n" + + # 保存包含 tool_calls 的 assistant 消息 + last_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 + ] + } + + # 执行当前轮的所有工具调用 + current_tool_results = [] for tool_call in message.tool_calls: tc = ToolCall( id=tool_call.id, @@ -266,7 +273,7 @@ F6 - 退出 arguments=json.loads(tool_call.function.arguments) ) tool_calls.append(tc) - + # 执行工具 start_time = datetime.now() result = await asyncio.get_event_loop().run_in_executor( @@ -274,7 +281,7 @@ F6 - 退出 lambda: execute_tool(self._graph, tc.name, tc.arguments) ) duration = (datetime.now() - start_time).total_seconds() - + # 保存工具结果 tr = ToolResult( tool_call_id=tc.id, @@ -284,7 +291,14 @@ F6 - 退出 success=not result.startswith("工具执行错误") ) tool_results.append(tr) - + + # 添加到当前轮结果 + current_tool_results.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": result + }) + # 添加日志 log_entry = LogEntry( timestamp=datetime.now(), @@ -294,14 +308,26 @@ F6 - 退出 duration=duration ) log.add_log(log_entry) - + # 继续调用API(参考demo的实现) - # 这里简化处理,实际应该像demo一样继续循环 - break + response = await asyncio.get_event_loop().run_in_executor( + None, + lambda: self._client.send_message( + user_input, + current_tool_results, + last_assistant_msg + ) + ) + message = response.choices[0].message # 添加助手消息(完整内容) - content = message.content or "(无回复)" - + # 使用累积的内容 + 最终内容 + 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] @@ -314,8 +340,12 @@ F6 - 退出 tool_calls=tool_calls if tool_calls else None, tool_results=tool_results if tool_results else None ) + history.add_message(assistant_message) + # 强制刷新界面 + self.refresh() + except Exception as e: # 显示详细错误 error_msg = str(e) @@ -362,6 +392,13 @@ API Key 未配置! # 持久化保存配置 self._config_manager.save(self._config) + # 同步更新 RightPanel 的配置 + try: + right_panel = self.query_one(RightPanel) + right_panel._config = self._config + except Exception: + pass + # 重新初始化客户端 self._init_client() diff --git a/graph_memory_tui/core/embedded_db.py b/graph_memory_tui/core/embedded_db.py index 1a039d9..ec126ef 100644 --- a/graph_memory_tui/core/embedded_db.py +++ b/graph_memory_tui/core/embedded_db.py @@ -94,31 +94,46 @@ class EmbeddedGraphDB: 检索结果 """ keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()] - - if not keywords and not seed_entities: - return {"entities": [], "relations": [], "message": "无查询关键词"} - + cursor = self.conn.cursor() - + # 搜索实体 entities = [] entity_ids = set() - - for keyword in keywords: + + # 如果没有关键词,返回所有实体(用于"我们都聊过什么"这类问题) + if not keywords and not seed_entities: cursor.execute(""" SELECT id, name, type, mention_count FROM entities - WHERE LOWER(name) LIKE ? - """, (f"%{keyword}%",)) - + ORDER BY mention_count DESC + LIMIT 50 + """) + 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'] - }) + 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'] + }) # 搜索关系 relations = [] diff --git a/graph_memory_tui/core/optimized_operations.py b/graph_memory_tui/core/optimized_operations.py index 9e0e6ac..48a4703 100644 --- a/graph_memory_tui/core/optimized_operations.py +++ b/graph_memory_tui/core/optimized_operations.py @@ -3,10 +3,114 @@ """ # 优化后的系统提示词 -OPTIMIZED_SYSTEM_PROMPT = """你是图数据库记忆助手。 +OPTIMIZED_SYSTEM_PROMPT = """你是一个拥有长期记忆能力的AI助手。 ## 核心职责 -你是用户的长期记忆助手。每次对话后,你**必须**主动决定是否需要将关键信息写入记忆图库。 +你是一个智能对话助手,**图数据库是你记忆的唯一载体**。你的主要任务是: +1. 与用户进行自然、流畅的对话 +2. 回答问题、提供建议、协助完成任务 +3. 根据对话内容灵活查询和使用记忆 +4. 将用户明确提到的信息写入记忆 + +**重要**: +- 图数据库是你记忆的唯一来源,没有其他记忆方式 +- 优先进行自然对话,根据需要灵活调用记忆工具 +- 用户明确提到的内容必须写入,推理得到的内容必须标注 + +## 记忆工具使用原则 + +### 何时检索记忆 (memory_recall) +- 用户询问"我们之前聊过X吗"、"你还记得X吗" → 查询X相关内容 +- 用户询问"我们都聊过什么"、"我们之前说了什么" → **使用空字符串或通配符查询所有记忆** +- 用户提到某个话题,你想确认是否有相关历史 → 查询该话题 +- 需要基于历史信息回答问题 → 查询相关信息 +- 对话中涉及之前可能讨论过的内容 → 查询相关内容 + +**灵活查询**:根据对话上下文,主动判断是否需要查询记忆,不要等待用户明确要求。 + +**重要**: +- 当用户问"我们都聊过什么"时,**不要查询"聊天记录"、"对话"等关键词** +- 应该使用空字符串 `""` 或通配符 `"*"` 来获取所有记忆内容 +- 或者使用非常宽泛的关键词如 `"用户,喜欢,项目,学习,研究,计划"` + +### 何时写入记忆 (memory_commit) +**必须写入的情况**(用户明确提到): +- 用户表达偏好:"我喜欢X"、"我讨厌X" +- 用户分享信息:"我在做X项目"、"我在学X" +- 用户制定计划:"我打算X"、"我计划X" +- 用户描述状态:"我现在在X" + +**禁止写入的情况**(AI推理得到): +- AI推断的用户偏好 +- AI猜测的用户意图 +- AI推导的结论 + +## memory_recall 使用方法 + +### 关键词提取 +query_intent 使用**逗号分隔的多个关键词**,包含同义词: + +```json +{ + "query_intent": "量子力学,quantum,物理,physics", + "depth": 2 +} +``` + +### 同义词扩展示例 +- "量子力学" → "量子力学,quantum,quantum mechanics,物理,physics" +- "项目" → "项目,project,工程,工作" +- "学习" → "学习,learn,study,掌握" + +### 查询规则 +1. 根据对话内容灵活提取关键词 +2. 第一轮使用广泛的关键词搜索 +3. 如果未找到,可以尝试相关概念 +4. 最多查询2-3轮,避免重复查询 + +## memory_commit 使用方法 + +使用三元组格式记录信息: +```json +{ + "triplets": [ + {"subject": "用户", "relation": "对领域感兴趣", "object": "量子力学"} + ] +} +``` + +## 区分事实与推理(重要!) + +### 用户明确提到的内容 +直接写入记忆,回复时直接陈述: +- 用户:"我喜欢Python" → 写入,回复:"好的,我会记住你喜欢Python" +- 用户:"我在学机器学习" → 写入,回复:"明白了,你在学习机器学习" + +### AI推理得到的内容 +**禁止写入记忆**,回复时必须在开头标注 **[猜测]**: +- AI推断用户可能喜欢X → 不写入,回复:"[猜测] 你可能对X感兴趣" +- AI推测用户意图 → 不写入,回复:"[猜测] 你可能是想..." + +**示例**: +``` +用户:我们聊过量子力学吗? +AI检索记忆 → 未找到 +AI回复:[猜测] 我们应该还没有聊过量子力学,因为记忆中没有相关记录。 +``` + +``` +用户:我最近在研究深度学习 +AI写入记忆 → {"subject": "用户", "relation": "正在研究", "object": "深度学习"} +AI回复:好的,我会记住你最近在研究深度学习。有什么具体问题想讨论吗? +``` + +## 可用工具 +1. **memory_recall** - 检索历史记忆(灵活使用) +2. **memory_commit** - 写入记忆(仅限用户明确提到的内容) +3. **memory_purge** - 删除/修正记忆 +4. **memory_introspect** - 查看记忆状态 + +现在开始对话!记住:图数据库是你记忆的唯一载体,明确提到的必须写入,推理得到的必须标注[猜测]。""" ## 多轮查询策略 **允许多轮查询**,但必须遵循以下规则: diff --git a/graph_memory_tui/handlers/message_handler.py b/graph_memory_tui/handlers/message_handler.py index b607f3e..eeec026 100644 --- a/graph_memory_tui/handlers/message_handler.py +++ b/graph_memory_tui/handlers/message_handler.py @@ -40,21 +40,44 @@ class MessageHandler: async def _process_response(self, user_input: str) -> None: """处理响应""" + streaming_message = None + async for event in self._chat_service.send_message(user_input): if event["type"] == "user_message": # 用户消息已处理 pass + elif event["type"] == "content_delta": + # 流式内容更新 + if streaming_message is None: + # 创建流式消息 + streaming_message = Message( + role="assistant", + content="", + timestamp=datetime.now() + ) + self._message_history.add_message(streaming_message) + + # 更新消息内容 + self._message_history.update_latest_message(event["content"]) + elif event["type"] == "assistant_message": - # 模型消息 - message = Message( - role="assistant", - content=event["content"], - timestamp=datetime.now(), - tool_calls=event.get("tool_calls"), - tool_results=event.get("tool_results") - ) - self._message_history.add_message(message) + # 模型消息完成 + if streaming_message: + # 更新最终消息(包含工具调用信息) + streaming_message.content = event["content"] + streaming_message.tool_calls = event.get("tool_calls") + streaming_message.tool_results = event.get("tool_results") + else: + # 如果没有流式消息,直接添加 + message = Message( + role="assistant", + content=event["content"], + timestamp=datetime.now(), + tool_calls=event.get("tool_calls"), + tool_results=event.get("tool_results") + ) + self._message_history.add_message(message) elif event["type"] == "tool_call": # 工具调用开始 diff --git a/graph_memory_tui/main.py b/graph_memory_tui/main.py index c0d29e2..ee4dfb2 100644 --- a/graph_memory_tui/main.py +++ b/graph_memory_tui/main.py @@ -15,11 +15,8 @@ from .models.config import AppConfig def main(): """主函数""" try: - # 加载配置 - config = AppConfig.from_env() - - # 创建并运行应用 - app = GraphMemoryApp(config=config) + # 不传入配置,让应用自己从配置文件或环境变量加载 + app = GraphMemoryApp() app.run() except KeyboardInterrupt: diff --git a/graph_memory_tui/services/chat_service.py b/graph_memory_tui/services/chat_service.py index ab6489a..039e543 100644 --- a/graph_memory_tui/services/chat_service.py +++ b/graph_memory_tui/services/chat_service.py @@ -34,18 +34,33 @@ class ChatService: } try: - # 2. 异步调用 API - response = await self._call_api_async(user_input) - + # 2. 使用流式API调用 + accumulated_content = "" + tool_calls_data = [] + + # 流式处理响应 + async for chunk in self._call_api_stream_async(user_input): + # 处理内容增量 + if chunk.get("content_delta"): + accumulated_content += chunk["content_delta"] + yield { + "type": "content_delta", + "content": accumulated_content + } + + # 处理工具调用 + if chunk.get("tool_calls"): + tool_calls_data = chunk["tool_calls"] + # 3. 处理工具调用 tool_calls = None tool_results = None - if response.tool_calls: + if tool_calls_data: tool_calls = [] tool_results = [] - for tool_call_data in response.tool_calls: + for tool_call_data in tool_calls_data: # 创建工具调用对象 tool_call = ToolCall( id=tool_call_data.id, @@ -75,7 +90,7 @@ class ChatService: # 4. 返回最终回复 yield { "type": "assistant_message", - "content": response.content, + "content": accumulated_content, "tool_calls": tool_calls, "tool_results": tool_results } @@ -87,13 +102,49 @@ class ChatService: "error": str(e) } - async def _call_api_async(self, message: str): - """异步调用 API""" + async def _call_api_stream_async(self, message: str) -> AsyncIterator[dict]: + """异步流式调用 API""" loop = asyncio.get_event_loop() - return await loop.run_in_executor( - None, - lambda: self._client.send_message(message) - ) + + # 在executor中运行同步流式API + def process_stream(): + stream = self._client.send_message_stream(message) + tool_calls_accumulated = [] + + for chunk in stream: + delta = chunk.choices[0].delta + + # 处理内容增量 + if delta.content: + yield {"content_delta": delta.content} + + # 处理工具调用 + if delta.tool_calls: + for tc in delta.tool_calls: + # 累积工具调用数据 + if tc.index >= len(tool_calls_accumulated): + tool_calls_accumulated.append({ + "id": tc.id, + "type": "function", + "function": { + "name": "", + "arguments": "" + } + }) + + if tc.function: + if tc.function.name: + tool_calls_accumulated[tc.index]["function"]["name"] = tc.function.name + if tc.function.arguments: + tool_calls_accumulated[tc.index]["function"]["arguments"] += tc.function.arguments + + # 返回完整的工具调用 + if tool_calls_accumulated: + yield {"tool_calls": tool_calls_accumulated} + + # 使用run_in_executor处理生成器 + for result in await loop.run_in_executor(None, lambda: list(process_stream())): + yield result def clear_history(self) -> None: """清空消息历史""" diff --git a/graph_memory_tui/styles/app.css b/graph_memory_tui/styles/app.css index bbcb205..20b29d7 100644 --- a/graph_memory_tui/styles/app.css +++ b/graph_memory_tui/styles/app.css @@ -25,6 +25,12 @@ RightPanel { width: 40; dock: right; background: $panel; + overflow-y: auto; +} + +RightPanel ScrollableContainer { + height: 1fr; + overflow-y: auto; } StatusBar { diff --git a/graph_memory_tui/styles/components.css b/graph_memory_tui/styles/components.css index 73e22af..46c4adb 100644 --- a/graph_memory_tui/styles/components.css +++ b/graph_memory_tui/styles/components.css @@ -19,18 +19,20 @@ InputBox Input { ConfigSection { background: $surface; padding: 1; - margin: 1; + margin: 0 0 1 0; + height: auto; } ConfigSection .config-title { color: $primary; text-style: bold; - margin: 1; + margin: 0 0 1 0; } ConfigSection .config-label { color: $text; - margin: 1 0 0 0; + margin: 0; + padding: 1 0 0 0; } ConfigSection .config-hint { @@ -41,7 +43,9 @@ ConfigSection .config-hint { ConfigSection Input { width: 1fr; + height: 3; margin: 0 0 1 0; + padding: 0 1; background: $surface-lighten-1; border: solid $primary; color: $text; @@ -52,14 +56,62 @@ OperationLog { background: $surface-darken-1; height: 1fr; margin: 1; + overflow-y: auto; } CypherQueryBox { border: solid green; margin: 1; + height: auto; } MessageHistory { height: 1fr; margin: 1; + overflow-y: auto; +} + +/* Message Widget */ +MessageWidget { + margin: 1 0; + height: auto; +} + +MessageWidget .message-header { + color: $text-muted; + text-style: bold; + margin: 0 0 0 0; +} + +MessageWidget .message-content { + color: $text; + margin: 0 0 0 2; + height: auto; +} + +MessageWidget .tool-indicator { + color: $warning; + text-style: bold; + margin: 1 0 0 2; +} + +MessageWidget .tool-details { + background: $surface-darken-1; + margin: 1 0 0 2; + padding: 1; +} + +MessageWidget .tool-name { + color: $accent; + text-style: bold; +} + +MessageWidget .tool-args { + color: $text-muted; + margin: 0 0 0 2; +} + +MessageWidget .tool-result { + color: $success; + margin: 0 0 0 2; } diff --git a/graph_memory_tui/widgets/config_section.py b/graph_memory_tui/widgets/config_section.py index 7f3a8d4..0a3b669 100644 --- a/graph_memory_tui/widgets/config_section.py +++ b/graph_memory_tui/widgets/config_section.py @@ -63,7 +63,7 @@ class ConfigSection(Vertical): yield hint def on_mount(self) -> None: - """组件挂载时设置Tab顺序""" + """组件挂载时设置Tab顺序并加载配置""" try: api_key = self.query_one("#api-key-input", Input) model = self.query_one("#model-input", Input) @@ -73,6 +73,14 @@ class ConfigSection(Vertical): 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: + model.value = self._config.model + if self._config.base_url: + base_url.value = self._config.base_url except Exception: pass diff --git a/graph_memory_tui/widgets/message_history.py b/graph_memory_tui/widgets/message_history.py index 480fdc0..e755551 100644 --- a/graph_memory_tui/widgets/message_history.py +++ b/graph_memory_tui/widgets/message_history.py @@ -27,6 +27,15 @@ class MessageHistory(ScrollableContainer): # 滚动到最新消息 self.scroll_to_widget(message_widget, animate=False) + def update_latest_message(self, content: str) -> None: + """更新最新消息的内容""" + if self.children: + latest_widget = self.children[-1] + if isinstance(latest_widget, MessageWidget): + latest_widget.update_content(content) + # 确保滚动到最新消息 + self.scroll_to_widget(latest_widget, animate=False) + def clear_messages(self) -> None: """清空消息历史""" self._messages.clear() diff --git a/graph_memory_tui/widgets/message_widget.py b/graph_memory_tui/widgets/message_widget.py index d9296ca..8db4958 100644 --- a/graph_memory_tui/widgets/message_widget.py +++ b/graph_memory_tui/widgets/message_widget.py @@ -13,6 +13,7 @@ class MessageWidget(Container): super().__init__(**kwargs) self._message = message self._show_tool_details = False + self._content_widget = None # 保存内容组件的引用 def compose(self): """构建消息组件""" @@ -20,12 +21,16 @@ class MessageWidget(Container): role_emoji = "🟠" if self._message.role == "user" else "🔵" timestamp_str = self._message.timestamp.strftime("%H:%M:%S") yield Static( - f"{role_emoji} [{self._message.role}] {timestamp_str}", + f"{role_emoji} {timestamp_str}", classes="message-header" ) - # 消息内容 - yield Static(self._message.content, classes="message-content") + # 消息内容 - 保存引用以便后续更新 + self._content_widget = Static( + self._message.content, + classes="message-content" + ) + yield self._content_widget # 工具调用指示器 if self._message.tool_calls: @@ -52,11 +57,22 @@ class MessageWidget(Container): if self._message.tool_results: for result in self._message.tool_results: if result.tool_call_id == tool_call.id: + # 显示完整结果,不截断 + result_text = result.result + # 如果结果太长,只显示前1000字符,但提供完整信息 + if len(result_text) > 1000: + result_text = result_text[:1000] + f"\n... (共{len(result.result)}字符,按F3查看完整内容)" yield Static( - f"结果: {result.result[:200]}...", + f"结果: {result_text}", classes="tool-result" ) + def update_content(self, new_content: str) -> None: + """更新消息内容""" + self._message.content = new_content + if self._content_widget: + self._content_widget.update(new_content) + def toggle_tool_details(self) -> None: """切换工具详情显示状态""" if self._message.tool_calls: diff --git a/graph_memory_tui/widgets/status_bar.py b/graph_memory_tui/widgets/status_bar.py index 01155eb..bd29dcb 100644 --- a/graph_memory_tui/widgets/status_bar.py +++ b/graph_memory_tui/widgets/status_bar.py @@ -15,22 +15,13 @@ class StatusBar(Static): def __init__(self, **kwargs): super().__init__(**kwargs) - self._focus_indicator = "[Input]" self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出" + self._license_info = "本项目由jianf设计,以GPLv3形式开源" def on_mount(self) -> None: """组件挂载时""" self._update_display() - def update_focus(self, focus_name: str) -> None: - """更新焦点指示器""" - self._focus_indicator = f"[{focus_name}]" - self._update_display() - def _update_display(self) -> None: """更新显示""" - self.update(f"{self._shortcuts} | 焦点: {self._focus_indicator}") - - def get_focus(self) -> str: - """获取当前焦点""" - return self._focus_indicator + self.update(f"{self._license_info} | {self._shortcuts}")