Build an AI Agent with Tool Use
Build a Gemini-powered AI agent that calls real tools to fetch weather, calculate, and tell time.
Introduction
30 Second Summary
Every time you ask an AI assistant to check the weather or do a calculation, it is not just generating text. It is deciding which tool to use, calling it, and weaving the result into a natural response.
In this project, you will build your own AI agent powered by Gemini that autonomously decides when to use external tools, calls them with the right parameters, and returns grounded answers.
What You'll Build
A terminal-based AI research assistant that can fetch real-time weather data, evaluate math expressions, and look up the current time in any timezone.
By the end of this project, you'll have:
- A working AI agent that decides which tools to call based on natural language input.
- The full tool-use loop implemented: prompt, function call detection, execution, and result integration.
- A multi-tool interactive assistant running in your terminal that fetches real weather data and performs calculations.
- Secret Mission: Multi-step tool chaining where the agent calls multiple tools sequentially to answer complex questions.
Are there any prerequisites?
You should be comfortable with intermediate Python (functions, dictionaries, loops) and have a basic understanding of APIs and JSON. No prior AI or machine learning experience is needed.
Before We Start
Before We Start...
Set Up Your Project
Before you can build an AI agent that calls tools, you need a working Python environment with the right packages installed and access to the Gemini API.
In this step, you'll create a project folder, install the packages your agent needs, and grab a free API key from Google AI Studio.
In this step, get ready to:
- Create a project folder with a Python virtual environment.
- Install the google-genai and requests packages.
- Get a free Gemini API key and create your main script file.
Create the project folder and virtual environment
A virtual environment gives your project its own isolated space for Python packages. This prevents conflicts with other projects on your machine.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ai-agent
cd ai-agent
- Confirm the folder was created by running:
pwd
You should see a path ending in /ai-agent.
Seeing 'no such file or directory'?
Make sure you ran cd ~/Desktop first. If your Desktop path is different, navigate to it manually before running mkdir.
Help me find my Desktop path in the terminal.
- Create a Python virtual environment by running:
python3 -m venv venv
- Confirm the venv folder was created by running:
ls
You should see a venv folder in the output.
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, if you see an error about execution policies, run this command first:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Then try the activate command again.
On macOS, make sure you used python3 (not python) when creating the venv.
Help me activate my Python virtual environment.
Install the required packages
Your agent needs two packages. The google-genai package connects your code to the Gemini API. The requests package lets your tools fetch data from external APIs.
- Install both packages by running:
pip install google-genai requests
You should see download progress bars followed by Successfully installed and a list of packages at the end.
Why google-genai and not google-generativeai?
Google deprecated the older google-generativeai package in November 2025. The new google-genai package is the official replacement with better support for function calling and tool use.
Seeing 'pip: command not found'?
Make sure your virtual environment is activated. You should see (venv) in your prompt. If not, run the activate command from the previous substep again.
Help me fix my pip installation.
Get your API key and create agent.py
Google AI Studio provides free API keys for the Gemini API. No credit card is required.
- Open your browser and go to aistudio.google.com.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Copy the generated API key. You will need it in the next step.
What is an API key?
An API key is a unique string that identifies your project to the Gemini service. It lets Google know who is making requests so it can apply rate limits and track usage against the free tier.
Now set the API key as an environment variable so your Python code can access it without hardcoding the secret.
- Back in your terminal, set the environment variable by running:
macOS
export GEMINI_API_KEY="your-api-key-here"
Replace your-api-key-here with the API key you copied from Google AI Studio.
Windows
$env:GEMINI_API_KEY="your-api-key-here"
Replace your-api-key-here with the API key you copied from Google AI Studio.
This variable lasts one session
The environment variable is only set for your current terminal session. If you close and reopen the terminal, you will need to run the export command again.
Now create the main script file where your agent will live.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type the name of your preferred editor (e.g. Google Antigravity, VS Code, or Cursor) and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the ai-agent folder, then click Open.
- Create a new file called agent.py in the root of the ai-agent folder.
- Add the following imports to agent.py:
import os
import json
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from google import genai
from google.genai import types
What do these imports do?
- os reads your API key from the environment variable.
- json handles JSON data when working with API responses.
- requests makes HTTP calls to external APIs like Open-Meteo for weather data.
- datetime and ZoneInfo power the timezone tool you will build later.
- genai and types are the Google Gemini SDK modules for calling the model and defining tool configurations.
- Save agent.py.
- Back in your terminal, verify the imports work by running:
python agent.py
You should see no output and no errors. The script runs the imports and exits cleanly.
Seeing 'ModuleNotFoundError'?
This means your virtual environment is not active or the packages did not install correctly. Check that (venv) appears in your terminal prompt.
If it does not, activate the virtual environment again, then re-run pip install google-genai requests.
Help me fix a ModuleNotFoundError when running agent.py.
✔️ Awesome, I've got everything!
Great. Make sure you have saved agent.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete agent.py so far:
import os
import json
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from google import genai
from google.genai import types
Your environment is ready. Next up, you'll define the tools your agent can use and write the Python functions that power them.
Define Your Tools
Your environment is ready and your API key is set. Now it's time to give your agent something it can actually do.
Tools are the Python functions your AI agent can call when it needs real-world information. In this step, you'll build three tools and describe them in a JSON schema format that Gemini understands.
In this step, get ready to:
- Implement three tool functions: weather, calculator, and time.
- Create JSON-formatted declarations that describe each tool to Gemini.
- Test the tools to confirm they return real data.
Implement the get_weather tool
Your first tool fetches live weather data for any city. It uses the Open-Meteo API, which requires no API key and no signup.
- In agent.py, add the following code below your import statements:
# --- Tool Implementations ---
def get_weather(city: str) -> dict:
"""Fetches current weather for a city using Open-Meteo API."""
# Step 1: Geocode the city name to coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_response = requests.get(geo_url, params={"name": city, "count": 1})
geo_data = geo_response.json()
if not geo_data.get("results"):
return {"error": f"City '{city}' not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
city_name = location["name"]
country = location.get("country", "")
# Step 2: Get weather data
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_response = requests.get(weather_url, params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"timezone": "auto"
})
weather_data = weather_response.json()
current = weather_data["current"]
return {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"humidity_percent": current["relative_humidity_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"]
}
What does this code do?
This function has two stages. First, it converts a city name to latitude/longitude coordinates using Open-Meteo's geocoding endpoint. Then it fetches the current weather for those coordinates.
- geo_url hits the geocoding API to turn "London" into coordinates like 51.5, -0.12.
- weather_url fetches the actual weather data (temperature, humidity, wind) for those coordinates.
- The function returns a clean dictionary with just the fields your agent needs to answer weather questions.
- Save agent.py.
You won't see a change yet. You'll test this function shortly after adding the remaining tools.
Implement the calculate tool
LLMs are notoriously bad at arithmetic. Your calculator tool gives the agent a way to get precise answers for any math expression.
- In agent.py, add the following function below get_weather:
def calculate(expression: str) -> dict:
"""Evaluates a mathematical expression safely."""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression. Only numbers and +-*/.() are allowed."}
try:
result = eval(expression)
return {"expression": expression, "result": result}
except Exception as e:
return {"error": f"Calculation failed: {str(e)}"}
What does this code do?
- allowed_chars is a security safeguard. It only permits digits, operators, parentheses, decimals, and spaces. This prevents anyone from injecting malicious Python code through the expression string.
- eval(expression) evaluates the math expression after it passes the character check.
- If anything goes wrong (like dividing by zero), the except block catches the error and returns a helpful message instead of crashing.
Why use a character allowlist before eval?
Python's eval() can execute arbitrary code, which makes it dangerous with untrusted input. The allowlist approach ensures only mathematical characters can pass through. The agent controls the input, but defense in depth is always good practice.
- Save agent.py.
You won't see a visible change yet. One more tool to go before testing.
Implement the get_time tool
Your third tool returns the current date and time for any timezone. This uses Python's built-in zoneinfo module, so no extra packages are needed.
- In agent.py, add the following function below calculate:
def get_time(timezone: str) -> dict:
"""Returns the current date and time for a given timezone."""
try:
tz = ZoneInfo(timezone)
now = datetime.now(tz)
return {
"timezone": timezone,
"datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
"day_of_week": now.strftime("%A")
}
except Exception as e:
return {"error": f"Invalid timezone '{timezone}': {str(e)}"}
What does this code do?
- ZoneInfo(timezone) creates a timezone object from an IANA timezone string like America/New_York or Asia/Tokyo.
- datetime.now(tz) gets the current time in that timezone, then strftime formats it as a readable string.
- If someone passes an invalid timezone string, the except block returns an error message instead of crashing.
- Save agent.py.
All three tool functions are in place. Time to verify they actually work.
Test each tool function
Before connecting these tools to Gemini, you should confirm each one returns real data on its own.
- Test get_weather by running this command in your terminal:
python -c "from agent import get_weather; print(get_weather('Tokyo'))"
You should see a dictionary with temperature, humidity, and wind speed values for Tokyo.
- Test calculate by running:
python -c "from agent import calculate; print(calculate('(25 * 9/5) + 32'))"
You should see {'expression': '(25 * 9/5) + 32', 'result': 77.0}.
- Test get_time by running:
python -c "from agent import get_time; print(get_time('America/New_York'))"
You should see a dictionary with the current date, time, and day of the week for New York.
Seeing a ModuleNotFoundError?
- If the error mentions requests, make sure your virtual environment is activated and that you ran pip install requests in Step 1.
- If the error mentions agent, make sure you're running the command from the same folder that contains agent.py.
- If get_weather returns a "City not found" error, check your internet connection.
Why aren't my tool functions working?
Create the tool declarations for Gemini
Your tool functions work, but Gemini doesn't know they exist yet. You need to describe each tool in a JSON schema format so the model knows what tools are available, what they do, and what parameters they accept.
- In agent.py, add the following code below the get_time function:
# --- Tool Declarations (for Gemini) ---
tool_declarations = [
{
"name": "get_weather",
"description": "Gets the current weather for a specified city. Returns temperature, humidity, and wind speed.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London' or 'Tokyo'"
}
},
"required": ["city"]
}
},
{
"name": "calculate",
"description": "Evaluates a mathematical expression. Use for any arithmetic calculations. Supports +, -, *, /, parentheses, and decimals.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(25 * 9/5) + 32'"
}
},
"required": ["expression"]
}
},
{
"name": "get_time",
"description": "Gets the current date and time for a specified timezone.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone in IANA format, e.g. 'America/New_York', 'Europe/London', 'Asia/Tokyo'"
}
},
"required": ["timezone"]
}
}
]
# Map function names to implementations
available_tools = {
"get_weather": get_weather,
"calculate": calculate,
"get_time": get_time,
}
What does this code do?
This code serves two purposes:
- tool_declarations is a list of JSON schemas that describe your tools to Gemini. Each declaration includes the tool's name, a natural language description, and the parameters it accepts (with types and examples). Gemini reads these to decide which tool to call and what arguments to pass.
- available_tools is a dictionary that maps each tool name string to its actual Python function. When Gemini responds with a function call, you'll look up the function by name in this dictionary and execute it.
Why describe tools separately from implementing them?
The declarations are a contract between your code and the model. Gemini never sees your Python function bodies. It only sees the name, description, and parameter schema you provide. The quality of these descriptions directly affects how well the model chooses and calls the right tool.
- Save agent.py.
You won't see a change from this code on its own. In the next step, you'll pass these declarations to Gemini and build the loop that connects everything together.
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py and move on to the next step.
ⓧ I'd like to double check the full code
Here's the complete agent.py file at this point:
import os
import json
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from google import genai
from google.genai import types
# --- Tool Implementations ---
def get_weather(city: str) -> dict:
"""Fetches current weather for a city using Open-Meteo API."""
# Step 1: Geocode the city name to coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_response = requests.get(geo_url, params={"name": city, "count": 1})
geo_data = geo_response.json()
if not geo_data.get("results"):
return {"error": f"City '{city}' not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
city_name = location["name"]
country = location.get("country", "")
# Step 2: Get weather data
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_response = requests.get(weather_url, params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"timezone": "auto"
})
weather_data = weather_response.json()
current = weather_data["current"]
return {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"humidity_percent": current["relative_humidity_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"]
}
def calculate(expression: str) -> dict:
"""Evaluates a mathematical expression safely."""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression. Only numbers and +-*/.() are allowed."}
try:
result = eval(expression)
return {"expression": expression, "result": result}
except Exception as e:
return {"error": f"Calculation failed: {str(e)}"}
def get_time(timezone: str) -> dict:
"""Returns the current date and time for a given timezone."""
try:
tz = ZoneInfo(timezone)
now = datetime.now(tz)
return {
"timezone": timezone,
"datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
"day_of_week": now.strftime("%A")
}
except Exception as e:
return {"error": f"Invalid timezone '{timezone}': {str(e)}"}
# --- Tool Declarations (for Gemini) ---
tool_declarations = [
{
"name": "get_weather",
"description": "Gets the current weather for a specified city. Returns temperature, humidity, and wind speed.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London' or 'Tokyo'"
}
},
"required": ["city"]
}
},
{
"name": "calculate",
"description": "Evaluates a mathematical expression. Use for any arithmetic calculations. Supports +, -, *, /, parentheses, and decimals.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(25 * 9/5) + 32'"
}
},
"required": ["expression"]
}
},
{
"name": "get_time",
"description": "Gets the current date and time for a specified timezone.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone in IANA format, e.g. 'America/New_York', 'Europe/London', 'Asia/Tokyo'"
}
},
"required": ["timezone"]
}
}
]
# Map function names to implementations
available_tools = {
"get_weather": get_weather,
"calculate": calculate,
"get_time": get_time,
}
Your tools are built and declared. Next up, you'll connect them to Gemini and build the agent loop that makes the model actually call these functions.
Build the Agent Loop
Your tools are built and declared. The model knows what each tool does thanks to those JSON schemas. But right now, there's no connection between Gemini and your Python functions.
In this step, you'll build the core agent loop. This is the pattern that makes an AI agent an agent instead of just a chatbot: receive a user message, let the model decide if it needs a tool, execute that tool, and feed the result back so the model can craft a grounded answer.
In this step, get ready to:
- Initialize the Gemini client and configure it with your tool declarations.
- Implement the full agent loop that detects and executes function calls.
- Handle function call ID matching required by the Gemini API.
Set up the Gemini client
Before the agent can make decisions, it needs a connection to the Gemini model and a configuration that tells the model which tools are available.
- In agent.py, find the comment # Map function names to implementations and the available_tools dictionary below it.
- After the available_tools dictionary, add the agent setup code:
# --- Agent Setup ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
tools = types.Tool(function_declarations=tool_declarations)
config = types.GenerateContentConfig(
tools=[tools],
system_instruction="You are a helpful research assistant. Use your tools to answer questions accurately. When asked about weather, time, or calculations, always use the appropriate tool rather than guessing. Be concise in your responses."
)
What does this code do?
This creates three things your agent needs to communicate with Gemini:
- client connects to the Gemini API using the key you set as an environment variable in Step 1.
- tools wraps your JSON tool declarations into the format the SDK expects.
- config bundles the tools with a system instruction that tells the model to use tools instead of guessing.
- Save agent.py.
- Verify there are no import errors by running:
python agent.py
The script should exit silently with no errors. If you see an error, check the troubleshooting callout below.
Seeing a ModuleNotFoundError or API key error?
- If you see ModuleNotFoundError: No module named 'google', make sure your virtual environment is activated and you installed google-genai with pip.
- If you see an API key error, confirm your GEMINI_API_KEY environment variable is set in the same terminal session you're running the script from.
Help me debug my Gemini client setup
Implement the core agent loop
The agent loop is where the magic happens. It sends the user's message to Gemini, checks if the model wants to call a tool, executes the tool, and sends the result back for a final human-readable answer.
- Below the config variable you just added, add the run_agent function:
# --- Agent Loop ---
def run_agent(user_message: str, history: list) -> str:
"""Runs one turn of the agent loop with tool calling support."""
# Add the user message to history
history.append(
types.Content(role="user", parts=[types.Part(text=user_message)])
)
# Send to the model
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Check if the model wants to call a function
candidate = response.candidates[0]
response_parts = candidate.content.parts
# Look for function calls
function_call = None
for part in response_parts:
if part.function_call:
function_call = part.function_call
break
if function_call:
# Execute the tool
tool_name = function_call.name
tool_args = dict(function_call.args)
print(f" [Agent calling tool: {tool_name}({tool_args})]")
# Run the function
if tool_name in available_tools:
result = available_tools[tool_name](**tool_args)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Add the model's response (with function call) to history
history.append(candidate.content)
# Add the function result to history
function_response_part = types.Part.from_function_response(
name=tool_name,
response={"result": result},
id=function_call.id,
)
history.append(
types.Content(role="user", parts=[function_response_part])
)
# Send function result back to the model for a final answer
final_response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Add the final response to history
assistant_message = final_response.text
history.append(final_response.candidates[0].content)
return assistant_message
else:
# No function call, just a text response
history.append(candidate.content)
return candidate.content.parts[0].text
What does this code do?
This function is the full tool-use loop pattern. Here's the flow:
- The user message gets wrapped as a types.Content object and appended to the conversation history.
- The model receives the full history and decides whether it needs a tool. If it does, it returns a function_call object with the tool name and arguments.
- Your code executes the matching Python function using available_tools and sends the result back to the model.
- The model reads the tool result and generates a final human-readable answer.
- If no function call is needed, the model responds with plain text directly.
- Save agent.py.
- Test the agent loop by running this command:
python -c "from agent import run_agent; print(run_agent('What is 2 + 2?', []))"
You should see the agent call the calculate tool and return an answer like "2 + 2 = 4" or similar. You'll also see a line showing which tool the agent chose to call.
Not seeing a response or getting an error?
- If you see a rate limit error, wait 60 seconds and try again. The free tier allows 10 requests per minute.
- If you see IndentationError, double-check that the entire run_agent function is indented consistently with spaces (not tabs).
- If you see AttributeError: 'NoneType', your API key may not be set. Run echo $GEMINI_API_KEY to confirm it has a value.
Help me debug my agent loop
Understand the function call ID matching
There's one critical detail inside the agent loop that's easy to miss. Look at this line inside your run_agent function:
function_response_part = types.Part.from_function_response(
name=tool_name,
response={"result": result},
id=function_call.id,
)
Why is the ID parameter required?
Every time Gemini requests a function call, it generates a unique id for that request. When you send the function result back, you must include the matching id so the model knows which request the result belongs to.
This matters for scenarios where the model calls multiple tools at once. Without IDs, the model couldn't match results to the correct requests. Forgetting this parameter will cause an API error.
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py before moving on.
ⓧ I'd like to double check the full code
Here's the complete agent.py file up to this point:
import os
import json
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from google import genai
from google.genai import types
# --- Tool Implementations ---
def get_weather(city: str) -> dict:
"""Fetches current weather for a city using Open-Meteo API."""
# Step 1: Geocode the city name to coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_response = requests.get(geo_url, params={"name": city, "count": 1})
geo_data = geo_response.json()
if not geo_data.get("results"):
return {"error": f"City '{city}' not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
city_name = location["name"]
country = location.get("country", "")
# Step 2: Get weather data
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_response = requests.get(weather_url, params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"timezone": "auto"
})
weather_data = weather_response.json()
current = weather_data["current"]
return {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"humidity_percent": current["relative_humidity_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"]
}
def calculate(expression: str) -> dict:
"""Evaluates a mathematical expression safely."""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression. Only numbers and +-*/.() are allowed."}
try:
result = eval(expression)
return {"expression": expression, "result": result}
except Exception as e:
return {"error": f"Calculation failed: {str(e)}"}
def get_time(timezone: str) -> dict:
"""Returns the current date and time for a given timezone."""
try:
tz = ZoneInfo(timezone)
now = datetime.now(tz)
return {
"timezone": timezone,
"datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
"day_of_week": now.strftime("%A")
}
except Exception as e:
return {"error": f"Invalid timezone '{timezone}': {str(e)}"}
# --- Tool Declarations (for Gemini) ---
tool_declarations = [
{
"name": "get_weather",
"description": "Gets the current weather for a specified city. Returns temperature, humidity, and wind speed.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London' or 'Tokyo'"
}
},
"required": ["city"]
}
},
{
"name": "calculate",
"description": "Evaluates a mathematical expression. Use for any arithmetic calculations. Supports +, -, *, /, parentheses, and decimals.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(25 * 9/5) + 32'"
}
},
"required": ["expression"]
}
},
{
"name": "get_time",
"description": "Gets the current date and time for a specified timezone.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone in IANA format, e.g. 'America/New_York', 'Europe/London', 'Asia/Tokyo'"
}
},
"required": ["timezone"]
}
}
]
# Map function names to implementations
available_tools = {
"get_weather": get_weather,
"calculate": calculate,
"get_time": get_time,
}
# --- Agent Setup ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
tools = types.Tool(function_declarations=tool_declarations)
config = types.GenerateContentConfig(
tools=[tools],
system_instruction="You are a helpful research assistant. Use your tools to answer questions accurately. When asked about weather, time, or calculations, always use the appropriate tool rather than guessing. Be concise in your responses."
)
# --- Agent Loop ---
def run_agent(user_message: str, history: list) -> str:
"""Runs one turn of the agent loop with tool calling support."""
# Add the user message to history
history.append(
types.Content(role="user", parts=[types.Part(text=user_message)])
)
# Send to the model
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Check if the model wants to call a function
candidate = response.candidates[0]
response_parts = candidate.content.parts
# Look for function calls
function_call = None
for part in response_parts:
if part.function_call:
function_call = part.function_call
break
if function_call:
# Execute the tool
tool_name = function_call.name
tool_args = dict(function_call.args)
print(f" [Agent calling tool: {tool_name}({tool_args})]")
# Run the function
if tool_name in available_tools:
result = available_tools[tool_name](**tool_args)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Add the model's response (with function call) to history
history.append(candidate.content)
# Add the function result to history
function_response_part = types.Part.from_function_response(
name=tool_name,
response={"result": result},
id=function_call.id,
)
history.append(
types.Content(role="user", parts=[function_response_part])
)
# Send function result back to the model for a final answer
final_response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Add the final response to history
assistant_message = final_response.text
history.append(final_response.candidates[0].content)
return assistant_message
else:
# No function call, just a text response
history.append(candidate.content)
return candidate.content.parts[0].text
Your agent can now think, decide, and act. Next up, you'll wrap this in an interactive chat so you can have a real conversation with it.
Create the Interactive Chat
Your agent loop can now receive a message, detect tool calls, execute tools, and return a grounded answer. The problem is there's no way for a user to actually talk to it.
In this step, you'll wrap everything in an interactive terminal chat. You'll also add conversation history so the agent remembers what you've discussed across multiple turns.
In this step, get ready to:
- Build an interactive input/output loop in your terminal.
- Add conversation memory so the agent tracks context across turns.
- Test the full agent with weather, math, and timezone queries.
Add the interactive chat interface
A chat interface needs three things: a way to collect input, a way to display output, and memory of the conversation so far. The history list you pass into run_agent() handles memory. Now you need the input/output loop around it.
- In agent.py, scroll to the very bottom of the file (after the run_agent function) and add the following code:
# --- Interactive Chat ---
def main():
print("=" * 50)
print(" AI Research Assistant (Gemini + Tools)")
print(" Type 'quit' to exit")
print("=" * 50)
print()
# Conversation memory - persists across turns
history = []
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
try:
response = run_agent(user_input, history)
print(f"\nAssistant: {response}\n")
except Exception as e:
print(f"\nError: {e}\n")
if __name__ == "__main__":
main()
What does this code do?
The main() function creates a loop that continuously reads your input and feeds it to the agent.
- history = [] creates an empty list that accumulates every message (yours and the agent's). This is what gives the agent memory across turns.
- The while True loop keeps the chat running until you type quit, exit, or q.
- The try/except block catches API errors or network issues without crashing the chat. You'll see the error printed and can keep going.
- if __name__ == "__main__" ensures main() only runs when you execute the file directly (not when importing it as a module).
Why does history give the agent memory?
Every time you send a message, run_agent() appends both your message and the model's response to the history list. On the next turn, the model receives the full conversation so far. This means you can ask follow-up questions like "What about Tokyo?" and the agent knows you're still talking about weather.
- Save agent.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before running it.
ⓧ I'd like to double check the full code
Here's the complete agent.py file. Compare yours against this:
import os
import json
import requests
from datetime import datetime
from zoneinfo import ZoneInfo
from google import genai
from google.genai import types
# --- Tool Implementations ---
def get_weather(city: str) -> dict:
"""Fetches current weather for a city using Open-Meteo API."""
# Step 1: Geocode the city name to coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_response = requests.get(geo_url, params={"name": city, "count": 1})
geo_data = geo_response.json()
if not geo_data.get("results"):
return {"error": f"City '{city}' not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
city_name = location["name"]
country = location.get("country", "")
# Step 2: Get weather data
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_response = requests.get(weather_url, params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"timezone": "auto"
})
weather_data = weather_response.json()
current = weather_data["current"]
return {
"city": city_name,
"country": country,
"temperature_celsius": current["temperature_2m"],
"humidity_percent": current["relative_humidity_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"]
}
def calculate(expression: str) -> dict:
"""Evaluates a mathematical expression safely."""
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression. Only numbers and +-*/.() are allowed."}
try:
result = eval(expression)
return {"expression": expression, "result": result}
except Exception as e:
return {"error": f"Calculation failed: {str(e)}"}
def get_time(timezone: str) -> dict:
"""Returns the current date and time for a given timezone."""
try:
tz = ZoneInfo(timezone)
now = datetime.now(tz)
return {
"timezone": timezone,
"datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
"day_of_week": now.strftime("%A")
}
except Exception as e:
return {"error": f"Invalid timezone '{timezone}': {str(e)}"}
# --- Tool Declarations (for Gemini) ---
tool_declarations = [
{
"name": "get_weather",
"description": "Gets the current weather for a specified city. Returns temperature, humidity, and wind speed.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London' or 'Tokyo'"
}
},
"required": ["city"]
}
},
{
"name": "calculate",
"description": "Evaluates a mathematical expression. Use for any arithmetic calculations. Supports +, -, *, /, parentheses, and decimals.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate, e.g. '(25 * 9/5) + 32'"
}
},
"required": ["expression"]
}
},
{
"name": "get_time",
"description": "Gets the current date and time for a specified timezone.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone in IANA format, e.g. 'America/New_York', 'Europe/London', 'Asia/Tokyo'"
}
},
"required": ["timezone"]
}
}
]
# Map function names to implementations
available_tools = {
"get_weather": get_weather,
"calculate": calculate,
"get_time": get_time,
}
# --- Agent Setup ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
tools = types.Tool(function_declarations=tool_declarations)
config = types.GenerateContentConfig(
tools=[tools],
system_instruction="You are a helpful research assistant. Use your tools to answer questions accurately. When asked about weather, time, or calculations, always use the appropriate tool rather than guessing. Be concise in your responses."
)
# --- Agent Loop ---
def run_agent(user_message: str, history: list) -> str:
"""Runs one turn of the agent loop with tool calling support."""
# Add the user message to history
history.append(
types.Content(role="user", parts=[types.Part(text=user_message)])
)
# Send to the model
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Check if the model wants to call a function
candidate = response.candidates[0]
response_parts = candidate.content.parts
# Look for function calls
function_call = None
for part in response_parts:
if part.function_call:
function_call = part.function_call
break
if function_call:
# Execute the tool
tool_name = function_call.name
tool_args = dict(function_call.args)
print(f" [Agent calling tool: {tool_name}({tool_args})]")
# Run the function
if tool_name in available_tools:
result = available_tools[tool_name](**tool_args)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Add the model's response (with function call) to history
history.append(candidate.content)
# Add the function result to history
function_response_part = types.Part.from_function_response(
name=tool_name,
response={"result": result},
id=function_call.id,
)
history.append(
types.Content(role="user", parts=[function_response_part])
)
# Send function result back to the model for a final answer
final_response = client.models.generate_content(
model="gemini-2.5-flash",
contents=history,
config=config,
)
# Add the final response to history
assistant_message = final_response.text
history.append(final_response.candidates[0].content)
return assistant_message
else:
# No function call, just a text response
history.append(candidate.content)
return candidate.content.parts[0].text
# --- Interactive Chat ---
def main():
print("=" * 50)
print(" AI Research Assistant (Gemini + Tools)")
print(" Type 'quit' to exit")
print("=" * 50)
print()
history = []
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
try:
response = run_agent(user_input, history)
print(f"\nAssistant: {response}\n")
except Exception as e:
print(f"\nError: {e}\n")
if __name__ == "__main__":
main()
Run and test your agent
Time to see your agent in action. You'll test it with different types of queries to confirm each tool works correctly.
- Launch your agent by running this command in your terminal:
python agent.py
You should see the welcome banner appear:
==================================================
AI Research Assistant (Gemini + Tools)
Type 'quit' to exit
==================================================
You:
- Test the weather tool by typing:
What's the weather in London right now?
You should see the agent print [Agent calling tool: get_weather({...})] followed by a natural language response with the current temperature, humidity, and wind speed for London.
- Test the calculator tool by typing:
What is 1847 * 23 + 156?
The agent should call calculate and return the exact result instead of guessing.
- Test the timezone tool by typing:
What time is it in Tokyo?
- Test conversation memory by asking a follow-up question:
And in New York?
The agent should understand you're still asking about the time and call get_time for New York without you needing to repeat "what time is it."
- Type quit to exit the chat when you're done testing.
Seeing an error when you run the agent?
- If you see Error: 400 or API key not valid, check that your GEMINI_API_KEY environment variable is still set in this terminal session.
- If you see ModuleNotFoundError: No module named 'requests', make sure your virtual environment is activated before running the script.
- If the agent responds without calling a tool (no [Agent calling tool: ...] message), try rephrasing your question to be more direct (e.g. "Get the weather in London" instead of "I wonder what London is like").
My agent is returning errors or not calling tools correctly.
Secret mission
Multi-Step Tool Chaining
Your agent handles one tool call at a time. But real-world questions often require multiple tools in sequence. What if someone asks for the temperature in Tokyo AND wants it converted to Fahrenheit? Upgrade your agent to chain tool calls automatically and handle parallel requests.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally (aside from the free Gemini API key), so there are no ongoing costs.
Resources you used:
- Project folder with Python virtual environment.
- Installed packages: google-genai and requests.
- Gemini API key from Google AI Studio (free tier, no billing attached).
- Environment variable: GEMINI_API_KEY.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with your agent further.
- Your project folder, virtual environment, and API key all stay in place.
- You can reactivate your virtual environment and run agent.py anytime.
- The Gemini free tier has no expiration date, so your API key remains valid.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and API key stay intact for later.
- Close any terminal sessions running the agent.
- Deactivate your virtual environment by running:
deactivate
- Your project folder and GEMINI_API_KEY environment variable remain available when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps to clean up completely.
- Delete your project folder (which includes the virtual environment and all code) by running:
cd ~/Desktop
rm -rf ai-agent-tools
- Remove the GEMINI_API_KEY environment variable from your shell profile (.bashrc, .zshrc, or system environment variables on Windows).
- Optionally, revoke your API key in Google AI Studio by visiting aistudio.google.com and deleting the key from your API keys page.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional AI agent that autonomously decides when and how to use external tools to answer questions.
You've learned how to:
- Built a working AI agent powered by Gemini 2.5 Flash that decides which tools to call based on natural language input.
- Implemented the complete tool-use loop: sending a prompt, detecting function calls, executing the right function, and returning results back to the model.
- Created an interactive chat interface with conversation memory so the agent remembers context across multiple turns.
- Added multi-step tool chaining so the agent can call multiple tools sequentially to answer complex, multi-part questions.
Ready to quiz yourself?