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,201 @@
1
+ """
2
+ Skill 动态加载器 — 加载 run.py / summarize_input / evolve.toml / evolve_prompt.yaml / skill.md。
3
+ """
4
+
5
+ import importlib.util
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ import yaml
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Skill 根目录:优先 SKILL_BASE_DIR 环境变量;兼容 housekeeping 的路径
16
+ _skill_base_env = os.getenv("SKILL_BASE_DIR", "")
17
+ if _skill_base_env:
18
+ DEFAULT_SKILL_BASE = Path(_skill_base_env)
19
+ else:
20
+ # 尝试相对于 housekeeping 项目根目录(兼容老部署)
21
+ _hk_root = Path(__file__).resolve().parents[3].parent / "housekeeping_ai_match"
22
+ _hk_skill = _hk_root / "backend" / "config" / "services" / "skill"
23
+ if _hk_skill.is_dir():
24
+ DEFAULT_SKILL_BASE = _hk_skill
25
+ else:
26
+ DEFAULT_SKILL_BASE = Path.cwd() / "backend" / "config" / "services" / "skill"
27
+
28
+
29
+ class SkillModule:
30
+ """已加载的 Skill 模块,包含 execute / benchmark / summarize_input 函数和元数据。"""
31
+
32
+ def __init__(
33
+ self,
34
+ skill_name: str,
35
+ execute: Callable,
36
+ benchmark: Callable | None = None,
37
+ summarize_input: Callable | None = None,
38
+ evolve_toml: dict[str, Any] | None = None,
39
+ evolve_prompt_yaml: dict[str, Any] | None = None,
40
+ skill_md: str | None = None,
41
+ ):
42
+ self.skill_name = skill_name
43
+ self.execute = execute
44
+ self.benchmark = benchmark or (lambda executor: (0, 0, []))
45
+ self.summarize_input = summarize_input
46
+ self.evolve_toml = evolve_toml or {}
47
+ self.evolve_prompt_yaml = evolve_prompt_yaml
48
+ self.skill_md = skill_md
49
+
50
+ @property
51
+ def ai_role(self) -> str:
52
+ """从 evolve.toml 读取 ai_role,默认 "correction"。"""
53
+ return self.evolve_toml.get("skill", {}).get("ai_role", "correction")
54
+
55
+
56
+ class SkillLoader:
57
+ """Skill 加载器,负责从磁盘动态导入 run.py 并解析配置文件。"""
58
+
59
+ def __init__(self, skill_base_dir: Path | None = None):
60
+ self._base_dir = skill_base_dir or DEFAULT_SKILL_BASE
61
+ self._cache: dict[str, SkillModule] = {}
62
+
63
+ def load(self, skill_name: str) -> SkillModule:
64
+ """加载指定 Skill(含缓存)"""
65
+ if skill_name in self._cache:
66
+ return self._cache[skill_name]
67
+
68
+ skill_dir = self._base_dir / skill_name
69
+ if not skill_dir.is_dir():
70
+ raise FileNotFoundError(f"Skill 目录不存在: {skill_dir}")
71
+
72
+ # 1. 加载 run.py(强制导出 execute)
73
+ run_path = skill_dir / "scripts" / "run.py"
74
+ if not run_path.is_file():
75
+ raise FileNotFoundError(f"run.py 不存在: {run_path}")
76
+
77
+ spec = importlib.util.spec_from_file_location(
78
+ f"skill_{skill_name.replace('-', '_')}", str(run_path)
79
+ )
80
+ if spec is None or spec.loader is None:
81
+ raise ImportError(f"无法加载 run.py: {run_path}")
82
+
83
+ module = importlib.util.module_from_spec(spec)
84
+ spec.loader.exec_module(module)
85
+
86
+ if not hasattr(module, "execute"):
87
+ raise AttributeError(f"{run_path} 缺少强制函数 execute()")
88
+
89
+ execute_fn = getattr(module, "execute")
90
+ benchmark_fn = getattr(module, "benchmark", None)
91
+ summarize_input_fn = getattr(module, "summarize_input", None)
92
+
93
+ # 2. 加载 evolve.toml
94
+ evolve_toml = self._load_evolve_toml(skill_dir)
95
+
96
+ # 3. 加载 evolve_prompt.yaml(Skill 自定义优先)
97
+ evolve_prompt_yaml = self._load_evolve_prompt_yaml(skill_dir)
98
+
99
+ # 4. 加载 skill.md
100
+ skill_md = self._load_skill_md(skill_dir)
101
+
102
+ skill_module = SkillModule(
103
+ skill_name=skill_name,
104
+ execute=execute_fn,
105
+ benchmark=benchmark_fn,
106
+ summarize_input=summarize_input_fn,
107
+ evolve_toml=evolve_toml,
108
+ evolve_prompt_yaml=evolve_prompt_yaml,
109
+ skill_md=skill_md,
110
+ )
111
+ self._cache[skill_name] = skill_module
112
+ logger.info("Skill 加载完成: %s (ai_role=%s)", skill_name, skill_module.ai_role)
113
+ return skill_module
114
+
115
+ def invalidate_cache(self, skill_name: str | None = None) -> None:
116
+ """清除缓存(配置热加载后调用)。"""
117
+ if skill_name:
118
+ self._cache.pop(skill_name, None)
119
+ else:
120
+ self._cache.clear()
121
+
122
+ @staticmethod
123
+ def _load_evolve_toml(skill_dir: Path) -> dict[str, Any]:
124
+ toml_path = skill_dir / "evolve.toml"
125
+ if not toml_path.is_file():
126
+ logger.debug("evolve.toml 未找到,使用默认值: %s", toml_path)
127
+ return {"skill": {"ai_role": "correction"}}
128
+
129
+ # 简单 TOML 解析(仅支持顶层表 [section] 和 key=value,不依赖 toml 库)
130
+ return _parse_simple_toml(toml_path.read_text(encoding="utf-8"))
131
+
132
+ @staticmethod
133
+ def _load_evolve_prompt_yaml(skill_dir: Path) -> dict[str, Any]:
134
+ yaml_path = skill_dir / "evolve_prompt.yaml"
135
+ if not yaml_path.is_file():
136
+ return {}
137
+ return yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
138
+
139
+ @staticmethod
140
+ def _load_skill_md(skill_dir: Path) -> str | None:
141
+ md_path = skill_dir / "skill.md"
142
+ if not md_path.is_file():
143
+ return None
144
+ return md_path.read_text(encoding="utf-8")
145
+
146
+
147
+ def _parse_simple_toml(content: str) -> dict[str, Any]:
148
+ """极简 TOML 解析器,支持一层嵌套的 [parent.child] 节。"""
149
+ result: dict[str, Any] = {}
150
+ current_path: list[str] = [] # 当前 section 路径栈
151
+
152
+ for line in content.split("\n"):
153
+ line = line.strip()
154
+ if not line or line.startswith("#"):
155
+ continue
156
+
157
+ # [section] 或 [section.sub]
158
+ if line.startswith("[") and "]" in line:
159
+ section_name = line[1:].split("]")[0].strip()
160
+ parts = section_name.split(".")
161
+ current_path = parts
162
+
163
+ # 确保路径存在(若中间节点已被占用为非 dict,则替换为 dict)
164
+ cursor = result
165
+ for part in parts:
166
+ if part not in cursor or not isinstance(cursor.get(part), dict):
167
+ cursor[part] = {}
168
+ cursor = cursor[part]
169
+ continue
170
+
171
+ # key = value
172
+ if "=" in line:
173
+ key, _, value = line.partition("=")
174
+ key = key.strip()
175
+ value = value.strip().strip('"').strip("'")
176
+
177
+ # 类型推断
178
+ if value.lower() in ("true", "false"):
179
+ parsed: Any = value.lower() == "true"
180
+ elif value.isdigit():
181
+ parsed = int(value)
182
+ elif _is_float(value):
183
+ parsed = float(value)
184
+ else:
185
+ parsed = value
186
+
187
+ # 写入当前 section
188
+ cursor = result
189
+ for part in current_path:
190
+ cursor = cursor[part]
191
+ cursor[key] = parsed
192
+
193
+ return result
194
+
195
+
196
+ def _is_float(s: str) -> bool:
197
+ try:
198
+ float(s)
199
+ return "." in s
200
+ except ValueError:
201
+ return False
@@ -0,0 +1,84 @@
1
+ """
2
+ JSONL 日志器 — 追加写入 Skill 执行日志,Pydantic 校验。
3
+
4
+ 日志路径: /data/skill-logs/{skill_name}/{date}.jsonl
5
+ (可通过 SKILL_LOG_DIR 环境变量覆盖)
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime, timezone, timedelta
12
+ from pathlib import Path
13
+
14
+ from skill_self_evolution.models import LogEntry
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _BEIJING_TZ = timezone(timedelta(hours=8))
19
+
20
+
21
+ def _beijing_now() -> datetime:
22
+ return datetime.now(_BEIJING_TZ)
23
+
24
+
25
+ def _beijing_today_str() -> str:
26
+ return _beijing_now().strftime("%Y-%m-%d")
27
+
28
+
29
+ def _get_log_dir(skill_name: str) -> Path:
30
+ """获取日志目录,优先取环境变量 SKILL_LOG_DIR。"""
31
+ base = os.environ.get("SKILL_LOG_DIR", "/data/skill-logs")
32
+ return Path(base) / skill_name
33
+
34
+
35
+ class SkillLogger:
36
+ """Skill 执行日志器,每行一个 JSON(Pydantic LogEntry 校验)。"""
37
+
38
+ def __init__(self, skill_name: str):
39
+ self.skill_name = skill_name
40
+ self._log_path: Path | None = None
41
+
42
+ @property
43
+ def log_path(self) -> Path:
44
+ if self._log_path is None:
45
+ log_dir = _get_log_dir(self.skill_name)
46
+ log_dir.mkdir(parents=True, exist_ok=True)
47
+ self._log_path = log_dir / f"{_beijing_today_str()}.jsonl"
48
+ return self._log_path
49
+
50
+ def write(self, entry: dict) -> None:
51
+ """追加一行 JSON 到日志文件(接受已校验的 dict)。"""
52
+ try:
53
+ with open(self.log_path, "a", encoding="utf-8") as f:
54
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
55
+ except Exception as e:
56
+ logger.warning("Skill 日志写入失败: %s", e)
57
+
58
+ def log_execution(
59
+ self,
60
+ trace_id: str,
61
+ is_failure: bool,
62
+ input_summary: dict,
63
+ rule_output: dict,
64
+ ai_validation: dict | None,
65
+ ai_reselection: dict | None,
66
+ final_output: dict,
67
+ warnings: list[str],
68
+ elapsed_ms: float,
69
+ ) -> None:
70
+ """写入标准执行日志条目(Pydantic 校验后持久化)。"""
71
+ entry = LogEntry(
72
+ trace_id=trace_id,
73
+ skill_name=self.skill_name,
74
+ timestamp=_beijing_now().isoformat(),
75
+ is_failure=is_failure,
76
+ input_summary=input_summary,
77
+ rule_output=rule_output,
78
+ ai_validation=ai_validation,
79
+ ai_reselection=ai_reselection,
80
+ final_output=final_output,
81
+ warnings=warnings,
82
+ elapsed_ms=round(elapsed_ms, 1),
83
+ )
84
+ self.write(entry.model_dump())
@@ -0,0 +1,147 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Pydantic input/output models ? all Skills must use these.
4
+
5
+ All AI-related enums/literals use pre-constructed string constants to
6
+ avoid encoding issues across platforms.
7
+ """
8
+
9
+ from typing import Any, Generic, Literal, TypeVar
10
+ from uuid import uuid4
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
13
+
14
+ T = TypeVar("T")
15
+
16
+ # ?? AI judgement literals ?????????????????????????????????????
17
+ # Use constants to avoid platform encoding quirks with Chinese Unicode
18
+ _REASONABLE = "\u5408\u7406" # ??
19
+ _UNREASONABLE = "\u4e0d\u5408\u7406" # ???
20
+
21
+
22
+ # ?? P0: AI intermediate results ??????????????????????????????
23
+
24
+ class AiValidationResult(BaseModel):
25
+ """Standardised output of AI common-sense validation."""
26
+
27
+ model_config = ConfigDict(frozen=True)
28
+
29
+ result: Literal[
30
+ "\u5408\u7406",
31
+ "\u4e0d\u5408\u7406",
32
+ ] = Field(..., description="AI judgement: reasonable / unreasonable")
33
+
34
+ reason: str = Field(default="", description="AI reasoning (max 500 chars)")
35
+
36
+ @field_validator("reason")
37
+ @classmethod
38
+ def _truncate_reason(cls, v: str) -> str:
39
+ return v[:500]
40
+
41
+
42
+ class AiReselectionResult(BaseModel):
43
+ """Standardised output of AI reselection."""
44
+
45
+ model_config = ConfigDict(frozen=True)
46
+
47
+ result: str = Field(
48
+ ..., description="Reselected value, or '\u4e0d\u5408\u7406' if still unreasonable"
49
+ )
50
+ reason: str = Field(default="", description="AI reasoning (max 500 chars)")
51
+
52
+ @field_validator("reason")
53
+ @classmethod
54
+ def _truncate_reason(cls, v: str) -> str:
55
+ return v[:500]
56
+
57
+
58
+ # ?? P1: DeepSeek client models ???????????????????????????????
59
+
60
+ class DeepSeekChatResponse(BaseModel):
61
+ """Validated OpenAI Chat Completions response wrapper."""
62
+
63
+ content: str = Field(default="")
64
+ finish_reason: str | None = Field(default=None)
65
+ prompt_tokens: int | None = Field(default=None)
66
+ completion_tokens: int | None = Field(default=None)
67
+
68
+
69
+ # ?? P1: Log entry ????????????????????????????????????????????
70
+
71
+ class LogEntry(BaseModel):
72
+ """Single JSONL log entry schema.
73
+
74
+ All fields defaulted so legacy / partial reads won't fail.
75
+ """
76
+
77
+ trace_id: str = Field(default="")
78
+ skill_name: str = Field(default="")
79
+ timestamp: str = Field(default="")
80
+ is_failure: bool = Field(default=False)
81
+ input_summary: dict[str, Any] = Field(default_factory=dict)
82
+ rule_output: dict[str, Any] = Field(default_factory=dict)
83
+ ai_validation: dict[str, Any] | None = Field(default=None)
84
+ ai_reselection: dict[str, Any] | None = Field(default=None)
85
+ final_output: dict[str, Any] = Field(default_factory=dict)
86
+ warnings: list[str] = Field(default_factory=list)
87
+ elapsed_ms: float = Field(default=0.0)
88
+
89
+
90
+ # ?? P2: Fallback config ??????????????????????????????????????
91
+
92
+ class FallbackConfigModel(BaseModel):
93
+ """Validated fallback configuration."""
94
+
95
+ validate_timeout_seconds: float = Field(default=3.0, gt=0)
96
+ reselect_timeout_seconds: float = Field(default=5.0, gt=0)
97
+ max_retries: int = Field(default=1, ge=0, le=5)
98
+ circuit_breaker_threshold: int = Field(default=3, ge=1)
99
+ circuit_breaker_cooldown_seconds: float = Field(default=60.0, gt=0)
100
+ conservative_mode: bool = Field(default=False)
101
+ enabled: bool = Field(default=True)
102
+
103
+
104
+ # ?? P2: Evolve proposal ??????????????????????????????????????
105
+
106
+ class EvolveProposalModel(BaseModel):
107
+ """Serializable evolve proposal."""
108
+
109
+ rules_changes: dict[str, Any] = Field(default_factory=dict)
110
+ prompt_changes: dict[str, Any] = Field(default_factory=dict)
111
+ rules_text: str | None = Field(default=None)
112
+ prompt_text: str | None = Field(default=None)
113
+ analysis_raw: str = Field(default="")
114
+ failure_count: int = Field(default=0)
115
+ applied: bool = Field(default=False)
116
+ rolled_back: bool = Field(default=False)
117
+
118
+
119
+ # ?? P3: Skill-specific result sub-models (example) ?????????
120
+
121
+ class NicknameSkillResult(BaseModel):
122
+ """nickname-selector Skill result sub-structure."""
123
+
124
+ nickname: str = Field(default="", description="Selected nickname")
125
+ source: str = Field(default="rule", description="rule | ai")
126
+ candidates: list[str] = Field(default_factory=list)
127
+ band_id: str = Field(default="")
128
+ screenshot_id: str = Field(default="")
129
+
130
+
131
+ # ?? Core framework models (existing) ????????????????????????
132
+
133
+ class SkillOutput(BaseModel):
134
+ """All Skills must return this structure."""
135
+
136
+ source: str = Field(..., description="rule | ai")
137
+ result: dict = Field(..., description="Business result (per-Skill schema)")
138
+ ai_validated: bool = False
139
+ ai_reselected: bool = False
140
+ warnings: list[str] = Field(default_factory=list)
141
+
142
+
143
+ class SkillInput(BaseModel, Generic[T]):
144
+ """All Skills must receive this structure."""
145
+
146
+ trace_id: str = Field(default_factory=lambda: str(uuid4()))
147
+ input_data: T = Field(..., description="Business input data")
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: skill_self_evolution
3
+ Version: 0.2.0
4
+ Summary: Skill 自进化框架:Pydantic 全链路校验 + 规则执行 + AI 常识判断 + 离线进化的可插拔 Skill 执行引擎
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pydantic>=2.0
7
+ Requires-Dist: pyyaml>=6.0
8
+ Requires-Dist: httpx>=0.25
9
+ Requires-Dist: pymysql>=1.1
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == "dev"
12
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
@@ -0,0 +1,16 @@
1
+ skill_self_evolution/__init__.py,sha256=jV_l30ZPiwizIGEZj4re3xcfo-Whm075T00zAdpi3Rk,931
2
+ skill_self_evolution/ai_assisted_executor.py,sha256=azSjW2RDuo5pJP59zM-BNhVMpAWzSRvhDxGIP4uNCKo,10250
3
+ skill_self_evolution/config.py,sha256=UPVWO9RBM8EA2Rm4K6vcfWyEIBt9hQyGwHomxMzdV6Q,2680
4
+ skill_self_evolution/config_loader.py,sha256=NZOZIUvhUDU-SJqPQRlUdYuTbCrDWsi9rxz1NJhOT9E,9919
5
+ skill_self_evolution/context.py,sha256=u5ZHIBobMQl43I_TQzMuqjWL5c8_3q8DQlck_19GEhs,635
6
+ skill_self_evolution/deepseek.py,sha256=8SGjMKRJUgOsmIxGX6RC8i3Jv6W0KlgOzAvGdSHyCoA,6110
7
+ skill_self_evolution/evolver.py,sha256=hvqgS-V2PCTQy6GiqvAPQfOjHczFBRu2ZpWlqTuYx1A,15617
8
+ skill_self_evolution/executor.py,sha256=sJOUf1kngMVNq_0rR6RQFRA5FgRd-VRnqYMfapWGhEc,20989
9
+ skill_self_evolution/fallback.py,sha256=F9fRVFiKt_EYN9T7mnyR4P_ycAr5ZoORK1jo7CH5xKg,4649
10
+ skill_self_evolution/loader.py,sha256=98c9AExsd50AQcyLRIZLGI7cf6-a_xv5JHYbZjHltuc,7257
11
+ skill_self_evolution/logger.py,sha256=dCAfgx6roBCXdRTJmrba4Nu8CmvIf2VfTKTOrYJJy1k,2671
12
+ skill_self_evolution/models.py,sha256=v6JuKArffGA_y8uFpqGRtl5-7fW1yfG42tcZwsRZCZ4,5033
13
+ skill_self_evolution-0.2.0.dist-info/METADATA,sha256=yKZmqXOAsTh6Ami9Mw-2SVK1gs_XXwh3gYzTxvlHhlM,463
14
+ skill_self_evolution-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ skill_self_evolution-0.2.0.dist-info/top_level.txt,sha256=dAqlk1foGIgtvJ_3WElSlP3TvIO69qSMVLThHpbFy0U,21
16
+ skill_self_evolution-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ skill_self_evolution