Conversation State Machine
Basic State Management
Copy
from openai import OpenAI
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
import json
class ConversationState(Enum):
"""States in the conversation flow."""
GREETING = "greeting"
GATHERING_INFO = "gathering_info"
CONFIRMING = "confirming"
PROCESSING = "processing"
COMPLETED = "completed"
ERROR = "error"
@dataclass
class ConversationContext:
"""Holds conversation state and collected data."""
state: ConversationState = ConversationState.GREETING
collected_data: dict = field(default_factory=dict)
history: list = field(default_factory=list)
retry_count: int = 0
metadata: dict = field(default_factory=dict)
class StatefulChatbot:
"""Chatbot with explicit state management."""
def __init__(self, model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
self.context = ConversationContext()
def _get_system_prompt(self) -> str:
"""Get system prompt based on current state."""
prompts = {
ConversationState.GREETING: """You are a helpful assistant.
Greet the user warmly and ask how you can help them today.
Keep it brief and friendly.""",
ConversationState.GATHERING_INFO: """You are gathering information.
Ask clarifying questions one at a time.
Be conversational but focused.
Acknowledge what the user tells you.""",
ConversationState.CONFIRMING: """You are confirming details.
Summarize what you've collected and ask for confirmation.
Be clear and organized in your summary.""",
ConversationState.PROCESSING: """You are processing the request.
Acknowledge the request and explain next steps.
Be reassuring and informative.""",
ConversationState.COMPLETED: """The task is complete.
Thank the user and offer further assistance.
Be warm and professional.""",
}
return prompts.get(
self.context.state,
"You are a helpful assistant. Respond appropriately."
)
def _build_messages(self, user_input: str) -> list[dict]:
"""Build message list for API call."""
messages = [{"role": "system", "content": self._get_system_prompt()}]
# Add conversation history
for msg in self.context.history[-10:]: # Last 10 messages
messages.append(msg)
# Add current user input
messages.append({"role": "user", "content": user_input})
return messages
def _determine_transition(self, user_input: str, response: str) -> ConversationState:
"""Determine next state based on conversation."""
# Use LLM to analyze and determine transition
analysis_prompt = f"""Analyze this conversation turn and determine the appropriate next state.
Current state: {self.context.state.value}
Collected data: {json.dumps(self.context.collected_data)}
User said: {user_input}
Assistant said: {response}
Available states:
- greeting: Initial greeting
- gathering_info: Collecting required information
- confirming: Verifying collected information
- processing: Executing the request
- completed: Task finished
- error: Something went wrong
Return JSON: {{"next_state": "state_name", "reason": "brief reason"}}"""
result = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": analysis_prompt}],
response_format={"type": "json_object"}
)
data = json.loads(result.choices[0].message.content)
return ConversationState(data.get("next_state", "gathering_info"))
def process_message(self, user_input: str) -> str:
"""Process user message and return response."""
messages = self._build_messages(user_input)
response = self.client.chat.completions.create(
model=self.model,
messages=messages
)
assistant_message = response.choices[0].message.content
# Update history
self.context.history.append({"role": "user", "content": user_input})
self.context.history.append({"role": "assistant", "content": assistant_message})
# Determine and apply state transition
new_state = self._determine_transition(user_input, assistant_message)
self.context.state = new_state
return assistant_message
def reset(self):
"""Reset conversation to initial state."""
self.context = ConversationContext()
# Usage
chatbot = StatefulChatbot()
# Simulate conversation
print(chatbot.process_message("Hello!"))
print(f"State: {chatbot.context.state}")
print(chatbot.process_message("I need help booking a flight"))
print(f"State: {chatbot.context.state}")
Slot Filling Pattern
Copy
from openai import OpenAI
from dataclasses import dataclass
from typing import Optional
import json
@dataclass
class Slot:
"""A piece of required information."""
name: str
description: str
required: bool = True
value: Optional[str] = None
validation_prompt: str = None
@property
def is_filled(self) -> bool:
return self.value is not None
class SlotFillingBot:
"""Bot that collects required information through conversation."""
def __init__(self, slots: list[Slot], model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
self.slots = {s.name: s for s in slots}
self.history: list[dict] = []
def _get_unfilled_slots(self) -> list[Slot]:
"""Get list of unfilled required slots."""
return [s for s in self.slots.values() if s.required and not s.is_filled]
def _extract_slot_values(self, user_input: str) -> dict:
"""Extract slot values from user input."""
slot_descriptions = {
name: slot.description
for name, slot in self.slots.items()
}
prompt = f"""Extract information from the user message.
Required slots:
{json.dumps(slot_descriptions, indent=2)}
User message: "{user_input}"
Return JSON with extracted values. Use null for slots not mentioned:
{{"slot_name": "extracted_value_or_null", ...}}"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def _validate_slot(self, slot: Slot, value: str) -> tuple[bool, str]:
"""Validate a slot value."""
if not slot.validation_prompt:
return True, value
prompt = f"""{slot.validation_prompt}
Value to validate: "{value}"
Return JSON: {{"valid": true/false, "corrected_value": "value or null", "reason": "if invalid"}}"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
if result.get("valid"):
return True, result.get("corrected_value", value)
return False, result.get("reason", "Invalid value")
def _generate_question(self, slot: Slot) -> str:
"""Generate a natural question for a slot."""
context = ""
filled = [s for s in self.slots.values() if s.is_filled]
if filled:
context = "Already collected: " + ", ".join(
f"{s.name}={s.value}" for s in filled
)
prompt = f"""Generate a natural, conversational question to ask for this information.
{context}
Slot to ask about:
- Name: {slot.name}
- Description: {slot.description}
Generate only the question, no preamble:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip()
def _generate_confirmation(self) -> str:
"""Generate confirmation message with all slots."""
slots_summary = "\n".join(
f"- {slot.name}: {slot.value}"
for slot in self.slots.values()
if slot.is_filled
)
prompt = f"""Generate a confirmation message summarizing this information:
{slots_summary}
Ask the user to confirm or correct anything. Be concise and clear:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
def process_message(self, user_input: str) -> dict:
"""Process user message and return response with status."""
# Extract slot values from input
extracted = self._extract_slot_values(user_input)
# Validate and fill slots
for name, value in extracted.items():
if value and name in self.slots:
slot = self.slots[name]
is_valid, result = self._validate_slot(slot, value)
if is_valid:
slot.value = result
# Update history
self.history.append({"role": "user", "content": user_input})
# Check if all required slots are filled
unfilled = self._get_unfilled_slots()
if not unfilled:
# All slots filled - confirm
response = self._generate_confirmation()
status = "confirming"
else:
# Ask for next unfilled slot
response = self._generate_question(unfilled[0])
status = "collecting"
self.history.append({"role": "assistant", "content": response})
return {
"response": response,
"status": status,
"filled_slots": {
name: slot.value
for name, slot in self.slots.items()
if slot.is_filled
},
"unfilled_slots": [s.name for s in unfilled]
}
# Usage - Flight booking example
slots = [
Slot(
name="origin",
description="Departure city or airport",
validation_prompt="Validate this is a valid city or airport name"
),
Slot(
name="destination",
description="Arrival city or airport",
validation_prompt="Validate this is a valid city or airport name"
),
Slot(
name="date",
description="Travel date",
validation_prompt="Validate this is a valid date format (YYYY-MM-DD preferred)"
),
Slot(
name="passengers",
description="Number of passengers",
validation_prompt="Validate this is a positive integer"
)
]
bot = SlotFillingBot(slots)
# Simulate conversation
result = bot.process_message("I want to fly from New York to London")
print(result["response"])
print(f"Filled: {result['filled_slots']}")
print(f"Still need: {result['unfilled_slots']}")
result = bot.process_message("Next Friday, just me")
print(result["response"])
print(f"Status: {result['status']}")
Multi-Turn Context Management
Copy
from openai import OpenAI
from dataclasses import dataclass, field
from typing import Optional
import json
@dataclass
class ConversationTurn:
"""A single turn in conversation."""
role: str
content: str
metadata: dict = field(default_factory=dict)
@dataclass
class Topic:
"""A conversation topic or thread."""
name: str
summary: str
turns: list[ConversationTurn] = field(default_factory=list)
resolved: bool = False
class ContextManager:
"""Manages multi-turn conversation context."""
def __init__(
self,
max_history: int = 20,
model: str = "gpt-4o-mini"
):
self.client = OpenAI()
self.model = model
self.max_history = max_history
self.history: list[ConversationTurn] = []
self.topics: list[Topic] = []
self.current_topic: Optional[Topic] = None
self.user_profile: dict = {}
def _summarize_old_context(self, turns: list[ConversationTurn]) -> str:
"""Summarize older conversation turns."""
if not turns:
return ""
history_text = "\n".join(
f"{t.role}: {t.content}" for t in turns
)
prompt = f"""Summarize this conversation history concisely.
Preserve key facts, decisions, and context needed for future turns.
History:
{history_text}
Summary:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
def _detect_topic_change(self, message: str) -> bool:
"""Detect if user is changing topics."""
if not self.current_topic:
return True
prompt = f"""Is this message changing to a new topic?
Current topic: {self.current_topic.name}
Topic summary: {self.current_topic.summary}
New message: "{message}"
Return JSON: {{"topic_change": true/false, "reason": "brief reason"}}"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
return result.get("topic_change", False)
def _identify_topic(self, message: str) -> str:
"""Identify the topic of a message."""
prompt = f"""Identify the main topic of this message in 2-4 words.
Message: "{message}"
Topic:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip()
def _extract_user_info(self, message: str) -> dict:
"""Extract user profile information from message."""
prompt = f"""Extract any personal information the user mentions about themselves.
Message: "{message}"
Return JSON with any of: name, preferences, location, occupation, interests, or other relevant info.
Return empty object if nothing is mentioned."""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def add_message(self, role: str, content: str) -> dict:
"""Add a message and manage context."""
turn = ConversationTurn(role=role, content=content)
self.history.append(turn)
# Extract user info if user message
if role == "user":
user_info = self._extract_user_info(content)
self.user_profile.update(user_info)
# Handle topic management
if self._detect_topic_change(content):
# Archive current topic
if self.current_topic:
self.current_topic.resolved = True
self.topics.append(self.current_topic)
# Start new topic
topic_name = self._identify_topic(content)
self.current_topic = Topic(
name=topic_name,
summary=content[:100]
)
if self.current_topic:
self.current_topic.turns.append(turn)
# Compress history if needed
context_summary = ""
if len(self.history) > self.max_history:
old_turns = self.history[:-self.max_history]
context_summary = self._summarize_old_context(old_turns)
self.history = self.history[-self.max_history:]
return {
"context_summary": context_summary,
"current_topic": self.current_topic.name if self.current_topic else None,
"user_profile": self.user_profile,
"history_length": len(self.history)
}
def get_context_for_prompt(self) -> str:
"""Get formatted context for LLM prompt."""
parts = []
# User profile
if self.user_profile:
parts.append(f"User profile: {json.dumps(self.user_profile)}")
# Current topic
if self.current_topic:
parts.append(f"Current topic: {self.current_topic.name}")
# Recent topics
recent_topics = [t.name for t in self.topics[-3:] if t.resolved]
if recent_topics:
parts.append(f"Previous topics discussed: {', '.join(recent_topics)}")
return "\n".join(parts)
def get_messages(self) -> list[dict]:
"""Get history as message list for API."""
return [
{"role": t.role, "content": t.content}
for t in self.history
]
# Usage
context_mgr = ContextManager(max_history=10)
# User provides information over time
context_mgr.add_message("user", "Hi, I'm Alex and I work in software engineering")
context_mgr.add_message("assistant", "Hello Alex! Nice to meet you. How can I help today?")
context_mgr.add_message("user", "I'm looking for Python learning resources")
context_mgr.add_message("assistant", "I can help with that. What's your current Python level?")
print(f"User profile: {context_mgr.user_profile}")
print(f"Current topic: {context_mgr.current_topic.name}")
print(f"Context: {context_mgr.get_context_for_prompt()}")
Intent Classification
Copy
from openai import OpenAI
from dataclasses import dataclass
from typing import Optional
import json
@dataclass
class Intent:
"""A user intent with handler."""
name: str
description: str
examples: list[str]
handler: callable = None
confidence_threshold: float = 0.8
class IntentClassifier:
"""Classify user intents for routing."""
def __init__(self, intents: list[Intent], model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
self.intents = {i.name: i for i in intents}
def _build_classification_prompt(self) -> str:
"""Build prompt for intent classification."""
intent_descriptions = []
for name, intent in self.intents.items():
examples = ", ".join(f'"{e}"' for e in intent.examples[:3])
intent_descriptions.append(
f"- {name}: {intent.description}\n Examples: {examples}"
)
return f"""Classify the user's intent into one of these categories:
{chr(10).join(intent_descriptions)}
Return JSON:
{{
"intent": "intent_name",
"confidence": 0.0-1.0,
"entities": {{"extracted_entity": "value"}},
"reasoning": "brief explanation"
}}"""
def classify(self, message: str) -> dict:
"""Classify a user message."""
system_prompt = self._build_classification_prompt()
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": message}
],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
intent_name = result.get("intent")
confidence = result.get("confidence", 0)
intent = self.intents.get(intent_name)
if intent and confidence >= intent.confidence_threshold:
result["matched"] = True
result["intent_object"] = intent
else:
result["matched"] = False
return result
def route(self, message: str, fallback: callable = None) -> any:
"""Route message to appropriate handler."""
classification = self.classify(message)
if classification["matched"]:
intent = classification["intent_object"]
if intent.handler:
return intent.handler(
message,
classification.get("entities", {})
)
if fallback:
return fallback(message)
return None
# Define handlers
def handle_booking(message: str, entities: dict) -> str:
return f"Starting booking process. Extracted: {entities}"
def handle_status(message: str, entities: dict) -> str:
return f"Checking status for: {entities}"
def handle_cancel(message: str, entities: dict) -> str:
return f"Processing cancellation: {entities}"
def handle_help(message: str, entities: dict) -> str:
return "Here are the things I can help with..."
# Create classifier
intents = [
Intent(
name="booking",
description="User wants to make a new booking or reservation",
examples=[
"I want to book a flight",
"Can you help me make a reservation?",
"I need to schedule an appointment"
],
handler=handle_booking
),
Intent(
name="status",
description="User wants to check status of existing booking",
examples=[
"What's the status of my order?",
"Where is my booking?",
"Track my reservation"
],
handler=handle_status
),
Intent(
name="cancel",
description="User wants to cancel something",
examples=[
"Cancel my booking",
"I need to cancel my order",
"Remove my reservation"
],
handler=handle_cancel
),
Intent(
name="help",
description="User needs help or information",
examples=[
"Help",
"What can you do?",
"I need assistance"
],
handler=handle_help
)
]
classifier = IntentClassifier(intents)
# Classify messages
result = classifier.classify("I'd like to book a hotel for next week")
print(f"Intent: {result['intent']} (confidence: {result['confidence']})")
print(f"Entities: {result.get('entities', {})}")
# Route to handler
response = classifier.route("Check my order status please", fallback=lambda m: "I didn't understand that")
print(response)
Conversation Flows
Copy
from openai import OpenAI
from dataclasses import dataclass, field
from typing import Callable, Optional
from enum import Enum
class FlowStep(Enum):
"""Standard flow steps."""
START = "start"
COLLECT = "collect"
VALIDATE = "validate"
CONFIRM = "confirm"
EXECUTE = "execute"
COMPLETE = "complete"
ERROR = "error"
@dataclass
class FlowNode:
"""A node in the conversation flow."""
name: str
prompt_template: str
next_steps: dict = field(default_factory=dict) # condition -> next_node
validator: Optional[Callable] = None
processor: Optional[Callable] = None
class ConversationFlow:
"""Define and execute conversation flows."""
def __init__(self, model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
self.nodes: dict[str, FlowNode] = {}
self.current_node: Optional[str] = None
self.context: dict = {}
self.history: list = []
def add_node(self, node: FlowNode):
"""Add a node to the flow."""
self.nodes[node.name] = node
def start(self, start_node: str):
"""Start the flow at a specific node."""
self.current_node = start_node
return self._execute_node()
def _execute_node(self) -> str:
"""Execute current node and return response."""
node = self.nodes.get(self.current_node)
if not node:
return "Flow error: node not found"
# Format prompt with context
prompt = node.prompt_template.format(**self.context)
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": prompt}
] + self.history[-10:]
)
return response.choices[0].message.content
def process_input(self, user_input: str) -> dict:
"""Process user input and advance flow."""
self.history.append({"role": "user", "content": user_input})
node = self.nodes.get(self.current_node)
if not node:
return {"error": "Invalid flow state"}
# Run validator if present
if node.validator:
is_valid, result = node.validator(user_input, self.context)
if not is_valid:
response = f"Invalid input: {result}. Please try again."
self.history.append({"role": "assistant", "content": response})
return {
"response": response,
"node": self.current_node,
"valid": False
}
# Run processor if present
if node.processor:
self.context = node.processor(user_input, self.context)
# Determine next node
next_node = self._determine_next(node, user_input)
if next_node:
self.current_node = next_node
# Execute new node
response = self._execute_node()
self.history.append({"role": "assistant", "content": response})
return {
"response": response,
"node": self.current_node,
"context": self.context,
"valid": True
}
def _determine_next(self, node: FlowNode, user_input: str) -> Optional[str]:
"""Determine next node based on input and conditions."""
# Check explicit conditions
for condition, next_node in node.next_steps.items():
if condition == "default":
continue
if condition.lower() in user_input.lower():
return next_node
# Use LLM for complex routing
if len(node.next_steps) > 1:
options = list(node.next_steps.keys())
prompt = f"""Based on the user's response, which path should we take?
User said: "{user_input}"
Options: {options}
Return just the option name:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
choice = response.choices[0].message.content.strip().lower()
if choice in node.next_steps:
return node.next_steps[choice]
return node.next_steps.get("default")
# Example: Support ticket flow
def validate_email(user_input: str, context: dict) -> tuple[bool, str]:
"""Validate email format."""
if "@" in user_input and "." in user_input:
return True, user_input
return False, "Please provide a valid email address"
def collect_email(user_input: str, context: dict) -> dict:
"""Store collected email."""
context["email"] = user_input
return context
# Build flow
flow = ConversationFlow()
flow.add_node(FlowNode(
name="welcome",
prompt_template="Welcome the user and ask for their email address for the support ticket.",
next_steps={"default": "collect_email"}
))
flow.add_node(FlowNode(
name="collect_email",
prompt_template="Ask for the user's email address.",
validator=validate_email,
processor=collect_email,
next_steps={"default": "collect_issue"}
))
flow.add_node(FlowNode(
name="collect_issue",
prompt_template="Email collected: {email}. Now ask them to describe their issue.",
next_steps={
"billing": "billing_flow",
"technical": "technical_flow",
"default": "general_support"
}
))
# Run flow
print(flow.start("welcome"))
result = flow.process_input("[email protected]")
print(result["response"])
Error Handling and Recovery
Copy
from openai import OpenAI
from dataclasses import dataclass
@dataclass
class ErrorContext:
"""Context for error recovery."""
error_type: str
message: str
retry_count: int
recoverable: bool
class RobustChatbot:
"""Chatbot with error handling and recovery."""
def __init__(self, model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
self.max_retries = 3
self.error_count = 0
self.last_error: ErrorContext = None
def _handle_unclear_input(self, message: str) -> str:
"""Handle unclear or ambiguous input."""
prompt = f"""The user's input was unclear. Generate a helpful clarification request.
User said: "{message}"
Ask for clarification in a friendly way:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
def _detect_frustration(self, messages: list[str]) -> bool:
"""Detect if user is frustrated."""
if len(messages) < 2:
return False
recent = " ".join(messages[-3:])
prompt = f"""Analyze if the user seems frustrated in these messages.
Consider: repeated questions, escalating tone, explicit frustration.
Messages: "{recent}"
Return just: yes or no"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return "yes" in response.choices[0].message.content.lower()
def _offer_escalation(self) -> str:
"""Offer to escalate to human support."""
return """I understand this has been frustrating. Would you like me to:
1. Connect you with a human support agent
2. Try a different approach to help
3. Start fresh with a new question
Please let me know how you'd like to proceed."""
def _recover_from_error(self, error: ErrorContext) -> str:
"""Generate recovery message based on error."""
prompts = {
"unclear_input": "Ask the user to rephrase their question more specifically.",
"missing_info": "Politely request the missing information.",
"validation_failed": "Explain what was wrong and how to correct it.",
"system_error": "Apologize for the technical issue and offer alternatives.",
}
prompt = prompts.get(
error.error_type,
"Acknowledge the issue and offer to help differently."
)
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
def process_with_recovery(
self,
message: str,
history: list[str]
) -> dict:
"""Process message with error recovery."""
try:
# Check for frustration
if self._detect_frustration(history + [message]):
return {
"response": self._offer_escalation(),
"escalation_offered": True,
"error": None
}
# Normal processing
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": message}
]
)
self.error_count = 0 # Reset on success
return {
"response": response.choices[0].message.content,
"escalation_offered": False,
"error": None
}
except Exception as e:
self.error_count += 1
error = ErrorContext(
error_type="system_error",
message=str(e),
retry_count=self.error_count,
recoverable=self.error_count < self.max_retries
)
if error.recoverable:
recovery = self._recover_from_error(error)
else:
recovery = self._offer_escalation()
return {
"response": recovery,
"escalation_offered": not error.recoverable,
"error": error
}
# Usage
bot = RobustChatbot()
# Simulated conversation with potential issues
history = [
"How do I reset my password?",
"I already tried that, it didn't work",
"This is ridiculous, I've been trying for 20 minutes"
]
result = bot.process_with_recovery(
"This is so frustrating!!!",
history
)
print(result["response"])
if result["escalation_offered"]:
print("(Escalation offered to user)")
Chatbot Design Principles
- Design conversation flows before implementation
- Always provide a way out or escalation path
- Extract and remember user information across turns
- Handle errors gracefully with recovery options
- Use explicit state management for complex flows
Practice Exercise
Build a customer service chatbot that:- Uses state machines for conversation flow
- Implements slot filling for order inquiries
- Maintains multi-turn context
- Classifies intents for routing
- Handles errors with graceful recovery
- Natural conversation flow
- Complete information gathering
- Appropriate escalation triggers
- Consistent user experience