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,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Engine — 自进化框架核心。
|
|
3
|
+
|
|
4
|
+
规则执行 → AI 常识判断 → 合理就过 / 不合理 AI 从原始数据重选 → 日志记录 → 离线进化 → 优化配置
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from skill_self_evolution.executor import SkillExecutor
|
|
8
|
+
from skill_self_evolution.models import SkillInput, SkillOutput
|
|
9
|
+
from skill_self_evolution.context import get_trace_id, set_trace_id
|
|
10
|
+
from skill_self_evolution.logger import SkillLogger
|
|
11
|
+
from skill_self_evolution.fallback import FallbackStrategy
|
|
12
|
+
from skill_self_evolution.config_loader import ConfigVersionManager
|
|
13
|
+
from skill_self_evolution.evolver import Evolver
|
|
14
|
+
from skill_self_evolution.ai_assisted_executor import AiAssistedExecutor
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SkillExecutor",
|
|
18
|
+
"SkillInput",
|
|
19
|
+
"SkillOutput",
|
|
20
|
+
"get_trace_id",
|
|
21
|
+
"set_trace_id",
|
|
22
|
+
"SkillLogger",
|
|
23
|
+
"FallbackStrategy",
|
|
24
|
+
"ConfigVersionManager",
|
|
25
|
+
"Evolver",
|
|
26
|
+
"AiAssistedExecutor",
|
|
27
|
+
]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AiAssistedExecutor — 通用 AI 辅助执行基类。
|
|
3
|
+
|
|
4
|
+
提供「验证 + 重选」的通用流程模板,Skill 只需定义:
|
|
5
|
+
- ai_validate():AI 常识判断(correction 角色) → AiValidationResult
|
|
6
|
+
- ai_reselect():AI 重新选择/提取(correction 角色) → AiReselectionResult
|
|
7
|
+
- ai_enhance():AI 增强(enhancement 角色) → dict | None
|
|
8
|
+
|
|
9
|
+
框架自动处理:降级、熔断、is_failure 计算、JSONL 日志。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from skill_self_evolution.context import set_trace_id
|
|
18
|
+
from skill_self_evolution.fallback import FallbackConfig, FallbackResult, FallbackStrategy
|
|
19
|
+
from skill_self_evolution.loader import SkillModule
|
|
20
|
+
from skill_self_evolution.logger import SkillLogger
|
|
21
|
+
from skill_self_evolution.models import (
|
|
22
|
+
AiReselectionResult,
|
|
23
|
+
AiValidationResult,
|
|
24
|
+
SkillInput,
|
|
25
|
+
SkillOutput,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AiAssistedExecutor(ABC):
|
|
32
|
+
"""AI 辅助执行基类。
|
|
33
|
+
|
|
34
|
+
子类只需实现 2-3 个抽象方法,框架自动处理流程编排和降级。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def ai_validate(
|
|
39
|
+
self,
|
|
40
|
+
skill_input: SkillInput,
|
|
41
|
+
rule_output: SkillOutput,
|
|
42
|
+
prompt_config: dict | None,
|
|
43
|
+
) -> AiValidationResult:
|
|
44
|
+
"""AI 常识判断(correction 角色)。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
AiValidationResult: result=合理/不合理 + reason
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def ai_reselect(
|
|
52
|
+
self,
|
|
53
|
+
skill_input: SkillInput,
|
|
54
|
+
rule_output: SkillOutput,
|
|
55
|
+
prompt_config: dict | None,
|
|
56
|
+
) -> AiReselectionResult:
|
|
57
|
+
"""AI 重新选择/提取(correction 角色)。
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
AiReselectionResult: result=重选值(或"不合理") + reason
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
async def ai_enhance(
|
|
64
|
+
self,
|
|
65
|
+
skill_input: SkillInput,
|
|
66
|
+
rule_output: SkillOutput,
|
|
67
|
+
prompt_config: dict | None,
|
|
68
|
+
) -> dict | None:
|
|
69
|
+
"""AI 增强(enhancement 角色)。默认不增强,子类按需覆盖。"""
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
async def run_correction_flow(
|
|
73
|
+
self,
|
|
74
|
+
skill: SkillModule,
|
|
75
|
+
skill_input: SkillInput,
|
|
76
|
+
rule_output: SkillOutput,
|
|
77
|
+
prompt_config: dict | None,
|
|
78
|
+
fallback: FallbackStrategy,
|
|
79
|
+
) -> SkillOutput:
|
|
80
|
+
"""correction 角色完整流程:验证 → 重选。"""
|
|
81
|
+
t0 = time.monotonic()
|
|
82
|
+
warnings: list[str] = list(rule_output.warnings) if rule_output.warnings else []
|
|
83
|
+
|
|
84
|
+
# 1. 熔断检查
|
|
85
|
+
fb_check = fallback.check_before_ai()
|
|
86
|
+
if fb_check.skip_ai:
|
|
87
|
+
warnings.extend(fb_check.warnings)
|
|
88
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
89
|
+
rule_output.warnings = warnings
|
|
90
|
+
self._log(skill, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
91
|
+
return rule_output
|
|
92
|
+
|
|
93
|
+
# 2. AI 验证
|
|
94
|
+
ai_validation: AiValidationResult | None = None
|
|
95
|
+
try:
|
|
96
|
+
ai_validation = await self.ai_validate(skill_input, rule_output, prompt_config)
|
|
97
|
+
rule_output.ai_validated = True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning("Skill [%s] AI 验证异常: %s", skill.skill_name, e)
|
|
100
|
+
fb_result = fallback.on_validate_failure(e)
|
|
101
|
+
warnings.extend(fb_result.warnings)
|
|
102
|
+
if fb_result.skip_ai:
|
|
103
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
104
|
+
rule_output.ai_validated = False
|
|
105
|
+
rule_output.warnings = warnings
|
|
106
|
+
is_failure = self._is_failure(skill.ai_role, rule_output, None, None)
|
|
107
|
+
self._log(skill, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
108
|
+
return rule_output
|
|
109
|
+
|
|
110
|
+
# 3. 验证不合理 → 重选
|
|
111
|
+
if ai_validation and ai_validation.result == "不合理":
|
|
112
|
+
fb_reselect = fallback.check_before_ai()
|
|
113
|
+
if fb_reselect.skip_ai:
|
|
114
|
+
warnings.extend(fb_reselect.warnings)
|
|
115
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
116
|
+
is_failure = self._is_failure(skill.ai_role, rule_output, ai_validation, None)
|
|
117
|
+
rule_output.warnings = warnings
|
|
118
|
+
self._log(skill, skill_input, rule_output, ai_validation, None, rule_output, warnings, elapsed)
|
|
119
|
+
return rule_output
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
ai_reselection = await self.ai_reselect(skill_input, rule_output, prompt_config)
|
|
123
|
+
if ai_reselection and ai_reselection.result != "不合理":
|
|
124
|
+
final_result = ai_reselection.result
|
|
125
|
+
final_output = SkillOutput(
|
|
126
|
+
source="ai",
|
|
127
|
+
result=final_result if isinstance(final_result, dict) else {"value": final_result},
|
|
128
|
+
ai_validated=True,
|
|
129
|
+
ai_reselected=True,
|
|
130
|
+
warnings=warnings,
|
|
131
|
+
)
|
|
132
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
133
|
+
is_failure = self._is_failure(skill.ai_role, rule_output, ai_validation, ai_reselection)
|
|
134
|
+
self._log(skill, skill_input, rule_output, ai_validation, ai_reselection, final_output, warnings, elapsed)
|
|
135
|
+
return final_output
|
|
136
|
+
else:
|
|
137
|
+
warnings.append("AI 重选后仍不合理")
|
|
138
|
+
rule_output.ai_reselected = True
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning("Skill [%s] AI 重选异常: %s", skill.skill_name, e)
|
|
141
|
+
fb_result2 = fallback.on_reselect_failure(e)
|
|
142
|
+
warnings.extend(fb_result2.warnings)
|
|
143
|
+
|
|
144
|
+
# 4. 验证合理或重选失败 → 返回规则结果
|
|
145
|
+
rule_output.warnings = warnings
|
|
146
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
147
|
+
is_failure = self._is_failure(skill.ai_role, rule_output, ai_validation, None)
|
|
148
|
+
self._log(skill, skill_input, rule_output, ai_validation, None, rule_output, warnings, elapsed)
|
|
149
|
+
return rule_output
|
|
150
|
+
|
|
151
|
+
async def run_enhancement_flow(
|
|
152
|
+
self,
|
|
153
|
+
skill: SkillModule,
|
|
154
|
+
skill_input: SkillInput,
|
|
155
|
+
rule_output: SkillOutput,
|
|
156
|
+
prompt_config: dict | None,
|
|
157
|
+
fallback: FallbackStrategy,
|
|
158
|
+
) -> SkillOutput:
|
|
159
|
+
"""enhancement 角色流程:规则 + AI 增强。"""
|
|
160
|
+
t0 = time.monotonic()
|
|
161
|
+
warnings: list[str] = list(rule_output.warnings) if rule_output.warnings else []
|
|
162
|
+
|
|
163
|
+
fb_check = fallback.check_before_ai()
|
|
164
|
+
if fb_check.skip_ai:
|
|
165
|
+
warnings.extend(fb_check.warnings)
|
|
166
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
167
|
+
rule_output.warnings = warnings
|
|
168
|
+
self._log(skill, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
169
|
+
return rule_output
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
ai_result = await self.ai_enhance(skill_input, rule_output, prompt_config)
|
|
173
|
+
merged = {**rule_output.result} if isinstance(rule_output.result, dict) else {}
|
|
174
|
+
if ai_result:
|
|
175
|
+
merged.update(ai_result)
|
|
176
|
+
final_output = SkillOutput(
|
|
177
|
+
source=rule_output.source,
|
|
178
|
+
result=merged,
|
|
179
|
+
ai_validated=True,
|
|
180
|
+
ai_reselected=False,
|
|
181
|
+
warnings=warnings,
|
|
182
|
+
)
|
|
183
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
184
|
+
self._log(skill, skill_input, rule_output, None, None, final_output, warnings, elapsed)
|
|
185
|
+
return final_output
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning("Skill [%s] AI 增强异常: %s", skill.skill_name, e)
|
|
188
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
189
|
+
rule_output.warnings = warnings
|
|
190
|
+
self._log(skill, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
|
|
191
|
+
return rule_output
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _is_failure(
|
|
195
|
+
ai_role: str,
|
|
196
|
+
rule_output: SkillOutput,
|
|
197
|
+
ai_validation: AiValidationResult | None,
|
|
198
|
+
ai_reselection: AiReselectionResult | None,
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""根据 ai_role 计算 is_failure(Pydantic 属性访问)。"""
|
|
201
|
+
if ai_role == "enhancement":
|
|
202
|
+
return False
|
|
203
|
+
if ai_role == "correction":
|
|
204
|
+
if ai_validation and ai_validation.result == "不合理":
|
|
205
|
+
return True
|
|
206
|
+
if ai_reselection and ai_reselection.result == "不合理":
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def _log(
|
|
211
|
+
self,
|
|
212
|
+
skill: SkillModule,
|
|
213
|
+
skill_input: SkillInput,
|
|
214
|
+
rule_output: SkillOutput,
|
|
215
|
+
ai_validation: AiValidationResult | None,
|
|
216
|
+
ai_reselection: AiReselectionResult | None,
|
|
217
|
+
final_output: SkillOutput,
|
|
218
|
+
warnings: list[str],
|
|
219
|
+
elapsed_ms: float,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""写入 JSONL 日志(Pydantic LogEntry 校验)。"""
|
|
222
|
+
try:
|
|
223
|
+
log_writer = SkillLogger(skill.skill_name)
|
|
224
|
+
if skill.summarize_input:
|
|
225
|
+
input_summary = skill.summarize_input(skill_input.input_data)
|
|
226
|
+
elif isinstance(skill_input.input_data, dict):
|
|
227
|
+
keys = list(skill_input.input_data.keys())[:5]
|
|
228
|
+
input_summary = {k: str(skill_input.input_data[k])[:100] for k in keys}
|
|
229
|
+
else:
|
|
230
|
+
input_summary = {"type": type(skill_input.input_data).__name__}
|
|
231
|
+
|
|
232
|
+
log_writer.log_execution(
|
|
233
|
+
trace_id=skill_input.trace_id or "",
|
|
234
|
+
is_failure=(ai_validation is not None and ai_validation.result == "不合理"),
|
|
235
|
+
input_summary=input_summary,
|
|
236
|
+
rule_output=rule_output.result,
|
|
237
|
+
ai_validation=ai_validation.model_dump() if ai_validation else None,
|
|
238
|
+
ai_reselection=ai_reselection.model_dump() if ai_reselection else None,
|
|
239
|
+
final_output=final_output.result,
|
|
240
|
+
warnings=warnings,
|
|
241
|
+
elapsed_ms=elapsed_ms,
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning("Skill 日志记录失败: %s", e)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
?????? ? ? APP_ENV ?? DeepSeek API ???????
|
|
3
|
+
|
|
4
|
+
- local: DeepSeek ?? API (api.deepseek.com)
|
|
5
|
+
- test/prod: ??? MaaS (api.modelarts-maas.com/v2)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
__all__ = ["DeepSeekEnvConfig", "DbConfig", "get_deepseek_config", "get_db_config"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeepSeekEnvConfig(BaseModel):
|
|
16
|
+
"""DeepSeek API ?????Pydantic ????"""
|
|
17
|
+
|
|
18
|
+
api_key: str = Field(default="", description="API Key")
|
|
19
|
+
api_base: str = Field(
|
|
20
|
+
default="https://api.deepseek.com/v1",
|
|
21
|
+
description="API ????",
|
|
22
|
+
)
|
|
23
|
+
model: str = Field(default="deepseek-chat", description="????")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DbConfig(BaseModel):
|
|
27
|
+
"""????????Pydantic ????"""
|
|
28
|
+
|
|
29
|
+
host: str = Field(default="127.0.0.1")
|
|
30
|
+
port: int = Field(default=3306, ge=1, le=65535)
|
|
31
|
+
user: str = Field(default="root")
|
|
32
|
+
password: str = Field(default="")
|
|
33
|
+
database: str = Field(default="housekeeping_ai_match_dev")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_app_env() -> str:
|
|
37
|
+
return os.getenv("APP_ENV", "local").strip().lower()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_deepseek_config(
|
|
41
|
+
api_key: str = "",
|
|
42
|
+
api_base: str = "",
|
|
43
|
+
model: str = "",
|
|
44
|
+
) -> DeepSeekEnvConfig:
|
|
45
|
+
env = _resolve_app_env()
|
|
46
|
+
if api_key and api_base:
|
|
47
|
+
return DeepSeekEnvConfig(
|
|
48
|
+
api_key=api_key,
|
|
49
|
+
api_base=api_base,
|
|
50
|
+
model=model or "deepseek-chat",
|
|
51
|
+
)
|
|
52
|
+
if env == "local":
|
|
53
|
+
key = api_key or os.getenv("DEEPSEEK_API_KEY", "")
|
|
54
|
+
base = api_base or os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1")
|
|
55
|
+
m = model or os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
|
|
56
|
+
return DeepSeekEnvConfig(api_key=key, api_base=base, model=m)
|
|
57
|
+
else:
|
|
58
|
+
key = api_key or os.getenv("WX_MATCH_DEEPSEEK_API_KEY", "")
|
|
59
|
+
base = api_base or os.getenv(
|
|
60
|
+
"WX_MATCH_DEEPSEEK_API_BASE", "https://api.modelarts-maas.com/v2"
|
|
61
|
+
)
|
|
62
|
+
m = model or os.getenv("WX_MATCH_AI_MODEL_NAME", "DeepSeek-V3.2")
|
|
63
|
+
return DeepSeekEnvConfig(api_key=key, api_base=base, model=m)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_db_config(
|
|
67
|
+
host: str = "",
|
|
68
|
+
port: int = 0,
|
|
69
|
+
user: str = "",
|
|
70
|
+
password: str = "",
|
|
71
|
+
database: str = "",
|
|
72
|
+
) -> DbConfig:
|
|
73
|
+
if host and user and password:
|
|
74
|
+
return DbConfig(
|
|
75
|
+
host=host,
|
|
76
|
+
port=port or 3306,
|
|
77
|
+
user=user,
|
|
78
|
+
password=password,
|
|
79
|
+
database=database or "housekeeping_ai_match_dev",
|
|
80
|
+
)
|
|
81
|
+
return DbConfig(
|
|
82
|
+
host=host or os.getenv("DB_HOST", "127.0.0.1"),
|
|
83
|
+
port=port or int(os.getenv("DB_PORT", "3306")),
|
|
84
|
+
user=user or os.getenv("DB_USER", "root"),
|
|
85
|
+
password=password or os.getenv("DB_PASSWORD", ""),
|
|
86
|
+
database=database or os.getenv("DB_NAME", "housekeeping_ai_match_dev"),
|
|
87
|
+
)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置版本管理器 — MySQL skill_config / skill_config_history 表操作。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 从 MySQL 加载当前激活版本的 rules_config / prompt YAML
|
|
6
|
+
- 写入新版本时自动归档旧版本到 skill_config_history
|
|
7
|
+
- 支持回滚到任意历史版本
|
|
8
|
+
|
|
9
|
+
housekeeping 项目的 VersionManager 可作为适配器实现同一接口。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import pymysql
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from skill_self_evolution.config import DbConfig, get_db_config
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigVersionManager:
|
|
25
|
+
"""框架内置版本管理:version 递增 + 历史归档。"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, db_config: dict[str, Any] | DbConfig | None = None):
|
|
28
|
+
from skill_self_evolution.config import DbConfig
|
|
29
|
+
if db_config is None:
|
|
30
|
+
self._db_config = get_db_config()
|
|
31
|
+
elif isinstance(db_config, DbConfig):
|
|
32
|
+
self._db_config = db_config
|
|
33
|
+
else:
|
|
34
|
+
self._db_config = db_config
|
|
35
|
+
self._conn: pymysql.Connection | None = None
|
|
36
|
+
|
|
37
|
+
def set_db_config(self, db_config: dict[str, Any] | DbConfig) -> None:
|
|
38
|
+
self._db_config = db_config
|
|
39
|
+
|
|
40
|
+
def _get_conn(self) -> pymysql.Connection:
|
|
41
|
+
"""获取数据库连接(懒连接 + 自动重连)。兼容 dict 和 DbConfig。"""
|
|
42
|
+
if self._conn is None or not self._conn.open:
|
|
43
|
+
db = self._db_config
|
|
44
|
+
if isinstance(db, DbConfig):
|
|
45
|
+
host, port, user, password, database = (
|
|
46
|
+
db.host, db.port, db.user, db.password, db.database
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
host = db.get("host", "localhost")
|
|
50
|
+
port = db.get("port", 3306)
|
|
51
|
+
user = db.get("user", "root")
|
|
52
|
+
password = db.get("password", "")
|
|
53
|
+
database = db.get("database", "housekeeping")
|
|
54
|
+
self._conn = pymysql.connect(
|
|
55
|
+
host=host,
|
|
56
|
+
port=port,
|
|
57
|
+
user=user,
|
|
58
|
+
password=password,
|
|
59
|
+
database=database,
|
|
60
|
+
charset="utf8mb4",
|
|
61
|
+
autocommit=True,
|
|
62
|
+
)
|
|
63
|
+
return self._conn
|
|
64
|
+
|
|
65
|
+
def ensure_tables(self) -> None:
|
|
66
|
+
"""自动建表(幂等)。首次使用时或部署阶段调用。"""
|
|
67
|
+
conn = self._get_conn()
|
|
68
|
+
with conn.cursor() as cur:
|
|
69
|
+
cur.execute(
|
|
70
|
+
"""
|
|
71
|
+
CREATE TABLE IF NOT EXISTS skill_config (
|
|
72
|
+
skill_name VARCHAR(128) NOT NULL COMMENT 'Skill 名称',
|
|
73
|
+
config_type ENUM('rules_config','prompt') NOT NULL COMMENT '配置类型',
|
|
74
|
+
content MEDIUMTEXT NOT NULL COMMENT 'YAML 字符串',
|
|
75
|
+
version INT NOT NULL DEFAULT 1 COMMENT '版本号,每次更新递增',
|
|
76
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
77
|
+
PRIMARY KEY (skill_name, config_type)
|
|
78
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
79
|
+
"""
|
|
80
|
+
)
|
|
81
|
+
cur.execute(
|
|
82
|
+
"""
|
|
83
|
+
CREATE TABLE IF NOT EXISTS skill_config_history (
|
|
84
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
85
|
+
skill_name VARCHAR(128) NOT NULL,
|
|
86
|
+
config_type ENUM('rules_config','prompt') NOT NULL,
|
|
87
|
+
content MEDIUMTEXT NOT NULL,
|
|
88
|
+
version INT NOT NULL,
|
|
89
|
+
archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
90
|
+
INDEX idx_skill_version (skill_name, config_type, version)
|
|
91
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
logger.info("skill_config / skill_config_history 表确认存在")
|
|
95
|
+
|
|
96
|
+
def load(self, skill_name: str, config_type: str) -> dict | None:
|
|
97
|
+
"""从 MySQL 加载当前激活版本,返回解析后的 dict。
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
skill_name: Skill 名称(如 "nickname-selector")
|
|
101
|
+
config_type: "rules_config" 或 "prompt"
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
解析后的 YAML dict,未找到配置时返回 None
|
|
105
|
+
"""
|
|
106
|
+
conn = self._get_conn()
|
|
107
|
+
with conn.cursor() as cur:
|
|
108
|
+
cur.execute(
|
|
109
|
+
"SELECT content FROM skill_config WHERE skill_name = %s AND config_type = %s",
|
|
110
|
+
(skill_name, config_type),
|
|
111
|
+
)
|
|
112
|
+
row = cur.fetchone()
|
|
113
|
+
if row is None:
|
|
114
|
+
logger.debug("skill_config 未找到: %s/%s", skill_name, config_type)
|
|
115
|
+
return None
|
|
116
|
+
return yaml.safe_load(row[0])
|
|
117
|
+
|
|
118
|
+
def load_raw(self, skill_name: str, config_type: str) -> str | None:
|
|
119
|
+
"""加载原始 YAML 字符串(用于进化分析 prompt 注入)。"""
|
|
120
|
+
conn = self._get_conn()
|
|
121
|
+
with conn.cursor() as cur:
|
|
122
|
+
cur.execute(
|
|
123
|
+
"SELECT content FROM skill_config WHERE skill_name = %s AND config_type = %s",
|
|
124
|
+
(skill_name, config_type),
|
|
125
|
+
)
|
|
126
|
+
row = cur.fetchone()
|
|
127
|
+
return row[0] if row else None
|
|
128
|
+
|
|
129
|
+
def save(self, skill_name: str, config_type: str, content: str) -> int:
|
|
130
|
+
"""写入新版本:先归档旧版本到 skill_config_history,再更新主表。
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
skill_name: Skill 名称
|
|
134
|
+
config_type: "rules_config" 或 "prompt"
|
|
135
|
+
content: YAML 字符串(非解析后的 dict)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
新版本号
|
|
139
|
+
"""
|
|
140
|
+
conn = self._get_conn()
|
|
141
|
+
with conn.cursor() as cur:
|
|
142
|
+
# 查询当前版本号和内容
|
|
143
|
+
cur.execute(
|
|
144
|
+
"SELECT version, content FROM skill_config WHERE skill_name = %s AND config_type = %s",
|
|
145
|
+
(skill_name, config_type),
|
|
146
|
+
)
|
|
147
|
+
row = cur.fetchone()
|
|
148
|
+
|
|
149
|
+
if row:
|
|
150
|
+
old_version = row[0]
|
|
151
|
+
old_content = row[1]
|
|
152
|
+
new_version = old_version + 1
|
|
153
|
+
# 归档旧版本
|
|
154
|
+
cur.execute(
|
|
155
|
+
"INSERT INTO skill_config_history (skill_name, config_type, content, version) "
|
|
156
|
+
"VALUES (%s, %s, %s, %s)",
|
|
157
|
+
(skill_name, config_type, old_content, old_version),
|
|
158
|
+
)
|
|
159
|
+
# 更新主表
|
|
160
|
+
cur.execute(
|
|
161
|
+
"UPDATE skill_config SET content = %s, version = %s, updated_at = %s "
|
|
162
|
+
"WHERE skill_name = %s AND config_type = %s",
|
|
163
|
+
(content, new_version, datetime.now(), skill_name, config_type),
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
new_version = 1
|
|
167
|
+
cur.execute(
|
|
168
|
+
"INSERT INTO skill_config (skill_name, config_type, content, version) "
|
|
169
|
+
"VALUES (%s, %s, %s, %s)",
|
|
170
|
+
(skill_name, config_type, content, new_version),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
logger.info("配置写入: %s/%s v%d", skill_name, config_type, new_version)
|
|
174
|
+
return new_version
|
|
175
|
+
|
|
176
|
+
def rollback(self, skill_name: str, config_type: str, target_version: int) -> bool:
|
|
177
|
+
"""回滚到指定版本。
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
skill_name: Skill 名称
|
|
181
|
+
config_type: "rules_config" 或 "prompt"
|
|
182
|
+
target_version: 目标版本号
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True 表示回滚成功,False 表示目标版本不存在
|
|
186
|
+
"""
|
|
187
|
+
conn = self._get_conn()
|
|
188
|
+
with conn.cursor() as cur:
|
|
189
|
+
# 查找目标版本
|
|
190
|
+
if target_version == 0:
|
|
191
|
+
# v0 = 删除当前配置
|
|
192
|
+
cur.execute(
|
|
193
|
+
"DELETE FROM skill_config WHERE skill_name = %s AND config_type = %s",
|
|
194
|
+
(skill_name, config_type),
|
|
195
|
+
)
|
|
196
|
+
logger.info("配置已删除(回滚到 v0): %s/%s", skill_name, config_type)
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
# 从历史表查找
|
|
200
|
+
cur.execute(
|
|
201
|
+
"SELECT content FROM skill_config_history "
|
|
202
|
+
"WHERE skill_name = %s AND config_type = %s AND version = %s "
|
|
203
|
+
"ORDER BY archived_at DESC LIMIT 1",
|
|
204
|
+
(skill_name, config_type, target_version),
|
|
205
|
+
)
|
|
206
|
+
hist = cur.fetchone()
|
|
207
|
+
if hist is None:
|
|
208
|
+
logger.warning("历史版本不存在: %s/%s v%d", skill_name, config_type, target_version)
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
# 从主表获取当前版本以归档
|
|
212
|
+
cur.execute(
|
|
213
|
+
"SELECT version, content FROM skill_config WHERE skill_name = %s AND config_type = %s",
|
|
214
|
+
(skill_name, config_type),
|
|
215
|
+
)
|
|
216
|
+
current = cur.fetchone()
|
|
217
|
+
|
|
218
|
+
if current:
|
|
219
|
+
# 归档当前版本
|
|
220
|
+
cur.execute(
|
|
221
|
+
"INSERT INTO skill_config_history (skill_name, config_type, content, version) "
|
|
222
|
+
"VALUES (%s, %s, %s, %s)",
|
|
223
|
+
(skill_name, config_type, current[1], current[0]),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# 写入目标版本内容
|
|
227
|
+
new_version = (current[0] + 1) if current else 1
|
|
228
|
+
cur.execute(
|
|
229
|
+
"REPLACE INTO skill_config (skill_name, config_type, content, version, updated_at) "
|
|
230
|
+
"VALUES (%s, %s, %s, %s, %s)",
|
|
231
|
+
(skill_name, config_type, hist[0], new_version, datetime.now()),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
logger.info("配置回滚成功: %s/%s → v%d (from history v%d)", skill_name, config_type, new_version, target_version)
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def close(self) -> None:
|
|
238
|
+
"""关闭数据库连接。"""
|
|
239
|
+
if self._conn and self._conn.open:
|
|
240
|
+
self._conn.close()
|
|
241
|
+
self._conn = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trace_id 上下文管理 — 基于 contextvars 的全链路追踪。
|
|
3
|
+
|
|
4
|
+
框架在 SkillExecutor.run() 入口自动生成 trace_id 并注入上下文,
|
|
5
|
+
Skill 实现(run.py)无需手动处理。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import contextvars
|
|
9
|
+
|
|
10
|
+
_current_trace_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
11
|
+
"trace_id", default=None
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_trace_id() -> str | None:
|
|
16
|
+
"""获取当前协程/线程的 trace_id。"""
|
|
17
|
+
return _current_trace_id.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_trace_id(trace_id: str) -> None:
|
|
21
|
+
"""设置当前协程/线程的 trace_id(框架内部使用)。"""
|
|
22
|
+
_current_trace_id.set(trace_id)
|