registry.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. """
  2. Tool Registry - 工具注册表和装饰器
  3. 职责:
  4. 1. @tool 装饰器:自动注册工具并生成 Schema
  5. 2. 管理所有工具的 Schema 和实现
  6. 3. 路由工具调用到具体实现
  7. 4. 支持域名过滤、敏感数据处理、工具统计
  8. 从 Resonote/llm/tools/registry.py 抽取并扩展
  9. """
  10. import json
  11. import inspect
  12. import logging
  13. import time
  14. from typing import Any, Callable, Dict, List, Optional, Sequence
  15. from agent.failures import (
  16. FailureDetail,
  17. FailureDisposition,
  18. ToolExecutionError,
  19. )
  20. from agent.tools.models import ToolCapability
  21. from agent.tools.url_matcher import filter_by_url
  22. logger = logging.getLogger(__name__)
  23. class ToolStats:
  24. """工具使用统计"""
  25. def __init__(self):
  26. self.call_count: int = 0
  27. self.success_count: int = 0
  28. self.failure_count: int = 0
  29. self.total_duration: float = 0.0
  30. self.last_called: Optional[float] = None
  31. @property
  32. def average_duration(self) -> float:
  33. """平均执行时间(秒)"""
  34. return self.total_duration / self.call_count if self.call_count > 0 else 0.0
  35. @property
  36. def success_rate(self) -> float:
  37. """成功率"""
  38. return self.success_count / self.call_count if self.call_count > 0 else 0.0
  39. def to_dict(self) -> Dict[str, Any]:
  40. return {
  41. "call_count": self.call_count,
  42. "success_count": self.success_count,
  43. "failure_count": self.failure_count,
  44. "average_duration": self.average_duration,
  45. "success_rate": self.success_rate,
  46. "last_called": self.last_called
  47. }
  48. def _record_tool_outcome(stats: ToolStats, start_time: float, *, success: bool) -> None:
  49. if success:
  50. stats.success_count += 1
  51. else:
  52. stats.failure_count += 1
  53. stats.total_duration += time.time() - start_time
  54. def _failure_result(failure: FailureDetail) -> Dict[str, Any]:
  55. payload = {"failure": failure.to_dict()}
  56. return {
  57. "text": json.dumps(payload, ensure_ascii=False, indent=2),
  58. "_control": {"failure": failure.to_dict()},
  59. }
  60. class ToolRegistry:
  61. """工具注册表"""
  62. _REGISTRATION_FIELDS = (
  63. "schema",
  64. "url_patterns",
  65. "hidden_params",
  66. "inject_params",
  67. "groups",
  68. "capabilities",
  69. "ui_metadata",
  70. )
  71. def __init__(self):
  72. self._tools: Dict[str, Dict[str, Any]] = {}
  73. self._stats: Dict[str, ToolStats] = {}
  74. def register(
  75. self,
  76. func: Callable,
  77. schema: Optional[Dict] = None,
  78. requires_confirmation: bool = False,
  79. editable_params: Optional[List[str]] = None,
  80. display: Optional[Dict[str, Dict[str, Any]]] = None,
  81. url_patterns: Optional[List[str]] = None,
  82. hidden_params: Optional[List[str]] = None,
  83. inject_params: Optional[Dict[str, Any]] = None,
  84. groups: Optional[List[str]] = None,
  85. capabilities: Optional[Sequence[ToolCapability | str]] = None,
  86. ):
  87. """
  88. 注册工具
  89. Args:
  90. func: 工具函数
  91. schema: 工具 Schema(如果为 None,自动生成)
  92. requires_confirmation: 是否需要用户确认
  93. editable_params: 允许用户编辑的参数列表
  94. display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
  95. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  96. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  97. inject_params: 注入参数规则 {param_name: injector_func}
  98. groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
  99. capabilities: 安全副作用标签;explicit_validation 工具必须显式声明
  100. """
  101. func_name = func.__name__
  102. # 如果没有提供 Schema,自动生成
  103. if schema is None:
  104. try:
  105. from agent.tools.schema import SchemaGenerator
  106. schema = SchemaGenerator.generate(func, hidden_params=hidden_params or [])
  107. except Exception as e:
  108. logger.error(f"Failed to generate schema for {func_name}: {e}")
  109. raise
  110. registration = {
  111. "func": func,
  112. "schema": schema,
  113. "url_patterns": url_patterns,
  114. "hidden_params": hidden_params or [],
  115. "inject_params": inject_params or {},
  116. "groups": groups or [],
  117. "capabilities": frozenset(ToolCapability(item) for item in capabilities or []),
  118. "ui_metadata": {
  119. "requires_confirmation": requires_confirmation,
  120. "editable_params": editable_params or [],
  121. "display": display or {}
  122. }
  123. }
  124. existing = self._tools.get(func_name)
  125. if existing is not None:
  126. same_func = existing["func"] is func
  127. same_definition = self._callable_origin(existing["func"]) == self._callable_origin(func)
  128. same_metadata = all(
  129. existing.get(field) == registration.get(field)
  130. for field in self._REGISTRATION_FIELDS
  131. )
  132. if same_func and same_metadata:
  133. # 重复 import 或装配代码可能对同一函数重复注册。这是安全的
  134. # 幂等操作,且不应重置已累计的调用统计。
  135. logger.debug("[ToolRegistry] Already registered (idempotent): %s", func_name)
  136. return
  137. if same_definition and same_metadata:
  138. # Host composition can recreate a closure from the same factory
  139. # definition to bind a fresh adapter/gateway. Replace only the
  140. # callable and preserve accumulated statistics.
  141. existing["func"] = func
  142. logger.debug(
  143. "[ToolRegistry] Rebound same-definition tool: %s (%s)",
  144. func_name,
  145. self._describe_callable(func),
  146. )
  147. return
  148. if self._is_host_layer_override(existing, registration):
  149. # A Host may deliberately replace a framework builtin with a
  150. # capability-equivalent adapter. This preserves the historical
  151. # layering contract while still rejecting builtin/builtin clashes,
  152. # capability escalation, and competing Host implementations.
  153. self._tools[func_name] = registration
  154. logger.warning(
  155. "[ToolRegistry] Host replaced builtin tool: %s (%s -> %s)",
  156. func_name,
  157. self._describe_callable(existing["func"]),
  158. self._describe_callable(func),
  159. )
  160. return
  161. existing_source = self._describe_callable(existing["func"])
  162. attempted_source = self._describe_callable(func)
  163. if same_func:
  164. changed_fields = [
  165. field
  166. for field in self._REGISTRATION_FIELDS
  167. if existing.get(field) != registration.get(field)
  168. ]
  169. detail = f"registration metadata differs: {', '.join(changed_fields)}"
  170. else:
  171. detail = "a different callable already owns this name"
  172. raise ValueError(
  173. f"Duplicate tool registration for '{func_name}': {detail}; "
  174. f"existing={existing_source}, attempted={attempted_source}"
  175. )
  176. self._tools[func_name] = registration
  177. # 初始化统计
  178. self._stats[func_name] = ToolStats()
  179. logger.debug(
  180. f"[ToolRegistry] Registered: {func_name} "
  181. f"(requires_confirmation={requires_confirmation}, "
  182. f"editable_params={editable_params or []}, "
  183. f"url_patterns={url_patterns or 'none'})"
  184. )
  185. @staticmethod
  186. def _describe_callable(func: Callable) -> str:
  187. """返回可用于定位重复注册来源的稳定描述。"""
  188. module = getattr(func, "__module__", type(func).__module__)
  189. qualname = getattr(func, "__qualname__", getattr(func, "__name__", type(func).__qualname__))
  190. code = getattr(func, "__code__", None)
  191. if code is None:
  192. return f"{module}.{qualname}"
  193. return f"{module}.{qualname} ({code.co_filename}:{code.co_firstlineno})"
  194. @staticmethod
  195. def _callable_origin(func: Callable) -> tuple[str, str, Optional[str], Optional[int]]:
  196. """Identify one source definition while allowing fresh closure instances."""
  197. module = getattr(func, "__module__", type(func).__module__)
  198. qualname = getattr(
  199. func,
  200. "__qualname__",
  201. getattr(func, "__name__", type(func).__qualname__),
  202. )
  203. code = getattr(func, "__code__", None)
  204. if code is None:
  205. return module, qualname, None, None
  206. return module, qualname, code.co_filename, code.co_firstlineno
  207. @staticmethod
  208. def _is_host_layer_override(
  209. existing: Dict[str, Any],
  210. registration: Dict[str, Any],
  211. ) -> bool:
  212. existing_module = getattr(existing["func"], "__module__", "")
  213. attempted_module = getattr(registration["func"], "__module__", "")
  214. existing_capabilities = existing.get("capabilities", frozenset())
  215. return (
  216. existing_module.startswith(("agent.tools.builtin.", "agent.trace."))
  217. and not attempted_module.startswith("agent.")
  218. and bool(existing_capabilities)
  219. and registration.get("capabilities") == existing_capabilities
  220. )
  221. @staticmethod
  222. def _resolve_key_path(context: Dict[str, Any], key_path: str) -> Any:
  223. """
  224. 从 context 中按路径取值。
  225. 支持 "obj.field" 格式:第一段从 context dict 取值,后续段用 getattr。
  226. 例如 "knowledge_config.default_tags" → context["knowledge_config"].default_tags
  227. Args:
  228. context: 上下文字典
  229. key_path: 取值路径
  230. Returns:
  231. 取到的值,路径无效返回 None
  232. """
  233. parts = key_path.split(".")
  234. value = context.get(parts[0])
  235. for part in parts[1:]:
  236. if value is None:
  237. return None
  238. value = getattr(value, part, None)
  239. return value
  240. def is_registered(self, tool_name: str) -> bool:
  241. """检查工具是否已注册"""
  242. return tool_name in self._tools
  243. def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
  244. """
  245. 获取工具 Schema
  246. Args:
  247. tool_names: 工具名称列表(None = 所有工具)
  248. Returns:
  249. OpenAI Tool Schema 列表
  250. """
  251. if tool_names is None:
  252. tool_names = list(self._tools.keys())
  253. schemas = []
  254. for name in tool_names:
  255. if name in self._tools:
  256. schemas.append(self._tools[name]["schema"])
  257. else:
  258. logger.warning(f"[ToolRegistry] Tool not found: {name}")
  259. return schemas
  260. def get_tool_names(self, current_url: Optional[str] = None, groups: Optional[List[str]] = None) -> List[str]:
  261. """
  262. 获取工具名称列表(可选 URL 过滤 + group 过滤)
  263. Args:
  264. current_url: 当前 URL(None = 不过滤 URL)
  265. groups: 工具分组白名单(None = 不过滤 group,返回所有工具)
  266. Returns:
  267. 工具名称列表
  268. """
  269. # 1. group 过滤
  270. if groups is not None:
  271. group_set = set(groups)
  272. candidates = {
  273. name for name, tool in self._tools.items()
  274. if group_set & set(tool.get("groups", []))
  275. }
  276. else:
  277. candidates = set(self._tools.keys())
  278. # 2. URL 过滤
  279. if current_url is None:
  280. return list(candidates)
  281. tool_items = [
  282. {"name": name, "url_patterns": self._tools[name]["url_patterns"]}
  283. for name in candidates
  284. ]
  285. filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
  286. return [item["name"] for item in filtered]
  287. def get_available_groups(self) -> List[str]:
  288. """获取所有已注册的工具分组"""
  289. groups = set()
  290. for tool in self._tools.values():
  291. groups.update(tool.get("groups", []))
  292. return sorted(groups)
  293. def get_capabilities(self, tool_name: str) -> frozenset[ToolCapability]:
  294. """Return declared effects; an empty set means unclassified."""
  295. tool = self._tools.get(tool_name)
  296. if not tool:
  297. return frozenset()
  298. return frozenset(tool.get("capabilities", ()))
  299. def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
  300. """
  301. 根据当前 URL 获取匹配的工具 Schema
  302. Args:
  303. current_url: 当前 URL(None = 返回无 URL 限制的工具)
  304. Returns:
  305. 过滤后的工具 Schema 列表
  306. """
  307. tool_names = self.get_tool_names(current_url)
  308. return self.get_schemas(tool_names)
  309. async def execute(
  310. self,
  311. name: str,
  312. arguments: Dict[str, Any],
  313. uid: str = "",
  314. context: Optional[Dict[str, Any]] = None,
  315. sensitive_data: Optional[Dict[str, Any]] = None,
  316. inject_values: Optional[Dict[str, Any]] = None
  317. ) -> str:
  318. """
  319. 执行工具调用
  320. Args:
  321. name: 工具名称
  322. arguments: 工具参数
  323. uid: 用户ID(自动注入)
  324. context: 额外上下文
  325. sensitive_data: 敏感数据字典(用于替换 <secret> 占位符)
  326. inject_values: 兼容注入值;仅在调用参数未提供时生效
  327. Returns:
  328. JSON 字符串格式的结果
  329. """
  330. if name not in self._tools:
  331. failure = FailureDetail(
  332. code="UNKNOWN_TOOL",
  333. message=f"Unknown tool: {name}",
  334. disposition=FailureDisposition.ABORT_RUN,
  335. source_tool=name,
  336. )
  337. logger.error("[ToolRegistry] %s", failure.message)
  338. return _failure_result(failure)
  339. start_time = time.time()
  340. stats = self._stats[name]
  341. stats.call_count += 1
  342. stats.last_called = start_time
  343. try:
  344. func = self._tools[name]["func"]
  345. tool_info = self._tools[name]
  346. # 处理敏感数据占位符
  347. if sensitive_data:
  348. from agent.tools.sensitive import replace_sensitive_data
  349. current_url = context.get("page_url") if context else None
  350. arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
  351. # 准备参数:只注入函数需要的参数
  352. sig = inspect.signature(func)
  353. # 过滤掉函数签名中不存在的参数(如 Claude SDK 发送的 {"_": true} 占位符)
  354. valid_params = set(sig.parameters.keys())
  355. kwargs = {k: v for k, v in arguments.items() if k in valid_params}
  356. # 注入隐藏参数(hidden_params)
  357. hidden_params = tool_info.get("hidden_params", [])
  358. if "uid" in hidden_params and "uid" in sig.parameters:
  359. kwargs["uid"] = uid
  360. if "context" in hidden_params and "context" in sig.parameters:
  361. kwargs["context"] = context
  362. # 旧调用方可以在执行时提供默认注入值。模型明确给出的
  363. # 参数优先,且仍只允许函数签名中存在的字段。
  364. for param_name, value in (inject_values or {}).items():
  365. if param_name in sig.parameters and param_name not in kwargs:
  366. kwargs[param_name] = value
  367. # 注入参数(inject_params)
  368. inject_params = tool_info.get("inject_params", {})
  369. for param_name, rule in inject_params.items():
  370. if param_name not in sig.parameters:
  371. continue
  372. if not isinstance(rule, dict) or "mode" not in rule:
  373. # 兼容旧格式:直接值或 callable
  374. if param_name not in kwargs or kwargs[param_name] is None:
  375. kwargs[param_name] = rule() if callable(rule) else rule
  376. continue
  377. mode = rule["mode"]
  378. key_path = rule.get("key")
  379. # 从 context 中按路径取值
  380. value = self._resolve_key_path(context, key_path) if key_path and context else None
  381. if value is None:
  382. continue
  383. if mode == "default":
  384. # 默认值模式:LLM 未提供则注入
  385. if param_name not in kwargs or kwargs[param_name] is None:
  386. kwargs[param_name] = value
  387. elif mode == "merge":
  388. # 合并模式:框架值始终保留,LLM 可追加新内容
  389. llm_value = kwargs.get(param_name)
  390. if isinstance(value, dict):
  391. # dict: LLM 追加新 key,同名 key 以框架值为准
  392. kwargs[param_name] = {**(llm_value or {}), **value}
  393. elif isinstance(value, list):
  394. # list: 合并去重
  395. kwargs[param_name] = list(set((llm_value or []) + value))
  396. else:
  397. kwargs[param_name] = value
  398. # 执行函数
  399. if inspect.iscoroutinefunction(func):
  400. result = await func(**kwargs)
  401. else:
  402. result = func(**kwargs)
  403. # 返回结果:ToolResult 转为可序列化格式
  404. if isinstance(result, str):
  405. _record_tool_outcome(stats, start_time, success=True)
  406. return result
  407. # 处理 ToolResult 对象
  408. from agent.tools.models import ToolResult
  409. if isinstance(result, ToolResult):
  410. failure = result.failure
  411. if failure is None and result.error:
  412. failure = FailureDetail(
  413. code="TOOL_REPORTED_ERROR",
  414. message=result.error,
  415. disposition=FailureDisposition.RETRY_CALL,
  416. source_tool=name,
  417. )
  418. result.failure = failure
  419. ret = {"text": result.to_llm_message()}
  420. if failure is not None:
  421. ret["_control"] = {"failure": failure.to_dict()}
  422. # Runner 消费的控制字段与业务文本分离,不能靠解析 output 猜测终止。
  423. if result.terminate_run:
  424. ret.setdefault("_control", {}).update({
  425. "terminate_run": True,
  426. "result_summary": result.result_summary or result.long_term_memory or result.output,
  427. })
  428. # 保留images
  429. if result.images:
  430. ret["images"] = result.images
  431. # 保留tool_usage
  432. if result.tool_usage:
  433. ret["tool_usage"] = result.tool_usage
  434. _record_tool_outcome(stats, start_time, success=failure is None)
  435. # 向后兼容:只有 text 时返回字符串;有控制信息时必须保留 dict。
  436. if len(ret) == 1:
  437. return ret["text"]
  438. return ret
  439. _record_tool_outcome(stats, start_time, success=True)
  440. return json.dumps(result, ensure_ascii=False, indent=2)
  441. except ToolExecutionError as exc:
  442. _record_tool_outcome(stats, start_time, success=False)
  443. failure = exc.failure
  444. if failure.source_tool is None:
  445. failure = FailureDetail(
  446. code=failure.code,
  447. message=failure.message,
  448. disposition=failure.disposition,
  449. source_tool=name,
  450. details=failure.details,
  451. )
  452. logger.warning(
  453. "[ToolRegistry] expected rejection tool=%s code=%s disposition=%s: %s",
  454. name,
  455. failure.code,
  456. failure.disposition.value,
  457. failure.message,
  458. )
  459. return _failure_result(failure)
  460. except Exception:
  461. _record_tool_outcome(stats, start_time, success=False)
  462. logger.exception("[ToolRegistry] unexpected failure executing tool '%s'", name)
  463. return _failure_result(
  464. FailureDetail(
  465. code="UNEXPECTED_TOOL_ERROR",
  466. message="The tool failed unexpectedly",
  467. disposition=FailureDisposition.ABORT_RUN,
  468. source_tool=name,
  469. )
  470. )
  471. def get_stats(self, tool_name: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
  472. """
  473. 获取工具统计信息
  474. Args:
  475. tool_name: 工具名称(None = 所有工具)
  476. Returns:
  477. 统计信息字典
  478. """
  479. if tool_name:
  480. if tool_name in self._stats:
  481. return {tool_name: self._stats[tool_name].to_dict()}
  482. return {}
  483. return {name: stats.to_dict() for name, stats in self._stats.items()}
  484. def get_top_tools(self, limit: int = 10, by: str = "call_count") -> List[str]:
  485. """
  486. 获取排名靠前的工具
  487. Args:
  488. limit: 返回数量
  489. by: 排序依据(call_count, success_rate, average_duration)
  490. Returns:
  491. 工具名称列表
  492. """
  493. if by == "call_count":
  494. sorted_tools = sorted(
  495. self._stats.items(),
  496. key=lambda x: x[1].call_count,
  497. reverse=True
  498. )
  499. elif by == "success_rate":
  500. sorted_tools = sorted(
  501. self._stats.items(),
  502. key=lambda x: x[1].success_rate,
  503. reverse=True
  504. )
  505. elif by == "average_duration":
  506. sorted_tools = sorted(
  507. self._stats.items(),
  508. key=lambda x: x[1].average_duration,
  509. reverse=False # 越快越好
  510. )
  511. else:
  512. raise ValueError(f"Invalid sort by: {by}")
  513. return [name for name, _ in sorted_tools[:limit]]
  514. def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
  515. """检查是否有工具需要用户确认"""
  516. for tc in tool_calls:
  517. tool_name = tc.get("function", {}).get("name")
  518. if tool_name and tool_name in self._tools:
  519. if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
  520. return True
  521. return False
  522. def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
  523. """返回每个工具是否需要确认"""
  524. flags = []
  525. for tc in tool_calls:
  526. tool_name = tc.get("function", {}).get("name")
  527. if tool_name and tool_name in self._tools:
  528. flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
  529. else:
  530. flags.append(False)
  531. return flags
  532. def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
  533. """检查是否有任何工具允许参数编辑"""
  534. for tc in tool_calls:
  535. tool_name = tc.get("function", {}).get("name")
  536. if tool_name and tool_name in self._tools:
  537. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  538. if editable_params:
  539. return True
  540. return False
  541. def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
  542. """返回每个工具调用的可编辑参数列表"""
  543. params_map = {}
  544. for tc in tool_calls:
  545. tool_call_id = tc.get("id")
  546. tool_name = tc.get("function", {}).get("name")
  547. if tool_name and tool_name in self._tools:
  548. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  549. params_map[tool_call_id] = editable_params
  550. else:
  551. params_map[tool_call_id] = []
  552. return params_map
  553. def get_ui_metadata(
  554. self,
  555. locale: str = "zh",
  556. tool_names: Optional[List[str]] = None
  557. ) -> Dict[str, Dict[str, Any]]:
  558. """
  559. 获取工具的UI元数据(用于前端展示)
  560. Returns:
  561. {
  562. "tool_name": {
  563. "display_name": "搜索笔记",
  564. "param_display_names": {"query": "搜索关键词"},
  565. "requires_confirmation": false,
  566. "editable_params": ["query"]
  567. }
  568. }
  569. """
  570. if tool_names is None:
  571. tool_names = list(self._tools.keys())
  572. metadata = {}
  573. for name in tool_names:
  574. if name not in self._tools:
  575. continue
  576. ui_meta = self._tools[name]["ui_metadata"]
  577. display = ui_meta.get("display", {}).get(locale, {})
  578. metadata[name] = {
  579. "display_name": display.get("name", name),
  580. "param_display_names": display.get("params", {}),
  581. "requires_confirmation": ui_meta.get("requires_confirmation", False),
  582. "editable_params": ui_meta.get("editable_params", [])
  583. }
  584. return metadata
  585. # 全局单例
  586. _global_registry = ToolRegistry()
  587. def tool(
  588. description: Optional[str] = None,
  589. param_descriptions: Optional[Dict[str, str]] = None,
  590. requires_confirmation: bool = False,
  591. editable_params: Optional[List[str]] = None,
  592. display: Optional[Dict[str, Dict[str, Any]]] = None,
  593. url_patterns: Optional[List[str]] = None,
  594. hidden_params: Optional[List[str]] = None,
  595. inject_params: Optional[Dict[str, Any]] = None,
  596. groups: Optional[List[str]] = None,
  597. capabilities: Optional[Sequence[ToolCapability | str]] = None,
  598. ):
  599. """
  600. 工具装饰器 - 自动注册工具并生成 Schema
  601. Args:
  602. description: 函数描述(可选,从 docstring 提取)
  603. param_descriptions: 参数描述(可选,从 docstring 提取)
  604. requires_confirmation: 是否需要用户确认(默认 False)
  605. editable_params: 允许用户编辑的参数列表
  606. display: i18n 展示信息
  607. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  608. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  609. inject_params: 注入参数规则 {param_name: injector_func}
  610. groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
  611. capabilities: 安全副作用标签(read/write/agent_spawn/external_send 等)
  612. Example:
  613. @tool(
  614. hidden_params=["context", "uid"],
  615. inject_params={
  616. "owner": lambda ctx: ctx.config.knowledge.get_owner(),
  617. },
  618. editable_params=["query"],
  619. url_patterns=["*.google.com"],
  620. display={
  621. "zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
  622. "en": {"name": "Search Notes", "params": {"query": "Query"}}
  623. }
  624. )
  625. async def search_blocks(
  626. query: str,
  627. limit: int = 10,
  628. owner: Optional[str] = None,
  629. context: Optional[ToolContext] = None,
  630. uid: str = ""
  631. ) -> str:
  632. '''搜索用户的笔记块'''
  633. ...
  634. """
  635. def decorator(func: Callable) -> Callable:
  636. schema = None
  637. if description is not None or param_descriptions:
  638. from agent.tools.schema import SchemaGenerator
  639. schema = SchemaGenerator.generate(
  640. func,
  641. hidden_params=hidden_params or [],
  642. )
  643. function_schema = schema["function"]
  644. if description is not None:
  645. function_schema["description"] = description
  646. properties = function_schema["parameters"].get("properties", {})
  647. for param_name, param_description in (param_descriptions or {}).items():
  648. if param_name in properties:
  649. properties[param_name]["description"] = param_description
  650. # 注册到全局 registry
  651. _global_registry.register(
  652. func,
  653. schema=schema,
  654. requires_confirmation=requires_confirmation,
  655. editable_params=editable_params,
  656. display=display,
  657. url_patterns=url_patterns,
  658. hidden_params=hidden_params,
  659. inject_params=inject_params,
  660. groups=groups,
  661. capabilities=capabilities,
  662. )
  663. return func
  664. return decorator
  665. def get_tool_registry() -> ToolRegistry:
  666. """获取全局工具注册表"""
  667. return _global_registry