feat: support multiple ACTIONs in one message - reply + execute in parallel
This commit is contained in:
+73
-64
@@ -168,9 +168,9 @@ class DeepSeekWebAdapter(BaseAdapter):
|
|||||||
await self._reply(task_id, sender_id, "DeepSeek 响应超时", scope, private_to_id)
|
await self._reply(task_id, sender_id, "DeepSeek 响应超时", scope, private_to_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
action = self._parse_action(response)
|
actions = self._parse_actions(response)
|
||||||
|
|
||||||
if action is None:
|
if not actions:
|
||||||
if "<<<ACTION" in response and "ACTION>>>" in response:
|
if "<<<ACTION" in response and "ACTION>>>" in response:
|
||||||
logger.warning("ACTION 解析失败,返回错误给 DeepSeek")
|
logger.warning("ACTION 解析失败,返回错误给 DeepSeek")
|
||||||
error_msg = (
|
error_msg = (
|
||||||
@@ -185,94 +185,103 @@ class DeepSeekWebAdapter(BaseAdapter):
|
|||||||
await self._reply(task_id, sender_id, clean_msg, scope, private_to_id)
|
await self._reply(task_id, sender_id, clean_msg, scope, private_to_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
action_type = action.get("type", "")
|
has_executable = False
|
||||||
|
result_parts = []
|
||||||
|
|
||||||
if action_type == "reply":
|
for action in actions:
|
||||||
reply_content = action.get("content", "")
|
action_type = action.get("type", "")
|
||||||
reply_status = action.get("status", "received")
|
|
||||||
logger.info(f"[reply] status={reply_status}, content={reply_content[:80]}...")
|
|
||||||
|
|
||||||
await self._reply(task_id, sender_id, reply_content, scope, private_to_id)
|
if action_type == "reply":
|
||||||
|
reply_content = action.get("content", "")
|
||||||
|
reply_status = action.get("status", "received")
|
||||||
|
logger.info(f"[reply] status={reply_status}, content={reply_content[:80]}...")
|
||||||
|
await self._reply(task_id, sender_id, reply_content, scope, private_to_id)
|
||||||
|
if reply_status == "completed":
|
||||||
|
logger.info(f"[任务完成] task={task_id}")
|
||||||
|
return
|
||||||
|
|
||||||
if reply_status == "completed":
|
elif action_type == "complete":
|
||||||
|
explanation = action.get("explanation", "任务已完成")
|
||||||
logger.info(f"[任务完成] task={task_id}")
|
logger.info(f"[任务完成] task={task_id}")
|
||||||
|
await self._reply(task_id, sender_id, explanation, scope, private_to_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.session.send_message("已转发回复给用户,请继续执行下一步操作。")
|
else:
|
||||||
continue
|
has_executable = True
|
||||||
|
cmd = action.get("cmd", "")
|
||||||
|
path = action.get("path", "")
|
||||||
|
action_content = action.get("content", "")
|
||||||
|
explanation = action.get("explanation", "")
|
||||||
|
|
||||||
if action_type == "complete":
|
if action_type == "write_file" and path:
|
||||||
explanation = action.get("explanation", "任务已完成")
|
cmd = path
|
||||||
logger.info(f"[任务完成] task={task_id}")
|
|
||||||
await self._reply(task_id, sender_id, explanation, scope, private_to_id)
|
logger.info(f"[执行] [{action_type}] {cmd or path} {explanation}")
|
||||||
|
|
||||||
|
result = await self.session.execute_action(action_type, cmd, action_content)
|
||||||
|
|
||||||
|
status = "[OK]" if result["success"] else "[ERR]"
|
||||||
|
output_preview = result["output"][:200].replace("\n", " ")
|
||||||
|
logger.info(f" {status} {output_preview}...")
|
||||||
|
|
||||||
|
display_cmd = path if action_type == "write_file" and path else cmd
|
||||||
|
result_parts.append(
|
||||||
|
f"操作: {action_type}\n"
|
||||||
|
f"命令: {display_cmd}\n"
|
||||||
|
f"成功: {result['success']}\n\n"
|
||||||
|
f"输出:\n```\n{result['output']}\n```"
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_executable and result_parts:
|
||||||
|
result_msg = (
|
||||||
|
f"[执行结果]\n"
|
||||||
|
+ "\n---\n".join(result_parts)
|
||||||
|
+ "\n\n请继续下一步或完成任务。"
|
||||||
|
)
|
||||||
|
await self.session.send_message(result_msg)
|
||||||
|
elif not has_executable:
|
||||||
return
|
return
|
||||||
|
|
||||||
cmd = action.get("cmd", "")
|
|
||||||
path = action.get("path", "")
|
|
||||||
action_content = action.get("content", "")
|
|
||||||
explanation = action.get("explanation", "")
|
|
||||||
|
|
||||||
if action_type == "write_file" and path:
|
|
||||||
cmd = path
|
|
||||||
|
|
||||||
logger.info(f"[执行] [{action_type}] {cmd or path} {explanation}")
|
|
||||||
|
|
||||||
result = await self.session.execute_action(action_type, cmd, action_content)
|
|
||||||
|
|
||||||
status = "[OK]" if result["success"] else "[ERR]"
|
|
||||||
output_preview = result["output"][:200].replace("\n", " ")
|
|
||||||
logger.info(f" {status} {output_preview}...")
|
|
||||||
|
|
||||||
display_cmd = path if action_type == "write_file" and path else cmd
|
|
||||||
result_msg = (
|
|
||||||
f"[执行结果]\n"
|
|
||||||
f"操作: {action_type}\n"
|
|
||||||
f"命令: {display_cmd}\n"
|
|
||||||
f"成功: {result['success']}\n\n"
|
|
||||||
f"输出:\n```\n{result['output']}\n```\n\n"
|
|
||||||
f"请继续下一步或完成任务。"
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.session.send_message(result_msg)
|
|
||||||
|
|
||||||
await self._reply(task_id, sender_id, "已达到最大交互轮数,任务终止", scope, private_to_id)
|
await self._reply(task_id, sender_id, "已达到最大交互轮数,任务终止", scope, private_to_id)
|
||||||
|
|
||||||
def _parse_action(self, text: str) -> Optional[dict]:
|
def _parse_actions(self, text: str) -> list:
|
||||||
if not text:
|
if not text:
|
||||||
return None
|
return []
|
||||||
|
|
||||||
action_pattern = r'<<<ACTION\s*\n?(\{[\s\S]*?\})\s*ACTION>>>'
|
action_pattern = r'<<<ACTION\s*\n?(\{[\s\S]*?\})\s*ACTION>>>'
|
||||||
action_match = re.search(action_pattern, text, re.DOTALL)
|
action_matches = re.findall(action_pattern, text, re.DOTALL)
|
||||||
|
|
||||||
if not action_match:
|
if not action_matches:
|
||||||
if "<<<ACTION" in text and "ACTION>>>" in text:
|
if "<<<ACTION" in text and "ACTION>>>" in text:
|
||||||
try:
|
try:
|
||||||
start = text.find("<<<ACTION") + len("<<<ACTION")
|
start = text.find("<<<ACTION") + len("<<<ACTION")
|
||||||
end = text.find("ACTION>>>")
|
end = text.find("ACTION>>>")
|
||||||
json_content = text[start:end].strip()
|
json_content = text[start:end].strip()
|
||||||
if json_content.startswith("{") and json_content.endswith("}"):
|
if json_content.startswith("{") and json_content.endswith("}"):
|
||||||
action_match = type('obj', (object,), {'group': lambda x, s=json_content: s})()
|
action_matches = [json_content]
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return []
|
||||||
else:
|
else:
|
||||||
return None
|
return []
|
||||||
|
|
||||||
try:
|
actions = []
|
||||||
json_str = action_match.group(1).strip()
|
for json_str in action_matches:
|
||||||
action = json.loads(json_str)
|
try:
|
||||||
|
action = json.loads(json_str.strip())
|
||||||
|
|
||||||
if action.get("type") == "write_file":
|
if action.get("type") == "write_file":
|
||||||
content_pattern = r'<<<CONTENT\s*\n?(.*?)\s*CONTENT>>>'
|
content_pattern = r'<<<CONTENT\s*\n?(.*?)\s*CONTENT>>>'
|
||||||
content_match = re.search(content_pattern, text, re.DOTALL)
|
content_match = re.search(content_pattern, text, re.DOTALL)
|
||||||
if content_match:
|
if content_match:
|
||||||
action["content"] = content_match.group(1)
|
action["content"] = content_match.group(1)
|
||||||
else:
|
else:
|
||||||
action["content"] = ""
|
action["content"] = ""
|
||||||
|
|
||||||
return action
|
actions.append(action)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.warning(f"JSON 解析失败: {e}")
|
logger.warning(f"JSON 解析失败: {e}")
|
||||||
return None
|
|
||||||
|
return actions
|
||||||
|
|
||||||
def _clean_action_markers(self, text: str) -> str:
|
def _clean_action_markers(self, text: str) -> str:
|
||||||
clean = re.sub(r'<<<ACTION\s*[\s\S]*?\s*ACTION>>>', '', text, flags=re.DOTALL)
|
clean = re.sub(r'<<<ACTION\s*[\s\S]*?\s*ACTION>>>', '', text, flags=re.DOTALL)
|
||||||
|
|||||||
@@ -70,11 +70,7 @@ ADAPTER_SYSTEM_PROMPT = """【系统指令】你是一个智能助手,同时
|
|||||||
例:用户说"在吗" → reply completed "在的,有什么可以帮你的?"
|
例:用户说"在吗" → reply completed "在的,有什么可以帮你的?"
|
||||||
|
|
||||||
情况B - 需要执行操作的任务:
|
情况B - 需要执行操作的任务:
|
||||||
第1步:reply received "收到,我来处理"
|
在一条消息里同时 reply + 执行命令,尽量减少来回轮次
|
||||||
第2步:执行相关命令
|
|
||||||
第3步:工作中随时 reply progress "正在xxx"
|
|
||||||
第4步:有疑问时 reply question "想确认一下..."
|
|
||||||
第5步:完成时 reply completed "任务结果..."
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════
|
||||||
【执行流程】
|
【执行流程】
|
||||||
@@ -94,66 +90,22 @@ ADAPTER_SYSTEM_PROMPT = """【系统指令】你是一个智能助手,同时
|
|||||||
═══════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════
|
||||||
【响应格式 - 必须严格遵守】
|
【响应格式 - 必须严格遵守】
|
||||||
═══════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════
|
||||||
每次响应必须包含两部分:
|
每条消息可以包含多个 ACTION 块,系统会按顺序依次执行:
|
||||||
|
|
||||||
1. ACTION 块(如需执行命令):
|
|
||||||
<<<ACTION
|
|
||||||
{"type": "命令类型", "cmd": "具体命令", "explanation": "简要说明"}
|
|
||||||
ACTION>>>
|
|
||||||
|
|
||||||
reply 命令格式:
|
|
||||||
<<<ACTION
|
|
||||||
{"type": "reply", "content": "回复内容", "status": "received/progress/question/completed"}
|
|
||||||
ACTION>>>
|
|
||||||
|
|
||||||
2. 结束标记(必须!):
|
|
||||||
<<<END_OF_MESSAGE>>>
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
【重要规则】
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
1. 【必须先回复】收到任务后,必须先执行 reply received,再开始工作
|
|
||||||
2. 【必须加结束标记】每次回复的最后一行必须是 <<<END_OF_MESSAGE>>>
|
|
||||||
3. 【非阻塞执行】command/powershell 不等待命令完成,立即返回
|
|
||||||
4. 【必须查询输出】执行命令后,必须用 get_terminal_output 查看结果
|
|
||||||
5. 【交互式程序】如果程序等待输入,用 send_input 发送输入,不要假设程序已结束
|
|
||||||
6. 【文件编码】write_file 配合 <<<CONTENT 块使用,内容不经过 JSON 转义
|
|
||||||
7. 【代码围栏 - 必须!】CONTENT 里的代码必须用 ```python 和 ``` 包裹,否则缩进会丢失
|
|
||||||
8. 【一轮一指令】每次只输出一个 ACTION,等待系统返回后再继续
|
|
||||||
9. 【主动汇报】工作过程中要主动 reply progress 告知用户进度
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
【示例 - 完整任务流程】
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
用户:帮我看看桌面上有什么文件
|
|
||||||
|
|
||||||
你(第1轮 - 先回复):
|
|
||||||
<<<ACTION
|
<<<ACTION
|
||||||
{"type": "reply", "content": "收到,我来看看桌面上有什么文件", "status": "received"}
|
{"type": "reply", "content": "收到,我来看看", "status": "received"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "list_dir", "cmd": "D:\\work", "explanation": "查看目录"}
|
||||||
ACTION>>>
|
ACTION>>>
|
||||||
<<<END_OF_MESSAGE>>>
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
你(第2轮 - 执行命令):
|
reply 命令格式:
|
||||||
<<<ACTION
|
<<<ACTION
|
||||||
{"type": "command", "cmd": "dir ~/Desktop", "explanation": "查看桌面文件"}
|
{"type": "reply", "content": "回复内容", "status": "received/progress/question/completed"}
|
||||||
ACTION>>>
|
ACTION>>>
|
||||||
<<<END_OF_MESSAGE>>>
|
|
||||||
|
|
||||||
你(第3轮 - 查看输出):
|
write_file 格式(CONTENT 块紧跟 ACTION 块):
|
||||||
<<<ACTION
|
|
||||||
{"type": "get_terminal_output", "cmd": "", "explanation": "查看命令输出"}
|
|
||||||
ACTION>>>
|
|
||||||
<<<END_OF_MESSAGE>>>
|
|
||||||
|
|
||||||
你(第4轮 - 汇报完成):
|
|
||||||
<<<ACTION
|
|
||||||
{"type": "reply", "content": "桌面上有3个文件:report.docx, photo.jpg, notes.txt", "status": "completed"}
|
|
||||||
ACTION>>>
|
|
||||||
<<<END_OF_MESSAGE>>>
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
【示例 - 写入文件(必须用代码围栏)】
|
|
||||||
═══════════════════════════════════════════════════════
|
|
||||||
<<<ACTION
|
<<<ACTION
|
||||||
{"type": "write_file", "path": "test.py", "explanation": "创建测试文件"}
|
{"type": "write_file", "path": "test.py", "explanation": "创建测试文件"}
|
||||||
ACTION>>>
|
ACTION>>>
|
||||||
@@ -161,9 +113,65 @@ ACTION>>>
|
|||||||
```python
|
```python
|
||||||
def hello():
|
def hello():
|
||||||
print("Hello")
|
print("Hello")
|
||||||
return 42
|
|
||||||
```
|
```
|
||||||
CONTENT>>>
|
CONTENT>>>
|
||||||
|
|
||||||
|
结束标记(每条消息最后一行必须!):
|
||||||
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
【重要规则】
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
1. 【多指令并行】一条消息里可以包含多个 ACTION,系统会按顺序执行。尽量在一条消息里同时 reply + 执行命令,减少来回轮次
|
||||||
|
2. 【必须加结束标记】每次回复的最后一行必须是 <<<END_OF_MESSAGE>>>
|
||||||
|
3. 【非阻塞执行】command/powershell 不等待命令完成,立即返回
|
||||||
|
4. 【必须查询输出】执行命令后,必须用 get_terminal_output 查看结果
|
||||||
|
5. 【交互式程序】如果程序等待输入,用 send_input 发送输入,不要假设程序已结束
|
||||||
|
6. 【文件编码】write_file 配合 <<<CONTENT 块使用,内容不经过 JSON 转义
|
||||||
|
7. 【代码围栏 - 必须!】CONTENT 里的代码必须用 ```python 和 ``` 包裹,否则缩进会丢失
|
||||||
|
8. 【主动汇报】工作过程中要主动 reply progress 告知用户进度
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
【示例 - 纯对话】
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
用户:在吗?
|
||||||
|
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "reply", "content": "在的,有什么可以帮你的?", "status": "completed"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
【示例 - 需要执行操作的任务(一条消息同时回复+执行)】
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
用户:帮我看看桌面上有什么文件
|
||||||
|
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "reply", "content": "收到,我来看看桌面上有什么文件", "status": "received"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "command", "cmd": "dir ~/Desktop", "explanation": "查看桌面文件"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
|
(系统返回命令输出后)
|
||||||
|
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "reply", "content": "桌面上有3个文件:report.docx, photo.jpg, notes.txt", "status": "completed"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
【示例 - 查看目录(一条消息同时回复+执行)】
|
||||||
|
═══════════════════════════════════════════════════════
|
||||||
|
用户:看看本地目录
|
||||||
|
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "reply", "content": "好的,我来查看本地目录", "status": "received"}
|
||||||
|
ACTION>>>
|
||||||
|
<<<ACTION
|
||||||
|
{"type": "list_dir", "cmd": "D:\\\\work", "explanation": "查看工作目录"}
|
||||||
|
ACTION>>>
|
||||||
<<<END_OF_MESSAGE>>>
|
<<<END_OF_MESSAGE>>>
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user