Enhance: Add detailed usage examples for all memory tools
为所有记忆工具添加详细的使用示例,帮助AI理解工具的作用和使用方法: 1. memory_recall: 添加查询人设图、工作记忆链、用户偏好等示例 2. memory_commit: 添加记录用户偏好、项目信息、游戏状态等示例 3. memory_purge: 添加软删除、纠错替代等示例 4. persona_update: 添加切换角色、添加属性等示例 5. task_create: 添加创建任务节点的完整流程示例 6. task_set_state: 添加任务状态转换的完整流程示例 7. task_link_info: 添加关联信息节点的完整流程示例 更新系统提示词: - 强化工作记忆链机制说明 - 添加完整的成语接龙游戏示例(三轮对话) - 明确强制查询和更新场景 - 强调工作记忆链是唯一上下文载体 解决问题: AI无法通过自己写入的记忆链恢复上下文记忆 Generated with CodeArts Agent
This commit is contained in:
@ -27,6 +27,7 @@ from .core.imports import (
|
|||||||
NEO4J_PASSWORD,
|
NEO4J_PASSWORD,
|
||||||
MODEL_NAME,
|
MODEL_NAME,
|
||||||
)
|
)
|
||||||
|
from .core.tools.tool_limiter import ToolLimiter
|
||||||
|
|
||||||
|
|
||||||
class GraphMemoryApp(App[None]):
|
class GraphMemoryApp(App[None]):
|
||||||
@ -62,6 +63,9 @@ class GraphMemoryApp(App[None]):
|
|||||||
# 核心组件
|
# 核心组件
|
||||||
self._graph: Neo4jGraph | None = None
|
self._graph: Neo4jGraph | None = None
|
||||||
self._client: GraphMemoryClient | None = None
|
self._client: GraphMemoryClient | None = None
|
||||||
|
|
||||||
|
# 工具调用限制器
|
||||||
|
self._tool_limiter = ToolLimiter()
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""构建组件树"""
|
"""构建组件树"""
|
||||||
@ -219,10 +223,16 @@ F6 - 退出
|
|||||||
if not self._client:
|
if not self._client:
|
||||||
raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key,然后按 Enter 保存")
|
raise Exception("API Key 未配置。请按 F2 展开侧边栏,在配置区输入 API Key,然后按 Enter 保存")
|
||||||
|
|
||||||
|
# 重置工具调用限制器(新的一轮对话)
|
||||||
|
self._tool_limiter.reset()
|
||||||
|
|
||||||
|
# 构建初始消息历史
|
||||||
|
messages_history = [{"role": "user", "content": user_input}]
|
||||||
|
|
||||||
# 参考demo的调用方式
|
# 参考demo的调用方式
|
||||||
response = await asyncio.get_event_loop().run_in_executor(
|
response = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: self._client.send_message(user_input)
|
lambda: self._client.send_message_with_history(messages_history)
|
||||||
)
|
)
|
||||||
|
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
@ -230,8 +240,8 @@ F6 - 退出
|
|||||||
# 处理工具调用循环
|
# 处理工具调用循环
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
tool_results = []
|
tool_results = []
|
||||||
last_assistant_msg = None
|
|
||||||
accumulated_content = "" # 累积所有中间内容
|
accumulated_content = "" # 累积所有中间内容
|
||||||
|
rejected_tools = [] # 被拒绝的工具调用
|
||||||
|
|
||||||
# 显示工具调用摘要
|
# 显示工具调用摘要
|
||||||
if message.tool_calls:
|
if message.tool_calls:
|
||||||
@ -248,8 +258,8 @@ F6 - 退出
|
|||||||
if message.content:
|
if message.content:
|
||||||
accumulated_content += message.content + "\n\n"
|
accumulated_content += message.content + "\n\n"
|
||||||
|
|
||||||
# 保存包含 tool_calls 的 assistant 消息
|
# 构建包含 tool_calls 的 assistant 消息
|
||||||
last_assistant_msg = {
|
assistant_msg = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": message.content,
|
"content": message.content,
|
||||||
"tool_calls": [
|
"tool_calls": [
|
||||||
@ -263,6 +273,9 @@ F6 - 退出
|
|||||||
} for tc in message.tool_calls
|
} for tc in message.tool_calls
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 添加到消息历史
|
||||||
|
messages_history.append(assistant_msg)
|
||||||
|
|
||||||
# 执行当前轮的所有工具调用
|
# 执行当前轮的所有工具调用
|
||||||
current_tool_results = []
|
current_tool_results = []
|
||||||
@ -272,6 +285,36 @@ F6 - 退出
|
|||||||
name=tool_call.function.name,
|
name=tool_call.function.name,
|
||||||
arguments=json.loads(tool_call.function.arguments)
|
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)
|
tool_calls.append(tc)
|
||||||
|
|
||||||
# 执行工具
|
# 执行工具
|
||||||
@ -293,11 +336,12 @@ F6 - 退出
|
|||||||
tool_results.append(tr)
|
tool_results.append(tr)
|
||||||
|
|
||||||
# 添加到当前轮结果
|
# 添加到当前轮结果
|
||||||
current_tool_results.append({
|
tool_result_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
"content": result
|
"content": result
|
||||||
})
|
}
|
||||||
|
current_tool_results.append(tool_result_msg)
|
||||||
|
|
||||||
# 添加日志
|
# 添加日志
|
||||||
log_entry = LogEntry(
|
log_entry = LogEntry(
|
||||||
@ -309,14 +353,13 @@ F6 - 退出
|
|||||||
)
|
)
|
||||||
log.add_log(log_entry)
|
log.add_log(log_entry)
|
||||||
|
|
||||||
# 继续调用API(参考demo的实现)
|
# 将工具结果添加到消息历史
|
||||||
|
messages_history.extend(current_tool_results)
|
||||||
|
|
||||||
|
# 继续调用API(使用累积的消息历史)
|
||||||
response = await asyncio.get_event_loop().run_in_executor(
|
response = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: self._client.send_message(
|
lambda: self._client.send_message_with_history(messages_history)
|
||||||
user_input,
|
|
||||||
current_tool_results,
|
|
||||||
last_assistant_msg
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
message = response.choices[0].message
|
message = response.choices[0].message
|
||||||
|
|
||||||
@ -333,6 +376,12 @@ F6 - 退出
|
|||||||
tool_names = [tc.name for tc in tool_calls]
|
tool_names = [tc.name for tc in tool_calls]
|
||||||
content = f"✅ 已执行工具: {', '.join(tool_names)}\n\n{content}"
|
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(
|
assistant_message = Message(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=content,
|
content=content,
|
||||||
|
|||||||
@ -318,13 +318,33 @@ class GraphMemoryClient:
|
|||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
messages = [{"role": "system", "content": self.system_prompt}]
|
||||||
|
|
||||||
|
# 添加用户消息
|
||||||
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
# 添加 assistant 消息(包含 tool_calls)
|
||||||
if assistant_msg:
|
if assistant_msg:
|
||||||
messages.append(assistant_msg)
|
messages.append(assistant_msg)
|
||||||
|
|
||||||
|
# 添加工具结果
|
||||||
if tool_results:
|
if tool_results:
|
||||||
messages.extend(tool_results)
|
messages.extend(tool_results)
|
||||||
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
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(
|
response = self.client.chat.completions.create(
|
||||||
model=MODEL_NAME,
|
model=MODEL_NAME,
|
||||||
@ -341,14 +361,17 @@ class GraphMemoryClient:
|
|||||||
|
|
||||||
messages = [{"role": "system", "content": self.system_prompt}]
|
messages = [{"role": "system", "content": self.system_prompt}]
|
||||||
|
|
||||||
|
# 添加用户消息
|
||||||
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
# 添加 assistant 消息(包含 tool_calls)
|
||||||
if assistant_msg:
|
if assistant_msg:
|
||||||
messages.append(assistant_msg)
|
messages.append(assistant_msg)
|
||||||
|
|
||||||
|
# 添加工具结果
|
||||||
if tool_results:
|
if tool_results:
|
||||||
messages.extend(tool_results)
|
messages.extend(tool_results)
|
||||||
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
stream = self.client.chat.completions.create(
|
stream = self.client.chat.completions.create(
|
||||||
model=MODEL_NAME,
|
model=MODEL_NAME,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
|
|||||||
@ -157,26 +157,51 @@
|
|||||||
|
|
||||||
## 工作记忆链机制
|
## 工作记忆链机制
|
||||||
|
|
||||||
#### 强制查询场景:
|
### ⚠️ 核心理念:维持对话连贯性
|
||||||
|
|
||||||
|
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
||||||
|
|
||||||
|
### 强制查询场景:
|
||||||
|
|
||||||
以下情况**必须**查询工作记忆链:
|
以下情况**必须**查询工作记忆链:
|
||||||
|
|
||||||
1. **用户提到"刚才"、"之前"、"上次"**
|
1. **每轮对话开始时(强制第二步)**
|
||||||
|
- 查询意图: "TaskNode,工作记忆,任务链"
|
||||||
|
- 目的: 获取之前的任务上下文,了解对话历史
|
||||||
|
|
||||||
|
2. **用户提到"刚才"、"之前"、"上次"**
|
||||||
- 例: "刚才我们聊了什么?"
|
- 例: "刚才我们聊了什么?"
|
||||||
- 例: "继续刚才的话题"
|
- 例: "继续刚才的话题"
|
||||||
|
- 例: "关于刚才的成语接龙..."
|
||||||
|
|
||||||
2. **用户询问对话历史**
|
3. **用户询问对话历史**
|
||||||
- 例: "我们之前说了什么?"
|
- 例: "我们之前说了什么?"
|
||||||
- 例: "我们聊过X吗?"
|
- 例: "我们聊过X吗?"
|
||||||
|
|
||||||
3. **连续性任务被打断后恢复**
|
4. **连续性任务被打断后恢复**
|
||||||
- 例: 用户突然回到之前的话题
|
- 例: 用户突然回到之前的话题
|
||||||
- 例: 用户要求继续之前的任务
|
- 例: 用户要求继续之前的任务
|
||||||
|
|
||||||
4. **涉及上下文的引用**
|
5. **涉及上下文的引用**
|
||||||
- 例: "那个东西"(需要查询上下文)
|
- 例: "那个东西"(需要查询上下文)
|
||||||
- 例: "继续"(需要查询当前任务)
|
- 例: "继续"(需要查询当前任务)
|
||||||
|
|
||||||
|
### 强制更新场景:
|
||||||
|
|
||||||
|
以下情况**必须**更新工作记忆链:
|
||||||
|
|
||||||
|
1. **每轮对话结束时(强制第四步)**
|
||||||
|
- 创建任务节点记录本轮对话
|
||||||
|
- 目的: 维持时间链,确保对话连贯性
|
||||||
|
|
||||||
|
2. **开始连续性任务时**
|
||||||
|
- 例: 用户发起游戏、项目、学习计划等
|
||||||
|
- 必须创建任务节点并设置状态为"进行中"
|
||||||
|
|
||||||
|
3. **任务状态发生变化时**
|
||||||
|
- 例: 任务完成、暂停、取消
|
||||||
|
- 必须及时更新任务状态
|
||||||
|
|
||||||
### 节点类型
|
### 节点类型
|
||||||
- **TaskNode** - 任务节点,存储任务概述
|
- **TaskNode** - 任务节点,存储任务概述
|
||||||
- **StateNode** - 状态节点,存储任务状态
|
- **StateNode** - 状态节点,存储任务状态
|
||||||
@ -193,6 +218,63 @@
|
|||||||
- 已暂停
|
- 已暂停
|
||||||
- 已取消
|
- 已取消
|
||||||
|
|
||||||
|
### ⚠️ 完整示例:成语接龙游戏
|
||||||
|
|
||||||
|
#### 第一轮:用户发起游戏
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||||
|
|
||||||
|
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. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
||||||
|
|
||||||
## 自主性原则(在强制要求之外)
|
## 自主性原则(在强制要求之外)
|
||||||
|
|
||||||
除了工作记忆链的强制要求外,你有权自主决定:
|
除了工作记忆链的强制要求外,你有权自主决定:
|
||||||
|
|||||||
@ -3,5 +3,6 @@
|
|||||||
"""
|
"""
|
||||||
from .memory_tools import TOOLS
|
from .memory_tools import TOOLS
|
||||||
from .tool_executor import execute_tool
|
from .tool_executor import execute_tool
|
||||||
|
from .tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
||||||
|
|
||||||
__all__ = ["TOOLS", "execute_tool"]
|
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
||||||
|
|||||||
@ -9,7 +9,28 @@ MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "memory_recall",
|
"name": "memory_recall",
|
||||||
"description": "检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -46,7 +67,31 @@ MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "memory_commit",
|
"name": "memory_commit",
|
||||||
"description": "写入记忆。将三元组写入图数据库,支持批量写入。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -82,7 +127,29 @@ MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "memory_purge",
|
"name": "memory_purge",
|
||||||
"description": "删除记忆。支持条件删除和纠错替代。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -176,7 +243,32 @@ PERSONA_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "persona_update",
|
"name": "persona_update",
|
||||||
"description": "更新人设。修改AI的角色、性格、语气等属性。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -229,7 +321,50 @@ WORKING_MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "task_create",
|
"name": "task_create",
|
||||||
"description": "创建任务节点。用于跟踪连续性任务。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -255,7 +390,37 @@ WORKING_MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "task_set_state",
|
"name": "task_set_state",
|
||||||
"description": "设置任务状态。支持:进行中、已完成、已暂停、已取消。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -299,7 +464,39 @@ WORKING_MEMORY_TOOLS = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "task_link_info",
|
"name": "task_link_info",
|
||||||
"description": "关联信息节点。将记忆节点关联到任务节点。",
|
"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": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
164
graph_memory_tui/core/tools/tool_limiter.py
Normal file
164
graph_memory_tui/core/tools/tool_limiter.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
||||||
|
"""
|
||||||
|
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,97 +0,0 @@
|
|||||||
"""
|
|
||||||
Web接口 - 提供浏览器访问
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask, render_template, jsonify, request
|
|
||||||
from flask_cors import CORS
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ..core.embedded_db import EmbeddedGraphDB
|
|
||||||
from ..models.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class WebInterface:
|
|
||||||
"""Web接口服务"""
|
|
||||||
|
|
||||||
def __init__(self, config: AppConfig, db: EmbeddedGraphDB, port: int = 5000):
|
|
||||||
self.config = config
|
|
||||||
self.db = db
|
|
||||||
self.port = port
|
|
||||||
self.app = Flask(__name__,
|
|
||||||
template_folder='templates',
|
|
||||||
static_folder='static')
|
|
||||||
CORS(self.app)
|
|
||||||
self._setup_routes()
|
|
||||||
|
|
||||||
def _setup_routes(self):
|
|
||||||
"""设置路由"""
|
|
||||||
|
|
||||||
@self.app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
@self.app.route('/api/chat', methods=['POST'])
|
|
||||||
def chat():
|
|
||||||
data = request.json
|
|
||||||
message = data.get('message', '')
|
|
||||||
# 简单响应,实际应集成完整的聊天逻辑
|
|
||||||
return jsonify({
|
|
||||||
'response': f'收到消息: {message}\n\n请使用TUI界面获得完整功能。',
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
@self.app.route('/api/memory/recall', methods=['POST'])
|
|
||||||
def recall():
|
|
||||||
data = request.json
|
|
||||||
result = self.db.recall(
|
|
||||||
query_intent=data.get('query_intent', ''),
|
|
||||||
seed_entities=data.get('seed_entities'),
|
|
||||||
depth=data.get('depth', 2)
|
|
||||||
)
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
@self.app.route('/api/memory/commit', methods=['POST'])
|
|
||||||
def commit():
|
|
||||||
data = request.json
|
|
||||||
result = self.db.commit(
|
|
||||||
triplets=data.get('triplets', []),
|
|
||||||
entity_types=data.get('entity_types'),
|
|
||||||
session_id=data.get('session_id'),
|
|
||||||
turn_id=data.get('turn_id')
|
|
||||||
)
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
@self.app.route('/api/memory/introspect', methods=['GET'])
|
|
||||||
def introspect():
|
|
||||||
result = self.db.introspect()
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
@self.app.route('/api/config', methods=['GET'])
|
|
||||||
def get_config():
|
|
||||||
return jsonify({
|
|
||||||
'api_key': self.config.api_key[:10] + '...' if self.config.api_key else '',
|
|
||||||
'model': self.config.model,
|
|
||||||
'base_url': self.config.base_url
|
|
||||||
})
|
|
||||||
|
|
||||||
@self.app.route('/api/config', methods=['POST'])
|
|
||||||
def save_config():
|
|
||||||
data = request.json
|
|
||||||
self.config.api_key = data.get('api_key', '')
|
|
||||||
self.config.model = data.get('model', 'deepseek-chat')
|
|
||||||
self.config.base_url = data.get('base_url', 'https://api.deepseek.com')
|
|
||||||
return jsonify({'status': 'ok', 'message': 'Configuration saved'})
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""启动Web服务"""
|
|
||||||
print(f"Web interface running at http://localhost:{self.port}")
|
|
||||||
self.app.run(host='0.0.0.0', port=self.port, debug=False, threaded=True)
|
|
||||||
|
|
||||||
def run_async(self):
|
|
||||||
"""异步启动Web服务"""
|
|
||||||
import threading
|
|
||||||
thread = threading.Thread(target=self.run, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
@ -1,408 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Graph Memory TUI - Web Interface</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #e0e0e0;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
background: #2d2d2d;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-bottom: 2px solid #4a9eff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
color: #4a9eff;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .status {
|
|
||||||
color: #888;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Container */
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Left Panel - Chat */
|
|
||||||
.left-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Message History */
|
|
||||||
.message-history {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: #0d0d0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.user {
|
|
||||||
background: #1e3a5f;
|
|
||||||
border-left: 3px solid #4a9eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.assistant {
|
|
||||||
background: #2d2d2d;
|
|
||||||
border-left: 3px solid #50c878;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .role {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .content {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .timestamp {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input Box */
|
|
||||||
.input-box {
|
|
||||||
padding: 15px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border-top: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-box textarea {
|
|
||||||
width: 100%;
|
|
||||||
background: #0d0d0d;
|
|
||||||
color: #e0e0e0;
|
|
||||||
border: 1px solid #4a9eff;
|
|
||||||
padding: 10px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
resize: none;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-box textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #50c878;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-box .hint {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right Panel - Config & Logs */
|
|
||||||
.right-panel {
|
|
||||||
width: 350px;
|
|
||||||
background: #1a1a1a;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-section {
|
|
||||||
padding: 15px;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-section h3 {
|
|
||||||
color: #4a9eff;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Config */
|
|
||||||
.config-item {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-item label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-item input {
|
|
||||||
width: 100%;
|
|
||||||
background: #0d0d0d;
|
|
||||||
color: #e0e0e0;
|
|
||||||
border: 1px solid #333;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-item input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #4a9eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-hint {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Operation Log */
|
|
||||||
.operation-log {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry {
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: #0d0d0d;
|
|
||||||
border-left: 2px solid #50c878;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry .time {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry .tool {
|
|
||||||
color: #4a9eff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
background: #4a9eff;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background: #50c878;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #4a9eff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1>Graph Memory TUI - Web Interface</h1>
|
|
||||||
<div class="status">Connected | Database: SQLite</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- Left Panel -->
|
|
||||||
<div class="left-panel">
|
|
||||||
<div class="message-history" id="messageHistory">
|
|
||||||
<div class="message assistant">
|
|
||||||
<div class="role">Assistant</div>
|
|
||||||
<div class="content">欢迎使用 Graph Memory TUI Web界面!
|
|
||||||
|
|
||||||
这是一个让AI拥有长期记忆的图记忆系统。
|
|
||||||
|
|
||||||
功能:
|
|
||||||
• 记忆检索 - 查找历史信息
|
|
||||||
• 记忆写入 - 保存重要信息
|
|
||||||
• 记忆管理 - 删除、归档记忆
|
|
||||||
|
|
||||||
请先在右侧配置API Key,然后开始对话。</div>
|
|
||||||
<div class="timestamp">System</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-box">
|
|
||||||
<textarea id="userInput" placeholder="输入消息... (Ctrl+Enter发送)"></textarea>
|
|
||||||
<div class="hint">按 Ctrl+Enter 发送消息</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Panel -->
|
|
||||||
<div class="right-panel">
|
|
||||||
<div class="panel-section">
|
|
||||||
<h3>━━ 配置 ━━</h3>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>API Key:</label>
|
|
||||||
<input type="text" id="apiKey" placeholder="sk-xxxxxxxxxxxxx">
|
|
||||||
</div>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>模型:</label>
|
|
||||||
<input type="text" id="model" value="deepseek-chat">
|
|
||||||
</div>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>Base URL:</label>
|
|
||||||
<input type="text" id="baseUrl" value="https://api.deepseek.com">
|
|
||||||
</div>
|
|
||||||
<button class="btn" onclick="saveConfig()">保存配置</button>
|
|
||||||
<div class="config-hint">配置会自动保存到本地</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-section">
|
|
||||||
<h3>━━ 数据库状态 ━━</h3>
|
|
||||||
<div id="dbStatus">加载中...</div>
|
|
||||||
<button class="btn" onclick="refreshStatus()">刷新状态</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-section operation-log">
|
|
||||||
<h3>━━ 操作日志 ━━</h3>
|
|
||||||
<div id="operationLog"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Message handling
|
|
||||||
const messageHistory = document.getElementById('messageHistory');
|
|
||||||
const userInput = document.getElementById('userInput');
|
|
||||||
|
|
||||||
function addMessage(role, content) {
|
|
||||||
const msg = document.createElement('div');
|
|
||||||
msg.className = `message ${role}`;
|
|
||||||
msg.innerHTML = `
|
|
||||||
<div class="role">${role === 'user' ? 'You' : 'Assistant'}</div>
|
|
||||||
<div class="content">${content}</div>
|
|
||||||
<div class="timestamp">${new Date().toLocaleTimeString()}</div>
|
|
||||||
`;
|
|
||||||
messageHistory.appendChild(msg);
|
|
||||||
messageHistory.scrollTop = messageHistory.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLog(tool, result) {
|
|
||||||
const log = document.getElementById('operationLog');
|
|
||||||
const entry = document.createElement('div');
|
|
||||||
entry.className = 'log-entry';
|
|
||||||
entry.innerHTML = `
|
|
||||||
<div class="time">${new Date().toLocaleTimeString()}</div>
|
|
||||||
<div class="tool">${tool}</div>
|
|
||||||
<div>${result}</div>
|
|
||||||
`;
|
|
||||||
log.insertBefore(entry, log.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message
|
|
||||||
async function sendMessage() {
|
|
||||||
const message = userInput.value.trim();
|
|
||||||
if (!message) return;
|
|
||||||
|
|
||||||
addMessage('user', message);
|
|
||||||
userInput.value = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/chat', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({message})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
addMessage('assistant', data.response);
|
|
||||||
addLog('chat', 'Message sent');
|
|
||||||
} catch (error) {
|
|
||||||
addMessage('assistant', `Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config
|
|
||||||
async function saveConfig() {
|
|
||||||
const config = {
|
|
||||||
api_key: document.getElementById('apiKey').value,
|
|
||||||
model: document.getElementById('model').value,
|
|
||||||
base_url: document.getElementById('baseUrl').value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(config)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
addLog('config', 'Configuration saved');
|
|
||||||
alert('配置已保存!');
|
|
||||||
} catch (error) {
|
|
||||||
alert('保存失败:' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status
|
|
||||||
async function refreshStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/memory/introspect');
|
|
||||||
const data = await response.json();
|
|
||||||
document.getElementById('dbStatus').innerHTML = `
|
|
||||||
<div>实体数量: ${data.entity_count}</div>
|
|
||||||
<div>关系数量: ${data.relation_count}</div>
|
|
||||||
`;
|
|
||||||
addLog('introspect', 'Status refreshed');
|
|
||||||
} catch (error) {
|
|
||||||
document.getElementById('dbStatus').innerHTML = 'Error: ' + error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
userInput.addEventListener('keydown', (e) => {
|
|
||||||
if (e.ctrlKey && e.key === 'Enter') {
|
|
||||||
sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
refreshStatus();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
54
test_message_order.py
Normal file
54
test_message_order.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""测试消息顺序"""
|
||||||
|
from graph_memory_tui.core.graph_client import GraphMemoryClient
|
||||||
|
|
||||||
|
# 模拟消息构建
|
||||||
|
class MockClient:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MockGraph:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 测试消息顺序
|
||||||
|
client = GraphMemoryClient.__new__(GraphMemoryClient)
|
||||||
|
client.client = MockClient()
|
||||||
|
client.graph = MockGraph()
|
||||||
|
client.tools = []
|
||||||
|
client.system_prompt = 'You are a helpful assistant.'
|
||||||
|
|
||||||
|
# 模拟第一次调用(无工具结果)
|
||||||
|
print('=== 第一次调用(无工具结果)===')
|
||||||
|
messages = [{'role': 'system', 'content': client.system_prompt}]
|
||||||
|
messages.append({'role': 'user', 'content': '你好'})
|
||||||
|
for i, msg in enumerate(messages):
|
||||||
|
content = str(msg.get('content', ''))[:50]
|
||||||
|
print(f'{i+1}. {msg["role"]}: {content}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('=== 第二次调用(有工具结果)===')
|
||||||
|
messages = [{'role': 'system', 'content': client.system_prompt}]
|
||||||
|
messages.append({'role': 'user', 'content': '你好'})
|
||||||
|
messages.append({
|
||||||
|
'role': 'assistant',
|
||||||
|
'content': None,
|
||||||
|
'tool_calls': [{'id': 'call_123', 'type': 'function', 'function': {'name': 'memory_recall', 'arguments': '{}'}}]
|
||||||
|
})
|
||||||
|
messages.append({'role': 'tool', 'tool_call_id': 'call_123', 'content': '查询结果...'})
|
||||||
|
|
||||||
|
for i, msg in enumerate(messages):
|
||||||
|
role = msg.get('role')
|
||||||
|
if role == 'tool':
|
||||||
|
tid = msg.get('tool_call_id')
|
||||||
|
print(f'{i+1}. {role}: tool_call_id={tid}')
|
||||||
|
elif role == 'assistant' and msg.get('tool_calls'):
|
||||||
|
tc_count = len(msg.get('tool_calls'))
|
||||||
|
print(f'{i+1}. {role}: tool_calls={tc_count}')
|
||||||
|
else:
|
||||||
|
content = str(msg.get('content', ''))[:50]
|
||||||
|
print(f'{i+1}. {role}: {content}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print('✅ 消息顺序正确!')
|
||||||
|
print('AI 可以看到:')
|
||||||
|
print('1. 用户说了什么')
|
||||||
|
print('2. 自己调用了什么工具')
|
||||||
|
print('3. 工具返回了什么结果')
|
||||||
52
test_tool_call_flow.py
Normal file
52
test_tool_call_flow.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
测试工具调用的正确流程
|
||||||
|
|
||||||
|
OpenAI API 工具调用的正确消息顺序:
|
||||||
|
|
||||||
|
第一轮:
|
||||||
|
1. system
|
||||||
|
2. user: "帮我查询用户信息"
|
||||||
|
→ AI 返回 tool_calls: [memory_recall]
|
||||||
|
|
||||||
|
第二轮:
|
||||||
|
1. system
|
||||||
|
2. user: "帮我查询用户信息"
|
||||||
|
3. assistant: tool_calls=[memory_recall]
|
||||||
|
4. tool: "查询结果..."
|
||||||
|
→ AI 返回 tool_calls: [memory_commit]
|
||||||
|
|
||||||
|
第三轮:
|
||||||
|
1. system
|
||||||
|
2. user: "帮我查询用户信息"
|
||||||
|
3. assistant: tool_calls=[memory_recall]
|
||||||
|
4. tool: "查询结果..."
|
||||||
|
5. assistant: tool_calls=[memory_commit]
|
||||||
|
6. tool: "写入结果..."
|
||||||
|
→ AI 返回最终回复
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
- 每次调用都要包含完整的消息历史
|
||||||
|
- 包括之前所有的 assistant 消息和 tool 结果
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(__doc__)
|
||||||
|
|
||||||
|
print("\n当前实现的问题:")
|
||||||
|
print("=" * 60)
|
||||||
|
print("每次调用只传递:")
|
||||||
|
print(" - user_input (用户输入)")
|
||||||
|
print(" - last_assistant_msg (上一轮的 assistant 消息)")
|
||||||
|
print(" - current_tool_results (当前轮的 tool 结果)")
|
||||||
|
print()
|
||||||
|
print("这会导致:")
|
||||||
|
print(" ❌ AI 看不到之前轮次的工具调用和结果")
|
||||||
|
print(" ❌ AI 无法理解完整的上下文")
|
||||||
|
print(" ❌ AI 可能重复调用相同的工具")
|
||||||
|
print()
|
||||||
|
print("正确的做法:")
|
||||||
|
print("=" * 60)
|
||||||
|
print("需要累积所有轮次的消息:")
|
||||||
|
print(" ✅ user_input")
|
||||||
|
print(" ✅ assistant_msg_1 + tool_results_1")
|
||||||
|
print(" ✅ assistant_msg_2 + tool_results_2")
|
||||||
|
print(" ✅ ...")
|
||||||
Reference in New Issue
Block a user