fix: handle natural language skill creation locally instead of routing to AI

Intercept 'create a skill for X' patterns before they reach the AI runtime.
Creates the SKILL.md with auto-derived triggers from the description,
reloads skills immediately, and confirms to the user  no AI call needed.
This commit is contained in:
2026-02-18 23:44:00 -05:00
parent 4e31e77286
commit e22306e9b8

74
main.py
View File

@@ -248,6 +248,20 @@ def ai_handler(msg: IncomingMessage) -> str:
if cmd.startswith("skill"):
return _handle_skill_command(msg.text.strip().lstrip("/"))
# Detect natural language skill creation requests and handle locally
# instead of routing to the AI (which may fail or be slow).
_SKILL_CREATE_RE = re.compile(
r"(?:create|make|add|build|write)\s+(?:a\s+)?(?:new\s+)?skill\s+(?:for|to|that|about|called|named)\s+(.+)",
re.IGNORECASE,
)
skill_match = _SKILL_CREATE_RE.match(text_lower)
if skill_match:
description = skill_match.group(1).strip().rstrip(".")
# Derive a slug name from the description
name = re.sub(r"[^a-z0-9]+", "-", description.lower()).strip("-")[:40]
if name:
return _create_skill_from_description(name, description)
# Build context from memory + skills
context = _build_context(msg)
@@ -1117,6 +1131,66 @@ def _handle_skill_command(text: str) -> str:
)
def _create_skill_from_description(name: str, description: str) -> str:
"""
Create a skill from a natural language description.
Generates a SKILL.md with sensible triggers derived from the description,
reloads the skills manager, and confirms to the user.
"""
global _skills
cfg = load_config()
workspace = os.path.expanduser(cfg.memory.workspace)
skill_dir = os.path.join(workspace, "skills", name)
skill_path = os.path.join(skill_dir, "SKILL.md")
if os.path.exists(skill_path):
return f"⚠️ Skill `{name}` already exists. Use `skill show {name}` to view it."
# Derive trigger words from the description
stop_words = {
"a", "an", "the", "for", "to", "at", "in", "on", "of", "and",
"or", "is", "it", "my", "me", "do", "get", "how", "what", "when",
"where", "that", "this", "with", "about", "checking", "check",
"current", "currently", "getting", "looking", "find", "finding",
}
words = re.findall(r"[a-z]+", description.lower())
triggers = [w for w in words if w not in stop_words and len(w) > 2]
# Deduplicate while preserving order
seen = set()
triggers = [t for t in triggers if not (t in seen or seen.add(t))]
if not triggers:
triggers = [name]
os.makedirs(skill_dir, exist_ok=True)
template = (
f"---\n"
f"name: {name}\n"
f"description: {description.capitalize()}\n"
f"triggers: [{', '.join(triggers)}]\n"
f"---\n\n"
f"# {name.replace('-', ' ').title()} Skill\n\n"
f"When the user asks about {description}:\n"
f"1. Use web search or available tools to find the information\n"
f"2. Present the results clearly and concisely\n"
f"3. Offer follow-up suggestions if relevant\n"
)
with open(skill_path, "w", encoding="utf-8") as f:
f.write(template)
# Reload so `skill list` picks it up immediately
if _skills:
_skills.reload()
return (
f"✅ Skill `{name}` created!\n"
f"Triggers: {', '.join(triggers)}\n\n"
f"Edit it: `skill show {name}`\n"
f"Or tell me what to change and I'll update it."
)
def _on_scheduled_job(job: ScheduledJob) -> None:
"""
Called by the scheduler when a job fires.