Build an AI Agent with Tool Use

Build a Python CLI agent that uses Gemini function calling to orchestrate tools.

Introduction

30 Second Summary

When you ask a virtual assistant to check the weather and then book a restaurant, it does not just generate text. It decides which tools to use, calls them, and combines the results into a single answer. This decision-making loop is the core architecture behind every production AI system.

In this project, you will build a command-line AI agent in Python that uses Google's Gemini API to reason about which tools to call, execute them, and synthesize a final answer.

What You'll Build

A Python CLI research assistant that uses function calling to dynamically select and execute custom tools (a calculator and a dictionary) through an agentic loop.

By the end of this project, you'll have:

  • A Python virtual environment with the google-genai SDK connected to Gemini's free tier.
  • A tool registry with two custom tools defined using JSON schema declarations.
  • A complete agentic loop that detects function calls, executes tools locally, and sends results back to Gemini for synthesis.
  • Secret Mission: Add a third tool and handle multi-step reasoning where the agent chains multiple tool calls to answer a single question.

Are there any prerequisites?

You need basic programming experience in any language. No prior Python experience is required. The project teaches Python fundamentals (virtual environments, dictionaries, control flow) as you go.

Set Up Your Environment

Before you can build an AI agent, you need a working Python environment with the right SDK and API access configured.

In this step, you'll create a project folder, set up a virtual environment, install the google-genai SDK, and connect to the Gemini API with a free API key.

In this step, get ready to:
  • Create a project directory and Python virtual environment.
  • Install the google-genai SDK.
  • Configure your Gemini API key and verify connectivity.
Create a project directory and virtual environment

A virtual environment isolates your project's packages from the rest of your system. This prevents version conflicts between projects.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (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 you're in the new folder by running:
pwd

You should see a path ending in /ai-agent.

Seeing an error when creating the folder?

If you see "File exists", a folder called ai-agent already exists on your Desktop. Either delete it first or choose a different name like ai-agent-2.

On Windows, use cd %USERPROFILE%\Desktop instead of cd ~/Desktop if the tilde doesn't work in Command Prompt.

Now verify that your Python version is 3.10 or higher. The google-genai SDK requires Python 3.10+ to run.

  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.10 or higher

You're all set. Move on to creating the virtual environment below.

ⓧ I see an older version

You have Python installed, but the version is below 3.10. You need to install a newer version.

  • Download the latest Python installer from python.org/downloads.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install.
  • Close and reopen your terminal, then run the version check command again.

Why does macOS sometimes show an older version?

macOS ships with a system Python that may be older than 3.10. After installing a newer version from python.org, use python3.12 (or whatever version you installed) to access it directly.

ⓧ Command not found

Python is not installed on your machine.

  • Download the latest Python installer from python.org/downloads.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install.
  • Close and reopen your terminal, then run the version check command again.
  • Create a virtual environment by running this command:

macOS

python3 -m venv .venv

Windows

python -m venv .venv

What is a virtual environment?

A virtual environment is a self-contained folder (.venv) that holds its own copy of Python and any packages you install. This keeps your project's dependencies separate from everything else on your computer.

Now open VS Code in this project folder. This is where you'll write all your code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the ai-agent folder, and click Open.
  • Open the integrated terminal by pressing Ctrl+` (both macOS and Windows).
  • Activate the virtual environment by running this command:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

You should see (.venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Not seeing (.venv) in your prompt?

On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell first, then try the activate command again.

On macOS, make sure you used source before the path. Without it, the command won't work.

Help me activate my Python virtual environment.

Install the google-genai SDK

The google-genai package is Google's official Python SDK for the Gemini API. It handles authentication, request formatting, and response parsing.

  • Install the SDK by running this command:
pip install google-genai

What does this command do?

The pip install command downloads the google-genai package and all of its dependencies from PyPI (Python's package registry) into your virtual environment. Because your venv is active, nothing gets installed system-wide.

  • Verify the installation succeeded by running:
python -c "from google import genai; print('google-genai installed successfully')"

You should see google-genai installed successfully printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active (you should see (.venv) in your prompt). If not, activate it again using the commands from the previous substep, then re-run pip install google-genai.

Help me fix my google-genai import error.

Create a free API key and test connectivity

The Gemini API uses an API key to authenticate your requests. Google AI Studio provides free API keys with no billing account required.

  • Open your browser and navigate to Google AI Studio API keys.
  • Sign in with your Google account if prompted.
  • Click Create API key.
  • Copy the generated key. You will only see the full key once, so copy it now.

Is this free?

Yes. Google AI Studio provides a free tier for the Gemini API with no credit card required. The free tier includes generous rate limits that are more than enough for this project.

Now set the API key as an environment variable so the SDK can find it automatically.

  • Back in VS Code's terminal, set the environment variable by running this command:

macOS

export GEMINI_API_KEY=your-api-key-here

Replace your-api-key-here with the key you just copied.

Windows

$env:GEMINI_API_KEY = "your-api-key-here"

Replace your-api-key-here with the key you just copied.

Why use an environment variable?

Storing your API key in an environment variable keeps it out of your code files. This prevents accidentally sharing your key if you push code to GitHub. The SDK reads GEMINI_API_KEY automatically when creating a client.

  • Test that your API key works by running this command:
python -c "from google import genai; import os; client = genai.Client(api_key=os.environ['GEMINI_API_KEY']); r = client.interactions.create(model='gemini-3-flash-preview', input='Say hello in one word'); print(r.output_text)"

You should see a short response from the Gemini model (something like "Hello" or a greeting). This confirms your API key, SDK, and network connection are all working.

Seeing an authentication or API error?

If you see an error about an invalid API key, double-check that you pasted the full key with no extra spaces. Run the export (macOS) or $env: (Windows) command again with the correct key.

If you see a connection error, check your internet connection. The Gemini API requires network access.

Help me fix my Gemini API connection issue.

Your environment is ready. You have Python, the Gemini SDK, and a working API key. Next up, you'll write your first script and make a proper API call.

Make Your First Gemini API Call

Your environment is ready with the SDK installed and your API key configured. Now it's time to write actual Python code that talks to Gemini.

In this step, you'll create your first Python script, send a message to the Gemini API, and get a response back. Along the way, you'll learn how Python imports, variables, and function calls work together.

In this step, get ready to:
  • Create a Python script that connects to the Gemini API.
  • Run the script and observe how Gemini responds.
  • Set a system instruction that gives your agent a persona.
Create agent.py and write a basic API call

Every Python project starts with a script file. This file will hold all the logic for your AI agent as you build it up step by step.

  • In the Explorer sidebar in VS Code, right-click inside your project folder and select New File.
  • Name the file agent.py and press Enter to create it.
  • Paste the following code into agent.py:
import json
import os
from google import genai

# Initialize the Gemini API client using your API key
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

# Set the model to use for all API calls
MODEL = "gemini-3-flash-preview"

# Send a message to Gemini and store the response
interaction = client.interactions.create(
    model=MODEL,
    input="Hello! What can you help me with?",
)

# Print Gemini's response text
print(interaction.output_text)

What does this code do?

  • import json and import os load Python's built-in modules for working with JSON data and environment variables. You'll use json in later steps for tool results.
  • from google import genai loads the Google GenAI SDK you installed in Step 1.
  • genai.Client(api_key=os.environ["GEMINI_API_KEY"]) creates an authenticated connection to the Gemini API using the key from your environment variable.
  • MODEL stores the model ID as a constant so you only change it in one place if needed.
  • client.interactions.create(...) sends your message to Gemini and returns an interaction object containing the response.
  • interaction.output_text extracts just the text portion of Gemini's response.
  • Save agent.py.

You'll run this script in the next substep to see Gemini's response.

Run the script and observe the response

Running a Python script sends your code to the Python interpreter, which executes each line from top to bottom. When it reaches the interactions.create call, it sends your message to Gemini's servers and waits for a response.

  • In the terminal with your virtual environment still active, run your script by entering this command:
python agent.py

You should see a text response from Gemini printed in your terminal. The exact wording varies each time because the model generates responses dynamically.

What should I see?

Gemini will respond with something like a greeting and a list of topics it can help with. The key thing is that you see text output and no error messages. This confirms your API key, SDK, and network connection are all working.

Seeing an error instead of a response?

  • If you see a KeyError about GEMINI_API_KEY, your environment variable isn't set in this terminal session. Re-run the export command from Step 1 in the same terminal.
  • If you see ModuleNotFoundError: No module named 'google', your virtual environment may not be active. Check that your terminal prompt shows the venv name in parentheses.
  • If you see a permission or authentication error, double-check that you copied the full API key with no extra spaces.

Why isn't my Gemini API call working?

Add a system instruction for the research assistant persona

A system instruction tells the model how to behave across all its responses. It's like giving your agent a job description before it starts answering questions.

  • In agent.py, find the interactions.create call. It currently looks like this:
interaction = client.interactions.create(
    model=MODEL,
    input="Hello! What can you help me with?",
)
  • Add a system_instruction parameter after the input line, so the call now looks like this:
interaction = client.interactions.create(
    model=MODEL,
    input="Hello! What can you help me with?",
    system_instruction="You are a helpful research assistant.",
)

What does system_instruction do?

The system_instruction parameter shapes the model's personality and behavior. Without it, Gemini responds as a general-purpose assistant.

With "You are a helpful research assistant.", Gemini will frame its responses around research and information gathering. Later, this persona will guide how the agent decides which tools to use.

  • Save agent.py and run it again:
python agent.py

You should see a response that's more focused on research topics and information gathering compared to the generic response you saw before.

Response looks the same as before?

  • Make sure you saved agent.py after adding the system_instruction line. Unsaved changes won't take effect until you save.
  • Check that system_instruction is inside the client.interactions.create(...) parentheses, not outside them.
  • Make sure there's a comma after the input line, before system_instruction.

Help me fix my system instruction in agent.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved agent.py and your script runs without errors.

ⓧ I'd like to double check the full code

Here's the complete agent.py file at this point:

import json
import os
from google import genai

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

MODEL = "gemini-3-flash-preview"

interaction = client.interactions.create(
    model=MODEL,
    input="Hello! What can you help me with?",
    system_instruction="You are a helpful research assistant.",
)

print(interaction.output_text)

Your script can now talk to Gemini and get intelligent responses back. Next up, you'll define custom tools that give your agent real capabilities beyond just chatting.

Define Custom Tools

Your agent can generate text, but right now it has no way to do anything. It can't perform calculations, look up data, or interact with the world.

In this step, you'll define "tools" using JSON schema declarations that tell Gemini exactly what actions are available. This is the foundation of function calling. You describe a tool's name, purpose, and parameters, and the model decides when to use it.

In this step, get ready to:
  • Define a calculator tool with a JSON schema specifying its parameters.
  • Define a dictionary lookup tool with sample definitions.
  • Pass both tools to the API and observe Gemini requesting a function call.
Define the calculator tool

A tool declaration is a Python dictionary that follows a specific schema. It tells Gemini the tool's name, what it does, and what parameters it accepts.

  • In agent.py, find the following code that starts with interaction = client.interactions.create(:
interaction = client.interactions.create(
    model=MODEL,
    input="Hello! What can you help me with?",
    system_instruction="You are a helpful research assistant.",
)

print(interaction.output_text)
  • Select all of this code (from the interaction variable through the print statement) and delete it.
  • Below the MODEL = "gemini-3-flash-preview" line, add the calculator tool declaration by pasting this code:
calculator_tool = {
    "type": "function",
    "name": "calculate",
    "description": "Performs a mathematical calculation. Use this for any arithmetic, algebra, or numeric computation.",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate, e.g. '2 + 2' or '847 * 23'",
            },
        },
        "required": ["expression"],
    },
}

What does this code do?

This dictionary follows the function declaration format that the Gemini API expects. Each key teaches Gemini something different about the tool.

  • "type": "function" tells Gemini this is a callable function.
  • "name" is the identifier Gemini uses when it decides to call this tool.
  • "description" helps Gemini decide WHEN to use the tool. A clear description means better tool selection.
  • "parameters" defines what input the tool needs. Here it's a single string called expression.

You won't see a change yet. The next chunk adds the second tool and registers them both.

Define the dictionary lookup tool

The second tool lets the agent look up word definitions. You'll also create a small dictionary of sample words and register both tools in a list.

  • Below the calculator_tool dictionary you just added, paste the dictionary tool declaration, the tools list, and the definitions data:
dictionary_tool = {
    "type": "function",
    "name": "lookup_word",
    "description": "Looks up the definition of an English word.",
    "parameters": {
        "type": "object",
        "properties": {
            "word": {
                "type": "string",
                "description": "The English word to define",
            },
        },
        "required": ["word"],
    },
}

tools = [calculator_tool, dictionary_tool]

DEFINITIONS = {
    "serendipity": "The occurrence of events by chance in a happy or beneficial way.",
    "ephemeral": "Lasting for a very short time.",
    "ubiquitous": "Present, appearing, or found everywhere.",
    "pragmatic": "Dealing with things sensibly and realistically.",
    "eloquent": "Fluent or persuasive in speaking or writing.",
}

What's happening here?

  • dictionary_tool follows the same schema as the calculator. Its single parameter is a word string.
  • tools = [calculator_tool, dictionary_tool] creates a list you'll pass to the API so Gemini knows about both tools at once.
  • DEFINITIONS is a Python dictionary that maps words to their meanings. In the next step, you'll write the logic that actually looks up words from this data.

You won't see output yet. The next chunk adds the API call that puts these tools to work.

Pass tools to the API call

Now you'll wire the tools into an API call. When you ask a question that matches a tool's description, Gemini responds with a function call request instead of plain text.

  • At the bottom of agent.py (below the DEFINITIONS dictionary), add this API call that passes the tools and asks a math question:
interaction = client.interactions.create(
    model=MODEL,
    input="What is 847 * 23?",
    tools=tools,
)

for step in interaction.steps:
    print(step)

What does this code do?

  • tools=tools passes your tool declarations to Gemini. The model now knows it can request a calculation or a word lookup.
  • input="What is 847 * 23?" is a question that clearly needs a calculator. Gemini should recognize this and request the calculate tool instead of answering directly.
  • The for step in interaction.steps loop prints each step in the interaction, including any function call requests.
  • Save agent.py.
  • Run the script in your terminal by running this command:
python agent.py
  • In the output, look for a step with type set to function_call. You should see Gemini requesting the calculate tool with the expression 847 * 23.

Why isn't there an answer yet?

Gemini isn't computing 847 * 23 itself. It's saying "I need to use the calculate tool with this expression." Your code hasn't actually executed the tool yet. In the next step, you'll build the agentic loop that detects these requests, runs the tool, and sends the result back.

Not seeing a function_call step?

  • Make sure you passed tools=tools in the client.interactions.create() call. Without it, Gemini doesn't know any tools exist.
  • Check that your tools list contains both calculator_tool and dictionary_tool. A typo in the variable name causes a NameError.
  • Verify you deleted the old interaction code completely. Having two interaction = ... assignments means only the last one runs.

Help me debug my tool declarations

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

import json
import os
from google import genai

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

MODEL = "gemini-3-flash-preview"

calculator_tool = {
    "type": "function",
    "name": "calculate",
    "description": "Performs a mathematical calculation. Use this for any arithmetic, algebra, or numeric computation.",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate, e.g. '2 + 2' or '847 * 23'",
            },
        },
        "required": ["expression"],
    },
}

dictionary_tool = {
    "type": "function",
    "name": "lookup_word",
    "description": "Looks up the definition of an English word.",
    "parameters": {
        "type": "object",
        "properties": {
            "word": {
                "type": "string",
                "description": "The English word to define",
            },
        },
        "required": ["word"],
    },
}

tools = [calculator_tool, dictionary_tool]

DEFINITIONS = {
    "serendipity": "The occurrence of events by chance in a happy or beneficial way.",
    "ephemeral": "Lasting for a very short time.",
    "ubiquitous": "Present, appearing, or found everywhere.",
    "pragmatic": "Dealing with things sensibly and realistically.",
    "eloquent": "Fluent or persuasive in speaking or writing.",
}

interaction = client.interactions.create(
    model=MODEL,
    input="What is 847 * 23?",
    tools=tools,
)

for step in interaction.steps:
    print(step)

Your tools are declared and Gemini knows how to request them. Next up, you'll build the agentic loop that actually executes the tools and feeds results back to Gemini for a final answer.

Build the Agentic Loop

In the previous step, you told Gemini what tools exist and watched it respond with a function_call step. The agent said "I want to use the calculator" but nothing actually happened.

Now you will close the loop. Your agent will detect when Gemini requests a tool, execute that tool locally, and send the result back so Gemini can synthesize a final answer. This detect-execute-return cycle is the agentic loop pattern that powers every production AI system at Google, Meta, and Amazon.

In this step, get ready to:
  • Write an execute_tool function that dispatches to the correct tool logic.
  • Build the agentic loop that detects function calls, executes tools, and sends results back.
  • Wrap everything in a run_agent function that takes a user query and returns the final synthesized response.
Write the execute_tool function

When Gemini returns a function_call step, it gives you the tool name and its arguments. Your code needs a dispatcher that routes to the correct implementation and returns a result.

  • In agent.py, scroll to the bottom and find this test code:
interaction = client.interactions.create(
    model=MODEL,
    input="What is 847 * 23?",
    tools=tools,
)

for step in interaction.steps:
    print(step)
  • Delete this entire block. It was useful for testing in the last step, but you will replace it with proper functions now.
  • Below the DEFINITIONS dictionary, add the execute_tool function by copying this code:
def execute_tool(name: str, arguments: dict) -> str:
    if name == "calculate":
        try:
            allowed_chars = set("0123456789+-*/.() ")
            expression = arguments["expression"]
            if not all(c in allowed_chars for c in expression):
                return json.dumps({"error": "Invalid characters in expression"})
            result = eval(expression)
            return json.dumps({"result": str(result)})
        except Exception as e:
            return json.dumps({"error": f"Calculation failed: {e}"})
    elif name == "lookup_word":
        word = arguments["word"].lower().strip()
        definition = DEFINITIONS.get(word, f"No definition found for '{word}'.")
        return json.dumps({"word": word, "definition": definition})
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})

What does this code do?

This function takes a tool name and its arguments dictionary, then routes to the correct implementation using if/elif branches.

  • The calculate branch uses Python's eval() to evaluate math expressions. The allowed_chars set restricts input to safe characters only (digits, operators, spaces, and parentheses).
  • The lookup_word branch looks up words in your DEFINITIONS dictionary and returns a fallback message if the word is not found.
  • Every branch returns a JSON string using json.dumps(). This keeps the format consistent for Gemini to parse later.

You will not see any output yet. The next substep builds the function that calls execute_tool as part of the full loop.

Understand the agentic loop pattern

The agentic loop is a cycle your code repeats every time the user asks a question. It is the foundational architecture behind production function calling systems.

How does the agentic loop work?

Your agent follows four steps on every query:

  • Send the user's question to Gemini along with the available tool declarations.
  • Check Gemini's response for a function_call step. If Gemini decides it needs a tool, it returns the tool name and arguments instead of a direct answer.
  • Execute the tool locally using execute_tool and capture the result.
  • Send the result back to Gemini as a function_result with previous_interaction_id to chain the conversation. Gemini then synthesizes the tool output into a natural-language answer.

If Gemini does not request a tool, it answers directly and the loop skips the execute/return steps entirely.

Add the run_agent function

This function implements the full agentic loop. It takes a user query, sends it to Gemini, handles tool calls if any, and returns the final synthesized response.

  • Below your execute_tool function in agent.py, add the run_agent function by copying this code:
def run_agent(user_query: str) -> str:
    print(f"\n{'='*50}")
    print(f"You: {user_query}")
    print("-" * 50)

    interaction = client.interactions.create(
        model=MODEL,
        input=user_query,
        tools=tools,
    )

    fc_step = None
    for step in interaction.steps:
        if step.type == "function_call":
            fc_step = step
            break

    if fc_step:
        print(f"[Agent] Calling tool: {fc_step.name}({fc_step.arguments})")
        result = execute_tool(fc_step.name, fc_step.arguments)
        print(f"[Agent] Tool result: {result}")

        final_interaction = client.interactions.create(
            model=MODEL,
            input=[
                {
                    "type": "function_result",
                    "name": fc_step.name,
                    "call_id": fc_step.id,
                    "result": [{"type": "text", "text": result}],
                }
            ],
            tools=tools,
            previous_interaction_id=interaction.id,
        )
        response = final_interaction.output_text
    else:
        response = interaction.output_text

    print(f"\nAgent: {response}")
    return response

What does this code do?

  • client.interactions.create() sends the user query to Gemini along with your tool declarations.
  • The for step in interaction.steps loop searches for a function_call step. If found, Gemini wants to use a tool.
  • execute_tool() runs the requested tool locally and captures the result as a JSON string.
  • A second client.interactions.create() call sends the function_result back to Gemini. The previous_interaction_id parameter chains this response to the original conversation so Gemini has full context.
  • The else branch handles questions Gemini can answer directly without tools. It simply grabs interaction.output_text as the final response.
  • Save agent.py.
  • Test your agent by running this command:
python -c "from agent import run_agent; run_agent('What is 847 * 23?')"

You should see the agent detect the calculate tool call, execute it locally, send the result back, and print a final natural-language answer with the value 19481.

Not seeing any output?

  • Make sure your virtual environment is still active. You should see (.venv) at the start of your terminal prompt.
  • Confirm your GEMINI_API_KEY environment variable is still set in this terminal session. If you opened a new terminal, you need to export it again.
  • Check that you deleted the old test code at the bottom of the file. If both the old code and the new functions exist, you may see unexpected behavior.

Help me debug my run_agent function

✔️ Awesome, I've got everything!

Great. Double check you have saved agent.py before moving on.

ⓧ I'd like to double check the full code

import json
import os
from google import genai

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

MODEL = "gemini-3-flash-preview"

calculator_tool = {
    "type": "function",
    "name": "calculate",
    "description": "Performs a mathematical calculation. Use this for any arithmetic, algebra, or numeric computation.",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate, e.g. '2 + 2' or '847 * 23'",
            },
        },
        "required": ["expression"],
    },
}

dictionary_tool = {
    "type": "function",
    "name": "lookup_word",
    "description": "Looks up the definition of an English word.",
    "parameters": {
        "type": "object",
        "properties": {
            "word": {
                "type": "string",
                "description": "The English word to define",
            },
        },
        "required": ["word"],
    },
}

tools = [calculator_tool, dictionary_tool]

DEFINITIONS = {
    "serendipity": "The occurrence of events by chance in a happy or beneficial way.",
    "ephemeral": "Lasting for a very short time.",
    "ubiquitous": "Present, appearing, or found everywhere.",
    "pragmatic": "Dealing with things sensibly and realistically.",
    "eloquent": "Fluent or persuasive in speaking or writing.",
}


def execute_tool(name: str, arguments: dict) -> str:
    if name == "calculate":
        try:
            allowed_chars = set("0123456789+-*/.() ")
            expression = arguments["expression"]
            if not all(c in allowed_chars for c in expression):
                return json.dumps({"error": "Invalid characters in expression"})
            result = eval(expression)
            return json.dumps({"result": str(result)})
        except Exception as e:
            return json.dumps({"error": f"Calculation failed: {e}"})
    elif name == "lookup_word":
        word = arguments["word"].lower().strip()
        definition = DEFINITIONS.get(word, f"No definition found for '{word}'.")
        return json.dumps({"word": word, "definition": definition})
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})


def run_agent(user_query: str) -> str:
    print(f"\n{'='*50}")
    print(f"You: {user_query}")
    print("-" * 50)

    interaction = client.interactions.create(
        model=MODEL,
        input=user_query,
        tools=tools,
    )

    fc_step = None
    for step in interaction.steps:
        if step.type == "function_call":
            fc_step = step
            break

    if fc_step:
        print(f"[Agent] Calling tool: {fc_step.name}({fc_step.arguments})")
        result = execute_tool(fc_step.name, fc_step.arguments)
        print(f"[Agent] Tool result: {result}")

        final_interaction = client.interactions.create(
            model=MODEL,
            input=[
                {
                    "type": "function_result",
                    "name": fc_step.name,
                    "call_id": fc_step.id,
                    "result": [{"type": "text", "text": result}],
                }
            ],
            tools=tools,
            previous_interaction_id=interaction.id,
        )
        response = final_interaction.output_text
    else:
        response = interaction.output_text

    print(f"\nAgent: {response}")
    return response

Your agent can now reason about tools, execute them, and synthesize answers. Next up, you will wrap this in an interactive CLI so you can have a real conversation with your agent.

Build the Interactive CLI

Your agent can already receive a query, decide whether to call a tool, execute it, and synthesize a response. The missing piece is letting a human actually talk to it.

In this step, you'll add an interactive command-line interface so you can type questions and watch the agent reason in real time. You'll also add error handling to keep the agent running even when the API hiccups.

In this step, get ready to:
  • Add a while loop that reads user input and sends it to your agent.
  • Test the agent with tool-triggering and direct queries.
  • Add error handling so the CLI doesn't crash on API failures.
Add a while True input loop

Right now, run_agent() handles a single query and exits. You need a loop that keeps asking for input until the user decides to quit.

  • In agent.py, scroll to the very bottom of the file (after the run_agent function).
  • Add the interactive loop by pasting this code at the end of the file:
if __name__ == "__main__":
    print("AI Research Assistant")
    print("Type 'quit' to exit.\n")

    while True:
        query = input("You: ")
        if query.lower().strip() in ("quit", "exit", "q"):
            print("Goodbye!")
            break
        if not query.strip():
            continue
        run_agent(query)

What does this code do?

if __name__ == "__main__": ensures this code only runs when you execute the file directly (not when importing it as a module).

The while True loop runs forever until the user types quit, exit, or q. Empty inputs are skipped with continue so the agent isn't called on blank lines.

  • Save agent.py.
  • Run your agent by executing this command in your terminal:
python agent.py
  • You should see the banner AI Research Assistant followed by a You: prompt waiting for your input.
  • Type quit and press Enter to exit. You should see Goodbye! printed.

Not seeing the You: prompt?

  • Make sure the if __name__ block is at the top level of the file (no indentation). It should not be inside any function.
  • Check that you saved the file before running the command.
  • Confirm your virtual environment is still active. You should see (.venv) at the start of your terminal prompt.

Help me debug why my agent.py isn't showing the input prompt when I run it.

Test with tool-triggering and direct queries

The real test of your agentic loop is watching Gemini decide when to use a tool and when to answer directly. Some questions require a tool call. Others don't.

  • Run your agent again by executing:
python agent.py
  • At the You: prompt, type What is 847 * 23? and press Enter.

You should see the agent call the calculate tool, display the result, and then print a natural language answer with the number 19481.

  • Now type Define ephemeral and press Enter.

This time the agent calls lookup_word and returns the definition from your DEFINITIONS dictionary.

  • Finally, type What is Python? and press Enter.

Notice that no [Agent] Calling tool: line appears. Gemini answered directly because no tool was needed. This is the agentic loop in action. The model decides when tools add value.

Why does the agent sometimes skip tools?

Gemini reads your tool declarations and decides whether any tool is relevant to the current query. General knowledge questions like "What is Python?" don't match any tool's description, so the model responds using its own training data instead.

This selective tool use is what makes an agent different from a hardcoded script. The model reasons about each query independently.

  • Type quit to exit the agent.
Add error handling for API failures

Your loop works, but if the Gemini API returns an error or you press Ctrl+C, the script crashes with a stack trace. Wrapping the loop body in a try/except block catches those failures gracefully.

  • In agent.py, find the while True: block inside if __name__ == "__main__":.
  • Replace the entire while True: block (everything from while True: to run_agent(query)) with this version that wraps the body in a try/except:
    while True:
        try:
            query = input("You: ")
            if query.lower().strip() in ("quit", "exit", "q"):
                print("Goodbye!")
                break
            if not query.strip():
                continue
            run_agent(query)
        except KeyboardInterrupt:
            print("\nGoodbye!")
            break
        except Exception as e:
            print(f"[Error] {e}")

What does this error handling do?

  • except KeyboardInterrupt catches when you press Ctrl+C. Instead of a stack trace, it prints a clean goodbye message and exits.
  • except Exception as e catches everything else: network timeouts, API rate limits, invalid responses. It prints the error and loops back to the prompt so you can keep going.
  • Save agent.py.
  • Run the agent again:
python agent.py
  • Press Ctrl+C while at the You: prompt. You should see Goodbye! printed cleanly instead of a traceback.

Still seeing a traceback on Ctrl+C?

  • Make sure the try: line is directly inside while True: with one level of indentation. The except blocks must align with try:.
  • Check that KeyboardInterrupt is spelled exactly (capital K, capital I, no spaces).

Help me fix my try/except block so Ctrl+C exits cleanly.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here is the complete agent.py at this point:

import json
import os
from google import genai

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

MODEL = "gemini-3-flash-preview"

calculator_tool = {
    "type": "function",
    "name": "calculate",
    "description": "Performs a mathematical calculation. Use this for any arithmetic, algebra, or numeric computation.",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate, e.g. '2 + 2' or '847 * 23'",
            },
        },
        "required": ["expression"],
    },
}

dictionary_tool = {
    "type": "function",
    "name": "lookup_word",
    "description": "Looks up the definition of an English word.",
    "parameters": {
        "type": "object",
        "properties": {
            "word": {
                "type": "string",
                "description": "The English word to define",
            },
        },
        "required": ["word"],
    },
}

tools = [calculator_tool, dictionary_tool]

DEFINITIONS = {
    "serendipity": "The occurrence of events by chance in a happy or beneficial way.",
    "ephemeral": "Lasting for a very short time.",
    "ubiquitous": "Present, appearing, or found everywhere.",
    "pragmatic": "Dealing with things sensibly and realistically.",
    "eloquent": "Fluent or persuasive in speaking or writing.",
}


def execute_tool(name: str, arguments: dict) -> str:
    if name == "calculate":
        try:
            allowed_chars = set("0123456789+-*/.() ")
            expression = arguments["expression"]
            if not all(c in allowed_chars for c in expression):
                return json.dumps({"error": "Invalid characters in expression"})
            result = eval(expression)
            return json.dumps({"result": str(result)})
        except Exception as e:
            return json.dumps({"error": f"Calculation failed: {e}"})
    elif name == "lookup_word":
        word = arguments["word"].lower().strip()
        definition = DEFINITIONS.get(word, f"No definition found for '{word}'.")
        return json.dumps({"word": word, "definition": definition})
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})


def run_agent(user_query: str) -> str:
    print(f"\n{'='*50}")
    print(f"You: {user_query}")
    print("-" * 50)

    interaction = client.interactions.create(
        model=MODEL,
        input=user_query,
        tools=tools,
    )

    fc_step = None
    for step in interaction.steps:
        if step.type == "function_call":
            fc_step = step
            break

    if fc_step:
        print(f"[Agent] Calling tool: {fc_step.name}({fc_step.arguments})")
        result = execute_tool(fc_step.name, fc_step.arguments)
        print(f"[Agent] Tool result: {result}")

        final_interaction = client.interactions.create(
            model=MODEL,
            input=[
                {
                    "type": "function_result",
                    "name": fc_step.name,
                    "call_id": fc_step.id,
                    "result": [{"type": "text", "text": result}],
                }
            ],
            tools=tools,
            previous_interaction_id=interaction.id,
        )
        response = final_interaction.output_text
    else:
        response = interaction.output_text

    print(f"\nAgent: {response}")
    return response


if __name__ == "__main__":
    print("AI Research Assistant")
    print("Type 'quit' to exit.\n")

    while True:
        try:
            query = input("You: ")
            if query.lower().strip() in ("quit", "exit", "q"):
                print("Goodbye!")
                break
            if not query.strip():
                continue
            run_agent(query)
        except KeyboardInterrupt:
            print("\nGoodbye!")
            break
        except Exception as e:
            print(f"[Error] {e}")

Your AI agent is complete. You have a working interactive CLI that reads user queries, lets Gemini decide whether to call a tool, executes the tool locally, and synthesizes a final response. This is the agentic loop pattern running end to end on your machine.

Secret mission

Multi-Tool Chaining

Your agent can call one tool at a time. But what happens when a question requires multiple tools in sequence? In this mission, you'll add a unit converter and teach your agent to chain tool calls automatically.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the Gemini API free tier, so there are no ongoing costs. Decide whether to keep your resources available, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop (contains agent.py and your .venv virtual environment).
  • Google AI Studio API key (free tier, no billing attached).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your agent or building new tools.

  • Your API key remains active on the free tier with rate limits. No charges will occur.
  • Your project folder stays on your Desktop, ready to run anytime you set the GEMINI_API_KEY environment variable in a new terminal session.
  • The .venv virtual environment remains intact. Reactivate it with source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows) next time you want to run the agent.

Pause - I'll come back to this later

Shut down running processes to free up your terminal. Your files and API key stay in place for next time.

  • If your agent is still running, press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate
  • Close the terminal window. The GEMINI_API_KEY environment variable only exists in that session, so it disappears when you close the terminal.
  • Your project files and API key are preserved. When you return, open a new terminal, navigate to your project folder, reactivate the virtual environment, and re-export your API key.

Delete - I don't want to use this again

Remove all project resources and revoke your API key. This cannot be undone.

  • Delete your project folder from the Desktop. On macOS/Linux, run:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
  • On Windows, run:
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • Delete your Gemini API key. Open the Google Cloud API Credentials page at https://console.cloud.google.com/apis/credentials.
  • Find the API key you created for this project and click the Actions icon (three dots).
  • Select Delete API key.
  • In the confirmation modal, click Delete. The deletion takes a few minutes to propagate.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional AI agent from scratch using Python and the Gemini API free tier.

You've learned how to:

  • Implement the agentic loop pattern from scratch, the core architecture behind production AI systems at FAANG companies.
  • Create custom tool declarations with JSON schema and connect them to an LLM through function calling.
  • Handle the full function calling lifecycle: tool definition, call detection, local execution, and result synthesis.
  • Secret Mission: Extended the agent to handle multi-tool chaining, proving you can architect complex AI workflows where the model calls multiple tools in sequence to answer a single question.

Ready to quiz yourself?