Contents

Chapter 1

Building a Chatbot from Scratch

This guide walks you through building a complete chatbot using the Conversational AI Templates framework. By the end, you'll have a working bot with intent classification, slot filling, multi-turn dialogue, and escalation — running without any API keys.

Prerequisites

  • Python 3.10+
  • This template package (unzipped)
  • For LLM-backed features: an OpenAI or Anthropic API key (optional)

Architecture Overview

User Message
     │
     ▼
┌──────────────────┐
│  Channel Adapter  │  ← Parses platform-specific payload (web JSON, Slack event)
└────────┬─────────┘
         │ IncomingMessage
         ▼
┌──────────────────┐
│  DialogueManager  │  ← Central orchestrator
│  ┌──────────────┐ │
│  │ Intent        │ │  ← Step 1: What does the user want?
│  │ Classifier    │ │
│  └──────────────┘ │
│  ┌──────────────┐ │
│  │ Slot Filler   │ │  ← Step 2: Extract structured data
│  └──────────────┘ │
│  ┌──────────────┐ │
│  │ State Machine │ │  ← Step 3: Determine next state
│  └──────────────┘ │
│  ┌──────────────┐ │
│  │ Response Gen  │ │  ← Step 4: Generate reply
│  └──────────────┘ │
│  ┌──────────────┐ │
│  │ Escalation    │ │  ← Step 5: Check if human needed
│  │ Manager       │ │
│  └──────────────┘ │
└────────┬─────────┘
         │ TurnResult
         ▼
┌──────────────────┐
│ ConversationCtx   │  ← Persistent state across turns
└──────────────────┘

Step 1: Define Your Intents

Intents are the possible things a user might want. Start by listing them:

python
from src.intent import IntentDefinition, KeywordIntentClassifier

classifier = KeywordIntentClassifier()

# Each intent has keywords (fast matching) and optional regex patterns
# (higher precision). Priority breaks ties between equally-matched intents.
classifier.add_intent(IntentDefinition(
    name="order_status",
    description="User wants to check their order status",
    keywords=["order", "status", "track", "where is", "delivery"],
    patterns=[r"(?:where|when).+(?:order|package|delivery)"],
    priority=1,
))

classifier.add_intent(IntentDefinition(
    name="return_request",
    description="User wants to return a product",
    keywords=["return", "refund", "send back", "exchange"],
    patterns=[r"(?:want|need|like)\s+(?:to\s+)?(?:return|exchange|refund)"],
    priority=1,
))

classifier.add_intent(IntentDefinition(
    name="greeting",
    description="User says hello",
    keywords=["hello", "hi", "hey"],
    priority=0,
))

Tip: Start with 5-10 intents. Add more as you discover what users actually ask. The keyword classifier is fast and needs no API key — switch to LLMIntentClassifier later for production accuracy.

Step 2: Define Your Slots

Slots are the pieces of data you need to collect. The SlotFiller has built-in extractors for common types:

python
from src.slots import SlotFiller, SlotDefinition

filler = SlotFiller([
    SlotDefinition(
        name="order_id",
        description="Order tracking number",
        slot_type="text",
        required=True,
        prompt="What's your order number?",
        validation_pattern=r"^[A-Z]{2}-\d{6}$",
    ),
    SlotDefinition(
        name="email",
        description="Customer email for lookup",
        slot_type="email",
        required=True,
        prompt="What email address is your account under?",
    ),
    SlotDefinition(
        name="reason",
        description="Reason for return",
        slot_type="choice",
        required=True,
        prompt="What's the reason for the return?",
        choices=["defective", "wrong item", "changed mind", "other"],
    ),
])

# Extract slots from a message
results = filler.extract_from_message(
    "My order is OR-123456 and my email is [email protected]"
)
for r in results:
    if r.value:
        print(f"  {r.slot_name}: {r.value} (confidence: {r.confidence:.2f})")

Supported slot types: text, email, phone, date, time, number, choice, free_text.

Step 3: Build the Dialogue Flow

The dialogue is a state machine. Each state can have an entry message, required slots, and transitions:

python
from src.dialogue import DialogueFlow, DialogueState, DialogueManager

flow = DialogueFlow(
    name="Order Support Bot",
    initial_state="greeting",
    # Global intents work from any state
    global_intents={
        "cancel": "greeting",
        "escalate": "escalated",
    },
)

flow.add_state(DialogueState(
    name="greeting",
    entry_message="Hi! I can help with order status and returns. What do you need?",
    transitions={
        "order_status": "collect_order_id",
        "return_request": "collect_return_info",
    },
))

flow.add_state(DialogueState(
    name="collect_order_id",
    entry_message="Sure! What's your order number? (Format: XX-123456)",
    required_slots=["order_id"],
    transitions={"confirm": "show_status"},
    max_turns=3,
))

flow.add_state(DialogueState(
    name="show_status",
    entry_message="Order {order_id} is currently in transit. Estimated delivery: 2 business days. Anything else?",
    transitions={"goodbye": "farewell"},
))

flow.add_state(DialogueState(
    name="collect_return_info",
    entry_message="I can help with that. What's the email on your account?",
    required_slots=["email", "reason"],
    transitions={"confirm": "return_submitted"},
    max_turns=5,
))

flow.add_state(DialogueState(
    name="return_submitted",
    entry_message="Return request submitted for {email}. Reason: {reason}. You'll get a shipping label by email within 24 hours.",
    transitions={"goodbye": "farewell"},
))

flow.add_state(DialogueState(name="farewell", entry_message="Thanks! Have a great day."))
flow.add_state(DialogueState(name="escalated", entry_message="Connecting you with an agent..."))

Step 4: Wire It Together

python
manager = DialogueManager(
    flow=flow,
    # Plug in your classifier as the intent_classifier
    intent_classifier=lambda msg, ctx: classifier.classify(msg).intent_confidence_tuple(),
)

# Start a conversation
ctx, greeting = manager.start_session(user_id="cust-42", channel="web")
print(f"Bot: {greeting}")

# Process user turns
messages = [
    "I want to track my order",
    "It's OR-789012",
    "Thanks, bye!",
]

for msg in messages:
    print(f"User: {msg}")
    result = manager.process_turn(msg, ctx)
    print(f"Bot: {result.response}")
    print(f"  [state={result.new_state}, intent={result.intent}]")
    print()

Step 5: Add Escalation

The EscalationManager watches for situations where a human should take over:

python
from src.escalation import EscalationManager, EscalationRule, EscalationReason

esc_manager = EscalationManager()
esc_manager.add_default_rules()  # Too many turns, low confidence, etc.

# Add a custom rule
esc_manager.add_rule(EscalationRule(
    name="angry_user",
    reason=EscalationReason.NEGATIVE_SENTIMENT,
    condition=lambda ctx: any(
        word in ctx.history[-1].content.lower()
        for word in ["furious", "terrible", "worst", "lawyer"]
    ) if ctx.history else False,
    priority="high",
    target_queue="senior-support",
    message="I can see you're frustrated. Let me connect you with a senior agent.",
))

# Check after each turn
ticket = esc_manager.check_and_escalate(ctx)
if ticket:
    print(f"Escalation: {ticket.ticket_id} → {ticket.assigned_to}")
    print(f"Summary: {ticket.summary}")

Step 6: Add a Channel Adapter

Channel adapters translate between platform formats and your bot:

python
from src.channels.web import WebChannelAdapter
from src.channels.base import OutgoingMessage

web = WebChannelAdapter()

# Parse an incoming web request
incoming = web.parse_incoming({
    "message": "I need help with an order",
    "user_id": "web-user-42",
    "session_id": "sess-abc",
})

# Process with dialogue manager
result = manager.process_turn(incoming.text, ctx)

# Format the response for the web widget
outgoing = OutgoingMessage(
    text=result.response,
    buttons=[
        {"label": "Order Status", "value": "order_status"},
        {"label": "Returns", "value": "return_request"},
    ],
)
response_json = web.format_response(outgoing, session_id="sess-abc")
# → {"response": "...", "session_id": "sess-abc", "buttons": [...]}

Step 7: Load from YAML Config

For production, define your bot in YAML instead of Python code:

python
import yaml
from src.dialogue import build_flow_from_config
from src.intent import build_classifier_from_config

with open("templates/support_bot.yaml") as f:
    config = yaml.safe_load(f)

flow = build_flow_from_config(config["dialogue_flow"])
classifier = build_classifier_from_config(config["intent_classifier"])

# The rest is the same — create a DialogueManager and process turns
manager = DialogueManager(
    flow=flow,
    intent_classifier=lambda msg, ctx: (
        classifier.classify(msg).intent,
        classifier.classify(msg).confidence,
    ),
)

See the templates/ directory for three ready-to-use configs: support_bot.yaml, faq_bot.yaml, and booking_bot.yaml.

Common Patterns

Pattern: Confirmation Loop

When collecting critical data, add a confirmation state:

python
flow.add_state(DialogueState(
    name="confirm_details",
    entry_message="To confirm: your order is {order_id}, email is {email}. Correct?",
    transitions={
        "confirm": "process_request",
        "deny": "collect_order_id",  # Start over
    },
))

Pattern: Fallback State

Use fallback_state to handle unrecognized inputs without crashing:

python
flow.add_state(DialogueState(
    name="main_menu",
    entry_message="Choose: orders, returns, or account",
    transitions={...},
    fallback_state="didnt_understand",
))

Pattern: Context-Aware Responses

Use callback hooks to run logic on state entry:

python
def on_enter_vip_check(ctx):
    if ctx.metadata.get("user_tier") == "premium":
        ctx.metadata["skip_queue"] = True

flow.add_state(DialogueState(
    name="support_queue",
    entry_message="Please hold...",
    on_enter=on_enter_vip_check,
))

Testing Your Bot

Run the included tests to verify everything works:

bash
python -m pytest tests/ -v
# Or without pytest:
python -m unittest discover tests/

Write your own test by mocking the classifier:

python
def test_my_flow():
    flow = build_my_flow()
    forced = {"intent": "greeting", "conf": 0.9}

    manager = DialogueManager(
        flow,
        intent_classifier=lambda msg, ctx: (forced["intent"], forced["conf"]),
    )
    ctx, greeting = manager.start_session()

    forced["intent"] = "order_status"
    result = manager.process_turn("track my order", ctx)
    assert result.new_state == "collect_order_id"

Next Steps

  • Add LLM classification: Swap KeywordIntentClassifier for LLMIntentClassifier with your API key
  • Add persistence: Serialize ctx.to_dict() to Redis/DynamoDB between turns
  • Add analytics: Log TurnResult data to track intent distribution and drop-off points
  • Add Slack: Use SlackChannelAdapter and register a Slack app webhook
  • Customize responses: Replace the default response generator with an LLM-backed one for natural language
Chapter 2
🔒 Available in full product

Channel Integration Guide

Chapter 3
🔒 Available in full product

Building a Chatbot from Scratch

Chapter 4
🔒 Available in full product

Channel Integration Guide

You’ve reached the end of the free preview

Get the full Conversational AI Templates and unlock everything.

All Chapters

Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.

Full Tool Suite

Access all interactive tools with complete data, all workload profiles, and the full scenario library.

Source Files

Downloadable source code, configuration files, and working examples from every chapter.

Lifetime Updates

Free updates for life. Every new chapter, tool, and improvement included.

Buy Now — $49 →
📦 Free sample included — download another copy or visit the store for the full product.
Conversational AI Templates v1.0.0 — Free Preview