Multi-Turn Conversation Flows for ChatGPT Apps: Complete Implementation Guide
Multi-turn conversation flows are the backbone of sophisticated ChatGPT applications. Unlike single-shot Q&A interactions, multi-turn conversations maintain context across multiple user inputs, enabling complex workflows like booking appointments, processing orders, or conducting diagnostic interviews. This guide demonstrates five proven architectural patterns for building robust conversation flows that feel natural and intelligent.
Table of Contents
- Understanding Multi-Turn Conversation Architecture
- State Machine Pattern
- Conversation Graph Pattern
- Slot Filling Pattern
- Context Stacking Pattern
- Conversation Memory Manager
- Topic Switching and Recovery
- Production Best Practices
Understanding Multi-Turn Conversation Architecture
Multi-turn conversations differ fundamentally from stateless interactions. Each user message must be interpreted within the context of previous exchanges, tracking conversation state, user intent, and collected information across turns.
Key Challenges in Multi-Turn Conversations
Context Continuity: Maintaining conversation context across turns without losing track of the user's original goal. A user might ask "What's the price?" five turns into a conversation about booking a yoga class—your app needs to know they're asking about the class price, not a generic inquiry.
State Management: Tracking where users are in multi-step workflows (e.g., booking flow: check availability → select time → confirm → payment). State must persist across HTTP requests in ChatGPT's stateless architecture.
Slot Filling: Collecting required information gradually across turns ("Which day works for you?" → "What time?" → "Your email?"). Missing slots should be requested naturally, not via robotic forms.
Topic Switching: Handling context shifts when users change subjects mid-conversation ("Actually, I want to book a massage instead of yoga"). The system must gracefully abandon partial state and start fresh.
Conversation Memory: Referencing earlier conversation turns ("Use the same time as last week" or "The email I gave you earlier"). This requires persistent memory beyond immediate context.
The following five patterns address these challenges with production-ready code examples.
State Machine Pattern
State machines model conversations as discrete states (nodes) and transitions (edges). Each state represents a conversation stage, with transitions triggered by user input or app logic. This pattern excels for linear workflows with clear stages.
When to Use State Machines
- Booking flows: Check availability → Select time → Confirm → Payment
- Onboarding sequences: Welcome → Collect name → Collect email → Set preferences → Complete
- Diagnostic interviews: Symptom description → Severity questions → Recommendation
- Form filling: Sequential data collection with validation at each step
State Machine Implementation
// state-machine-conversation.js - 120 lines
// State machine for fitness class booking flow
class StateMachineConversation {
constructor(userId) {
this.userId = userId;
this.currentState = 'INITIAL';
this.context = {};
// Define state machine transitions
this.states = {
INITIAL: {
enter: () => this.askClassType(),
transitions: { HAS_CLASS_TYPE: 'SELECT_DATE' }
},
SELECT_DATE: {
enter: () => this.askDate(),
transitions: {
HAS_DATE: 'SELECT_TIME',
INVALID_DATE: 'SELECT_DATE' // Stay in same state
}
},
SELECT_TIME: {
enter: () => this.askTime(),
transitions: {
HAS_TIME: 'CONFIRM',
NO_AVAILABILITY: 'SELECT_DATE' // Go back
}
},
CONFIRM: {
enter: () => this.showConfirmation(),
transitions: {
CONFIRMED: 'PAYMENT',
CANCELLED: 'INITIAL' // Start over
}
},
PAYMENT: {
enter: () => this.processPayment(),
transitions: {
PAYMENT_SUCCESS: 'COMPLETE',
PAYMENT_FAILED: 'PAYMENT' // Retry payment
}
},
COMPLETE: {
enter: () => this.sendConfirmationEmail(),
transitions: {}
}
};
}
// Process user message and advance state
async processMessage(userMessage) {
const currentStateConfig = this.states[this.currentState];
// Extract entities from user message
const entities = await this.extractEntities(userMessage);
// Determine transition based on entities
const transition = this.determineTransition(entities);
if (transition && currentStateConfig.transitions[transition]) {
// Valid transition - move to next state
const nextState = currentStateConfig.transitions[transition];
this.currentState = nextState;
// Execute entry action for new state
return await this.states[nextState].enter();
} else {
// No valid transition - stay in current state, ask for clarification
return this.handleInvalidInput(userMessage);
}
}
// State entry actions
askClassType() {
return {
message: "What type of class would you like to book? We offer Yoga, Pilates, Spinning, and CrossFit.",
expectedInput: "class type",
currentState: this.currentState
};
}
askDate() {
const classType = this.context.classType;
return {
message: `Great choice! When would you like to attend ${classType}? (e.g., "tomorrow", "this Saturday", "December 28")`,
expectedInput: "date",
currentState: this.currentState
};
}
askTime() {
const availableTimes = this.context.availableSlots;
return {
message: `Available times for ${this.context.classType} on ${this.context.date}:\n${availableTimes.join('\n')}\n\nWhich time works best?`,
expectedInput: "time selection",
currentState: this.currentState
};
}
showConfirmation() {
const { classType, date, time, instructor } = this.context;
return {
message: `Perfect! You're booking:\n\n**${classType}** with ${instructor}\n${date} at ${time}\n\nConfirm booking?`,
expectedInput: "yes/no confirmation",
currentState: this.currentState,
actions: ['Confirm Booking', 'Cancel']
};
}
async processPayment() {
// Integration with payment processor
const paymentResult = await this.chargeCard(this.context.amount);
if (paymentResult.success) {
this.context.bookingId = paymentResult.bookingId;
return await this.processMessage('PAYMENT_SUCCESS');
} else {
return {
message: `Payment failed: ${paymentResult.error}. Please try again or contact support.`,
currentState: this.currentState
};
}
}
sendConfirmationEmail() {
const { email, bookingId, classType, date, time } = this.context;
// Email sending logic here
return {
message: `Booking confirmed! 🎉\n\nConfirmation #${bookingId} sent to ${email}.\n\nSee you at ${classType} on ${date} at ${time}!`,
currentState: this.currentState,
conversationComplete: true
};
}
// Transition logic
determineTransition(entities) {
switch (this.currentState) {
case 'INITIAL':
if (entities.classType) {
this.context.classType = entities.classType;
return 'HAS_CLASS_TYPE';
}
break;
case 'SELECT_DATE':
if (entities.date && this.isValidDate(entities.date)) {
this.context.date = entities.date;
this.context.availableSlots = this.fetchAvailableSlots(
this.context.classType,
entities.date
);
return 'HAS_DATE';
} else {
return 'INVALID_DATE';
}
case 'SELECT_TIME':
if (entities.time && this.isSlotAvailable(entities.time)) {
this.context.time = entities.time;
return 'HAS_TIME';
} else {
return 'NO_AVAILABILITY';
}
case 'CONFIRM':
if (entities.confirmation === 'yes') {
return 'CONFIRMED';
} else if (entities.confirmation === 'no') {
return 'CANCELLED';
}
break;
}
return null; // No valid transition
}
handleInvalidInput(userMessage) {
return {
message: `I didn't quite catch that. ${this.states[this.currentState].enter().message}`,
currentState: this.currentState
};
}
// Helper methods (implementation details omitted for brevity)
async extractEntities(message) { /* NLP entity extraction */ }
isValidDate(date) { /* Date validation */ }
fetchAvailableSlots(classType, date) { /* Database query */ }
isSlotAvailable(time) { /* Check availability */ }
async chargeCard(amount) { /* Payment processing */ }
}
module.exports = StateMachineConversation;
Key advantages: Predictable conversation flow, clear state transitions, easy to debug and visualize. Ideal for workflows where users follow a defined sequence.
Limitations: Rigid structure doesn't handle non-linear conversations well. Users who jump between steps (e.g., "Actually, change the date to Friday") require explicit back-transition handling.
For more flexible conversation routing, see the Conversation Graph Pattern below.
Conversation Graph Pattern
Conversation graphs extend state machines with non-linear navigation, allowing users to move freely between topics. This pattern uses a directed graph where nodes represent conversation topics and edges represent possible transitions.
When to Use Conversation Graphs
- Customer support: Users jump between billing questions, technical support, and account management
- Product discovery: Users explore different product categories non-linearly
- Multi-topic assistants: Apps handling diverse requests (booking + FAQs + account management)
- Exploratory conversations: Users don't follow a predetermined path
Conversation Graph Implementation
// conversation-graph.js - 130 lines
// Graph-based conversation routing for multi-topic chatbot
class ConversationGraph {
constructor(userId) {
this.userId = userId;
this.currentNode = 'ROOT';
this.conversationHistory = [];
this.context = {};
// Define conversation graph
this.graph = {
ROOT: {
handler: () => this.welcomeMessage(),
edges: {
'book_class': 'BOOKING_FLOW',
'check_schedule': 'SCHEDULE_VIEW',
'account_settings': 'ACCOUNT_MENU',
'pricing': 'PRICING_INFO',
'help': 'HELP_MENU'
}
},
BOOKING_FLOW: {
handler: () => this.startBookingFlow(),
edges: {
'select_class': 'CLASS_SELECTION',
'cancel': 'ROOT',
'help': 'HELP_MENU'
}
},
CLASS_SELECTION: {
handler: () => this.showClassOptions(),
edges: {
'yoga': 'YOGA_BOOKING',
'pilates': 'PILATES_BOOKING',
'spinning': 'SPINNING_BOOKING',
'back': 'BOOKING_FLOW',
'check_schedule': 'SCHEDULE_VIEW' // Allow topic switch
}
},
YOGA_BOOKING: {
handler: () => this.bookYogaClass(),
edges: {
'select_time': 'TIME_SELECTION',
'pricing': 'PRICING_INFO', // Jump to pricing mid-booking
'cancel': 'ROOT'
}
},
TIME_SELECTION: {
handler: () => this.selectTime(),
edges: {
'confirm': 'PAYMENT',
'change_date': 'CLASS_SELECTION',
'back': 'YOGA_BOOKING'
}
},
SCHEDULE_VIEW: {
handler: () => this.showSchedule(),
edges: {
'book_class': 'BOOKING_FLOW',
'filter_by_instructor': 'SCHEDULE_VIEW', // Self-loop with filter
'back': 'ROOT'
}
},
PRICING_INFO: {
handler: () => this.showPricing(),
edges: {
'book_class': 'BOOKING_FLOW',
'back': this.getPreviousNode() // Dynamic back navigation
}
},
ACCOUNT_MENU: {
handler: () => this.showAccountOptions(),
edges: {
'update_email': 'UPDATE_EMAIL',
'cancel_membership': 'CANCEL_FLOW',
'payment_methods': 'PAYMENT_SETTINGS',
'back': 'ROOT'
}
},
HELP_MENU: {
handler: () => this.showHelpOptions(),
edges: {
'contact_support': 'SUPPORT_CONTACT',
'faq': 'FAQ_VIEW',
'back': this.getPreviousNode() // Return to previous context
}
}
};
}
async processMessage(userMessage) {
// Store message in history
this.conversationHistory.push({
role: 'user',
message: userMessage,
node: this.currentNode,
timestamp: Date.now()
});
// Extract intent from user message
const intent = await this.extractIntent(userMessage);
// Get current node configuration
const currentNodeConfig = this.graph[this.currentNode];
// Check if intent matches an edge from current node
if (currentNodeConfig.edges[intent]) {
// Valid transition - navigate to new node
const nextNode = currentNodeConfig.edges[intent];
// Handle dynamic edge resolution (e.g., "back" to previous node)
const resolvedNextNode = typeof nextNode === 'function'
? nextNode()
: nextNode;
this.currentNode = resolvedNextNode;
// Execute handler for new node
const response = await this.graph[this.currentNode].handler();
this.conversationHistory.push({
role: 'assistant',
message: response.message,
node: this.currentNode,
timestamp: Date.now()
});
return response;
} else {
// Intent not found in current node's edges
// Try global intent resolution (help, cancel, etc.)
const globalIntent = this.resolveGlobalIntent(intent);
if (globalIntent) {
this.currentNode = globalIntent.node;
return await this.graph[this.currentNode].handler();
}
// No valid transition - clarify with user
return this.handleUnknownIntent(userMessage, currentNodeConfig.edges);
}
}
// Node handlers
welcomeMessage() {
return {
message: "Welcome to FitnessPro! 💪\n\nI can help you:\n• Book a class\n• Check your schedule\n• Manage your account\n• Answer pricing questions\n\nWhat would you like to do?",
currentNode: this.currentNode,
availableActions: Object.keys(this.graph[this.currentNode].edges)
};
}
startBookingFlow() {
return {
message: "Great! Let's book a class. What type of class are you interested in?",
currentNode: this.currentNode,
availableActions: ['Yoga', 'Pilates', 'Spinning', 'CrossFit']
};
}
showClassOptions() {
return {
message: "We offer:\n\n🧘 **Yoga** - Flexibility & mindfulness\n💪 **Pilates** - Core strength\n🚴 **Spinning** - Cardio workout\n🏋️ **CrossFit** - High-intensity training\n\nWhich interests you?",
currentNode: this.currentNode
};
}
async bookYogaClass() {
const upcomingClasses = await this.fetchUpcomingClasses('yoga');
return {
message: `Yoga classes this week:\n\n${this.formatClassList(upcomingClasses)}\n\nWhich class would you like to book?`,
currentNode: this.currentNode,
data: { classes: upcomingClasses }
};
}
showSchedule() {
const schedule = this.fetchUserSchedule(this.userId);
return {
message: `Your upcoming classes:\n\n${this.formatSchedule(schedule)}\n\nWould you like to book another class?`,
currentNode: this.currentNode,
data: { schedule }
};
}
showPricing() {
return {
message: "💰 **Pricing Plans**\n\n• Drop-in: $20/class\n• 10-pack: $180 ($18/class)\n• Unlimited Monthly: $150\n\nWhich plan interests you?",
currentNode: this.currentNode
};
}
// Navigation helpers
getPreviousNode() {
// Find last node before current node
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
const entry = this.conversationHistory[i];
if (entry.role === 'assistant' && entry.node !== this.currentNode) {
return entry.node;
}
}
return 'ROOT'; // Default to root if no history
}
resolveGlobalIntent(intent) {
// Global intents available from any node
const globalIntents = {
'help': { node: 'HELP_MENU' },
'cancel': { node: 'ROOT' },
'start_over': { node: 'ROOT' }
};
return globalIntents[intent] || null;
}
handleUnknownIntent(userMessage, availableEdges) {
const edgeList = Object.keys(availableEdges).join(', ');
return {
message: `I'm not sure what you mean. From here, you can: ${edgeList}. What would you like to do?`,
currentNode: this.currentNode,
clarificationNeeded: true
};
}
// Helper methods
async extractIntent(message) {
// NLP intent classification (integrate with OpenAI, DialogFlow, etc.)
// Returns intent string matching edge keys
return 'book_class'; // Simplified for example
}
async fetchUpcomingClasses(classType) { /* Database query */ }
fetchUserSchedule(userId) { /* Fetch user's booked classes */ }
formatClassList(classes) { /* Format for display */ }
formatSchedule(schedule) { /* Format for display */ }
}
module.exports = ConversationGraph;
Key advantages: Flexible navigation, supports topic switching, scales to complex multi-domain conversations. Users can explore freely without feeling constrained.
Limitations: Requires robust intent classification. Graph can become complex with many nodes—use visualization tools to maintain graph structure.
For structured data collection across non-linear conversations, combine this with the Slot Filling Pattern.
Slot Filling Pattern
Slot filling collects required information incrementally, regardless of conversation flow. Each "slot" represents a required piece of data (e.g., name, email, date). The system tracks which slots are filled and requests missing ones naturally.
When to Use Slot Filling
- Form completion: Collecting profile information, preferences, or settings
- Transaction workflows: Gathering payment details, shipping addresses, or booking information
- Flexible data collection: Users can provide information in any order ("I want yoga on Friday at 6pm" fills three slots at once)
- Partial input handling: Users might say "Book yoga on Friday" without specifying time—system asks for missing time slot
Slot Filling Implementation
// slot-filling-conversation.js - 110 lines
// Flexible slot-based conversation for booking system
class SlotFillingConversation {
constructor(userId) {
this.userId = userId;
this.intent = null;
// Define required slots for different intents
this.slotDefinitions = {
book_class: {
required: ['class_type', 'date', 'time', 'email'],
optional: ['instructor_preference', 'special_requests'],
prompts: {
class_type: "What type of class would you like to book? (Yoga, Pilates, Spinning, CrossFit)",
date: "Which day works for you?",
time: "What time do you prefer?",
email: "What's your email address for confirmation?",
instructor_preference: "Do you have an instructor preference?",
special_requests: "Any special requests or accommodations needed?"
},
validators: {
class_type: (value) => ['yoga', 'pilates', 'spinning', 'crossfit'].includes(value.toLowerCase()),
date: (value) => this.isValidFutureDate(value),
time: (value) => this.isValidTime(value),
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
},
cancel_booking: {
required: ['booking_id', 'confirmation'],
prompts: {
booking_id: "What's your booking confirmation number?",
confirmation: "Are you sure you want to cancel this booking? (Yes/No)"
},
validators: {
booking_id: (value) => /^[A-Z0-9]{8}$/.test(value),
confirmation: (value) => ['yes', 'no'].includes(value.toLowerCase())
}
},
reschedule_booking: {
required: ['booking_id', 'new_date', 'new_time'],
prompts: {
booking_id: "What's your booking confirmation number?",
new_date: "Which new date works for you?",
new_time: "What time on that day?"
},
validators: {
booking_id: (value) => /^[A-Z0-9]{8}$/.test(value),
new_date: (value) => this.isValidFutureDate(value),
new_time: (value) => this.isValidTime(value)
}
}
};
// Current conversation state
this.slots = {};
this.filledSlots = new Set();
}
async processMessage(userMessage) {
// Extract intent if not already set
if (!this.intent) {
this.intent = await this.extractIntent(userMessage);
if (!this.slotDefinitions[this.intent]) {
return {
message: "I'm not sure what you'd like to do. You can book a class, cancel a booking, or reschedule.",
needsClarification: true
};
}
}
// Extract entities/slots from user message
const extractedSlots = await this.extractSlots(userMessage, this.intent);
// Validate and fill slots
for (const [slotName, slotValue] of Object.entries(extractedSlots)) {
const validator = this.slotDefinitions[this.intent].validators[slotName];
if (validator && validator(slotValue)) {
this.slots[slotName] = slotValue;
this.filledSlots.add(slotName);
} else if (validator) {
// Validation failed - notify user
return {
message: `Sorry, "${slotValue}" isn't a valid ${slotName}. ${this.slotDefinitions[this.intent].prompts[slotName]}`,
invalidSlot: slotName
};
} else {
// No validator - accept value
this.slots[slotName] = slotValue;
this.filledSlots.add(slotName);
}
}
// Check if all required slots are filled
const requiredSlots = this.slotDefinitions[this.intent].required;
const missingSlots = requiredSlots.filter(slot => !this.filledSlots.has(slot));
if (missingSlots.length === 0) {
// All required slots filled - execute intent
return await this.executeIntent();
} else {
// Ask for next missing slot
const nextSlot = missingSlots[0];
return {
message: this.slotDefinitions[this.intent].prompts[nextSlot],
requestingSlot: nextSlot,
filledSlots: Array.from(this.filledSlots),
missingSlots
};
}
}
async executeIntent() {
switch (this.intent) {
case 'book_class':
return await this.bookClass();
case 'cancel_booking':
return await this.cancelBooking();
case 'reschedule_booking':
return await this.rescheduleBooking();
default:
return { message: "Intent execution not implemented." };
}
}
async bookClass() {
const { class_type, date, time, email } = this.slots;
// Check availability
const isAvailable = await this.checkAvailability(class_type, date, time);
if (!isAvailable) {
// Clear time slot and ask for alternative
this.slots.time = null;
this.filledSlots.delete('time');
const alternativeTimes = await this.getAlternativeTimes(class_type, date);
return {
message: `Sorry, ${time} is fully booked. Available times on ${date}:\n\n${alternativeTimes.join('\n')}\n\nWhich works for you?`,
requestingSlot: 'time',
alternatives: alternativeTimes
};
}
// Create booking
const booking = await this.createBooking({
userId: this.userId,
classType: class_type,
date,
time,
email
});
return {
message: `Booking confirmed! 🎉\n\n**${class_type}** class\n${date} at ${time}\n\nConfirmation #${booking.id} sent to ${email}.`,
bookingId: booking.id,
conversationComplete: true
};
}
async cancelBooking() {
const { booking_id, confirmation } = this.slots;
if (confirmation.toLowerCase() === 'no') {
return {
message: "Okay, your booking remains active. Is there anything else I can help with?",
conversationComplete: true
};
}
// Cancel booking
await this.performCancellation(booking_id);
return {
message: `Booking ${booking_id} has been cancelled. You'll receive a confirmation email shortly.`,
conversationComplete: true
};
}
async rescheduleBooking() {
const { booking_id, new_date, new_time } = this.slots;
// Verify booking exists
const booking = await this.fetchBooking(booking_id);
if (!booking) {
return {
message: `I couldn't find booking ${booking_id}. Please check the confirmation number.`,
invalidSlot: 'booking_id'
};
}
// Check new time availability
const isAvailable = await this.checkAvailability(booking.classType, new_date, new_time);
if (!isAvailable) {
this.slots.new_time = null;
this.filledSlots.delete('new_time');
return {
message: `${new_time} is unavailable. Please select another time.`,
requestingSlot: 'new_time'
};
}
// Update booking
await this.updateBooking(booking_id, { date: new_date, time: new_time });
return {
message: `Booking rescheduled! Your ${booking.classType} class is now on ${new_date} at ${new_time}.`,
conversationComplete: true
};
}
// Helper methods
async extractIntent(message) { /* NLP intent classification */ }
async extractSlots(message, intent) { /* NLP entity extraction */ }
isValidFutureDate(date) { /* Date validation */ }
isValidTime(time) { /* Time validation */ }
async checkAvailability(classType, date, time) { /* Database query */ }
async getAlternativeTimes(classType, date) { /* Database query */ }
async createBooking(bookingData) { /* Database insert */ }
async performCancellation(bookingId) { /* Database update */ }
async fetchBooking(bookingId) { /* Database query */ }
async updateBooking(bookingId, updates) { /* Database update */ }
}
module.exports = SlotFillingConversation;
Key advantages: Flexible input order, natural conversation flow, handles partial information gracefully. Users can say "Book yoga on Friday at 6pm with Sarah" and fill four slots at once.
Limitations: Requires robust entity extraction. Complex slot dependencies (e.g., time slot depends on selected date) need custom logic.
For managing context across unrelated conversation topics, see the Context Stacking Pattern.
Context Stacking Pattern
Context stacking maintains multiple conversation contexts simultaneously, allowing users to interrupt one topic, handle another, and return seamlessly. Think of it as a "call stack" for conversations—each context is pushed onto the stack and popped when complete.
When to Use Context Stacking
- Interruption handling: User is booking a class, asks a pricing question mid-booking, then continues booking
- Nested conversations: "While you're checking availability, can you also tell me about instructors?"
- Multi-tasking: Customer support handling multiple issues in one conversation
- Context switching: Users who frequently change topics but expect to return to previous context
Context Stacking Implementation
// context-stack-conversation.js - 100 lines
// Stack-based context management for interruption handling
class ContextStackConversation {
constructor(userId) {
this.userId = userId;
this.contextStack = [];
this.currentContext = null;
}
async processMessage(userMessage) {
// Check if message is a context switch (new topic)
const newIntent = await this.detectContextSwitch(userMessage);
if (newIntent && newIntent !== this.currentContext?.intent) {
// Push current context onto stack if it's not complete
if (this.currentContext && !this.currentContext.complete) {
this.contextStack.push(this.currentContext);
}
// Create new context
this.currentContext = {
intent: newIntent,
slots: {},
conversationHistory: [],
createdAt: Date.now(),
complete: false
};
return await this.handleIntent(userMessage);
} else {
// Continue current context
return await this.handleIntent(userMessage);
}
}
async handleIntent(userMessage) {
// Add message to current context history
this.currentContext.conversationHistory.push({
role: 'user',
message: userMessage,
timestamp: Date.now()
});
// Process based on current intent
let response;
switch (this.currentContext.intent) {
case 'book_class':
response = await this.handleBooking(userMessage);
break;
case 'pricing_inquiry':
response = await this.handlePricingInquiry(userMessage);
break;
case 'schedule_check':
response = await this.handleScheduleCheck(userMessage);
break;
case 'instructor_info':
response = await this.handleInstructorInfo(userMessage);
break;
default:
response = { message: "I'm not sure how to help with that." };
}
// Add response to context history
this.currentContext.conversationHistory.push({
role: 'assistant',
message: response.message,
timestamp: Date.now()
});
// Check if current context is complete
if (response.contextComplete) {
this.currentContext.complete = true;
// Pop previous context from stack
if (this.contextStack.length > 0) {
const previousContext = this.contextStack.pop();
const resumeMessage = this.generateResumeMessage(previousContext);
this.currentContext = previousContext;
return {
message: `${response.message}\n\n---\n\n${resumeMessage}`,
contextSwitched: true,
resumedContext: previousContext.intent
};
} else {
// No previous context - conversation complete
return {
...response,
conversationComplete: true
};
}
}
return response;
}
async handleBooking(userMessage) {
// Booking logic with slot filling
const requiredSlots = ['class_type', 'date', 'time'];
const extractedSlots = await this.extractSlots(userMessage);
// Update slots
Object.assign(this.currentContext.slots, extractedSlots);
// Check if all slots filled
const missingSlots = requiredSlots.filter(
slot => !this.currentContext.slots[slot]
);
if (missingSlots.length === 0) {
// Complete booking
const booking = await this.createBooking(this.currentContext.slots);
return {
message: `Booking confirmed! Confirmation #${booking.id}`,
contextComplete: true
};
} else {
return {
message: `What ${missingSlots[0].replace('_', ' ')} works for you?`,
contextComplete: false
};
}
}
async handlePricingInquiry(userMessage) {
// Quick pricing response
const pricingInfo = this.getPricingInfo();
return {
message: `Our pricing:\n\n${pricingInfo}\n\nAnything else you'd like to know?`,
contextComplete: true // Pricing is typically a quick interruption
};
}
async handleScheduleCheck(userMessage) {
const schedule = await this.fetchUserSchedule(this.userId);
return {
message: `Your upcoming classes:\n\n${this.formatSchedule(schedule)}`,
contextComplete: true
};
}
async handleInstructorInfo(userMessage) {
const instructor = await this.extractInstructorName(userMessage);
const info = await this.fetchInstructorInfo(instructor);
return {
message: `About ${instructor}:\n\n${info.bio}\n\nSpecializes in: ${info.specialties.join(', ')}`,
contextComplete: true
};
}
generateResumeMessage(context) {
const intent = context.intent.replace('_', ' ');
const filledSlots = Object.keys(context.slots);
if (filledSlots.length > 0) {
const summary = filledSlots
.map(slot => `${slot}: ${context.slots[slot]}`)
.join(', ');
return `Let's continue with your ${intent}. So far: ${summary}. What else do you need to provide?`;
} else {
return `Let's continue with your ${intent}. What would you like to do?`;
}
}
async detectContextSwitch(message) {
// Detect if message indicates new intent/topic
const intent = await this.extractIntent(message);
// Keywords indicating context switch
const switchKeywords = ['actually', 'wait', 'before that', 'first', 'also', 'by the way'];
const containsSwitchKeyword = switchKeywords.some(keyword =>
message.toLowerCase().includes(keyword)
);
if (containsSwitchKeyword || (intent && intent !== this.currentContext?.intent)) {
return intent;
}
return null;
}
// Helper methods
async extractIntent(message) { /* NLP intent classification */ }
async extractSlots(message) { /* NLP entity extraction */ }
async extractInstructorName(message) { /* NLP entity extraction */ }
async createBooking(slots) { /* Database insert */ }
async fetchUserSchedule(userId) { /* Database query */ }
async fetchInstructorInfo(instructor) { /* Database query */ }
getPricingInfo() { /* Return pricing details */ }
formatSchedule(schedule) { /* Format for display */ }
}
module.exports = ContextStackConversation;
Key advantages: Natural interruption handling, maintains context across topic switches, supports complex multi-threaded conversations. Users feel understood even when jumping between topics.
Limitations: Stack depth can grow large in complex conversations—implement stack size limits. Requires clear signals to detect context switches.
For long-term memory across conversations, see the Conversation Memory Manager below.
Conversation Memory Manager
Conversation memory persists information across conversations, enabling continuity between sessions. Unlike context (which is conversation-scoped), memory is user-scoped and permanent.
When to Use Conversation Memory
- Personalization: "Book my usual class" or "Same time as last week"
- Preference tracking: Remembering favorite instructors, preferred times, or class types
- Historical reference: "The email I gave you last time" or "My booking from two weeks ago"
- Long-term relationships: Apps where users return repeatedly and expect continuity
Conversation Memory Implementation
// memory-manager.js - 80 lines
// Persistent memory system for cross-conversation continuity
class ConversationMemoryManager {
constructor(userId, storage) {
this.userId = userId;
this.storage = storage; // Database/Redis/Firestore connection
this.shortTermMemory = []; // Current conversation only
this.longTermMemory = null; // Loaded from storage
}
async initialize() {
// Load user's long-term memory from storage
this.longTermMemory = await this.storage.get(`memory:${this.userId}`) || {
preferences: {},
facts: {},
history: [],
entities: {}
};
}
async storeShortTerm(key, value, context = {}) {
// Store in current conversation memory
this.shortTermMemory.push({
key,
value,
context,
timestamp: Date.now()
});
}
async storeReference(referenceType, referenceValue) {
// Store referenceable entities (e.g., "my email", "my instructor")
this.longTermMemory.entities[referenceType] = {
value: referenceValue,
lastUsed: Date.now()
};
await this.persist();
}
async storePreference(preferenceType, preferenceValue) {
// Store user preferences for future conversations
this.longTermMemory.preferences[preferenceType] = {
value: preferenceValue,
confidence: 1.0, // Explicit preference = 100% confidence
source: 'explicit',
timestamp: Date.now()
};
await this.persist();
}
async inferPreference(preferenceType, inferredValue, confidence = 0.5) {
// Infer preferences from behavior
const existing = this.longTermMemory.preferences[preferenceType];
if (!existing || existing.confidence < confidence) {
this.longTermMemory.preferences[preferenceType] = {
value: inferredValue,
confidence,
source: 'inferred',
timestamp: Date.now()
};
await this.persist();
}
}
async storeFact(factType, factValue) {
// Store factual information about user
this.longTermMemory.facts[factType] = {
value: factValue,
timestamp: Date.now()
};
await this.persist();
}
async recall(key) {
// Try short-term memory first (current conversation)
const shortTerm = this.shortTermMemory
.filter(entry => entry.key === key)
.sort((a, b) => b.timestamp - a.timestamp)[0];
if (shortTerm) {
return shortTerm.value;
}
// Try long-term memory (across conversations)
if (this.longTermMemory.entities[key]) {
return this.longTermMemory.entities[key].value;
}
return null;
}
async getPreference(preferenceType) {
const preference = this.longTermMemory.preferences[preferenceType];
return preference?.value || null;
}
async getFact(factType) {
const fact = this.longTermMemory.facts[factType];
return fact?.value || null;
}
async addToHistory(conversationSummary) {
// Store conversation summary in history
this.longTermMemory.history.push({
summary: conversationSummary,
timestamp: Date.now()
});
// Keep only last 50 conversations
if (this.longTermMemory.history.length > 50) {
this.longTermMemory.history = this.longTermMemory.history.slice(-50);
}
await this.persist();
}
async getRecentHistory(limit = 5) {
return this.longTermMemory.history
.slice(-limit)
.reverse();
}
async resolveReference(referencePhrase) {
// Resolve natural language references to stored data
const referenceMap = {
'my email': () => this.recall('email'),
'my usual class': () => this.getPreference('class_type'),
'my favorite instructor': () => this.getPreference('instructor'),
'my preferred time': () => this.getPreference('time_slot'),
'last week': () => this.getHistoricalBooking(-7),
'same time as last time': () => this.getLastBookingTime()
};
for (const [pattern, resolver] of Object.entries(referenceMap)) {
if (referencePhrase.toLowerCase().includes(pattern)) {
return await resolver();
}
}
return null;
}
async persist() {
// Save long-term memory to storage
await this.storage.set(`memory:${this.userId}`, this.longTermMemory);
}
// Helper methods for memory queries
async getHistoricalBooking(daysAgo) {
const targetDate = Date.now() + (daysAgo * 24 * 60 * 60 * 1000);
// Query booking database for bookings near targetDate
return null; // Simplified
}
async getLastBookingTime() {
const history = await this.getRecentHistory(10);
// Extract last booking time from history
return null; // Simplified
}
}
module.exports = ConversationMemoryManager;
Usage example:
const memory = new ConversationMemoryManager(userId, firestoreStorage);
await memory.initialize();
// User says: "Book my usual class"
const usualClass = await memory.getPreference('class_type');
// Returns: "yoga"
// User says: "Use the email I gave you last time"
const email = await memory.recall('email');
// Returns: "user@example.com"
// Store new preference when user books yoga 3 times
await memory.inferPreference('class_type', 'yoga', 0.8);
// Store explicit fact
await memory.storeFact('membership_tier', 'premium');
Key advantages: Personalized experiences, reduces repetitive questions, builds long-term user relationships. Users feel recognized across conversations.
Limitations: Privacy concerns—clearly disclose what's stored and provide data deletion. Memory can become stale—implement expiration logic for time-sensitive data.
For more advanced personalization techniques, see our guide on ChatGPT App Analytics & Tracking Optimization.
Topic Switching and Recovery
Topic switching occurs when users change subjects mid-conversation. Robust conversation systems detect switches, preserve previous context, and offer graceful returns.
Detecting Topic Switches
async function detectTopicSwitch(userMessage, currentIntent) {
// Signal words indicating topic change
const switchSignals = [
'actually', 'wait', 'never mind', 'instead',
'before that', 'first', 'also', 'by the way'
];
const containsSignal = switchSignals.some(signal =>
userMessage.toLowerCase().includes(signal)
);
// Extract new intent
const newIntent = await extractIntent(userMessage);
// Topic switch if signal present OR intent changed dramatically
if (containsSignal || (newIntent && newIntent !== currentIntent)) {
return {
switched: true,
newIntent,
signal: containsSignal ? 'explicit' : 'implicit'
};
}
return { switched: false };
}
Offering Context Recovery
async function offerContextRecovery(abandonedContext) {
const { intent, slots, progress } = abandonedContext;
// Calculate how much progress was made
const completionPercentage = (
Object.keys(slots).length / requiredSlots.length
) * 100;
if (completionPercentage > 50) {
// Significant progress - offer to resume
return {
message: `Would you like to continue your ${intent}? You were ${Math.round(completionPercentage)}% complete.`,
actions: ['Resume', 'Start Over', 'Cancel']
};
} else {
// Minimal progress - just acknowledge
return {
message: "Okay, let's start fresh. What would you like to do?",
contextDiscarded: true
};
}
}
Handling Ambiguous Switches
async function handleAmbiguousSwitch(userMessage, currentContext) {
// When user message could belong to current context OR new topic
// Score message relevance to current context
const currentContextScore = await scoreRelevance(
userMessage,
currentContext.intent
);
// Score message as potential new topic
const newTopicScores = await scoreAsNewTopics(userMessage);
const bestNewTopic = newTopicScores[0];
if (currentContextScore > 0.7) {
// Likely continuation of current context
return { continueContext: true };
} else if (bestNewTopic.score > 0.8) {
// Clearly a new topic
return {
switchContext: true,
newIntent: bestNewTopic.intent
};
} else {
// Ambiguous - ask user
return {
clarificationNeeded: true,
message: `Are you asking about ${currentContext.intent} or starting a new topic (${bestNewTopic.intent})?`,
options: [currentContext.intent, bestNewTopic.intent]
};
}
}
For more on conversation design patterns, see our comprehensive guide on Building ChatGPT Applications.
Production Best Practices
1. Implement Conversation Timeouts
Conversations shouldn't persist indefinitely. Implement timeouts to clear stale context:
class ConversationTimeout {
constructor(timeoutMinutes = 30) {
this.timeoutMs = timeoutMinutes * 60 * 1000;
this.lastActivity = Date.now();
}
updateActivity() {
this.lastActivity = Date.now();
}
isExpired() {
return Date.now() - this.lastActivity > this.timeoutMs;
}
getTimeRemaining() {
const elapsed = Date.now() - this.lastActivity;
return Math.max(0, this.timeoutMs - elapsed);
}
}
2. Store Conversation State in Database
For ChatGPT apps (stateless HTTP requests), persist state between messages:
async function saveConversationState(userId, state) {
await firestore.collection('conversations').doc(userId).set({
currentState: state.currentState,
context: state.context,
slots: state.slots,
history: state.history,
lastUpdated: Date.now()
}, { merge: true });
}
async function loadConversationState(userId) {
const doc = await firestore.collection('conversations').doc(userId).get();
return doc.exists ? doc.data() : null;
}
3. Log Conversation Analytics
Track conversation metrics to identify improvement opportunities:
async function logConversationMetrics(conversationId, metrics) {
await analytics.track({
conversationId,
userId: metrics.userId,
totalTurns: metrics.turns,
completionRate: metrics.completed ? 1 : 0,
averageResponseTime: metrics.avgResponseTime,
topicSwitches: metrics.switchCount,
slotsFilledPerTurn: metrics.slotsFilledPerTurn,
intentConfidence: metrics.avgIntentConfidence,
conversationDuration: metrics.duration
});
}
4. Handle Conversation Errors Gracefully
async function handleConversationError(error, context) {
// Log error for debugging
console.error('Conversation error:', error, { context });
// Provide helpful fallback to user
return {
message: "I'm having trouble processing that. Could you rephrase, or type 'start over' to begin fresh?",
error: true,
fallbackOptions: ['Start Over', 'Speak to Human', 'Try Again']
};
}
5. Implement Conversation Testing
Test conversation flows with automated scenarios:
async function testConversationFlow(flow) {
const conversation = new StateMachineConversation(testUserId);
for (const turn of flow.turns) {
const response = await conversation.processMessage(turn.userMessage);
assert.equal(response.currentState, turn.expectedState);
assert.include(response.message, turn.expectedKeywords);
}
}
// Example test
testConversationFlow({
turns: [
{
userMessage: "Book yoga",
expectedState: "SELECT_DATE",
expectedKeywords: ["when", "date"]
},
{
userMessage: "Tomorrow",
expectedState: "SELECT_TIME",
expectedKeywords: ["time", "available"]
}
]
});
For advanced testing strategies, see our guide on ChatGPT App Testing & QA.
Related Resources
- Complete Guide to Building ChatGPT Applications - Comprehensive ChatGPT app development guide (pillar page)
- ChatGPT Widget Development Complete Guide - UI/UX patterns for ChatGPT widgets
- Function Calling Advanced Patterns in ChatGPT - Tool use optimization techniques
- Prompt Engineering for ChatGPT Apps - System prompt design strategies
- OAuth 2.1 for ChatGPT Apps Complete Guide - Authentication implementation
- ChatGPT App Performance Optimization - Response time optimization
- Streaming Responses & Real-Time UX in ChatGPT - Streaming response patterns
For more ChatGPT app development tutorials and guides, visit the MakeAIHQ Blog.
Ready to build ChatGPT apps with sophisticated conversation flows? Start your free trial on MakeAIHQ and deploy production-ready ChatGPT applications without writing code. Our visual conversation builder implements all five patterns covered in this guide—state machines, conversation graphs, slot filling, context stacking, and memory management—with zero configuration required.