This tutorial walks through building a customer support chatbot that combines two NoLag Blueprints: @nolag/chat for the user-facing interface and @nolag/agents for AI-powered response generation. The result is a production-ready chatbot with typing indicators, conversation history, sentiment-based escalation, and full observability.
Architecture Overview
The system has three components:
- Chat frontend - Users interact with a standard chat room powered by
@nolag/chat. They see typing indicators, message history, and presence. - Bridge service - A server-side process that connects to both the chat app and the agents app. It forwards user messages to the agent workflow and sends responses back to chat.
- AI workers - Agent processes that receive tasks via the Handoff pattern, call an LLM, and return responses. They can be scaled horizontally.
Step 1: Install Dependencies
npm install @nolag/chat @nolag/agents @nolag/js-sdkStep 2: Set Up Apps in the Portal
Create two apps in your NoLag project:
- A Chat Blueprint app (slug:
support-chat) - An Agents Blueprint app (slug:
support-agents)
Create three actors:
- Bridge actor - granted access to both apps
- Worker actor - granted access to the agents app
- User actors - granted access to the chat app (one per user, or use dynamic tokens)
Step 3: Build the Bridge
The bridge is the key piece. It connects to both apps simultaneously and translates between chat messages and agent tasks:
import { NoLagChat } from "@nolag/chat";
import { NoLagAgents } from "@nolag/agents";
// The bridge actor connects to both apps
const chat = new NoLagChat(BRIDGE_CHAT_TOKEN, {
user: { id: "bot", name: "Support Bot", avatar: "/bot.png" },
});
const agents = new NoLagAgents(BRIDGE_AGENT_TOKEN);
await Promise.all([chat.connect(), agents.connect()]);
const chatRoom = chat.joinRoom("support");
const agentRoom = agents.joinRoom("support-workflow");
// Forward user messages to the agent workflow
chatRoom.on("message", async (msg) => {
// Don't process our own messages
if (msg.sender.id === "bot") return;
// Show typing indicator while agent works
chatRoom.startTyping();
await agentRoom.handoff.dispatch({
type: "respond",
payload: {
userMessage: msg.text,
userId: msg.sender.id,
history: chatRoom.getMessages().slice(-10),
},
capabilities: ["customer-support"],
});
});
// Forward agent responses back to chat
agentRoom.handoff.onResult(async (result) => {
chatRoom.stopTyping();
await chatRoom.sendMessage(result.data.response);
});The bridge shows typing indicators while the agent is processing, giving users a natural chat experience. Message history is passed to the agent so it has conversation context.
Step 4: Build the AI Worker
Workers register their capabilities and process incoming tasks. You can use any LLM - OpenAI, Anthropic, a local model, or a custom pipeline:
import { NoLagAgents } from "@nolag/agents";
const agents = new NoLagAgents(WORKER_TOKEN);
await agents.connect();
const room = agents.joinRoom("support-workflow");
room.handoff.register({
capabilities: ["customer-support"],
concurrency: 5,
});
room.handoff.onTask(async (task) => {
const { userMessage, history } = task.payload;
// Call your LLM of choice
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{ role: "system", content: "You are a helpful customer support agent." },
...history.map((m: any) => ({
role: m.sender.id === "bot" ? "assistant" : "user",
content: m.text,
})),
{ role: "user", content: userMessage },
],
});
await task.complete({
response: response.choices[0].message.content,
});
});Workers are stateless and horizontally scalable. Run multiple instances and they'll automatically load-balance tasks via the Handoff pattern.
Step 5: Add Escalation
Use the Approve pattern to escalate sensitive conversations to a human agent. The worker detects negative sentiment and requests approval before responding:
// In the worker, detect when to escalate
room.handoff.onTask(async (task) => {
const { userMessage } = task.payload;
const sentiment = await analyzeSentiment(userMessage);
if (sentiment.score < -0.7) {
// Escalate: request human approval before responding
const approval = await room.approve.request({
action: "escalate-to-human",
description: "Negative sentiment detected. Review before responding.",
payload: { userMessage, suggestedResponse: "..." },
});
if (approval.approved) {
// Human may have modified the response
await task.complete({
response: approval.modifications?.response || approval.payload.suggestedResponse,
});
}
} else {
// Normal LLM response
const response = await generateResponse(task.payload);
await task.complete({ response });
}
});Step 6: Monitor Everything
The Observe pattern gives you full visibility into the chatbot's performance:
const room = agents.joinRoom("support-workflow");
// Track metrics
let totalQueries = 0;
let avgResponseTime = 0;
room.observe.on("task:completed", (event) => {
totalQueries++;
avgResponseTime = (avgResponseTime * (totalQueries - 1) + event.data.duration) / totalQueries;
console.log(`Avg response: ${avgResponseTime}ms | Total: ${totalQueries}`);
});
room.observe.on("task:failed", (event) => {
console.error("Agent failed:", event.data.error);
});
// Monitor agent health
const onlineAgents = room.getPresence();
console.log("Workers online:", onlineAgents.length);What You Get
- A chat interface with typing indicators and presence - users don't know they're talking to an AI
- Horizontally scalable AI workers with automatic load balancing
- Human escalation for sensitive conversations
- Full observability: response times, error rates, agent health
- Persistent message history with replay on reconnect
- Per-topic access control separating user traffic from agent traffic
The entire system runs on NoLag's real-time infrastructure. No Redis queues, no custom WebSocket servers, no separate monitoring stack.