client.py 8.0 KB

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