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.
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
└──────────────────┘
Intents are the possible things a user might want. Start by listing them:
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.
Slots are the pieces of data you need to collect. The SlotFiller has built-in extractors for common types:
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.
The dialogue is a state machine. Each state can have an entry message, required slots, and transitions:
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..."))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()The EscalationManager watches for situations where a human should take over:
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}")Channel adapters translate between platform formats and your bot:
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": [...]}For production, define your bot in YAML instead of Python code:
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.
When collecting critical data, add a confirmation state:
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
},
))Use fallback_state to handle unrecognized inputs without crashing:
flow.add_state(DialogueState(
name="main_menu",
entry_message="Choose: orders, returns, or account",
transitions={...},
fallback_state="didnt_understand",
))Use callback hooks to run logic on state entry:
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,
))Run the included tests to verify everything works:
python -m pytest tests/ -v
# Or without pytest:
python -m unittest discover tests/Write your own test by mocking the classifier:
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"KeywordIntentClassifier for LLMIntentClassifier with your API keyctx.to_dict() to Redis/DynamoDB between turnsTurnResult data to track intent distribution and drop-off pointsSlackChannelAdapter and register a Slack app webhookGet the full Conversational AI Templates and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.