Build a Personal Assistant Agent
Build a Claude-powered AI agent that autonomously calls tools to answer questions.
Introduction
30 Second Summary
Small tasks pile up quickly when every calculation, reminder, and lookup needs a separate app. Switching between them takes more effort than the task itself.
In this project, you will build a command-line personal assistant powered by Gemini. Your agent decides which tool to use whenever you ask it a question.
What You'll Build
You'll ask a question such as What is 1847 * 29 + 15? and watch your Gemini-powered agent call its calculator before answering in plain language.
By the end of this project, you'll have:
- A working AI agent that chooses between conversation, calculations, and to-do actions.
- A visible agentic loop that shows each function call and result in your terminal.
- A reusable Python template for building more Gemini-powered agents with custom functions.
- Secret Mission: Add a dictionary lookup tool that fetches live word definitions from a free API.
Are there any prerequisites?
No prior coding experience is needed. This guide walks you through every step, from creating your account to running your finished agent.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Environment and Get Your API Key
Your assistant needs a safe Python workspace and permission to call Gemini. This step prepares both before you write application code.
You will confirm Python 3.10 or newer and prepare a virtual environment with the Google Gen AI SDK. You will then open the project in Visual Studio Code and load a Gemini API key into your terminal session.
In this step, get ready to:
- Confirm that Python 3.10 or newer is available.
- Create a virtual environment and install the Google Gen AI SDK.
- Open the project in Visual Studio Code and safely load a Gemini API key.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open system search.
- Type Terminal (macOS) or PowerShell (Windows).
- Press Enter to open the command-line window.
Check your Python version
The Google Gen AI SDK requires Python 3.10 or newer. Check the version before creating the project so package installation does not fail later.
- Check your installed Python version by running:
python --version
✔️ I see Python 3.10 or newer
Your Python version supports the Google Gen AI SDK. Continue to the project setup below.
ⓧ I see an older version
Your installed Python version is too old for the current SDK. Install Python 3.10 or newer before continuing.
- Open the official Python downloads page.
- Install the latest supported Python 3 release for your operating system.
- Run the version check again after the installation completes.
ⓧ Command not found
Python is missing or uses the python3 command on your computer.
- Try the alternate version command by running:
python3 --version
- Open the official Python downloads page if neither command works.
Create the Python environment
A virtual environment keeps this project's packages separate from the rest of your computer. The install uses google-genai 2.25.0, which requires Python 3.10 or newer.
macOS
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the personal-assistant folder and move into it by running:
mkdir personal-assistant
cd personal-assistant
- Confirm your location by running:
pwd
You should see a path ending in Desktop/personal-assistant.
- Create the virtual environment by running:
python3 -m venv venv
- Activate the virtual environment by running:
source venv/bin/activate
You should see (venv) at the start of the terminal prompt.
- Install the pinned Google Gen AI SDK by running:
python -m pip install google-genai==2.25.0
The first install can take a minute while Python downloads the SDK and its dependencies.
- Verify the SDK installation by running:
python -c "from google import genai; print('Google Gen AI SDK ready')"
You should see Google Gen AI SDK ready in the terminal.
- Install Visual Studio Code from the official download page if it is not already installed.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code.
- Press Enter to open the editor.
- Click File in the top menu bar.
- Click Open Folder.
- Select the personal-assistant folder on your Desktop.
- Click Open.
You should see personal-assistant in the Explorer sidebar.
Windows
- Move to your Desktop by running this command:
Set-Location "$HOME\Desktop"
- Create the personal-assistant folder and move into it by running:
New-Item -ItemType Directory -Name "personal-assistant"
Set-Location "personal-assistant"
- Confirm your location by running:
Get-Location
You should see a path ending in Desktop\personal-assistant.
- Create the virtual environment by running:
python -m venv venv
- Activate the virtual environment by running:
.\venv\Scripts\Activate.ps1
You should see (venv) at the start of the PowerShell prompt.
- Install the pinned Google Gen AI SDK by running:
python -m pip install google-genai==2.25.0
The first install can take a minute while Python downloads the SDK and its dependencies.
- Verify the SDK installation by running:
python -c "from google import genai; print('Google Gen AI SDK ready')"
You should see Google Gen AI SDK ready in PowerShell.
- Install the Visual Studio Code User Installer if the editor is not already installed.
- Press the Windows key to open system search.
- Type Visual Studio Code.
- Press Enter to open the editor.
- Click File in the top menu bar.
- Click Open Folder.
- Select the personal-assistant folder on your Desktop.
- Click Select Folder.
You should see personal-assistant in the Explorer sidebar.
SDK installation failed?
Confirm that your terminal shows (venv) before the prompt. If it does not, activate the virtual environment and run the install command again.
Help me debug the Google Gen AI SDK installation.
Create and load your Gemini API key
An API key authorizes your script to use the Gemini API. Treat it like a password because anyone with the value can make requests through your project.
- Open Google AI Studio API Keys.
- Sign in with your Google account.
- Click Create API key.
- Copy the new key into a password manager.
- Close the key dialog before taking any screenshots.
Loading the key into the current terminal keeps it out of agent.py. Replace the example text below with your real key, but never share or commit that command.
macOS
- Load the key into the current terminal session by running:
export GEMINI_API_KEY="paste-your-key-here"
- Confirm that Python can detect the key without printing it by running:
python -c "import os; print('Gemini key loaded' if os.getenv('GEMINI_API_KEY') else 'Gemini key missing')"
Windows
- Load the key into the current PowerShell session by running:
$env:GEMINI_API_KEY = "paste-your-key-here"
- Confirm that Python can detect the key without printing it by running:
python -c "import os; print('Gemini key loaded' if os.getenv('GEMINI_API_KEY') else 'Gemini key missing')"
You should see Gemini key loaded. Your credential is available to the SDK without appearing in your Python file.
Key not detected?
Run the environment-variable command and the verification command in the same terminal window. Opening a new terminal clears this temporary value.
Help me load my Gemini API key safely.
Your Python workspace and Gemini credential are ready. Next, you will send a real request and see your assistant answer in the terminal.
Make Your First API Call to Gemini
Gemini now has a secure connection from your Python environment. This step proves that connection with a real response.
You will begin with one request, then turn the script into an assistant that accepts new questions from the terminal.
In this step, get ready to:
- Create agent.py and send one Gemini request.
- Turn the script into an interactive command-line assistant.
- Expose the missing function capability.
Send your first request
The Google Gen AI SDK handles authentication and request formatting. Your code provides the model ID and the text you want Gemini to answer.
- Create agent.py inside the personal-assistant folder from your editor's file sidebar.
- Copy this first request into agent.py.
from google import genai
# Create a client that reads GEMINI_API_KEY
client = genai.Client()
# Send one prompt to the free-tier Gemini model
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Introduce yourself as a personal assistant in one sentence.",
)
print(response.text)
What does this code do?
- genai.Client() reads GEMINI_API_KEY from your terminal environment.
- gemini-3.5-flash selects a model with documented free-tier usage.
- response.text gives you the final text response.
- Save agent.py.
- Predict whether Gemini will answer or report an authentication error.
- Run your first request with:
python agent.py
You should see a one-sentence introduction from Gemini. That response proves your key, SDK, model, and Python file work together.
Request failed?
Set GEMINI_API_KEY again if the SDK cannot find it. If authentication fails, create a replacement key in Google AI Studio and load the new value.
Help me debug my first Gemini request.
Build the interactive assistant
One hard-coded prompt proves the connection. An interactive loop lets you enter new questions without editing the file for every request.
- Replace everything in agent.py with this interactive version:
from google import genai
# Create one client for the whole session
client = genai.Client()
def main():
print("Personal Assistant Agent (type 'quit' to exit)")
while True:
# Read one request from the terminal
user_input = input("\nYou: ").strip()
if user_input.lower() == "quit":
print("Goodbye.")
break
if not user_input:
continue
# Send the request and print Gemini's reply
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=user_input,
)
print(f"\nAssistant: {response.text}")
if __name__ == "__main__":
main()
How does the conversation work?
- while True keeps the assistant ready for more questions.
- input() captures each message you type.
- quit gives the loop a clean exit path.
- Save agent.py.
- Start the interactive assistant by running:
python agent.py
- Type Give me a two-item morning checklist.
- Press Enter.
You should see a short checklist below Assistant:. The program remains ready for another message.
Reveal the missing capability
Gemini can produce a plausible calculation in text, but your program cannot prove that it used a calculator. This missing action layer separates a chatbot from an agent.
Before you test it, predict whether the terminal will show a calculator call.
- Type Calculate 1847 * 29 + 15 and show which calculator tool you used.
- Press Enter.
Gemini may produce the correct number, but you will not see a calculator call or a verified result. Your script only exchanges text, so it cannot perform or audit an external action.
- Type quit to stop the assistant.
✔️ Awesome, I've got everything!
Your interactive Gemini assistant is saved and responding in the terminal.
ⓧ I'd like to double check the full code
from google import genai
# Create one client for the whole session
client = genai.Client()
def main():
print("Personal Assistant Agent (type 'quit' to exit)")
while True:
# Read one request from the terminal
user_input = input("\nYou: ").strip()
if user_input.lower() == "quit":
print("Goodbye.")
break
if not user_input:
continue
# Send the request and print Gemini's reply
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=user_input,
)
print(f"\nAssistant: {response.text}")
if __name__ == "__main__":
main()
Your assistant can hold a text conversation, but it cannot show its work through functions. Next, you will give Gemini a calculator and complete the first action round trip.
Give Gemini Its First Tool
Your Gemini assistant can answer questions, but its calculations are still unverified text. A calculator function gives it a real action with an observable result.
Function calling lets Gemini request a function by name with structured arguments. Your Python code runs that function and returns the result for Gemini to explain.
In this step, get ready to:
- Declare a calculator function for Gemini.
- Observe the unfinished function-call response.
- Execute the calculator and return its result to Gemini.
Declare and observe the calculator call
A function declaration describes the function's name, purpose, and input shape. Gemini reads this schema to decide when the calculator fits a request.
- Replace the import and client section at the top of agent.py with this configuration:
from google import genai
from google.genai import types
# Create the Gemini client
client = genai.Client()
# Describe the calculator and its required input
calculator_function = types.FunctionDeclaration(
name="calculator",
description="Evaluate a mathematical expression.",
parameters={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression such as 1847 * 29 + 15",
}
},
"required": ["expression"],
},
)
config = types.GenerateContentConfig(
tools=[types.Tool(function_declarations=[calculator_function])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
What does this configuration do?
- FunctionDeclaration gives Gemini the calculator's schema.
- expression is the required string argument.
- AutomaticFunctionCallingConfig leaves execution under your Python code's control.
- Find the existing generate_content() call inside main().
- Add config=config so the call matches this version:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=user_input,
config=config,
)
- Save agent.py.
- Start the assistant by running:
python agent.py
Before you ask, predict whether declaring the calculator is enough to produce a final answer.
- Type What is 1847 * 29 + 15?
- Press Enter.
You will not get a useful final answer. Gemini requested the calculator, but your program has not executed that request or returned a result.
- Type quit to stop the assistant.
Execute the calculator locally
The function router turns Gemini's structured request into local Python work. This first version accepts only the calculator name.
- Add this function below config.
def process_tool_call(tool_name, tool_args):
# Run only the calculator declared above
if tool_name == "calculator":
try:
expression = tool_args["expression"]
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as error:
return f"Calculation error: {error}"
return f"Unknown tool: {tool_name}"
Keep this calculator local
eval() can execute Python expressions, so this restricted version is only suitable for a local learning project. Production agents should use a dedicated math parser with a strict allowlist.
- Save agent.py.
- Verify the local calculator function by running:
python -c "from agent import process_tool_call; print(process_tool_call('calculator', {'expression': '21 * 4'}))"
You should see 84. The local calculator works before Gemini starts using it.
Complete the function-call round trip
The remaining loop preserves Gemini's function call, attaches a matching result, and asks the model for the final response.
- Add this function below process_tool_call().
def ask_agent(user_input):
# Start this request with the user's message
contents = [types.Content(
role="user", parts=[types.Part(text=user_input)]
)]
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=contents,
config=config,
)
part = response.candidates[0].content.parts[0]
if not part.function_call:
return response.text
# Execute Gemini's requested function
function_call = part.function_call
result = process_tool_call(function_call.name, function_call.args)
contents.append(response.candidates[0].content)
# Match the local result to Gemini's function call
function_response = types.FunctionResponse(
name=function_call.name, response={"result": result}, id=function_call.id
)
function_result = types.Part(function_response=function_response)
contents.append(types.Content(role="user", parts=[function_result]))
final_response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config
)
return final_response.text
How does the round trip work?
- function_call provides the requested name, arguments, and call ID.
- FunctionResponse packages the local result with that same name and ID.
- The user role returns the function response to Gemini in the next turn.
- The second model call turns the raw result into a natural-language answer.
- Find the request and print lines inside main().
- Replace them with these lines:
answer = ask_agent(user_input)
print(f"\nAssistant: {answer}")
- Save agent.py.
- Start the assistant by running:
python agent.py
Before you test it, predict whether the response will now contain the exact result.
- Type What is 1847 * 29 + 15?
- Press Enter.
You should receive the exact result, 53578. Your program executed the calculator and Gemini explained its verified output.
- Type quit to stop the assistant.
Still missing a final answer?
Confirm that disable=True appears in the automatic function-calling configuration. Check that FunctionResponse uses the original function name and call ID inside a user message.
Help me debug the calculator function call.
✔️ Awesome, I've got everything!
Your calculator round trip is saved and returning verified results.
ⓧ I'd like to double check the full code
from google import genai
from google.genai import types
# Create one client for the terminal session
client = genai.Client()
# Describe the calculator and its input
calculator_function = types.FunctionDeclaration(
name="calculator",
description="Evaluate a mathematical expression.",
parameters={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression such as 1847 * 29 + 15",
}
},
"required": ["expression"],
},
)
# Keep function execution under local Python control
config = types.GenerateContentConfig(
tools=[types.Tool(function_declarations=[calculator_function])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
def process_tool_call(tool_name, tool_args):
# Route the declared calculator to local Python
if tool_name == "calculator":
try:
expression = tool_args["expression"]
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as error:
return f"Calculation error: {error}"
return f"Unknown tool: {tool_name}"
def ask_agent(user_input):
# Start this request with the user's message
contents = [types.Content(
role="user", parts=[types.Part(text=user_input)]
)]
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config
)
part = response.candidates[0].content.parts[0]
if not part.function_call:
return response.text
# Run the requested function and preserve Gemini's response
function_call = part.function_call
result = process_tool_call(function_call.name, function_call.args)
contents.append(response.candidates[0].content)
# Match the local result to Gemini's function call
function_response = types.FunctionResponse(
name=function_call.name, response={"result": result}, id=function_call.id
)
function_result = types.Part(function_response=function_response)
contents.append(types.Content(role="user", parts=[function_result]))
final_response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config
)
return final_response.text
def main():
# Keep accepting requests until the learner exits
print("Personal Assistant Agent (type 'quit' to exit)")
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() == "quit":
print("Goodbye.")
break
if not user_input:
continue
answer = ask_agent(user_input)
print(f"\nAssistant: {answer}")
if __name__ == "__main__":
main()
Gemini can now request one calculator call and use its result. Next, you will build a loop that handles several functions and remembers the conversation.
Build the Agentic Loop
Gemini can now request a calculator and use its result. The current code handles only one function-call round trip, so a second request would stop the workflow early.
An agentic loop keeps calling Gemini, executing every requested function, and returning results until Gemini produces a final text answer.
In this step, get ready to:
- Refactor one function call into a repeating agentic loop.
- Add functions for saving and viewing to-do items.
- Test one request that needs several actions.
Build the repeating loop
Gemini can request several functions in one response or request another function after seeing a result. The loop must preserve every model response because Gemini 3 uses the history to match function calls and reasoning across turns.
- Replace ask_agent() with this repeating chat() function:
def chat(user_input, contents):
# Add the new user message to the shared history
contents.append(types.Content(
role="user", parts=[types.Part(text=user_input)]
))
while True:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=contents,
config=config,
)
model_content = response.candidates[0].content
function_calls = [
part.function_call for part in model_content.parts
if part.function_call
]
contents.append(model_content)
# A text response means the agent has finished this turn
if not function_calls:
return response.text
# Execute every requested function and return the results
result_parts = []
for function_call in function_calls:
result = process_tool_call(function_call.name, function_call.args)
function_response = types.FunctionResponse(
name=function_call.name,
response={"result": result},
id=function_call.id,
)
result_parts.append(types.Part(function_response=function_response))
contents.append(types.Content(role="user", parts=result_parts))
What makes this agentic?
- contents stores the complete, unmodified conversation history.
- function_calls collects every function request from Gemini's response.
- while True repeats until the response contains text without another function call.
- Find main() near the bottom of agent.py.
- Add the shared history immediately below the welcome message:
# Preserve the conversation across user turns
contents = []
- Replace the existing ask_agent() line with:
answer = chat(user_input, contents)
print(f"\nAssistant: {answer}")
- Save agent.py.
- Verify the refactor by running:
python agent.py
- Type What is 21 * 4?
- Press Enter.
You should see an answer containing 84. The same calculator now runs inside a loop that can continue for more function requests.
- Type quit to stop the assistant.
Add the to-do functions
A personal assistant becomes more useful when it can change state. The next two functions store tasks in a Python list and return that list on demand.
- Add these declarations below calculator_function and above config.
add_todo_function = types.FunctionDeclaration(
name="add_todo",
description="Add one item to the user's to-do list.",
parameters={
"type": "object",
"properties": {
"item": {"type": "string", "description": "The item to add"}
},
"required": ["item"],
},
)
get_todos_function = types.FunctionDeclaration(
name="get_todos",
description="Return every item on the user's to-do list.",
parameters={"type": "object", "properties": {}},
)
- Replace the existing config with this three-function configuration:
config = types.GenerateContentConfig(
tools=[types.Tool(function_declarations=[
calculator_function,
add_todo_function,
get_todos_function,
])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
# Keep to-do items for the current terminal session
todo_list = []
What do the new declarations provide?
- add_todo accepts one required item.
- get_todos has no required input.
- todo_list keeps items in memory until you stop the program.
- Replace process_tool_call() with this three-function router:
def process_tool_call(tool_name, tool_args):
# Route each requested name to local Python code
if tool_name == "calculator":
try:
expression = tool_args["expression"]
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as error:
return f"Calculation error: {error}"
if tool_name == "add_todo":
item = tool_args["item"]
todo_list.append(item)
return f"Added '{item}' to the to-do list."
if tool_name == "get_todos":
if not todo_list:
return "The to-do list is empty."
return "\n".join(f"- {item}" for item in todo_list)
return f"Unknown tool: {tool_name}"
- Save agent.py.
- Start the assistant by running:
python agent.py
- Type Add book a dentist appointment to my to-do list.
- Press Enter.
- Type What's on my to-do list?
- Press Enter.
You should see Gemini confirm the saved item, then return book a dentist appointment when you ask for the list.
Test several actions in one request
Your loop can now process several function calls and repeat when Gemini needs another result. One combined request proves the control flow works beyond a single action.
Before you run the test, predict which functions Gemini will need.
- Type Add plan my budget to my list, calculate 12 * 8, and show my full list.
- Press Enter.
You should see the exact result 96 and a list containing both saved tasks. That result proves the loop can coordinate calculator and to-do functions in one turn.
- Type quit to stop the assistant.
One of the actions missing?
Check that every FunctionResponse uses the requested function name and matching call ID inside a user message. If the list is empty, confirm todo_list is defined outside every function so all calls share it.
Help me debug the multi-function loop.
✔️ Awesome, I've got everything!
Your complete agent.py is saved and can coordinate calculator and to-do functions.
ⓧ I'd like to double check the full code
from google import genai
from google.genai import types
# Create one client for the terminal session
client = genai.Client()
# Describe the calculator and its input
calculator_function = types.FunctionDeclaration(
name="calculator",
description="Evaluate a mathematical expression.",
parameters={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression such as 1847 * 29 + 15",
}
},
"required": ["expression"],
},
)
# Describe the two to-do actions
add_todo_function = types.FunctionDeclaration(
name="add_todo",
description="Add one item to the user's to-do list.",
parameters={
"type": "object",
"properties": {
"item": {"type": "string", "description": "The item to add"}
},
"required": ["item"],
},
)
get_todos_function = types.FunctionDeclaration(
name="get_todos",
description="Return every item on the user's to-do list.",
parameters={"type": "object", "properties": {}},
)
# Give Gemini all three function schemas
config = types.GenerateContentConfig(
tools=[types.Tool(function_declarations=[
calculator_function,
add_todo_function,
get_todos_function,
])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
# Keep to-do items for the current terminal session
todo_list = []
def process_tool_call(tool_name, tool_args):
# Route each requested name to local Python
if tool_name == "calculator":
try:
expression = tool_args["expression"]
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as error:
return f"Calculation error: {error}"
if tool_name == "add_todo":
item = tool_args["item"]
todo_list.append(item)
return f"Added '{item}' to the to-do list."
if tool_name == "get_todos":
if not todo_list:
return "The to-do list is empty."
return "\n".join(f"- {item}" for item in todo_list)
return f"Unknown tool: {tool_name}"
def chat(user_input, contents):
# Add the new user message to the shared history
contents.append(types.Content(
role="user", parts=[types.Part(text=user_input)]
))
while True:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=contents,
config=config,
)
model_content = response.candidates[0].content
function_calls = [
part.function_call for part in model_content.parts
if part.function_call
]
contents.append(model_content)
if not function_calls:
return response.text
# Execute every request and match each result to its call
result_parts = []
for function_call in function_calls:
result = process_tool_call(function_call.name, function_call.args)
function_response = types.FunctionResponse(
name=function_call.name,
response={"result": result},
id=function_call.id,
)
result_parts.append(types.Part(function_response=function_response))
contents.append(types.Content(role="user", parts=result_parts))
def main():
# Preserve history across every user turn
print("Personal Assistant Agent (type 'quit' to exit)")
contents = []
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() == "quit":
print("Goodbye.")
break
if not user_input:
continue
answer = chat(user_input, contents)
print(f"\nAssistant: {answer}")
if __name__ == "__main__":
main()
Your agent now keeps context and coordinates several functions. Next, you'll shape its behavior with a system instruction and expose every function call in the terminal.
Polish Your Agent with a System Prompt and Pretty Output
Your agent can now remember a conversation and coordinate three functions. The behavior works, but the terminal hides which function Gemini chose and why the result appeared.
A system instruction sets the assistant's operating rules. Function-call logging then makes each action visible enough to inspect and debug.
In this step, get ready to:
- Add a system instruction for the assistant's behavior.
- Log every Gemini function call and local result.
- Run a complete demonstration of the finished agent.
Add the system instruction
System instructions guide Gemini before it reads the conversation. Clear rules help it choose functions consistently while leaving ordinary conversation alone.
- Add this instruction immediately above config in agent.py.
# Define the assistant's role and function rules
system_instruction = """You are a concise personal assistant.
Use calculator for every arithmetic request.
Use add_todo when the user asks to save a task.
Use get_todos when the user asks to view saved tasks.
Use several functions when one request needs several actions.
For ordinary conversation, answer without calling a function."""
- Replace the existing config with this version:
config = types.GenerateContentConfig(
system_instruction=system_instruction,
tools=[types.Tool(function_declarations=[
calculator_function,
add_todo_function,
get_todos_function,
])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
Why use both schemas and instructions?
- system_instruction gives every model call the same rules.
- The function descriptions explain capabilities while the instruction defines when Gemini should use them.
- Manual function handling remains enabled so your Python loop stays visible.
- Save agent.py.
- Start the assistant by running:
python agent.py
- Type Hello. What can you help me with?
- Press Enter.
You should receive a concise description of the assistant's capabilities without changing your to-do list or performing a calculation.
- Type quit to stop the assistant.
Expose function calls in the terminal
Logging shows the boundary between Gemini's decision and your Python function. You can now see the chosen function, its arguments, and the result returned to the model.
- Add import json as the first line of agent.py.
- Find the for function_call in function_calls loop inside chat().
- Replace that loop with this logged version:
for function_call in function_calls:
# Show Gemini's requested function and arguments
print(
f" [Function Call: {function_call.name}] "
f"{json.dumps(dict(function_call.args))}"
)
result = process_tool_call(
function_call.name, function_call.args
)
print(f" [Function Result] {result}")
function_response = types.FunctionResponse(
name=function_call.name,
response={"result": result},
id=function_call.id,
)
result_parts.append(types.Part(
function_response=function_response
))
What do the logs reveal?
- function_call.name shows the function Gemini selected.
- json.dumps() formats its arguments as readable JSON.
- result shows the local output sent back to Gemini.
- Save agent.py.
- Start the logged assistant by running:
python agent.py
- Type What is 1847 * 29 + 15?
- Press Enter.
You should see [Function Call: calculator] and [Function Result] 53578 before the final answer.
Logs not appearing?
Confirm that import json is at the top of the file. Check that both print statements are inside the function-call loop and aligned with the line that executes process_tool_call().
Help me debug the missing function logs.
Run the finished assistant
Your final check covers direct conversation, arithmetic, saved state, and a request that needs several functions. Each output gives you evidence that a different part of the agent works.
- Start the finished assistant by running:
python agent.py
Before each request, predict whether Gemini will answer directly or call a function.
- Type Tell me a one-line joke.
- Confirm that Gemini answers without a function log.
- Type Add renew my passport to my to-do list.
- Confirm that add_todo appears in the function log.
- Type Calculate 25 * 16 and show my to-do list.
- Confirm that the response contains 400 and your passport task.
- Type quit to stop the assistant.
That's the full agent working: Gemini selects functions, Python executes them, and the loop carries results back until the request is complete.
✔️ Awesome, I've got everything!
Your finished agent.py is saved and passes the full demonstration.
ⓧ I'd like to double check the full code
import json
from google import genai
from google.genai import types
# Create one client for the terminal session
client = genai.Client()
# Describe the calculator and its input
calculator_function = types.FunctionDeclaration(
name="calculator",
description="Evaluate a mathematical expression.",
parameters={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression such as 1847 * 29 + 15",
}
},
"required": ["expression"],
},
)
# Describe the two to-do actions
add_todo_function = types.FunctionDeclaration(
name="add_todo",
description="Add one item to the user's to-do list.",
parameters={
"type": "object",
"properties": {
"item": {"type": "string", "description": "The item to add"}
},
"required": ["item"],
},
)
get_todos_function = types.FunctionDeclaration(
name="get_todos",
description="Return every item on the user's to-do list.",
parameters={"type": "object", "properties": {}},
)
# Set the assistant's role and function rules
system_instruction = """You are a concise personal assistant.
Use calculator for every arithmetic request.
Use add_todo when the user asks to save a task.
Use get_todos when the user asks to view saved tasks.
Use several functions when one request needs several actions.
For ordinary conversation, answer without calling a function."""
# Give Gemini the instruction and all three function schemas
config = types.GenerateContentConfig(
system_instruction=system_instruction,
tools=[types.Tool(function_declarations=[
calculator_function,
add_todo_function,
get_todos_function,
])],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True
),
)
# Keep to-do items for the current terminal session
todo_list = []
def process_tool_call(tool_name, tool_args):
# Route each requested name to local Python
if tool_name == "calculator":
try:
expression = tool_args["expression"]
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as error:
return f"Calculation error: {error}"
if tool_name == "add_todo":
item = tool_args["item"]
todo_list.append(item)
return f"Added '{item}' to the to-do list."
if tool_name == "get_todos":
if not todo_list:
return "The to-do list is empty."
return "\n".join(f"- {item}" for item in todo_list)
return f"Unknown tool: {tool_name}"
def chat(user_input, contents):
# Add the new user message to the shared history
contents.append(types.Content(
role="user", parts=[types.Part(text=user_input)]
))
while True:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents=contents,
config=config,
)
model_content = response.candidates[0].content
function_calls = [
part.function_call for part in model_content.parts
if part.function_call
]
contents.append(model_content)
if not function_calls:
return response.text
# Execute each function and expose the round trip
result_parts = []
for function_call in function_calls:
print(
f" [Function Call: {function_call.name}] "
f"{json.dumps(dict(function_call.args))}"
)
result = process_tool_call(function_call.name, function_call.args)
print(f" [Function Result] {result}")
function_response = types.FunctionResponse(
name=function_call.name,
response={"result": result},
id=function_call.id,
)
result_parts.append(types.Part(
function_response=function_response
))
contents.append(types.Content(role="user", parts=result_parts))
def main():
# Preserve history across every user turn
print("Personal Assistant Agent (type 'quit' to exit)")
contents = []
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() == "quit":
print("Goodbye.")
break
if not user_input:
continue
answer = chat(user_input, contents)
print(f"\nAssistant: {answer}")
if __name__ == "__main__":
main()
Secret mission
Add a Dictionary Lookup Tool
Extend your Gemini assistant with a live dictionary function. It will fetch real definitions from the internet and return them through the same agentic loop.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your files, pause the local environment, or delete the project. The main path uses Gemini 3.5 Flash within its documented free tier, so this project does not require paid API usage.
Resources you used:
- Gemini API authorization key created in Google AI Studio.
- GEMINI_API_KEY set for your terminal session.
- Local personal-assistant folder containing agent.py and venv.
Keep everything running
No action is needed. Choose this if you plan to keep extending the Gemini assistant.
- Keep the Gemini authorization key active for future API requests.
- Keep personal-assistant on your Desktop with agent.py and venv.
- Set GEMINI_API_KEY again whenever you open a new terminal session.
Pause - I'll come back to this later
Shut down the active Python environment while keeping your files and Gemini key for later.
- Deactivate the virtual environment by running:
deactivate
- Close your code editor.
- Close the terminal to clear the temporary GEMINI_API_KEY value.
- Keep the personal-assistant folder on your Desktop.
Delete - I don't want to use this again
Deleting the key and local folder removes the resources created for this project. Your Google account and Google Cloud project remain available.
- Open Google AI Studio API Keys.
- Find the authorization key created for this project.
- Open the key's action menu.
- Select Delete to revoke the key.
- Close the terminal to remove the temporary GEMINI_API_KEY value.
Deleting the project folder is permanent. The commands below remove agent.py and its virtual environment together.
macOS
- Delete the local project folder by running:
rm -rf ~/Desktop/personal-assistant
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see personal-assistant in the output.
Windows
- Delete the local project folder by running:
Remove-Item -Recurse -Force "$HOME\Desktop\personal-assistant"
- Confirm the folder is gone by running:
Get-ChildItem "$HOME\Desktop"
You should no longer see personal-assistant in the output.
Nice Work!
Nice Work!
Nice work! You built a working AI agent with Gemini. It can calculate, manage a to-do list, preserve context, and choose functions from natural-language requests.
You've learned how to:
- Built a Gemini-powered assistant that chooses between direct answers and local functions.
- Implemented an agentic loop that preserves history, executes function calls, and returns matching results until Gemini finishes.
- Defined function schemas for calculations and to-do management, then exposed each action through terminal logs.
- Secret Mission: Connected the agent to a live dictionary API and handled missing words and network failures.
Ready to quiz yourself?