Patterns

View as Markdown

Design Patterns

Common architectural patterns and solutions for building SignalWire voice AI agents.

Overview

PatternDescription
Decorator PatternAdd functions with @agent.tool decorator
Class-Based AgentSubclass AgentBase for reusable agents
Multi-Agent RouterRoute calls to specialized agents
State MachineUse contexts for multi-step workflows
DataMap IntegrationServerless API integration
Skill CompositionCombine built-in skills
Dynamic ConfigurationRuntime agent customization

Decorator Pattern

The simplest way to create an agent with functions:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.function_result import SwaigFunctionResult
3
4agent = AgentBase(name="helper", route="/helper")
5agent.prompt_add_section("Role", "You help users with account information.")
6agent.add_language("English", "en-US", "rime.spore")
7
8@agent.tool(description="Look up account by ID")
9def 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")
14def update_status(account_id: str, status: str) -> SwaigFunctionResult:
15 # Update logic here
16 return SwaigFunctionResult(f"Account {account_id} updated to {status}.")
17
18if __name__ == "__main__":
19 agent.run()

Class-Based Agent Pattern

For reusable, shareable agent definitions:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.function_result import SwaigFunctionResult
3
4class 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
28if __name__ == "__main__":
29 agent = SupportAgent()
30 agent.run()

Multi-Agent Router Pattern

Route calls to specialized agents based on intent:

1from signalwire_agents import AgentBase, AgentServer
2from signalwire_agents.core.function_result import SwaigFunctionResult
3
4
5class 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
35if __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:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.contexts import ContextBuilder
3from signalwire_agents.core.function_result import SwaigFunctionResult
4
5
6class 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:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.data_map import DataMap
3
4agent = AgentBase(name="weather", route="/weather")
5agent.prompt_add_section("Role", "You provide weather information.")
6agent.add_language("English", "en-US", "rime.spore")
7
8## Define DataMap tool
9weather_map = DataMap(
10 name="get_weather",
11 description="Get current weather for a city"
12)
13
14weather_map.add_parameter("city", "string", "City name", required=True)
15
16weather_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
27agent.add_data_map_tool(weather_map)
28
29if __name__ == "__main__":
30 agent.run()

Skill Composition Pattern

Combine multiple skills for comprehensive functionality:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.function_result import SwaigFunctionResult
3
4agent = AgentBase(name="assistant", route="/assistant")
5agent.prompt_add_section("Role", """
6You 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""")
12agent.add_language("English", "en-US", "rime.spore")
13
14## Add built-in skills
15agent.add_skill("datetime")
16agent.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")
24def escalate(reason: str) -> SwaigFunctionResult:
25 return (
26 SwaigFunctionResult(f"Escalating: {reason}")
27 .connect("+15551234567", final=True)
28 )
29
30if __name__ == "__main__":
31 agent.run()

Dynamic Configuration Pattern

Configure agents dynamically at runtime:

1from signalwire_agents import AgentBase
2from signalwire_agents.core.function_result import SwaigFunctionResult
3from typing import Dict, Any
4
5
6class 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
32if __name__ == "__main__":
33 agent = DynamicAgent()
34 agent.run()

Pattern Selection Guide

ScenarioRecommended Pattern
Quick prototype or simple agentDecorator Pattern
Reusable agent for sharingClass-Based Agent
Multiple specialized agentsMulti-Agent Router
Step-by-step workflowsState Machine (Contexts)
External API integrationDataMap Integration
Feature-rich agentSkill Composition
Per-call customizationDynamic Configuration

Anti-Patterns to Avoid

Prompt-Driven Logic (Don’t Do This)

1# BAD: Business rules in prompts
2agent.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")
3def 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
2class DoEverythingAgent(AgentBase):
3 # 50+ functions for sales, support, billing, HR...

Split into specialized agents:

1# GOOD: Specialized agents with router
2class SalesAgent(AgentBase): ...
3class SupportAgent(AgentBase): ...
4class 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")
3def 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")
3def 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")
2def 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

1MAX_VERIFICATION_ATTEMPTS = 3
2
3@agent.tool(description="Verify identity")
4def 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

1import logging
2from datetime import datetime
3
4logger = logging.getLogger(__name__)
5
6@agent.tool(description="Process sensitive operation")
7def 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

TopicReference
Examples by featureExamples
Code-driven architectureExamples by Complexity - Expert section
State managementState Management
Multi-agent systemsMulti-Agent Servers