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:
74
main.py
74
main.py
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user