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"):
|
if cmd.startswith("skill"):
|
||||||
return _handle_skill_command(msg.text.strip().lstrip("/"))
|
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
|
# Build context from memory + skills
|
||||||
context = _build_context(msg)
|
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:
|
def _on_scheduled_job(job: ScheduledJob) -> None:
|
||||||
"""
|
"""
|
||||||
Called by the scheduler when a job fires.
|
Called by the scheduler when a job fires.
|
||||||
|
|||||||
Reference in New Issue
Block a user