Threat_Hunter / skills /skill_loader.py
EricChen2005's picture
Deploy ThreatHunter - AMD MI300X + Qwen2.5-32B
c8d30bc
"""
skills/skill_loader.py โ€” ๅ‹•ๆ…‹ Skill ็†ฑ่ผ‰ๅ…ฅ็ณป็ตฑ (Phase 4D)
============================================================
่จญ่จˆ็›ฎๆจ™๏ผš
- ็„ก้œ€้‡ๅ•Ÿ FastAPI ๆœๅ‹™ๅณๅฏๆ›ดๆ–ฐ SOP .md ๆช”ๆกˆ
- mtime ๅž‹ LRU Cache๏ผš่ฎ€ๅ– + ๅฟซๅ–๏ผŒไฟฎๆ”นๅพŒ่‡ชๅ‹•ๅคฑๆ•ˆ
- ๅŸท่กŒ็ท’ๅฎ‰ๅ…จ๏ผšๆ‰€ๆœ‰ๅ…ฌ้–‹ๆ–นๆณ•ๅ‡ๅ— threading.RLock ไฟ่ญท
- Graceful Degradation๏ผšๆช”ๆกˆ้บๅคฑๆ™‚ๅ›ž้€€ๅˆฐๅตŒๅ…ฅๅผ fallback SOP
- ๅฏ่ง€ๆธฌๆ€ง๏ผšๆไพ›ๅฎŒๆ•ด็š„ registry API ไพ› /api/skills ็ซฏ้ปžไฝฟ็”จ
ๆžถๆง‹๏ผš
SkillLoader๏ผˆๅ–ฎไพ‹๏ผ‰
โ”œโ”€โ”€ _load_with_mtime() ่ฎ€ๅ– .md โ†’ ๅฟซๅ– (content, mtime, load_time)
โ”œโ”€โ”€ load_skill() ๅ…ฌ้–‹ๅ–ๅพ—ไป‹้ข๏ผˆmtime ้ฉ—่ญ‰๏ผŒ้ŽๆœŸ่‡ชๅ‹• reload๏ผ‰
โ”œโ”€โ”€ reload_skill() ๅผทๅˆถ้‡่ผ‰๏ผˆไธ็ฎก mtime๏ผ‰
โ”œโ”€โ”€ reload_all() ๅผทๅˆถ้‡่ผ‰ๅ…จ้ƒจ
โ””โ”€โ”€ get_registry() ๅˆ—ๅ‡บๆ‰€ๆœ‰ๅทฒๅฟซๅ–็š„ skill + ็‰ˆๆœฌ่ณ‡่จŠ
็›ธๅฎนๆ€ง๏ผš
- ๆ‰€ๆœ‰็พๆœ‰ Agent ็š„ _load_skill() ๅฏ็„ก็ธซๆ›ฟๆ›็‚บ
skill_loader.load_skill(filename)
- ๆ–ฐๅขž server.py API ็ซฏ้ปžไฝฟ็”จ skill_loader.get_registry()
้ตๅฎˆ๏ผšproject_CONSTITUTION.md + AGENTS.md + HARNESS_ENGINEERING.md
"""
import logging
import os
import threading
import time
from pathlib import Path
from typing import Optional
logger = logging.getLogger("ThreatHunter.skill_loader")
# โ”€โ”€ Skill ็›ฎ้Œ„ๅฎšไฝ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_PROJECT_ROOT = Path(__file__).parent.parent
_SKILLS_DIR = Path(__file__).parent # skills/ ็›ฎ้Œ„ๆœฌ่บซ
# โ”€โ”€ ๅฟซๅ– TTL๏ผˆไปฅ็ง’่จˆ๏ผ‰๏ผšๅทฒไฟฎๆ”น็š„ๆช”ๆกˆๅœจๆญคๆ™‚้–“ๅพŒๅผทๅˆถ reload โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ่จญ็ฝฎ็‚บ 0 ไปฃ่กจๆฏๆฌก้ƒฝ้ฉ—่ญ‰ mtime๏ผˆๆœ€ๅณๆ™‚ไฝ†ๆ€ง่ƒฝ็จๅทฎ๏ผ‰
# ่จญ็ฝฎ็‚บๆญฃๆ•ธไปฃ่กจ stall ๆœŸ้–“๏ผŒTTL ๅ…งไธ check mtime๏ผˆ้ซ˜ๆ€ง่ƒฝ๏ผ‰
CACHE_TTL_SECONDS: float = float(os.getenv("SKILL_CACHE_TTL", "5.0"))
# โ”€โ”€ ๆ‰€ๆœ‰ skill ๆช”ๆกˆ็š„ๅฐๆ‡‰ fallback SOP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_FALLBACK_SOPS: dict[str, str] = {
"threat_intel.md": """
# Skill: Threat Intel Scout (fallback)
## SOP
1. read_memory(agent_name="scout")
2. search_nvd ๆŸฅ่ฉขๆฏๅ€‹ๅฅ—ไปถ
3. CVSS >= 7.0 โ†’ search_otx
4. ๆฏ”ๅฐๆญทๅฒๆจ™่จ˜ is_new
5. write_memory ๅฏซๅ…ฅ
6. ่ผธๅ‡บ็ด” JSON
""".strip(),
"source_code_audit.md": """
# Skill: Source Code Audit (fallback)
## SOP
1. Identify imported packages from code
2. search_nvd for each package
3. Flag hardcoded secrets (OWASP A07)
4. write_memory, output JSON
""".strip(),
"ai_security_audit.md": """
# Skill: AI Security Audit (fallback)
## SOP
1. Classify input: prompt injection / jailbreak / data poisoning
2. Map to OWASP LLM Top10
3. Rate severity 1-10
4. Output JSON (no CVE calls needed)
""".strip(),
"config_audit.md": """
# Skill: Config Audit (fallback)
## SOP
1. Check for hardcoded secrets
2. Validate against CIS Benchmark
3. Flag misconfigurations
4. Output JSON
""".strip(),
}
_DEFAULT_FALLBACK = """
# Skill SOP (generic fallback)
Follow security analysis best practices.
Output structured JSON with findings.
Do not fabricate CVE IDs.
""".strip()
class _CacheEntry:
"""ๅ–ฎไธ€ Skill ็š„ๅฟซๅ–ๆข็›ฎ"""
__slots__ = ("content", "mtime", "load_time", "filename", "size_bytes")
def __init__(self, filename: str, content: str, mtime: float):
self.filename = filename
self.content = content
self.mtime = mtime # ็ฃ็ขŸไธŠ็š„ mtime๏ผˆfloat๏ผŒUnix timestamp๏ผ‰
self.load_time = time.time() # ๆœฌๆฌก reload ็š„ๆ™‚้–“
self.size_bytes = len(content.encode("utf-8"))
class SkillLoader:
"""
ๅŸท่กŒ็ท’ๅฎ‰ๅ…จ็š„ Skill ็†ฑ่ผ‰ๅ…ฅๅ™จ๏ผˆๅ–ฎไพ‹ๆŽจ่–ฆ๏ผ‰ใ€‚
ไฝฟ็”จ็ฏ„ไพ‹๏ผš
from skills.skill_loader import skill_loader
sop = skill_loader.load_skill("threat_intel.md")
API๏ผš
load_skill(filename) โ†’ str ๏ผˆๅฟซๅ– + ่‡ชๅ‹•ๅคฑๆ•ˆ๏ผ‰
reload_skill(filename) โ†’ str ๏ผˆๅผทๅˆถ้‡่ผ‰๏ผ‰
reload_all() โ†’ dict ๏ผˆ้‡่ผ‰ๅ…จ้ƒจๅทฒๅฟซๅ–๏ผ‰
get_registry() โ†’ dict ๏ผˆๅˆ—ๅ‡บๆ‰€ๆœ‰ๅฟซๅ–ๅ…งๅฎน๏ผ‰
invalidate(filename) โ†’ None ๏ผˆ็งป้™คๅ–ฎไธ€ๅฟซๅ–ๆข็›ฎ๏ผ‰
invalidate_all() โ†’ None ๏ผˆๆธ…็ฉบๅ…จ้ƒจๅฟซๅ–๏ผ‰
"""
def __init__(self, skills_dir: Path | str | None = None):
self._skills_dir = Path(skills_dir) if skills_dir else _SKILLS_DIR
self._cache: dict[str, _CacheEntry] = {}
self._lock = threading.RLock()
logger.info("[SkillLoader] ๅˆๅง‹ๅŒ–ๅฎŒๆˆ | skills_dir=%s", self._skills_dir)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๆ ธๅฟƒ่ฎ€ๅ–
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _get_mtime(self, filepath: Path) -> Optional[float]:
"""ๅ–ๅพ—ๆช”ๆกˆ็š„ mtime๏ผˆ่‹ฅไธๅญ˜ๅœจๅ‰‡ๅ›žๅ‚ณ None๏ผ‰"""
try:
return filepath.stat().st_mtime
except (OSError, FileNotFoundError):
return None
def _read_file(self, filepath: Path) -> Optional[str]:
"""ๅ˜—่ฉฆๅคš็จฎ็ทจ็ขผ่ฎ€ๅ– .md ๆช”ๆกˆ๏ผŒๅคฑๆ•—ๅ›žๅ‚ณ None"""
for enc in ("utf-8", "utf-8-sig", "latin-1"):
try:
content = filepath.read_text(encoding=enc).strip()
if content:
return content
except (OSError, UnicodeDecodeError):
continue
return None
def _load_with_mtime(self, filename: str) -> _CacheEntry:
"""
ๅพž็ฃ็ขŸ่ฎ€ๅ– skill ไธฆๅปบ็ซ‹ๅฟซๅ–ๆข็›ฎใ€‚
่‹ฅ่ฎ€ๅ–ๅคฑๆ•—๏ผŒไฝฟ็”จ fallback SOP ๅปบ็ซ‹ๆข็›ฎ๏ผˆmtime=-1 ๆจ™่ญ˜็‚บ fallback๏ผ‰ใ€‚
"""
filepath = self._skills_dir / filename
mtime = self._get_mtime(filepath)
if mtime is not None:
content = self._read_file(filepath)
if content:
logger.info("[SkillLoader] ่ผ‰ๅ…ฅ: %s (%d chars)", filename, len(content))
return _CacheEntry(filename, content, mtime)
else:
logger.warning("[SkillLoader] ๆช”ๆกˆ็‚บ็ฉบ: %s๏ผŒไฝฟ็”จ fallback", filename)
else:
logger.warning("[SkillLoader] ๆ‰พไธๅˆฐๆช”ๆกˆ: %s๏ผŒไฝฟ็”จ fallback", filename)
# Fallback๏ผšไฝฟ็”จๅตŒๅ…ฅๅผ SOP
fallback_content = _FALLBACK_SOPS.get(filename, _DEFAULT_FALLBACK)
return _CacheEntry(filename, fallback_content, -1.0)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅ…ฌ้–‹ API
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def load_skill(self, filename: str) -> str:
"""
ๅ–ๅพ— Skill SOP ๅ…งๅฎน๏ผˆๅฟซๅ–ๅ„ชๅ…ˆ๏ผŒmtime ้ฉ—่ญ‰่‡ชๅ‹•ๅคฑๆ•ˆ๏ผ‰ใ€‚
้‚่ผฏ๏ผš
1. ่‹ฅๅฟซๅ–ไธญ็„กๆญคๆช” โ†’ ๅพž็ฃ็ขŸ่ผ‰ๅ…ฅ โ†’ ๅฟซๅ–
2. ่‹ฅๅฟซๅ–ๅญ˜ๅœจ + TTL ๅ…ง โ†’ ็›ดๆŽฅๅ›žๅ‚ณ๏ผˆๆœ€้ซ˜ๆ•ˆ๏ผ‰
3. ่‹ฅๅฟซๅ–ๅญ˜ๅœจ + TTL ้ŽๆœŸ โ†’ ้ฉ—่ญ‰ mtime๏ผš
mtime ไธ่ฎŠ โ†’ ๆ›ดๆ–ฐ load_time๏ผŒ็นผ็บŒไฝฟ็”จ
mtime ๆ”น่ฎŠ โ†’ ้‡ๆ–ฐๅพž็ฃ็ขŸ่ผ‰ๅ…ฅ๏ผˆ็†ฑ่ผ‰ๅ…ฅ๏ผ๏ผ‰
4. fallback entry๏ผˆmtime=-1๏ผ‰โ†’ ๆฏๆฌก้‡่ฉฆ็ฃ็ขŸ็ขบ่ชๆ˜ฏๅฆๅทฒๅปบ็ซ‹
Args:
filename: Skill .md ๆ–‡ไปถๅ๏ผˆไธๅซ่ทฏๅพ‘๏ผ‰๏ผŒๅฆ‚ "threat_intel.md"
Returns:
str: Skill ๆ–‡ไปถๅ…งๅฎน๏ผˆๆˆ– fallback SOP๏ผ‰
"""
with self._lock:
entry = self._cache.get(filename)
# ๆƒ…ๆณ 1๏ผšๅฐšๆœชๅฟซๅ– โ†’ ่ผ‰ๅ…ฅ
if entry is None:
entry = self._load_with_mtime(filename)
self._cache[filename] = entry
return entry.content
# ๆƒ…ๆณ 2๏ผšTTL ๅ…ง โ†’ ็›ดๆŽฅๅ›žๅ‚ณ
age = time.time() - entry.load_time
if age < CACHE_TTL_SECONDS:
return entry.content
# ๆƒ…ๆณ 3 / 4๏ผšTTL ้ŽๆœŸ๏ผŒ้ฉ—่ญ‰ mtime
current_mtime = self._get_mtime(self._skills_dir / filename)
if current_mtime is None:
# ๆช”ๆกˆๆถˆๅคฑไบ† โ†’ ่‹ฅๆ˜ฏ fallback ๅฐฑ็นผ็บŒ็”จ๏ผŒๅฆๅ‰‡ๅˆ‡ๆ› fallback
if entry.mtime == -1.0:
entry.load_time = time.time() # ๅˆทๆ–ฐ TTL
else:
logger.warning("[SkillLoader] ็†ฑ่ผ‰ๅ…ฅๅตๆธฌ๏ผš%s ๅทฒๅˆช้™ค๏ผŒๅˆ‡ๆ› fallback", filename)
entry = self._load_with_mtime(filename) # ๆœƒ่ตฐ fallback ่ทฏๅพ‘
self._cache[filename] = entry
return entry.content
if current_mtime == entry.mtime:
# ๆช”ๆกˆๆœชๆ›ดๅ‹• โ†’ ๆ›ดๆ–ฐ load_time ๅˆทๆ–ฐ TTL
entry.load_time = time.time()
return entry.content
# ๆช”ๆกˆๅทฒๆ›ดๆ–ฐ๏ผ็†ฑ่ผ‰ๅ…ฅ
logger.info(
"[SkillLoader] ๐Ÿ”„ ็†ฑ่ผ‰ๅ…ฅ %s (่ˆŠ mtime=%.3f โ†’ ๆ–ฐ mtime=%.3f)",
filename, entry.mtime, current_mtime,
)
entry = self._load_with_mtime(filename)
self._cache[filename] = entry
return entry.content
def reload_skill(self, filename: str) -> str:
"""
ๅผทๅˆถ้‡่ผ‰ๆŒ‡ๅฎš Skill๏ผˆไธ็ฎก mtime ๅ’Œ TTL๏ผ‰ใ€‚
้ฉ็”จๆ–ผ๏ผš/api/skills/reload API ่ขซๅ‘ผๅซๆ™‚ใ€‚
Returns:
str: ้‡ๆ–ฐ่ผ‰ๅ…ฅๅพŒ็š„ Skill ๅ…งๅฎน
"""
with self._lock:
logger.info("[SkillLoader] ๅผทๅˆถ้‡่ผ‰: %s", filename)
entry = self._load_with_mtime(filename)
self._cache[filename] = entry
return entry.content
def reload_all(self) -> dict[str, str]:
"""
ๅผทๅˆถ้‡่ผ‰ๆ‰€ๆœ‰ๅทฒๅฟซๅ–็š„ Skillใ€‚
Returns:
dict[filename โ†’ new_content]๏ผˆๅŒ…ๅซ fallback entry๏ผ‰
"""
with self._lock:
results = {}
for filename in list(self._cache.keys()):
entry = self._load_with_mtime(filename)
self._cache[filename] = entry
results[filename] = entry.content
logger.info("[SkillLoader] ๅ…จ้ƒจ้‡่ผ‰ๅฎŒๆˆ, %d ๅ€‹ skill", len(results))
return results
def invalidate(self, filename: str) -> None:
"""็งป้™คๅ–ฎไธ€ๅฟซๅ–ๆข็›ฎ๏ผˆไธ‹ๆฌก load_skill ๆ™‚้‡ๆ–ฐ่ฎ€ๅ–๏ผ‰"""
with self._lock:
removed = self._cache.pop(filename, None)
if removed:
logger.info("[SkillLoader] ๅฟซๅ–ๅคฑๆ•ˆ: %s", filename)
def invalidate_all(self) -> None:
"""ๆธ…็ฉบๅ…จ้ƒจๅฟซๅ–๏ผˆไธ‹ๆฌก load_skill ๆ™‚้‡ๆ–ฐ่ฎ€ๅ–ๆ‰€ๆœ‰๏ผ‰"""
with self._lock:
count = len(self._cache)
self._cache.clear()
logger.info("[SkillLoader] ๅ…จ้ƒจๅฟซๅ–ๆธ…็ฉบ (%d ๅ€‹)", count)
def get_registry(self) -> dict:
"""
ๅ›žๅ‚ณๆ‰€ๆœ‰ๅทฒๅฟซๅ–็š„ Skill ็‹€ๆ…‹๏ผŒไพ› /api/skills ็ซฏ้ปžไฝฟ็”จใ€‚
Returns:
dict:
{
"skills_dir": str,
"cache_ttl_seconds": float,
"total": int,
"skills": [
{
"filename": str,
"size_bytes": int,
"mtime": float, # -1 = fallback SOP
"load_time": float,
"age_seconds": float,
"is_fallback": bool,
"content_preview": str # ๅ‰ 200 ๅญ—ๅ…ƒ
}
]
}
"""
with self._lock:
now = time.time()
skills_list = []
for filename, entry in self._cache.items():
skills_list.append({
"filename": filename,
"size_bytes": entry.size_bytes,
"mtime": entry.mtime,
"load_time": entry.load_time,
"age_seconds": round(now - entry.load_time, 2),
"is_fallback": entry.mtime == -1.0,
"content_preview": entry.content[:200],
})
return {
"skills_dir": str(self._skills_dir),
"cache_ttl_seconds": CACHE_TTL_SECONDS,
"total": len(skills_list),
"skills": skills_list,
}
def get_skill_content(self, filename: str) -> Optional[str]:
"""
ๅ›žๅ‚ณๅทฒๅฟซๅ–็š„ Skill ๅŽŸๅง‹ๅ…งๅฎน๏ผˆ่‹ฅๅฐšๆœชๅฟซๅ–ๅ‰‡ๅ…ˆ่ผ‰ๅ…ฅ๏ผ‰ใ€‚
ไพ› /api/skills/{name} ็ซฏ้ปžไฝฟ็”จใ€‚
"""
return self.load_skill(filename)
def list_available_skills(self) -> list[str]:
"""
ๆŽƒๆ skills/ ็›ฎ้Œ„๏ผŒๅ›žๅ‚ณๆ‰€ๆœ‰ๅฏ็”จ็š„ .md ๆช”ๆกˆๆธ…ๅ–ฎใ€‚
๏ผˆๅŒ…ๅซๆœชๅฟซๅ–็š„ๆช”ๆกˆ๏ผ‰
"""
try:
return sorted(
f.name for f in self._skills_dir.iterdir()
if f.is_file() and f.suffix == ".md"
)
except OSError as e:
logger.warning("[SkillLoader] ็„กๆณ•ๆŽƒๆ skills/ ็›ฎ้Œ„: %s", e)
return list(_FALLBACK_SOPS.keys())
def get_stats(self) -> dict:
"""
ๅ›žๅ‚ณ SkillLoader ็š„ๆ•ˆ่ƒฝ็ตฑ่จˆใ€‚
"""
with self._lock:
return {
"cached_skills": len(self._cache),
"fallback_count": sum(1 for e in self._cache.values() if e.mtime == -1.0),
"skills_dir": str(self._skills_dir),
"cache_ttl_seconds": CACHE_TTL_SECONDS,
}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅ…จๅŸŸๅ–ฎไพ‹๏ผˆไพ›ๆ‰€ๆœ‰ Agent ไฝฟ็”จ็š„ๅ…ฑไบซๅฏฆไพ‹๏ผ‰
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
skill_loader = SkillLoader()
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ไพฟๅˆฉๅ‡ฝๅผ๏ผˆๅ‘ๅพŒ็›ธๅฎน โ€”โ€” ๅ–ไปฃ agents/ ไธญ็š„ _load_skill()๏ผ‰
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def load_skill(filename: str) -> str:
"""
ๅ…จๅŸŸไพฟๅˆฉๅ‡ฝๅผ๏ผŒ็ญ‰ๅŒๆ–ผ skill_loader.load_skill(filename)ใ€‚
Agent ๅฏ็›ดๆŽฅ from skills.skill_loader import load_skill ไฝฟ็”จใ€‚
"""
return skill_loader.load_skill(filename)
def reload_skill(filename: str) -> str:
"""ๅผทๅˆถ้‡่ผ‰ๅ–ฎไธ€ Skill"""
return skill_loader.reload_skill(filename)
def get_registry() -> dict:
"""ๅ–ๅพ—ๆ‰€ๆœ‰ Skill ็š„ๅฟซๅ–็‹€ๆ…‹"""
return skill_loader.get_registry()