knowledge.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. """
  2. 知识管理工具 - KnowHub API 封装
  3. 所有工具通过 HTTP API 调用 KnowHub Server。
  4. """
  5. import os
  6. import logging
  7. import httpx
  8. from typing import List, Dict, Optional, Any
  9. from agent.tools import tool, ToolResult, ToolContext
  10. logger = logging.getLogger(__name__)
  11. # KnowHub Server API 地址
  12. KNOWHUB_API = os.getenv("KNOWHUB_API", "http://localhost:8000")
  13. @tool()
  14. async def knowledge_search(
  15. query: str,
  16. top_k: int = 5,
  17. min_score: int = 3,
  18. tags_type: Optional[List[str]] = None,
  19. context: Optional[ToolContext] = None,
  20. ) -> ToolResult:
  21. """
  22. 检索知识(两阶段:语义路由 + 质量精排)
  23. Args:
  24. query: 搜索查询(任务描述)
  25. top_k: 返回数量(默认 5)
  26. min_score: 最低评分过滤(默认 3)
  27. tags_type: 按类型过滤(tool/usecase/definition/plan/strategy)
  28. context: 工具上下文
  29. Returns:
  30. 相关知识列表
  31. """
  32. try:
  33. params = {
  34. "q": query,
  35. "top_k": top_k,
  36. "min_score": min_score,
  37. }
  38. if tags_type:
  39. params["tags_type"] = ",".join(tags_type)
  40. async with httpx.AsyncClient(timeout=60.0) as client:
  41. response = await client.get(f"{KNOWHUB_API}/api/knowledge/search", params=params)
  42. response.raise_for_status()
  43. data = response.json()
  44. results = data.get("results", [])
  45. count = data.get("count", 0)
  46. if not results:
  47. return ToolResult(
  48. title="🔍 未找到相关知识",
  49. output=f"查询: {query}\n\n知识库中暂无相关的高质量知识。",
  50. long_term_memory=f"知识检索: 未找到相关知识 - {query[:50]}"
  51. )
  52. # 格式化输出
  53. output_lines = [f"查询: {query}\n", f"找到 {count} 条相关知识:\n"]
  54. for idx, item in enumerate(results, 1):
  55. output_lines.append(f"\n### {idx}. [{item['id']}] (⭐ {item.get('score', 3)})")
  56. output_lines.append(f"**场景**: {item['scenario'][:150]}...")
  57. output_lines.append(f"**内容**: {item['content'][:200]}...")
  58. return ToolResult(
  59. title="✅ 知识检索成功",
  60. output="\n".join(output_lines),
  61. long_term_memory=f"知识检索: 找到 {count} 条相关知识 - {query[:50]}",
  62. metadata={
  63. "count": count,
  64. "knowledge_ids": [item["id"] for item in results],
  65. "items": results
  66. }
  67. )
  68. except Exception as e:
  69. logger.error(f"知识检索失败: {e}")
  70. return ToolResult(
  71. title="❌ 检索失败",
  72. output=f"错误: {str(e)}",
  73. error=str(e)
  74. )
  75. @tool()
  76. async def knowledge_save(
  77. scenario: str,
  78. content: str,
  79. tags_type: List[str],
  80. urls: List[str] = None,
  81. agent_id: str = "research_agent",
  82. score: int = 3,
  83. message_id: str = "",
  84. context: Optional[ToolContext] = None,
  85. ) -> ToolResult:
  86. """
  87. 保存新知识
  88. Args:
  89. scenario: 任务描述(在什么情景下 + 要完成什么目标)
  90. content: 核心内容
  91. tags_type: 知识类型标签,可选:tool, usecase, definition, plan, strategy
  92. urls: 参考来源链接列表
  93. agent_id: 执行此调研的 agent ID
  94. score: 初始评分 1-5(默认 3)
  95. message_id: 来源 Message ID
  96. context: 工具上下文
  97. Returns:
  98. 保存结果
  99. """
  100. try:
  101. payload = {
  102. "scenario": scenario,
  103. "content": content,
  104. "tags_type": tags_type,
  105. "urls": urls or [],
  106. "agent_id": agent_id,
  107. "score": score,
  108. "message_id": message_id
  109. }
  110. async with httpx.AsyncClient(timeout=30.0) as client:
  111. response = await client.post(f"{KNOWHUB_API}/api/knowledge", json=payload)
  112. response.raise_for_status()
  113. data = response.json()
  114. knowledge_id = data.get("knowledge_id", "unknown")
  115. return ToolResult(
  116. title="✅ 知识已保存",
  117. output=f"知识 ID: {knowledge_id}\n\n场景:\n{scenario[:100]}...",
  118. long_term_memory=f"保存知识: {knowledge_id} - {scenario[:50]}",
  119. metadata={"knowledge_id": knowledge_id}
  120. )
  121. except Exception as e:
  122. logger.error(f"保存知识失败: {e}")
  123. return ToolResult(
  124. title="❌ 保存失败",
  125. output=f"错误: {str(e)}",
  126. error=str(e)
  127. )
  128. @tool()
  129. async def knowledge_update(
  130. knowledge_id: str,
  131. add_helpful_case: Optional[Dict] = None,
  132. add_harmful_case: Optional[Dict] = None,
  133. update_score: Optional[int] = None,
  134. evolve_feedback: Optional[str] = None,
  135. context: Optional[ToolContext] = None,
  136. ) -> ToolResult:
  137. """
  138. 更新已有知识的评估反馈
  139. Args:
  140. knowledge_id: 知识 ID
  141. add_helpful_case: 添加好用的案例
  142. add_harmful_case: 添加不好用的案例
  143. update_score: 更新评分(1-5)
  144. evolve_feedback: 经验进化反馈(触发 LLM 重写)
  145. context: 工具上下文
  146. Returns:
  147. 更新结果
  148. """
  149. try:
  150. payload = {}
  151. if add_helpful_case:
  152. payload["add_helpful_case"] = add_helpful_case
  153. if add_harmful_case:
  154. payload["add_harmful_case"] = add_harmful_case
  155. if update_score is not None:
  156. payload["update_score"] = update_score
  157. if evolve_feedback:
  158. payload["evolve_feedback"] = evolve_feedback
  159. if not payload:
  160. return ToolResult(
  161. title="⚠️ 无更新",
  162. output="未指定任何更新内容",
  163. long_term_memory="尝试更新知识但未指定更新内容"
  164. )
  165. async with httpx.AsyncClient(timeout=60.0) as client:
  166. response = await client.put(f"{KNOWHUB_API}/api/knowledge/{knowledge_id}", json=payload)
  167. response.raise_for_status()
  168. summary = []
  169. if add_helpful_case:
  170. summary.append("添加 helpful 案例")
  171. if add_harmful_case:
  172. summary.append("添加 harmful 案例")
  173. if update_score is not None:
  174. summary.append(f"更新评分: {update_score}")
  175. if evolve_feedback:
  176. summary.append("知识进化: 基于反馈重写内容")
  177. return ToolResult(
  178. title="✅ 知识已更新",
  179. output=f"知识 ID: {knowledge_id}\n\n更新内容:\n" + "\n".join(f"- {s}" for s in summary),
  180. long_term_memory=f"更新知识: {knowledge_id}"
  181. )
  182. except Exception as e:
  183. logger.error(f"更新知识失败: {e}")
  184. return ToolResult(
  185. title="❌ 更新失败",
  186. output=f"错误: {str(e)}",
  187. error=str(e)
  188. )
  189. @tool()
  190. async def knowledge_batch_update(
  191. feedback_list: List[Dict[str, Any]],
  192. context: Optional[ToolContext] = None,
  193. ) -> ToolResult:
  194. """
  195. 批量反馈知识的有效性
  196. Args:
  197. feedback_list: 评价列表,每个元素包含:
  198. - knowledge_id: (str) 知识 ID
  199. - is_effective: (bool) 是否有效
  200. - feedback: (str, optional) 改进建议,若有效且有建议则触发知识进化
  201. context: 工具上下文
  202. Returns:
  203. 批量更新结果
  204. """
  205. try:
  206. if not feedback_list:
  207. return ToolResult(
  208. title="⚠️ 反馈列表为空",
  209. output="未提供任何反馈",
  210. long_term_memory="批量更新知识: 反馈列表为空"
  211. )
  212. payload = {"feedback_list": feedback_list}
  213. async with httpx.AsyncClient(timeout=120.0) as client:
  214. response = await client.post(f"{KNOWHUB_API}/api/knowledge/batch_update", json=payload)
  215. response.raise_for_status()
  216. data = response.json()
  217. updated = data.get("updated", 0)
  218. return ToolResult(
  219. title="✅ 批量更新完成",
  220. output=f"成功更新 {updated} 条知识",
  221. long_term_memory=f"批量更新知识: 成功 {updated} 条"
  222. )
  223. except Exception as e:
  224. logger.error(f"批量更新知识失败: {e}")
  225. return ToolResult(
  226. title="❌ 批量更新失败",
  227. output=f"错误: {str(e)}",
  228. error=str(e)
  229. )
  230. @tool()
  231. async def knowledge_list(
  232. limit: int = 10,
  233. tags_type: Optional[List[str]] = None,
  234. context: Optional[ToolContext] = None,
  235. ) -> ToolResult:
  236. """
  237. 列出已保存的知识
  238. Args:
  239. limit: 返回数量限制(默认 10)
  240. tags_type: 按类型过滤(可选)
  241. context: 工具上下文
  242. Returns:
  243. 知识列表
  244. """
  245. try:
  246. params = {"limit": limit}
  247. if tags_type:
  248. params["tags_type"] = ",".join(tags_type)
  249. async with httpx.AsyncClient(timeout=30.0) as client:
  250. response = await client.get(f"{KNOWHUB_API}/api/knowledge", params=params)
  251. response.raise_for_status()
  252. data = response.json()
  253. results = data.get("results", [])
  254. count = data.get("count", 0)
  255. if not results:
  256. return ToolResult(
  257. title="📂 知识库为空",
  258. output="还没有保存任何知识",
  259. long_term_memory="知识库为空"
  260. )
  261. output_lines = [f"共找到 {count} 条知识:\n"]
  262. for item in results:
  263. score = item.get("eval", {}).get("score", 3)
  264. output_lines.append(f"- [{item['id']}] (⭐{score}) {item['scenario'][:60]}...")
  265. return ToolResult(
  266. title="📚 知识列表",
  267. output="\n".join(output_lines),
  268. long_term_memory=f"列出 {count} 条知识"
  269. )
  270. except Exception as e:
  271. logger.error(f"列出知识失败: {e}")
  272. return ToolResult(
  273. title="❌ 列表失败",
  274. output=f"错误: {str(e)}",
  275. error=str(e)
  276. )
  277. @tool()
  278. async def knowledge_slim(
  279. model: str = "anthropic/claude-sonnet-4.5",
  280. context: Optional[ToolContext] = None,
  281. ) -> ToolResult:
  282. """
  283. 知识库瘦身:调用顶级大模型,将知识库中语义相似的知识合并精简
  284. Args:
  285. model: 使用的模型(默认 claude-sonnet-4.5)
  286. context: 工具上下文
  287. Returns:
  288. 瘦身结果报告
  289. """
  290. try:
  291. async with httpx.AsyncClient(timeout=300.0) as client:
  292. response = await client.post(f"{KNOWHUB_API}/api/knowledge/slim", params={"model": model})
  293. response.raise_for_status()
  294. data = response.json()
  295. before = data.get("before", 0)
  296. after = data.get("after", 0)
  297. report = data.get("report", "")
  298. result = f"瘦身完成:{before} → {after} 条知识"
  299. if report:
  300. result += f"\n{report}"
  301. return ToolResult(
  302. title="✅ 知识库瘦身完成",
  303. output=result,
  304. long_term_memory=f"知识库瘦身: {before} → {after} 条"
  305. )
  306. except Exception as e:
  307. logger.error(f"知识库瘦身失败: {e}")
  308. return ToolResult(
  309. title="❌ 瘦身失败",
  310. output=f"错误: {str(e)}",
  311. error=str(e)
  312. )
  313. # 兼容接口:get_experience
  314. @tool(description="检索历史经验(strategy 标签的知识)")
  315. async def get_experience(
  316. query: str,
  317. k: int = 3,
  318. context: Optional[ToolContext] = None,
  319. ) -> ToolResult:
  320. """
  321. 检索历史经验(兼容接口,实际调用 knowledge_search 并过滤 strategy 标签)
  322. Args:
  323. query: 搜索查询(任务描述)
  324. k: 返回数量(默认 3)
  325. context: 工具上下文
  326. Returns:
  327. 相关经验列表
  328. """
  329. return await knowledge_search(
  330. query=query,
  331. top_k=k,
  332. min_score=1, # 经验的评分门槛较低
  333. tags_type=["strategy"],
  334. context=context
  335. )