712 lines
32 KiB
Python
712 lines
32 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import base64
|
||
import json
|
||
import mimetypes
|
||
import re
|
||
import threading
|
||
import time
|
||
import uuid
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
|
||
|
||
_QUESTION_ID_RE = re.compile(r"^id:\s*(\S+)", re.MULTILINE)
|
||
|
||
from modules.sub_agent.toolkit import (
|
||
SUB_AGENT_TOOLS,
|
||
FINISH_TOOL,
|
||
_format_tool_result,
|
||
_build_sub_agent_profile,
|
||
)
|
||
from utils.api_client import DeepSeekClient
|
||
from utils.logger import setup_logger
|
||
|
||
if TYPE_CHECKING:
|
||
from modules.sub_agent.manager import SubAgentManager
|
||
from modules.multi_agent.state import MultiAgentState
|
||
|
||
from modules.multi_agent.debug_logger import ma_debug
|
||
|
||
logger = setup_logger(__name__)
|
||
|
||
# 多智能体模式下额外加载的工具定义
|
||
def _load_multi_agent_sub_agent_tools() -> List[Dict[str, Any]]:
|
||
try:
|
||
from modules.multi_agent.tools import build_sub_agent_tools_for_role
|
||
return build_sub_agent_tools_for_role()
|
||
except Exception as exc:
|
||
logger.warning(f"[SubAgentTask] 加载多智能体工具失败: {exc}")
|
||
return []
|
||
|
||
|
||
class SubAgentTask:
|
||
"""单个后台子智能体任务。"""
|
||
|
||
def __init__(
|
||
self,
|
||
manager: "SubAgentManager",
|
||
task_record: Dict[str, Any],
|
||
task_message: str,
|
||
system_prompt: str,
|
||
model_key: Optional[str],
|
||
thinking_mode: Optional[str],
|
||
*,
|
||
multi_agent_mode: bool = False,
|
||
multi_agent_state: Optional["MultiAgentState"] = None,
|
||
display_name: Optional[str] = None,
|
||
):
|
||
self.manager = manager
|
||
self.task_record = task_record
|
||
self.task_message = task_message
|
||
self.system_prompt = system_prompt
|
||
self.model_key = model_key
|
||
self.thinking_mode = thinking_mode or "fast"
|
||
self.task_id = task_record["task_id"]
|
||
self.agent_id = task_record["agent_id"]
|
||
raw_timeout = task_record.get("timeout_seconds")
|
||
self.timeout_seconds = int(raw_timeout) if raw_timeout is not None else None
|
||
self.deliverables_dir = Path(task_record["deliverables_dir"])
|
||
|
||
self.output_file = Path(task_record["output_file"])
|
||
self.stats_file = Path(task_record["stats_file"])
|
||
self.progress_file = Path(task_record["progress_file"])
|
||
self.conversation_file = Path(task_record["conversation_file"])
|
||
|
||
self.messages: List[Dict[str, Any]] = [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": task_message},
|
||
]
|
||
self.stats = {
|
||
"runtime_start": time.time() * 1000,
|
||
"runtime_seconds": 0,
|
||
"files_read": 0,
|
||
"write_files": 0,
|
||
"edit_files": 0,
|
||
"searches": 0,
|
||
"web_pages": 0,
|
||
"commands": 0,
|
||
"api_calls": 0,
|
||
"token_usage": {"prompt": 0, "completion": 0, "total": 0},
|
||
}
|
||
self._stdout_lines: List[str] = []
|
||
self._cancelled = False
|
||
self._task: Optional[asyncio.Task] = None
|
||
|
||
# 多智能体模式相关字段
|
||
self.multi_agent_mode = bool(multi_agent_mode)
|
||
self.multi_agent_state = multi_agent_state
|
||
# display_name 不传时回退为 'Agent_{self.agent_id}'
|
||
self.display_name = display_name or f"Agent_{self.agent_id}"
|
||
# 多智能体运行期控制
|
||
# 使用 asyncio.Event 在子智能体自己的事件循环内等待;
|
||
# inject_message 可能跨线程调用,通过 loop.call_soon_threadsafe 唤醒。
|
||
self._continue_event: Optional[asyncio.Event] = None
|
||
self._idle = False
|
||
self._pending_answer_question_id: Optional[str] = None
|
||
self._answered_question_ids: Set[str] = set()
|
||
|
||
def emit(self, type_: str, data: Dict[str, Any]) -> None:
|
||
"""输出一行 JSONL 到 progress 文件并缓存。"""
|
||
line = json.dumps({"type": type_, **data}, ensure_ascii=False)
|
||
self._stdout_lines.append(line)
|
||
try:
|
||
self.progress_file.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(self.progress_file, "a", encoding="utf-8") as f:
|
||
f.write(line + "\n")
|
||
except Exception:
|
||
pass
|
||
|
||
async def run(self) -> None:
|
||
"""主 LLM 循环。"""
|
||
# 在子智能体自己的事件循环内初始化 asyncio.Event
|
||
self._continue_event = asyncio.Event()
|
||
try:
|
||
await self._run_loop()
|
||
except asyncio.CancelledError:
|
||
self._cancelled = True
|
||
logger.debug(f"[SubAgent] task={self.task_id} 被取消")
|
||
ma_debug("sub_agent_run_cancelled", task_id=self.task_id, agent_id=self.agent_id, display_name=self.display_name)
|
||
# shield 避免取消信号中断最终状态落盘
|
||
await asyncio.shield(self._write_failure("子智能体被手动终止"))
|
||
raise
|
||
except Exception as exc:
|
||
logger.exception(f"[SubAgent] task={self.task_id} 执行异常")
|
||
ma_debug("sub_agent_run_exception", task_id=self.task_id, agent_id=self.agent_id, display_name=self.display_name, error=str(exc))
|
||
await self._write_failure(f"执行异常: {exc}")
|
||
|
||
async def _run_loop(self) -> None:
|
||
client, model_key = self._build_client()
|
||
if self.multi_agent_mode:
|
||
tools = list(SUB_AGENT_TOOLS)
|
||
tools.extend(_load_multi_agent_sub_agent_tools())
|
||
# 多智能体模式下不要求 finish_task,自然输出结束即进入 idle,可继续接收消息
|
||
else:
|
||
tools = list(SUB_AGENT_TOOLS)
|
||
tools.append(FINISH_TOOL)
|
||
|
||
start_time = time.time()
|
||
max_turns = 50
|
||
turn = 0
|
||
|
||
while not self._cancelled:
|
||
elapsed = time.time() - start_time
|
||
if self.timeout_seconds is not None and elapsed > self.timeout_seconds:
|
||
await self._write_timeout(elapsed)
|
||
return
|
||
|
||
# 多智能体模式下,idle 时等待新消息或外部回答;只有真正被注入消息时才继续运行
|
||
if self.multi_agent_mode and self._idle:
|
||
event_set = False
|
||
try:
|
||
if self._continue_event is None:
|
||
self._continue_event = asyncio.Event()
|
||
await asyncio.wait_for(self._continue_event.wait(), timeout=1.0)
|
||
event_set = True
|
||
except asyncio.TimeoutError:
|
||
pass
|
||
if self._cancelled:
|
||
break
|
||
if not event_set:
|
||
# 只是周期性检查取消状态,没有新消息,保持 idle 继续等待
|
||
continue
|
||
self._continue_event.clear()
|
||
ma_debug(
|
||
"sub_agent_idle_wake",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
pending_messages=[m.get("role") for m in self.messages[-3:]],
|
||
)
|
||
self._idle = False
|
||
continue
|
||
|
||
turn += 1
|
||
if turn > max_turns:
|
||
await self._write_failure("任务执行超过最大轮次限制", max_turns_exceeded=True)
|
||
return
|
||
|
||
self.stats["api_calls"] += 1
|
||
self.stats["turn_count"] = turn
|
||
self.stats["runtime_seconds"] = int(elapsed)
|
||
self.emit("stats", {**self.stats, "turn_count": turn})
|
||
|
||
# 多智能体模式:在模型调用前识别是否有待回答的提问
|
||
if self.multi_agent_mode:
|
||
self._pending_answer_question_id = self._peek_pending_question_id()
|
||
|
||
# 调试:记录进入本轮模型调用前的上下文摘要
|
||
ma_debug(
|
||
"sub_agent_model_call_start",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
turn=turn,
|
||
message_count=len(self.messages),
|
||
last_user_message=self.messages[-1].get("content", "")[:300] if self.messages and self.messages[-1].get("role") == "user" else "",
|
||
pending_answer_question_id=self._pending_answer_question_id,
|
||
)
|
||
|
||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||
if usage:
|
||
self._apply_usage(usage)
|
||
|
||
# 多智能体模式:把 assistant 文本输出作为进度/完成 output 转发到主对话
|
||
if self.multi_agent_mode and self.multi_agent_state and assistant_message.strip():
|
||
self._forward_output_to_master(assistant_message, is_final=not tool_calls)
|
||
|
||
final_message: Dict[str, Any] = {"role": "assistant", "content": assistant_message}
|
||
if reasoning:
|
||
final_message["reasoning_content"] = reasoning
|
||
if tool_calls:
|
||
final_message["tool_calls"] = tool_calls
|
||
self.messages.append(final_message)
|
||
self._persist_conversation(partial_summary=assistant_message[:200])
|
||
|
||
if not tool_calls:
|
||
# 多智能体模式:没有 tool_calls 表示本轮结束,进入 idle 等待
|
||
if self.multi_agent_mode:
|
||
self._mark_idle()
|
||
self._idle = True
|
||
self._persist_conversation(partial_summary=assistant_message[:200])
|
||
continue
|
||
# 普通模式:prompt 并要求继续 / finish_task
|
||
self.messages.append({
|
||
"role": "user",
|
||
"content": "如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。",
|
||
})
|
||
continue
|
||
|
||
for tool_call in tool_calls:
|
||
if self._cancelled:
|
||
break
|
||
name = tool_call.get("function", {}).get("name", "")
|
||
args = self._parse_args(tool_call)
|
||
progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
|
||
|
||
if name == "finish_task":
|
||
await self._write_finish(args, elapsed)
|
||
return
|
||
|
||
self.emit("progress", {"id": progress_id, "tool": name, "status": "running", "args": args, "ts": int(time.time() * 1000)})
|
||
result = await self._execute_tool(name, args)
|
||
self.emit("progress", {"id": progress_id, "tool": name, "status": "completed" if result.get("success") else "failed", "args": args, "ts": int(time.time() * 1000)})
|
||
|
||
self._update_stats(name)
|
||
|
||
content = _format_tool_result(name, result)
|
||
if name == "read_mediafile" and result.get("success"):
|
||
content = self._build_media_tool_content(result) or content
|
||
self.messages.append({
|
||
"role": "tool",
|
||
"tool_call_id": tool_call.get("id", progress_id),
|
||
"content": content,
|
||
})
|
||
self._persist_conversation(partial_summary=assistant_message[:200])
|
||
|
||
# 循环结束(取消或 idle 被外部终止)后的清理
|
||
if self.multi_agent_mode and self._cancelled:
|
||
if self.multi_agent_state:
|
||
self.multi_agent_state.mark_status(self.agent_id, "terminated")
|
||
|
||
def _forward_output_to_master(self, output_text: str, *, is_final: bool = False) -> None:
|
||
"""把子智能体的 assistant 文本输出转发成主对话的 user 消息,并写入进度文件供前端查看。"""
|
||
if not self.multi_agent_state:
|
||
return
|
||
# 如果这是对 pending 提问的回答,不走主对话转发,而是返回到 ask 工具结果
|
||
if self._provide_answer(output_text):
|
||
return
|
||
try:
|
||
from modules.multi_agent.state import build_sub_agent_output_text
|
||
msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final)
|
||
self.multi_agent_state.push_master_message(msg)
|
||
# 同时记录到实例状态,供 list_active_sub_agents 使用
|
||
inst = self.multi_agent_state.get_instance(self.agent_id)
|
||
if inst:
|
||
inst.last_output = output_text[:500]
|
||
# 写入进度文件,前端子智能体进度弹窗可直接展示
|
||
self.emit("progress", {
|
||
"subtype": "output",
|
||
"content": output_text,
|
||
"is_final": is_final,
|
||
"ts": int(time.time() * 1000),
|
||
})
|
||
ma_debug(
|
||
"sub_agent_output_forwarded",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
is_final=is_final,
|
||
content_preview=output_text[:300],
|
||
)
|
||
except Exception as exc:
|
||
logger.warning(f"[SubAgentTask] forward output to master failed: {exc}")
|
||
|
||
def _mark_idle(self) -> None:
|
||
"""多智能体模式下,子智能体自然结束即本轮任务结束,进入 idle 状态。"""
|
||
ma_debug(
|
||
"sub_agent_mark_idle",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
)
|
||
if self.multi_agent_state:
|
||
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
||
|
||
def inject_message(self, message_text: str) -> None:
|
||
"""外部向子智能体上下文插入 user 消息,并唤醒 idle 状态。"""
|
||
self.messages.append({"role": "user", "content": message_text})
|
||
ma_debug(
|
||
"sub_agent_message_injected",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
message_preview=str(message_text)[:500],
|
||
was_idle=self._idle,
|
||
)
|
||
# inject_message 可能从其他线程(主对话线程)调用,需要线程安全唤醒。
|
||
# 优先使用子智能体 Task 所属事件循环投递 set(),避免跨线程直接操作 Future。
|
||
if self._continue_event is None:
|
||
self._continue_event = asyncio.Event()
|
||
if self._task is not None:
|
||
try:
|
||
task_loop = self._task.get_loop()
|
||
if task_loop.is_running():
|
||
task_loop.call_soon_threadsafe(self._continue_event.set)
|
||
return
|
||
except Exception:
|
||
pass
|
||
self._continue_event.set()
|
||
|
||
def _peek_pending_question_id(self) -> Optional[str]:
|
||
"""检查最后一条 user 消息是否是向本智能体提问,返回 question_id。"""
|
||
if not self.multi_agent_mode or not self.messages:
|
||
return None
|
||
for msg in reversed(self.messages):
|
||
if msg.get("role") == "user":
|
||
content = msg.get("content") or ""
|
||
if "的提问" in content:
|
||
m = _QUESTION_ID_RE.search(content)
|
||
if m:
|
||
qid = m.group(1)
|
||
if qid not in self._answered_question_ids:
|
||
return qid
|
||
break
|
||
return None
|
||
|
||
def _provide_answer(self, output_text: str) -> bool:
|
||
"""如果当前输出是对 pending 提问的回答,把回答写回 future 并阻止转发到主对话。"""
|
||
if not self._pending_answer_question_id or not self.multi_agent_state:
|
||
return False
|
||
self.multi_agent_state.provide_answer(
|
||
self._pending_answer_question_id,
|
||
output_text.strip(),
|
||
)
|
||
self._answered_question_ids.add(self._pending_answer_question_id)
|
||
self._pending_answer_question_id = None
|
||
return True
|
||
|
||
def _build_client(self) -> tuple:
|
||
"""加载模型配置并初始化 DeepSeekClient。"""
|
||
config_path = self.manager.models_config_file
|
||
models: List[Dict[str, Any]] = []
|
||
default_key = ""
|
||
if Path(config_path).exists():
|
||
try:
|
||
raw = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
||
models = raw.get("models", []) if isinstance(raw, dict) else (raw if isinstance(raw, list) else [])
|
||
default_key = str(raw.get("default_model", "")) if isinstance(raw, dict) else ""
|
||
except Exception as exc:
|
||
logger.error(f"[SubAgent] 加载模型配置失败: {exc}")
|
||
|
||
model_map = {}
|
||
valid_models = []
|
||
for item in models:
|
||
profile = _build_sub_agent_profile(item)
|
||
if profile:
|
||
key = profile["name"]
|
||
model_map[key] = profile
|
||
valid_models.append(key)
|
||
|
||
chosen_key = self.model_key or default_key
|
||
if chosen_key not in model_map and valid_models:
|
||
chosen_key = valid_models[0]
|
||
if chosen_key not in model_map:
|
||
raise RuntimeError(f"未找到可用子智能体模型配置: {config_path}")
|
||
|
||
client = DeepSeekClient(thinking_mode=(self.thinking_mode == "thinking"), web_mode=True)
|
||
client.model_key = chosen_key
|
||
client.project_path = str(self.manager.project_path)
|
||
if self.thinking_mode == "thinking":
|
||
# 子智能体的 thinking 模式应全程使用思考模型
|
||
client.deep_thinking_session = True
|
||
client.apply_profile(model_map[chosen_key])
|
||
return client, chosen_key
|
||
|
||
async def _call_model(
|
||
self,
|
||
client: DeepSeekClient,
|
||
model_key: str,
|
||
tools: List[Dict[str, Any]],
|
||
) -> tuple:
|
||
"""调用模型并解析 assistant 消息。"""
|
||
assistant_message = ""
|
||
reasoning = ""
|
||
tool_calls: List[Dict[str, Any]] = []
|
||
usage = None
|
||
|
||
async for chunk in client.chat(self.messages, tools=tools, stream=True):
|
||
if self._cancelled:
|
||
break
|
||
if chunk.get("error"):
|
||
raise RuntimeError(f"API 调用失败: {chunk.get('error')}")
|
||
choice = (chunk.get("choices") or [{}])[0]
|
||
delta = choice.get("delta") or {}
|
||
if delta.get("content"):
|
||
assistant_message += delta["content"]
|
||
if delta.get("reasoning_content"):
|
||
reasoning += delta["reasoning_content"]
|
||
elif delta.get("reasoning_details"):
|
||
rd = delta["reasoning_details"]
|
||
if isinstance(rd, list):
|
||
reasoning += "".join(str(d.get("text") or "") for d in rd)
|
||
elif isinstance(rd, str):
|
||
reasoning += rd
|
||
elif isinstance(rd, dict):
|
||
reasoning += str(rd.get("text") or "")
|
||
|
||
for tc in delta.get("tool_calls") or []:
|
||
idx = tc.get("index")
|
||
if idx is None:
|
||
continue
|
||
while len(tool_calls) <= idx:
|
||
tool_calls.append({"id": "", "type": "function", "function": {"name": "", "arguments": ""}})
|
||
existing = tool_calls[idx]
|
||
if tc.get("id"):
|
||
existing["id"] = tc["id"]
|
||
fn = tc.get("function") or {}
|
||
if fn.get("name"):
|
||
existing["function"]["name"] += fn["name"]
|
||
if fn.get("arguments"):
|
||
existing["function"]["arguments"] += fn["arguments"]
|
||
|
||
if chunk.get("usage"):
|
||
usage = chunk["usage"]
|
||
|
||
return assistant_message, reasoning, tool_calls, usage
|
||
|
||
def _parse_args(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
||
raw = tool_call.get("function", {}).get("arguments") or "{}"
|
||
try:
|
||
return json.loads(raw)
|
||
except Exception:
|
||
return {"_raw": raw}
|
||
|
||
async def _execute_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""通过 manager 调用主进程执行工具。
|
||
|
||
多智能体模式下,对于通信工具(ask_master / ask_other_agent / answer_other_agent/
|
||
list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。
|
||
"""
|
||
if self.multi_agent_mode and self.multi_agent_state:
|
||
result = await self._execute_multi_agent_tool(name, args)
|
||
if result is not None:
|
||
return result
|
||
return await self.manager.execute_tool_for_sub_agent(name, args)
|
||
|
||
async def _execute_multi_agent_tool(self, name: str, args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
"""处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。"""
|
||
state = self.multi_agent_state
|
||
if not state:
|
||
return None
|
||
try:
|
||
if name == "ask_master":
|
||
question = str(args.get("question") or "").strip()
|
||
question_id = str(args.get("question_id") or f"ask_master_{uuid.uuid4().hex[:10]}")
|
||
ma_debug(
|
||
"sub_agent_tool_ask_master",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
question_id=question_id,
|
||
question=question[:500],
|
||
)
|
||
if not question:
|
||
return {"success": False, "error": "question 不能为空"}
|
||
# 插入到主对话
|
||
from modules.multi_agent.state import build_sub_agent_ask_master_text
|
||
msg = build_sub_agent_ask_master_text(self.display_name, question, question_id)
|
||
state.push_master_message(msg)
|
||
inst = state.get_instance(self.agent_id)
|
||
if inst:
|
||
inst.last_output = f"[ask_master] {question[:200]}"
|
||
# 阻塞等待回答(状态标为正在等待主智能体回答)
|
||
state.mark_status(self.agent_id, "running")
|
||
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
|
||
state.mark_status(self.agent_id, "running")
|
||
return {"success": True, "answer": answer, "question_id": question_id}
|
||
|
||
if name == "ask_other_agent":
|
||
target_id = int(args.get("target_agent_id") or 0)
|
||
question = str(args.get("question") or "").strip()
|
||
question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}")
|
||
ma_debug(
|
||
"sub_agent_tool_ask_other_agent",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
target_agent_id=target_id,
|
||
question_id=question_id,
|
||
question=question[:500],
|
||
)
|
||
if not target_id or not question:
|
||
return {"success": False, "error": "参数缺失"}
|
||
# 查找目标实例
|
||
target_inst = state.get_instance(target_id)
|
||
if not target_inst:
|
||
return {"success": False, "error": f"agent {target_id} 不存在"}
|
||
# 构造提问消息并插入到目标子对话;同时要求其在下一轮调用 answer_other_agent
|
||
from modules.multi_agent.state import build_sub_agent_ask_other_text
|
||
target_display = target_inst.display_name
|
||
msg = build_sub_agent_ask_other_text(self.display_name, target_display, question, question_id)
|
||
self.manager.inject_message_to_sub_agent(target_id, msg)
|
||
# 阻塞等待回答
|
||
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
|
||
return {"success": True, "answer": answer, "question_id": question_id}
|
||
|
||
if name == "answer_other_agent":
|
||
source_id = int(args.get("source_agent_id") or 0)
|
||
question_id = str(args.get("question_id") or "")
|
||
answer = str(args.get("answer") or "").strip()
|
||
if not question_id or not answer:
|
||
return {"success": False, "error": "参数缺失"}
|
||
ok = state.provide_answer(question_id, answer)
|
||
return {"success": bool(ok), "question_id": question_id}
|
||
|
||
if name == "list_active_sub_agents":
|
||
return {"success": True, "agents": [a.to_dict() for a in state.list_all()]}
|
||
except asyncio.TimeoutError:
|
||
return {"success": False, "error": "等待回答超时", "question_id": args.get("question_id")}
|
||
except Exception as exc:
|
||
logger.exception(f"[SubAgent] 多智能体工具异常: {name}")
|
||
return {"success": False, "error": f"多智能体工具异常: {exc}"}
|
||
return None
|
||
|
||
def _update_stats(self, name: str) -> None:
|
||
if name == "read_file":
|
||
self.stats["files_read"] += 1
|
||
elif name == "write_file":
|
||
self.stats["write_files"] += 1
|
||
elif name == "edit_file":
|
||
self.stats["edit_files"] += 1
|
||
elif name == "search_workspace":
|
||
self.stats["searches"] += 1
|
||
elif name in ("web_search", "extract_webpage"):
|
||
self.stats["web_pages"] += 1
|
||
elif name == "run_command":
|
||
self.stats["commands"] += 1
|
||
|
||
def _apply_usage(self, usage: Any) -> None:
|
||
try:
|
||
if isinstance(usage, dict):
|
||
prompt = usage.get("prompt_tokens") or usage.get("prompt") or 0
|
||
completion = usage.get("completion_tokens") or usage.get("completion") or 0
|
||
total = usage.get("total_tokens") or usage.get("total") or (prompt + completion)
|
||
self.stats["token_usage"]["prompt"] += int(prompt)
|
||
self.stats["token_usage"]["completion"] += int(completion)
|
||
self.stats["token_usage"]["total"] += int(total)
|
||
except Exception:
|
||
pass
|
||
|
||
def _build_media_tool_content(self, result: Dict[str, Any]) -> Any:
|
||
"""把 read_mediafile 结果转成 OpenAI 多模态 content。"""
|
||
b64 = result.get("b64")
|
||
mime = result.get("mime")
|
||
file_type = result.get("type")
|
||
if not b64 or not mime:
|
||
return None
|
||
if file_type == "image":
|
||
return [
|
||
{"type": "text", "text": f"已附加图片: {result.get('path')}"},
|
||
{"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}},
|
||
]
|
||
if file_type == "video":
|
||
return [
|
||
{"type": "text", "text": f"已附加视频: {result.get('path')}"},
|
||
{"type": "video_url", "video_url": {"url": f"data:{mime};base64,{b64}"}},
|
||
]
|
||
return None
|
||
|
||
async def _write_finish(self, args: Dict[str, Any], elapsed: float) -> None:
|
||
success = bool(args.get("success", False))
|
||
summary = str(args.get("summary") or "").strip()
|
||
self._finalize_task(success, summary, elapsed)
|
||
|
||
async def _write_timeout(self, elapsed: float) -> None:
|
||
self._finalize_task(False, "任务超时未完成", elapsed, timeout=True)
|
||
|
||
async def _write_failure(self, message: str, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
||
elapsed = time.time() - (self.stats["runtime_start"] / 1000)
|
||
ma_debug(
|
||
"sub_agent_write_failure",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
message=message,
|
||
max_turns_exceeded=max_turns_exceeded,
|
||
timeout=timeout,
|
||
idle=self._idle,
|
||
cancelled=self._cancelled,
|
||
)
|
||
self._finalize_task(False, message, elapsed, max_turns_exceeded=max_turns_exceeded, timeout=timeout)
|
||
|
||
def _persist_conversation(self, *, partial_summary: str = "") -> None:
|
||
"""每轮结束后立即落盘子智能体对话,避免跑完了才存一次导致中间状态丢失。"""
|
||
try:
|
||
runtime_seconds = int((time.time() * 1000 - self.stats["runtime_start"]) / 1000)
|
||
status = "running"
|
||
if self._cancelled:
|
||
status = "terminated"
|
||
elif self.multi_agent_mode and self._idle:
|
||
status = "idle"
|
||
ma_debug(
|
||
"sub_agent_persist_conversation",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
status=status,
|
||
idle=self._idle,
|
||
cancelled=self._cancelled,
|
||
multi_agent_mode=self.multi_agent_mode,
|
||
)
|
||
conversation_data = {
|
||
"agent_id": self.agent_id,
|
||
"task_id": self.task_id,
|
||
"created_at": datetime.fromtimestamp(self.stats["runtime_start"] / 1000).isoformat(),
|
||
"updated_at": datetime.now().isoformat(),
|
||
"status": status,
|
||
"success": None,
|
||
"summary": partial_summary,
|
||
"messages": self.messages,
|
||
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
||
}
|
||
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
||
self.conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
|
||
|
||
output_data = {
|
||
"success": None,
|
||
"status": status,
|
||
"summary": partial_summary,
|
||
"stats": conversation_data["stats"],
|
||
}
|
||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||
self.output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||
except Exception as exc:
|
||
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
|
||
|
||
def _finalize_task(self, success: bool, summary: str, elapsed: float, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
||
runtime_seconds = int(elapsed)
|
||
ma_debug(
|
||
"sub_agent_finalize_task",
|
||
task_id=self.task_id,
|
||
agent_id=self.agent_id,
|
||
display_name=self.display_name,
|
||
success=success,
|
||
summary=summary,
|
||
idle=self._idle,
|
||
cancelled=self._cancelled,
|
||
)
|
||
output_data = {
|
||
"success": success,
|
||
"summary": summary,
|
||
"timeout": timeout,
|
||
"max_turns_exceeded": max_turns_exceeded,
|
||
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
||
}
|
||
conversation_data = {
|
||
"agent_id": self.agent_id,
|
||
"created_at": datetime.fromtimestamp(self.stats["runtime_start"] / 1000).isoformat(),
|
||
"completed_at": datetime.now().isoformat(),
|
||
"success": success,
|
||
"summary": summary,
|
||
"messages": self.messages,
|
||
"stats": output_data["stats"],
|
||
}
|
||
stats_data = {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)}
|
||
|
||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||
self.output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||
self.stats_file.write_text(json.dumps(stats_data, ensure_ascii=False), encoding="utf-8")
|
||
self.conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
|
||
|
||
self.emit("output", output_data)
|
||
self.emit("conversation", conversation_data)
|
||
|
||
self.manager._mark_task_done(self.task_id, success, summary, runtime_seconds)
|
||
|
||
def cancel(self) -> None:
|
||
self._cancelled = True
|
||
if self._task and not self._task.done():
|
||
self._task.cancel()
|