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