Files
TrulyMEM-TrueHumanMEM/graph_memory_tui/app.py
2026-04-11 09:30:56 +08:00

409 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""主应用类 - 参考demo实现"""
import asyncio
import json
from pathlib import Path
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Static
from textual.containers import Container
from datetime import datetime
from .widgets.left_panel import LeftPanel
from .widgets.right_panel import RightPanel
from .widgets.status_bar import StatusBar
from .widgets.input_box import InputBox
from .widgets.message_history import MessageHistory
from .models.message import Message, ToolCall, ToolResult
from .models.config import AppConfig
from .models.log_entry import LogEntry
from .services.config_manager import ConfigManager
from .core.imports import (
Neo4jGraph,
GraphMemoryClient,
execute_tool,
NEO4J_URI,
NEO4J_USER,
NEO4J_PASSWORD,
MODEL_NAME,
)
class GraphMemoryApp(App[None]):
"""Textual TUI 主应用"""
CSS_PATH = [
Path(__file__).parent / "styles" / "app.css",
Path(__file__).parent / "styles" / "messages.css",
Path(__file__).parent / "styles" / "components.css",
]
BINDINGS = [
Binding("f1", "show_help", "帮助"),
Binding("f2", "toggle_sidebar", "侧边栏"),
Binding("f3", "toggle_tool_details", "工具详情"),
Binding("f4", "focus_query", "查询"),
Binding("f5", "clear_history", "清屏"),
Binding("f6", "quit", "退出"),
]
def __init__(self, config: AppConfig | None = None, **kwargs):
super().__init__(**kwargs)
# 配置管理器
self._config_manager = ConfigManager()
# 优先使用传入的配置,其次加载持久化配置,最后使用环境变量
if config:
self._config = config
elif self._config_manager.exists():
self._config = self._config_manager.load()
else:
self._config = AppConfig.from_env()
# 核心组件
self._graph: Neo4jGraph | None = None
self._client: GraphMemoryClient | None = None
def compose(self) -> ComposeResult:
"""构建组件树"""
yield LeftPanel()
yield RightPanel(self._config)
yield StatusBar()
def on_mount(self) -> None:
"""应用启动初始化"""
history = self.query_one(MessageHistory)
try:
# 初始化内嵌图数据库
self._graph = Neo4jGraph(db_path="graph_memory.db")
# 初始化 API 客户端
self._init_client()
# 显示连接成功消息
welcome = Message(
role="assistant",
content="✅ 系统初始化成功!\n\n"
f"• 数据库: 内嵌SQLite (graph_memory.db)\n"
f"• API Key: {'已配置' if self._config.api_key else '未配置'}\n\n"
"现在可以开始对话了!",
timestamp=datetime.now()
)
history.add_message(welcome)
except Exception as e:
# 显示错误消息
error = Message(
role="assistant",
content=f"❌ 初始化失败: {str(e)}\n\n"
"请检查:\n"
"1. Neo4j 数据库是否启动 (运行: docker start neo4j)\n"
"2. API Key 是否配置\n"
"3. 网络连接是否正常\n\n"
"启动Neo4j: docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/graphmemory123 neo4j:latest",
timestamp=datetime.now()
)
history.add_message(error)
def _init_client(self) -> None:
"""初始化API客户端"""
if self._config.api_key and self._graph:
self._client = GraphMemoryClient(
api_key=self._config.api_key,
base_url=self._config.base_url,
graph=self._graph
)
def on_unmount(self) -> None:
"""应用退出清理"""
if self._graph:
self._graph.close()
# 快捷键动作
def action_show_help(self) -> None:
"""显示帮助"""
help_text = """
快捷键:
F1 - 帮助
F2 - 切换侧边栏
F3 - 工具详情
F4 - 查询框
F5 - 清屏
F6 - 退出
输入消息后按 Enter 发送
"""
self.notify(help_text, title="帮助", timeout=10)
def action_toggle_sidebar(self) -> None:
"""切换侧边栏"""
sidebar = self.query_one(RightPanel)
sidebar.toggle()
sidebar.update_title()
def action_toggle_tool_details(self) -> None:
"""切换工具详情"""
history = self.query_one(MessageHistory)
history.toggle_latest_tool_details()
def action_focus_query(self) -> None:
"""聚焦查询框"""
sidebar = self.query_one(RightPanel)
if sidebar.is_collapsed():
sidebar.toggle()
sidebar.update_title()
try:
query_box = sidebar.get_cypher_query_box()
query_box.focus()
except:
pass
def action_clear_history(self) -> None:
"""清屏"""
history = self.query_one(MessageHistory)
history.clear_messages()
# 事件处理
def on_input_box_send_message(self, event: InputBox.SendMessage) -> None:
"""处理发送消息事件"""
try:
# 添加用户消息
history = self.query_one(MessageHistory)
user_message = Message(
role="user",
content=event.content,
timestamp=datetime.now()
)
history.add_message(user_message)
# 简单响应(避免崩溃)
if not self._config.api_key:
response_msg = Message(
role="assistant",
content="请先配置API Key。\n\n按F2打开侧边栏输入API Key后按Enter保存。",
timestamp=datetime.now()
)
history.add_message(response_msg)
return
# 显示处理中消息
processing_msg = Message(
role="assistant",
content="正在处理...",
timestamp=datetime.now()
)
history.add_message(processing_msg)
# 异步处理
asyncio.create_task(self._process_message_async(event.content))
except Exception as e:
# 显示错误
error_msg = Message(
role="assistant",
content=f"错误: {str(e)}",
timestamp=datetime.now()
)
history.add_message(error_msg)
async def _process_message_async(self, user_input: str) -> None:
"""异步处理消息 - 参考demo实现"""
history = self.query_one(MessageHistory)
log = self.query_one(RightPanel).get_operation_log()
try:
# 检查客户端
if not self._client:
# 尝试重新初始化
self._init_client()
if not self._client:
raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key然后按 Enter 保存")
# 参考demo的调用方式
response = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._client.send_message(user_input)
)
message = response.choices[0].message
# 处理工具调用循环
tool_calls = []
tool_results = []
last_assistant_msg = None
accumulated_content = "" # 累积所有中间内容
# 显示工具调用摘要
if message.tool_calls:
tool_summary = f"🔧 正在调用 {len(message.tool_calls)} 个工具..."
summary_msg = Message(
role="assistant",
content=tool_summary,
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,
name=tool_call.function.name,
arguments=json.loads(tool_call.function.arguments)
)
tool_calls.append(tc)
# 执行工具
start_time = datetime.now()
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: execute_tool(self._graph, tc.name, tc.arguments)
)
duration = (datetime.now() - start_time).total_seconds()
# 保存工具结果
tr = ToolResult(
tool_call_id=tc.id,
name=tc.name,
arguments=tc.arguments,
result=result,
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(),
tool_name=tc.name,
arguments=tc.arguments,
result=result,
duration=duration
)
log.add_log(log_entry)
# 继续调用API参考demo的实现
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
# 添加助手消息(完整内容)
# 使用累积的内容 + 最终内容
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}"
assistant_message = Message(
role="assistant",
content=content,
timestamp=datetime.now(),
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)
if "Connection error" in error_msg or "connection" in error_msg.lower():
help_text = """
网络连接错误!可能的原因:
1. API Key 未配置或无效
2. 网络无法访问 API 服务器
3. API 服务器暂时不可用
解决方法:
• 按 F2 展开侧边栏,检查并配置 API Key
• 检查网络连接
• 尝试使用代理或 VPN
"""
elif "API Key" in error_msg:
help_text = """
API Key 未配置!
请按以下步骤配置:
1. 按 F2 展开右侧边栏
2. 点击"配置"展开配置区
3. 在 API Key 输入框输入你的密钥
4. 按 Enter 键保存配置
获取 API Key: https://platform.deepseek.com/
"""
else:
help_text = f"\n详细错误: {error_msg}"
error_message = Message(
role="assistant",
content=f"❌ 错误: {error_msg}\n{help_text}",
timestamp=datetime.now()
)
history.add_message(error_message)
def on_config_section_config_changed(self, event) -> None:
"""处理配置变更事件"""
# 更新配置
self._config = event.config
# 持久化保存配置
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()
if self._client:
self.notify("✅ 配置已保存并应用", title="配置")
else:
self.notify("⚠️ 配置已保存但API Key无效", title="警告")