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.
- skill_advisor/__init__.py +41 -0
- skill_advisor/adapters/__init__.py +313 -0
- skill_advisor/advisor.py +325 -0
- skill_advisor/cli.py +858 -0
- skill_advisor/config/skill_map.json +22 -0
- skill_advisor/indexer.py +199 -0
- skill_advisor/matcher.py +214 -0
- skill_advisor/memory.py +261 -0
- skill_advisor/runtime/commands.py +356 -0
- skill_advisor/runtime/config.py +58 -0
- skill_advisor/runtime/daemon.py +633 -0
- skill_advisor/runtime/dropin.py +206 -0
- skill_advisor/runtime/endpoints/validate.py +104 -0
- skill_advisor/runtime/force.py +231 -0
- skill_advisor/runtime/lock.py +154 -0
- skill_advisor/runtime/validate_core.py +169 -0
- skill_advisor/skill_map.py +217 -0
- skill_advisor/synonyms.py +121 -0
- sra_agent-0.0.0.dev0.dist-info/METADATA +431 -0
- sra_agent-0.0.0.dev0.dist-info/RECORD +24 -0
- sra_agent-0.0.0.dev0.dist-info/WHEEL +5 -0
- sra_agent-0.0.0.dev0.dist-info/entry_points.txt +3 -0
- sra_agent-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- sra_agent-0.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -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))
|
skill_advisor/advisor.py
ADDED
|
@@ -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
|
+
}
|