runner.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. """
  2. Agent Runner - Agent 执行引擎
  3. 核心职责:
  4. 1. 执行 Agent 任务(循环调用 LLM + 工具)
  5. 2. 记录执行轨迹(Trace + Messages + GoalTree)
  6. 3. 检索和注入记忆(Experience + Skill)
  7. 4. 管理执行计划(GoalTree)
  8. 5. 收集反馈,提取经验
  9. """
  10. from agent.tools.builtin.browser import browser_read_long_content
  11. import logging
  12. from dataclasses import dataclass
  13. from datetime import datetime
  14. from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal, Union
  15. from agent.trace.models import Trace, Message
  16. from agent.trace.protocols import TraceStore
  17. from agent.trace.goal_models import GoalTree
  18. from agent.memory.models import Experience, Skill
  19. from agent.memory.protocols import MemoryStore, StateStore
  20. from agent.memory.skill_loader import load_skills_from_dir
  21. from agent.tools import ToolRegistry, get_tool_registry
  22. logger = logging.getLogger(__name__)
  23. @dataclass
  24. class AgentConfig:
  25. """Agent 配置"""
  26. agent_type: str = "default"
  27. max_iterations: int = 200
  28. enable_memory: bool = True
  29. auto_execute_tools: bool = True
  30. @dataclass
  31. class CallResult:
  32. """单次调用结果"""
  33. reply: str
  34. tool_calls: Optional[List[Dict]] = None
  35. trace_id: Optional[str] = None
  36. step_id: Optional[str] = None
  37. tokens: Optional[Dict[str, int]] = None
  38. cost: float = 0.0
  39. # 内置工具列表(始终自动加载)
  40. BUILTIN_TOOLS = [
  41. # 文件操作工具
  42. "read_file",
  43. "edit_file",
  44. "write_file",
  45. "glob_files",
  46. "grep_content",
  47. # 系统工具
  48. "bash_command",
  49. # 技能和目标管理
  50. "skill",
  51. "list_skills",
  52. "goal",
  53. "subagent",
  54. # 搜索工具
  55. "search_posts",
  56. "get_search_suggestions",
  57. # 沙箱工具
  58. "sandbox_create_environment",
  59. "sandbox_run_shell",
  60. "sandbox_rebuild_with_ports",
  61. "sandbox_destroy_environment",
  62. # 浏览器工具
  63. "browser_navigate_to_url",
  64. "browser_search_web",
  65. "browser_go_back",
  66. "browser_wait",
  67. "browser_click_element",
  68. "browser_input_text",
  69. "browser_send_keys",
  70. "browser_upload_file",
  71. "browser_scroll_page",
  72. "browser_find_text",
  73. "browser_screenshot",
  74. "browser_switch_tab",
  75. "browser_close_tab",
  76. "browser_get_dropdown_options",
  77. "browser_select_dropdown_option",
  78. "browser_extract_content",
  79. "browser_read_long_content",
  80. "browser_get_page_html",
  81. "browser_get_selector_map",
  82. "browser_evaluate",
  83. "browser_ensure_login_with_cookies",
  84. "browser_wait_for_user_action",
  85. "browser_done",
  86. # 飞书工具
  87. "feishu_get_chat_history",
  88. "feishu_get_contact_replies",
  89. "feishu_send_message_to_contact",
  90. "feishu_get_contact_list",
  91. ]
  92. class AgentRunner:
  93. """
  94. Agent 执行引擎
  95. 支持两种模式:
  96. 1. call(): 单次 LLM 调用(简洁 API)
  97. 2. run(): Agent 模式(循环 + 记忆 + 追踪)
  98. """
  99. def __init__(
  100. self,
  101. trace_store: Optional[TraceStore] = None,
  102. memory_store: Optional[MemoryStore] = None,
  103. state_store: Optional[StateStore] = None,
  104. tool_registry: Optional[ToolRegistry] = None,
  105. llm_call: Optional[Callable] = None,
  106. config: Optional[AgentConfig] = None,
  107. skills_dir: Optional[str] = None,
  108. goal_tree: Optional[GoalTree] = None,
  109. debug: bool = False,
  110. ):
  111. """
  112. 初始化 AgentRunner
  113. Args:
  114. trace_store: Trace 存储(可选,不提供则不记录)
  115. memory_store: Memory 存储(可选,不提供则不使用记忆)
  116. state_store: State 存储(可选,用于任务状态)
  117. tool_registry: 工具注册表(可选,默认使用全局注册表)
  118. llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
  119. config: Agent 配置
  120. skills_dir: Skills 目录路径(可选,不提供则不加载 skills)
  121. goal_tree: 执行计划(可选,不提供则在运行时按需创建)
  122. debug: 保留参数(已废弃,请使用 API Server 可视化)
  123. """
  124. self.trace_store = trace_store
  125. self.memory_store = memory_store
  126. self.state_store = state_store
  127. self.tools = tool_registry or get_tool_registry()
  128. self.llm_call = llm_call
  129. self.config = config or AgentConfig()
  130. self.skills_dir = skills_dir
  131. self.goal_tree = goal_tree
  132. self.debug = debug
  133. def _generate_id(self) -> str:
  134. """生成唯一 ID"""
  135. import uuid
  136. return str(uuid.uuid4())
  137. # ===== 单次调用 =====
  138. async def call(
  139. self,
  140. messages: List[Dict],
  141. model: str = "gpt-4o",
  142. tools: Optional[List[str]] = None,
  143. uid: Optional[str] = None,
  144. trace: bool = True,
  145. **kwargs
  146. ) -> CallResult:
  147. """
  148. 单次 LLM 调用
  149. Args:
  150. messages: 消息列表
  151. model: 模型名称
  152. tools: 工具名称列表
  153. uid: 用户 ID
  154. trace: 是否记录 Trace
  155. **kwargs: 其他参数传递给 LLM
  156. Returns:
  157. CallResult
  158. """
  159. if not self.llm_call:
  160. raise ValueError("llm_call function not provided")
  161. trace_id = None
  162. message_id = None
  163. # 准备工具 Schema
  164. tool_names = BUILTIN_TOOLS.copy()
  165. if tools:
  166. for tool in tools:
  167. if tool not in tool_names:
  168. tool_names.append(tool)
  169. tool_schemas = self.tools.get_schemas(tool_names)
  170. # 创建 Trace
  171. if trace and self.trace_store:
  172. trace_obj = Trace.create(
  173. mode="call",
  174. uid=uid,
  175. model=model,
  176. tools=tool_schemas, # 保存工具定义
  177. llm_params=kwargs, # 保存 LLM 参数
  178. )
  179. trace_id = await self.trace_store.create_trace(trace_obj)
  180. # 调用 LLM
  181. result = await self.llm_call(
  182. messages=messages,
  183. model=model,
  184. tools=tool_schemas,
  185. **kwargs
  186. )
  187. # 记录 Message(单次调用模式不使用 GoalTree)
  188. if trace and self.trace_store and trace_id:
  189. msg = Message.create(
  190. trace_id=trace_id,
  191. role="assistant",
  192. sequence=1,
  193. goal_id=None, # 单次调用没有 goal
  194. content={"text": result.get("content", ""), "tool_calls": result.get("tool_calls")},
  195. prompt_tokens=result.get("prompt_tokens", 0),
  196. completion_tokens=result.get("completion_tokens", 0),
  197. finish_reason=result.get("finish_reason"),
  198. cost=result.get("cost", 0),
  199. )
  200. message_id = await self.trace_store.add_message(msg)
  201. # 完成 Trace
  202. await self.trace_store.update_trace(
  203. trace_id,
  204. status="completed",
  205. completed_at=datetime.now(),
  206. )
  207. return CallResult(
  208. reply=result.get("content", ""),
  209. tool_calls=result.get("tool_calls"),
  210. trace_id=trace_id,
  211. step_id=message_id, # 兼容字段名
  212. tokens={
  213. "prompt": result.get("prompt_tokens", 0),
  214. "completion": result.get("completion_tokens", 0),
  215. },
  216. cost=result.get("cost", 0)
  217. )
  218. # ===== Agent 模式 =====
  219. async def run_result(
  220. self,
  221. task: str,
  222. messages: Optional[List[Dict]] = None,
  223. system_prompt: Optional[str] = None,
  224. model: str = "gpt-4o",
  225. tools: Optional[List[str]] = None,
  226. agent_type: Optional[str] = None,
  227. uid: Optional[str] = None,
  228. max_iterations: Optional[int] = None,
  229. enable_memory: Optional[bool] = None,
  230. auto_execute_tools: Optional[bool] = None,
  231. trace_id: Optional[str] = None,
  232. **kwargs
  233. ) -> Dict[str, Any]:
  234. """
  235. Agent 结果模式执行。
  236. 消费 run() 的流式事件,返回结构化结果(最后一条有文本的 assistant + trace 统计)。
  237. """
  238. last_assistant_text = ""
  239. final_trace: Optional[Trace] = None
  240. async for item in self.run(
  241. task=task,
  242. messages=messages,
  243. system_prompt=system_prompt,
  244. model=model,
  245. tools=tools,
  246. agent_type=agent_type,
  247. uid=uid,
  248. max_iterations=max_iterations,
  249. enable_memory=enable_memory,
  250. auto_execute_tools=auto_execute_tools,
  251. trace_id=trace_id,
  252. **kwargs
  253. ):
  254. if isinstance(item, Message) and item.role == "assistant":
  255. content = item.content
  256. text = ""
  257. if isinstance(content, dict):
  258. text = content.get("text", "") or ""
  259. elif isinstance(content, str):
  260. text = content
  261. if text and text.strip():
  262. last_assistant_text = text
  263. elif isinstance(item, Trace):
  264. final_trace = item
  265. if not final_trace and trace_id and self.trace_store:
  266. final_trace = await self.trace_store.get_trace(trace_id)
  267. status = final_trace.status if final_trace else "unknown"
  268. error = final_trace.error_message if final_trace else None
  269. summary = last_assistant_text
  270. if not summary:
  271. status = "failed"
  272. error = error or "Sub-Agent 没有产生 assistant 文本结果"
  273. return {
  274. "status": status,
  275. "summary": summary,
  276. "trace_id": final_trace.trace_id if final_trace else trace_id,
  277. "error": error,
  278. "stats": {
  279. "total_messages": final_trace.total_messages if final_trace else 0,
  280. "total_tokens": final_trace.total_tokens if final_trace else 0,
  281. "total_cost": final_trace.total_cost if final_trace else 0.0,
  282. },
  283. }
  284. async def run(
  285. self,
  286. task: str,
  287. messages: Optional[List[Dict]] = None,
  288. system_prompt: Optional[str] = None,
  289. model: str = "gpt-4o",
  290. tools: Optional[List[str]] = None,
  291. agent_type: Optional[str] = None,
  292. uid: Optional[str] = None,
  293. max_iterations: Optional[int] = None,
  294. enable_memory: Optional[bool] = None,
  295. auto_execute_tools: Optional[bool] = None,
  296. trace_id: Optional[str] = None,
  297. **kwargs
  298. ) -> AsyncIterator[Union[Trace, Message]]:
  299. """
  300. Agent 模式执行
  301. Args:
  302. task: 任务描述
  303. messages: 初始消息(可选)
  304. system_prompt: 系统提示(可选)
  305. model: 模型名称
  306. tools: 工具名称列表
  307. agent_type: Agent 类型
  308. uid: 用户 ID
  309. max_iterations: 最大迭代次数
  310. enable_memory: 是否启用记忆
  311. auto_execute_tools: 是否自动执行工具
  312. trace_id: Trace ID(可选,传入时复用已有 Trace)
  313. **kwargs: 其他参数
  314. Yields:
  315. Union[Trace, Message]: Trace 对象(状态变化)或 Message 对象(执行过程)
  316. """
  317. if not self.llm_call:
  318. raise ValueError("llm_call function not provided")
  319. # 使用配置默认值
  320. agent_type = agent_type or self.config.agent_type
  321. max_iterations = max_iterations or self.config.max_iterations
  322. enable_memory = enable_memory if enable_memory is not None else self.config.enable_memory
  323. auto_execute_tools = auto_execute_tools if auto_execute_tools is not None else self.config.auto_execute_tools
  324. # 准备工具 Schema(提前准备,用于 Trace)
  325. tool_names = BUILTIN_TOOLS.copy()
  326. if tools:
  327. for tool in tools:
  328. if tool not in tool_names:
  329. tool_names.append(tool)
  330. tool_schemas = self.tools.get_schemas(tool_names)
  331. # 创建或复用 Trace
  332. if trace_id:
  333. if self.trace_store:
  334. trace_obj = await self.trace_store.get_trace(trace_id)
  335. if not trace_obj:
  336. raise ValueError(f"Trace not found: {trace_id}")
  337. else:
  338. trace_obj = Trace(
  339. trace_id=trace_id,
  340. mode="agent",
  341. task=task,
  342. agent_type=agent_type,
  343. uid=uid,
  344. model=model,
  345. tools=tool_schemas,
  346. llm_params=kwargs,
  347. status="running"
  348. )
  349. else:
  350. trace_id = self._generate_id()
  351. trace_obj = Trace(
  352. trace_id=trace_id,
  353. mode="agent",
  354. task=task,
  355. agent_type=agent_type,
  356. uid=uid,
  357. model=model,
  358. tools=tool_schemas, # 保存工具定义
  359. llm_params=kwargs, # 保存 LLM 参数
  360. status="running"
  361. )
  362. if self.trace_store:
  363. await self.trace_store.create_trace(trace_obj)
  364. # 初始化 GoalTree
  365. goal_tree = self.goal_tree or GoalTree(mission=task)
  366. await self.trace_store.update_goal_tree(trace_id, goal_tree)
  367. # 返回 Trace(表示开始)
  368. yield trace_obj
  369. try:
  370. # 加载记忆(Experience 和 Skill)
  371. experiences_text = ""
  372. skills_text = ""
  373. if enable_memory and self.memory_store:
  374. scope = f"agent:{agent_type}"
  375. experiences = await self.memory_store.search_experiences(scope, task)
  376. experiences_text = self._format_experiences(experiences)
  377. logger.info(f"加载 {len(experiences)} 条经验")
  378. # 加载 Skills(内置 + 用户自定义)
  379. skills = load_skills_from_dir(self.skills_dir)
  380. if skills:
  381. skills_text = self._format_skills(skills)
  382. if self.skills_dir:
  383. logger.info(f"加载 {len(skills)} 个 skills (内置 + 自定义: {self.skills_dir})")
  384. else:
  385. logger.info(f"加载 {len(skills)} 个内置 skills")
  386. # 构建初始消息
  387. sequence = 1
  388. if messages is None:
  389. if trace_id and self.trace_store:
  390. existing_messages = await self.trace_store.get_trace_messages(trace_id)
  391. messages = []
  392. for msg in existing_messages:
  393. msg_dict = {"role": msg.role}
  394. if isinstance(msg.content, dict):
  395. if msg.content.get("text"):
  396. msg_dict["content"] = msg.content["text"]
  397. if msg.content.get("tool_calls"):
  398. msg_dict["tool_calls"] = msg.content["tool_calls"]
  399. else:
  400. msg_dict["content"] = msg.content
  401. if msg.role == "tool" and msg.tool_call_id:
  402. msg_dict["tool_call_id"] = msg.tool_call_id
  403. msg_dict["name"] = msg.description or "unknown"
  404. messages.append(msg_dict)
  405. if existing_messages:
  406. sequence = existing_messages[-1].sequence + 1
  407. else:
  408. messages = []
  409. # 记录初始 system 和 user 消息到 trace
  410. if system_prompt and not any(m.get("role") == "system" for m in messages):
  411. # 注入记忆和 skills 到 system prompt
  412. full_system = system_prompt
  413. if skills_text:
  414. full_system += f"\n\n## Skills\n{skills_text}"
  415. if experiences_text:
  416. full_system += f"\n\n## 相关经验\n{experiences_text}"
  417. messages = [{"role": "system", "content": full_system}] + messages
  418. # 保存 system 消息
  419. if self.trace_store:
  420. system_msg = Message.create(
  421. trace_id=trace_id,
  422. role="system",
  423. sequence=sequence,
  424. goal_id=None, # 初始消息没有 goal
  425. content=full_system,
  426. )
  427. await self.trace_store.add_message(system_msg)
  428. yield system_msg
  429. sequence += 1
  430. # 添加任务描述(支持 continue_from 场景再次追加)
  431. if task:
  432. messages.append({"role": "user", "content": task})
  433. # 保存 user 消息(任务描述)
  434. if self.trace_store:
  435. user_msg = Message.create(
  436. trace_id=trace_id,
  437. role="user",
  438. sequence=sequence,
  439. goal_id=None, # 初始消息没有 goal
  440. content=task,
  441. )
  442. await self.trace_store.add_message(user_msg)
  443. yield user_msg
  444. sequence += 1
  445. # 获取 GoalTree
  446. goal_tree = None
  447. if self.trace_store:
  448. goal_tree = await self.trace_store.get_goal_tree(trace_id)
  449. # 设置 goal_tree 到 goal 工具(供 LLM 调用)
  450. from agent.trace.goal_tool import set_goal_tree
  451. set_goal_tree(goal_tree)
  452. # 执行循环
  453. for iteration in range(max_iterations):
  454. # 注入当前计划到 messages(如果有 goals)
  455. llm_messages = list(messages)
  456. if goal_tree and goal_tree.goals:
  457. plan_text = f"\n## Current Plan\n\n{goal_tree.to_prompt()}"
  458. # 在最后一条 system 消息之后注入
  459. llm_messages.append({"role": "system", "content": plan_text})
  460. # 调用 LLM
  461. result = await self.llm_call(
  462. messages=llm_messages,
  463. model=model,
  464. tools=tool_schemas,
  465. **kwargs
  466. )
  467. response_content = result.get("content", "")
  468. tool_calls = result.get("tool_calls")
  469. finish_reason = result.get("finish_reason")
  470. prompt_tokens = result.get("prompt_tokens", 0)
  471. completion_tokens = result.get("completion_tokens", 0)
  472. step_tokens = prompt_tokens + completion_tokens
  473. step_cost = result.get("cost", 0)
  474. # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
  475. if goal_tree and not goal_tree.goals and tool_calls:
  476. has_goal_call = any(
  477. tc.get("function", {}).get("name") == "goal"
  478. for tc in tool_calls
  479. )
  480. if not has_goal_call:
  481. root_desc = goal_tree.mission[:200] if len(goal_tree.mission) > 200 else goal_tree.mission
  482. goal_tree.add_goals(
  483. descriptions=[root_desc],
  484. reasons=["系统自动创建:Agent 未显式创建目标"],
  485. parent_id=None
  486. )
  487. goal_tree.focus(goal_tree.goals[0].id)
  488. if self.trace_store:
  489. await self.trace_store.update_goal_tree(trace_id, goal_tree)
  490. await self.trace_store.add_goal(trace_id, goal_tree.goals[0])
  491. logger.info(f"自动创建 root goal: {goal_tree.goals[0].id}")
  492. # 获取当前 goal_id
  493. current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
  494. # 记录 assistant Message
  495. assistant_msg = Message.create(
  496. trace_id=trace_id,
  497. role="assistant",
  498. sequence=sequence,
  499. goal_id=current_goal_id,
  500. content={"text": response_content, "tool_calls": tool_calls},
  501. prompt_tokens=prompt_tokens,
  502. completion_tokens=completion_tokens,
  503. finish_reason=finish_reason,
  504. cost=step_cost,
  505. )
  506. if self.trace_store:
  507. await self.trace_store.add_message(assistant_msg)
  508. # WebSocket 广播由 add_message 内部的 append_event 触发
  509. yield assistant_msg
  510. sequence += 1
  511. # 处理工具调用
  512. if tool_calls and auto_execute_tools:
  513. # 添加 assistant 消息到对话历史
  514. messages.append({
  515. "role": "assistant",
  516. "content": response_content,
  517. "tool_calls": tool_calls,
  518. })
  519. for tc in tool_calls:
  520. # 每次工具执行前重新获取最新的 goal_id(处理并行 tool_calls 的情况)
  521. current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
  522. tool_name = tc["function"]["name"]
  523. tool_args = tc["function"]["arguments"]
  524. # 解析参数
  525. if isinstance(tool_args, str):
  526. if tool_args.strip(): # 非空字符串
  527. import json
  528. tool_args = json.loads(tool_args)
  529. else:
  530. tool_args = {} # 空字符串转换为空字典
  531. elif tool_args is None:
  532. tool_args = {} # None 转换为空字典
  533. # 执行工具(统一处理,传递 context)
  534. tool_result = await self.tools.execute(
  535. tool_name,
  536. tool_args,
  537. uid=uid or "",
  538. context={
  539. "store": self.trace_store,
  540. "trace_id": trace_id,
  541. "goal_id": current_goal_id,
  542. "runner": self,
  543. }
  544. )
  545. # 记录 tool Message
  546. tool_msg = Message.create(
  547. trace_id=trace_id,
  548. role="tool",
  549. sequence=sequence,
  550. goal_id=current_goal_id,
  551. tool_call_id=tc["id"],
  552. content={"tool_name": tool_name, "result": tool_result},
  553. )
  554. if self.trace_store:
  555. await self.trace_store.add_message(tool_msg)
  556. yield tool_msg
  557. sequence += 1
  558. # 添加到消息历史
  559. messages.append({
  560. "role": "tool",
  561. "tool_call_id": tc["id"],
  562. "name": tool_name,
  563. "content": str(tool_result),
  564. })
  565. continue # 继续循环
  566. # 无工具调用,任务完成
  567. break
  568. # 完成 Trace
  569. if self.trace_store:
  570. trace_obj = await self.trace_store.get_trace(trace_id)
  571. if trace_obj:
  572. await self.trace_store.update_trace(
  573. trace_id,
  574. status="completed",
  575. completed_at=datetime.now(),
  576. )
  577. # 重新获取更新后的 Trace 并返回
  578. trace_obj = await self.trace_store.get_trace(trace_id)
  579. if trace_obj:
  580. yield trace_obj
  581. except Exception as e:
  582. logger.error(f"Agent run failed: {e}")
  583. if self.trace_store:
  584. await self.trace_store.update_trace(
  585. trace_id,
  586. status="failed",
  587. error_message=str(e),
  588. completed_at=datetime.now()
  589. )
  590. trace_obj = await self.trace_store.get_trace(trace_id)
  591. if trace_obj:
  592. yield trace_obj
  593. raise
  594. # ===== 辅助方法 =====
  595. def _format_skills(self, skills: List[Skill]) -> str:
  596. """格式化技能为 Prompt 文本"""
  597. if not skills:
  598. return ""
  599. return "\n\n".join(s.to_prompt_text() for s in skills)
  600. def _format_experiences(self, experiences: List[Experience]) -> str:
  601. """格式化经验为 Prompt 文本"""
  602. if not experiences:
  603. return ""
  604. return "\n".join(f"- {e.to_prompt_text()}" for e in experiences)