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.
@@ -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)