fix: multiple UI and API issues
- Fix tool calls format for DeepSeek API (add type field) - Fix config event handler name - Fix message history display (preserve all messages) - Fix right panel width and scrolling - Add async config update to prevent UI freeze - Add prompt caching with singleton pattern - Update build scripts with complete hidden imports
This commit is contained in:
@ -1,22 +1,63 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Building macOS binary..."
|
echo "===== Building TrulyMEM for macOS ====="
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "Project root: $PROJECT_ROOT"
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
if ! command -v python3 &> /dev/null; then
|
||||||
echo "Error: python3 not found"
|
echo "Error: python3 not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Installing dependencies..."
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
python3 -m PyInstaller --clean --onefile --console \
|
echo "Cleaning previous builds..."
|
||||||
|
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Running PyInstaller..."
|
||||||
|
python3 -m PyInstaller trulymem_entry.py \
|
||||||
|
--clean \
|
||||||
|
--onefile \
|
||||||
|
--console \
|
||||||
|
--name TrulyMEM \
|
||||||
--add-data "ui/styles:ui/styles" \
|
--add-data "ui/styles:ui/styles" \
|
||||||
--add-data "core/prompts/templates:core/prompts/templates" \
|
--add-data "core/prompts/templates:core/prompts/templates" \
|
||||||
--hidden-import textual \
|
--hidden-import textual \
|
||||||
|
--hidden-import textual.app \
|
||||||
|
--hidden-import textual.widgets \
|
||||||
|
--hidden-import textual.css \
|
||||||
--hidden-import openai \
|
--hidden-import openai \
|
||||||
|
--hidden-import openai._client \
|
||||||
--hidden-import neo4j \
|
--hidden-import neo4j \
|
||||||
|
--hidden-import sqlite3 \
|
||||||
|
--hidden-import core \
|
||||||
|
--hidden-import core.embedded_db \
|
||||||
|
--hidden-import core.graph_client \
|
||||||
|
--hidden-import core.tool_executor \
|
||||||
|
--hidden-import core.tool_limiter \
|
||||||
|
--hidden-import core.tools \
|
||||||
|
--hidden-import core.tools.memory_tools \
|
||||||
|
--hidden-import core.prompts \
|
||||||
|
--hidden-import core.prompts.prompt_manager \
|
||||||
|
--hidden-import ui \
|
||||||
|
--hidden-import ui.app \
|
||||||
|
--hidden-import ui.models \
|
||||||
|
--hidden-import ui.models.message \
|
||||||
|
--hidden-import ui.models.config \
|
||||||
|
--hidden-import ui.models.log_entry \
|
||||||
|
--hidden-import ui.widgets \
|
||||||
|
--hidden-import ui.handlers \
|
||||||
|
--hidden-import ui.services \
|
||||||
|
--hidden-import ui.services.config_manager \
|
||||||
--collect-all textual \
|
--collect-all textual \
|
||||||
trulymem_entry.py
|
--noconfirm
|
||||||
|
|
||||||
echo "Done! Binary: dist/TrulyMEM"
|
echo "===== Build Complete ====="
|
||||||
|
echo "Binary: dist/TrulyMEM"
|
||||||
|
ls -la dist/
|
||||||
@ -1,5 +1,9 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo Building Windows binary...
|
echo ===== Building TrulyMEM for Windows =====
|
||||||
|
|
||||||
|
REM 切换到脚本所在目录的上一级目录(项目根目录)
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
echo Project root: %CD%
|
||||||
|
|
||||||
python --version >nul 2>&1
|
python --version >nul 2>&1
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@ -7,16 +11,48 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo Installing dependencies...
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
python -m PyInstaller --clean --onefile --console ^
|
echo Running PyInstaller...
|
||||||
|
python -m PyInstaller trulymem_entry.py ^
|
||||||
|
--clean ^
|
||||||
|
--onefile ^
|
||||||
|
--console ^
|
||||||
|
--name TrulyMEM ^
|
||||||
--add-data "ui/styles;ui/styles" ^
|
--add-data "ui/styles;ui/styles" ^
|
||||||
--add-data "core/prompts/templates;core/prompts/templates" ^
|
--add-data "core/prompts/templates;core/prompts/templates" ^
|
||||||
--hidden-import textual ^
|
--hidden-import textual ^
|
||||||
|
--hidden-import textual.app ^
|
||||||
|
--hidden-import textual.widgets ^
|
||||||
|
--hidden-import textual.css ^
|
||||||
--hidden-import openai ^
|
--hidden-import openai ^
|
||||||
|
--hidden-import openai._client ^
|
||||||
--hidden-import neo4j ^
|
--hidden-import neo4j ^
|
||||||
|
--hidden-import sqlite3 ^
|
||||||
|
--hidden-import core ^
|
||||||
|
--hidden-import core.embedded_db ^
|
||||||
|
--hidden-import core.graph_client ^
|
||||||
|
--hidden-import core.tool_executor ^
|
||||||
|
--hidden-import core.tool_limiter ^
|
||||||
|
--hidden-import core.tools ^
|
||||||
|
--hidden-import core.tools.memory_tools ^
|
||||||
|
--hidden-import core.prompts ^
|
||||||
|
--hidden-import core.prompts.prompt_manager ^
|
||||||
|
--hidden-import ui ^
|
||||||
|
--hidden-import ui.app ^
|
||||||
|
--hidden-import ui.models ^
|
||||||
|
--hidden-import ui.models.message ^
|
||||||
|
--hidden-import ui.models.config ^
|
||||||
|
--hidden-import ui.models.log_entry ^
|
||||||
|
--hidden-import ui.widgets ^
|
||||||
|
--hidden-import ui.handlers ^
|
||||||
|
--hidden-import ui.services ^
|
||||||
|
--hidden-import ui.services.config_manager ^
|
||||||
--collect-all textual ^
|
--collect-all textual ^
|
||||||
trulymem_entry.py
|
--noconfirm
|
||||||
|
|
||||||
echo Done! Binary: dist\TrulyMEM.exe
|
echo ===== Build Complete =====
|
||||||
|
echo Binary: dist\TrulyMEM.exe
|
||||||
|
dir dist\TrulyMEM.exe
|
||||||
pause
|
pause
|
||||||
@ -96,27 +96,50 @@ class BackendServer:
|
|||||||
messages = [{"role": "user", "content": user_input}]
|
messages = [{"role": "user", "content": user_input}]
|
||||||
response = self._client.send_message_with_history(messages)
|
response = self._client.send_message_with_history(messages)
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
content = message.content or "(无<EFBFBD><EFBFBD>复)"
|
content = message.content or "(无回复)"
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
|
||||||
while message.tool_calls:
|
while message.tool_calls:
|
||||||
args = {}
|
# 处理所有工具调用
|
||||||
try:
|
tool_results = []
|
||||||
import json
|
|
||||||
args = json.loads(message.tool_calls[0].function.arguments)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
allowed, reason = limiter.can_call(message.tool_calls[0].function.name, args)
|
for tool_call in message.tool_calls:
|
||||||
if not allowed:
|
args = {}
|
||||||
tool_calls.append({"name": message.tool_calls[0].function.name, "result": f"工具调用被拒绝: {reason}"})
|
try:
|
||||||
continue
|
import json
|
||||||
|
args = json.loads(tool_call.function.arguments)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
allowed, reason = limiter.can_call(tool_call.function.name, args)
|
||||||
|
if not allowed:
|
||||||
|
tool_calls.append({"name": tool_call.function.name, "result": f"工具调用被拒绝: {reason}"})
|
||||||
|
tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": f"工具调用被拒绝: {reason}"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
limiter.record_call(tool_call.function.name, args)
|
||||||
|
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||||
|
tool_calls.append({"name": tool_call.function.name, "result": result})
|
||||||
|
tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
|
||||||
|
|
||||||
|
# 添加 assistant 消息(包含完整的 tool_calls,必须有 type 字段)
|
||||||
|
assistant_msg = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": message.content or "",
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": tc.id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tc.function.name,
|
||||||
|
"arguments": tc.function.arguments
|
||||||
|
}
|
||||||
|
} for tc in message.tool_calls]
|
||||||
|
}
|
||||||
|
messages.append(assistant_msg)
|
||||||
|
|
||||||
|
# 添加工具结果
|
||||||
|
messages.extend(tool_results)
|
||||||
|
|
||||||
limiter.record_call(message.tool_calls[0].function.name, args)
|
|
||||||
result = execute_tool(self._graph, message.tool_calls[0].function.name, args)
|
|
||||||
tool_calls.append({"name": message.tool_calls[0].function.name, "result": result})
|
|
||||||
messages.append({"role": "assistant", "tool_calls": [{"id": "1", "function": {"name": message.tool_calls[0].function.name, "arguments": message.tool_calls[0].function.arguments}}]})
|
|
||||||
messages.append({"role": "tool", "tool_call_id": "1", "content": result})
|
|
||||||
response = self._client.send_message_with_history(messages)
|
response = self._client.send_message_with_history(messages)
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
content = message.content or content
|
content = message.content or content
|
||||||
|
|||||||
@ -7,17 +7,33 @@ from pathlib import Path
|
|||||||
class PromptManager:
|
class PromptManager:
|
||||||
"""提示词管理器"""
|
"""提示词管理器"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_cached_prompt = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
"""单例模式,避免重复加载"""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.prompts_dir = Path(__file__).parent / "templates"
|
if not hasattr(self, '_initialized'):
|
||||||
|
self.prompts_dir = Path(__file__).parent / "templates"
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
def get_system_prompt(self) -> str:
|
def get_system_prompt(self) -> str:
|
||||||
"""获取系统提示词"""
|
"""获取系统提示词(带缓存)"""
|
||||||
|
if PromptManager._cached_prompt is not None:
|
||||||
|
return PromptManager._cached_prompt
|
||||||
|
|
||||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
prompt_file = self.prompts_dir / "system_prompt.md"
|
||||||
if prompt_file.exists():
|
if prompt_file.exists():
|
||||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||||
return f.read()
|
PromptManager._cached_prompt = f.read()
|
||||||
else:
|
else:
|
||||||
return self._build_default_prompt()
|
PromptManager._cached_prompt = self._build_default_prompt()
|
||||||
|
|
||||||
|
return PromptManager._cached_prompt
|
||||||
|
|
||||||
def _build_default_prompt(self) -> str:
|
def _build_default_prompt(self) -> str:
|
||||||
"""构建默认提示词(精简版)"""
|
"""构建默认提示词(精简版)"""
|
||||||
|
|||||||
78
ui/app.py
78
ui/app.py
@ -26,6 +26,7 @@ class GraphMemoryApp(App):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._backend_server = backend_server
|
self._backend_server = backend_server
|
||||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||||
|
self._api_configured = False
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
from .widgets.left_panel import LeftPanel
|
from .widgets.left_panel import LeftPanel
|
||||||
@ -36,21 +37,26 @@ class GraphMemoryApp(App):
|
|||||||
yield StatusBar()
|
yield StatusBar()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
from .widgets.status_bar import StatusBar
|
||||||
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
if not self._backend_server:
|
if not self._backend_server:
|
||||||
from .widgets.message_history import MessageHistory
|
from .widgets.message_history import MessageHistory
|
||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
error = Message(role="assistant", content="后端未初始化")
|
error = Message(role="assistant", content="后端未初始化")
|
||||||
history.add_message(error)
|
history.add_message(error)
|
||||||
|
status_bar.set_api_status(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
status = self._backend_client.get_status()
|
status = self._backend_client.get_status()
|
||||||
api_configured = status.get("config", {}).get("api_key", "") != ""
|
self._api_configured = status.get("config", {}).get("api_key", "") != ""
|
||||||
|
status_bar.set_api_status(self._api_configured)
|
||||||
|
|
||||||
from .widgets.message_history import MessageHistory
|
from .widgets.message_history import MessageHistory
|
||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
welcome = Message(
|
welcome = Message(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=f"系统就绪\nAPI Key: {'已配置' if api_configured else '未配置'}\n\n输入消息开始对话"
|
content=f"系统就绪\nAPI Key: {'已配置' if self._api_configured else '未配置'}\n\n输入消息开始对话"
|
||||||
)
|
)
|
||||||
history.add_message(welcome)
|
history.add_message(welcome)
|
||||||
|
|
||||||
@ -78,48 +84,82 @@ class GraphMemoryApp(App):
|
|||||||
|
|
||||||
def on_input_box_send_message(self, event) -> None:
|
def on_input_box_send_message(self, event) -> None:
|
||||||
if not self._backend_client:
|
if not self._backend_client:
|
||||||
|
self.notify("后端未初始化", title="错误", severity="error")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._api_configured:
|
||||||
|
self.notify("请先配置 API Key (按 F2 打开侧边栏)", title="提示", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
user_input = event.content
|
user_input = event.content
|
||||||
from .widgets.message_history import MessageHistory
|
from .widgets.message_history import MessageHistory
|
||||||
|
from .widgets.status_bar import StatusBar
|
||||||
|
|
||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
history.add_message(Message(role="user", content=user_input))
|
history.add_message(Message(role="user", content=user_input))
|
||||||
history.add_message(Message(role="assistant", content="处理中..."))
|
history.add_message(Message(role="assistant", content="⏳ 正在处理..."))
|
||||||
|
status_bar.set_processing(True)
|
||||||
|
|
||||||
asyncio.create_task(self._process(user_input))
|
asyncio.create_task(self._process(user_input))
|
||||||
|
|
||||||
async def _process(self, user_input: str) -> None:
|
async def _process(self, user_input: str) -> None:
|
||||||
|
from .widgets.message_history import MessageHistory
|
||||||
|
from .widgets.status_bar import StatusBar
|
||||||
|
|
||||||
|
history = self.query_one(MessageHistory)
|
||||||
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: self._backend_client.send_message(user_input)
|
lambda: self._backend_client.send_message(user_input)
|
||||||
)
|
)
|
||||||
|
|
||||||
from .widgets.message_history import MessageHistory
|
# 更新"处理中"消息为实际回复
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
history.clear_messages()
|
|
||||||
|
|
||||||
history.add_message(Message(role="user", content=user_input))
|
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
content = result.get("content", "(无回复)")
|
content = result.get("content", "(无回复)")
|
||||||
history.add_message(Message(role="assistant", content=content))
|
history.update_latest_message(content)
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "未知错误")
|
error = result.get("error", "未知错误")
|
||||||
history.add_message(Message(role="assistant", content=f"错误: {error}"))
|
history.update_latest_message(f"❌ 错误: {error}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from .widgets.message_history import MessageHistory
|
history.update_latest_message(f"❌ 异常: {str(e)}")
|
||||||
history = self.query_one(MessageHistory)
|
finally:
|
||||||
history.add_message(Message(role="assistant", content=f"错误: {str(e)}"))
|
status_bar.set_processing(False)
|
||||||
|
|
||||||
def on_config_changed(self, event) -> None:
|
def on_config_section_config_changed(self, event) -> None:
|
||||||
|
"""处理配置变更事件"""
|
||||||
if not self._backend_client:
|
if not self._backend_client:
|
||||||
|
self.notify("后端未初始化,无法保存配置", title="错误", severity="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
api_key = event.api_key
|
config = event.config
|
||||||
base_url = event.base_url
|
api_key = config.api_key
|
||||||
|
base_url = config.base_url
|
||||||
|
|
||||||
result = self._backend_client.update_config(api_key=api_key, base_url=base_url)
|
# 异步更新配置,避免阻塞UI
|
||||||
self.notify("配置已保存" if result.get("success") else "配置失败", title="配置")
|
asyncio.create_task(self._update_config_async(api_key, base_url))
|
||||||
|
|
||||||
|
async def _update_config_async(self, api_key: str, base_url: str) -> None:
|
||||||
|
"""异步更新配置"""
|
||||||
|
from .widgets.status_bar import StatusBar
|
||||||
|
status_bar = self.query_one(StatusBar)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: self._backend_client.update_config(api_key=api_key, base_url=base_url)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
self._api_configured = bool(api_key)
|
||||||
|
status_bar.set_api_status(self._api_configured)
|
||||||
|
self.notify("✅ 配置已保存并生效", title="配置成功", severity="information")
|
||||||
|
else:
|
||||||
|
error = result.get("error", "未知错误")
|
||||||
|
self.notify(f"❌ 配置失败: {error}", title="配置失败", severity="error")
|
||||||
|
except Exception as e:
|
||||||
|
self.notify(f"❌ 配置异常: {str(e)}", title="配置失败", severity="error")
|
||||||
@ -22,15 +22,14 @@ LeftPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RightPanel {
|
RightPanel {
|
||||||
width: 70;
|
width: 35;
|
||||||
dock: right;
|
dock: right;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RightPanel ScrollableContainer {
|
RightPanel ScrollableContainer {
|
||||||
height: 1fr;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusBar {
|
StatusBar {
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class RightPanel(Container):
|
|||||||
self.styles.width = 0
|
self.styles.width = 0
|
||||||
self.styles.display = "none"
|
self.styles.display = "none"
|
||||||
else:
|
else:
|
||||||
self.styles.width = 70
|
self.styles.width = 35
|
||||||
self.styles.display = "block"
|
self.styles.display = "block"
|
||||||
|
|
||||||
def is_collapsed(self) -> bool:
|
def is_collapsed(self) -> bool:
|
||||||
|
|||||||
@ -15,8 +15,10 @@ class StatusBar(Static):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出"
|
self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F5:清屏 F6:退出"
|
||||||
self._license_info = "本项目由jianf设计,以GPLv3形式开源"
|
self._license_info = "本项目由jianf设计,以GPLv3形式开源"
|
||||||
|
self._api_status = "未配置"
|
||||||
|
self._processing = False
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""组件挂载时"""
|
"""组件挂载时"""
|
||||||
@ -24,4 +26,16 @@ class StatusBar(Static):
|
|||||||
|
|
||||||
def _update_display(self) -> None:
|
def _update_display(self) -> None:
|
||||||
"""更新显示"""
|
"""更新显示"""
|
||||||
self.update(f"{self._license_info} | {self._shortcuts}")
|
status_icon = "●" if self._api_status == "已配置" else "○"
|
||||||
|
processing_indicator = " [处理中...]" if self._processing else ""
|
||||||
|
self.update(f"{status_icon} API: {self._api_status}{processing_indicator} | {self._license_info} | {self._shortcuts}")
|
||||||
|
|
||||||
|
def set_api_status(self, configured: bool) -> None:
|
||||||
|
"""设置API状态"""
|
||||||
|
self._api_status = "已配置" if configured else "未配置"
|
||||||
|
self._update_display()
|
||||||
|
|
||||||
|
def set_processing(self, processing: bool) -> None:
|
||||||
|
"""设置处理状态"""
|
||||||
|
self._processing = processing
|
||||||
|
self._update_display()
|
||||||
|
|||||||
Reference in New Issue
Block a user