*** id: 781b2404-667c-4502-b394-cf60bf6b07a0 title: Custom Skills sidebar-title: Custom Skills slug: /python/guides/custom max-toc-depth: 3 ---------------- ## Custom Skills 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 ```python ## 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python @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: ```python 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 ```python #!/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: ```python 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()`: 1. The registry scans the directory for valid skill packages 2. Each subdirectory with a `skill.py` is considered a potential skill 3. Skills are validated but not loaded yet (lazy loading) 4. 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:** ```python # 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:** ```bash # 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:** ```python 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: ```python skill_registry.add_skill_directory("/path/to/my_company_skills") ``` **Option 2: Python Package** Package skills for pip installation using entry points: ```python # 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: ```bash 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