Custom Skills
Custom Skills
Summary: Create your own skills by inheriting from
SkillBase. Custom skills can be reused across agents and shared with others.
Creating custom skills is worthwhile when you have functionality you want to reuse across multiple agents or share with your team. A skill packages a capability—functions, prompts, hints, and configuration—into a single reusable unit.
When to Create a Custom Skill
Create a skill when:
- You'll use the same functionality in multiple agents
- You want to share a capability with your team
- The functionality is complex enough to benefit from encapsulation
- You want version-controlled, tested components
Just use define_tool() when:
- The function is specific to one agent
- You need quick iteration during development
- The logic is simple and unlikely to be reused
Skill Structure
Create a directory with these files:
my_custom_skill/
__init__.py # Empty or exports skill class
skill.py # Skill implementation
requirements.txt # Optional dependencies
What each file does:
| File | Purpose |
|---|---|
__init__.py | Makes the directory a Python package. Can be empty or export the skill class |
skill.py | Contains the skill class that inherits from SkillBase |
requirements.txt | Lists Python packages the skill needs (pip format) |
Basic Custom Skill
## my_custom_skill/skill.py
from typing import List, Dict, Any
from signalwire_agents.core.skill_base import SkillBase
from signalwire_agents.core.function_result import SwaigFunctionResult
class GreetingSkill(SkillBase):
"""A skill that provides personalized greetings"""
# Required class attributes
SKILL_NAME = "greeting"
SKILL_DESCRIPTION = "Provides personalized greetings"
SKILL_VERSION = "1.0.0"
# Optional requirements
REQUIRED_PACKAGES = []
REQUIRED_ENV_VARS = []
def setup(self) -> bool:
"""Initialize the skill. Return True if successful."""
# Get configuration parameter with default
self.greeting_style = self.params.get("style", "friendly")
return True
def register_tools(self) -> None:
"""Register SWAIG tools with the agent."""
self.define_tool(
name="greet_user",
description="Generate a personalized greeting",
parameters={
"name": {
"type": "string",
"description": "Name of the person to greet"
}
},
handler=self.greet_handler
)
def greet_handler(self, args, raw_data):
"""Handle greeting requests."""
name = args.get("name", "friend")
if self.greeting_style == "formal":
greeting = f"Good day, {name}. How may I assist you?"
else:
greeting = f"Hey {name}! Great to hear from you!"
return SwaigFunctionResult(greeting)
Required Class Attributes
| Attribute | Type | Description |
|---|---|---|
SKILL_NAME | str | Unique identifier for the skill |
SKILL_DESCRIPTION | str | Human-readable description |
SKILL_VERSION | str | Semantic version (default: "1.0.0") |
Optional Attributes:
| Attribute | Type | Description |
|---|---|---|
REQUIRED_PACKAGES | List[str] | Python packages needed |
REQUIRED_ENV_VARS | List[str] | Environment variables needed |
SUPPORTS_MULTIPLE | bool | Allow multiple instances |
Required Methods
setup()
Initialize the skill and validate requirements:
def setup(self) -> bool:
"""
Initialize the skill.
Returns:
True if setup successful, False otherwise
"""
# Validate packages are installed
if not self.validate_packages():
return False
# Validate environment variables
if not self.validate_env_vars():
return False
# Initialize from parameters
self.api_url = self.params.get("api_url", "https://api.example.com")
self.timeout = self.params.get("timeout", 30)
# Any other initialization
return True
register_tools()
Register SWAIG functions:
def register_tools(self) -> None:
"""Register all tools this skill provides."""
self.define_tool(
name="my_function",
description="Does something useful",
parameters={
"param1": {
"type": "string",
"description": "First parameter"
},
"param2": {
"type": "integer",
"description": "Second parameter"
}
},
handler=self.my_handler
)
# Register multiple tools if needed
self.define_tool(
name="another_function",
description="Does something else",
parameters={},
handler=self.another_handler
)
Optional Methods
get_hints()
Provide speech recognition hints:
def get_hints(self) -> List[str]:
"""Return words to improve speech recognition."""
return ["greeting", "hello", "hi", "welcome"]
get_prompt_sections()
Add sections to the agent's prompt:
def get_prompt_sections(self) -> List[Dict[str, Any]]:
"""Return prompt sections for the agent."""
return [
{
"title": "Greeting Capability",
"body": "You can greet users by name.",
"bullets": [
"Use greet_user when someone introduces themselves",
"Match the greeting style to the conversation tone"
]
}
]
get_global_data()
Provide data for the agent's global context:
def get_global_data(self) -> Dict[str, Any]:
"""Return data to add to global context."""
return {
"greeting_skill_enabled": True,
"greeting_style": self.greeting_style
}
cleanup()
Release resources when skill is unloaded:
def cleanup(self) -> None:
"""Clean up when skill is removed."""
# Close connections, release resources
if hasattr(self, "connection"):
self.connection.close()
Parameter Schema
Define parameters your skill accepts:
@classmethod
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
"""Define the parameters this skill accepts."""
# Start with base schema
schema = super().get_parameter_schema()
# Add skill-specific parameters
schema.update({
"style": {
"type": "string",
"description": "Greeting style",
"default": "friendly",
"enum": ["friendly", "formal", "casual"],
"required": False
},
"api_key": {
"type": "string",
"description": "API key for external service",
"required": True,
"hidden": True,
"env_var": "MY_SKILL_API_KEY"
}
})
return schema
Multi-Instance Skills
Support multiple instances with different configurations:
class MultiInstanceSkill(SkillBase):
SKILL_NAME = "multi_search"
SKILL_DESCRIPTION = "Searchable with multiple instances"
SKILL_VERSION = "1.0.0"
# Enable multiple instances
SUPPORTS_MULTIPLE_INSTANCES = True
def get_instance_key(self) -> str:
"""Return unique key for this instance."""
tool_name = self.params.get("tool_name", self.SKILL_NAME)
return f"{self.SKILL_NAME}_{tool_name}"
def setup(self) -> bool:
self.tool_name = self.params.get("tool_name", "search")
return True
def register_tools(self) -> None:
# Use custom tool name
self.define_tool(
name=self.tool_name,
description="Search function",
parameters={
"query": {"type": "string", "description": "Search query"}
},
handler=self.search_handler
)
Complete Example
#!/usr/bin/env python3
## product_search_skill.py - Custom skill for product search
from typing import List, Dict, Any
import requests
from signalwire_agents.core.skill_base import SkillBase
from signalwire_agents.core.function_result import SwaigFunctionResult
class ProductSearchSkill(SkillBase):
"""Search product catalog"""
SKILL_NAME = "product_search"
SKILL_DESCRIPTION = "Search and lookup products in catalog"
SKILL_VERSION = "1.0.0"
REQUIRED_PACKAGES = ["requests"]
REQUIRED_ENV_VARS = []
SUPPORTS_MULTIPLE_INSTANCES = False
def setup(self) -> bool:
if not self.validate_packages():
return False
self.api_url = self.params.get("api_url")
self.api_key = self.params.get("api_key")
if not self.api_url or not self.api_key:
self.logger.error("api_url and api_key are required")
return False
return True
def register_tools(self) -> None:
self.define_tool(
name="search_products",
description="Search for products by name or category",
parameters={
"query": {
"type": "string",
"description": "Search term"
},
"category": {
"type": "string",
"description": "Product category filter",
"enum": ["electronics", "clothing", "home", "all"]
}
},
handler=self.search_handler
)
self.define_tool(
name="get_product_details",
description="Get details for a specific product",
parameters={
"product_id": {
"type": "string",
"description": "Product ID"
}
},
handler=self.details_handler
)
def search_handler(self, args, raw_data):
query = args.get("query", "")
category = args.get("category", "all")
try:
response = requests.get(
f"{self.api_url}/search",
params={"q": query, "cat": category},
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=10
)
response.raise_for_status()
data = response.json()
products = data.get("products", [])
if not products:
return SwaigFunctionResult(f"No products found for '{query}'")
result = f"Found {len(products)} products:\n"
for p in products[:5]:
result += f"- {p['name']} (${p['price']})\n"
return SwaigFunctionResult(result)
except Exception as e:
self.logger.error(f"Search failed: {e}")
return SwaigFunctionResult("Product search is temporarily unavailable")
def details_handler(self, args, raw_data):
product_id = args.get("product_id")
try:
response = requests.get(
f"{self.api_url}/products/{product_id}",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=10
)
response.raise_for_status()
product = response.json()
return SwaigFunctionResult(
f"{product['name']}: {product['description']}. "
f"Price: ${product['price']}. In stock: {product['stock']}"
)
except Exception as e:
self.logger.error(f"Details lookup failed: {e}")
return SwaigFunctionResult("Could not retrieve product details")
def get_hints(self) -> List[str]:
return ["product", "search", "find", "lookup", "catalog"]
def get_prompt_sections(self) -> List[Dict[str, Any]]:
return [
{
"title": "Product Search",
"body": "You can search the product catalog.",
"bullets": [
"Use search_products to find products",
"Use get_product_details for specific items"
]
}
]
@classmethod
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
schema = super().get_parameter_schema()
schema.update({
"api_url": {
"type": "string",
"description": "Product catalog API URL",
"required": True
},
"api_key": {
"type": "string",
"description": "API authentication key",
"required": True,
"hidden": True
}
})
return schema
Using Custom Skills
Register the skill directory:
from signalwire_agents.skills.registry import skill_registry
## Add your skills directory
skill_registry.add_skill_directory("/path/to/my_skills")
## Now use in agent
class MyAgent(AgentBase):
def __init__(self):
super().__init__(name="my-agent")
self.add_language("English", "en-US", "rime.spore")
self.add_skill("product_search", {
"api_url": "https://api.mystore.com",
"api_key": "secret"
})
How Skill Registration Works
When you call skill_registry.add_skill_directory():
- The registry scans the directory for valid skill packages
- Each subdirectory with a
skill.pyis considered a potential skill - Skills are validated but not loaded yet (lazy loading)
- When
add_skill()is called, the skill class is instantiated
Registration order matters: If multiple directories contain skills with the same name, the first registered takes precedence.
Testing Custom Skills
Test your skill before using it in production:
1. Test the skill class directly:
# test_my_skill.py
from my_skills.product_search.skill import ProductSearchSkill
# Create a mock agent for testing
class MockAgent:
def define_tool(self, **kwargs):
print(f"Registered tool: {kwargs['name']}")
class log:
@staticmethod
def info(msg): print(f"INFO: {msg}")
@staticmethod
def error(msg): print(f"ERROR: {msg}")
# Test setup
skill = ProductSearchSkill(MockAgent())
skill.params = {"api_url": "http://test", "api_key": "test"}
assert skill.setup() == True
# Test tools register
skill.register_tools()
2. Test with a real agent using swaig-test:
# Create a test agent that uses your skill
swaig-test test_agent.py --dump-swml
# Test a specific function
swaig-test test_agent.py --function search_products --args '{"query": "test"}'
3. Validate skill structure:
from signalwire_agents.skills.registry import skill_registry
# Add and validate your skills
skill_registry.add_skill_directory("/path/to/my_skills")
# Check it loaded
available = skill_registry.list_available_skills()
print(f"Available skills: {available}")
Publishing and Sharing Skills
Option 1: Git Repository
Share your skills via Git:
my_company_skills/
README.md
product_search/
__init__.py
skill.py
crm_integration/
__init__.py
skill.py
requirements.txt
Users clone and register:
skill_registry.add_skill_directory("/path/to/my_company_skills")
Option 2: Python Package
Package skills for pip installation using entry points:
# setup.py or pyproject.toml
setup(
name="my_company_skills",
entry_points={
"signalwire_agents.skills": [
"product_search = my_company_skills.product_search.skill:ProductSearchSkill",
"crm_integration = my_company_skills.crm_integration.skill:CRMSkill",
]
}
)
After pip install, skills are automatically discoverable.
Option 3: Environment Variable
Set SIGNALWIRE_SKILL_PATHS to include your skills directory:
export SIGNALWIRE_SKILL_PATHS="/opt/company_skills:/home/user/my_skills"
Skill Development Best Practices
DO:
- Use descriptive SKILL_NAME and SKILL_DESCRIPTION
- Validate all parameters in setup()
- Return user-friendly error messages
- Log technical errors for debugging
- Include speech hints for better recognition
- Write clear prompt sections explaining usage
- Handle network/API failures gracefully
- Version your skills meaningfully
DON'T:
- Hard-code configuration values
- Expose internal errors to users
- Skip parameter validation
- Forget to handle edge cases
- Make setup() do heavy work (defer to first use)
- Use global state between instances