Build a Multi-Agent Telecom Support System with CopilotKit & LangGraph JS

AI Summary11 min read

TL;DR

This tutorial demonstrates building a multi-agent telecom support system using CopilotKit and LangGraph JS. It automates customer service by coordinating specialized agents for intent classification, customer lookup, responses, and escalation, with real-time UI updates.

Key Takeaways

  • CopilotKit enables building AI copilots that integrate directly into applications, facilitating real-time context sharing and state synchronization between backend agents and frontend UIs.
  • The system uses four specialized agents (Intent, Customer Lookup, Reply, Escalation) to handle diverse customer queries efficiently, reducing reliance on human intervention.
  • The architecture combines Next.js for the frontend, LangGraph for stateful agent workflows, and CopilotKit for seamless communication, ensuring scalable and responsive support automation.

Tags

webdevaiprogrammingopensource

Introduction

Customer support is chaotic, and thinking about ways to automate it can be tricky. You never know what a customer will ask for, or when. To handle this, people usually rely on patterned automated systems that work based on a few conditions. If a request fits those conditions, it works fine. If not, it gets passed to a human.

That’s the problem. Customers don’t follow patterns. A simple question might need escalation, and an urgent issue might have a quick fix.

In this tutorial, we’ll build a multi-agent support system using CopilotKit and LangGraph. In this system, multiple agents will coordinate resolutions based on the customers' questions or issues.

Before we start building, let’s first understand what CopilotKit is and why it fits this problem.

What is CopilotKit?

CopilotKit is an open-source framework for building AI copilots that integrate directly into your applications. It provides UI components and infrastructure to connect backend agents with frontend UIs, enabling real-time context sharing and state synchronization.

Check out CopilotKit's GitHub ⭐️

copilotkit

In this project, it will also use AG-UI (Agentic UI) under the hood, an event-based protocol that lets agents do more than just respond in chat. Agents can trigger actions, update UI state, and interact directly with the application. For example, an agent can modify a service plan, update a customer record, or adjust billing settings through frontend tools.

CopilotKit works with multiple agent frameworks, including LangGraph, and uses AG-UI as a common event layer. If you’re new to CopilotKit, you can explore the documentation to get a better overview before continuing.

What We're Building

We’ll build a telecom support application with a chat interface that lets customers manage their services, update plans, check billing, and get instant help.

Here's what happens when a customer asks something like, "Add international calling to my plan":

multi-agent

The system features four specialized agents:

  • Intent Agent: Classifies customer messages and determines urgency level
  • Customer Lookup Agent: Retrieves customer profiles and service details from the database
  • Reply Agent: Generates personalized responses and processes service changes
  • Escalation Agent: Routes complex issues to appropriate human support teams with full context

The Tech Stack

At the core, we're using this stack for building the multi-agent support system:

  • Next.js: Frontend framework with TypeScript
  • CopilotKit SDK: Embed agents into UI (@copilotkit/react-core, @copilotkit/runtime, @copilotkit/react-ui)
  • LangGraph (StateGraph): Stateful agent workflows with coordination
  • OpenAI: LLM for reasoning and response generation
  • LangChain's OpenAI adapter: Connects OpenAI to LangChain workflows
  • Turborepo: Monorepo management for web + agent apps
  • pnpm workspaces: Package management across the monorepo

Architecture Overview

Here's the high-level architecture showing how the frontend, CopilotKit runtime, and LangGraph agents work together:

copliotkit working

The Next.js frontend communicates with LangGraph agents through the CopilotKit Runtime API route. When a customer sends a message, it flows through the runtime to the multi-agent system, where specialized agents coordinate to handle the request. State updates stream back to the UI in real-time, so customers see changes as they happen.

Project Structure

This project is organized as a monorepo. It consists of two main applications: the Next.js frontend and the LangGraph multi-agent backend, along with shared configuration files.

support-help/
├── apps/
│   ├── web/                          <- Next.js frontend application
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── page.tsx          <- Main dashboard UI
│   │   │   │   ├── layout.tsx        <- Root layout with CopilotKit provider
│   │   │   │   ├── globals.css       <- Global styles
│   │   │   │   └── api/
│   │   │   │       └── copilotkit/   <- CopilotKit runtime endpoint
│   │   │   │           └── route.ts  <- API route handler
│   │   │   ├── components/
│   │   │   │   └── WelcomeScreen.tsx  
│   │   │   ├── hooks/
│   │   │   │   └── CustomerContext.tsx <- Customer state & frontend tools
│   │   │   ├── data/
│   │   │   │   └── ticketsData.ts    <- Sample ticket data
│   │   │   └── utils/
│   │   │       └── servicePricing.ts <- Service pricing calculations
│   │   ├── package.json
│   │   └── next.config.ts
│   │
│   └── agent/                        <- LangGraph multi-agent backend
│       ├── src/
│       │   ├── agent.ts              <- Main agent workflow & graph definition
│       │   ├── index.ts              <- Entry point
│       │   ├── agents/               <- Agents
│       │   │   ├── intentAgent.ts   
│       │   │   ├── ...
│       │   ├── tools/                <- Tools
│       │   │   ├── intentClassifier.ts
│       │   │   ├── ...
│       │   ├── types/
│       │   │   └── state.ts 
│       │   └── utils/
│       │       └── dataLoader.ts
│       ├── package.json
│       └── langgraph.json            # LangGraph configuration
│
Enter fullscreen mode Exit fullscreen mode

Here's the GitHub repository if you want to see the project and explore the implementation in more detail.

Add API Key

Create a .env file under the agent directory and add your OpenAI API Key to the file.

OPENAI_API_KEY=<<your-openai-key-here>>

Let's build the frontend first.

Frontend

The frontend is in apps/web and handles the customer dashboard, chat interface, and real-time state management. We’ll connect it to the LangGraph agents using CopilotKit.

If you're starting fresh, create a new Next.js app with TypeScript:

npx create-next-app@latest web
Enter fullscreen mode Exit fullscreen mode

In our cloned repository, everything's already set up, so just install dependencies:

cd apps/web
pnpm install
Enter fullscreen mode Exit fullscreen mode

Step 1: Install CopilotKit Packages

Install the necessary CopilotKit packages:

pnpm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime

Enter fullscreen mode Exit fullscreen mode

Here's what each package does:

  • @copilotkit/react-core - Core hooks and context to connect your React app with the agent backend
  • @copilotkit/react-ui - Ready-made UI components like <CopilotSidebar /> for chat interfaces
  • @copilotkit/runtime - Server-side runtime that connects your Next.js API route to LangGraph agents

Step 2: Set Up the Root Layout

The <CopilotKit> provider needs to wrap your app. Open src/app/layout.tsx:

import type { Metadata } from "next";
import { Geist } from "next/font/google";
import "./globals.css";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";

// ...
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={geist.className}>
        <CopilotKit runtimeUrl="/api/copilotkit">
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}

Enter fullscreen mode Exit fullscreen mode

The runtimeUrl specifies where CopilotKit can find your agent endpoint. It is also the way to securely communicate on the backend. We'll create that next.

Step 3: Create the CopilotKit Runtime Endpoint

Create src/app/api/copilotkit/route.ts. This is the bridge between your frontend and the LangGraph agent:

import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";

// 1. You can use any service adapter here for multi-agent support.
const serviceAdapter = new ExperimentalEmptyAdapter();

// 2. Create the CopilotRuntime instance and utilize the LangGraph AG-UI
//    integration to set up the connection.
const runtime = new CopilotRuntime({
  agents: {
    starterAgent: new LangGraphAgent({
      deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
      graphId: "starterAgent", // <- Graph id will be the agent name that we'll use next in our context file when we'll define the Agent using useAgent hook
      langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
    })
  }
});

// 3. Build a Next.js API route to handle CopilotKit runtime requests.
export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

Enter fullscreen mode Exit fullscreen mode

This API route proxies requests between the frontend and your LangGraph agent, handling authentication and keeping API keys secure on the server.

CopilotRuntime manages agent communication, OpenAIAdapter connects to GPT model, and copilotRuntimeNextJSAppRouterEndpoint processes and routes incoming requests.

Step 4: Create Customer Context with Frontend Tools

Here's the thing about building with CopilotKit: if you want agents to actually modify your UI in real-time, your state management needs to be solid. Scattering state across multiple files or skipping the global state entirely makes it nearly impossible for agents to properly update the UI.

In this system, we're using React Context to centralize customer data and expose it to agents through useAgent and useFrontendTool. This pattern ensures that agents and UI components use the same source of truth and supports bidirectional state synchronization.

Create src/hooks/CustomerContext.tsx. This is where we centralize customer state and expose tools that agents can call to modify the UI.

First, set up the basic structure:

"use client";

import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from "react";
import { useAgent, useFrontendTool } from "@copilotkit/react-core/v2";
import { initialCustomers } from "@/data/ticketsData";

interface Customer {
  id: number;
  customerID: string;
  gender: string;
  SeniorCitizen: string;
  Partner: "Yes" | "No";
  MonthlyCharges: string;
  InternetService: "DSL" | "Fiber optic";
  PaperlessBilling: "Yes" | "No";
  StreamingTV: "Yes" | "No";
  // ... other service fields
}

type AgentState = {
  customers: Customer[];
};

const CustomerContext = createContext<CustomerContextType | undefined>(undefined);

export function CustomerProvider({ children }: { children: React.ReactNode }) {
  // Use useAgent for bidirectional state synchronization with the agent
  const { agent } = useAgent({ agentId: "starterAgent" }) // <- Agent name same as graphId

  // Initialize agent state if not already set
  useEffect(() => {
    if (!agent.state.customers) {
      agent.setState({
        ...agent.state,
        customers: initialCustomers,
      });
    }
  }, [agent]);

  // Optimistic local state with agent sync
  const [localCustomers, setLocalCustomers] = useState<Customer[] | null>(null);

  // Derive customers: local override takes precedence over agent state
  const customers = localCustomers ?? agent.state.customers ?? [];

  // Keep a ref to always have the latest customers in closures
  const customersRef = useRef<Customer[]>([]);
  useEffect(() => {
    customersRef.current = customers;
  }, [customers]);

Enter fullscreen mode Exit fullscreen mode

Next, define helper functions for state updates:

These functions handle the actual logic for modifying customer data, adding or removing services, or updating settings.

const recalculateCharges = useCallback((customer: Customer) => {
    const calculation = calculateMonthlyCharges(customer);
    const monthlyCharges = calculation.total;
    const tenure = parseInt(customer.tenure) || 0;
    const totalCharges = monthlyCharges * tenure;
    return { monthlyCharges, totalCharges };
  }, []);

  // Helper: Add a service addon to a customer
  const addAddon = useCallback((customerId: string, addon: AddonService) => {
    let updatedCustomer: Customer | null = null;

    const updatedCustomers = customersRef.current.map((customer) => {
      if (customer.customerID === customerId) {
        const updates: Partial<Customer> = {};

        // Handle service dependencies (e.g., PhoneService required for MultipleLines)
        if (addon === "MultipleLines" && customer.PhoneService === "No") {
          return customer; // Can't add MultipleLines without PhoneService
        }

        updates[addon] = "Yes";
        const updated = { ...customer, ...updates };

        // Recalculate charges automatically
        const { monthlyCharges, totalCharges } = recalculateCharges(updated);
        updated.MonthlyCharges = monthlyCharges.toFixed(2);
        updated.TotalCharges = totalCharges.toFixed(2);

        updatedCustomer = updated;
        return updated;
      }
      return customer;
    });

    if (updatedCustomer) {
      // Update BOTH local and agent state
      setLocalCustomers(updatedCustomers);
      agent.setState({ ...agent.state, customers: updatedCustomers });
    }

    return updatedCustomer;
  }, [recalculateCharges, agent]);

  // Helper: Update customer settings
  const updateCustomer = useCallback((customerId: number, updates: Partial<Customer>) => {
    let updatedCustomer: Customer | null = null;

    const updatedCustomers = customers.map((customer) => {
      if (customer.id === customerId) {
        const updated = { ...customer, ...updates };

        // Handle service dependencies
        if (updates.PhoneService === "No") {
          updated.MultipleLines = "No phone service";
        }

        // Recalculate charges if any service changed
        const { monthlyCharges, totalCharges } = recalculateCharges(updated);
        updated.MonthlyCharges = monthlyCharges.toFixed(2);
        updated.TotalCharges = totalCharges.toFixed(2);

        updatedCustomer = updated;
        return updated;
      }
      return customer;
    });

    setLocalCustomers(updatedCustomers);
    agent.setState({ ...agent.state, customers: updatedCustomers });
    return updatedCustomer;
  }, [customers, recalculateCharges, agent]);

Enter fullscreen mode Exit fullscreen mode

Define the first frontend tool: Add Addon

Use useFrontendTool to create a tool that agents can call. The handler executes our addAddon function when the agent needs to add a service.

// Frontend Tool: Add service addon
  useFrontendTool({
    name: "addAddonToCustomer",
    description: "Add a service addon to a customer. This will enable a specific service for the customer and recalculate their monthly charges automatically.",
    parameters: [
      {
        name: "customerID",
        type: "string",
        description: "The unique customer ID (e.g., '5575-GNVDE', '7590-VHVEG')",
        required: true,
      },
      {
        name: "addonName",
        type: "string",
        description: "The name of the addon service to add. Valid options: PhoneService, MultipleLines, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies",
        required: true,
      },
    ],
    handler: async ({ customerID, addonName }) => {
      // Access current state direc

Visit Website