refactor: restructure to core/ + ui/ with multi-threaded backend

This commit is contained in:
root
2026-04-12 18:15:55 +08:00
parent c13d3f671c
commit 83906a6985
64 changed files with 5269 additions and 571 deletions

279
ui/app.py Normal file
View File

@ -0,0 +1,279 @@
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
class GraphMemoryApp(App[None]):
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, backend_server: BackendServer | 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()
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
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 StatusBar()
def on_mount(self) -> None:
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)
def on_unmount(self) -> None:
if self._backend_server:
self._backend_server.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)
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
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:
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()
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)
)
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)
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/
"""
else:
help_text = f"\n详细错误: {error_msg}"
error_message = Message(role="assistant", content=f"错误: {error_msg}\n{help_text}")
history.add_message(error_message)
def on_config_section_config_changed(self, event) -> None:
from .widgets.right_panel import RightPanel
self._config = event.config
self._config_manager.save(self._config)
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="警告")