Multi-Agent Code Reviewer with LangGraph
Build a local AI system that reviews code with parallel agents via LangGraph.
Introduction
30 Second Summary
Every developer knows the wait for code review feedback. You submit your code and hope someone catches the bugs before they hit production.
In this project, you will build CodeMentor AI, a local AI-powered development assistant that runs three specialized review agents in parallel to analyze your code for quality, security, and performance issues. You will also build a code generation mode that produces complete files from natural language descriptions.
What You'll Build
CodeMentor AI is a full-stack application with a Python backend orchestrating multiple AI agents via LangGraph and a Next.js frontend that streams results in real time.
By the end of this project, you'll have:
- A LangGraph pipeline with three parallel review agents (Quality, Security, Performance) plus a Synthesizer that combines findings into structured feedback.
- A code generation mode that produces complete, explained code files from natural language descriptions.
- A real-time streaming interface built with Next.js and Server-Sent Events that displays agent results as they arrive.
- Secret Mission: Add a conversational follow-up mode where users can ask questions about review results or iterate on generated code.
Are there any prerequisites?
You should be familiar with Python, Node.js, and basic web development concepts. You will also need Ollama installed and running on your machine before starting.
Set Up the Project and Install Dependencies
Before you can build a multi-agent system, you need a workspace with the right tools in place.
In this step, you will create the project folder structure, install the Python packages that power the backend, pull a local AI model, and scaffold the frontend.
In this step, get ready to:
- Create the project structure and install Python backend dependencies.
- Pull the llama3.1 model in Ollama and verify it responds.
- Scaffold the Next.js frontend with Tailwind CSS.
Create the project structure and install Python dependencies
Your project needs two main pieces: a Python backend that orchestrates the AI agents, and a Next.js frontend that displays results. You will create a single parent directory with separate subdirectories for each.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and the backend subdirectory, then move into the backend by running these commands:
mkdir -p codementor-ai/backend
cd codementor-ai/backend
What do these commands do?
mkdir -p codementor-ai/backend creates two nested directories in a single command. The -p flag means "create parent directories as needed".
cd codementor-ai/backend moves your terminal into the new backend folder so everything you do next lives in the right place.
Python projects use virtual environments to keep dependencies isolated from your system. This prevents version conflicts between projects.
- Create and activate a virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install here won't affect other projects on your machine.
After activation, you will see (venv) at the start of your terminal prompt. This confirms you are working inside the environment.
On Windows? Use a different activate command.
If you are on Windows using PowerShell, replace the source line with .\venv\Scripts\Activate.ps1 instead.
help me activate my Python virtual environment on Windows
Now you need a file that lists all the packages your backend depends on.
- Create a requirements.txt file by running this command:
cat > requirements.txt << 'EOF'
fastapi
uvicorn
langgraph
langchain-ollama
python-multipart
sse-starlette
EOF
What are these packages?
- fastapi is the web framework that will serve your API endpoints.
- uvicorn is the ASGI server that runs FastAPI.
- langgraph orchestrates your AI agents using a graph-based state machine.
- langchain-ollama connects LangGraph to your local Ollama models.
- python-multipart handles file uploads in FastAPI.
- sse-starlette enables Server-Sent Events for real-time streaming.
- Install all the dependencies by running:
pip install -r requirements.txt
- Confirm the installation succeeded by running:
pip list | grep -i fastapi
You should see fastapi with a version number in the output.
Installation failing?
Make sure you see (venv) in your terminal prompt. If you do not, re-run source venv/bin/activate before installing.
Also check that your Python version is 3.10 or higher. LangGraph requires Python >=3.10.
help me fix pip install errors in my virtual environment
✔️ Awesome, I've got everything!
Great. Double check you have requirements.txt saved in your backend/ folder.
ⓧ I'd like to double check the full code
Here is the complete requirements.txt file:
fastapi
uvicorn
langgraph
langchain-ollama
python-multipart
sse-starlette
Pull the llama3.1 model and verify it responds
Your AI agents need a language model to reason about code. Ollama lets you run models locally on your machine with no API keys or cloud costs.
- Download the llama3.1 model by running this command:
ollama pull llama3.1
How long does the download take?
The llama3.1 model is approximately 4.7 GB. Depending on your internet speed, this could take a few minutes. You will see a progress bar in your terminal as it downloads.
- Once the download finishes, verify the model is available by running:
ollama ls
You should see llama3.1:latest in the list of models.
- Test that the model actually responds by running:
ollama run llama3.1 "Say hello in one sentence"
You should see a short natural language response from the model in your terminal. This confirms Ollama is working and the model can generate text.
Not getting a response?
Make sure Ollama is running in the background. On macOS, check that the Ollama icon appears in your menu bar. On Windows, verify the Ollama service is active in your system tray.
If you see a connection error, try running ollama serve in a separate terminal window to start the Ollama server manually.
help me troubleshoot Ollama not responding to my run command
Scaffold the Next.js frontend
The frontend provides the web interface where users upload code files and see agent results streaming in. Next.js with Tailwind CSS gives you a fast React setup with utility-first styling out of the box.
- Navigate back to the parent project directory by running:
cd ~/Desktop/codementor-ai
- Scaffold the Next.js project into a frontend folder by running:
npx create-next-app@latest frontend
What prompts will I see?
The create-next-app wizard will ask you a series of questions. Select the recommended defaults option when prompted. This gives you TypeScript, ESLint, Tailwind CSS, and the App Router.
If you see a prompt about using the recommended Next.js defaults, select Yes, use recommended defaults. The CLI will create the frontend/ folder and install all dependencies automatically.
- Once scaffolding completes, verify the frontend folder exists by running:
ls frontend/package.json
You should see frontend/package.json printed in the output. This confirms Next.js scaffolded correctly.
Scaffold command not working?
Make sure you are in the codementor-ai directory (not inside backend/) before running the create-next-app command. The tool creates a new folder named frontend inside your current working directory.
If npx is not recognized, verify Node.js is installed by running node --version in your terminal.
help me troubleshoot npx create-next-app failing
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the codementor-ai folder, then click Open.
You should see both backend/ and frontend/ directories listed in the VS Code Explorer sidebar.
Your environment is ready. Next up, you will build the LangGraph multi-agent pipeline that powers the code review system.
Build the LangGraph Multi-Agent Review Pipeline
Your environment is ready and Ollama is responding. Now it is time to build the brain of CodeMentor AI.
A single AI model reviewing code can only do so much. Different aspects of code quality require different expertise. In this step, you will build a multi-agent pipeline using LangGraph where three specialized agents analyze code in parallel, then a synthesizer agent combines their findings into one structured review.
In this step, get ready to:
- Define the shared state schema that all agents read from and write to.
- Implement three parallel review agents (quality, security, performance) powered by Ollama.
- Wire the full StateGraph with parallel execution feeding into a synthesizer.
Define the shared state schema
Every LangGraph pipeline needs a shared state object that flows between nodes. Each agent reads input from this state and writes its result back. Python's TypedDict gives you a clean, type-safe way to define these fields.
- Create a new file called review_pipeline.py inside your backend/ folder.
- Add the imports and state definition by pasting this code:
from typing import TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, START, END
# Define the shared state that flows between all agent nodes
class ReviewState(TypedDict):
code_content: str # The source code to review
file_name: str # Name of the uploaded file
file_extension: str # File extension (e.g. .py, .js)
quality_result: str # Output from the quality agent
security_result: str # Output from the security agent
performance_result: str # Output from the performance agent
synthesis_result: str # Combined final review from synthesizer
What does this code do?
The ReviewState TypedDict defines the contract between all agents in the pipeline. Each agent reads code_content, file_name, and file_extension as inputs.
Each agent writes to its own result field (quality_result, security_result, performance_result). The synthesizer reads all three and writes synthesis_result. This separation prevents agents from overwriting each other's work during parallel execution.
Implement the three review agent nodes
Each agent is a Python function that receives the full ReviewState, calls ChatOllama with a specialized system prompt, and returns a dictionary updating its result field. All three agents run in parallel because LangGraph executes nodes concurrently when they share no data dependencies.
- Add the quality agent below the ReviewState class in review_pipeline.py:
# Initialize the LLM (shared across agents)
llm = ChatOllama(model="llama3.1")
def quality_agent(state: ReviewState) -> dict:
"""Analyzes code for quality issues like readability, maintainability, and best practices."""
system_prompt = """You are a code quality expert. Analyze the following code for:
- Code readability and clarity
- Naming conventions
- Function/method length and complexity
- DRY principle violations
- Error handling patterns
- Documentation and comments
Provide specific, actionable feedback with severity levels (high/medium/low).
Format each issue as: [SEVERITY] Issue description - Suggestion for fix."""
# Build the prompt with file context
user_message = f"""Review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
# Call the LLM with the specialized prompt
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"quality_result": response.content}
What does this code do?
The quality_agent function receives the pipeline state, constructs a detailed system prompt focused on code quality concerns, and sends the code to llama3.1 via ChatOllama.
The function returns a dictionary with only the quality_result key. LangGraph merges this partial update into the shared state without touching other fields.
- Add the security agent below the quality agent:
def security_agent(state: ReviewState) -> dict:
"""Analyzes code for security vulnerabilities and unsafe patterns."""
system_prompt = """You are a security expert. Analyze the following code for:
- SQL injection vulnerabilities
- Cross-site scripting (XSS) risks
- Insecure data handling
- Hardcoded secrets or credentials
- Input validation issues
- Authentication/authorization flaws
- Dependency vulnerabilities
Provide specific, actionable feedback with severity levels (critical/high/medium/low).
Format each issue as: [SEVERITY] Issue description - Recommendation."""
user_message = f"""Security review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"security_result": response.content}
- Add the performance agent below the security agent:
def performance_agent(state: ReviewState) -> dict:
"""Analyzes code for performance issues and optimization opportunities."""
system_prompt = """You are a performance optimization expert. Analyze the following code for:
- Algorithm complexity issues (time and space)
- Unnecessary memory allocations
- N+1 query patterns
- Missing caching opportunities
- Inefficient loops or data structures
- Resource leaks (file handles, connections)
- Concurrency issues
Provide specific, actionable feedback with severity levels (high/medium/low).
Format each issue as: [SEVERITY] Issue description - Optimization suggestion."""
user_message = f"""Performance review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"performance_result": response.content}
Why three separate agents instead of one?
Each agent has a focused system prompt that makes it an expert in one domain. A single model asked to check everything at once tends to produce shallow analysis. Splitting into three agents produces deeper, more targeted feedback.
Because these agents write to different state fields (quality_result, security_result, performance_result), LangGraph can run them in parallel. Three sequential calls would take 3x longer.
- Save review_pipeline.py.
Seeing a red underline on ChatOllama?
Make sure your virtual environment is active. If VS Code is using the wrong Python interpreter, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) and select Python: Select Interpreter. Choose the one inside your backend/ virtual environment.
help me fix import errors in my LangGraph review pipeline
Implement the synthesizer and wire the StateGraph
The synthesizer agent is the final node in the pipeline. It reads the output from all three review agents and combines them into a single, structured review. After defining it, you will wire the full StateGraph so the three agents run in parallel, then feed into the synthesizer.
- Add the synthesizer agent below the performance agent in review_pipeline.py:
def synthesizer_agent(state: ReviewState) -> dict:
"""Combines all agent results into a final structured review."""
system_prompt = """You are a senior code review synthesizer. You receive analysis from three specialized reviewers:
1. A code quality reviewer
2. A security reviewer
3. A performance reviewer
Your job is to:
- Combine their findings into a unified, structured review
- Remove duplicate issues mentioned by multiple reviewers
- Prioritize issues by overall impact
- Provide an executive summary with the top 3 most critical findings
- Give an overall health score (1-10) for the code
Format your response as:
## Executive Summary
[Top 3 findings and overall score]
## Critical Issues
[Issues that must be fixed]
## Improvements
[Suggested improvements ranked by impact]
## Positive Aspects
[Things done well in the code]"""
user_message = f"""Here are the reviews for '{state['file_name']}':
### Quality Review:
{state['quality_result']}
### Security Review:
{state['security_result']}
### Performance Review:
{state['performance_result']}
Please synthesize these into a final unified review."""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"synthesis_result": response.content}
What does the synthesizer do?
The synthesizer reads quality_result, security_result, and performance_result from the state. It de-duplicates findings, prioritizes by impact, and outputs a structured review with an executive summary and health score. This is the final output the user sees.
- Add the StateGraph wiring at the bottom of review_pipeline.py:
# Build the review pipeline graph
workflow = StateGraph(ReviewState)
# Add all agent nodes
workflow.add_node("quality_agent", quality_agent)
workflow.add_node("security_agent", security_agent)
workflow.add_node("performance_agent", performance_agent)
workflow.add_node("synthesizer_agent", synthesizer_agent)
# Wire parallel execution: START fans out to all three agents
workflow.add_edge(START, "quality_agent")
workflow.add_edge(START, "security_agent")
workflow.add_edge(START, "performance_agent")
# All three agents fan in to the synthesizer
workflow.add_edge("quality_agent", "synthesizer_agent")
workflow.add_edge("security_agent", "synthesizer_agent")
workflow.add_edge("performance_agent", "synthesizer_agent")
# Synthesizer outputs the final result
workflow.add_edge("synthesizer_agent", END)
# Compile the graph into a runnable
review_graph = workflow.compile()
How does parallel execution work here?
When you add multiple edges from START (e.g., START → quality_agent, START → security_agent, START → performance_agent), LangGraph recognizes these nodes have no data dependencies on each other. It executes all three concurrently.
The synthesizer_agent node has edges from all three agents. LangGraph waits for all three to finish before invoking the synthesizer. This is the fan-out / fan-in pattern.
- Save review_pipeline.py.
- You will not see visible output yet because this file defines the pipeline without running it. The next step adds a generation pipeline, then you will test both from the command line.
Getting an import error when you try to run the file?
If you see an error about langgraph or langchain_ollama not being found, confirm your virtual environment is activated. You should see (venv) or similar at the start of your terminal prompt.
help me debug import errors in my review_pipeline.py
✔️ Awesome, I've got everything!
Great. Double check you have saved review_pipeline.py in your backend/ folder.
ⓧ I'd like to double check the full code
from typing import TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, START, END
# Define the shared state that flows between all agent nodes
class ReviewState(TypedDict):
code_content: str # The source code to review
file_name: str # Name of the uploaded file
file_extension: str # File extension (e.g. .py, .js)
quality_result: str # Output from the quality agent
security_result: str # Output from the security agent
performance_result: str # Output from the performance agent
synthesis_result: str # Combined final review from synthesizer
# Initialize the LLM (shared across agents)
llm = ChatOllama(model="llama3.1")
def quality_agent(state: ReviewState) -> dict:
"""Analyzes code for quality issues like readability, maintainability, and best practices."""
system_prompt = """You are a code quality expert. Analyze the following code for:
- Code readability and clarity
- Naming conventions
- Function/method length and complexity
- DRY principle violations
- Error handling patterns
- Documentation and comments
Provide specific, actionable feedback with severity levels (high/medium/low).
Format each issue as: [SEVERITY] Issue description - Suggestion for fix."""
# Build the prompt with file context
user_message = f"""Review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
# Call the LLM with the specialized prompt
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"quality_result": response.content}
def security_agent(state: ReviewState) -> dict:
"""Analyzes code for security vulnerabilities and unsafe patterns."""
system_prompt = """You are a security expert. Analyze the following code for:
- SQL injection vulnerabilities
- Cross-site scripting (XSS) risks
- Insecure data handling
- Hardcoded secrets or credentials
- Input validation issues
- Authentication/authorization flaws
- Dependency vulnerabilities
Provide specific, actionable feedback with severity levels (critical/high/medium/low).
Format each issue as: [SEVERITY] Issue description - Recommendation."""
user_message = f"""Security review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"security_result": response.content}
def performance_agent(state: ReviewState) -> dict:
"""Analyzes code for performance issues and optimization opportunities."""
system_prompt = """You are a performance optimization expert. Analyze the following code for:
- Algorithm complexity issues (time and space)
- Unnecessary memory allocations
- N+1 query patterns
- Missing caching opportunities
- Inefficient loops or data structures
- Resource leaks (file handles, connections)
- Concurrency issues
Provide specific, actionable feedback with severity levels (high/medium/low).
Format each issue as: [SEVERITY] Issue description - Optimization suggestion."""
user_message = f"""Performance review this {state['file_extension']} file named '{state['file_name']}':
```
{state['code_content']}
```"""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"performance_result": response.content}
def synthesizer_agent(state: ReviewState) -> dict:
"""Combines all agent results into a final structured review."""
system_prompt = """You are a senior code review synthesizer. You receive analysis from three specialized reviewers:
1. A code quality reviewer
2. A security reviewer
3. A performance reviewer
Your job is to:
- Combine their findings into a unified, structured review
- Remove duplicate issues mentioned by multiple reviewers
- Prioritize issues by overall impact
- Provide an executive summary with the top 3 most critical findings
- Give an overall health score (1-10) for the code
Format your response as:
## Executive Summary
[Top 3 findings and overall score]
## Critical Issues
[Issues that must be fixed]
## Improvements
[Suggested improvements ranked by impact]
## Positive Aspects
[Things done well in the code]"""
user_message = f"""Here are the reviews for '{state['file_name']}':
### Quality Review:
{state['quality_result']}
### Security Review:
{state['security_result']}
### Performance Review:
{state['performance_result']}
Please synthesize these into a final unified review."""
response = llm.invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
return {"synthesis_result": response.content}
# Build the review pipeline graph
workflow = StateGraph(ReviewState)
# Add all agent nodes
workflow.add_node("quality_agent", quality_agent)
workflow.add_node("security_agent", security_agent)
workflow.add_node("performance_agent", performance_agent)
workflow.add_node("synthesizer_agent", synthesizer_agent)
# Wire parallel execution: START fans out to all three agents
workflow.add_edge(START, "quality_agent")
workflow.add_edge(START, "security_agent")
workflow.add_edge(START, "performance_agent")
# All three agents fan in to the synthesizer
workflow.add_edge("quality_agent", "synthesizer_agent")
workflow.add_edge("security_agent", "synthesizer_agent")
workflow.add_edge("performance_agent", "synthesizer_agent")
# Synthesizer outputs the final result
workflow.add_edge("synthesizer_agent", END)
# Compile the graph into a runnable
review_graph = workflow.compile()
Your multi-agent review pipeline is complete. Next up, you will build a second pipeline for code generation, then test both from the command line.
Build the Code Generation Pipeline
The review pipeline handles code analysis, but CodeMentor AI also needs to generate code from scratch. When a user describes what they want to build, the system should produce complete, working code files.
You will build a second LangGraph pipeline with two agents working in sequence. The architect agent plans the file structure and approach, then the generator agent produces the actual code based on that plan.
In this step, get ready to:
- Build the architect and generator agent nodes.
- Wire the generation StateGraph with sequential execution.
- Test both pipelines from the command line.
Create the generation state and implement agent nodes
The generation pipeline needs its own state schema to track the user's prompt, the architecture plan, and the generated output. You will define this in a new file alongside the two agent functions.
- Create a new file called generation_pipeline.py inside your backend folder.
- Add the imports, state schema, and LLM initialization by pasting this code:
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_ollama import ChatOllama
# Define the shared state for the generation pipeline
class GenerationState(TypedDict):
"""Tracks the prompt, plan, generated code, and explanation."""
prompt: str
architecture_plan: str
generated_files: str
explanation: str
# Initialize the LLM
llm = ChatOllama(model="llama3.1")
What does this code do?
GenerationState is a TypedDict that defines the data flowing through the generation pipeline. Each field represents a stage of the process: the user's original prompt, the architecture_plan produced by the first agent, the generated_files output, and a summary explanation.
ChatOllama connects to your local Ollama instance running llama3.1, the same model you used in the review pipeline.
- Save generation_pipeline.py.
- Confirm the file appears in the backend folder in your VS Code Explorer sidebar.
- Next, add the architect_agent function below the llm line in generation_pipeline.py:
def architect_agent(state: GenerationState) -> dict:
"""Plans the file structure and approach for the requested code."""
system_prompt = (
"You are a software architect. Given a description of what to build, "
"plan the file structure and approach. Include:\n"
"- List of files needed with their paths\n"
"- The purpose of each file\n"
"- Key dependencies or libraries required\n"
"- The overall architecture pattern to follow\n\n"
"Be specific and practical. Output a clear, structured plan."
)
# Send the user's prompt to the LLM with the architect system prompt
messages = [
("system", system_prompt),
("human", f"Plan the architecture for: {state['prompt']}")
]
response = llm.invoke(messages)
return {"architecture_plan": response.content}
What does the architect agent do?
The architect agent receives the user's natural language description and produces a structured plan. It lists the files needed, explains each file's purpose, and recommends an architecture pattern.
This plan becomes the input for the generator agent in the next stage of the pipeline. Separating planning from code production gives the generator clearer instructions to work from.
- You won't see a change yet. The next function completes the agent pair.
- Add the generator_agent function below architect_agent in the same file:
def generator_agent(state: GenerationState) -> dict:
"""Produces complete code files based on the architecture plan."""
system_prompt = (
"You are an expert code generator. Given an architecture plan and the "
"original request, produce complete, working code files. For each file:\n"
"- Include the file name and path\n"
"- Write complete, runnable code with no placeholders\n"
"- Add brief inline comments explaining key sections\n\n"
"Format output with file separators like:\n"
"--- filename: path/to/file.py ---\n"
"[code here]\n\n"
"After all files, provide a brief explanation of how they work together."
)
# Combine the original prompt with the architecture plan for context
messages = [
("system", system_prompt),
("human", (
f"Original request: {state['prompt']}\n\n"
f"Architecture plan:\n{state['architecture_plan']}\n\n"
"Generate the complete code files."
))
]
response = llm.invoke(messages)
return {
"generated_files": response.content,
"explanation": f"Generated code based on: {state['architecture_plan'][:100]}"
}
What does the generator agent do?
The generator agent takes the architecture plan from the previous stage plus the original user prompt. It produces complete, runnable code files with inline comments and a structured format.
It returns both the generated_files content and a short explanation summarizing what was built.
- Save generation_pipeline.py.
- You won't see a visible change yet. The next substep wires the graph so both agents run in sequence.
Seeing a syntax error?
Check that both function definitions use def followed by the function name, parentheses, and a colon. Make sure the multi-line strings inside parentheses have matching opening and closing parentheses.
help me fix a syntax error in my generation_pipeline.py agent functions
Wire the generation StateGraph
With both agent functions defined, you can now connect them into a sequential graph. The architect runs first, then its output feeds into the generator.
- Add the graph wiring code at the bottom of generation_pipeline.py, below the generator_agent function:
# Build the generation StateGraph
generation_workflow = StateGraph(GenerationState)
# Add agent nodes
generation_workflow.add_node("architect", architect_agent)
generation_workflow.add_node("generator", generator_agent)
# Wire sequential flow: architect plans first, then generator produces code
generation_workflow.add_edge(START, "architect")
generation_workflow.add_edge("architect", "generator")
generation_workflow.add_edge("generator", END)
# Compile the graph
generation_graph = generation_workflow.compile()
What does this wiring do?
Unlike the review pipeline which fans out to three agents in parallel, this graph is purely sequential. The START node flows into the architect, the architect flows into the generator, and the generator flows to END.
generation_graph is the compiled, invokable pipeline. When you call generation_graph.invoke() with a prompt, LangGraph automatically passes state from one node to the next.
- Save generation_pipeline.py.
- The file is now complete. You will verify it works in the next substep by running it from the command line.
Seeing an import error on save?
Make sure your virtual environment is active (you should see the environment name in your terminal prompt). If langgraph or langchain_ollama are not found, re-run the pip install command from Step 1.
help me fix an import error in generation_pipeline.py
✔️ Awesome, I've got everything!
Great. Double check you have saved generation_pipeline.py with all four sections: imports and state, architect agent, generator agent, and graph wiring.
ⓧ I'd like to double check the full code
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_ollama import ChatOllama
# Define the shared state for the generation pipeline
class GenerationState(TypedDict):
"""Tracks the prompt, plan, generated code, and explanation."""
prompt: str
architecture_plan: str
generated_files: str
explanation: str
# Initialize the LLM
llm = ChatOllama(model="llama3.1")
def architect_agent(state: GenerationState) -> dict:
"""Plans the file structure and approach for the requested code."""
system_prompt = (
"You are a software architect. Given a description of what to build, "
"plan the file structure and approach. Include:\n"
"- List of files needed with their paths\n"
"- The purpose of each file\n"
"- Key dependencies or libraries required\n"
"- The overall architecture pattern to follow\n\n"
"Be specific and practical. Output a clear, structured plan."
)
# Send the user's prompt to the LLM with the architect system prompt
messages = [
("system", system_prompt),
("human", f"Plan the architecture for: {state['prompt']}")
]
response = llm.invoke(messages)
return {"architecture_plan": response.content}
def generator_agent(state: GenerationState) -> dict:
"""Produces complete code files based on the architecture plan."""
system_prompt = (
"You are an expert code generator. Given an architecture plan and the "
"original request, produce complete, working code files. For each file:\n"
"- Include the file name and path\n"
"- Write complete, runnable code with no placeholders\n"
"- Add brief inline comments explaining key sections\n\n"
"Format output with file separators like:\n"
"--- filename: path/to/file.py ---\n"
"[code here]\n\n"
"After all files, provide a brief explanation of how they work together."
)
# Combine the original prompt with the architecture plan for context
messages = [
("system", system_prompt),
("human", (
f"Original request: {state['prompt']}\n\n"
f"Architecture plan:\n{state['architecture_plan']}\n\n"
"Generate the complete code files."
))
]
response = llm.invoke(messages)
return {
"generated_files": response.content,
"explanation": f"Generated code based on: {state['architecture_plan'][:100]}"
}
# Build the generation StateGraph
generation_workflow = StateGraph(GenerationState)
# Add agent nodes
generation_workflow.add_node("architect", architect_agent)
generation_workflow.add_node("generator", generator_agent)
# Wire sequential flow: architect plans first, then generator produces code
generation_workflow.add_edge(START, "architect")
generation_workflow.add_edge("architect", "generator")
generation_workflow.add_edge("generator", END)
# Compile the graph
generation_graph = generation_workflow.compile()
Test both pipelines from the command line
Both pipelines are now complete. You will invoke each one from the terminal to confirm they produce real output from your local Ollama model.
- Make sure Ollama is running in the background. If it is not running, start it before continuing.
- Test the generation pipeline by running this command in your terminal (from inside the backend folder):
python -c "
from generation_pipeline import generation_graph
result = generation_graph.invoke({'prompt': 'Create a simple Python hello world script'})
print('=== Architecture Plan ===')
print(result['architecture_plan'][:300])
print()
print('=== Generated Files ===')
print(result['generated_files'][:300])
"
What should I see?
After a few seconds (the model needs time to generate), you should see an architecture plan describing the file structure for a hello world script, followed by the generated code. The output is truncated to 300 characters for readability.
- Now test the review pipeline to confirm it still works. Run this command:
python -c "
from review_pipeline import review_graph
result = review_graph.invoke({'code_content': 'def add(a, b): return a + b', 'file_name': 'math_utils.py', 'file_extension': '.py'})
print('=== Synthesis Result ===')
print(result['synthesis_result'][:300])
"
- You should see a synthesized review of the simple add function, with feedback about quality, security, and performance.
Seeing a connection error?
This means Ollama is not running. Open a separate terminal and start Ollama, then retry the test command. Ollama must be running on http://localhost:11434 for ChatOllama to connect.
help me troubleshoot a connection error when testing my LangGraph pipelines with Ollama
Getting a model not found error?
Make sure you pulled the llama3.1 model in Step 1. You can verify by running:
ollama pull llama3.1
help me fix a model not found error with Ollama and ChatOllama
Both pipelines are working end-to-end. Next up, you will wrap these pipelines in a FastAPI backend with real-time streaming so the frontend can display results as each agent completes.
Create the FastAPI Backend with SSE Streaming
You have two working LangGraph pipelines that can review and generate code from the command line. Now you need a way for the frontend to trigger these pipelines and receive results in real time.
In this step, you will build a FastAPI backend that exposes your pipelines as HTTP endpoints. The key feature is Server-Sent Events (SSE) streaming, which pushes each agent's result to the frontend the moment it finishes instead of making the user wait for all agents to complete.
In this step, get ready to:
- Create a FastAPI app with CORS and a file upload endpoint for code review.
- Add a code generation endpoint that accepts a text prompt.
- Implement SSE streaming so agent results arrive in real time.
Set up the FastAPI app with CORS and the review endpoint
Your Next.js frontend will run on port 3000. Your FastAPI backend will run on a different port. Browsers block requests between different origins by default, so you need CORS middleware to allow communication between them.
- Create a new file called main.py inside your backend folder.
- Add the following code to set up the FastAPI app, CORS middleware, and the /api/review endpoint:
import json
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette import EventSourceResponse, ServerSentEvent
from typing import Optional
# Import the compiled review pipeline
from review_pipeline import review_graph
# Create the FastAPI app
app = FastAPI(title="CodeMentor AI")
# Allow the Next.js frontend to communicate with this backend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/api/review")
async def review_code(
file: UploadFile = File(...),
prompt: Optional[str] = Form(None)
):
# Read the uploaded file content
content = await file.read()
code_content = content.decode("utf-8")
async def stream_review():
# Run the review pipeline with the uploaded code
result = review_graph.invoke({
"code_content": code_content,
"file_name": file.filename,
"prompt": prompt or "",
"quality_review": "",
"security_review": "",
"performance_review": "",
"final_synthesis": ""
})
# Stream each agent's result as a separate event
yield ServerSentEvent(
data=json.dumps({"agent": "quality", "result": result["quality_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "security", "result": result["security_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "performance", "result": result["performance_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "synthesizer", "result": result["final_synthesis"]}),
event="agent_result"
)
# Signal that the stream is complete
yield ServerSentEvent(data=json.dumps({"status": "complete"}), event="done")
return EventSourceResponse(stream_review())
What does this code do?
The imports pull in FastAPI's file upload handling (UploadFile), form data parsing (Form), and the sse-starlette package for streaming.
The CORS middleware is configured to accept requests only from http://localhost:3000, which is where your Next.js dev server runs.
The /api/review endpoint accepts a multipart file upload and an optional text prompt. It runs the review pipeline, then yields each agent's result as a ServerSentEvent so the frontend can display results progressively.
- Save main.py.
Seeing an import error for review_pipeline?
Make sure review_pipeline.py exists in the same backend folder as main.py. Python resolves imports relative to the directory you run the server from.
I'm getting an import error when trying to import review_pipeline in my FastAPI main.py file. How do I fix the module path?
Create the code generation endpoint
The generation endpoint works differently from the review endpoint. Instead of receiving a file, it accepts a JSON body with a text prompt describing what code to generate.
- In main.py, add the following code below the /api/review endpoint:
from pydantic import BaseModel
from generation_pipeline import generation_graph
# Define the request body shape for code generation
class GenerateRequest(BaseModel):
prompt: str
@app.post("/api/generate")
async def generate_code(request: GenerateRequest):
async def stream_generation():
# Run the generation pipeline with the user's prompt
result = generation_graph.invoke({
"prompt": request.prompt,
"architecture_plan": "",
"generated_files": "",
"explanation": ""
})
# Stream the architect's plan first
yield ServerSentEvent(
data=json.dumps({"agent": "architect", "result": result["architecture_plan"]}),
event="agent_result"
)
# Then stream the generated code
yield ServerSentEvent(
data=json.dumps({"agent": "generator", "result": result["generated_files"]}),
event="agent_result"
)
# Stream the explanation
yield ServerSentEvent(
data=json.dumps({"agent": "explanation", "result": result["explanation"]}),
event="agent_result"
)
# Signal completion
yield ServerSentEvent(data=json.dumps({"status": "complete"}), event="done")
return EventSourceResponse(stream_generation())
What does this code do?
The GenerateRequest model uses Pydantic to validate that the incoming JSON has a prompt field. FastAPI automatically parses and validates the request body against this schema.
The /api/generate endpoint invokes generation_graph and streams the architect's plan, the generated files, and an explanation as separate SSE events. This lets the frontend show each piece as soon as it is ready.
- Save main.py.
Seeing a validation error on the request body?
Make sure you are sending a JSON body with the Content-Type: application/json header. The GenerateRequest model expects a JSON object with a prompt key.
My /api/generate endpoint is returning a 422 validation error. How do I fix the request format?
Test the backend with uvicorn
With both endpoints in place, you can start the backend server and confirm it launches without errors.
- Start the FastAPI server by running this command from inside your backend folder:
uvicorn main:app --reload --port 8000
What does this command do?
The uvicorn command starts your FastAPI app. main:app tells uvicorn to look in main.py for the object named app.
The --reload flag auto-restarts the server when you change code. The --port 8000 flag sets the port so it does not conflict with Ollama on port 11434 or Next.js on port 3000.
- You should see output showing the server started on http://127.0.0.1:8000.
- Open http://127.0.0.1:8000/docs in your browser to confirm the interactive API documentation loads with both endpoints listed.
Server won't start?
If you see a module not found error, make sure you are running the command from inside the backend folder where main.py lives.
If the port is already in use, stop any other process on port 8000 or choose a different port with --port 8001.
My uvicorn server is failing to start with an error. Help me figure out what is wrong based on the error message.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and that the server is running without errors.
ⓧ I'd like to double check the full code
Here is the complete backend/main.py file:
import json
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette import EventSourceResponse, ServerSentEvent
from typing import Optional
from pydantic import BaseModel
# Import the compiled pipelines
from review_pipeline import review_graph
from generation_pipeline import generation_graph
# Create the FastAPI app
app = FastAPI(title="CodeMentor AI")
# Allow the Next.js frontend to communicate with this backend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/api/review")
async def review_code(
file: UploadFile = File(...),
prompt: Optional[str] = Form(None)
):
# Read the uploaded file content
content = await file.read()
code_content = content.decode("utf-8")
async def stream_review():
# Run the review pipeline with the uploaded code
result = review_graph.invoke({
"code_content": code_content,
"file_name": file.filename,
"prompt": prompt or "",
"quality_review": "",
"security_review": "",
"performance_review": "",
"final_synthesis": ""
})
# Stream each agent's result as a separate event
yield ServerSentEvent(
data=json.dumps({"agent": "quality", "result": result["quality_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "security", "result": result["security_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "performance", "result": result["performance_review"]}),
event="agent_result"
)
yield ServerSentEvent(
data=json.dumps({"agent": "synthesizer", "result": result["final_synthesis"]}),
event="agent_result"
)
# Signal that the stream is complete
yield ServerSentEvent(data=json.dumps({"status": "complete"}), event="done")
return EventSourceResponse(stream_review())
# Define the request body shape for code generation
class GenerateRequest(BaseModel):
prompt: str
@app.post("/api/generate")
async def generate_code(request: GenerateRequest):
async def stream_generation():
# Run the generation pipeline with the user's prompt
result = generation_graph.invoke({
"prompt": request.prompt,
"architecture_plan": "",
"generated_files": "",
"explanation": ""
})
# Stream the architect's plan first
yield ServerSentEvent(
data=json.dumps({"agent": "architect", "result": result["architecture_plan"]}),
event="agent_result"
)
# Then stream the generated code
yield ServerSentEvent(
data=json.dumps({"agent": "generator", "result": result["generated_files"]}),
event="agent_result"
)
# Stream the explanation
yield ServerSentEvent(
data=json.dumps({"agent": "explanation", "result": result["explanation"]}),
event="agent_result"
)
# Signal completion
yield ServerSentEvent(data=json.dumps({"status": "complete"}), event="done")
return EventSourceResponse(stream_generation())
Your backend is live and ready to serve real-time agent results. Next up, you will build the Next.js frontend that connects to these streaming endpoints and displays results as they arrive.
Build the Next.js Frontend
Your backend is ready to serve review and generation results as real-time streams. Now you need a way for users to actually interact with it.
In this step, you will build the Next.js frontend that lets users switch between Review and Generate modes, upload code files or type prompts, and watch agent results stream in live.
In this step, get ready to:
- Create the main page layout with a tab switcher and input areas for both modes.
- Implement an SSE client that streams agent results in real-time as styled cards.
- Add a results panel with copy buttons and syntax highlighting.
Install the frontend UI dependencies
Your Next.js scaffold already has Tailwind CSS set up. You need two additional packages to handle file drag-and-drop and syntax-highlighted code display.
- Open a new terminal in VS Code for the frontend. Navigate to the frontend folder and install the additional dependencies by running:
cd frontend
npm install react-dropzone react-syntax-highlighter @types/react-syntax-highlighter
What are these packages?
react-dropzone provides a useDropzone hook that turns any element into a drag-and-drop file upload zone with zero boilerplate.
react-syntax-highlighter renders code strings with Prism-based syntax highlighting and inline styles, so your generated code looks professional.
- Confirm the packages installed successfully by checking for a success message in your terminal output.
Seeing install errors?
Make sure you are inside the frontend directory, not the project root or the backend folder. Run pwd to check.
help me fix npm install errors in my Next.js project
Create the main page with tab switcher and input areas
The entire CodeMentor AI interface lives in a single page component. You will use the App Router's page.tsx file with a "use client" directive since the component relies on browser APIs for file reading and SSE streaming.
- Open frontend/app/page.tsx in VS Code. Delete all the default scaffold content.
- Add the client directive and imports at the top of the file by pasting this code:
"use client";
import { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
Why use the CJS import path?
Next.js can have compatibility issues with the ESM build of react-syntax-highlighter. Importing from dist/cjs/styles/prism uses the CommonJS build, which works reliably with the Next.js bundler.
- Below the imports, add the TypeScript interfaces that define the shape of results from your backend agents:
// Define the shape of agent results from the backend
interface AgentResult {
agent: string;
content: string;
status: "complete" | "error";
}
interface ReviewFinding {
severity: "critical" | "warning" | "info";
title: string;
description: string;
suggestion?: string;
}
What do these interfaces represent?
AgentResult matches the JSON structure your FastAPI backend sends in each SSE event. Each event names the agent, its content, and status.
ReviewFinding represents individual findings parsed from an agent's output, with severity levels that your UI will display as colored badges.
- Now add the main component with state management and the dropzone hook:
export default function Home() {
// Track which mode the user is in
const [mode, setMode] = useState<"review" | "generate">("review");
// Store the uploaded file for review mode
const [file, setFile] = useState<File | null>(null);
// Store the text prompt for generate mode
const [prompt, setPrompt] = useState("");
// Store streaming results from agents
const [results, setResults] = useState<AgentResult[]>([]);
// Track whether we're currently streaming
const [isStreaming, setIsStreaming] = useState(false);
// Configure the drag-and-drop file upload zone
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setFile(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"text/*": [".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".go", ".rs"],
},
maxFiles: 1,
});
What does useDropzone do here?
The useDropzone hook from react-dropzone provides getRootProps and getInputProps which turn any div into a drag-and-drop target. The accept option restricts uploads to code files only.
- Next, add the JSX return that renders the tab switcher and the mode-specific input areas. Paste this below the dropzone setup, still inside the Home component:
return (
<main className="min-h-screen bg-gray-950 text-white p-8">
<div className="max-w-4xl mx-auto">
{/* Header */}
<h1 className="text-3xl font-bold mb-2">CodeMentor AI</h1>
<p className="text-gray-400 mb-8">
Multi-agent code review and generation platform
</p>
{/* Tab Switcher */}
<div className="flex gap-2 mb-8">
<button
onClick={() => setMode("review")}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === "review"
? "bg-blue-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-white"
}`}
>
Review Code
</button>
<button
onClick={() => setMode("generate")}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === "generate"
? "bg-purple-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-white"
}`}
>
Generate Code
</button>
</div>
{/* Review Mode: File Upload Dropzone */}
{mode === "review" && (
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-12 text-center cursor-pointer transition-colors ${
isDragActive
? "border-blue-500 bg-blue-500/10"
: file
? "border-green-500 bg-green-500/10"
: "border-gray-700 hover:border-gray-500"
}`}
>
<input {...getInputProps()} />
{file ? (
<div>
<p className="text-green-400 font-medium">{file.name}</p>
<p className="text-gray-500 text-sm mt-1">
{(file.size / 1024).toFixed(1)} KB - Click or drop to replace
</p>
</div>
) : isDragActive ? (
<p className="text-blue-400">Drop your code file here...</p>
) : (
<div>
<p className="text-gray-300">Drag and drop a code file here</p>
<p className="text-gray-500 text-sm mt-1">
or click to select (.py, .js, .ts, .java, .go, .rs)
</p>
</div>
)}
</div>
)}
{/* Generate Mode: Text Input */}
{mode === "generate" && (
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe what you want to build... e.g. 'A Python FastAPI endpoint that validates email addresses'"
className="w-full h-40 bg-gray-900 border border-gray-700 rounded-xl p-4 text-white placeholder-gray-500 resize-none focus:outline-none focus:border-purple-500"
/>
)}
</div>
</main>
);
}
- Save frontend/app/page.tsx.
TypeScript errors on save?
If you see type errors about react-syntax-highlighter, confirm you installed @types/react-syntax-highlighter in the dependency install step. Run npm install @types/react-syntax-highlighter again if needed.
help me fix TypeScript errors with react-syntax-highlighter in Next.js
Implement the SSE client and real-time results rendering
Your backend uses POST-based Server-Sent Events. The native EventSource API only supports GET requests, so you will use fetch with a ReadableStream reader to process the SSE stream from POST endpoints.
- In frontend/app/page.tsx, find the line that says const { getRootProps, getInputProps, isDragActive } = useDropzone({ and scroll down past the closing of the dropzone config. Add the SSE streaming function below it, before the return statement:
// Handle form submission and stream results from the backend
const handleSubmit = async () => {
setResults([]);
setIsStreaming(true);
try {
let response: Response;
if (mode === "review" && file) {
// Send the file as multipart form data to the review endpoint
const formData = new FormData();
formData.append("file", file);
response = await fetch("http://localhost:8000/api/review", {
method: "POST",
body: formData,
});
} else if (mode === "generate" && prompt) {
// Send the prompt as JSON to the generation endpoint
response = await fetch("http://localhost:8000/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
} else {
setIsStreaming(false);
return;
}
// Read the SSE stream using a ReadableStream reader
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });
// Parse SSE events from the buffer
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6)) as AgentResult;
setResults((prev) => [...prev, data]);
} catch {
// Skip malformed JSON lines
}
}
}
}
} catch (error) {
console.error("Stream error:", error);
} finally {
setIsStreaming(false);
}
};
How does this SSE client work?
Since your backend uses POST endpoints for streaming, you cannot use the browser's built-in EventSource (which only supports GET). Instead, fetch gets the response as a readable stream. The code reads chunks incrementally and parses lines that start with data: as SSE events.
Each parsed event is added to the results state array, which triggers React to re-render and show new agent results instantly.
- Now add a submit button to the JSX. In the return statement, find the closing </div> of the Generate mode textarea section (or the dropzone section if in review mode). Add this button below both mode sections, before the closing </div> of the max-w-4xl container:
{/* Submit Button */}
<button
onClick={handleSubmit}
disabled={isStreaming || (mode === "review" ? !file : !prompt)}
className={`mt-6 w-full py-3 rounded-xl font-medium transition-colors ${
isStreaming
? "bg-gray-700 text-gray-400 cursor-not-allowed"
: mode === "review"
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-purple-600 hover:bg-purple-700 text-white"
}`}
>
{isStreaming
? "Analyzing..."
: mode === "review"
? "Start Review"
: "Generate Code"}
</button>
{/* Results Panel */}
{results.length > 0 && (
<div className="mt-8 space-y-4">
<h2 className="text-xl font-semibold">
{mode === "review" ? "Review Results" : "Generated Code"}
</h2>
{results.map((result, index) => (
<ResultCard key={index} result={result} mode={mode} />
))}
</div>
)}
- Save frontend/app/page.tsx.
Add the results panel with severity badges and copy buttons
Each agent result appears as a card with a header showing the agent name, severity badges for review findings, and copy-to-clipboard buttons for generated code.
- In frontend/app/page.tsx, add the ResultCard component above the Home component (after the interfaces, before export default function Home):
// Renders a single agent result as a styled card
function ResultCard({
result,
mode,
}: {
result: AgentResult;
mode: "review" | "generate";
}) {
const [copied, setCopied] = useState(false);
// Map agent names to display-friendly labels
const agentLabels: Record<string, string> = {
quality_agent: "Quality Review",
security_agent: "Security Review",
performance_agent: "Performance Review",
synthesizer_agent: "Final Summary",
architect_agent: "Architecture Plan",
generator_agent: "Generated Code",
};
// Get severity badge color based on content keywords
const getSeverityColor = (text: string) => {
if (text.toLowerCase().includes("critical")) return "bg-red-500";
if (text.toLowerCase().includes("warning")) return "bg-yellow-500";
return "bg-blue-500";
};
// Copy content to clipboard
const handleCopy = () => {
navigator.clipboard.writeText(result.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
{/* Card Header */}
<div className="flex items-center justify-between px-4 py-3 bg-gray-800/50">
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
result.status === "complete" ? "bg-green-400" : "bg-red-400"
}`}
/>
<span className="font-medium text-sm">
{agentLabels[result.agent] || result.agent}
</span>
</div>
<div className="flex items-center gap-2">
{mode === "review" && (
<span
className={`px-2 py-0.5 rounded text-xs font-medium text-white ${getSeverityColor(
result.content
)}`}
>
{result.content.toLowerCase().includes("critical")
? "Critical"
: result.content.toLowerCase().includes("warning")
? "Warning"
: "Info"}
</span>
)}
<button
onClick={handleCopy}
className="text-gray-400 hover:text-white text-xs px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
>
{copied ? "Copied!" : "Copy"}
</button>
</div>
</div>
{/* Card Body */}
<div className="p-4">
{mode === "generate" || result.agent === "generator_agent" ? (
<SyntaxHighlighter
language="python"
style={oneDark}
customStyle={{
background: "transparent",
padding: 0,
margin: 0,
fontSize: "0.875rem",
}}
>
{result.content}
</SyntaxHighlighter>
) : (
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">
{result.content}
</pre>
)}
</div>
</div>
);
}
What does this component do?
The ResultCard component renders each streamed agent result. It shows a green dot for completed agents, maps internal agent names to friendly labels, and adds a severity badge for review results.
For generated code, it uses SyntaxHighlighter with the oneDark theme to render properly highlighted code. The copy button uses the browser's clipboard API to let users grab results instantly.
- Save frontend/app/page.tsx.
- Start the backend in one terminal by running:
cd backend
source venv/bin/activate
uvicorn main:app --reload --port 8000
- Start the frontend in your other terminal by running:
cd frontend
npm run dev
- Open http://localhost:3000 in your browser. You should see the CodeMentor AI interface with the Review/Generate tab switcher, a dashed file dropzone area, and the dark theme applied.
- Click the **Generate Code** tab. The dropzone should disappear and a text area should appear in its place.
- Try dragging a .py or .js file onto the dropzone in Review mode. The dropzone should turn green and display the filename.
Page not loading or showing errors?
If you see a module resolution error mentioning react-syntax-highlighter, double check that your import uses dist/cjs/styles/prism (not dist/esm). The ESM path has known compatibility issues with recent versions of Next.js.
If the page loads but the dropzone does not respond to files, confirm you have the "use client" directive as the very first line in page.tsx. Without it, React hooks will not work.
help me debug my Next.js frontend for CodeMentor AI
✔️ Awesome, I've got everything!
Great. Make sure both the backend and frontend are running. You are ready to test the full flow.
ⓧ I'd like to double check the full code
Here is the complete frontend/app/page.tsx file:
"use client";
import { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
// Define the shape of agent results from the backend
interface AgentResult {
agent: string;
content: string;
status: "complete" | "error";
}
interface ReviewFinding {
severity: "critical" | "warning" | "info";
title: string;
description: string;
suggestion?: string;
}
// Renders a single agent result as a styled card
function ResultCard({
result,
mode,
}: {
result: AgentResult;
mode: "review" | "generate";
}) {
const [copied, setCopied] = useState(false);
// Map agent names to display-friendly labels
const agentLabels: Record<string, string> = {
quality_agent: "Quality Review",
security_agent: "Security Review",
performance_agent: "Performance Review",
synthesizer_agent: "Final Summary",
architect_agent: "Architecture Plan",
generator_agent: "Generated Code",
};
// Get severity badge color based on content keywords
const getSeverityColor = (text: string) => {
if (text.toLowerCase().includes("critical")) return "bg-red-500";
if (text.toLowerCase().includes("warning")) return "bg-yellow-500";
return "bg-blue-500";
};
// Copy content to clipboard
const handleCopy = () => {
navigator.clipboard.writeText(result.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
{/* Card Header */}
<div className="flex items-center justify-between px-4 py-3 bg-gray-800/50">
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
result.status === "complete" ? "bg-green-400" : "bg-red-400"
}`}
/>
<span className="font-medium text-sm">
{agentLabels[result.agent] || result.agent}
</span>
</div>
<div className="flex items-center gap-2">
{mode === "review" && (
<span
className={`px-2 py-0.5 rounded text-xs font-medium text-white ${getSeverityColor(
result.content
)}`}
>
{result.content.toLowerCase().includes("critical")
? "Critical"
: result.content.toLowerCase().includes("warning")
? "Warning"
: "Info"}
</span>
)}
<button
onClick={handleCopy}
className="text-gray-400 hover:text-white text-xs px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
>
{copied ? "Copied!" : "Copy"}
</button>
</div>
</div>
{/* Card Body */}
<div className="p-4">
{mode === "generate" || result.agent === "generator_agent" ? (
<SyntaxHighlighter
language="python"
style={oneDark}
customStyle={{
background: "transparent",
padding: 0,
margin: 0,
fontSize: "0.875rem",
}}
>
{result.content}
</SyntaxHighlighter>
) : (
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">
{result.content}
</pre>
)}
</div>
</div>
);
}
export default function Home() {
// Track which mode the user is in
const [mode, setMode] = useState<"review" | "generate">("review");
// Store the uploaded file for review mode
const [file, setFile] = useState<File | null>(null);
// Store the text prompt for generate mode
const [prompt, setPrompt] = useState("");
// Store streaming results from agents
const [results, setResults] = useState<AgentResult[]>([]);
// Track whether we're currently streaming
const [isStreaming, setIsStreaming] = useState(false);
// Configure the drag-and-drop file upload zone
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setFile(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"text/*": [".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".go", ".rs"],
},
maxFiles: 1,
});
// Handle form submission and stream results from the backend
const handleSubmit = async () => {
setResults([]);
setIsStreaming(true);
try {
let response: Response;
if (mode === "review" && file) {
// Send the file as multipart form data to the review endpoint
const formData = new FormData();
formData.append("file", file);
response = await fetch("http://localhost:8000/api/review", {
method: "POST",
body: formData,
});
} else if (mode === "generate" && prompt) {
// Send the prompt as JSON to the generation endpoint
response = await fetch("http://localhost:8000/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
} else {
setIsStreaming(false);
return;
}
// Read the SSE stream using a ReadableStream reader
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });
// Parse SSE events from the buffer
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6)) as AgentResult;
setResults((prev) => [...prev, data]);
} catch {
// Skip malformed JSON lines
}
}
}
}
} catch (error) {
console.error("Stream error:", error);
} finally {
setIsStreaming(false);
}
};
return (
<main className="min-h-screen bg-gray-950 text-white p-8">
<div className="max-w-4xl mx-auto">
{/* Header */}
<h1 className="text-3xl font-bold mb-2">CodeMentor AI</h1>
<p className="text-gray-400 mb-8">
Multi-agent code review and generation platform
</p>
{/* Tab Switcher */}
<div className="flex gap-2 mb-8">
<button
onClick={() => setMode("review")}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === "review"
? "bg-blue-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-white"
}`}
>
Review Code
</button>
<button
onClick={() => setMode("generate")}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === "generate"
? "bg-purple-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-white"
}`}
>
Generate Code
</button>
</div>
{/* Review Mode: File Upload Dropzone */}
{mode === "review" && (
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-12 text-center cursor-pointer transition-colors ${
isDragActive
? "border-blue-500 bg-blue-500/10"
: file
? "border-green-500 bg-green-500/10"
: "border-gray-700 hover:border-gray-500"
}`}
>
<input {...getInputProps()} />
{file ? (
<div>
<p className="text-green-400 font-medium">{file.name}</p>
<p className="text-gray-500 text-sm mt-1">
{(file.size / 1024).toFixed(1)} KB - Click or drop to replace
</p>
</div>
) : isDragActive ? (
<p className="text-blue-400">Drop your code file here...</p>
) : (
<div>
<p className="text-gray-300">Drag and drop a code file here</p>
<p className="text-gray-500 text-sm mt-1">
or click to select (.py, .js, .ts, .java, .go, .rs)
</p>
</div>
)}
</div>
)}
{/* Generate Mode: Text Input */}
{mode === "generate" && (
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe what you want to build... e.g. 'A Python FastAPI endpoint that validates email addresses'"
className="w-full h-40 bg-gray-900 border border-gray-700 rounded-xl p-4 text-white placeholder-gray-500 resize-none focus:outline-none focus:border-purple-500"
/>
)}
{/* Submit Button */}
<button
onClick={handleSubmit}
disabled={isStreaming || (mode === "review" ? !file : !prompt)}
className={`mt-6 w-full py-3 rounded-xl font-medium transition-colors ${
isStreaming
? "bg-gray-700 text-gray-400 cursor-not-allowed"
: mode === "review"
? "bg-blue-600 hover:bg-blue-700 text-white"
: "bg-purple-600 hover:bg-purple-700 text-white"
}`}
>
{isStreaming
? "Analyzing..."
: mode === "review"
? "Start Review"
: "Generate Code"}
</button>
{/* Results Panel */}
{results.length > 0 && (
<div className="mt-8 space-y-4">
<h2 className="text-xl font-semibold">
{mode === "review" ? "Review Results" : "Generated Code"}
</h2>
{results.map((result, index) => (
<ResultCard key={index} result={result} mode={mode} />
))}
</div>
)}
</div>
</main>
);
}
Your full-stack CodeMentor AI platform is now connected end-to-end. The frontend streams real-time results from your multi-agent backend, displaying each agent's analysis as it completes. Next up, you will test the entire system with real code files and see all your agents in action.
Secret mission
Add Conversational Follow-Up Mode
Your code review and generation system works great for one-shot analysis. But what if the learner wants to dig deeper? In this bonus challenge, you will add a conversational follow-up mode. After a review or generation completes, users can ask questions like "How would I fix the security issue in line 12?" and get context-aware responses streamed back in real time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Ollama, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Ollama llama3.1 model (stored on disk).
- Python virtual environment and backend files (backend/ directory).
- Next.js frontend project (frontend/ directory).
- Running processes: uvicorn server on port 8000, Next.js dev server on port 3000.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with CodeMentor AI.
- Your uvicorn backend and Next.js dev server will keep running until you close their terminal windows.
- The Ollama llama3.1 model stays on disk and is available for other projects too.
- Your Python virtual environment stays in the project folder and activates again when you return.
Pause - I'll come back to this later
Shut down running processes to free up memory and CPU. Your project files and downloaded model stay on disk.
- In the terminal running your backend, press Ctrl+C to stop the uvicorn server.
- In the terminal running your frontend, press Ctrl+C to stop the Next.js dev server.
- Deactivate your Python virtual environment by running:
deactivate
When you come back, navigate to your project folder, activate the virtual environment, and start both servers again.
Delete - I don't want to use this again
Remove all project resources and free up disk space completely.
- Stop both running servers by pressing Ctrl+C in each terminal window.
- Remove the downloaded Ollama model by running:
ollama rm llama3.1
- Delete the entire project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf codementor-ai
- On Windows, run:
cd ~/Desktop
Remove-Item -Recurse -Force codementor-ai
This removes your backend, frontend, virtual environment, and all project files.
What about Ollama itself?
If you want to keep Ollama for future projects, leave it installed. It takes minimal disk space without models downloaded.
To fully uninstall Ollama on Windows, open Settings, select Apps, then Installed apps, find Ollama, and click Uninstall.
On macOS, remove the app and its data by running:
sudo rm -rf /Applications/Ollama.app
rm -rf ~/.ollama
Nice Work!
Nice Work!
You just built CodeMentor AI, a fully functional multi-agent code review and generation platform running entirely on your local machine.
You've learned how to:
- Build a multi-agent code review system with three specialized agents running in parallel via LangGraph, each analyzing code for quality, security, and performance issues.
- Create a FastAPI backend with real-time Server-Sent Events streaming and file upload handling, delivering agent results to the browser as they complete.
- Build a Next.js frontend with mode switching, drag-and-drop file upload, and live-updating result cards with severity badges and syntax highlighting.
- Secret Mission: Added a conversational follow-up mode that maintains context from previous reviews or generations, enabling iterative development through natural language questions.
Ready to quiz yourself?