File size: 8,077 Bytes
816198f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | import os
from pathlib import Path
from typing import Any, Dict, Iterable, List
SKILL_USAGE_GUIDE_TEXT = """# Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
## Available Skills
{available_skills}
## How to Use Skills
* Discovery: Skills are listed with their name, description, and file path. You can open the source for full instructions.
* Triggering: If the user names a skill OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
* Missing/Blocked Skills: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
* Using a Skill (progressive disclosure):
1. After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
2. If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
3. If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
4. If `assets/` or templates exist, reuse them instead of recreating from scratch.
* Coordination and Sequencing:
* For multiple applicable skills, choose the minimal set and state the order.
* Announce which skill(s) you're using and why (one short line).If you skip an obvious skill, say why.
* Context Hygiene:
* Summarize long sections instead of pasting them; load extra files only when needed.
* Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
* For variants (frameworks, providers, domains), pick the relevant reference files and note the choice.
* Fallback: If a skill can’t be applied due to missing files or unclear instructions, state the issue, pick the next-best approach, and proceed."""
def _has_non_empty_value(value: Any) -> bool:
"""
判定值非空
"""
if value is None:
return False
if isinstance(value, str):
return value.strip() != ""
if isinstance(value, (list, tuple, set, dict)):
return len(value) > 0
return True
def normalize_skill_path(skill_path: str) -> str:
"""
把 skill path 整理成 skills/name/... 的形式
"""
if not isinstance(skill_path, str):
return ""
normalized = skill_path.replace("\\", "/").strip()
if not normalized:
return ""
normalized = normalized.rstrip("/")
if normalized.startswith("./"):
normalized = normalized[2:]
if normalized.startswith("skills/"):
return normalized
marker = "/skills/"
idx = normalized.rfind(marker)
if idx >= 0:
return normalized[idx + 1 :]
idx = normalized.find("skills/")
if idx >= 0:
return normalized[idx:]
return normalized
def normalize_skill_dir_path(skill_path: str) -> str:
"""
只返回 skill_path 中的 skill 对应的文件夹 skills/name
"""
normalized = normalize_skill_path(skill_path)
if normalized.endswith("/SKILL.md"):
return normalized[: -len("/SKILL.md")]
if normalized == "SKILL.md":
return "skills"
return normalized
def to_skill_file_path(skill_path: str) -> str:
"""
返回 f"{normalized}/SKILL.md"
"""
normalized = normalize_skill_dir_path(skill_path)
if not normalized:
return ""
if normalized.endswith("SKILL.md"):
return normalized
return f"{normalized}/SKILL.md"
def _normalize_single_skill(skill_obj: Dict[str, Any]) -> Dict[str, str]:
"""
获取 name,description,skill_path 字段返回
"""
name = str(skill_obj.get("name", "") or "").strip()
description = str(skill_obj.get("description", "") or "").strip()
skill_path = str(skill_obj.get("skill_path", "") or "").strip()
if "raw_skill_path" in skill_obj:
raw_skill_path = str(skill_obj.get("raw_skill_path", "") or "").strip()
else:
# 只有第一次从数据集中读取的时候,是读取正式的路径结果,后续的 skill_path 都已经被处理成相对路径了
raw_skill_path = str(skill_obj.get("skill_path", "") or "").strip()
if skill_path:
skill_path = normalize_skill_dir_path(skill_path)
return {
"name": name,
"description": description,
"skill_path": skill_path,
"raw_skill_path": raw_skill_path
}
def deduplicate_skills(skills: Iterable[Dict[str, Any]]) -> List[Dict[str, str]]:
"""
去重重复的 skill(按照 skill path + name 同时作为一致性索引)
"""
deduped: List[Dict[str, str]] = []
seen = set()
for skill in skills:
normalized = _normalize_single_skill(skill if isinstance(skill, dict) else {})
name = normalized.get("name", "")
description = normalized.get("description", "")
skill_path = normalized.get("skill_path", "")
if not (name or skill_path):
continue
key = (skill_path.lower(), name.lower()) if skill_path else ("", name.lower())
if key in seen:
continue
seen.add(key)
if not skill_path and name:
skill_path = f"skills/{name}"
normalized["skill_path"] = skill_path
deduped.append(normalized)
return deduped
def extract_skills_from_row(row: Dict[str, Any]) -> List[Dict[str, str]]:
"""
找到 row 中的所有 skill
"""
if not isinstance(row, dict):
return []
collected: List[Dict[str, Any]] = []
other_skills = row.get("skills")
if _has_non_empty_value(other_skills):
if isinstance(other_skills, dict):
collected.append(other_skills)
elif isinstance(other_skills, list):
for item in other_skills:
if isinstance(item, dict) and _has_non_empty_value(item):
collected.append(item)
return deduplicate_skills(collected)
def build_skills_system_text(skills: List[Dict[str, Any]]) -> str:
"""
构建 system prompt 的 skill 部分完整文本,包括
##Available Skills 文本 动态拼接 和该部分内容的前后的所有 skill 相关的提示词
需要注意的是,如果 skill 对应的 SKILL.md 文件不存在,这个 skill 会被判定为无效且跳过
"""
normalized_skills = deduplicate_skills(skills)
if not normalized_skills:
return ""
lines = []
for skill in normalized_skills:
name = skill.get("name", "").strip() or "unknown-skill"
description = skill.get("description", "").strip() or "No description provided."
file_path = to_skill_file_path(skill.get("skill_path", "").strip())
if not file_path:
# 跳过所有无效的 skill
continue
lines.append(f"- {name}: {description} (file: {file_path})")
available_skills = "\n".join(lines)
return SKILL_USAGE_GUIDE_TEXT.format(available_skills=available_skills).strip()
def resolve_skill_source_dirs(skills: List[Dict[str, Any]], project_root: str) -> List[str]:
"""
返回所有不重复的 skill 的文件夹的绝对路径(读取 raw_skill_path 拿到真实 skill 所在的路径)
"""
dirs: List[str] = []
seen = set()
for skill in deduplicate_skills(skills):
skill_path = skill.get("raw_skill_path", "").strip()
print("skill_path: ", skill_path)
if not skill_path:
continue
abs_path = skill_path if os.path.isabs(skill_path) else os.path.join(project_root, skill_path)
p = Path(abs_path)
if p.is_file() and p.name == "SKILL.md":
p = p.parent
p_str = str(p.resolve()) if p.exists() else str(p)
if p_str in seen:
continue
seen.add(p_str)
dirs.append(p_str)
return dirs
|