mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-15 12:15:32 +00:00
feat: add config/history management to core layer
- Add GET_CONFIG, SET_CONFIG, GET_HISTORY, SAVE_HISTORY to BackendServer - Update BackendClient with get_config, get_message_history, save_message_history - All API key and message history now stored in core layer
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from .server import BackendServer
|
from .server import BackendServer, MessageType
|
||||||
|
|
||||||
|
|
||||||
class BackendClient:
|
class BackendClient:
|
||||||
@ -20,8 +20,42 @@ class BackendClient:
|
|||||||
self._request_counter += 1
|
self._request_counter += 1
|
||||||
return self._server.execute_tool(tool_name, arguments, timeout)
|
return self._server.execute_tool(tool_name, arguments, timeout)
|
||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> None:
|
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> Dict[str, Any]:
|
||||||
self._server.update_config(api_key, base_url)
|
return self._send_request(MessageType.SET_CONFIG, {"api_key": api_key, "base_url": base_url})
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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"}
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self._server.shutdown()
|
self._server.shutdown()
|
||||||
@ -16,6 +16,10 @@ class MessageType(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"
|
||||||
|
SET_CONFIG = "set_config"
|
||||||
|
GET_HISTORY = "get_history"
|
||||||
|
SAVE_HISTORY = "save_history"
|
||||||
SHUTDOWN = "shutdown"
|
SHUTDOWN = "shutdown"
|
||||||
|
|
||||||
|
|
||||||
@ -91,6 +95,14 @@ class BackendServer:
|
|||||||
self._handle_execute_tool(request)
|
self._handle_execute_tool(request)
|
||||||
elif request.message_type == MessageType.GET_STATUS:
|
elif request.message_type == MessageType.GET_STATUS:
|
||||||
self._handle_get_status(request)
|
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:
|
elif request.message_type == MessageType.SHUTDOWN:
|
||||||
self._running = False
|
self._running = False
|
||||||
self._send_response(request, BackendResponse(
|
self._send_response(request, BackendResponse(
|
||||||
@ -262,6 +274,69 @@ class BackendServer:
|
|||||||
error=str(e)
|
error=str(e)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
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_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:
|
def _send_response(self, request: BackendRequest, response: BackendResponse) -> None:
|
||||||
if request.response_queue:
|
if request.response_queue:
|
||||||
request.response_queue.put(response)
|
request.response_queue.put(response)
|
||||||
@ -333,9 +408,19 @@ class BackendServer:
|
|||||||
|
|
||||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> None:
|
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com") -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
self._config = {"api_key": api_key, "base_url": base_url}
|
||||||
if api_key and self._graph:
|
if api_key and self._graph:
|
||||||
self._client = GraphMemoryClient(
|
self._client = GraphMemoryClient(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
graph=self._graph
|
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", [])
|
||||||
@ -1,3 +0,0 @@
|
|||||||
"""Graph Memory TUI - Terminal User Interface for Graph Memory System"""
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
@ -1,462 +0,0 @@
|
|||||||
"""主应用类 - 参考demo实现"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from textual.app import App, ComposeResult
|
|
||||||
from textual.binding import Binding
|
|
||||||
from textual.widgets import Static
|
|
||||||
from textual.containers import Container
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from .widgets.left_panel import LeftPanel
|
|
||||||
from .widgets.right_panel import RightPanel
|
|
||||||
from .widgets.status_bar import StatusBar
|
|
||||||
from .widgets.input_box import InputBox
|
|
||||||
from .widgets.message_history import MessageHistory
|
|
||||||
from .models.message import Message, ToolCall, ToolResult
|
|
||||||
from .models.config import AppConfig
|
|
||||||
from .models.log_entry import LogEntry
|
|
||||||
from .services.config_manager import ConfigManager
|
|
||||||
from .core.imports import (
|
|
||||||
Neo4jGraph,
|
|
||||||
GraphMemoryClient,
|
|
||||||
execute_tool,
|
|
||||||
NEO4J_URI,
|
|
||||||
NEO4J_USER,
|
|
||||||
NEO4J_PASSWORD,
|
|
||||||
MODEL_NAME,
|
|
||||||
USE_EMBEDDED_DB,
|
|
||||||
)
|
|
||||||
from .core.tools.tool_limiter import ToolLimiter
|
|
||||||
|
|
||||||
|
|
||||||
class GraphMemoryApp(App[None]):
|
|
||||||
"""Textual TUI 主应用"""
|
|
||||||
|
|
||||||
CSS_PATH = [
|
|
||||||
Path(__file__).parent / "styles" / "app.css",
|
|
||||||
Path(__file__).parent / "styles" / "messages.css",
|
|
||||||
Path(__file__).parent / "styles" / "components.css",
|
|
||||||
]
|
|
||||||
|
|
||||||
BINDINGS = [
|
|
||||||
Binding("f1", "show_help", "帮助"),
|
|
||||||
Binding("f2", "toggle_sidebar", "侧边栏"),
|
|
||||||
Binding("f3", "toggle_tool_details", "工具详情"),
|
|
||||||
Binding("f4", "focus_query", "查询"),
|
|
||||||
Binding("f5", "clear_history", "清屏"),
|
|
||||||
Binding("f6", "quit", "退出"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, config: AppConfig | None = None, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
# 配置管理器
|
|
||||||
self._config_manager = ConfigManager()
|
|
||||||
# 优先使用传入的配置,其次加载持久化配置,最后使用环境变量
|
|
||||||
if config:
|
|
||||||
self._config = config
|
|
||||||
elif self._config_manager.exists():
|
|
||||||
self._config = self._config_manager.load()
|
|
||||||
else:
|
|
||||||
self._config = AppConfig.from_env()
|
|
||||||
|
|
||||||
# 核心组件
|
|
||||||
self._graph: Neo4jGraph | None = None
|
|
||||||
self._client: GraphMemoryClient | None = None
|
|
||||||
|
|
||||||
# 工具调用限制器
|
|
||||||
self._tool_limiter = ToolLimiter()
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
"""构建组件树"""
|
|
||||||
yield LeftPanel()
|
|
||||||
yield RightPanel(self._config, use_embedded_db=USE_EMBEDDED_DB)
|
|
||||||
yield StatusBar()
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
|
||||||
"""应用启动初始化"""
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 初始化内嵌图数据库
|
|
||||||
self._graph = Neo4jGraph(db_path="graph_memory.db")
|
|
||||||
|
|
||||||
# 初始化 API 客户端
|
|
||||||
self._init_client()
|
|
||||||
|
|
||||||
# 显示连接成功消息
|
|
||||||
welcome = Message(
|
|
||||||
role="assistant",
|
|
||||||
content="✅ 系统初始化成功!\n\n"
|
|
||||||
f"• 数据库: 内嵌SQLite (graph_memory.db)\n"
|
|
||||||
f"• API Key: {'已配置' if self._config.api_key else '未配置'}\n\n"
|
|
||||||
"现在可以开始对话了!",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(welcome)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 显示错误消息
|
|
||||||
error = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=f"❌ 初始化失败: {str(e)}\n\n"
|
|
||||||
"请检查:\n"
|
|
||||||
"1. Neo4j 数据库是否启动 (运行: docker start neo4j)\n"
|
|
||||||
"2. API Key 是否配置\n"
|
|
||||||
"3. 网络连接是否正常\n\n"
|
|
||||||
"启动Neo4j: docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/graphmemory123 neo4j:latest",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(error)
|
|
||||||
|
|
||||||
def _init_client(self) -> None:
|
|
||||||
"""初始化API客户端"""
|
|
||||||
if self._config.api_key and self._graph:
|
|
||||||
self._client = GraphMemoryClient(
|
|
||||||
api_key=self._config.api_key,
|
|
||||||
base_url=self._config.base_url,
|
|
||||||
graph=self._graph
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
|
||||||
"""应用退出清理"""
|
|
||||||
if self._graph:
|
|
||||||
self._graph.close()
|
|
||||||
|
|
||||||
# 快捷键动作
|
|
||||||
def action_show_help(self) -> None:
|
|
||||||
"""显示帮助"""
|
|
||||||
help_text = """
|
|
||||||
快捷键:
|
|
||||||
F1 - 帮助
|
|
||||||
F2 - 切换侧边栏
|
|
||||||
F3 - 工具详情
|
|
||||||
F4 - 查询框(仅 Neo4j 模式)
|
|
||||||
F5 - 清屏
|
|
||||||
F6 - 退出
|
|
||||||
|
|
||||||
输入消息后按 Enter 发送
|
|
||||||
"""
|
|
||||||
self.notify(help_text, title="帮助", timeout=10)
|
|
||||||
|
|
||||||
def action_toggle_sidebar(self) -> None:
|
|
||||||
"""切换侧边栏"""
|
|
||||||
sidebar = self.query_one(RightPanel)
|
|
||||||
sidebar.toggle()
|
|
||||||
sidebar.update_title()
|
|
||||||
|
|
||||||
def action_toggle_tool_details(self) -> None:
|
|
||||||
"""切换工具详情"""
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
history.toggle_latest_tool_details()
|
|
||||||
|
|
||||||
def action_focus_query(self) -> None:
|
|
||||||
"""聚焦查询框(仅 Neo4j 模式可用)"""
|
|
||||||
sidebar = self.query_one(RightPanel)
|
|
||||||
|
|
||||||
if not sidebar.has_cypher_query_box():
|
|
||||||
self.notify("查询框仅在 Neo4j 模式下可用", title="提示", timeout=3)
|
|
||||||
return
|
|
||||||
|
|
||||||
if sidebar.is_collapsed():
|
|
||||||
sidebar.toggle()
|
|
||||||
sidebar.update_title()
|
|
||||||
|
|
||||||
query_box = sidebar.get_cypher_query_box()
|
|
||||||
if query_box:
|
|
||||||
query_box.focus()
|
|
||||||
|
|
||||||
def action_clear_history(self) -> None:
|
|
||||||
"""清屏"""
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
history.clear_messages()
|
|
||||||
|
|
||||||
# 事件处理
|
|
||||||
def on_input_box_send_message(self, event: InputBox.SendMessage) -> None:
|
|
||||||
"""处理发送消息事件"""
|
|
||||||
try:
|
|
||||||
# 添加用户消息
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
user_message = Message(
|
|
||||||
role="user",
|
|
||||||
content=event.content,
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(user_message)
|
|
||||||
|
|
||||||
# 简单响应(避免崩溃)
|
|
||||||
if not self._config.api_key:
|
|
||||||
response_msg = Message(
|
|
||||||
role="assistant",
|
|
||||||
content="请先配置API Key。\n\n按F2打开侧边栏,输入API Key后按Enter保存。",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(response_msg)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 显示处理中消息
|
|
||||||
processing_msg = Message(
|
|
||||||
role="assistant",
|
|
||||||
content="正在处理...",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(processing_msg)
|
|
||||||
|
|
||||||
# 异步处理
|
|
||||||
asyncio.create_task(self._process_message_async(event.content))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 显示错误
|
|
||||||
error_msg = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=f"错误: {str(e)}",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(error_msg)
|
|
||||||
|
|
||||||
async def _process_message_async(self, user_input: str) -> None:
|
|
||||||
"""异步处理消息 - 参考demo实现"""
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
log = self.query_one(RightPanel).get_operation_log()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 检查客户端
|
|
||||||
if not self._client:
|
|
||||||
# 尝试重新初始化
|
|
||||||
self._init_client()
|
|
||||||
if not self._client:
|
|
||||||
raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key,然后按 Enter 保存")
|
|
||||||
|
|
||||||
# 重置工具调用限制器(新的一轮对话)
|
|
||||||
self._tool_limiter.reset()
|
|
||||||
|
|
||||||
# 构建初始消息历史
|
|
||||||
messages_history = [{"role": "user", "content": user_input}]
|
|
||||||
|
|
||||||
# 参考demo的调用方式
|
|
||||||
response = await asyncio.get_event_loop().run_in_executor(
|
|
||||||
None,
|
|
||||||
lambda: self._client.send_message_with_history(messages_history)
|
|
||||||
)
|
|
||||||
|
|
||||||
message = response.choices[0].message
|
|
||||||
|
|
||||||
# 处理工具调用循环
|
|
||||||
tool_calls = []
|
|
||||||
tool_results = []
|
|
||||||
accumulated_content = "" # 累积所有中间内容
|
|
||||||
rejected_tools = [] # 被拒绝的工具调用
|
|
||||||
|
|
||||||
# 显示工具调用摘要
|
|
||||||
if message.tool_calls:
|
|
||||||
tool_summary = f"🔧 正在调用 {len(message.tool_calls)} 个工具..."
|
|
||||||
summary_msg = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=tool_summary,
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(summary_msg)
|
|
||||||
|
|
||||||
while message.tool_calls:
|
|
||||||
# 累积中间内容(如果有)
|
|
||||||
if message.content:
|
|
||||||
accumulated_content += message.content + "\n\n"
|
|
||||||
|
|
||||||
# 构建包含 tool_calls 的 assistant 消息
|
|
||||||
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:
|
|
||||||
tc = ToolCall(
|
|
||||||
id=tool_call.id,
|
|
||||||
name=tool_call.function.name,
|
|
||||||
arguments=json.loads(tool_call.function.arguments)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查工具调用限制
|
|
||||||
allowed, reason = self._tool_limiter.can_call(tc.name, tc.arguments)
|
|
||||||
|
|
||||||
if not allowed:
|
|
||||||
# 拒绝调用
|
|
||||||
rejected_tools.append((tc.name, reason))
|
|
||||||
result = f"⚠️ 工具调用被拒绝: {reason}"
|
|
||||||
|
|
||||||
# 添加到当前轮结果
|
|
||||||
tool_result_msg = {
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": tc.id,
|
|
||||||
"content": result
|
|
||||||
}
|
|
||||||
current_tool_results.append(tool_result_msg)
|
|
||||||
|
|
||||||
# 添加日志
|
|
||||||
log_entry = LogEntry(
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_name=tc.name,
|
|
||||||
arguments=tc.arguments,
|
|
||||||
result=result,
|
|
||||||
duration=0.0
|
|
||||||
)
|
|
||||||
log.add_log(log_entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 记录调用
|
|
||||||
self._tool_limiter.record_call(tc.name, tc.arguments)
|
|
||||||
tool_calls.append(tc)
|
|
||||||
|
|
||||||
# 执行工具
|
|
||||||
start_time = datetime.now()
|
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
|
||||||
None,
|
|
||||||
lambda: execute_tool(self._graph, tc.name, tc.arguments)
|
|
||||||
)
|
|
||||||
duration = (datetime.now() - start_time).total_seconds()
|
|
||||||
|
|
||||||
# 保存工具结果
|
|
||||||
tr = ToolResult(
|
|
||||||
tool_call_id=tc.id,
|
|
||||||
name=tc.name,
|
|
||||||
arguments=tc.arguments,
|
|
||||||
result=result,
|
|
||||||
success=not result.startswith("工具执行错误")
|
|
||||||
)
|
|
||||||
tool_results.append(tr)
|
|
||||||
|
|
||||||
# 添加到当前轮结果
|
|
||||||
tool_result_msg = {
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": tc.id,
|
|
||||||
"content": result
|
|
||||||
}
|
|
||||||
current_tool_results.append(tool_result_msg)
|
|
||||||
|
|
||||||
# 添加日志
|
|
||||||
log_entry = LogEntry(
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_name=tc.name,
|
|
||||||
arguments=tc.arguments,
|
|
||||||
result=result,
|
|
||||||
duration=duration
|
|
||||||
)
|
|
||||||
log.add_log(log_entry)
|
|
||||||
|
|
||||||
# 将工具结果添加到消息历史
|
|
||||||
messages_history.extend(current_tool_results)
|
|
||||||
|
|
||||||
# 继续调用API(使用累积的消息历史)
|
|
||||||
response = await asyncio.get_event_loop().run_in_executor(
|
|
||||||
None,
|
|
||||||
lambda: 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()}"
|
|
||||||
|
|
||||||
assistant_message = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=content,
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_calls=tool_calls if tool_calls else None,
|
|
||||||
tool_results=tool_results if tool_results else None
|
|
||||||
)
|
|
||||||
|
|
||||||
history.add_message(assistant_message)
|
|
||||||
|
|
||||||
# 强制刷新界面
|
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 显示详细错误
|
|
||||||
error_msg = str(e)
|
|
||||||
|
|
||||||
if "Connection error" in error_msg or "connection" in error_msg.lower():
|
|
||||||
help_text = """
|
|
||||||
网络连接错误!可能的原因:
|
|
||||||
1. API Key 未配置或无效
|
|
||||||
2. 网络无法访问 API 服务器
|
|
||||||
3. API 服务器暂时不可用
|
|
||||||
|
|
||||||
解决方法:
|
|
||||||
• 按 F2 展开侧边栏,检查并配置 API Key
|
|
||||||
• 检查网络连接
|
|
||||||
• 尝试使用代理或 VPN
|
|
||||||
"""
|
|
||||||
elif "API Key" in error_msg:
|
|
||||||
help_text = """
|
|
||||||
API Key 未配置!
|
|
||||||
|
|
||||||
请按以下步骤配置:
|
|
||||||
1. 按 F2 展开右侧边栏
|
|
||||||
2. 点击"配置"展开配置区
|
|
||||||
3. 在 API Key 输入框输入你的密钥
|
|
||||||
4. 按 Enter 键保存配置
|
|
||||||
|
|
||||||
获取 API Key: https://platform.deepseek.com/
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
help_text = f"\n详细错误: {error_msg}"
|
|
||||||
|
|
||||||
error_message = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=f"❌ 错误: {error_msg}\n{help_text}",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(error_message)
|
|
||||||
|
|
||||||
def on_config_section_config_changed(self, event) -> None:
|
|
||||||
"""处理配置变更事件"""
|
|
||||||
# 更新配置
|
|
||||||
self._config = event.config
|
|
||||||
|
|
||||||
# 持久化保存配置
|
|
||||||
self._config_manager.save(self._config)
|
|
||||||
|
|
||||||
# 同步更新 RightPanel 的配置
|
|
||||||
try:
|
|
||||||
right_panel = self.query_one(RightPanel)
|
|
||||||
right_panel._config = self._config
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 重新初始化客户端
|
|
||||||
self._init_client()
|
|
||||||
|
|
||||||
if self._client:
|
|
||||||
self.notify("✅ 配置已保存并应用", title="配置")
|
|
||||||
else:
|
|
||||||
self.notify("⚠️ 配置已保存,但API Key无效", title="警告")
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Core Logic for Graph Memory TUI"""
|
|
||||||
@ -1,438 +0,0 @@
|
|||||||
"""
|
|
||||||
内嵌图数据库 - 基于SQLite实现
|
|
||||||
无需Docker,开箱即用
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Dict, Optional, Any
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddedGraphDB:
|
|
||||||
"""内嵌图数据库 - SQLite实现"""
|
|
||||||
|
|
||||||
def __init__(self, db_path: str = "graph_memory.db"):
|
|
||||||
"""
|
|
||||||
初始化数据库
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_path: 数据库文件路径
|
|
||||||
"""
|
|
||||||
self.db_path = Path(db_path)
|
|
||||||
self.conn = None
|
|
||||||
self._init_db()
|
|
||||||
|
|
||||||
def _init_db(self):
|
|
||||||
"""初始化数据库表"""
|
|
||||||
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
|
||||||
self.conn.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 创建实体表
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS entities (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT UNIQUE NOT NULL,
|
|
||||||
type TEXT,
|
|
||||||
mention_count INTEGER DEFAULT 1,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# 创建关系表
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS relations (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
source_id INTEGER NOT NULL,
|
|
||||||
target_id INTEGER NOT NULL,
|
|
||||||
relation_type TEXT NOT NULL,
|
|
||||||
confidence REAL DEFAULT 1.0,
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
session_id TEXT,
|
|
||||||
turn_id INTEGER,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
date_bucket TEXT,
|
|
||||||
superseded_by INTEGER,
|
|
||||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
|
||||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# 创建索引
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)")
|
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)")
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def ensure_constraints(self):
|
|
||||||
"""确保约束(兼容Neo4j接口)"""
|
|
||||||
pass # SQLite自动处理
|
|
||||||
|
|
||||||
def recall(self, query_intent: str, seed_entities: List[str] = None,
|
|
||||||
depth: int = 2, time_range: Dict = None,
|
|
||||||
session_filter: str = None) -> Dict:
|
|
||||||
"""
|
|
||||||
检索相关记忆
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query_intent: 查询关键词(逗号分隔)
|
|
||||||
seed_entities: 种子实体
|
|
||||||
depth: 搜索深度
|
|
||||||
time_range: 时间范围
|
|
||||||
session_filter: 会话过滤
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
检索结果
|
|
||||||
"""
|
|
||||||
keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()]
|
|
||||||
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 搜索实体
|
|
||||||
entities = []
|
|
||||||
entity_ids = set()
|
|
||||||
|
|
||||||
# 如果没有关键词,返回所有实体(用于"我们都聊过什么"这类问题)
|
|
||||||
if not keywords and not seed_entities:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT id, name, type, mention_count
|
|
||||||
FROM entities
|
|
||||||
ORDER BY mention_count DESC
|
|
||||||
LIMIT 50
|
|
||||||
""")
|
|
||||||
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
entity_ids.add(row['id'])
|
|
||||||
entities.append({
|
|
||||||
'name': row['name'],
|
|
||||||
'type': row['type'] or 'unknown',
|
|
||||||
'mention_count': row['mention_count']
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
# 有关键词,按关键词搜索
|
|
||||||
for keyword in keywords:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT id, name, type, mention_count
|
|
||||||
FROM entities
|
|
||||||
WHERE LOWER(name) LIKE ?
|
|
||||||
""", (f"%{keyword}%",))
|
|
||||||
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
if row['id'] not in entity_ids:
|
|
||||||
entity_ids.add(row['id'])
|
|
||||||
entities.append({
|
|
||||||
'name': row['name'],
|
|
||||||
'type': row['type'] or 'unknown',
|
|
||||||
'mention_count': row['mention_count']
|
|
||||||
})
|
|
||||||
|
|
||||||
# 搜索关系
|
|
||||||
relations = []
|
|
||||||
|
|
||||||
if entity_ids:
|
|
||||||
placeholders = ','.join('?' * len(entity_ids))
|
|
||||||
|
|
||||||
query = f"""
|
|
||||||
SELECT r.id, e1.name as source, e2.name as target,
|
|
||||||
r.relation_type as type, r.confidence, r.session_id,
|
|
||||||
r.turn_id, r.created_at, r.status
|
|
||||||
FROM relations r
|
|
||||||
JOIN entities e1 ON r.source_id = e1.id
|
|
||||||
JOIN entities e2 ON r.target_id = e2.id
|
|
||||||
WHERE (r.source_id IN ({placeholders}) OR r.target_id IN ({placeholders}))
|
|
||||||
AND r.status = 'active'
|
|
||||||
"""
|
|
||||||
|
|
||||||
params = list(entity_ids) + list(entity_ids)
|
|
||||||
|
|
||||||
if session_filter:
|
|
||||||
query += " AND r.session_id = ?"
|
|
||||||
params.append(session_filter)
|
|
||||||
|
|
||||||
cursor.execute(query, params)
|
|
||||||
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
relations.append({
|
|
||||||
'source': row['source'],
|
|
||||||
'target': row['target'],
|
|
||||||
'type': row['type'],
|
|
||||||
'confidence': row['confidence'],
|
|
||||||
'session_id': row['session_id'],
|
|
||||||
'turn_id': row['turn_id'],
|
|
||||||
'created_at': row['created_at'],
|
|
||||||
'status': row['status']
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entities": entities,
|
|
||||||
"relations": relations,
|
|
||||||
"message": f"找到 {len(entities)} 个实体, {len(relations)} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def commit(self, triplets: List[Dict], entity_types: Dict = None,
|
|
||||||
temporal_tag: str = None, session_id: str = None,
|
|
||||||
turn_id: int = None) -> Dict:
|
|
||||||
"""
|
|
||||||
写入记忆
|
|
||||||
|
|
||||||
Args:
|
|
||||||
triplets: 三元组列表
|
|
||||||
entity_types: 实体类型
|
|
||||||
temporal_tag: 时间标签
|
|
||||||
session_id: 会话ID
|
|
||||||
turn_id: 轮次ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
写入结果
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
created_entities = 0
|
|
||||||
created_relations = 0
|
|
||||||
|
|
||||||
for triplet in triplets:
|
|
||||||
subject = triplet.get('subject')
|
|
||||||
relation = triplet.get('relation')
|
|
||||||
obj = triplet.get('object')
|
|
||||||
confidence = triplet.get('confidence', 1.0)
|
|
||||||
|
|
||||||
if not all([subject, relation, obj]):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 创建或更新实体
|
|
||||||
for entity_name in [subject, obj]:
|
|
||||||
entity_type = entity_types.get(entity_name) if entity_types else None
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO entities (name, type)
|
|
||||||
VALUES (?, ?)
|
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
|
||||||
mention_count = mention_count + 1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
""", (entity_name, entity_type))
|
|
||||||
|
|
||||||
if cursor.rowcount > 0:
|
|
||||||
created_entities += 1
|
|
||||||
|
|
||||||
# 获取实体ID
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (subject,))
|
|
||||||
source_id = cursor.fetchone()['id']
|
|
||||||
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (obj,))
|
|
||||||
target_id = cursor.fetchone()['id']
|
|
||||||
|
|
||||||
# 创建关系
|
|
||||||
date_bucket = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO relations (
|
|
||||||
source_id, target_id, relation_type, confidence,
|
|
||||||
session_id, turn_id, date_bucket
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""", (source_id, target_id, relation, confidence,
|
|
||||||
session_id, turn_id, date_bucket))
|
|
||||||
|
|
||||||
created_relations += 1
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"created_entities": created_entities,
|
|
||||||
"created_relations": created_relations,
|
|
||||||
"message": f"创建了 {created_entities} 个实体, {created_relations} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def purge(self, criteria: Dict, mode: str = "soft",
|
|
||||||
new_relation: Dict = None) -> Dict:
|
|
||||||
"""
|
|
||||||
删除或修正记忆
|
|
||||||
|
|
||||||
Args:
|
|
||||||
criteria: 删除条件
|
|
||||||
mode: 删除模式 (soft/hard)
|
|
||||||
new_relation: 替代关系
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除结果
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = []
|
|
||||||
params = []
|
|
||||||
|
|
||||||
if criteria.get('source'):
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['source'],))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
conditions.append("source_id = ?")
|
|
||||||
params.append(row['id'])
|
|
||||||
|
|
||||||
if criteria.get('target'):
|
|
||||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['target'],))
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
conditions.append("target_id = ?")
|
|
||||||
params.append(row['id'])
|
|
||||||
|
|
||||||
if criteria.get('relation'):
|
|
||||||
conditions.append("relation_type = ?")
|
|
||||||
params.append(criteria['relation'])
|
|
||||||
|
|
||||||
if not conditions:
|
|
||||||
return {"deleted": 0, "message": "无删除条件"}
|
|
||||||
|
|
||||||
where_clause = " AND ".join(conditions)
|
|
||||||
|
|
||||||
if mode == "soft":
|
|
||||||
cursor.execute(f"""
|
|
||||||
UPDATE relations
|
|
||||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE {where_clause} AND status = 'active'
|
|
||||||
""", params)
|
|
||||||
else:
|
|
||||||
cursor.execute(f"""
|
|
||||||
DELETE FROM relations
|
|
||||||
WHERE {where_clause}
|
|
||||||
""", params)
|
|
||||||
|
|
||||||
deleted = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"deleted": deleted,
|
|
||||||
"mode": mode,
|
|
||||||
"message": f"删除了 {deleted} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def introspect(self, session_id: str = None) -> Dict:
|
|
||||||
"""
|
|
||||||
查看会话状态
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session_id: 会话ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
会话状态
|
|
||||||
"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
# 统计实体
|
|
||||||
cursor.execute("SELECT COUNT(*) as count FROM entities")
|
|
||||||
entity_count = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
# 统计关系
|
|
||||||
cursor.execute("SELECT COUNT(*) as count FROM relations WHERE status = 'active'")
|
|
||||||
relation_count = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entity_count": entity_count,
|
|
||||||
"relation_count": relation_count,
|
|
||||||
"session_id": session_id,
|
|
||||||
"message": f"数据库包含 {entity_count} 个实体, {relation_count} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def archive(self, days: int = 30) -> Dict:
|
|
||||||
"""归档旧关系"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE relations
|
|
||||||
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE status = 'active'
|
|
||||||
AND created_at < datetime('now', ?)
|
|
||||||
""", (f'-{days} days',))
|
|
||||||
|
|
||||||
archived = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"archived": archived,
|
|
||||||
"message": f"归档了 {archived} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def cleanup(self, dry_run: bool = True) -> Dict:
|
|
||||||
"""清理已删除数据"""
|
|
||||||
cursor = self.conn.cursor()
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM relations
|
|
||||||
WHERE status = 'deleted'
|
|
||||||
AND updated_at < datetime('now', '-90 days')
|
|
||||||
""")
|
|
||||||
deleted_relations = cursor.fetchone()['count']
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": True,
|
|
||||||
"deleted_relations": deleted_relations,
|
|
||||||
"message": f"将删除 {deleted_relations} 条关系"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
cursor.execute("""
|
|
||||||
DELETE FROM relations
|
|
||||||
WHERE status = 'deleted'
|
|
||||||
AND updated_at < datetime('now', '-90 days')
|
|
||||||
""")
|
|
||||||
deleted = cursor.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": False,
|
|
||||||
"deleted": deleted,
|
|
||||||
"message": f"删除了 {deleted} 条关系"
|
|
||||||
}
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭数据库连接"""
|
|
||||||
if self.conn:
|
|
||||||
self.conn.close()
|
|
||||||
self.conn = None
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
# 兼容性别名
|
|
||||||
Neo4jGraph = EmbeddedGraphDB
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# 测试
|
|
||||||
print("Testing Embedded Graph Database...")
|
|
||||||
|
|
||||||
with EmbeddedGraphDB("test.db") as db:
|
|
||||||
# 写入测试
|
|
||||||
result = db.commit(
|
|
||||||
triplets=[
|
|
||||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
|
||||||
{"subject": "用户", "relation": "学习", "object": "AI"}
|
|
||||||
],
|
|
||||||
session_id="test-session",
|
|
||||||
turn_id=1
|
|
||||||
)
|
|
||||||
print(f"Commit: {result}")
|
|
||||||
|
|
||||||
# 检索测试
|
|
||||||
result = db.recall("Python,AI")
|
|
||||||
print(f"Recall: {result}")
|
|
||||||
|
|
||||||
# 状态测试
|
|
||||||
result = db.introspect()
|
|
||||||
print(f"Introspect: {result}")
|
|
||||||
|
|
||||||
print("\nTest completed!")
|
|
||||||
@ -1,383 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Graph Memory Client - 图记忆客户端核心实现(重构版)
|
|
||||||
使用模块化的工具和提示词系统
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
|
||||||
from openai import OpenAI
|
|
||||||
|
|
||||||
# 导入新的工具和提示词模块
|
|
||||||
from .tools import TOOLS, execute_tool
|
|
||||||
from .prompts import PromptManager
|
|
||||||
|
|
||||||
# 环境配置
|
|
||||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
|
||||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
|
||||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
|
|
||||||
|
|
||||||
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
|
||||||
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
|
|
||||||
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "neo4j")
|
|
||||||
|
|
||||||
# 会话配置
|
|
||||||
CURRENT_SESSION_ID = f"session-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:4]}"
|
|
||||||
CURRENT_TURN = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Neo4jGraph:
|
|
||||||
"""Neo4j图数据库客户端"""
|
|
||||||
|
|
||||||
def __init__(self, uri: str, user: str, password: str):
|
|
||||||
from neo4j import GraphDatabase
|
|
||||||
self.driver = GraphDatabase.driver(uri, auth=(user, password))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.driver.close()
|
|
||||||
|
|
||||||
def ensure_constraints(self):
|
|
||||||
"""确保约束和索引存在"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
# 实体约束
|
|
||||||
session.run("CREATE CONSTRAINT entity_name_constraint IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE")
|
|
||||||
session.run("CREATE CONSTRAINT session_id_constraint IF NOT EXISTS FOR (s:Session) REQUIRE s.session_id IS UNIQUE")
|
|
||||||
|
|
||||||
# 关系索引
|
|
||||||
session.run("CREATE INDEX rel_created_at IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.created_at")
|
|
||||||
session.run("CREATE INDEX rel_session_id IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.session_id")
|
|
||||||
session.run("CREATE INDEX rel_type IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.type")
|
|
||||||
session.run("CREATE INDEX rel_status IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.status")
|
|
||||||
session.run("CREATE INDEX rel_date_bucket IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.date_bucket")
|
|
||||||
|
|
||||||
# 实体索引
|
|
||||||
session.run("CREATE INDEX entity_type IF NOT EXISTS FOR (e:Entity) ON e.type")
|
|
||||||
session.run("CREATE INDEX entity_mention_count IF NOT EXISTS FOR (e:Entity) ON e.mention_count")
|
|
||||||
|
|
||||||
def recall(self, query_intent: str, seed_entities: list = None, depth: int = 2,
|
|
||||||
time_range: dict = None, session_filter: str = None) -> dict:
|
|
||||||
"""检索记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
# 支持逗号分隔的多个关键词
|
|
||||||
keywords = [w.strip() for w in query_intent.replace(',', ' ').split() if len(w.strip()) > 0]
|
|
||||||
|
|
||||||
if not keywords and not seed_entities:
|
|
||||||
return {"entities": [], "relations": [], "message": "无查询关键词"}
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
cond_parts = ["r.status = 'active'"]
|
|
||||||
|
|
||||||
if session_filter:
|
|
||||||
cond_parts.append("r.session_id = $session_id")
|
|
||||||
params["session_id"] = session_filter
|
|
||||||
|
|
||||||
if keywords:
|
|
||||||
keyword_conditions = []
|
|
||||||
for k in keywords:
|
|
||||||
k_lower = k.lower()
|
|
||||||
keyword_conditions.append(f"toLower(e.name) CONTAINS '{k_lower}'")
|
|
||||||
keyword_conditions.append(f"toLower(t.name) CONTAINS '{k_lower}'")
|
|
||||||
keyword_conditions.append(f"toLower(r.type) CONTAINS '{k_lower}'")
|
|
||||||
cond_parts.append(f"({' OR '.join(keyword_conditions)})")
|
|
||||||
|
|
||||||
if seed_entities:
|
|
||||||
placeholders = ",".join([f"'{s}'" for s in seed_entities])
|
|
||||||
cond_parts.append(f"(e.name IN [{placeholders}] OR t.name IN [{placeholders}])")
|
|
||||||
|
|
||||||
if time_range and "days" in time_range:
|
|
||||||
cond_parts.append(f"r.created_at >= datetime() - duration('P{time_range['days']}D')")
|
|
||||||
|
|
||||||
where_clause = " AND ".join(cond_parts)
|
|
||||||
|
|
||||||
cypher = f"""
|
|
||||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE {where_clause}
|
|
||||||
RETURN e, r, t
|
|
||||||
ORDER BY r.created_at DESC
|
|
||||||
LIMIT 30
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = session.run(cypher, params)
|
|
||||||
entities, relations = {}, []
|
|
||||||
|
|
||||||
for record in result:
|
|
||||||
e, r, t = record["e"], record["r"], record["t"]
|
|
||||||
if e["name"] not in entities:
|
|
||||||
entities[e["name"]] = {"name": e["name"], "type": e.get("type", "unknown"), "mention_count": e.get("mention_count", 1)}
|
|
||||||
if t["name"] not in entities:
|
|
||||||
entities[t["name"]] = {"name": t["name"], "type": t.get("type", "unknown"), "mention_count": t.get("mention_count", 1)}
|
|
||||||
|
|
||||||
relations.append({
|
|
||||||
"source": e["name"],
|
|
||||||
"target": t["name"],
|
|
||||||
"type": r["type"],
|
|
||||||
"created_at": str(r.get("created_at", "")),
|
|
||||||
"session_id": r.get("session_id", ""),
|
|
||||||
"turn_id": r.get("turn_id", 0),
|
|
||||||
"confidence": r.get("confidence", 1.0)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"entities": list(entities.values()), "relations": relations[:20]}
|
|
||||||
|
|
||||||
def commit(self, triplets: list, entity_types: list = None, temporal_tag: str = None) -> dict:
|
|
||||||
"""写入记忆"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
with self.driver.session() as session:
|
|
||||||
valid_triplets = [t for t in triplets if t.get("subject") and t.get("relation") and t.get("object")]
|
|
||||||
|
|
||||||
if not valid_triplets:
|
|
||||||
return {"committed_count": 0, "details": []}
|
|
||||||
|
|
||||||
etype = entity_types[0] if entity_types else "unknown"
|
|
||||||
date_bucket = temporal_tag or datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for triplet in valid_triplets:
|
|
||||||
subject = triplet.get("subject", "").strip()
|
|
||||||
relation = triplet.get("relation", "").strip()
|
|
||||||
obj = triplet.get("object", "").strip()
|
|
||||||
confidence = triplet.get("confidence", 0.9)
|
|
||||||
|
|
||||||
session.run("""
|
|
||||||
MERGE (s:Entity {name: $subject})
|
|
||||||
ON CREATE SET s.type = $type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
|
||||||
ON MATCH SET s.mention_count = coalesce(s.mention_count, 0) + 1, s.updated_at = datetime()
|
|
||||||
|
|
||||||
MERGE (t:Entity {name: $object})
|
|
||||||
ON CREATE SET t.type = $type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
|
||||||
ON MATCH SET t.mention_count = coalesce(t.mention_count, 0) + 1, t.updated_at = datetime()
|
|
||||||
|
|
||||||
CREATE (s)-[r:RELATES {
|
|
||||||
type: $relation,
|
|
||||||
created_at: datetime(),
|
|
||||||
session_id: $session_id,
|
|
||||||
turn_id: $turn_id,
|
|
||||||
role: 'user',
|
|
||||||
status: 'active',
|
|
||||||
confidence: $confidence,
|
|
||||||
date_bucket: $date_bucket
|
|
||||||
}]->(t)
|
|
||||||
""", subject=subject, object=obj, relation=relation, type=etype,
|
|
||||||
session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, confidence=confidence,
|
|
||||||
date_bucket=date_bucket)
|
|
||||||
|
|
||||||
results.append(f"{subject} -[{relation}]-> {obj}")
|
|
||||||
|
|
||||||
return {"committed_count": len(results), "details": results}
|
|
||||||
|
|
||||||
def purge(self, criteria: dict, mode: str = "soft", new_relation: dict = None) -> dict:
|
|
||||||
"""删除记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
subject_pattern = criteria.get("subject_contains", "")
|
|
||||||
rel_type = criteria.get("relation_type", "")
|
|
||||||
target_pattern = criteria.get("target_contains", "")
|
|
||||||
session_id = criteria.get("session_id", CURRENT_SESSION_ID)
|
|
||||||
|
|
||||||
cond_parts = ["r.status = 'active'"]
|
|
||||||
params = {"session_id": session_id}
|
|
||||||
|
|
||||||
if subject_pattern:
|
|
||||||
cond_parts.append("e.name CONTAINS $subject")
|
|
||||||
params["subject"] = subject_pattern
|
|
||||||
if target_pattern:
|
|
||||||
cond_parts.append("t.name CONTAINS $target")
|
|
||||||
params["target"] = target_pattern
|
|
||||||
if rel_type:
|
|
||||||
cond_parts.append("r.type = $rel_type")
|
|
||||||
params["rel_type"] = rel_type
|
|
||||||
|
|
||||||
where_clause = " AND ".join(cond_parts)
|
|
||||||
|
|
||||||
if mode == "supersede" and new_relation:
|
|
||||||
new_rel = new_relation.get("relation", "")
|
|
||||||
new_target = new_relation.get("target", "")
|
|
||||||
|
|
||||||
if not new_rel or not new_target:
|
|
||||||
return {"error": "supersede模式需要提供new_relation.relation和new_relation.target"}
|
|
||||||
|
|
||||||
result = session.run(f"""
|
|
||||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE {where_clause}
|
|
||||||
SET r.status = 'superseded', r.updated_at = datetime()
|
|
||||||
RETURN count(r) as count
|
|
||||||
""", params)
|
|
||||||
|
|
||||||
count = result.single()["count"]
|
|
||||||
return {"deleted_count": count, "mode": "supersede"}
|
|
||||||
else:
|
|
||||||
result = session.run(f"""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE {where_clause}
|
|
||||||
SET r.status = 'deleted', r.updated_at = datetime()
|
|
||||||
RETURN count(r) as deleted
|
|
||||||
""", params)
|
|
||||||
count = result.single()["deleted"]
|
|
||||||
|
|
||||||
return {"deleted_count": count, "mode": "soft"}
|
|
||||||
|
|
||||||
def introspect(self, session_id: str = None) -> dict:
|
|
||||||
"""查看记忆状态"""
|
|
||||||
target_session = session_id or CURRENT_SESSION_ID
|
|
||||||
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result = session.run("""
|
|
||||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
|
||||||
WHERE r.session_id = $session_id AND r.status = 'active'
|
|
||||||
RETURN collect(DISTINCT s.name) as source_entities,
|
|
||||||
collect(DISTINCT t.name) as target_entities,
|
|
||||||
count(r) as rel_count,
|
|
||||||
collect(DISTINCT r.type) as rel_types
|
|
||||||
""", session_id=target_session)
|
|
||||||
record = result.single()
|
|
||||||
|
|
||||||
result2 = session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
RETURN e.name as name, e.mention_count as count, e.type as type
|
|
||||||
ORDER BY e.mention_count DESC
|
|
||||||
LIMIT 10
|
|
||||||
""")
|
|
||||||
hotspots = [(r["name"], r["count"], r["type"]) for r in result2]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"session_id": target_session,
|
|
||||||
"total_turns": CURRENT_TURN,
|
|
||||||
"entities_discussed": list(set((record["source_entities"] or []) + (record["target_entities"] or []))),
|
|
||||||
"relation_count": record["rel_count"] if record else 0,
|
|
||||||
"relation_types": record["rel_types"] if record else [],
|
|
||||||
"memory_hotspots": hotspots
|
|
||||||
}
|
|
||||||
|
|
||||||
def archive(self, days: int = 30) -> dict:
|
|
||||||
"""归档旧记忆"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result = session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'active' AND r.created_at < datetime() - duration('P' + $days + 'D')
|
|
||||||
SET r.status = 'archived', r.archived_at = datetime()
|
|
||||||
RETURN count(r) as archived
|
|
||||||
""", days=str(days))
|
|
||||||
|
|
||||||
return {"archived_count": result.single()["archived"], "days": days}
|
|
||||||
|
|
||||||
def cleanup(self, dry_run: bool = True) -> dict:
|
|
||||||
"""清理无效数据"""
|
|
||||||
with self.driver.session() as session:
|
|
||||||
result1 = session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
|
||||||
RETURN count(r) as to_delete
|
|
||||||
""")
|
|
||||||
deleted_relations = result1.single()["to_delete"]
|
|
||||||
|
|
||||||
result2 = session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
WHERE NOT (e)-[:RELATES]-()
|
|
||||||
RETURN count(e) as orphans
|
|
||||||
""")
|
|
||||||
orphan_nodes = result2.single()["orphans"]
|
|
||||||
|
|
||||||
if not dry_run and deleted_relations > 0:
|
|
||||||
session.run("""
|
|
||||||
MATCH ()-[r:RELATES]->()
|
|
||||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
|
||||||
DELETE r
|
|
||||||
""")
|
|
||||||
|
|
||||||
if not dry_run and orphan_nodes > 0:
|
|
||||||
session.run("""
|
|
||||||
MATCH (e:Entity)
|
|
||||||
WHERE NOT (e)-[:RELATES]-()
|
|
||||||
DELETE e
|
|
||||||
""")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"dry_run": dry_run,
|
|
||||||
"deleted_relations": deleted_relations,
|
|
||||||
"orphan_nodes": orphan_nodes,
|
|
||||||
"action_taken": not dry_run
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GraphMemoryClient:
|
|
||||||
"""图记忆客户端"""
|
|
||||||
|
|
||||||
def __init__(self, api_key: str, base_url: str, graph):
|
|
||||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
||||||
self.graph = graph
|
|
||||||
self.tools = TOOLS
|
|
||||||
|
|
||||||
# 使用新的提示词管理器
|
|
||||||
prompt_manager = PromptManager()
|
|
||||||
self.system_prompt = prompt_manager.get_system_prompt()
|
|
||||||
|
|
||||||
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
|
||||||
"""发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
|
|
||||||
# 添加用户消息
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
# 添加 assistant 消息(包含 tool_calls)
|
|
||||||
if assistant_msg:
|
|
||||||
messages.append(assistant_msg)
|
|
||||||
|
|
||||||
# 添加工具结果
|
|
||||||
if tool_results:
|
|
||||||
messages.extend(tool_results)
|
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
|
||||||
model=MODEL_NAME,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_message_with_history(self, messages_history: list) -> dict:
|
|
||||||
"""使用消息历史发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
# 构建完整消息列表
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
messages.extend(messages_history)
|
|
||||||
|
|
||||||
response = self.client.chat.completions.create(
|
|
||||||
model=MODEL_NAME,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_message_stream(self, user_input: str, tool_results: list = None, assistant_msg: dict = None):
|
|
||||||
"""流式发送消息"""
|
|
||||||
global CURRENT_TURN
|
|
||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
|
||||||
|
|
||||||
# 添加用户消息
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
# 添加 assistant 消息(包含 tool_calls)
|
|
||||||
if assistant_msg:
|
|
||||||
messages.append(assistant_msg)
|
|
||||||
|
|
||||||
# 添加工具结果
|
|
||||||
if tool_results:
|
|
||||||
messages.extend(tool_results)
|
|
||||||
|
|
||||||
stream = self.client.chat.completions.create(
|
|
||||||
model=MODEL_NAME,
|
|
||||||
messages=messages,
|
|
||||||
tools=self.tools,
|
|
||||||
tool_choice="auto",
|
|
||||||
stream=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
"""
|
|
||||||
核心逻辑导入 - 优先使用内嵌数据库
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加项目根目录到路径
|
|
||||||
project_root = Path(__file__).parent.parent.parent
|
|
||||||
if str(project_root) not in sys.path:
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
|
||||||
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
|
||||||
MODEL_NAME = os.getenv("MODEL_NAME", "deepseek-chat")
|
|
||||||
|
|
||||||
# 数据库配置
|
|
||||||
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
|
||||||
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "graphmemory123")
|
|
||||||
|
|
||||||
# 优先使用内嵌数据库
|
|
||||||
USE_EMBEDDED_DB = os.getenv("USE_EMBEDDED_DB", "true").lower() == "true"
|
|
||||||
|
|
||||||
if USE_EMBEDDED_DB:
|
|
||||||
# 使用内嵌SQLite数据库
|
|
||||||
from .embedded_db import EmbeddedGraphDB as Neo4jGraph
|
|
||||||
print("[INFO] Using embedded SQLite database (no Docker needed)")
|
|
||||||
else:
|
|
||||||
# 使用Neo4j数据库
|
|
||||||
try:
|
|
||||||
from .graph_client import Neo4jGraph
|
|
||||||
print("[INFO] Using Neo4j database")
|
|
||||||
except ImportError:
|
|
||||||
from .embedded_db import EmbeddedGraphDB as Neo4jGraph
|
|
||||||
print("[INFO] Fallback to embedded SQLite database")
|
|
||||||
|
|
||||||
# 导入其他组件
|
|
||||||
from .graph_client import (
|
|
||||||
GraphMemoryClient,
|
|
||||||
TOOLS,
|
|
||||||
execute_tool,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Neo4jGraph",
|
|
||||||
"GraphMemoryClient",
|
|
||||||
"TOOLS",
|
|
||||||
"execute_tool",
|
|
||||||
"DEEPSEEK_API_KEY",
|
|
||||||
"DEEPSEEK_BASE_URL",
|
|
||||||
"MODEL_NAME",
|
|
||||||
"NEO4J_URI",
|
|
||||||
"NEO4J_USER",
|
|
||||||
"NEO4J_PASSWORD",
|
|
||||||
]
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
提示词管理模块
|
|
||||||
"""
|
|
||||||
from .prompt_manager import PromptManager
|
|
||||||
|
|
||||||
__all__ = ["PromptManager"]
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
"""
|
|
||||||
提示词管理器
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class PromptManager:
|
|
||||||
"""提示词管理器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.prompts_dir = Path(__file__).parent / "templates"
|
|
||||||
|
|
||||||
def get_system_prompt(self) -> str:
|
|
||||||
"""获取系统提示词"""
|
|
||||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
|
||||||
return f.read()
|
|
||||||
else:
|
|
||||||
return self._build_default_prompt()
|
|
||||||
|
|
||||||
def _build_default_prompt(self) -> str:
|
|
||||||
"""构建默认提示词(精简版)"""
|
|
||||||
return """你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
|
||||||
|
|
||||||
## 核心能力
|
|
||||||
|
|
||||||
1. **长期记忆** - 基于图数据库存储实体关系
|
|
||||||
2. **人设管理** - 支持角色扮演和性格设定
|
|
||||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
|
||||||
|
|
||||||
## 记忆原则
|
|
||||||
|
|
||||||
- **明确内容必须写入** - 用户明确提到的信息必须存储
|
|
||||||
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
|
|
||||||
- **图数据库是唯一记忆源** - 没有其他记忆方式
|
|
||||||
|
|
||||||
## 工具使用
|
|
||||||
|
|
||||||
### 记忆工具
|
|
||||||
- `memory_recall` - 检索记忆
|
|
||||||
- `memory_commit` - 写入记忆
|
|
||||||
- `memory_purge` - 删除记忆
|
|
||||||
- `memory_introspect` - 查看状态
|
|
||||||
|
|
||||||
### 人设工具
|
|
||||||
- `persona_update` - 更新人设
|
|
||||||
- `persona_clear` - 清除人设
|
|
||||||
|
|
||||||
### 任务工具
|
|
||||||
- `task_create` - 创建任务
|
|
||||||
- `task_set_state` - 设置状态
|
|
||||||
- `task_delete` - 删除任务
|
|
||||||
- `task_link_info` - 关联信息
|
|
||||||
|
|
||||||
## 自主性
|
|
||||||
|
|
||||||
你有权根据对话上下文自主决定:
|
|
||||||
- 是否需要查询记忆
|
|
||||||
- 是否需要写入记忆
|
|
||||||
- 是否需要维护任务链
|
|
||||||
- 如何使用工具
|
|
||||||
|
|
||||||
记住:灵活应对,保持自然对话体验。"""
|
|
||||||
@ -1,324 +0,0 @@
|
|||||||
# TrulyMEM 系统提示词
|
|
||||||
|
|
||||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
|
||||||
|
|
||||||
## ⚠️ 最高优先级:只回复一次
|
|
||||||
|
|
||||||
**每轮对话只能回复一次!**
|
|
||||||
|
|
||||||
- 执行完所有工具调用后,给出一个完整的回复
|
|
||||||
- 不要在工具调用过程中多次回复
|
|
||||||
- 不要重复说相同的内容
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 关键约束:无传统上下文系统
|
|
||||||
|
|
||||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
|
||||||
|
|
||||||
- ❌ **没有** messages数组存储历史对话
|
|
||||||
- ❌ **没有** 传统的多轮对话上下文
|
|
||||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
|
||||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
|
||||||
|
|
||||||
## 核心身份
|
|
||||||
|
|
||||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
|
||||||
- **能力**: 基于图数据库的长期记忆
|
|
||||||
- **理念**: 让AI的记忆方式更像人类
|
|
||||||
|
|
||||||
## 核心能力
|
|
||||||
|
|
||||||
### 1. 长期记忆
|
|
||||||
- 图数据库存储实体关系
|
|
||||||
- 支持时间范围查询
|
|
||||||
- 支持会话过滤
|
|
||||||
|
|
||||||
### 2. 人设管理(关键)
|
|
||||||
- 角色扮演支持
|
|
||||||
- 性格、语气设定
|
|
||||||
- 动态切换人设
|
|
||||||
- **每轮必须查询人设图**
|
|
||||||
|
|
||||||
### 3. 任务跟踪(关键)
|
|
||||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
|
||||||
- 任务状态管理
|
|
||||||
- 上下文恢复
|
|
||||||
|
|
||||||
## 记忆原则
|
|
||||||
|
|
||||||
### 必须写入的情况
|
|
||||||
- 用户明确表达偏好:"我喜欢X"
|
|
||||||
- 用户分享信息:"我在做X项目"
|
|
||||||
- 用户制定计划:"我打算X"
|
|
||||||
- 用户描述状态:"我现在在X"
|
|
||||||
|
|
||||||
### 禁止写入的情况
|
|
||||||
- AI推断的用户偏好
|
|
||||||
- AI猜测的用户意图
|
|
||||||
- AI推导的结论
|
|
||||||
|
|
||||||
### 标注规则
|
|
||||||
- 推理内容必须标注 **[猜测]**
|
|
||||||
- 明确内容直接陈述
|
|
||||||
|
|
||||||
## 工具系统
|
|
||||||
|
|
||||||
### 记忆工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
|
||||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
|
||||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
|
||||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
|
||||||
|
|
||||||
### 人设工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
|
||||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
|
||||||
|
|
||||||
### 任务工具
|
|
||||||
| 工具 | 功能 | 使用场景 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
|
||||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
|
||||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
|
||||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
|
||||||
|
|
||||||
## 每轮对话强制要求
|
|
||||||
|
|
||||||
### ⚠️ 执行顺序(每轮必须)
|
|
||||||
|
|
||||||
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
|
|
||||||
|
|
||||||
#### 步骤1: 查询人设图(最高优先级)
|
|
||||||
```
|
|
||||||
必须调用: memory_recall
|
|
||||||
参数: {
|
|
||||||
"query_intent": "AI,人设,角色,性格,语气,说话风格",
|
|
||||||
"depth": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**目的**: 获取当前人设,确保角色一致性。
|
|
||||||
**处理**:
|
|
||||||
- 找到人设 → 严格按照人设回复
|
|
||||||
- 未找到 → 使用默认TrulyMEM身份
|
|
||||||
|
|
||||||
#### 步骤2: 查询工作记忆链
|
|
||||||
```
|
|
||||||
必须调用: memory_recall
|
|
||||||
参数: {
|
|
||||||
"query_intent": "TaskNode,工作记忆,任务链",
|
|
||||||
"depth": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**目的**: 获取之前的任务上下文,了解对话历史。
|
|
||||||
|
|
||||||
#### 步骤3: 处理对话
|
|
||||||
- 理解用户意图
|
|
||||||
- 根据人设和工作记忆链生成回复
|
|
||||||
- 执行其他必要的记忆操作
|
|
||||||
|
|
||||||
#### 步骤4: 更新工作记忆链
|
|
||||||
```
|
|
||||||
必须调用: task_create
|
|
||||||
参数: {
|
|
||||||
"task_id": "Task_当前轮次ID",
|
|
||||||
"description": "本轮对话概述",
|
|
||||||
"info_nodes": ["相关记忆节点"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**目的**: 记录本轮对话,维持时间链。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 人设图机制
|
|
||||||
|
|
||||||
### 强制查询
|
|
||||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
|
||||||
|
|
||||||
### 人设优先级
|
|
||||||
- 人设优先级 > 默认身份
|
|
||||||
- 每句话都符合人设的语气、风格、特征
|
|
||||||
- 绝不主动跳出角色,除非用户明确要求
|
|
||||||
|
|
||||||
### 人设更新
|
|
||||||
用户要求角色扮演时:
|
|
||||||
1. 使用 `persona_update` 更新人设
|
|
||||||
2. 立即按照新人设回复
|
|
||||||
|
|
||||||
### 人设清除
|
|
||||||
用户要求恢复默认身份时:
|
|
||||||
1. 使用 `persona_clear` 清除人设
|
|
||||||
2. 恢复为TrulyMEM默认身份
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 工作记忆链机制
|
|
||||||
|
|
||||||
### ⚠️ 核心理念:维持对话连贯性
|
|
||||||
|
|
||||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
|
||||||
|
|
||||||
### 强制查询场景:
|
|
||||||
|
|
||||||
以下情况**必须**查询工作记忆链:
|
|
||||||
|
|
||||||
1. **每轮对话开始时(强制第二步)**
|
|
||||||
- 查询意图: "TaskNode,工作记忆,任务链"
|
|
||||||
- 目的: 获取之前的任务上下文,了解对话历史
|
|
||||||
|
|
||||||
2. **用户提到"刚才"、"之前"、"上次"**
|
|
||||||
- 例: "刚才我们聊了什么?"
|
|
||||||
- 例: "继续刚才的话题"
|
|
||||||
- 例: "关于刚才的成语接龙..."
|
|
||||||
|
|
||||||
3. **用户询问对话历史**
|
|
||||||
- 例: "我们之前说了什么?"
|
|
||||||
- 例: "我们聊过X吗?"
|
|
||||||
|
|
||||||
4. **连续性任务被打断后恢复**
|
|
||||||
- 例: 用户突然回到之前的话题
|
|
||||||
- 例: 用户要求继续之前的任务
|
|
||||||
|
|
||||||
5. **涉及上下文的引用**
|
|
||||||
- 例: "那个东西"(需要查询上下文)
|
|
||||||
- 例: "继续"(需要查询当前任务)
|
|
||||||
|
|
||||||
### 强制更新场景:
|
|
||||||
|
|
||||||
以下情况**必须**更新工作记忆链:
|
|
||||||
|
|
||||||
1. **每轮对话结束时(强制第四步)**
|
|
||||||
- 创建任务节点记录本轮对话
|
|
||||||
- 目的: 维持时间链,确保对话连贯性
|
|
||||||
|
|
||||||
2. **开始连续性任务时**
|
|
||||||
- 例: 用户发起游戏、项目、学习计划等
|
|
||||||
- 必须创建任务节点并设置状态为"进行中"
|
|
||||||
|
|
||||||
3. **任务状态发生变化时**
|
|
||||||
- 例: 任务完成、暂停、取消
|
|
||||||
- 必须及时更新任务状态
|
|
||||||
|
|
||||||
### 节点类型
|
|
||||||
- **TaskNode** - 任务节点,存储任务概述
|
|
||||||
- **StateNode** - 状态节点,存储任务状态
|
|
||||||
- **InfoNode** - 信息节点,存储具体信息
|
|
||||||
|
|
||||||
### 边类型
|
|
||||||
- **NEXT_TASK** - 时间链,连接任务节点
|
|
||||||
- **HAS_STATE** - 状态,任务指向状态
|
|
||||||
- **CONTAINS_INFO** - 信息,任务指向信息节点
|
|
||||||
|
|
||||||
### 任务状态
|
|
||||||
- 进行中
|
|
||||||
- 已完成
|
|
||||||
- 已暂停
|
|
||||||
- 已取消
|
|
||||||
|
|
||||||
### ⚠️ 完整示例:成语接龙游戏
|
|
||||||
|
|
||||||
#### 第一轮:用户发起游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 记录游戏状态:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
|
||||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 第二轮:话题被打断
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 长门有希
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
|
||||||
3. 使用 task_set_state 暂停任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
|
||||||
4. 使用 task_create 创建新任务:
|
|
||||||
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
|
||||||
5. 回复关于长门有希的内容
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 第三轮:用户要求继续游戏
|
|
||||||
|
|
||||||
```
|
|
||||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设(猫娘)
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
|
||||||
3. 使用 task_set_state 恢复任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
|
||||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
|
||||||
```
|
|
||||||
|
|
||||||
### ⚠️ 关键要点
|
|
||||||
|
|
||||||
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
|
||||||
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
|
|
||||||
3. **任务状态必须及时更新**: 确保状态转换的正确性
|
|
||||||
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
|
|
||||||
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
|
||||||
|
|
||||||
## 自主性原则(在强制要求之外)
|
|
||||||
|
|
||||||
除了工作记忆链的强制要求外,你有权自主决定:
|
|
||||||
|
|
||||||
1. **是否查询其他记忆**
|
|
||||||
- 用户询问历史 → 查询
|
|
||||||
- 涉及之前内容 → 查询
|
|
||||||
- 不确定时 → 可查询
|
|
||||||
|
|
||||||
2. **是否写入其他记忆**
|
|
||||||
- 用户明确提到 → 必须写入
|
|
||||||
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
|
|
||||||
|
|
||||||
3. **如何使用其他工具**
|
|
||||||
- 根据上下文灵活选择
|
|
||||||
- 避免过度使用
|
|
||||||
- 保持自然对话
|
|
||||||
|
|
||||||
**注意**: 工作记忆链的强制要求不受自主性影响。
|
|
||||||
|
|
||||||
## 对话风格
|
|
||||||
|
|
||||||
- 自然、流畅
|
|
||||||
- 避免机械式工具调用
|
|
||||||
- 优先理解用户意图
|
|
||||||
- 适时使用记忆增强体验
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 执行检查清单
|
|
||||||
|
|
||||||
每轮对话必须检查:
|
|
||||||
|
|
||||||
- [ ] 步骤1: 是否查询了人设图?
|
|
||||||
- [ ] 步骤2: 是否查询了工作记忆链?
|
|
||||||
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
|
|
||||||
- [ ] 步骤4: 是否更新了工作记忆链?
|
|
||||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
|
||||||
- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**记住**:
|
|
||||||
1. 图数据库是你记忆的唯一载体
|
|
||||||
2. 人设图确保角色一致性(最高优先级)
|
|
||||||
3. 工作记忆链维持对话连贯性
|
|
||||||
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
工具定义模块
|
|
||||||
"""
|
|
||||||
from .memory_tools import TOOLS
|
|
||||||
from .tool_executor import execute_tool
|
|
||||||
from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
|
||||||
|
|
||||||
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
|
||||||
@ -1,520 +0,0 @@
|
|||||||
"""
|
|
||||||
记忆工具定义 - 优化版
|
|
||||||
精简描述,避免过拟合,保留AI自主性
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 基础记忆工具
|
|
||||||
MEMORY_TOOLS = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_recall",
|
|
||||||
"description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 查询人设图(每轮必须首先执行):
|
|
||||||
{"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
|
||||||
|
|
||||||
2. 查询工作记忆链(每轮必须第二步执行):
|
|
||||||
{"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
|
||||||
|
|
||||||
3. 查询用户偏好:
|
|
||||||
{"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]}
|
|
||||||
|
|
||||||
4. 查询特定主题:
|
|
||||||
{"query_intent": "Python,编程,项目", "seed_entities": ["Python"]}
|
|
||||||
|
|
||||||
5. 查询最近7天的记忆:
|
|
||||||
{"query_intent": "任务,工作", "time_range": {"days": 7}}
|
|
||||||
|
|
||||||
【重要】每轮对话必须按顺序执行:
|
|
||||||
- 步骤1: 查询人设图(最高优先级)
|
|
||||||
- 步骤2: 查询工作记忆链(维持对话连贯性)
|
|
||||||
- 步骤3: 根据需要查询其他记忆""",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"query_intent": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "查询意图,支持逗号分隔多个关键词"
|
|
||||||
},
|
|
||||||
"seed_entities": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "种子实体(可选)"
|
|
||||||
},
|
|
||||||
"depth": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "遍历深度,默认2"
|
|
||||||
},
|
|
||||||
"time_range": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "时间范围(可选)",
|
|
||||||
"properties": {
|
|
||||||
"days": {"type": "integer", "description": "最近N天"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"session_filter": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "会话ID过滤(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["query_intent"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_commit",
|
|
||||||
"description": """写入记忆。将三元组写入图数据库,支持批量写入。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 记录用户偏好:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "用户", "relation": "喜欢", "object": "Python编程", "confidence": 0.9},
|
|
||||||
{"subject": "用户", "relation": "正在学习", "object": "机器学习"}
|
|
||||||
]}
|
|
||||||
|
|
||||||
2. 记录项目信息:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "项目A", "relation": "使用技术", "object": "React"},
|
|
||||||
{"subject": "项目A", "relation": "状态", "object": "开发中"}
|
|
||||||
]}
|
|
||||||
|
|
||||||
3. 记录游戏状态(配合工作记忆链):
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "画龙点睛"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
|
|
||||||
【重要】写入原则:
|
|
||||||
- 用户明确表达的信息 → 必须写入
|
|
||||||
- AI推理得到的信息 → 可以写入,但需标注[推测]
|
|
||||||
- 避免写入冗余或无意义的信息""",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"triplets": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"subject": {"type": "string"},
|
|
||||||
"relation": {"type": "string"},
|
|
||||||
"object": {"type": "string"},
|
|
||||||
"confidence": {"type": "number"}
|
|
||||||
},
|
|
||||||
"required": ["subject", "relation", "object"]
|
|
||||||
},
|
|
||||||
"description": "三元组列表"
|
|
||||||
},
|
|
||||||
"entity_types": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "实体类型(可选)"
|
|
||||||
},
|
|
||||||
"temporal_tag": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "时间标记(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["triplets"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_purge",
|
|
||||||
"description": """删除记忆。支持条件删除和纠错替代。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 软删除特定关系:
|
|
||||||
{"criteria": {"subject_contains": "用户", "relation_type": "喜欢"}, "mode": "soft"}
|
|
||||||
|
|
||||||
2. 纠错替代(修正错误信息):
|
|
||||||
{
|
|
||||||
"criteria": {"subject_contains": "用户", "relation_type": "年龄"},
|
|
||||||
"mode": "supersede",
|
|
||||||
"new_relation": {"relation": "年龄", "target": "25岁"}
|
|
||||||
}
|
|
||||||
|
|
||||||
3. 删除特定会话的记忆:
|
|
||||||
{"criteria": {"session_id": "session_123"}, "mode": "soft"}
|
|
||||||
|
|
||||||
4. 删除旧记忆:
|
|
||||||
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
|
|
||||||
|
|
||||||
【重要】删除原则:
|
|
||||||
- 优先使用 supersede 模式修正错误
|
|
||||||
- 软删除不会物理删除数据
|
|
||||||
- 谨慎使用删除操作""",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"criteria": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"subject_contains": {"type": "string"},
|
|
||||||
"relation_type": {"type": "string"},
|
|
||||||
"target_contains": {"type": "string"},
|
|
||||||
"time_before": {"type": "string"},
|
|
||||||
"session_id": {"type": "string"}
|
|
||||||
},
|
|
||||||
"description": "删除条件"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["soft", "supersede"],
|
|
||||||
"description": "删除模式:soft=逻辑删除, supersede=纠错替代",
|
|
||||||
"default": "soft"
|
|
||||||
},
|
|
||||||
"new_relation": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "新关系(supersede模式)",
|
|
||||||
"properties": {
|
|
||||||
"relation": {"type": "string"},
|
|
||||||
"target": {"type": "string"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["criteria"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_introspect",
|
|
||||||
"description": "查看记忆状态。返回会话统计、实体热点、关系分布。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"session_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "会话ID(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_archive",
|
|
||||||
"description": "归档旧记忆。将N天前的非活跃关系标记为归档状态。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"days": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "归档天数,默认30"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "memory_cleanup",
|
|
||||||
"description": "清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"dry_run": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "仅预览不删除",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 人设图管理工具
|
|
||||||
PERSONA_TOOLS = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "persona_update",
|
|
||||||
"description": """更新人设。修改AI的角色、性格、语气等属性。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 切换为猫娘角色:
|
|
||||||
{"attributes": [
|
|
||||||
{"attribute": "扮演角色", "value": "猫娘"},
|
|
||||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
|
||||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
|
||||||
], "mode": "replace"}
|
|
||||||
|
|
||||||
2. 添加新属性(保留现有属性):
|
|
||||||
{"attributes": [
|
|
||||||
{"attribute": "口头禅", "value": "喵呜~"}
|
|
||||||
], "mode": "merge"}
|
|
||||||
|
|
||||||
3. 设置专业角色:
|
|
||||||
{"attributes": [
|
|
||||||
{"attribute": "扮演角色", "value": "Python专家"},
|
|
||||||
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
|
|
||||||
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
|
|
||||||
], "mode": "replace"}
|
|
||||||
|
|
||||||
【重要】人设更新后:
|
|
||||||
- 立即按照新人设回复
|
|
||||||
- 每句话都符合人设的语气、风格、特征
|
|
||||||
- 绝不主动跳出角色,除非用户明确要求""",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"attributes": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"attribute": {"type": "string", "description": "属性名(如:扮演角色、说话风格、性格特点)"},
|
|
||||||
"value": {"type": "string", "description": "属性值"}
|
|
||||||
},
|
|
||||||
"required": ["attribute", "value"]
|
|
||||||
},
|
|
||||||
"description": "人设属性列表"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["replace", "merge"],
|
|
||||||
"description": "更新模式:replace=替换, merge=合并",
|
|
||||||
"default": "merge"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["attributes"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "persona_clear",
|
|
||||||
"description": "清除人设。删除AI的角色设定,恢复默认身份。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"confirm": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "确认清除",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 工作记忆链管理工具
|
|
||||||
WORKING_MEMORY_TOOLS = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_create",
|
|
||||||
"description": """创建任务节点。用于跟踪连续性任务,维持对话连贯性。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 创建成语接龙游戏任务:
|
|
||||||
{
|
|
||||||
"task_id": "Task_成语接龙",
|
|
||||||
"description": "用户发起成语接龙游戏,当前成语:为所欲为",
|
|
||||||
"info_nodes": ["成语接龙_当前成语"]
|
|
||||||
}
|
|
||||||
|
|
||||||
2. 创建编程学习任务:
|
|
||||||
{
|
|
||||||
"task_id": "Task_Python学习",
|
|
||||||
"description": "用户正在学习Python,当前主题:装饰器",
|
|
||||||
"info_nodes": ["Python学习_当前主题"]
|
|
||||||
}
|
|
||||||
|
|
||||||
3. 创建简单对话任务(每轮必须):
|
|
||||||
{
|
|
||||||
"task_id": "Task_当前轮次",
|
|
||||||
"description": "本轮对话的简要概述"
|
|
||||||
}
|
|
||||||
|
|
||||||
【重要】工作记忆链机制:
|
|
||||||
- 每轮对话结束时必须创建任务节点
|
|
||||||
- 任务节点通过 NEXT_TASK 边形成时间链
|
|
||||||
- 任务节点通过 HAS_STATE 边指向状态节点
|
|
||||||
- 任务节点通过 CONTAINS_INFO 边指向信息节点
|
|
||||||
- info_nodes 参数用于关联具体信息节点
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 记录游戏状态:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
|
||||||
5. 回复: "好的喵!我接:为虎作伥喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID(如:Task_001)"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务概述"
|
|
||||||
},
|
|
||||||
"info_nodes": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "关联的信息节点名称(可选)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "description"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_set_state",
|
|
||||||
"description": """设置任务状态。支持:进行中、已完成、已暂停、已取消。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 标记任务为进行中:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
|
|
||||||
2. 标记任务为已完成:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已完成"}
|
|
||||||
|
|
||||||
3. 暂停任务(话题被打断时):
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
|
||||||
|
|
||||||
4. 取消任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "已取消"}
|
|
||||||
|
|
||||||
【重要】状态转换场景:
|
|
||||||
- 进行中 → 已暂停: 话题被打断时
|
|
||||||
- 进行中 → 已完成: 任务完成时
|
|
||||||
- 已暂停 → 进行中: 任务恢复时
|
|
||||||
- 进行中 → 已取消: 任务被取消时
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
|
||||||
3. 使用 task_set_state 恢复任务:
|
|
||||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
|
||||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
|
||||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["进行中", "已完成", "已暂停", "已取消"],
|
|
||||||
"description": "任务状态"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "state"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_delete",
|
|
||||||
"description": "删除任务节点。同时删除关联的信息节点。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"delete_info_nodes": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "是否删除关联的信息节点",
|
|
||||||
"default": True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "task_link_info",
|
|
||||||
"description": """关联信息节点。将记忆节点关联到任务节点,用于存储任务的具体信息。
|
|
||||||
|
|
||||||
【使用示例】
|
|
||||||
1. 关联游戏状态到任务:
|
|
||||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]}
|
|
||||||
|
|
||||||
2. 关联学习主题到任务:
|
|
||||||
{"task_id": "Task_Python学习", "info_node_names": ["Python学习_当前主题", "Python学习_学习进度"]}
|
|
||||||
|
|
||||||
3. 关联项目信息到任务:
|
|
||||||
{"task_id": "Task_项目开发", "info_node_names": ["项目A_技术栈", "项目A_当前阶段"]}
|
|
||||||
|
|
||||||
【重要】使用场景:
|
|
||||||
- 先使用 memory_commit 创建信息节点
|
|
||||||
- 再使用 task_link_info 将信息节点关联到任务节点
|
|
||||||
- 信息节点通过 CONTAINS_INFO 边与任务节点连接
|
|
||||||
|
|
||||||
【完整流程示例】
|
|
||||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
|
||||||
|
|
||||||
AI操作步骤:
|
|
||||||
1. 查询人设图 → 获取当前人设
|
|
||||||
2. 查询工作记忆链 → 无进行中任务
|
|
||||||
3. 使用 memory_commit 创建信息节点:
|
|
||||||
{"triplets": [
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
|
||||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
|
||||||
]}
|
|
||||||
4. 使用 task_create 创建任务节点:
|
|
||||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏"}
|
|
||||||
5. 使用 task_link_info 关联信息节点:
|
|
||||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语"]}
|
|
||||||
6. 回复: "好的喵!我接:为虎作伥喵!" """,
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "任务ID"
|
|
||||||
},
|
|
||||||
"info_node_names": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "信息节点名称列表"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "info_node_names"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 所有工具
|
|
||||||
TOOLS = MEMORY_TOOLS + PERSONA_TOOLS + WORKING_MEMORY_TOOLS
|
|
||||||
@ -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()
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Event Handlers for Graph Memory TUI"""
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
"""焦点管理器"""
|
|
||||||
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
|
|
||||||
class FocusHandler:
|
|
||||||
"""焦点管理器"""
|
|
||||||
|
|
||||||
# 焦点循环顺序
|
|
||||||
FOCUS_RING = [
|
|
||||||
"input-textarea", # 左侧输入框
|
|
||||||
"api-key-input", # 右侧配置区 API Key
|
|
||||||
"model-input", # 右侧配置区 Model
|
|
||||||
"base-url-input", # 右侧配置区 Base URL
|
|
||||||
"cypher-textarea", # 右侧 Cypher 查询框
|
|
||||||
]
|
|
||||||
|
|
||||||
# 焦点名称映射
|
|
||||||
FOCUS_NAMES = {
|
|
||||||
"input-textarea": "Input",
|
|
||||||
"api-key-input": "Config-API",
|
|
||||||
"model-input": "Config-Model",
|
|
||||||
"base-url-input": "Config-URL",
|
|
||||||
"cypher-textarea": "Query",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._current_index = 0
|
|
||||||
|
|
||||||
def next_focus(self, app: App) -> None:
|
|
||||||
"""切换到下一个焦点"""
|
|
||||||
self._current_index = (self._current_index + 1) % len(self.FOCUS_RING)
|
|
||||||
widget_id = self.FOCUS_RING[self._current_index]
|
|
||||||
self._focus_widget(app, widget_id)
|
|
||||||
|
|
||||||
def prev_focus(self, app: App) -> None:
|
|
||||||
"""切换到上一个焦点"""
|
|
||||||
self._current_index = (self._current_index - 1) % len(self.FOCUS_RING)
|
|
||||||
widget_id = self.FOCUS_RING[self._current_index]
|
|
||||||
self._focus_widget(app, widget_id)
|
|
||||||
|
|
||||||
def focus_input(self, app: App) -> None:
|
|
||||||
"""聚焦到输入框"""
|
|
||||||
self._current_index = 0
|
|
||||||
self._focus_widget(app, self.FOCUS_RING[0])
|
|
||||||
|
|
||||||
def focus_query(self, app: App) -> None:
|
|
||||||
"""聚焦到查询框"""
|
|
||||||
self._current_index = len(self.FOCUS_RING) - 1
|
|
||||||
self._focus_widget(app, self.FOCUS_RING[-1])
|
|
||||||
|
|
||||||
def get_current_focus_name(self) -> str:
|
|
||||||
"""获取当前焦点名称"""
|
|
||||||
widget_id = self.FOCUS_RING[self._current_index]
|
|
||||||
return self.FOCUS_NAMES.get(widget_id, "Unknown")
|
|
||||||
|
|
||||||
def _focus_widget(self, app: App, widget_id: str) -> None:
|
|
||||||
"""聚焦到指定组件"""
|
|
||||||
try:
|
|
||||||
widget = app.query_one(f"#{widget_id}")
|
|
||||||
widget.focus()
|
|
||||||
except Exception:
|
|
||||||
# 如果找不到组件,回退到输入框
|
|
||||||
self.focus_input(app)
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
"""快捷键处理器"""
|
|
||||||
|
|
||||||
from textual.app import App
|
|
||||||
from textual.message import Message
|
|
||||||
from .focus_handler import FocusHandler
|
|
||||||
|
|
||||||
|
|
||||||
class KeyHandler:
|
|
||||||
"""快捷键处理器"""
|
|
||||||
|
|
||||||
class ShowHelp(Message):
|
|
||||||
"""显示帮助事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ToggleSidebar(Message):
|
|
||||||
"""切换侧边栏事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ToggleToolDetails(Message):
|
|
||||||
"""切换工具详情事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class FocusQuery(Message):
|
|
||||||
"""聚焦查询框事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ClearHistory(Message):
|
|
||||||
"""清屏事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class QuitApp(Message):
|
|
||||||
"""退出应用事件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, focus_handler: FocusHandler):
|
|
||||||
self._focus_handler = focus_handler
|
|
||||||
|
|
||||||
def handle_f1(self, app: App) -> None:
|
|
||||||
"""处理 F1 键 - 显示帮助"""
|
|
||||||
app.post_message(self.ShowHelp())
|
|
||||||
|
|
||||||
def handle_f2(self, app: App) -> None:
|
|
||||||
"""处理 F2 键 - 切换侧边栏"""
|
|
||||||
app.post_message(self.ToggleSidebar())
|
|
||||||
|
|
||||||
def handle_f3(self, app: App) -> None:
|
|
||||||
"""处理 F3 键 - 切换工具详情"""
|
|
||||||
app.post_message(self.ToggleToolDetails())
|
|
||||||
|
|
||||||
def handle_f4(self, app: App) -> None:
|
|
||||||
"""处理 F4 键 - 聚焦查询框"""
|
|
||||||
app.post_message(self.FocusQuery())
|
|
||||||
|
|
||||||
def handle_f5(self, app: App) -> None:
|
|
||||||
"""处理 F5 键 - 清屏"""
|
|
||||||
app.post_message(self.ClearHistory())
|
|
||||||
|
|
||||||
def handle_f6(self, app: App) -> None:
|
|
||||||
"""处理 F6 键 - 退出"""
|
|
||||||
app.post_message(self.QuitApp())
|
|
||||||
|
|
||||||
def handle_tab(self, app: App) -> None:
|
|
||||||
"""处理 Tab 键 - 焦点循环"""
|
|
||||||
self._focus_handler.next_focus(app)
|
|
||||||
|
|
||||||
def handle_shift_tab(self, app: App) -> None:
|
|
||||||
"""处理 Shift+Tab 键 - 反向焦点循环"""
|
|
||||||
self._focus_handler.prev_focus(app)
|
|
||||||
@ -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,32 +0,0 @@
|
|||||||
"""Graph Memory TUI 入口"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加项目根目录到路径
|
|
||||||
project_root = Path(__file__).parent.parent
|
|
||||||
if str(project_root) not in sys.path:
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
from .app import GraphMemoryApp
|
|
||||||
from .models.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主函数"""
|
|
||||||
try:
|
|
||||||
# 不传入配置,让应用自己从配置文件或环境变量加载
|
|
||||||
app = GraphMemoryApp()
|
|
||||||
app.run()
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n应用已退出")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"应用启动失败: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Data Models for Graph Memory TUI"""
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
"""配置数据模型"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AppConfig:
|
|
||||||
"""应用配置"""
|
|
||||||
api_key: str = ""
|
|
||||||
model: str = "deepseek-chat"
|
|
||||||
base_url: str = "https://api.deepseek.com"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_env(cls) -> "AppConfig":
|
|
||||||
"""从环境变量加载配置"""
|
|
||||||
return cls(
|
|
||||||
api_key=os.getenv("DEEPSEEK_API_KEY", ""),
|
|
||||||
model=os.getenv("MODEL_NAME", "deepseek-chat"),
|
|
||||||
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_file(cls, path: Path) -> "AppConfig":
|
|
||||||
"""从文件加载配置"""
|
|
||||||
if not path.exists():
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
api_key=data.get("api_key", ""),
|
|
||||||
model=data.get("model", "deepseek-chat"),
|
|
||||||
base_url=data.get("base_url", "https://api.deepseek.com"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, path: Path) -> None:
|
|
||||||
"""保存配置到文件"""
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(asdict(self), f, indent=2, ensure_ascii=False)
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
"""日志条目数据模型"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LogEntry:
|
|
||||||
"""日志条目"""
|
|
||||||
timestamp: datetime
|
|
||||||
tool_name: str
|
|
||||||
arguments: Dict[str, Any]
|
|
||||||
result: str
|
|
||||||
duration: float
|
|
||||||
|
|
||||||
@property
|
|
||||||
def args_summary(self) -> str:
|
|
||||||
"""参数摘要(截断到50字符)"""
|
|
||||||
args_str = str(self.arguments)
|
|
||||||
if len(args_str) > 50:
|
|
||||||
return args_str[:50] + "..."
|
|
||||||
return args_str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result_summary(self) -> str:
|
|
||||||
"""结果摘要(截断到100字符)"""
|
|
||||||
if len(self.result) > 100:
|
|
||||||
return self.result[:100] + "..."
|
|
||||||
return self.result
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
"""消息数据模型"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, List, Literal, Optional, Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolCall:
|
|
||||||
"""工具调用"""
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
arguments: Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ToolResult:
|
|
||||||
"""工具执行结果"""
|
|
||||||
tool_call_id: str
|
|
||||||
name: str
|
|
||||||
arguments: Dict[str, Any]
|
|
||||||
result: str
|
|
||||||
success: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Message:
|
|
||||||
"""消息"""
|
|
||||||
role: Literal["user", "assistant", "system"]
|
|
||||||
content: str
|
|
||||||
timestamp: datetime = field(default_factory=datetime.now)
|
|
||||||
tool_calls: Optional[List[ToolCall]] = None
|
|
||||||
tool_results: Optional[List[ToolResult]] = None
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Business Services for Graph Memory TUI"""
|
|
||||||
@ -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,46 +0,0 @@
|
|||||||
"""
|
|
||||||
配置管理 - 支持持久化
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from ..models.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
|
||||||
"""配置管理器 - 支持持久化"""
|
|
||||||
|
|
||||||
def __init__(self, config_file: str = "config.json"):
|
|
||||||
self.config_file = Path(config_file)
|
|
||||||
|
|
||||||
def save(self, config: AppConfig) -> None:
|
|
||||||
"""保存配置到文件"""
|
|
||||||
data = {
|
|
||||||
"api_key": config.api_key,
|
|
||||||
"model": config.model,
|
|
||||||
"base_url": config.base_url
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
|
|
||||||
def load(self) -> AppConfig:
|
|
||||||
"""从文件加载配置"""
|
|
||||||
if not self.config_file.exists():
|
|
||||||
return AppConfig()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
return AppConfig(
|
|
||||||
api_key=data.get("api_key", ""),
|
|
||||||
model=data.get("model", "deepseek-chat"),
|
|
||||||
base_url=data.get("base_url", "https://api.deepseek.com")
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
return AppConfig()
|
|
||||||
|
|
||||||
def exists(self) -> bool:
|
|
||||||
"""检查配置文件是否存在"""
|
|
||||||
return self.config_file.exists()
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
"""配置服务"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from ..models.config import AppConfig
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..core.imports import GraphMemoryClient
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigService:
|
|
||||||
"""配置服务"""
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILE = Path.home() / ".graph_memory_tui" / "config.json"
|
|
||||||
|
|
||||||
def __init__(self, config_file: Path | None = None):
|
|
||||||
self._config_file = config_file or self.DEFAULT_CONFIG_FILE
|
|
||||||
self._config = self._load_config()
|
|
||||||
|
|
||||||
def _load_config(self) -> AppConfig:
|
|
||||||
"""加载配置"""
|
|
||||||
# 优先从文件加载
|
|
||||||
if self._config_file.exists():
|
|
||||||
return AppConfig.from_file(self._config_file)
|
|
||||||
|
|
||||||
# 否则从环境变量加载
|
|
||||||
return AppConfig.from_env()
|
|
||||||
|
|
||||||
def get_config(self) -> AppConfig:
|
|
||||||
"""获取当前配置"""
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
def set_config(self, config: AppConfig) -> None:
|
|
||||||
"""设置配置"""
|
|
||||||
self._config = config
|
|
||||||
self._save_config()
|
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
|
||||||
"""保存配置"""
|
|
||||||
self._config.save(self._config_file)
|
|
||||||
|
|
||||||
def apply_to_client(self, client: "GraphMemoryClient") -> None:
|
|
||||||
"""应用配置到 API 客户端"""
|
|
||||||
# 更新客户端配置
|
|
||||||
client.api_key = self._config.api_key
|
|
||||||
client.base_url = self._config.base_url
|
|
||||||
client.model = self._config.model
|
|
||||||
|
|
||||||
def get_config_file(self) -> Path:
|
|
||||||
"""获取配置文件路径"""
|
|
||||||
return self._config_file
|
|
||||||
@ -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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Styles for Graph Memory TUI"""
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/* Global Styles for Graph Memory TUI */
|
|
||||||
|
|
||||||
GraphMemoryApp {
|
|
||||||
background: $surface;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 全局Input样式 - 确保可见 */
|
|
||||||
Input {
|
|
||||||
background: $surface-lighten-1;
|
|
||||||
color: $text;
|
|
||||||
border: solid $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
Input:focus {
|
|
||||||
border: double $accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
LeftPanel {
|
|
||||||
width: 1fr;
|
|
||||||
dock: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
RightPanel {
|
|
||||||
width: 70;
|
|
||||||
dock: right;
|
|
||||||
background: $panel;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
RightPanel ScrollableContainer {
|
|
||||||
height: 1fr;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusBar {
|
|
||||||
dock: bottom;
|
|
||||||
height: 1;
|
|
||||||
background: $primary;
|
|
||||||
color: $text-primary;
|
|
||||||
}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
/* Component Styles for Graph Memory TUI */
|
|
||||||
|
|
||||||
/* Input Box - 最重要 */
|
|
||||||
InputBox {
|
|
||||||
background: $surface;
|
|
||||||
padding: 1 2;
|
|
||||||
height: auto;
|
|
||||||
border: solid $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputBox Input {
|
|
||||||
width: 100%;
|
|
||||||
background: $surface-lighten-1;
|
|
||||||
color: $text;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Config Section */
|
|
||||||
ConfigSection {
|
|
||||||
background: $surface;
|
|
||||||
padding: 1;
|
|
||||||
margin: 0 0 1 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigSection .config-title {
|
|
||||||
color: $primary;
|
|
||||||
text-style: bold;
|
|
||||||
margin: 0 0 1 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigSection .config-label {
|
|
||||||
color: $text;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigSection .config-hint {
|
|
||||||
color: $text-muted;
|
|
||||||
text-style: italic;
|
|
||||||
margin: 1 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigSection Input {
|
|
||||||
width: 1fr;
|
|
||||||
height: 3;
|
|
||||||
margin: 0 0 1 0;
|
|
||||||
padding: 0 1;
|
|
||||||
background: $surface-lighten-1;
|
|
||||||
border: solid $primary;
|
|
||||||
color: $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Other Components */
|
|
||||||
OperationLog {
|
|
||||||
background: $surface-darken-1;
|
|
||||||
height: 1fr;
|
|
||||||
margin: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
OperationLog .log-entry {
|
|
||||||
color: $text;
|
|
||||||
margin: 0 0 1 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
OperationLog .log-empty {
|
|
||||||
color: $text-muted;
|
|
||||||
text-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
CypherQueryBox {
|
|
||||||
border: solid green;
|
|
||||||
margin: 1;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageHistory {
|
|
||||||
height: 1fr;
|
|
||||||
margin: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Message Widget */
|
|
||||||
MessageWidget {
|
|
||||||
margin: 1 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .message-header {
|
|
||||||
color: $text-muted;
|
|
||||||
text-style: bold;
|
|
||||||
margin: 0 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .message-content {
|
|
||||||
color: $text;
|
|
||||||
margin: 0 0 0 2;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .tool-indicator {
|
|
||||||
color: $warning;
|
|
||||||
text-style: bold;
|
|
||||||
margin: 1 0 0 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .tool-details {
|
|
||||||
background: $surface-darken-1;
|
|
||||||
margin: 1 0 0 2;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .tool-name {
|
|
||||||
color: $accent;
|
|
||||||
text-style: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .tool-args {
|
|
||||||
color: $text-muted;
|
|
||||||
margin: 0 0 0 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageWidget .tool-result {
|
|
||||||
color: $success;
|
|
||||||
margin: 0 0 0 2;
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
/* Message Styles for Graph Memory TUI */
|
|
||||||
|
|
||||||
UserMessage {
|
|
||||||
border: solid orange;
|
|
||||||
margin: 1 0;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelMessage {
|
|
||||||
border: solid blue;
|
|
||||||
margin: 1 0;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolCallIndicator {
|
|
||||||
color: yellow;
|
|
||||||
text-style: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolCallDetails {
|
|
||||||
background: $surface-darken-1;
|
|
||||||
margin: 1 0 0 2;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""UI Widgets for Graph Memory TUI"""
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
"""配置区组件"""
|
|
||||||
|
|
||||||
from textual.containers import Vertical
|
|
||||||
from textual.widgets import Static, Input, Collapsible
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from textual.message import Message
|
|
||||||
from ..models.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigSection(Vertical):
|
|
||||||
"""可折叠配置区"""
|
|
||||||
|
|
||||||
class ConfigChanged(Message):
|
|
||||||
"""配置变更事件"""
|
|
||||||
def __init__(self, config: AppConfig) -> None:
|
|
||||||
self.config = config
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def __init__(self, config: AppConfig | None = None, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._config = config or AppConfig()
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
"""构建配置区"""
|
|
||||||
# 直接显示配置,不使用Collapsible
|
|
||||||
title = Static("━━ 配置 ━━", classes="config-title")
|
|
||||||
title.can_focus = False
|
|
||||||
yield title
|
|
||||||
|
|
||||||
label1 = Static("API Key:", classes="config-label")
|
|
||||||
label1.can_focus = False
|
|
||||||
yield label1
|
|
||||||
|
|
||||||
yield Input(
|
|
||||||
value=self._config.api_key,
|
|
||||||
placeholder="sk-xxxxxxxxxxxxx",
|
|
||||||
id="api-key-input",
|
|
||||||
password=True
|
|
||||||
)
|
|
||||||
|
|
||||||
label2 = Static("模型:", classes="config-label")
|
|
||||||
label2.can_focus = False
|
|
||||||
yield label2
|
|
||||||
|
|
||||||
yield Input(
|
|
||||||
value=self._config.model,
|
|
||||||
placeholder="deepseek-chat",
|
|
||||||
id="model-input"
|
|
||||||
)
|
|
||||||
|
|
||||||
label3 = Static("Base URL:", classes="config-label")
|
|
||||||
label3.can_focus = False
|
|
||||||
yield label3
|
|
||||||
|
|
||||||
yield Input(
|
|
||||||
value=self._config.base_url,
|
|
||||||
placeholder="https://api.deepseek.com",
|
|
||||||
id="base-url-input"
|
|
||||||
)
|
|
||||||
|
|
||||||
hint = Static("按Enter保存配置", classes="config-hint")
|
|
||||||
hint.can_focus = False
|
|
||||||
yield hint
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
|
||||||
"""组件挂载时设置Tab顺序并加载配置"""
|
|
||||||
try:
|
|
||||||
api_key = self.query_one("#api-key-input", Input)
|
|
||||||
model = self.query_one("#model-input", Input)
|
|
||||||
base_url = self.query_one("#base-url-input", Input)
|
|
||||||
|
|
||||||
# 设置Tab索引
|
|
||||||
api_key.tab_index = 0
|
|
||||||
model.tab_index = 1
|
|
||||||
base_url.tab_index = 2
|
|
||||||
|
|
||||||
# 如果配置有值,更新输入框
|
|
||||||
if self._config.api_key:
|
|
||||||
api_key.value = self._config.api_key
|
|
||||||
if self._config.model:
|
|
||||||
model.value = self._config.model
|
|
||||||
if self._config.base_url:
|
|
||||||
base_url.value = self._config.base_url
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_input_changed(self, event: Input.Changed) -> None:
|
|
||||||
"""处理输入变更事件"""
|
|
||||||
# 防抖:只在用户停止输入时更新
|
|
||||||
pass # 不在输入时实时更新,避免卡顿
|
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
"""处理输入提交事件(按Enter或Tab)"""
|
|
||||||
# 只在提交时更新配置
|
|
||||||
try:
|
|
||||||
api_key_input = self.query_one("#api-key-input", Input)
|
|
||||||
model_input = self.query_one("#model-input", Input)
|
|
||||||
base_url_input = self.query_one("#base-url-input", Input)
|
|
||||||
|
|
||||||
# 更新配置
|
|
||||||
self._config = AppConfig(
|
|
||||||
api_key=api_key_input.value,
|
|
||||||
model=model_input.value,
|
|
||||||
base_url=base_url_input.value
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发送配置变更事件
|
|
||||||
self.post_message(self.ConfigChanged(self._config))
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_config(self) -> AppConfig:
|
|
||||||
"""获取当前配置"""
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
def set_config(self, config: AppConfig) -> None:
|
|
||||||
"""设置配置"""
|
|
||||||
self._config = config
|
|
||||||
try:
|
|
||||||
api_key_input = self.query_one("#api-key-input", Input)
|
|
||||||
model_input = self.query_one("#model-input", Input)
|
|
||||||
base_url_input = self.query_one("#base-url-input", Input)
|
|
||||||
|
|
||||||
api_key_input.value = config.api_key
|
|
||||||
model_input.value = config.model
|
|
||||||
base_url_input.value = config.base_url
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
"""Cypher查询框组件"""
|
|
||||||
|
|
||||||
from textual.containers import Container, Horizontal
|
|
||||||
from textual.widgets import Static, TextArea, Button
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from textual.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class CypherQueryBox(Container):
|
|
||||||
"""快捷Cypher查询输入框"""
|
|
||||||
|
|
||||||
class ExecuteQuery(Message):
|
|
||||||
"""执行查询事件"""
|
|
||||||
def __init__(self, query: str) -> None:
|
|
||||||
self.query = query
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
"""构建查询框"""
|
|
||||||
yield Static("F4:执行Cypher查询", classes="query-title")
|
|
||||||
yield TextArea(
|
|
||||||
placeholder="输入Cypher查询语句...",
|
|
||||||
id="cypher-textarea"
|
|
||||||
)
|
|
||||||
with Horizontal(classes="query-buttons"):
|
|
||||||
yield Button("执行", id="execute-button", variant="primary")
|
|
||||||
yield Button("清空", id="clear-button")
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
"""处理按钮点击"""
|
|
||||||
if event.button.id == "execute-button":
|
|
||||||
self._execute_query()
|
|
||||||
elif event.button.id == "clear-button":
|
|
||||||
self._clear_query()
|
|
||||||
|
|
||||||
def on_key(self, event) -> None:
|
|
||||||
"""处理按键事件"""
|
|
||||||
if event.key == "enter" and event.ctrl:
|
|
||||||
event.stop()
|
|
||||||
self._execute_query()
|
|
||||||
|
|
||||||
def _execute_query(self) -> None:
|
|
||||||
"""执行查询"""
|
|
||||||
textarea = self.query_one("#cypher-textarea", TextArea)
|
|
||||||
query = textarea.text.strip()
|
|
||||||
if query:
|
|
||||||
self.post_message(self.ExecuteQuery(query))
|
|
||||||
|
|
||||||
def _clear_query(self) -> None:
|
|
||||||
"""清空查询"""
|
|
||||||
textarea = self.query_one("#cypher-textarea", TextArea)
|
|
||||||
textarea.clear()
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
|
||||||
"""聚焦查询框"""
|
|
||||||
textarea = self.query_one("#cypher-textarea", TextArea)
|
|
||||||
textarea.focus()
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
"""输入框组件"""
|
|
||||||
|
|
||||||
from textual.containers import Container
|
|
||||||
from textual.widgets import Input
|
|
||||||
from textual.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class InputBox(Container):
|
|
||||||
"""输入框组件"""
|
|
||||||
|
|
||||||
class SendMessage(Message):
|
|
||||||
"""发送消息事件"""
|
|
||||||
def __init__(self, content: str) -> None:
|
|
||||||
self.content = content
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._history: list[str] = []
|
|
||||||
self._history_index: int = -1
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
"""构建输入框"""
|
|
||||||
yield Input(
|
|
||||||
placeholder="输入消息... (Enter发送)",
|
|
||||||
id="input-textarea"
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
|
||||||
"""组件挂载时"""
|
|
||||||
# 设置焦点
|
|
||||||
input_widget = self.query_one(Input)
|
|
||||||
input_widget.focus()
|
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
"""处理输入提交事件"""
|
|
||||||
content = event.value.strip()
|
|
||||||
if content:
|
|
||||||
# 保存到历史
|
|
||||||
self._history.append(content)
|
|
||||||
self._history_index = len(self._history)
|
|
||||||
# 发送消息
|
|
||||||
self.post_message(self.SendMessage(content))
|
|
||||||
# 清空输入框
|
|
||||||
event.input.value = ""
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
|
||||||
"""聚焦输入框"""
|
|
||||||
input_widget = self.query_one(Input)
|
|
||||||
input_widget.focus()
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
"""左侧面板"""
|
|
||||||
|
|
||||||
from textual.containers import Container
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from .message_history import MessageHistory
|
|
||||||
from .input_box import InputBox
|
|
||||||
|
|
||||||
|
|
||||||
class LeftPanel(Container):
|
|
||||||
"""左侧主面板"""
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
"""构建左侧面板"""
|
|
||||||
yield MessageHistory()
|
|
||||||
yield InputBox()
|
|
||||||
|
|
||||||
def get_message_history(self) -> MessageHistory:
|
|
||||||
"""获取消息历史组件"""
|
|
||||||
return self.query_one(MessageHistory)
|
|
||||||
|
|
||||||
def get_input_box(self) -> InputBox:
|
|
||||||
"""获取输入框组件"""
|
|
||||||
return self.query_one(InputBox)
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
"""消息历史组件"""
|
|
||||||
|
|
||||||
from textual.containers import ScrollableContainer
|
|
||||||
from textual.message import Message
|
|
||||||
from .message_widget import MessageWidget
|
|
||||||
from ..models.message import Message as MessageModel
|
|
||||||
|
|
||||||
|
|
||||||
class MessageHistory(ScrollableContainer):
|
|
||||||
"""消息历史区域"""
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._messages: list[MessageModel] = []
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
"""构建消息历史"""
|
|
||||||
for message in self._messages:
|
|
||||||
yield MessageWidget(message)
|
|
||||||
|
|
||||||
def add_message(self, message: MessageModel) -> None:
|
|
||||||
"""添加新消息"""
|
|
||||||
self._messages.append(message)
|
|
||||||
# 添加新组件
|
|
||||||
message_widget = MessageWidget(message)
|
|
||||||
self.mount(message_widget)
|
|
||||||
# 滚动到最新消息
|
|
||||||
self.scroll_to_widget(message_widget, animate=False)
|
|
||||||
|
|
||||||
def update_latest_message(self, content: str) -> None:
|
|
||||||
"""更新最新消息的内容"""
|
|
||||||
if self.children:
|
|
||||||
latest_widget = self.children[-1]
|
|
||||||
if isinstance(latest_widget, MessageWidget):
|
|
||||||
latest_widget.update_content(content)
|
|
||||||
# 确保滚动到最新消息
|
|
||||||
self.scroll_to_widget(latest_widget, animate=False)
|
|
||||||
|
|
||||||
def clear_messages(self) -> None:
|
|
||||||
"""清空消息历史"""
|
|
||||||
self._messages.clear()
|
|
||||||
# 移除所有子组件
|
|
||||||
for child in self.children:
|
|
||||||
child.remove()
|
|
||||||
|
|
||||||
def get_latest_message(self) -> MessageModel | None:
|
|
||||||
"""获取最新消息"""
|
|
||||||
if self._messages:
|
|
||||||
return self._messages[-1]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def toggle_latest_tool_details(self) -> None:
|
|
||||||
"""切换最新消息的工具详情"""
|
|
||||||
if self.children:
|
|
||||||
latest_widget = self.children[-1]
|
|
||||||
if isinstance(latest_widget, MessageWidget):
|
|
||||||
latest_widget.toggle_tool_details()
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
"""消息组件"""
|
|
||||||
|
|
||||||
from textual.containers import Container, Vertical
|
|
||||||
from textual.widgets import Static
|
|
||||||
from textual.message import Message
|
|
||||||
from textual.css.query import NoMatches
|
|
||||||
from ..models.message import Message as MessageModel
|
|
||||||
|
|
||||||
|
|
||||||
class MessageWidget(Container):
|
|
||||||
"""单条消息组件"""
|
|
||||||
|
|
||||||
def __init__(self, message: MessageModel, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._message = message
|
|
||||||
self._show_tool_details = False
|
|
||||||
self._content_widget = None # 保存内容组件的引用
|
|
||||||
self._tool_details_container = None # 保存工具详情容器引用
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
"""构建消息组件"""
|
|
||||||
# 消息头
|
|
||||||
role_emoji = "🟠" if self._message.role == "user" else "🔵"
|
|
||||||
timestamp_str = self._message.timestamp.strftime("%H:%M:%S")
|
|
||||||
yield Static(
|
|
||||||
f"{role_emoji} {timestamp_str}",
|
|
||||||
classes="message-header"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 消息内容 - 保存引用以便后续更新
|
|
||||||
self._content_widget = Static(
|
|
||||||
self._message.content,
|
|
||||||
classes="message-content"
|
|
||||||
)
|
|
||||||
yield self._content_widget
|
|
||||||
|
|
||||||
# 工具调用指示器
|
|
||||||
if self._message.tool_calls:
|
|
||||||
tool_count = len(self._message.tool_calls)
|
|
||||||
toggle_hint = "(F3折叠)" if self._show_tool_details else "(F3展开)"
|
|
||||||
yield Static(
|
|
||||||
f"[工具:{tool_count}次] {toggle_hint}",
|
|
||||||
classes="tool-indicator"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 工具调用详情容器 - 始终创建,但根据状态显示/隐藏
|
|
||||||
self._tool_details_container = Vertical(classes="tool-details")
|
|
||||||
with self._tool_details_container:
|
|
||||||
for i, tool_call in enumerate(self._message.tool_calls, 1):
|
|
||||||
yield Static(
|
|
||||||
f"工具 {i}: {tool_call.name}",
|
|
||||||
classes="tool-name"
|
|
||||||
)
|
|
||||||
yield Static(
|
|
||||||
f"参数: {tool_call.arguments}",
|
|
||||||
classes="tool-args"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 显示执行结果
|
|
||||||
if self._message.tool_results:
|
|
||||||
for result in self._message.tool_results:
|
|
||||||
if result.tool_call_id == tool_call.id:
|
|
||||||
# 显示完整结果,不截断
|
|
||||||
result_text = result.result
|
|
||||||
# 如果结果太长,只显示前1000字符,但提供完整信息
|
|
||||||
if len(result_text) > 1000:
|
|
||||||
result_text = result_text[:1000] + f"\n... (共{len(result.result)}字符,按F3查看完整内容)"
|
|
||||||
yield Static(
|
|
||||||
f"结果: {result_text}",
|
|
||||||
classes="tool-result"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 根据状态设置初始显示/隐藏
|
|
||||||
if not self._show_tool_details:
|
|
||||||
self._tool_details_container.styles.display = "none"
|
|
||||||
|
|
||||||
def update_content(self, new_content: str) -> None:
|
|
||||||
"""更新消息内容"""
|
|
||||||
self._message.content = new_content
|
|
||||||
if self._content_widget:
|
|
||||||
self._content_widget.update(new_content)
|
|
||||||
|
|
||||||
def toggle_tool_details(self) -> None:
|
|
||||||
"""切换工具详情显示状态"""
|
|
||||||
if self._message.tool_calls and self._tool_details_container:
|
|
||||||
self._show_tool_details = not self._show_tool_details
|
|
||||||
|
|
||||||
# 切换显示/隐藏
|
|
||||||
if self._show_tool_details:
|
|
||||||
self._tool_details_container.styles.display = "block"
|
|
||||||
else:
|
|
||||||
self._tool_details_container.styles.display = "none"
|
|
||||||
|
|
||||||
# 更新指示器文字
|
|
||||||
self._update_indicator()
|
|
||||||
|
|
||||||
# 刷新布局
|
|
||||||
self.refresh(layout=True)
|
|
||||||
|
|
||||||
def _update_indicator(self) -> None:
|
|
||||||
"""更新工具调用指示器文字"""
|
|
||||||
try:
|
|
||||||
indicator = self.query_one(".tool-indicator", Static)
|
|
||||||
tool_count = len(self._message.tool_calls)
|
|
||||||
toggle_hint = "(F3折叠)" if self._show_tool_details else "(F3展开)"
|
|
||||||
indicator.update(f"[工具:{tool_count}次] {toggle_hint}")
|
|
||||||
except NoMatches:
|
|
||||||
pass
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
"""操作日志组件"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from textual.containers import ScrollableContainer
|
|
||||||
from textual.widgets import Static
|
|
||||||
from ..models.log_entry import LogEntry
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLog(ScrollableContainer):
|
|
||||||
"""图操作日志区域"""
|
|
||||||
|
|
||||||
def __init__(self, max_entries: int = 100, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._logs: list[LogEntry] = []
|
|
||||||
self._max_entries = max_entries
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
"""构建日志区域"""
|
|
||||||
if not self._logs:
|
|
||||||
yield Static("暂无操作日志", classes="log-empty")
|
|
||||||
|
|
||||||
def add_log(self, entry: LogEntry) -> None:
|
|
||||||
"""添加日志(插入到顶部)"""
|
|
||||||
# 限制日志数量
|
|
||||||
if len(self._logs) >= self._max_entries:
|
|
||||||
self._logs.pop()
|
|
||||||
# 移除最旧的组件
|
|
||||||
if self.children:
|
|
||||||
self.children[-1].remove()
|
|
||||||
|
|
||||||
# 插入到列表开头
|
|
||||||
self._logs.insert(0, entry)
|
|
||||||
|
|
||||||
# 创建日志显示组件
|
|
||||||
log_widget = self._create_log_widget(entry)
|
|
||||||
|
|
||||||
# 挂载到顶部
|
|
||||||
self.mount(log_widget, before=0 if self.children else None)
|
|
||||||
|
|
||||||
# 滚动到顶部
|
|
||||||
self.scroll_to(0, animate=False)
|
|
||||||
|
|
||||||
def _create_log_widget(self, entry: LogEntry) -> Static:
|
|
||||||
"""创建日志显示组件"""
|
|
||||||
timestamp_str = entry.timestamp.strftime("%H:%M:%S")
|
|
||||||
text = (
|
|
||||||
f"[{timestamp_str}] {entry.tool_name}\n"
|
|
||||||
f" 参数: {entry.args_summary}\n"
|
|
||||||
f" 结果: {entry.result_summary}\n"
|
|
||||||
f" 耗时: {entry.duration:.2f}s"
|
|
||||||
)
|
|
||||||
return Static(text, classes="log-entry")
|
|
||||||
|
|
||||||
def clear_logs(self) -> None:
|
|
||||||
"""清空日志"""
|
|
||||||
self._logs.clear()
|
|
||||||
for child in self.children:
|
|
||||||
child.remove()
|
|
||||||
# 显示空状态
|
|
||||||
self.mount(Static("暂无操作日志", classes="log-empty"))
|
|
||||||
|
|
||||||
def get_latest_log(self) -> LogEntry | None:
|
|
||||||
"""获取最新日志"""
|
|
||||||
if self._logs:
|
|
||||||
return self._logs[0]
|
|
||||||
return None
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
"""右侧面板"""
|
|
||||||
|
|
||||||
from textual.containers import Container, ScrollableContainer
|
|
||||||
from textual.css.query import NoMatches
|
|
||||||
from textual.widgets import Static
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from .config_section import ConfigSection
|
|
||||||
from .operation_log import OperationLog
|
|
||||||
from .cypher_query_box import CypherQueryBox
|
|
||||||
from ..models.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class RightPanel(Container):
|
|
||||||
"""右侧边栏"""
|
|
||||||
|
|
||||||
def __init__(self, config: AppConfig | None = None, use_embedded_db: bool = True, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._is_collapsed = False
|
|
||||||
self._config = config or AppConfig()
|
|
||||||
self._use_embedded_db = use_embedded_db
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
"""构建右侧面板"""
|
|
||||||
yield Static("F2:隐藏侧边栏", classes="sidebar-title")
|
|
||||||
with ScrollableContainer():
|
|
||||||
yield ConfigSection(self._config)
|
|
||||||
yield OperationLog()
|
|
||||||
if not self._use_embedded_db:
|
|
||||||
yield CypherQueryBox()
|
|
||||||
|
|
||||||
def toggle(self) -> None:
|
|
||||||
"""切换折叠/展开"""
|
|
||||||
self._is_collapsed = not self._is_collapsed
|
|
||||||
if self._is_collapsed:
|
|
||||||
self.styles.width = 0
|
|
||||||
self.styles.display = "none"
|
|
||||||
else:
|
|
||||||
self.styles.width = 70
|
|
||||||
self.styles.display = "block"
|
|
||||||
|
|
||||||
def is_collapsed(self) -> bool:
|
|
||||||
"""检查是否折叠"""
|
|
||||||
return self._is_collapsed
|
|
||||||
|
|
||||||
def get_config_section(self) -> ConfigSection:
|
|
||||||
"""获取配置区组件"""
|
|
||||||
return self.query_one(ConfigSection)
|
|
||||||
|
|
||||||
def get_operation_log(self) -> OperationLog:
|
|
||||||
"""获取操作日志组件"""
|
|
||||||
return self.query_one(OperationLog)
|
|
||||||
|
|
||||||
def get_cypher_query_box(self) -> CypherQueryBox | None:
|
|
||||||
"""获取Cypher查询框组件(可能不存在)"""
|
|
||||||
try:
|
|
||||||
return self.query_one(CypherQueryBox)
|
|
||||||
except NoMatches:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def has_cypher_query_box(self) -> bool:
|
|
||||||
"""检查是否存在Cypher查询框"""
|
|
||||||
return not self._use_embedded_db
|
|
||||||
|
|
||||||
def update_title(self) -> None:
|
|
||||||
"""更新标题"""
|
|
||||||
title = self.query_one(Static)
|
|
||||||
title.update("F2:展开侧边栏" if self._is_collapsed else "F2:隐藏侧边栏")
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
"""状态栏组件"""
|
|
||||||
|
|
||||||
from textual.widgets import Static
|
|
||||||
from textual.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(Static):
|
|
||||||
"""底部状态栏"""
|
|
||||||
|
|
||||||
class FocusChanged(Message):
|
|
||||||
"""焦点变更事件"""
|
|
||||||
def __init__(self, focus_name: str) -> None:
|
|
||||||
self.focus_name = focus_name
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出"
|
|
||||||
self._license_info = "本项目由jianf设计,以GPLv3形式开源"
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
|
||||||
"""组件挂载时"""
|
|
||||||
self._update_display()
|
|
||||||
|
|
||||||
def _update_display(self) -> None:
|
|
||||||
"""更新显示"""
|
|
||||||
self.update(f"{self._license_info} | {self._shortcuts}")
|
|
||||||
Reference in New Issue
Block a user