Basic Chatbot Tutorial¶
Build a complete chatbot from scratch using lex-helper, progressing from simple responses to advanced features like session management, error handling, and testing.
What You'll Build¶
By the end of this tutorial, you'll have created a Travel Assistant Bot with:
- Multiple intent handlers (greeting, booking, help)
- Type-safe session attributes for user context
- Robust error handling and validation
- Channel-aware responses
- Comprehensive test suite
- Proper project organization
Estimated time: 45-60 minutes
Prerequisites¶
- Completed installation guide and quick start
- Basic understanding of Python and AWS Lambda
- Familiarity with core concepts
Step 1: Project Setup¶
Let's create a well-organized project structure for our travel assistant bot.
1.1 Create Project Structure¶
mkdir travel-assistant-bot
cd travel-assistant-bot
# Create the main package structure
mkdir -p src/travel_bot/{intents,utils,tests}
touch src/travel_bot/__init__.py
touch src/travel_bot/intents/__init__.py
touch src/travel_bot/utils/__init__.py
touch src/travel_bot/tests/__init__.py
# Create configuration files
touch requirements.txt
touch lambda_function.py
1.2 Install Dependencies¶
Create requirements.txt:
Install dependencies:
1.3 Create Session Attributes¶
Create src/travel_bot/session_attributes.py:
from pydantic import Field
from lex_helper import SessionAttributes
class TravelBotSessionAttributes(SessionAttributes):
"""
Session attributes for the Travel Assistant Bot.
These attributes persist across conversation turns and help
maintain context about the user's travel preferences and booking state.
"""
# User preferences
preferred_destination: str = Field(
default="",
description="User's preferred travel destination"
)
travel_dates: str = Field(
default="",
description="Preferred travel dates"
)
budget_range: str = Field(
default="",
description="User's budget range for travel"
)
# Conversation state
greeting_count: int = Field(
default=0,
description="Number of times user has been greeted"
)
booking_step: str = Field(
default="",
description="Current step in the booking process"
)
# Error handling
consecutive_errors: int = Field(
default=0,
description="Count of consecutive errors for recovery"
)
Step 2: Create Your First Intent Handler¶
Let's start with a simple greeting intent that demonstrates basic lex-helper patterns.
2.1 Greeting Intent¶
Create src/travel_bot/intents/greeting.py:
from lex_helper import LexPlainText, LexRequest, LexResponse, dialog
from ..session_attributes import TravelBotSessionAttributes
def handler(lex_request: LexRequest[TravelBotSessionAttributes]) -> LexResponse[TravelBotSessionAttributes]:
"""
Handle greeting intents with personalized responses.
This handler demonstrates:
- Accessing session attributes
- Modifying session state
- Returning different responses based on context
"""
# Access session attributes
session_attrs = lex_request.sessionState.sessionAttributes
# Increment greeting count
session_attrs.greeting_count += 1
# Personalize response based on greeting count
if session_attrs.greeting_count == 1:
message = "Hello! Welcome to Travel Assistant. I can help you plan your next adventure. What would you like to do today?"
elif session_attrs.greeting_count <= 3:
message = f"Hello again! This is visit #{session_attrs.greeting_count}. How can I assist with your travel plans?"
else:
message = "You're becoming a regular! How can I help you with your travel needs today?"
return dialog.close(
messages=[LexPlainText(content=message)],
lex_request=lex_request,
)
2.2 Help Intent¶
Create src/travel_bot/intents/help.py:
from lex_helper import LexPlainText, LexRequest, LexResponse, dialog
from ..session_attributes import TravelBotSessionAttributes
def handler(lex_request: LexRequest[TravelBotSessionAttributes]) -> LexResponse[TravelBotSessionAttributes]:
"""
Provide help information about bot capabilities.
"""
help_message = """
I'm your Travel Assistant! Here's what I can help you with:
๐ **Travel Planning**: Get destination recommendations
โ๏ธ **Flight Booking**: Help you find and book flights
๐จ **Hotel Booking**: Find accommodations for your trip
๐ฐ **Budget Planning**: Get cost estimates for your travel
Just tell me what you'd like to do, like:
- "I want to plan a trip to Paris"
- "Help me book a flight"
- "Find hotels in Tokyo"
What would you like to start with?
""".strip()
return dialog.close(
messages=[LexPlainText(content=help_message)],
lex_request=lex_request,
)
Step 3: Advanced Intent with Slot Handling¶
Now let's create a more complex intent that demonstrates slot elicitation and validation.
3.1 Travel Planning Intent¶
Create src/travel_bot/intents/plan_travel.py:
from typing import Optional
from lex_helper import Intent, LexPlainText, LexRequest, LexResponse, dialog
from ..session_attributes import TravelBotSessionAttributes
def get_slot_value(slot_name: str, intent: Intent) -> Optional[str]:
"""Helper function to safely get slot values."""
if intent.slots and slot_name in intent.slots:
slot = intent.slots[slot_name]
return slot.value if slot else None
return None
def validate_destination(destination: str) -> tuple[bool, str]:
"""
Validate destination input.
Returns:
tuple: (is_valid, message)
"""
if not destination:
return False, "Please tell me where you'd like to travel."
# Simple validation - in real implementation, you might check against a database
invalid_destinations = ["nowhere", "hell", "prison"]
if destination.lower() in invalid_destinations:
return False, f"I can't help you travel to {destination}. Please choose a real destination!"
return True, ""
def validate_travel_dates(dates: str) -> tuple[bool, str]:
"""
Validate travel dates input.
Returns:
tuple: (is_valid, message)
"""
if not dates:
return False, "When would you like to travel? Please provide your preferred dates."
# Basic validation - in production, you'd parse and validate actual dates
if len(dates) < 3:
return False, "Please provide more specific travel dates, like 'next month' or 'December 15th'."
return True, ""
def handler(lex_request: LexRequest[TravelBotSessionAttributes]) -> LexResponse[TravelBotSessionAttributes]:
"""
Handle travel planning with progressive slot collection and validation.
This demonstrates:
- Slot elicitation patterns
- Input validation
- Session attribute updates
- Multi-turn conversation flow
"""
intent = lex_request.sessionState.intent
session_attrs = lex_request.sessionState.sessionAttributes
# Get slot values
destination = get_slot_value("Destination", intent)
travel_dates = get_slot_value("TravelDates", intent)
budget = get_slot_value("Budget", intent)
# Step 1: Collect destination
if not destination:
return dialog.elicit_slot(
slot_to_elicit="Destination",
messages=[LexPlainText(content="Where would you like to travel? I can help you plan a trip anywhere in the world!")],
lex_request=lex_request,
)
# Validate destination
is_valid, error_message = validate_destination(destination)
if not is_valid:
return dialog.elicit_slot(
slot_to_elicit="Destination",
messages=[LexPlainText(content=error_message)],
lex_request=lex_request,
)
# Step 2: Collect travel dates
if not travel_dates:
session_attrs.preferred_destination = destination
return dialog.elicit_slot(
slot_to_elicit="TravelDates",
messages=[LexPlainText(content=f"Great choice! {destination} is wonderful. When are you planning to travel?")],
lex_request=lex_request,
)
# Validate travel dates
is_valid, error_message = validate_travel_dates(travel_dates)
if not is_valid:
return dialog.elicit_slot(
slot_to_elicit="TravelDates",
messages=[LexPlainText(content=error_message)],
lex_request=lex_request,
)
# Step 3: Collect budget (optional)
if not budget:
session_attrs.travel_dates = travel_dates
return dialog.elicit_slot(
slot_to_elicit="Budget",
messages=[LexPlainText(content="What's your budget range for this trip? (e.g., '$1000-2000' or 'budget-friendly')")],
lex_request=lex_request,
)
# All information collected - provide travel plan
session_attrs.budget_range = budget
session_attrs.booking_step = "planning_complete"
travel_plan = f"""
Perfect! Here's your travel plan summary:
๐ **Destination**: {destination}
๐
**Travel Dates**: {travel_dates}
๐ฐ **Budget**: {budget}
Based on your preferences, here are my recommendations:
โ๏ธ **Flights**: I recommend booking 2-3 months in advance for the best prices
๐จ **Accommodation**: Consider staying in the city center for easy access to attractions
๐ฏ **Activities**: I can suggest popular attractions and local experiences
Would you like me to help you with the next step, like finding flights or hotels?
""".strip()
return dialog.close(
messages=[LexPlainText(content=travel_plan)],
lex_request=lex_request,
)
Step 4: Error Handling and Recovery¶
Let's add robust error handling to make our bot more resilient.
4.1 Create Error Handling Utilities¶
Create src/travel_bot/utils/error_handling.py:
from lex_helper import LexPlainText, LexRequest, LexResponse, dialog
from ..session_attributes import TravelBotSessionAttributes
def handle_error_with_recovery(
lex_request: LexRequest[TravelBotSessionAttributes],
error_message: str,
max_errors: int = 3
) -> LexResponse[TravelBotSessionAttributes]:
"""
Handle errors with progressive recovery strategies.
Args:
lex_request: The current Lex request
error_message: The error message to display
max_errors: Maximum consecutive errors before escalation
Returns:
LexResponse with appropriate recovery action
"""
session_attrs = lex_request.sessionState.sessionAttributes
session_attrs.consecutive_errors += 1
if session_attrs.consecutive_errors >= max_errors:
# Escalate to human or reset conversation
escalation_message = """
I'm having trouble understanding. Let me transfer you to a human agent, or you can:
- Type "help" to see what I can do
- Type "start over" to begin fresh
- Try rephrasing your request
How would you like to proceed?
""".strip()
# Reset error count after escalation
session_attrs.consecutive_errors = 0
return dialog.close(
messages=[LexPlainText(content=escalation_message)],
lex_request=lex_request,
)
# Progressive error messages
if session_attrs.consecutive_errors == 1:
recovery_message = f"{error_message} Could you please try rephrasing that?"
elif session_attrs.consecutive_errors == 2:
recovery_message = f"{error_message} Let me try to help differently. What specifically would you like to do?"
else:
recovery_message = f"{error_message} I'm still having trouble. Type 'help' to see what I can assist with."
return dialog.close(
messages=[LexPlainText(content=recovery_message)],
lex_request=lex_request,
)
def reset_error_count(session_attrs: TravelBotSessionAttributes) -> None:
"""Reset error count when user provides valid input."""
session_attrs.consecutive_errors = 0
4.2 Fallback Intent with Error Recovery¶
Create src/travel_bot/intents/fallback_intent.py:
from lex_helper import LexRequest, LexResponse
from ..session_attributes import TravelBotSessionAttributes
from ..utils.error_handling import handle_error_with_recovery
def handler(lex_request: LexRequest[TravelBotSessionAttributes]) -> LexResponse[TravelBotSessionAttributes]:
"""
Handle unrecognized user input with helpful recovery.
This demonstrates:
- Graceful error handling
- Progressive error recovery
- User guidance and assistance
"""
error_message = """
I didn't quite understand that. I'm a travel assistant and I can help you with:
โข Planning trips and getting destination advice
โข Finding flights and travel information
โข Hotel recommendations
โข Budget planning for your travels
""".strip()
return handle_error_with_recovery(
lex_request=lex_request,
error_message=error_message,
max_errors=3
)
Step 5: Main Lambda Handler¶
Create the main lambda_function.py:
from typing import Any
from lex_helper import Config, LexHelper
from src.travel_bot.session_attributes import TravelBotSessionAttributes
def lambda_handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
"""
Main Lambda handler for the Travel Assistant Bot.
This demonstrates:
- Proper lex-helper configuration
- Custom session attributes
- Automatic exception handling
- Package-based intent routing
"""
# Configure lex-helper with our custom session attributes
config = Config(
session_attributes=TravelBotSessionAttributes(),
package_name="src.travel_bot", # Points to our intent handlers
auto_handle_exceptions=True, # Automatically handle exceptions
error_message="I encountered an error. Please try again or type 'help' for assistance."
)
# Initialize and run the handler
lex_helper = LexHelper(config=config)
return lex_helper.handler(event, context)
Step 6: Testing Your Chatbot¶
Let's create comprehensive tests to ensure our bot works correctly.
6.1 Basic Unit Tests¶
Create src/travel_bot/tests/test_greeting.py:
import pytest
from unittest.mock import Mock
from lex_helper import LexRequest, Intent, SessionState
from ..intents.greeting import handler
from ..session_attributes import TravelBotSessionAttributes
def create_mock_request(greeting_count: int = 0) -> LexRequest[TravelBotSessionAttributes]:
"""Helper to create mock Lex requests for testing."""
session_attrs = TravelBotSessionAttributes(greeting_count=greeting_count)
mock_request = Mock(spec=LexRequest)
mock_request.sessionState = Mock(spec=SessionState)
mock_request.sessionState.sessionAttributes = session_attrs
mock_request.sessionState.intent = Mock(spec=Intent)
return mock_request
def test_first_greeting():
"""Test first-time greeting response."""
request = create_mock_request(greeting_count=0)
response = handler(request)
# Check that greeting count was incremented
assert request.sessionState.sessionAttributes.greeting_count == 1
# Check response contains welcome message
assert "Welcome to Travel Assistant" in response.messages[0].content
def test_repeat_greeting():
"""Test repeat greeting response."""
request = create_mock_request(greeting_count=2)
response = handler(request)
# Check that greeting count was incremented
assert request.sessionState.sessionAttributes.greeting_count == 3
# Check response acknowledges repeat visit
assert "visit #3" in response.messages[0].content
def test_frequent_visitor():
"""Test frequent visitor greeting."""
request = create_mock_request(greeting_count=5)
response = handler(request)
# Check response for frequent visitors
assert "becoming a regular" in response.messages[0].content
6.2 Integration Tests¶
Create src/travel_bot/tests/test_integration.py:
import pytest
from unittest.mock import Mock, patch
from lex_helper import Config, LexHelper
from ..session_attributes import TravelBotSessionAttributes
@pytest.fixture
def lex_helper():
"""Create a configured LexHelper for testing."""
config = Config(
session_attributes=TravelBotSessionAttributes(),
package_name="src.travel_bot",
auto_handle_exceptions=True
)
return LexHelper(config=config)
def create_lex_event(intent_name: str, slots: dict = None, session_attrs: dict = None):
"""Create a mock Lex event for testing."""
return {
"sessionState": {
"intent": {
"name": intent_name,
"slots": slots or {},
"state": "InProgress"
},
"sessionAttributes": session_attrs or {}
},
"inputTranscript": "test input",
"bot": {
"name": "TravelBot",
"version": "1.0"
}
}
def test_greeting_flow(lex_helper):
"""Test complete greeting conversation flow."""
# Create greeting event
event = create_lex_event("greeting")
context = Mock()
# Process the event
response = lex_helper.handler(event, context)
# Verify response structure
assert "sessionState" in response
assert "messages" in response
assert len(response["messages"]) > 0
# Verify session attributes were updated
session_attrs = response["sessionState"]["sessionAttributes"]
assert session_attrs["greeting_count"] == "1"
def test_travel_planning_flow(lex_helper):
"""Test travel planning conversation flow."""
# Start travel planning
event = create_lex_event("plan_travel")
context = Mock()
response = lex_helper.handler(event, context)
# Should ask for destination
assert any("where would you like to travel" in msg["content"].lower()
for msg in response["messages"])
# Provide destination
event = create_lex_event(
"plan_travel",
slots={"Destination": {"value": "Paris"}}
)
response = lex_helper.handler(event, context)
# Should ask for travel dates
assert any("when are you planning" in msg["content"].lower()
for msg in response["messages"])
def test_error_handling(lex_helper):
"""Test error handling and recovery."""
# Trigger fallback intent
event = create_lex_event("FallbackIntent")
context = Mock()
response = lex_helper.handler(event, context)
# Should provide helpful error message
assert any("didn't quite understand" in msg["content"].lower()
for msg in response["messages"])
6.3 Running Tests¶
Create a simple test runner script run_tests.py:
import pytest
import sys
from pathlib import Path
# Add src to Python path
src_path = Path(__file__).parent / "src"
sys.path.insert(0, str(src_path))
if __name__ == "__main__":
# Run tests with verbose output
pytest.main([
"src/travel_bot/tests/",
"-v",
"--tb=short",
"--color=yes"
])
Run your tests:
Step 7: Channel-Aware Responses¶
Let's add channel-specific formatting to make our bot work well across different platforms.
7.1 Enhanced Response Formatting¶
Create src/travel_bot/utils/response_formatter.py:
from typing import List
from lex_helper import LexPlainText, LexRequest
from lex_helper.channels import format_for_channel
def create_channel_aware_response(
content: str,
lex_request: LexRequest,
use_markdown: bool = True
) -> List[LexPlainText]:
"""
Create responses optimized for different channels.
Args:
content: The message content
lex_request: Current Lex request for channel detection
use_markdown: Whether to use markdown formatting
Returns:
List of formatted messages
"""
# Format content for the specific channel
formatted_content = format_for_channel(
content=content,
lex_request=lex_request,
use_markdown=use_markdown
)
return [LexPlainText(content=formatted_content)]
def create_travel_summary_response(
destination: str,
dates: str,
budget: str,
lex_request: LexRequest
) -> List[LexPlainText]:
"""Create a formatted travel summary response."""
# Rich formatting for web/app channels
rich_content = f"""
**๐ Your Travel Plan**
**Destination:** {destination}
**Travel Dates:** {dates}
**Budget:** {budget}
**Next Steps:**
โข โ๏ธ Find flights
โข ๐จ Book accommodation
โข ๐ฏ Plan activities
Would you like help with any of these?
""".strip()
# Simple formatting for SMS
simple_content = f"""
Your Travel Plan:
Destination: {destination}
Dates: {dates}
Budget: {budget}
Next: flights, hotels, or activities?
""".strip()
# Choose format based on channel
channel = getattr(lex_request, 'channel', 'web')
content = simple_content if channel == 'sms' else rich_content
return create_channel_aware_response(content, lex_request)
Step 8: Deployment and Testing¶
8.1 Create Deployment Package¶
Create deploy.py:
import zipfile
import os
from pathlib import Path
def create_deployment_package():
"""Create a deployment package for AWS Lambda."""
# Create deployment zip
with zipfile.ZipFile('travel-bot-deployment.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
# Add main handler
zipf.write('lambda_function.py')
# Add source code
for root, dirs, files in os.walk('src'):
for file in files:
if file.endswith('.py'):
file_path = os.path.join(root, file)
zipf.write(file_path)
# Add requirements (you'd typically use a layer for this)
# zipf.write('requirements.txt')
print("โ
Deployment package created: travel-bot-deployment.zip")
print("๐ฆ Package size:", os.path.getsize('travel-bot-deployment.zip'), "bytes")
if __name__ == "__main__":
create_deployment_package()
8.2 Local Testing Script¶
Create test_locally.py:
import json
from lambda_function import lambda_handler
def test_greeting():
"""Test greeting intent locally."""
event = {
"sessionState": {
"intent": {
"name": "greeting",
"slots": {},
"state": "ReadyForFulfillment"
},
"sessionAttributes": {}
},
"inputTranscript": "hello",
"bot": {
"name": "TravelBot",
"version": "1.0"
}
}
response = lambda_handler(event, None)
print("๐ค Bot Response:")
print(json.dumps(response, indent=2))
return response
def test_travel_planning():
"""Test travel planning intent locally."""
event = {
"sessionState": {
"intent": {
"name": "plan_travel",
"slots": {},
"state": "InProgress"
},
"sessionAttributes": {}
},
"inputTranscript": "I want to plan a trip",
"bot": {
"name": "TravelBot",
"version": "1.0"
}
}
response = lambda_handler(event, None)
print("๐ค Bot Response:")
print(json.dumps(response, indent=2))
return response
if __name__ == "__main__":
print("๐งช Testing Travel Assistant Bot locally...\n")
print("=" * 50)
print("TEST 1: Greeting Intent")
print("=" * 50)
test_greeting()
print("\n" + "=" * 50)
print("TEST 2: Travel Planning Intent")
print("=" * 50)
test_travel_planning()
print("\nโ
Local testing complete!")
Run local tests:
What You've Accomplished¶
Congratulations! You've built a comprehensive travel assistant chatbot that demonstrates:
โ Core lex-helper Features¶
- Intent routing with automatic handler discovery
- Type-safe session attributes for maintaining conversation state
- Slot elicitation with validation and error handling
- Progressive conversation flows with multi-turn interactions
โ Best Practices¶
- Proper project organization with clear separation of concerns
- Comprehensive error handling with recovery strategies
- Input validation and user guidance
- Channel-aware responses for different platforms
โ Testing and Quality¶
- Unit tests for individual intent handlers
- Integration tests for complete conversation flows
- Local testing tools for development and debugging
- Deployment packaging for AWS Lambda
โ Advanced Patterns¶
- Session state management for context preservation
- Progressive error recovery with escalation strategies
- Flexible response formatting for different channels
- Modular code organization for maintainability
Next Steps¶
Now that you've mastered the basics, you're ready to explore more advanced topics:
- Airline Bot Walkthrough - Learn advanced patterns from a real-world example
- Multi-language Support - Add internationalization to your bot
- Production Deployment - Deploy your bot with best practices
- Smart Disambiguation - Handle ambiguous user input intelligently
Troubleshooting¶
Common Issues¶
Import errors when testing locally:
Session attributes not persisting:
- Ensure you're returning the modified session attributes in your response
- Check that your session attribute class inherits from SessionAttributes
Intent handlers not found:
- Verify your package name in the Config matches your directory structure
- Ensure all __init__.py files are present
- Check that handler functions are named handler
Slot elicitation not working:
- Verify slot names match exactly between your Lex bot and your code
- Check that you're using dialog.elicit_slot() correctly
- Ensure your intent state is set to "InProgress"
This tutorial is part of the comprehensive lex-helper documentation. Ready for more advanced patterns? Continue with the Airline Bot Walkthrough โ