Course
Give your AI a permanent memory of your business. A course for people who use ChatGPT or Claude daily.Compound Context
Mar 24, 2026Tutorials11 min read

Build a Code Review AI Agent in Under 100 Lines of Python

NBNikolas Barwicki
AI AgentsCode ReviewPythonOpenAITutorial

Production tools like CodeRabbit use the same core pattern. Here is how to build your own code review agent in under 100 lines of Python.

Tools like CodeRabbit and Qodo's PR-Agent can review entire pull requests, flag security issues, and suggest fixes — automatically. They feel like magic, but the underlying pattern is not complicated.

A code review agent is just an LLM that can read files. Give it tools to browse a directory and read source code, wire up the tool calling loop, and you have a working reviewer. This tutorial builds one from scratch in under 100 lines of Python using the OpenAI API.

By the end, you will have a script you can point at any Python project to get a structured review — bugs, security issues, style problems, and suggested fixes.

Why Code Review Is a Perfect Agent Use Case

Code review maps naturally to the agent pattern. The LLM cannot review code it has not seen, so it needs tools to discover and read files. It also needs multiple turns — first listing what is in the project, then reading files one by one, then synthesizing findings across everything it read.

This is fundamentally different from pasting a file into ChatGPT:

  • No file access — you manually copy-paste each file
  • Fixed context window — large projects don't fit in a single prompt
  • No autonomy — the model can't decide which files to inspect next

An agent solves all three. It decides what to look at, reads as many files as it needs, and reasons across all of them before delivering a verdict. That decision loop — reason, act, observe, repeat — is the same ReAct pattern behind every agent framework. Code review just gives it a sharp, practical purpose.

Prerequisites

pip install openai

Set your API key as an environment variable:

export OPENAI_API_KEY="your-key-here"

Step 1: Define the Agent's Tools

An agent can only interact with the world through its tools. For code review, we need two:

  • list_python_files — scans a directory and returns all .py files
  • read_file — reads the contents of a specific file

Here are the Python functions:

import os
import json

def list_python_files(directory: str) -> str:
    files = [
        os.path.join(directory, f)
        for f in os.listdir(directory)
        if f.endswith(".py")
    ]
    return json.dumps(files)

def read_file(path: str) -> str:
    with open(path) as f:
        return f.read()

Both return strings because tool results get sent back to the LLM as text. The file listing returns JSON so the model can parse individual paths.

Next, describe these tools in the format OpenAI's API expects — a JSON schema for each function:

tools = [
    {
        "type": "function",
        "function": {
            "name": "list_python_files",
            "description": "List all Python files in a directory",
            "parameters": {
                "type": "object",
                "properties": {
                    "directory": {
                        "type": "string",
                        "description": "Path to the directory to scan",
                    }
                },
                "required": ["directory"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read the contents of a file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Path to the file to read",
                    }
                },
                "required": ["path"],
            },
        },
    },
]

# Maps function names to actual Python functions
available_tools = {
    "list_python_files": list_python_files,
    "read_file": read_file,
}

When the LLM asks to call read_file, we look up the name in available_tools and execute it. This dict is the bridge between the model's intent and your code.

Step 2: Write the System Prompt

The system prompt defines what kind of reviewer this agent is. Be specific about what to look for and how to report findings:

SYSTEM_PROMPT = """You are a senior Python code reviewer. Review all Python
files in a project directory and provide actionable feedback.

For each file, check for:
- Bugs and logic errors
- Security vulnerabilities (SQL injection, command injection, etc.)
- Performance issues
- Non-Pythonic patterns and style problems
- Missing error handling where it matters

Steps:
1. List the Python files in the target directory.
2. Read and review each file.
3. Provide a structured summary of all findings.

For each issue, include: severity (high/medium/low), file name and line
number, description of the problem, and a suggested fix."""

Two things worth noting. The prompt includes explicit steps — this guides the model to use its tools in the right order instead of trying to answer immediately. And the output format (severity, file, line, fix) gives the model structure without requiring rigid JSON. You get readable output that is still organized.

Step 3: Build the Agent Loop

This is the engine. The loop sends messages to the API, checks if the model wants to call tools, executes those tools, feeds the results back, and repeats until the model delivers its final answer:

import sys
from openai import OpenAI

client = OpenAI()

def run_review(directory: str):
    print(f"Reviewing Python files in: {directory}\n")

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"Review all Python files in: {directory}"},
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o", messages=messages, tools=tools
        )
        message = response.choices[0].message
        messages.append(message)

        # No tool calls means the model is done
        if not message.tool_calls:
            print(message.content)
            break

        # Execute each tool the model requested
        for tool_call in message.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            result = available_tools[name](**args)
            print(f"  -> {name}({json.dumps(args)})")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

if __name__ == "__main__":
    run_review(sys.argv[1] if len(sys.argv) > 1 else ".")

The while True loop is the agent. Without it, you have a single-shot API call. With it, the model can take multiple actions, observe results, and build up context before answering. Each iteration is one "turn" — the model either calls a tool (and the loop continues) or returns a text response (and the loop breaks).

If you want a deeper walkthrough of this pattern across multiple LLM providers, see How to Build an AI Agent from Scratch.

Step 4: Run It on Real Code

Let's test the agent on a file with some intentional problems. Create example.py:

import os
import json
import sqlite3


def get_user(user_id):
    conn = sqlite3.connect("app.db")
    query = f"SELECT * FROM users WHERE id = {user_id}"
    result = conn.execute(query).fetchone()
    return result


def process_items(items=[]):
    items.append("processed")
    return items


def read_config(path):
    try:
        with open(path) as f:
            return f.read()
    except:
        return None


def calculate_average(numbers):
    total = 0
    for n in numbers:
        total = total + n
    average = total / len(numbers)
    return average

This file has at least seven issues hiding in plain sight. Run the agent:

python review_agent.py ./

The terminal shows the agent's tool calls in real time:

Reviewing Python files in: ./

  -> list_python_files({"directory": "./"})
  -> read_file({"path": "./example.py"})

After reading the file, the agent delivers its review:

## Code Review: example.py

**HIGH — SQL Injection (line 8)**
The query uses an f-string to interpolate `user_id` directly into SQL.
An attacker can pass malicious input to extract or destroy data.
Fix: Use parameterized queries — `conn.execute("SELECT * FROM users
WHERE id = ?", (user_id,))`

**HIGH — Mutable Default Argument (line 13)**
`items=[]` is shared across all calls to `process_items`. Each call
mutates the same list.
Fix: Use `items=None` and initialize inside the function.

**MEDIUM — Bare Except (line 22)**
Catches all exceptions including KeyboardInterrupt and SystemExit.
Fix: Catch `FileNotFoundError` or `OSError` specifically.

**MEDIUM — Connection Never Closed (line 7)**
The SQLite connection is opened but never closed, risking resource leaks.
Fix: Use a `with` statement — `with sqlite3.connect("app.db") as conn:`

**LOW — Unused Import (line 2)**
`json` is imported but never used.
Fix: Remove the import.

**LOW — Not Using sum() Builtin (line 28)**
The manual loop can be replaced with `total = sum(numbers)`.

**LOW — ZeroDivisionError Risk (line 30)**
`len(numbers)` could be zero. Add a guard clause or return 0 for
empty lists.

Seven issues, ranked by severity, with concrete fixes. The agent decided on its own to list the directory first, then read the file, then analyze everything it found.

Step 5: Improve the Output with Structured Responses

The text output above is readable, but sometimes you need machine-readable results — to pipe into a CI check or post as GitHub comments. OpenAI's structured outputs feature lets you enforce a JSON schema on the response.

Define a Pydantic model for the review:

from pydantic import BaseModel

class Issue(BaseModel):
    severity: str
    file: str
    line: int
    description: str
    fix: str

class CodeReview(BaseModel):
    issues: list[Issue]
    summary: str

Then pass it to the API call using response_format:

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=messages,
    response_format=CodeReview,
)

review = response.choices[0].message.parsed
for issue in review.issues:
    print(f"[{issue.severity.upper()}] {issue.file}:{issue.line} - {issue.description}")

This gives you structured data you can feed into dashboards, Slack notifications, or CI pipelines. A few extra lines, but a big step toward production use.

The Complete Code

Here is the full agent in one copy-pasteable script — everything from imports to execution:

import os
import sys
import json
from openai import OpenAI

client = OpenAI()


def list_python_files(directory: str) -> str:
    files = [
        os.path.join(directory, f)
        for f in os.listdir(directory)
        if f.endswith(".py")
    ]
    return json.dumps(files)


def read_file(path: str) -> str:
    with open(path) as f:
        return f.read()


tools = [
    {
        "type": "function",
        "function": {
            "name": "list_python_files",
            "description": "List all Python files in a directory",
            "parameters": {
                "type": "object",
                "properties": {
                    "directory": {
                        "type": "string",
                        "description": "Path to the directory to scan",
                    }
                },
                "required": ["directory"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read the contents of a file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Path to the file to read",
                    }
                },
                "required": ["path"],
            },
        },
    },
]

available_tools = {
    "list_python_files": list_python_files,
    "read_file": read_file,
}

SYSTEM_PROMPT = """You are a senior Python code reviewer. Review all Python
files in a project directory and provide actionable feedback.

For each file, check for:
- Bugs and logic errors
- Security vulnerabilities (SQL injection, command injection, etc.)
- Performance issues
- Non-Pythonic patterns and style problems
- Missing error handling where it matters

Steps:
1. List the Python files in the target directory.
2. Read and review each file.
3. Provide a structured summary of all findings.

For each issue, include: severity (high/medium/low), file name and line
number, description of the problem, and a suggested fix."""


def run_review(directory: str):
    print(f"Reviewing Python files in: {directory}\n")

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"Review all Python files in: {directory}"},
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o", messages=messages, tools=tools
        )
        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            print(message.content)
            break

        for tool_call in message.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            result = available_tools[name](**args)
            print(f"  -> {name}({json.dumps(args)})")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })


if __name__ == "__main__":
    run_review(sys.argv[1] if len(sys.argv) > 1 else ".")

Save this as review_agent.py and run it with python review_agent.py ./your-project.

Where to Go From Here

This agent reviews files in a flat directory. Here is how to make it genuinely useful:

  • Add recursive scanning — replace os.listdir with os.walk to handle real project structures with nested directories.
  • Integrate with git — add a get_git_diff tool so the agent reviews only changed files, not the entire codebase. This is how production tools like CodeRabbit operate.
  • Connect to GitHub PRs — use the GitHub REST API to pull PR diffs and post review comments directly on the pull request.
  • Add more tools — let the agent run pytest, check type hints with mypy, or look up documentation. More tools mean deeper reviews.
  • Scale with multiple agents — assign different agents to security, style, and performance reviews running in parallel. See Multi-Agent AI Systems for how this works.
  • Add guardrails — if the agent will run in CI or touch sensitive repos, add constraints on which files it can read and how much it can spend on API calls. Our guardrails guide covers the key patterns.

Conclusion

You built a working code review agent in under 100 lines of Python. It discovers files, reads source code, reasons about what it found, and delivers a structured review with severity ratings and concrete fixes. The pattern — tools, a system prompt, and a loop — is the same one behind every AI agent. The tools are what make it specialized.

The next time you are reviewing a pull request, try running this script alongside your own review. You might be surprised how many issues it catches that humans routinely miss.

Reach 25,000+ AI enthusiasts every month

Promote your AI tool with featured placement, measurable visibility, and referral traffic.

Learn more →