DEV Community

Cover image for Building a Production-Ready AI Customer Service Agent with HazelJS
Muhammad Arslan
Muhammad Arslan

Posted on • Edited on

Building a Production-Ready AI Customer Service Agent with HazelJS

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
  ) {}
Enter fullscreen mode Exit fullscreen mode

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, ... };
}
Enter fullscreen mode Exit fullscreen mode

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 })), ... };
}
Enter fullscreen mode Exit fullscreen mode

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_KEY for production; optionally PINECONE_INDEX, PINECONE_ENVIRONMENT
  • Qdrant — Set QDRANT_URL (and optionally QDRANT_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

  • RESTPOST /api/csr/chat for synchronous chat, POST /api/csr/ingest for document ingestion
  • SSEPOST /api/csr/chat/stream for server-sent events
  • WebSocket — Real-time chat at ws://localhost:3001/csr via @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 checksGET /api/csr/health for monitoring
  • Observability — Event system for execution, steps, and tool calls
  • Optional vector stores — Pinecone or Qdrant for production RAG; set PINECONE_API_KEY or QDRANT_URL to 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
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. 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
Enter fullscreen mode Exit fullscreen mode

For production RAG, set PINECONE_API_KEY (or QDRANT_URL) and install the optional vector store package. See .env.example for full config.

  1. Explore the packages:

  2. 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)