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

1
ui/services/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Business Services for Graph Memory TUI"""

226
ui/services/chat_service.py Normal file
View File

@ -0,0 +1,226 @@
"""聊天服务"""
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()

View File

@ -0,0 +1,46 @@
"""
配置管理 - 支持持久化
"""
import json
from pathlib import Path
from ..models.config import AppConfig
class ConfigManager:
"""配置管理器 - 支持持久化"""
def __init__(self, config_file: str = "config.json"):
self.config_file = Path(config_file)
def save(self, config: AppConfig) -> None:
"""保存配置到文件"""
data = {
"api_key": config.api_key,
"model": config.model,
"base_url": config.base_url
}
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
def load(self) -> AppConfig:
"""从文件加载配置"""
if not self.config_file.exists():
return AppConfig()
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return AppConfig(
api_key=data.get("api_key", ""),
model=data.get("model", "deepseek-chat"),
base_url=data.get("base_url", "https://api.deepseek.com")
)
except Exception:
return AppConfig()
def exists(self) -> bool:
"""检查配置文件是否存在"""
return self.config_file.exists()

View File

@ -0,0 +1,51 @@
"""配置服务"""
from pathlib import Path
from typing import TYPE_CHECKING
from ..models.config import AppConfig
if TYPE_CHECKING:
from ..core.imports import GraphMemoryClient
class ConfigService:
"""配置服务"""
DEFAULT_CONFIG_FILE = Path.home() / ".graph_memory_tui" / "config.json"
def __init__(self, config_file: Path | None = None):
self._config_file = config_file or self.DEFAULT_CONFIG_FILE
self._config = self._load_config()
def _load_config(self) -> AppConfig:
"""加载配置"""
# 优先从文件加载
if self._config_file.exists():
return AppConfig.from_file(self._config_file)
# 否则从环境变量加载
return AppConfig.from_env()
def get_config(self) -> AppConfig:
"""获取当前配置"""
return self._config
def set_config(self, config: AppConfig) -> None:
"""设置配置"""
self._config = config
self._save_config()
def _save_config(self) -> None:
"""保存配置"""
self._config.save(self._config_file)
def apply_to_client(self, client: "GraphMemoryClient") -> None:
"""应用配置到 API 客户端"""
# 更新客户端配置
client.api_key = self._config.api_key
client.base_url = self._config.base_url
client.model = self._config.model
def get_config_file(self) -> Path:
"""获取配置文件路径"""
return self._config_file

View File

@ -0,0 +1,88 @@
"""工具服务"""
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