registry.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  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 copy import deepcopy
  15. from typing import Any, Callable, Dict, List, Optional
  16. from cyber_agent.tools.approval import tool_argument_hash
  17. from cyber_agent.tools.errors import RecoverableToolExecutionError
  18. from cyber_agent.tools.url_matcher import filter_by_url, match_url_with_patterns
  19. logger = logging.getLogger(__name__)
  20. class ToolStats:
  21. """工具使用统计"""
  22. def __init__(self):
  23. self.call_count: int = 0
  24. self.success_count: int = 0
  25. self.failure_count: int = 0
  26. self.total_duration: float = 0.0
  27. self.last_called: Optional[float] = None
  28. @property
  29. def average_duration(self) -> float:
  30. """平均执行时间(秒)"""
  31. return self.total_duration / self.call_count if self.call_count > 0 else 0.0
  32. @property
  33. def success_rate(self) -> float:
  34. """成功率"""
  35. return self.success_count / self.call_count if self.call_count > 0 else 0.0
  36. def to_dict(self) -> Dict[str, Any]:
  37. return {
  38. "call_count": self.call_count,
  39. "success_count": self.success_count,
  40. "failure_count": self.failure_count,
  41. "average_duration": self.average_duration,
  42. "success_rate": self.success_rate,
  43. "last_called": self.last_called
  44. }
  45. class ToolRegistry:
  46. """工具注册表"""
  47. def __init__(self):
  48. self._tools: Dict[str, Dict[str, Any]] = {}
  49. self._stats: Dict[str, ToolStats] = {}
  50. self._frozen = False
  51. def register(
  52. self,
  53. func: Callable,
  54. schema: Optional[Dict] = None,
  55. requires_confirmation: bool = False,
  56. editable_params: Optional[List[str]] = None,
  57. display: Optional[Dict[str, Dict[str, Any]]] = None,
  58. url_patterns: Optional[List[str]] = None,
  59. hidden_params: Optional[List[str]] = None,
  60. inject_params: Optional[Dict[str, Any]] = None,
  61. groups: Optional[List[str]] = None,
  62. ):
  63. """
  64. 注册工具
  65. Args:
  66. func: 工具函数
  67. schema: 工具 Schema(如果为 None,自动生成)
  68. requires_confirmation: 是否需要用户确认
  69. editable_params: 允许用户编辑的参数列表
  70. display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
  71. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  72. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  73. inject_params: 注入参数规则 {param_name: injector_func}
  74. groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
  75. """
  76. if self._frozen:
  77. raise RuntimeError("ToolRegistry is frozen")
  78. func_name = func.__name__
  79. # 如果没有提供 Schema,自动生成
  80. if schema is None:
  81. try:
  82. from cyber_agent.tools.schema import SchemaGenerator
  83. schema = SchemaGenerator.generate(func, hidden_params=hidden_params or [])
  84. except Exception as e:
  85. logger.error(f"Failed to generate schema for {func_name}: {e}")
  86. raise
  87. self._tools[func_name] = {
  88. "func": func,
  89. "schema": schema,
  90. "url_patterns": url_patterns,
  91. "hidden_params": hidden_params or [],
  92. "inject_params": inject_params or {},
  93. "groups": groups or [],
  94. "ui_metadata": {
  95. "requires_confirmation": requires_confirmation,
  96. "editable_params": editable_params or [],
  97. "display": display or {}
  98. }
  99. }
  100. # 初始化统计
  101. self._stats[func_name] = ToolStats()
  102. logger.debug(
  103. f"[ToolRegistry] Registered: {func_name} "
  104. f"(requires_confirmation={requires_confirmation}, "
  105. f"editable_params={editable_params or []}, "
  106. f"url_patterns={url_patterns or 'none'})"
  107. )
  108. @staticmethod
  109. def _resolve_key_path(context: Dict[str, Any], key_path: str) -> Any:
  110. """
  111. 从 context 中按路径取值。
  112. 支持 "obj.field" 格式:第一段从 context dict 取值,后续段用 getattr。
  113. 例如 "knowledge_config.default_tags" → context["knowledge_config"].default_tags
  114. Args:
  115. context: 上下文字典
  116. key_path: 取值路径
  117. Returns:
  118. 取到的值,路径无效返回 None
  119. """
  120. parts = key_path.split(".")
  121. value = context.get(parts[0])
  122. for part in parts[1:]:
  123. if value is None:
  124. return None
  125. value = getattr(value, part, None)
  126. return value
  127. def is_registered(self, tool_name: str) -> bool:
  128. """检查工具是否已注册"""
  129. return tool_name in self._tools
  130. @property
  131. def frozen(self) -> bool:
  132. """Whether registrations are permanently disabled for this instance."""
  133. return self._frozen
  134. def freeze(self) -> "ToolRegistry":
  135. """Freeze this registry in place and return it for fluent assembly."""
  136. self._frozen = True
  137. return self
  138. def clone(
  139. self,
  140. tool_names: Optional[List[str]] = None,
  141. *,
  142. freeze: bool = False,
  143. ) -> "ToolRegistry":
  144. """Create an isolated registry with fresh statistics and shared callables."""
  145. names = sorted(self._tools.keys() if tool_names is None else tool_names)
  146. missing = [name for name in names if name not in self._tools]
  147. if missing:
  148. raise ValueError(f"Unknown tools for registry clone: {missing}")
  149. cloned = ToolRegistry()
  150. for name in names:
  151. definition = self._tools[name]
  152. cloned._tools[name] = {
  153. "func": definition["func"],
  154. "schema": deepcopy(definition["schema"]),
  155. "url_patterns": deepcopy(definition.get("url_patterns")),
  156. "hidden_params": list(definition.get("hidden_params", [])),
  157. "inject_params": dict(definition.get("inject_params", {})),
  158. "groups": list(definition.get("groups", [])),
  159. "ui_metadata": deepcopy(definition.get("ui_metadata", {})),
  160. }
  161. cloned._stats[name] = ToolStats()
  162. if freeze:
  163. cloned.freeze()
  164. return cloned
  165. def export_definitions(
  166. self,
  167. tool_names: Optional[List[str]] = None,
  168. ) -> List[Dict[str, Any]]:
  169. """Export stable execution declarations without functions or statistics."""
  170. names = sorted(self._tools.keys() if tool_names is None else tool_names)
  171. missing = [name for name in names if name not in self._tools]
  172. if missing:
  173. raise ValueError(f"Unknown tools for definition export: {missing}")
  174. definitions: List[Dict[str, Any]] = []
  175. for name in names:
  176. definition = self._tools[name]
  177. inject_params = {}
  178. for key, value in sorted(definition.get("inject_params", {}).items()):
  179. if isinstance(value, dict):
  180. inject_params[key] = {
  181. item_key: item_value
  182. for item_key, item_value in sorted(value.items())
  183. if not callable(item_value)
  184. }
  185. else:
  186. inject_params[key] = "runtime_value"
  187. definitions.append({
  188. "tool_name": name,
  189. "schema": deepcopy(definition["schema"]),
  190. "url_patterns": deepcopy(definition.get("url_patterns")),
  191. "hidden_params": sorted(definition.get("hidden_params", [])),
  192. "inject_params": inject_params,
  193. "groups": sorted(definition.get("groups", [])),
  194. "ui_metadata": deepcopy(definition.get("ui_metadata", {})),
  195. })
  196. return definitions
  197. def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
  198. """
  199. 获取工具 Schema
  200. Args:
  201. tool_names: 工具名称列表(None = 所有工具)
  202. Returns:
  203. OpenAI Tool Schema 列表
  204. """
  205. if tool_names is None:
  206. tool_names = list(self._tools.keys())
  207. schemas = []
  208. for name in tool_names:
  209. if name in self._tools:
  210. schemas.append(self._tools[name]["schema"])
  211. else:
  212. logger.warning(f"[ToolRegistry] Tool not found: {name}")
  213. return schemas
  214. def get_tool_names(self, current_url: Optional[str] = None, groups: Optional[List[str]] = None) -> List[str]:
  215. """
  216. 获取工具名称列表(可选 URL 过滤 + group 过滤)
  217. Args:
  218. current_url: 当前 URL(None = 不过滤 URL)
  219. groups: 工具分组白名单(None = 不过滤 group,返回所有工具)
  220. Returns:
  221. 工具名称列表
  222. """
  223. # 1. group 过滤
  224. if groups is not None:
  225. group_set = set(groups)
  226. candidates = {
  227. name for name, tool in self._tools.items()
  228. if group_set & set(tool.get("groups", []))
  229. }
  230. else:
  231. candidates = set(self._tools.keys())
  232. # 2. URL 过滤
  233. if current_url is None:
  234. return list(candidates)
  235. tool_items = [
  236. {"name": name, "url_patterns": self._tools[name]["url_patterns"]}
  237. for name in candidates
  238. ]
  239. filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
  240. return [item["name"] for item in filtered]
  241. def get_available_groups(self) -> List[str]:
  242. """获取所有已注册的工具分组"""
  243. groups = set()
  244. for tool in self._tools.values():
  245. groups.update(tool.get("groups", []))
  246. return sorted(groups)
  247. def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
  248. """
  249. 根据当前 URL 获取匹配的工具 Schema
  250. Args:
  251. current_url: 当前 URL(None = 返回无 URL 限制的工具)
  252. Returns:
  253. 过滤后的工具 Schema 列表
  254. """
  255. tool_names = self.get_tool_names(current_url)
  256. return self.get_schemas(tool_names)
  257. def get_runtime_policy(self, tool_name: str) -> Dict[str, Any]:
  258. """Return an immutable copy of execution-relevant tool policy metadata."""
  259. if tool_name not in self._tools:
  260. return {
  261. "requires_confirmation": False,
  262. "editable_params": [],
  263. "url_patterns": None,
  264. }
  265. tool = self._tools[tool_name]
  266. ui = tool.get("ui_metadata", {})
  267. return {
  268. "requires_confirmation": bool(ui.get("requires_confirmation", False)),
  269. "editable_params": list(ui.get("editable_params", [])),
  270. "url_patterns": (
  271. list(tool["url_patterns"])
  272. if tool.get("url_patterns") is not None
  273. else None
  274. ),
  275. }
  276. async def execute(
  277. self,
  278. name: str,
  279. arguments: Dict[str, Any],
  280. uid: str = "",
  281. context: Optional[Dict[str, Any]] = None,
  282. sensitive_data: Optional[Dict[str, Any]] = None,
  283. inject_values: Optional[Dict[str, Any]] = None,
  284. allowed_tool_names: Optional[set[str]] = None,
  285. tool_call_id: Optional[str] = None,
  286. approval_grant: Optional[Dict[str, Any]] = None,
  287. ) -> str:
  288. """
  289. 执行一次工具调用,是 AgentRunner 的统一 dispatch 入口。
  290. Recursive 运行会传入 ``allowed_tool_names`` 作为 Schema 之外的硬门禁,
  291. 在参数注入、统计和函数调用前拒绝伪造的未授权 Tool Call。
  292. Args:
  293. name: 工具名称
  294. arguments: 工具参数
  295. uid: 用户ID(自动注入)
  296. context: 额外上下文
  297. sensitive_data: 敏感数据字典(用于替换 <secret> 占位符)
  298. allowed_tool_names: 当前运行允许的工具集;None 表示保留旧行为
  299. Returns:
  300. JSON 字符串格式的结果
  301. """
  302. if allowed_tool_names is not None and name not in allowed_tool_names:
  303. error_msg = f"Tool not allowed in current run: {name}"
  304. logger.warning(f"[ToolRegistry] {error_msg}")
  305. return json.dumps({"error": error_msg}, ensure_ascii=False)
  306. if name not in self._tools:
  307. error_msg = f"Unknown tool: {name}"
  308. logger.error(f"[ToolRegistry] {error_msg}")
  309. return json.dumps({"error": error_msg}, ensure_ascii=False)
  310. tool_policy = self.get_runtime_policy(name)
  311. url_patterns = tool_policy.get("url_patterns")
  312. if url_patterns:
  313. current_url = context.get("page_url") if context else None
  314. if not current_url or not match_url_with_patterns(current_url, url_patterns):
  315. error_msg = f"Tool URL policy denied: {name}"
  316. logger.warning("[ToolRegistry] %s", error_msg)
  317. return json.dumps({"error": error_msg}, ensure_ascii=False)
  318. if tool_policy.get("requires_confirmation"):
  319. grant = approval_grant or {}
  320. context_trace_id = context.get("trace_id") if context else None
  321. expected_hash = (
  322. tool_argument_hash(
  323. tool_call_id=tool_call_id,
  324. tool_name=name,
  325. arguments=arguments,
  326. )
  327. if tool_call_id
  328. else None
  329. )
  330. valid_grant = (
  331. tool_call_id is not None
  332. and bool(grant.get("batch_id"))
  333. and context_trace_id is not None
  334. and grant.get("trace_id") == context_trace_id
  335. and grant.get("tool_call_id") == tool_call_id
  336. and grant.get("tool_name") == name
  337. and grant.get("argument_hash") == expected_hash
  338. and grant.get("decision") in {"approved", "auto_approved"}
  339. )
  340. if not valid_grant:
  341. error_msg = f"Tool confirmation required: {name}"
  342. logger.warning("[ToolRegistry] %s", error_msg)
  343. return json.dumps({"error": error_msg}, ensure_ascii=False)
  344. start_time = time.time()
  345. stats = self._stats[name]
  346. stats.call_count += 1
  347. stats.last_called = start_time
  348. try:
  349. func = self._tools[name]["func"]
  350. tool_info = self._tools[name]
  351. # 处理敏感数据占位符
  352. if sensitive_data:
  353. from cyber_agent.tools.sensitive import replace_sensitive_data
  354. current_url = context.get("page_url") if context else None
  355. arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
  356. # 准备参数:只注入函数需要的参数
  357. sig = inspect.signature(func)
  358. # 过滤掉函数签名中不存在的参数(如 Claude SDK 发送的 {"_": true} 占位符)
  359. valid_params = set(sig.parameters.keys())
  360. kwargs = {k: v for k, v in arguments.items() if k in valid_params}
  361. # 注入隐藏参数(hidden_params)
  362. hidden_params = tool_info.get("hidden_params", [])
  363. if "uid" in hidden_params and "uid" in sig.parameters:
  364. kwargs["uid"] = uid
  365. if "context" in hidden_params and "context" in sig.parameters:
  366. kwargs["context"] = context
  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. # 记录成功
  404. stats.success_count += 1
  405. duration = time.time() - start_time
  406. stats.total_duration += duration
  407. # 返回结果:ToolResult 转为可序列化格式
  408. if isinstance(result, str):
  409. return result
  410. # 处理 ToolResult 对象
  411. from cyber_agent.tools.models import ToolResult
  412. if isinstance(result, ToolResult):
  413. ret = {"text": result.to_llm_message()}
  414. # 保留images
  415. if result.images:
  416. ret["images"] = result.images
  417. # 保留tool_usage
  418. if result.tool_usage:
  419. ret["tool_usage"] = result.tool_usage
  420. # 仅透传标准 artifact_refs;不把任意 metadata 暴露给模型或 Validator。
  421. artifact_refs = result.artifact_refs or result.metadata.get("artifact_refs", [])
  422. if artifact_refs:
  423. ret["artifact_refs"] = artifact_refs
  424. # 向后兼容:只有text时返回字符串
  425. if len(ret) == 1:
  426. return ret["text"]
  427. return ret
  428. return json.dumps(result, ensure_ascii=False, indent=2)
  429. except RecoverableToolExecutionError:
  430. # The command already has a durable intent and may have committed an
  431. # external idempotent side effect. Returning a normal error result
  432. # would close the original assistant call and lose its command ID.
  433. stats.failure_count += 1
  434. stats.total_duration += time.time() - start_time
  435. raise
  436. except Exception as e:
  437. # 记录失败
  438. stats.failure_count += 1
  439. duration = time.time() - start_time
  440. stats.total_duration += duration
  441. error_msg = f"Error executing tool '{name}': {str(e)}"
  442. logger.error(f"[ToolRegistry] {error_msg}")
  443. import traceback
  444. logger.error(traceback.format_exc())
  445. return json.dumps({"error": error_msg}, ensure_ascii=False)
  446. def get_stats(self, tool_name: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
  447. """
  448. 获取工具统计信息
  449. Args:
  450. tool_name: 工具名称(None = 所有工具)
  451. Returns:
  452. 统计信息字典
  453. """
  454. if tool_name:
  455. if tool_name in self._stats:
  456. return {tool_name: self._stats[tool_name].to_dict()}
  457. return {}
  458. return {name: stats.to_dict() for name, stats in self._stats.items()}
  459. def get_top_tools(self, limit: int = 10, by: str = "call_count") -> List[str]:
  460. """
  461. 获取排名靠前的工具
  462. Args:
  463. limit: 返回数量
  464. by: 排序依据(call_count, success_rate, average_duration)
  465. Returns:
  466. 工具名称列表
  467. """
  468. if by == "call_count":
  469. sorted_tools = sorted(
  470. self._stats.items(),
  471. key=lambda x: x[1].call_count,
  472. reverse=True
  473. )
  474. elif by == "success_rate":
  475. sorted_tools = sorted(
  476. self._stats.items(),
  477. key=lambda x: x[1].success_rate,
  478. reverse=True
  479. )
  480. elif by == "average_duration":
  481. sorted_tools = sorted(
  482. self._stats.items(),
  483. key=lambda x: x[1].average_duration,
  484. reverse=False # 越快越好
  485. )
  486. else:
  487. raise ValueError(f"Invalid sort by: {by}")
  488. return [name for name, _ in sorted_tools[:limit]]
  489. def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
  490. """检查是否有工具需要用户确认"""
  491. for tc in tool_calls:
  492. tool_name = tc.get("function", {}).get("name")
  493. if tool_name and tool_name in self._tools:
  494. if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
  495. return True
  496. return False
  497. def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
  498. """返回每个工具是否需要确认"""
  499. flags = []
  500. for tc in tool_calls:
  501. tool_name = tc.get("function", {}).get("name")
  502. if tool_name and tool_name in self._tools:
  503. flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
  504. else:
  505. flags.append(False)
  506. return flags
  507. def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
  508. """检查是否有任何工具允许参数编辑"""
  509. for tc in tool_calls:
  510. tool_name = tc.get("function", {}).get("name")
  511. if tool_name and tool_name in self._tools:
  512. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  513. if editable_params:
  514. return True
  515. return False
  516. def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
  517. """返回每个工具调用的可编辑参数列表"""
  518. params_map = {}
  519. for tc in tool_calls:
  520. tool_call_id = tc.get("id")
  521. tool_name = tc.get("function", {}).get("name")
  522. if tool_name and tool_name in self._tools:
  523. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  524. params_map[tool_call_id] = editable_params
  525. else:
  526. params_map[tool_call_id] = []
  527. return params_map
  528. def get_ui_metadata(
  529. self,
  530. locale: str = "zh",
  531. tool_names: Optional[List[str]] = None
  532. ) -> Dict[str, Dict[str, Any]]:
  533. """
  534. 获取工具的UI元数据(用于前端展示)
  535. Returns:
  536. {
  537. "tool_name": {
  538. "display_name": "搜索笔记",
  539. "param_display_names": {"query": "搜索关键词"},
  540. "requires_confirmation": false,
  541. "editable_params": ["query"]
  542. }
  543. }
  544. """
  545. if tool_names is None:
  546. tool_names = list(self._tools.keys())
  547. metadata = {}
  548. for name in tool_names:
  549. if name not in self._tools:
  550. continue
  551. ui_meta = self._tools[name]["ui_metadata"]
  552. display = ui_meta.get("display", {}).get(locale, {})
  553. metadata[name] = {
  554. "display_name": display.get("name", name),
  555. "param_display_names": display.get("params", {}),
  556. "requires_confirmation": ui_meta.get("requires_confirmation", False),
  557. "editable_params": ui_meta.get("editable_params", [])
  558. }
  559. return metadata
  560. # 全局单例
  561. _global_registry = ToolRegistry()
  562. def tool(
  563. description: Optional[str] = None,
  564. param_descriptions: Optional[Dict[str, str]] = None,
  565. requires_confirmation: bool = False,
  566. editable_params: Optional[List[str]] = None,
  567. display: Optional[Dict[str, Dict[str, Any]]] = None,
  568. url_patterns: Optional[List[str]] = None,
  569. hidden_params: Optional[List[str]] = None,
  570. inject_params: Optional[Dict[str, Any]] = None,
  571. groups: Optional[List[str]] = None,
  572. ):
  573. """
  574. 工具装饰器 - 自动注册工具并生成 Schema
  575. Args:
  576. description: 函数描述(可选,从 docstring 提取)
  577. param_descriptions: 参数描述(可选,从 docstring 提取)
  578. requires_confirmation: 是否需要用户确认(默认 False)
  579. editable_params: 允许用户编辑的参数列表
  580. display: i18n 展示信息
  581. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  582. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  583. inject_params: 注入参数规则 {param_name: injector_func}
  584. groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
  585. Example:
  586. @tool(
  587. hidden_params=["context", "uid"],
  588. inject_params={
  589. "owner": lambda ctx: ctx.config.knowledge.get_owner(),
  590. },
  591. editable_params=["query"],
  592. url_patterns=["*.google.com"],
  593. display={
  594. "zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
  595. "en": {"name": "Search Notes", "params": {"query": "Query"}}
  596. }
  597. )
  598. async def search_blocks(
  599. query: str,
  600. limit: int = 10,
  601. owner: Optional[str] = None,
  602. context: Optional[ToolContext] = None,
  603. uid: str = ""
  604. ) -> str:
  605. '''搜索用户的笔记块'''
  606. ...
  607. """
  608. def decorator(func: Callable) -> Callable:
  609. # 注册到全局 registry
  610. _global_registry.register(
  611. func,
  612. requires_confirmation=requires_confirmation,
  613. editable_params=editable_params,
  614. display=display,
  615. url_patterns=url_patterns,
  616. hidden_params=hidden_params,
  617. inject_params=inject_params,
  618. groups=groups,
  619. )
  620. return func
  621. return decorator
  622. def get_tool_registry() -> ToolRegistry:
  623. """获取全局工具注册表"""
  624. return _global_registry