| """Skill Creation System — automatic skill abstraction from conversations. |
| |
| Adapted from inc_llm_v1's SkillManager + SkillFactory pattern. |
| |
| After every conversation (voice or text): |
| 1. Extract the pattern (what was asked → what worked) |
| 2. Create a reusable skill with trigger conditions |
| 3. Store in the skill library for future use |
| |
| Skill types: conversation, code, speed, voice, tool |
| Bayesian effectiveness scoring: skills that work well get higher priority. |
| Meta-skills: after 10+ interactions in a category, summarize best practices. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import logging |
| import time |
| from collections import defaultdict, deque |
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
| import numpy as np |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| @dataclass |
| class Skill: |
| """A reusable skill extracted from conversations.""" |
| id: str |
| name: str |
| description: str |
| content: str |
| category: str |
| trigger_conditions: list[str] = field(default_factory=list) |
| created_at: float = field(default_factory=time.time) |
| last_used: float = field(default_factory=time.time) |
| use_count: int = 0 |
| success_count: int = 0 |
| failure_count: int = 0 |
| effectiveness_score: float = 0.5 |
| confidence: float = 0.0 |
| version: int = 1 |
|
|
|
|
| class SkillFactory: |
| """Creates skills from conversation patterns. |
| |
| Analyzes conversations and extracts reusable patterns. |
| """ |
|
|
| def __init__(self) -> None: |
| self._interaction_buffer: deque[dict[str, Any]] = deque(maxlen=500) |
| self._category_counts: dict[str, int] = defaultdict(int) |
|
|
| def record_interaction( |
| self, |
| user_message: str, |
| assistant_response: str, |
| channel: str = "cli", |
| success: bool = True, |
| ) -> None: |
| """Record a conversation interaction for skill extraction.""" |
| self._interaction_buffer.append({ |
| "user_message": user_message, |
| "assistant_response": assistant_response, |
| "channel": channel, |
| "success": success, |
| "timestamp": time.time(), |
| }) |
|
|
| |
| category = self._categorize(user_message, channel) |
| self._category_counts[category] += 1 |
|
|
| def _categorize(self, message: str, channel: str) -> str: |
| """Categorize an interaction.""" |
| msg_lower = message.lower() |
| if channel == "voice" or channel == "jarvis": |
| return "voice" |
| if any(kw in msg_lower for kw in ["code", "function", "bug", "error", "debug", "python", "javascript"]): |
| return "code" |
| if any(kw in msg_lower for kw in ["tool", "search", "run", "execute", "file"]): |
| return "tool" |
| if len(message) < 20: |
| return "speed" |
| return "conversation" |
|
|
| def extract_skill(self) -> Skill | None: |
| """Extract a skill from recent interactions. |
| |
| Looks for patterns in recent interactions and creates a skill |
| if a clear pattern emerges. |
| """ |
| if len(self._interaction_buffer) < 3: |
| return None |
|
|
| |
| recent = list(self._interaction_buffer)[-20:] |
| categories: dict[str, list[dict[str, Any]]] = defaultdict(list) |
| for interaction in recent: |
| cat = self._categorize(interaction["user_message"], interaction["channel"]) |
| categories[cat].append(interaction) |
|
|
| |
| best_cat = max(categories.items(), key=lambda x: len(x[1])) |
| if len(best_cat[1]) < 3: |
| return None |
|
|
| category, interactions = best_cat |
|
|
| |
| success_rate = sum(1 for i in interactions if i["success"]) / len(interactions) |
| if success_rate < 0.5: |
| return None |
|
|
| |
| skill_id = hashlib.sha256( |
| f"{category}:{time.time()}".encode() |
| ).hexdigest()[:16] |
|
|
| examples = "\n".join( |
| f" Q: {i['user_message'][:80]}\n A: {i['assistant_response'][:80]}" |
| for i in interactions[:5] |
| ) |
|
|
| content = ( |
| f"Skill: {category.title()} Interaction Pattern\n" |
| f"Success rate: {success_rate:.0%}\n" |
| f"Examples:\n{examples}\n" |
| f"Best practice: Be concise and direct for {category} interactions." |
| ) |
|
|
| triggers = self._extract_triggers(interactions, category) |
|
|
| skill = Skill( |
| id=skill_id, |
| name=f"{category}-pattern-{skill_id[:8]}", |
| description=f"Learned {category} interaction pattern ({success_rate:.0%} success)", |
| content=content, |
| category=category, |
| trigger_conditions=triggers, |
| effectiveness_score=success_rate, |
| confidence=min(1.0, len(interactions) / 10.0), |
| ) |
|
|
| return skill |
|
|
| def _extract_triggers(self, interactions: list[dict[str, Any]], category: str) -> list[str]: |
| """Extract trigger conditions from interactions.""" |
| triggers = [category] |
| |
| word_freq: dict[str, int] = defaultdict(int) |
| for i in interactions: |
| for word in i["user_message"].lower().split(): |
| if len(word) > 3: |
| word_freq[word] += 1 |
| |
| top_words = sorted(word_freq.items(), key=lambda x: -x[1])[:5] |
| triggers.extend(w for w, _ in top_words) |
| return triggers |
|
|
| def maybe_create_meta_skill(self, skill_manager: "SkillManager") -> Skill | None: |
| """Create a meta-skill after enough interactions in a category.""" |
| for category, count in self._category_counts.items(): |
| if count >= 10: |
| existing = skill_manager.read(f"{category}-meta") |
| if not existing: |
| meta_id = hashlib.sha256( |
| f"meta:{category}:{time.time()}".encode() |
| ).hexdigest()[:16] |
| skill = Skill( |
| id=meta_id, |
| name=f"{category}-meta", |
| description=f"Meta-skill for {category} — learned patterns across all {category} interactions", |
| content=( |
| f"Meta-Skill: {category.title()}\n" |
| f"Total interactions: {count}\n" |
| f"Best practices:\n" |
| f"- Be concise and direct\n" |
| f"- Match the user's tone\n" |
| f"- Provide actionable responses\n" |
| ), |
| category=f"{category}_meta", |
| trigger_conditions=[category, "meta"], |
| effectiveness_score=0.7, |
| confidence=min(1.0, count / 20.0), |
| ) |
| return skill |
| return None |
|
|
| def get_stats(self) -> dict[str, Any]: |
| return { |
| "interactions_buffered": len(self._interaction_buffer), |
| "category_counts": dict(self._category_counts), |
| } |
|
|
|
|
| class SkillManager: |
| """Manages skills — storage, retrieval, scoring, and lifecycle. |
| |
| Uses Bayesian effectiveness scoring. Skills that work well get |
| higher priority. Skills that fail get deprecated. |
| """ |
|
|
| def __init__(self, storage: Any = None) -> None: |
| self.storage = storage |
| self._skills: dict[str, Skill] = {} |
| self._trigger_index: dict[str, set[str]] = defaultdict(set) |
| self._stats = { |
| "skills_created": 0, |
| "skills_used": 0, |
| "skills_deprecated": 0, |
| "meta_skills_created": 0, |
| } |
|
|
| def create(self, skill: Skill) -> bool: |
| """Create a new skill.""" |
| if skill.id in self._skills: |
| return False |
| self._skills[skill.id] = skill |
| for trigger in skill.trigger_conditions: |
| self._trigger_index[trigger.lower()].add(skill.id) |
| self._stats["skills_created"] += 1 |
| if "meta" in skill.category: |
| self._stats["meta_skills_created"] += 1 |
|
|
| if self.storage: |
| self.storage.save_skill(skill) |
|
|
| logger.info("Created skill: %s (category=%s, score=%.2f)", |
| skill.name, skill.category, skill.effectiveness_score) |
| return True |
|
|
| def read(self, name: str) -> Skill | None: |
| """Read a skill by name.""" |
| for skill in self._skills.values(): |
| if skill.name == name: |
| return skill |
| return None |
|
|
| def find_by_triggers(self, message: str, max_results: int = 3) -> list[Skill]: |
| """Find skills that match trigger conditions in the message.""" |
| msg_lower = message.lower() |
| matched: dict[str, float] = defaultdict(float) |
|
|
| for trigger, skill_ids in self._trigger_index.items(): |
| if trigger in msg_lower: |
| for sid in skill_ids: |
| skill = self._skills.get(sid) |
| if skill: |
| matched[sid] += skill.effectiveness_score * skill.confidence |
|
|
| sorted_ids = sorted(matched.items(), key=lambda x: -x[1])[:max_results] |
| return [self._skills[sid] for sid, _ in sorted_ids if sid in self._skills] |
|
|
| def record_use(self, skill_id: str, success: bool) -> None: |
| """Record a skill use and update effectiveness score.""" |
| skill = self._skills.get(skill_id) |
| if not skill: |
| return |
|
|
| skill.use_count += 1 |
| skill.last_used = time.time() |
| if success: |
| skill.success_count += 1 |
| else: |
| skill.failure_count += 1 |
|
|
| |
| total = skill.success_count + skill.failure_count |
| if total > 0: |
| success_rate = skill.success_count / total |
| |
| w = 2.0 |
| skill.effectiveness_score = (0.5 * w + success_rate * total) / (w + total) |
| skill.confidence = min(1.0, total / 10.0) |
|
|
| |
| if skill.effectiveness_score < 0.2 and skill.use_count > 5: |
| self._stats["skills_deprecated"] += 1 |
| logger.info("Deprecated skill: %s (score=%.2f)", skill.name, skill.effectiveness_score) |
|
|
| self._stats["skills_used"] += 1 |
|
|
| def get_relevant_skills(self, message: str, channel: str = "cli") -> list[Skill]: |
| """Get skills relevant to a message and channel.""" |
| skills = self.find_by_triggers(message, max_results=5) |
| |
| if channel in ("voice", "jarvis"): |
| voice_skills = [s for s in skills if s.category in ("voice", "speed")] |
| if voice_skills: |
| return voice_skills |
| return skills |
|
|
| def get_skill_context(self, message: str, channel: str = "cli") -> str: |
| """Get skill context to inject into the prompt.""" |
| skills = self.get_relevant_skills(message, channel) |
| if not skills: |
| return "" |
| parts = [s.content[:200] for s in skills[:3]] |
| return " | ".join(parts) |
|
|
| def get_stats(self) -> dict[str, Any]: |
| return { |
| **self._stats, |
| "total_skills": len(self._skills), |
| "active_skills": sum(1 for s in self._skills.values() if s.effectiveness_score > 0.2), |
| "trigger_index_size": len(self._trigger_index), |
| } |
|
|