How we built a full-fledged Agent CSR (Customer Service Representative) with AI, RAG, memory, and real-time streaming—and why HazelJS is the right framework for AI-native applications.
If you like it don't forget to Star HazelJS repository
The Use Case: AI-Powered Customer Support
Customer support teams face a constant challenge: scaling personalized, accurate assistance while keeping costs manageable. Customers expect instant answers about orders, refunds, policies, and product availability—often outside business hours. Traditional chatbots fall short because they lack:
- Context — No memory of prior conversations
- Knowledge — No access to internal docs, FAQs, or policies
- Actions — No ability to look up orders, process refunds, or create tickets
- Safety — No human-in-the-loop for sensitive operations like refunds
Our goal was to build an Agent CSR: an AI-powered customer service representative that combines large language models (LLMs) with retrieval-augmented generation (RAG), persistent memory, and a tool system—all production-ready with rate limiting, circuit breakers, and observability.
Where HazelJS Stands
HazelJS is a modern, TypeScript-first Node.js framework designed for building scalable server-side applications. Unlike generic backend frameworks, HazelJS was built with AI-native applications in mind from day one.
The AI-Native Framework Gap
Most Node.js frameworks (Express, Fastify, NestJS) treat AI as an afterthought. You bolt on OpenAI SDKs, wire up vector databases manually, and build agent orchestration from scratch. HazelJS takes a different approach:
| Concern | Traditional Framework | HazelJS |
|---|---|---|
| LLM Integration | Manual SDK wiring | Built-in @hazeljs/ai with OpenAI, Anthropic, Gemini, Cohere, Ollama |
| Agent Runtime | Custom implementation | @hazeljs/agent with state machine, tools, memory |
| RAG / Vector Search | DIY pipelines | @hazeljs/rag with Pinecone, Weaviate, Qdrant, ChromaDB |
| Memory | Ad-hoc storage | Conversation, entity, and semantic memory in @hazeljs/rag |
| Real-time | Socket.io or custom | @hazeljs/websocket with decorators |
| Background Jobs | Bull/Agenda setup | @hazeljs/queue with BullMQ |
HazelJS provides a modular, decorator-based architecture that lets you compose AI capabilities the same way you compose REST controllers and services. The Agent Runtime documentation describes it as "stateful, long-running agents with tools, memory, and human-in-the-loop"—exactly what we needed.
What We Built: The Agent CSR Example
The hazeljs-csr-agent demonstrates a complete AI customer support agent. Here's the architecture:
1. The CSR Agent
The agent is a single TypeScript class decorated with @Agent and @Tool from @hazeljs/agent. No boilerplate—just inject your business services and define tools declaratively.
Agent configuration — The @Agent decorator configures the agent’s behavior:
@Agent({
name: 'csr-agent',
description: 'AI-powered customer support agent',
systemPrompt: CSR_SYSTEM_PROMPT,
enableMemory: true,
enableRAG: true,
ragTopK: 5,
maxSteps: 15,
temperature: 0.7,
})
export class CSRAgent {
constructor(
private orderService: OrderService,
private inventoryService: InventoryService,
private refundService: RefundService,
private ticketService: TicketService,
private ragService: RAGService,
private queueService?: QueueService
) {}
The constructor injects domain services (orders, inventory, refunds, tickets), the RAG service for knowledge search, and an optional queue for async processing. The runtime wires these in when the agent is registered.
Tools — Each tool is a method decorated with @Tool. Parameters are declared once; the runtime turns them into LLM function schemas and validates inputs:
| Tool | Purpose | Approval |
|---|---|---|
| lookupOrder | Fetch order status, items, tracking | No |
| checkInventory | Check product availability and restock dates | No |
| processRefund | Process refunds (amount, reason) | Yes |
| updateShippingAddress | Change shipping address | Yes |
| createTicket | Create support tickets; optionally enqueues via @hazeljs/queue | No |
| searchKnowledgeBase | RAG-powered FAQ and policy search via @hazeljs/rag | No |
Human-in-the-loop — Tools that change data or money use requiresApproval: true:
@Tool({
description: 'Process a refund for an order',
requiresApproval: true,
timeout: 60000,
parameters: [
{ name: 'orderId', type: 'string', required: true },
{ name: 'amount', type: 'number', required: true },
{ name: 'reason', type: 'string', required: true },
],
})
async processRefund(input: { orderId: string; amount: number; reason: string }) {
const refund = await this.refundService.process(input);
return { success: true, refundId: refund.id, ... };
}
When the LLM calls processRefund, the runtime pauses and emits a TOOL_APPROVAL_REQUESTED event. Your backend calls POST /api/csr/approve with the request ID to approve or reject before the tool runs.
RAG integration — The searchKnowledgeBase tool calls ragService.search() with the user’s query. Results are returned to the LLM, which cites them in its answer:
async searchKnowledgeBase(input: { query: string; topK?: number }) {
const results = await this.ragService.search(input.query, {
topK: input.topK || 5,
includeMetadata: true,
minScore: 0.5,
});
return { success: true, documents: results.map(r => ({ id: r.id, content: r.content, score: r.score })), ... };
}
Optional queue — If queueService is configured (Redis), createTicket enqueues a job for async follow-up (e.g., notifications). If Redis is unavailable, the ticket is still created; the queue call is wrapped in try/catch.
2. RAG for Knowledge Retrieval
We use @hazeljs/rag to index FAQs, policies, and documentation. The agent retrieves relevant chunks before answering, so responses are grounded in your actual content—not hallucinated. The example supports pluggable vector stores via environment variables:
- In-memory (default) — No config; ideal for development and demos
-
Pinecone — Set
PINECONE_API_KEYfor production; optionallyPINECONE_INDEX,PINECONE_ENVIRONMENT -
Qdrant — Set
QDRANT_URL(and optionallyQDRANT_COLLECTION) when Pinecone is not configured
If PINECONE_API_KEY is set, the example uses Pinecone. Otherwise, if QDRANT_URL is set, it uses Qdrant. If neither is set, it falls back to in-memory storage. Install optional deps as needed: @pinecone-database/pinecone or @qdrant/js-client-rest. The RAG package documentation covers all vector stores, embeddings (OpenAI, Cohere, HuggingFace), and retrieval strategies (similarity, MMR, hybrid).
3. Memory for Conversation Context
Conversation memory is handled by MemoryManager in @hazeljs/rag. The agent remembers prior messages, entities (e.g., customer name, order IDs), and working memory within a session. For development we use BufferMemory; for production you can switch to HybridMemory (buffer + vector store) for long-term semantic search across past conversations.
4. REST + WebSocket + SSE
-
REST —
POST /api/csr/chatfor synchronous chat,POST /api/csr/ingestfor document ingestion -
SSE —
POST /api/csr/chat/streamfor server-sent events -
WebSocket — Real-time chat at
ws://localhost:3001/csrvia @hazeljs/websocket
5. Production Features
The Agent Runtime ships with production-ready features out of the box:
- Rate limiting — Token bucket (e.g., 60 requests/minute)
- Circuit breaker — Stops cascading failures when LLM or RAG is down
- Retry logic — Exponential backoff for transient errors
-
Health checks —
GET /api/csr/healthfor monitoring - Observability — Event system for execution, steps, and tool calls
-
Optional vector stores — Pinecone or Qdrant for production RAG; set
PINECONE_API_KEYorQDRANT_URLto switch from in-memory
Benefits of HazelJS for AI Applications
1. First-Class AI Primitives
HazelJS doesn't treat AI as a plugin. The @hazeljs/ai package provides a unified interface for multiple providers, streaming, caching, and function calling. The @hazeljs/agent package gives you a full agent runtime—state machine, tool execution, approval workflows—without building it yourself.
2. Modular, Composable Architecture
Install only what you need:
npm install @hazeljs/core @hazeljs/ai @hazeljs/agent @hazeljs/rag
Add @hazeljs/queue for async jobs, @hazeljs/websocket for real-time, @hazeljs/cache for response caching. The HazelJS documentation explains the module system and how to compose features.
3. Type Safety and Decorators
TypeScript and decorators keep the codebase clean and maintainable. Define agents and tools declaratively—no manual tool schemas, parameter validation, or LLM function formatting:
@Agent({ name: 'csr-agent', enableMemory: true, enableRAG: true })
export class CSRAgent {
@Tool({
description: 'Look up order by order ID',
parameters: [{ name: 'orderId', type: 'string', description: 'Order ID (ORD-XXXXX)', required: true }],
})
async lookupOrder(input: { orderId: string }) {
const order = await this.orderService.findById(input.orderId);
return order ? { found: true, status: order.status, items: order.items, ... } : { found: false };
}
}
The runtime infers tool schemas from the decorator metadata and validates inputs before invoking the method.
4. Production-Ready by Default
Rate limiting, circuit breakers, retries, and health checks are built into the agent runtime. The Production Readiness guide covers Redis state persistence, metrics, and deployment patterns.
If you like it don't forget to Star HazelJS repository
5. Lighter Than NestJS, Richer Than Express
Compared to NestJS, HazelJS has a smaller footprint and a simpler learning curve. Compared to Express, it adds dependency injection, decorators, validation, and built-in AI. The HazelJS comparison section highlights these trade-offs.
Getting Started
- Clone and run the example:
git clone https://github.com/hazel-js/hazeljs-csr-agent.git
cd hazeljs-csr-agent
npm install
export OPENAI_API_KEY=your-key
npm run dev
For production RAG, set PINECONE_API_KEY (or QDRANT_URL) and install the optional vector store package. See .env.example for full config.
-
Explore the packages:
- @hazeljs/core — Framework core
- @hazeljs/ai — AI providers
- @hazeljs/agent — Agent runtime
- @hazeljs/rag — RAG and memory
-
Read the docs:
Summary
Building an AI-powered customer service agent requires more than an LLM API—it needs RAG for knowledge, memory for context, tools for actions, and production-grade resilience. HazelJS provides these primitives in a cohesive, TypeScript-first framework. The Agent CSR example shows how to combine @hazeljs/agent, @hazeljs/ai, @hazeljs/rag, @hazeljs/queue, and @hazeljs/websocket into a production-ready application.
If you're building AI-native backends, give HazelJS a try—and join the community on GitHub or clone the CSR agent example to get started.
Top comments (0)