config_search.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """
  2. config_search.py — 配置库检索工具
  3. ==================================
  4. 为 Retriever agent 提供配置搜索和读取能力。
  5. 搜索 experiments/raw_configs/ 中的 437 个真实世界配置。
  6. """
  7. from __future__ import annotations
  8. import json
  9. import re
  10. from pathlib import Path
  11. from typing import Dict, List, Optional
  12. import yaml
  13. PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
  14. RAW_CONFIGS_DIR = PROJECT_ROOT / "experiments" / "raw_configs"
  15. # ── 索引缓存 ──────────────────────────────────────────────
  16. _index: Optional[List[Dict]] = None
  17. def _build_index() -> List[Dict]:
  18. """扫描 raw_configs 目录,构建配置索引。"""
  19. global _index
  20. if _index is not None:
  21. return _index
  22. _index = []
  23. if not RAW_CONFIGS_DIR.exists():
  24. return _index
  25. for yml_path in RAW_CONFIGS_DIR.rglob("*.yml"):
  26. try:
  27. text = yml_path.read_text(encoding="utf-8", errors="ignore")
  28. config = yaml.safe_load(text)
  29. if not isinstance(config, dict):
  30. continue
  31. entry = {
  32. "path": str(yml_path.relative_to(PROJECT_ROOT)),
  33. "source": yml_path.parent.name,
  34. "text": text[:2000],
  35. "fields": {
  36. "type": config.get("type"),
  37. "tools": _extract_tools(config),
  38. "model_provider": _nested_get(config, "model", "provider"),
  39. "has_rag": bool(config.get("rag", {}).get("enabled")),
  40. "has_mcp": bool(config.get("mcp")),
  41. "has_guard": bool(config.get("guard")),
  42. },
  43. "keywords": _extract_keywords(text),
  44. }
  45. _index.append(entry)
  46. except Exception:
  47. continue
  48. # 同样扫描 .yaml 文件
  49. for yml_path in RAW_CONFIGS_DIR.rglob("*.yaml"):
  50. try:
  51. text = yml_path.read_text(encoding="utf-8", errors="ignore")
  52. config = yaml.safe_load(text)
  53. if not isinstance(config, dict):
  54. continue
  55. entry = {
  56. "path": str(yml_path.relative_to(PROJECT_ROOT)),
  57. "source": yml_path.parent.name,
  58. "text": text[:2000],
  59. "fields": {
  60. "type": config.get("type"),
  61. "tools": _extract_tools(config),
  62. "model_provider": _nested_get(config, "model", "provider"),
  63. "has_rag": bool(config.get("rag", {}).get("enabled")),
  64. "has_mcp": bool(config.get("mcp")),
  65. "has_guard": bool(config.get("guard")),
  66. },
  67. "keywords": _extract_keywords(text),
  68. }
  69. _index.append(entry)
  70. except Exception:
  71. continue
  72. return _index
  73. def _extract_tools(config: dict) -> List[str]:
  74. """从配置中提取工具列表。"""
  75. tools = []
  76. mcp = config.get("mcp", {})
  77. if isinstance(mcp, dict):
  78. tools.extend(mcp.get("localTools", []))
  79. for server, tool_list in mcp.get("onlineTool", {}).items():
  80. if isinstance(tool_list, list):
  81. tools.extend(tool_list)
  82. return tools
  83. def _nested_get(d: dict, *keys):
  84. """安全的嵌套字典取值。"""
  85. for k in keys:
  86. if isinstance(d, dict):
  87. d = d.get(k)
  88. else:
  89. return None
  90. return d
  91. def _extract_keywords(text: str) -> List[str]:
  92. """从配置文本中提取关键词(用于搜索匹配)。"""
  93. # 提取英文单词和中文词
  94. words = re.findall(r'[a-zA-Z_]{3,}', text.lower())
  95. # 去重,保留前 50 个
  96. seen = set()
  97. result = []
  98. for w in words:
  99. if w not in seen and w not in _STOP_WORDS:
  100. seen.add(w)
  101. result.append(w)
  102. if len(result) >= 50:
  103. break
  104. return result
  105. _STOP_WORDS = {
  106. "the", "and", "for", "with", "that", "this", "from", "have", "has",
  107. "are", "was", "were", "been", "being", "will", "would", "could",
  108. "should", "may", "might", "must", "shall", "can", "not", "but",
  109. "true", "false", "null", "none", "string", "int", "float", "bool",
  110. "default", "type", "name", "value", "description",
  111. }
  112. # ── 公开接口 ──────────────────────────────────────────────
  113. def search_configs(keywords: List[str], max_results: int = 5) -> List[Dict]:
  114. """
  115. 按关键词搜索配置库。
  116. Args:
  117. keywords: 搜索关键词列表(英文)
  118. max_results: 最大返回数量
  119. Returns:
  120. 匹配的配置列表,按相关性排序
  121. """
  122. index = _build_index()
  123. if not index:
  124. return []
  125. keywords_lower = [k.lower() for k in keywords]
  126. scored = []
  127. for entry in index:
  128. score = 0.0
  129. entry_keywords = set(entry["keywords"])
  130. entry_text = entry["text"].lower()
  131. for kw in keywords_lower:
  132. # 精确关键词匹配
  133. if kw in entry_keywords:
  134. score += 2.0
  135. # 文本子串匹配
  136. elif kw in entry_text:
  137. score += 1.0
  138. # 部分匹配(关键词包含搜索词)
  139. elif any(kw in ek for ek in entry_keywords):
  140. score += 0.5
  141. # 字段加分
  142. fields = entry["fields"]
  143. for kw in keywords_lower:
  144. if fields.get("type") and kw in str(fields["type"]).lower():
  145. score += 1.5
  146. if kw in [t.lower() for t in fields.get("tools", [])]:
  147. score += 1.5
  148. if score > 0:
  149. scored.append((score, entry))
  150. scored.sort(key=lambda x: x[0], reverse=True)
  151. results = []
  152. for score, entry in scored[:max_results]:
  153. results.append({
  154. "path": entry["path"],
  155. "source": entry["source"],
  156. "relevance": round(min(score / (len(keywords_lower) * 3), 1.0), 2),
  157. "fields": entry["fields"],
  158. "preview": entry["text"][:500],
  159. })
  160. return results
  161. def read_config(config_path: str) -> Optional[Dict]:
  162. """
  163. 读取指定路径的配置文件全文。
  164. Args:
  165. config_path: 相对于 PROJECT_ROOT 的路径
  166. Returns:
  167. 解析后的配置字典,或 None
  168. """
  169. full_path = PROJECT_ROOT / config_path
  170. if not full_path.exists():
  171. return None
  172. try:
  173. text = full_path.read_text(encoding="utf-8")
  174. config = yaml.safe_load(text)
  175. return {
  176. "path": config_path,
  177. "raw_text": text,
  178. "parsed": config if isinstance(config, dict) else {},
  179. }
  180. except Exception as e:
  181. return {"path": config_path, "error": str(e)}