Files
sanguo_moziplus_v2/src/daemon/skill_system.py
T
cfdaily f5bf671410
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 0s
[moz] chore(daemon): S6 deprecated 代码标记 + ticker 经验蒸馏空转修复
B4: ticker.py ExperienceDistiller 调用改为 debug 日志空转(§19 双层 daily cron 替代)
B5: skill_system.py / experience.py 文件头部标记 DEPRECATED

保留代码向后兼容,P3 时再物理删除。
2026-06-18 23:04:44 +08:00

250 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DEPRECATED per §19 重设计 — 不再参与 skill 发现/加载
# 实际 skill 发现走 openclaw 原生 <available_skills> 机制
# 保留代码供参考,后续 P3 清理时物理删除
"""Skill System — 技能注册、加载、匹配、执行
三层自由度:
- 高自由(原则层):只有名称+描述,Agent 自由发挥
- 中自由(模板层):有 prompt templateAgent 按模板填
- 低自由(脚本层):有脚本/工具,Agent 调用执行
"""
from __future__ import annotations
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger("moziplus-v2.skill")
class SkillFreedom(str, Enum):
HIGH = "high" # 原则层
MEDIUM = "medium" # 模板层
LOW = "low" # 脚本层
@dataclass
class Skill:
"""技能描述"""
id: str
name: str
description: str
freedom: str = SkillFreedom.HIGH.value
prompt_template: Optional[str] = None
script_path: Optional[str] = None
tags: List[str] = field(default_factory=list)
input_schema: Optional[Dict[str, Any]] = None
output_schema: Optional[Dict[str, Any]] = None
enabled: bool = True
created_at: Optional[str] = None
updated_at: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"description": self.description,
"freedom": self.freedom,
"prompt_template": self.prompt_template,
"script_path": self.script_path,
"tags": self.tags,
"enabled": self.enabled,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Skill:
return cls(**{k: v for k, v in data.items()
if k in cls.__dataclass_fields__})
class SkillRegistry:
"""技能注册表"""
def __init__(self, skills_dir: Optional[Path] = None):
self._skills: Dict[str, Skill] = {}
self.skills_dir = skills_dir
if skills_dir and skills_dir.exists():
self._load_from_dir(skills_dir)
def _load_from_dir(self, dir_path: Path) -> None:
"""从目录加载 skill 定义(每个 .json 一个)"""
for f in dir_path.glob("*.json"):
try:
data = json.loads(f.read_text())
skill = Skill.from_dict(data)
self._skills[skill.id] = skill
except Exception:
logger.warning("Failed to load skill from %s", f)
def register(self, skill: Skill) -> str:
self._skills[skill.id] = skill
return skill.id
def unregister(self, skill_id: str) -> bool:
if skill_id in self._skills:
del self._skills[skill_id]
return True
return False
def get(self, skill_id: str) -> Optional[Skill]:
return self._skills.get(skill_id)
def list_skills(
self,
freedom: Optional[str] = None,
tags: Optional[List[str]] = None,
enabled_only: bool = True,
) -> List[Skill]:
results = list(self._skills.values())
if enabled_only:
results = [s for s in results if s.enabled]
if freedom:
results = [s for s in results if s.freedom == freedom]
if tags:
results = [s for s in results
if any(t in s.tags for t in tags)]
return results
def match(
self,
query: str,
task_type: Optional[str] = None,
limit: int = 5,
) -> List[Tuple[Skill, float]]:
"""匹配技能
Returns:
[(Skill, relevance_score), ...]
"""
q_lower = query.lower()
scored = []
for skill in self._skills.values():
if not skill.enabled:
continue
score = 0.0
# 名称匹配(权重最高)
if q_lower in skill.name.lower():
score += 3.0
# 描述匹配
if q_lower in skill.description.lower():
score += 2.0
# 标签匹配
if task_type and task_type.lower() in skill.tags:
score += 1.5
for tag in skill.tags:
if tag.lower() in q_lower:
score += 1.0
if score > 0:
scored.append((skill, score))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:limit]
def count(self) -> int:
return len(self._skills)
class SkillExecutor:
"""技能执行器"""
def __init__(
self,
registry: Optional[SkillRegistry] = None,
allow_scripts: bool = False,
):
self.registry = registry or SkillRegistry()
self.allow_scripts = allow_scripts
self._execution_log: List[Dict[str, Any]] = []
def build_prompt(
self,
skill_id: str,
variables: Optional[Dict[str, str]] = None,
) -> Optional[str]:
"""构建 skill prompt"""
skill = self.registry.get(skill_id)
if not skill:
return None
if skill.freedom == SkillFreedom.HIGH.value:
return f"## Skill: {skill.name}\n{skill.description}\n\nPrinciple: Apply the above guidelines."
if skill.freedom == SkillFreedom.MEDIUM.value:
template = skill.prompt_template or skill.description
if variables:
for key, value in variables.items():
template = template.replace(f"{{{{{key}}}}}", value)
return template
if skill.freedom == SkillFreedom.LOW.value:
parts = [f"## Skill: {skill.name}"]
parts.append(skill.description)
if skill.script_path:
parts.append(f"Execute: `{skill.script_path}`")
return "\n".join(parts)
return skill.description
def execute(
self,
skill_id: str,
context: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""执行技能
Returns:
{"status": "success"|"error", "prompt": str, "skill": str}
"""
skill = self.registry.get(skill_id)
if not skill:
return {"status": "error", "error": f"Skill not found: {skill_id}"}
if not skill.enabled:
return {"status": "error", "error": f"Skill disabled: {skill_id}"}
prompt = self.build_prompt(skill_id, context)
# 脚本执行需要显式允许
if skill.freedom == SkillFreedom.LOW.value and skill.script_path:
if not self.allow_scripts:
return {
"status": "error",
"error": "Script execution not allowed",
"prompt": prompt,
}
result = {
"status": "success",
"prompt": prompt,
"skill": skill_id,
"freedom": skill.freedom,
}
self._execution_log.append({
"skill_id": skill_id,
"timestamp": datetime.utcnow().isoformat(),
"status": result["status"],
})
return result
@property
def execution_log(self) -> List[Dict[str, Any]]:
return list(self._execution_log)