PHP Code Review Agent with Gemini
Build a Python CLI agent that autonomously reviews PHP files using Gemini AI.
Introduction
30 Second Summary
Code reviews catch bugs before they reach production, but reviewing every PHP file manually takes time and expertise you might not always have on hand.
In this project, you will build a Python CLI tool that uses Google Gemini as an AI agent to autonomously read and review your PHP files for security issues, best practice violations, and potential bugs.
What You'll Build
You'll create a terminal-based AI agent that accepts a PHP file or directory path and produces a structured code review with severity-tagged findings.
By the end of this project, you'll have:
- A working AI agent that reads PHP files and provides structured code review feedback from your terminal.
- A deep understanding of the agent loop pattern (prompt, tool call, execute, respond) that powers modern AI tools like ChatGPT plugins and GitHub Copilot.
- Hands-on experience with Gemini's function calling API using the official Google GenAI Python SDK.
- Secret Mission: Add a PHP syntax checking tool that runs php -l and feeds results back to the agent.
Are there any prerequisites?
You should have basic Python knowledge (variables, functions, loops) and be familiar with using the terminal. No prior experience with AI APIs or PHP is required.
Set Up Your Environment
Before you can build an AI agent, you need the right tools in place. The google-genai package gives your Python scripts the ability to communicate with Google's Gemini models.
In this step, you will install the SDK, grab a free API key from Google AI Studio, and confirm everything works with a quick test script.
In this step, get ready to:
- Install the Google GenAI Python SDK.
- Get a free Gemini API key.
- Test the connection with a simple script.
Install the Google GenAI SDK
The google-genai package is the official Python SDK for interacting with Google's Gemini API. It handles authentication, request formatting, and response parsing so you can focus on building your agent logic.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) and searching for Terminal (macOS) or Command Prompt (Windows).
- Install the SDK by running this command:
pip install google-genai
What does this command do?
The pip install command downloads and installs the google-genai package from PyPI. This gives you access to the genai module, which is the entry point for all Gemini API interactions.
- Verify the installation by running:
pip show google-genai
You should see output listing the package details, including a Version field showing 2.8.0 or later.
✔️ I see version 2.8.0 or later
The SDK is installed and ready to go.
ⓧ I see an error or the package is not found
If you see a "not found" message, the install may have failed silently.
- Try running the install command again with the --user flag:
pip install --user google-genai
- If you have multiple Python versions installed, try pip3 install google-genai instead.
- Run pip show google-genai again to confirm.
Get your Gemini API key
To use the Gemini API, you need an API key that identifies your requests to Google's servers. Google AI Studio provides free API keys that work with Flash models at no cost.
- Open this link in your browser:
- https://aistudio.google.com/apikey
- Sign in with your Google account if prompted.
- Click Create API key.
- Select a Google Cloud project (if this is your first time, AI Studio creates one for you automatically).
- Copy the generated API key and keep it somewhere safe. You will only see it once.
Why an environment variable?
Storing your API key in an environment variable keeps it out of your source code. If you ever share your code, the key stays private because it only lives in your terminal session.
- Back in your terminal, set the API key as an environment variable by running the command for your operating system:
macOS
export GEMINI_API_KEY=your-api-key-here
Replace your-api-key-here with the key you just copied from Google AI Studio.
Windows
set GEMINI_API_KEY=your-api-key-here
Replace your-api-key-here with the key you just copied from Google AI Studio.
Having trouble creating a key?
If you see a permission error, make sure you are signed in with a personal Google account. Work or school accounts managed by an organization may have restrictions.
Try opening the link in an incognito window or a different browser. Help me troubleshoot my API key setup.
Create agent.py and test the connection
Now you will create a project folder, write a short test script, and run it to confirm your API key and the SDK are working together.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir php-review-agent
cd php-review-agent
- Confirm the folder was created by running:
ls
You should see the php-review-agent folder listed in the output (or an empty listing if you are already inside it).
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows) and searching for VS Code.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the php-review-agent folder you just created, then click Open.
- Create a new file by clicking File then New File.
- Save it immediately as agent.py by pressing Cmd+S (macOS) or Ctrl+S (Windows) and typing agent.py as the file name.
- Copy the following code into agent.py to test your connection to Gemini:
import os
from google import genai
# Create a client using your API key from the environment variable
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# Send a simple prompt to Gemini to test the connection
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Say hello!",
)
print(response.text)
What does this code do?
The genai.Client creates a connection to the Gemini API using the key stored in your GEMINI_API_KEY environment variable.
The generate_content() method sends the prompt "Say hello!" to the gemini-2.5-flash model and returns the AI's response. The response.text property extracts just the text content from the response object.
- Save agent.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Make sure your API key environment variable is set in this terminal session. Run the appropriate command for your OS (same command from earlier):
macOS
export GEMINI_API_KEY=your-api-key-here
Windows
set GEMINI_API_KEY=your-api-key-here
- Run the script by entering:
python agent.py
You should see a friendly greeting from Gemini printed in your terminal, something like "Hello! How can I help you today?" or similar.
Seeing an error instead of a greeting?
- If you see KeyError: 'GEMINI_API_KEY', you need to re-run the export or set command in the same terminal where you run the script. Environment variables do not persist across terminal sessions.
- If you see an authentication or permission error, double-check that you pasted the full API key with no extra spaces.
- If you see ModuleNotFoundError: No module named 'google', make sure you installed the package with the same Python that you are running. Try pip3 install google-genai and then python3 agent.py.
Help me debug why my agent.py test script is failing.
✔️ Awesome, I've got everything!
Great. Double check that you have saved your file.
ⓧ I'd like to double check the full code
Here is what your agent.py file should look like at this point:
import os
from google import genai
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Say hello!",
)
print(response.text)
Your environment is ready and Gemini is responding to your requests. Next up, you will define the tools that give your agent the ability to read files and explore directories.
Define Your Agent's Tools
Your environment is working and Gemini is responding to your requests. Now you need to give your agent the ability to interact with the file system.
An AI model on its own can only generate text. To make it useful as a code reviewer, you need to provide it with tools that let it read files and discover what files exist in a directory. In this step, you will write the tool functions and describe them to Gemini so it knows what is available.
In this step, get ready to:
- Write two tool functions that read files and list directories.
- Create function declarations that describe your tools to Gemini.
- Build a mapping dictionary that connects function names to implementations.
Write the agent's tool functions
Your agent needs two capabilities: reading a file's contents and listing the entries in a directory. Each capability is a plain Python function that returns a string result.
The existing test code in agent.py was just to verify your API key. You will now replace it with the real agent code, starting with imports and the first tool function.
- In VS Code, open agent.py and select all the existing code.
- Replace everything with the following imports, client setup, and read_file function:
import os
import sys
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def read_file(file_path: str) -> str:
"""Read the contents of a file and return it as a string."""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: File '{file_path}' not found."
except Exception as e:
return f"Error reading file: {str(e)}"
What does this code do?
- import sys and from google.genai import types are added for later steps when you build the agent loop.
- read_file opens a file at the given path and returns its full text content.
- The try/except blocks handle missing files gracefully by returning an error string instead of crashing the script.
- Save agent.py.
- You won't see output yet since the script no longer has code that runs on its own. The next chunk adds the second tool function.
Now add the second tool function directly below read_file. This one lists all entries in a directory so the agent can discover which PHP files exist.
- In agent.py, add the following function below the read_file function (leave two blank lines between them):
def list_directory(directory_path: str) -> str:
"""List all files and subdirectories in the given directory."""
try:
entries = os.listdir(directory_path)
return "\n".join(entries)
except FileNotFoundError:
return f"Error: Directory '{directory_path}' not found."
except Exception as e:
return f"Error listing directory: {str(e)}"
What does this code do?
- os.listdir() returns a list of filenames and subdirectory names at the given path.
- The function joins them with newlines into a single string so Gemini can read the result as plain text.
- Like read_file, it handles missing directories by returning an error message instead of raising an exception.
- Save agent.py.
- Test both functions by running this command in your terminal:
python -c "from agent import read_file, list_directory; print(read_file('agent.py')[:80]); print('---'); print(list_directory('.'))"
- You should see the first 80 characters of agent.py printed, followed by ---, followed by a list of files in your current directory.
Seeing a ModuleNotFoundError?
- Make sure your terminal is in the same folder as agent.py. Run ls and confirm you see agent.py in the output.
- Check that you saved the file after pasting the code. Unsaved changes mean Python reads the old version.
- If you see a SyntaxError, double-check your indentation. Python requires consistent spacing inside try/except blocks.
Help me fix my tool function import error.
Create the function declarations
Your Python functions work, but Gemini does not know they exist yet. You need to provide function declarations that describe each tool's name, purpose, and expected parameters in a format Gemini understands.
These declarations follow the OpenAPI schema format. The model reads them to decide which tool to call and what arguments to pass.
- In agent.py, add the following tool_declarations list below the list_directory function (leave two blank lines between them):
tool_declarations = [
{
"name": "read_file",
"description": "Read the contents of a file at the given path. Use this to read PHP files for code review.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The path to the file to read.",
}
},
"required": ["file_path"],
},
},
{
"name": "list_directory",
"description": "List all files and subdirectories in the given directory path. Use this to discover PHP files to review.",
"parameters": {
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The path to the directory to list.",
}
},
"required": ["directory_path"],
},
},
]
What does this code do?
Each dictionary in the list is a declaration for one tool. It tells Gemini:
- name matches the Python function name exactly so you can look it up later.
- description tells the model when this tool is useful. Gemini reads this to decide whether to call it.
- parameters describes the arguments the tool accepts, including their types and what they mean.
The model never executes these functions itself. It only requests a call with arguments. Your code is responsible for actually running the function.
- Save agent.py.
- You won't see a runtime change yet. The declarations are data structures the agent loop will use in the next step.
Seeing a SyntaxError after pasting?
- Make sure each dictionary ends with a closing brace } followed by a comma. The last item in the outer list also needs a trailing comma before the closing ].
- Check that you have matching quotes. Every "description" key uses double quotes, not single quotes.
Help me fix a syntax error in my tool_declarations list.
Create the tools mapping
When Gemini requests a tool call, it sends back the function name as a string (like "read_file"). You need a way to look up the actual Python function from that string. A dictionary makes this lookup instant.
- In agent.py, add the following available_tools dictionary below tool_declarations (leave two blank lines between them):
available_tools = {
"read_file": read_file,
"list_directory": list_directory,
}
What does this code do?
This dictionary maps the string name Gemini will return ("read_file") to the actual Python function object (read_file).
In the next step, your agent loop will use this to dynamically dispatch tool calls. When Gemini says "call read_file with these arguments", your code does available_tools[function_name](**args) to execute it.
- Save agent.py.
- Verify the full file has no errors by running:
python -c "import agent; print('All good!')"
- You should see All good! printed with no errors.
Seeing an error on import?
- A NameError mentioning read_file or list_directory means available_tools is defined before the functions. Make sure your functions appear above the mapping dictionary.
- An IndentationError usually means mixed tabs and spaces. Use only spaces (4 per indent level) throughout the file.
Help me debug why importing agent.py fails.
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py and the import test prints All good! with no errors.
ⓧ I'd like to double check the full code
Here is the complete agent.py at this point:
import os
import sys
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def read_file(file_path: str) -> str:
"""Read the contents of a file and return it as a string."""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: File '{file_path}' not found."
except Exception as e:
return f"Error reading file: {str(e)}"
def list_directory(directory_path: str) -> str:
"""List all files and subdirectories in the given directory."""
try:
entries = os.listdir(directory_path)
return "\n".join(entries)
except FileNotFoundError:
return f"Error: Directory '{directory_path}' not found."
except Exception as e:
return f"Error listing directory: {str(e)}"
tool_declarations = [
{
"name": "read_file",
"description": "Read the contents of a file at the given path. Use this to read PHP files for code review.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The path to the file to read.",
}
},
"required": ["file_path"],
},
},
{
"name": "list_directory",
"description": "List all files and subdirectories in the given directory path. Use this to discover PHP files to review.",
"parameters": {
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The path to the directory to list.",
}
},
"required": ["directory_path"],
},
},
]
available_tools = {
"read_file": read_file,
"list_directory": list_directory,
}
Your tools are defined, declared, and mapped. Next up, you will build the agent loop that sends a prompt to Gemini, detects when it wants to call one of these tools, executes the call, and feeds the result back.
Build the Agent Loop
You have the tools defined. Now you need the brain that decides when to use them. The agent loop is the core pattern behind every modern AI agent. It sends a prompt to an LLM, checks if the model wants to call a tool, executes that tool, feeds the result back, and repeats until the model has a final answer.
In this step, you'll build the run_agent function that orchestrates this entire cycle using Gemini's function calling API.
In this step, get ready to:
- Send an initial prompt to Gemini along with your tool declarations.
- Detect and execute tool calls inside a loop.
- Feed tool results back to Gemini until it produces a final text response.
Write the run_agent function setup
The agent loop starts by packaging your tool declarations and the user's prompt into the format Gemini expects. This setup happens once per agent run.
- In agent.py, scroll to the bottom of the file (below the available_tools dictionary).
- Add the following function:
def run_agent(user_prompt: str) -> str:
"""Run the agent loop until Gemini provides a final text response."""
# Wrap declarations in a Tool object Gemini can use
tools = types.Tool(function_declarations=tool_declarations)
# Configure the request with tools and a basic system instruction
config = types.GenerateContentConfig(
tools=[tools],
system_instruction="You are a helpful assistant.",
)
# Start the conversation history with the user's prompt
contents = [
types.Content(role="user", parts=[types.Part(text=user_prompt)])
]
print("Agent is analyzing your code...\n")
What does this code do?
This sets up the three things every agent run needs:
- types.Tool(function_declarations=tool_declarations) wraps your tool declarations in the format Gemini's API expects.
- types.GenerateContentConfig bundles the tools and a system instruction together. You'll replace the placeholder instruction with a proper code review prompt in the next step.
- contents is the conversation history list. It starts with the user's prompt and grows as tools are called and results come back.
- Save agent.py.
Seeing a syntax error on save?
- Make sure the function is at the top level (no extra indentation) and starts after a blank line below the available_tools dictionary.
- Check that the triple-quoted docstring uses matching triple double-quotes on both sides.
Help me fix a syntax error in my run_agent function setup.
Add the loop that detects and executes tool calls
The heart of the agent loop is a while True loop that keeps calling Gemini until it stops requesting tools. Each iteration sends the conversation history, checks if the model wants to call a function, and executes it.
- In agent.py, find the print("Agent is analyzing your code...\n") line inside your run_agent function.
- Add the following code directly below it:
while True:
# Send the full conversation history to Gemini
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=contents,
config=config,
)
# Check if Gemini wants to call any tools
if response.function_calls:
# First pass: print what the agent is doing
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
print(f" [Tool Call] {function_name}({function_args})")
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
What's happening here?
- client.models.generate_content() sends the entire conversation (including any previous tool results) to Gemini. The model sees the full context each time.
- response.function_calls is a list because Gemini can request multiple tool calls in a single response. This is called parallel function calling.
- The first for loop iterates over each requested call, prints it so you can see what the agent is doing, and executes the matching function from available_tools.
Why does Gemini never execute the tools itself?
The model only decides WHEN to call a tool and WHAT arguments to pass. Your code is always responsible for actually running the function. This is a security boundary. It means the model can never access your filesystem or run commands without your code explicitly allowing it.
- Save agent.py.
- You won't see a change yet. The next chunk completes the loop by feeding results back to Gemini.
IndentationError on the while loop?
- The while True: line must be indented one level inside the run_agent function (4 spaces in from the def line).
- Make sure there is no extra blank line between the print statement and the while loop that might confuse your editor.
Help me fix my indentation in the agent loop.
Feed tool results back to Gemini
Executing the tool is only half the job. Gemini needs to see the result so it can decide what to do next. This is what closes the loop and allows the agent to chain multiple tool calls together autonomously.
- In agent.py, find the line result = f"Error: Unknown function '{function_name}'" at the end of the first for loop inside the if response.function_calls: block.
- Add the following code directly below it, at the same indent level as the first for loop:
# Add Gemini's response (with tool call requests) to history
contents.append(response.candidates[0].content)
# Second pass: execute tools and send results back
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
# Package the result in the format Gemini expects
contents.append(
types.Content(
role="user",
parts=[
types.Part.from_function_response(
name=function_name,
response={"result": result},
id=function_call.id,
)
],
)
)
else:
# No tool calls means Gemini has a final answer
return response.text
What does this code do?
- contents.append(response.candidates[0].content) adds Gemini's full response (including its tool call requests) to the conversation history. Gemini needs to see its own prior messages to maintain context.
- types.Part.from_function_response() packages each tool result in the format Gemini expects. The id field maps each result back to the specific tool call that requested it.
- The else branch triggers when Gemini's response has no function calls. That means it has finished using tools and is ready to deliver a final text answer. The loop exits here.
Why is the id field important?
Gemini can request multiple tool calls in a single response. Each call gets a unique id. When you send results back, including the matching id tells Gemini which result belongs to which request. Without it, the model cannot correctly associate responses in parallel function calling scenarios.
- Save agent.py.
- Test the agent by temporarily adding this line at the very bottom of agent.py:
print(run_agent("Read the file agent.py and tell me what it does"))
- Run the script by running:
python agent.py
You should see output that includes [Tool Call] read_file(...) followed by a text summary of what agent.py does. The agent autonomously decided to read the file and then summarized it.
- Delete the test line (print(run_agent(...))) from the bottom of agent.py and save the file. You'll add a proper CLI entry point in the next step.
Seeing an error about GEMINI_API_KEY?
- Make sure you set the environment variable in the same terminal session where you run the script. If you closed and reopened your terminal, you need to set it again with set GEMINI_API_KEY=your-key-here (Windows) or export GEMINI_API_KEY=your-key-here (macOS/Linux).
- Double check that your API key has no extra spaces or quotes around it.
Help me fix my Gemini API key error in agent.py.
Script hangs or returns an empty response?
- Check that the else: return response.text is aligned with the if response.function_calls: line. If indentation is wrong, the loop may never exit.
- Make sure you have both the contents.append(response.candidates[0].content) line and the second for loop. Without these, Gemini never receives the tool results and keeps requesting the same calls.
Help me debug my agent loop that hangs or returns nothing.
✔️ Awesome, I've got everything!
Great. Make sure you deleted the test line and saved agent.py.
ⓧ I'd like to double check the full code
Here is the full agent.py at the end of this step:
import os
import sys
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def read_file(file_path: str) -> str:
"""Read the contents of a file and return it as a string."""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: File '{file_path}' not found."
except Exception as e:
return f"Error reading file: {str(e)}"
def list_directory(directory_path: str) -> str:
"""List all files and subdirectories in the given directory."""
try:
entries = os.listdir(directory_path)
return "\n".join(entries)
except FileNotFoundError:
return f"Error: Directory '{directory_path}' not found."
except Exception as e:
return f"Error listing directory: {str(e)}"
tool_declarations = [
{
"name": "read_file",
"description": "Read the contents of a file at the given path. Use this to read PHP files for code review.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The path to the file to read.",
}
},
"required": ["file_path"],
},
},
{
"name": "list_directory",
"description": "List all files and subdirectories in the given directory path. Use this to discover PHP files to review.",
"parameters": {
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The path to the directory to list.",
}
},
"required": ["directory_path"],
},
},
]
available_tools = {
"read_file": read_file,
"list_directory": list_directory,
}
def run_agent(user_prompt: str) -> str:
"""Run the agent loop until Gemini provides a final text response."""
tools = types.Tool(function_declarations=tool_declarations)
config = types.GenerateContentConfig(
tools=[tools],
system_instruction="You are a helpful assistant.",
)
contents = [
types.Content(role="user", parts=[types.Part(text=user_prompt)])
]
print("Agent is analyzing your code...\n")
while True:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=contents,
config=config,
)
if response.function_calls:
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
print(f" [Tool Call] {function_name}({function_args})")
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
contents.append(response.candidates[0].content)
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
contents.append(
types.Content(
role="user",
parts=[
types.Part.from_function_response(
name=function_name,
response={"result": result},
id=function_call.id,
)
],
)
)
else:
return response.text
Your agent loop is working. It can autonomously read files and directories when asked. Next up, you'll wire this into a proper CLI tool with a system prompt that turns Gemini into a PHP code review expert.
Wire Up the CLI
Your agent loop is built and can orchestrate tool calls with Gemini. Right now though, it has a generic system instruction and no way to run from the command line.
In this step, you will give your agent a focused personality as a PHP code review expert and wire it up as a proper CLI tool. By the end, you will be able to point it at any PHP file and get a structured security review.
In this step, get ready to:
- Add a system prompt that turns Gemini into a PHP code reviewer.
- Write a main function that accepts file paths from the terminal.
- Test the full agent on a sample PHP file with intentional vulnerabilities.
Add a system prompt for PHP code review
A system prompt shapes how an LLM behaves for an entire conversation. Instead of a generic assistant, you want Gemini to act as a specialist that knows exactly what to look for in PHP code.
- In agent.py, find the line that says client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]).
- Add the following SYSTEM_PROMPT constant below that line, with a blank line separating them:
SYSTEM_PROMPT = """You are an expert PHP code reviewer. When given a file or directory path, use your tools to read the PHP files and provide a thorough code review.
For each file you review, check for:
1. Security issues (SQL injection, XSS, CSRF vulnerabilities)
2. Best practice violations (PSR standards, naming conventions)
3. Potential bugs (type errors, null references, logic errors)
4. Performance concerns (N+1 queries, unnecessary loops)
If given a directory, first list the files to find PHP files, then read and review them.
Provide your review in a clear, structured format with severity levels (HIGH, MEDIUM, LOW) for each issue found."""
What does this system prompt do?
This prompt gives Gemini four specific categories to check: security issues, best practice violations, bugs, and performance concerns. It also tells the model to tag each finding with a severity level (HIGH, MEDIUM, LOW) so the output is structured and actionable.
The instruction about directories teaches the agent to use list_directory first to discover files before reading them. Without this, the agent would not know how to handle a folder path.
Now you need to tell run_agent to use this new prompt instead of the placeholder.
- In the run_agent function, find this line inside the GenerateContentConfig:
system_instruction="You are a helpful assistant.",
- Replace "You are a helpful assistant." with SYSTEM_PROMPT, so the line now looks like this:
system_instruction=SYSTEM_PROMPT,
- Save agent.py.
Write the main function
Your agent can review code, but there is no way to launch it from the terminal yet. The main() function will read a file or directory path from the command line, validate it, and pass it into the agent loop.
- At the very bottom of agent.py (after the run_agent function), add the following code:
def main():
# Check if the user provided a file/directory path
if len(sys.argv) < 2:
print("Usage: python agent.py <file_or_directory_path>")
print("Example: python agent.py ./src/index.php")
sys.exit(1)
target_path = sys.argv[1]
# Validate the path exists before sending to the agent
if not os.path.exists(target_path):
print(f"Error: Path '{target_path}' does not exist.")
sys.exit(1)
# Build the prompt based on whether it's a file or directory
if os.path.isfile(target_path):
prompt = f"Please review the PHP file at: {target_path}"
else:
prompt = f"Please review the PHP files in the directory: {target_path}"
# Run the agent and display formatted results
result = run_agent(prompt)
print("\n" + "=" * 60)
print("CODE REVIEW RESULTS")
print("=" * 60 + "\n")
print(result)
if __name__ == "__main__":
main()
What does this code do?
- sys.argv[1] grabs the first argument the user typed after python agent.py. If nothing was typed, the script prints usage instructions and exits.
- os.path.exists() and os.path.isfile() validate the path before the agent starts. This prevents confusing errors from the agent trying to read non-existent files.
- The prompt changes based on whether the target is a single file or a directory. This gives the agent clear context about what to do.
- if __name__ == "__main__" ensures main() only runs when you execute the script directly, not when it is imported as a module.
- Save agent.py.
Seeing a SyntaxError when you save?
Make sure the if __name__ == "__main__": line is at the top level of the file (no indentation). It should not be inside any function.
Also check that the def main(): line is also at the top level, placed after the closing of the run_agent function.
Help me debug a SyntaxError in my agent.py file
Create a sample PHP file and test the agent
You need a PHP file with real vulnerabilities to test whether the agent catches them. This sample file contains common security mistakes that the agent should flag.
- Create a new file called sample.php in the same folder as agent.py.
- Paste the following code into sample.php:
<?php
// Sample PHP file with intentional issues for testing the code reviewer
$username = $_GET['user'];
$password = $_GET['pass'];
// SQL Injection vulnerability
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
// XSS vulnerability
echo "<h1>Welcome, " . $_GET['name'] . "</h1>";
// No password hashing
function createUser($name, $password) {
$query = "INSERT INTO users (name, password) VALUES ('$name', '$password')";
return mysqli_query($GLOBALS['conn'], $query);
}
// Unused variable
$temp = "this is never used";
// No error handling
$file = file_get_contents("config.php");
$data = json_decode($file);
echo $data->secret_key;
?>
What vulnerabilities are in this file?
- SQL injection: User input is placed directly into SQL queries without escaping or parameterization.
- XSS: Raw $_GET values are echoed into HTML without sanitization.
- No password hashing: Passwords are stored as plain text.
- No error handling: File operations and JSON parsing have no fallback if something goes wrong.
- Save sample.php.
- Run the agent on your sample file by running this command in the terminal:
python agent.py sample.php
- You should see [Tool Call] lines showing the agent reading your file, followed by a CODE REVIEW RESULTS header and a structured review with severity-tagged findings like HIGH, MEDIUM, and LOW.
Agent not producing output?
- If you see Error: Path 'sample.php' does not exist. make sure sample.php is in the same folder where you are running the command. Run ls to check.
- If the script hangs for more than 30 seconds, Gemini may be taking time to process. Give it up to a minute for the first request.
- If you see an API error mentioning your key, confirm your GEMINI_API_KEY environment variable is still set in this terminal session.
Help me debug why my PHP code review agent is not producing output
✔️ Awesome, I've got everything!
Great. Double check you have saved both agent.py and sample.php.
ⓧ I'd like to double check the full code
Here is the complete agent.py file at this point:
import os
import sys
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
SYSTEM_PROMPT = """You are an expert PHP code reviewer. When given a file or directory path, use your tools to read the PHP files and provide a thorough code review.
For each file you review, check for:
1. Security issues (SQL injection, XSS, CSRF vulnerabilities)
2. Best practice violations (PSR standards, naming conventions)
3. Potential bugs (type errors, null references, logic errors)
4. Performance concerns (N+1 queries, unnecessary loops)
If given a directory, first list the files to find PHP files, then read and review them.
Provide your review in a clear, structured format with severity levels (HIGH, MEDIUM, LOW) for each issue found."""
def read_file(file_path: str) -> str:
"""Read the contents of a file and return it as a string."""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: File '{file_path}' not found."
except Exception as e:
return f"Error reading file: {str(e)}"
def list_directory(directory_path: str) -> str:
"""List all files and subdirectories in the given directory."""
try:
entries = os.listdir(directory_path)
return "\n".join(entries)
except FileNotFoundError:
return f"Error: Directory '{directory_path}' not found."
except Exception as e:
return f"Error listing directory: {str(e)}"
tool_declarations = [
{
"name": "read_file",
"description": "Read the contents of a file at the given path. Use this to read PHP files for code review.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The path to the file to read.",
}
},
"required": ["file_path"],
},
},
{
"name": "list_directory",
"description": "List all files and subdirectories in the given directory path. Use this to discover PHP files to review.",
"parameters": {
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The path to the directory to list.",
}
},
"required": ["directory_path"],
},
},
]
available_tools = {
"read_file": read_file,
"list_directory": list_directory,
}
def run_agent(user_prompt: str) -> str:
"""Run the agent loop until Gemini provides a final text response."""
tools = types.Tool(function_declarations=tool_declarations)
config = types.GenerateContentConfig(
tools=[tools],
system_instruction=SYSTEM_PROMPT,
)
contents = [
types.Content(role="user", parts=[types.Part(text=user_prompt)])
]
print("Agent is analyzing your code...\n")
while True:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=contents,
config=config,
)
if response.function_calls:
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
print(f" [Tool Call] {function_name}({function_args})")
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
contents.append(response.candidates[0].content)
for function_call in response.function_calls:
function_name = function_call.name
function_args = function_call.args
if function_name in available_tools:
result = available_tools[function_name](**function_args)
else:
result = f"Error: Unknown function '{function_name}'"
contents.append(
types.Content(
role="user",
parts=[
types.Part.from_function_response(
name=function_name,
response={"result": result},
id=function_call.id,
)
],
)
)
else:
return response.text
def main():
if len(sys.argv) < 2:
print("Usage: python agent.py <file_or_directory_path>")
print("Example: python agent.py ./src/index.php")
sys.exit(1)
target_path = sys.argv[1]
if not os.path.exists(target_path):
print(f"Error: Path '{target_path}' does not exist.")
sys.exit(1)
if os.path.isfile(target_path):
prompt = f"Please review the PHP file at: {target_path}"
else:
prompt = f"Please review the PHP files in the directory: {target_path}"
result = run_agent(prompt)
print("\n" + "=" * 60)
print("CODE REVIEW RESULTS")
print("=" * 60 + "\n")
print(result)
if __name__ == "__main__":
main()
And here is the complete sample.php file:
<?php
// Sample PHP file with intentional issues for testing the code reviewer
$username = $_GET['user'];
$password = $_GET['pass'];
// SQL Injection vulnerability
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
// XSS vulnerability
echo "<h1>Welcome, " . $_GET['name'] . "</h1>";
// No password hashing
function createUser($name, $password) {
$query = "INSERT INTO users (name, password) VALUES ('$name', '$password')";
return mysqli_query($GLOBALS['conn'], $query);
}
// Unused variable
$temp = "this is never used";
// No error handling
$file = file_get_contents("config.php");
$data = json_decode($file);
echo $data->secret_key;
?>
Secret mission
Add a PHP Syntax Checker Tool
Your agent can read files and list directories, but it cannot interact with external programs. In this secret mission, you will add a third tool that executes PHP's built-in syntax linter and feeds the results back to the agent. This teaches you how AI agents can orchestrate system-level processes beyond simple file I/O.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. The Gemini API calls you made are covered by the free tier for Flash models.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- agent.py and sample.php local files.
- google-genai Python package.
- Gemini API key in Google AI Studio.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your PHP code review agent or want to extend it with more tools.
- Your agent.py and sample.php files stay where they are.
- The google-genai package remains installed in your Python environment.
- Your Gemini API key stays active in Google AI Studio.
- The GEMINI_API_KEY environment variable is session-scoped. It disappears when you close your terminal. You will need to set it again next time you run the agent.
Pause - I'll come back to this later
Since this project is local and the API key costs nothing to keep, pausing is the same as keeping. Your files stay on disk and your API key stays active.
- No running processes to stop. The agent only runs when you execute it.
- When you come back, set the GEMINI_API_KEY environment variable again in your terminal before running the script.
Delete - I don't want to use this again
Remove all project files, uninstall the package, and delete your API key.
- Delete the project files by running this in the directory where you created them:
macOS
rm agent.py sample.php
Windows
del agent.py sample.php
- Uninstall the google-genai package by running:
pip uninstall google-genai
- Confirm the uninstall when prompted by entering y.
- Delete your Gemini API key by visiting https://aistudio.google.com/apikey. Find the key you created for this project and delete it.
Nice Work!
Nice Work!
You just built a fully functional AI agent that autonomously reviews PHP code from your terminal. This is the same agent loop pattern that powers tools like ChatGPT plugins, GitHub Copilot, and every production AI agent.
You've learned how to:
- Build a manual agent loop that orchestrates the full cycle of prompting an LLM, detecting tool calls, executing tools locally, and feeding results back until the model produces a final answer.
- Write function declarations using the OpenAPI schema format that teach Gemini what tools are available, what arguments they accept, and when to use them.
- Use Google Gemini's function calling API with the official google-genai Python SDK to build a CLI tool that reads files, lists directories, and delivers structured code reviews.
- Secret Mission: Extended the agent with a system-level tool that executes PHP's built-in syntax checker using subprocess, feeding parse error results back into the agent's review.
Ready to quiz yourself?