Overview
@yuaskme/chatbot-sdk is a backend framework for real-time chatbots on Socket.IO. Every message passes through an ordered pipeline of small async steps (classify, check rules, add context, call the AI, act on the result), so each concern stays separate and testable.
- Guards run before the AI and can stop the pipeline, for example when the store is closed.
- Enrichers add live data such as a cart or loyalty tier to the prompt.
- Dispatchers run after the AI replies, to save an order or show a booking form.
- A session store keeps per-customer state across turns, with no extra infrastructure.
Source: GitHub · npm · ISC license
Install
npm install @yuaskme/chatbot-sdk socket.ioQuick start
A server with one guard, one enricher, and one dispatcher:
import http from "http";
import express from "express";
import {
createChatbotSocket,
createSessionStore,
buildGuardRegistry,
buildEnricherRegistry,
buildDispatcherRegistry,
runGuards,
runEnrichers,
runDispatcher,
} from "@yuaskme/chatbot-sdk";
const sessionStore = { ...createSessionStore(), cartItems: new Map() };
const outsideHoursGuard = async ({ socket }) => {
const hour = new Date().getHours();
if (hour < 8 || hour >= 20) {
socket.emit("message", { message: "We are open 8 AM – 8 PM." });
return true; // stop the pipeline
}
return false;
};
const loyaltyEnricher = async ({ socket, sessionStore }) => {
const tier = sessionStore.loyaltyTier?.get(socket.id);
return tier ? { systemNote: `Customer is a ${tier} loyalty member.` } : null;
};
const bookingDispatcher = async ({ socket, responseData }) => {
socket.emit("show_booking_form", { prefill: responseData?.extracted });
};
const pipeline = [
async (ctx) => {
const category = ctx.message.category ?? "general";
const PromptIntent = await myAI.classify(ctx.message.message, category);
return { ...ctx, PromptIntent, category };
},
async (ctx) => {
const handled = await runGuards(ctx.guardRegistry[ctx.category] ?? [], ctx);
return handled ? { ...ctx, done: true } : ctx;
},
async (ctx) => {
const enrichments = await runEnrichers(ctx.enricherRegistry[ctx.category] ?? [], ctx);
return { ...ctx, enrichments };
},
async (ctx) => {
const reply = await myAI.respond(ctx.message.message, ctx.PromptIntent.Intent);
ctx.socket.emit("message", { message: reply });
return { ...ctx, responseData: { text: reply } };
},
async (ctx) => {
ctx.sessionStore.lastIntent.set(ctx.socket.id, ctx.PromptIntent.Intent);
await runDispatcher(ctx.dispatcherRegistry, ctx.PromptIntent.Intent, ctx);
return ctx;
},
];
const server = http.createServer(express());
createChatbotSocket(server, {
pipeline,
sessionStore,
guardRegistry: buildGuardRegistry([{ for: "general", guard: outsideHoursGuard }]),
enricherRegistry: buildEnricherRegistry([{ for: "general", enricher: loyaltyEnricher }]),
dispatcherRegistry: buildDispatcherRegistry([
{ intent: "booking", dispatcher: bookingDispatcher },
{ intent: "default", dispatcher: async () => {} },
]),
corsOptions: { origin: "http://localhost:5173" },
onConnection: (io, socket, sessionStore) => {
socket.on("disconnect", () => sessionStore.cartItems.delete(socket.id));
},
});
server.listen(3001);myAI stands in for your own AI calls; the SDK doesn't pick a provider for you.
Concepts
Pipeline
An ordered array of async (ctx) => ctx steps. Each step can add fields to the context for later steps. Setting ctx.done = true stops the rest. A step that returns nothing throws an error naming the step.
Guards
Run per category, in the order you register them. The first guard that returns true stops the pipeline. Check sessionStore.lastIntent to protect multi-step flows such as order confirmation.
Enrichers
Every enricher for a category runs; their non-null results are collected into an array you add to the prompt.
Dispatchers
Map intents to side effects. Several intents can share a dispatcher, and "default" catches anything unmatched.
Session store
One store of Maps keyed by socket.id, shared by all connections. The SDK initializes its own Maps on connect and clears them on disconnect; clear any Maps you add inside onConnection's disconnect handler, which runs before the SDK cleanup.
Name gate
Pass extractName and set sessionStore.awaitingName for a socket to collect the customer's name before the conversation continues. The SDK emits name_updated once a name is confirmed.
API reference
createChatbotSocket(httpServer, config)
Creates a Socket.IO server wired to your pipeline and returns it.
| Option | Type | Default | Description |
|---|---|---|---|
pipeline | StepFn[] | [] | Ordered pipeline steps |
sessionStore | SessionStore | auto | Share it when you add your own Maps |
guardRegistry | object | {} | From buildGuardRegistry() |
enricherRegistry | object | {} | From buildEnricherRegistry() |
dispatcherRegistry | object | {} | From buildDispatcherRegistry() |
chatEvent | string | "chat" | Socket event for incoming messages |
extractName | async fn | — | Enables the name gate |
nameGateMessages | object | — | Override the name gate prompts |
onError | async fn | — | (error, ctx) => void |
corsOptions | object | {} | Merged into Socket.IO CORS settings |
maxHttpBufferSize | number | 5e6 | Max message size in bytes |
onConnection | fn | — | (io, socket, sessionStore) => void |
Other exports
| Export | Use |
|---|---|
buildGuardRegistry / buildEnricherRegistry / buildDispatcherRegistry | Build the registries |
runGuards / runEnrichers / runDispatcher | Run a registry from inside a step |
createSessionStore() | A fresh store of Maps keyed by socket.id |
initSocketSession / cleanupSocketSession | Set up or clear one socket (automatic with createChatbotSocket) |
createChatbotHandler(...) | One message handler without creating a server |
runPipeline(steps, ctx, onError) | Run a steps array directly |
withNameGate(socket, handler, options) | Wrap any handler with the name gate |
React chat widget
The repository's examples/react folder has a ChatWidget component and a useChatbot hook that manage the socket connection, messages, and typing state:
<ChatWidget
serverUrl="http://localhost:3001"
identifier="my-workspace"
language="english"
title="Chat with us"
placeholder="Ask me anything…"
/>Something unclear or out of date? Report a docs issue.
YuAskMe