mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-15 12:15:32 +00:00
feat: 实现流式消息显示和界面优化
This commit is contained in:
5
config.json
Normal file
5
config.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"api_key": "sk-7d3e23384c04415b9c828a925ef4ab11",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"base_url": "https://api.deepseek.com"
|
||||||
|
}
|
||||||
@ -490,16 +490,20 @@ def format_recall_result(result: dict) -> str:
|
|||||||
if result.get("entities"):
|
if result.get("entities"):
|
||||||
lines.append(f"\n相关实体 ({len(result['entities'])} 个):")
|
lines.append(f"\n相关实体 ({len(result['entities'])} 个):")
|
||||||
for e in result["entities"]:
|
for e in result["entities"]:
|
||||||
lines.append(f" - {e['name']} (类型: {e.get('type', 'unknown')}, 提及: {e.get('mention_count', 1)}次)")
|
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"):
|
if result.get("relations"):
|
||||||
lines.append(f"\n相关关系 ({len(result['relations'])} 条):")
|
lines.append(f"\n相关关系 ({len(result['relations'])} 条):")
|
||||||
for r in result["relations"]:
|
for r in result["relations"]:
|
||||||
lines.append(f" - {r['source']} --[{r['type']}]--> {r['target']}")
|
if r and isinstance(r, dict):
|
||||||
created = r.get("created_at", "N/A")
|
lines.append(f" - {r.get('source', 'N/A')} --[{r.get('type', 'N/A')}]--> {r.get('target', 'N/A')}")
|
||||||
if created and created != "N/A":
|
created = r.get("created_at", "N/A")
|
||||||
created = created[:19] if "T" in str(created) else str(created)
|
if created and created != "N/A":
|
||||||
lines.append(f" 时间: {created}, 会话: {r.get('session_id', 'N/A')[:20]}, 轮次: {r.get('turn_id', 0)}, 置信度: {r.get('confidence', 1.0)}")
|
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"):
|
if not result.get("entities") and not result.get("relations"):
|
||||||
lines.append("\n(未找到相关记忆)")
|
lines.append("\n(未找到相关记忆)")
|
||||||
@ -517,120 +521,119 @@ class GraphMemoryClient:
|
|||||||
self.system_prompt = self._build_system_prompt()
|
self.system_prompt = self._build_system_prompt()
|
||||||
|
|
||||||
def _build_system_prompt(self) -> str:
|
def _build_system_prompt(self) -> str:
|
||||||
return """你是图数据库记忆助手。
|
return """你是TrulyMEM,TrueHumanMEM项目的AI助手,拥有长期记忆能力。
|
||||||
|
|
||||||
|
## 身份说明
|
||||||
|
- 你的名字是TrulyMEM,是TrueHumanMEM项目的产物
|
||||||
|
- 你拥有基于图数据库的长期记忆能力
|
||||||
|
- 你的设计理念是让AI的记忆方式更像人类
|
||||||
|
|
||||||
## 核心职责
|
## 核心职责
|
||||||
你是用户的长期记忆助手。每次对话后,你**必须**主动决定是否需要将关键信息写入记忆图库。
|
你是一个智能对话助手,**图数据库是你记忆的唯一载体**。你的主要任务是:
|
||||||
|
1. 与用户进行自然、流畅的对话
|
||||||
|
2. 回答问题、提供建议、协助完成任务
|
||||||
|
3. 根据对话内容灵活查询和使用记忆
|
||||||
|
4. 将用户明确提到的信息写入记忆
|
||||||
|
|
||||||
## 多轮查询策略
|
**重要**:
|
||||||
**允许多轮查询**,但必须遵循以下规则:
|
- 图数据库是你记忆的唯一来源,没有其他记忆方式
|
||||||
|
- 优先进行自然对话,根据需要灵活调用记忆工具
|
||||||
|
- 用户明确提到的内容必须写入,推理得到的内容必须标注
|
||||||
|
|
||||||
1. **渐进式查询**:每轮查询应该基于上一轮的结果,缩小或扩大范围
|
## 记忆工具使用原则
|
||||||
- 第一轮:广泛搜索,使用多个同义词
|
|
||||||
- 第二轮:基于第一轮结果,精确搜索
|
|
||||||
- 第三轮:如果仍未找到,尝试相关概念
|
|
||||||
|
|
||||||
2. **禁止重复查询**:
|
### 何时检索记忆 (memory_recall)
|
||||||
- ❌ 禁止:使用相同的 query_intent 连续查询
|
- 用户询问"我们之前聊过X吗"、"你还记得X吗" → 查询X相关内容
|
||||||
- ❌ 禁止:查询后立即用相同关键词再查
|
- 用户询问"我们都聊过什么"、"我们之前说了什么" → **使用空字符串或通配符查询所有记忆**
|
||||||
- ✅ 允许:第一轮查"鸿蒙",第二轮查"鸿蒙,工具链,IDE"
|
- 用户提到某个话题,你想确认是否有相关历史 → 查询该话题
|
||||||
|
- 需要基于历史信息回答问题 → 查询相关信息
|
||||||
|
- 对话中涉及之前可能讨论过的内容 → 查询相关内容
|
||||||
|
|
||||||
3. **查询历史追踪**:
|
**灵活查询**:根据对话上下文,主动判断是否需要查询记忆,不要等待用户明确要求。
|
||||||
- 记住已经查询过的关键词
|
|
||||||
- 每次新查询必须使用不同的关键词组合
|
|
||||||
- 如果3轮查询仍未找到,告知用户"未找到相关记忆"
|
|
||||||
|
|
||||||
## memory_recall 正确用法
|
**重要**:
|
||||||
|
- 当用户问"我们都聊过什么"时,**不要查询"聊天记录"、"对话"等关键词**
|
||||||
|
- 应该使用空字符串 `""` 或通配符 `"*"` 来获取所有记忆内容
|
||||||
|
- 或者使用非常宽泛的关键词如 `"用户,喜欢,项目,学习,研究,计划"`
|
||||||
|
|
||||||
### 关键词提取规则
|
### 何时写入记忆 (memory_commit)
|
||||||
query_intent 应该是**逗号分隔的多个关键词**,包含**同义词/近义词**:
|
**必须写入的情况**(用户明确提到):
|
||||||
|
- 用户表达偏好:"我喜欢X"、"我讨厌X"
|
||||||
|
- 用户分享信息:"我在做X项目"、"我在学X"
|
||||||
|
- 用户制定计划:"我打算X"、"我计划X"
|
||||||
|
- 用户描述状态:"我现在在X"
|
||||||
|
|
||||||
|
**禁止写入的情况**(AI推理得到):
|
||||||
|
- AI推断的用户偏好
|
||||||
|
- AI猜测的用户意图
|
||||||
|
- AI推导的结论
|
||||||
|
|
||||||
|
## memory_recall 使用方法
|
||||||
|
|
||||||
|
### 关键词提取
|
||||||
|
query_intent 使用**逗号分隔的多个关键词**,包含同义词:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"query_intent": "鸿蒙,harmony,工具链,toolchain,开发环境,IDE",
|
"query_intent": "量子力学,quantum,物理,physics",
|
||||||
"depth": 2
|
"depth": 2
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 搜索范围
|
|
||||||
- 实体名称(subject/target)
|
|
||||||
- 关系类型(relation)
|
|
||||||
- 实体类型(entity type)
|
|
||||||
|
|
||||||
### 同义词扩展示例
|
### 同义词扩展示例
|
||||||
- "鸿蒙" → "鸿蒙,harmony,openharmony,华为"
|
- "量子力学" → "量子力学,quantum,quantum mechanics,物理,physics"
|
||||||
- "工具链" → "工具链,toolchain,sdk,开发环境,IDE"
|
|
||||||
- "项目" → "项目,project,工程,工作"
|
- "项目" → "项目,project,工程,工作"
|
||||||
- "学习" → "学习,learn,study,掌握,了解"
|
- "学习" → "学习,learn,study,掌握"
|
||||||
|
|
||||||
## 重要规则:必须写入记忆的情况
|
### 查询规则
|
||||||
当用户提到以下内容时,你**必须**调用 memory_commit 写入记忆:
|
1. 根据对话内容灵活提取关键词
|
||||||
1. 用户的**偏好**("我喜欢X")
|
2. 第一轮使用广泛的关键词搜索
|
||||||
2. 用户的**项目**("我在做X项目")
|
3. 如果未找到,可以尝试相关概念
|
||||||
3. 用户的**学习内容**("我在学Python")
|
4. 最多查询2-3轮,避免重复查询
|
||||||
4. 讨论的**主题**("量子力学")
|
|
||||||
5. 用户的**计划**("我打算X")
|
|
||||||
6. 用户的**状态**("我现在在X")
|
|
||||||
|
|
||||||
## 区分事实与猜测
|
## memory_commit 使用方法
|
||||||
当基于记忆检索结果回复时,**必须**使用"应该"标注你的推理:
|
|
||||||
- ✅ 正确: "根据记忆,你的鸿蒙工具链**应该**在 opt 目录下"
|
|
||||||
- ❌ 错误: "你的鸿蒙工具链在 opt 目录下"(没有标注"应该")
|
|
||||||
|
|
||||||
原因:数据库中的记录可能不完整或已过期,你需要标注这是**推断**而非**确认**的事实
|
使用三元组格式记录信息:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"triplets": [
|
||||||
|
{"subject": "用户", "relation": "对领域感兴趣", "object": "量子力学"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 区分事实与推理(重要!)
|
||||||
|
|
||||||
|
### 用户明确提到的内容
|
||||||
|
直接写入记忆,回复时直接陈述:
|
||||||
|
- 用户:"我喜欢Python" → 写入,回复:"好的,我会记住你喜欢Python"
|
||||||
|
- 用户:"我在学机器学习" → 写入,回复:"明白了,你在学习机器学习"
|
||||||
|
|
||||||
|
### AI推理得到的内容
|
||||||
|
**禁止写入记忆**,回复时必须在开头标注 **[猜测]**:
|
||||||
|
- AI推断用户可能喜欢X → 不写入,回复:"[猜测] 你可能对X感兴趣"
|
||||||
|
- AI推测用户意图 → 不写入,回复:"[猜测] 你可能是想..."
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户:我们聊过量子力学吗?
|
||||||
|
AI检索记忆 → 未找到
|
||||||
|
AI回复:[猜测] 我们应该还没有聊过量子力学,因为记忆中没有相关记录。
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
用户:我最近在研究深度学习
|
||||||
|
AI写入记忆 → {"subject": "用户", "relation": "正在研究", "object": "深度学习"}
|
||||||
|
AI回复:好的,我会记住你最近在研究深度学习。有什么具体问题想讨论吗?
|
||||||
|
```
|
||||||
|
|
||||||
## 可用工具
|
## 可用工具
|
||||||
1. **memory_recall** - 检索历史记忆
|
1. **memory_recall** - 检索历史记忆(灵活使用)
|
||||||
- query_intent: 支持逗号分隔的多关键词
|
2. **memory_commit** - 写入记忆(仅限用户明确提到的内容)
|
||||||
- depth: 搜索深度(1-3)
|
3. **memory_purge** - 删除/修正记忆
|
||||||
- seed_entities: 种子实体(可选)
|
4. **memory_introspect** - 查看记忆状态
|
||||||
- time_range: 时间范围(可选)
|
|
||||||
|
|
||||||
2. **memory_commit** - 写入记忆(三元组格式)
|
现在开始对话!记住:图数据库是你记忆的唯一载体,明确提到的必须写入,推理得到的必须标注[猜测]。"""
|
||||||
- triplets: [{"subject": "A", "relation": "关系", "object": "B"}]
|
|
||||||
- entity_types: 实体类型标注(可选)
|
|
||||||
- temporal_tag: 时间标签(可选)
|
|
||||||
|
|
||||||
3. **memory_purge** - 修正/删除记忆
|
|
||||||
- criteria: 删除条件
|
|
||||||
- mode: "soft" 或 "hard"
|
|
||||||
|
|
||||||
4. **memory_introspect** - 查看会话状态
|
|
||||||
|
|
||||||
## 错误策略(禁止)
|
|
||||||
- ❌ query_intent 使用完整句子
|
|
||||||
- ❌ 连续使用相同的 query_intent 查询
|
|
||||||
- ❌ 查询后立即用相同关键词再查
|
|
||||||
- ❌ 超过3轮查询仍未找到结果时继续查询
|
|
||||||
|
|
||||||
## 查询示例
|
|
||||||
|
|
||||||
### 正确的多轮查询
|
|
||||||
```
|
|
||||||
用户: 我的鸿蒙开发环境在哪?
|
|
||||||
|
|
||||||
第一轮查询:
|
|
||||||
{
|
|
||||||
"query_intent": "鸿蒙,harmony,开发环境,IDE,工具链",
|
|
||||||
"depth": 2
|
|
||||||
}
|
|
||||||
|
|
||||||
如果未找到,第二轮查询:
|
|
||||||
{
|
|
||||||
"query_intent": "鸿蒙,harmony,安装路径,目录,位置",
|
|
||||||
"depth": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
如果仍未找到,告知用户并询问是否需要记录。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误的重复查询
|
|
||||||
```
|
|
||||||
❌ 第一轮: {"query_intent": "鸿蒙"}
|
|
||||||
❌ 第二轮: {"query_intent": "鸿蒙"} // 禁止重复!
|
|
||||||
```
|
|
||||||
|
|
||||||
现在开始对话!"""
|
|
||||||
|
|
||||||
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
||||||
global CURRENT_TURN
|
global CURRENT_TURN
|
||||||
@ -656,7 +659,35 @@ query_intent 应该是**逗号分隔的多个关键词**,包含**同义词/近
|
|||||||
)
|
)
|
||||||
|
|
||||||
return response
|
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}]
|
||||||
|
|
||||||
|
# 添加之前的 assistant 消息(包含 tool_calls)
|
||||||
|
if assistant_msg:
|
||||||
|
messages.append(assistant_msg)
|
||||||
|
|
||||||
|
# 添加之前的工具结果
|
||||||
|
if tool_results:
|
||||||
|
messages.extend(tool_results)
|
||||||
|
|
||||||
|
# 添加当前用户输入
|
||||||
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
# 使用流式传输
|
||||||
|
stream = self.client.chat.completions.create(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
messages=messages,
|
||||||
|
tools=self.tools,
|
||||||
|
tool_choice="auto",
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return stream
|
||||||
|
|
||||||
def chat_loop(self):
|
def chat_loop(self):
|
||||||
global CURRENT_TURN
|
global CURRENT_TURN
|
||||||
|
|
||||||
|
|||||||
@ -72,11 +72,11 @@ class GraphMemoryApp(App[None]):
|
|||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""应用启动初始化"""
|
"""应用启动初始化"""
|
||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 初始化内嵌图数据库
|
# 初始化内嵌图数据库
|
||||||
self._graph = Neo4jGraph(db_path="graph_memory.db")
|
self._graph = Neo4jGraph(db_path="graph_memory.db")
|
||||||
|
|
||||||
# 初始化 API 客户端
|
# 初始化 API 客户端
|
||||||
self._init_client()
|
self._init_client()
|
||||||
|
|
||||||
@ -194,8 +194,8 @@ F6 - 退出
|
|||||||
)
|
)
|
||||||
history.add_message(processing_msg)
|
history.add_message(processing_msg)
|
||||||
|
|
||||||
# 异步处理(使用call_later避免崩溃)
|
# 异步处理
|
||||||
self.call_later(self._process_message_safe, event.content)
|
asyncio.create_task(self._process_message_async(event.content))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 显示错误
|
# 显示错误
|
||||||
@ -206,22 +206,6 @@ F6 - 退出
|
|||||||
)
|
)
|
||||||
history.add_message(error_msg)
|
history.add_message(error_msg)
|
||||||
|
|
||||||
def _process_message_safe(self, user_input: str) -> None:
|
|
||||||
"""安全处理消息"""
|
|
||||||
try:
|
|
||||||
history = self.query_one(MessageHistory)
|
|
||||||
|
|
||||||
# 简单测试响应
|
|
||||||
response_msg = Message(
|
|
||||||
role="assistant",
|
|
||||||
content=f"收到消息: {user_input}\n\n完整功能需要配置API Key并连接API服务。",
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
history.add_message(response_msg)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
async def _process_message_async(self, user_input: str) -> None:
|
async def _process_message_async(self, user_input: str) -> None:
|
||||||
"""异步处理消息 - 参考demo实现"""
|
"""异步处理消息 - 参考demo实现"""
|
||||||
history = self.query_one(MessageHistory)
|
history = self.query_one(MessageHistory)
|
||||||
@ -246,7 +230,9 @@ F6 - 退出
|
|||||||
# 处理工具调用循环
|
# 处理工具调用循环
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
tool_results = []
|
tool_results = []
|
||||||
|
last_assistant_msg = None
|
||||||
|
accumulated_content = "" # 累积所有中间内容
|
||||||
|
|
||||||
# 显示工具调用摘要
|
# 显示工具调用摘要
|
||||||
if message.tool_calls:
|
if message.tool_calls:
|
||||||
tool_summary = f"🔧 正在调用 {len(message.tool_calls)} 个工具..."
|
tool_summary = f"🔧 正在调用 {len(message.tool_calls)} 个工具..."
|
||||||
@ -256,9 +242,30 @@ F6 - 退出
|
|||||||
timestamp=datetime.now()
|
timestamp=datetime.now()
|
||||||
)
|
)
|
||||||
history.add_message(summary_msg)
|
history.add_message(summary_msg)
|
||||||
|
|
||||||
while message.tool_calls:
|
while message.tool_calls:
|
||||||
# 保存工具调用信息
|
# 累积中间内容(如果有)
|
||||||
|
if message.content:
|
||||||
|
accumulated_content += message.content + "\n\n"
|
||||||
|
|
||||||
|
# 保存包含 tool_calls 的 assistant 消息
|
||||||
|
last_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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行当前轮的所有工具调用
|
||||||
|
current_tool_results = []
|
||||||
for tool_call in message.tool_calls:
|
for tool_call in message.tool_calls:
|
||||||
tc = ToolCall(
|
tc = ToolCall(
|
||||||
id=tool_call.id,
|
id=tool_call.id,
|
||||||
@ -266,7 +273,7 @@ F6 - 退出
|
|||||||
arguments=json.loads(tool_call.function.arguments)
|
arguments=json.loads(tool_call.function.arguments)
|
||||||
)
|
)
|
||||||
tool_calls.append(tc)
|
tool_calls.append(tc)
|
||||||
|
|
||||||
# 执行工具
|
# 执行工具
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
@ -274,7 +281,7 @@ F6 - 退出
|
|||||||
lambda: execute_tool(self._graph, tc.name, tc.arguments)
|
lambda: execute_tool(self._graph, tc.name, tc.arguments)
|
||||||
)
|
)
|
||||||
duration = (datetime.now() - start_time).total_seconds()
|
duration = (datetime.now() - start_time).total_seconds()
|
||||||
|
|
||||||
# 保存工具结果
|
# 保存工具结果
|
||||||
tr = ToolResult(
|
tr = ToolResult(
|
||||||
tool_call_id=tc.id,
|
tool_call_id=tc.id,
|
||||||
@ -284,7 +291,14 @@ F6 - 退出
|
|||||||
success=not result.startswith("工具执行错误")
|
success=not result.startswith("工具执行错误")
|
||||||
)
|
)
|
||||||
tool_results.append(tr)
|
tool_results.append(tr)
|
||||||
|
|
||||||
|
# 添加到当前轮结果
|
||||||
|
current_tool_results.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tc.id,
|
||||||
|
"content": result
|
||||||
|
})
|
||||||
|
|
||||||
# 添加日志
|
# 添加日志
|
||||||
log_entry = LogEntry(
|
log_entry = LogEntry(
|
||||||
timestamp=datetime.now(),
|
timestamp=datetime.now(),
|
||||||
@ -294,14 +308,26 @@ F6 - 退出
|
|||||||
duration=duration
|
duration=duration
|
||||||
)
|
)
|
||||||
log.add_log(log_entry)
|
log.add_log(log_entry)
|
||||||
|
|
||||||
# 继续调用API(参考demo的实现)
|
# 继续调用API(参考demo的实现)
|
||||||
# 这里简化处理,实际应该像demo一样继续循环
|
response = await asyncio.get_event_loop().run_in_executor(
|
||||||
break
|
None,
|
||||||
|
lambda: self._client.send_message(
|
||||||
|
user_input,
|
||||||
|
current_tool_results,
|
||||||
|
last_assistant_msg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
message = response.choices[0].message
|
||||||
|
|
||||||
# 添加助手消息(完整内容)
|
# 添加助手消息(完整内容)
|
||||||
content = message.content or "(无回复)"
|
# 使用累积的内容 + 最终内容
|
||||||
|
final_content = message.content or ""
|
||||||
|
content = accumulated_content + final_content if accumulated_content else final_content
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
content = "(无回复)"
|
||||||
|
|
||||||
# 如果有工具调用,添加工具调用摘要
|
# 如果有工具调用,添加工具调用摘要
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
tool_names = [tc.name for tc in tool_calls]
|
tool_names = [tc.name for tc in tool_calls]
|
||||||
@ -314,8 +340,12 @@ F6 - 退出
|
|||||||
tool_calls=tool_calls if tool_calls else None,
|
tool_calls=tool_calls if tool_calls else None,
|
||||||
tool_results=tool_results if tool_results else None
|
tool_results=tool_results if tool_results else None
|
||||||
)
|
)
|
||||||
|
|
||||||
history.add_message(assistant_message)
|
history.add_message(assistant_message)
|
||||||
|
|
||||||
|
# 强制刷新界面
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 显示详细错误
|
# 显示详细错误
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
@ -362,6 +392,13 @@ API Key 未配置!
|
|||||||
# 持久化保存配置
|
# 持久化保存配置
|
||||||
self._config_manager.save(self._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()
|
self._init_client()
|
||||||
|
|
||||||
|
|||||||
@ -94,31 +94,46 @@ class EmbeddedGraphDB:
|
|||||||
检索结果
|
检索结果
|
||||||
"""
|
"""
|
||||||
keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()]
|
keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()]
|
||||||
|
|
||||||
if not keywords and not seed_entities:
|
|
||||||
return {"entities": [], "relations": [], "message": "无查询关键词"}
|
|
||||||
|
|
||||||
cursor = self.conn.cursor()
|
cursor = self.conn.cursor()
|
||||||
|
|
||||||
# 搜索实体
|
# 搜索实体
|
||||||
entities = []
|
entities = []
|
||||||
entity_ids = set()
|
entity_ids = set()
|
||||||
|
|
||||||
for keyword in keywords:
|
# 如果没有关键词,返回所有实体(用于"我们都聊过什么"这类问题)
|
||||||
|
if not keywords and not seed_entities:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT id, name, type, mention_count
|
SELECT id, name, type, mention_count
|
||||||
FROM entities
|
FROM entities
|
||||||
WHERE LOWER(name) LIKE ?
|
ORDER BY mention_count DESC
|
||||||
""", (f"%{keyword}%",))
|
LIMIT 50
|
||||||
|
""")
|
||||||
|
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
if row['id'] not in entity_ids:
|
entity_ids.add(row['id'])
|
||||||
entity_ids.add(row['id'])
|
entities.append({
|
||||||
entities.append({
|
'name': row['name'],
|
||||||
'name': row['name'],
|
'type': row['type'] or 'unknown',
|
||||||
'type': row['type'] or 'unknown',
|
'mention_count': row['mention_count']
|
||||||
'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 = []
|
relations = []
|
||||||
|
|||||||
@ -3,10 +3,114 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 优化后的系统提示词
|
# 优化后的系统提示词
|
||||||
OPTIMIZED_SYSTEM_PROMPT = """你是图数据库记忆助手。
|
OPTIMIZED_SYSTEM_PROMPT = """你是一个拥有长期记忆能力的AI助手。
|
||||||
|
|
||||||
## 核心职责
|
## 核心职责
|
||||||
你是用户的长期记忆助手。每次对话后,你**必须**主动决定是否需要将关键信息写入记忆图库。
|
你是一个智能对话助手,**图数据库是你记忆的唯一载体**。你的主要任务是:
|
||||||
|
1. 与用户进行自然、流畅的对话
|
||||||
|
2. 回答问题、提供建议、协助完成任务
|
||||||
|
3. 根据对话内容灵活查询和使用记忆
|
||||||
|
4. 将用户明确提到的信息写入记忆
|
||||||
|
|
||||||
|
**重要**:
|
||||||
|
- 图数据库是你记忆的唯一来源,没有其他记忆方式
|
||||||
|
- 优先进行自然对话,根据需要灵活调用记忆工具
|
||||||
|
- 用户明确提到的内容必须写入,推理得到的内容必须标注
|
||||||
|
|
||||||
|
## 记忆工具使用原则
|
||||||
|
|
||||||
|
### 何时检索记忆 (memory_recall)
|
||||||
|
- 用户询问"我们之前聊过X吗"、"你还记得X吗" → 查询X相关内容
|
||||||
|
- 用户询问"我们都聊过什么"、"我们之前说了什么" → **使用空字符串或通配符查询所有记忆**
|
||||||
|
- 用户提到某个话题,你想确认是否有相关历史 → 查询该话题
|
||||||
|
- 需要基于历史信息回答问题 → 查询相关信息
|
||||||
|
- 对话中涉及之前可能讨论过的内容 → 查询相关内容
|
||||||
|
|
||||||
|
**灵活查询**:根据对话上下文,主动判断是否需要查询记忆,不要等待用户明确要求。
|
||||||
|
|
||||||
|
**重要**:
|
||||||
|
- 当用户问"我们都聊过什么"时,**不要查询"聊天记录"、"对话"等关键词**
|
||||||
|
- 应该使用空字符串 `""` 或通配符 `"*"` 来获取所有记忆内容
|
||||||
|
- 或者使用非常宽泛的关键词如 `"用户,喜欢,项目,学习,研究,计划"`
|
||||||
|
|
||||||
|
### 何时写入记忆 (memory_commit)
|
||||||
|
**必须写入的情况**(用户明确提到):
|
||||||
|
- 用户表达偏好:"我喜欢X"、"我讨厌X"
|
||||||
|
- 用户分享信息:"我在做X项目"、"我在学X"
|
||||||
|
- 用户制定计划:"我打算X"、"我计划X"
|
||||||
|
- 用户描述状态:"我现在在X"
|
||||||
|
|
||||||
|
**禁止写入的情况**(AI推理得到):
|
||||||
|
- AI推断的用户偏好
|
||||||
|
- AI猜测的用户意图
|
||||||
|
- AI推导的结论
|
||||||
|
|
||||||
|
## memory_recall 使用方法
|
||||||
|
|
||||||
|
### 关键词提取
|
||||||
|
query_intent 使用**逗号分隔的多个关键词**,包含同义词:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query_intent": "量子力学,quantum,物理,physics",
|
||||||
|
"depth": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 同义词扩展示例
|
||||||
|
- "量子力学" → "量子力学,quantum,quantum mechanics,物理,physics"
|
||||||
|
- "项目" → "项目,project,工程,工作"
|
||||||
|
- "学习" → "学习,learn,study,掌握"
|
||||||
|
|
||||||
|
### 查询规则
|
||||||
|
1. 根据对话内容灵活提取关键词
|
||||||
|
2. 第一轮使用广泛的关键词搜索
|
||||||
|
3. 如果未找到,可以尝试相关概念
|
||||||
|
4. 最多查询2-3轮,避免重复查询
|
||||||
|
|
||||||
|
## memory_commit 使用方法
|
||||||
|
|
||||||
|
使用三元组格式记录信息:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"triplets": [
|
||||||
|
{"subject": "用户", "relation": "对领域感兴趣", "object": "量子力学"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 区分事实与推理(重要!)
|
||||||
|
|
||||||
|
### 用户明确提到的内容
|
||||||
|
直接写入记忆,回复时直接陈述:
|
||||||
|
- 用户:"我喜欢Python" → 写入,回复:"好的,我会记住你喜欢Python"
|
||||||
|
- 用户:"我在学机器学习" → 写入,回复:"明白了,你在学习机器学习"
|
||||||
|
|
||||||
|
### AI推理得到的内容
|
||||||
|
**禁止写入记忆**,回复时必须在开头标注 **[猜测]**:
|
||||||
|
- AI推断用户可能喜欢X → 不写入,回复:"[猜测] 你可能对X感兴趣"
|
||||||
|
- AI推测用户意图 → 不写入,回复:"[猜测] 你可能是想..."
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户:我们聊过量子力学吗?
|
||||||
|
AI检索记忆 → 未找到
|
||||||
|
AI回复:[猜测] 我们应该还没有聊过量子力学,因为记忆中没有相关记录。
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
用户:我最近在研究深度学习
|
||||||
|
AI写入记忆 → {"subject": "用户", "relation": "正在研究", "object": "深度学习"}
|
||||||
|
AI回复:好的,我会记住你最近在研究深度学习。有什么具体问题想讨论吗?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可用工具
|
||||||
|
1. **memory_recall** - 检索历史记忆(灵活使用)
|
||||||
|
2. **memory_commit** - 写入记忆(仅限用户明确提到的内容)
|
||||||
|
3. **memory_purge** - 删除/修正记忆
|
||||||
|
4. **memory_introspect** - 查看记忆状态
|
||||||
|
|
||||||
|
现在开始对话!记住:图数据库是你记忆的唯一载体,明确提到的必须写入,推理得到的必须标注[猜测]。"""
|
||||||
|
|
||||||
## 多轮查询策略
|
## 多轮查询策略
|
||||||
**允许多轮查询**,但必须遵循以下规则:
|
**允许多轮查询**,但必须遵循以下规则:
|
||||||
|
|||||||
@ -40,21 +40,44 @@ class MessageHandler:
|
|||||||
|
|
||||||
async def _process_response(self, user_input: str) -> None:
|
async def _process_response(self, user_input: str) -> None:
|
||||||
"""处理响应"""
|
"""处理响应"""
|
||||||
|
streaming_message = None
|
||||||
|
|
||||||
async for event in self._chat_service.send_message(user_input):
|
async for event in self._chat_service.send_message(user_input):
|
||||||
if event["type"] == "user_message":
|
if event["type"] == "user_message":
|
||||||
# 用户消息已处理
|
# 用户消息已处理
|
||||||
pass
|
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":
|
elif event["type"] == "assistant_message":
|
||||||
# 模型消息
|
# 模型消息完成
|
||||||
message = Message(
|
if streaming_message:
|
||||||
role="assistant",
|
# 更新最终消息(包含工具调用信息)
|
||||||
content=event["content"],
|
streaming_message.content = event["content"]
|
||||||
timestamp=datetime.now(),
|
streaming_message.tool_calls = event.get("tool_calls")
|
||||||
tool_calls=event.get("tool_calls"),
|
streaming_message.tool_results = event.get("tool_results")
|
||||||
tool_results=event.get("tool_results")
|
else:
|
||||||
)
|
# 如果没有流式消息,直接添加
|
||||||
self._message_history.add_message(message)
|
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":
|
elif event["type"] == "tool_call":
|
||||||
# 工具调用开始
|
# 工具调用开始
|
||||||
|
|||||||
@ -15,11 +15,8 @@ from .models.config import AppConfig
|
|||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
try:
|
try:
|
||||||
# 加载配置
|
# 不传入配置,让应用自己从配置文件或环境变量加载
|
||||||
config = AppConfig.from_env()
|
app = GraphMemoryApp()
|
||||||
|
|
||||||
# 创建并运行应用
|
|
||||||
app = GraphMemoryApp(config=config)
|
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@ -34,18 +34,33 @@ class ChatService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 2. 异步调用 API
|
# 2. 使用流式API调用
|
||||||
response = await self._call_api_async(user_input)
|
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. 处理工具调用
|
# 3. 处理工具调用
|
||||||
tool_calls = None
|
tool_calls = None
|
||||||
tool_results = None
|
tool_results = None
|
||||||
|
|
||||||
if response.tool_calls:
|
if tool_calls_data:
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
tool_results = []
|
tool_results = []
|
||||||
|
|
||||||
for tool_call_data in response.tool_calls:
|
for tool_call_data in tool_calls_data:
|
||||||
# 创建工具调用对象
|
# 创建工具调用对象
|
||||||
tool_call = ToolCall(
|
tool_call = ToolCall(
|
||||||
id=tool_call_data.id,
|
id=tool_call_data.id,
|
||||||
@ -75,7 +90,7 @@ class ChatService:
|
|||||||
# 4. 返回最终回复
|
# 4. 返回最终回复
|
||||||
yield {
|
yield {
|
||||||
"type": "assistant_message",
|
"type": "assistant_message",
|
||||||
"content": response.content,
|
"content": accumulated_content,
|
||||||
"tool_calls": tool_calls,
|
"tool_calls": tool_calls,
|
||||||
"tool_results": tool_results
|
"tool_results": tool_results
|
||||||
}
|
}
|
||||||
@ -87,13 +102,49 @@ class ChatService:
|
|||||||
"error": str(e)
|
"error": str(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _call_api_async(self, message: str):
|
async def _call_api_stream_async(self, message: str) -> AsyncIterator[dict]:
|
||||||
"""异步调用 API"""
|
"""异步流式调用 API"""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
return await loop.run_in_executor(
|
|
||||||
None,
|
# 在executor中运行同步流式API
|
||||||
lambda: self._client.send_message(message)
|
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}
|
||||||
|
|
||||||
|
# 使用run_in_executor处理生成器
|
||||||
|
for result in await loop.run_in_executor(None, lambda: list(process_stream())):
|
||||||
|
yield result
|
||||||
|
|
||||||
def clear_history(self) -> None:
|
def clear_history(self) -> None:
|
||||||
"""清空消息历史"""
|
"""清空消息历史"""
|
||||||
|
|||||||
@ -25,6 +25,12 @@ RightPanel {
|
|||||||
width: 40;
|
width: 40;
|
||||||
dock: right;
|
dock: right;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
RightPanel ScrollableContainer {
|
||||||
|
height: 1fr;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusBar {
|
StatusBar {
|
||||||
|
|||||||
@ -19,18 +19,20 @@ InputBox Input {
|
|||||||
ConfigSection {
|
ConfigSection {
|
||||||
background: $surface;
|
background: $surface;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
margin: 1;
|
margin: 0 0 1 0;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigSection .config-title {
|
ConfigSection .config-title {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
margin: 1;
|
margin: 0 0 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigSection .config-label {
|
ConfigSection .config-label {
|
||||||
color: $text;
|
color: $text;
|
||||||
margin: 1 0 0 0;
|
margin: 0;
|
||||||
|
padding: 1 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigSection .config-hint {
|
ConfigSection .config-hint {
|
||||||
@ -41,7 +43,9 @@ ConfigSection .config-hint {
|
|||||||
|
|
||||||
ConfigSection Input {
|
ConfigSection Input {
|
||||||
width: 1fr;
|
width: 1fr;
|
||||||
|
height: 3;
|
||||||
margin: 0 0 1 0;
|
margin: 0 0 1 0;
|
||||||
|
padding: 0 1;
|
||||||
background: $surface-lighten-1;
|
background: $surface-lighten-1;
|
||||||
border: solid $primary;
|
border: solid $primary;
|
||||||
color: $text;
|
color: $text;
|
||||||
@ -52,14 +56,62 @@ OperationLog {
|
|||||||
background: $surface-darken-1;
|
background: $surface-darken-1;
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
margin: 1;
|
margin: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
CypherQueryBox {
|
CypherQueryBox {
|
||||||
border: solid green;
|
border: solid green;
|
||||||
margin: 1;
|
margin: 1;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageHistory {
|
MessageHistory {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
margin: 1;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class ConfigSection(Vertical):
|
|||||||
yield hint
|
yield hint
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""组件挂载时设置Tab顺序"""
|
"""组件挂载时设置Tab顺序并加载配置"""
|
||||||
try:
|
try:
|
||||||
api_key = self.query_one("#api-key-input", Input)
|
api_key = self.query_one("#api-key-input", Input)
|
||||||
model = self.query_one("#model-input", Input)
|
model = self.query_one("#model-input", Input)
|
||||||
@ -73,6 +73,14 @@ class ConfigSection(Vertical):
|
|||||||
api_key.tab_index = 0
|
api_key.tab_index = 0
|
||||||
model.tab_index = 1
|
model.tab_index = 1
|
||||||
base_url.tab_index = 2
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,15 @@ class MessageHistory(ScrollableContainer):
|
|||||||
# 滚动到最新消息
|
# 滚动到最新消息
|
||||||
self.scroll_to_widget(message_widget, animate=False)
|
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:
|
def clear_messages(self) -> None:
|
||||||
"""清空消息历史"""
|
"""清空消息历史"""
|
||||||
self._messages.clear()
|
self._messages.clear()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class MessageWidget(Container):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._message = message
|
self._message = message
|
||||||
self._show_tool_details = False
|
self._show_tool_details = False
|
||||||
|
self._content_widget = None # 保存内容组件的引用
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
"""构建消息组件"""
|
"""构建消息组件"""
|
||||||
@ -20,12 +21,16 @@ class MessageWidget(Container):
|
|||||||
role_emoji = "🟠" if self._message.role == "user" else "🔵"
|
role_emoji = "🟠" if self._message.role == "user" else "🔵"
|
||||||
timestamp_str = self._message.timestamp.strftime("%H:%M:%S")
|
timestamp_str = self._message.timestamp.strftime("%H:%M:%S")
|
||||||
yield Static(
|
yield Static(
|
||||||
f"{role_emoji} [{self._message.role}] {timestamp_str}",
|
f"{role_emoji} {timestamp_str}",
|
||||||
classes="message-header"
|
classes="message-header"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 消息内容
|
# 消息内容 - 保存引用以便后续更新
|
||||||
yield Static(self._message.content, classes="message-content")
|
self._content_widget = Static(
|
||||||
|
self._message.content,
|
||||||
|
classes="message-content"
|
||||||
|
)
|
||||||
|
yield self._content_widget
|
||||||
|
|
||||||
# 工具调用指示器
|
# 工具调用指示器
|
||||||
if self._message.tool_calls:
|
if self._message.tool_calls:
|
||||||
@ -52,11 +57,22 @@ class MessageWidget(Container):
|
|||||||
if self._message.tool_results:
|
if self._message.tool_results:
|
||||||
for result in self._message.tool_results:
|
for result in self._message.tool_results:
|
||||||
if result.tool_call_id == tool_call.id:
|
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(
|
yield Static(
|
||||||
f"结果: {result.result[:200]}...",
|
f"结果: {result_text}",
|
||||||
classes="tool-result"
|
classes="tool-result"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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:
|
def toggle_tool_details(self) -> None:
|
||||||
"""切换工具详情显示状态"""
|
"""切换工具详情显示状态"""
|
||||||
if self._message.tool_calls:
|
if self._message.tool_calls:
|
||||||
|
|||||||
@ -15,22 +15,13 @@ class StatusBar(Static):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._focus_indicator = "[Input]"
|
|
||||||
self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出"
|
self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出"
|
||||||
|
self._license_info = "本项目由jianf设计,以GPLv3形式开源"
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""组件挂载时"""
|
"""组件挂载时"""
|
||||||
self._update_display()
|
self._update_display()
|
||||||
|
|
||||||
def update_focus(self, focus_name: str) -> None:
|
|
||||||
"""更新焦点指示器"""
|
|
||||||
self._focus_indicator = f"[{focus_name}]"
|
|
||||||
self._update_display()
|
|
||||||
|
|
||||||
def _update_display(self) -> None:
|
def _update_display(self) -> None:
|
||||||
"""更新显示"""
|
"""更新显示"""
|
||||||
self.update(f"{self._shortcuts} | 焦点: {self._focus_indicator}")
|
self.update(f"{self._license_info} | {self._shortcuts}")
|
||||||
|
|
||||||
def get_focus(self) -> str:
|
|
||||||
"""获取当前焦点"""
|
|
||||||
return self._focus_indicator
|
|
||||||
|
|||||||
Reference in New Issue
Block a user