SignalWire SWML Service Guide
Introduction
The SWMLService
class provides a foundation for creating and serving SignalWire Markup Language (SWML) documents. It serves as the base class for all SignalWire services, including AI Agents, and handles common tasks such as:
- SWML document creation and manipulation
- Schema validation
- Web service functionality
- Authentication
- Centralized logging
The class is designed to be extended for specific use cases, while providing powerful capabilities out of the box.
Installation
The SWMLService
class is part of the SignalWire AI Agent SDK. Install it using pip:
pip install signalwire-agents
Basic Usage
Here's a simple example of creating an SWML service:
from signalwire_agents.core.swml_service import SWMLService
class SimpleVoiceService(SWMLService):
def __init__(self, host="0.0.0.0", port=3000):
super().__init__(
name="voice-service",
route="/voice",
host=host,
port=port
)
# Build the SWML document
self.build_document()
def build_document(self):
# Reset the document to start fresh
self.reset_document()
# Add answer verb
self.add_answer_verb()
# Add play verb for greeting
self.add_verb("play", {
"url": "say:Hello, thank you for calling our service."
})
# Add hangup verb
self.add_hangup_verb()
# Create and start the service
service = SimpleVoiceService()
service.run()
Centralized Logging System
The SWMLService
class includes a centralized logging system based on structlog
that provides structured, JSON-formatted logs. This logging system is automatically set up when you import the module, so you don't need to configure it in each service or example.
How It Works
- When
swml_service.py
is imported, it configuresstructlog
(if not already configured) - Each
SWMLService
instance gets a logger bound to its service name - All logs include contextual information like service name, timestamp, and log level
- Logs are formatted as JSON for easy parsing and analysis
Using the Logger
Every SWMLService
instance has a log
attribute that can be used for logging:
# Basic logging
self.log.info("service_started")
# Logging with context
self.log.debug("document_created", size=len(document))
# Error logging
try:
# Some operation
pass
except Exception as e:
self.log.error("operation_failed", error=str(e))
Log Levels
The following log levels are available (in increasing order of severity):
debug
: Detailed information for debugginginfo
: General information about operationwarning
: Warning about potential issueserror
: Error information when operations failcritical
: Critical error that might cause the application to terminate
Suppressing Logs
To suppress logs when running a service, you can set the log level:
import logging
logging.getLogger().setLevel(logging.WARNING) # Only show warnings and above
You can also pass suppress_logs=True
when initializing an agent or service:
service = SWMLService(
name="my-service",
suppress_logs=True
)
SWML Document Creation
The SWMLService
class provides methods for creating and manipulating SWML documents.
Document Structure
SWML documents have the following basic structure:
{
"version": "1.0.0",
"sections": {
"main": [
{ "verb1": { /* configuration */ } },
{ "verb2": { /* configuration */ } }
],
"section1": [
{ "verb3": { /* configuration */ } }
]
}
}
Document Methods
reset_document()
: Reset the document to an empty stateadd_verb(verb_name, config)
: Add a verb to the main sectionadd_section(section_name)
: Add a new sectionadd_verb_to_section(section_name, verb_name, config)
: Add a verb to a specific sectionget_document()
: Get the current document as a dictionaryrender_document()
: Get the current document as a JSON string
Common Verb Shortcuts
add_answer_verb(max_duration=None, codecs=None)
: Add an answer verbadd_hangup_verb(reason=None)
: Add a hangup verbadd_ai_verb(prompt_text=None, prompt_pom=None, post_prompt=None, post_prompt_url=None, swaig=None, params=None)
: Add an AI verb
Verb Handling
The SWMLService
class provides validation for SWML verbs using the SignalWire schema.
Verb Validation
When adding a verb, the service validates it against the schema to ensure it has the correct structure and parameters.
# This will validate the configuration against the schema
self.add_verb("play", {
"url": "say:Hello, world!",
"volume": 5
})
# This would fail validation (invalid parameter)
self.add_verb("play", {
"invalid_param": "value"
})
Custom Verb Handlers
You can register custom verb handlers for specialized verb processing:
from signalwire_agents.core.swml_handler import SWMLVerbHandler
class CustomPlayHandler(SWMLVerbHandler):
def __init__(self):
super().__init__("play")
def validate_config(self, config):
# Custom validation logic
return True, []
def build_config(self, **kwargs):
# Custom configuration building
return kwargs
service.register_verb_handler(CustomPlayHandler())
Web Service Features
The SWMLService
class includes built-in web service capabilities for serving SWML documents.
Endpoints
By default, a service provides the following endpoints:
GET /route
: Return the SWML documentPOST /route
: Process request data and return the SWML documentGET /route/
: Same as above but with trailing slashPOST /route/
: Same as above but with trailing slash
Where route
is the route path specified when creating the service.
Authentication
Basic authentication is automatically set up for all endpoints. Credentials are generated if not provided, or can be specified:
service = SWMLService(
name="my-service",
basic_auth=("username", "password")
)
You can also set credentials using environment variables:
SWML_BASIC_AUTH_USER
SWML_BASIC_AUTH_PASSWORD
Dynamic SWML Generation
You can override the on_swml_request
method to customize SWML documents based on request data:
def on_swml_request(self, request_data=None):
if not request_data:
return None
# Customize document based on request_data
self.reset_document()
self.add_answer_verb()
# Add custom verbs based on request_data
if request_data.get("caller_type") == "vip":
self.add_verb("play", {
"url": "say:Welcome VIP caller!"
})
else:
self.add_verb("play", {
"url": "say:Welcome caller!"
})
# Return modifications to the document
# or None to use the document we've built without modifications
return None
Custom Routing Callbacks
The SWMLService
class allows you to register custom routing callbacks that can examine incoming requests and determine where they should be routed.
Registering a Routing Callback
You can use the register_routing_callback
method to register a function that will be called to process requests to a specific path:
def my_routing_callback(request, body):
"""
Process incoming requests and determine routing
Args:
request: FastAPI Request object
body: Parsed JSON body as a dictionary
Returns:
Optional[str]: If a string is returned, the request will be redirected to that URL.
If None is returned, the request will be processed normally.
"""
# Example: Route based on a field in the request body
if "customer_id" in body:
customer_id = body["customer_id"]
return f"/customer/{customer_id}"
# Process request normally
return None
# Register the callback for a specific path
service.register_routing_callback(my_routing_callback, path="/customer")
How Routing Works
- When a request is received at the registered path, the routing callback is executed
- The callback inspects the request and can decide whether to redirect it
- If the callback returns a URL string, the request is redirected with HTTP 307 (temporary redirect)
- If the callback returns
None
, the request is processed normally by theon_request
method
Serving Different Content for Different Paths
You can use the callback_path
parameter passed to on_request
to serve different content for different paths:
def on_request(self, request_data=None, callback_path=None):
"""
Called when SWML is requested
Args:
request_data: Optional dictionary containing the parsed POST body
callback_path: Optional callback path from the request
Returns:
Optional dict to modify/augment the SWML document
"""
# Serve different content based on the callback path
if callback_path == "/customer":
return {
"sections": {
"main": [
{"answer": {}},
{"play": {"url": "say:Welcome to customer service!"}}
]
}
}
elif callback_path == "/product":
return {
"sections": {
"main": [
{"answer": {}},
{"play": {"url": "say:Welcome to product support!"}}
]
}
}
# Default content
return None
Example: Multi-Section Service
Here's an example of a service that uses routing callbacks to handle different types of requests:
from signalwire_agents.core.swml_service import SWMLService
from fastapi import Request
from typing import Dict, Any, Optional
class MultiSectionService(SWMLService):
def __init__(self):
super().__init__(
name="multi-section",
route="/main"
)
# Create the main document
self.reset_document()
self.add_answer_verb()
self.add_verb("play", {"url": "say:Hello from the main service!"})
self.add_verb("hangup", {})
# Register customer and product routes
self.register_customer_route()
self.register_product_route()
def register_customer_route(self):
def customer_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
# Check if we need to route to a specific customer ID
if "customer_id" in body:
customer_id = body["customer_id"]
# In a real implementation, you might redirect to another service
# Here we just log it and process normally
print(f"Processing request for customer ID: {customer_id}")
return None
# Register the callback at the /customer path
self.register_routing_callback(customer_callback, path="/customer")
# Create the customer SWML section
self.add_section("customer_section")
self.add_verb_to_section("customer_section", "answer", {})
self.add_verb_to_section("customer_section", "play",
{"url": "say:Welcome to customer service!"})
self.add_verb_to_section("customer_section", "hangup", {})
def register_product_route(self):
def product_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
# Check if we need to route to a specific product ID
if "product_id" in body:
product_id = body["product_id"]
print(f"Processing request for product ID: {product_id}")
return None
# Register the callback at the /product path
self.register_routing_callback(product_callback, path="/product")
# Create the product SWML section
self.add_section("product_section")
self.add_verb_to_section("product_section", "answer", {})
self.add_verb_to_section("product_section", "play",
{"url": "say:Welcome to product support!"})
self.add_verb_to_section("product_section", "hangup", {})
def on_request(self, request_data=None, callback_path=None):
# Serve different content based on the callback path
if callback_path == "/customer":
return {
"sections": {
"main": self.get_document()["sections"]["customer_section"]
}
}
elif callback_path == "/product":
return {
"sections": {
"main": self.get_document()["sections"]["product_section"]
}
}
return None
In this example:
- The service registers two custom route paths:
/customer
and/product
- Each path has its own callback function to handle routing decisions
- The
on_request
method uses thecallback_path
to determine which content to serve - Different SWML sections are served for different paths
Advanced Usage
Creating a FastAPI Router
You can get a FastAPI router for the service to include in a larger application:
from fastapi import FastAPI
app = FastAPI()
service = SWMLService(name="my-service")
router = service.as_router()
app.include_router(router, prefix="/voice")
Schema Path Customization
You can specify a custom path to the schema file:
service = SWMLService(
name="my-service",
schema_path="/path/to/schema.json"
)
API Reference
Constructor Parameters
name
: Service name/identifier (required)route
: HTTP route path (default: "/")host
: Host to bind to (default: "0.0.0.0")port
: Port to bind to (default: 3000)basic_auth
: Optional tuple of (username, password)schema_path
: Optional path to schema.jsonsuppress_logs
: Whether to suppress structured logs (default: False)
Document Methods
reset_document()
add_verb(verb_name, config)
add_section(section_name)
add_verb_to_section(section_name, verb_name, config)
get_document()
render_document()
Service Methods
as_router()
: Get a FastAPI router for the servicerun()
: Start the servicestop()
: Stop the serviceget_basic_auth_credentials(include_source=False)
: Get the basic auth credentialson_swml_request(request_data=None)
: Called when SWML is requestedregister_routing_callback(callback_fn, path="/sip")
: Register a callback for request routing
Verb Helper Methods
add_answer_verb(max_duration=None, codecs=None)
add_hangup_verb(reason=None)
add_ai_verb(prompt_text=None, prompt_pom=None, post_prompt=None, post_prompt_url=None, swaig=None, params=None)
Examples
Basic Voicemail Service
from signalwire_agents.core.swml_service import SWMLService
class VoicemailService(SWMLService):
def __init__(self, host="0.0.0.0", port=3000):
super().__init__(
name="voicemail",
route="/voicemail",
host=host,
port=port
)
# Build the SWML document
self.build_voicemail_document()
def build_voicemail_document(self):
"""Build the voicemail SWML document"""
# Reset the document
self.reset_document()
# Add answer verb
self.add_answer_verb()
# Add play verb for greeting
self.add_verb("play", {
"url": "say:Hello, you've reached the voicemail service. Please leave a message after the beep."
})
# Play a beep
self.add_verb("play", {
"url": "https://example.com/beep.wav"
})
# Record the message
self.add_verb("record", {
"format": "mp3",
"stereo": False,
"max_length": 120, # 2 minutes max
"terminators": "#"
})
# Thank the caller
self.add_verb("play", {
"url": "say:Thank you for your message. Goodbye!"
})
# Hang up
self.add_hangup_verb()
self.log.debug("voicemail_document_built")
Dynamic Call Routing Service
class CallRouterService(SWMLService):
def on_swml_request(self, request_data=None):
# If there's no request data, use default routing
if not request_data:
self.log.debug("no_request_data_using_default")
return None
# Create a new document
self.reset_document()
self.add_answer_verb()
# Get routing parameters
department = request_data.get("department", "").lower()
# Add play verb for greeting
self.add_verb("play", {
"url": f"say:Thank you for calling our {department} department. Please hold."
})
# Route based on department
phone_numbers = {
"sales": "+15551112222",
"support": "+15553334444",
"billing": "+15555556666"
}
# Get the appropriate number or use default
to_number = phone_numbers.get(department, "+15559990000")
# Connect to the department
self.add_verb("connect", {
"to": to_number,
"timeout": 30,
"answer_on_bridge": True
})
# Add fallback message and hangup
self.add_verb("play", {
"url": "say:We're sorry, but all of our agents are currently busy. Please try again later."
})
self.add_hangup_verb()
return None # Use the document we've built
For more examples, see the examples
directory in the SignalWire AI Agent SDK repository.