Skip to main content

Overview

Follow these best practices to build robust, maintainable voice agents that perform well in production.

Tool Design

Write Clear Tool Descriptions

# ❌ Bad - vague description
@tool("Get data")
def get_data(self, id: str) -> dict:
    pass

# ✅ Good - clear, specific description
@tool("Get customer information including name, email, and account status")
def get_customer(self, customer_id: str) -> dict:
    pass

Keep Tools Focused

# ❌ Bad - tool does too much
@tool("Get customer and process order and send email")
def do_everything(self, customer_id: str, order_id: str) -> dict:
    pass

# ✅ Good - separate concerns
@tool("Get customer information")
def get_customer(self, customer_id: str) -> dict:
    pass

@tool("Process customer order")
def process_order(self, order_id: str) -> dict:
    pass

@tool("Send email notification")
async def send_email(self, email: str, message: str) -> dict:
    pass

Use Type Hints

# ✅ Type hints help with validation
@tool("Create booking")
def create_booking(
    self,
    customer_id: str,
    date: str,
    guests: int,
    special_requests: str = ""
) -> dict:
    """
    Create a restaurant booking

    Args:
        customer_id: Customer ID
        date: Booking date (YYYY-MM-DD)
        guests: Number of guests (1-20)
        special_requests: Optional special requests

    Returns:
        Booking confirmation with ID
    """
    pass

Error Handling

Handle Errors Gracefully

@tool("Get product")
def get_product(self, product_id: str) -> dict:
    """Always handle errors in tools"""
    try:
        product = database.get_product(product_id)

        if not product:
            return {
                "error": "not_found",
                "message": "Product not found"
            }

        return {"success": True, "product": product}

    except DatabaseError:
        return {
            "error": "service_unavailable",
            "message": "Unable to fetch product. Please try again."
        }

Return Useful Error Messages

# ❌ Bad - technical error message
return {"error": "NullPointerException in line 42"}

# ✅ Good - user-friendly error message
return {
    "error": "product_unavailable",
    "message": "This product is currently unavailable. Would you like to see similar products?"
}

State Management

Clean Up State

def on_conversation_ended(self, conversation_id: str):
    """Always clean up"""
    # Save important data
    state = self.conversations.get(conversation_id)
    if state and state.get("cart"):
        self.save_abandoned_cart(conversation_id, state["cart"])

    # Remove from memory
    self.conversations.pop(conversation_id, None)

Keep State Minimal

# ❌ Bad - storing too much
self.conversations[conv_id] = {
    "all_products": database.get_all_products(),  # Don't cache everything
    "customer_history": get_all_history()         # Too much data
}

# ✅ Good - store only what's needed
self.conversations[conv_id] = {
    "customer_id": customer_id,
    "current_step": "browsing",
    "cart_items": []
}

Performance

Use Async for I/O Operations

# ❌ Bad - blocks event loop
@tool("Send email")
def send_email(self, email: str) -> dict:
    return email_service.send(email)  # Blocking call

# ✅ Good - non-blocking
@tool_async("Send email")
async def send_email(self, email: str) -> dict:
    return await email_service.send_async(email)

Cache Frequently Accessed Data

from functools import lru_cache
from datetime import timedelta

class CachingAgent(ConversimpleAgent):
    @lru_cache(maxsize=100)
    @tool("Get product details")
    def get_product(self, product_id: str) -> dict:
        """Cached product lookup"""
        return database.get_product(product_id)

Set Timeouts

@tool_async("Call external API")
async def call_api(self, endpoint: str) -> dict:
    """API call with timeout"""
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(endpoint, timeout=5) as response:
                return await response.json()
    except asyncio.TimeoutError:
        return {"error": "timeout", "message": "Request timed out"}

Security

Validate Inputs

@tool("Process payment")
def process_payment(self, amount: float, card_number: str) -> dict:
    """Validate inputs before processing"""
    # Validate amount
    if amount <= 0:
        return {"error": "invalid_amount"}

    if amount > 10000:
        return {"error": "amount_too_large"}

    # Validate card format
    if not self.is_valid_card(card_number):
        return {"error": "invalid_card"}

    return payment_service.charge(amount, card_number)

Don’t Log Sensitive Data

# ❌ Bad - logs sensitive data
logger.info(f"Processing payment for card {card_number}")

# ✅ Good - logs safely
logger.info(f"Processing payment for card ending in {card_number[-4:]}")

Logging

Log Important Events

def on_conversation_started(self, conversation_id: str):
    """Log conversation start"""
    logger.info(
        "Conversation started",
        extra={"conversation_id": conversation_id}
    )

@tool("Process order")
def process_order(self, order_id: str) -> dict:
    """Log tool execution"""
    logger.info(f"Processing order {order_id}")

    try:
        result = order_service.process(order_id)
        logger.info(f"Order {order_id} processed successfully")
        return result
    except Exception as e:
        logger.error(f"Order {order_id} failed: {e}")
        raise

Use Appropriate Log Levels

# DEBUG - Detailed information
logger.debug(f"Tool parameters: {params}")

# INFO - General information
logger.info("Conversation started")

# WARNING - Unexpected but handled
logger.warning("Slow API response: 5s")

# ERROR - Error occurred
logger.error("Payment processing failed")

Testing

Test Each Tool

def test_get_customer():
    """Test customer lookup"""
    agent = MyAgent(api_key="test", customer_id="test")

    # Test successful lookup
    result = agent.get_customer("C123")
    assert result["name"] == "John Doe"

    # Test not found
    result = agent.get_customer("INVALID")
    assert "error" in result

Mock External Services

from unittest.mock import patch

def test_send_email():
    """Test email with mocked service"""
    with patch('email_service.send') as mock_send:
        mock_send.return_value = {"message_id": "MSG123"}

        agent = MyAgent(api_key="test", customer_id="test")
        result = agent.send_email("test@example.com", "Test")

        assert result["success"] == True

Configuration

Use Environment Variables

# ✅ Good - use environment variables
API_KEY = os.getenv('CONVERSIMPLE_API_KEY')
DATABASE_URL = os.getenv('DATABASE_URL')

Different Configs Per Environment

# config.py
class Config:
    API_KEY = os.getenv('CONVERSIMPLE_API_KEY')
    LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')

class DevelopmentConfig(Config):
    DEBUG = True
    LOG_LEVEL = 'DEBUG'

class ProductionConfig(Config):
    DEBUG = False
    LOG_LEVEL = 'WARNING'

Monitoring

Track Key Metrics

class MonitoredAgent(ConversimpleAgent):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.metrics = {
            "conversations_started": 0,
            "conversations_ended": 0,
            "tool_calls": 0,
            "errors": 0
        }

    def on_conversation_started(self, conversation_id: str):
        self.metrics["conversations_started"] += 1

    def on_conversation_ended(self, conversation_id: str):
        self.metrics["conversations_ended"] += 1

    def on_error(self, error_type: str, message: str, details: dict):
        self.metrics["errors"] += 1

Checklist

Before deploying to production:
  • All tools have clear descriptions
  • Error handling in all tools
  • Sensitive data not logged
  • Tests written for tools
  • Environment variables configured
  • Logging configured
  • Health checks implemented
  • Monitoring set up

Next Steps