Bottom Linear Gradient  Lines image

Learn

Resources

Article

18 min

read

Holy Guacamole: A Voice AI Drive-Thru You Can Actually Build (and Scale)

Code-powered AI prompting for more reliable applications

Brian West

Brian West

Director of Support and Head of Developer Experience

In this article

Share

Angular Gradient Image

Build it free.

Create a space and ship your first call flow in minutes.

Subscribe

This article presents Holy Guacamole, an extensible, production-grade reference application showing how to build a reliable voice-AI drive-thru system with SignalWire’s Python Agents SDK, emphasizing backend-controlled state management, real-time menu matching, and deterministic logic instead of prompt-only approaches. It covers core architectural patterns like state machines, business rule enforcement, TF-IDF and alias matching for menus, SWAIG functions for structured operations, and real-time UI updates using Call Fabric and WebRTC.

Live demo: https://holyguacamole.signalwire.me

Source code: GitHub repo

How to build better drive-thru voice AI

Fast food drive-thrus are a tough problem for AI systems. You’re dealing with ambient noise, unpredictable phrasing, and complex menu logic. Yet most implementations rely on simple prompt engineering and think that will be good enough.

Holy Guacamole is SignalWire’s example of how to do drive-thru AI correctly. It’s a developer-first reference application built with SignalWire’s Python Agents SDK. The entire interaction is driven by your application logic, not the LLM. That means deterministic behavior, production-grade control, and a significantly better customer experience.

This demo acts as a blueprint for building scalable, voice-first systems using SignalWire’s Call Fabric architecture the correct way, carrying context, managing state, implementing proper guardrails, and planning ahead for business logic failures.

Core features

  • Voice-first interaction using SignalWire's Agents SDK

  • Real-time frontend order display via WebRTC

  • Intelligent menu matching with TF-IDF, aliases, and fallback algorithms

  • Automatic combo detection handled entirely in code

  • Production-grade protections like rate limiting, max value enforcement, and error handling

The backend handles all state transitions, menu matching, and business logic. The AI agent is simply an interface for natural language understanding and response generation, tightly controlled by application logic.

Code-powered AI prompting

Instead of relying on LLMs to remember instructions, the Holy Guacamole model demonstrates a more effective method of prompting: enforcing rules through structured functions.

Traditional approach (unreliable)

prompt = """You are a drive-thru assistant..."""
prompt = """You are a drive-thru assistant..."""
prompt = """You are a drive-thru assistant..."""
prompt = """You are a drive-thru assistant..."""

This relies on the LLM to behave correctly, remember constraints, and apply logic on its own.

Code-driven approach (reliable)

copy

01

02

03

04

05

06

07

08

09

10

11

12

def add_item(args, raw_data):
    # 1. Code validates limits
    if quantity > MAX_ITEMS_PER_TYPE:
        quantity = MAX_ITEMS_PER_TYPE
        
    # 2. Code detects combos
    combo_suggestion = check_combo_opportunity(items)
    
    # 3. Code calculates prices
    subtotal = sum(item["total"] for item in items)
    
    # 4. Return structured response that guides the LLM
    response = f"Added {quantity} {item_data['name']}"
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"
    
    result = SwaigFunctionResult(response)
    result.swml_user_event({
        "type": "item_added",
        "items": items
    })
    return resultclient = Client("...", "..."
    signalwire_space_url="your
-space.signalwire.com")
# reference a live call by SID and redirect it mid-flight
call = client.calls("call-sid").update(
    url="https://example.com/new-handler")

copy

01

02

03

04

05

06

07

08

09

10

11

12

def add_item(args, raw_data):
    # 1. Code validates limits
    if quantity > MAX_ITEMS_PER_TYPE:
        quantity = MAX_ITEMS_PER_TYPE
        
    # 2. Code detects combos
    combo_suggestion = check_combo_opportunity(items)
    
    # 3. Code calculates prices
    subtotal = sum(item["total"] for item in items)
    
    # 4. Return structured response that guides the LLM
    response = f"Added {quantity} {item_data['name']}"
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"
    
    result = SwaigFunctionResult(response)
    result.swml_user_event({
        "type": "item_added",
        "items": items
    })
    return resultclient = Client("...", "..."
    signalwire_space_url="your
-space.signalwire.com")
# reference a live call by SID and redirect it mid-flight
call = client.calls("call-sid").update(
    url="https://example.com/new-handler")

copy

01

02

03

04

05

06

07

08

09

10

11

12

def add_item(args, raw_data):
    # 1. Code validates limits
    if quantity > MAX_ITEMS_PER_TYPE:
        quantity = MAX_ITEMS_PER_TYPE
        
    # 2. Code detects combos
    combo_suggestion = check_combo_opportunity(items)
    
    # 3. Code calculates prices
    subtotal = sum(item["total"] for item in items)
    
    # 4. Return structured response that guides the LLM
    response = f"Added {quantity} {item_data['name']}"
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"
    
    result = SwaigFunctionResult(response)
    result.swml_user_event({
        "type": "item_added",
        "items": items
    })
    return resultclient = Client("...", "..."
    signalwire_space_url="your
-space.signalwire.com")
# reference a live call by SID and redirect it mid-flight
call = client.calls("call-sid").update(
    url="https://example.com/new-handler")

copy

01

02

03

04

05

06

07

08

09

10

11

12

def add_item(args, raw_data):
    # 1. Code validates limits
    if quantity > MAX_ITEMS_PER_TYPE:
        quantity = MAX_ITEMS_PER_TYPE
        
    # 2. Code detects combos
    combo_suggestion = check_combo_opportunity(items)
    
    # 3. Code calculates prices
    subtotal = sum(item["total"] for item in items)
    
    # 4. Return structured response that guides the LLM
    response = f"Added {quantity} {item_data['name']}"
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"
    
    result = SwaigFunctionResult(response)
    result.swml_user_event({
        "type": "item_added",
        "items": items
    })
    return resultclient = Client("...", "..."
    signalwire_space_url="your
-space.signalwire.com")
# reference a live call by SID and redirect it mid-flight
call = client.calls("call-sid").update(
    url="https://example.com/new-handler")



This guarantees predictable outcomes regardless of LLM behavior.

Project setup

The application is written in Python and built on the SignalWire Agents SDK.

git clone https://github.com/signalwire/sigmond-holyguacamole.git
cd sigmond-holyguacamole
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
git clone https://github.com/signalwire/sigmond-holyguacamole.git
cd sigmond-holyguacamole
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
git clone https://github.com/signalwire/sigmond-holyguacamole.git
cd sigmond-holyguacamole
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
git clone https://github.com/signalwire/sigmond-holyguacamole.git
cd sigmond-holyguacamole
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Start the application locally:

python holy_guacamole.py
python holy_guacamole.py
python holy_guacamole.py
python holy_guacamole.py

Instructions for deploying with Dokku or other production environments are in the repository.

State machine overview

Example: Greeting state

The conversation flows through structured states:

copy

01

02

03

04

05

06

07

08

09

10

11

12

def _configure_states(self):
    # Define conversation contexts
    contexts = self.define_contexts()
    
    default_context = contexts.add_context("default") \
        .add_section("Goal", "Take accurate food orders efficiently")
    
    # Greeting state - Entry point
    default_context.add_step("greeting") \
        .add_section("Current Task", "Welcome the customer and start their order") \
        .add_bullets("Process", [
            "Welcome them warmly to Holy Guacamole!",
            "Ask what they'd like to order"
        ]) \
        .set_functions(["add_item"]) \
        .set_valid_steps(["taking_order"])
    
    # Taking order state - Main interaction
    default_context.add_step("taking_order") \
        .set_functions([
            "add_item", "remove_item", "modify_quantity",
            "review_order", "finalize_order", "upgrade_to_combo"
        ]) \
        .set_valid_steps(["confirming_order"])

copy

01

02

03

04

05

06

07

08

09

10

11

12

def _configure_states(self):
    # Define conversation contexts
    contexts = self.define_contexts()
    
    default_context = contexts.add_context("default") \
        .add_section("Goal", "Take accurate food orders efficiently")
    
    # Greeting state - Entry point
    default_context.add_step("greeting") \
        .add_section("Current Task", "Welcome the customer and start their order") \
        .add_bullets("Process", [
            "Welcome them warmly to Holy Guacamole!",
            "Ask what they'd like to order"
        ]) \
        .set_functions(["add_item"]) \
        .set_valid_steps(["taking_order"])
    
    # Taking order state - Main interaction
    default_context.add_step("taking_order") \
        .set_functions([
            "add_item", "remove_item", "modify_quantity",
            "review_order", "finalize_order", "upgrade_to_combo"
        ]) \
        .set_valid_steps(["confirming_order"])

copy

01

02

03

04

05

06

07

08

09

10

11

12

def _configure_states(self):
    # Define conversation contexts
    contexts = self.define_contexts()
    
    default_context = contexts.add_context("default") \
        .add_section("Goal", "Take accurate food orders efficiently")
    
    # Greeting state - Entry point
    default_context.add_step("greeting") \
        .add_section("Current Task", "Welcome the customer and start their order") \
        .add_bullets("Process", [
            "Welcome them warmly to Holy Guacamole!",
            "Ask what they'd like to order"
        ]) \
        .set_functions(["add_item"]) \
        .set_valid_steps(["taking_order"])
    
    # Taking order state - Main interaction
    default_context.add_step("taking_order") \
        .set_functions([
            "add_item", "remove_item", "modify_quantity",
            "review_order", "finalize_order", "upgrade_to_combo"
        ]) \
        .set_valid_steps(["confirming_order"])

copy

01

02

03

04

05

06

07

08

09

10

11

12

def _configure_states(self):
    # Define conversation contexts
    contexts = self.define_contexts()
    
    default_context = contexts.add_context("default") \
        .add_section("Goal", "Take accurate food orders efficiently")
    
    # Greeting state - Entry point
    default_context.add_step("greeting") \
        .add_section("Current Task", "Welcome the customer and start their order") \
        .add_bullets("Process", [
            "Welcome them warmly to Holy Guacamole!",
            "Ask what they'd like to order"
        ]) \
        .set_functions(["add_item"]) \
        .set_valid_steps(["taking_order"])
    
    # Taking order state - Main interaction
    default_context.add_step("taking_order") \
        .set_functions([
            "add_item", "remove_item", "modify_quantity",
            "review_order", "finalize_order", "upgrade_to_combo"
        ]) \
        .set_valid_steps(["confirming_order"])



Each state includes:

  • Allowed function calls

  • Next valid transitions

  • Defined goals and business rules

Transitions are controlled by the backend, never left to the LLM.

SWAIG functions: Structured interactions

SWAIG functions define all operations the AI agent can invoke. Each is explicitly defined, typed, and safely handled.

Example: add_item

copy

01

02

03

04

05

06

07

08

09

10

11

12

@self.tool(
    name="add_item",
    description="Add an item to the order",
    parameters={
        "type": "object",
        "properties": {
            "item_name": {
                "type": "string",
                "description": "The menu item to add"
            },
            "quantity": {
                "type": "integer",
                "description": "Number of items to add",
                "default": 1
            }
        },
        "required": ["item_name"]
    }
)
def add_item(args, raw_data):
    # 1. Extract order state
    order_state, global_data = get_order_state(raw_data)
    item_name = args["item_name"]
    quantity = args.get("quantity", 1)
    
    # 2. Apply limits
    MAX_ITEMS_PER_TYPE = 20
    MAX_TOTAL_ITEMS = 50
    if quantity > 10:
        quantity = 10  # Limit per add operation
    
    # 3. Find item in menu using fuzzy matching
    sku, item_data, category = find_menu_item(item_name)
    if not sku:
        return SwaigFunctionResult(f"I couldn't find '{item_name}' on our menu")
    
    # 4. Check/update existing items or add new
    existing_item = next((item for item in order_state["items"] 
                          if item["sku"] == sku), None)
    if existing_item:
        existing_item["quantity"] += quantity
        existing_item["total"] = existing_item["quantity"] * existing_item["price"]
    else:
        order_state["items"].append({
            "sku": sku,
            "name": item_data["name"],
            "quantity": quantity,
            "price": item_data["price"],
            "total": quantity * item_data["price"]
        })
    
    # 5. Update totals
    order_state["subtotal"] = sum(item["total"] for item in order_state["items"])
    order_state["tax"] = round(order_state["subtotal"] * 0.10, 2)
    order_state["total"] = order_state["subtotal"] + order_state["tax"]
    order_state["item_count"] = sum(item["quantity"] for item in order_state["items"])
    
    # 6. Check for combo opportunity (AUTOMATIC!)
    combo_suggestion = check_combo_opportunity(order_state["items"])
    
    # 7. Build response
    response = f"I've added {quantity} {item_data['name']}"
    response += f" for ${quantity * item_data['price']:.2f}."
    response += f" Your total is now ${order_state['total']:.2f}."
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"  # Guides LLM to offer upgrade
    
    # 8. Save state and send real-time update
    result = SwaigFunctionResult(response)
    save_order_state(result, order_state, global_data)
    result.swml_user_event({
        "type": "item_added",
        "items": order_state["items"],
        "subtotal": order_state["subtotal"],
        "tax": order_state["tax"],
        "total": order_state["total"]
    })
    
    # 9. Auto-transition from greeting to taking_order
    if global_data.get("current_step") == "greeting":
        result.context = "taking_order"
    
    return result

copy

01

02

03

04

05

06

07

08

09

10

11

12

@self.tool(
    name="add_item",
    description="Add an item to the order",
    parameters={
        "type": "object",
        "properties": {
            "item_name": {
                "type": "string",
                "description": "The menu item to add"
            },
            "quantity": {
                "type": "integer",
                "description": "Number of items to add",
                "default": 1
            }
        },
        "required": ["item_name"]
    }
)
def add_item(args, raw_data):
    # 1. Extract order state
    order_state, global_data = get_order_state(raw_data)
    item_name = args["item_name"]
    quantity = args.get("quantity", 1)
    
    # 2. Apply limits
    MAX_ITEMS_PER_TYPE = 20
    MAX_TOTAL_ITEMS = 50
    if quantity > 10:
        quantity = 10  # Limit per add operation
    
    # 3. Find item in menu using fuzzy matching
    sku, item_data, category = find_menu_item(item_name)
    if not sku:
        return SwaigFunctionResult(f"I couldn't find '{item_name}' on our menu")
    
    # 4. Check/update existing items or add new
    existing_item = next((item for item in order_state["items"] 
                          if item["sku"] == sku), None)
    if existing_item:
        existing_item["quantity"] += quantity
        existing_item["total"] = existing_item["quantity"] * existing_item["price"]
    else:
        order_state["items"].append({
            "sku": sku,
            "name": item_data["name"],
            "quantity": quantity,
            "price": item_data["price"],
            "total": quantity * item_data["price"]
        })
    
    # 5. Update totals
    order_state["subtotal"] = sum(item["total"] for item in order_state["items"])
    order_state["tax"] = round(order_state["subtotal"] * 0.10, 2)
    order_state["total"] = order_state["subtotal"] + order_state["tax"]
    order_state["item_count"] = sum(item["quantity"] for item in order_state["items"])
    
    # 6. Check for combo opportunity (AUTOMATIC!)
    combo_suggestion = check_combo_opportunity(order_state["items"])
    
    # 7. Build response
    response = f"I've added {quantity} {item_data['name']}"
    response += f" for ${quantity * item_data['price']:.2f}."
    response += f" Your total is now ${order_state['total']:.2f}."
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"  # Guides LLM to offer upgrade
    
    # 8. Save state and send real-time update
    result = SwaigFunctionResult(response)
    save_order_state(result, order_state, global_data)
    result.swml_user_event({
        "type": "item_added",
        "items": order_state["items"],
        "subtotal": order_state["subtotal"],
        "tax": order_state["tax"],
        "total": order_state["total"]
    })
    
    # 9. Auto-transition from greeting to taking_order
    if global_data.get("current_step") == "greeting":
        result.context = "taking_order"
    
    return result

copy

01

02

03

04

05

06

07

08

09

10

11

12

@self.tool(
    name="add_item",
    description="Add an item to the order",
    parameters={
        "type": "object",
        "properties": {
            "item_name": {
                "type": "string",
                "description": "The menu item to add"
            },
            "quantity": {
                "type": "integer",
                "description": "Number of items to add",
                "default": 1
            }
        },
        "required": ["item_name"]
    }
)
def add_item(args, raw_data):
    # 1. Extract order state
    order_state, global_data = get_order_state(raw_data)
    item_name = args["item_name"]
    quantity = args.get("quantity", 1)
    
    # 2. Apply limits
    MAX_ITEMS_PER_TYPE = 20
    MAX_TOTAL_ITEMS = 50
    if quantity > 10:
        quantity = 10  # Limit per add operation
    
    # 3. Find item in menu using fuzzy matching
    sku, item_data, category = find_menu_item(item_name)
    if not sku:
        return SwaigFunctionResult(f"I couldn't find '{item_name}' on our menu")
    
    # 4. Check/update existing items or add new
    existing_item = next((item for item in order_state["items"] 
                          if item["sku"] == sku), None)
    if existing_item:
        existing_item["quantity"] += quantity
        existing_item["total"] = existing_item["quantity"] * existing_item["price"]
    else:
        order_state["items"].append({
            "sku": sku,
            "name": item_data["name"],
            "quantity": quantity,
            "price": item_data["price"],
            "total": quantity * item_data["price"]
        })
    
    # 5. Update totals
    order_state["subtotal"] = sum(item["total"] for item in order_state["items"])
    order_state["tax"] = round(order_state["subtotal"] * 0.10, 2)
    order_state["total"] = order_state["subtotal"] + order_state["tax"]
    order_state["item_count"] = sum(item["quantity"] for item in order_state["items"])
    
    # 6. Check for combo opportunity (AUTOMATIC!)
    combo_suggestion = check_combo_opportunity(order_state["items"])
    
    # 7. Build response
    response = f"I've added {quantity} {item_data['name']}"
    response += f" for ${quantity * item_data['price']:.2f}."
    response += f" Your total is now ${order_state['total']:.2f}."
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"  # Guides LLM to offer upgrade
    
    # 8. Save state and send real-time update
    result = SwaigFunctionResult(response)
    save_order_state(result, order_state, global_data)
    result.swml_user_event({
        "type": "item_added",
        "items": order_state["items"],
        "subtotal": order_state["subtotal"],
        "tax": order_state["tax"],
        "total": order_state["total"]
    })
    
    # 9. Auto-transition from greeting to taking_order
    if global_data.get("current_step") == "greeting":
        result.context = "taking_order"
    
    return result

copy

01

02

03

04

05

06

07

08

09

10

11

12

@self.tool(
    name="add_item",
    description="Add an item to the order",
    parameters={
        "type": "object",
        "properties": {
            "item_name": {
                "type": "string",
                "description": "The menu item to add"
            },
            "quantity": {
                "type": "integer",
                "description": "Number of items to add",
                "default": 1
            }
        },
        "required": ["item_name"]
    }
)
def add_item(args, raw_data):
    # 1. Extract order state
    order_state, global_data = get_order_state(raw_data)
    item_name = args["item_name"]
    quantity = args.get("quantity", 1)
    
    # 2. Apply limits
    MAX_ITEMS_PER_TYPE = 20
    MAX_TOTAL_ITEMS = 50
    if quantity > 10:
        quantity = 10  # Limit per add operation
    
    # 3. Find item in menu using fuzzy matching
    sku, item_data, category = find_menu_item(item_name)
    if not sku:
        return SwaigFunctionResult(f"I couldn't find '{item_name}' on our menu")
    
    # 4. Check/update existing items or add new
    existing_item = next((item for item in order_state["items"] 
                          if item["sku"] == sku), None)
    if existing_item:
        existing_item["quantity"] += quantity
        existing_item["total"] = existing_item["quantity"] * existing_item["price"]
    else:
        order_state["items"].append({
            "sku": sku,
            "name": item_data["name"],
            "quantity": quantity,
            "price": item_data["price"],
            "total": quantity * item_data["price"]
        })
    
    # 5. Update totals
    order_state["subtotal"] = sum(item["total"] for item in order_state["items"])
    order_state["tax"] = round(order_state["subtotal"] * 0.10, 2)
    order_state["total"] = order_state["subtotal"] + order_state["tax"]
    order_state["item_count"] = sum(item["quantity"] for item in order_state["items"])
    
    # 6. Check for combo opportunity (AUTOMATIC!)
    combo_suggestion = check_combo_opportunity(order_state["items"])
    
    # 7. Build response
    response = f"I've added {quantity} {item_data['name']}"
    response += f" for ${quantity * item_data['price']:.2f}."
    response += f" Your total is now ${order_state['total']:.2f}."
    if combo_suggestion:
        response += f"\n\n{combo_suggestion}"  # Guides LLM to offer upgrade
    
    # 8. Save state and send real-time update
    result = SwaigFunctionResult(response)
    save_order_state(result, order_state, global_data)
    result.swml_user_event({
        "type": "item_added",
        "items": order_state["items"],
        "subtotal": order_state["subtotal"],
        "tax": order_state["tax"],
        "total": order_state["total"]
    })
    
    # 9. Auto-transition from greeting to taking_order
    if global_data.get("current_step") == "greeting":
        result.context = "taking_order"
    
    return result



These functions handle validation, pricing, real-time UI updates, and business logic, all outside the LLM’s control.

Menu matching logic

The backend uses a multi-algorithm approach to interpret what users want:

  1. TF-IDF vector similarity for fast matching

  2. Alias mapping for exact matches

  3. Fallback string matching when others fail

copy

01

02

03

04

05

06

07

08

09

10

11

12

def find_menu_item(item_name):
    """Multi-algorithm menu matching"""
    item_lower = item_name.lower().strip()
    
    # Algorithm 1: TF-IDF Vector Similarity
    if HAS_SKLEARN and self.vectorizer:
        user_vector = self.vectorizer.transform([item_lower])
        similarities = cosine_similarity(user_vector, self.menu_vectors)[0]
        best_idx = np.argmax(similarities)
        best_score = similarities[best_idx]
        
        if best_score > 0.3:  # Threshold for accepting a match
            sku, item_data, category = self.sku_map[best_idx]
            return sku, item_data, category
    
    # Algorithm 2: Alias Matching
    for sku, aliases in MENU_ALIASES.items():
        if item_lower in [alias.lower() for alias in aliases]:
            for category, items in MENU.items():
                if sku in items:
                    return sku, items[sku], category
    
    # Algorithm 3: Fuzzy String Matching (fallback)
    # Score-based matching with partial word matches...

copy

01

02

03

04

05

06

07

08

09

10

11

12

def find_menu_item(item_name):
    """Multi-algorithm menu matching"""
    item_lower = item_name.lower().strip()
    
    # Algorithm 1: TF-IDF Vector Similarity
    if HAS_SKLEARN and self.vectorizer:
        user_vector = self.vectorizer.transform([item_lower])
        similarities = cosine_similarity(user_vector, self.menu_vectors)[0]
        best_idx = np.argmax(similarities)
        best_score = similarities[best_idx]
        
        if best_score > 0.3:  # Threshold for accepting a match
            sku, item_data, category = self.sku_map[best_idx]
            return sku, item_data, category
    
    # Algorithm 2: Alias Matching
    for sku, aliases in MENU_ALIASES.items():
        if item_lower in [alias.lower() for alias in aliases]:
            for category, items in MENU.items():
                if sku in items:
                    return sku, items[sku], category
    
    # Algorithm 3: Fuzzy String Matching (fallback)
    # Score-based matching with partial word matches...

copy

01

02

03

04

05

06

07

08

09

10

11

12

def find_menu_item(item_name):
    """Multi-algorithm menu matching"""
    item_lower = item_name.lower().strip()
    
    # Algorithm 1: TF-IDF Vector Similarity
    if HAS_SKLEARN and self.vectorizer:
        user_vector = self.vectorizer.transform([item_lower])
        similarities = cosine_similarity(user_vector, self.menu_vectors)[0]
        best_idx = np.argmax(similarities)
        best_score = similarities[best_idx]
        
        if best_score > 0.3:  # Threshold for accepting a match
            sku, item_data, category = self.sku_map[best_idx]
            return sku, item_data, category
    
    # Algorithm 2: Alias Matching
    for sku, aliases in MENU_ALIASES.items():
        if item_lower in [alias.lower() for alias in aliases]:
            for category, items in MENU.items():
                if sku in items:
                    return sku, items[sku], category
    
    # Algorithm 3: Fuzzy String Matching (fallback)
    # Score-based matching with partial word matches...

copy

01

02

03

04

05

06

07

08

09

10

11

12

def find_menu_item(item_name):
    """Multi-algorithm menu matching"""
    item_lower = item_name.lower().strip()
    
    # Algorithm 1: TF-IDF Vector Similarity
    if HAS_SKLEARN and self.vectorizer:
        user_vector = self.vectorizer.transform([item_lower])
        similarities = cosine_similarity(user_vector, self.menu_vectors)[0]
        best_idx = np.argmax(similarities)
        best_score = similarities[best_idx]
        
        if best_score > 0.3:  # Threshold for accepting a match
            sku, item_data, category = self.sku_map[best_idx]
            return sku, item_data, category
    
    # Algorithm 2: Alias Matching
    for sku, aliases in MENU_ALIASES.items():
        if item_lower in [alias.lower() for alias in aliases]:
            for category, items in MENU.items():
                if sku in items:
                    return sku, items[sku], category
    
    # Algorithm 3: Fuzzy String Matching (fallback)
    # Score-based matching with partial word matches...



The combination allows the system to handle natural phrasing without sacrificing accuracy.

Combo detection

No AI prompt is needed to upsell a combo. Instead, detection happens entirely in code:

copy

01

02

03

04

05

06

07

08

09

10

11

12

def check_combo_opportunity(items):
    """Automatic combo detection without LLM awareness"""
    if not items:
        return None
    
    # Count actual quantities of each item type
    taco_count = sum(item["quantity"] for item in items 
                     if "taco" in item["name"].lower())
    burrito_count = sum(item["quantity"] for item in items 
                        if "burrito" in item["name"].lower())
    chips_count = sum(item["quantity"] for item in items 
                      if "chips" in item["name"].lower() 
                      and "salsa" in item["name"].lower())
    drink_count = sum(item["quantity"] for item in items 
                      if "small" in item["name"].lower() 
                      and "drink" in item["name"].lower())
    
    # Check for taco combo (2 tacos + 1 chips + 1 drink)
    if taco_count >= 2 and chips_count >= 1 and drink_count >= 1:
        taco_price = 3.49 * 2
        chips_price = 2.99
        drink_price = 1.99
        current_total = taco_price + chips_price + drink_price  # $11.96
        combo_price = 9.99
        savings = round(current_total - combo_price, 2)  # $1.97
        return (f"💡 Great news! I can upgrade your 2 tacos, chips & salsa, "
               f"and drink to a Taco Combo and save you ${savings:.2f}!")
    
    return None  # No combo opportunity

copy

01

02

03

04

05

06

07

08

09

10

11

12

def check_combo_opportunity(items):
    """Automatic combo detection without LLM awareness"""
    if not items:
        return None
    
    # Count actual quantities of each item type
    taco_count = sum(item["quantity"] for item in items 
                     if "taco" in item["name"].lower())
    burrito_count = sum(item["quantity"] for item in items 
                        if "burrito" in item["name"].lower())
    chips_count = sum(item["quantity"] for item in items 
                      if "chips" in item["name"].lower() 
                      and "salsa" in item["name"].lower())
    drink_count = sum(item["quantity"] for item in items 
                      if "small" in item["name"].lower() 
                      and "drink" in item["name"].lower())
    
    # Check for taco combo (2 tacos + 1 chips + 1 drink)
    if taco_count >= 2 and chips_count >= 1 and drink_count >= 1:
        taco_price = 3.49 * 2
        chips_price = 2.99
        drink_price = 1.99
        current_total = taco_price + chips_price + drink_price  # $11.96
        combo_price = 9.99
        savings = round(current_total - combo_price, 2)  # $1.97
        return (f"💡 Great news! I can upgrade your 2 tacos, chips & salsa, "
               f"and drink to a Taco Combo and save you ${savings:.2f}!")
    
    return None  # No combo opportunity

copy

01

02

03

04

05

06

07

08

09

10

11

12

def check_combo_opportunity(items):
    """Automatic combo detection without LLM awareness"""
    if not items:
        return None
    
    # Count actual quantities of each item type
    taco_count = sum(item["quantity"] for item in items 
                     if "taco" in item["name"].lower())
    burrito_count = sum(item["quantity"] for item in items 
                        if "burrito" in item["name"].lower())
    chips_count = sum(item["quantity"] for item in items 
                      if "chips" in item["name"].lower() 
                      and "salsa" in item["name"].lower())
    drink_count = sum(item["quantity"] for item in items 
                      if "small" in item["name"].lower() 
                      and "drink" in item["name"].lower())
    
    # Check for taco combo (2 tacos + 1 chips + 1 drink)
    if taco_count >= 2 and chips_count >= 1 and drink_count >= 1:
        taco_price = 3.49 * 2
        chips_price = 2.99
        drink_price = 1.99
        current_total = taco_price + chips_price + drink_price  # $11.96
        combo_price = 9.99
        savings = round(current_total - combo_price, 2)  # $1.97
        return (f"💡 Great news! I can upgrade your 2 tacos, chips & salsa, "
               f"and drink to a Taco Combo and save you ${savings:.2f}!")
    
    return None  # No combo opportunity

copy

01

02

03

04

05

06

07

08

09

10

11

12

def check_combo_opportunity(items):
    """Automatic combo detection without LLM awareness"""
    if not items:
        return None
    
    # Count actual quantities of each item type
    taco_count = sum(item["quantity"] for item in items 
                     if "taco" in item["name"].lower())
    burrito_count = sum(item["quantity"] for item in items 
                        if "burrito" in item["name"].lower())
    chips_count = sum(item["quantity"] for item in items 
                      if "chips" in item["name"].lower() 
                      and "salsa" in item["name"].lower())
    drink_count = sum(item["quantity"] for item in items 
                      if "small" in item["name"].lower() 
                      and "drink" in item["name"].lower())
    
    # Check for taco combo (2 tacos + 1 chips + 1 drink)
    if taco_count >= 2 and chips_count >= 1 and drink_count >= 1:
        taco_price = 3.49 * 2
        chips_price = 2.99
        drink_price = 1.99
        current_total = taco_price + chips_price + drink_price  # $11.96
        combo_price = 9.99
        savings = round(current_total - combo_price, 2)  # $1.97
        return (f"💡 Great news! I can upgrade your 2 tacos, chips & salsa, "
               f"and drink to a Taco Combo and save you ${savings:.2f}!")
    
    return None  # No combo opportunity



The function returns a suggestion that the LLM simply relays. No reasoning or memory required.

Real-time event handling frontend integration

The frontend uses SignalWire Call Fabric Browser SDK for media and user event delivery over WebRTC. Events are handled in JavaScript:

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}



This ensures low-latency UI updates and a smooth ordering experience.

Advanced features

  • Order protection: Limits on total quantity, item types, and maximum order value

  • Natural language totals: Converts prices to spoken words (e.g., “thirteen dollars and fifty cents”)

  • Multi-item parsing: Handles complex inputs like “two tacos and three burritos”

  • Error recovery: Offers suggestions or categories if an item isn’t found

  • Reusable state machine structure

  • Real-time visual feedback via swml_user_event()

Real-time UI updates via events

Event flow:

Customer AI Agent Backend Logic swml_user_event() Frontend Event Handler UI Update
Customer AI Agent Backend Logic swml_user_event() Frontend Event Handler UI Update
Customer AI Agent Backend Logic swml_user_event() Frontend Event Handler UI Update
Customer AI Agent Backend Logic swml_user_event() Frontend Event Handler UI Update

Consistent event types like item_added, combo_upgraded, order_completed, etc., ensure frontend sync.

Start building a production-ready, customer-facing voice AI agent

Holy Guacamole is a blueprint for building real-time, voice-controlled AI experiences that are actually maintainable in production. It demonstrates how to:

  • Control the LLM with code

  • Enforce business logic outside the model

  • Use SignalWire’s Programmable Unified Communications platform to power intelligent audio apps

  • Scale deterministic AI logic that works across edge cases

This is production-ready architecture for anyone serious about voice automation.

Ready to build your own? Sign up for a free account, and bring your questions to our community of developers on Discord.

Frequently asked questions

What is Holy Guacamole in the context of voice-AI drive-thru systems?
Holy Guacamole is a developer-first reference application built with the SignalWire Python Agents SDK that demonstrates how to build a scalable, reliable voice-AI drive-thru using backend logic for state and business rules instead of relying on prompts alone.

Why isn’t a prompt-only approach sufficient for drive-thru voice AI?
Prompt-only approaches rely on the model to remember instructions and manage logic, which can lead to unpredictable behavior; backend-controlled logic and state machines provide deterministic outcomes and consistent business rule enforcement. How does the system handle menu matching and business logic?

The system uses a multi-algorithm backend matching strategy — such as TF-IDF vector similarity, alias mapping, and fallback string matching — plus structured state transitions and SWAIG functions to enforce rules and handle complex phrases reliably.

What role do SWAIG functions play in the Holy Guacamole application?
SWAIG functions define structured operations the AI agent can invoke — such as adding items, validating orders, updating UI state, and enforcing limits — executing them server-side instead of having the LLM reason about logic.

How does Holy Guacamole update frontend interfaces in real time?
The application uses Call Fabric Browser SDK and WebRTC events to send structured user events (e.g., item_added, order_completed) that keep the frontend synchronized with backend state in low-latency UI flows.

Top Linear Gradient  Lines image

Frequently asked questions

Frequently asked questions

The questions we hear most, answered.

The questions we hear most, answered.

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}

copy

01

02

03

04

05

06

07

08

09

10

11

12

function handleUserEvent(event) {
    const { type, items, subtotal, tax, total } = event.detail;
    
    switch(type) {
        case 'item_added':
        case 'item_removed':
            updateOrderDisplay(items);
            updateTotals(subtotal, tax, total);
            break;
            
        case 'combo_upgraded':
            showComboAnimation();
            updateOrderDisplay(items);
            break;
            
        case 'order_complete':
            showOrderNumber(event.detail.order_number);
            break;
    }
}


Bottom Linear Gradient  Lines image

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.