A / APR 6, 2026



Building a Production-Ready Composable AI Agent System with CopilotKit and LangGraph

Building a Production-Ready Composable AI Agent System with CopilotKit and LangGraph

By Ayush Gupta • 11 min read

Introduction

Building AI agents is one thing. Building agents that actually work together in a real application? That's where it gets tricky.
Today, we're going to build a composable multi-agent system that combines three specialized agents - a Summarizer, a Q&A engine, and a Code Generator - into a single, coordinated workflow. We'll use Next.js for the frontend, LangGraph for agent orchestration, and CopilotKit to wire everything together with a beautiful, real-time UI.
You'll find architecture, the key patterns, how state flows between agents, and the step-by-step guide to building this from scratch.
Let's build it.
Check out the full source code on GitHub and the CopilotKit GitHub ⭐️

What is CopilotKit?

CopilotKit is an open-source framework that makes it easy for developers to add AI copilots right into web applications. It has tools for connecting LLMs (like the OpenAI models) to React or Next.js components so that users interact with AI directly in your application - through chat, forms, or even in live dashboards.
CopilotKit enables:

  • Contextually aware copilots that understand user state and application context.
  • Real time streaming between the front-end and the AI models.
  • Composable agent architecture, to allow you to plug-and-play various AI modules to act as summarizers, analyzers, or generators.
  • Developer-first APIs for easy, customizable integration with React and Next.js. In short, CopilotKit bridges your frontend UI and the backend AI - creating an intelligent layer for modern web applications. Visit the official documentation

What are we building?

We are constructing a complete stack of an agent system (i.e., full-stack composable agent system where multiple agents with specific abilities can work together in a chain [or pipeline]).
Below is an illustration of how the different agents operate as part of the full pipeline sequence. When a user requests an action from the system, the action would follow this simplified flow:

[User Enters Prompt]
↓
Next.js UI (CopilotChat)
↓
(POST /api/copilotkit → GraphQL)
Next.js API
↓
(LangGraphHttpAgent forwards request to FastAPI)
FastAPI (copilotkit + LangGraph)
↓
(LangGraphAgent Workflow: Summarizer → Q&A → CodeGen)
↓
(Invoke OpenAI GPT-4o-mini)
↓
Streams to the front-end UI producing real-time updates.
Enter fullscreen mode Exit fullscreen mode


The beauty of the composable design is that the agents are completely independent of one another. Therefore, agents can easily be swapped, re-ordered, or added without impacting the other agents in the architecture.
The Architecture and Tech Stack

At the core, we are using this stack:

  • Next.js 14 with App Router and TypeScript on the frontend
  • CopilotKit's SDK embed agents in the UI: @copilotkit/react-core, @copilotkit/runtime, @copilotkit/react-ui
  • FastAPI & Uvicorn as the backend framework for serving agents
  • LangGraph for building stateless agent workflows
  • LangChain for orchestrating the LLM C/API and handling messages for the LLM from the client
  • OpenAI GPT-4o-mini LLM used for serving agent outputs to their corresponding input requests, or reasoning, and producing them; and
  • LangServe for serving LangChain runnables as REST-style APIs
  • Tailwind CSS & Framer Motion for developing glassmorphic UIs and incorporating animations.

High-level architecture.

Motivations for Using Composable Agents

Prior to delving into source code, let's talk about why we may need this pattern.
Monolithic Agents Are a Bad Idea
Monolithic agent systems traditionally look like the following:

def handle_request(user_input):
    # One giant function doing everything
    summary = summarize(user_input)
    analysis = analyze(summary)
    code = generate_code(analysis)
    return code
Enter fullscreen mode Exit fullscreen mode


While this approach might suffice for demonstration purposes, it does not work in production due to many limitations such as:

  • Difficulty in testing individual functions.
  • Difficulty in debugging when things go wrong.
  • Inability to utilize agents across multiple workflows.
  • Requires the complete rewrite of the entire system if one wants to scale or upgrade the current configuration.

The Composable Approach to Problem Solving

LangGraph defines each agent as an independent node.

workflow = StateGraph(AgentState)

# Each agent is a separate, testable node
workflow.add_node("summarizer", summarizer_node)
workflow.add_node("qna", qna_node)
workflow.add_node("codegen", codegen_node)

# Define the flow
workflow.set_entry_point("summarizer")
workflow.add_edge("summarizer", "qna")
workflow.add_edge("qna", "codegen")
workflow.add_edge("codegen", END)

Enter fullscreen mode Exit fullscreen mode

This means that:

  • Each agent is independently verifiable.
  • You can easily trace back through the flow of the sequence from any point on the workflow if an error is encountered.
  • You can utilize agents in many different workflows.
  • You can add additional agents without affecting existing agents.

4. Project Setup

Prerequisites

  • Node.js 18+ and npm
  • Python 3.9+
  • OpenAI API key

Installation

Frontend Setup


# Create Next.js app
npx create-next-app@latest composable-copilotkit-app --typescript --tailwind --app

cd composable-copilotkit-app

# Install CopilotKit dependencies
npm install@copilotkit/react-core@^1.51.3\
@copilotkit/react-ui@^1.51.3\
@copilotkit/runtime@^1.51.3\
framer-motion@^11.0.3
Enter fullscreen mode Exit fullscreen mode


Backend Setup


# Create agent directory
mkdir agent
cd agent

# Create virtual environment
python -m venv venv

# Activate (Windows)
venv\Scripts\activate

# Activate (macOS/Linux)
source venv/bin/activate

# Install dependencies
pip install \
langchain==0.3.13\
langchain-openai==0.2.14\
langgraph==0.2.62\
copilotkit>=0.1.39\
fastapi==0.115.6\
uvicorn==0.34.0\
python-dotenv==1.0.1
Enter fullscreen mode Exit fullscreen mode

Project Structure


composable-copilotkit-app/
├── app/
│   ├── api/
│   │   └── copilotkit/
│   │       └── route.ts          # CopilotKit API endpoint
│   ├── globals.css               # Glassmorphic styles
│   ├── layout.tsx                # Root layout with CopilotKit provider
│   └── page.tsx                  # Main UI
├── components/
│   └── LangGraphAgent.tsx        # Agent wrapper
├── agent/
│   ├── agent.py                  # LangGraph workflow
│   ├── server.py                 # FastAPI server
│   └── requirements.txt
└── package.json
Enter fullscreen mode Exit fullscreen mode


Add Environment Variables
Create .env.local in the root:

env

OPENAI_API_KEY=your_openai_api_key_here
LANGGRAPH_URL=http://127.0.0.1:8000/copilotkit
Enter fullscreen mode Exit fullscreen mode


Create agent/.env:

env

OPENAI_API_KEY=your_openai_api_key_here
Enter fullscreen mode Exit fullscreen mode

5. Frontend: Wiring the Agent to the UI

Step 1. Install and configure the CopilotKit provider in
app/layout.tsx as follows:

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

const outfit = Outfit({
  subsets: ['latin'],
  display: 'swap',
})

export const metadata: Metadata = {
  title: 'Composable CopilotKit: Modular Agent App',
  description: 'Multi-agent system with CopilotKit & LangGraph',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={outfit.className}>
        <CopilotKit
          runtimeUrl="/api/copilotkit"
          agent="researcher"
        >
          {children}
        </CopilotKit>
      </body>
    </html>
  )
}
Enter fullscreen mode Exit fullscreen mode


Important notes:

  • Here we specify our Next.js API route with runtimeUrl="/api/copilotkit"
  • The name of the agent we want to use is indicative of the agent we created in our API route, through the agent="researcher" property.

Step 2: Next.js API Integration: Route to FastAPI
To accomplish this, create a Next.js Next API Route at app/api/copilotkit/route.ts:

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

export const dynamic = "force-dynamic";

const runtime = new CopilotRuntime({
    remoteEndpoints: [
        copilotKitEndpoint({ url: "http://127.0.0.1:8000/copilotkit" }),
    ],
});

const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter: new EmptyAdapter(),
    endpoint: "/api/copilotkit",
});

export const GET = handleRequest;
export const POST = handleRequest;

Enter fullscreen mode Exit fullscreen mode


This route will allow you to:

  • Create a new CopilotRuntime using your LangGraph agent.
  • Use your LangGraphHttpAgent to communicate with your FastAPI back-end.
  • Service both GET and POST requests for streaming purpose.

Step 3: Building the Chat UI

Create the main page in app/page.tsx:


'use client';

import React from 'react';
import {CopilotChat }from"@copilotkit/react-ui";
import {motion }from'framer-motion';
import"@copilotkit/react-ui/styles.css";

export default function Home() {
return (
<div className="flex h-screen w-full overflow-hidden">
      {/* Animated Background */}
<divclassName="bg-mesh"/>

      {/* Glassmorphic Sidebar */}
<motion.aside
initial={{x:-20,opacity:0 }}
animate={{x:0,opacity:1 }}
className="w-80 glass-sidebar flex flex-col hidden md:flex z-20 m-4 rounded-[2rem]"
>
<divclassName="p-8 border-b border-white/40">
<h1className="font-extrabold text-xl">Composable</h1>
<pclassName="text-xs text-violet-600 font-bold">COPILOTKIT</p>
</div>

<divclassName="p-8 flex-1">
<h2className="text-xs font-bold text-slate-400 uppercase">
ActiveNeuralNodes
</h2>
<divclassName="space-y-4 mt-4">
<AgentCardicon="📝"name="Summarizer"/>
<AgentCardicon="🧠"name="Q&A Engine"/>
<AgentCardicon="⚙️"name="Code Generator"/>
</div>
</div>
</motion.aside>

      {/* Main Chat Area */}
<mainclassName="flex-1 flex flex-col relative z-10 m-4">
<headerclassName="h-20 flex items-center px-8">
<div>
<h2className="text-2xl font-black">AgentWorkspace</h2>
<pclassName="text-xs text-slate-500">
Orchestratingmulti-agentworkflows
</p>
</div>
</header>

<divclassName="flex-1 flex items-center justify-center p-4">
<motion.div
initial={{y:30,opacity:0 }}
animate={{y:0,opacity:1 }}
className="w-full max-w-6xl h-full glass-card-ultra"
>
<CopilotChat
className="h-full ultra-chat"
instructions="You are a multi-agent system. Be precise and helpful."
labels={{
title:"Neural Output",
initial:"Awaiting initialization...",
placeholder:"Describe what you want to build..."
              }}
/>
</motion.div>
</div>
</main>
</div>
  );
}

functionAgentCard({icon,name }: {icon:string,name:string }) {
return (
<motion.div
whileHover={{x:5 }}
className="p-4 rounded-2xl bg-white/40 border border-white/40"
>
<divclassName="flex items-center space-x-4">
<divclassName="text-xl">{icon}</div>
<h3className="text-sm font-bold">{name}</h3>
</div>
</motion.div>
  );
}
Enter fullscreen mode Exit fullscreen mode


Step 4: Glassmorphic Styling
Add these styles to app/globals.css:


@tailwind base;
@tailwind components;
@tailwind utilities;

/* Animated Mesh Background */
.bg-mesh {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  background-color: #ffffff;
}

.bg-mesh::after {
  content: "";
  position: absolute;
  width: 100%;
  height: 100%;
  background-image:
    radial-gradient(at 10% 10%, hsla(250, 100%, 95%, 1) 0%, transparent 40%),
    radial-gradient(at 90% 10%, hsla(280, 100%, 95%, 1) 0%, transparent 40%),
    radial-gradient(at 50% 50%, hsla(220, 100%, 97%, 1) 0%, transparent 50%);
  filter: blur(80px) saturate(180%);
  opacity: 0.8;
  animation: mesh-drift 15s ease-in-out infinite alternate;
}

@keyframes mesh-drift {
  0% {
    transform: scale(1) translate(0, 0);
  }
  100% {
    transform: scale(1.15) translate(30px, -20px);
  }
}

/* Glassmorphism */
.glass-sidebar {
  background: rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.4);
}

.glass-card-ultra {
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(30px);
  border: 1px solid rgba(255, 255, 255, 0.9);
  border-radius: 2.5rem;
  box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.08);
}
Enter fullscreen mode Exit fullscreen mode

6. Backend: Building the Agent Service (FastAPI + LangGraph)

Step 1: Define Agent State
Create agent/agent.py:

from typing import TypedDict, Annotated
import operator

from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage


class AgentState(TypedDict):
    """State that gets passed between nodes"""

    messages: Annotated[list, operator.add]
    summary: str
    qna_result: str
    code_result: str
Enter fullscreen mode Exit fullscreen mode


Key insight: Annotated[list, operator.add] means messages accumulate across nodes instead of being replaced.

Step 2: Implement Agent Nodes

Summarizer Agent

def summarizer_node(state: AgentState) -> AgentState:
"""
    Summarizer Agent Node
    Extracts key insights from user input
    """
print("🔵 Running Summarizer Node")

    messages= state.get("messages", [])
ifnot messages:
return state

    user_message=next((msg.contentfor msginreversed(messages)
ifisinstance(msg, HumanMessage)),"")

    llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.3,
max_tokens=150
    )

    system_msg= SystemMessage(
content="You are a professional summarizer. Create a concise summary."
    )
    human_msg= HumanMessage(content=f"Summarize:{user_message}")

    response= llm.invoke([system_msg, human_msg])
    summary= response.content

print(f"✅ Summary:{summary}")

return {
**state,
"summary": summary,
"messages": [AIMessage(content=f"📝 **Summary**:{summary}")]
    }    
Enter fullscreen mode Exit fullscreen mode

Q&A Agent

defqna_node(state: AgentState) -> AgentState:
"""
    Q&A Agent Node
    Provides contextual answers using summary
    """
print("🟢 Running Q&A Node")

    messages= state.get("messages", [])
    summary= state.get("summary","")

    user_message=next((msg.contentfor msgin messages
ifisinstance(msg, HumanMessage)),"")

    llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.5,
max_tokens=300
    )

    system_msg= SystemMessage(
content="You are a Q&A assistant. Provide comprehensive answers."
    )
    context=f"Question:{user_message}\n\nContext:{summary}"
    human_msg= HumanMessage(content=f"Answer based on:\n{context}")

    response= llm.invoke([system_msg, human_msg])
    qna_result= response.content

return {
**state,
"qna_result": qna_result,
"messages": [AIMessage(content=f"❓ **Analysis**:{qna_result}")]
    }
Enter fullscreen mode Exit fullscreen mode

CodeGen Agent


defcodegen_node(state: AgentState) -> AgentState:
"""
    CodeGen Agent Node
    Generates TypeScript/React code
    """
print("🟣 Running CodeGen Node")

    messages= state.get("messages", [])
    summary= state.get("summary","")
    qna_result= state.get("qna_result","")

    user_message=next((msg.contentfor msgin messages
ifisinstance(msg, HumanMessage)),"")

    llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
max_tokens=500
    )

    system_msg= SystemMessage(
content="You are an expert TypeScript/React developer."
    )
    context=f"Request:{user_message}\nSummary:{summary}\nAnalysis:{qna_result}"
    human_msg= HumanMessage(content=f"Generate code for:\n{context}")

    response= llm.invoke([system_msg, human_msg])
    code_result= response.content

return {
**state,
"code_result": code_result,
"messages": [AIMessage(content=f"💻 **Code**:\n{code_result}")]
    }
Enter fullscreen mode Exit fullscreen mode

Step 3: Build the LangGraph Workflow


# Build the workflow
workflow= StateGraph(AgentState)

# Add nodes
workflow.add_node("summarizer", summarizer_node)
workflow.add_node("qna", qna_node)
workflow.add_node("codegen", codegen_node)

# Define the flow
workflow.set_entry_point("summarizer")
workflow.add_edge("summarizer","qna")
workflow.add_edge("qna","codegen")
workflow.add_edge("codegen", END)

# Compile
agent= workflow.compile()
print("✅ LangGraph agent compiled successfully!")
Enter fullscreen mode Exit fullscreen mode

Step 4: FastAPI Server Setup

Create agent/server.py:



from fastapi import FastAPI
from fastapi.middleware.corsimport CORSMiddleware
from langserveimport add_routes
from agentimport agent
from dotenvimport load_dotenv

load_dotenv()

app= FastAPI(
title="LangGraph Agent Server",
description="Composable multi-agent system",
version="1.0.0"
)

# CORS for frontend communication
app.add_middleware(
    CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Add LangServe routes
add_routes(
    app,
    agent,
path="/copilotkit",
enabled_endpoints=["invoke","stream","playground"],
)

@app.get("/")
asyncdefroot():
return {
"message":"LangGraph Agent Server",
"status":"running",
"endpoints": {
"stream":"/copilotkit/stream",
"playground":"/copilotkit/playground"
        }
    }

if__name__=="__main__":
import uvicorn
print("🚀 Starting LangGraph Agent Server on http://localhost:8000")
    uvicorn.run("server:app",host="0.0.0.0",port=8000,reload=True)
Enter fullscreen mode Exit fullscreen mode


7. Running the Application

Start the Backend


cd agent
source venv/bin/activate# or venv\Scripts\activate on Windows
python server.py
Enter fullscreen mode Exit fullscreen mode


You should see:


🚀 Starting LangGraph Agent Server on http://localhost:8000
✅ LangGraph agent compiled successfully!
INFO:     Uvicorn running on http://0.0.0.0:8000
Enter fullscreen mode Exit fullscreen mode


Start the Frontend

In a new terminal:


npm run dev
Enter fullscreen mode Exit fullscreen mode


Open http://localhost:3000 in your browser.

8. How All The Parts Work Together

Let me walk you through a request being processed:
User Input
I'm requesting that you create a todo list in React.
1. Getting Summarized
User inputs: raw request from user
Output:

Summary: The user is looking for a basic React component to create, Read, Update, & Delete CRUD todos.
Enter fullscreen mode Exit fullscreen mode

2. QA Answering Questions Based On The Original Request
User inputs: original user request + summary
Output:

Analysis of the Requirements: The todo list app needs a method of state management (useState), has a method of submitting new todos (form submission), can delete (id as a key) a todo, and follows best practices for building components in React.
Enter fullscreen mode Exit fullscreen mode

3. Generating The Code
User inputs: original user request + summary + analysis
Output:

import React, { useState } from 'react';

interface Todo {
  id: number;
  text: string;
}

export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, { id: Date.now(), text: input }]);
      setInput('');
    }
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="p-4">
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        placeholder="Add todo..."
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.text}
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

9. Key Patterns & Best Practices

1. The Operator for State Management
An operator, like one defined with operator.add should be used as an accumulator for the agent's current state.

classAgentState(TypedDict):
    messages: Annotated[list, operator.add]# Accumulates
    summary:str# Replaces
Enter fullscreen mode Exit fullscreen mode


2. Error Handling
When using try-except in the production environment, wrap the agent's logic in try-except blocks.


defsummarizer_node(state: AgentState) -> AgentState:
try:
# ... agent logic
exceptExceptionas e:
print(f"❌ Error in summarizer:{e}")
return {
**state,
"messages": [AIMessage(content=f"Error:{str(e)}")]
        }
Enter fullscreen mode Exit fullscreen mode

3. Temperature Settings
Each agent type has its own temperature settings to control the creativity of the output:

Summarizer: temperature=0.3 (i.e. focus vs. deterministically)
Q&A: temperature=0.5 ( i.e. balanced)
CodeGen: temperature=0.2 (i.e. deterministic and structured for reliable code output )

4. Streaming Support
When you use add_routes with enabled_endpoints=["stream"]. LangServe will automatically manage your streaming behaviour.

10. Extending the System

Add a New Agent

Define the node function:

defreviewer_node(state: AgentState) -> AgentState:
# Review and improve code
    code= state.get("code_result","")
# ... review logic
return {**state,"reviewed_code": improved_code}
Enter fullscreen mode Exit fullscreen mode


Add to workflow:

workflow.add_node("reviewer", reviewer_node)
workflow.add_edge("codegen","reviewer")
workflow.add_edge("reviewer", END)
Enter fullscreen mode Exit fullscreen mode

Conditional Routing

Route based on state:

defshould_review(state: AgentState) ->str:
    code= state.get("code_result","")
return"reviewer"iflen(code)>100else END

workflow.add_conditional_edges("codegen", should_review)
Enter fullscreen mode Exit fullscreen mode

Parallel Processing

Run agents in parallel:

workflow.add_edge("summarizer","qna")
workflow.add_edge("summarizer","codegen")# Both run in parallel
Enter fullscreen mode Exit fullscreen mode

What's Next?

At this point in time, you've got a complete functioning composable multi-agent system. Now, here are some ideas for what you can do with it.

  • Add additional specialized agents (e.g. image generation, web search, or database queries)
  • Implement conditional routing to different agents based on user intent
  • Add human-in-the-loop approval steps
  • Store conversation history in a database
  • Deploy to production (e.g., Vercel + Railway/Render) The composable pattern will allow your system to perform better and scale out with additional agents beyond the current number of agents you have, while adding no additional complexity.

Stay Connected
If you enjoyed this, explore my work on GitHub and check Twitter for more insights 🚀 Also, follow CopilotKit on Twitter and say hi 👋 - the community is super active and always building something exciting!

Want to learn more?

Read original article →