skill-self-evolution 0.2.0__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_self_evolution/__init__.py +27 -0
- skill_self_evolution/ai_assisted_executor.py +244 -0
- skill_self_evolution/config.py +87 -0
- skill_self_evolution/config_loader.py +241 -0
- skill_self_evolution/context.py +22 -0
- skill_self_evolution/deepseek.py +168 -0
- skill_self_evolution/evolver.py +386 -0
- skill_self_evolution/executor.py +471 -0
- skill_self_evolution/fallback.py +129 -0
- skill_self_evolution/loader.py +201 -0
- skill_self_evolution/logger.py +84 -0
- skill_self_evolution/models.py +147 -0
- skill_self_evolution-0.2.0.dist-info/METADATA +12 -0
- skill_self_evolution-0.2.0.dist-info/RECORD +16 -0
- skill_self_evolution-0.2.0.dist-info/WHEEL +5 -0
- skill_self_evolution-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SkillExecutor — Skill 执行引擎主类。
|
|
3
|
+
|
|
4
|
+
流程:
|
|
5
|
+
1. Pydantic 输入校验
|
|
6
|
+
2. trace_id 生成 + contextvars 注入
|
|
7
|
+
3. 加载 evolve.toml(获取 ai_role)
|
|
8
|
+
4. 加载 skill.md + run.py(磁盘)
|
|
9
|
+
5. 加载 rules_config + prompt(MySQL)
|
|
10
|
+
6. execute() → AI 常识判断 → 合理则过 / 不合理则 AI 从原始数据重选
|
|
11
|
+
7. 写 JSONL 日志(根据 ai_role 自动计算 is_failure)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from skill_self_evolution.context import set_trace_id
|
|
21
|
+
from skill_self_evolution.deepseek import CircuitBreaker, DeepSeekClient
|
|
22
|
+
from skill_self_evolution.fallback import FallbackConfig, FallbackStrategy
|
|
23
|
+
from skill_self_evolution.loader import SkillLoader, SkillModule
|
|
24
|
+
from skill_self_evolution.logger import SkillLogger
|
|
25
|
+
from skill_self_evolution.models import (
|
|
26
|
+
AiReselectionResult,
|
|
27
|
+
AiValidationResult,
|
|
28
|
+
FallbackConfigModel,
|
|
29
|
+
SkillInput,
|
|
30
|
+
SkillOutput,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SkillExecutor:
|
|
37
|
+
"""Skill 执行引擎。
|
|
38
|
+
|
|
39
|
+
使用方式:
|
|
40
|
+
executor = SkillExecutor(db_config=...)
|
|
41
|
+
output = await executor.run("nickname-selector", input_data)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
skill_base_dir: Path | None = None,
|
|
47
|
+
deepseek_api_key: str = "",
|
|
48
|
+
deepseek_api_base: str = "https://api.deepseek.com/v1",
|
|
49
|
+
deepseek_model: str = "deepseek-v4-flash",
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Args:
|
|
53
|
+
skill_base_dir: Skill 根目录(默认 backend/config/services/skill/)
|
|
54
|
+
deepseek_api_key: DeepSeek API Key
|
|
55
|
+
deepseek_api_base: DeepSeek API 基础地址
|
|
56
|
+
deepseek_model: 模型名称
|
|
57
|
+
"""
|
|
58
|
+
self._loader = SkillLoader(skill_base_dir)
|
|
59
|
+
self._deepseek = DeepSeekClient(
|
|
60
|
+
api_key=deepseek_api_key,
|
|
61
|
+
api_base=deepseek_api_base,
|
|
62
|
+
model=deepseek_model,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def circuit_breaker(self) -> CircuitBreaker:
|
|
67
|
+
"""获取全局熔断器,可在外部调整阈值。"""
|
|
68
|
+
return self._deepseek.circuit_breaker
|
|
69
|
+
|
|
70
|
+
def load_skill(self, skill_name: str) -> SkillModule:
|
|
71
|
+
"""预加载 Skill(可选,run() 会自动加载)。"""
|
|
72
|
+
return self._loader.load(skill_name)
|
|
73
|
+
|
|
74
|
+
async def run(
|
|
75
|
+
self,
|
|
76
|
+
skill_name: str,
|
|
77
|
+
input_data: Any,
|
|
78
|
+
*,
|
|
79
|
+
rules_config: dict | None = None,
|
|
80
|
+
prompt_config: dict | None = None,
|
|
81
|
+
trace_id: str | None = None,
|
|
82
|
+
) -> SkillOutput:
|
|
83
|
+
"""执行 Skill 主流程。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
skill_name: Skill 名称(如 "nickname-selector")
|
|
87
|
+
input_data: 业务输入数据(dict 或 Pydantic model)
|
|
88
|
+
rules_config: rules_config.yaml 解析后的 dict(可选,默认从 MySQL 加载)
|
|
89
|
+
prompt_config: prompt.yaml 解析后的 dict(可选,默认从 MySQL 加载)
|
|
90
|
+
trace_id: 外部传入的 trace_id(可选,未传入则使用 input_data 中的或自动生成)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
SkillOutput: 执行结果
|
|
94
|
+
"""
|
|
95
|
+
start_time = time.monotonic()
|
|
96
|
+
|
|
97
|
+
# 1. 加载 Skill 模块
|
|
98
|
+
skill = self._loader.load(skill_name)
|
|
99
|
+
|
|
100
|
+
# 2. 确定 trace_id
|
|
101
|
+
effective_trace_id = (
|
|
102
|
+
trace_id
|
|
103
|
+
or (getattr(input_data, "trace_id", None))
|
|
104
|
+
or str(__import__("uuid").uuid4())
|
|
105
|
+
)
|
|
106
|
+
set_trace_id(effective_trace_id)
|
|
107
|
+
|
|
108
|
+
# 3. 构建 SkillInput(Pydantic 校验入口)
|
|
109
|
+
if isinstance(input_data, dict):
|
|
110
|
+
skill_input = SkillInput(trace_id=effective_trace_id, input_data=input_data)
|
|
111
|
+
else:
|
|
112
|
+
skill_input = SkillInput(trace_id=effective_trace_id, input_data=input_data)
|
|
113
|
+
|
|
114
|
+
warnings: list[str] = []
|
|
115
|
+
|
|
116
|
+
# 4. 加载降级配置(Pydantic 校验)
|
|
117
|
+
fallback_cfg = self._build_fallback_config(rules_config or {})
|
|
118
|
+
fallback = FallbackStrategy(fallback_cfg, self._deepseek.circuit_breaker)
|
|
119
|
+
|
|
120
|
+
# 5. 合并配置传给 execute()
|
|
121
|
+
merged_config = {
|
|
122
|
+
"rules_config": rules_config or {},
|
|
123
|
+
"prompt_config": prompt_config or {},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# 6. 执行规则阶段
|
|
127
|
+
try:
|
|
128
|
+
rule_output = skill.execute(skill_input, merged_config)
|
|
129
|
+
if not isinstance(rule_output, SkillOutput):
|
|
130
|
+
rule_output = SkillOutput(
|
|
131
|
+
source="rule",
|
|
132
|
+
result=rule_output if isinstance(rule_output, dict) else {"value": rule_output},
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.exception("Skill [%s] 规则执行异常", skill_name)
|
|
136
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
137
|
+
output = SkillOutput(
|
|
138
|
+
source="rule",
|
|
139
|
+
result={"error": str(e)},
|
|
140
|
+
warnings=[f"规则执行异常: {e}"],
|
|
141
|
+
)
|
|
142
|
+
self._log(skill, effective_trace_id, True, skill_input, output, None, None, output, warnings, elapsed)
|
|
143
|
+
return output
|
|
144
|
+
|
|
145
|
+
ai_validation: AiValidationResult | None = None
|
|
146
|
+
ai_reselection: AiReselectionResult | None = None
|
|
147
|
+
|
|
148
|
+
# 7. AI 处理阶段
|
|
149
|
+
if skill.ai_role == "correction":
|
|
150
|
+
# 纠错型:AI 常识判断 → 不合理则重选
|
|
151
|
+
fb_check = fallback.check_before_ai()
|
|
152
|
+
if fb_check.skip_ai:
|
|
153
|
+
warnings.extend(fb_check.warnings)
|
|
154
|
+
rule_output.ai_validated = False
|
|
155
|
+
rule_output.warnings = warnings
|
|
156
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
157
|
+
is_failure = self._compute_is_failure(skill.ai_role, rule_output, None, None)
|
|
158
|
+
self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
159
|
+
return rule_output
|
|
160
|
+
|
|
161
|
+
# 7a. AI 验证(Pydantic 输出)
|
|
162
|
+
try:
|
|
163
|
+
ai_validation = await self._ai_validate(skill, rule_output, prompt_config)
|
|
164
|
+
rule_output.ai_validated = True
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.warning("Skill [%s] AI 验证异常: %s", skill_name, e)
|
|
167
|
+
fb_result = fallback.on_validate_failure(e)
|
|
168
|
+
warnings.extend(fb_result.warnings)
|
|
169
|
+
if fb_result.skip_ai:
|
|
170
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
171
|
+
rule_output.ai_validated = False
|
|
172
|
+
rule_output.warnings = warnings
|
|
173
|
+
is_failure = self._compute_is_failure(skill.ai_role, rule_output, None, None)
|
|
174
|
+
self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
175
|
+
return rule_output
|
|
176
|
+
|
|
177
|
+
# 7b. 若验证不合理 → AI 重选
|
|
178
|
+
if ai_validation and ai_validation.result == "不合理":
|
|
179
|
+
fb_reselect = fallback.check_before_ai()
|
|
180
|
+
if fb_reselect.skip_ai:
|
|
181
|
+
warnings.extend(fb_reselect.warnings)
|
|
182
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
183
|
+
is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, None)
|
|
184
|
+
self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, None, rule_output, warnings, elapsed)
|
|
185
|
+
return rule_output
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
ai_reselection = await self._ai_reselect(skill, skill_input, rule_output, prompt_config)
|
|
189
|
+
if ai_reselection and ai_reselection.result != "不合理":
|
|
190
|
+
# 包装 AI 重选结果 — 确保 result 是 dict
|
|
191
|
+
selected_nickname = ai_reselection.result
|
|
192
|
+
if isinstance(selected_nickname, dict):
|
|
193
|
+
reselected_dict = selected_nickname
|
|
194
|
+
else:
|
|
195
|
+
reselected_dict = {
|
|
196
|
+
**rule_output.result,
|
|
197
|
+
"nickname": str(selected_nickname),
|
|
198
|
+
"source": "ai",
|
|
199
|
+
}
|
|
200
|
+
final_output = SkillOutput(
|
|
201
|
+
source="ai",
|
|
202
|
+
result=reselected_dict,
|
|
203
|
+
ai_validated=True,
|
|
204
|
+
ai_reselected=True,
|
|
205
|
+
warnings=warnings,
|
|
206
|
+
)
|
|
207
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
208
|
+
is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, ai_reselection)
|
|
209
|
+
self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, ai_reselection, final_output, warnings, elapsed)
|
|
210
|
+
return final_output
|
|
211
|
+
else:
|
|
212
|
+
# 重选仍不合理
|
|
213
|
+
rule_output.ai_reselected = True
|
|
214
|
+
rule_output.warnings = warnings
|
|
215
|
+
rule_output.warnings.append("AI 重选后仍不合理")
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.warning("Skill [%s] AI 重选异常: %s", skill_name, e)
|
|
218
|
+
fb_result2 = fallback.on_reselect_failure(e)
|
|
219
|
+
warnings.extend(fb_result2.warnings)
|
|
220
|
+
|
|
221
|
+
# 验证合理或重选失败 → 返回规则结果
|
|
222
|
+
rule_output.warnings = warnings
|
|
223
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
224
|
+
is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, ai_reselection)
|
|
225
|
+
self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, ai_reselection, rule_output, warnings, elapsed)
|
|
226
|
+
return rule_output
|
|
227
|
+
|
|
228
|
+
elif skill.ai_role == "enhancement":
|
|
229
|
+
# 加分型:AI 增强(如语义评分)
|
|
230
|
+
fb_check = fallback.check_before_ai()
|
|
231
|
+
if fb_check.skip_ai:
|
|
232
|
+
warnings.extend(fb_check.warnings)
|
|
233
|
+
rule_output.warnings = warnings
|
|
234
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
235
|
+
self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
236
|
+
return rule_output
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
ai_result = await self._ai_enhance(skill, skill_input, rule_output, prompt_config)
|
|
240
|
+
merged_result = {**rule_output.result}
|
|
241
|
+
if ai_result:
|
|
242
|
+
merged_result.update(ai_result)
|
|
243
|
+
final_output = SkillOutput(
|
|
244
|
+
source=rule_output.source,
|
|
245
|
+
result=merged_result,
|
|
246
|
+
ai_validated=True,
|
|
247
|
+
ai_reselected=False,
|
|
248
|
+
warnings=warnings,
|
|
249
|
+
)
|
|
250
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
251
|
+
self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, final_output, warnings, elapsed)
|
|
252
|
+
return final_output
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warning("Skill [%s] AI 增强异常: %s", skill_name, e)
|
|
255
|
+
rule_output.warnings = warnings
|
|
256
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
257
|
+
self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
258
|
+
return rule_output
|
|
259
|
+
|
|
260
|
+
else:
|
|
261
|
+
# 未知 ai_role → 纯规则返回
|
|
262
|
+
logger.warning("Skill [%s] 未知 ai_role=%s,纯规则输出", skill_name, skill.ai_role)
|
|
263
|
+
rule_output.warnings = warnings
|
|
264
|
+
elapsed = (time.monotonic() - start_time) * 1000
|
|
265
|
+
self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
266
|
+
return rule_output
|
|
267
|
+
|
|
268
|
+
# ── 私有方法 ──
|
|
269
|
+
|
|
270
|
+
def _build_fallback_config(self, rules_config: dict) -> FallbackConfig:
|
|
271
|
+
"""从 rules_config 的 ai_fallback 段构建降级配置(Pydantic 校验)。"""
|
|
272
|
+
af = rules_config.get("ai_fallback", {})
|
|
273
|
+
validated = FallbackConfigModel(
|
|
274
|
+
validate_timeout_seconds=float(af.get("validate_timeout_seconds", 3)),
|
|
275
|
+
reselect_timeout_seconds=float(af.get("reselect_timeout_seconds", 5)),
|
|
276
|
+
max_retries=int(af.get("max_retries", 1)),
|
|
277
|
+
circuit_breaker_threshold=int(af.get("circuit_breaker_threshold", 3)),
|
|
278
|
+
circuit_breaker_cooldown_seconds=float(af.get("circuit_breaker_cooldown_seconds", 60)),
|
|
279
|
+
conservative_mode=bool(af.get("conservative_mode", False)),
|
|
280
|
+
)
|
|
281
|
+
return FallbackConfig(
|
|
282
|
+
validate_timeout_seconds=validated.validate_timeout_seconds,
|
|
283
|
+
reselect_timeout_seconds=validated.reselect_timeout_seconds,
|
|
284
|
+
max_retries=validated.max_retries,
|
|
285
|
+
circuit_breaker_threshold=validated.circuit_breaker_threshold,
|
|
286
|
+
circuit_breaker_cooldown_seconds=validated.circuit_breaker_cooldown_seconds,
|
|
287
|
+
conservative_mode=validated.conservative_mode,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _compute_is_failure(
|
|
292
|
+
ai_role: str,
|
|
293
|
+
rule_output: SkillOutput,
|
|
294
|
+
ai_validation: AiValidationResult | None,
|
|
295
|
+
ai_reselection: AiReselectionResult | None,
|
|
296
|
+
) -> bool:
|
|
297
|
+
"""根据 ai_role 计算 is_failure 标记。"""
|
|
298
|
+
if ai_role == "enhancement":
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
if ai_role == "correction":
|
|
302
|
+
if ai_validation and ai_validation.result == "不合理":
|
|
303
|
+
return True
|
|
304
|
+
if ai_reselection and ai_reselection.result == "不合理":
|
|
305
|
+
return True
|
|
306
|
+
result = rule_output.result
|
|
307
|
+
if not result or result.get("error"):
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
async def _ai_validate(
|
|
313
|
+
self,
|
|
314
|
+
skill: SkillModule,
|
|
315
|
+
rule_output: SkillOutput,
|
|
316
|
+
prompt_config: dict | None,
|
|
317
|
+
) -> AiValidationResult:
|
|
318
|
+
"""调用 AI 进行常识验证。返回 Pydantic 模型。"""
|
|
319
|
+
import json as _json, re as _re
|
|
320
|
+
|
|
321
|
+
system_prompt = (prompt_config or {}).get("system_prompt", "你是合理性判断专家。")
|
|
322
|
+
user_template = (prompt_config or {}).get("user_template_validate", "请判断以下结果是否合理:{{result}}")
|
|
323
|
+
|
|
324
|
+
nick = rule_output.result.get("nickname", "")
|
|
325
|
+
candidates = _json.dumps(rule_output.result.get("candidates", [])[:5], ensure_ascii=False)
|
|
326
|
+
user_message = self._render_template(user_template, {"result": nick, "candidates": candidates})
|
|
327
|
+
|
|
328
|
+
resp = await self._deepseek.chat(
|
|
329
|
+
messages=[
|
|
330
|
+
{"role": "system", "content": system_prompt},
|
|
331
|
+
{"role": "user", "content": user_message},
|
|
332
|
+
],
|
|
333
|
+
temperature=0.1,
|
|
334
|
+
max_tokens=256,
|
|
335
|
+
)
|
|
336
|
+
content = resp.content.strip()
|
|
337
|
+
|
|
338
|
+
# 尝试 JSON 解析
|
|
339
|
+
for candidate in [content]:
|
|
340
|
+
if candidate.startswith("```"):
|
|
341
|
+
lines = candidate.split("\n")
|
|
342
|
+
end = -1 if lines[-1].strip() == "```" else len(lines)
|
|
343
|
+
start = 1 if lines[0].startswith("```json") or lines[0].startswith("```") else 0
|
|
344
|
+
candidate = "\n".join(lines[start:end])
|
|
345
|
+
try:
|
|
346
|
+
parsed = _json.loads(candidate)
|
|
347
|
+
return AiValidationResult(
|
|
348
|
+
result=parsed.get("result", "合理"),
|
|
349
|
+
reason=parsed.get("reason", ""),
|
|
350
|
+
)
|
|
351
|
+
except (_json.JSONDecodeError, ValueError):
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# 非 JSON 回退:先检查"不合理",避免"不合理"中的"合理"被误匹配
|
|
355
|
+
if _re.search(r"(不合理|unreasonable|invalid|不是)", content, _re.IGNORECASE):
|
|
356
|
+
return AiValidationResult(result="不合理", reason=content[:120])
|
|
357
|
+
if _re.search(r"(合理|reasonable|valid)", content, _re.IGNORECASE):
|
|
358
|
+
return AiValidationResult(result="合理", reason=content[:120])
|
|
359
|
+
return AiValidationResult(result="合理", reason="no explicit judgement")
|
|
360
|
+
|
|
361
|
+
async def _ai_reselect(
|
|
362
|
+
self,
|
|
363
|
+
skill: SkillModule,
|
|
364
|
+
skill_input: SkillInput,
|
|
365
|
+
rule_output: SkillOutput,
|
|
366
|
+
prompt_config: dict | None,
|
|
367
|
+
) -> AiReselectionResult:
|
|
368
|
+
"""调用 AI 重新选择/提取。返回 Pydantic 模型。"""
|
|
369
|
+
import json as _json2
|
|
370
|
+
|
|
371
|
+
system_prompt = (prompt_config or {}).get("system_prompt", "你是信息提取专家。")
|
|
372
|
+
user_template = (prompt_config or {}).get("user_template_reselect", "请从以下数据中重新选择:{{candidates}}")
|
|
373
|
+
|
|
374
|
+
candidates_list = rule_output.result.get("candidates", []) if isinstance(rule_output.result, dict) else []
|
|
375
|
+
candidates_json = _json2.dumps(candidates_list, ensure_ascii=False)
|
|
376
|
+
|
|
377
|
+
user_message = self._render_template(
|
|
378
|
+
user_template,
|
|
379
|
+
{"candidates": candidates_json},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
response = await self._deepseek.chat_json(
|
|
383
|
+
messages=[
|
|
384
|
+
{"role": "system", "content": system_prompt},
|
|
385
|
+
{"role": "user", "content": user_message},
|
|
386
|
+
],
|
|
387
|
+
temperature=0.2,
|
|
388
|
+
max_tokens=1024,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return AiReselectionResult(
|
|
392
|
+
result=str(response.get("result", "")),
|
|
393
|
+
reason=str(response.get("reason", "")),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
async def _ai_enhance(
|
|
397
|
+
self,
|
|
398
|
+
skill: SkillModule,
|
|
399
|
+
skill_input: SkillInput,
|
|
400
|
+
rule_output: SkillOutput,
|
|
401
|
+
prompt_config: dict | None,
|
|
402
|
+
) -> dict | None:
|
|
403
|
+
"""调用 AI 增强规则结果(enhancement 角色)。"""
|
|
404
|
+
system_prompt = (prompt_config or {}).get("system_prompt", "你是评分增强专家。")
|
|
405
|
+
user_template = (prompt_config or {}).get("user_template", "请根据以下信息评分:{{input_data}}")
|
|
406
|
+
|
|
407
|
+
user_message = self._render_template(user_template, {"input_data": skill_input.input_data})
|
|
408
|
+
|
|
409
|
+
response = await self._deepseek.chat_json(
|
|
410
|
+
messages=[
|
|
411
|
+
{"role": "system", "content": system_prompt},
|
|
412
|
+
{"role": "user", "content": user_message},
|
|
413
|
+
],
|
|
414
|
+
temperature=0.3,
|
|
415
|
+
max_tokens=2048,
|
|
416
|
+
)
|
|
417
|
+
return response
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def _render_template(template: str, context: dict) -> str:
|
|
421
|
+
"""简单 Jinja2 风格模板渲染(仅支持 {{var}})。"""
|
|
422
|
+
result = template
|
|
423
|
+
for key, value in context.items():
|
|
424
|
+
placeholder = "{{" + key + "}}"
|
|
425
|
+
if isinstance(value, dict):
|
|
426
|
+
import json
|
|
427
|
+
result = result.replace(placeholder, json.dumps(value, ensure_ascii=False))
|
|
428
|
+
else:
|
|
429
|
+
result = result.replace(placeholder, str(value))
|
|
430
|
+
return result
|
|
431
|
+
|
|
432
|
+
def _log(
|
|
433
|
+
self,
|
|
434
|
+
skill: SkillModule,
|
|
435
|
+
trace_id: str,
|
|
436
|
+
is_failure: bool,
|
|
437
|
+
skill_input: SkillInput,
|
|
438
|
+
rule_output: SkillOutput,
|
|
439
|
+
ai_validation: AiValidationResult | None,
|
|
440
|
+
ai_reselection: AiReselectionResult | None,
|
|
441
|
+
final_output: SkillOutput,
|
|
442
|
+
warnings: list[str],
|
|
443
|
+
elapsed_ms: float,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""写入 JSONL 日志(Pydantic LogEntry 校验)。"""
|
|
446
|
+
try:
|
|
447
|
+
log_writer = SkillLogger(skill.skill_name)
|
|
448
|
+
|
|
449
|
+
# 生成 input_summary
|
|
450
|
+
if skill.summarize_input:
|
|
451
|
+
input_summary = skill.summarize_input(skill_input.input_data)
|
|
452
|
+
elif isinstance(skill_input.input_data, dict):
|
|
453
|
+
keys = list(skill_input.input_data.keys())[:5]
|
|
454
|
+
input_summary = {k: str(skill_input.input_data[k])[:100] for k in keys}
|
|
455
|
+
else:
|
|
456
|
+
input_summary = {"type": type(skill_input.input_data).__name__}
|
|
457
|
+
|
|
458
|
+
# 通过 log_execution 统一校验(LogEntry Pydantic 模型)后写入
|
|
459
|
+
log_writer.log_execution(
|
|
460
|
+
trace_id=trace_id,
|
|
461
|
+
is_failure=is_failure,
|
|
462
|
+
input_summary=input_summary,
|
|
463
|
+
rule_output=rule_output.result,
|
|
464
|
+
ai_validation=ai_validation.model_dump() if ai_validation else None,
|
|
465
|
+
ai_reselection=ai_reselection.model_dump() if ai_reselection else None,
|
|
466
|
+
final_output=final_output.result,
|
|
467
|
+
warnings=warnings,
|
|
468
|
+
elapsed_ms=round(elapsed_ms, 1),
|
|
469
|
+
)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
logger.warning("Skill 日志记录失败: %s", e)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI 降级策略 — 乐观/保守模式 + 熔断检查。
|
|
3
|
+
|
|
4
|
+
统一降级总原则:
|
|
5
|
+
- 输入非法 → 框架层直接返回 400,不执行业务逻辑
|
|
6
|
+
- AI 验证失败/超时 → 标记跳过,采信规则结果
|
|
7
|
+
- AI 重选失败/超时 → 直接返回规则原始结果
|
|
8
|
+
- AI 全局熔断 → 全链路跳过 AI,纯走规则
|
|
9
|
+
- 保守降级模式 → 可选开关:AI 不可用时标记「需人工复核」而非直接通过
|
|
10
|
+
- warnings → 仅用于日志和监控,不阻断流程
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from skill_self_evolution.deepseek import CircuitBreaker
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FallbackMode(str, Enum):
|
|
25
|
+
OPTIMISTIC = "optimistic"
|
|
26
|
+
CONSERVATIVE = "conservative"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FallbackConfig:
|
|
31
|
+
"""降级配置,来源 rules_config.yaml 的 ai_fallback 段。
|
|
32
|
+
|
|
33
|
+
注:此结构保持 @dataclass(非 Pydantic),原因:
|
|
34
|
+
- 构造来源已通过 FallbackConfigModel(Pydantic)校验
|
|
35
|
+
- 内嵌在 FallbackStrategy 中,无独立序列化需求
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
validate_timeout_seconds: float = 3.0
|
|
39
|
+
reselect_timeout_seconds: float = 5.0
|
|
40
|
+
max_retries: int = 1
|
|
41
|
+
circuit_breaker_threshold: int = 3
|
|
42
|
+
circuit_breaker_cooldown_seconds: float = 60.0
|
|
43
|
+
conservative_mode: bool = False
|
|
44
|
+
enabled: bool = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FallbackResult(BaseModel):
|
|
48
|
+
"""降级处理结果(Pydantic 校验)。"""
|
|
49
|
+
|
|
50
|
+
skip_ai: bool = Field(default=False, description="是否应跳过 AI 步骤")
|
|
51
|
+
reason: str = Field(default="", description="降级原因")
|
|
52
|
+
warnings: list[str] = Field(default_factory=list, description="降级时的警告信息")
|
|
53
|
+
needs_review: bool = Field(default=False, description="AI 不可用时是否标记需人工复核")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FallbackStrategy:
|
|
57
|
+
"""AI 降级策略管理器。
|
|
58
|
+
|
|
59
|
+
使用方式:
|
|
60
|
+
strategy = FallbackStrategy(config, circuit_breaker)
|
|
61
|
+
result = strategy.on_validate_failure(error)
|
|
62
|
+
if result.skip_ai:
|
|
63
|
+
return fallback_output
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, config: FallbackConfig, circuit_breaker: CircuitBreaker):
|
|
67
|
+
self.config = config
|
|
68
|
+
self.circuit_breaker = circuit_breaker
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def mode(self) -> FallbackMode:
|
|
72
|
+
return FallbackMode.CONSERVATIVE if self.config.conservative_mode else FallbackMode.OPTIMISTIC
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_circuit_open(self) -> bool:
|
|
76
|
+
return self.circuit_breaker.is_open
|
|
77
|
+
|
|
78
|
+
def check_before_ai(self) -> FallbackResult:
|
|
79
|
+
"""在调用 AI 之前检查是否应跳过。"""
|
|
80
|
+
warnings: list[str] = []
|
|
81
|
+
|
|
82
|
+
if not self.config.enabled:
|
|
83
|
+
return FallbackResult(skip_ai=True, reason="AI 全局已禁用", warnings=warnings)
|
|
84
|
+
|
|
85
|
+
if self.circuit_breaker.is_open:
|
|
86
|
+
msg = "AI 熔断中,跳过本步骤"
|
|
87
|
+
warnings.append(msg)
|
|
88
|
+
return FallbackResult(
|
|
89
|
+
skip_ai=True,
|
|
90
|
+
reason=msg,
|
|
91
|
+
warnings=warnings,
|
|
92
|
+
needs_review=self.mode == FallbackMode.CONSERVATIVE,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return FallbackResult(skip_ai=False)
|
|
96
|
+
|
|
97
|
+
def on_validate_failure(self, error: Exception | None = None) -> FallbackResult:
|
|
98
|
+
"""AI 验证失败/超时时的降级处理。"""
|
|
99
|
+
warnings: list[str] = []
|
|
100
|
+
reason = f"AI 验证不可用: {error}" if error else "AI 验证不可用"
|
|
101
|
+
|
|
102
|
+
if self.mode == FallbackMode.CONSERVATIVE:
|
|
103
|
+
warnings.append("需人工复核: AI验证不可用")
|
|
104
|
+
return FallbackResult(
|
|
105
|
+
skip_ai=True,
|
|
106
|
+
reason=reason,
|
|
107
|
+
warnings=warnings,
|
|
108
|
+
needs_review=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# 乐观模式:默认"验证通过"
|
|
112
|
+
return FallbackResult(skip_ai=True, reason=reason, warnings=warnings)
|
|
113
|
+
|
|
114
|
+
def on_reselect_failure(self, error: Exception | None = None) -> FallbackResult:
|
|
115
|
+
"""AI 重选失败/超时时的降级处理。"""
|
|
116
|
+
warnings: list[str] = []
|
|
117
|
+
reason = f"AI 重选不可用: {error}" if error else "AI 重选不可用"
|
|
118
|
+
|
|
119
|
+
if self.mode == FallbackMode.CONSERVATIVE:
|
|
120
|
+
warnings.append("需人工复核: AI重选不可用")
|
|
121
|
+
return FallbackResult(
|
|
122
|
+
skip_ai=True,
|
|
123
|
+
reason=reason,
|
|
124
|
+
warnings=warnings,
|
|
125
|
+
needs_review=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# 乐观模式:返回规则原始结果
|
|
129
|
+
return FallbackResult(skip_ai=True, reason=reason, warnings=warnings)
|