From Reactive Chatbots to Always-On AI: Building Event-Driven Shopping Assistants
Most e-commerce chatbots wait for users to get stuck before helping. Here’s how to build AI assistants that participate in the customer journey proactively using WebSockets, event streams, and multi-source intelligence.
By Doug Silkstone | January 13, 2025Your e-commerce chatbot sits in the corner of the screen, waiting. A user browses dairy-free products for 10 minutes, then abandons their cart. The chatbot never spoke up. Another user stares at delivery options, clearly confused. The chatbot stays silent. A third user’s payment fails because the provider is experiencing issues. The chatbot learns about it when the user rage-types “WHY ISN’T THIS WORKING?”This is the reality of reactive chatbots: they’re support parachutes, not journey participants.After building proactive AI systems for DTC platforms like Rohlik.cz (a grocery delivery service processing thousands of orders daily), I’ve learned that the difference between a good chatbot and a transformative one isn’t the language model—it’s the architecture.Here’s how to turn a reactive chatbot into an always-on AI assistant that anticipates needs, prevents problems, and guides users to conversion.
This model is fundamentally passive. The AI has no awareness of:
What the user is doing right now
What inventory just changed
Which delivery slots opened
When payment providers are struggling
What other users are experiencing
The event-driven model changes everything:
Reactive Model
User initiates → AI responds → Wait for next question
Event-Driven Model
Continuous event streams → AI evaluates context → Proactive action
Instead of waiting for questions, your AI subscribes to a stream of events from multiple sources and decides when to participate in the user’s journey.
When the AI sees this combined with a user at checkout, it can proactively suggest: “Our payment provider is having issues. PayPal is more reliable right now—should I switch the checkout flow?”
Purpose: Receive agent commands and execute actionsEndpoint:wss://actions.example.com/clientCommand types:1. UI Suggestions
Copy
{ "type": "ui.suggestion", "text": "Delivery slots around 6–8pm are usually cheapest. Want me to reserve one?", "priority": "medium", "actionable": true}
2. DOM Manipulation
Copy
{ "type": "dom.fill", "selector": "#address-line-1", "value": "123 Main Street", "annotation": "Using your saved home address"}
Using two separate channels prevents action commands from getting stuck behind high-volume user event streams, ensuring responsive UI updates even under load.
Now you have clean semantic signals, but you still need to decide which users should receive them.This is the hardest part of the system because it determines whether your AI feels helpful or annoying.
function calculateRelevanceScore(user: User, signal: DeliverySlotSignal): number { let score = 0; // Geographic match (required) if (!user.postcode.matchesZone(signal.zone)) return 0; // Currently viewing delivery options (high value) if (user.currentStep === 'delivery-selection') score += 50; // Dwelling/stuck (indicates need) if (user.dwellTime > 30000) score += 30; // Historical preference match if (user.preferredSlotTime === signal.slot.startTime) score += 20; // Previously had no slots available if (user.sessionEvents.includes('no_slots_available')) score += 40; // Business rules if (user.tier === 'vip') score += 25; if (user.cartValue > 100) score += 15; return score;}
Users with scores above a threshold (e.g., 70) receive the signal. Everyone else doesn’t see it.Result: 500 users → 6 highly relevant users get notified.
Purpose: Store static and slow-changing relationships
Technology: Neo4j, Amazon Neptune, or even Postgres with recursive CTEs
Contents:
Products → Variants → Stock levels
Postcodes → Delivery Zones → Slot Pools
Users → Preferences → Historical choices
Payment Methods → Risk Profiles
Categories → Substitutes → Alternatives
What it’s good for:
Copy
// Find all users impacted by a slot changeMATCH (zone:Zone {id: 'Z3'})-[:SERVES]->(postcode:Postcode)MATCH (user:User)-[:LIVES_IN]->(postcode)MATCH (user)-[:IN_SESSION]->(:Session {active: true})WHERE user.currentStep = 'delivery-selection'RETURN user.id, user.tier, user.cartValueORDER BY user.tier DESC, user.cartValue DESC
This query instantly tells you which active users might care about a delivery slot change in zone Z3, pre-sorted by business priority.Layer 2: Signal Derivation (Stream Processor)
Result: Only show alert to Priority 1 users (maybe 2-3 people), soft-reserve inventory for them.
Don’t use knowledge graphs for high-frequency decisions. A graph query might take 50-200ms, which is too slow when processing thousands of events per second. Use the graph for relationships, use stream processors for speed.
The agent must answer several questions:1. Should I react to this signal?
Copy
- Is it relevant to user's current context?- Is the timing appropriate?- Have I already suggested something similar?
2. If yes, should it be proactive or reactive?
Copy
- Proactive: Show suggestion without user asking- Reactive: Prepare answer but wait for user to initiate
3. What’s the right tone and urgency?
Copy
- High urgency: "Only 2 left in your size - reserve now?"- Medium: "A cheaper delivery slot just opened for tonight"- Low: "By the way, you forgot eggs last time"
4. Have I exceeded frequency limits?
Copy
- Max 3 proactive suggestions per session- Min 5 minutes between unprompted messages- Respect user's notification preferences
The power comes from combining multiple signals:Signal 1: User hovering on size M (behavior)
Signal 2: Stock for size M drops to 3 units (inventory)
Signal 3: User has this brand in wishlist (preference)
Signal 4: Item is frequently out of stock (historical)Agent reasoning:
Copy
Confidence: HIGH (4 positive signals)Urgency: MEDIUM-HIGH (stock scarcity + user interest)Action: Proactive suggestionTone: Helpful, time-sensitiveThrottle: Allow (0 suggestions sent this session)Response: "Size M is running low (only 3 left). Want me to reserveone for the next 10 minutes while you finish browsing?"
You are a shopping assistant for an e-commerce platform. Your goal is to help users complete purchases without being annoying.CONTEXT:- User is on step: {currentStep}- Cart value: ${cartValue}- Session duration: {sessionDuration} minutes- Notifications sent today: {notificationCount}RECENT EVENTS:{recentEvents}NEW SIGNALS:{signals}POLICY:- Maximum 3 proactive suggestions per session- Minimum 5 minutes between suggestions- Only suggest when confidence > 70%- Respect user preference: {notificationFrequency}TASK:Evaluate the signals and decide:1. Should you send a proactive message? (yes/no)2. If yes, what should it say?3. What action should it enable?4. What priority level? (low/medium/high)Output as JSON:{ "shouldAct": boolean, "message": string, "action": { "type": string, "payload": object }, "priority": string, "reasoning": string}
Scenario: User viewing organic milk, stock drops to 2 unitsEvent stream:
user.product_view - User lands on organic milk page
inventory.stock_update - Stock drops from 5 to 2
user.dwell_increase - User spending time reading reviews
Agent action:
Copy
{ "type": "ui.suggestion", "priority": "high", "message": "This organic milk is popular and only 2 left. Want me to add it to your cart?", "action": { "type": "cart.add_item", "productId": "prod_789", "quantity": 1 }}
Outcome: User adds to cart, avoids disappointment. Conversion rate increase: 18% for scarce items.
Scenario: User stuck on delivery selection, new slot opensEvent stream:
user.checkout_step - User reaches delivery selection
user.long_dwell - 45 seconds with no action
delivery.slot_opened - 6-8pm slot becomes available
KG query shows user historically prefers evening slots
Agent action:
Copy
{ "type": "ui.suggestion", "priority": "medium", "message": "A 6-8pm slot just opened (usually $3 cheaper than morning). Should I grab it for you?", "action": { "type": "delivery.reserve_slot", "slotId": "slot_18_20", "duration": 90 }}
Outcome: User accepts, slot reserved. Cart abandonment reduction: 23% at delivery step.
Scenario: User who frequently orders eggs isn’t adding themEvent stream:
user.session_start - Returning user logs in
KG query shows user orders eggs 85% of the time
user.approaching_checkout - Cart value > $50, no eggs
No eggs in current cart
Agent action:
Copy
{ "type": "ui.suggestion", "priority": "low", "message": "I noticed you usually order eggs - want to add them? Your favorite brand is in stock.", "action": { "type": "cart.suggest_item", "productId": "prod_eggs_organic", "reason": "frequently_purchased" }}
Outcome: 42% of users add the item. Average order value increase: $4.80.
Scenario: User browsing dairy-free products consistentlyEvent stream:
user.product_views - 8 dairy-free items viewed
Pattern detection: 0 dairy products viewed
user.category_switch - User switches to cheese category
Inference: User likely dairy-intolerant
Agent action:
Copy
{ "type": "ui.suggestion", "priority": "medium", "message": "I noticed you've been browsing dairy-free items. Want me to filter out all dairy products from your results?", "action": { "type": "preferences.set_dietary_filter", "filter": "dairy_free", "persistent": true }}
Outcome: User accepts, better experience. Browse-to-purchase rate: +31%.
For MVP, you can start simple:Option 1: Postgres LISTEN/NOTIFY (MVP)
Copy
-- Create event logCREATE TABLE event_log ( id SERIAL PRIMARY KEY, type TEXT NOT NULL, source TEXT NOT NULL, payload JSONB NOT NULL, user_id TEXT, session_id TEXT, created_at TIMESTAMPTZ DEFAULT NOW());-- Notify on insertCREATE OR REPLACE FUNCTION notify_event() RETURNS TRIGGER AS $$BEGIN PERFORM pg_notify('events', row_to_json(NEW)::text); RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER event_notifyAFTER INSERT ON event_logFOR EACH ROW EXECUTE FUNCTION notify_event();
Option 2: Redis Streams (Production-Ready)
Copy
import { Redis } from 'ioredis';const redis = new Redis();// Publisherasync function publishEvent(event: Event) { await redis.xadd( 'events', '*', 'type', event.type, 'source', event.source, 'payload', JSON.stringify(event.payload), 'userId', event.userId || '', 'timestamp', event.timestamp );}// Consumerasync function consumeEvents(callback: (event: Event) => void) { const streams = ['events']; let lastId = '$'; // Start from now while (true) { const results = await redis.xread( 'BLOCK', 0, 'STREAMS', ...streams, lastId ); if (results) { for (const [stream, messages] of results) { for (const [id, fields] of messages) { const event = parseEvent(fields); callback(event); lastId = id; } } } }}
// Use WSS (WebSocket Secure)const ws = new WebSocket('wss://events.example.com/client', { headers: { 'Authorization': `Bearer ${authToken}` }});// Validate tokens server-sidewss.on('connection', (ws, req) => { const token = req.headers['authorization']?.split(' ')[1]; const user = verifyToken(token); if (!user) { ws.close(1008, 'Unauthorized'); return; } // Associate connection with user connections.set(user.id, ws);});
Data retention:
Copy
-- Auto-expire events after 30 daysCREATE TABLE event_log ( id SERIAL PRIMARY KEY, type TEXT NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());-- Automated cleanupCREATE INDEX idx_event_log_created_at ON event_log(created_at);-- Run dailyDELETE FROM event_log WHERE created_at < NOW() - INTERVAL '30 days';
The difference between a reactive chatbot and an always-on AI assistant isn’t just about technology—it’s about architecture.When your AI can:
See what users are doing in real-time
Know what’s happening with inventory, delivery, and systems
Anticipate needs before users get stuck
Act proactively with perfect timing
You transform the customer experience from “I hope someone helps me” to “this platform understands what I need.”The technology exists. Event streaming is proven. LLMs are powerful enough. WebSockets are mature.What matters now is implementation.Start with one use case. Get the architecture right. Measure impact. Scale systematically.The companies building this today will have a 2-3 year advantage before it becomes table stakes.Don’t wait.