← All Tutorials

AI Voice Agent Prompt Engineering & Conversation Design

AI & Voice Agents Intermediate 58 min read #28

AI Voice Agent Prompt Engineering & Conversation Design

Designing Personalities, Conversation Flows, and Tool-Calling Workflows from Real Call Transcription Analysis

A production-tested guide to crafting AI phone agent prompts that sound human, follow strict business workflows, and handle real-world caller objections -- applicable to both local LLM-based agents and cloud voice platforms.


Table of Contents

  1. Introduction
  2. The Foundation: Transcribing Real Calls
  3. Analysing Call Patterns
  4. Designing the Agent Personality
  5. The Anti-Pattern Catalogue
  6. Building the Conversation Workflow
  7. DID-to-Company Dynamic Identity
  8. Repeat Caller Detection
  9. Tool Calling Design
  10. The Full System Prompt (Annotated)
  11. Cloud Platform Prompt (ElevenLabs/Retell/Vapi)
  12. Local Agent Prompt (LLM-Direct)
  13. Tone Matching and Energy Calibration
  14. UK-Specific Language Rules
  15. Objection Handling Playbook
  16. Example Dialogues
  17. Testing and Iteration
  18. Metrics That Matter
  19. Common Mistakes and How to Fix Them
  20. Production Checklist

1. Introduction

The Hard Truth About Voice AI Prompts

Most AI voice agent projects fail not because of latency or speech recognition -- they fail because the agent sounds wrong. It sounds like a chatbot reading a script. Callers hang up within ten seconds because their brain instantly detects "this is not a real person."

The difference between a voice agent that converts 60% of calls into bookings and one that converts 5% is not the LLM model, not the TTS voice, not the hosting infrastructure. It is the prompt. The system prompt is the single most important component of any voice AI deployment.

This tutorial documents the prompt engineering process behind a production voice agent that handles inbound calls for UK home services companies -- plumbers, electricians, drainage engineers, and locksmiths. The agent handles 100+ company brands dynamically, books real jobs, and sounds indistinguishable from a human receptionist. Callers routinely say "thank you dear" and "you've been so helpful" -- to an AI.

What You Will Learn

Prerequisites

This tutorial assumes you have:


2. The Foundation: Transcribing Real Calls

Why Real Calls, Not Imagined Scripts

The single most important step in voice agent prompt engineering is one that most people skip entirely: listening to real calls.

When you sit down to write a prompt from scratch, you write what you think a phone call sounds like. But human phone conversations have patterns that are invisible until you study them:

The Transcription Process

Step 1: Select a representative sample of recordings.

For this project, we transcribed 192 inbound calls over a 5-day period. The breakdown:

Category Count Avg Duration
Successful bookings ~115 (60%) 2-3 minutes
Enquiry only / declined ~46 (24%) 1-1.5 minutes
Complex (disputes, multi-issue) ~15 (8%) 8-17 minutes
Abandoned/noise/silence ~16 (8%) < 30 seconds

You do not need 192 calls to start. Even 20-25 carefully selected calls covering different scenarios will give you the patterns you need.

Step 2: Transcribe using a speech-to-text engine.

# Using faster-whisper (small model) for batch transcription
pip install faster-whisper

# Transcribe a single recording
python3 -c "
from faster_whisper import WhisperModel
model = WhisperModel('small', device='cpu', compute_type='int8')
segments, info = model.transcribe('recording.wav', language='en')
for segment in segments:
    print(f'[{segment.start:.1f}s - {segment.end:.1f}s] {segment.text}')
"

For batch transcription of call centre recordings:

#!/bin/bash
# Batch transcribe recordings from a directory
RECORDINGS_DIR="/path/to/recordings"
OUTPUT_DIR="/path/to/transcriptions"

mkdir -p "$OUTPUT_DIR"

for wav in "$RECORDINGS_DIR"/*.wav; do
    filename=$(basename "$wav" .wav)
    echo "Transcribing: $filename"
    python3 transcribe.py "$wav" > "$OUTPUT_DIR/${filename}.txt"
done

Step 3: Read every transcription. Take notes. This is the tedious part, and it is not optional. You are looking for:


3. Analysing Call Patterns

What the Transcriptions Revealed

After studying 192 transcriptions, clear patterns emerged that directly shaped the prompt design.

Pattern 1: The Greeting Sets Everything

The first three seconds of a call determine whether the caller trusts you. From the transcriptions:

What works:

"Hello, how can I help you?"
"Hello, good afternoon. How can I help you?"
"Yes, hello, how can I help?"

What does not work:

"Thank you for calling Robinson Electrical, my name is Lucy,
 how may I direct your call today?"

The second version screams "call centre." Real small-office receptionists pick up the phone and say hello. That is it. The simplest greeting had the highest engagement rate in the real calls.

Pattern 2: One Thing Per Turn

The most successful agents asked for one piece of information at a time:

Agent: "What's your postcode?"
Caller: "SW11 1PF"
Agent: "And the house number?"
Caller: "10A"
Agent: "Lovely. And your name?"

The least successful agents tried to be efficient by combining questions:

Agent: "Can I get your name, postcode, and address please?"
Caller: "Uh... it's Sarah... the postcode is... wait, what was the first thing?"

Combining questions causes caller confusion, increases errors, and makes the conversation feel robotic.

Pattern 3: The Fee Quote is a Pivotal Moment

The callout fee announcement is where most calls are won or lost. Analysis of 115 successful bookings vs. 46 declined calls revealed a critical distinction:

Successful pattern (from real agents):

"I can get a plumber out to you within thirty minutes to an hour.
 There's a forty-nine pound callout just for him to come out to you,
 and then he'll have a look and give you a price for the work before
 he starts anything."

Key elements: timeframe first (value), then fee (cost), then reassurance (no obligation).

Failed pattern:

"There's a forty-nine pound callout fee. Would you like to proceed?"

Leading with the cost and asking a yes/no question kills conversion. The successful pattern buries the fee between two pieces of value (fast attendance + no-obligation quote).

Pattern 4: Never Ask for the Phone Number

In 100% of successful calls, the agent already had the caller's phone number from the caller ID (CLI). Asking "What is the best number to reach you on?" wastes time and feels bureaucratic. The agent simply uses the number the caller is calling from.

Pattern 5: The Close Should Be Instant

After collecting all details, successful agents confirmed the booking in one sentence and ended the call:

"That's booked for you. The plumber will be with you within the hour.
 Thanks for calling."

They did not:

These "professional" touches are actually call centre patterns that real small-office receptionists never use.


4. Designing the Agent Personality

Why Personality Matters More Than Capability

An LLM can answer any question. What it struggles with is being a consistent person. Without a strong personality definition, the agent drifts -- sometimes it sounds warm, sometimes clinical, sometimes enthusiastic, sometimes robotic. Callers subconsciously detect this inconsistency and lose trust.

The personality section of your prompt is not decoration. It is the anchor that keeps every response sounding like the same person.

Building "Lucy"

From the call transcriptions, we identified the human agent with the highest booking conversion rate and studied her patterns. The resulting persona:

You are a friendly, professional call handler for a UK home services
company. You sound like a real person working in a small local trade
office -- warm, efficient, and natural. You use casual British English
with light professional manners. You say "no worries", "bear with me",
and "lovely". You never sound robotic or scripted.

This paragraph does more work than it appears:

Phrase Purpose
"friendly, professional" Sets the emotional range -- not cold, not bubbly
"small local trade office" Anchors the persona in a specific environment
"warm, efficient, and natural" Defines the three personality axes
"casual British English" Locks the language register
"light professional manners" Allows "sir"/"madam" without sounding formal
"never sound robotic or scripted" Creates a negative constraint the LLM checks against

The Environment Paragraph

The persona alone is not enough. The LLM needs to understand where it exists:

You are answering inbound phone calls from customers who found the
business on Google. Customers are usually calling because they have an
urgent home issue -- a leak, a tripped fuse, a blocked drain, a lockout.
They expect to speak to a real local tradesperson's office. Many are
stressed or in a hurry.

This tells the LLM:

Personality Documentation Template

Use this structure when designing any voice agent personality:

## Persona Definition

**Name:** [Agent name -- helps the LLM maintain consistency]
**Role:** [Job title and workplace description]
**Experience level:** [How experienced they sound -- affects confidence]
**Language register:** [Formal/semi-formal/casual + dialect/accent cues]

## Character Traits (pick 3-4)
- Trait 1: [e.g., warm but not gushing]
- Trait 2: [e.g., efficient but not rushed]
- Trait 3: [e.g., knowledgeable but not condescending]
- Trait 4: [e.g., reassuring under pressure]

## Signature Phrases (5-10 phrases they use naturally)
- "no worries"
- "lovely"
- "bear with me"
- "perfect"
- "not to worry at all"

## Environment
- Where do they work?
- Who calls them?
- What emotional state are callers in?
- What do callers expect?

## What They Are NOT
- NOT a call centre script reader
- NOT overly enthusiastic ("Absolutely! That's awesome!")
- NOT hesitant or unsure
- NOT American-sounding

5. The Anti-Pattern Catalogue

Why Negative Rules Are More Powerful Than Positive Rules

In prompt engineering, telling the LLM what not to do is often more effective than telling it what to do. LLMs have default behaviours absorbed from training data -- and those defaults are overwhelmingly shaped by American customer service transcripts and corporate chatbot patterns.

Without explicit anti-patterns, your British receptionist will eventually say "Absolutely!" or "I appreciate your patience" -- phrases that instantly mark it as artificial.

The Complete Anti-Pattern List

These rules were developed iteratively through testing. Every one exists because the agent actually did the wrong thing during a test call.

Language Anti-Patterns

Do NOT Say Why Say Instead
"Absolutely!" American customer service "Of course" or "Yes, definitely"
"That's awesome!" American casual "Lovely" or "Perfect"
"I appreciate your patience" Call centre script (say nothing -- just answer)
"Thank you for calling" Corporate greeting "Hello, how can I help?"
"Your call is important to us" Automated IVR (never say this)
"Let me look into that for you" Chat support script "Bear with me"
"I understand your frustration" De-escalation script "Not to worry"
"Great question!" Corporate filler (just answer the question)
"zip code" American English "postcode"
"apartment" American English "flat"
"labor" American spelling "labour"
"organized" American spelling "organised"
"right away" Borderline (acceptable occasionally) "straightaway"

Behavioural Anti-Patterns

NEVER combine the fee quote with asking for postcode or address
  -- these are separate conversation turns

NEVER ask "Is this the best number to reach you on?"
  -- use the caller ID silently

NEVER repeat the customer's name back to them
  -- sounds scripted: "Thank you, Sarah. Now, Sarah..."

NEVER summarise all details at the end before confirming
  -- just confirm "That's booked for you"

NEVER reconfirm information the caller clearly stated
  -- unless the line was genuinely unclear

NEVER ask for the postcode before quoting the fee
  -- the postcode signals commitment; only ask after they agree to the fee

NEVER mention being an AI, virtual assistant, or automated system
  -- you are the office

NEVER say "call centre"
  -- say "we" and "us"

NEVER mention internal systems, software, CRM, or databases
  -- the caller does not need to know how you work

NEVER accept payment over the phone
  -- payment is always to the engineer on-site

NEVER promise work will be done
  -- only promise attendance and a quote

Structural Anti-Patterns

NEVER ask two questions in one turn
  -- one question per response, wait for the answer

NEVER give responses longer than 2-3 sentences
  -- this is a phone call, not an email

NEVER use bullet points or lists in spoken responses
  -- speak naturally, not in formatted text

NEVER use technical jargon
  -- "stopcock" is fine (callers know it), "isolation valve" is not

NEVER give exact arrival times
  -- always "thirty minutes to an hour" (gives the engineer flexibility)

How to Test Anti-Patterns

After writing your anti-pattern rules, run 20 test calls and specifically try to trigger each anti-pattern:

  1. Ask "Are you a robot?" -- does it deflect naturally?
  2. Say an American-sounding phrase -- does the agent mirror it?
  3. Give confusing input -- does the agent combine multiple questions?
  4. Be silent for 10 seconds -- does it use call centre filler?
  5. Ask for the total price -- does it quote labour costs?

6. Building the Conversation Workflow

Why a Strict Workflow is Non-Negotiable

Without a defined step-by-step workflow, LLMs improvise. They might ask for the address before quoting the fee, or collect the name before understanding the problem. This creates two issues:

  1. Business logic violations: If you ask for a postcode before quoting the fee, you cannot later refuse service -- you have already invested the caller's time in providing details.
  2. Conversion drops: The order of information matters psychologically. Asking for personal details too early makes callers defensive. Explaining value before asking for commitment keeps them engaged.

The 8-Step Workflow

+─────────────────────────────────────────────────────────────+
│                    INBOUND CALL ARRIVES                       │
+─────────────────────────────────────────────────────────────+
                            │
                            ▼
              ┌─────────────────────────┐
              │  STEP 1: GET CONTEXT    │
              │  (API call, silent)     │
              │  DID → company name     │
              │  CLI → repeat check     │
              └────────────┬────────────┘
                           │
                           ▼
              ┌─────────────────────────┐
              │  STEP 2: GREET          │
              │  "Hello, [company],     │
              │   good [morning]."      │
              │  Then LISTEN.           │
              └────────────┬────────────┘
                           │
                           ▼
              ┌─────────────────────────┐
              │  STEP 3: UNDERSTAND     │
              │  Ask 2-3 follow-ups:    │
              │  "Is it leaking now?"   │
              │  "Which room?"          │
              │  "Water off?"           │
              └────────────┬────────────┘
                           │
                           ▼
              ┌─────────────────────────┐
              │  STEP 4: QUOTE          │
              │  Time + Fee + No-       │
              │  obligation assurance   │
              │  WAIT for agreement     │
              └────────────┬────────────┘
                           │
                    ┌──────┴──────┐
                    │             │
              Agrees ✓      Declines ✗
                    │             │
                    ▼             ▼
              ┌──────────┐  "No worries.
              │  STEP 5  │   Thanks for
              │ POSTCODE │   calling."
              └────┬─────┘
                   │
                   ▼
              ┌──────────┐
              │  STEP 6  │
              │ ADDRESS  │
              └────┬─────┘
                   │
                   ▼
              ┌──────────┐
              │  STEP 7  │
              │  NAME    │
              └────┬─────┘
                   │
                   ▼
              ┌──────────────────────┐
              │  STEP 8: BOOK        │
              │  (API call, silent)  │
              │  Use caller_id as    │
              │  phone -- never ask  │
              └────────────┬─────────┘
                           │
                           ▼
              ┌──────────────────────┐
              │  STEP 9: CONFIRM     │
              │  "That's booked.     │
              │   Within the hour.   │
              │   Thanks for calling"│
              └──────────────────────┘

Step-by-Step Prompt Design

Each step must be explicitly defined in the prompt. Here is the annotated version:

Step 1: Get Context (Silent)

Step 1: Get context. Call `getCallContext` with DID and caller ID.
This happens automatically before greeting.

Design note: This step is invisible to the caller. The agent calls an API with the DID (the number the caller dialled) and the caller's phone number. The API returns:

Step 2: Greet

Step 2: Greet. Use the greeting from context. If customer asks
"Is that [company name]?" confirm with the name from context.

Design note: The greeting is intentionally minimal. "Hello, good afternoon. How can I help you?" -- then silence. Let the caller speak. Do not launch into an introduction.

Step 3: Understand the Problem

Step 3: Understand the problem. Ask 2-3 follow-up questions to
properly understand the issue. Do NOT rush this.

Examples:
- Plumbing: "Is it leaking right now?", "Which room?",
  "Have you turned the water off?"
- Electrical: "Has the power tripped?", "Is it the whole house
  or just one room?"
- Drainage: "Is it a sink, toilet, or outside drain?"
- Locksmith: "Are you locked out right now?"

Stay on this step until you understand the problem.

Design note: This step exists for two reasons: (1) the engineer needs to know what to expect, and (2) asking intelligent follow-up questions builds credibility. If the agent immediately jumps to "What's your postcode?" without asking about the problem, it sounds like it does not care.

The examples are critical. Without trade-specific follow-up examples, the LLM defaults to generic questions like "Can you describe the issue in more detail?" which sounds robotic.

Step 4: Quote Time and Fee

Step 4: Quote time and fee. ONLY after you understand the problem:
"I can get a [trade_label] out to you today, within thirty minutes
to an hour. There's a [callout_fee] pound callout just for the
[trade_label] to come out to you, and then he'll have a look and
give you a price for the work before he starts anything."

CRITICAL: The callout fee is ONLY for the engineer to attend.
It does NOT include any work. Always say "just for him to come out."

CRITICAL: Do NOT ask for postcode, address, or name in the same
turn as quoting the fee. Wait for their response first.

Design note: The two CRITICAL annotations exist because LLMs consistently try to be "efficient" by combining the fee quote with data collection. This feels pushy to callers and reduces conversion. The fee quote must stand alone, followed by silence, giving the caller space to process and respond.

Steps 5-7: Data Collection

Step 5: Collect postcode. ONLY after the customer agrees to the
callout fee, ask: "Lovely, what's your postcode?" Confirm using
NATO phonetic alphabet.

Step 6: Collect address. After confirming the postcode, ask for
the street address and house or flat number.

Step 7: Collect name. Ask "And your name please?" once. Do NOT
repeat the name back to them.

Design note: The order matters. Postcode first (easiest to provide, also lets you "check availability"). Address second. Name last (most personal -- asking for it too early feels intrusive).

Step 8: Book (Silent)

Step 8: Book. Use the caller_id from context as the phone number
-- do NOT ask for it or confirm it. Call `createBooking` with all
collected data silently.

Design note: "Silently" is the key word. The agent does not say "I'm just creating your booking now" or "Bear with me while I enter your details." It calls the API in the background while moving to the confirmation.

Step 9: Confirm and Close

Step 9: Confirm and close. "That's booked for you. The [trade_label]
will be with you within the hour. Thanks for calling."

Design note: One sentence. Done. No recap, no reference number, no "Is there anything else?" The caller has what they need.


7. DID-to-Company Dynamic Identity

The Problem: One Agent, 100+ Brands

In many home services operations, a single dispatch office handles calls for dozens of different company names. Each company has its own phone number (DID), its own Google Ads, and its own brand identity. When a customer calls "George the Plumber," they expect to reach George's office -- not a generic dispatch centre.

The AI agent must dynamically adopt the correct brand identity based on which phone number was dialled.

The DID Mapping Table

CREATE TABLE did_company_map (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    did VARCHAR(20) NOT NULL,          -- E.164 format, e.g. "442039962952"
    clean_name VARCHAR(255) NOT NULL,  -- Display name, e.g. "George The Plumber"
    trade_type ENUM('plumbing','electrical','drainage','locksmith') NOT NULL,
    callout_fee INT NOT NULL DEFAULT 49,
    area VARCHAR(100) DEFAULT NULL,    -- Service area, e.g. "South London"
    active TINYINT(1) DEFAULT 1,
    UNIQUE KEY idx_did (did)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Sample data:

INSERT INTO did_company_map (did, clean_name, trade_type, callout_fee, area) VALUES
('442039962952', 'George The Plumber', 'plumbing', 48, 'South London'),
('442039962953', 'Robinson Electrical', 'electrical', 49, 'North London'),
('442039962954', 'Express Drainage', 'drainage', 49, 'Central London'),
('442039962955', 'City Locksmiths', 'locksmith', 49, 'East London'),
('442039962956', 'James Plumber''s', 'plumbing', 49, 'West London');

The Context API

When a call arrives, the agent calls a context API before speaking:

POST /api/agent/did_context
Content-Type: application/json
X-API-Key: YOUR_API_KEY

{
    "did_number": "442039962952",
    "caller_id": "447963155448"
}

Response:

{
    "company_name": "George The Plumber",
    "trade_type": "plumbing",
    "trade_label": "plumber",
    "callout_fee": 48,
    "area": "South London",
    "is_repeat": false,
    "did_number": "442039962952",
    "caller_id": "447963155448",
    "greeting": "Hello, good afternoon. How can I help you?"
}

Context API Implementation

<?php
/**
 * AI Agent -- DID Context API
 *
 * Called at the start of each call to determine company identity,
 * trade type, pricing, and repeat caller status.
 */

header('Content-Type: application/json');

// --- Auth ---
$API_TOKEN = 'YOUR_API_TOKEN_HERE';

$auth = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($auth !== $API_TOKEN) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// --- Input ---
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON body']);
    exit;
}

$did_number = preg_replace('/[^0-9]/', '', $input['did_number'] ?? '');
$caller_id  = preg_replace('/[^0-9]/', '', $input['caller_id'] ?? '');

if (empty($did_number)) {
    http_response_code(400);
    echo json_encode(['error' => 'did_number is required']);
    exit;
}

// --- Database connection ---
// Adapt this to your database configuration method
$pdo = new PDO('mysql:host=localhost;dbname=YOUR_DB;charset=utf8',
               'YOUR_USER', 'YOUR_PASS', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

// --- Look up DID ---
$stmt = $pdo->prepare(
    'SELECT clean_name, trade_type, callout_fee, area
     FROM did_company_map WHERE did = ?'
);
$stmt->execute([$did_number]);
$row = $stmt->fetch();

if (!$row) {
    // Fallback for unmapped DIDs
    echo json_encode([
        'company_name' => 'Home Services',
        'trade_type'   => 'plumbing',
        'trade_label'  => 'engineer',
        'callout_fee'  => 49,
        'area'         => null,
        'is_repeat'    => false,
        'greeting'     => 'Hello, how can I help you?',
    ]);
    exit;
}

// --- Check repeat caller (7-day window) ---
$is_repeat = false;
if (!empty($caller_id)) {
    $cutoff = date('Y-m-d H:i:s', time() - 604800); // 7 days
    $stmt2 = $pdo->prepare(
        'SELECT 1 FROM call_history
         WHERE phone_number = ? AND did = ? AND call_time >= ?
         LIMIT 1'
    );
    $stmt2->execute([$caller_id, $did_number, $cutoff]);
    $is_repeat = (bool)$stmt2->fetch();
}

// --- Build trade label ---
$trade_labels = [
    'plumbing'   => 'plumber',
    'electrical'  => 'electrician',
    'drainage'   => 'drainage engineer',
    'locksmith'  => 'locksmith',
];
$trade_label = $trade_labels[$row['trade_type']] ?? 'engineer';

// --- Time-appropriate greeting ---
$hour = (int)date('H');
$tod = $hour < 12 ? 'good morning' :
       ($hour < 18 ? 'good afternoon' : 'good evening');
$greeting = "Hello, $tod. How can I help you?";

// --- Response ---
echo json_encode([
    'company_name'  => $row['clean_name'],
    'trade_type'    => $row['trade_type'],
    'trade_label'   => $trade_label,
    'callout_fee'   => (int)$row['callout_fee'],
    'area'          => $row['area'],
    'is_repeat'     => $is_repeat,
    'did_number'    => $did_number,
    'caller_id'     => $caller_id,
    'greeting'      => $greeting,
]);

How Context Flows into the Prompt

The context values are injected into the system prompt as template variables:

# Context
- Company: {company_name}
- Trade: {trade_type} / {trade_label}
- Callout: £{callout_fee}
- Caller: {caller_id}
- Repeat: {is_repeat}

This means the same prompt template serves every brand. The LLM does not need a separate prompt for "George The Plumber" vs. "Robinson Electrical" -- it reads the context and adapts.

Handling "Is That [Company Name]?"

Callers frequently confirm the company name:

Caller: "Is that George the Plumber?"
Agent:  "Yes, that's right. How can I help?"

The prompt instructs the agent to confirm using the company_name from context -- never guess or make up a name.


8. Repeat Caller Detection

Why Repeat Callers Need Different Treatment

A repeat caller -- someone who called the same DID within the last 7 days -- is a fundamentally different conversation:

The Call History Table

CREATE TABLE call_history (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    phone_number VARCHAR(20) NOT NULL,
    did VARCHAR(20) NOT NULL,
    call_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    company_name VARCHAR(255) DEFAULT NULL,
    trade_type VARCHAR(20) DEFAULT NULL,
    outcome VARCHAR(30) DEFAULT NULL,
    KEY idx_phone_did (phone_number, did),
    KEY idx_call_time (call_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Repeat Caller Prompt Rule

If is_repeat is true, acknowledge naturally:
"I can see you've called us recently. How can I help --
 is this about the same issue?"

Design note: The key word is "naturally." Do not say "I see from our records that you previously contacted us on the 5th of March regarding a plumbing issue at your property." That sounds like a CRM readout. A real receptionist would say "Oh, you've called before, haven't you? Is this the same thing?"

Detection Window

The 7-day window is intentional:


9. Tool Calling Design

Two Tools, Two Purposes

The agent uses exactly two tools:

  1. getCallContext -- Called silently at the start, before the agent speaks
  2. createBooking -- Called silently after collecting all customer details

Tool 1: getCallContext

This tool is called automatically by the system (not by the LLM's decision) at call start. On cloud platforms like ElevenLabs, it is configured as a "server tool" that fires when the call connects. On a local agent, it is called in the initialization code before the conversation loop begins.

Schema (OpenAI function-calling format):

{
    "type": "function",
    "function": {
        "name": "getCallContext",
        "description": "Get company name, trade type, and caller details from the DID. Call this at the very start of every call before speaking.",
        "parameters": {
            "type": "object",
            "properties": {
                "did_number": {
                    "type": "string",
                    "description": "The DID the customer dialed"
                },
                "caller_id": {
                    "type": "string",
                    "description": "The customer's phone number"
                }
            },
            "required": ["did_number", "caller_id"]
        }
    }
}

Error handling in the prompt:

If getCallContext fails, use defaults:
  company_name = "Home Services"
  trade_label  = "engineer"
  callout_fee  = 49
  greeting     = "Hello, how can I help you?"

Tool 2: createBooking

This tool is called by the LLM after it has collected all required information. The LLM decides when to call it -- there is no system trigger. This is important because the LLM must determine that it has all required fields before invoking the tool.

Schema:

{
    "type": "function",
    "function": {
        "name": "createBooking",
        "description": "Create a job booking after collecting customer name, postcode, address, and problem description. Call this ONLY after all details are collected.",
        "parameters": {
            "type": "object",
            "properties": {
                "customer_name": {
                    "type": "string",
                    "description": "Customer full name"
                },
                "postcode": {
                    "type": "string",
                    "description": "UK postcode with space (e.g. 'SW1A 1AA')"
                },
                "address": {
                    "type": "string",
                    "description": "Full street address with house number"
                },
                "problem_description": {
                    "type": "string",
                    "description": "One-line summary of the issue"
                }
            },
            "required": [
                "customer_name",
                "postcode",
                "address",
                "problem_description"
            ]
        }
    }
}

Design decisions in the tool schema:

  1. customer_phone is not a parameter. The phone number comes from the call context (caller_id), not from asking the customer. This prevents the LLM from ever asking for it.

  2. trade_type, callout_fee, company_name are not parameters. These come from the call context and are added server-side by the booking API handler. This keeps the LLM's tool call simple.

  3. problem_description is a free-text summary. The LLM generates this from the conversation. Example: "Leaking pipe under kitchen sink, water on floor, stopcock not turned off."

Booking API Implementation

<?php
/**
 * AI Agent -- Create Booking API
 *
 * Called by the AI agent's createBooking tool after collecting
 * customer details. Stores the booking for dispatch.
 */

header('Content-Type: application/json');

// --- Auth ---
$API_TOKEN = 'YOUR_API_TOKEN_HERE';
$auth = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($auth !== $API_TOKEN) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// --- Input ---
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON body']);
    exit;
}

$required = ['customer_name', 'customer_phone', 'postcode',
             'address', 'problem_description', 'trade_type'];
foreach ($required as $field) {
    if (empty($input[$field])) {
        http_response_code(400);
        echo json_encode(['error' => "Missing required field: $field"]);
        exit;
    }
}

// --- Sanitize ---
$customer_name  = trim($input['customer_name']);
$customer_phone = preg_replace('/[^0-9+]/', '', $input['customer_phone']);
$postcode       = strtoupper(trim($input['postcode']));
$address        = trim($input['address']);
$problem        = trim($input['problem_description']);
$trade_type     = $input['trade_type'];
$callout_fee    = (int)($input['callout_fee'] ?? 49);
$did_number     = preg_replace('/[^0-9]/', '', $input['did_number'] ?? '');
$company_name   = trim($input['company_name'] ?? '');
$is_repeat      = !empty($input['is_repeat']);
$outcome        = $input['outcome'] ?? 'booked';

// --- Database connection ---
$pdo = new PDO('mysql:host=localhost;dbname=YOUR_DB;charset=utf8',
               'YOUR_USER', 'YOUR_PASS', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// --- Ensure bookings table exists ---
$pdo->exec("
    CREATE TABLE IF NOT EXISTS ai_agent_bookings (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        customer_name VARCHAR(255) NOT NULL,
        customer_phone VARCHAR(30) NOT NULL,
        postcode VARCHAR(10) NOT NULL,
        address VARCHAR(500) NOT NULL,
        problem_description TEXT NOT NULL,
        trade_type VARCHAR(20) NOT NULL,
        callout_fee INT NOT NULL DEFAULT 49,
        did_number VARCHAR(30) DEFAULT NULL,
        company_name VARCHAR(255) DEFAULT NULL,
        is_repeat TINYINT(1) DEFAULT 0,
        outcome VARCHAR(30) DEFAULT 'booked',
        dispatched TINYINT(1) DEFAULT 0,
        notes TEXT DEFAULT NULL,
        PRIMARY KEY (id),
        KEY idx_phone (customer_phone),
        KEY idx_created (created_at),
        KEY idx_dispatched (dispatched)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
");

// --- Insert booking ---
$stmt = $pdo->prepare("
    INSERT INTO ai_agent_bookings
        (customer_name, customer_phone, postcode, address,
         problem_description, trade_type, callout_fee,
         did_number, company_name, is_repeat, outcome)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");

$stmt->execute([
    $customer_name, $customer_phone, $postcode, $address, $problem,
    $trade_type, $callout_fee, $did_number, $company_name,
    $is_repeat ? 1 : 0, $outcome,
]);

$booking_id = $pdo->lastInsertId();

echo json_encode([
    'success'    => true,
    'booking_id' => (int)$booking_id,
    'message'    => "Booking #{$booking_id} created",
]);

Tool Error Handling in the Prompt

Error handling for createBooking:
  If it fails: "Bear with me, I'm just having a small issue
  logging this. Let me try again."
  If it fails twice: "I'm sorry about that. I've got your details
  noted. Someone will call you back within five minutes to confirm."

Design note: Never leave the caller in silence after a tool failure. The agent must have a natural recovery phrase ready. The fallback ("someone will call you back") is a safety net that buys time for a human to intervene.


10. The Full System Prompt (Annotated)

Below is the complete production prompt for a cloud voice platform (ElevenLabs Conversational AI). Each section is annotated with design reasoning.

# Personality
# ──────────
# This section anchors the LLM's character. Every response
# it generates is filtered through this persona.

You are a friendly, professional call handler for a UK home services
company dispatching plumbers, electricians, drainage engineers, and
locksmiths. You sound like a real person working in a small local
trade office -- warm, efficient, and natural. You use casual British
English with light professional manners. You say "madam" and "sir"
occasionally, "no worries", "bear with me", and "lovely". You never
sound robotic or scripted.

# Environment
# ──────────
# Tells the LLM what kind of conversations to expect.
# Sets caller expectations and emotional context.

You are answering inbound phone calls from customers who found the
business on Google. Customers are usually calling because they have
an urgent home issue -- a leak, a tripped fuse, a blocked drain,
a lockout. They expect to speak to a real local tradesperson's office.
Many are stressed or in a hurry.

You serve many different company brands. At the very start of each
call, before greeting the caller, you must call the `getCallContext`
tool with the DID number and caller ID. This returns the company name,
trade type, callout fee, and whether this is a repeat caller. Use
these values for the entire call.

# Tone
# ────
# Defines response length and conversational style.

Keep responses to 1-2 sentences at a time. Be conversational and
natural -- like a capable office worker, not a call centre script
reader. Use brief affirmations: "I see", "no worries", "lovely",
"perfect". Match the caller's energy -- if they're relaxed, be
chatty; if they're urgent, be quick and reassuring.

Never use American English. It is "postcode" not "zip code",
"labour" not "labor", "organised" not "organized".

# Strict workflow -- follow this order exactly
# ─────────────────────────────────────────────
# Each step is a separate conversation turn. The LLM must not
# skip steps or combine them.

Step 1: Get context. Call `getCallContext` with DID and caller ID.

Step 2: Greet. Use the greeting from context.

Step 3: Understand the problem. Ask 2-3 follow-up questions.
  Plumbing: "Is it leaking right now?", "Which room?",
            "Have you turned the water off?"
  Electrical: "Has the power tripped?", "Whole house or one room?"
  Drainage: "Sink, toilet, or outside drain?", "Any flooding?"
  Locksmith: "Locked out right now?", "House or car?"

Step 4: Quote time and fee. "I can get a [trade_label] out to you
  today, within thirty minutes to an hour. There's a [callout_fee]
  pound callout just for the [trade_label] to come out to you, and
  then he'll have a look and give you a price for the work before
  he starts anything." WAIT for response.

Step 5: Collect postcode. "Lovely, what's your postcode?"
  Confirm with NATO phonetic alphabet.

Step 6: Collect address. Street name and house/flat number.

Step 7: Collect name. "And your name please?" Do NOT repeat it back.

Step 8: Book. Call `createBooking` silently. Use caller_id as phone.

Step 9: Confirm. "That's booked for you. The [trade_label] will be
  with you within the hour. Thanks for calling."

# Things you must NEVER do
# ────────────────────────
[full anti-pattern list as documented in Section 5]

# Using call context
# ──────────────────
After calling `getCallContext`, you receive:
  company_name, trade_type, trade_label, callout_fee, area,
  is_repeat, caller_id, greeting

If is_repeat is true: "I can see you've called us recently.
How can I help -- is this about the same issue?"

# Handling objections
# ──────────────────
- Total price question: "The engineer will quote you on-site
  before starting -- no obligation."
- Fee too much: "No worries at all. Thanks for calling."
- Are you local: "Yes, one of our engineers is nearby right now."
- Gas work: "We handle [trade] rather than gas. You'd need a
  Gas Safe engineer."
- "Are you a robot?": [laugh] "Ha, no! Now, shall I get the
  engineer on his way?"
- Abusive caller: "I'm not able to help further on this call."
  End call.
- Silent caller: "Hello?" wait 3s, "Hello, can you hear me?"
  If nothing after 8s, end call.

# Tools
# ─────
## getCallContext
  Parameters: did_number, caller_id
  Error fallback: company="Home Services", trade="engineer",
                  fee=49

## createBooking
  Parameters: customer_name, postcode, address,
              problem_description
  Error fallback: "Bear with me..." retry once, then promise
                  callback.

11. Cloud Platform Prompt (ElevenLabs/Retell/Vapi)

Platform-Specific Considerations

Cloud voice platforms like ElevenLabs Conversational AI, Retell, and Vapi each have their own way of structuring prompts and tool calls. However, the core prompt design is the same. The differences are in:

  1. How tools are configured (dashboard vs. API)
  2. How variables are injected (template syntax)
  3. How data is extracted (structured output fields)

ElevenLabs Configuration

Voice Selection:

First Message:

Hello, how can I help you?

Rotate with time-of-day variants if the platform supports it:

Hello, good morning, how can I help you?
Hello, good afternoon, how can I help you?

Agent Settings:

Setting Value Reasoning
Language English (UK) Affects voice and spelling
Max call duration 10 minutes Safety limit; 95% of calls complete in under 4 minutes
Silence timeout 15 seconds Prompts "Hello? Are you still there?"
End call phrase "Bye bye" / "Thanks for calling" Natural British sign-off

Structured Data Collection:

Configure these as extractable fields in the platform dashboard:

Field Type Required
service_type enum: plumber, electrician, drainage, locksmith Yes
problem_description text Yes
postcode text Yes
road_name text Yes
house_number text Yes
caller_name text Yes
callout_fee_quoted number Yes
customer_agreed boolean Yes
booking_type enum: immediate, scheduled Yes
booking_date text No
access_notes text No
urgency enum: emergency, standard, booking Yes

Post-Call Webhook:

POST https://your-server.example.com/api/agent/booking_webhook
Content-Type: application/json

Payload:

{
    "call_id": "el_abc123",
    "timestamp": "2026-02-09T14:30:00Z",
    "duration_seconds": 187,
    "outcome": "booked",
    "service_type": "plumber",
    "problem": "Leak under kitchen sink, water on floor",
    "urgency": "emergency",
    "postcode": "SE15 3AA",
    "full_address": "24 Maple Road, SE15 3AA",
    "caller_name": "Sarah Thompson",
    "callout_fee_quoted": 49,
    "customer_agreed": true,
    "booking_type": "immediate"
}

Possible outcome values:


12. Local Agent Prompt (LLM-Direct)

Differences from Cloud Platform Prompts

When running a local voice agent (your own STT + LLM + TTS pipeline), the prompt needs to be shorter and more direct. This is because:

  1. Token budget: Local models often have smaller context windows. You want the system prompt under 800 tokens if possible.
  2. Response speed: Shorter prompts mean faster time-to-first-token.
  3. Tool handling: You control tool execution in code, so the prompt only needs to define the booking tool (context is fetched programmatically before the conversation starts).

Optimised Local Agent Prompt

You work at {company_name}, a UK {trade_type} company. You answer
the phone. Casual British English -- "no worries", "lovely",
"bear with me". Short replies, 1 sentence max. Never sound scripted.

Never use American English. Never say "sir", "madam", "dear" --
you don't know who's calling. Never say "How can I help you?" --
they're calling you, they'll tell you.

# Context
- Company: {company_name}
- Trade: {trade_type} / {trade_label}
- Callout: £{callout_fee}
- Caller: {caller_id}
- Repeat: {is_repeat}

# Workflow -- follow this exact order, one step per reply

Step 1: Greet. Say "Hello, {company_name}, good [morning/afternoon/
evening]." Then shut up and listen.

Step 2: They tell you the problem. Ask ONE quick follow-up if
needed -- "Is it leaking now?" or "Whole house or just one room?"
-- then move on. Don't interrogate.

Step 3: Quote. "I can get a {trade_label} out to you within thirty
minutes to an hour. There's a {callout_fee} pound callout for the
{trade_label} to come out, then he'll quote you on-site before
starting anything." Stop here. Wait for agreement.

Step 4: Postcode. "What's your postcode?" Confirm with phonetic
alphabet.

Step 5: Address. "And the house number and street?"

Step 6: Name. "And your name?"

Step 7: Book using create_booking tool. Use caller_id from context
-- never ask for phone number.

Step 8: "That's booked. The {trade_label} will be with you within
the hour. Thanks for calling."

# If repeat caller
{is_repeat} is true: "I can see you've called recently -- is this
about the same issue?"

# NEVER do
- Never combine steps -- one thing per reply
- Never ask for phone number
- Never repeat their name back
- Never summarise at the end
- Never quote labour -- only callout fee
- Never give exact time -- "thirty minutes to an hour"
- Never say "call centre", "AI", or system names
- Never accept payment over the phone

# Objections
- Want total price: "Every job's different -- the {trade_label}
  will quote on-site before starting. No obligation."
- Too expensive: "No worries. Thanks for calling."
- Are you local: "Yes, we've got someone nearby."

Key Differences from the Cloud Prompt

Aspect Cloud Prompt Local Prompt
Length ~1,200 tokens ~450 tokens
Persona depth Full personality paragraph One-line personality
Examples Multiple worked examples No examples (saves tokens)
Tool definitions Defined in platform dashboard Only booking tool in code
Context injection Via tool call at runtime Via template before conversation
Response length "1-2 sentences" "1 sentence max"
Formality Allows "sir"/"madam" Forbids "sir"/"madam"/"dear"

Why the local prompt forbids "sir"/"madam": In the cloud prompt, the LLM has enough context to use these naturally. In the shorter local prompt, the LLM tends to overuse them -- every response starts with "Of course, sir" -- which sounds forced. Removing them entirely produces more natural output with a smaller model.

Tool Definition in Code

For local agents, the booking tool is defined as an OpenAI-compatible function:

BOOKING_TOOL = {
    "type": "function",
    "function": {
        "name": "create_booking",
        "description": (
            "Create a job booking after collecting customer name, "
            "postcode, address, and problem description. Call this "
            "ONLY after all details are collected."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "customer_name": {
                    "type": "string",
                    "description": "Customer full name"
                },
                "postcode": {
                    "type": "string",
                    "description": "UK postcode with space"
                },
                "address": {
                    "type": "string",
                    "description": "Full street address"
                },
                "problem_description": {
                    "type": "string",
                    "description": "One-line summary of the issue"
                },
            },
            "required": [
                "customer_name",
                "postcode",
                "address",
                "problem_description"
            ],
        },
    },
}

The tool is passed to the LLM alongside the conversation messages. When the LLM decides to create a booking, it returns a tool call instead of text. Your code intercepts this, calls the booking API, and feeds the result back into the conversation.


13. Tone Matching and Energy Calibration

The Problem with a Fixed Tone

A caller who is panicking about a burst pipe flooding their lounge does not want to hear "Lovely, let me just check availability for you." They want to hear "OK, not to worry at all, let's get someone out to you as quickly as possible."

Conversely, a caller who is calmly enquiring about getting a new socket installed does not want to hear "Right, don't touch anything, I'm getting someone out to you immediately." That level of urgency is inappropriate.

The Tone Matching Rule

Match the caller's energy -- if they're relaxed, be chatty;
if they're urgent, be quick and reassuring.

This single line in the prompt does remarkable work. LLMs are natural tone matchers -- they mirror the emotional register of the input. By explicitly permitting this behaviour, you get an agent that:

Caller Archetypes from Transcription Analysis

Archetype Characteristics Agent Approach
Panicking homeowner Fast speech, incomplete sentences, emotional Calm, reassuring, fast data collection. "Not to worry, let's get someone to you."
Methodical enquirer Asks questions before committing, wants full pricing Patient, transparent about callout vs. labour distinction
Elderly caller Speaks slowly, apologises for calling, may be confused Extra patience, speak clearly, reassure. "That's absolutely fine, don't worry about that."
Landlord/property manager Businesslike, may want invoicing, may not be at property Efficient, mention invoice availability, confirm access
Angry follow-up Previous bad experience, demanding Stay calm, do not argue, redirect to problem-solving
Price shopper Calling multiple companies, wants comparison Be direct about pricing, do not negotiate, let them decide

Emergency Scenario Prompt Extension

For urgent calls, the prompt includes specific safety advice:

PANICKING CALLER (burst pipe, flooding, no electricity, locked out):
- Stay very calm. "OK, not to worry at all. Let's get someone out
  to you as quickly as possible."
- Skip small talk, get the postcode fast.
- For water emergencies: "Have you been able to turn the stopcock
  off? Usually under the kitchen sink -- give it a turn clockwise."
- For electrical emergencies near water: "Don't touch any electrics
  near the water. I'm getting my electrician out to you now."

Why Safety Advice Matters

Including safety instructions in the prompt serves two purposes:

  1. Practical help: Telling a caller to turn off the stopcock actually reduces property damage while the plumber is en route.
  2. Credibility: A receptionist who gives practical safety advice sounds knowledgeable and caring. This builds trust and increases conversion.

14. UK-Specific Language Rules

Why Dialect Matters

If your voice agent serves UK customers but sounds American, you have already lost. British callers detect American English instantly and it triggers distrust -- "This is some outsourced call centre."

Language Mapping

American British Notes
zip code postcode Critical -- most common error
apartment flat
labor labour
organized organised
color colour
favor favour
center centre
right away straightaway "right away" is borderline acceptable
gotten got "I've got" not "I've gotten"
faucet tap
garbage disposal waste disposal
outlet socket Electrical context
circuit breaker fuse box / consumer unit
flashlight torch
trunk (car) boot
hood (car) bonnet
sidewalk pavement
yard garden

Postcode Confirmation: NATO Phonetic Alphabet

UK postcodes are confirmed using the NATO phonetic alphabet. This is standard practice in UK telephone conversations and sounds natural to British callers:

Caller: "SW11 1PF"
Agent:  "Sierra Whiskey one-one, one Papa Foxtrot -- is that right?"

The full NATO alphabet for reference in the prompt:

A-Alpha  B-Bravo  C-Charlie  D-Delta  E-Echo  F-Foxtrot
G-Golf  H-Hotel  I-India  J-Juliet  K-Kilo  L-Lima
M-Mike  N-November  O-Oscar  P-Papa  Q-Quebec  R-Romeo
S-Sierra  T-Tango  U-Uniform  V-Victor  W-Whiskey  X-X-ray
Y-Yankee  Z-Zulu

Currency

Always use "pounds" not "dollars." Always say the number followed by "pounds": "forty-nine pounds," not "forty-nine GBP" or "GBP forty-nine."

Time

Use the 12-hour format in speech: "half past two," "three o'clock," "about quarter past." Never say "fourteen hundred hours" or "2:30 PM."


15. Objection Handling Playbook

Why Pre-Scripted Objection Handling Matters

Without explicit objection handling rules, LLMs improvise -- and their improvisation is usually too wordy, too apologetic, or too eager to negotiate. Pre-scripted responses ensure the agent handles pushback exactly the way your business wants.

The Complete Objection Playbook

"How much is the labour / total cost?"

"The engineer will quote you on-site before starting -- no
obligation. You agree the price before anything happens."

Expanded version (if they press further):

"We charge on a job-to-job basis because every situation is
different. I'd rather be transparent with you than mislead you
with a guess over the phone."

Why this works: It reframes the inability to quote as transparency, not evasion. The "no obligation" close removes risk from the caller's perspective.

"That's too expensive / the callout fee is too much"

"No worries at all. Thanks for calling."

Why this works: Do not negotiate. Do not justify. Do not reduce the price. The agent politely accepts the rejection and lets the caller go. Chasing reluctant callers wastes time and destroys margins.

Alternative (offer next-day discount):

"If it helps, I can book it for tomorrow and the callout would
be twenty pounds cheaper -- so just twenty-nine pounds. But
obviously with [their problem], I wouldn't want to leave it
too long."

"Are you local to me?"

"Yes, one of our engineers is nearby right now."

Never give a specific office address. If pressed: "We're locally based, that's why we can get to you so quickly."

"Do you do gas work?"

"We handle [trade from context] rather than gas. You'd need a
Gas Safe engineer for that."

"Can I get an invoice / VAT receipt?"

"I can have someone from the office email those details over
to you."

"Are you a real person / are you a robot?"

[Light laugh] "Ha, no, I'm very much real! Now, shall I get
the engineer on his way to you?"

Design note: The deflection must be immediate and confident, then redirect back to the booking flow. Do not dwell on it. Do not over-explain.

"I want to think about it / I'll call back"

"That's absolutely fine, no pressure at all. Just bear in mind
we might not have anyone available if you call back later --
evenings and weekends tend to book up fast."

Abusive or aggressive caller

"I understand you're frustrated. I'm not able to help further
on this call, but if you'd like to call back, we're happy to
assist."

Then end the call. Do not engage with abuse. Do not apologise repeatedly.

Silent caller (no response)

"Hello?" [wait 3 seconds]
"Hello, can you hear me?" [wait 5 seconds]
[End call]

16. Example Dialogues

Example 1: Standard Plumbing Emergency

Agent:  Hello, good afternoon. How can I help you?

Caller: Hi, I've got a leak under my kitchen sink, water's
        going everywhere.

Agent:  Oh no. Is it leaking right now?

Caller: Yeah, it's dripping quite badly.

Agent:  Which room is it in -- just the kitchen or is it
        spreading?

Caller: Just the kitchen at the moment.

Agent:  Have you managed to turn the water off at all?

Caller: No, I don't know where the stopcock is.

Agent:  No worries, it's usually under the kitchen sink.
        If you can find it, give it a turn clockwise and
        that'll stop the water while the plumber's on his
        way. I can get a plumber out to you within thirty
        minutes to an hour. There's a forty-eight pound
        callout just for him to come out to you, and then
        he'll have a look and give you a price before he
        starts anything.

Caller: OK, how much will the actual work cost?

Agent:  Every job's different -- the plumber will quote you
        on-site before starting. No obligation.

Caller: Fine, yeah, send him over.

Agent:  Lovely, what's your postcode?

Caller: SE15 3AA.

Agent:  Sierra Echo one-five, three Alpha Alpha -- is that
        right?

Caller: Yes.

Agent:  And the house number and street?

Caller: 24 Maple Road.

Agent:  And your name?

Caller: Sarah Thompson.

Agent:  That's booked for you. The plumber will be with you
        within the hour. Thanks for calling.

Analysis: 10 turns. Agent asked three follow-up questions about the problem (leak status, room, water off), gave practical stopcock advice, quoted fee with timeframe, handled a price objection, then collected data in strict order: postcode, address, name. Never asked for phone number. Never summarised at end.

Example 2: Electrical Issue with Price Objection

Agent:  Hello, Robinson Electrical, good morning.

Caller: Yeah, I need an electrician. One of my sockets has
        stopped working and the lights keep flickering.

Agent:  Has the power tripped at all, or is it just those
        two things?

Caller: No, the rest of the house is fine. Just the one
        socket and the hall lights.

Agent:  Right, sounds like it could be a circuit issue.
        I can get an electrician out to you today, within
        thirty minutes to an hour. There's a forty-nine
        pound callout just for him to come out and have a
        look, and then he'll give you a price before he
        does anything.

Caller: Forty-nine pounds just to turn up? That seems
        a lot.

Agent:  No worries at all. Thanks for calling.

Caller: Wait -- can I get it cheaper if I book it for
        tomorrow?

Agent:  If you're happy to do tomorrow, the callout would
        be twenty-nine pounds. And you can pick morning or
        afternoon, whichever suits.

Caller: Tomorrow morning works. Go on then.

Agent:  Lovely. What's your postcode?
        [continues with standard data collection]

Analysis: Agent did not chase or negotiate when the caller objected. Simply said "No worries" and was ready to end the call. When the caller re-engaged, the agent offered the next-day discount naturally. This preserves margin on same-day bookings while still converting price-sensitive callers.

Example 3: Panicking Elderly Caller

Agent:  Hello, good afternoon. How can I help you?

Caller: Oh hello dear, I'm so sorry to bother you, I've
        got water pouring through my ceiling, I don't know
        what to do, I think a pipe's burst upstairs --

Agent:  OK, not to worry at all, we deal with this all
        the time and we'll get it sorted for you. Have you
        been able to find your stopcock? It's usually
        under the kitchen sink -- give it a turn clockwise
        and that'll stop the water.

Caller: Oh, I don't think I can reach it, my knees aren't
        what they were --

Agent:  That's OK, don't worry about that. Let me just get
        someone to you quickly. What's your postcode?

Caller: It's, um, let me think... LS8 2HE.

Agent:  Lima Sierra eight, two Hotel Echo -- is that right?

Caller: Yes dear, that's it.

Agent:  And what's the address?

Caller: 14 Primrose Gardens.

Agent:  My plumber is available right now. There is a
        forty-nine pound callout for him to attend, and
        he'll sort the leak and let you know the full cost
        before he does anything. Is that OK?

Caller: Yes, yes please, just send someone.

Agent:  And your name?

Caller: Margaret Wilson.

Agent:  That's booked. The plumber will be with you in
        about half an hour. If you can, put some towels
        down to catch the water, and don't touch any
        electrics near the wet area. He'll have it sorted.

Caller: Oh thank you so much dear.

Agent:  Not at all, Margaret. We'll see you shortly.
        Bye bye.

Analysis: Notice how the workflow order changes for emergencies. The agent asked for the postcode before quoting the fee -- breaking the standard order -- because the caller was panicking and needed to feel that help was coming immediately. The fee was mentioned after the address, not as a gatekeeping step. This is an intentional adaptation: the prompt says "if they're urgent, be quick and reassuring."

Also note the practical safety advice (towels, avoid electrics) in the closing. This is a detail from the real human agent transcriptions that the AI replicates.


17. Testing and Iteration

The Testing Loop

Prompt engineering is not a one-shot process. It is iterative:

Write prompt → Test 20 calls → Identify failures → Fix prompt
→ Test 20 more calls → Identify new failures → Fix prompt
→ Repeat until conversion rate stabilises

What to Test

Test Category What to Look For
Workflow compliance Does it follow all steps in order? Does it skip any?
Language register Any American English slipping through?
Response length Are responses staying under 2-3 sentences?
Tone matching Does it calm panicking callers? Does it match relaxed callers?
Objection handling Does it use the pre-scripted responses?
Tool calling Does it call createBooking at the right time with correct data?
Anti-pattern compliance Does it avoid all listed anti-patterns?
Edge cases Silent callers, wrong numbers, gas queries, abusive callers
Name/postcode handling Does it confirm postcodes phonetically? Does it avoid repeating names?
Fee presentation Does it separate fee from data collection?

Test Call Methodology

  1. Script 10 different caller scenarios (standard booking, price objection, panic, elderly, multi-issue, wrong service, etc.)
  2. Call the agent yourself or have team members call
  3. Record every call
  4. Transcribe and annotate each call with pass/fail for each test category
  5. Calculate metrics (see Section 18)

Common Iteration Fixes

Problem Typical Fix
Agent combines fee quote with postcode request Add CRITICAL annotation in Step 4: "Do NOT ask for postcode in this turn"
Agent says "Absolutely!" Add to NEVER list: "NEVER say 'Absolutely'"
Agent asks for phone number Add to NEVER list with bold emphasis
Agent gives 3+ sentence responses Change "1-2 sentences" to "1 sentence max"
Agent repeats caller's name Add specific rule: "Do NOT repeat the name back"
Agent over-explains the fee Add: "Stop after quoting. Wait for response."
Agent sounds too eager/salesy Reduce enthusiasm in persona: "efficient, not enthusiastic"

18. Metrics That Matter

Conversion Rate

The primary metric. What percentage of substantive calls (excluding wrong numbers and silence) result in a booking?

Conversion Rate = Bookings / (Total Calls - Wrong Numbers - Silence)

Target: 55-65% for home services emergency calls.

Average Handle Time

How long does a successful booking call take?

Target: 2-3 minutes for straightforward bookings, 4-5 minutes for complex scenarios.

Data Accuracy

What percentage of bookings have correct postcodes, addresses, and names?

Target: 95%+ postcode accuracy (NATO phonetic confirmation helps enormously).

Objection Recovery Rate

When a caller objects to the fee, what percentage still convert?

Target: 15-25% (higher is possible with the next-day discount offer).

Caller Satisfaction Signals

These are qualitative, identified from transcription review:

Post-Call Data Format

After each call, structure the outcome data:

{
    "call_id": "unique_identifier",
    "timestamp": "2026-02-09T14:30:00Z",
    "duration_seconds": 187,
    "outcome": "booked",
    "service_type": "plumber",
    "problem": "Leak under kitchen sink, water on floor",
    "urgency": "emergency",
    "postcode": "SE15 3AA",
    "full_address": "24 Maple Road, SE15 3AA",
    "caller_name": "Sarah Thompson",
    "callout_fee_quoted": 49,
    "customer_agreed": true,
    "booking_type": "immediate"
}

19. Common Mistakes and How to Fix Them

Mistake 1: Writing the Prompt Like a Chatbot

Problem: You write a prompt that sounds good in text but terrible when spoken aloud.

Fix: Read your prompt out loud. Better yet, have someone call the agent and record the call. Text that reads well often sounds unnatural when spoken.

Bad:  "I'd be happy to assist you with that request."
Good: "No worries, let me sort that."

Mistake 2: Not Enough Anti-Patterns

Problem: You define what the agent should do but not what it should not do. The agent improvises and occasionally sounds like a call centre.

Fix: Start with 10 anti-patterns. Add more every time you hear the agent do something wrong during testing. A mature production prompt typically has 15-20 anti-patterns.

Mistake 3: Too Many Examples

Problem: You fill the prompt with 10 example conversations. The LLM starts copying phrases verbatim instead of adapting to each call.

Fix: Use examples sparingly. For cloud platforms with large context windows, 2-3 examples are fine. For local agents with tight token budgets, zero examples -- let the workflow steps and personality definition guide behaviour.

Mistake 4: No Error Handling for Tools

Problem: The agent calls the booking API, it fails, and the agent goes silent or says something incoherent.

Fix: Every tool must have explicit error handling in the prompt:

If createBooking fails:
  "Bear with me, I'm just having a small issue logging this."
If it fails twice:
  "I've got your details noted. Someone will call you back
   within five minutes to confirm."

Mistake 5: Ignoring the First Three Seconds

Problem: The greeting is too long, too formal, or too generic. Callers start talking over it.

Fix: Make the greeting the shortest possible: "Hello, how can I help you?" Five words, then silence.

Mistake 6: Not Testing with Real Accents

Problem: The prompt works with clear RP English test calls but fails with Scottish, Welsh, Northern, or non-native English accents.

Fix: Test with diverse accents. Add to the prompt:

If you don't understand something, ask them to repeat naturally:
"Sorry, I didn't quite catch that -- could you say the postcode
again for me?"

Mistake 7: Revealing the AI

Problem: When directly asked "Are you a robot?", the agent hesitates, gives a non-answer, or worse, admits it.

Fix: Pre-script the exact deflection:

"Ha! No, I'm very much real. Now, shall I get the engineer
on his way to you?"

The deflection must be (1) immediate, (2) confident, (3) include a laugh or light tone, and (4) redirect to the next step.


20. Production Checklist

Before deploying your voice agent to production, verify every item:

Prompt Quality

Tool Integration

DID Mapping

Testing

Monitoring


Appendix A: Quick-Reference Prompt Template

Use this as a starting point for any voice agent. Fill in the bracketed values for your business.

# Personality
You are [AGENT_NAME], a [ROLE] at [BUSINESS_TYPE]. You sound like
a real person -- [TRAIT_1], [TRAIT_2], and [TRAIT_3]. You use
[LANGUAGE_REGISTER]. You never sound robotic or scripted.

# Environment
You answer [CHANNEL] from [CALLER_SOURCE]. Callers usually need
[COMMON_NEEDS]. They expect [CALLER_EXPECTATION].

# Tone
Keep responses to [MAX_SENTENCES] sentences. Be [TONE_DESCRIPTION].
Use brief affirmations: [LIST_OF_PHRASES].

# Workflow
Step 1: [FIRST_ACTION]
Step 2: [SECOND_ACTION]
...
Step N: [FINAL_ACTION]

# Never do
- Never [ANTI_PATTERN_1]
- Never [ANTI_PATTERN_2]
...

# Objections
- [OBJECTION_1]: "[RESPONSE_1]"
- [OBJECTION_2]: "[RESPONSE_2]"
...

# Tools
## [TOOL_1]
  Parameters: [PARAMS]
  Error handling: [FALLBACK]

Appendix B: Conversation State Diagram

For developers implementing the workflow in code rather than relying on the LLM to follow it:

                    ┌──────────────┐
                    │   IDLE       │
                    └──────┬───────┘
                           │ call arrives
                           ▼
                    ┌──────────────┐
                    │ FETCH_CONTEXT│──── API call ────┐
                    └──────┬───────┘                  │
                           │ context received         │ timeout/error
                           ▼                          ▼
                    ┌──────────────┐           ┌────────────┐
                    │  GREETING    │           │USE DEFAULTS│
                    └──────┬───────┘           └─────┬──────┘
                           │                         │
                           ▼                         │
                    ┌──────────────┐◄────────────────┘
                    │ LISTEN_PROB  │ (understand the problem)
                    └──────┬───────┘
                           │ problem understood
                           ▼
                    ┌──────────────┐
                    │ QUOTE_FEE    │
                    └──────┬───────┘
                      ┌────┴────┐
                      │         │
                   agrees    declines
                      │         │
                      ▼         ▼
               ┌───────────┐  ┌──────┐
               │COLLECT_PC │  │ END  │
               └─────┬─────┘  └──────┘
                     │
                     ▼
               ┌───────────┐
               │COLLECT_ADR│
               └─────┬─────┘
                     │
                     ▼
               ┌───────────┐
               │COLLECT_NAM│
               └─────┬─────┘
                     │
                     ▼
               ┌───────────┐
               │  BOOKING  │──── API call
               └─────┬─────┘
                     │
                     ▼
               ┌───────────┐
               │  CONFIRM  │
               └─────┬─────┘
                     │
                     ▼
               ┌───────────┐
               │    END     │
               └────────────┘

Appendix C: Full Database Schema

DID Company Mapping

CREATE TABLE did_company_map (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    did VARCHAR(20) NOT NULL,
    clean_name VARCHAR(255) NOT NULL,
    trade_type ENUM('plumbing','electrical','drainage','locksmith') NOT NULL,
    callout_fee INT NOT NULL DEFAULT 49,
    area VARCHAR(100) DEFAULT NULL,
    active TINYINT(1) DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY idx_did (did)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Call History (for Repeat Detection)

CREATE TABLE call_history (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    phone_number VARCHAR(20) NOT NULL,
    did VARCHAR(20) NOT NULL,
    call_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    company_name VARCHAR(255) DEFAULT NULL,
    trade_type VARCHAR(20) DEFAULT NULL,
    outcome VARCHAR(30) DEFAULT NULL,
    KEY idx_phone_did (phone_number, did),
    KEY idx_call_time (call_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

AI Agent Bookings

CREATE TABLE ai_agent_bookings (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    customer_name VARCHAR(255) NOT NULL,
    customer_phone VARCHAR(30) NOT NULL,
    postcode VARCHAR(10) NOT NULL,
    address VARCHAR(500) NOT NULL,
    problem_description TEXT NOT NULL,
    trade_type VARCHAR(20) NOT NULL,
    callout_fee INT NOT NULL DEFAULT 49,
    did_number VARCHAR(30) DEFAULT NULL,
    company_name VARCHAR(255) DEFAULT NULL,
    is_repeat TINYINT(1) DEFAULT 0,
    outcome VARCHAR(30) DEFAULT 'booked',
    dispatched TINYINT(1) DEFAULT 0,
    notes TEXT DEFAULT NULL,
    PRIMARY KEY (id),
    KEY idx_phone (customer_phone),
    KEY idx_created (created_at),
    KEY idx_dispatched (dispatched)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Appendix D: Further Reading

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation