| 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: |
| |
| 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: |
| |
| 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 |
|
|