registry.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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
  15. from agent.tools.url_matcher import filter_by_url
  16. logger = logging.getLogger(__name__)
  17. class ToolStats:
  18. """工具使用统计"""
  19. def __init__(self):
  20. self.call_count: int = 0
  21. self.success_count: int = 0
  22. self.failure_count: int = 0
  23. self.total_duration: float = 0.0
  24. self.last_called: Optional[float] = None
  25. @property
  26. def average_duration(self) -> float:
  27. """平均执行时间(秒)"""
  28. return self.total_duration / self.call_count if self.call_count > 0 else 0.0
  29. @property
  30. def success_rate(self) -> float:
  31. """成功率"""
  32. return self.success_count / self.call_count if self.call_count > 0 else 0.0
  33. def to_dict(self) -> Dict[str, Any]:
  34. return {
  35. "call_count": self.call_count,
  36. "success_count": self.success_count,
  37. "failure_count": self.failure_count,
  38. "average_duration": self.average_duration,
  39. "success_rate": self.success_rate,
  40. "last_called": self.last_called
  41. }
  42. class ToolRegistry:
  43. """工具注册表"""
  44. def __init__(self):
  45. self._tools: Dict[str, Dict[str, Any]] = {}
  46. self._stats: Dict[str, ToolStats] = {}
  47. def register(
  48. self,
  49. func: Callable,
  50. schema: Optional[Dict] = None,
  51. requires_confirmation: bool = False,
  52. editable_params: Optional[List[str]] = None,
  53. display: Optional[Dict[str, Dict[str, Any]]] = None,
  54. url_patterns: Optional[List[str]] = None,
  55. hidden_params: Optional[List[str]] = None,
  56. inject_params: Optional[Dict[str, Any]] = None
  57. ):
  58. """
  59. 注册工具
  60. Args:
  61. func: 工具函数
  62. schema: 工具 Schema(如果为 None,自动生成)
  63. requires_confirmation: 是否需要用户确认
  64. editable_params: 允许用户编辑的参数列表
  65. display: i18n 展示信息 {"zh": {"name": "xx", "params": {...}}, "en": {...}}
  66. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  67. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  68. inject_params: 注入参数规则 {param_name: injector_func}
  69. """
  70. func_name = func.__name__
  71. # 如果没有提供 Schema,自动生成
  72. if schema is None:
  73. try:
  74. from agent.tools.schema import SchemaGenerator
  75. schema = SchemaGenerator.generate(func, hidden_params=hidden_params or [])
  76. except Exception as e:
  77. logger.error(f"Failed to generate schema for {func_name}: {e}")
  78. raise
  79. self._tools[func_name] = {
  80. "func": func,
  81. "schema": schema,
  82. "url_patterns": url_patterns,
  83. "hidden_params": hidden_params or [],
  84. "inject_params": inject_params or {},
  85. "ui_metadata": {
  86. "requires_confirmation": requires_confirmation,
  87. "editable_params": editable_params or [],
  88. "display": display or {}
  89. }
  90. }
  91. # 初始化统计
  92. self._stats[func_name] = ToolStats()
  93. logger.debug(
  94. f"[ToolRegistry] Registered: {func_name} "
  95. f"(requires_confirmation={requires_confirmation}, "
  96. f"editable_params={editable_params or []}, "
  97. f"url_patterns={url_patterns or 'none'})"
  98. )
  99. @staticmethod
  100. def _resolve_key_path(context: Dict[str, Any], key_path: str) -> Any:
  101. """
  102. 从 context 中按路径取值。
  103. 支持 "obj.field" 格式:第一段从 context dict 取值,后续段用 getattr。
  104. 例如 "knowledge_config.default_tags" → context["knowledge_config"].default_tags
  105. Args:
  106. context: 上下文字典
  107. key_path: 取值路径
  108. Returns:
  109. 取到的值,路径无效返回 None
  110. """
  111. parts = key_path.split(".")
  112. value = context.get(parts[0])
  113. for part in parts[1:]:
  114. if value is None:
  115. return None
  116. value = getattr(value, part, None)
  117. return value
  118. def is_registered(self, tool_name: str) -> bool:
  119. """检查工具是否已注册"""
  120. return tool_name in self._tools
  121. def get_schemas(self, tool_names: Optional[List[str]] = None) -> List[Dict]:
  122. """
  123. 获取工具 Schema
  124. Args:
  125. tool_names: 工具名称列表(None = 所有工具)
  126. Returns:
  127. OpenAI Tool Schema 列表
  128. """
  129. if tool_names is None:
  130. tool_names = list(self._tools.keys())
  131. schemas = []
  132. for name in tool_names:
  133. if name in self._tools:
  134. schemas.append(self._tools[name]["schema"])
  135. else:
  136. logger.warning(f"[ToolRegistry] Tool not found: {name}")
  137. return schemas
  138. def get_tool_names(self, current_url: Optional[str] = None) -> List[str]:
  139. """
  140. 获取工具名称列表(可选 URL 过滤)
  141. Args:
  142. current_url: 当前 URL(None = 返回所有工具)
  143. Returns:
  144. 工具名称列表
  145. """
  146. if current_url is None:
  147. return list(self._tools.keys())
  148. # 过滤工具
  149. tool_items = [
  150. {"name": name, "url_patterns": tool["url_patterns"]}
  151. for name, tool in self._tools.items()
  152. ]
  153. filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
  154. return [item["name"] for item in filtered]
  155. def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
  156. """
  157. 根据当前 URL 获取匹配的工具 Schema
  158. Args:
  159. current_url: 当前 URL(None = 返回无 URL 限制的工具)
  160. Returns:
  161. 过滤后的工具 Schema 列表
  162. """
  163. tool_names = self.get_tool_names(current_url)
  164. return self.get_schemas(tool_names)
  165. async def execute(
  166. self,
  167. name: str,
  168. arguments: Dict[str, Any],
  169. uid: str = "",
  170. context: Optional[Dict[str, Any]] = None,
  171. sensitive_data: Optional[Dict[str, Any]] = None,
  172. inject_values: Optional[Dict[str, Any]] = None
  173. ) -> str:
  174. """
  175. 执行工具调用
  176. Args:
  177. name: 工具名称
  178. arguments: 工具参数
  179. uid: 用户ID(自动注入)
  180. context: 额外上下文
  181. sensitive_data: 敏感数据字典(用于替换 <secret> 占位符)
  182. Returns:
  183. JSON 字符串格式的结果
  184. """
  185. if name not in self._tools:
  186. error_msg = f"Unknown tool: {name}"
  187. logger.error(f"[ToolRegistry] {error_msg}")
  188. return json.dumps({"error": error_msg}, ensure_ascii=False)
  189. start_time = time.time()
  190. stats = self._stats[name]
  191. stats.call_count += 1
  192. stats.last_called = start_time
  193. try:
  194. func = self._tools[name]["func"]
  195. tool_info = self._tools[name]
  196. # 处理敏感数据占位符
  197. if sensitive_data:
  198. from agent.tools.sensitive import replace_sensitive_data
  199. current_url = context.get("page_url") if context else None
  200. arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
  201. # 准备参数:只注入函数需要的参数
  202. kwargs = {**arguments}
  203. sig = inspect.signature(func)
  204. # 注入隐藏参数(hidden_params)
  205. hidden_params = tool_info.get("hidden_params", [])
  206. if "uid" in hidden_params and "uid" in sig.parameters:
  207. kwargs["uid"] = uid
  208. if "context" in hidden_params and "context" in sig.parameters:
  209. kwargs["context"] = context
  210. # 注入参数(inject_params)
  211. inject_params = tool_info.get("inject_params", {})
  212. for param_name, rule in inject_params.items():
  213. if param_name not in sig.parameters:
  214. continue
  215. if not isinstance(rule, dict) or "mode" not in rule:
  216. # 兼容旧格式:直接值或 callable
  217. if param_name not in kwargs or kwargs[param_name] is None:
  218. kwargs[param_name] = rule() if callable(rule) else rule
  219. continue
  220. mode = rule["mode"]
  221. key_path = rule.get("key")
  222. # 从 context 中按路径取值
  223. value = self._resolve_key_path(context, key_path) if key_path and context else None
  224. if value is None:
  225. continue
  226. if mode == "default":
  227. # 默认值模式:LLM 未提供则注入
  228. if param_name not in kwargs or kwargs[param_name] is None:
  229. kwargs[param_name] = value
  230. elif mode == "merge":
  231. # 合并模式:框架值始终保留,LLM 可追加新内容
  232. llm_value = kwargs.get(param_name)
  233. if isinstance(value, dict):
  234. # dict: LLM 追加新 key,同名 key 以框架值为准
  235. kwargs[param_name] = {**(llm_value or {}), **value}
  236. elif isinstance(value, list):
  237. # list: 合并去重
  238. kwargs[param_name] = list(set((llm_value or []) + value))
  239. else:
  240. kwargs[param_name] = value
  241. # 执行函数
  242. if inspect.iscoroutinefunction(func):
  243. result = await func(**kwargs)
  244. else:
  245. result = func(**kwargs)
  246. # 记录成功
  247. stats.success_count += 1
  248. duration = time.time() - start_time
  249. stats.total_duration += duration
  250. # 返回结果:ToolResult 转为可序列化格式
  251. if isinstance(result, str):
  252. return result
  253. # 处理 ToolResult 对象
  254. from agent.tools.models import ToolResult
  255. if isinstance(result, ToolResult):
  256. ret = {"text": result.to_llm_message()}
  257. # 保留images
  258. if result.images:
  259. ret["images"] = result.images
  260. # 保留tool_usage
  261. if result.tool_usage:
  262. ret["tool_usage"] = result.tool_usage
  263. # 向后兼容:只有text时返回字符串
  264. if len(ret) == 1:
  265. return ret["text"]
  266. return ret
  267. return json.dumps(result, ensure_ascii=False, indent=2)
  268. except Exception as e:
  269. # 记录失败
  270. stats.failure_count += 1
  271. duration = time.time() - start_time
  272. stats.total_duration += duration
  273. error_msg = f"Error executing tool '{name}': {str(e)}"
  274. logger.error(f"[ToolRegistry] {error_msg}")
  275. import traceback
  276. logger.error(traceback.format_exc())
  277. return json.dumps({"error": error_msg}, ensure_ascii=False)
  278. def get_stats(self, tool_name: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
  279. """
  280. 获取工具统计信息
  281. Args:
  282. tool_name: 工具名称(None = 所有工具)
  283. Returns:
  284. 统计信息字典
  285. """
  286. if tool_name:
  287. if tool_name in self._stats:
  288. return {tool_name: self._stats[tool_name].to_dict()}
  289. return {}
  290. return {name: stats.to_dict() for name, stats in self._stats.items()}
  291. def get_top_tools(self, limit: int = 10, by: str = "call_count") -> List[str]:
  292. """
  293. 获取排名靠前的工具
  294. Args:
  295. limit: 返回数量
  296. by: 排序依据(call_count, success_rate, average_duration)
  297. Returns:
  298. 工具名称列表
  299. """
  300. if by == "call_count":
  301. sorted_tools = sorted(
  302. self._stats.items(),
  303. key=lambda x: x[1].call_count,
  304. reverse=True
  305. )
  306. elif by == "success_rate":
  307. sorted_tools = sorted(
  308. self._stats.items(),
  309. key=lambda x: x[1].success_rate,
  310. reverse=True
  311. )
  312. elif by == "average_duration":
  313. sorted_tools = sorted(
  314. self._stats.items(),
  315. key=lambda x: x[1].average_duration,
  316. reverse=False # 越快越好
  317. )
  318. else:
  319. raise ValueError(f"Invalid sort by: {by}")
  320. return [name for name, _ in sorted_tools[:limit]]
  321. def check_confirmation_required(self, tool_calls: List[Dict]) -> bool:
  322. """检查是否有工具需要用户确认"""
  323. for tc in tool_calls:
  324. tool_name = tc.get("function", {}).get("name")
  325. if tool_name and tool_name in self._tools:
  326. if self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False):
  327. return True
  328. return False
  329. def get_confirmation_flags(self, tool_calls: List[Dict]) -> List[bool]:
  330. """返回每个工具是否需要确认"""
  331. flags = []
  332. for tc in tool_calls:
  333. tool_name = tc.get("function", {}).get("name")
  334. if tool_name and tool_name in self._tools:
  335. flags.append(self._tools[tool_name]["ui_metadata"].get("requires_confirmation", False))
  336. else:
  337. flags.append(False)
  338. return flags
  339. def check_any_param_editable(self, tool_calls: List[Dict]) -> bool:
  340. """检查是否有任何工具允许参数编辑"""
  341. for tc in tool_calls:
  342. tool_name = tc.get("function", {}).get("name")
  343. if tool_name and tool_name in self._tools:
  344. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  345. if editable_params:
  346. return True
  347. return False
  348. def get_editable_params_map(self, tool_calls: List[Dict]) -> Dict[str, List[str]]:
  349. """返回每个工具调用的可编辑参数列表"""
  350. params_map = {}
  351. for tc in tool_calls:
  352. tool_call_id = tc.get("id")
  353. tool_name = tc.get("function", {}).get("name")
  354. if tool_name and tool_name in self._tools:
  355. editable_params = self._tools[tool_name]["ui_metadata"].get("editable_params", [])
  356. params_map[tool_call_id] = editable_params
  357. else:
  358. params_map[tool_call_id] = []
  359. return params_map
  360. def get_ui_metadata(
  361. self,
  362. locale: str = "zh",
  363. tool_names: Optional[List[str]] = None
  364. ) -> Dict[str, Dict[str, Any]]:
  365. """
  366. 获取工具的UI元数据(用于前端展示)
  367. Returns:
  368. {
  369. "tool_name": {
  370. "display_name": "搜索笔记",
  371. "param_display_names": {"query": "搜索关键词"},
  372. "requires_confirmation": false,
  373. "editable_params": ["query"]
  374. }
  375. }
  376. """
  377. if tool_names is None:
  378. tool_names = list(self._tools.keys())
  379. metadata = {}
  380. for name in tool_names:
  381. if name not in self._tools:
  382. continue
  383. ui_meta = self._tools[name]["ui_metadata"]
  384. display = ui_meta.get("display", {}).get(locale, {})
  385. metadata[name] = {
  386. "display_name": display.get("name", name),
  387. "param_display_names": display.get("params", {}),
  388. "requires_confirmation": ui_meta.get("requires_confirmation", False),
  389. "editable_params": ui_meta.get("editable_params", [])
  390. }
  391. return metadata
  392. # 全局单例
  393. _global_registry = ToolRegistry()
  394. def tool(
  395. description: Optional[str] = None,
  396. param_descriptions: Optional[Dict[str, str]] = None,
  397. requires_confirmation: bool = False,
  398. editable_params: Optional[List[str]] = None,
  399. display: Optional[Dict[str, Dict[str, Any]]] = None,
  400. url_patterns: Optional[List[str]] = None,
  401. hidden_params: Optional[List[str]] = None,
  402. inject_params: Optional[Dict[str, Any]] = None
  403. ):
  404. """
  405. 工具装饰器 - 自动注册工具并生成 Schema
  406. Args:
  407. description: 函数描述(可选,从 docstring 提取)
  408. param_descriptions: 参数描述(可选,从 docstring 提取)
  409. requires_confirmation: 是否需要用户确认(默认 False)
  410. editable_params: 允许用户编辑的参数列表
  411. display: i18n 展示信息
  412. url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
  413. hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
  414. inject_params: 注入参数规则 {param_name: injector_func}
  415. Example:
  416. @tool(
  417. hidden_params=["context", "uid"],
  418. inject_params={
  419. "owner": lambda ctx: ctx.config.knowledge.get_owner(),
  420. },
  421. editable_params=["query"],
  422. url_patterns=["*.google.com"],
  423. display={
  424. "zh": {"name": "搜索笔记", "params": {"query": "搜索关键词"}},
  425. "en": {"name": "Search Notes", "params": {"query": "Query"}}
  426. }
  427. )
  428. async def search_blocks(
  429. query: str,
  430. limit: int = 10,
  431. owner: Optional[str] = None,
  432. context: Optional[ToolContext] = None,
  433. uid: str = ""
  434. ) -> str:
  435. '''搜索用户的笔记块'''
  436. ...
  437. """
  438. def decorator(func: Callable) -> Callable:
  439. # 注册到全局 registry
  440. _global_registry.register(
  441. func,
  442. requires_confirmation=requires_confirmation,
  443. editable_params=editable_params,
  444. display=display,
  445. url_patterns=url_patterns,
  446. hidden_params=hidden_params,
  447. inject_params=inject_params
  448. )
  449. return func
  450. return decorator
  451. def get_tool_registry() -> ToolRegistry:
  452. """获取全局工具注册表"""
  453. return _global_registry