protocol_tools.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. """
  2. 协议工具集合
  3. 提供基于协议实现的工具接口:
  4. - MCP Tool: 基于 fastmcp 库,用于连接和调用 MCP 服务器
  5. - A2A Tool: 基于官方 a2a 库,用于 Agent 间通信(需要安装 a2a)
  6. - ANP Tool: 基于概念实现,用于服务发现和网络管理
  7. """
  8. from typing import Dict, Any, List, Optional
  9. from ..base import Tool, ToolParameter
  10. import os
  11. # todo: 修改by xc
  12. import gc
  13. import asyncio
  14. import sys
  15. if sys.platform == "win32":
  16. # Windows 10+: 使用 SelectorEventLoop 替代 ProactorEventLoop,
  17. # 可避免 GetQueuedCompletionStatus 阻塞问题
  18. if sys.version_info >= (3, 8):
  19. asyncio.set_event_loop_policy(
  20. asyncio.WindowsSelectorEventLoopPolicy()
  21. )
  22. # MCP服务器环境变量映射表
  23. # 用于自动检测常见MCP服务器需要的环境变量
  24. MCP_SERVER_ENV_MAP = {
  25. "server-github": ["GITHUB_PERSONAL_ACCESS_TOKEN"],
  26. "server-slack": ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"],
  27. "server-google-drive": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN"],
  28. "server-postgres": ["POSTGRES_CONNECTION_STRING"],
  29. "server-sqlite": [], # 不需要环境变量
  30. "server-filesystem": [], # 不需要环境变量
  31. }
  32. class MCPTool(Tool):
  33. """MCP (Model Context Protocol) 工具
  34. 连接到 MCP 服务器并调用其提供的工具、资源和提示词。
  35. 功能:
  36. - 列出服务器提供的工具
  37. - 调用服务器工具
  38. - 读取服务器资源
  39. - 获取提示词模板
  40. 使用示例:
  41. >>> from hello_agents.tools.builtin import MCPTool
  42. >>>
  43. >>> # 方式1: 使用内置演示服务器
  44. >>> tool = MCPTool() # 自动创建内置服务器
  45. >>> result = tool.run({"action": "list_tools"})
  46. >>>
  47. >>> # 方式2: 连接到外部 MCP 服务器
  48. >>> tool = MCPTool(server_command=["python", "examples/mcp_example.py"])
  49. >>> result = tool.run({"action": "list_tools"})
  50. >>>
  51. >>> # 方式3: 使用自定义 FastMCP 服务器
  52. >>> from fastmcp import FastMCP
  53. >>> server = FastMCP("MyServer")
  54. >>> tool = MCPTool(server=server)
  55. 注意:使用 fastmcp 库,已包含在依赖中
  56. """
  57. def __init__(self,
  58. name: str = "mcp",
  59. description: Optional[str] = None,
  60. server_command: Optional[List[str]] = None,
  61. server_args: Optional[List[str]] = None,
  62. server: Optional[Any] = None,
  63. auto_expand: bool = True,
  64. env: Optional[Dict[str, str]] = None,
  65. env_keys: Optional[List[str]] = None):
  66. """
  67. 初始化 MCP 工具
  68. Args:
  69. name: 工具名称(默认为"mcp",建议为不同服务器指定不同名称)
  70. description: 工具描述(可选,默认为通用描述)
  71. server_command: 服务器启动命令(如 ["python", "server.py"])
  72. server_args: 服务器参数列表
  73. server: FastMCP 服务器实例(可选,用于内存传输)
  74. auto_expand: 是否自动展开为独立工具(默认True)
  75. env: 环境变量字典(优先级最高,直接传递给MCP服务器)
  76. env_keys: 要从系统环境变量加载的key列表(优先级中等)
  77. 环境变量优先级(从高到低):
  78. 1. 直接传递的env参数
  79. 2. env_keys指定的环境变量
  80. 3. 自动检测的环境变量(根据server_command)
  81. 注意:如果所有参数都为空,将创建内置演示服务器
  82. 示例:
  83. >>> # 方式1:直接传递环境变量(优先级最高)
  84. >>> github_tool = MCPTool(
  85. ... name="github",
  86. ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"],
  87. ... env={"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"}
  88. ... )
  89. >>>
  90. >>> # 方式2:从.env文件加载指定的环境变量
  91. >>> github_tool = MCPTool(
  92. ... name="github",
  93. ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"],
  94. ... env_keys=["GITHUB_PERSONAL_ACCESS_TOKEN"]
  95. ... )
  96. >>>
  97. >>> # 方式3:自动检测(最简单,推荐)
  98. >>> github_tool = MCPTool(
  99. ... name="github",
  100. ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"]
  101. ... # 自动从环境变量加载GITHUB_PERSONAL_ACCESS_TOKEN
  102. ... )
  103. """
  104. self.server_command = server_command
  105. self.server_args = server_args or []
  106. self.server = server
  107. self._client = None
  108. self._available_tools = []
  109. self.auto_expand = auto_expand
  110. self.prefix = f"{name}_" if auto_expand else ""
  111. # 环境变量处理(优先级:env > env_keys > 自动检测)
  112. self.env = self._prepare_env(env, env_keys, server_command)
  113. # 如果没有指定任何服务器,创建内置演示服务器
  114. if not server_command and not server:
  115. self.server = self._create_builtin_server()
  116. # 自动发现工具
  117. self._discover_tools()
  118. # 设置默认描述或自动生成
  119. if description is None:
  120. description = self._generate_description()
  121. super().__init__(
  122. name=name,
  123. description=description
  124. )
  125. def _prepare_env(self,
  126. env: Optional[Dict[str, str]],
  127. env_keys: Optional[List[str]],
  128. server_command: Optional[List[str]]) -> Dict[str, str]:
  129. """
  130. 准备环境变量
  131. 优先级:env > env_keys > 自动检测
  132. Args:
  133. env: 直接传递的环境变量字典
  134. env_keys: 要从系统环境变量加载的key列表
  135. server_command: 服务器命令(用于自动检测)
  136. Returns:
  137. 合并后的环境变量字典
  138. """
  139. result_env = {}
  140. # 1. 自动检测(优先级最低)
  141. if server_command:
  142. # 从命令中提取服务器名称
  143. server_name = None
  144. for part in server_command:
  145. if "server-" in part:
  146. # 提取类似 "@modelcontextprotocol/server-github" 中的 "server-github"
  147. server_name = part.split("/")[-1] if "/" in part else part
  148. break
  149. # 查找映射表
  150. if server_name and server_name in MCP_SERVER_ENV_MAP:
  151. auto_keys = MCP_SERVER_ENV_MAP[server_name]
  152. for key in auto_keys:
  153. value = os.getenv(key)
  154. if value:
  155. result_env[key] = value
  156. print(f"🔑 自动加载环境变量: {key}")
  157. # 2. env_keys指定的环境变量(优先级中等)
  158. if env_keys:
  159. for key in env_keys:
  160. value = os.getenv(key)
  161. if value:
  162. result_env[key] = value
  163. print(f"🔑 从env_keys加载环境变量: {key}")
  164. else:
  165. print(f"⚠️ 警告: 环境变量 {key} 未设置")
  166. # 3. 直接传递的env(优先级最高)
  167. if env:
  168. result_env.update(env)
  169. for key in env.keys():
  170. print(f"🔑 使用直接传递的环境变量: {key}")
  171. return result_env
  172. def _create_builtin_server(self):
  173. """创建内置演示服务器"""
  174. try:
  175. from fastmcp import FastMCP
  176. server = FastMCP("HelloAgents-BuiltinServer")
  177. @server.tool()
  178. def add(a: float, b: float) -> float:
  179. """加法计算器"""
  180. return a + b
  181. @server.tool()
  182. def subtract(a: float, b: float) -> float:
  183. """减法计算器"""
  184. return a - b
  185. @server.tool()
  186. def multiply(a: float, b: float) -> float:
  187. """乘法计算器"""
  188. return a * b
  189. @server.tool()
  190. def divide(a: float, b: float) -> float:
  191. """除法计算器"""
  192. if b == 0:
  193. raise ValueError("除数不能为零")
  194. return a / b
  195. @server.tool()
  196. def greet(name: str = "World") -> str:
  197. """友好问候"""
  198. return f"Hello, {name}! 欢迎使用 HelloAgents MCP 工具!"
  199. @server.tool()
  200. def get_system_info() -> dict:
  201. """获取系统信息"""
  202. import platform
  203. import sys
  204. return {
  205. "platform": platform.system(),
  206. "python_version": sys.version,
  207. "server_name": "HelloAgents-BuiltinServer",
  208. "tools_count": 6
  209. }
  210. return server
  211. except ImportError:
  212. raise ImportError(
  213. "创建内置 MCP 服务器需要 fastmcp 库。请安装: pip install fastmcp"
  214. )
  215. def _discover_tools(self):
  216. """发现MCP服务器提供的所有工具"""
  217. try:
  218. from hello_agents.protocols.mcp.client import MCPClient
  219. import asyncio
  220. async def discover():
  221. client_source = self.server if self.server else self.server_command
  222. async with MCPClient(client_source, self.server_args, env=self.env) as client:
  223. tools = await client.list_tools()
  224. return tools
  225. # 运行异步发现
  226. try:
  227. loop = asyncio.get_running_loop()
  228. # 如果已有循环,在新线程中运行
  229. import concurrent.futures
  230. def run_in_thread():
  231. new_loop = asyncio.new_event_loop()
  232. asyncio.set_event_loop(new_loop)
  233. try:
  234. return new_loop.run_until_complete(discover())
  235. finally:
  236. new_loop.close()
  237. with concurrent.futures.ThreadPoolExecutor() as executor:
  238. future = executor.submit(run_in_thread)
  239. self._available_tools = future.result()
  240. except RuntimeError:
  241. # 没有运行中的循环
  242. self._available_tools = asyncio.run(discover())
  243. except Exception as e:
  244. # 工具发现失败不影响初始化
  245. self._available_tools = []
  246. def _generate_description(self) -> str:
  247. """生成增强的工具描述"""
  248. if not self._available_tools:
  249. return "连接到 MCP 服务器,调用工具、读取资源和获取提示词。支持内置服务器和外部服务器。"
  250. if self.auto_expand:
  251. # 展开模式:简单描述
  252. return f"MCP工具服务器,包含{len(self._available_tools)}个工具。这些工具会自动展开为独立的工具供Agent使用。"
  253. else:
  254. # 非展开模式:详细描述
  255. desc_parts = [
  256. f"MCP工具服务器,提供{len(self._available_tools)}个工具:"
  257. ]
  258. # 列出所有工具
  259. for tool in self._available_tools:
  260. tool_name = tool.get('name', 'unknown')
  261. tool_desc = tool.get('description', '无描述')
  262. # 简化描述,只取第一句
  263. short_desc = tool_desc.split('.')[0] if tool_desc else '无描述'
  264. desc_parts.append(f" • {tool_name}: {short_desc}")
  265. # 添加调用格式说明
  266. desc_parts.append("\n调用格式:返回JSON格式的参数")
  267. desc_parts.append('{"action": "call_tool", "tool_name": "工具名", "arguments": {...}}')
  268. # 添加示例
  269. if self._available_tools:
  270. first_tool = self._available_tools[0]
  271. tool_name = first_tool.get('name', 'example')
  272. desc_parts.append(f'\n示例:{{"action": "call_tool", "tool_name": "{tool_name}", "arguments": {{...}}}}')
  273. return "\n".join(desc_parts)
  274. def get_expanded_tools(self) -> List['Tool']: # type: ignore
  275. """
  276. 获取展开的工具列表
  277. 将MCP服务器的每个工具包装成独立的Tool对象
  278. Returns:
  279. Tool对象列表
  280. """
  281. if not self.auto_expand:
  282. return []
  283. from .mcp_wrapper_tool import MCPWrappedTool
  284. expanded_tools = []
  285. for tool_info in self._available_tools:
  286. wrapped_tool = MCPWrappedTool(
  287. mcp_tool=self,
  288. tool_info=tool_info,
  289. prefix=self.prefix
  290. )
  291. expanded_tools.append(wrapped_tool)
  292. return expanded_tools
  293. def run(self, parameters: Dict[str, Any]) -> str:
  294. """
  295. 执行 MCP 操作
  296. Args:
  297. parameters: 包含以下参数的字典
  298. - action: 操作类型 (list_tools, call_tool, list_resources, read_resource, list_prompts, get_prompt)
  299. 如果不指定action但指定了tool_name,会自动推断为call_tool
  300. - tool_name: 工具名称(call_tool 需要)
  301. - arguments: 工具参数(call_tool 需要)
  302. - uri: 资源 URI(read_resource 需要)
  303. - prompt_name: 提示词名称(get_prompt 需要)
  304. - prompt_arguments: 提示词参数(get_prompt 可选)
  305. Returns:
  306. 操作结果
  307. """
  308. from hello_agents.protocols.mcp.client import MCPClient
  309. timeout = getattr(self, 'timeout', 10)
  310. # 智能推断action:如果没有action但有tool_name,自动设置为call_tool
  311. action = parameters.get("action", "").lower()
  312. if not action and "tool_name" in parameters:
  313. action = "call_tool"
  314. parameters["action"] = action
  315. if not action:
  316. return "错误:必须指定 action 参数或 tool_name 参数"
  317. try:
  318. # 使用增强的异步客户端
  319. import asyncio
  320. from hello_agents.protocols.mcp.client import MCPClient
  321. async def run_mcp_operation():
  322. # 根据配置选择客户端创建方式
  323. if self.server:
  324. # 使用内置服务器(内存传输)
  325. client_source = self.server
  326. else:
  327. # 使用外部服务器命令
  328. client_source = self.server_command
  329. async with MCPClient(client_source, self.server_args, env=self.env) as client:
  330. if action == "list_tools":
  331. tools = await client.list_tools()
  332. if not tools:
  333. return "没有找到可用的工具"
  334. result = f"找到 {len(tools)} 个工具:\n"
  335. for tool in tools:
  336. result += f"- {tool['name']}: {tool['description']}\n"
  337. return result
  338. elif action == "call_tool":
  339. tool_name = parameters.get("tool_name")
  340. arguments = parameters.get("arguments", {})
  341. if not tool_name:
  342. return "错误:必须指定 tool_name 参数"
  343. # todo: 修改by xc
  344. result = await asyncio.wait_for(client.call_tool(tool_name, arguments), timeout=timeout)
  345. # result = await client.call_tool(tool_name, arguments)
  346. return f"工具 '{tool_name}' 执行结果:\n{result}"
  347. elif action == "list_resources":
  348. resources = await client.list_resources()
  349. if not resources:
  350. return "没有找到可用的资源"
  351. result = f"找到 {len(resources)} 个资源:\n"
  352. for resource in resources:
  353. result += f"- {resource['uri']}: {resource['name']}\n"
  354. return result
  355. elif action == "read_resource":
  356. uri = parameters.get("uri")
  357. if not uri:
  358. return "错误:必须指定 uri 参数"
  359. content = await client.read_resource(uri)
  360. return f"资源 '{uri}' 内容:\n{content}"
  361. elif action == "list_prompts":
  362. prompts = await client.list_prompts()
  363. if not prompts:
  364. return "没有找到可用的提示词"
  365. result = f"找到 {len(prompts)} 个提示词:\n"
  366. for prompt in prompts:
  367. result += f"- {prompt['name']}: {prompt['description']}\n"
  368. return result
  369. elif action == "get_prompt":
  370. prompt_name = parameters.get("prompt_name")
  371. prompt_arguments = parameters.get("prompt_arguments", {})
  372. if not prompt_name:
  373. return "错误:必须指定 prompt_name 参数"
  374. messages = await client.get_prompt(prompt_name, prompt_arguments)
  375. result = f"提示词 '{prompt_name}':\n"
  376. for msg in messages:
  377. result += f"[{msg['role']}] {msg['content']}\n"
  378. return result
  379. else:
  380. return f"错误:不支持的操作 '{action}'"
  381. # 运行异步操作
  382. try:
  383. # 检查是否已有运行中的事件循环
  384. try:
  385. loop = asyncio.get_running_loop()
  386. # 如果有运行中的循环,在新线程中运行新的事件循环
  387. import concurrent.futures
  388. import threading
  389. def run_in_thread():
  390. # 在新线程中创建新的事件循环
  391. new_loop = asyncio.new_event_loop()
  392. asyncio.set_event_loop(new_loop)
  393. try:
  394. return new_loop.run_until_complete(run_mcp_operation())
  395. finally:
  396. # todo: 修改by xc
  397. # 🔑 关键清理 1:取消所有残留任务,防止 transport 未关闭
  398. pending = asyncio.all_tasks(new_loop)
  399. for task in pending:
  400. task.cancel()
  401. if pending:
  402. new_loop.run_until_complete(
  403. asyncio.gather(*pending, return_exceptions=True)
  404. )
  405. new_loop.close()
  406. # todo: 修改by xc
  407. executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
  408. try:
  409. future = executor.submit(run_in_thread)
  410. # 🔑 关键清理 2:设置超时,避免永久阻塞
  411. return future.result(timeout=timeout)
  412. finally:
  413. # 🔑 关键清理 3:不等待残留线程,直接关闭线程池
  414. executor.shutdown(wait=False, cancel_futures=True)
  415. except RuntimeError:
  416. # 没有运行中的循环,直接运行
  417. return asyncio.run(run_mcp_operation())
  418. except Exception as e:
  419. return f"异步操作失败: {str(e)}"
  420. finally:
  421. # todo: 修改by xc
  422. # 🔑 关键清理 4:强制回收未关闭的管道/文件描述符
  423. gc.collect()
  424. except Exception as e:
  425. return f"MCP 操作失败: {str(e)}"
  426. def get_parameters(self) -> List[ToolParameter]:
  427. """获取工具参数定义"""
  428. return [
  429. ToolParameter(
  430. name="action",
  431. type="string",
  432. description="操作类型: list_tools, call_tool, list_resources, read_resource, list_prompts, get_prompt",
  433. required=True
  434. ),
  435. ToolParameter(
  436. name="tool_name",
  437. type="string",
  438. description="工具名称(call_tool 操作需要)",
  439. required=False
  440. ),
  441. ToolParameter(
  442. name="arguments",
  443. type="object",
  444. description="工具参数(call_tool 操作需要)",
  445. required=False
  446. ),
  447. ToolParameter(
  448. name="uri",
  449. type="string",
  450. description="资源 URI(read_resource 操作需要)",
  451. required=False
  452. ),
  453. ToolParameter(
  454. name="prompt_name",
  455. type="string",
  456. description="提示词名称(get_prompt 操作需要)",
  457. required=False
  458. ),
  459. ToolParameter(
  460. name="prompt_arguments",
  461. type="object",
  462. description="提示词参数(get_prompt 操作可选)",
  463. required=False
  464. )
  465. ]
  466. class A2ATool(Tool):
  467. """A2A (Agent-to-Agent Protocol) 工具
  468. 连接到 A2A Agent 并进行通信。
  469. 功能:
  470. - 向 Agent 提问
  471. - 获取 Agent 信息
  472. - 发送自定义消息
  473. 使用示例:
  474. >>> from hello_agents.tools.builtin import A2ATool
  475. >>> # 连接到 A2A Agent(使用默认名称)
  476. >>> tool = A2ATool(agent_url="http://localhost:5000")
  477. >>> # 连接到 A2A Agent(自定义名称和描述)
  478. >>> tool = A2ATool(
  479. ... agent_url="http://localhost:5000",
  480. ... name="tech_expert",
  481. ... description="技术专家,回答技术相关问题"
  482. ... )
  483. >>> # 提问
  484. >>> result = tool.run({"action": "ask", "question": "计算 2+2"})
  485. >>> # 获取信息
  486. >>> result = tool.run({"action": "get_info"})
  487. 注意:需要安装官方 a2a-sdk 库: pip install a2a-sdk
  488. 详见文档: docs/chapter10/A2A_GUIDE.md
  489. 官方仓库: https://github.com/a2aproject/a2a-python
  490. """
  491. def __init__(self, agent_url: str, name: str = "a2a", description: str = None):
  492. """
  493. 初始化 A2A 工具
  494. Args:
  495. agent_url: Agent URL
  496. name: 工具名称(可选,默认为 "a2a")
  497. description: 工具描述(可选)
  498. """
  499. if description is None:
  500. description = "连接到 A2A Agent,支持提问和获取信息。需要安装官方 a2a-sdk 库。"
  501. super().__init__(
  502. name=name,
  503. description=description
  504. )
  505. self.agent_url = agent_url
  506. def run(self, parameters: Dict[str, Any]) -> str:
  507. """
  508. 执行 A2A 操作
  509. Args:
  510. parameters: 包含以下参数的字典
  511. - action: 操作类型 (ask, get_info)
  512. - question: 问题文本(ask 需要)
  513. Returns:
  514. 操作结果
  515. """
  516. try:
  517. from hello_agents.protocols.a2a.implementation import A2AClient, A2A_AVAILABLE
  518. if not A2A_AVAILABLE:
  519. return ("错误:需要安装 a2a-sdk 库\n"
  520. "安装命令: pip install a2a-sdk\n"
  521. "详见文档: docs/chapter10/A2A_GUIDE.md\n"
  522. "官方仓库: https://github.com/a2aproject/a2a-python")
  523. except ImportError:
  524. return ("错误:无法导入 A2A 模块\n"
  525. "安装命令: pip install a2a-sdk\n"
  526. "详见文档: docs/chapter10/A2A_GUIDE.md\n"
  527. "官方仓库: https://github.com/a2aproject/a2a-python")
  528. action = parameters.get("action", "").lower()
  529. if not action:
  530. return "错误:必须指定 action 参数"
  531. try:
  532. client = A2AClient(self.agent_url)
  533. if action == "ask":
  534. question = parameters.get("question")
  535. if not question:
  536. return "错误:必须指定 question 参数"
  537. response = client.ask(question)
  538. return f"Agent 回答:\n{response}"
  539. elif action == "get_info":
  540. info = client.get_info()
  541. result = "Agent 信息:\n"
  542. for key, value in info.items():
  543. result += f"- {key}: {value}\n"
  544. return result
  545. else:
  546. return f"错误:不支持的操作 '{action}'"
  547. except Exception as e:
  548. return f"A2A 操作失败: {str(e)}"
  549. def get_parameters(self) -> List[ToolParameter]:
  550. """获取工具参数定义"""
  551. return [
  552. ToolParameter(
  553. name="action",
  554. type="string",
  555. description="操作类型: ask(提问), get_info(获取信息)",
  556. required=True
  557. ),
  558. ToolParameter(
  559. name="question",
  560. type="string",
  561. description="问题文本(ask 操作需要)",
  562. required=False
  563. )
  564. ]
  565. class ANPTool(Tool):
  566. """ANP (Agent Network Protocol) 工具
  567. 提供智能体网络管理功能,包括服务发现、节点管理和消息路由。
  568. 这是一个概念性实现,用于演示 Agent 网络管理的核心理念。
  569. 功能:
  570. - 注册和发现服务
  571. - 添加和管理网络节点
  572. - 消息路由
  573. - 网络统计
  574. 使用示例:
  575. >>> from hello_agents.tools.builtin import ANPTool
  576. >>> tool = ANPTool()
  577. >>> # 注册服务
  578. >>> result = tool.run({
  579. ... "action": "register_service",
  580. ... "service_id": "calc-1",
  581. ... "service_type": "calculator",
  582. ... "endpoint": "http://localhost:5001"
  583. ... })
  584. >>> # 发现服务
  585. >>> result = tool.run({
  586. ... "action": "discover_services",
  587. ... "service_type": "calculator"
  588. ... })
  589. >>> # 添加节点
  590. >>> result = tool.run({
  591. ... "action": "add_node",
  592. ... "node_id": "agent-1",
  593. ... "endpoint": "http://localhost:5001"
  594. ... })
  595. 注意:这是概念性实现,不需要额外依赖
  596. 详见文档: docs/chapter10/ANP_CONCEPTS.md
  597. """
  598. def __init__(self, name: str = "anp", description: str = None, discovery=None, network=None):
  599. """初始化 ANP 工具
  600. Args:
  601. name: 工具名称
  602. description: 工具描述
  603. discovery: 可选的 ANPDiscovery 实例,如果不提供则创建新实例
  604. network: 可选的 ANPNetwork 实例,如果不提供则创建新实例
  605. """
  606. if description is None:
  607. description = "智能体网络管理工具,支持服务发现、节点管理和消息路由。概念性实现。"
  608. super().__init__(
  609. name=name,
  610. description=description
  611. )
  612. from hello_agents.protocols.anp.implementation import ANPDiscovery, ANPNetwork
  613. self._discovery = discovery if discovery is not None else ANPDiscovery()
  614. self._network = network if network is not None else ANPNetwork()
  615. def run(self, parameters: Dict[str, Any]) -> str:
  616. """
  617. 执行 ANP 操作
  618. Args:
  619. parameters: 包含以下参数的字典
  620. - action: 操作类型 (register_service, discover_services, add_node, route_message, get_stats)
  621. - service_id, service_type, endpoint: 服务信息(register_service 需要)
  622. - node_id, endpoint: 节点信息(add_node 需要)
  623. - from_node, to_node, message: 路由信息(route_message 需要)
  624. Returns:
  625. 操作结果
  626. """
  627. from hello_agents.protocols.anp.implementation import ServiceInfo
  628. action = parameters.get("action", "").lower()
  629. if not action:
  630. return "错误:必须指定 action 参数"
  631. try:
  632. if action == "register_service":
  633. service_id = parameters.get("service_id")
  634. service_type = parameters.get("service_type")
  635. endpoint = parameters.get("endpoint")
  636. metadata = parameters.get("metadata", {})
  637. if not all([service_id, service_type, endpoint]):
  638. return "错误:必须指定 service_id, service_type 和 endpoint 参数"
  639. service = ServiceInfo(service_id, service_type, endpoint, metadata)
  640. self._discovery.register_service(service)
  641. return f"✅ 已注册服务 '{service_id}'"
  642. elif action == "unregister_service":
  643. service_id = parameters.get("service_id")
  644. if not service_id:
  645. return "错误:必须指定 service_id 参数"
  646. # 使用 ANPDiscovery 的 unregister_service 方法
  647. success = self._discovery.unregister_service(service_id)
  648. if success:
  649. return f"✅ 已注销服务 '{service_id}'"
  650. else:
  651. return f"错误:服务 '{service_id}' 不存在"
  652. elif action == "discover_services":
  653. service_type = parameters.get("service_type")
  654. services = self._discovery.discover_services(service_type)
  655. if not services:
  656. return "没有找到服务"
  657. result = f"找到 {len(services)} 个服务:\n\n"
  658. for service in services:
  659. result += f"服务ID: {service.service_id}\n"
  660. result += f" 名称: {service.service_name}\n"
  661. result += f" 类型: {service.service_type}\n"
  662. result += f" 端点: {service.endpoint}\n"
  663. if service.capabilities:
  664. result += f" 能力: {', '.join(service.capabilities)}\n"
  665. if service.metadata:
  666. result += f" 元数据: {service.metadata}\n"
  667. result += "\n"
  668. return result
  669. elif action == "add_node":
  670. node_id = parameters.get("node_id")
  671. endpoint = parameters.get("endpoint")
  672. metadata = parameters.get("metadata", {})
  673. if not all([node_id, endpoint]):
  674. return "错误:必须指定 node_id 和 endpoint 参数"
  675. self._network.add_node(node_id, endpoint, metadata)
  676. return f"✅ 已添加节点 '{node_id}'"
  677. elif action == "route_message":
  678. from_node = parameters.get("from_node")
  679. to_node = parameters.get("to_node")
  680. message = parameters.get("message", {})
  681. if not all([from_node, to_node]):
  682. return "错误:必须指定 from_node 和 to_node 参数"
  683. path = self._network.route_message(from_node, to_node, message)
  684. if path:
  685. return f"消息路由路径: {' -> '.join(path)}"
  686. else:
  687. return "无法找到路由路径"
  688. elif action == "get_stats":
  689. stats = self._network.get_network_stats()
  690. result = "网络统计:\n"
  691. for key, value in stats.items():
  692. result += f"- {key}: {value}\n"
  693. return result
  694. else:
  695. return f"错误:不支持的操作 '{action}'"
  696. except Exception as e:
  697. return f"ANP 操作失败: {str(e)}"
  698. def get_parameters(self) -> List[ToolParameter]:
  699. """获取工具参数定义"""
  700. return [
  701. ToolParameter(
  702. name="action",
  703. type="string",
  704. description="操作类型: register_service, unregister_service, discover_services, add_node, route_message, get_stats",
  705. required=True
  706. ),
  707. ToolParameter(
  708. name="service_id",
  709. type="string",
  710. description="服务 ID(register_service, unregister_service 需要)",
  711. required=False
  712. ),
  713. ToolParameter(
  714. name="service_type",
  715. type="string",
  716. description="服务类型(register_service 需要)",
  717. required=False
  718. ),
  719. ToolParameter(
  720. name="endpoint",
  721. type="string",
  722. description="端点地址(register_service, add_node 需要)",
  723. required=False
  724. ),
  725. ToolParameter(
  726. name="node_id",
  727. type="string",
  728. description="节点 ID(add_node 需要)",
  729. required=False
  730. ),
  731. ToolParameter(
  732. name="from_node",
  733. type="string",
  734. description="源节点 ID(route_message 需要)",
  735. required=False
  736. ),
  737. ToolParameter(
  738. name="to_node",
  739. type="string",
  740. description="目标节点 ID(route_message 需要)",
  741. required=False
  742. ),
  743. ToolParameter(
  744. name="message",
  745. type="object",
  746. description="消息内容(route_message 需要)",
  747. required=False
  748. ),
  749. ToolParameter(
  750. name="metadata",
  751. type="object",
  752. description="元数据(register_service, add_node 可选)",
  753. required=False
  754. )
  755. ]