mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 09:28:19 +00:00
refactor: 统一 Packet 通信协议 + 后端配置管理 + UI 清理
- 合并 server.py 到 core/__init__.py,使用统一 Packet 协议 - 后端管理配置持久化 (~/.trulymem/config.json) - 前端移除 ConfigService,通过 BackendClient 与后端通信 - 删除 UI 中冗余的 AI 推理逻辑 (chat_service, tool_service, message_handler) - 删除 core/tools 重复文件 (tool_executor, tool_limiter) - 提示词管理器支持用户自定义 (~/.trulyemem/system_prompt.md) - 启动入口优化配置路径逻辑 - 更新测试覆盖 (42 tests) - 更新文档
This commit is contained in:
75
ui/app.py
75
ui/app.py
@ -5,7 +5,6 @@ from textual.binding import Binding
|
||||
|
||||
from core import BackendServer, BackendClient
|
||||
from .models.message import Message
|
||||
from .services.config_service import ConfigService
|
||||
|
||||
|
||||
class GraphMemoryApp(App):
|
||||
@ -23,11 +22,10 @@ class GraphMemoryApp(App):
|
||||
Binding("f6", "quit", "退出"),
|
||||
]
|
||||
|
||||
def __init__(self, backend_server: BackendServer = None, config_service: ConfigService = None, **kwargs):
|
||||
def __init__(self, backend_server: BackendServer = None, config_file: str = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._backend_server = backend_server
|
||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||
self._config_service = config_service
|
||||
self._api_configured = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
@ -36,8 +34,16 @@ class GraphMemoryApp(App):
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .models.config import AppConfig
|
||||
|
||||
# 获取初始配置
|
||||
initial_config = self._config_service.get_config() if self._config_service else AppConfig()
|
||||
if self._backend_client:
|
||||
result = self._backend_client.get_config()
|
||||
config_data = result.get("data", {})
|
||||
initial_config = AppConfig(
|
||||
api_key=config_data.get("api_key", ""),
|
||||
base_url=config_data.get("base_url", "https://api.deepseek.com"),
|
||||
model=config_data.get("model", "deepseek-chat")
|
||||
)
|
||||
else:
|
||||
initial_config = AppConfig()
|
||||
|
||||
yield LeftPanel()
|
||||
yield RightPanel(config=initial_config)
|
||||
@ -56,7 +62,8 @@ class GraphMemoryApp(App):
|
||||
return
|
||||
|
||||
status = self._backend_client.get_status()
|
||||
self._api_configured = status.get("config", {}).get("api_key", "") != ""
|
||||
data = status.get("data", {})
|
||||
self._api_configured = data.get("config", {}).get("api_key", "") != ""
|
||||
status_bar.set_api_status(self._api_configured)
|
||||
|
||||
from .widgets.message_history import MessageHistory
|
||||
@ -118,24 +125,19 @@ class GraphMemoryApp(App):
|
||||
history = self.query_one(MessageHistory)
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
try:
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.send_message(user_input)
|
||||
)
|
||||
|
||||
# 更新"处理中"消息为实际回复
|
||||
if result.get("success"):
|
||||
content = result.get("content", "(无回复)")
|
||||
history.update_latest_message(content)
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
history.update_latest_message(f"❌ 错误: {error}")
|
||||
|
||||
except Exception as e:
|
||||
history.update_latest_message(f"❌ 异常: {str(e)}")
|
||||
finally:
|
||||
status_bar.set_processing(False)
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.process_message(user_input)
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
content = result.get("content", "(无回复)")
|
||||
history.update_latest_message(content)
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
history.update_latest_message(f"❌ 错误: {error}")
|
||||
|
||||
status_bar.set_processing(False)
|
||||
|
||||
def on_config_section_config_changed(self, event) -> None:
|
||||
"""处理配置变更事件"""
|
||||
@ -151,24 +153,41 @@ class GraphMemoryApp(App):
|
||||
async def _update_config_async(self, config) -> None:
|
||||
"""异步更新配置"""
|
||||
from .widgets.status_bar import StatusBar
|
||||
from .widgets.config_section import ConfigSection
|
||||
|
||||
status_bar = self.query_one(StatusBar)
|
||||
|
||||
api_key = config.api_key
|
||||
base_url = config.base_url
|
||||
model = getattr(config, 'model', 'deepseek-chat')
|
||||
|
||||
try:
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.update_config(api_key=api_key, base_url=base_url)
|
||||
lambda: self._backend_client.update_config(api_key=api_key, base_url=base_url, model=model)
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
self._api_configured = bool(api_key)
|
||||
status_bar.set_api_status(self._api_configured)
|
||||
|
||||
# 保存配置到文件(使用完整的config对象,保留model字段)
|
||||
if self._config_service:
|
||||
self._config_service.set_config(config)
|
||||
# 从后端重新获取配置并刷新 UI
|
||||
config_result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self._backend_client.get_config()
|
||||
)
|
||||
config_data = config_result.get("data", {})
|
||||
|
||||
# 刷新输入框
|
||||
try:
|
||||
config_section = self.query_one(ConfigSection)
|
||||
config_section.set_config(AppConfig(
|
||||
api_key=config_data.get("api_key", ""),
|
||||
base_url=config_data.get("base_url", "https://api.deepseek.com"),
|
||||
model=config_data.get("model", "deepseek-chat")
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.notify("✅ 配置已保存并生效", title="配置成功", severity="information")
|
||||
else:
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
"""消息处理器"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from ..models.message import Message, ToolCall, ToolResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..services.chat_service import ChatService
|
||||
from ..widgets.message_history import MessageHistory
|
||||
from ..widgets.operation_log import OperationLog
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
"""消息处理器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat_service: "ChatService",
|
||||
message_history: "MessageHistory",
|
||||
operation_log: "OperationLog"
|
||||
):
|
||||
self._chat_service = chat_service
|
||||
self._message_history = message_history
|
||||
self._operation_log = operation_log
|
||||
|
||||
async def handle_user_message(self, content: str) -> None:
|
||||
"""处理用户消息"""
|
||||
# 创建用户消息
|
||||
user_message = Message(
|
||||
role="user",
|
||||
content=content,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
# 添加到历史
|
||||
self._message_history.add_message(user_message)
|
||||
|
||||
# 发送到聊天服务
|
||||
await self._process_response(content)
|
||||
|
||||
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":
|
||||
# 模型消息完成
|
||||
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":
|
||||
# 工具调用开始
|
||||
pass
|
||||
|
||||
elif event["type"] == "tool_result":
|
||||
# 工具执行结果
|
||||
log_entry = event["log_entry"]
|
||||
self._operation_log.add_log(log_entry)
|
||||
|
||||
elif event["type"] == "error":
|
||||
# 错误处理
|
||||
error_message = Message(
|
||||
role="assistant",
|
||||
content=f"错误: {event['error']}",
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
self._message_history.add_message(error_message)
|
||||
@ -1,226 +0,0 @@
|
||||
"""聊天服务"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import AsyncIterator, TYPE_CHECKING, List, Dict, Any
|
||||
from ..core.imports import GraphMemoryClient
|
||||
from ..models.message import ToolCall, ToolResult
|
||||
from .tool_service import ToolService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core.imports import Neo4jGraph
|
||||
|
||||
|
||||
class ChatService:
|
||||
"""聊天业务服务"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph: "Neo4jGraph",
|
||||
client: GraphMemoryClient,
|
||||
tool_service: ToolService
|
||||
):
|
||||
self._graph = graph
|
||||
self._client = client
|
||||
self._tool_service = tool_service
|
||||
self._messages: List[Dict[str, Any]] = []
|
||||
|
||||
async def send_message(self, user_input: str) -> AsyncIterator[dict]:
|
||||
"""发送消息并流式返回事件"""
|
||||
# 1. 发送用户消息事件
|
||||
yield {
|
||||
"type": "user_message",
|
||||
"content": user_input
|
||||
}
|
||||
|
||||
try:
|
||||
# 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. 如果有工具调用,执行并继续调用API
|
||||
tool_calls = None
|
||||
tool_results = None
|
||||
|
||||
if tool_calls_data:
|
||||
tool_calls = []
|
||||
tool_results = []
|
||||
|
||||
# 执行所有工具
|
||||
for tool_call_data in tool_calls_data:
|
||||
tool_call = ToolCall(
|
||||
id=tool_call_data["id"],
|
||||
name=tool_call_data["function"]["name"],
|
||||
arguments=tool_call_data["function"]["arguments"]
|
||||
)
|
||||
tool_calls.append(tool_call)
|
||||
|
||||
yield {
|
||||
"type": "tool_call",
|
||||
"tool_call": tool_call
|
||||
}
|
||||
|
||||
result = await self._tool_service.execute(tool_call)
|
||||
tool_results.append(result)
|
||||
|
||||
log_entry = ToolService._create_log_entry(tool_call, result)
|
||||
yield {
|
||||
"type": "tool_result",
|
||||
"tool_result": result,
|
||||
"log_entry": log_entry
|
||||
}
|
||||
|
||||
# 构建工具结果消息
|
||||
tool_messages = []
|
||||
for tc, tr in zip(tool_calls, tool_results):
|
||||
tool_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tc.id,
|
||||
"content": tr.content
|
||||
})
|
||||
|
||||
# 构建assistant消息(包含tool_calls)
|
||||
assistant_message = {
|
||||
"role": "assistant",
|
||||
"content": accumulated_content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.name,
|
||||
"arguments": tc.arguments
|
||||
}
|
||||
} for tc in tool_calls
|
||||
]
|
||||
}
|
||||
|
||||
# 第二次API调用,传入工具结果
|
||||
final_content = ""
|
||||
async for chunk in self._call_api_stream_with_tools(
|
||||
user_input,
|
||||
assistant_message,
|
||||
tool_messages
|
||||
):
|
||||
if chunk.get("content_delta"):
|
||||
final_content += chunk["content_delta"]
|
||||
yield {
|
||||
"type": "content_delta",
|
||||
"content": final_content
|
||||
}
|
||||
|
||||
accumulated_content = final_content
|
||||
|
||||
# 4. 返回最终回复
|
||||
yield {
|
||||
"type": "assistant_message",
|
||||
"content": accumulated_content,
|
||||
"tool_calls": tool_calls,
|
||||
"tool_results": tool_results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
yield {
|
||||
"type": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def _call_api_stream_async(self, message: str) -> AsyncIterator[dict]:
|
||||
"""异步流式调用 API"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
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}
|
||||
|
||||
for result in await loop.run_in_executor(None, lambda: list(process_stream())):
|
||||
yield result
|
||||
|
||||
async def _call_api_stream_with_tools(
|
||||
self,
|
||||
user_input: str,
|
||||
assistant_message: dict,
|
||||
tool_messages: list
|
||||
) -> AsyncIterator[dict]:
|
||||
"""带工具结果的流式调用"""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def process_stream():
|
||||
# 构建完整的消息列表
|
||||
messages = [
|
||||
{"role": "system", "content": self._client.system_prompt},
|
||||
{"role": "user", "content": user_input},
|
||||
assistant_message
|
||||
]
|
||||
messages.extend(tool_messages)
|
||||
|
||||
# 调用API
|
||||
response = self._client.client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=messages,
|
||||
tools=self._client.tools,
|
||||
tool_choice="auto",
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
delta = chunk.choices[0].delta
|
||||
if delta.content:
|
||||
yield {"content_delta": delta.content}
|
||||
|
||||
# 处理可能的工具调用
|
||||
if delta.tool_calls:
|
||||
# 如果还有工具调用,说明AI想继续调用工具
|
||||
# 但我们限制只调用一次,所以忽略
|
||||
pass
|
||||
|
||||
for result in await loop.run_in_executor(None, lambda: list(process_stream())):
|
||||
yield result
|
||||
|
||||
def clear_history(self) -> None:
|
||||
"""清空消息历史"""
|
||||
self._messages.clear()
|
||||
|
||||
def get_history(self) -> list[dict]:
|
||||
"""获取消息历史"""
|
||||
return self._messages.copy()
|
||||
@ -1,88 +0,0 @@
|
||||
"""工具服务"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
from ..core.imports import execute_tool
|
||||
from ..models.log_entry import LogEntry
|
||||
from ..models.message import ToolCall, ToolResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core.imports import Neo4jGraph
|
||||
|
||||
|
||||
class ToolService:
|
||||
"""工具执行服务"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph: "Neo4jGraph",
|
||||
log_callback: Callable[[LogEntry], None] | None = None
|
||||
):
|
||||
self._graph = graph
|
||||
self._log_callback = log_callback
|
||||
|
||||
async def execute(self, tool_call: ToolCall) -> ToolResult:
|
||||
"""异步执行工具"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 在线程池中执行同步工具
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: execute_tool(self._graph, tool_call.name, tool_call.arguments)
|
||||
)
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
# 创建日志条目
|
||||
log_entry = LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
tool_name=tool_call.name,
|
||||
arguments=tool_call.arguments,
|
||||
result=result,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
# 回调日志
|
||||
if self._log_callback:
|
||||
self._log_callback(log_entry)
|
||||
|
||||
# 返回结果
|
||||
return ToolResult(
|
||||
tool_call_id=tool_call.id,
|
||||
name=tool_call.name,
|
||||
arguments=tool_call.arguments,
|
||||
result=result,
|
||||
success=not result.startswith("工具执行错误")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
error_msg = f"工具执行异常: {str(e)}"
|
||||
|
||||
# 创建错误日志
|
||||
log_entry = LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
tool_name=tool_call.name,
|
||||
arguments=tool_call.arguments,
|
||||
result=error_msg,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
if self._log_callback:
|
||||
self._log_callback(log_entry)
|
||||
|
||||
return ToolResult(
|
||||
tool_call_id=tool_call.id,
|
||||
name=tool_call.name,
|
||||
arguments=tool_call.arguments,
|
||||
result=error_msg,
|
||||
success=False
|
||||
)
|
||||
|
||||
def set_log_callback(self, callback: Callable[[LogEntry], None]) -> None:
|
||||
"""设置日志回调"""
|
||||
self._log_callback = callback
|
||||
Reference in New Issue
Block a user