Patterns
Design Patterns
Common architectural patterns and solutions for building SignalWire voice AI agents.
Overview
| Pattern | Description |
|---|---|
| Decorator Pattern | Add functions with @agent.tool decorator |
| Class-Based Agent | Subclass AgentBase for reusable agents |
| Multi-Agent Router | Route calls to specialized agents |
| State Machine | Use contexts for multi-step workflows |
| DataMap Integration | Serverless API integration |
| Skill Composition | Combine built-in skills |
| Dynamic Configuration | Runtime agent customization |
Decorator Pattern
The simplest way to create an agent with functions:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.function_result import SwaigFunctionResult 3 4 agent = AgentBase(name="helper", route="/helper") 5 agent.prompt_add_section("Role", "You help users with account information.") 6 agent.add_language("English", "en-US", "rime.spore") 7 8 @agent.tool(description="Look up account by ID") 9 def lookup_account(account_id: str) -> SwaigFunctionResult: 10 # Lookup logic here 11 return SwaigFunctionResult(f"Account {account_id} found.") 12 13 @agent.tool(description="Update account status") 14 def update_status(account_id: str, status: str) -> SwaigFunctionResult: 15 # Update logic here 16 return SwaigFunctionResult(f"Account {account_id} updated to {status}.") 17 18 if __name__ == "__main__": 19 agent.run()
Class-Based Agent Pattern
For reusable, shareable agent definitions:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.function_result import SwaigFunctionResult 3 4 class SupportAgent(AgentBase): 5 def __init__(self): 6 super().__init__(name="support", route="/support") 7 self.prompt_add_section("Role", "You are a technical support agent.") 8 self.prompt_add_section("Guidelines", """ 9 - Be patient and helpful 10 - Gather issue details before troubleshooting 11 - Escalate complex issues to human support 12 """) 13 self.add_language("English", "en-US", "rime.spore") 14 self.add_skill("datetime") 15 16 @AgentBase.tool(description="Create support ticket") 17 def create_ticket(self, issue: str, priority: str = "normal") -> SwaigFunctionResult: 18 ticket_id = f"TKT-{id(self) % 10000:04d}" 19 return SwaigFunctionResult(f"Created ticket {ticket_id} for: {issue}") 20 21 @AgentBase.tool(description="Transfer to human support") 22 def transfer_to_human(self) -> SwaigFunctionResult: 23 return ( 24 SwaigFunctionResult("Connecting you to a support representative.") 25 .connect("+15551234567", final=True) 26 ) 27 28 if __name__ == "__main__": 29 agent = SupportAgent() 30 agent.run()
Multi-Agent Router Pattern
Route calls to specialized agents based on intent:
1 from signalwire_agents import AgentBase, AgentServer 2 from signalwire_agents.core.function_result import SwaigFunctionResult 3 4 5 class RouterAgent(AgentBase): 6 def __init__(self, base_url: str): 7 super().__init__(name="router", route="/") 8 self.base_url = base_url 9 self.prompt_add_section("Role", """ 10 You are a receptionist. Determine what the caller needs and 11 route them to the appropriate department. 12 """) 13 self.prompt_add_section("Departments", """ 14 - Sales: Product inquiries, pricing, purchases 15 - Support: Technical help, troubleshooting 16 - Billing: Payments, invoices, account issues 17 """) 18 self.add_language("English", "en-US", "rime.spore") 19 20 @AgentBase.tool(description="Transfer to sales department") 21 def transfer_sales(self) -> SwaigFunctionResult: 22 return ( 23 SwaigFunctionResult("Transferring to sales.") 24 .connect(f"{self.base_url}/sales", final=True) 25 ) 26 27 @AgentBase.tool(description="Transfer to support department") 28 def transfer_support(self) -> SwaigFunctionResult: 29 return ( 30 SwaigFunctionResult("Transferring to support.") 31 .connect(f"{self.base_url}/support", final=True) 32 ) 33 34 35 if __name__ == "__main__": 36 server = AgentServer(host="0.0.0.0", port=8080) 37 server.register(RouterAgent("https://agent.example.com")) 38 server.run()
State Machine Pattern (Contexts)
Use contexts for structured multi-step workflows:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.contexts import ContextBuilder 3 from signalwire_agents.core.function_result import SwaigFunctionResult 4 5 6 class VerificationAgent(AgentBase): 7 def __init__(self): 8 super().__init__(name="verify", route="/verify") 9 self.add_language("English", "en-US", "rime.spore") 10 self._setup_contexts() 11 12 def _setup_contexts(self): 13 ctx = ContextBuilder("verification") 14 15 ctx.add_step( 16 "greeting", 17 "Welcome the caller and ask for their account number.", 18 functions=["verify_account"], 19 valid_steps=["collect_info"] 20 ) 21 22 ctx.add_step( 23 "collect_info", 24 "Verify the caller's identity by asking security questions.", 25 functions=["verify_security"], 26 valid_steps=["authenticated", "failed"] 27 ) 28 29 ctx.add_step( 30 "authenticated", 31 "The caller is verified. Ask how you can help them today.", 32 functions=["check_balance", "transfer_funds", "end_call"], 33 valid_steps=["end"] 34 ) 35 36 self.add_context(ctx.build(), default=True) 37 38 @AgentBase.tool(description="Verify account number") 39 def verify_account(self, account_number: str) -> SwaigFunctionResult: 40 return SwaigFunctionResult(f"Account {account_number} found.") 41 42 @AgentBase.tool(description="Check account balance") 43 def check_balance(self, account_id: str) -> SwaigFunctionResult: 44 return SwaigFunctionResult("Current balance is $1,234.56")
DataMap Integration Pattern
Use DataMap for serverless API integration:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.data_map import DataMap 3 4 agent = AgentBase(name="weather", route="/weather") 5 agent.prompt_add_section("Role", "You provide weather information.") 6 agent.add_language("English", "en-US", "rime.spore") 7 8 ## Define DataMap tool 9 weather_map = DataMap( 10 name="get_weather", 11 description="Get current weather for a city" 12 ) 13 14 weather_map.add_parameter("city", "string", "City name", required=True) 15 16 weather_map.add_webhook( 17 url="https://api.weather.com/v1/current?q=${enc:args.city}&key=API_KEY", 18 method="GET", 19 output_map={ 20 "response": "Weather in ${args.city}: ${response.temp}F, ${response.condition}" 21 }, 22 error_map={ 23 "response": "Could not retrieve weather for ${args.city}" 24 } 25 ) 26 27 agent.add_data_map_tool(weather_map) 28 29 if __name__ == "__main__": 30 agent.run()
Skill Composition Pattern
Combine multiple skills for comprehensive functionality:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.function_result import SwaigFunctionResult 3 4 agent = AgentBase(name="assistant", route="/assistant") 5 agent.prompt_add_section("Role", """ 6 You are a comprehensive assistant that can: 7 8 - Tell the current time and date 9 - Search our knowledge base 10 - Look up weather information 11 """) 12 agent.add_language("English", "en-US", "rime.spore") 13 14 ## Add built-in skills 15 agent.add_skill("datetime") 16 agent.add_skill("native_vector_search", { 17 "index_path": "./knowledge.swsearch", 18 "tool_name": "search_docs", 19 "tool_description": "Search documentation" 20 }) 21 22 ## Add custom function alongside skills 23 @agent.tool(description="Escalate to human agent") 24 def escalate(reason: str) -> SwaigFunctionResult: 25 return ( 26 SwaigFunctionResult(f"Escalating: {reason}") 27 .connect("+15551234567", final=True) 28 ) 29 30 if __name__ == "__main__": 31 agent.run()
Dynamic Configuration Pattern
Configure agents dynamically at runtime:
1 from signalwire_agents import AgentBase 2 from signalwire_agents.core.function_result import SwaigFunctionResult 3 from typing import Dict, Any 4 5 6 class DynamicAgent(AgentBase): 7 def __init__(self): 8 super().__init__(name="dynamic", route="/dynamic") 9 self.add_language("English", "en-US", "rime.spore") 10 self.set_dynamic_config_callback(self.configure_from_call) 11 12 def configure_from_call( 13 self, 14 query_params: Dict[str, Any], 15 body_params: Dict[str, Any], 16 headers: Dict[str, str], 17 agent: 'AgentBase' 18 ) -> None: 19 # Get caller's phone number from body 20 caller = body_params.get("call", {}).get("from", "") 21 22 # Customize prompt based on caller 23 if caller.startswith("+1555"): 24 agent.prompt_add_section("Role", "You are a VIP support agent.") 25 else: 26 agent.prompt_add_section("Role", "You are a standard support agent.") 27 28 # Add caller info to global data 29 agent.set_global_data({"caller_number": caller}) 30 31 32 if __name__ == "__main__": 33 agent = DynamicAgent() 34 agent.run()
Pattern Selection Guide
| Scenario | Recommended Pattern |
|---|---|
| Quick prototype or simple agent | Decorator Pattern |
| Reusable agent for sharing | Class-Based Agent |
| Multiple specialized agents | Multi-Agent Router |
| Step-by-step workflows | State Machine (Contexts) |
| External API integration | DataMap Integration |
| Feature-rich agent | Skill Composition |
| Per-call customization | Dynamic Configuration |
Anti-Patterns to Avoid
Prompt-Driven Logic (Don’t Do This)
1 # BAD: Business rules in prompts 2 agent.prompt_add_section("Rules", """ 3 - Maximum order is $500 4 - Apply 10% discount for orders over $100 5 - Don't accept returns after 30 days 6 """)
LLMs may ignore or misapply these rules. Instead, enforce in code:
1 # GOOD: Business rules in code 2 @agent.tool(description="Place an order") 3 def place_order(amount: float) -> SwaigFunctionResult: 4 if amount > 500: 5 return SwaigFunctionResult("Orders are limited to $500.") 6 discount = 0.10 if amount > 100 else 0 7 final = amount * (1 - discount) 8 return SwaigFunctionResult(f"Order total: ${final:.2f}")
Monolithic Agents (Don’t Do This)
1 # BAD: One agent does everything 2 class DoEverythingAgent(AgentBase): 3 # 50+ functions for sales, support, billing, HR...
Split into specialized agents:
1 # GOOD: Specialized agents with router 2 class SalesAgent(AgentBase): ... 3 class SupportAgent(AgentBase): ... 4 class RouterAgent(AgentBase): 5 # Routes to appropriate specialist
Stateless Functions (Don’t Do This)
1 # BAD: No state tracking 2 @agent.tool(description="Add item to cart") 3 def add_to_cart(item: str) -> SwaigFunctionResult: 4 return SwaigFunctionResult(f"Added {item}") 5 # Where does the cart live?
Use global_data for state:
1 # GOOD: State in global_data 2 @agent.tool(description="Add item to cart") 3 def add_to_cart(item: str, args=None, raw_data=None) -> SwaigFunctionResult: 4 cart = raw_data.get("global_data", {}).get("cart", []) 5 cart.append(item) 6 return ( 7 SwaigFunctionResult(f"Added {item}. Cart has {len(cart)} items.") 8 .update_global_data({"cart": cart}) 9 )
Production Patterns
Graceful Error Handling
1 @agent.tool(description="Look up account") 2 def lookup_account(account_id: str) -> SwaigFunctionResult: 3 try: 4 account = database.get(account_id) 5 if not account: 6 return SwaigFunctionResult("I couldn't find that account. Can you verify the number?") 7 return SwaigFunctionResult(f"Account {account_id}: {account['status']}") 8 except DatabaseError: 9 return SwaigFunctionResult("I'm having trouble accessing accounts right now. Let me transfer you to someone who can help.")
Retry with Escalation
1 MAX_VERIFICATION_ATTEMPTS = 3 2 3 @agent.tool(description="Verify identity") 4 def verify_identity(answer: str, args=None, raw_data=None) -> SwaigFunctionResult: 5 attempts = raw_data.get("global_data", {}).get("verify_attempts", 0) + 1 6 7 if verify_answer(answer): 8 return SwaigFunctionResult("Verified!").update_global_data({"verified": True}) 9 10 if attempts >= MAX_VERIFICATION_ATTEMPTS: 11 return ( 12 SwaigFunctionResult("Let me connect you to a representative.") 13 .connect("+15551234567", final=True) 14 ) 15 16 return ( 17 SwaigFunctionResult(f"That doesn't match. You have {MAX_VERIFICATION_ATTEMPTS - attempts} attempts left.") 18 .update_global_data({"verify_attempts": attempts}) 19 )
Audit Trail Pattern
1 import logging 2 from datetime import datetime 3 4 logger = logging.getLogger(__name__) 5 6 @agent.tool(description="Process sensitive operation") 7 def sensitive_operation(account_id: str, action: str, args=None, raw_data=None) -> SwaigFunctionResult: 8 call_id = raw_data.get("call_id", "unknown") 9 caller = raw_data.get("caller_id_number", "unknown") 10 11 # Log for audit 12 logger.info(f"AUDIT: call={call_id} caller={caller} account={account_id} action={action} time={datetime.utcnow().isoformat()}") 13 14 # Process action 15 result = perform_action(account_id, action) 16 17 return SwaigFunctionResult(f"Action completed: {result}")
See Also
| Topic | Reference |
|---|---|
| Examples by feature | Examples |
| Code-driven architecture | Examples by Complexity - Expert section |
| State management | State Management |
| Multi-agent systems | Multi-Agent Servers |