"I know Kung Fu." — Your LLM agent, probably. A dynamic prompt composition system that lets your AI instantly load specialized skills mid-conversation, just like Neo downloading abilities in The Matrix.
You know that scene? Neo sits in a chair, they plug in the neural interface, and suddenly he knows Kung Fu. That's IKKF—except for your language models, and without the whole "machines enslaving humanity" subplot.
Instead of bloating your agent's context window with every possible instruction from day one, ikkf dynamically injects the exact skills your agent needs, precisely when it needs them. Think of it as a specialized neural download for conversational scenarios. Skills load based on:
- Keywords: Spotting the right terminology in user messages
- Semantic vibes: Matching by meaning, not just words
- Tool usage: Activating when your agent needs specific capabilities
- Conversation progression: Leveling up as the chat gets deeper
- Explicit inclusion: Core skills that are always ready to go
uv pip install ikkfIf you're setting up for development (which means you're about to become the One):
uv sync --all-groupsFor model escalation (LLM-based skill selection), set your provider's API key:
# For Anthropic (default)
export ANTHROPIC_API_KEY="your-key-here"
# For Google Gemini
export GEMINI_API_KEY="your-key-here"
# For OpenAI
export OPENAI_API_KEY="your-key-here"
# For other providers, see litellm docsModel escalation is enabled by default. To disable it, see the Model Escalation section.
from ikkf import Skills
# Load your skill set (uses bundled core skills out of the box)
skills = Skills()
# Ask what the user needs
selected = skills.select("What tools do you have?")
# Deploy the right skills for the situation
for skill in selected:
print(f"Skill: {skill.name}")
print(skill.content)from pathlib import Path
from ikkf import Skills, SkillsConfig
# Teach IKKF about your own skill library
config = SkillsConfig(
custom_skills_dirs=[Path("./my_agent/skills")],
)
skills = Skills(config)Want just your custom skills and nothing else? Easy:
from ikkf import Skills, SkillsConfig
config = SkillsConfig(
custom_skills_dirs=[Path("./my_skills")],
)
skills = Skills(config)Ready to expand your agent's abilities? Each skill is a pair of files: YAML for the configuration, Markdown for the actual content. No neural interface required.
id: my_skill
name: My Custom Skill
category: custom # technical, conversation_management, knowledge, tool_guidance, custom
description: Custom skill that does a thing
trigger_type: keyword # keyword, semantic, tool_usage, turn_based, explicit
trigger_config:
keywords:
- "specific term"
- "another term"
priority: 50 # 0-100, higher = loads first
dependencies: [] # Companion skills that load together
incompatible_with: [] # Skills that don't mix well### My Custom Skill
When the user asks about specific terms:
1. First, do this
2. Then, do that
3. Finally, wrap up
Keep responses focused on the key concepts.Your skills need to know when to activate. Here's how to set that up:
Match exact terms. Crude but effective—like recognizing "the One" by his vibes:
trigger_type: keyword
trigger_config:
keywords:
- "pricing"
- "cost"
- "quote"
history_decay: 0.5 # How much to weight older messagesGo by meaning, not just keywords. When the user's saying the same thing but in different words:
trigger_type: semantic
trigger_config:
importance: medium # low, medium, high, critical
reference_phrases:
- "What's your pricing?"
- "How much does it cost?"Automatically activate when your agent might need specific tools. Perfect for tool-aware agents:
trigger_type: tool_usage
trigger_config:
tools:
- get_device_properties
- list_submodules
patterns:
- "\\bstatus\\b"
- "\\bshow me\\b"Load skills based on how far into the conversation you are. Got sophisticated moves? Save them for later in the fight:
trigger_type: turn_based
trigger_config:
min_turn: 3
max_turn: 10Always loaded. These are your essential moves—the ones you need locked and loaded from the start:
trigger_type: explicitWhen embedding-based matching isn't confident enough, IKKF can escalate to an LLM for smarter skill selection. Think of it as calling in expert backup when the situation gets ambiguous.
Escalation automatically triggers when:
- No embedding matches are found
- Low confidence on the best match (below threshold)
- Ambiguous results with multiple close matches
- Very short messages where embeddings are less reliable
When triggered, IKKF sends the candidate skills to an LLM for intelligent reranking based on the user's message.
IKKF uses litellm for provider-agnostic model access. Model names use the format provider/model:
from ikkf import Skills, SkillsConfig
# Anthropic Haiku-4.5 (fast and cost-effective)
config = SkillsConfig(
enable_model_escalation=True, # Enabled by default
escalation_model="anthropic/claude-haiku-4-5-20251001,
escalation_threshold=0.75, # Confidence threshold for triggering
escalation_max_candidates=10, # Max skills to send to model
)
skills = Skills(config)litellm supports 100+ providers. Just use the provider/model format:
# Google Gemini 2.0 Flash (experimental)
config = SkillsConfig(
escalation_model="gemini/gemini-2.0-flash-exp"
)
# Anthropic Claude Sonnet
config = SkillsConfig(
escalation_model="anthropic/claude-3-5-sonnet-20241022"
)
# OpenAI GPT-4o Mini
config = SkillsConfig(
escalation_model="openai/gpt-4o-mini"
)
# Disable escalation entirely
config = SkillsConfig(
enable_model_escalation=False
)Note: Make sure you have the appropriate API keys set in your environment (e.g., GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY). litellm handles provider authentication automatically.
Want to know if model escalation was used? Use select_with_metadata():
from ikkf import Skills
skills = Skills()
# Get selection result with metadata
result = skills.select_with_metadata("What tools do you have?")
# Check if escalation happened
if result.escalated:
print(f"Model escalation triggered: {result.escalation_reason}")
print(f"Selected {len(result.skills)} skills via LLM")
else:
print(f"Selected {len(result.skills)} skills via embeddings")
# Iterate over skills (result acts like a list)
for skill in result:
print(skill.name)The SelectionResult object is iterable (works like a list) but also includes:
escalated: bool— Whether model escalation was triggeredescalation_reason: str | None— Why escalation happened (e.g., "low_confidence:0.62")skills: list[Skill]— The selected skills
The main controls for managing your skill arsenal:
Your central skill hub. Need a specific ability? This is where you ask for it:
class Skills:
def __init__(self, config: SkillsConfig | None = None): ...
def select(
self,
user_message: str,
conversation_history: list[dict] | None = None,
pending_tool_calls: list[str] | None = None,
turn_count: int = 0,
) -> list[Skill]: ...
def select_with_metadata(
self,
user_message: str,
conversation_history: list[dict] | None = None,
pending_tool_calls: list[str] | None = None,
turn_count: int = 0,
) -> SelectionResult: ...
def get_skill(self, skill_id: str) -> Skill | None: ...
def list_skills(self) -> list[Skill]: ...
def list_skills_by_category(self, category: SkillCategory) -> list[Skill]: ...
def add_skills_directory(self, path: Path) -> int: ...
def reload(self) -> None: ...The dials and switches that control how aggressively IKKF swaps out skills. Turn these knobs to fine-tune your agent's behavior:
@dataclass
class SkillsConfig:
# Skill loading
custom_skills_dirs: list[Path] = field(default_factory=list)
# Semantic matching thresholds
skill_base_threshold: float = 0.68 # Base similarity threshold (0.0-1.0)
skill_importance_low_weight: float = 0.9 # Multiplier for low importance
skill_importance_medium_weight: float = 1.0 # Multiplier for medium importance
skill_importance_high_weight: float = 1.1 # Multiplier for high importance
# Model escalation (LLM-based reranking)
enable_model_escalation: bool = True # Enable/disable escalation
escalation_model: str = "anthropic/claude-haiku-4-5-20251001" # litellm model
escalation_threshold: float = 0.75 # Confidence threshold for triggering
escalation_max_candidates: int = 10 # Max skills to send to modelThis project uses uv for dependencies and Task for automation. No red pills necessary—just good tooling.
uv sync --all-groupstask test
# Want coverage stats too?
task test:covKeep the code clean and consistent:
task lint # Static analysis (ruff + mypy)
task format # Auto-format with rufftask check # Runs lint + test togethertask buildMIT