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:
root
2026-04-14 11:49:39 +08:00
parent 2a76fc6477
commit c742a30e1b
19 changed files with 1020 additions and 2349 deletions

View File

@ -1,242 +1,12 @@
import threading
import queue
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 .server import BackendServer, Packet, PacketType, PacketResponse
from .client import BackendClient
from .embedded_db import EmbeddedGraphDB
class BackendServer:
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True):
self._db_path = db_path
self._use_embedded_db = use_embedded_db
self._graph = None
self._client = None
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"]
__all__ = [
"BackendServer",
"BackendClient",
"EmbeddedGraphDB",
"Packet",
"PacketType",
"PacketResponse"
]

View File

@ -1,61 +1,75 @@
import threading
import time
from typing import Any, Dict
from .server import BackendServer, MessageType
from .server import BackendServer, Packet, PacketType
class BackendClient:
def __init__(self, server: BackendServer):
self._server = server
self._request_counter = 0
self._counter = 0
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:
self._request_counter += 1
return self._server.process_message(user_input, timeout)
self._counter += 1
return f"{time.time()}_{self._counter}"
def execute_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: float = 10.0) -> str:
with self._lock:
self._request_counter += 1
return self._server.execute_tool(tool_name, arguments, timeout)
def send(self, message: str) -> Dict:
return self.process_message(message)
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> Dict[str, Any]:
return self._send_request(MessageType.SET_CONFIG, {"api_key": api_key, "base_url": base_url})
def process_message(self, user_input: str) -> Dict:
return self._server.process_message(user_input)
def get_config(self) -> Dict[str, str]:
result = self._send_request(MessageType.GET_CONFIG, {})
return result.get("data", {"api_key": "", "base_url": "https://api.deepseek.com"})
def get_message_history(self) -> list:
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
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> Dict:
packet = Packet(
id=self._next_id(),
type=PacketType.SET_CONFIG,
body={"api_key": api_key, "base_url": base_url, "model": model}
)
self._server._request_queue.put(request)
try:
response = response_queue.get(timeout=5.0)
if not response.success:
raise Exception(response.error)
return {"success": True, "data": response.data}
except queue.Empty:
return {"success": False, "error": "timeout"}
return self._server.send(packet).body
def execute_tool(self, name: str, arguments: Dict) -> Dict:
packet = Packet(
id=self._next_id(),
type=PacketType.EXECUTE_TOOL,
body={"tool_name": name, "arguments": arguments}
)
return self._server.send(packet).body
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:
self._server.shutdown()

View File

@ -303,12 +303,12 @@ class Neo4jGraph:
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.graph = graph
self.tools = TOOLS
self.model = model
# 使用新的提示词管理器
prompt_manager = PromptManager()
self.system_prompt = prompt_manager.get_system_prompt()
@ -330,7 +330,7 @@ class GraphMemoryClient:
messages.extend(tool_results)
response = self.client.chat.completions.create(
model=MODEL_NAME,
model=self.model,
messages=messages,
tools=self.tools,
tool_choice="auto"
@ -347,7 +347,7 @@ class GraphMemoryClient:
messages.extend(messages_history)
response = self.client.chat.completions.create(
model=MODEL_NAME,
model=self.model,
messages=messages,
tools=self.tools,
tool_choice="auto"
@ -373,7 +373,7 @@ class GraphMemoryClient:
messages.extend(tool_results)
stream = self.client.chat.completions.create(
model=MODEL_NAME,
model=self.model,
messages=messages,
tools=self.tools,
tool_choice="auto",

View File

@ -10,6 +10,9 @@ class PromptManager:
_instance = None
_cached_prompt = None
# 用户自定义提示词路径
USER_PROMPT_PATH = Path.home() / ".trulymem" / "system_prompt.md"
def __new__(cls):
"""单例模式,避免重复加载"""
if cls._instance is None:
@ -26,11 +29,19 @@ class PromptManager:
if PromptManager._cached_prompt is not None:
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"
if prompt_file.exists():
with open(prompt_file, "r", encoding="utf-8") as f:
PromptManager._cached_prompt = f.read()
else:
# 3. 使用内置默认提示词
PromptManager._cached_prompt = self._build_default_prompt()
return PromptManager._cached_prompt
@ -78,3 +89,9 @@ class PromptManager:
- 如何使用工具
记住:灵活应对,保持自然对话体验。"""
@staticmethod
def clear_cache():
"""清除缓存,强制重新加载提示词"""
PromptManager._cached_prompt = None
PromptManager._instance = None

View File

@ -2,17 +2,16 @@ import threading
import queue
import time
import json
import os
from pathlib import Path
from typing import Any, Dict, Optional
from dataclasses import dataclass, field
from enum import Enum
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"
EXECUTE_TOOL = "execute_tool"
GET_STATUS = "get_status"
@ -24,46 +23,66 @@ class MessageType(Enum):
@dataclass
class BackendRequest:
request_id: str
message_type: MessageType
payload: Dict[str, Any]
response_queue: queue.Queue = field(default=None)
class Packet:
id: str
type: PacketType
body: Dict[str, Any]
response_queue: Optional[queue.Queue] = field(default=None)
created_at: float = field(default_factory=time.time)
@dataclass
class BackendResponse:
request_id: str
class PacketResponse:
id: str
success: bool
data: Any = None
error: Optional[str] = None
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._use_embedded_db = use_embedded_db
self._config_file = Path(config_file) if config_file else self.DEFAULT_CONFIG_PATH
self._graph = 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._thread: Optional[threading.Thread] = None
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:
return
self._init_graph()
self._load_config()
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(
api_key=api_key,
base_url=base_url,
api_key=self._config["api_key"],
base_url=self._config["base_url"],
model=self._config.get("model", "deepseek-chat"),
graph=self._graph
)
@ -71,6 +90,24 @@ class BackendServer:
self._thread = threading.Thread(target=self._run_loop, daemon=True)
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:
if self._use_embedded_db:
self._graph = EmbeddedGraphDB(db_path=self._db_path)
@ -85,310 +122,276 @@ class BackendServer:
def _run_loop(self) -> None:
while self._running:
try:
request = self._request_queue.get(timeout=0.1)
packet = self._input_queue.get(timeout=0.1)
except queue.Empty:
continue
if request.message_type == MessageType.PROCESS_MESSAGE:
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"}
))
self._process_packet(packet)
def _handle_process_message(self, request: BackendRequest) -> None:
def _process_packet(self, packet: Packet) -> None:
response_body = {"error": "not implemented"}
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:
self._send_response(request, BackendResponse(
request_id=request.request_id,
success=False,
error="API Key 未配置"
))
return
if "success" not in response_body:
response_body["success"] = True
except Exception as e:
response_body["success"] = False
response_body["error"] = str(e)
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()
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"
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
assistant_msg = {
"role": "assistant",
"content": message.content,
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
current_tool_results.append(tool_result_msg)
continue
self._tool_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,
"arguments": args,
"result": result
})
} 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)
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)
message = response.choices[0].message
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()}"
self._send_response(request, BackendResponse(
request_id=request.request_id,
success=True,
data={
"content": content,
"tool_calls": tool_calls,
"rejected_tools": rejected_tools
result = execute_tool(self._graph, tool_call.function.name, args)
tool_calls.append({
"name": tool_call.function.name,
"arguments": args,
"result": result
})
tool_result_msg = {
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
}
))
current_tool_results.append(tool_result_msg)
except Exception as e:
self._send_response(request, BackendResponse(
request_id=request.request_id,
success=False,
error=str(e)
))
messages_history.extend(current_tool_results)
response = self._client.send_message_with_history(messages_history)
message = response.choices[0].message
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:
tool_name = request.payload.get("tool_name")
arguments = request.payload.get("arguments", {})
tool_name = body.get("tool_name")
arguments = body.get("arguments", {})
# 前端直接调用的工具不受次数限制,直接执行
result = execute_tool(self._graph, tool_name, arguments)
self._send_response(request, BackendResponse(
request_id=request.request_id,
success=True,
data={"result": result}
))
return {"success": True, "result": result}
except Exception as e:
self._send_response(request, BackendResponse(
request_id=request.request_id,
success=False,
error=str(e)
))
return {"success": False, "error": str(e)}
def _handle_get_status(self, request: BackendRequest) -> None:
try:
status = {
"graph_initialized": self._graph is not None,
"client_initialized": self._client is not None,
"running": self._running
}
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_status(self) -> Dict:
return {
"running": self._running,
"config": self._config,
"graph_initialized": self._graph is not None,
"client_initialized": self._client is not None
}
def _handle_get_config(self, request: BackendRequest) -> None:
try:
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_get_config(self) -> Dict:
return self._config.copy()
def _handle_set_config(self, request: BackendRequest) -> None:
try:
api_key = request.payload.get("api_key", "")
base_url = request.payload.get("base_url", "https://api.deepseek.com")
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()
def _handle_set_config(self, body: Dict) -> Dict:
api_key = body.get("api_key", "")
base_url = body.get("base_url", "https://api.deepseek.com")
model = body.get("model", "deepseek-chat")
request = BackendRequest(
request_id=request_id,
message_type=MessageType.PROCESS_MESSAGE,
payload={"user_input": user_input},
response_queue=response_queue
self.update_config(api_key, base_url, model)
self._save_config()
return {"status": "config_updated"}
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)
try:
response = response_queue.get(timeout=timeout)
if not response.success:
raise Exception(response.error)
return response.data
except queue.Empty:
raise TimeoutError("请求超时")
response = self.send(packet)
return response.body
def execute_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: float = 10.0) -> str:
request_id = f"{time.time()}"
response_queue = queue.Queue()
request = BackendRequest(
request_id=request_id,
message_type=MessageType.EXECUTE_TOOL,
payload={"tool_name": tool_name, "arguments": arguments},
response_queue=response_queue
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
packet = Packet(
id=f"{time.time()}",
type=PacketType.EXECUTE_TOOL,
body={"tool_name": tool_name, "arguments": arguments}
)
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:
response = response_queue.get(timeout=timeout)
if not response.success:
raise Exception(response.error)
return response.data["result"]
except queue.Empty:
raise TimeoutError("工具执行超时")
if api_key and self._graph:
from .graph_client import GraphMemoryClient
self._client = GraphMemoryClient(
api_key=api_key,
base_url=base_url,
model=model,
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:
if not self._running:
return
request_id = f"{time.time()}"
response_queue = queue.Queue()
request = BackendRequest(
request_id=request_id,
message_type=MessageType.SHUTDOWN,
payload={},
response_queue=response_queue
packet = Packet(
id=f"{time.time()}",
type=PacketType.SHUTDOWN,
body={}
)
self._request_queue.put(request)
self.send(packet)
if self._thread:
self._thread.join(timeout=2.0)
@ -396,22 +399,5 @@ class BackendServer:
if self._graph:
self._graph.close()
self._graph = None
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> None:
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", [])
self._running = False

View File

@ -2,7 +2,7 @@
工具定义模块
"""
from .memory_tools import TOOLS
from .tool_executor import execute_tool
from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
from ..tool_executor import execute_tool
from ..tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]

View File

@ -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
}

View File

@ -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()