mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 17:38:18 +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:
250
core/__init__.py
250
core/__init__.py
@ -1,242 +1,12 @@
|
|||||||
import threading
|
from .server import BackendServer, Packet, PacketType, PacketResponse
|
||||||
import queue
|
from .client import BackendClient
|
||||||
import time
|
|
||||||
from typing import Any, Dict
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class PacketType(Enum):
|
|
||||||
MESSAGE = "message"
|
|
||||||
CONFIG = "config"
|
|
||||||
TOOL = "tool"
|
|
||||||
STATUS = "status"
|
|
||||||
HISTORY = "history"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Packet:
|
|
||||||
id: str
|
|
||||||
type: PacketType
|
|
||||||
body: Dict[str, Any]
|
|
||||||
created_at: float = field(default_factory=time.time)
|
|
||||||
|
|
||||||
|
|
||||||
from .embedded_db import EmbeddedGraphDB
|
from .embedded_db import EmbeddedGraphDB
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
class BackendServer:
|
"BackendServer",
|
||||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True):
|
"BackendClient",
|
||||||
self._db_path = db_path
|
"EmbeddedGraphDB",
|
||||||
self._use_embedded_db = use_embedded_db
|
"Packet",
|
||||||
self._graph = None
|
"PacketType",
|
||||||
self._client = None
|
"PacketResponse"
|
||||||
self._running = False
|
]
|
||||||
self._thread = None
|
|
||||||
self._input_queue: queue.Queue[Packet] = queue.Queue()
|
|
||||||
self._response_queues: Dict[str, queue.Queue] = {}
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self._config = {"api_key": "", "base_url": "https://api.deepseek.com"}
|
|
||||||
|
|
||||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com") -> None:
|
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
self._init_graph()
|
|
||||||
self._config = {"api_key": api_key, "base_url": base_url}
|
|
||||||
if api_key:
|
|
||||||
from .graph_client import GraphMemoryClient
|
|
||||||
self._client = GraphMemoryClient(api_key=api_key, base_url=base_url, graph=self._graph)
|
|
||||||
self._running = True
|
|
||||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def _init_graph(self) -> None:
|
|
||||||
if self._use_embedded_db:
|
|
||||||
self._graph = EmbeddedGraphDB(db_path=self._db_path)
|
|
||||||
else:
|
|
||||||
from .graph_client import Neo4jGraph
|
|
||||||
self._graph = Neo4jGraph(uri="bolt://localhost:7687", user="neo4j", password="graphmemory123")
|
|
||||||
|
|
||||||
def _run_loop(self) -> None:
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
packet = self._input_queue.get(timeout=0.1)
|
|
||||||
except queue.Empty:
|
|
||||||
continue
|
|
||||||
self._process_packet(packet)
|
|
||||||
|
|
||||||
def _process_packet(self, packet: Packet) -> None:
|
|
||||||
body = {"success": False, "error": "not implemented"}
|
|
||||||
try:
|
|
||||||
if packet.type == PacketType.MESSAGE:
|
|
||||||
body = self._handle_message(packet.body)
|
|
||||||
elif packet.type == PacketType.CONFIG:
|
|
||||||
body = self._handle_config(packet.body)
|
|
||||||
elif packet.type == PacketType.TOOL:
|
|
||||||
body = self._handle_tool(packet.body)
|
|
||||||
elif packet.type == PacketType.STATUS:
|
|
||||||
body = self._handle_status()
|
|
||||||
elif packet.type == PacketType.HISTORY:
|
|
||||||
body = self._handle_history(packet.body)
|
|
||||||
body["success"] = True
|
|
||||||
except Exception as e:
|
|
||||||
body["error"] = str(e)
|
|
||||||
finally:
|
|
||||||
self._send_response(packet.id, Packet(id=packet.id, type=packet.type, body=body))
|
|
||||||
|
|
||||||
def _handle_message(self, body: Dict) -> Dict:
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
from .tool_limiter import ToolLimiter
|
|
||||||
|
|
||||||
limiter = ToolLimiter()
|
|
||||||
user_input = body.get("message", "")
|
|
||||||
if not self._client:
|
|
||||||
return {"error": "API Key not configured", "content": "请先配置 API Key"}
|
|
||||||
|
|
||||||
messages = [{"role": "user", "content": user_input}]
|
|
||||||
response = self._client.send_message_with_history(messages)
|
|
||||||
message = response.choices[0].message
|
|
||||||
content = message.content or "(无回复)"
|
|
||||||
tool_calls = []
|
|
||||||
|
|
||||||
while message.tool_calls:
|
|
||||||
# 处理所有工具调用
|
|
||||||
tool_results = []
|
|
||||||
|
|
||||||
for tool_call in message.tool_calls:
|
|
||||||
args = {}
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
args = json.loads(tool_call.function.arguments)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
allowed, reason = limiter.can_call(tool_call.function.name, args)
|
|
||||||
if not allowed:
|
|
||||||
tool_calls.append({"name": tool_call.function.name, "result": f"工具调用被拒绝: {reason}"})
|
|
||||||
tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": f"工具调用被拒绝: {reason}"})
|
|
||||||
continue
|
|
||||||
|
|
||||||
limiter.record_call(tool_call.function.name, args)
|
|
||||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
|
||||||
tool_calls.append({"name": tool_call.function.name, "result": result})
|
|
||||||
tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
|
|
||||||
|
|
||||||
# 添加 assistant 消息(包含完整的 tool_calls,必须有 type 字段)
|
|
||||||
assistant_msg = {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": message.content or "",
|
|
||||||
"tool_calls": [{
|
|
||||||
"id": tc.id,
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": tc.function.name,
|
|
||||||
"arguments": tc.function.arguments
|
|
||||||
}
|
|
||||||
} for tc in message.tool_calls]
|
|
||||||
}
|
|
||||||
messages.append(assistant_msg)
|
|
||||||
|
|
||||||
# 添加工具结果
|
|
||||||
messages.extend(tool_results)
|
|
||||||
|
|
||||||
response = self._client.send_message_with_history(messages)
|
|
||||||
message = response.choices[0].message
|
|
||||||
content = message.content or content
|
|
||||||
|
|
||||||
return {"content": content, "tool_calls": tool_calls}
|
|
||||||
|
|
||||||
def _handle_config(self, body: Dict) -> Dict:
|
|
||||||
api_key = body.get("api_key", "")
|
|
||||||
base_url = body.get("base_url", "https://api.deepseek.com")
|
|
||||||
self._config = {"api_key": api_key, "base_url": base_url}
|
|
||||||
if api_key and self._graph:
|
|
||||||
from .graph_client import GraphMemoryClient
|
|
||||||
self._client = GraphMemoryClient(api_key=api_key, base_url=base_url, graph=self._graph)
|
|
||||||
return {"status": "config_updated"}
|
|
||||||
|
|
||||||
def _handle_tool(self, body: Dict) -> Dict:
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
name = body.get("name", "")
|
|
||||||
args = body.get("arguments", {})
|
|
||||||
result = execute_tool(self._graph, name, args)
|
|
||||||
return {"result": result}
|
|
||||||
|
|
||||||
def _handle_status(self) -> Dict:
|
|
||||||
return {"running": self._running, "config": self._config, "client_ready": self._client is not None}
|
|
||||||
|
|
||||||
def _handle_history(self, body: Dict) -> Dict:
|
|
||||||
action = body.get("action", "get")
|
|
||||||
if action == "save":
|
|
||||||
self._message_history = body.get("messages", [])
|
|
||||||
return {"status": "saved"}
|
|
||||||
return {"history": getattr(self, "_message_history", [])}
|
|
||||||
|
|
||||||
def send(self, packet: Packet) -> Packet:
|
|
||||||
resp_q = queue.Queue()
|
|
||||||
with self._lock:
|
|
||||||
self._response_queues[packet.id] = resp_q
|
|
||||||
self._input_queue.put(packet)
|
|
||||||
try:
|
|
||||||
return resp_q.get(timeout=30.0)
|
|
||||||
except queue.Empty:
|
|
||||||
return Packet(id=packet.id, type=packet.type, body={"success": False, "error": "timeout"})
|
|
||||||
finally:
|
|
||||||
with self._lock:
|
|
||||||
self._response_queues.pop(packet.id, None)
|
|
||||||
|
|
||||||
def _send_response(self, request_id: str, packet: Packet) -> None:
|
|
||||||
with self._lock:
|
|
||||||
q = self._response_queues.pop(request_id, None)
|
|
||||||
if q:
|
|
||||||
q.put(packet)
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
self._running = False
|
|
||||||
if self._thread:
|
|
||||||
self._thread.join(timeout=2.0)
|
|
||||||
if self._graph:
|
|
||||||
self._graph.close()
|
|
||||||
self._graph = None
|
|
||||||
|
|
||||||
|
|
||||||
class BackendClient:
|
|
||||||
def __init__(self, server: BackendServer):
|
|
||||||
self._server = server
|
|
||||||
self._counter = 0
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
|
|
||||||
def _next_id(self) -> str:
|
|
||||||
with self._lock:
|
|
||||||
self._counter += 1
|
|
||||||
return f"{time.time()}_{self._counter}"
|
|
||||||
|
|
||||||
def send_message(self, message: str) -> Dict:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.MESSAGE, body={"message": message})
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> Dict:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.CONFIG, body={"api_key": api_key, "base_url": base_url})
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def execute_tool(self, name: str, arguments: Dict) -> str:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.TOOL, body={"name": name, "arguments": arguments})
|
|
||||||
return self._server.send(packet).body.get("result", "")
|
|
||||||
|
|
||||||
def get_status(self) -> Dict:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.STATUS, body={})
|
|
||||||
return self._server.send(packet).body
|
|
||||||
|
|
||||||
def save_history(self, messages: list) -> None:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.HISTORY, body={"action": "save", "messages": messages})
|
|
||||||
self._server.send(packet)
|
|
||||||
|
|
||||||
def get_history(self) -> list:
|
|
||||||
packet = Packet(id=self._next_id(), type=PacketType.HISTORY, body={"action": "get"})
|
|
||||||
return self._server.send(packet).body.get("history", [])
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
self._server.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BackendServer", "BackendClient", "EmbeddedGraphDB", "Packet", "PacketType"]
|
|
||||||
100
core/client.py
100
core/client.py
@ -1,61 +1,75 @@
|
|||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from .server import BackendServer, MessageType
|
from .server import BackendServer, Packet, PacketType
|
||||||
|
|
||||||
|
|
||||||
class BackendClient:
|
class BackendClient:
|
||||||
|
|
||||||
def __init__(self, server: BackendServer):
|
def __init__(self, server: BackendServer):
|
||||||
self._server = server
|
self._server = server
|
||||||
self._request_counter = 0
|
self._counter = 0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def process_message(self, user_input: str, timeout: float = 30.0) -> Dict[str, Any]:
|
def _next_id(self) -> str:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._request_counter += 1
|
self._counter += 1
|
||||||
return self._server.process_message(user_input, timeout)
|
return f"{time.time()}_{self._counter}"
|
||||||
|
|
||||||
def execute_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: float = 10.0) -> str:
|
def send(self, message: str) -> Dict:
|
||||||
with self._lock:
|
return self.process_message(message)
|
||||||
self._request_counter += 1
|
|
||||||
return self._server.execute_tool(tool_name, arguments, timeout)
|
|
||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> Dict[str, Any]:
|
def process_message(self, user_input: str) -> Dict:
|
||||||
return self._send_request(MessageType.SET_CONFIG, {"api_key": api_key, "base_url": base_url})
|
return self._server.process_message(user_input)
|
||||||
|
|
||||||
def get_config(self) -> Dict[str, str]:
|
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> Dict:
|
||||||
result = self._send_request(MessageType.GET_CONFIG, {})
|
packet = Packet(
|
||||||
return result.get("data", {"api_key": "", "base_url": "https://api.deepseek.com"})
|
id=self._next_id(),
|
||||||
|
type=PacketType.SET_CONFIG,
|
||||||
def get_message_history(self) -> list:
|
body={"api_key": api_key, "base_url": base_url, "model": model}
|
||||||
result = self._send_request(MessageType.GET_HISTORY, {})
|
|
||||||
return result.get("data", {}).get("history", [])
|
|
||||||
|
|
||||||
def save_message_history(self, messages: list) -> None:
|
|
||||||
self._send_request(MessageType.SAVE_HISTORY, {"messages": messages})
|
|
||||||
|
|
||||||
def _send_request(self, message_type: MessageType, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
import queue
|
|
||||||
request_id = f"{id(self)}"
|
|
||||||
response_queue = queue.Queue()
|
|
||||||
|
|
||||||
from .server import BackendRequest
|
|
||||||
request = BackendRequest(
|
|
||||||
request_id=request_id,
|
|
||||||
message_type=message_type,
|
|
||||||
payload=payload,
|
|
||||||
response_queue=response_queue
|
|
||||||
)
|
)
|
||||||
|
return self._server.send(packet).body
|
||||||
self._server._request_queue.put(request)
|
|
||||||
|
def execute_tool(self, name: str, arguments: Dict) -> Dict:
|
||||||
try:
|
packet = Packet(
|
||||||
response = response_queue.get(timeout=5.0)
|
id=self._next_id(),
|
||||||
if not response.success:
|
type=PacketType.EXECUTE_TOOL,
|
||||||
raise Exception(response.error)
|
body={"tool_name": name, "arguments": arguments}
|
||||||
return {"success": True, "data": response.data}
|
)
|
||||||
except queue.Empty:
|
return self._server.send(packet).body
|
||||||
return {"success": False, "error": "timeout"}
|
|
||||||
|
def get_status(self) -> Dict:
|
||||||
|
packet = Packet(
|
||||||
|
id=self._next_id(),
|
||||||
|
type=PacketType.GET_STATUS,
|
||||||
|
body={}
|
||||||
|
)
|
||||||
|
return self._server.send(packet).body
|
||||||
|
|
||||||
|
def get_config(self) -> Dict:
|
||||||
|
packet = Packet(
|
||||||
|
id=self._next_id(),
|
||||||
|
type=PacketType.GET_CONFIG,
|
||||||
|
body={}
|
||||||
|
)
|
||||||
|
return self._server.send(packet).body
|
||||||
|
|
||||||
|
def save_history(self, messages: list) -> None:
|
||||||
|
packet = Packet(
|
||||||
|
id=self._next_id(),
|
||||||
|
type=PacketType.SAVE_HISTORY,
|
||||||
|
body={"messages": messages}
|
||||||
|
)
|
||||||
|
self._server.send(packet)
|
||||||
|
|
||||||
|
def get_history(self) -> list:
|
||||||
|
packet = Packet(
|
||||||
|
id=self._next_id(),
|
||||||
|
type=PacketType.GET_HISTORY,
|
||||||
|
body={}
|
||||||
|
)
|
||||||
|
return self._server.send(packet).body.get("history", [])
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self._server.shutdown()
|
self._server.shutdown()
|
||||||
@ -303,12 +303,12 @@ class Neo4jGraph:
|
|||||||
class GraphMemoryClient:
|
class GraphMemoryClient:
|
||||||
"""图记忆客户端"""
|
"""图记忆客户端"""
|
||||||
|
|
||||||
def __init__(self, api_key: str, base_url: str, graph):
|
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
|
||||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
self.tools = TOOLS
|
self.tools = TOOLS
|
||||||
|
self.model = model
|
||||||
|
|
||||||
# 使用新的提示词管理器
|
|
||||||
prompt_manager = PromptManager()
|
prompt_manager = PromptManager()
|
||||||
self.system_prompt = prompt_manager.get_system_prompt()
|
self.system_prompt = prompt_manager.get_system_prompt()
|
||||||
|
|
||||||
@ -330,7 +330,7 @@ class GraphMemoryClient:
|
|||||||
messages.extend(tool_results)
|
messages.extend(tool_results)
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
model=MODEL_NAME,
|
model=self.model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
tools=self.tools,
|
tools=self.tools,
|
||||||
tool_choice="auto"
|
tool_choice="auto"
|
||||||
@ -347,7 +347,7 @@ class GraphMemoryClient:
|
|||||||
messages.extend(messages_history)
|
messages.extend(messages_history)
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
model=MODEL_NAME,
|
model=self.model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
tools=self.tools,
|
tools=self.tools,
|
||||||
tool_choice="auto"
|
tool_choice="auto"
|
||||||
@ -373,7 +373,7 @@ class GraphMemoryClient:
|
|||||||
messages.extend(tool_results)
|
messages.extend(tool_results)
|
||||||
|
|
||||||
stream = self.client.chat.completions.create(
|
stream = self.client.chat.completions.create(
|
||||||
model=MODEL_NAME,
|
model=self.model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
tools=self.tools,
|
tools=self.tools,
|
||||||
tool_choice="auto",
|
tool_choice="auto",
|
||||||
|
|||||||
@ -10,6 +10,9 @@ class PromptManager:
|
|||||||
_instance = None
|
_instance = None
|
||||||
_cached_prompt = None
|
_cached_prompt = None
|
||||||
|
|
||||||
|
# 用户自定义提示词路径
|
||||||
|
USER_PROMPT_PATH = Path.home() / ".trulymem" / "system_prompt.md"
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
"""单例模式,避免重复加载"""
|
"""单例模式,避免重复加载"""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
@ -26,11 +29,19 @@ class PromptManager:
|
|||||||
if PromptManager._cached_prompt is not None:
|
if PromptManager._cached_prompt is not None:
|
||||||
return PromptManager._cached_prompt
|
return PromptManager._cached_prompt
|
||||||
|
|
||||||
|
# 1. 优先使用用户自定义提示词
|
||||||
|
if self.USER_PROMPT_PATH.exists():
|
||||||
|
with open(self.USER_PROMPT_PATH, "r", encoding="utf-8") as f:
|
||||||
|
PromptManager._cached_prompt = f.read()
|
||||||
|
return PromptManager._cached_prompt
|
||||||
|
|
||||||
|
# 2. 使用打包的提示词
|
||||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
prompt_file = self.prompts_dir / "system_prompt.md"
|
||||||
if prompt_file.exists():
|
if prompt_file.exists():
|
||||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||||
PromptManager._cached_prompt = f.read()
|
PromptManager._cached_prompt = f.read()
|
||||||
else:
|
else:
|
||||||
|
# 3. 使用内置默认提示词
|
||||||
PromptManager._cached_prompt = self._build_default_prompt()
|
PromptManager._cached_prompt = self._build_default_prompt()
|
||||||
|
|
||||||
return PromptManager._cached_prompt
|
return PromptManager._cached_prompt
|
||||||
@ -78,3 +89,9 @@ class PromptManager:
|
|||||||
- 如何使用工具
|
- 如何使用工具
|
||||||
|
|
||||||
记住:灵活应对,保持自然对话体验。"""
|
记住:灵活应对,保持自然对话体验。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear_cache():
|
||||||
|
"""清除缓存,强制重新加载提示词"""
|
||||||
|
PromptManager._cached_prompt = None
|
||||||
|
PromptManager._instance = None
|
||||||
588
core/server.py
588
core/server.py
@ -2,17 +2,16 @@ import threading
|
|||||||
import queue
|
import queue
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from .embedded_db import EmbeddedGraphDB
|
from .embedded_db import EmbeddedGraphDB
|
||||||
from .graph_client import GraphMemoryClient
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
from .tool_limiter import ToolLimiter
|
|
||||||
|
|
||||||
|
|
||||||
class MessageType(Enum):
|
class PacketType(Enum):
|
||||||
PROCESS_MESSAGE = "process_message"
|
PROCESS_MESSAGE = "process_message"
|
||||||
EXECUTE_TOOL = "execute_tool"
|
EXECUTE_TOOL = "execute_tool"
|
||||||
GET_STATUS = "get_status"
|
GET_STATUS = "get_status"
|
||||||
@ -24,46 +23,66 @@ class MessageType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackendRequest:
|
class Packet:
|
||||||
request_id: str
|
id: str
|
||||||
message_type: MessageType
|
type: PacketType
|
||||||
payload: Dict[str, Any]
|
body: Dict[str, Any]
|
||||||
response_queue: queue.Queue = field(default=None)
|
response_queue: Optional[queue.Queue] = field(default=None)
|
||||||
|
created_at: float = field(default_factory=time.time)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackendResponse:
|
class PacketResponse:
|
||||||
request_id: str
|
id: str
|
||||||
success: bool
|
success: bool
|
||||||
data: Any = None
|
data: Any = None
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class BackendServer:
|
class BackendServer:
|
||||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True):
|
|
||||||
|
DEFAULT_CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True, config_file: str = None):
|
||||||
self._db_path = db_path
|
self._db_path = db_path
|
||||||
self._use_embedded_db = use_embedded_db
|
self._use_embedded_db = use_embedded_db
|
||||||
|
self._config_file = Path(config_file) if config_file else self.DEFAULT_CONFIG_PATH
|
||||||
|
|
||||||
self._graph = None
|
self._graph = None
|
||||||
self._client = None
|
self._client = None
|
||||||
self._tool_limiter = ToolLimiter()
|
self._tool_limiter = None
|
||||||
|
|
||||||
self._request_queue: queue.Queue[BackendRequest] = queue.Queue()
|
self._input_queue: queue.Queue[Packet] = queue.Queue()
|
||||||
|
self._response_queues: Dict[str, queue.Queue] = {}
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
|
||||||
|
self._message_history: list = []
|
||||||
|
|
||||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com") -> None:
|
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||||
if self._running:
|
if self._running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._init_graph()
|
self._load_config()
|
||||||
|
|
||||||
if api_key:
|
if api_key:
|
||||||
|
self._config["api_key"] = api_key
|
||||||
|
if base_url:
|
||||||
|
self._config["base_url"] = base_url
|
||||||
|
if model:
|
||||||
|
self._config["model"] = model
|
||||||
|
|
||||||
|
self._init_graph()
|
||||||
|
self._tool_limiter = self._create_tool_limiter()
|
||||||
|
|
||||||
|
if self._config["api_key"]:
|
||||||
|
from .graph_client import GraphMemoryClient
|
||||||
self._client = GraphMemoryClient(
|
self._client = GraphMemoryClient(
|
||||||
api_key=api_key,
|
api_key=self._config["api_key"],
|
||||||
base_url=base_url,
|
base_url=self._config["base_url"],
|
||||||
|
model=self._config.get("model", "deepseek-chat"),
|
||||||
graph=self._graph
|
graph=self._graph
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,6 +90,24 @@ class BackendServer:
|
|||||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
|
def _load_config(self) -> None:
|
||||||
|
if self._config_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self._config_file, 'r') as f:
|
||||||
|
saved = json.load(f)
|
||||||
|
self._config.update(saved)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_config(self) -> None:
|
||||||
|
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self._config_file, 'w') as f:
|
||||||
|
json.dump(self._config, f, indent=2)
|
||||||
|
|
||||||
|
def _create_tool_limiter(self):
|
||||||
|
from .tool_limiter import ToolLimiter
|
||||||
|
return ToolLimiter()
|
||||||
|
|
||||||
def _init_graph(self) -> None:
|
def _init_graph(self) -> None:
|
||||||
if self._use_embedded_db:
|
if self._use_embedded_db:
|
||||||
self._graph = EmbeddedGraphDB(db_path=self._db_path)
|
self._graph = EmbeddedGraphDB(db_path=self._db_path)
|
||||||
@ -85,310 +122,276 @@ class BackendServer:
|
|||||||
def _run_loop(self) -> None:
|
def _run_loop(self) -> None:
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try:
|
||||||
request = self._request_queue.get(timeout=0.1)
|
packet = self._input_queue.get(timeout=0.1)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if request.message_type == MessageType.PROCESS_MESSAGE:
|
self._process_packet(packet)
|
||||||
self._handle_process_message(request)
|
|
||||||
elif request.message_type == MessageType.EXECUTE_TOOL:
|
|
||||||
self._handle_execute_tool(request)
|
|
||||||
elif request.message_type == MessageType.GET_STATUS:
|
|
||||||
self._handle_get_status(request)
|
|
||||||
elif request.message_type == MessageType.GET_CONFIG:
|
|
||||||
self._handle_get_config(request)
|
|
||||||
elif request.message_type == MessageType.SET_CONFIG:
|
|
||||||
self._handle_set_config(request)
|
|
||||||
elif request.message_type == MessageType.GET_HISTORY:
|
|
||||||
self._handle_get_history(request)
|
|
||||||
elif request.message_type == MessageType.SAVE_HISTORY:
|
|
||||||
self._handle_save_history(request)
|
|
||||||
elif request.message_type == MessageType.SHUTDOWN:
|
|
||||||
self._running = False
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={"status": "shutdown"}
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_process_message(self, request: BackendRequest) -> None:
|
def _process_packet(self, packet: Packet) -> None:
|
||||||
|
response_body = {"error": "not implemented"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_input = request.payload.get("user_input", "")
|
if packet.type == PacketType.PROCESS_MESSAGE:
|
||||||
|
response_body = self._handle_process_message(packet.body)
|
||||||
|
elif packet.type == PacketType.EXECUTE_TOOL:
|
||||||
|
response_body = self._handle_execute_tool(packet.body)
|
||||||
|
elif packet.type == PacketType.GET_STATUS:
|
||||||
|
response_body = self._handle_get_status()
|
||||||
|
elif packet.type == PacketType.GET_CONFIG:
|
||||||
|
response_body = self._handle_get_config()
|
||||||
|
elif packet.type == PacketType.SET_CONFIG:
|
||||||
|
response_body = self._handle_set_config(packet.body)
|
||||||
|
elif packet.type == PacketType.GET_HISTORY:
|
||||||
|
response_body = self._handle_get_history()
|
||||||
|
elif packet.type == PacketType.SAVE_HISTORY:
|
||||||
|
response_body = self._handle_save_history(packet.body)
|
||||||
|
elif packet.type == PacketType.SHUTDOWN:
|
||||||
|
self._running = False
|
||||||
|
response_body = {"success": True, "status": "shutdown"}
|
||||||
|
|
||||||
if not self._client:
|
if "success" not in response_body:
|
||||||
self._send_response(request, BackendResponse(
|
response_body["success"] = True
|
||||||
request_id=request.request_id,
|
except Exception as e:
|
||||||
success=False,
|
response_body["success"] = False
|
||||||
error="API Key 未配置"
|
response_body["error"] = str(e)
|
||||||
))
|
|
||||||
return
|
self._send_response(packet.id, PacketResponse(
|
||||||
|
id=packet.id,
|
||||||
|
success=response_body.get("success", False),
|
||||||
|
data=response_body if response_body.get("success") else None,
|
||||||
|
error=response_body.get("error")
|
||||||
|
))
|
||||||
|
|
||||||
|
def _handle_process_message(self, body: Dict) -> Dict:
|
||||||
|
from .tool_executor import execute_tool
|
||||||
|
|
||||||
|
user_input = body.get("user_input", "")
|
||||||
|
|
||||||
|
if not self._client:
|
||||||
|
return {"success": False, "error": "API Key 未配置", "content": "请先配置 API Key"}
|
||||||
|
|
||||||
|
self._tool_limiter.reset()
|
||||||
|
|
||||||
|
messages_history = [{"role": "user", "content": user_input}]
|
||||||
|
|
||||||
|
response = self._client.send_message_with_history(messages_history)
|
||||||
|
message = response.choices[0].message
|
||||||
|
|
||||||
|
tool_calls = []
|
||||||
|
accumulated_content = ""
|
||||||
|
rejected_tools = []
|
||||||
|
|
||||||
|
while message.tool_calls:
|
||||||
|
if message.content:
|
||||||
|
accumulated_content += message.content + "\n\n"
|
||||||
|
|
||||||
self._tool_limiter.reset()
|
assistant_msg = {
|
||||||
|
"role": "assistant",
|
||||||
messages_history = [{"role": "user", "content": user_input}]
|
"content": message.content,
|
||||||
|
"tool_calls": [
|
||||||
response = self._client.send_message_with_history(messages_history)
|
{
|
||||||
message = response.choices[0].message
|
"id": tc.id,
|
||||||
|
"type": "function",
|
||||||
tool_calls = []
|
"function": {
|
||||||
accumulated_content = ""
|
"name": tc.function.name,
|
||||||
rejected_tools = []
|
"arguments": tc.function.arguments
|
||||||
|
|
||||||
while message.tool_calls:
|
|
||||||
if message.content:
|
|
||||||
accumulated_content += message.content + "\n\n"
|
|
||||||
|
|
||||||
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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
messages_history.append(assistant_msg)
|
|
||||||
|
|
||||||
current_tool_results = []
|
|
||||||
for tool_call in message.tool_calls:
|
|
||||||
args = json.loads(tool_call.function.arguments)
|
|
||||||
|
|
||||||
allowed, reason = self._tool_limiter.can_call(tool_call.function.name, args)
|
|
||||||
|
|
||||||
if not allowed:
|
|
||||||
rejected_tools.append((tool_call.function.name, reason))
|
|
||||||
result = f"工具调用被拒绝: {reason}"
|
|
||||||
tool_result_msg = {
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": tool_call.id,
|
|
||||||
"content": result
|
|
||||||
}
|
}
|
||||||
current_tool_results.append(tool_result_msg)
|
} for tc in message.tool_calls
|
||||||
continue
|
]
|
||||||
|
}
|
||||||
self._tool_limiter.record_call(tool_call.function.name, args)
|
messages_history.append(assistant_msg)
|
||||||
|
|
||||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
current_tool_results = []
|
||||||
tool_calls.append({
|
for tool_call in message.tool_calls:
|
||||||
"name": tool_call.function.name,
|
args = json.loads(tool_call.function.arguments)
|
||||||
"arguments": args,
|
|
||||||
"result": result
|
allowed, reason = self._tool_limiter.can_call(tool_call.function.name, args)
|
||||||
})
|
|
||||||
|
if not allowed:
|
||||||
|
rejected_tools.append((tool_call.function.name, reason))
|
||||||
|
result = f"工具调用被拒绝: {reason}"
|
||||||
tool_result_msg = {
|
tool_result_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": tool_call.id,
|
"tool_call_id": tool_call.id,
|
||||||
"content": result
|
"content": result
|
||||||
}
|
}
|
||||||
current_tool_results.append(tool_result_msg)
|
current_tool_results.append(tool_result_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
messages_history.extend(current_tool_results)
|
self._tool_limiter.record_call(tool_call.function.name, args)
|
||||||
|
|
||||||
response = self._client.send_message_with_history(messages_history)
|
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||||
message = response.choices[0].message
|
tool_calls.append({
|
||||||
|
"name": tool_call.function.name,
|
||||||
final_content = message.content or ""
|
"arguments": args,
|
||||||
content = accumulated_content + final_content if accumulated_content else final_content
|
"result": result
|
||||||
|
})
|
||||||
if not content:
|
|
||||||
content = "(无回复)"
|
tool_result_msg = {
|
||||||
|
"role": "tool",
|
||||||
if tool_calls:
|
"tool_call_id": tool_call.id,
|
||||||
tool_names = [tc["name"] for tc in tool_calls]
|
"content": result
|
||||||
content = f"已执行工具: {', '.join(tool_names)}\n\n{content}"
|
|
||||||
|
|
||||||
if rejected_tools:
|
|
||||||
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
|
|
||||||
content += f"\n\n部分工具调用被限制:\n{rejected_info}"
|
|
||||||
content += f"\n\n工具调用统计:\n{self._tool_limiter.get_summary()}"
|
|
||||||
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={
|
|
||||||
"content": content,
|
|
||||||
"tool_calls": tool_calls,
|
|
||||||
"rejected_tools": rejected_tools
|
|
||||||
}
|
}
|
||||||
))
|
current_tool_results.append(tool_result_msg)
|
||||||
|
|
||||||
except Exception as e:
|
messages_history.extend(current_tool_results)
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
response = self._client.send_message_with_history(messages_history)
|
||||||
success=False,
|
message = response.choices[0].message
|
||||||
error=str(e)
|
|
||||||
))
|
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}"
|
||||||
|
|
||||||
|
if rejected_tools:
|
||||||
|
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
|
||||||
|
content += f"\n\n部分工具调用被限制:\n{rejected_info}"
|
||||||
|
content += f"\n\n工具调用统计:\n{self._tool_limiter.get_summary()}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"content": content,
|
||||||
|
"tool_calls": tool_calls,
|
||||||
|
"rejected_tools": rejected_tools
|
||||||
|
}
|
||||||
|
|
||||||
def _handle_execute_tool(self, request: BackendRequest) -> None:
|
def _handle_execute_tool(self, body: Dict) -> Dict:
|
||||||
"""处理直接工具调用请求(前端直接调用,不受次数限制)"""
|
from .tool_executor import execute_tool
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tool_name = request.payload.get("tool_name")
|
tool_name = body.get("tool_name")
|
||||||
arguments = request.payload.get("arguments", {})
|
arguments = body.get("arguments", {})
|
||||||
|
|
||||||
# 前端直接调用的工具不受次数限制,直接执行
|
|
||||||
result = execute_tool(self._graph, tool_name, arguments)
|
result = execute_tool(self._graph, tool_name, arguments)
|
||||||
|
|
||||||
self._send_response(request, BackendResponse(
|
return {"success": True, "result": result}
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={"result": result}
|
|
||||||
))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._send_response(request, BackendResponse(
|
return {"success": False, "error": str(e)}
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_get_status(self, request: BackendRequest) -> None:
|
def _handle_get_status(self) -> Dict:
|
||||||
try:
|
return {
|
||||||
status = {
|
"running": self._running,
|
||||||
"graph_initialized": self._graph is not None,
|
"config": self._config,
|
||||||
"client_initialized": self._client is not None,
|
"graph_initialized": self._graph is not None,
|
||||||
"running": self._running
|
"client_initialized": self._client is not None
|
||||||
}
|
}
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data=status
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_get_config(self, request: BackendRequest) -> None:
|
def _handle_get_config(self) -> Dict:
|
||||||
try:
|
return self._config.copy()
|
||||||
config = self.get_config()
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data=config
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_set_config(self, request: BackendRequest) -> None:
|
def _handle_set_config(self, body: Dict) -> Dict:
|
||||||
try:
|
api_key = body.get("api_key", "")
|
||||||
api_key = request.payload.get("api_key", "")
|
base_url = body.get("base_url", "https://api.deepseek.com")
|
||||||
base_url = request.payload.get("base_url", "https://api.deepseek.com")
|
model = body.get("model", "deepseek-chat")
|
||||||
self.update_config(api_key, base_url)
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={"status": "config_updated"}
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_get_history(self, request: BackendRequest) -> None:
|
|
||||||
try:
|
|
||||||
history = self.get_message_history()
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={"history": history}
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_save_history(self, request: BackendRequest) -> None:
|
|
||||||
try:
|
|
||||||
messages = request.payload.get("messages", [])
|
|
||||||
self.save_message_history(messages)
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=True,
|
|
||||||
data={"status": "history_saved"}
|
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
self._send_response(request, BackendResponse(
|
|
||||||
request_id=request.request_id,
|
|
||||||
success=False,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
def _send_response(self, request: BackendRequest, response: BackendResponse) -> None:
|
|
||||||
if request.response_queue:
|
|
||||||
request.response_queue.put(response)
|
|
||||||
|
|
||||||
def process_message(self, user_input: str, timeout: float = 30.0) -> Dict[str, Any]:
|
|
||||||
request_id = f"{time.time()}"
|
|
||||||
response_queue = queue.Queue()
|
|
||||||
|
|
||||||
request = BackendRequest(
|
self.update_config(api_key, base_url, model)
|
||||||
request_id=request_id,
|
self._save_config()
|
||||||
message_type=MessageType.PROCESS_MESSAGE,
|
return {"status": "config_updated"}
|
||||||
payload={"user_input": user_input},
|
|
||||||
response_queue=response_queue
|
def _handle_get_history(self) -> Dict:
|
||||||
|
return {"history": self._message_history}
|
||||||
|
|
||||||
|
def _handle_save_history(self, body: Dict) -> Dict:
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
self._message_history = messages
|
||||||
|
return {"status": "history_saved"}
|
||||||
|
|
||||||
|
def _send_response(self, request_id: str, response: PacketResponse) -> None:
|
||||||
|
with self._lock:
|
||||||
|
q = self._response_queues.pop(request_id, None)
|
||||||
|
if q:
|
||||||
|
q.put(response)
|
||||||
|
|
||||||
|
def send(self, packet: Packet) -> Packet:
|
||||||
|
resp_q = queue.Queue()
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._response_queues[packet.id] = resp_q
|
||||||
|
|
||||||
|
self._input_queue.put(packet)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = resp_q.get(timeout=30.0)
|
||||||
|
return Packet(
|
||||||
|
id=response.id,
|
||||||
|
type=packet.type,
|
||||||
|
body={
|
||||||
|
"success": response.success,
|
||||||
|
"data": response.data,
|
||||||
|
"error": response.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except queue.Empty:
|
||||||
|
return Packet(
|
||||||
|
id=packet.id,
|
||||||
|
type=packet.type,
|
||||||
|
body={"success": False, "error": "timeout"}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
with self._lock:
|
||||||
|
self._response_queues.pop(packet.id, None)
|
||||||
|
|
||||||
|
def process_message(self, user_input: str) -> Dict[str, Any]:
|
||||||
|
packet = Packet(
|
||||||
|
id=f"{time.time()}",
|
||||||
|
type=PacketType.PROCESS_MESSAGE,
|
||||||
|
body={"user_input": user_input}
|
||||||
)
|
)
|
||||||
|
|
||||||
self._request_queue.put(request)
|
response = self.send(packet)
|
||||||
|
return response.body
|
||||||
try:
|
|
||||||
response = response_queue.get(timeout=timeout)
|
|
||||||
if not response.success:
|
|
||||||
raise Exception(response.error)
|
|
||||||
return response.data
|
|
||||||
except queue.Empty:
|
|
||||||
raise TimeoutError("请求超时")
|
|
||||||
|
|
||||||
def execute_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: float = 10.0) -> str:
|
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
request_id = f"{time.time()}"
|
packet = Packet(
|
||||||
response_queue = queue.Queue()
|
id=f"{time.time()}",
|
||||||
|
type=PacketType.EXECUTE_TOOL,
|
||||||
request = BackendRequest(
|
body={"tool_name": tool_name, "arguments": arguments}
|
||||||
request_id=request_id,
|
|
||||||
message_type=MessageType.EXECUTE_TOOL,
|
|
||||||
payload={"tool_name": tool_name, "arguments": arguments},
|
|
||||||
response_queue=response_queue
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._request_queue.put(request)
|
response = self.send(packet)
|
||||||
|
return response.body
|
||||||
|
|
||||||
|
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._config["api_key"] = api_key
|
||||||
|
self._config["base_url"] = base_url
|
||||||
|
self._config["model"] = model
|
||||||
|
|
||||||
try:
|
if api_key and self._graph:
|
||||||
response = response_queue.get(timeout=timeout)
|
from .graph_client import GraphMemoryClient
|
||||||
if not response.success:
|
self._client = GraphMemoryClient(
|
||||||
raise Exception(response.error)
|
api_key=api_key,
|
||||||
return response.data["result"]
|
base_url=base_url,
|
||||||
except queue.Empty:
|
model=model,
|
||||||
raise TimeoutError("工具执行超时")
|
graph=self._graph
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_config(self) -> Dict[str, str]:
|
||||||
|
return self._config.copy()
|
||||||
|
|
||||||
|
def save_message_history(self, messages: list) -> None:
|
||||||
|
self._message_history = messages
|
||||||
|
|
||||||
|
def get_message_history(self) -> list:
|
||||||
|
return self._message_history.copy()
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
|
|
||||||
request_id = f"{time.time()}"
|
packet = Packet(
|
||||||
response_queue = queue.Queue()
|
id=f"{time.time()}",
|
||||||
|
type=PacketType.SHUTDOWN,
|
||||||
request = BackendRequest(
|
body={}
|
||||||
request_id=request_id,
|
|
||||||
message_type=MessageType.SHUTDOWN,
|
|
||||||
payload={},
|
|
||||||
response_queue=response_queue
|
|
||||||
)
|
)
|
||||||
|
self.send(packet)
|
||||||
self._request_queue.put(request)
|
|
||||||
|
|
||||||
if self._thread:
|
if self._thread:
|
||||||
self._thread.join(timeout=2.0)
|
self._thread.join(timeout=2.0)
|
||||||
@ -396,22 +399,5 @@ class BackendServer:
|
|||||||
if self._graph:
|
if self._graph:
|
||||||
self._graph.close()
|
self._graph.close()
|
||||||
self._graph = None
|
self._graph = None
|
||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> None:
|
self._running = False
|
||||||
with self._lock:
|
|
||||||
self._config = {"api_key": api_key, "base_url": base_url}
|
|
||||||
if api_key and self._graph:
|
|
||||||
self._client = GraphMemoryClient(
|
|
||||||
api_key=api_key,
|
|
||||||
base_url=base_url,
|
|
||||||
graph=self._graph
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config(self) -> Dict[str, str]:
|
|
||||||
return getattr(self, "_config", {"api_key": "", "base_url": "https://api.deepseek.com"})
|
|
||||||
|
|
||||||
def save_message_history(self, messages: list) -> None:
|
|
||||||
self._message_history = messages
|
|
||||||
|
|
||||||
def get_message_history(self) -> list:
|
|
||||||
return getattr(self, "_message_history", [])
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
工具定义模块
|
工具定义模块
|
||||||
"""
|
"""
|
||||||
from .memory_tools import TOOLS
|
from .memory_tools import TOOLS
|
||||||
from .tool_executor import execute_tool
|
from ..tool_executor import execute_tool
|
||||||
from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
from ..tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
||||||
|
|
||||||
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
||||||
|
|||||||
@ -1,307 +0,0 @@
|
|||||||
"""
|
|
||||||
工具执行器
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
|
|
||||||
def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
|
||||||
"""执行工具调用"""
|
|
||||||
print(f"\n[工具调用] {tool_name}")
|
|
||||||
print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 基础记忆工具
|
|
||||||
if tool_name == "memory_recall":
|
|
||||||
result = graph.recall(
|
|
||||||
query_intent=arguments.get("query_intent", ""),
|
|
||||||
seed_entities=arguments.get("seed_entities"),
|
|
||||||
depth=arguments.get("depth", 2),
|
|
||||||
time_range=arguments.get("time_range"),
|
|
||||||
session_filter=arguments.get("session_filter")
|
|
||||||
)
|
|
||||||
return format_recall_result(result)
|
|
||||||
|
|
||||||
elif tool_name == "memory_commit":
|
|
||||||
result = graph.commit(
|
|
||||||
triplets=arguments.get("triplets", []),
|
|
||||||
entity_types=arguments.get("entity_types"),
|
|
||||||
temporal_tag=arguments.get("temporal_tag")
|
|
||||||
)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "memory_purge":
|
|
||||||
result = graph.purge(
|
|
||||||
criteria=arguments.get("criteria", {}),
|
|
||||||
mode=arguments.get("mode", "soft"),
|
|
||||||
new_relation=arguments.get("new_relation")
|
|
||||||
)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "memory_introspect":
|
|
||||||
result = graph.introspect(session_id=arguments.get("session_id"))
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "memory_archive":
|
|
||||||
result = graph.archive(days=arguments.get("days", 30))
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "memory_cleanup":
|
|
||||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
# 人设图管理工具
|
|
||||||
elif tool_name == "persona_update":
|
|
||||||
result = execute_persona_update(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "persona_clear":
|
|
||||||
result = execute_persona_clear(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
# 工作记忆链管理工具
|
|
||||||
elif tool_name == "task_create":
|
|
||||||
result = execute_task_create(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "task_set_state":
|
|
||||||
result = execute_task_set_state(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "task_delete":
|
|
||||||
result = execute_task_delete(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
elif tool_name == "task_link_info":
|
|
||||||
result = execute_task_link_info(graph, arguments)
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
return f"未知工具: {tool_name}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"工具执行错误: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def format_recall_result(result: dict) -> str:
|
|
||||||
"""格式化检索结果"""
|
|
||||||
lines = ["===== 记忆检索结果 ====="]
|
|
||||||
|
|
||||||
if result.get("entities"):
|
|
||||||
lines.append(f"\n实体 ({len(result['entities'])} 个):")
|
|
||||||
for e in result["entities"]:
|
|
||||||
if e and isinstance(e, dict):
|
|
||||||
lines.append(f" - {e.get('name', 'N/A')} (类型: {e.get('type', 'unknown')}, 提及: {e.get('mention_count', 1)}次)")
|
|
||||||
|
|
||||||
if result.get("relations"):
|
|
||||||
lines.append(f"\n关系 ({len(result['relations'])} 条):")
|
|
||||||
for r in result["relations"]:
|
|
||||||
if r and isinstance(r, dict):
|
|
||||||
lines.append(f" - {r.get('source', 'N/A')} --[{r.get('type', 'N/A')}]--> {r.get('target', 'N/A')}")
|
|
||||||
created = r.get("created_at", "N/A")
|
|
||||||
if created and created != "N/A":
|
|
||||||
created = created[:19] if "T" in str(created) else str(created)
|
|
||||||
session_id = r.get('session_id', 'N/A')
|
|
||||||
session_display = session_id[:20] if session_id and session_id != 'N/A' else 'N/A'
|
|
||||||
lines.append(f" 时间: {created}, 会话: {session_display}, 轮次: {r.get('turn_id', 0)}, 置信度: {r.get('confidence', 1.0)}")
|
|
||||||
|
|
||||||
if not result.get("entities") and not result.get("relations"):
|
|
||||||
lines.append("\n(未找到相关记忆)")
|
|
||||||
|
|
||||||
lines.append("=" * 30)
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
# 人设图管理工具实现
|
|
||||||
def execute_persona_update(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""更新人设"""
|
|
||||||
attributes = arguments.get("attributes", [])
|
|
||||||
mode = arguments.get("mode", "merge")
|
|
||||||
|
|
||||||
if mode == "replace":
|
|
||||||
# 先清除旧人设
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 写入新人设
|
|
||||||
triplets = []
|
|
||||||
for attr in attributes:
|
|
||||||
triplets.append({
|
|
||||||
"subject": "AI",
|
|
||||||
"relation": attr["attribute"],
|
|
||||||
"object": attr["value"],
|
|
||||||
"confidence": 1.0
|
|
||||||
})
|
|
||||||
|
|
||||||
result = graph.commit(triplets=triplets)
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"mode": mode,
|
|
||||||
"updated_attributes": len(attributes),
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_persona_clear(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""清除人设"""
|
|
||||||
if not arguments.get("confirm", True):
|
|
||||||
return {"status": "cancelled", "message": "需要确认才能清除人设"}
|
|
||||||
|
|
||||||
# 删除所有人设相关关系
|
|
||||||
result1 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result2 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result3 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
result4 = graph.purge(
|
|
||||||
criteria={"subject_contains": "AI", "relation_type": "语气特征"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
total_deleted = (
|
|
||||||
result1.get("deleted_count", 0) +
|
|
||||||
result2.get("deleted_count", 0) +
|
|
||||||
result3.get("deleted_count", 0) +
|
|
||||||
result4.get("deleted_count", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"deleted_count": total_deleted,
|
|
||||||
"message": "人设已清除,恢复默认身份"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 工作记忆链管理工具实现
|
|
||||||
def execute_task_create(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""创建任务节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
description = arguments.get("description")
|
|
||||||
info_nodes = arguments.get("info_nodes", [])
|
|
||||||
|
|
||||||
# 创建任务节点
|
|
||||||
triplets = [
|
|
||||||
{"subject": task_id, "relation": "is_type", "object": "TaskNode"},
|
|
||||||
{"subject": task_id, "relation": "has_description", "object": description},
|
|
||||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_进行中"}
|
|
||||||
]
|
|
||||||
|
|
||||||
result = graph.commit(triplets=triplets)
|
|
||||||
|
|
||||||
# 关联信息节点
|
|
||||||
if info_nodes:
|
|
||||||
link_triplets = []
|
|
||||||
for node_name in info_nodes:
|
|
||||||
link_triplets.append({
|
|
||||||
"subject": task_id,
|
|
||||||
"relation": "CONTAINS_INFO",
|
|
||||||
"object": node_name
|
|
||||||
})
|
|
||||||
graph.commit(triplets=link_triplets)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"description": description,
|
|
||||||
"info_nodes": info_nodes,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_set_state(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""设置任务状态"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
state = arguments.get("state")
|
|
||||||
|
|
||||||
# 删除旧状态
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": task_id, "relation_type": "HAS_STATE"},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置新状态
|
|
||||||
state_node = f"State_{state}"
|
|
||||||
result = graph.commit(
|
|
||||||
triplets=[{"subject": task_id, "relation": "HAS_STATE", "object": state_node}]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"new_state": state,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_delete(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""删除任务节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
delete_info_nodes = arguments.get("delete_info_nodes", True)
|
|
||||||
|
|
||||||
# 查询关联的信息节点
|
|
||||||
if delete_info_nodes:
|
|
||||||
recall_result = graph.recall(
|
|
||||||
query_intent=f"{task_id},CONTAINS_INFO",
|
|
||||||
depth=1
|
|
||||||
)
|
|
||||||
|
|
||||||
# 删除信息节点
|
|
||||||
for relation in recall_result.get("relations", []):
|
|
||||||
if relation.get("type") == "CONTAINS_INFO" and relation.get("source") == task_id:
|
|
||||||
info_node = relation.get("target")
|
|
||||||
graph.purge(
|
|
||||||
criteria={"subject_contains": info_node},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 删除任务节点
|
|
||||||
result = graph.purge(
|
|
||||||
criteria={"subject_contains": task_id},
|
|
||||||
mode="soft"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"deleted_info_nodes": delete_info_nodes,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def execute_task_link_info(graph: Any, arguments: dict) -> dict:
|
|
||||||
"""关联信息节点"""
|
|
||||||
task_id = arguments.get("task_id")
|
|
||||||
info_node_names = arguments.get("info_node_names", [])
|
|
||||||
|
|
||||||
triplets = []
|
|
||||||
for node_name in info_node_names:
|
|
||||||
triplets.append({
|
|
||||||
"subject": task_id,
|
|
||||||
"relation": "CONTAINS_INFO",
|
|
||||||
"object": node_name
|
|
||||||
})
|
|
||||||
|
|
||||||
result = graph.commit(triplets=triplets)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"task_id": task_id,
|
|
||||||
"linked_nodes": info_node_names,
|
|
||||||
"details": result
|
|
||||||
}
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
"""
|
|
||||||
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
|
||||||
"""
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolLimits:
|
|
||||||
"""工具调用限制配置"""
|
|
||||||
# 人设图限制
|
|
||||||
persona_query_max: int = 1 # 每轮最多查询1次人设图
|
|
||||||
persona_update_max: int = 1 # 每轮最多修改1次人设图
|
|
||||||
|
|
||||||
# 工作记忆链限制
|
|
||||||
task_query_max: int = 4 # 每轮最多查询4次工作记忆链
|
|
||||||
task_update_max: int = 2 # 每轮最多修改2次工作记忆链
|
|
||||||
|
|
||||||
# 一般记忆限制
|
|
||||||
memory_query_max: int = 20 # 每轮最多查询20次一般记忆
|
|
||||||
memory_update_max: int = 10 # 每轮最多修改10次一般记忆
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolCallCount:
|
|
||||||
"""工具调用计数"""
|
|
||||||
# 人设图
|
|
||||||
persona_query: int = 0
|
|
||||||
persona_update: int = 0
|
|
||||||
|
|
||||||
# 工作记忆链
|
|
||||||
task_query: int = 0
|
|
||||||
task_update: int = 0
|
|
||||||
|
|
||||||
# 一般记忆
|
|
||||||
memory_query: int = 0
|
|
||||||
memory_update: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ToolLimiter:
|
|
||||||
"""工具调用限制器"""
|
|
||||||
|
|
||||||
def __init__(self, limits: Optional[ToolLimits] = None):
|
|
||||||
self.limits = limits or ToolLimits()
|
|
||||||
self.counts = ToolCallCount()
|
|
||||||
|
|
||||||
def _classify_tool(self, tool_name: str, arguments: dict) -> tuple:
|
|
||||||
"""
|
|
||||||
分类工具调用
|
|
||||||
返回: (category, operation)
|
|
||||||
category: 'persona', 'task', 'memory'
|
|
||||||
operation: 'query', 'update'
|
|
||||||
"""
|
|
||||||
# 人设图工具
|
|
||||||
if tool_name in ('persona_update', 'persona_clear'):
|
|
||||||
return ('persona', 'update')
|
|
||||||
|
|
||||||
# 工作记忆链工具
|
|
||||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
|
|
||||||
# task_link_info 是关联操作,算作更新
|
|
||||||
return ('task', 'update')
|
|
||||||
|
|
||||||
# 一般记忆工具
|
|
||||||
if tool_name == 'memory_recall':
|
|
||||||
# 判断是查询人设图、工作记忆链还是一般记忆
|
|
||||||
query_intent = arguments.get('query_intent', '').lower()
|
|
||||||
|
|
||||||
# 检查是否查询人设图
|
|
||||||
if any(kw in query_intent for kw in ['人设', '角色', '性格', '语气', '说话风格', '扮演']):
|
|
||||||
return ('persona', 'query')
|
|
||||||
|
|
||||||
# 检查是否查询工作记忆链
|
|
||||||
if any(kw in query_intent for kw in ['tasknode', '工作记忆', '任务链', '任务', 'task']):
|
|
||||||
return ('task', 'query')
|
|
||||||
|
|
||||||
# 一般记忆查询
|
|
||||||
return ('memory', 'query')
|
|
||||||
|
|
||||||
if tool_name == 'memory_commit':
|
|
||||||
return ('memory', 'update')
|
|
||||||
|
|
||||||
if tool_name == 'memory_purge':
|
|
||||||
return ('memory', 'update')
|
|
||||||
|
|
||||||
if tool_name == 'memory_introspect':
|
|
||||||
return ('memory', 'query')
|
|
||||||
|
|
||||||
if tool_name in ('memory_archive', 'memory_cleanup'):
|
|
||||||
return ('memory', 'update')
|
|
||||||
|
|
||||||
# 未知工具,归类为一般记忆更新
|
|
||||||
return ('memory', 'update')
|
|
||||||
|
|
||||||
def can_call(self, tool_name: str, arguments: dict) -> tuple:
|
|
||||||
"""
|
|
||||||
检查是否允许调用工具
|
|
||||||
返回: (allowed, reason)
|
|
||||||
"""
|
|
||||||
category, operation = self._classify_tool(tool_name, arguments)
|
|
||||||
|
|
||||||
# 获取当前计数和限制
|
|
||||||
if category == 'persona':
|
|
||||||
if operation == 'query':
|
|
||||||
if self.counts.persona_query >= self.limits.persona_query_max:
|
|
||||||
return (False, f"人设图查询次数已达上限({self.limits.persona_query_max}次)")
|
|
||||||
else: # update
|
|
||||||
if self.counts.persona_update >= self.limits.persona_update_max:
|
|
||||||
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
|
|
||||||
|
|
||||||
elif category == 'task':
|
|
||||||
if operation == 'query':
|
|
||||||
if self.counts.task_query >= self.limits.task_query_max:
|
|
||||||
return (False, f"工作记忆链查询次数已达上限({self.limits.task_query_max}次)")
|
|
||||||
else: # update
|
|
||||||
if self.counts.task_update >= self.limits.task_update_max:
|
|
||||||
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
|
|
||||||
|
|
||||||
elif category == 'memory':
|
|
||||||
if operation == 'query':
|
|
||||||
if self.counts.memory_query >= self.limits.memory_query_max:
|
|
||||||
return (False, f"一般记忆查询次数已达上限({self.limits.memory_query_max}次)")
|
|
||||||
else: # update
|
|
||||||
if self.counts.memory_update >= self.limits.memory_update_max:
|
|
||||||
return (False, f"一般记忆修改次数已达上限({self.limits.memory_update_max}次)")
|
|
||||||
|
|
||||||
return (True, "允许调用")
|
|
||||||
|
|
||||||
def record_call(self, tool_name: str, arguments: dict) -> None:
|
|
||||||
"""记录工具调用"""
|
|
||||||
category, operation = self._classify_tool(tool_name, arguments)
|
|
||||||
|
|
||||||
if category == 'persona':
|
|
||||||
if operation == 'query':
|
|
||||||
self.counts.persona_query += 1
|
|
||||||
else:
|
|
||||||
self.counts.persona_update += 1
|
|
||||||
|
|
||||||
elif category == 'task':
|
|
||||||
if operation == 'query':
|
|
||||||
self.counts.task_query += 1
|
|
||||||
else:
|
|
||||||
self.counts.task_update += 1
|
|
||||||
|
|
||||||
elif category == 'memory':
|
|
||||||
if operation == 'query':
|
|
||||||
self.counts.memory_query += 1
|
|
||||||
else:
|
|
||||||
self.counts.memory_update += 1
|
|
||||||
|
|
||||||
def get_summary(self) -> str:
|
|
||||||
"""获取调用统计摘要"""
|
|
||||||
lines = [
|
|
||||||
f"人设图: 查询{self.counts.persona_query}/{self.limits.persona_query_max}次, "
|
|
||||||
f"修改{self.counts.persona_update}/{self.limits.persona_update_max}次",
|
|
||||||
f"工作记忆链: 查询{self.counts.task_query}/{self.limits.task_query_max}次, "
|
|
||||||
f"修改{self.counts.task_update}/{self.limits.task_update_max}次",
|
|
||||||
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
|
|
||||||
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次"
|
|
||||||
]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""重置计数(新的一轮对话开始时调用)"""
|
|
||||||
self.counts = ToolCallCount()
|
|
||||||
152
docs/api.md
152
docs/api.md
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
TrulyMEM 后端采用**请求-响应队列模式**,通过 `queue.Queue` 实现线程安全通信。后端在独立线程中运行,处理来自客户端的请求。
|
TrulyMEM 后端采用 **Packet 通信协议**,通过 `queue.Queue` 实现线程安全通信。后端在独立线程中运行,处理来自客户端的请求。
|
||||||
|
|
||||||
### 核心组件
|
### 核心组件
|
||||||
|
|
||||||
@ -12,50 +12,51 @@ TrulyMEM 后端采用**请求-响应队列模式**,通过 `queue.Queue` 实现
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `BackendServer` | 后端服务器,独立线程运行 |
|
| `BackendServer` | 后端服务器,独立线程运行 |
|
||||||
| `BackendClient` | 客户端封装,提供便捷方法 |
|
| `BackendClient` | 客户端封装,提供便捷方法 |
|
||||||
| `MessageType` | 请求类型枚举 |
|
| `PacketType` | 请求类型枚举 |
|
||||||
| `BackendRequest` | 请求数据包 |
|
| `Packet` | 数据包(请求) |
|
||||||
| `BackendResponse` | 响应数据包 |
|
| `PacketResponse` | 数据包响应 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 请求类型 (MessageType)
|
## 请求类型 (PacketType)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MessageType(Enum):
|
class PacketType(Enum):
|
||||||
PROCESS_MESSAGE = "process_message" # 处理消息
|
PROCESS_MESSAGE = "process_message" # 处理消息
|
||||||
EXECUTE_TOOL = "execute_tool" # 执行工具
|
EXECUTE_TOOL = "execute_tool" # 执行工具
|
||||||
GET_STATUS = "get_status" # 获取状态
|
GET_STATUS = "get_status" # 获取状态
|
||||||
GET_CONFIG = "get_config" # 获取配置
|
GET_CONFIG = "get_config" # 获取配置
|
||||||
SET_CONFIG = "set_config" # 设置配置
|
SET_CONFIG = "set_config" # 设置配置
|
||||||
GET_HISTORY = "get_history" # 获取历史
|
GET_HISTORY = "get_history" # 获取历史
|
||||||
SAVE_HISTORY = "save_history" # 保存历史
|
SAVE_HISTORY = "save_history" # 保存历史
|
||||||
SHUTDOWN = "shutdown" # 关闭服务
|
SHUTDOWN = "shutdown" # 关闭服务
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 数据包格式
|
## 数据包格式
|
||||||
|
|
||||||
### BackendRequest
|
### Packet
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackendRequest:
|
class Packet:
|
||||||
request_id: str # 请求唯一标识
|
id: str # 唯一标识
|
||||||
message_type: MessageType # 请求类型
|
type: PacketType # 请求类型
|
||||||
payload: Dict[str, Any] # 请求参数
|
body: Dict[str, Any] # 请求参数
|
||||||
response_queue: queue.Queue # 响应队列(用于返回结果)
|
response_queue: queue.Queue # 响应队列(可选)
|
||||||
|
created_at: float # 创建时间
|
||||||
```
|
```
|
||||||
|
|
||||||
### BackendResponse
|
### PacketResponse
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackendResponse:
|
class PacketResponse:
|
||||||
request_id: str # 对应的请求ID
|
id: str # 对应的请求ID
|
||||||
success: bool # 是否成功
|
success: bool # 是否成功
|
||||||
data: Any = None # 返回数据
|
data: Any = None # 返回数据
|
||||||
error: Optional[str] = None # 错误信息
|
error: Optional[str] = None # 错误信息
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -68,14 +69,15 @@ class BackendResponse:
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {
|
body = {
|
||||||
"user_input": str # 用户输入的消息
|
"user_input": str # 用户输入的消息
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
|
"success": True,
|
||||||
"content": str, # AI 回复内容
|
"content": str, # AI 回复内容
|
||||||
"tool_calls": [ # 工具调用记录
|
"tool_calls": [ # 工具调用记录
|
||||||
{
|
{
|
||||||
@ -92,11 +94,16 @@ data = {
|
|||||||
|
|
||||||
**示例:**
|
**示例:**
|
||||||
```python
|
```python
|
||||||
from core import BackendClient
|
from core import BackendServer, BackendClient
|
||||||
|
|
||||||
|
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||||
|
server.start(api_key="your-api-key")
|
||||||
|
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
result = client.process_message("你好,请记住我的名字是小明")
|
result = client.process_message("你好,请记住我的名字是小明")
|
||||||
# result = {"success": True, "data": {"content": "...", "tool_calls": [...]}}
|
|
||||||
|
if result.get("success"):
|
||||||
|
print(result["content"])
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -109,15 +116,16 @@ result = client.process_message("你好,请记住我的名字是小明")
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {
|
body = {
|
||||||
"tool_name": str, # 工具名称
|
"tool_name": str, # 工具名称
|
||||||
"arguments": dict # 工具参数
|
"arguments": dict # 工具参数
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
|
"success": True,
|
||||||
"result": str # 工具执行结果
|
"result": str # 工具执行结果
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -135,22 +143,23 @@ result = client.execute_tool("memory_recall", {"query_intent": "用户信息"})
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {} # 无参数
|
body = {} # 无参数
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"graph_initialized": bool, # 图数据库是否初始化
|
"running": bool, # 后端是否运行中
|
||||||
"client_initialized": bool, # API 客户端是否初始化
|
"config": dict, # 当前配置
|
||||||
"running": bool # 后端是否运行中
|
"graph_initialized": bool, # 图数据库是否初始化
|
||||||
|
"client_initialized": bool # API 客户端是否初始化
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**示例:**
|
**示例:**
|
||||||
```python
|
```python
|
||||||
status = client.get_status()
|
status = client.get_status()
|
||||||
# status = {"success": True, "data": {"running": True, ...}}
|
print(status["data"]["running"]) # True
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -161,12 +170,12 @@ status = client.get_status()
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {} # 无参数
|
body = {} # 无参数
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"api_key": str, # API Key
|
"api_key": str, # API Key
|
||||||
"base_url": str # API Base URL
|
"base_url": str # API Base URL
|
||||||
}
|
}
|
||||||
@ -180,7 +189,7 @@ data = {
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {
|
body = {
|
||||||
"api_key": str, # API Key
|
"api_key": str, # API Key
|
||||||
"base_url": str # API Base URL (默认: https://api.deepseek.com)
|
"base_url": str # API Base URL (默认: https://api.deepseek.com)
|
||||||
}
|
}
|
||||||
@ -188,7 +197,7 @@ payload = {
|
|||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"status": "config_updated"
|
"status": "config_updated"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -209,12 +218,12 @@ result = client.update_config(
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {} # 无参数
|
body = {} # 无参数
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"history": list # 消息历史列表
|
"history": list # 消息历史列表
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -227,14 +236,14 @@ data = {
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {
|
body = {
|
||||||
"messages": list # 消息列表
|
"messages": list # 消息列表
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"status": "history_saved"
|
"status": "history_saved"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -247,12 +256,12 @@ data = {
|
|||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
```python
|
```python
|
||||||
payload = {} # 无参数
|
body = {} # 无参数
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应数据:**
|
**响应数据:**
|
||||||
```python
|
```python
|
||||||
data = {
|
{
|
||||||
"status": "shutdown"
|
"status": "shutdown"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -275,36 +284,36 @@ client = BackendClient(server)
|
|||||||
|
|
||||||
# 3. 发送消息
|
# 3. 发送消息
|
||||||
result = client.process_message("你好")
|
result = client.process_message("你好")
|
||||||
print(result["data"]["content"])
|
print(result["content"])
|
||||||
|
|
||||||
# 4. 关闭
|
# 4. 关闭
|
||||||
client.shutdown()
|
client.shutdown()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 直接使用请求队列
|
### 使用 Packet 协议
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import queue
|
import queue
|
||||||
from core import BackendServer, MessageType, BackendRequest, BackendResponse
|
from core import BackendServer, Packet, PacketType
|
||||||
|
|
||||||
server = BackendServer()
|
server = BackendServer()
|
||||||
server.start(api_key="your-key")
|
server.start(api_key="your-key")
|
||||||
|
|
||||||
# 创建请求
|
# 创建请求包
|
||||||
response_queue = queue.Queue()
|
response_queue = queue.Queue()
|
||||||
request = BackendRequest(
|
packet = Packet(
|
||||||
request_id="req-001",
|
id="req-001",
|
||||||
message_type=MessageType.PROCESS_MESSAGE,
|
type=PacketType.PROCESS_MESSAGE,
|
||||||
payload={"user_input": "你好"},
|
body={"user_input": "你好"},
|
||||||
response_queue=response_queue
|
response_queue=response_queue
|
||||||
)
|
)
|
||||||
|
|
||||||
# 发送请求
|
# 发送请求
|
||||||
server._request_queue.put(request)
|
result = server.send(packet)
|
||||||
|
print(result.body)
|
||||||
|
|
||||||
# 等待响应
|
# 关闭
|
||||||
response = response_queue.get(timeout=30.0)
|
server.shutdown()
|
||||||
print(response.data)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -357,7 +366,7 @@ client = BackendClient(server)
|
|||||||
async def handler(websocket):
|
async def handler(websocket):
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
data = json.loads(message)
|
data = json.loads(message)
|
||||||
msg_type = data["type"]
|
msg_type = data.get("type")
|
||||||
|
|
||||||
if msg_type == "message":
|
if msg_type == "message":
|
||||||
result = client.process_message(data["content"])
|
result = client.process_message(data["content"])
|
||||||
@ -373,41 +382,18 @@ async def handler(websocket):
|
|||||||
async def main():
|
async def main():
|
||||||
server.start()
|
server.start()
|
||||||
async with websockets.serve(handler, "localhost", 8765):
|
async with websockets.serve(handler, "localhost", 8765):
|
||||||
await asyncio.Future() # run forever
|
await asyncio.Future()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
```
|
```
|
||||||
|
|
||||||
### 扩展为 gRPC
|
|
||||||
|
|
||||||
```protobuf
|
|
||||||
// truly_mem.proto
|
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
service TrulyMEM {
|
|
||||||
rpc ProcessMessage(MessageRequest) returns (MessageResponse);
|
|
||||||
rpc UpdateConfig(ConfigRequest) returns (ConfigResponse);
|
|
||||||
rpc GetStatus(Empty) returns (StatusResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
message MessageRequest {
|
|
||||||
string message = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message MessageResponse {
|
|
||||||
bool success = 1;
|
|
||||||
string content = 2;
|
|
||||||
string error = 3;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 线程安全说明
|
## 线程安全说明
|
||||||
|
|
||||||
- `BackendServer` 使用 `threading.Lock` 保护共享资源
|
- `BackendServer` 使用 `threading.Lock` 保护共享资源
|
||||||
- 所有请求通过 `queue.Queue` 传递,线程安全
|
- 所有请求通过 `queue.Queue` 传递,线程安全
|
||||||
- 响应通过每个请求独立的 `response_queue` 返回
|
- 响应通过每个请求独立的响应队列返回
|
||||||
- 默认超时时间:30 秒
|
- 默认超时时间:30 秒
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -463,4 +449,4 @@ message MessageResponse {
|
|||||||
|---------|------|
|
|---------|------|
|
||||||
| `API Key 未配置` | 未设置 API Key |
|
| `API Key 未配置` | 未设置 API Key |
|
||||||
| `timeout` | 请求超时 |
|
| `timeout` | 请求超时 |
|
||||||
| `工具调用被拒绝: ...` | 工具调用频率超限 |
|
| `工具调用被拒绝: ...` | 工具调用频率超限 |
|
||||||
@ -5,20 +5,17 @@
|
|||||||
### 从源码运行
|
### 从源码运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
|
||||||
git clone <repo-url>
|
git clone <repo-url>
|
||||||
cd TrulyMEM-TrueHumanMEM
|
cd TrulyMEM-TrueHumanMEM
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 运行
|
|
||||||
python trulymem_entry.py
|
python trulymem_entry.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 打包后运行
|
### 打包后运行
|
||||||
|
|
||||||
打包后会生成可执行文件(Windows: TrulyMEM.exe, Linux/macOS: TrulyMEM):
|
打包后会生成可执行文件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux/macOS
|
# Linux/macOS
|
||||||
@ -38,9 +35,11 @@ TrulyMEM.exe
|
|||||||
|
|
||||||
1. 运行应用
|
1. 运行应用
|
||||||
2. 按 **F2** 展开侧边栏
|
2. 按 **F2** 展开侧边栏
|
||||||
3. 输入 **API Key**
|
3. 输入 **API Key**、**模型**、**Base URL**
|
||||||
4. 按 **Enter** 保存
|
4. 按 **Enter** 保存
|
||||||
|
|
||||||
|
配置会自动保存到 `~/.trulymem/config.json`,下次启动自动加载。
|
||||||
|
|
||||||
## 快捷键
|
## 快捷键
|
||||||
|
|
||||||
| 按键 | 功能 |
|
| 按键 | 功能 |
|
||||||
@ -53,9 +52,32 @@ TrulyMEM.exe
|
|||||||
|
|
||||||
## 数据存储
|
## 数据存储
|
||||||
|
|
||||||
- **数据库**: `graph_memory.db`(应用目录)
|
| 数据 | 位置 |
|
||||||
- **配置**: `config.json`(应用目录)
|
|------|------|
|
||||||
- **格式**: SQLite
|
| 图数据库 | `graph_memory.db`(应用目录) |
|
||||||
|
| 配置文件 | `~/.trulymem/config.json` |
|
||||||
|
| 数据库格式 | SQLite |
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
### 通信协议
|
||||||
|
|
||||||
|
UI 与后端通过 **Packet 协议** 通信:
|
||||||
|
|
||||||
|
```
|
||||||
|
UI (Textual TUI)
|
||||||
|
↓ BackendClient
|
||||||
|
Packet → queue.Queue → BackendServer (独立线程)
|
||||||
|
↓
|
||||||
|
处理请求 → 返回响应
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置管理
|
||||||
|
|
||||||
|
- **存储位置**: `~/.trulymem/config.json`
|
||||||
|
- **自动加载**: 启动时从文件读取配置
|
||||||
|
- **动态更新**: 运行时修改配置立即生效
|
||||||
|
- **持久化**: 修改后自动保存到文件
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
@ -85,7 +107,7 @@ pip install -r requirements.txt
|
|||||||
# 运行测试
|
# 运行测试
|
||||||
pytest tests/
|
pytest tests/
|
||||||
|
|
||||||
# 打包(需 PyInstaller)
|
# 打包
|
||||||
bash build/build_windows.bat # Windows
|
bash build/build_windows.bat # Windows
|
||||||
bash build/build_linux.sh # Linux
|
bash build/build_linux.sh # Linux
|
||||||
```
|
```
|
||||||
206
docs/架构.md
206
docs/架构.md
@ -6,6 +6,7 @@
|
|||||||
- 极简视觉,信息密度优先
|
- 极简视觉,信息密度优先
|
||||||
- 工具痕迹默认隐藏,需要时可展开
|
- 工具痕迹默认隐藏,需要时可展开
|
||||||
- TUI 与后端分离,多线程通信
|
- TUI 与后端分离,多线程通信
|
||||||
|
- **一切皆图**,AI 推理全部在后端
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@ -14,53 +15,25 @@ TrulyMEM-TrueHumanMEM/
|
|||||||
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
|
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
|
||||||
├── core/ # 后端/业务逻辑
|
├── core/ # 后端/业务逻辑
|
||||||
│ ├── __init__.py # 导出 BackendServer, BackendClient, EmbeddedGraphDB
|
│ ├── __init__.py # 导出 BackendServer, BackendClient, EmbeddedGraphDB
|
||||||
│ ├── server.py # BackendServer (多线程队列通信)
|
│ ├── server.py # BackendServer (Packet 通信协议)
|
||||||
│ ├── client.py # BackendClient
|
│ ├── client.py # BackendClient (Packet 协议客户端)
|
||||||
│ ├── embedded_db.py # SQLite 图数据库实现
|
│ ├── embedded_db.py # SQLite 图数据库实现
|
||||||
│ ├── graph_client.py
|
│ ├── graph_client.py # OpenAI/DeepSeek API 客户端
|
||||||
│ ├── tool_executor.py # 工具执行器
|
│ ├── tool_executor.py # 工具执行器
|
||||||
│ ├── tool_limiter.py # 工具调用限制器
|
│ ├── tool_limiter.py # 工具调用限制器
|
||||||
│ ├── memory_tools.py # 工具定义
|
│ ├── tools/ # 工具定义
|
||||||
│ ├── prompts/ # 提示词管理
|
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── prompt_manager.py
|
│ │ └── memory_tools.py
|
||||||
│ │ └── templates/
|
│ └── prompts/ # 提示词管理
|
||||||
│ │ └── system_prompt.md
|
├── ui/ # TUI 显示层(仅显示,无 AI 逻辑)
|
||||||
│ └── tools/ # 工具模块
|
│ ├── __init__.py # 导出 GraphMemoryApp, AppConfig
|
||||||
│ ├── __init__.py
|
│ ├── app.py # GraphMemoryApp (通过 BackendClient 通信)
|
||||||
│ ├── memory_tools.py
|
│ ├── widgets/ # TUI 组件
|
||||||
│ ├── tool_executor.py
|
│ ├── handlers/ # 事件处理
|
||||||
│ └── tool_limiter.py
|
│ ├── models/ # 数据模型
|
||||||
├── ui/ # TUI 显示层
|
│ ├── services/ # 服务层(仅配置管理)
|
||||||
│ ├── __init__.py # 导出 GraphMemoryApp, AppConfig
|
│ └── styles/ # 样式文件
|
||||||
│ ├── app.py # GraphMemoryApp (纯显示)
|
└── tests/ # 测试 (42 tests)
|
||||||
│ ├── widgets/ # TUI 组件
|
|
||||||
│ │ ├── left_panel.py
|
|
||||||
│ │ ├── right_panel.py
|
|
||||||
│ │ ├── message_history.py
|
|
||||||
│ │ ├── message_widget.py
|
|
||||||
│ │ ├── input_box.py
|
|
||||||
│ │ ├── config_section.py
|
|
||||||
│ │ ├── operation_log.py
|
|
||||||
│ │ ├── cypher_query_box.py
|
|
||||||
│ │ └── status_bar.py
|
|
||||||
│ ├── handlers/ # 事件处理
|
|
||||||
│ │ ├── focus_handler.py
|
|
||||||
│ │ ├── key_handler.py
|
|
||||||
│ │ └── message_handler.py
|
|
||||||
│ ├── models/ # 数据模型
|
|
||||||
│ │ ├── message.py
|
|
||||||
│ │ ├── config.py
|
|
||||||
│ │ └── log_entry.py
|
|
||||||
│ ├── services/ # 服务层
|
|
||||||
│ │ ├── config_manager.py
|
|
||||||
│ │ ├── config_service.py
|
|
||||||
│ │ └── chat_service.py
|
|
||||||
│ └── styles/ # 样式文件
|
|
||||||
│ ├── app.css
|
|
||||||
│ ├── components.css
|
|
||||||
│ └── messages.css
|
|
||||||
└── tests/ # 测试 (38 tests)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构图
|
## 架构图
|
||||||
@ -69,13 +42,14 @@ TrulyMEM-TrueHumanMEM/
|
|||||||
trulymem_entry.py
|
trulymem_entry.py
|
||||||
│
|
│
|
||||||
├─ BackendServer.start() → 独立线程运行
|
├─ BackendServer.start() → 独立线程运行
|
||||||
│ ├─ 处理 PROCESS_MESSAGE 请求
|
│ ├─ 处理 PROCESS_MESSAGE 请求 → AI 推理 + 工具调用
|
||||||
│ ├─ 处理 EXECUTE_TOOL 请求
|
│ ├─ 处理 EXECUTE_TOOL 请求 → 外部工具调用(不限次数)
|
||||||
|
│ ├─ 处理 GET/SET_CONFIG 请求
|
||||||
│ └─ 管理 GraphMemoryClient, EmbeddedGraphDB
|
│ └─ 管理 GraphMemoryClient, EmbeddedGraphDB
|
||||||
│
|
│
|
||||||
└─ GraphMemoryApp(backend_server=server)
|
└─ GraphMemoryApp(backend_server=server)
|
||||||
│
|
│
|
||||||
└─ BackendClient ← queue.Queue → BackendServer
|
└─ BackendClient ← Packet 通信 → BackendServer
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件职责
|
## 组件职责
|
||||||
@ -84,22 +58,38 @@ trulymem_entry.py
|
|||||||
|
|
||||||
| 组件 | 职责 |
|
| 组件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `server.py` | 多线程队列通信,处理消息和工具调用 |
|
| `server.py` | Packet 协议处理,多线程队列通信,AI 推理,工具限制 |
|
||||||
| `client.py` | TUI 端的通信客户端 |
|
| `client.py` | 客户端封装,UI 与后端通信桥梁 |
|
||||||
| `embedded_db.py` | SQLite 图数据库 CRUD |
|
| `embedded_db.py` | SQLite 图数据库 CRUD |
|
||||||
| `graph_client.py` | OpenAI/DeepSeek API 客户端 |
|
| `graph_client.py` | OpenAI/DeepSeek API 客户端 |
|
||||||
| `tool_executor.py` | 工具执行逻辑 |
|
| `tool_executor.py` | 工具执行逻辑 |
|
||||||
| `tool_limiter.py` | 工具调用频率限制 |
|
| `tool_limiter.py` | 工具调用频率限制(仅限 AI 推理) |
|
||||||
|
|
||||||
### ui/ (显示层)
|
### ui/ (显示层)
|
||||||
|
|
||||||
| 组件 | 职责 |
|
| 组件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `app.py` | Textual 应用主类 |
|
| `app.py` | Textual 应用主类,仅通过 BackendClient 通信 |
|
||||||
| `widgets/` | TUI 组件(面板、输入框等) |
|
| `services/` | 仅配置管理,无 AI 逻辑 |
|
||||||
| `handlers/` | 事件处理(键盘、焦点) |
|
|
||||||
| `models/` | 数据模型(消息、配置) |
|
### 通信协议
|
||||||
| `services/` | 配置管理、服务层 |
|
|
||||||
|
UI 与后端通过 **Packet 通信协议** 交互:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core import BackendServer, BackendClient, Packet, PacketType
|
||||||
|
|
||||||
|
# 后端启动
|
||||||
|
server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
||||||
|
server.start(api_key="your-key")
|
||||||
|
|
||||||
|
# 客户端通信
|
||||||
|
client = BackendClient(server)
|
||||||
|
result = client.process_message("你好") # AI 推理
|
||||||
|
result = client.execute_tool("memory_introspect", {}) # 外部工具调用
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 数据流
|
## 数据流
|
||||||
|
|
||||||
@ -108,21 +98,27 @@ trulymem_entry.py
|
|||||||
↓
|
↓
|
||||||
BackendClient.process_message(user_input)
|
BackendClient.process_message(user_input)
|
||||||
↓
|
↓
|
||||||
queue.Queue → BackendServer (独立线程)
|
Packet (type=PROCESS_MESSAGE) → queue.Queue
|
||||||
|
↓
|
||||||
|
BackendServer (独立线程)
|
||||||
↓
|
↓
|
||||||
GraphMemoryClient.send_message_with_history()
|
GraphMemoryClient.send_message_with_history()
|
||||||
↓
|
↓
|
||||||
OpenAI API / DeepSeek API
|
OpenAI API / DeepSeek API
|
||||||
↓
|
↓
|
||||||
execute_tool() → EmbeddedGraphDB
|
execute_tool() + ToolLimiter (AI 推理时受限)
|
||||||
|
↓
|
||||||
|
EmbeddedGraphDB (图数据库)
|
||||||
↓
|
↓
|
||||||
循环调用 API 直到无 tool_calls
|
循环调用 API 直到无 tool_calls
|
||||||
↓
|
↓
|
||||||
queue.Queue → 返回结果
|
Packet 响应返回
|
||||||
↓
|
↓
|
||||||
MessageHistory 显示
|
MessageHistory 显示
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 启动流程
|
## 启动流程
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -146,82 +142,7 @@ def main():
|
|||||||
backend_server.shutdown()
|
backend_server.shutdown()
|
||||||
```
|
```
|
||||||
|
|
||||||
## 布局结构
|
---
|
||||||
|
|
||||||
### 默认视图(右侧展开)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┬─────────────────────┐
|
|
||||||
│ │ F2:隐藏侧边栏 │
|
|
||||||
│ 🟠 14:30:25 │ ────────────────────│
|
|
||||||
│ 用户: 量子力学是什么? │ API Key: *** │
|
|
||||||
│ │ 模型: deepseek-chat │
|
|
||||||
│ 🔵 14:30:26 │ Base URL: ... │
|
|
||||||
│ 是的!根据记忆... │ ────────────────────│
|
|
||||||
│ [工具:2次] (F3展开) │ [操作日志] │
|
|
||||||
│ │ 14:30:26 recall │
|
|
||||||
│ ┌─────────────────────────────┐ │ 实体: 量子力学 │
|
|
||||||
│ │ 🟠 [输入框...] │ │ ───────────────────│
|
|
||||||
│ └─────────────────────────────┘ │ >[查询...] │
|
|
||||||
└─────────────────────────────────────┴─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### F2后(右侧折叠)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ 🟠 14:30:25 │
|
|
||||||
│ 用户: 量子力学是什么? │
|
|
||||||
│ │
|
|
||||||
│ 🔵 14:30:26 │
|
|
||||||
│ 是的!根据记忆... │
|
|
||||||
│ [工具:2次] (F3展开) │
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────┐ │
|
|
||||||
│ │ 🟠 [输入框...] │ │
|
|
||||||
│ └─────────────────────────────┘ │
|
|
||||||
│ F1:帮助 F2:展开 F5:清屏 F6:退出 │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快捷键
|
|
||||||
|
|
||||||
| 按键 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| F1 | 显示帮助 |
|
|
||||||
| F2 | 切换侧边栏 |
|
|
||||||
| F3 | 工具详情 |
|
|
||||||
| F5 | 清屏 |
|
|
||||||
| F6 | 退出 |
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
| 技术 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| Python 3.8+ | 编程语言 |
|
|
||||||
| Textual 0.47+ | TUI 框架 |
|
|
||||||
| SQLite | 图数据库(默认内嵌) |
|
|
||||||
| OpenAI SDK | API 调用(兼容 DeepSeek) |
|
|
||||||
| threading.Queue | 多线程通信 |
|
|
||||||
| PyInstaller | 打包 |
|
|
||||||
|
|
||||||
## 数据库模式
|
|
||||||
|
|
||||||
### SQLite 内嵌(默认)
|
|
||||||
|
|
||||||
```python
|
|
||||||
# core/embedded_db.py
|
|
||||||
class EmbeddedGraphDB:
|
|
||||||
def __init__(self, db_path="graph_memory.db"):
|
|
||||||
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 可选:Neo4j
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export USE_EMBEDDED_DB=false
|
|
||||||
docker run -d --name neo4j -p 7474:7474 -p 7687:7687 neo4j:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工具系统
|
## 工具系统
|
||||||
|
|
||||||
@ -241,4 +162,19 @@ docker run -d --name neo4j -p 7474:7474 -p 7687:7687 neo4j:latest
|
|||||||
- `task_create` - 创建任务
|
- `task_create` - 创建任务
|
||||||
- `task_set_state` - 设置状态
|
- `task_set_state` - 设置状态
|
||||||
- `task_delete` - 删除任务
|
- `task_delete` - 删除任务
|
||||||
- `task_link_info` - 关联信息
|
- `task_link_info` - 关联信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理原则
|
||||||
|
|
||||||
|
所有 API **不抛出异常**,错误通过返回字典传递:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = client.process_message("hello")
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
print(result["content"])
|
||||||
|
else:
|
||||||
|
print(result["error"]) # 错误描述
|
||||||
|
```
|
||||||
@ -9,304 +9,300 @@ os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
|||||||
|
|
||||||
|
|
||||||
class TestPacketTypeEnum:
|
class TestPacketTypeEnum:
|
||||||
def test_packet_type_message_exists(self):
|
"""测试 PacketType 枚举"""
|
||||||
|
|
||||||
|
def test_packet_type_process_message_exists(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
assert PacketType.MESSAGE is not None
|
assert PacketType.PROCESS_MESSAGE is not None
|
||||||
assert PacketType.MESSAGE.value == "message"
|
assert PacketType.PROCESS_MESSAGE.value == "process_message"
|
||||||
|
|
||||||
def test_packet_type_config_exists(self):
|
def test_packet_type_execute_tool_exists(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
assert PacketType.CONFIG is not None
|
assert PacketType.EXECUTE_TOOL is not None
|
||||||
assert PacketType.CONFIG.value == "config"
|
assert PacketType.EXECUTE_TOOL.value == "execute_tool"
|
||||||
|
|
||||||
def test_packet_type_tool_exists(self):
|
def test_packet_type_get_status_exists(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
assert PacketType.TOOL is not None
|
assert PacketType.GET_STATUS is not None
|
||||||
assert PacketType.TOOL.value == "tool"
|
assert PacketType.GET_STATUS.value == "get_status"
|
||||||
|
|
||||||
def test_packet_type_status_exists(self):
|
def test_packet_type_get_config_exists(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
assert PacketType.STATUS is not None
|
assert PacketType.GET_CONFIG is not None
|
||||||
assert PacketType.STATUS.value == "status"
|
assert PacketType.GET_CONFIG.value == "get_config"
|
||||||
|
|
||||||
def test_packet_type_history_exists(self):
|
def test_packet_type_set_config_exists(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
assert PacketType.HISTORY is not None
|
assert PacketType.SET_CONFIG is not None
|
||||||
assert PacketType.HISTORY.value == "history"
|
assert PacketType.SET_CONFIG.value == "set_config"
|
||||||
|
|
||||||
|
def test_packet_type_get_history_exists(self):
|
||||||
|
from core import PacketType
|
||||||
|
assert PacketType.GET_HISTORY is not None
|
||||||
|
assert PacketType.GET_HISTORY.value == "get_history"
|
||||||
|
|
||||||
|
def test_packet_type_save_history_exists(self):
|
||||||
|
from core import PacketType
|
||||||
|
assert PacketType.SAVE_HISTORY is not None
|
||||||
|
assert PacketType.SAVE_HISTORY.value == "save_history"
|
||||||
|
|
||||||
|
def test_packet_type_shutdown_exists(self):
|
||||||
|
from core import PacketType
|
||||||
|
assert PacketType.SHUTDOWN is not None
|
||||||
|
assert PacketType.SHUTDOWN.value == "shutdown"
|
||||||
|
|
||||||
def test_packet_type_all_values(self):
|
def test_packet_type_all_values(self):
|
||||||
from core import PacketType
|
from core import PacketType
|
||||||
values = [pt.value for pt in PacketType]
|
values = [pt.value for pt in PacketType]
|
||||||
assert "message" in values
|
assert "process_message" in values
|
||||||
assert "config" in values
|
assert "execute_tool" in values
|
||||||
assert "tool" in values
|
assert "get_status" in values
|
||||||
assert "status" in values
|
assert "get_config" in values
|
||||||
assert "history" in values
|
assert "set_config" in values
|
||||||
assert len(values) == 5
|
assert "get_history" in values
|
||||||
|
assert "save_history" in values
|
||||||
|
assert "shutdown" in values
|
||||||
|
assert len(values) == 8
|
||||||
|
|
||||||
|
|
||||||
class TestPacketCreation:
|
class TestPacketCreation:
|
||||||
|
"""测试 Packet 创建"""
|
||||||
|
|
||||||
def test_packet_with_id_and_type(self):
|
def test_packet_with_id_and_type(self):
|
||||||
from core import Packet, PacketType
|
from core import Packet, PacketType
|
||||||
packet = Packet(id="test-1", type=PacketType.MESSAGE, body={"message": "hello"})
|
packet = Packet(id="test-1", type=PacketType.PROCESS_MESSAGE, body={"user_input": "hello"})
|
||||||
assert packet.id == "test-1"
|
assert packet.id == "test-1"
|
||||||
assert packet.type == PacketType.MESSAGE
|
assert packet.type == PacketType.PROCESS_MESSAGE
|
||||||
|
|
||||||
def test_packet_body(self):
|
def test_packet_body(self):
|
||||||
from core import Packet, PacketType
|
from core import Packet, PacketType
|
||||||
body = {"message": "test", "extra": "data"}
|
body = {"user_input": "test", "extra": "data"}
|
||||||
packet = Packet(id="test-2", type=PacketType.CONFIG, body=body)
|
packet = Packet(id="test-2", type=PacketType.EXECUTE_TOOL, body=body)
|
||||||
assert packet.body == body
|
assert packet.body == body
|
||||||
|
|
||||||
|
def test_packet_with_empty_body(self):
|
||||||
|
from core import Packet, PacketType
|
||||||
|
packet = Packet(id="test-3", type=PacketType.GET_STATUS, body={})
|
||||||
|
assert packet.body == {}
|
||||||
|
|
||||||
def test_packet_created_at_default(self):
|
def test_packet_created_at_default(self):
|
||||||
from core import Packet, PacketType
|
from core import Packet, PacketType
|
||||||
before = time.time()
|
before = time.time()
|
||||||
packet = Packet(id="test-3", type=PacketType.STATUS, body={})
|
packet = Packet(id="test-4", type=PacketType.GET_CONFIG, body={})
|
||||||
after = time.time()
|
after = time.time()
|
||||||
assert before <= packet.created_at <= after
|
assert before <= packet.created_at <= after
|
||||||
|
|
||||||
def test_packet_with_custom_created_at(self):
|
|
||||||
from core import Packet, PacketType
|
class TestPacketResponse:
|
||||||
custom_time = 1234567890.0
|
"""测试 PacketResponse"""
|
||||||
packet = Packet(id="test-4", type=PacketType.HISTORY, body={}, created_at=custom_time)
|
|
||||||
assert packet.created_at == custom_time
|
def test_packet_response_success(self):
|
||||||
|
from core import PacketResponse
|
||||||
|
response = PacketResponse(id="resp-1", success=True, data={"result": "ok"})
|
||||||
|
assert response.id == "resp-1"
|
||||||
|
assert response.success is True
|
||||||
|
assert response.data == {"result": "ok"}
|
||||||
|
|
||||||
|
def test_packet_response_error(self):
|
||||||
|
from core import PacketResponse
|
||||||
|
response = PacketResponse(id="resp-2", success=False, error="error msg")
|
||||||
|
assert response.id == "resp-2"
|
||||||
|
assert response.success is False
|
||||||
|
assert response.error == "error msg"
|
||||||
|
|
||||||
|
|
||||||
class TestBackendServerInit:
|
class TestBackendServerCreation:
|
||||||
def test_create_server_defaults(self):
|
"""测试 BackendServer 创建"""
|
||||||
|
|
||||||
|
def test_backend_server_init(self):
|
||||||
|
from core import BackendServer
|
||||||
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
|
assert server._db_path == ":memory:"
|
||||||
|
assert server._use_embedded_db is True
|
||||||
|
assert server._graph is None
|
||||||
|
assert server._client is None
|
||||||
|
assert server._tool_limiter is None
|
||||||
|
|
||||||
|
def test_backend_server_default_params(self):
|
||||||
from core import BackendServer
|
from core import BackendServer
|
||||||
server = BackendServer()
|
server = BackendServer()
|
||||||
assert server._db_path == "graph_memory.db"
|
assert server._db_path == "graph_memory.db"
|
||||||
assert server._use_embedded_db is True
|
assert server._use_embedded_db is True
|
||||||
assert server._graph is None
|
|
||||||
assert server._client is None
|
|
||||||
|
class TestBackendServerLifecycle:
|
||||||
|
"""测试 BackendServer 生命周期"""
|
||||||
|
|
||||||
|
def test_backend_server_start_stop(self):
|
||||||
|
from core import BackendServer
|
||||||
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
|
server.start(api_key="")
|
||||||
|
|
||||||
|
assert server._running is True
|
||||||
|
assert server._graph is not None
|
||||||
|
assert server._tool_limiter is not None
|
||||||
|
|
||||||
|
server.shutdown()
|
||||||
assert server._running is False
|
assert server._running is False
|
||||||
|
|
||||||
def test_create_server_custom_db(self):
|
def test_backend_server_start_with_api_key(self):
|
||||||
from core import BackendServer
|
from core import BackendServer
|
||||||
server = BackendServer(db_path="custom.db")
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
assert server._db_path == "custom.db"
|
server.start(api_key="test-key", base_url="https://api.deepseek.com")
|
||||||
|
|
||||||
def test_create_server_no_embedded(self):
|
assert server._client is not None
|
||||||
from core import BackendServer
|
assert server._config["api_key"] == "test-key"
|
||||||
server = BackendServer(use_embedded_db=False)
|
|
||||||
assert server._use_embedded_db is False
|
server.shutdown()
|
||||||
|
|
||||||
|
|
||||||
class TestBackendServerStart:
|
class TestBackendClientCreation:
|
||||||
def test_start_without_api_key(self):
|
"""测试 BackendClient 创建"""
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
def test_backend_client_init(self):
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
|
||||||
assert server._running is True
|
|
||||||
assert server._graph is not None
|
|
||||||
assert server._client is None
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_start_with_api_key(self):
|
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="test-key", base_url="https://api.test.com")
|
|
||||||
assert server._running is True
|
|
||||||
assert server._config["api_key"] == "test-key"
|
|
||||||
assert server._config["base_url"] == "https://api.test.com"
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_start_twice_returns_early(self):
|
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
running_before = server._running
|
|
||||||
server.start(api_key="")
|
|
||||||
running_after = server._running
|
|
||||||
assert running_before is True
|
|
||||||
assert running_after is True
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBackendServerShutdown:
|
|
||||||
def test_shutdown_stops_server(self):
|
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
assert server._running is True
|
|
||||||
server.shutdown()
|
|
||||||
assert server._running is False
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_shutdown_closes_graph(self):
|
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
server.shutdown()
|
|
||||||
assert server._graph is None
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBackendServerPacketHandling:
|
|
||||||
def test_send_message_without_client(self):
|
|
||||||
from core import BackendServer, Packet, PacketType
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
packet = Packet(id="1", type=PacketType.MESSAGE, body={"message": "hello"})
|
|
||||||
response = server.send(packet)
|
|
||||||
assert response.body["success"] is True
|
|
||||||
assert "API Key not configured" in response.body["error"]
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_send_status_packet(self):
|
|
||||||
from core import BackendServer, Packet, PacketType
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
packet = Packet(id="2", type=PacketType.STATUS, body={})
|
|
||||||
response = server.send(packet)
|
|
||||||
assert response.body["success"] is True
|
|
||||||
assert response.body["running"] is True
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBackendClientInit:
|
|
||||||
def test_create_client_with_server(self):
|
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
db_path = f.name
|
client = BackendClient(server)
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
assert client._server is server
|
||||||
server.start(api_key="")
|
assert client._counter == 0
|
||||||
client = BackendClient(server)
|
|
||||||
assert client._server is server
|
|
||||||
assert client._counter == 0
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBackendClientMethods:
|
class TestBackendClientAPI:
|
||||||
|
"""测试 BackendClient API"""
|
||||||
|
|
||||||
def test_get_status(self):
|
def test_get_status(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
db_path = f.name
|
server.start(api_key="")
|
||||||
try:
|
client = BackendClient(server)
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
result = client.get_status()
|
||||||
client = BackendClient(server)
|
assert result.get("success") is True
|
||||||
status = client.get_status()
|
data = result.get("data", {})
|
||||||
assert status["success"] is True
|
assert data.get("running") is True
|
||||||
assert status["running"] is True
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_update_config(self):
|
def test_update_config(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
db_path = f.name
|
server.start(api_key="")
|
||||||
try:
|
client = BackendClient(server)
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
result = client.update_config(api_key="new-key", base_url="https://api.deepseek.com")
|
||||||
client = BackendClient(server)
|
assert result.get("success") is True
|
||||||
result = client.update_config(api_key="new-key", base_url="https://new-api.test.com")
|
|
||||||
assert result["success"] is True
|
server.shutdown()
|
||||||
assert result["status"] == "config_updated"
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
def test_save_and_get_history(self):
|
def test_get_config(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
db_path = f.name
|
server.start(api_key="test-key")
|
||||||
try:
|
client = BackendClient(server)
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
result = client.get_config()
|
||||||
client = BackendClient(server)
|
assert result.get("success") is True
|
||||||
|
assert result.get("data", {}).get("api_key") == "test-key"
|
||||||
test_messages = [{"role": "user", "content": "hello"}]
|
|
||||||
client.save_history(test_messages)
|
server.shutdown()
|
||||||
|
|
||||||
history = client.get_history()
|
|
||||||
assert history == test_messages
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
def test_process_message_no_api_key(self):
|
||||||
class TestMultipleClients:
|
|
||||||
def test_multiple_clients_thread_safety(self):
|
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
db_path = f.name
|
server.start(api_key="")
|
||||||
try:
|
client = BackendClient(server)
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
# 无 API key 应该返回错误(success 为 False)
|
||||||
client = BackendClient(server)
|
result = client.process_message("hello")
|
||||||
|
# 由于 API 调用失败,success 应该是 False
|
||||||
status1 = client.get_status()
|
assert result.get("success") is False
|
||||||
status2 = client.get_status()
|
|
||||||
|
server.shutdown()
|
||||||
assert status1["success"] is True
|
|
||||||
assert status2["success"] is True
|
def test_execute_tool(self):
|
||||||
server.shutdown()
|
from core import BackendServer, BackendClient
|
||||||
finally:
|
server = BackendServer(db_path=":memory:", use_embedded_db=True)
|
||||||
if os.path.exists(db_path):
|
server.start(api_key="")
|
||||||
os.unlink(db_path)
|
client = BackendClient(server)
|
||||||
|
|
||||||
|
result = client.execute_tool("memory_introspect", {})
|
||||||
|
assert result.get("success") is True
|
||||||
|
|
||||||
|
server.shutdown()
|
||||||
|
|
||||||
|
|
||||||
class TestServerStateAfterShutdown:
|
class TestToolLimiter:
|
||||||
def test_server_state_not_running(self):
|
"""测试工具限制器"""
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
def test_tool_limiter_init(self):
|
||||||
db_path = f.name
|
from core.tool_limiter import ToolLimiter
|
||||||
try:
|
limiter = ToolLimiter()
|
||||||
server = BackendServer(db_path=db_path)
|
assert limiter.counts.persona_query == 0
|
||||||
server.start(api_key="")
|
assert limiter.counts.persona_update == 0
|
||||||
assert server._running is True
|
|
||||||
server.shutdown()
|
def test_tool_limiter_classify(self):
|
||||||
assert server._running is False
|
from core.tool_limiter import ToolLimiter
|
||||||
finally:
|
limiter = ToolLimiter()
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
category, operation = limiter._classify_tool("memory_recall", {"query_intent": "test"})
|
||||||
|
assert category == "memory"
|
||||||
|
assert operation == "query"
|
||||||
|
|
||||||
|
category, operation = limiter._classify_tool("persona_update", {})
|
||||||
|
assert category == "persona"
|
||||||
|
assert operation == "update"
|
||||||
|
|
||||||
|
category, operation = limiter._classify_tool("task_create", {})
|
||||||
|
assert category == "task"
|
||||||
|
assert operation == "update"
|
||||||
|
|
||||||
|
def test_tool_limiter_can_call(self):
|
||||||
|
from core.tool_limiter import ToolLimiter
|
||||||
|
limiter = ToolLimiter()
|
||||||
|
|
||||||
|
allowed, reason = limiter.can_call("persona_update", {})
|
||||||
|
assert allowed is True
|
||||||
|
|
||||||
|
limiter.record_call("persona_update", {})
|
||||||
|
allowed, reason = limiter.can_call("persona_update", {})
|
||||||
|
assert allowed is False
|
||||||
|
assert "已达上限" in reason
|
||||||
|
|
||||||
|
def test_tool_limiter_reset(self):
|
||||||
|
from core.tool_limiter import ToolLimiter
|
||||||
|
limiter = ToolLimiter()
|
||||||
|
|
||||||
|
limiter.record_call("persona_update", {})
|
||||||
|
assert limiter.counts.persona_update == 1
|
||||||
|
|
||||||
|
limiter.reset()
|
||||||
|
assert limiter.counts.persona_update == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmbeddedGraphDB:
|
||||||
|
"""测试图数据库"""
|
||||||
|
|
||||||
|
def test_embedded_db_init(self):
|
||||||
|
from core.embedded_db import EmbeddedGraphDB
|
||||||
|
db = EmbeddedGraphDB(db_path=":memory:")
|
||||||
|
assert db.conn is not None
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def test_embedded_db_commit_and_recall(self):
|
||||||
|
from core.embedded_db import EmbeddedGraphDB
|
||||||
|
db = EmbeddedGraphDB(db_path=":memory:")
|
||||||
|
|
||||||
|
# 写入记忆 (使用 triplets 参数)
|
||||||
|
result = db.commit(
|
||||||
|
triplets=[
|
||||||
|
{"subject": "测试", "relation": "是", "object": "test"}
|
||||||
|
],
|
||||||
|
session_id="test-session"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 读取记忆
|
||||||
|
results = db.recall("测试")
|
||||||
|
assert len(results.get("entities", [])) > 0
|
||||||
|
|
||||||
|
db.close()
|
||||||
@ -6,8 +6,10 @@ os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
|||||||
|
|
||||||
|
|
||||||
class TestIntegrationPacketFlow:
|
class TestIntegrationPacketFlow:
|
||||||
def test_packet_round_trip_message(self):
|
"""测试 Packet 通信流程"""
|
||||||
from core import BackendServer, BackendClient, Packet, PacketType
|
|
||||||
|
def test_packet_round_trip_process_message(self):
|
||||||
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
db_path = f.name
|
db_path = f.name
|
||||||
try:
|
try:
|
||||||
@ -15,9 +17,10 @@ class TestIntegrationPacketFlow:
|
|||||||
server.start(api_key="")
|
server.start(api_key="")
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
result = client.send_message("test message")
|
# 无 API key 时应该返回错误而非抛异常
|
||||||
assert result["success"] is True
|
result = client.process_message("test message")
|
||||||
assert "API Key not configured" in result.get("error", "")
|
assert result.get("success") is False
|
||||||
|
assert "error" in result
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
@ -34,12 +37,12 @@ class TestIntegrationPacketFlow:
|
|||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
result = client.update_config(api_key="test-api", base_url="https://test.com")
|
result = client.update_config(api_key="test-api", base_url="https://test.com")
|
||||||
assert result["success"] is True
|
assert result.get("success") is True
|
||||||
assert result["status"] == "config_updated"
|
|
||||||
|
|
||||||
status = client.get_status()
|
status = client.get_status()
|
||||||
assert status["config"]["api_key"] == "test-api"
|
data = status.get("data", {})
|
||||||
assert status["config"]["base_url"] == "https://test.com"
|
assert data.get("config", {}).get("api_key") == "test-api"
|
||||||
|
assert data.get("config", {}).get("base_url") == "https://test.com"
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
@ -55,17 +58,18 @@ class TestIntegrationPacketFlow:
|
|||||||
server.start(api_key="")
|
server.start(api_key="")
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
status = client.get_status()
|
result = client.get_status()
|
||||||
assert status["success"] is True
|
assert result.get("success") is True
|
||||||
assert status["running"] is True
|
data = result.get("data", {})
|
||||||
assert status["client_ready"] is False
|
assert data.get("running") is True
|
||||||
|
assert data.get("graph_initialized") is True
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(db_path):
|
if os.path.exists(db_path):
|
||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
def test_packet_round_trip_history(self):
|
def test_packet_round_trip_execute_tool(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
db_path = f.name
|
db_path = f.name
|
||||||
@ -74,14 +78,8 @@ class TestIntegrationPacketFlow:
|
|||||||
server.start(api_key="")
|
server.start(api_key="")
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
messages = [
|
result = client.execute_tool("memory_introspect", {})
|
||||||
{"role": "user", "content": "hello"},
|
assert result.get("success") is True
|
||||||
{"role": "assistant", "content": "hi there"}
|
|
||||||
]
|
|
||||||
client.save_history(messages)
|
|
||||||
|
|
||||||
retrieved = client.get_history()
|
|
||||||
assert retrieved == messages
|
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
@ -89,8 +87,10 @@ class TestIntegrationPacketFlow:
|
|||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationSequentialOperations:
|
class TestIntegrationToolLimiter:
|
||||||
def test_sequential_config_updates(self):
|
"""测试工具限制器集成"""
|
||||||
|
|
||||||
|
def test_external_tool_call_not_limited(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
db_path = f.name
|
db_path = f.name
|
||||||
@ -99,36 +99,38 @@ class TestIntegrationSequentialOperations:
|
|||||||
server.start(api_key="")
|
server.start(api_key="")
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
client.update_config(api_key="key1", base_url="https://api1.com")
|
# 外部调用多次应该成功
|
||||||
status1 = client.get_status()
|
for i in range(5):
|
||||||
assert status1["config"]["api_key"] == "key1"
|
result = client.execute_tool("memory_introspect", {})
|
||||||
|
assert result.get("success") is True
|
||||||
client.update_config(api_key="key2", base_url="https://api2.com")
|
|
||||||
status2 = client.get_status()
|
|
||||||
assert status2["config"]["api_key"] == "key2"
|
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(db_path):
|
if os.path.exists(db_path):
|
||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
def test_save_history_override(self):
|
def test_internal_tool_call_limited(self):
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
db_path = f.name
|
db_path = f.name
|
||||||
try:
|
try:
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||||
server.start(api_key="")
|
server.start(api_key="fake-key") # 假 key 会失败但不影响测试
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
client.save_history([{"role": "user", "content": "first"}])
|
# 内部调用受限,tool_limiter 存在
|
||||||
history1 = client.get_history()
|
assert server._tool_limiter is not None
|
||||||
assert len(history1) == 1
|
|
||||||
|
|
||||||
client.save_history([{"role": "user", "content": "second"}])
|
# 初始状态
|
||||||
history2 = client.get_history()
|
assert server._tool_limiter.counts.persona_update == 0
|
||||||
assert len(history2) == 1
|
|
||||||
assert history2[0]["content"] == "second"
|
# 记录一次调用
|
||||||
|
server._tool_limiter.record_call("persona_update", {})
|
||||||
|
assert server._tool_limiter.counts.persona_update == 1
|
||||||
|
|
||||||
|
# 再次调用应该被拒绝
|
||||||
|
allowed, reason = server._tool_limiter.can_call("persona_update", {})
|
||||||
|
assert allowed is False
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
finally:
|
finally:
|
||||||
@ -136,117 +138,10 @@ class TestIntegrationSequentialOperations:
|
|||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationMultipleClients:
|
|
||||||
def test_two_clients_same_server(self):
|
|
||||||
from core import BackendServer, BackendClient
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
|
||||||
|
|
||||||
client1 = BackendClient(server)
|
|
||||||
client2 = BackendClient(server)
|
|
||||||
|
|
||||||
status1 = client1.get_status()
|
|
||||||
status2 = client2.get_status()
|
|
||||||
|
|
||||||
assert status1["success"] is True
|
|
||||||
assert status2["success"] is True
|
|
||||||
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationDatabase:
|
|
||||||
def test_server_creates_database(self):
|
|
||||||
from core import BackendServer
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
db_path = os.path.join(tmpdir, "test.db")
|
|
||||||
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server.start(api_key="")
|
|
||||||
|
|
||||||
assert os.path.exists(db_path)
|
|
||||||
|
|
||||||
server.shutdown()
|
|
||||||
assert os.path.exists(db_path)
|
|
||||||
|
|
||||||
def test_server_persists_across_restart(self):
|
|
||||||
from core import BackendServer, BackendClient
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
db_path = os.path.join(tmpdir, "persist.db")
|
|
||||||
|
|
||||||
server1 = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server1.start(api_key="")
|
|
||||||
server1.shutdown()
|
|
||||||
|
|
||||||
server2 = BackendServer(db_path=db_path, use_embedded_db=True)
|
|
||||||
server2.start(api_key="")
|
|
||||||
assert os.path.exists(db_path)
|
|
||||||
server2.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationEntryPoint:
|
|
||||||
def test_entry_import(self):
|
|
||||||
import trulymem_entry
|
|
||||||
assert trulymem_entry is not None
|
|
||||||
|
|
||||||
def test_entry_has_main(self):
|
|
||||||
import trulymem_entry
|
|
||||||
assert hasattr(trulymem_entry, 'main')
|
|
||||||
assert callable(trulymem_entry.main)
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationAllPacketTypes:
|
|
||||||
def test_message_type_string(self):
|
|
||||||
from core import PacketType
|
|
||||||
assert PacketType.MESSAGE.value == "message"
|
|
||||||
|
|
||||||
def test_config_type_string(self):
|
|
||||||
from core import PacketType
|
|
||||||
assert PacketType.CONFIG.value == "config"
|
|
||||||
|
|
||||||
def test_tool_type_string(self):
|
|
||||||
from core import PacketType
|
|
||||||
assert PacketType.TOOL.value == "tool"
|
|
||||||
|
|
||||||
def test_status_type_string(self):
|
|
||||||
from core import PacketType
|
|
||||||
assert PacketType.STATUS.value == "status"
|
|
||||||
|
|
||||||
def test_history_type_string(self):
|
|
||||||
from core import PacketType
|
|
||||||
assert PacketType.HISTORY.value == "history"
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationErrorHandling:
|
class TestIntegrationErrorHandling:
|
||||||
def test_timeout_on_slow_response(self):
|
"""测试错误处理"""
|
||||||
from core import BackendServer, Packet, PacketType
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
def test_process_message_returns_error_not_raise(self):
|
||||||
db_path = f.name
|
|
||||||
try:
|
|
||||||
server = BackendServer(db_path=db_path)
|
|
||||||
server.start(api_key="")
|
|
||||||
server._running = False
|
|
||||||
|
|
||||||
packet = Packet(id="timeout-test", type=PacketType.MESSAGE, body={"message": "test"})
|
|
||||||
|
|
||||||
original_timeout = 30.0
|
|
||||||
server.send = lambda p, timeout=original_timeout: (
|
|
||||||
setattr(server, '_running', True),
|
|
||||||
server.send(p)
|
|
||||||
)[1]
|
|
||||||
|
|
||||||
server.shutdown()
|
|
||||||
finally:
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
os.unlink(db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationFullWorkflow:
|
|
||||||
def test_complete_workflow(self):
|
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
db_path = f.name
|
db_path = f.name
|
||||||
@ -255,24 +150,30 @@ class TestIntegrationFullWorkflow:
|
|||||||
server.start(api_key="")
|
server.start(api_key="")
|
||||||
client = BackendClient(server)
|
client = BackendClient(server)
|
||||||
|
|
||||||
status_before = client.get_status()
|
# 应该返回错误,而不是抛出异常
|
||||||
assert status_before["success"] is True
|
result = client.process_message("hello")
|
||||||
|
assert result.get("success") is False
|
||||||
client.update_config(api_key="workflow-key", base_url="https://workflow.com")
|
assert "error" in result
|
||||||
status_after_config = client.get_status()
|
|
||||||
assert status_after_config["config"]["api_key"] == "workflow-key"
|
server.shutdown()
|
||||||
|
finally:
|
||||||
history = [{"role": "user", "content": "test workflow"}]
|
if os.path.exists(db_path):
|
||||||
client.save_history(history)
|
os.unlink(db_path)
|
||||||
retrieved_history = client.get_history()
|
|
||||||
assert retrieved_history == history
|
def test_execute_tool_error_handling(self):
|
||||||
|
from core import BackendServer, BackendClient
|
||||||
message_result = client.send_message("test")
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
assert message_result["success"] is True
|
db_path = f.name
|
||||||
|
try:
|
||||||
|
server = BackendServer(db_path=db_path, use_embedded_db=True)
|
||||||
|
server.start(api_key="")
|
||||||
|
client = BackendClient(server)
|
||||||
|
|
||||||
|
# 不存在的工具应该返回错误
|
||||||
|
result = client.execute_tool("nonexistent_tool", {})
|
||||||
|
assert result.get("success") is False
|
||||||
|
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
status_after_shutdown = client.get_status()
|
|
||||||
assert status_after_shutdown["running"] is False
|
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(db_path):
|
if os.path.exists(db_path):
|
||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
@ -6,7 +6,9 @@ from pathlib import Path
|
|||||||
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
|
||||||
|
|
||||||
|
|
||||||
class TestUIImportFull:
|
class TestUIImport:
|
||||||
|
"""测试 UI 模块导入"""
|
||||||
|
|
||||||
def test_import_graphmemoryapp(self):
|
def test_import_graphmemoryapp(self):
|
||||||
from ui import GraphMemoryApp
|
from ui import GraphMemoryApp
|
||||||
assert GraphMemoryApp is not None
|
assert GraphMemoryApp is not None
|
||||||
@ -15,52 +17,24 @@ class TestUIImportFull:
|
|||||||
from ui import AppConfig
|
from ui import AppConfig
|
||||||
assert AppConfig is not None
|
assert AppConfig is not None
|
||||||
|
|
||||||
def test_import_from_models(self):
|
def test_import_message(self):
|
||||||
from ui.models.message import Message, ToolCall, ToolResult
|
from ui.models.message import Message, ToolCall, ToolResult
|
||||||
assert Message is not None
|
assert Message is not None
|
||||||
assert ToolCall is not None
|
assert ToolCall is not None
|
||||||
assert ToolResult is not None
|
assert ToolResult is not None
|
||||||
|
|
||||||
def test_import_from_models_config(self):
|
def test_import_config(self):
|
||||||
from ui.models.config import AppConfig
|
from ui.models.config import AppConfig
|
||||||
assert AppConfig is not None
|
assert AppConfig is not None
|
||||||
|
|
||||||
def test_import_from_models_log_entry(self):
|
def test_import_log_entry(self):
|
||||||
from ui.models.log_entry import LogEntry
|
from ui.models.log_entry import LogEntry
|
||||||
assert LogEntry is not None
|
assert LogEntry is not None
|
||||||
|
|
||||||
def test_import_from_widgets(self):
|
|
||||||
from ui.widgets.left_panel import LeftPanel
|
|
||||||
from ui.widgets.right_panel import RightPanel
|
|
||||||
from ui.widgets.input_box import InputBox
|
|
||||||
from ui.widgets.message_history import MessageHistory
|
|
||||||
from ui.widgets.status_bar import StatusBar
|
|
||||||
assert LeftPanel is not None
|
|
||||||
assert RightPanel is not None
|
|
||||||
assert InputBox is not None
|
|
||||||
assert MessageHistory is not None
|
|
||||||
assert StatusBar is not None
|
|
||||||
|
|
||||||
def test_import_from_handlers(self):
|
class TestAppConfig:
|
||||||
from ui.handlers.focus_handler import FocusHandler
|
"""测试配置模型"""
|
||||||
from ui.handlers.key_handler import KeyHandler
|
|
||||||
from ui.handlers.message_handler import MessageHandler
|
|
||||||
assert FocusHandler is not None
|
|
||||||
assert KeyHandler is not None
|
|
||||||
assert MessageHandler is not None
|
|
||||||
|
|
||||||
def test_import_from_services(self):
|
|
||||||
from ui.services.config_manager import ConfigManager
|
|
||||||
from ui.services.config_service import ConfigService
|
|
||||||
from ui.services.chat_service import ChatService
|
|
||||||
from ui.services.tool_service import ToolService
|
|
||||||
assert ConfigManager is not None
|
|
||||||
assert ConfigService is not None
|
|
||||||
assert ChatService is not None
|
|
||||||
assert ToolService is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestAppConfigFull:
|
|
||||||
def test_config_default_values(self):
|
def test_config_default_values(self):
|
||||||
from ui.models.config import AppConfig
|
from ui.models.config import AppConfig
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
@ -68,25 +42,15 @@ class TestAppConfigFull:
|
|||||||
assert config.model == "deepseek-chat"
|
assert config.model == "deepseek-chat"
|
||||||
assert config.base_url == "https://api.deepseek.com"
|
assert config.base_url == "https://api.deepseek.com"
|
||||||
|
|
||||||
def test_config_from_env_with_key(self):
|
def test_config_from_env(self):
|
||||||
from ui.models.config import AppConfig
|
from ui.models.config import AppConfig
|
||||||
config = AppConfig.from_env()
|
config = AppConfig.from_env()
|
||||||
assert "fake-test-key" in config.api_key
|
assert "fake-test-key" in config.api_key
|
||||||
|
|
||||||
def test_config_from_env_custom(self):
|
|
||||||
os.environ["DEEPSEEK_API_KEY"] = "my-key"
|
|
||||||
os.environ["MODEL_NAME"] = "my-model"
|
|
||||||
os.environ["DEEPSEEK_BASE_URL"] = "https://my-api.com"
|
|
||||||
|
|
||||||
from ui.models.config import AppConfig
|
|
||||||
config = AppConfig.from_env()
|
|
||||||
|
|
||||||
assert config.api_key == "my-key"
|
|
||||||
assert config.model == "my-model"
|
|
||||||
assert config.base_url == "https://my-api.com"
|
|
||||||
|
|
||||||
|
class TestMessageModel:
|
||||||
class TestMessageModelFull:
|
"""测试消息模型"""
|
||||||
|
|
||||||
def test_message_creation_user(self):
|
def test_message_creation_user(self):
|
||||||
from ui.models.message import Message
|
from ui.models.message import Message
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -99,119 +63,36 @@ class TestMessageModelFull:
|
|||||||
from ui.models.message import Message
|
from ui.models.message import Message
|
||||||
msg = Message(role="assistant", content="assistant response")
|
msg = Message(role="assistant", content="assistant response")
|
||||||
assert msg.role == "assistant"
|
assert msg.role == "assistant"
|
||||||
assert msg.content == "assistant response"
|
|
||||||
|
|
||||||
def test_message_with_tool_calls(self):
|
def test_message_with_tool_calls(self):
|
||||||
from ui.models.message import Message, ToolCall
|
from ui.models.message import Message, ToolCall
|
||||||
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
|
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
|
||||||
msg = Message(role="assistant", content="response", tool_calls=[tc])
|
msg = Message(role="assistant", content="response", tool_calls=[tc])
|
||||||
assert msg.role == "assistant"
|
|
||||||
assert msg.tool_calls is not None
|
assert msg.tool_calls is not None
|
||||||
assert len(msg.tool_calls) == 1
|
assert len(msg.tool_calls) == 1
|
||||||
assert msg.tool_calls[0].name == "memory_recall"
|
|
||||||
|
|
||||||
def test_message_with_tool_results(self):
|
|
||||||
from ui.models.message import Message, ToolResult
|
|
||||||
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="result", success=True)
|
|
||||||
msg = Message(role="assistant", content="response", tool_results=[tr])
|
|
||||||
assert msg.tool_results is not None
|
|
||||||
assert len(msg.tool_results) == 1
|
|
||||||
assert msg.tool_results[0].success is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestToolCallModelFull:
|
class TestAppCSSPath:
|
||||||
def test_toolcall_creation(self):
|
"""测试 App CSS 配置"""
|
||||||
from ui.models.message import ToolCall
|
|
||||||
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
|
|
||||||
assert tc.id == "call-1"
|
|
||||||
assert tc.name == "memory_recall"
|
|
||||||
assert tc.arguments["query"] == "test"
|
|
||||||
|
|
||||||
|
|
||||||
class TestToolResultModelFull:
|
|
||||||
def test_toolresult_creation_success(self):
|
|
||||||
from ui.models.message import ToolResult
|
|
||||||
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="success result", success=True)
|
|
||||||
assert tr.tool_call_id == "call-1"
|
|
||||||
assert tr.name == "memory_recall"
|
|
||||||
assert tr.success is True
|
|
||||||
assert tr.result == "success result"
|
|
||||||
|
|
||||||
def test_toolresult_creation_failure(self):
|
|
||||||
from ui.models.message import ToolResult
|
|
||||||
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="error", success=False)
|
|
||||||
assert tr.success is False
|
|
||||||
assert tr.result == "error"
|
|
||||||
|
|
||||||
|
|
||||||
class TestLogEntryModelFull:
|
|
||||||
def test_logentry_creation(self):
|
|
||||||
from ui.models.log_entry import LogEntry
|
|
||||||
from datetime import datetime
|
|
||||||
entry = LogEntry(
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_name="memory_recall",
|
|
||||||
arguments={"query": "test"},
|
|
||||||
result="result",
|
|
||||||
duration=0.5
|
|
||||||
)
|
|
||||||
assert entry.tool_name == "memory_recall"
|
|
||||||
assert entry.duration == 0.5
|
|
||||||
|
|
||||||
def test_logentry_args_summary(self):
|
|
||||||
from ui.models.log_entry import LogEntry
|
|
||||||
from datetime import datetime
|
|
||||||
entry = LogEntry(
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_name="memory_recall",
|
|
||||||
arguments={"query": "test query with many characters"},
|
|
||||||
result="result",
|
|
||||||
duration=0.5
|
|
||||||
)
|
|
||||||
summary = entry.args_summary
|
|
||||||
assert isinstance(summary, str)
|
|
||||||
assert "query" in summary
|
|
||||||
|
|
||||||
def test_logentry_result_summary(self):
|
|
||||||
from ui.models.log_entry import LogEntry
|
|
||||||
from datetime import datetime
|
|
||||||
entry = LogEntry(
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_name="memory_recall",
|
|
||||||
arguments={},
|
|
||||||
result="a" * 200,
|
|
||||||
duration=0.5
|
|
||||||
)
|
|
||||||
summary = entry.result_summary
|
|
||||||
assert len(summary) <= 103
|
|
||||||
|
|
||||||
|
|
||||||
class TestAppCSSPathFull:
|
|
||||||
def test_app_has_css_path(self):
|
def test_app_has_css_path(self):
|
||||||
from ui import GraphMemoryApp
|
from ui import GraphMemoryApp
|
||||||
assert hasattr(GraphMemoryApp, 'CSS_PATH')
|
assert hasattr(GraphMemoryApp, 'CSS_PATH')
|
||||||
assert len(GraphMemoryApp.CSS_PATH) > 0
|
assert len(GraphMemoryApp.CSS_PATH) > 0
|
||||||
|
|
||||||
def test_css_path_are_paths(self):
|
|
||||||
from ui import GraphMemoryApp
|
|
||||||
for path in GraphMemoryApp.CSS_PATH:
|
|
||||||
assert isinstance(path, Path)
|
|
||||||
|
|
||||||
|
class TestAppBindings:
|
||||||
class TestAppBindingsFull:
|
"""测试 App 快捷键"""
|
||||||
|
|
||||||
def test_app_has_bindings(self):
|
def test_app_has_bindings(self):
|
||||||
from ui import GraphMemoryApp
|
from ui import GraphMemoryApp
|
||||||
assert hasattr(GraphMemoryApp, 'BINDINGS')
|
assert hasattr(GraphMemoryApp, 'BINDINGS')
|
||||||
assert len(GraphMemoryApp.BINDINGS) > 0
|
assert len(GraphMemoryApp.BINDINGS) > 0
|
||||||
|
|
||||||
def test_bindings_have_required_keys(self):
|
|
||||||
from ui import GraphMemoryApp
|
|
||||||
for binding in GraphMemoryApp.BINDINGS:
|
|
||||||
assert hasattr(binding, 'key')
|
|
||||||
assert hasattr(binding, 'action')
|
|
||||||
|
|
||||||
|
class TestWidgetImports:
|
||||||
class TestWidgetImportsFull:
|
"""测试组件导入"""
|
||||||
|
|
||||||
def test_import_left_panel(self):
|
def test_import_left_panel(self):
|
||||||
from ui.widgets.left_panel import LeftPanel
|
from ui.widgets.left_panel import LeftPanel
|
||||||
assert LeftPanel is not None
|
assert LeftPanel is not None
|
||||||
@ -228,28 +109,14 @@ class TestWidgetImportsFull:
|
|||||||
from ui.widgets.message_history import MessageHistory
|
from ui.widgets.message_history import MessageHistory
|
||||||
assert MessageHistory is not None
|
assert MessageHistory is not None
|
||||||
|
|
||||||
def test_import_message_widget(self):
|
|
||||||
from ui.widgets.message_widget import MessageWidget
|
|
||||||
assert MessageWidget is not None
|
|
||||||
|
|
||||||
def test_import_status_bar(self):
|
def test_import_status_bar(self):
|
||||||
from ui.widgets.status_bar import StatusBar
|
from ui.widgets.status_bar import StatusBar
|
||||||
assert StatusBar is not None
|
assert StatusBar is not None
|
||||||
|
|
||||||
def test_import_config_section(self):
|
|
||||||
from ui.widgets.config_section import ConfigSection
|
|
||||||
assert ConfigSection is not None
|
|
||||||
|
|
||||||
def test_import_operation_log(self):
|
class TestHandlerImports:
|
||||||
from ui.widgets.operation_log import OperationLog
|
"""测试处理器导入"""
|
||||||
assert OperationLog is not None
|
|
||||||
|
|
||||||
def test_import_cypher_query_box(self):
|
|
||||||
from ui.widgets.cypher_query_box import CypherQueryBox
|
|
||||||
assert CypherQueryBox is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestHandlerImportsFull:
|
|
||||||
def test_import_focus_handler(self):
|
def test_import_focus_handler(self):
|
||||||
from ui.handlers.focus_handler import FocusHandler
|
from ui.handlers.focus_handler import FocusHandler
|
||||||
assert FocusHandler is not None
|
assert FocusHandler is not None
|
||||||
@ -258,30 +125,22 @@ class TestHandlerImportsFull:
|
|||||||
from ui.handlers.key_handler import KeyHandler
|
from ui.handlers.key_handler import KeyHandler
|
||||||
assert KeyHandler is not None
|
assert KeyHandler is not None
|
||||||
|
|
||||||
def test_import_message_handler(self):
|
|
||||||
from ui.handlers.message_handler import MessageHandler
|
|
||||||
assert MessageHandler is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestServiceImportsFull:
|
|
||||||
def test_import_config_manager(self):
|
|
||||||
from ui.services.config_manager import ConfigManager
|
|
||||||
assert ConfigManager is not None
|
|
||||||
|
|
||||||
|
class TestServiceImports:
|
||||||
|
"""测试服务导入"""
|
||||||
|
|
||||||
def test_import_config_service(self):
|
def test_import_config_service(self):
|
||||||
from ui.services.config_service import ConfigService
|
from ui.services.config_service import ConfigService
|
||||||
assert ConfigService is not None
|
assert ConfigService is not None
|
||||||
|
|
||||||
def test_import_chat_service(self):
|
def test_import_config_manager(self):
|
||||||
from ui.services.chat_service import ChatService
|
from ui.services.config_manager import ConfigManager
|
||||||
assert ChatService is not None
|
assert ConfigManager is not None
|
||||||
|
|
||||||
def test_import_tool_service(self):
|
|
||||||
from ui.services.tool_service import ToolService
|
|
||||||
assert ToolService is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestAppInitializationFull:
|
class TestAppInitialization:
|
||||||
|
"""测试 App 初始化"""
|
||||||
|
|
||||||
def test_app_without_backend(self):
|
def test_app_without_backend(self):
|
||||||
from ui import GraphMemoryApp
|
from ui import GraphMemoryApp
|
||||||
app = GraphMemoryApp()
|
app = GraphMemoryApp()
|
||||||
@ -305,18 +164,56 @@ class TestAppInitializationFull:
|
|||||||
os.unlink(db_path)
|
os.unlink(db_path)
|
||||||
|
|
||||||
|
|
||||||
class TestConfigManagerFull:
|
class TestUIWithBackendClient:
|
||||||
def test_config_manager_creation(self):
|
"""测试 UI 与后端通信"""
|
||||||
from ui.services.config_manager import ConfigManager
|
|
||||||
cm = ConfigManager()
|
def test_app_sends_message_via_backend_client(self):
|
||||||
assert cm is not None
|
from ui import GraphMemoryApp
|
||||||
|
from core import BackendServer, BackendClient
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
|
db_path = f.name
|
||||||
|
try:
|
||||||
|
server = BackendServer(db_path=db_path)
|
||||||
|
server.start(api_key="")
|
||||||
|
|
||||||
|
app = GraphMemoryApp(backend_server=server)
|
||||||
|
client = app._backend_client
|
||||||
|
|
||||||
|
# 测试 get_status
|
||||||
|
status = client.get_status()
|
||||||
|
assert status.get("success") is True
|
||||||
|
|
||||||
|
# 测试 update_config
|
||||||
|
result = client.update_config(api_key="sk-test", base_url="https://api.deepseek.com")
|
||||||
|
assert result.get("success") is True
|
||||||
|
|
||||||
|
server.shutdown()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
os.unlink(db_path)
|
||||||
|
|
||||||
def test_config_manager_config_path(self):
|
def test_ui_only_uses_backend_client(self):
|
||||||
from ui.services.config_manager import ConfigManager
|
from ui import GraphMemoryApp
|
||||||
cm = ConfigManager()
|
from core import BackendServer
|
||||||
assert cm._config_path is not None
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
def test_config_manager_exists_false(self):
|
db_path = f.name
|
||||||
from ui.services.config_manager import ConfigManager
|
try:
|
||||||
cm = ConfigManager()
|
server = BackendServer(db_path=db_path)
|
||||||
assert cm.exists() is False
|
server.start(api_key="")
|
||||||
|
|
||||||
|
app = GraphMemoryApp(backend_server=server)
|
||||||
|
|
||||||
|
# UI 不应该直接访问后端内部
|
||||||
|
assert hasattr(app, '_backend_client')
|
||||||
|
assert app._backend_client is not None
|
||||||
|
|
||||||
|
# 不应该有 _graph, _client 等直接访问
|
||||||
|
assert not hasattr(app, '_graph')
|
||||||
|
assert not hasattr(app, '_client')
|
||||||
|
|
||||||
|
server.shutdown()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
os.unlink(db_path)
|
||||||
@ -1,33 +1,43 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
if getattr(sys, 'frozen', False):
|
# 用户配置文件始终放在用户目录
|
||||||
application_path = Path(sys.executable).parent
|
CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||||
else:
|
DB_PATH = Path.home() / ".trulymem" / "graph_memory.db"
|
||||||
application_path = Path(__file__).parent
|
|
||||||
|
|
||||||
sys.path.insert(0, str(application_path))
|
# 源码运行时使用项目目录,打包后使用用户目录
|
||||||
import os
|
if getattr(sys, 'frozen', False):
|
||||||
os.chdir(application_path)
|
# 打包版本:创建用户目录
|
||||||
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
else:
|
||||||
|
# 源码版本:检查项目目录是否有配置(向后兼容)
|
||||||
|
project_dir = Path(__file__).parent
|
||||||
|
project_config = project_dir / "config.json"
|
||||||
|
project_db = project_dir / "graph_memory.db"
|
||||||
|
|
||||||
|
if project_config.exists():
|
||||||
|
CONFIG_PATH = project_config
|
||||||
|
if project_db.exists():
|
||||||
|
DB_PATH = project_db
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
os.chdir(Path(__file__).parent)
|
||||||
|
|
||||||
from core import BackendServer
|
from core import BackendServer
|
||||||
from ui import GraphMemoryApp
|
from ui import GraphMemoryApp
|
||||||
from ui.services.config_service import ConfigService
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 配置文件保存在应用根目录(exe同级目录或源码根目录)
|
backend_server = BackendServer(
|
||||||
config_file = application_path / "config.json"
|
db_path=str(DB_PATH),
|
||||||
|
use_embedded_db=True,
|
||||||
|
config_file=str(CONFIG_PATH)
|
||||||
|
)
|
||||||
|
backend_server.start()
|
||||||
|
|
||||||
# 加载配置
|
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
|
||||||
config_service = ConfigService(config_file=config_file)
|
|
||||||
config = config_service.get_config()
|
|
||||||
|
|
||||||
backend_server = BackendServer(db_path="graph_memory.db", use_embedded_db=True)
|
|
||||||
backend_server.start(api_key=config.api_key, base_url=config.base_url)
|
|
||||||
|
|
||||||
app = GraphMemoryApp(backend_server=backend_server, config_service=config_service)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
75
ui/app.py
75
ui/app.py
@ -5,7 +5,6 @@ from textual.binding import Binding
|
|||||||
|
|
||||||
from core import BackendServer, BackendClient
|
from core import BackendServer, BackendClient
|
||||||
from .models.message import Message
|
from .models.message import Message
|
||||||
from .services.config_service import ConfigService
|
|
||||||
|
|
||||||
|
|
||||||
class GraphMemoryApp(App):
|
class GraphMemoryApp(App):
|
||||||
@ -23,11 +22,10 @@ class GraphMemoryApp(App):
|
|||||||
Binding("f6", "quit", "退出"),
|
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)
|
super().__init__(**kwargs)
|
||||||
self._backend_server = backend_server
|
self._backend_server = backend_server
|
||||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||||
self._config_service = config_service
|
|
||||||
self._api_configured = False
|
self._api_configured = False
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
@ -36,8 +34,16 @@ class GraphMemoryApp(App):
|
|||||||
from .widgets.status_bar import StatusBar
|
from .widgets.status_bar import StatusBar
|
||||||
from .models.config import AppConfig
|
from .models.config import AppConfig
|
||||||
|
|
||||||
# 获取初始配置
|
if self._backend_client:
|
||||||
initial_config = self._config_service.get_config() if self._config_service else AppConfig()
|
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 LeftPanel()
|
||||||
yield RightPanel(config=initial_config)
|
yield RightPanel(config=initial_config)
|
||||||
@ -56,7 +62,8 @@ class GraphMemoryApp(App):
|
|||||||
return
|
return
|
||||||
|
|
||||||
status = self._backend_client.get_status()
|
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)
|
status_bar.set_api_status(self._api_configured)
|
||||||
|
|
||||||
from .widgets.message_history import MessageHistory
|
from .widgets.message_history import MessageHistory
|
||||||
@ -118,24 +125,19 @@ class GraphMemoryApp(App):
|
|||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
status_bar = self.query_one(StatusBar)
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
try:
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
None,
|
||||||
None,
|
lambda: self._backend_client.process_message(user_input)
|
||||||
lambda: self._backend_client.send_message(user_input)
|
)
|
||||||
)
|
|
||||||
|
if result.get("success"):
|
||||||
# 更新"处理中"消息为实际回复
|
content = result.get("content", "(无回复)")
|
||||||
if result.get("success"):
|
history.update_latest_message(content)
|
||||||
content = result.get("content", "(无回复)")
|
else:
|
||||||
history.update_latest_message(content)
|
error = result.get("error", "未知错误")
|
||||||
else:
|
history.update_latest_message(f"❌ 错误: {error}")
|
||||||
error = result.get("error", "未知错误")
|
|
||||||
history.update_latest_message(f"❌ 错误: {error}")
|
status_bar.set_processing(False)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
history.update_latest_message(f"❌ 异常: {str(e)}")
|
|
||||||
finally:
|
|
||||||
status_bar.set_processing(False)
|
|
||||||
|
|
||||||
def on_config_section_config_changed(self, event) -> None:
|
def on_config_section_config_changed(self, event) -> None:
|
||||||
"""处理配置变更事件"""
|
"""处理配置变更事件"""
|
||||||
@ -151,24 +153,41 @@ class GraphMemoryApp(App):
|
|||||||
async def _update_config_async(self, config) -> None:
|
async def _update_config_async(self, config) -> None:
|
||||||
"""异步更新配置"""
|
"""异步更新配置"""
|
||||||
from .widgets.status_bar import StatusBar
|
from .widgets.status_bar import StatusBar
|
||||||
|
from .widgets.config_section import ConfigSection
|
||||||
|
|
||||||
status_bar = self.query_one(StatusBar)
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
api_key = config.api_key
|
api_key = config.api_key
|
||||||
base_url = config.base_url
|
base_url = config.base_url
|
||||||
|
model = getattr(config, 'model', 'deepseek-chat')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
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"):
|
if result.get("success"):
|
||||||
self._api_configured = bool(api_key)
|
self._api_configured = bool(api_key)
|
||||||
status_bar.set_api_status(self._api_configured)
|
status_bar.set_api_status(self._api_configured)
|
||||||
|
|
||||||
# 保存配置到文件(使用完整的config对象,保留model字段)
|
# 从后端重新获取配置并刷新 UI
|
||||||
if self._config_service:
|
config_result = await asyncio.get_event_loop().run_in_executor(
|
||||||
self._config_service.set_config(config)
|
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")
|
self.notify("✅ 配置已保存并生效", title="配置成功", severity="information")
|
||||||
else:
|
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