Duration 2 hours
Day 3 of 5

Learning Objectives

By the end of this module, students will be able to:

  • Create custom skills using SkillBase
  • Define skill parameters and configuration
  • Package skills for distribution
  • Register skills with agents

Topics

1. SkillBase Architecture (25 min)

Why Custom Skills?

  • Reusability - Use across multiple agents
  • Encapsulation - Bundle related functionality
  • Distribution - Share with team or community
  • Separation - Keep agent code clean

SkillBase Class

from signalwire_agents.skills import SkillBase

class MySkill(SkillBase):
    # Required class attributes
    SKILL_NAME = "my_skill"
    SKILL_DESCRIPTION = "What this skill does"
    SKILL_VERSION = "1.0.0"

    def setup(self, config: dict):
        """Configure the skill with provided parameters."""
        pass

    def register_functions(self, agent):
        """Register functions with the agent."""
        pass

    def get_prompts(self) -> list:
        """Return additional prompts for the agent."""
        return []

    def get_hints(self) -> list:
        """Return speech recognition hints."""
        return []

2. Creating a Basic Skill (30 min)

Example: Greeting Skill

from signalwire_agents.skills import SkillBase
from signalwire_agents import SwaigFunctionResult


class GreetingSkill(SkillBase):
    """Skill for personalized greetings."""

    SKILL_NAME = "greeting"
    SKILL_DESCRIPTION = "Provides personalized greeting functions"
    SKILL_VERSION = "1.0.0"

    def setup(self, config: dict):
        """Store configuration."""
        self.company_name = config.get("company_name", "Our Company")
        self.greeting_style = config.get("style", "formal")

    def register_functions(self, agent):
        """Add greeting function to agent."""

        @agent.tool(description=f"Get a greeting for {self.company_name}")
        def get_greeting(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            name = args.get("name", "")
            if self.greeting_style == "formal":
                msg = f"Good day, {name}. Welcome to {self.company_name}."
            else:
                msg = f"Hey {name}! Welcome to {self.company_name}!"
            return SwaigFunctionResult(msg)

    def get_prompts(self) -> list:
        """Add greeting-related prompts."""
        return [
            {
                "section": "Greeting Style",
                "body": f"Use a {self.greeting_style} greeting style. "
                        f"You represent {self.company_name}."
            }
        ]

Using the Skill

from signalwire_agents import AgentBase

agent = AgentBase(name="my-agent")

# Add skill with configuration
agent.add_skill("greeting", {
    "company_name": "TechCorp",
    "style": "casual"
})

3. Skill with Multiple Functions (25 min)

Example: Customer Lookup Skill

class CustomerSkill(SkillBase):
    """Skill for customer data operations."""

    SKILL_NAME = "customer"
    SKILL_DESCRIPTION = "Customer lookup and management"
    SKILL_VERSION = "1.0.0"

    def setup(self, config: dict):
        self.api_url = config.get("api_url")
        self.api_key = config.get("api_key")

        # Validate required config
        if not self.api_url:
            raise ValueError("customer skill requires api_url")

    def register_functions(self, agent):
        """Register all customer functions."""

        @agent.tool(description="Look up customer by ID")
        def get_customer(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            customer_id = args.get("customer_id", "")
            # Call API
            customer = self._fetch_customer(customer_id)
            if customer:
                return SwaigFunctionResult(
                    f"Customer {customer['name']}, account since {customer['since']}."
                )
            return SwaigFunctionResult("Customer not found.")

        @agent.tool(description="Get customer's recent orders")
        def get_customer_orders(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            customer_id = args.get("customer_id", "")
            orders = self._fetch_orders(customer_id)
            if orders:
                return SwaigFunctionResult(
                    f"Found {len(orders)} orders for this customer."
                )
            return SwaigFunctionResult("No orders found.")

        @agent.tool(description="Update customer phone number")
        def update_customer_phone(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            customer_id = args.get("customer_id", "")
            phone = args.get("phone", "")
            success = self._update_phone(customer_id, phone)
            if success:
                return SwaigFunctionResult("Phone number updated.")
            return SwaigFunctionResult("Could not update phone number.")

    def _fetch_customer(self, customer_id: str) -> dict:
        """Internal: Fetch customer from API."""
        import requests
        response = requests.get(
            f"{self.api_url}/customers/{customer_id}",
            headers={"Authorization": f"Bearer {self.api_key}"}
        )
        return response.json() if response.ok else None

    def _fetch_orders(self, customer_id: str) -> list:
        """Internal: Fetch orders from API."""
        import requests
        response = requests.get(
            f"{self.api_url}/customers/{customer_id}/orders",
            headers={"Authorization": f"Bearer {self.api_key}"}
        )
        return response.json() if response.ok else []

    def _update_phone(self, customer_id: str, phone: str) -> bool:
        """Internal: Update customer phone."""
        import requests
        response = requests.patch(
            f"{self.api_url}/customers/{customer_id}",
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={"phone": phone}
        )
        return response.ok

    def get_hints(self) -> list:
        """Speech recognition hints."""
        return ["customer ID", "account number", "phone number"]

4. Skill Registration (20 min)

Local Registration

For skills in your project:

from signalwire_agents import AgentBase
from my_skills.customer import CustomerSkill

agent = AgentBase(name="support")

# Register the skill class
agent.register_skill_class(CustomerSkill)

# Then use it
agent.add_skill("customer", {
    "api_url": "https://api.example.com",
    "api_key": os.getenv("CUSTOMER_API_KEY")
})

Auto-Discovery

Skills can be discovered via entry points in setup.py:

# setup.py
setup(
    name="my-skills",
    entry_points={
        "signalwire_agents.skills": [
            "customer = my_skills.customer:CustomerSkill",
            "greeting = my_skills.greeting:GreetingSkill",
        ]
    }
)

Then use by name:

agent.add_skill("customer", {...})

5. Error Handling in Skills (20 min)

Configuration Validation

def setup(self, config: dict):
    """Validate and store configuration."""
    self.api_key = config.get("api_key")

    if not self.api_key:
        raise ValueError(
            f"{self.SKILL_NAME} skill requires 'api_key' parameter"
        )

    # Validate API key format
    if not self.api_key.startswith("sk-"):
        raise ValueError("Invalid API key format")

Function Error Handling

def register_functions(self, agent):
    @agent.tool(description="Get data")
    def get_data(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
        id = args.get("id", "")
        try:
            data = self._fetch_data(id)
            return SwaigFunctionResult(f"Found: {data}")
        except ConnectionError:
            return SwaigFunctionResult(
                "I'm having trouble connecting. Please try again."
            )
        except Exception as e:
            # Log internally, don't expose
            self.logger.error(f"get_data failed: {e}")
            return SwaigFunctionResult(
                "Something went wrong. Let me transfer you to support."
            )

Complete Example

Click to reveal complete solution

CRM Integration Skill

#!/usr/bin/env python3
"""CRM integration skill."""

import os
import logging
from signalwire_agents.skills import SkillBase
from signalwire_agents import SwaigFunctionResult

logger = logging.getLogger(__name__)


class CRMSkill(SkillBase):
    """Skill for CRM integration."""

    SKILL_NAME = "crm"
    SKILL_DESCRIPTION = "CRM lookup and customer management"
    SKILL_VERSION = "1.0.0"

    def setup(self, config: dict):
        """Configure CRM connection."""
        self.crm_url = config.get("crm_url")
        self.api_key = config.get("api_key")
        self.timeout = config.get("timeout", 5)

        if not self.crm_url or not self.api_key:
            raise ValueError("CRM skill requires crm_url and api_key")

    def register_functions(self, agent):
        """Register CRM functions."""
        skill = self  # Capture for closures

        @agent.tool(
            description="Look up a contact in the CRM by phone number",
            fillers=["Looking up your account...", "One moment..."]
        )
        def lookup_contact(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            phone = args.get("phone", "")
            contact = skill._api_call(f"/contacts?phone={phone}")
            if contact:
                return SwaigFunctionResult(
                    f"Found {contact['name']}, a {contact['type']} customer "
                    f"since {contact['created']}."
                )
            return SwaigFunctionResult(
                "I don't see that number in our system. "
                "Would you like me to create a new contact?"
            )

        @agent.tool(
            description="Create a new contact in the CRM"
        )
        def create_contact(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            name = args.get("name", "")
            phone = args.get("phone", "")
            email = args.get("email")
            data = {"name": name, "phone": phone}
            if email:
                data["email"] = email

            result = skill._api_call("/contacts", method="POST", data=data)
            if result:
                return SwaigFunctionResult(
                    f"Created contact for {name}. Their ID is {result['id']}."
                )
            return SwaigFunctionResult("Could not create contact.")

        @agent.tool(
            description="Log a note on a contact's account"
        )
        def add_note(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            contact_id = args.get("contact_id", "")
            note = args.get("note", "")
            result = skill._api_call(
                f"/contacts/{contact_id}/notes",
                method="POST",
                data={"note": note}
            )
            if result:
                return SwaigFunctionResult("Note added to the account.")
            return SwaigFunctionResult("Could not add note.")

    def _api_call(self, endpoint: str, method: str = "GET", data: dict = None):
        """Make API call to CRM."""
        import requests

        url = f"{self.crm_url}{endpoint}"
        headers = {"Authorization": f"Bearer {self.api_key}"}

        try:
            if method == "GET":
                response = requests.get(url, headers=headers, timeout=self.timeout)
            elif method == "POST":
                response = requests.post(url, headers=headers, json=data, timeout=self.timeout)
            else:
                return None

            if response.ok:
                return response.json()
        except Exception as e:
            logger.error(f"CRM API error: {e}")

        return None

    def get_prompts(self) -> list:
        """CRM-related prompts."""
        return [
            {
                "section": "CRM Usage",
                "body": "When a customer calls, look up their phone number in the CRM. "
                        "Always log notes about the conversation."
            }
        ]

    def get_hints(self) -> list:
        """Speech hints for CRM terms."""
        return ["CRM", "contact ID", "account number"]

Key Takeaways

  1. SkillBase provides structure - Consistent skill interface
  2. setup() for configuration - Validate and store config
  3. register_functions() for tools - Add functions to agent
  4. Prompts and hints included - Complete capability package
  5. Error handling is critical - Never expose internal errors

Preparation for Lab 2.4

  • Think of a capability to package as a skill
  • Identify configuration parameters needed
  • Plan 2-3 functions for the skill

Lab Preview

In Lab 2.4, you will:

  1. Create a custom skill from scratch
  2. Add multiple functions
  3. Implement configuration validation
  4. Test the skill in an agent

Back to top

SignalWire AI Agents Certification Program