demand_pattern_tools.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Pattern 数据查询工具
  5. 提供给 TopicBuildClaudeCodeAgent 使用的 pattern 挖掘结果查询函数。
  6. 所有工具均为只读操作,不修改数据库。
  7. 工具列表:
  8. - get_category_tree: 分类树快照(紧凑文本格式)
  9. - get_frequent_itemsets: 搜索频繁项集(按分类ID、维度模式筛选,分组返回)
  10. - get_itemset_detail: 单个项集详情
  11. - get_post_elements: 帖子结构化元素
  12. - search_elements: 按关键词搜索元素
  13. - get_element_category_chain: 元素名称列表批量反查分类链
  14. - get_category_detail: 分类节点详情
  15. - search_categories: 按关键词搜索分类
  16. - get_category_elements: 分类下的元素列表
  17. 【重要】所有作为 Agent tool 注册的函数,必须包含完整的 docstring 签名。
  18. """
  19. import json
  20. import os
  21. from typing import Any
  22. from agent import tool
  23. from examples.demand.demand_agent_context import TopicBuildAgentContext
  24. from examples.demand.log_capture import log
  25. from examples.demand.pattern_builds import pg_pattern_service as pattern_service
  26. def _log_tool_input(tool_name: str, params: dict):
  27. """工具调用前立即打印参数"""
  28. log(f"\n[FOLD:🔧 {tool_name}]")
  29. log(f"[FOLD:📥 调用参数]")
  30. log(json.dumps(params, ensure_ascii=False, indent=2))
  31. log(f"[/FOLD]")
  32. def _log_tool_output(tool_name: str, result: str) -> str:
  33. """工具执行完成后打印返回内容"""
  34. log(f"[FOLD:📤 返回内容]")
  35. log(result)
  36. log(f"[/FOLD]")
  37. log(f"[/FOLD]\n")
  38. return result
  39. def _normalize_itemset_ids(itemset_ids: Any) -> list[int]:
  40. """Normalize LLM tool args into a list of integer itemset ids."""
  41. if itemset_ids is None:
  42. return []
  43. if isinstance(itemset_ids, int):
  44. return [itemset_ids]
  45. if isinstance(itemset_ids, str):
  46. raw = itemset_ids.strip()
  47. if not raw:
  48. return []
  49. try:
  50. loaded = json.loads(raw)
  51. except json.JSONDecodeError:
  52. loaded = [part.strip() for part in raw.split(",") if part.strip()]
  53. return _normalize_itemset_ids(loaded)
  54. if isinstance(itemset_ids, (list, tuple, set)):
  55. normalized: list[int] = []
  56. seen: set[int] = set()
  57. for value in itemset_ids:
  58. try:
  59. int_value = int(str(value).strip())
  60. except (TypeError, ValueError):
  61. continue
  62. if int_value not in seen:
  63. seen.add(int_value)
  64. normalized.append(int_value)
  65. return normalized
  66. try:
  67. return [int(itemset_ids)]
  68. except (TypeError, ValueError):
  69. return []
  70. def _env_int(name: str, default: int) -> int:
  71. try:
  72. return int(os.getenv(name, str(default)))
  73. except ValueError:
  74. return default
  75. def _env_positive_int(name: str) -> int | None:
  76. value = os.getenv(name)
  77. if value is None or not str(value).strip():
  78. return None
  79. try:
  80. parsed = int(value)
  81. except ValueError:
  82. return None
  83. return parsed if parsed > 0 else None
  84. def _is_mysql_demand_content_entrypoint() -> bool:
  85. return os.getenv("DEMAND_MYSQL_ENTRYPOINT") in {
  86. "run_existing_execution_mysql",
  87. "run_hive_gap_mysql",
  88. }
  89. def _context_scope_value(key: str) -> Any:
  90. return TopicBuildAgentContext.get_metadata(key)
  91. def _resolve_scope_arg(value: Any, metadata_key: str) -> Any:
  92. if value is not None:
  93. if isinstance(value, str) and not value.strip():
  94. return _context_scope_value(metadata_key)
  95. return value
  96. return _context_scope_value(metadata_key)
  97. def _compact_itemset_detail_for_mysql(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
  98. if not _is_mysql_demand_content_entrypoint():
  99. return data
  100. max_post_ids = _env_positive_int("DEMAND_ITEMSET_DETAIL_MAX_POST_IDS")
  101. if max_post_ids is None:
  102. return data
  103. compacted: list[dict[str, Any]] = []
  104. for raw_itemset in data:
  105. itemset = dict(raw_itemset)
  106. for key in ("post_ids", "matched_post_ids"):
  107. post_ids = itemset.get(key)
  108. if isinstance(post_ids, list) and len(post_ids) > max_post_ids:
  109. itemset[f"{key}_total"] = len(post_ids)
  110. itemset[key] = post_ids[:max_post_ids]
  111. compacted.append(itemset)
  112. return compacted
  113. # ============================================================================
  114. # 执行 & 配置 & 分类树
  115. # ============================================================================
  116. @tool(
  117. "获取二级品类分类树结构快照。分类树是所有数据的骨架——帖子中的元素按'实质/形式/意图'三个维度归类到树形分类节点中。"
  118. "\n\n返回紧凑文本格式,包含每个分类节点的名称、层级和元素数量。这是理解数据整体结构的起点。"
  119. "\n\n使用场景:"
  120. "\n- 启动时调用,了解数据涵盖哪些主题领域和维度"
  121. "\n- 按 source_type 筛选单个维度,聚焦分析"
  122. "\n- 从树中发现感兴趣的分类节点,获取其 ID 后用于 get_frequent_itemsets、get_category_detail 等"
  123. )
  124. def get_category_tree(source_type: str = None) -> str:
  125. """获取当前执行的分类树快照,返回紧凑文本格式(节省token)。
  126. Args:
  127. source_type: 按元素类型筛选:实质/形式/意图。不传则返回所有类型。
  128. Returns:
  129. 分类树的紧凑文本字符串。
  130. """
  131. execution_id = TopicBuildAgentContext.get_execution_id()
  132. params = {"execution_id": execution_id, "source_type": source_type}
  133. _log_tool_input("get_category_tree", params)
  134. result = pattern_service.get_category_tree_compact(execution_id, source_type=source_type)
  135. return _log_tool_output("get_category_tree", result)
  136. # ============================================================================
  137. # 项集查询
  138. # ============================================================================
  139. @tool("获取频繁项集——即经常在同一帖子中共同出现的分类组合。结果按 dimension_mode/depth 分组返回,每组各返回 top_n 条。"
  140. "\n\n核心概念:每个频繁项集 = 一组高概率共现的分类节点。absolute_support = 同时出现在多少个帖子中。"
  141. "\n\n返回结构:groups 字典,key 为 'dimension_mode/target_depth',每组含 dimension_mode、target_depth、total(该组总数)、itemsets 列表。"
  142. "\n\n使用场景:"
  143. "\n- 全局探索:不传 category_ids,浏览支持度最高的共现模式"
  144. "\n- 定向查询:传入 category_ids=[A],获取所有包含分类A的项集,即A和哪些分类共现"
  145. "\n- 交叉共现:传入 category_ids=[A,B],获取同时包含A和B的项集,发现A+B还经常和什么一起出现"
  146. "\n- 渐进探索:先查 category_ids=[A] 发现B共现多,再查 category_ids=[A,B] 缩小范围"
  147. "\n- 维度聚焦:用 dimension_mode 筛选特定挖掘模式的结果"
  148. "\n- 帖子范围筛选:用 account_name/merge_leve2 限定帖子来源,筛选后重算 support"
  149. "\n\n提示:category_ids 需要分类节点ID,可先用 search_categories 按名称查ID。项集详情(匹配帖子等)通过 get_itemset_detail 获取。")
  150. def get_frequent_itemsets(
  151. top_n: int = 20,
  152. category_ids: list = None,
  153. dimension_mode: str = None,
  154. min_support: int = None,
  155. min_item_count: int = None,
  156. max_item_count: int = None,
  157. sort_by: str = "absolute_support",
  158. account_name=None,
  159. merge_leve2=None,
  160. platform=None,
  161. ) -> str:
  162. """获取频繁项集——即经常在同一帖子中共同出现的分类组合,按 dimension_mode/depth 分组返回。
  163. 使用场景:
  164. - 全局探索: 不传 category_ids,浏览支持度最高的共现模式
  165. - 定向查询: category_ids=[A],获取包含分类A的所有项集,即A和哪些分类共现
  166. - 交叉共现: category_ids=[A,B],获取同时包含A和B的项集,发现A+B还经常和什么一起出现
  167. - 渐进探索: 先查 [A] 发现B共现多,再查 [A,B] 缩小范围,逐步聚焦
  168. - 维度聚焦: dimension_mode 筛选特定挖掘模式
  169. category_ids 需要分类节点ID,可先用 search_categories 按名称查找。
  170. 返回精简信息,详情(匹配帖子等)通过 get_itemset_detail 获取。
  171. Args:
  172. top_n: 返回前N个项集,默认20。
  173. category_ids: 分类节点ID列表(AND逻辑)。传入后返回同时包含所有这些分类的项集;不传则返回全局Top。
  174. dimension_mode: 挖掘维度模式筛选。full=点类型×元素类型(混合),substance_form_only=仅元素类型,point_type_only=仅点类型。
  175. min_support: 最低绝对支持度(共现帖子数)阈值。
  176. min_item_count: 项集最少包含的分类数量。
  177. max_item_count: 项集最多包含的分类数量。
  178. sort_by: 排序方式:absolute_support=共现帖子数(默认),support=相对支持度,item_count=分类数量。
  179. account_name: 按账号名筛选,支持单个字符串或列表(多个取OR)。
  180. merge_leve2: 按二级品类筛选,支持单个字符串或列表(多个取OR)。
  181. platform: 按平台筛选,默认读取当前任务 platform_type。
  182. Returns:
  183. 按 dimension_mode/depth 分组的项集JSON,每组含 itemsets 列表。
  184. """
  185. execution_id = TopicBuildAgentContext.get_execution_id()
  186. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  187. platform = _resolve_scope_arg(platform, "platform")
  188. params = {
  189. "execution_id": execution_id, "top_n": top_n,
  190. "category_ids": category_ids, "dimension_mode": dimension_mode,
  191. "min_support": min_support,
  192. "min_item_count": min_item_count, "max_item_count": max_item_count,
  193. "sort_by": sort_by,
  194. "account_name": account_name, "merge_leve2": merge_leve2, "platform": platform,
  195. }
  196. _log_tool_input("get_frequent_itemsets", params)
  197. data = pattern_service.search_top_itemsets(
  198. execution_id=execution_id, top_n=top_n,
  199. category_ids=category_ids, dimension_mode=dimension_mode,
  200. min_support=min_support,
  201. min_item_count=min_item_count, max_item_count=max_item_count,
  202. sort_by=sort_by,
  203. account_name=account_name, merge_leve2=merge_leve2, platform=platform,
  204. )
  205. result = json.dumps(data, ensure_ascii=False, indent=2)
  206. return _log_tool_output("get_frequent_itemsets", result)
  207. @tool("获取一个或多个频繁项集的完整详情。相比 get_frequent_itemsets 的精简列表,这里返回每个项集的所有信息。"
  208. "\n\n返回内容:每个项集的 dimension_mode(维度模式)、target_depth(挖掘深度)、items 完整结构(含分类路径、元素名称、维度、点类型)、post_ids(匹配的帖子ID列表)、支持度等。"
  209. "\n\n使用场景:"
  210. "\n- 从 get_frequent_itemsets 中发现有价值的项集后,批量查看其匹配了哪些帖子"
  211. "\n- 获取 post_ids 后可传给 get_post_elements 查看帖子的具体内容"
  212. "\n- 支持传入多个 itemset_id,一次获取多个项集的详情,减少调用次数")
  213. def get_itemset_detail(itemset_ids, merge_leve2=None, platform=None) -> str:
  214. """获取一个或多个频繁项集的详情,包括每个项集的维度模式、depth、items 结构(含分类路径、元素名称、维度、点类型)和匹配的帖子ID列表。
  215. Args:
  216. itemset_ids: 项集ID,单个整数或整数列表。
  217. Returns:
  218. 项集详情列表的JSON字符串,每项含 id, dimension_mode, target_depth, items, post_ids, absolute_support。
  219. """
  220. itemset_ids = _normalize_itemset_ids(itemset_ids)
  221. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  222. platform = _resolve_scope_arg(platform, "platform")
  223. if _is_mysql_demand_content_entrypoint():
  224. max_ids = _env_positive_int("DEMAND_ITEMSET_DETAIL_MAX_IDS")
  225. if max_ids is not None:
  226. itemset_ids = itemset_ids[:max_ids]
  227. params = {"itemset_ids": itemset_ids, "merge_leve2": merge_leve2, "platform": platform}
  228. _log_tool_input("get_itemset_detail", params)
  229. if not itemset_ids:
  230. return _log_tool_output("get_itemset_detail", "错误: itemset_ids 为空或无法解析")
  231. execution_id = TopicBuildAgentContext.get_execution_id()
  232. data = pattern_service.get_itemset_posts(
  233. itemset_ids,
  234. execution_id=execution_id,
  235. merge_leve2=merge_leve2,
  236. platform=platform,
  237. )
  238. if not data:
  239. return _log_tool_output("get_itemset_detail", f"未找到 itemset_ids={itemset_ids} 的项集")
  240. result = json.dumps(_compact_itemset_detail_for_mysql(data), ensure_ascii=False, indent=2)
  241. return _log_tool_output("get_itemset_detail", result)
  242. # ============================================================================
  243. # 帖子 & 元素
  244. # ============================================================================
  245. @tool("获取指定帖子的结构化元素数据。每个帖子的内容被拆解为多个'选题点',每个点下有实质/形式/意图三个维度的元素。"
  246. "\n\n返回结构:按帖子分组 → 按点类型分组 → 每个点含 point_text(点的原文)和 elements(三维度元素列表)。"
  247. "\n\n使用场景:"
  248. "\n- 从 get_itemset_detail 获取 post_ids 后,深入查看这些帖子的具体内容结构"
  249. "\n- 验证某个共现模式在帖子中的实际表现"
  250. "\n- 发现帖子中未被分类捕捉到的内容细节")
  251. def get_post_elements(post_ids: list) -> str:
  252. """获取指定帖子的结构化元素数据。按帖子分组,每个帖子按 点类型→元素类型 组织。用于深入了解某个项集匹配的帖子具体内容。
  253. Args:
  254. post_ids: 帖子ID列表(建议每次不超过10个)。
  255. Returns:
  256. 帖子元素数据的JSON字符串,结构: {post_id: {point_type: [{point_text, elements: {实质, 形式, 意图}}]}}。
  257. """
  258. execution_id = TopicBuildAgentContext.get_execution_id()
  259. params = {"execution_id": execution_id, "post_ids": post_ids}
  260. _log_tool_input("get_post_elements", params)
  261. if len(post_ids) > 20:
  262. return _log_tool_output("get_post_elements",
  263. f"错误: post_ids 数量过多({len(post_ids)}),请每次不超过20个")
  264. data = pattern_service.get_post_elements(execution_id, post_ids)
  265. result = json.dumps(data, ensure_ascii=False, indent=2)
  266. return _log_tool_output("get_post_elements", result)
  267. @tool("按名称关键词搜索元素。元素是帖子中的具体内容实体(如'水煮鱼'、'短视频'),归属于分类树的叶子节点下。"
  268. "\n\n返回去重聚合结果:每个元素附带 point_types(该元素出现在哪些点类型中,如['灵感点','关键点'])、"
  269. "所属分类(category_id、category_path)、出现次数和帖子数。"
  270. "\n\n使用场景:"
  271. "\n- 从某个关键词出发,找到相关元素及其分类归属"
  272. "\n- 通过 point_types 了解元素在灵感点/目的点/关键点中的分布"
  273. "\n- 获取元素名称后,传给 get_element_co_occurrences 查共现关系"
  274. "\n- 通过元素的 category_id 桥接到 get_frequent_itemsets 做分类级分析")
  275. def search_elements(keyword: str, element_type: str = None, limit: int = 50,
  276. account_name=None, merge_leve2=None, platform=None) -> str:
  277. """按名称关键词搜索元素。返回去重聚合后的元素列表,每个元素附带其所属分类信息(category_id、category_path)、出现次数和帖子数。
  278. 使用场景:
  279. - 从某个关键词出发,找到相关元素及其分类归属
  280. - 了解某个元素在数据中出现的频率
  281. Args:
  282. keyword: 搜索关键词(模糊匹配元素名称)。
  283. element_type: 按维度筛选:实质/形式/意图。不传则搜索所有维度。
  284. limit: 最多返回数量,默认50。
  285. account_name: 按账号名筛选,支持单个字符串或列表(多个取OR)。
  286. merge_leve2: 按二级品类筛选,支持单个字符串或列表(多个取OR)。
  287. Returns:
  288. 元素列表的JSON字符串,每个元素含 name、element_type、category_id、category_path、occurrence_count、post_count。
  289. """
  290. execution_id = TopicBuildAgentContext.get_execution_id()
  291. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  292. platform = _resolve_scope_arg(platform, "platform")
  293. params = {"execution_id": execution_id, "keyword": keyword,
  294. "element_type": element_type, "limit": limit,
  295. "account_name": account_name, "merge_leve2": merge_leve2, "platform": platform}
  296. _log_tool_input("search_elements", params)
  297. data = pattern_service.search_elements(execution_id, keyword, element_type=element_type, limit=limit,
  298. account_name=account_name, merge_leve2=merge_leve2, platform=platform)
  299. result = json.dumps({
  300. "keyword": keyword,
  301. "count": len(data),
  302. "elements": data,
  303. }, ensure_ascii=False, indent=2)
  304. return _log_tool_output("search_elements", result)
  305. @tool("从元素名称列表批量反查各自所属的完整分类链。每个元素可能归属于多个分类节点,每项返回每个归属分类从根到叶的完整祖先路径。"
  306. "\n\n使用场景:"
  307. "\n- 知道多个元素名称,想了解它们在分类树中的位置"
  308. "\n- 从元素出发向上回溯分类层级,获取 category_id 用于 get_frequent_itemsets"
  309. "\n- 理解同一元素在不同维度下的分类归属差异")
  310. def get_element_category_chain(element_names: list[str], element_type: str = None) -> str:
  311. """从元素名称批量反查所属分类链。对每个名称返回其出现在哪些分类下,以及每个分类的完整祖先路径(从根到叶)。
  312. 使用场景:
  313. - 知道多个元素名称,想了解它们在分类树中的位置
  314. - 从元素出发,向上回溯分类层级,为泛化推理提供路径
  315. Args:
  316. element_names: 元素名称列表(精确匹配,顺序与返回 results 一一对应)。
  317. element_type: 按维度筛选:实质/形式/意图。不传则查所有维度(对所有名称共用)。
  318. Returns:
  319. JSON 字符串。含 query_count 与 results,每项含 element_name、category_chains(含 category_id、category_path、ancestors 等)。
  320. """
  321. execution_id = TopicBuildAgentContext.get_execution_id()
  322. params = {"execution_id": execution_id, "element_names": element_names,
  323. "element_type": element_type}
  324. _log_tool_input("get_element_category_chain", params)
  325. if not element_names:
  326. return _log_tool_output("get_element_category_chain", "错误: element_names 不能为空列表")
  327. if not isinstance(element_names, list):
  328. return _log_tool_output(
  329. "get_element_category_chain",
  330. f"错误: element_names 必须为列表,当前类型: {type(element_names).__name__}",
  331. )
  332. stripped: list[str] = []
  333. for i, n in enumerate(element_names):
  334. if n is None or (isinstance(n, str) and not n.strip()):
  335. return _log_tool_output(
  336. "get_element_category_chain",
  337. f"错误: element_names[{i}] 不能为空",
  338. )
  339. stripped.append(str(n).strip())
  340. results = []
  341. for name in stripped:
  342. data = pattern_service.get_element_category_chain(
  343. execution_id, name, element_type=element_type
  344. )
  345. results.append({"element_name": name, "category_chains": data})
  346. out = {
  347. "element_type": element_type,
  348. "query_count": len(stripped),
  349. "results": results,
  350. }
  351. result = json.dumps(out, ensure_ascii=False, indent=2)
  352. return _log_tool_output("get_element_category_chain", result)
  353. # ============================================================================
  354. # 分类导航
  355. # ============================================================================
  356. @tool("获取分类节点的完整上下文信息,用于在分类树中导航和理解某个分类的位置。"
  357. "\n\n返回内容:自身信息(名称、层级、元素数)、祖先链(从根到当前的路径)、直接子节点、同级兄弟节点、"
  358. "该分类下的元素列表(Top100,每个元素含 point_types 列表表示出现在哪些点类型中)。"
  359. "\n\n使用场景:"
  360. "\n- 从 get_frequent_itemsets 中发现某个分类后,了解它的层级上下文"
  361. "\n- 向上泛化:查看祖先节点,理解更宏观的领域"
  362. "\n- 向下细化:查看子节点,找到更具体的方向"
  363. "\n- 平行探索:查看兄弟节点,发现同级别的其他内容领域"
  364. "\n- 获取子节点的 category_id 后可传给 get_frequent_itemsets 做进一步分析")
  365. def get_category_detail(category_id: int) -> str:
  366. """获取分类节点的完整上下文。包括: 自身信息、祖先链(从根到当前节点的路径)、直接子节点列表、同级兄弟节点列表、该分类下的元素列表(去重聚合Top100)。
  367. 使用场景:
  368. - 查看某个分类节点的全貌
  369. - 从分类出发向上回溯(祖先)或向下展开(子节点)
  370. - 查看同级兄弟节点,发现平行的内容领域
  371. Args:
  372. category_id: PG Pattern V2 `pattern_mining_category.id`。
  373. Returns:
  374. 分类详情的JSON字符串,含 category、ancestors、children、siblings、elements。
  375. """
  376. execution_id = TopicBuildAgentContext.get_execution_id()
  377. params = {"execution_id": execution_id, "category_id": category_id}
  378. _log_tool_input("get_category_detail", params)
  379. data = pattern_service.get_category_detail_with_context(execution_id, category_id)
  380. if not data:
  381. return _log_tool_output("get_category_detail", f"未找到 category_id={category_id}")
  382. result = json.dumps(data, ensure_ascii=False, indent=2)
  383. return _log_tool_output("get_category_detail", result)
  384. @tool("按名称关键词搜索分类节点。分类节点是分类树上的一个层级(如'中餐'),区别于具体元素(如'水煮鱼')。"
  385. "\n\n返回匹配的分类列表,含 id、name、path、level、element_count、point_types(该分类下元素涉及的点类型列表,如['灵感点','关键点'])。"
  386. "\n\n使用场景:"
  387. "\n- 获取分类的 category_id,用于 get_frequent_itemsets(category_ids=[...]) 查共现"
  388. "\n- 通过 point_types 了解分类在灵感点/目的点/关键点中的分布"
  389. "\n- 获取 category_id 后传给 get_category_detail 查看层级上下文"
  390. "\n- 作为 get_frequent_itemsets 的前置步骤:先按名称找到 ID,再查频繁项集")
  391. def search_categories(keyword: str, source_type: str = None) -> str:
  392. """按名称关键词搜索分类节点。返回匹配的分类列表,含 id、name、path、level、element_count 等。
  393. 使用场景:
  394. - 用关键词定位分类节点,然后用 get_category_detail 或 get_frequent_itemsets_by_category 进一步探索
  395. Args:
  396. keyword: 搜索关键词(模糊匹配分类名称)。
  397. source_type: 按维度筛选:实质/形式/意图。不传则搜索所有维度。
  398. Returns:
  399. 分类列表的JSON字符串。
  400. """
  401. execution_id = TopicBuildAgentContext.get_execution_id()
  402. params = {"execution_id": execution_id, "keyword": keyword, "source_type": source_type}
  403. _log_tool_input("search_categories", params)
  404. data = pattern_service.search_categories(execution_id, keyword, source_type=source_type)
  405. result = json.dumps({
  406. "keyword": keyword,
  407. "count": len(data),
  408. "categories": data,
  409. }, ensure_ascii=False, indent=2)
  410. return _log_tool_output("search_categories", result)
  411. @tool("获取某个分类节点下的具体元素列表。分类是抽象方向(如'中餐'),元素是具体实例(如'水煮鱼'、'麻婆豆腐')。"
  412. "\n\n返回按名称去重聚合的元素列表,按出现次数降序,含 name、element_type、point_types(该元素出现在哪些点类型中)、occurrence_count、post_count。"
  413. "\n\n使用场景:"
  414. "\n- 从分类节点下钻到具体元素,了解该分类包含哪些内容"
  415. "\n- 通过 point_types 了解元素在灵感点/目的点/关键点中的分布"
  416. "\n- 获取元素名称后,传给 get_element_co_occurrences 查元素级共现"
  417. "\n- 从频繁项集的分类出发,落地到可用于选题的具体元素")
  418. def get_category_elements(category_id: int, account_name=None, merge_leve2=None, platform=None, top_n: int = None) -> str:
  419. """获取某个分类节点下的元素列表(按名称去重聚合),按出现次数降序。
  420. Args:
  421. category_id: 分类节点ID。
  422. account_name: 按账号名筛选,支持单个字符串或列表(多个取OR)。
  423. merge_leve2: 按二级品类筛选,支持单个字符串或列表(多个取OR)。
  424. top_n: 最多返回多少个元素;兼容 LLM 按旧工具习惯传入 top_n。
  425. Returns:
  426. 元素列表的JSON字符串,每个元素含 name、element_type、occurrence_count、post_count。
  427. """
  428. execution_id = TopicBuildAgentContext.get_execution_id()
  429. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  430. platform = _resolve_scope_arg(platform, "platform")
  431. params = {
  432. "category_id": category_id,
  433. "account_name": account_name,
  434. "merge_leve2": merge_leve2,
  435. "platform": platform,
  436. "top_n": top_n,
  437. }
  438. _log_tool_input("get_category_elements", params)
  439. data = pattern_service.get_category_elements(category_id, execution_id=execution_id,
  440. account_name=account_name, merge_leve2=merge_leve2, platform=platform)
  441. if top_n is not None:
  442. try:
  443. data = data[:max(int(top_n), 0)]
  444. except (TypeError, ValueError):
  445. pass
  446. result = json.dumps({
  447. "category_id": category_id,
  448. "element_count": len(data),
  449. "elements": data,
  450. }, ensure_ascii=False, indent=2)
  451. return _log_tool_output("get_category_elements", result)
  452. # ============================================================================
  453. # 共现查询
  454. # ============================================================================
  455. @tool("查询分类级共现关系——找到同时包含指定分类下元素的帖子,统计这些帖子中其他分类的出现频率。"
  456. "\n\n核心概念:与 get_frequent_itemsets 不同,此工具是实时从帖子数据中计算共现,"
  457. "不依赖预计算的频繁项集,因此可以灵活组合任意分类进行探索。"
  458. "\n\n使用场景:"
  459. "\n- 单分类探索:category_ids=[123],发现经常和该分类一起出现的其他分类"
  460. "\n- 多分类交叉:category_ids=[123,456],发现同时涉及这两个分类的帖子中还包含什么分类"
  461. "\n- 渐进聚焦:先查单个分类,发现高频共现后叠加查询缩小范围"
  462. "\n- 验证频繁项集:将 get_frequent_itemsets 中发现的模式用此工具做更细粒度的验证"
  463. "\n\n提示:需要分类节点ID,可先用 search_categories 按名称查找。")
  464. def get_category_co_occurrences(category_ids: list, top_n: int = 30,
  465. account_name=None, merge_leve2=None, platform=None) -> str:
  466. """查询多个分类的共现关系。找到同时包含所有指定分类下元素的帖子,返回这些帖子中其他分类的出现频率。
  467. 支持叠加多分类,传入越多分类,结果越精确(交集缩小)。
  468. Args:
  469. category_ids: 分类节点ID列表(AND逻辑)。传入多个时取帖子交集。
  470. top_n: 返回共现频率最高的前N个分类,默认30。
  471. account_name: 按账号名筛选帖子范围,支持单个字符串或列表(多个取OR)。
  472. merge_leve2: 按二级品类筛选帖子范围,支持单个字符串或列表(多个取OR)。
  473. Returns:
  474. 共现分类列表的JSON字符串,含 matched_post_count(交集帖子数)和 co_categories(共现分类排名)。
  475. """
  476. execution_id = TopicBuildAgentContext.get_execution_id()
  477. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  478. platform = _resolve_scope_arg(platform, "platform")
  479. params = {"execution_id": execution_id, "category_ids": category_ids, "top_n": top_n,
  480. "account_name": account_name, "merge_leve2": merge_leve2, "platform": platform}
  481. _log_tool_input("get_category_co_occurrences", params)
  482. if not category_ids:
  483. return _log_tool_output("get_category_co_occurrences", "错误: category_ids 不能为空")
  484. data = pattern_service.get_category_co_occurrences(
  485. execution_id=execution_id, category_ids=category_ids, top_n=top_n,
  486. account_name=account_name, merge_leve2=merge_leve2, platform=platform,
  487. )
  488. result = json.dumps(data, ensure_ascii=False, indent=2)
  489. return _log_tool_output("get_category_co_occurrences", result)
  490. @tool("查询元素级共现关系——找到同时包含指定元素的帖子,统计这些帖子中其他元素的出现频率。"
  491. "\n\n与 get_category_co_occurrences(分类级共现)互补:此工具在具体元素粒度上分析共现,更适合落地到选题细节。"
  492. "\n\n返回的每个共现元素含 point_types(该元素出现在哪些点类型中,如['灵感点','关键点'])。"
  493. "\n\n使用场景:"
  494. "\n- 单元素探索:element_names=['猫咪'],发现经常和猫咪一起出现的其他元素"
  495. "\n- 多元素交叉:element_names=['猫咪','拟人化'],发现同时涉及这两个元素的帖子还包含什么"
  496. "\n- 渐进聚焦:先查单个元素,发现高频共现后叠加查询缩小范围"
  497. "\n\n提示:element_names 需要精确匹配,可先用 search_elements 按关键词查找确切名称。")
  498. def get_element_co_occurrences(element_names: list, top_n: int = 30,
  499. account_name=None, merge_leve2=None, platform=None) -> str:
  500. """查询多个元素的共现关系。找到同时包含所有指定元素的帖子,返回这些帖子中其他元素的出现频率。
  501. 支持叠加多元素,传入越多元素,结果越精确(交集缩小)。
  502. Args:
  503. element_names: 元素名称列表(精确匹配)。传入多个时取帖子交集。
  504. top_n: 返回共现频率最高的前N个元素,默认30。
  505. account_name: 按账号名筛选帖子范围,支持单个字符串或列表(多个取OR)。
  506. merge_leve2: 按二级品类筛选帖子范围,支持单个字符串或列表(多个取OR)。
  507. Returns:
  508. 共现元素列表的JSON字符串,含 matched_post_count(交集帖子数)和 co_elements(共现元素排名)。
  509. """
  510. execution_id = TopicBuildAgentContext.get_execution_id()
  511. merge_leve2 = _resolve_scope_arg(merge_leve2, "merge_leve2")
  512. platform = _resolve_scope_arg(platform, "platform")
  513. params = {"execution_id": execution_id, "element_names": element_names, "top_n": top_n,
  514. "account_name": account_name, "merge_leve2": merge_leve2, "platform": platform}
  515. _log_tool_input("get_element_co_occurrences", params)
  516. if not element_names:
  517. return _log_tool_output("get_element_co_occurrences", "错误: element_names 不能为空")
  518. data = pattern_service.get_element_co_occurrences(
  519. execution_id=execution_id, element_names=element_names, top_n=top_n,
  520. account_name=account_name, merge_leve2=merge_leve2, platform=platform,
  521. )
  522. result = json.dumps(data, ensure_ascii=False, indent=2)
  523. return _log_tool_output("get_element_co_occurrences", result)
  524. if __name__ == '__main__':
  525. TopicBuildAgentContext.set_execution_id(1)
  526. print(get_category_tree('实质'))