| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- """
- 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)}
|