Multi-Agent Research Assistant

Build a LangGraph + CopilotKit full-stack AI research assistant.

Introduction

30 Second Summary

When you research a topic online, you naturally break it into questions, dig into each one, and then pull your findings together into a coherent picture. AI agents can follow this same pattern, but they do it in seconds.

In this project, you will build a multi-agent research assistant that takes any topic, plans research questions, investigates each one, and synthesizes the results into a polished report.

What You'll Build

A full-stack AI application with a Python LangGraph backend orchestrating three specialized agent nodes and a Next.js CopilotKit frontend providing a real-time chat interface.

By the end of this project, you'll have:

  • A LangGraph StateGraph with three nodes (plan, research, synthesize) connected by sequential edges.
  • A FastAPI server exposing your agent via the AG-UI protocol for frontend communication.
  • A Next.js app with a CopilotKit chat sidebar that streams agent responses in real time.
  • Secret Mission: Add intermediate state streaming so the UI shows live progress updates as each research step completes.

Are there any prerequisites?

You need Python 3.10+, an OpenAI API key, and Cursor installed on your machine. No prior experience with LangGraph, React, or CopilotKit is needed.

Set Up the Project

A full-stack AI research assistant needs two pieces working together: a Python backend that orchestrates the AI agent, and a Next.js frontend that gives users a polished chat interface.

In this step, you will install Node.js, create the project structure, set up a Python virtual environment with all the required packages, and scaffold the Next.js frontend with CopilotKit.

In this step, get ready to:
  • Install Node.js and create the project directory structure.
  • Set up the Python environment and install backend dependencies.
  • Scaffold the Next.js frontend and install CopilotKit packages.
Install Node.js and create the project structure

The CopilotKit frontend runs on Next.js, which requires Node.js 20 or higher.

  • Check if Node.js is already installed by running this command in your terminal:
node --version

✔️ I see version 20 or higher

Your Node.js version is ready for this project. Move on to the next instruction below.

ⓧ I see an older version

You have Node.js installed, but it is too old for this project. Upgrade it by running:

brew upgrade node

After the upgrade completes, run node --version again to confirm you see v20 or higher.

Homebrew not found?

If you see a "command not found" error for brew, install Homebrew first by visiting https://brew.sh and running the install command shown on the page.

ⓧ Command not found

Node.js is not installed yet. Install it with Homebrew by running:

brew install node

After installation completes, verify it worked by running:

node --version

You should see a version number starting with v20 or higher.

Homebrew not found?

If you see a "command not found" error for brew, install Homebrew first by visiting https://brew.sh and running the install command shown on the page. Then retry brew install node.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and navigate into it by running these commands:
mkdir research-assistant
cd research-assistant
mkdir agent
  • Confirm the folder was created by running:
ls

You should see agent listed in the output.

Why two separate folders?

The agent/ folder holds the Python backend (LangGraph agent + FastAPI server). The frontend/ folder will hold the Next.js app. Keeping them separate makes each deployable independently.

Set up the Python environment and install dependencies

A virtual environment isolates this project's Python packages from anything else on your machine. This prevents version conflicts between projects.

  • Navigate into the agent folder and create a virtual environment by running:
cd agent
python3 -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activation command from inside the agent/ folder. If the venv folder does not exist, re-run python3 -m venv venv first.

Help me fix my virtual environment activation

  • Create the requirements.txt file that lists all backend dependencies. In Cursor, create a new file called requirements.txt inside the agent/ folder and paste this content:
copilotkit==0.1.94
langchain-openai>=0.3.0
langgraph>=1.2.0
python-dotenv>=1.0.0

What are these packages?

  • copilotkit connects your LangGraph agent to the CopilotKit frontend via the AG-UI protocol.
  • langchain-openai provides the ChatOpenAI wrapper for calling the OpenAI API.
  • langgraph lets you build multi-step agent workflows as a state graph.
  • python-dotenv loads environment variables from your .env file.
  • Install the dependencies by running:
pip install -r requirements.txt
  • Verify the installation succeeded by running:
pip list | grep -i copilotkit

You should see copilotkit with version 0.1.94 in the output.

Installation failed?

Make sure (venv) appears in your terminal prompt before running pip install. If it does not, re-run the activation command from the platform tabs above.

If you see a network error, check your internet connection and retry.

Help me troubleshoot my pip install error

  • Create the .env file to store your OpenAI API key. In Cursor, create a new file called .env inside the agent/ folder and paste this content:
OPENAI_API_KEY=your-api-key-here
  • Replace your-api-key-here with your actual OpenAI API key.

Where do I find my OpenAI API key?

Go to platform.openai.com/api-keys, sign in, and copy your key. If you do not have one yet, click Create new secret key.

  • Save both files in Cursor.

✔️ Awesome, I've got everything!

Great. Double check you have saved both requirements.txt and .env inside the agent/ folder.

ⓧ I'd like to double check the full code

Here is what each file should look like:

copilotkit==0.1.94
langchain-openai>=0.3.0
langgraph>=1.2.0
python-dotenv>=1.0.0
OPENAI_API_KEY=your-api-key-here

Make sure your-api-key-here is replaced with your actual OpenAI API key in .env.

Scaffold the Next.js frontend and install CopilotKit

CopilotKit provides pre-built React chat components that connect directly to your Python agent. You need a Next.js app to host these components.

  • Navigate back to the project root by running:
cd ~/Desktop/research-assistant
  • Scaffold the Next.js frontend by running:
npx create-next-app@latest frontend

What will the setup wizard ask?

The wizard will first ask if you want to use recommended defaults. Select No, customize settings so you can turn off Tailwind CSS (this project uses inline styles instead).

Then answer the individual prompts as follows:

  • Would you like to use TypeScript? Yes.
  • Which linter would you like to use? ESLint.
  • Would you like to use React Compiler? No.
  • Would you like to use Tailwind CSS? No.
  • Would you like your code inside a src/ directory? No.
  • Would you like to use App Router? Yes.
  • Would you like to customize the import alias? No.

After answering the prompts, the wizard installs all Next.js dependencies automatically. This may take a minute.

  • Navigate into the new frontend folder by running:
cd frontend
  • Install the CopilotKit packages by running:
npm install @copilotkit/react-core @copilotkit/runtime

What do these packages do?

@copilotkit/react-core provides the React provider and UI components (like the chat sidebar). @copilotkit/runtime handles the server-side connection between your Next.js app and the Python agent backend.

  • Verify everything is working by starting the Next.js dev server:
npm run dev

You should see output indicating the server is running at http://localhost:3000.

  • Open your browser and navigate to http://localhost:3000.
  • You should see the default Next.js welcome page.

Dev server won't start?

Make sure you are inside the frontend/ folder when running npm run dev. Run pwd to check your current directory.

If you see a port conflict, another process is using port 3000. Stop it or try a different port.

Help me fix my Next.js dev server

  • Stop the dev server by pressing Ctrl+C in the terminal. You will restart it later when connecting to the Python backend.

Your development environment is fully configured. Next up, you will build the LangGraph research agent that plans, researches, and synthesizes reports on any topic.

Build the LangGraph Research Agent

Your environment is set up and ready to go. Now it is time to build the brain of the application.

A research assistant needs to break a complex topic into manageable questions, investigate each one, and then pull everything together into a coherent report. In this step, you will build a LangGraph StateGraph that does exactly this through three connected nodes.

In this step, get ready to:
  • Define the agent's typed state and three processing nodes.
  • Wire the nodes into a graph with sequential edges and compile it.
  • Test the agent locally to confirm it produces a research report.
Define the agent state and create the node functions

Every LangGraph graph needs a typed state definition. The state is a Python dictionary that flows through each node, accumulating results as it goes.

  • Create a new file called main.py inside your agent/ folder by running this command:
touch main.py
  • Confirm the file exists by running:
ls

You should see main.py alongside your .env and requirements.txt files.

  • Open main.py in Cursor and add the following imports and state definition:
import os
from typing import TypedDict, Annotated
from dotenv import load_dotenv

from fastapi import FastAPI
import uvicorn

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

from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import LangGraphAGUIAgent

load_dotenv()

# Define the agent state
class ResearchState(TypedDict):
    messages: list
    research_topic: str
    research_plan: list[str]
    research_findings: list[str]
    final_report: str

# Initialize the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)

What does this code do?

The imports bring in the tools you need for the entire agent file. Some (like FastAPI and uvicorn) will be used in the next step when you serve the agent.

The ResearchState class is a TypedDict that defines the exact shape of data flowing through the graph. Each node reads from this state and returns an updated version.

The llm variable initializes OpenAI's gpt-4o model with a temperature of 0.7 for varied, high-quality research output.

Now you will create the first node. This node takes the research topic and generates focused research questions.

  • Add the plan_research function below the llm initialization line:
# Node 1: Plan the research
def plan_research(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    
    response = llm.invoke([
        HumanMessage(content=f"""You are a research planning expert. Given the topic: "{topic}"
        
Generate exactly 3 focused research questions that would help create a comprehensive understanding of this topic. Return ONLY the questions, one per line, numbered 1-3.""")
    ])
    
    questions = [q.strip() for q in response.content.strip().split("\n") if q.strip()]
    
    return {
        **state,
        "research_plan": questions,
    }

What does this code do?

The plan_research function is the first node in your graph. It reads research_topic from the state, asks the LLM to generate three research questions, and returns the updated state with those questions in research_plan.

The **state spread pattern preserves all existing state fields while updating only the ones this node is responsible for.

Next, the second node takes those questions and researches each one individually.

  • Add the conduct_research function below plan_research:
# Node 2: Conduct research
def conduct_research(state: ResearchState) -> ResearchState:
    questions = state["research_plan"]
    findings = []
    
    for question in questions:
        response = llm.invoke([
            HumanMessage(content=f"""You are a thorough researcher. Answer this research question in 2-3 concise paragraphs with specific facts and details:

{question}""")
        ])
        findings.append(f"## {question}\n\n{response.content}")
    
    return {
        **state,
        "research_findings": findings,
    }

What does this code do?

The conduct_research node loops over each question in research_plan and asks the LLM to provide a detailed answer. Each answer is formatted as a markdown section with the question as a heading.

The collected findings are stored in research_findings as a list of strings, ready for the final synthesis node.

The third and final node combines all findings into a polished report.

  • Add the synthesize_report function below conduct_research:
# Node 3: Synthesize the report
def synthesize_report(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    findings = "\n\n".join(state["research_findings"])
    
    response = llm.invoke([
        HumanMessage(content=f"""You are an expert report writer. Based on the following research findings about "{topic}", write a polished, well-structured research report.

Include:
- A brief introduction
- Key findings organized by theme
- A conclusion with insights

Research findings:
{findings}

Write the report in markdown format.""")
    ])
    
    return {
        **state,
        "final_report": response.content,
        "messages": state.get("messages", []) + [AIMessage(content=response.content)],
    }

What does this code do?

The synthesize_report node joins all research findings into one string, then asks the LLM to write a structured markdown report. It saves the report in final_report and appends it to the messages list as an AIMessage.

Adding the report to messages is what allows CopilotKit to display the response in the chat interface later.

  • Save main.py.

Seeing red underlines in Cursor?

If Cursor shows import errors for langgraph or langchain_openai, make sure your virtual environment is activated. Check that your Cursor Python interpreter is pointing to the venv inside your agent/ folder.

help me fix import errors in my LangGraph project

Wire the nodes into a StateGraph

You have three standalone functions. Now you need to connect them into a graph so LangGraph knows the execution order.

  • Add the build_research_graph function and the compiled graph at the bottom of main.py:
# Build the graph
def build_research_graph():
    graph = StateGraph(ResearchState)
    
    graph.add_node("plan_research", plan_research)
    graph.add_node("conduct_research", conduct_research)
    graph.add_node("synthesize_report", synthesize_report)
    
    graph.add_edge(START, "plan_research")
    graph.add_edge("plan_research", "conduct_research")
    graph.add_edge("conduct_research", "synthesize_report")
    graph.add_edge("synthesize_report", END)
    
    return graph.compile()

# Create the compiled graph
research_agent = build_research_graph()

What does this code do?

The build_research_graph function creates a StateGraph instance typed with ResearchState. It registers each function as a named node, then connects them with edges that define the execution order.

The flow is: START to plan_research to conduct_research to synthesize_report to END. This is a sequential pipeline where each node's output feeds into the next.

Calling .compile() validates the graph structure and returns a runnable object stored in research_agent.

  • Save main.py.

Getting an error about edges or nodes?

Make sure the string names passed to add_node match the strings used in add_edge. A typo like plan_reserach instead of plan_research will cause a compilation error.

help me debug my LangGraph graph compilation error

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py before moving on.

ⓧ I'd like to double check the full code

import os
from typing import TypedDict, Annotated
from dotenv import load_dotenv

from fastapi import FastAPI
import uvicorn

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

from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import LangGraphAGUIAgent

load_dotenv()

# Define the agent state
class ResearchState(TypedDict):
    messages: list
    research_topic: str
    research_plan: list[str]
    research_findings: list[str]
    final_report: str

# Initialize the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)

# Node 1: Plan the research
def plan_research(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    
    response = llm.invoke([
        HumanMessage(content=f"""You are a research planning expert. Given the topic: "{topic}"
        
Generate exactly 3 focused research questions that would help create a comprehensive understanding of this topic. Return ONLY the questions, one per line, numbered 1-3.""")
    ])
    
    questions = [q.strip() for q in response.content.strip().split("\n") if q.strip()]
    
    return {
        **state,
        "research_plan": questions,
    }

# Node 2: Conduct research
def conduct_research(state: ResearchState) -> ResearchState:
    questions = state["research_plan"]
    findings = []
    
    for question in questions:
        response = llm.invoke([
            HumanMessage(content=f"""You are a thorough researcher. Answer this research question in 2-3 concise paragraphs with specific facts and details:

{question}""")
        ])
        findings.append(f"## {question}\n\n{response.content}")
    
    return {
        **state,
        "research_findings": findings,
    }

# Node 3: Synthesize the report
def synthesize_report(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    findings = "\n\n".join(state["research_findings"])
    
    response = llm.invoke([
        HumanMessage(content=f"""You are an expert report writer. Based on the following research findings about "{topic}", write a polished, well-structured research report.

Include:
- A brief introduction
- Key findings organized by theme
- A conclusion with insights

Research findings:
{findings}

Write the report in markdown format.""")
    ])
    
    return {
        **state,
        "final_report": response.content,
        "messages": state.get("messages", []) + [AIMessage(content=response.content)],
    }

# Build the graph
def build_research_graph():
    graph = StateGraph(ResearchState)
    
    graph.add_node("plan_research", plan_research)
    graph.add_node("conduct_research", conduct_research)
    graph.add_node("synthesize_report", synthesize_report)
    
    graph.add_edge(START, "plan_research")
    graph.add_edge("plan_research", "conduct_research")
    graph.add_edge("conduct_research", "synthesize_report")
    graph.add_edge("synthesize_report", END)
    
    return graph.compile()

# Create the compiled graph
research_agent = build_research_graph()
Test the agent locally

Before wiring up the frontend, you should confirm the graph runs end-to-end and produces a real research report.

  • In your terminal (still inside the agent/ folder with your virtual environment active), test the agent by running:
python3 -c "
from main import research_agent

result = research_agent.invoke({
    'messages': [],
    'research_topic': 'quantum computing',
    'research_plan': [],
    'research_findings': [],
    'final_report': ''
})

print(result['final_report'])
"

What does this command do?

This imports your compiled graph and invokes it with an initial state. The research_topic is set to quantum computing and all other fields start empty. The graph will flow through all three nodes and print the final synthesized report.

This may take 15-30 seconds because it makes multiple calls to the OpenAI API (one for planning, three for research, one for synthesis).

You should see a multi-paragraph markdown research report about quantum computing printed to your terminal.

Seeing an authentication error?

Make sure your .env file in the agent/ folder contains a valid OPENAI_API_KEY. The load_dotenv() call at the top of main.py reads from this file automatically.

help me fix my OpenAI API authentication error in my LangGraph agent

Your research agent is working. Next up, you will serve it through a FastAPI server so the frontend can connect to it.

Serve the Agent with FastAPI

Your research agent can plan, research, and synthesize reports. Right now it only runs as a Python script that you invoke directly from the terminal.

For CopilotKit to talk to your agent, it needs an HTTP endpoint it can send requests to. In this step, you will wrap the agent in a FastAPI server that speaks the AG-UI protocol. This protocol is a standardized contract between frontends and agents that handles streaming, state sync, and tool calls automatically.

In this step, get ready to:
  • Attach your LangGraph agent to a FastAPI server using the AG-UI endpoint.
  • Start the server and verify it is running on port 8123.
Create the FastAPI endpoint

The add_langgraph_fastapi_endpoint function from the ag-ui-langgraph package does the heavy lifting. It takes your compiled graph, wraps it in the AG-UI protocol, and mounts it as a route on your FastAPI app.

  • In main.py, scroll to the bottom of the file and find this line:
research_agent = build_research_graph()
  • Add the FastAPI server and AG-UI endpoint below that line by copying this code:
# Set up FastAPI with AG-UI endpoint
app = FastAPI()

add_langgraph_fastapi_endpoint(
    app=app,
    agent=LangGraphAGUIAgent(
        name="research_assistant",
        description="A multi-step research assistant that plans, researches, and synthesizes reports on any topic.",
        graph=research_agent,
    ),
    path="/",
)

What does this code do?

app = FastAPI() creates a new FastAPI web server instance.

add_langgraph_fastapi_endpoint mounts your compiled graph onto the server at the root path /. The LangGraphAGUIAgent wrapper gives your agent a name and description so CopilotKit can identify it when connecting.

  • Now add the entry point function that starts the server. Copy this code below the endpoint you just added:
def main():
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8123,
        reload=True,
    )

if __name__ == "__main__":
    main()

What does this code do?

uvicorn.run("main:app", ...) starts an ASGI server that serves your FastAPI app. The "main:app" string tells uvicorn to look for the app object inside main.py.

host="0.0.0.0" makes the server accessible from any network interface (needed for the frontend to connect). port=8123 is the default port CopilotKit expects. reload=True restarts the server automatically when you save code changes.

  • Save main.py.

You will verify the server works in the next substep by running it.

✔️ Awesome, I've got everything!

Double check you have saved main.py.

ⓧ I'd like to double check the full code

Here is the full agent/main.py file at this point. Compare yours against it:

import os
from typing import TypedDict, Annotated
from dotenv import load_dotenv

from fastapi import FastAPI
import uvicorn

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

from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import LangGraphAGUIAgent

load_dotenv()

# Define the agent state
class ResearchState(TypedDict):
    messages: list
    research_topic: str
    research_plan: list[str]
    research_findings: list[str]
    final_report: str

# Initialize the LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)

# Node 1: Plan the research
def plan_research(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    
    response = llm.invoke([
        HumanMessage(content=f"""You are a research planning expert. Given the topic: "{topic}"
        
Generate exactly 3 focused research questions that would help create a comprehensive understanding of this topic. Return ONLY the questions, one per line, numbered 1-3.""")
    ])
    
    questions = [q.strip() for q in response.content.strip().split("\n") if q.strip()]
    
    return {
        **state,
        "research_plan": questions,
    }

# Node 2: Conduct research
def conduct_research(state: ResearchState) -> ResearchState:
    questions = state["research_plan"]
    findings = []
    
    for question in questions:
        response = llm.invoke([
            HumanMessage(content=f"""You are a thorough researcher. Answer this research question in 2-3 concise paragraphs with specific facts and details:

{question}""")
        ])
        findings.append(f"## {question}\n\n{response.content}")
    
    return {
        **state,
        "research_findings": findings,
    }

# Node 3: Synthesize the report
def synthesize_report(state: ResearchState) -> ResearchState:
    topic = state["research_topic"]
    findings = "\n\n".join(state["research_findings"])
    
    response = llm.invoke([
        HumanMessage(content=f"""You are an expert report writer. Based on the following research findings about "{topic}", write a polished, well-structured research report.

Include:
- A brief introduction
- Key findings organized by theme
- A conclusion with insights

Research findings:
{findings}

Write the report in markdown format.""")
    ])
    
    return {
        **state,
        "final_report": response.content,
        "messages": state.get("messages", []) + [AIMessage(content=response.content)],
    }

# Build the graph
def build_research_graph():
    graph = StateGraph(ResearchState)
    
    graph.add_node("plan_research", plan_research)
    graph.add_node("conduct_research", conduct_research)
    graph.add_node("synthesize_report", synthesize_report)
    
    graph.add_edge(START, "plan_research")
    graph.add_edge("plan_research", "conduct_research")
    graph.add_edge("conduct_research", "synthesize_report")
    graph.add_edge("synthesize_report", END)
    
    return graph.compile()

# Create the compiled graph
research_agent = build_research_graph()

# Set up FastAPI with AG-UI endpoint
app = FastAPI()

add_langgraph_fastapi_endpoint(
    app=app,
    agent=LangGraphAGUIAgent(
        name="research_assistant",
        description="A multi-step research assistant that plans, researches, and synthesizes reports on any topic.",
        graph=research_agent,
    ),
    path="/",
)

def main():
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8123,
        reload=True,
    )

if __name__ == "__main__":
    main()
Start the server

With the endpoint configured, you can now boot the server and confirm it responds.

  • In your terminal (still inside the agent/ directory with your virtual environment active), start the server by running:
python main.py
  • You should see output that includes:
INFO:     Uvicorn running on http://0.0.0.0:8123 (Press CTRL+C to quit)

Your agent is now listening for incoming requests from CopilotKit.

Why does the AG-UI protocol matter?

The AG-UI protocol defines 16 standardized event types for frontend-agent communication. This means you can swap your agent backend (LangGraph, CrewAI, or any custom agent) without changing the frontend code.

The add_langgraph_fastapi_endpoint function handles the translation between LangGraph's internal state format and the AG-UI events that CopilotKit expects.

Server not starting?

  • If you see ModuleNotFoundError, make sure your virtual environment is active. You should see (venv) in your terminal prompt.
  • If port 8123 is already in use, check whether another process is running on that port and stop it first.
  • If you see an indentation error, double check that def main(): is at the top level of the file (no extra indentation).

Help me debug why my FastAPI server won't start on port 8123.

Your agent is now live and ready to accept connections. Next up, you will build the CopilotKit frontend that talks to this endpoint and displays the research reports in a polished chat interface.

Connect the CopilotKit Frontend

Your Python agent is running and ready to accept connections. Now you need a frontend that can communicate with it so users interact through a browser instead of a terminal.

CopilotKit handles the connection layer between your Next.js app and the Python backend using the AG-UI protocol. In this step, you'll create an API route that bridges the two, wrap the app in the CopilotKit provider, and test the full end-to-end research flow.

In this step, get ready to:
  • Create the CopilotKit runtime API route.
  • Add the CopilotKit provider and chat sidebar.
  • Test the full research pipeline from browser to agent and back.
Create the CopilotKit runtime API route

Your Next.js app needs a server-side API route that acts as a bridge between the browser and your Python agent. This route initializes the CopilotKit runtime and tells it where to find your LangGraph agent.

  • In Cursor, navigate to the frontend/app folder in the **Explorer** sidebar.
  • Create a new folder called api inside app.
  • Create a new folder called copilotkit inside api.
  • Create a new file called route.ts inside copilotkit and paste the following code:
import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";

// Use an empty adapter since we're routing to an external agent
const serviceAdapter = new ExperimentalEmptyAdapter();

// Configure the CopilotKit runtime with our LangGraph agent
const runtime = new CopilotRuntime({
  agents: {
    research_assistant: new LangGraphAgent({
      deploymentUrl: "http://localhost:8123",
      graphId: "research_assistant",
      langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
    }),
  },
});

// Handle POST requests from the CopilotKit frontend
export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

What does this code do?

This API route is the bridge between your browser and the Python agent running on port 8123.

  • ExperimentalEmptyAdapter tells CopilotKit not to use a built-in LLM. All intelligence comes from your external Python agent.
  • LangGraphAgent points to your FastAPI server at http://localhost:8123 and identifies the agent by its graphId (matching the name you set in Python).
  • copilotRuntimeNextJSAppRouterEndpoint wires everything together into a Next.js API route handler that speaks the AG-UI protocol.
  • Save route.ts.

Why is langsmithApiKey empty?

The langsmithApiKey field is optional. LangSmith is a tracing tool for debugging agent runs. By defaulting to an empty string, you skip that setup entirely. The agent works fine without it.

Seeing a TypeScript error in route.ts?

Make sure you installed both @copilotkit/react-core and @copilotkit/runtime in the previous step. The LangGraphAgent class comes from the runtime package.

help me fix TypeScript errors in my CopilotKit route.ts file

✔️ Awesome, I've got everything!

Great. Make sure you've saved route.ts before moving on.

ⓧ I'd like to double check the full code

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

const serviceAdapter = new ExperimentalEmptyAdapter();

const runtime = new CopilotRuntime({
  agents: {
    research_assistant: new LangGraphAgent({
      deploymentUrl: "http://localhost:8123",
      graphId: "research_assistant",
      langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
    }),
  },
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};
Add the CopilotKit provider and sidebar

CopilotKit needs a provider component wrapping your entire app so the chat sidebar can communicate with the runtime. You'll replace the default layout and page files that Next.js scaffolded.

  • Open frontend/app/layout.tsx in the Explorer sidebar.
  • Select all the existing content and replace it with the following code:
import type { Metadata } from "next";
import { CopilotKit } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
import "./globals.css";

export const metadata: Metadata = {
  title: "Research Assistant",
  description: "AI-powered multi-agent research assistant",
};

// Wrap the entire app with CopilotKit provider
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <CopilotKit runtimeUrl="/api/copilotkit">
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}

What does this code do?

  • CopilotKit is the provider that gives every child component access to the agent connection. The runtimeUrl points to the API route you just created.
  • The styles import (@copilotkit/react-core/v2/styles.css) loads CopilotKit's built-in chat UI styling so the sidebar looks polished without custom CSS.
  • The /v2 import path uses CopilotKit v2's component API with the AG-UI protocol.
  • Save layout.tsx.

Seeing module not found errors?

If your editor shows an error on the @copilotkit/react-core/v2 import, confirm you're inside the frontend/ directory and that the packages installed correctly. Try running npm install again from the frontend/ folder.

help me fix CopilotKit import errors

✔️ Awesome, I've got everything!

Great. Make sure you've saved layout.tsx before continuing.

ⓧ I'd like to double check the full code

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

export const metadata: Metadata = {
  title: "Research Assistant",
  description: "AI-powered multi-agent research assistant",
};

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

Now update the home page to add the chat sidebar component.

  • Open frontend/app/page.tsx in the Explorer sidebar.
  • Select all the existing content and replace it with the following code:
import { CopilotSidebar } from "@copilotkit/react-core/v2";

export default function Home() {
  return (
    <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
      <h1>Research Assistant</h1>
      <p>
        Ask me to research any topic. I will plan research questions, investigate
        each one, and synthesize a comprehensive report for you.
      </p>
      <p style={{ color: "#666", marginTop: "1rem" }}>
        Try: &quot;Research the impact of large language models on software engineering&quot;
      </p>
      <CopilotSidebar />
    </main>
  );
}

What does this code do?

  • CopilotSidebar renders a slide-out chat panel on the right side of the page. Users type messages here to interact with your agent.
  • The page content gives users context about what the assistant does and suggests a sample prompt to try.
  • Save page.tsx.

✔️ Awesome, I've got everything!

Great. Both frontend files are ready. Time to test the full pipeline.

ⓧ I'd like to double check the full code

import { CopilotSidebar } from "@copilotkit/react-core/v2";

export default function Home() {
  return (
    <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
      <h1>Research Assistant</h1>
      <p>
        Ask me to research any topic. I will plan research questions, investigate
        each one, and synthesize a comprehensive report for you.
      </p>
      <p style={{ color: "#666", marginTop: "1rem" }}>
        Try: &quot;Research the impact of large language models on software engineering&quot;
      </p>
      <CopilotSidebar />
    </main>
  );
}
Test the full research pipeline

Both the backend and frontend code are ready. Now you'll start both servers and send a research query through the chat sidebar.

  • Confirm your Python backend is still running in its terminal (you should see "Uvicorn running on http://0.0.0.0:8123"). If it stopped, restart it from the agent/ directory.
  • Open a new terminal in Cursor by pressing **Cmd+Shift+`** (macOS) or **Ctrl+Shift+`** (Windows).
  • Navigate to the frontend directory and start the dev server by running these commands:
cd frontend
npm run dev
  • Open your browser and go to http://localhost:3000.
  • You should see the "Research Assistant" heading and a CopilotKit chat sidebar on the right side of the page.
  • Click the chat input in the sidebar and type:
Research the history of artificial intelligence
  • Press **Enter** and wait. The agent takes 30-60 seconds to plan questions, research each one, and synthesize the report.
  • You should see a structured research report stream back into the chat sidebar, complete with an introduction, research findings, and a conclusion.

What's happening behind the scenes?

When you send a message, CopilotKit sends it from the browser to your /api/copilotkit route. The route forwards it to your Python FastAPI server at port 8123 using the AG-UI protocol. The LangGraph agent runs all three nodes (plan, research, synthesize) and streams the final report back through the same chain.

Chat sidebar not appearing?

  • Make sure you imported the CopilotKit stylesheet in layout.tsx (@copilotkit/react-core/v2/styles.css). Without it, the sidebar has no visible styling.
  • Confirm the CopilotKit provider wraps {children} in layout.tsx. The sidebar component needs the provider above it in the tree.
  • Check your browser console for errors. A connection refused error means the Python backend isn't running on port 8123.

help me debug why the CopilotKit sidebar isn't working

Secret mission

Add Real-Time Progress Streaming

Your research assistant works, but the user stares at a loading state while the agent processes three separate stages. In a production app, users need feedback. Can you add real-time progress streaming so the chat shows exactly which research stage is running and how far along it is?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs. OpenAI API charges (approximately $1-3 total) only occur when you actively run the agent.

Resources you used:

  • Your project folder on the Desktop (contains agent/ and frontend/ directories).
  • A Python virtual environment inside agent/.
  • Node.js packages inside frontend/node_modules/.
  • OpenAI API usage (charged per use, no ongoing cost when the agent is not running).

Keep everything running

No action needed. Choose this if you plan to keep building on this project or want it available as a portfolio piece.

  • Your project files stay on your Desktop. No ongoing cost is incurred unless you actively run the agent and make OpenAI API calls.
  • Node.js remains installed on your machine for future projects.

Pause - I'll come back to this later

Shut down any running servers to free up terminal sessions and memory. Your files remain on disk for next time.

  • In each terminal where a server is running (the Python backend and the Next.js frontend), press Ctrl+C to stop the process.
  • When you return, start the Python backend with python main.py from the agent/ folder and the frontend with npm run dev from the frontend/ folder.

Delete - I don't want to use this again

Remove all project files from your machine. This deletes your code, the virtual environment, and all installed packages.

  • Stop any running servers by pressing Ctrl+C in each terminal.
  • Delete the project folder from your Desktop by running this command (macOS/Linux):
cd ~/Desktop
rm -rf research-assistant

On Windows, run this instead:

cd ~\Desktop
rmdir /s /q research-assistant
  • Your OpenAI API key remains active in your OpenAI account. No further charges will occur unless you use the key in another project.

Nice Work!

Nice Work!

Nice work! You just built a full-stack multi-agent research assistant from scratch, combining a Python LangGraph backend with a polished CopilotKit chat frontend.

You've learned how to:

  • Build a LangGraph StateGraph with typed state, multiple nodes, and sequential edges to orchestrate a multi-step research pipeline (plan, research, synthesize).
  • Create a FastAPI server exposing your agent via the AG-UI protocol so any compatible frontend can connect to it.
  • Wire a Next.js app with CopilotKit v2 to your Python backend for real-time streaming of agent responses through a professional chat interface.
  • Secret Mission: Added intermediate state streaming with copilotkit_emit_state and a custom progress indicator component so the UI shows live updates as each research step completes.

Ready to quiz yourself?