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.
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
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)
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
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
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
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
Create agent/.env:
env
OPENAI_API_KEY=your_openai_api_key_here
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>
)
}
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;
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>
);
}
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);
}
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
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}")]
}
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}")]
}
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}")]
}
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!")
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)
7. Running the Application
Start the Backend
cd agent
source venv/bin/activate# or venv\Scripts\activate on Windows
python server.py
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
Start the Frontend
In a new terminal:
npm run dev
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.
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.
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>
);
}
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
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)}")]
}
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}
Add to workflow:
workflow.add_node("reviewer", reviewer_node)
workflow.add_edge("codegen","reviewer")
workflow.add_edge("reviewer", END)
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)
Parallel Processing
Run agents in parallel:
workflow.add_edge("summarizer","qna")
workflow.add_edge("summarizer","codegen")# Both run in parallel
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!




Top comments (1)
great work Ayush! really like the sequence diagram in "How All The Parts Work Together"
how was your experience working with copilotkit & langgraph ?