tool_search.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. """
  2. ToolSearch — 工具懒加载元工具
  3. 借鉴 Claude Code 的 deferred tools 模式:
  4. 不在 system prompt 中注入全部工具描述,
  5. 而是提供一个 ToolSearch 元工具, LLM 需要时才查找具体工具。
  6. Lambda 语义:
  7. ToolSearch = λquery. filter(Γ_tools, query)
  8. 效果: system prompt 从 42 个工具描述缩减为 1 个元工具描述
  9. → token 节省 ~60%, 首次响应快 2-3x
  10. 用法:
  11. LLM 输出: {"action": "ToolSearch", "input": {"query": "文件操作"}}
  12. 返回: 匹配的工具列表 + 简要描述, LLM 在下一步选择具体工具
  13. """
  14. from __future__ import annotations
  15. import json
  16. from typing import Any, Dict, List, Optional
  17. from lambdagent.core import Term, Context
  18. # ════════════════════════════════════════════════════════════
  19. # 工具目录: 所有可用工具的元数据 (不含实际实现)
  20. # ════════════════════════════════════════════════════════════
  21. TOOL_CATALOG: Dict[str, Dict[str, Any]] = {
  22. # ── 文件操作 (5) ──
  23. "ReadFile": {
  24. "category": "file",
  25. "description": "读取文件内容",
  26. "input_schema": {"path": "str, 文件路径", "offset": "int, 起始行(可选)", "limit": "int, 行数(可选)"},
  27. "tags": ["文件", "读取", "查看", "file", "read"],
  28. },
  29. "EditFile": {
  30. "category": "file",
  31. "description": "编辑文件: 查找并替换指定文本片段",
  32. "input_schema": {"path": "str", "old_text": "str", "new_text": "str"},
  33. "tags": ["文件", "编辑", "修改", "替换", "file", "edit"],
  34. },
  35. "WriteFile": {
  36. "category": "file",
  37. "description": "写入/创建文件 (覆盖写入)",
  38. "input_schema": {"path": "str", "content": "str"},
  39. "tags": ["文件", "写入", "创建", "保存", "file", "write", "create"],
  40. },
  41. "ListFiles": {
  42. "category": "file",
  43. "description": "列出目录下的文件和子目录",
  44. "input_schema": {"path": "str, 目录路径", "pattern": "str, glob模式(可选)"},
  45. "tags": ["文件", "列表", "目录", "ls", "file", "list", "directory"],
  46. },
  47. "SearchContent": {
  48. "category": "file",
  49. "description": "在文件内容中搜索关键词 (grep)",
  50. "input_schema": {"pattern": "str, 正则表达式", "path": "str, 搜索目录", "glob": "str, 文件过滤(可选)"},
  51. "tags": ["搜索", "grep", "内容", "查找", "search", "find"],
  52. },
  53. # ── 代码分析 (3) ──
  54. "CodeSearch": {
  55. "category": "code",
  56. "description": "在代码库中搜索函数/类/变量定义",
  57. "input_schema": {"query": "str, 搜索词", "language": "str, 语言(可选)"},
  58. "tags": ["代码", "搜索", "函数", "类", "code", "search", "definition"],
  59. },
  60. "ProjectMap": {
  61. "category": "code",
  62. "description": "生成项目结构图和代码地图",
  63. "input_schema": {"path": "str, 项目根目录"},
  64. "tags": ["项目", "结构", "地图", "架构", "project", "structure", "map"],
  65. },
  66. "RunTests": {
  67. "category": "code",
  68. "description": "运行测试用例 (pytest/unittest/maven test)",
  69. "input_schema": {"path": "str, 测试文件或目录", "framework": "str, 框架(可选)"},
  70. "tags": ["测试", "运行", "pytest", "test", "run"],
  71. },
  72. # ── Shell (1) ──
  73. "Bash": {
  74. "category": "shell",
  75. "description": "执行 Shell 命令 (支持持久 CWD 和后台执行)",
  76. "input_schema": {"command": "str, 命令", "timeout": "int, 超时秒数(可选)", "background": "bool(可选)"},
  77. "tags": ["终端", "命令", "shell", "bash", "terminal", "命令行"],
  78. },
  79. # ── Git (5) ──
  80. "GitStatus": {
  81. "category": "git",
  82. "description": "查看 Git 工作区状态 (git status)",
  83. "input_schema": {"path": "str, 仓库路径(可选)"},
  84. "tags": ["git", "状态", "status"],
  85. },
  86. "GitDiff": {
  87. "category": "git",
  88. "description": "查看 Git 差异 (git diff)",
  89. "input_schema": {"path": "str(可选)", "staged": "bool(可选)"},
  90. "tags": ["git", "差异", "diff", "变更"],
  91. },
  92. "GitLog": {
  93. "category": "git",
  94. "description": "查看 Git 提交历史 (git log)",
  95. "input_schema": {"path": "str(可选)", "limit": "int(可选)"},
  96. "tags": ["git", "历史", "log", "提交"],
  97. },
  98. "GitCommit": {
  99. "category": "git",
  100. "description": "创建 Git 提交 (git add + commit)",
  101. "input_schema": {"message": "str", "files": "list[str](可选)"},
  102. "tags": ["git", "提交", "commit"],
  103. },
  104. "GitBranch": {
  105. "category": "git",
  106. "description": "Git 分支操作 (create/switch/list/delete)",
  107. "input_schema": {"action": "str, create|switch|list|delete", "name": "str(可选)"},
  108. "tags": ["git", "分支", "branch"],
  109. },
  110. # ── Web (2) ──
  111. "WebSearch": {
  112. "category": "web",
  113. "description": "搜索引擎查询 (返回摘要和链接)",
  114. "input_schema": {"query": "str, 搜索词"},
  115. "tags": ["网络", "搜索", "web", "search", "google", "查询"],
  116. },
  117. "WebFetch": {
  118. "category": "web",
  119. "description": "获取网页内容 (HTML → Markdown)",
  120. "input_schema": {"url": "str, 网址"},
  121. "tags": ["网络", "网页", "抓取", "web", "fetch", "url"],
  122. },
  123. # ── Notebook & 文档 (4) ──
  124. "NotebookEdit": {
  125. "category": "doc",
  126. "description": "编辑 Jupyter Notebook 单元格",
  127. "input_schema": {"path": "str", "cell_index": "int", "content": "str", "cell_type": "str(可选)"},
  128. "tags": ["notebook", "jupyter", "编辑", "cell"],
  129. },
  130. "DocGen": {
  131. "category": "doc",
  132. "description": "文档生成 (Markdown → HTML/PDF/Word)",
  133. "input_schema": {"content": "str", "format": "str, html|pdf|docx", "output_path": "str"},
  134. "tags": ["文档", "生成", "导出", "doc", "pdf", "word"],
  135. },
  136. "ChunkSplit": {
  137. "category": "doc",
  138. "description": "文本分块 (用于 RAG 或长文处理)",
  139. "input_schema": {"text": "str", "chunk_size": "int(可选)", "overlap": "int(可选)"},
  140. "tags": ["分块", "切分", "chunk", "split", "RAG"],
  141. },
  142. "OCR": {
  143. "category": "doc",
  144. "description": "图片文字识别 (OCR)",
  145. "input_schema": {"image_path": "str"},
  146. "tags": ["OCR", "图片", "文字", "识别", "image"],
  147. },
  148. # ── 知识库 (4) ──
  149. "KBCreate": {
  150. "category": "knowledge",
  151. "description": "创建知识库",
  152. "input_schema": {"name": "str", "description": "str(可选)"},
  153. "tags": ["知识库", "创建", "knowledge", "create", "KB"],
  154. },
  155. "KBAdd": {
  156. "category": "knowledge",
  157. "description": "向知识库添加文档",
  158. "input_schema": {"kb_name": "str", "content": "str", "metadata": "dict(可选)"},
  159. "tags": ["知识库", "添加", "导入", "knowledge", "add"],
  160. },
  161. "KBSearch": {
  162. "category": "knowledge",
  163. "description": "在知识库中检索",
  164. "input_schema": {"kb_name": "str", "query": "str", "top_k": "int(可选)"},
  165. "tags": ["知识库", "检索", "搜索", "knowledge", "search", "query"],
  166. },
  167. "KBList": {
  168. "category": "knowledge",
  169. "description": "列出所有知识库",
  170. "input_schema": {},
  171. "tags": ["知识库", "列表", "knowledge", "list"],
  172. },
  173. # ── 任务/记忆/调度 (10) ──
  174. "TaskCreate": {
  175. "category": "task",
  176. "description": "创建任务",
  177. "input_schema": {"title": "str", "description": "str(可选)", "priority": "str(可选)"},
  178. "tags": ["任务", "创建", "todo", "task", "create"],
  179. },
  180. "TaskUpdate": {
  181. "category": "task",
  182. "description": "更新任务状态",
  183. "input_schema": {"task_id": "str", "status": "str", "note": "str(可选)"},
  184. "tags": ["任务", "更新", "task", "update"],
  185. },
  186. "TaskList": {
  187. "category": "task",
  188. "description": "列出任务",
  189. "input_schema": {"status": "str(可选)", "limit": "int(可选)"},
  190. "tags": ["任务", "列表", "task", "list"],
  191. },
  192. "MemoryStore": {
  193. "category": "memory",
  194. "description": "保存记忆 (持久化存储)",
  195. "input_schema": {"key": "str", "value": "str", "tags": "list[str](可选)"},
  196. "tags": ["记忆", "保存", "存储", "memory", "store", "save"],
  197. },
  198. "MemoryRecall": {
  199. "category": "memory",
  200. "description": "回忆 (按 key 或语义检索)",
  201. "input_schema": {"query": "str", "limit": "int(可选)"},
  202. "tags": ["记忆", "回忆", "检索", "memory", "recall", "retrieve"],
  203. },
  204. "MemoryList": {
  205. "category": "memory",
  206. "description": "列出所有记忆",
  207. "input_schema": {},
  208. "tags": ["记忆", "列表", "memory", "list"],
  209. },
  210. "MemoryForget": {
  211. "category": "memory",
  212. "description": "删除记忆",
  213. "input_schema": {"key": "str"},
  214. "tags": ["记忆", "删除", "遗忘", "memory", "forget", "delete"],
  215. },
  216. "ScheduleCreate": {
  217. "category": "schedule",
  218. "description": "创建定时任务",
  219. "input_schema": {"cron": "str, cron表达式", "task": "str, 要执行的任务"},
  220. "tags": ["调度", "定时", "schedule", "cron", "定时任务"],
  221. },
  222. "ScheduleList": {
  223. "category": "schedule",
  224. "description": "列出定时任务",
  225. "input_schema": {},
  226. "tags": ["调度", "列表", "schedule", "list"],
  227. },
  228. "ScheduleDelete": {
  229. "category": "schedule",
  230. "description": "删除定时任务",
  231. "input_schema": {"schedule_id": "str"},
  232. "tags": ["调度", "删除", "schedule", "delete"],
  233. },
  234. # ── 系统控制 (4, macOS) ──
  235. "browser": {
  236. "category": "system",
  237. "description": "浏览器控制 (打开URL, 获取标签页, 执行JS)",
  238. "input_schema": {"action": "str, open|tabs|execute_js", "url": "str(可选)", "script": "str(可选)"},
  239. "tags": ["浏览器", "browser", "网页", "URL", "系统"],
  240. },
  241. "app": {
  242. "category": "system",
  243. "description": "应用控制 (启动/切换/关闭 macOS 应用)",
  244. "input_schema": {"action": "str, launch|switch|quit|list", "name": "str(可选)"},
  245. "tags": ["应用", "app", "启动", "切换", "系统"],
  246. },
  247. "system": {
  248. "category": "system",
  249. "description": "系统信息查询 (CPU/内存/磁盘/网络)",
  250. "input_schema": {"query": "str, cpu|memory|disk|network|all"},
  251. "tags": ["系统", "信息", "system", "info", "CPU", "内存"],
  252. },
  253. "screenshot": {
  254. "category": "system",
  255. "description": "屏幕截图",
  256. "input_schema": {"region": "str, full|window|area(可选)", "save_path": "str(可选)"},
  257. "tags": ["截图", "screenshot", "屏幕", "系统"],
  258. },
  259. # ── 画像 & 学习 (4) ──
  260. "ProfileGet": {
  261. "category": "profile",
  262. "description": "获取用户画像",
  263. "input_schema": {},
  264. "tags": ["画像", "profile", "用户", "偏好"],
  265. },
  266. "ProfileUpdate": {
  267. "category": "profile",
  268. "description": "更新用户画像",
  269. "input_schema": {"key": "str", "value": "str"},
  270. "tags": ["画像", "更新", "profile", "update"],
  271. },
  272. "LearningFeedback": {
  273. "category": "profile",
  274. "description": "提交学习反馈 (帮助agent自我改进)",
  275. "input_schema": {"feedback": "str", "rating": "int(可选)"},
  276. "tags": ["学习", "反馈", "learning", "feedback"],
  277. },
  278. "LearningStrategies": {
  279. "category": "profile",
  280. "description": "查看当前学习策略",
  281. "input_schema": {},
  282. "tags": ["学习", "策略", "learning", "strategies"],
  283. },
  284. # ── 通知 & 事件 (3) ──
  285. "Notify": {
  286. "category": "notify",
  287. "description": "发送通知 (终端/macOS 通知中心)",
  288. "input_schema": {"message": "str", "title": "str(可选)", "sound": "bool(可选)"},
  289. "tags": ["通知", "notify", "提醒", "alert"],
  290. },
  291. "EventSubscribe": {
  292. "category": "notify",
  293. "description": "订阅事件 (文件变化/定时/webhook)",
  294. "input_schema": {"event_type": "str", "config": "dict"},
  295. "tags": ["事件", "订阅", "event", "subscribe", "watch"],
  296. },
  297. "EventList": {
  298. "category": "notify",
  299. "description": "列出已订阅的事件",
  300. "input_schema": {},
  301. "tags": ["事件", "列表", "event", "list"],
  302. },
  303. }
  304. # 按类别索引
  305. CATEGORIES = {}
  306. for tool_name, meta in TOOL_CATALOG.items():
  307. cat = meta["category"]
  308. if cat not in CATEGORIES:
  309. CATEGORIES[cat] = []
  310. CATEGORIES[cat].append(tool_name)
  311. # ════════════════════════════════════════════════════════════
  312. # ToolSearch: 懒加载元工具
  313. # ════════════════════════════════════════════════════════════
  314. class ToolSearch(Term):
  315. """
  316. 工具懒加载元工具。
  317. Lambda 语义:
  318. ToolSearch = λquery. filter(TOOL_CATALOG, query)
  319. 不执行任何工具, 只返回匹配的工具描述,
  320. 让 LLM 在下一步选择具体工具。
  321. 搜索策略:
  322. 1. 按类别名精确匹配 (如 "file", "git", "web")
  323. 2. 按工具名精确匹配 (如 "ReadFile", "Bash")
  324. 3. 按 tags 模糊匹配 (如 "搜索", "编辑")
  325. 4. 按 description 模糊匹配
  326. """
  327. def __init__(self):
  328. super().__init__("ToolSearch")
  329. def apply(self, input: Any, ctx: Context | None = None) -> str:
  330. ctx = ctx or Context()
  331. # 解析输入
  332. if isinstance(input, str):
  333. try:
  334. data = json.loads(input)
  335. query = data.get("query", data.get("keyword", input))
  336. category = data.get("category", "")
  337. except (json.JSONDecodeError, AttributeError):
  338. query = input.strip()
  339. category = ""
  340. elif isinstance(input, dict):
  341. query = input.get("query", input.get("keyword", ""))
  342. category = input.get("category", "")
  343. else:
  344. query = str(input)
  345. category = ""
  346. results = self.search(query, category)
  347. if not results:
  348. return json.dumps({
  349. "matched": 0,
  350. "hint": f"没有找到匹配 '{query}' 的工具。可用类别: {', '.join(sorted(CATEGORIES.keys()))}",
  351. "categories": {cat: tools for cat, tools in sorted(CATEGORIES.items())},
  352. }, ensure_ascii=False, indent=2)
  353. return json.dumps({
  354. "matched": len(results),
  355. "tools": results,
  356. "hint": "请从以上工具中选择一个, 用 JSON 代码块调用它。",
  357. }, ensure_ascii=False, indent=2)
  358. def search(self, query: str, category: str = "") -> List[dict]:
  359. """搜索工具目录"""
  360. results = []
  361. query_lower = query.lower()
  362. query_words = query_lower.split()
  363. for tool_name, meta in TOOL_CATALOG.items():
  364. score = 0
  365. # 按类别精确匹配
  366. if category and meta["category"] == category:
  367. score += 10
  368. # 按工具名匹配
  369. if query_lower in tool_name.lower():
  370. score += 8
  371. # 按类别名匹配
  372. if query_lower == meta["category"]:
  373. score += 10
  374. # 按 tags 匹配
  375. tags_text = " ".join(meta["tags"]).lower()
  376. for word in query_words:
  377. if word in tags_text:
  378. score += 3
  379. # 按 description 匹配
  380. desc_lower = meta["description"].lower()
  381. for word in query_words:
  382. if word in desc_lower:
  383. score += 2
  384. if score > 0:
  385. results.append({
  386. "name": tool_name,
  387. "category": meta["category"],
  388. "description": meta["description"],
  389. "input_schema": meta["input_schema"],
  390. "_score": score,
  391. })
  392. # 按 score 排序, 返回前 10 个
  393. results.sort(key=lambda x: -x["_score"])
  394. for r in results:
  395. del r["_score"]
  396. return results[:10]
  397. @staticmethod
  398. def list_categories() -> dict:
  399. """列出所有工具类别"""
  400. return {
  401. cat: {
  402. "count": len(tools),
  403. "tools": tools,
  404. }
  405. for cat, tools in sorted(CATEGORIES.items())
  406. }
  407. @staticmethod
  408. def get_tool_description() -> str:
  409. """返回 ToolSearch 自身的工具描述 (注入 system prompt)"""
  410. category_summary = ", ".join(
  411. f"{cat}({len(tools)})" for cat, tools in sorted(CATEGORIES.items())
  412. )
  413. return (
  414. f"ToolSearch: 搜索可用工具。共 {len(TOOL_CATALOG)} 个工具, "
  415. f"分 {len(CATEGORIES)} 类: {category_summary}。\n"
  416. f"输入: {{\"query\": \"关键词\"}} 或 {{\"category\": \"类别名\"}}\n"
  417. f"返回: 匹配的工具列表 + 调用格式。\n"
  418. f"你应该先用 ToolSearch 找到需要的工具, 再调用具体工具。"
  419. )
  420. # 单例
  421. tool_search = ToolSearch()