sra-agent 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ # SRA Agent — Skill Runtime Advisor
2
+ # 让 AI Agent 知道自己有什么能力,以及什么时候该用什么能力。
3
+ # License: MIT
4
+
5
+ # ⚠️ 版本号由 setuptools-scm 从 git tag 自动生成
6
+ # 不要手动修改!版本来源见 pyproject.toml [tool.setuptools_scm]
7
+ # 层级 1: setuptools-scm 生成的 _version.py(built 包)
8
+ try:
9
+ from ._version import version as __version__
10
+ except ImportError:
11
+ try:
12
+ # 层级 2: importlib.metadata(editable install + pip install)
13
+ from importlib.metadata import version as _v
14
+ __version__ = _v("sra-agent")
15
+ except (ImportError, ModuleNotFoundError):
16
+ # 层级 3: git describe(开发环境 fallback)
17
+ try:
18
+ import os
19
+ import subprocess
20
+ tag = subprocess.check_output(
21
+ ["git", "describe", "--tags", "--dirty=.dirty", "--always"],
22
+ cwd=os.path.dirname(os.path.abspath(__file__)),
23
+ stderr=subprocess.DEVNULL,
24
+ timeout=5,
25
+ ).decode().strip().lstrip("v")
26
+ __version__ = tag
27
+ except Exception:
28
+ __version__ = "0.0.0-dev"
29
+
30
+ __author__ = "Emma (SRA Team), Kei"
31
+
32
+ from .adapters import get_adapter, list_adapters
33
+ from .advisor import SkillAdvisor
34
+ from .runtime.daemon import SRaDDaemon
35
+
36
+ __all__ = [
37
+ "SkillAdvisor",
38
+ "SRaDDaemon",
39
+ "get_adapter",
40
+ "list_adapters",
41
+ ]
@@ -0,0 +1,313 @@
1
+ """
2
+ SRA Agent 适配器 — 让 SRA 能接入任何 AI Agent 系统
3
+
4
+ 适配器将 SRA 的推荐结果转换成特定 Agent 能理解的格式。
5
+ 每种 Agent 一个适配器类,统一接口。
6
+
7
+ 支持的 Agent:
8
+ - Hermes Agent (原生)
9
+ - Claude Code (Anthropic CLI)
10
+ - OpenAI Codex CLI
11
+ - OpenCode CLI
12
+ - 通用 OpenAI API 格式
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import socket
18
+ import sys
19
+ from typing import Dict, List
20
+
21
+ # ── Socket 客户端 ───────────────────────────
22
+
23
+ SOCKET_FILE = os.path.expanduser("~/.sra/srad.sock")
24
+
25
+
26
+ def _sra_socket_request(request: dict, timeout: float = 5.0) -> dict:
27
+ """通过 Unix Socket 向 SRA Daemon 发送请求"""
28
+ if not os.path.exists(SOCKET_FILE):
29
+ return {"error": "SRA Daemon 未运行", "suggestion": "请先运行 'sra start'"}
30
+
31
+ try:
32
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
33
+ client.settimeout(timeout)
34
+ client.connect(SOCKET_FILE)
35
+ client.sendall(json.dumps(request).encode("utf-8"))
36
+ response = client.recv(65536).decode("utf-8")
37
+ client.close()
38
+ return json.loads(response)
39
+ except socket.timeout:
40
+ return {"error": "SRA Daemon 超时"}
41
+ except ConnectionRefusedError:
42
+ return {"error": "SRA Daemon 连接被拒", "suggestion": "请检查 'sra status'"}
43
+ except Exception as e:
44
+ return {"error": str(e)}
45
+
46
+
47
+ # ── 基础适配器接口 ─────────────────────────
48
+
49
+ class BaseAdapter:
50
+ """所有 Agent 适配器的基类"""
51
+
52
+ def __init__(self, socket_path: str = SOCKET_FILE):
53
+ self.socket_path = socket_path
54
+
55
+ def recommend(self, query: str, top_k: int = 3) -> List[Dict]:
56
+ """推荐技能 — 所有适配器共享的核心逻辑"""
57
+ result = _sra_socket_request({
58
+ "action": "recommend",
59
+ "params": {"query": query, "top_k": top_k},
60
+ })
61
+ if "error" in result:
62
+ return []
63
+ return result.get("result", {}).get("recommendations", [])
64
+
65
+ def format_suggestion(self, recommendations: List[Dict]) -> str:
66
+ """将推荐结果格式化为该 Agent 的提示文本"""
67
+ raise NotImplementedError
68
+
69
+ def ping(self) -> bool:
70
+ """检查 SRA Daemon 是否运行"""
71
+ result = _sra_socket_request({"action": "ping"})
72
+ return result.get("pong", False) and result.get("status") == "ok"
73
+
74
+
75
+ # ── Hermes Agent 适配器 ─────────────────────
76
+
77
+ class HermesAdapter(BaseAdapter):
78
+ """Hermes Agent 适配器 — 生成 <available_skills> 增强块"""
79
+
80
+ def format_suggestion(self, recommendations: List[Dict]) -> str:
81
+ if not recommendations:
82
+ return ""
83
+
84
+ lines = ["💡 SRA 技能推荐:"]
85
+ for r in recommendations:
86
+ icon = "✅" if r.get("confidence") == "high" else "💡"
87
+ lines.append(
88
+ f" {icon} `{r['skill']}` (得分: {r['score']})"
89
+ )
90
+ if r.get("reasons"):
91
+ lines.append(f" 理由: {'; '.join(r['reasons'][:2])}")
92
+
93
+ top = recommendations[0]
94
+ if top.get("confidence") == "high":
95
+ lines.append(f"\n⚡ 建议自动加载: skill_view('{top['skill']}')")
96
+
97
+ return "\n".join(lines)
98
+
99
+ def to_system_prompt_block(self, skills_count: int = None) -> str:
100
+ """生成增强版 system prompt 块"""
101
+ stats = _sra_socket_request({"action": "stats"})
102
+ if "error" in stats:
103
+ return ""
104
+
105
+ s = stats.get("stats", stats)
106
+ count = skills_count or s.get("skills_count", 0)
107
+
108
+ return (
109
+ f"\n## SRA Runtime ({s.get('version', '1.2.1')})\n"
110
+ f"SRA 是一个独立运行的技能推荐引擎。\n"
111
+ f"当前管理 {count} 个技能。\n"
112
+ f"API: Unix Socket ({SOCKET_FILE}) / HTTP (:{s.get('config', {}).get('http_port', 8536)})\n"
113
+ f"使用 'sra --query <输入>' 或 HTTP POST 查询推荐。\n"
114
+ )
115
+
116
+ def to_proxy_format(self, message: str) -> dict:
117
+ """以 Proxy 格式获取推荐(消息前置推理用)
118
+
119
+ POST /recommend 请求 {'message': msg} 的响应格式。
120
+ 返回 rag_context + should_auto_load + recommendations。
121
+ """
122
+ result = _sra_socket_request({
123
+ "action": "recommend",
124
+ "params": {"query": message, "top_k": 5},
125
+ })
126
+ if "error" in result:
127
+ return {
128
+ "rag_context": "",
129
+ "recommendations": [],
130
+ "top_skill": None,
131
+ "should_auto_load": False,
132
+ "sra_available": False,
133
+ }
134
+
135
+ recs = result.get("result", {}).get("recommendations", [])
136
+ top_skill = None
137
+ should_auto_load = False
138
+ rag_lines = []
139
+
140
+ if recs:
141
+ top = recs[0]
142
+ top_skill = top["skill"]
143
+ score = top["score"]
144
+ should_auto_load = score >= 80
145
+
146
+ rag_lines.append("── [SRA Skill 推荐] ──────────────────────────────")
147
+ for i, r in enumerate(recs[:5]):
148
+ flag = "⭐" if i == 0 else " "
149
+ conf = r["confidence"]
150
+ s_name = r["skill"]
151
+ s_score = r["score"]
152
+ s_reasons = " | ".join(r.get("reasons", [])[:2])
153
+ rag_lines.append(f" {flag} [{conf:>6}] {s_name} ({s_score:.1f}分) — {s_reasons}")
154
+
155
+ if should_auto_load:
156
+ rag_lines.append(f"\n ⚡ 强推荐自动加载: {top_skill}")
157
+ else:
158
+ rag_lines.append("\n 💡 建议: 可参考上述 skill")
159
+
160
+ rag_lines.append("── ──────────────────────────────────────────────")
161
+
162
+ return {
163
+ "rag_context": "\n".join(rag_lines),
164
+ "recommendations": [
165
+ {
166
+ "skill": r["skill"],
167
+ "score": r["score"],
168
+ "confidence": r["confidence"],
169
+ "reasons": r.get("reasons", []),
170
+ "description": r.get("description", ""),
171
+ }
172
+ for r in recs[:5]
173
+ ],
174
+ "top_skill": top_skill,
175
+ "should_auto_load": should_auto_load,
176
+ "sra_available": True,
177
+ }
178
+
179
+
180
+ # ── Claude Code 适配器 ──────────────────────
181
+
182
+ class ClaudeCodeAdapter(BaseAdapter):
183
+ """Anthropic Claude Code CLI 适配器"""
184
+
185
+ def format_suggestion(self, recommendations: List[Dict]) -> str:
186
+ if not recommendations:
187
+ return ""
188
+
189
+ lines = ["[SRA Skill Recommendation]"]
190
+ for r in recommendations[:2]:
191
+ lines.append(f"- {r['skill']} (confidence: {r.get('confidence', 'medium')})")
192
+ if r.get("description"):
193
+ lines.append(f" {r['description'][:80]}")
194
+
195
+ return "\n".join(lines)
196
+
197
+ def to_claude_tool_format(self, recommendations: List[Dict]) -> List[Dict]:
198
+ """转换为 Claude Tool Use 格式"""
199
+ tools = []
200
+ for r in recommendations[:3]:
201
+ tools.append({
202
+ "name": r["skill"],
203
+ "description": r.get("description", "")[:200],
204
+ "input_schema": {
205
+ "type": "object",
206
+ "properties": {
207
+ "task": {
208
+ "type": "string",
209
+ "description": f"Use {r['skill']} to handle the user request"
210
+ }
211
+ },
212
+ "required": ["task"]
213
+ }
214
+ })
215
+ return tools
216
+
217
+
218
+ # ── OpenAI Codex CLI 适配器 ─────────────────
219
+
220
+ class CodexAdapter(BaseAdapter):
221
+ """OpenAI Codex CLI 适配器"""
222
+
223
+ def format_suggestion(self, recommendations: List[Dict]) -> str:
224
+ if not recommendations:
225
+ return ""
226
+
227
+ lines = ["# SRA recommended skills"]
228
+ for r in recommendations:
229
+ lines.append(f"# - {r['skill']}: {r.get('description', '')[:80]}")
230
+
231
+ return "\n".join(lines)
232
+
233
+ def to_openai_tool_format(self, recommendations: List[Dict]) -> List[Dict]:
234
+ """转换为 OpenAI Function Calling 格式"""
235
+ return [
236
+ {
237
+ "type": "function",
238
+ "function": {
239
+ "name": r["skill"],
240
+ "description": r.get("description", "")[:200],
241
+ "parameters": {
242
+ "type": "object",
243
+ "properties": {
244
+ "query": {
245
+ "type": "string",
246
+ "description": f"The task for {r['skill']}"
247
+ }
248
+ },
249
+ "required": ["query"]
250
+ }
251
+ }
252
+ }
253
+ for r in recommendations[:3]
254
+ ]
255
+
256
+
257
+ # ── 通用 CLI 适配器 ─────────────────────────
258
+
259
+ class GenericCLIAdapter(BaseAdapter):
260
+ """通用 CLI Agent 适配器 — 输出纯文本格式"""
261
+
262
+ def format_suggestion(self, recommendations: List[Dict]) -> str:
263
+ if not recommendations:
264
+ return ""
265
+
266
+ lines = ["=== SRA Skill Recommendation ==="]
267
+ for i, r in enumerate(recommendations[:3], 1):
268
+ lines.append(f"{i}. {r['skill']}")
269
+ lines.append(f" Score: {r['score']} | Confidence: {r.get('confidence', 'medium')}")
270
+ if r.get("description"):
271
+ lines.append(f" {r['description'][:100]}")
272
+ lines.append("=" * 35)
273
+
274
+ return "\n".join(lines)
275
+
276
+
277
+ # ── 适配器工厂 ──────────────────────────────
278
+
279
+ ADAPTER_REGISTRY = {
280
+ "hermes": HermesAdapter,
281
+ "claude": ClaudeCodeAdapter,
282
+ "codex": CodexAdapter,
283
+ "opencode": GenericCLIAdapter,
284
+ "generic": GenericCLIAdapter,
285
+ }
286
+
287
+
288
+ def get_adapter(agent_type: str = "hermes") -> BaseAdapter:
289
+ """获取对应 Agent 的适配器"""
290
+ adapter_class = ADAPTER_REGISTRY.get(agent_type.lower(), GenericCLIAdapter)
291
+ return adapter_class()
292
+
293
+
294
+ def list_adapters() -> List[str]:
295
+ """列出所有支持的 Agent 类型"""
296
+ return list(ADAPTER_REGISTRY.keys())
297
+
298
+
299
+ # ── 独立测试 ────────────────────────────────
300
+
301
+ if __name__ == "__main__":
302
+ import sys
303
+
304
+ agent = sys.argv[1] if len(sys.argv) > 1 else "hermes"
305
+ query = sys.argv[2] if len(sys.argv) > 2 else "帮我画个架构图"
306
+
307
+ adapter = get_adapter(agent)
308
+ recs = adapter.recommend(query)
309
+
310
+ print(f"Agent: {agent}")
311
+ print(f"Query: {query}")
312
+ print()
313
+ print(adapter.format_suggestion(recs))
@@ -0,0 +1,325 @@
1
+ """
2
+ SRA - Skill Runtime Advisor
3
+ 让 AI Agent 知道自己有什么能力,以及什么时候该用什么能力。
4
+
5
+ 主入口:SkillAdvisor 类
6
+ """
7
+
8
+ import os
9
+ import time
10
+ from typing import Dict, List
11
+
12
+ from .indexer import SkillIndexer
13
+ from .matcher import SkillMatcher
14
+ from .memory import SceneMemory
15
+ from .synonyms import SYNONYMS
16
+
17
+
18
+ class SkillAdvisor:
19
+ """技能推荐引擎主类"""
20
+
21
+ # 推荐阈值
22
+ THRESHOLD_STRONG = 80 # 强推荐:自动加载
23
+ THRESHOLD_WEAK = 40 # 弱推荐:附加提示
24
+
25
+ def __init__(self, skills_dir: str = None, data_dir: str = None):
26
+ """
27
+ 初始化 SRA 引擎
28
+
29
+ Args:
30
+ skills_dir: 技能目录路径。默认为 ~/.hermes/skills
31
+ data_dir: 数据持久化目录。默认为 ~/.sra/data
32
+ """
33
+ self.skills_dir = skills_dir or os.path.expanduser("~/.hermes/skills")
34
+ self.data_dir = data_dir or os.path.expanduser(
35
+ os.environ.get("SRA_DATA_DIR", "~/.sra/data")
36
+ )
37
+
38
+ # 确保数据目录存在
39
+ os.makedirs(self.data_dir, exist_ok=True)
40
+
41
+ # 初始化子模块
42
+ self.indexer = SkillIndexer(self.skills_dir, self.data_dir)
43
+ self.matcher = SkillMatcher(SYNONYMS)
44
+ self.memory = SceneMemory(self.data_dir)
45
+
46
+ # 懒加载索引
47
+ self._index_loaded = False
48
+
49
+ def _ensure_index(self):
50
+ """确保索引已加载"""
51
+ if not self._index_loaded:
52
+ self.indexer.load_or_build()
53
+ self._index_loaded = True
54
+
55
+ def refresh_index(self) -> int:
56
+ """强制刷新技能索引"""
57
+ count = self.indexer.build()
58
+ self._index_loaded = True
59
+ return count
60
+
61
+ def build_contract(self, query: str, scored: List[Dict]) -> Dict:
62
+ """为推荐结果构建技能契约
63
+
64
+ Args:
65
+ query: 用户原始输入
66
+ scored: 已评分排序的技能列表(带 .score 字段)
67
+
68
+ Returns:
69
+ {
70
+ "task_type": str, # 推测的任务类型
71
+ "required_skills": [str], # 强推荐(score >= 80)
72
+ "optional_skills": [str], # 可选推荐(40 <= score < 80)
73
+ "confidence": str, # high / medium / low
74
+ "summary": str, # 自然语言描述
75
+ }
76
+ """
77
+ required = [s["skill"] for s in scored if s.get("score", 0) >= self.THRESHOLD_STRONG]
78
+ optional = [s["skill"] for s in scored if self.THRESHOLD_WEAK <= s.get("score", 0) < self.THRESHOLD_STRONG]
79
+
80
+ # 推测任务类型:取最高分技能的 category
81
+ categories = {s.get("category", "") for s in scored if s.get("category")}
82
+ task_type = next(iter(categories)) if len(categories) == 1 else (", ".join(categories) if categories else "general")
83
+
84
+ # 置信度
85
+ max_score = max((s.get("score", 0) for s in scored), default=0)
86
+ if max_score >= 80:
87
+ confidence = "high"
88
+ elif max_score >= 60:
89
+ confidence = "medium"
90
+ else:
91
+ confidence = "low"
92
+
93
+ # 自然语言总结
94
+ parts = []
95
+ if required:
96
+ parts.append(f"必须加载: {', '.join(required)}")
97
+ if optional:
98
+ parts.append(f"建议参考: {', '.join(optional)}")
99
+ summary = f"任务类型「{task_type}」— " + ("; ".join(parts) if parts else "无特定技能推荐")
100
+
101
+ return {
102
+ "task_type": task_type,
103
+ "required_skills": required,
104
+ "optional_skills": optional,
105
+ "confidence": confidence,
106
+ "summary": summary,
107
+ }
108
+
109
+ def recommend(self, query: str, top_k: int = 3) -> Dict:
110
+ """
111
+ 推荐匹配技能
112
+
113
+ Args:
114
+ query: 用户输入
115
+ top_k: 返回 top-k 结果
116
+
117
+ Returns:
118
+ {
119
+ "recommendations": [...],
120
+ "processing_ms": float,
121
+ "skills_scanned": int,
122
+ "query": str,
123
+ "contract": { # 🆕 契约机制
124
+ "task_type": str,
125
+ "required_skills": [str],
126
+ "optional_skills": [str],
127
+ "confidence": str,
128
+ "summary": str,
129
+ }
130
+ }
131
+ """
132
+ self._ensure_index()
133
+ start = time.time()
134
+
135
+ skills = self.indexer.get_skills()
136
+ stats = self.memory.load()
137
+
138
+ # 提取输入关键词
139
+ input_words = self.indexer.extract_keywords(query)
140
+ input_expanded = self.indexer.expand_with_synonyms(input_words)
141
+
142
+ if not input_expanded:
143
+ return {"recommendations": [], "processing_ms": 0, "skills_scanned": 0, "query": query}
144
+
145
+ # 对所有 skill 评分
146
+ scored = []
147
+ for skill in skills:
148
+ total, details, reasons = self.matcher.score(
149
+ input_expanded, skill, stats
150
+ )
151
+
152
+ if total >= self.THRESHOLD_WEAK:
153
+ scored.append({
154
+ "skill": skill["name"],
155
+ "description": skill.get("description", "")[:120],
156
+ "category": skill.get("category", ""),
157
+ "score": round(total, 1),
158
+ "confidence": "high" if total >= self.THRESHOLD_STRONG else "medium",
159
+ "reasons": reasons[:3],
160
+ "details": details,
161
+ })
162
+
163
+ # 排序取 top-k
164
+ scored.sort(key=lambda x: x["score"], reverse=True)
165
+ top = scored[:top_k]
166
+
167
+ # 更新推荐计数
168
+ self.memory.increment_recommendations()
169
+
170
+ # 🆕 构建契约
171
+ contract = self.build_contract(query, scored)
172
+
173
+ elapsed = round((time.time() - start) * 1000, 1)
174
+
175
+ return {
176
+ "recommendations": top,
177
+ "processing_ms": elapsed,
178
+ "skills_scanned": len(skills),
179
+ "query": query,
180
+ "contract": contract, # 🆕
181
+ }
182
+
183
+ def recheck(self, conversation_summary: str, loaded_skills: List[str] = None,
184
+ top_k: int = 5) -> Dict:
185
+ """
186
+ 长任务上下文漂移重检
187
+
188
+ 在长任务执行过程中定期调用,检测上下文是否漂移,
189
+ 以及是否有新的技能应该被加载。
190
+
191
+ Args:
192
+ conversation_summary: 当前对话摘要
193
+ loaded_skills: 已经加载的技能名称列表
194
+ top_k: 推荐 top-k 技能
195
+
196
+ Returns:
197
+ {
198
+ "has_drift": bool, # 是否检测到漂移
199
+ "missing_skills": [...], # 推荐但未加载的技能
200
+ "drift_score": float, # 漂移程度 (0-1)
201
+ "recommendations": [...], # 完整推荐结果
202
+ "loaded_skills_count": int,
203
+ "processing_ms": float,
204
+ }
205
+ """
206
+ # 1. 运行推荐算法
207
+ result = self.recommend(conversation_summary, top_k=top_k)
208
+ recs = result.get("recommendations", [])
209
+ elapsed = result.get("processing_ms", 0)
210
+
211
+ # 2. 对比已加载技能
212
+ loaded = loaded_skills or []
213
+ loaded_lower = [s.lower() for s in loaded]
214
+ [r["skill"].lower() for r in recs]
215
+
216
+ missing = [r for r in recs if r["skill"].lower() not in loaded_lower]
217
+
218
+ # 3. 计算漂移分数
219
+ if recs:
220
+ drift_score = round(len(missing) / len(recs), 2)
221
+ else:
222
+ drift_score = 0.0
223
+ has_drift = len(missing) > 0 and drift_score >= 0.2
224
+
225
+ # 4. 记录推荐
226
+ if has_drift:
227
+ self.memory.increment_recommendations()
228
+
229
+ return {
230
+ "has_drift": has_drift,
231
+ "drift_score": drift_score,
232
+ "missing_skills": missing,
233
+ "recommendations": recs,
234
+ "loaded_skills_count": len(loaded),
235
+ "processing_ms": elapsed,
236
+ "query": conversation_summary[:100],
237
+ }
238
+
239
+ def record_usage(self, skill_name: str, user_input: str, accepted: bool = True):
240
+ """记录技能使用场景"""
241
+ self.memory.record_usage(skill_name, user_input, accepted)
242
+
243
+ def record_view(self, skill_name: str):
244
+ """记录技能被查看"""
245
+ self.memory.record_view(skill_name)
246
+
247
+ def record_use(self, skill_name: str):
248
+ """记录技能被使用"""
249
+ self.memory.record_use(skill_name)
250
+
251
+ def record_skip(self, skill_name: str, reason: str = ""):
252
+ """记录技能被跳过"""
253
+ self.memory.record_skip(skill_name, reason)
254
+
255
+ def get_compliance_stats(self) -> Dict:
256
+ """获取遵循率统计"""
257
+ return self.memory.get_compliance_stats()
258
+
259
+ def show_stats(self) -> Dict:
260
+ """获取统计信息"""
261
+ self._ensure_index()
262
+ stats = self.memory.load()
263
+ skills = self.indexer.get_skills()
264
+
265
+ return {
266
+ "total_skills": len(skills),
267
+ "total_recommendations": stats.get("total_recommendations", 0),
268
+ "scene_patterns": len(stats.get("scene_patterns", [])),
269
+ "skills_with_stats": len(stats.get("skills", {})),
270
+ "memory": stats,
271
+ }
272
+
273
+ def analyze_coverage(self) -> Dict:
274
+ """
275
+ 分析 SRA 对技能目录的覆盖率
276
+ 返回:每个 skill 是否能被 SRA 的 trigger 机制识别
277
+ """
278
+ self._ensure_index()
279
+ skills = self.indexer.get_skills()
280
+ stats = self.memory.load()
281
+
282
+ results = []
283
+ covered = 0
284
+ for skill in skills:
285
+ # 用 skill 自己的 triggers 作为测试查询
286
+ triggers = skill.get("triggers", [])
287
+ name = skill.get("name", "")
288
+
289
+ # 构造测试查询
290
+ test_queries = []
291
+ if triggers:
292
+ test_queries.extend(triggers[:3])
293
+ # 用 name 作为后备查询
294
+ test_queries.append(name.replace("-", " "))
295
+
296
+ # 测试所有查询
297
+ max_score = 0
298
+ for q in test_queries:
299
+ if not q:
300
+ continue
301
+ input_words = self.indexer.extract_keywords(q)
302
+ input_expanded = self.indexer.expand_with_synonyms(input_words)
303
+ if input_expanded:
304
+ total, _, _ = self.matcher.score(input_expanded, skill, stats)
305
+ max_score = max(max_score, total)
306
+
307
+ is_covered = max_score >= self.THRESHOLD_WEAK
308
+ if is_covered:
309
+ covered += 1
310
+
311
+ results.append({
312
+ "name": skill["name"],
313
+ "category": skill.get("category", ""),
314
+ "has_triggers": len(triggers) > 0,
315
+ "max_score": round(max_score, 1),
316
+ "covered": is_covered,
317
+ })
318
+
319
+ return {
320
+ "total": len(skills),
321
+ "covered": covered,
322
+ "coverage_rate": round(covered / len(skills) * 100, 1) if skills else 0,
323
+ "not_covered": [r for r in results if not r["covered"]],
324
+ "details": results,
325
+ }