mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 09:28:19 +00:00
refactor: packet-based IPC with core/ui separation
- All logic in core layer (BackendServer runs independently)
- Packet protocol: {id, type, body} for all communications
- UI simplified: just input→backend→display
- Config, history, tools all handled by core
- BackendServer can run standalone without UI
This commit is contained in:
266
ui/app.py
266
ui/app.py
@ -2,17 +2,12 @@ import asyncio
|
||||
from pathlib import Path
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from datetime import datetime
|
||||
|
||||
from core import BackendServer, BackendClient
|
||||
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 import BackendClient
|
||||
from .models.message import Message
|
||||
|
||||
|
||||
class GraphMemoryApp(App[None]):
|
||||
class GraphMemoryApp(App):
|
||||
CSS_PATH = [
|
||||
Path(__file__).parent / "styles" / "app.css",
|
||||
Path(__file__).parent / "styles" / "messages.css",
|
||||
@ -23,257 +18,108 @@ class GraphMemoryApp(App[None]):
|
||||
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, backend_server: BackendServer | None = None, **kwargs):
|
||||
def __init__(self, backend_server: BackendServer = 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()
|
||||
|
||||
if backend_server:
|
||||
self._backend_server = backend_server
|
||||
self._backend_client = BackendClient(backend_server)
|
||||
else:
|
||||
self._backend_server: BackendServer | None = None
|
||||
self._backend_client: BackendClient | None = None
|
||||
self._backend_server = backend_server
|
||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
from .widgets.left_panel import LeftPanel
|
||||
from .widgets.right_panel import RightPanel
|
||||
from .widgets.status_bar import StatusBar
|
||||
|
||||
yield LeftPanel()
|
||||
yield RightPanel(self._config, use_embedded_db=True)
|
||||
yield RightPanel()
|
||||
yield StatusBar()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
if not self._backend_server:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
error = Message(role="assistant", content="后端未初始化")
|
||||
history.add_message(error)
|
||||
return
|
||||
|
||||
status = self._backend_client.get_status()
|
||||
api_configured = status.get("config", {}).get("api_key", "") != ""
|
||||
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
|
||||
try:
|
||||
self._backend_server = BackendServer(
|
||||
db_path="graph_memory.db",
|
||||
use_embedded_db=True
|
||||
)
|
||||
self._backend_server.start(
|
||||
api_key=self._config.api_key,
|
||||
base_url=self._config.base_url
|
||||
)
|
||||
|
||||
self._backend_client = BackendClient(self._backend_server)
|
||||
|
||||
welcome = Message(
|
||||
role="assistant",
|
||||
content="系统初始化成功!\n\n"
|
||||
f"数据库: 内嵌SQLite (graph_memory.db)\n"
|
||||
f"API Key: {'已配置' if self._config.api_key else '未配置'}\n\n"
|
||||
"现在可以开始对话了!",
|
||||
)
|
||||
history.add_message(welcome)
|
||||
|
||||
except Exception as e:
|
||||
error = Message(
|
||||
role="assistant",
|
||||
content=f"初始化失败: {str(e)}\n\n"
|
||||
"请检查:\n"
|
||||
"1. API Key 是否配置\n"
|
||||
"2. 网络连接是否正常\n\n"
|
||||
"按F2打开侧边栏配置API Key",
|
||||
)
|
||||
history.add_message(error)
|
||||
welcome = Message(
|
||||
role="assistant",
|
||||
content=f"系统就绪\nAPI Key: {'已配置' if api_configured else '未配置'}\n\n输入消息开始对话"
|
||||
)
|
||||
history.add_message(welcome)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
if self._backend_server:
|
||||
self._backend_server.shutdown()
|
||||
if self._backend_client:
|
||||
self._backend_client.shutdown()
|
||||
|
||||
def action_show_help(self) -> None:
|
||||
help_text = """
|
||||
快捷键:
|
||||
F1 - 帮助
|
||||
F2 - 切换侧边栏
|
||||
F3 - 工具详情
|
||||
F4 - 查询框
|
||||
F5 - 清屏
|
||||
F6 - 退出
|
||||
|
||||
输入消<EFBFBD><EFBFBD>后按 Enter 发送
|
||||
"""
|
||||
self.notify(help_text, title="帮助", timeout=10)
|
||||
self.notify("F1-帮助 F2-侧边栏 F3-工具详情 F5-清屏 F6-退出", title="快捷键", timeout=10)
|
||||
|
||||
def action_toggle_sidebar(self) -> None:
|
||||
from .widgets.right_panel import RightPanel
|
||||
sidebar = self.query_one(RightPanel)
|
||||
sidebar.toggle()
|
||||
sidebar.update_title()
|
||||
|
||||
def action_toggle_tool_details(self) -> None:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.toggle_latest_tool_details()
|
||||
|
||||
def action_focus_query(self) -> None:
|
||||
from .widgets.right_panel import RightPanel
|
||||
sidebar = self.query_one(RightPanel)
|
||||
|
||||
if not sidebar.has_cypher_query_box():
|
||||
self.notify("查询框仅在 Neo4j 模式下可用", title="提示", timeout=3)
|
||||
return
|
||||
|
||||
if sidebar.is_collapsed():
|
||||
sidebar.toggle()
|
||||
sidebar.update_title()
|
||||
|
||||
query_box = sidebar.get_cypher_query_box()
|
||||
if query_box:
|
||||
query_box.focus()
|
||||
|
||||
def action_clear_history(self) -> None:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.clear_messages()
|
||||
|
||||
def on_input_box_send_message(self, event) -> None:
|
||||
from .widgets.input_box import InputBox
|
||||
from .widgets.message_history import MessageHistory
|
||||
from .widgets.right_panel import RightPanel
|
||||
if not self._backend_client:
|
||||
return
|
||||
|
||||
try:
|
||||
history = self.query_one(MessageHistory)
|
||||
user_message = Message(role="user", content=event.content)
|
||||
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保存。",
|
||||
)
|
||||
history.add_message(response_msg)
|
||||
return
|
||||
|
||||
processing_msg = Message(role="assistant", content="正在处理...")
|
||||
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)}")
|
||||
history.add_message(error_msg)
|
||||
|
||||
async def _process_message_async(self, user_input: str) -> None:
|
||||
user_input = event.content
|
||||
from .widgets.message_history import MessageHistory
|
||||
from .widgets.right_panel import RightPanel
|
||||
|
||||
history = self.query_one(MessageHistory)
|
||||
log = self.query_one(RightPanel).get_operation_log()
|
||||
|
||||
history.add_message(Message(role="user", content=user_input))
|
||||
history.add_message(Message(role="assistant", content="处理中..."))
|
||||
|
||||
asyncio.create_task(self._process(user_input))
|
||||
|
||||
async def _process(self, user_input: str) -> None:
|
||||
try:
|
||||
if not self._backend_client:
|
||||
raise Exception("后端未初始化")
|
||||
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.process_message(user_input)
|
||||
lambda: self._backend_client.send_message(user_input)
|
||||
)
|
||||
|
||||
content = result.get("content", "(无回复)")
|
||||
tool_calls_data = result.get("tool_calls", [])
|
||||
rejected_tools = result.get("rejected_tools", [])
|
||||
|
||||
tool_calls = []
|
||||
tool_results = []
|
||||
|
||||
for tc in tool_calls_data:
|
||||
tc_obj = ToolCall(
|
||||
id=tc.get("id", ""),
|
||||
name=tc.get("name", ""),
|
||||
arguments=tc.get("arguments", {})
|
||||
)
|
||||
tool_calls.append(tc_obj)
|
||||
|
||||
tr = ToolResult(
|
||||
tool_call_id=tc_obj.id,
|
||||
name=tc_obj.name,
|
||||
arguments=tc_obj.arguments,
|
||||
result=tc.get("result", ""),
|
||||
success=not tc.get("result", "").startswith("工具执行<EFBFBD><EFBFBD><EFBFBD>误")
|
||||
)
|
||||
tool_results.append(tr)
|
||||
|
||||
log_entry = LogEntry(
|
||||
tool_name=tc_obj.name,
|
||||
arguments=tc_obj.arguments,
|
||||
result=tc.get("result", ""),
|
||||
)
|
||||
log.add_log(log_entry)
|
||||
|
||||
assistant_message = Message(
|
||||
role="assistant",
|
||||
content=content,
|
||||
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)
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.clear_messages()
|
||||
|
||||
if "Connection error" in error_msg or "connection" in error_msg.lower():
|
||||
help_text = """
|
||||
网络连接错误!可能的原因:
|
||||
1. API Key 未配置或无效
|
||||
2. 网络无法访问 API 服务器
|
||||
3. API 服务器暂时不可用
|
||||
|
||||
解决方法:
|
||||
按 F2 展开侧边栏,检查并配置 API Key
|
||||
检查网络连接
|
||||
"""
|
||||
elif "API Key" in error_msg:
|
||||
help_text = """
|
||||
API Key 未配置!
|
||||
|
||||
请按以下步骤配置:
|
||||
1. 按 F2 展开右侧边栏
|
||||
2. 点击"配置"展开配置区
|
||||
3. 在 API Key 输入框输入你的密钥
|
||||
4. 按 Enter 键保存配置
|
||||
|
||||
获取 API Key: https://platform.deepseek.com/
|
||||
"""
|
||||
history.add_message(Message(role="user", content=user_input))
|
||||
|
||||
if result.get("success"):
|
||||
content = result.get("content", "(无回复)")
|
||||
history.add_message(Message(role="assistant", content=content))
|
||||
else:
|
||||
help_text = f"\n详细错误: {error_msg}"
|
||||
|
||||
error_message = Message(role="assistant", content=f"错误: {error_msg}\n{help_text}")
|
||||
history.add_message(error_message)
|
||||
error = result.get("error", "未知错误")
|
||||
history.add_message(Message(role="assistant", content=f"错误: {error}"))
|
||||
|
||||
except Exception as e:
|
||||
from .widgets.message_history import MessageHistory
|
||||
history = self.query_one(MessageHistory)
|
||||
history.add_message(Message(role="assistant", content=f"错误: {str(e)}"))
|
||||
|
||||
def on_config_section_config_changed(self, event) -> None:
|
||||
from .widgets.right_panel import RightPanel
|
||||
def on_config_changed(self, event) -> None:
|
||||
if not self._backend_client:
|
||||
return
|
||||
|
||||
self._config = event.config
|
||||
self._config_manager.save(self._config)
|
||||
api_key = event.api_key
|
||||
base_url = event.base_url
|
||||
|
||||
try:
|
||||
right_panel = self.query_one(RightPanel)
|
||||
right_panel._config = self._config
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._backend_client:
|
||||
self._backend_client.update_config(
|
||||
api_key=self._config.api_key,
|
||||
base_url=self._config.base_url
|
||||
)
|
||||
self.notify("配置已保存并应用", title="配置")
|
||||
else:
|
||||
self.notify("配置已保存,但后端未初始化", title="警告")
|
||||
result = self._backend_client.update_config(api_key=api_key, base_url=base_url)
|
||||
self.notify("配置已保存" if result.get("success") else "配置失败", title="配置")
|
||||
Reference in New Issue
Block a user