Best Practices

View as Markdown

Best Practices

Guidelines and recommendations for building production-quality SignalWire voice AI agents.

Overview

CategoryFocus Area
Prompt DesignEffective prompts and POM structure
Function DesignWell-structured SWAIG functions
Error HandlingGraceful failure and recovery
SecurityAuthentication and data protection
PerformanceOptimization and efficiency
TestingValidation and quality assurance
MonitoringLogging and observability

Prompt Design

Use POM (Prompt Object Model)

Structure prompts with clear sections:

1from signalwire_agents import AgentBase
2
3agent = AgentBase(name="service", route="/service")
4
5## Good: Structured sections
6agent.prompt_add_section("Role", """
7You are a customer service representative for Acme Corp.
8""")
9
10agent.prompt_add_section("Guidelines", body="Follow these rules:", bullets=[
11 "Be professional and courteous",
12 "Verify customer identity before account access",
13 "Never share sensitive information",
14 "Escalate complex issues to human agents"
15])
16
17agent.add_language("English", "en-US", "rime.spore")

Be Specific About Behavior

1## Good: Specific instructions
2agent.prompt_add_section("Response Style", """
3- Keep responses under 3 sentences for simple questions
4- Ask one question at a time
5- Confirm understanding before taking action
6- Use the customer's name when known
7""")

Function Design

Clear Descriptions

1from signalwire_agents import AgentBase
2from signalwire_agents.core.function_result import SwaigFunctionResult
3
4agent = AgentBase(name="accounts", route="/accounts")
5
6## Good: Descriptive with parameter details
7@agent.tool(
8 description="Look up customer account by account number. "
9 "Returns account status, balance, and last activity date."
10)
11def lookup_account(
12 account_number: str # The 8-digit account number
13) -> SwaigFunctionResult:
14 pass

Return Actionable Information

1@agent.tool(description="Check product availability")
2def check_availability(product_id: str) -> SwaigFunctionResult:
3 stock = get_stock(product_id)
4
5 if stock > 10:
6 return SwaigFunctionResult(
7 f"Product {product_id} is in stock with {stock} units available. "
8 "The customer can place an order."
9 )
10 elif stock > 0:
11 return SwaigFunctionResult(
12 f"Product {product_id} has limited stock ({stock} units). "
13 "Suggest ordering soon."
14 )
15 else:
16 return SwaigFunctionResult(
17 f"Product {product_id} is out of stock. "
18 "Expected restock date: next week."
19 )

Error Handling

Graceful Degradation

1@agent.tool(description="Look up order status")
2def order_status(order_id: str) -> SwaigFunctionResult:
3 try:
4 order = fetch_order(order_id)
5 return SwaigFunctionResult(
6 f"Order {order_id}: Status is {order['status']}"
7 )
8 except OrderNotFoundError:
9 return SwaigFunctionResult(
10 f"Order {order_id} was not found. "
11 "Please verify the order number and try again."
12 )
13 except ServiceUnavailableError:
14 return SwaigFunctionResult(
15 "The order system is temporarily unavailable. "
16 "Please try again in a few minutes."
17 )

Security

Use Authentication

1import os
2
3agent = AgentBase(
4 name="secure",
5 route="/secure",
6 basic_auth=(
7 os.environ.get("AGENT_USER", "agent"),
8 os.environ.get("AGENT_PASSWORD")
9 )
10)

Secure Function Flag

The secure=True flag pauses call recording during function execution. This is useful for sensitive operations but does not prevent data from reaching the LLM.

1@agent.tool(
2 description="Collect sensitive information",
3 secure=True # Pauses recording during execution
4)
5def collect_ssn(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
6 # Recording is paused, but LLM still sees the data
7 ssn = args.get("ssn", "")
8 # Process securely...
9 return SwaigFunctionResult("Information received.")

Secure Payment Processing

For payment card collection, never collect card data through SWAIG function parameters. Use the .pay() method instead, which collects card data via IVR and sends it directly to your payment gateway—the LLM never sees the card number, CVV, or expiry.

1@agent.tool(
2 description="Process payment for order",
3 parameters={
4 "type": "object",
5 "properties": {
6 "amount": {"type": "string", "description": "Amount to charge"}
7 },
8 "required": ["amount"]
9 }
10)
11def process_payment(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
12 amount = args.get("amount", "0.00")
13
14 # Card data collected via IVR, sent directly to payment gateway
15 # LLM never sees card number, CVV, or expiry
16 return (
17 SwaigFunctionResult(
18 "I'll collect your payment information now.",
19 post_process=True
20 )
21 .pay(
22 payment_connector_url="https://payments.example.com/charge",
23 charge_amount=amount,
24 input_method="dtmf",
25 security_code=True,
26 postal_code=True
27 )
28 )
ApproachCard Data ExposureUse Case
.pay() methodNever reaches LLMPayment processing (PCI compliant)
secure=TrueLLM sees data, recording pausedNon-payment sensitive data

Environment Variables

VariablePurpose
SWML_BASIC_AUTH_USERBasic auth username
SWML_BASIC_AUTH_PASSWORDBasic auth password (required for production)
SWML_SSL_ENABLEDEnable HTTPS
SWML_SSL_CERT_PATHSSL certificate path
SWML_SSL_KEY_PATHSSL key path

Performance

Use DataMap for Simple API Calls

1from signalwire_agents.core.data_map import DataMap
2
3## Good: DataMap for simple lookups (no webhook roundtrip)
4weather_map = DataMap(
5 name="get_weather",
6 description="Get weather for a city"
7)
8weather_map.add_parameter("city", "string", "City name", required=True)
9weather_map.add_webhook(
10 url="https://api.weather.com/v1/current?q=${enc:args.city}",
11 method="GET",
12 output_map={"response": "Weather: ${response.temp}F, ${response.condition}"}
13)
14agent.add_data_map_tool(weather_map)

Use Fillers for Long Operations

1@agent.tool(
2 description="Search database",
3 fillers=["Searching...", "This may take a moment..."]
4)
5def search_db(query: str) -> SwaigFunctionResult:
6 # Long-running search
7 results = search_database(query)
8 return SwaigFunctionResult(f"Found {len(results)} matching orders.")

Testing

Use swaig-test

$## Validate agent configuration
$swaig-test agent.py --dump-swml
$
$## List available functions
$swaig-test agent.py --list-tools
$
$## Test specific function
$swaig-test agent.py --exec lookup_account --account_number "12345678"

Monitoring

Use Structured Logging

1import structlog
2
3logger = structlog.get_logger()
4
5@agent.tool(description="Process refund")
6def process_refund(order_id: str, amount: float) -> SwaigFunctionResult:
7 logger.info(
8 "refund_requested",
9 order_id=order_id,
10 amount=amount
11 )
12 # Process refund
13 return SwaigFunctionResult(f"Refund of ${amount} processed.")

Production Readiness Checklist

  • Authentication configured (basic_auth or environment variables)
  • SSL/HTTPS enabled for production
  • Sensitive functions marked as secure
  • Error handling in all functions
  • Input validation for user-provided data
  • Logging configured (no sensitive data in logs)
  • All functions tested with swaig-test
  • Edge cases and error scenarios tested
  • Prompts reviewed for clarity and completeness
  • Transfer/escalation paths defined
  • Timeout values appropriate for use case
  • Summary handling for call analytics