client.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """
  2. Agent SDK 入口 —— 统一调用 remote / 本地 Agent 的公开 API。
  3. 使用方式(任何进程,只要装了 cyber-agent 包):
  4. import asyncio
  5. from agent import invoke_agent
  6. # 远端(HTTP 调用 KnowHub 服务器)
  7. result = asyncio.run(invoke_agent(
  8. agent_type="remote_librarian",
  9. task="ControlNet 相关的工具知识",
  10. skills=["ask_strategy"],
  11. ))
  12. # 本地(在当前进程起 AgentRunner)
  13. result = asyncio.run(invoke_agent(
  14. agent_type="deconstruct",
  15. task="...",
  16. project_root="./examples/production_plan",
  17. ))
  18. skill 脚本只需要 `from agent import invoke_agent` 然后透传命令行参数即可——
  19. 不依赖仓库相对路径,也不会触发 `agent.tools.builtin` 的 eager tool registry 加载。
  20. """
  21. import importlib
  22. import importlib.util
  23. import logging
  24. import os
  25. import sys
  26. from pathlib import Path
  27. from typing import Any, Dict, List, Optional
  28. logger = logging.getLogger(__name__)
  29. async def invoke_agent(
  30. agent_type: str,
  31. task: str,
  32. skills: Optional[List[str]] = None,
  33. continue_from: Optional[str] = None,
  34. messages: Optional[List[Dict[str, Any]]] = None,
  35. project_root: Optional[str] = None,
  36. ) -> Dict[str, Any]:
  37. """
  38. 统一调用远端或本地 Agent。
  39. Args:
  40. agent_type: Agent 类型。以 "remote_" 开头 → HTTP 调用 KnowHub;否则本地执行。
  41. task: 任务描述
  42. skills: 指定 skill 列表(远端由服务器白名单过滤;本地覆盖项目 RUN_CONFIG.skills)
  43. continue_from: 已有 sub_trace_id,传入则续跑
  44. messages: 预置 OpenAI 格式消息(远端 1D、本地 1D)
  45. project_root: 本地 agent 必填——项目目录(含 config.py / presets.json / tools/)
  46. Returns:
  47. {"mode", "agent_type", "sub_trace_id", "status", "summary", "stats", "error"?}
  48. """
  49. if agent_type.startswith("remote_"):
  50. # 懒 import,避免加载整个 tool registry(远端调用只需要 httpx)
  51. from agent.tools.builtin.subagent import _run_remote_agent
  52. return await _run_remote_agent(
  53. agent_type=agent_type,
  54. task=task,
  55. messages=messages,
  56. continue_from=continue_from,
  57. skills=skills,
  58. )
  59. if not project_root:
  60. return {
  61. "mode": "local",
  62. "agent_type": agent_type,
  63. "status": "failed",
  64. "error": "本地 agent 需要 project_root 指定项目目录(含 config.py)",
  65. }
  66. return await _run_local_agent(
  67. agent_type=agent_type,
  68. task=task,
  69. skills=skills,
  70. continue_from=continue_from,
  71. messages=messages,
  72. project_root=project_root,
  73. )
  74. async def _run_local_agent(
  75. agent_type: str,
  76. task: str,
  77. skills: Optional[List[str]],
  78. continue_from: Optional[str],
  79. messages: Optional[List[Dict[str, Any]]],
  80. project_root: str,
  81. ) -> Dict[str, Any]:
  82. """
  83. 在当前进程中起 AgentRunner 跑本地 agent。
  84. 项目目录约定:
  85. project_root/
  86. ├── config.py # 必需:定义 RUN_CONFIG(RunConfig 实例),可选 SKILLS_DIR / TRACE_STORE_PATH
  87. ├── presets.json # 可选:agent_type preset
  88. └── tools/ # 可选:项目自定义工具(有 __init__.py 则 import 触发 @tool 注册)
  89. .env 会自动从 project_root 或上两级目录查找并加载。
  90. """
  91. root = Path(project_root).resolve()
  92. if not root.is_dir():
  93. return {"mode": "local", "agent_type": agent_type, "status": "failed",
  94. "error": f"project_root 不存在: {root}"}
  95. # 1. 把项目根加入 sys.path(让 `import config` / `import tools` 能找到)
  96. if str(root) not in sys.path:
  97. sys.path.insert(0, str(root))
  98. # 2. 加载 .env:依次查项目根、两级父目录(兼容 monorepo)、cyber-agent 仓库根
  99. try:
  100. from dotenv import load_dotenv
  101. import agent as _agent_pkg
  102. agent_repo_root = Path(_agent_pkg.__file__).parent.parent
  103. for candidate in (
  104. root / ".env",
  105. root.parent / ".env",
  106. root.parent.parent / ".env",
  107. agent_repo_root / ".env",
  108. ):
  109. if candidate.exists():
  110. load_dotenv(candidate)
  111. break
  112. except ImportError:
  113. pass
  114. # 3. 加载项目 config
  115. config_path = root / "config.py"
  116. if not config_path.exists():
  117. return {"mode": "local", "agent_type": agent_type, "status": "failed",
  118. "error": f"缺少 {config_path}"}
  119. try:
  120. spec = importlib.util.spec_from_file_location("_project_config", config_path)
  121. cfg_mod = importlib.util.module_from_spec(spec)
  122. spec.loader.exec_module(cfg_mod)
  123. except Exception as e:
  124. return {"mode": "local", "agent_type": agent_type, "status": "failed",
  125. "error": f"加载 {config_path} 失败: {e}"}
  126. run_config = getattr(cfg_mod, "RUN_CONFIG", None)
  127. if run_config is None:
  128. return {"mode": "local", "agent_type": agent_type, "status": "failed",
  129. "error": f"{config_path} 未定义 RUN_CONFIG"}
  130. # 覆盖 agent_type / skills / continue_from
  131. run_config.agent_type = agent_type
  132. if skills is not None:
  133. run_config.skills = skills
  134. if continue_from:
  135. run_config.trace_id = continue_from
  136. # 4. 加载项目 presets.json(如果有)
  137. presets_path = root / "presets.json"
  138. if presets_path.exists():
  139. try:
  140. from agent.core.presets import load_presets_from_json
  141. load_presets_from_json(str(presets_path))
  142. except Exception as e:
  143. logger.warning(f"加载 presets.json 失败: {e}")
  144. # 5. 触发项目自定义工具注册(约定:project_root/tools/__init__.py)
  145. if (root / "tools" / "__init__.py").exists():
  146. try:
  147. importlib.import_module("tools")
  148. except Exception as e:
  149. logger.warning(f"加载 tools 包失败: {e}")
  150. # 6. 创建 AgentRunner
  151. from agent.core.runner import AgentRunner
  152. from agent.trace import FileSystemTraceStore
  153. from agent.llm import create_qwen_llm_call
  154. trace_store_path = getattr(cfg_mod, "TRACE_STORE_PATH", ".trace")
  155. skills_dir = getattr(cfg_mod, "SKILLS_DIR", "./skills")
  156. # 相对路径以 project_root 为基准
  157. if not os.path.isabs(trace_store_path):
  158. trace_store_path = str(root / trace_store_path)
  159. if not os.path.isabs(skills_dir):
  160. skills_dir = str(root / skills_dir)
  161. runner = AgentRunner(
  162. trace_store=FileSystemTraceStore(base_path=trace_store_path),
  163. llm_call=create_qwen_llm_call(model=run_config.model),
  164. skills_dir=skills_dir,
  165. )
  166. # 7. 构建消息
  167. msgs = list(messages) if messages else []
  168. msgs.append({"role": "user", "content": task})
  169. # 8. 运行
  170. try:
  171. result = await runner.run_result(messages=msgs, config=run_config)
  172. except Exception as e:
  173. logger.exception("本地 agent 运行失败")
  174. return {"mode": "local", "agent_type": agent_type, "status": "failed",
  175. "error": f"{type(e).__name__}: {e}"}
  176. return {
  177. "mode": "local",
  178. "agent_type": agent_type,
  179. "sub_trace_id": result.get("trace_id"),
  180. "status": result.get("status", "unknown"),
  181. "summary": result.get("summary", ""),
  182. "stats": result.get("stats", {}),
  183. "error": result.get("error"),
  184. }