""" config_search.py — 配置库检索工具 ================================== 为 Retriever agent 提供配置搜索和读取能力。 搜索 experiments/raw_configs/ 中的 437 个真实世界配置。 """ from __future__ import annotations import json import re from pathlib import Path from typing import Dict, List, Optional import yaml PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent RAW_CONFIGS_DIR = PROJECT_ROOT / "experiments" / "raw_configs" # ── 索引缓存 ────────────────────────────────────────────── _index: Optional[List[Dict]] = None def _build_index() -> List[Dict]: """扫描 raw_configs 目录,构建配置索引。""" global _index if _index is not None: return _index _index = [] if not RAW_CONFIGS_DIR.exists(): return _index for yml_path in RAW_CONFIGS_DIR.rglob("*.yml"): try: text = yml_path.read_text(encoding="utf-8", errors="ignore") config = yaml.safe_load(text) if not isinstance(config, dict): continue entry = { "path": str(yml_path.relative_to(PROJECT_ROOT)), "source": yml_path.parent.name, "text": text[:2000], "fields": { "type": config.get("type"), "tools": _extract_tools(config), "model_provider": _nested_get(config, "model", "provider"), "has_rag": bool(config.get("rag", {}).get("enabled")), "has_mcp": bool(config.get("mcp")), "has_guard": bool(config.get("guard")), }, "keywords": _extract_keywords(text), } _index.append(entry) except Exception: continue # 同样扫描 .yaml 文件 for yml_path in RAW_CONFIGS_DIR.rglob("*.yaml"): try: text = yml_path.read_text(encoding="utf-8", errors="ignore") config = yaml.safe_load(text) if not isinstance(config, dict): continue entry = { "path": str(yml_path.relative_to(PROJECT_ROOT)), "source": yml_path.parent.name, "text": text[:2000], "fields": { "type": config.get("type"), "tools": _extract_tools(config), "model_provider": _nested_get(config, "model", "provider"), "has_rag": bool(config.get("rag", {}).get("enabled")), "has_mcp": bool(config.get("mcp")), "has_guard": bool(config.get("guard")), }, "keywords": _extract_keywords(text), } _index.append(entry) except Exception: continue return _index def _extract_tools(config: dict) -> List[str]: """从配置中提取工具列表。""" tools = [] mcp = config.get("mcp", {}) if isinstance(mcp, dict): tools.extend(mcp.get("localTools", [])) for server, tool_list in mcp.get("onlineTool", {}).items(): if isinstance(tool_list, list): tools.extend(tool_list) return tools def _nested_get(d: dict, *keys): """安全的嵌套字典取值。""" for k in keys: if isinstance(d, dict): d = d.get(k) else: return None return d def _extract_keywords(text: str) -> List[str]: """从配置文本中提取关键词(用于搜索匹配)。""" # 提取英文单词和中文词 words = re.findall(r'[a-zA-Z_]{3,}', text.lower()) # 去重,保留前 50 个 seen = set() result = [] for w in words: if w not in seen and w not in _STOP_WORDS: seen.add(w) result.append(w) if len(result) >= 50: break return result _STOP_WORDS = { "the", "and", "for", "with", "that", "this", "from", "have", "has", "are", "was", "were", "been", "being", "will", "would", "could", "should", "may", "might", "must", "shall", "can", "not", "but", "true", "false", "null", "none", "string", "int", "float", "bool", "default", "type", "name", "value", "description", } # ── 公开接口 ────────────────────────────────────────────── def search_configs(keywords: List[str], max_results: int = 5) -> List[Dict]: """ 按关键词搜索配置库。 Args: keywords: 搜索关键词列表(英文) max_results: 最大返回数量 Returns: 匹配的配置列表,按相关性排序 """ index = _build_index() if not index: return [] keywords_lower = [k.lower() for k in keywords] scored = [] for entry in index: score = 0.0 entry_keywords = set(entry["keywords"]) entry_text = entry["text"].lower() for kw in keywords_lower: # 精确关键词匹配 if kw in entry_keywords: score += 2.0 # 文本子串匹配 elif kw in entry_text: score += 1.0 # 部分匹配(关键词包含搜索词) elif any(kw in ek for ek in entry_keywords): score += 0.5 # 字段加分 fields = entry["fields"] for kw in keywords_lower: if fields.get("type") and kw in str(fields["type"]).lower(): score += 1.5 if kw in [t.lower() for t in fields.get("tools", [])]: score += 1.5 if score > 0: scored.append((score, entry)) scored.sort(key=lambda x: x[0], reverse=True) results = [] for score, entry in scored[:max_results]: results.append({ "path": entry["path"], "source": entry["source"], "relevance": round(min(score / (len(keywords_lower) * 3), 1.0), 2), "fields": entry["fields"], "preview": entry["text"][:500], }) return results def read_config(config_path: str) -> Optional[Dict]: """ 读取指定路径的配置文件全文。 Args: config_path: 相对于 PROJECT_ROOT 的路径 Returns: 解析后的配置字典,或 None """ full_path = PROJECT_ROOT / config_path if not full_path.exists(): return None try: text = full_path.read_text(encoding="utf-8") config = yaml.safe_load(text) return { "path": config_path, "raw_text": text, "parsed": config if isinstance(config, dict) else {}, } except Exception as e: return {"path": config_path, "error": str(e)}