Build an AI Agent From Scratch

Build a ReAct-pattern AI agent in Python with tool use and a reasoning loop.

Introduction

30 Second Summary

Every time you ask ChatGPT to look something up or run a calculation, an AI agent is deciding which tool to use, calling it, reading the result, and figuring out what to do next. That loop of reasoning and acting is the core of every agent framework out there.

In this project, you will build an AI agent from scratch in Python that can reason about tasks, call tools, and solve multi-step problems on its own. By the end, you'll understand exactly what frameworks like LangChain and CrewAI abstract away.

What You'll Build

A command-line AI agent powered by Google's Gemini LLM that autonomously solves problems by reasoning, calling tools, observing results, and deciding when it's done.

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

  • A complete agent loop that handles multi-step reasoning and tool execution in about 80 lines of Python.
  • Custom tool functions with proper schemas that the LLM can invoke autonomously.
  • A working memory system (message history) and explicit termination control so your agent knows when to stop.
  • Secret Mission: Add a Wikipedia search tool that lets your agent answer factual questions by fetching real data from the web.

Are there any prerequisites?

You'll need basic Python knowledge (functions, loops, and dictionaries). No prior experience with AI APIs or agent frameworks is required.

Define Your Agent's Tools

Your environment is ready with Python, the google-genai SDK, and your API key. Now it's time to give your agent something to work with: tools.

Tools are Python functions that the LLM can choose to call when it needs to perform a specific action. In this step, you'll define three tools that give your agent the ability to do math, check the time, and decide when a task is finished.

In this step, get ready to:
  • Create the agent.py file and configure the Gemini client.
  • Define a calculator function that safely evaluates math expressions.
  • Define time and finish functions, then register all tools.
Create the calculator tool

Every tool your agent uses is a plain Python function with type hints and a docstring. The google-genai SDK reads the function's signature and docstring to automatically generate the tool schema that the model sees. No manual JSON schema writing required.

  • Create a new file called agent.py in your project folder.
  • Add the following imports and configuration to the top of the file:
import os
import json
from datetime import datetime
from google import genai
from google.genai import types

# --- Configuration ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.5-flash"

What does this code do?

The imports bring in everything you'll need across the project: os to read your API key from the environment, json for data handling, datetime for the time tool, and genai plus types from the Google SDK.

The client connects to Gemini using your API key, and MODEL stores the model name so you can change it in one place later.

  • Save agent.py.
  • Verify the imports work by running this in your terminal:
python agent.py

You should see no output and no errors. That confirms your imports and client configuration are valid.

Seeing a ModuleNotFoundError?

If you see ModuleNotFoundError: No module named 'google', the SDK isn't installed in the Python environment you're running. Make sure you run pip install google-genai in the same terminal session.

If you see an error about the API key, confirm your GEMINI_API_KEY environment variable is set in the same terminal window where you run the script.

Still stuck? Help me debug my agent.py imports

Now let's add your first tool. The calculator gives your agent the ability to evaluate math expressions safely.

  • In agent.py, add the following function below the configuration section:
# --- Tool Functions ---

def calculator(expression: str) -> str:
    """Evaluates a mathematical expression and returns the result.

    Args:
        expression: A math expression to evaluate, e.g. '(1847 * 23) + (12 / 4)'
    """
    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 str(result)
    except Exception as e:
        return f"Error: {e}"

What does this code do?

The calculator function takes a math expression as a string and evaluates it. Before running eval(), it checks that the input only contains safe characters (digits and basic math operators). This prevents the agent from accidentally running arbitrary Python code.

Notice the detailed docstring with the Args: section. The SDK reads this docstring to tell the model what the tool does and what arguments it expects. Better docstrings mean better tool calls from the model.

  • Save agent.py.
  • Test the calculator by running:
python -c "from agent import calculator; print(calculator('2 + 2'))"

You should see 4 printed in your terminal.

Seeing an error instead of 4?

Check that the function is pasted below the configuration section with no indentation errors. Python is sensitive to whitespace, so the function definition should start at column 0.

If you see a syntax error, make sure the triple-quoted docstring is closed properly with three double-quotes.

Need help? Help me fix my calculator function

Create the time tool

Your second tool is simpler. It gives the agent awareness of the current date and time, which is useful for any time-based calculations.

  • In agent.py, add this function below the calculator function:
def get_current_time() -> str:
    """Returns the current date and time in ISO format."""
    return datetime.now().isoformat()

What does this code do?

This function takes no arguments and returns the current timestamp. The model can call it whenever it needs to know what time it is. Notice that even a function with no parameters needs a clear docstring so the model knows when to use it.

  • Save agent.py.
  • Test the time tool by running:
python -c "from agent import get_current_time; print(get_current_time())"

You should see the current date and time printed in ISO format, something like 2026-06-03T14:30:22.123456.

Seeing a NameError for datetime?

Make sure your import at the top of the file says from datetime import datetime. If you wrote import datetime instead, the call would need to be datetime.datetime.now().

Help me debug my get_current_time function

Create the finish tool and register all tools

Most agent implementations have a subtle problem: how does the agent know when to stop? Without explicit termination, an agent loop can run forever. The finish tool solves this by giving the model a way to signal "I'm done, here's my answer."

  • In agent.py, add the finish function below get_current_time:
def finish(answer: str) -> str:
    """Call this tool when the task is complete with your final answer.

    Args:
        answer: The final answer to return to the user.
    """
    return answer

What does this code do?

The finish function simply passes through whatever answer the model provides. Its power isn't in the logic. It's in the docstring that tells the model: "use this when you're done." Your agent loop (built in the next step) will check for this specific tool call to know when to stop iterating.

Now you need a way to look up tools by name when the model calls them. A registry maps tool names to their functions.

  • Add the tool registry below your finish function:
# --- Tool Registry ---
TOOLS = {
    "calculator": calculator,
    "get_current_time": get_current_time,
    "finish": finish,
}

TOOL_FUNCTIONS = [calculator, get_current_time, finish]

Why two collections?

TOOLS is a dictionary that maps tool names (strings) to functions. When the model says "call calculator", your loop will use this dict to find the right function to execute.

TOOL_FUNCTIONS is a list of the function objects themselves. You'll pass this list to the Gemini API so it can generate schemas from each function's type hints and docstrings.

  • Save agent.py.
  • Confirm everything works together by running:
python -c "from agent import TOOLS; print(list(TOOLS.keys()))"

You should see ['calculator', 'get_current_time', 'finish'] printed, confirming all three tools are registered.

Seeing a NameError for one of the functions?

Make sure the TOOLS dictionary and TOOL_FUNCTIONS list are placed BELOW all three function definitions. Python reads files top to bottom, so referencing a function before it's defined causes a NameError.

Help me fix my tool registry

✔️ Awesome, I've got everything!

Great. Double check you've saved agent.py with all three tool functions and the registry at the bottom.

ⓧ I'd like to double check the full code

Here's what your complete agent.py should look like at this point:

import os
import json
from datetime import datetime
from google import genai
from google.genai import types

# --- Configuration ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.5-flash"

# --- Tool Functions ---

def calculator(expression: str) -> str:
    """Evaluates a mathematical expression and returns the result.

    Args:
        expression: A math expression to evaluate, e.g. '(1847 * 23) + (12 / 4)'
    """
    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 str(result)
    except Exception as e:
        return f"Error: {e}"


def get_current_time() -> str:
    """Returns the current date and time in ISO format."""
    return datetime.now().isoformat()


def finish(answer: str) -> str:
    """Call this tool when the task is complete with your final answer.

    Args:
        answer: The final answer to return to the user.
    """
    return answer


# --- Tool Registry ---
TOOLS = {
    "calculator": calculator,
    "get_current_time": get_current_time,
    "finish": finish,
}

TOOL_FUNCTIONS = [calculator, get_current_time, finish]

Your agent now has three capabilities: math, time awareness, and the ability to decide when it's done. Next up, you'll build the reasoning loop that lets the agent actually use these tools autonomously.

Build the Agent Loop

Your agent now has three tools it can use: a calculator, a time checker, and a finish signal. But tools alone don't make an agent.

In this step, you'll build the core reasoning loop. This is the engine that sends a question to the LLM, checks if it wants to call a tool, executes that tool, and feeds the result back so the LLM can decide what to do next.

In this step, get ready to:
  • Configure the Gemini client for manual function calling.
  • Build the loop that drives the Reason-Act-Observe cycle.
  • Add a safety guard that prevents infinite loops.
Set up the Gemini client and configure function calling

The google-genai SDK defaults to automatic function calling, where the SDK handles the entire tool-calling loop behind the scenes. You'd call generate_content() once and get back a final text answer with no visibility into what happened.

To learn how agents actually work, you need to disable that automation and drive the loop yourself.

  • In agent.py, find the TOOL_FUNCTIONS list at the bottom of the file, then add the following code below it:
# --- Agent Loop ---

def run_agent(user_message: str, max_iterations: int = 10) -> str:
    """Run the agent loop until completion or max iterations."""
    print(f"\n{'='*50}")
    print(f"User: {user_message}")
    print(f"{'='*50}\n")

    config = types.GenerateContentConfig(
        tools=TOOL_FUNCTIONS,
        automatic_function_calling=types.AutomaticFunctionCallingConfig(
            disable=True
        ),
        system_instruction="You are a helpful assistant. Use your tools to solve problems step by step. When you have the final answer, use the finish tool to return it.",
    )

    contents = [types.Content(role="user", parts=[types.Part.from_text(text=user_message)])]

What does this code do?

  • run_agent is a function that takes a user's question and a safety limit (max_iterations) for how many loop cycles the agent can run.
  • config tells Gemini which tools are available and disables automatic function calling so YOU control the loop.
  • system_instruction gives the model a persona and tells it to always use the finish tool when done.
  • contents is the conversation memory. It starts with just the user's message and grows as the agent acts.

Why disable automatic function calling?

With automatic calling enabled, the SDK runs the entire tool-use loop internally. You'd get a final answer with zero visibility into the agent's reasoning steps.

By disabling it, the model returns a function_call object in its response instead of executing the tool itself. Your code then decides what to do with that request. This is exactly how frameworks like LangChain work under the hood.

  • Save agent.py.
  • You won't see a change yet. The next chunk builds the loop that uses this config.
Build the while loop that drives the agent

This is the heart of your agent. Each iteration of the loop is one Reason-Act-Observe cycle: the model reasons about what to do, acts by calling a tool, and observes the result before deciding its next move.

  • In agent.py, add the following code directly below the contents = [...] line you just wrote, still inside the run_agent function:
    for iteration in range(max_iterations):
        print(f"--- Iteration {iteration + 1} ---")

        response = client.models.generate_content(
            model=MODEL,
            contents=contents,
            config=config,
        )

        # Check if the model wants to call a function
        if response.candidates[0].content.parts[0].function_call:
            fc = response.candidates[0].content.parts[0].function_call
            tool_name = fc.name
            tool_args = dict(fc.args) if fc.args else {}

            print(f"Tool call: {tool_name}({tool_args})")

            # Check for termination
            if tool_name == "finish":
                print(f"\nAgent finished: {tool_args.get('answer', '')}")
                return tool_args.get("answer", "")

            # Execute the tool
            tool_fn = TOOLS.get(tool_name)
            if tool_fn:
                result = tool_fn(**tool_args)
            else:
                result = f"Error: Unknown tool '{tool_name}'"

            print(f"Result: {result}\n")

            # Add assistant message and tool result to history
            contents.append(response.candidates[0].content)
            contents.append(
                types.Content(
                    role="user",
                    parts=[
                        types.Part.from_function_response(
                            name=tool_name,
                            response={"result": result},
                        )
                    ],
                )
            )
        else:
            # Model returned text without a tool call
            text = response.candidates[0].content.parts[0].text
            print(f"\nAgent response: {text}")
            return text

    return "Error: Max iterations reached without finishing."

What does this loop do?

Each iteration follows the same pattern:

  • Send the full conversation history to Gemini via generate_content().
  • Check if the response contains a function_call. If yes, extract the tool name and arguments.
  • If the tool is finish, the agent is done. Return the final answer immediately.
  • Otherwise, look up the matching function in TOOLS, execute it, and append both the assistant's message and the tool result to contents (memory).
  • If the model responds with plain text (no tool call), return that text as the answer.

Why append results back to contents?

The contents list is your agent's memory. By appending the assistant's tool call and the tool's result, the model can see what it already tried on the next iteration. Without this, the agent would forget its previous actions and repeat them forever.

  • Save agent.py.
  • You won't see output yet. In the next step, you'll run the agent and watch the loop in action.

Seeing indentation errors?

  • The entire for loop must be indented one level inside run_agent (4 spaces). The if/else inside the loop needs 8 spaces total.
  • Make sure the final return "Error: Max iterations reached without finishing." line is at 4 spaces (inside the function, outside the loop).
  • Check that you haven't mixed tabs and spaces. Your editor should use 4 spaces per indent level.

Help me fix indentation in my agent loop.

Understand the max iterations guard

Without a safety limit, a confused model could loop forever, calling tools in circles without reaching a final answer. This is called the "infinite loop problem" in agent design.

Your agent has three exit paths that prevent this:

  • The finish tool. The model calls this when it has a final answer. This is explicit termination.
  • A plain text response. If the model decides to answer without calling any tool, the loop returns that text directly.
  • The max_iterations guard. If neither of the above happens within 10 cycles, the loop stops and returns an error message. This is your safety net.

What is the ReAct pattern?

ReAct stands for Reason + Act. The model reasons about what to do (picks a tool and arguments), acts (calls the tool), and observes the result. Then it reasons again based on what it learned.

Your for loop IS the ReAct pattern. Each iteration is one Reason-Act-Observe cycle. The contents list accumulates observations so the model can reason about everything it has learned so far.

✔️ 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 so far. Compare your code against this:

import os
import json
from datetime import datetime
from google import genai
from google.genai import types

# --- Configuration ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.5-flash"

# --- Tool Functions ---

def calculator(expression: str) -> str:
    """Evaluates a mathematical expression and returns the result.

    Args:
        expression: A math expression to evaluate, e.g. '(1847 * 23) + (12 / 4)'
    """
    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 str(result)
    except Exception as e:
        return f"Error: {e}"


def get_current_time() -> str:
    """Returns the current date and time in ISO format."""
    return datetime.now().isoformat()


def finish(answer: str) -> str:
    """Call this tool when the task is complete with your final answer.

    Args:
        answer: The final answer to return to the user.
    """
    return answer


# --- Tool Registry ---
TOOLS = {
    "calculator": calculator,
    "get_current_time": get_current_time,
    "finish": finish,
}

TOOL_FUNCTIONS = [calculator, get_current_time, finish]

# --- Agent Loop ---

def run_agent(user_message: str, max_iterations: int = 10) -> str:
    """Run the agent loop until completion or max iterations."""
    print(f"\n{'='*50}")
    print(f"User: {user_message}")
    print(f"{'='*50}\n")

    config = types.GenerateContentConfig(
        tools=TOOL_FUNCTIONS,
        automatic_function_calling=types.AutomaticFunctionCallingConfig(
            disable=True
        ),
        system_instruction="You are a helpful assistant. Use your tools to solve problems step by step. When you have the final answer, use the finish tool to return it.",
    )

    contents = [types.Content(role="user", parts=[types.Part.from_text(text=user_message)])]

    for iteration in range(max_iterations):
        print(f"--- Iteration {iteration + 1} ---")

        response = client.models.generate_content(
            model=MODEL,
            contents=contents,
            config=config,
        )

        # Check if the model wants to call a function
        if response.candidates[0].content.parts[0].function_call:
            fc = response.candidates[0].content.parts[0].function_call
            tool_name = fc.name
            tool_args = dict(fc.args) if fc.args else {}

            print(f"Tool call: {tool_name}({tool_args})")

            # Check for termination
            if tool_name == "finish":
                print(f"\nAgent finished: {tool_args.get('answer', '')}")
                return tool_args.get("answer", "")

            # Execute the tool
            tool_fn = TOOLS.get(tool_name)
            if tool_fn:
                result = tool_fn(**tool_args)
            else:
                result = f"Error: Unknown tool '{tool_name}'"

            print(f"Result: {result}\n")

            # Add assistant message and tool result to history
            contents.append(response.candidates[0].content)
            contents.append(
                types.Content(
                    role="user",
                    parts=[
                        types.Part.from_function_response(
                            name=tool_name,
                            response={"result": result},
                        )
                    ],
                )
            )
        else:
            # Model returned text without a tool call
            text = response.candidates[0].content.parts[0].text
            print(f"\nAgent response: {text}")
            return text

    return "Error: Max iterations reached without finishing."

Your agent loop is complete. In the next step, you'll wire it to a real question that requires multiple tool calls and watch the Reason-Act-Observe cycle play out in your terminal.

Wire It All Together and Run Your Agent

Your agent loop is fully built. It can send messages to the model, detect tool calls, execute the right function, and feed results back into the conversation. But right now it's just a function definition sitting in a file with no way to actually fire.

In this step, you'll add an entry point that launches the agent with a real multi-step question. Then you'll run it and watch the Thought/Action/Observation cycle play out live in your terminal.

In this step, get ready to:
  • Understand how tool results connect back into conversation memory.
  • Add an entry point that runs the agent with a multi-step question.
  • Watch the agent reason, call tools, and reach a final answer.
Connect the tool execution results back into the conversation history

Before you add the entry point, take a moment to understand the most important pattern in your agent: how tool results loop back into the conversation. This is what gives your agent memory.

  • In agent.py, find these lines inside your run_agent function's loop:
            # Add assistant message and tool result to history
            contents.append(response.candidates[0].content)
            contents.append(
                types.Content(
                    role="user",
                    parts=[
                        types.Part.from_function_response(
                            name=tool_name,
                            response={"result": result},
                        )
                    ],
                )
            )

Why does this create memory?

Every iteration, the contents list grows with two new entries: the model's tool call (what it decided to do) and the tool's result (what happened). On the next iteration, the model sees the entire history and can reason about what to do next.

Without this pattern, the model would forget what it already tried. Memory is what turns a single LLM call into a multi-step agent.

What is the ReAct pattern?

Your agent follows the ReAct pattern: Reason + Act. Each iteration, the model reasons about what to do (picks a tool and arguments), acts (the tool runs), and observes the result (fed back via memory). This cycle repeats until the model calls finish.

Run the agent with a multi-step question that requires both tools

Your agent needs an entry point so you can actually run it from the terminal. You'll give it a question that requires multiple tool calls to answer, forcing the agent through several iterations of the loop.

  • In agent.py, scroll to the very bottom of the file (after the run_agent function) and add this code:

# --- Run It ---
if __name__ == "__main__":
    result = run_agent(
        "What is the current hour multiplied by 7, plus 42? Show your work."
    )
    print(f"\nFinal answer: {result}")

What does this code do?

  • if __name__ == "__main__": makes this block run only when you execute the file directly (not when importing it as a module).
  • The question requires the agent to first call get_current_time to learn the current hour, then call calculator to do the math, then call finish to return the answer.
  • This forces the agent through at least 3 iterations of the loop, proving that memory and multi-step reasoning work.
  • Save agent.py.
Observe the Thought/Action/Observation cycle in your terminal output

Time to see your agent in action. When you run it, watch the terminal output carefully. Each iteration shows you exactly what the model decided to do and what happened.

  • Run your agent by executing this command in your terminal:
python agent.py

You should see output showing the agent cycling through multiple iterations. It will look something like this:

  • Iteration 1: The agent calls get_current_time and sees the current date/time in ISO format.
  • Iteration 2: The agent calls calculator with an expression that multiplies the hour by 7 and adds 42.
  • Iteration 3: The agent calls finish with the final answer, ending the loop.

What just happened?

You just watched an autonomous AI agent reason through a multi-step problem. The model decided which tools to use, in what order, and when it had enough information to finish. You didn't hard-code those steps. The LLM figured them out on its own using your loop and memory system.

Seeing an error or no output?

  • If you see a GEMINI_API_KEY error, make sure your environment variable is set in the same terminal session where you run the script.
  • If the agent seems stuck or you see a rate limit error, wait 60 seconds and try again. The free tier has per-minute limits.
  • If you see a syntax error, make sure the if __name__ block is at the top level of the file (not indented inside the run_agent function).

Help me debug my AI agent output

✔️ Awesome, I've got everything!

Great. Double check you've saved agent.py and that your agent ran successfully.

ⓧ I'd like to double check the full code

Here is the complete agent.py file. Compare it against yours to spot any differences.

import os
import json
from datetime import datetime
from google import genai
from google.genai import types

# --- Configuration ---
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.5-flash"

# --- Tool Functions ---

def calculator(expression: str) -> str:
    """Evaluates a mathematical expression and returns the result.

    Args:
        expression: A math expression to evaluate, e.g. '(1847 * 23) + (12 / 4)'
    """
    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 str(result)
    except Exception as e:
        return f"Error: {e}"


def get_current_time() -> str:
    """Returns the current date and time in ISO format."""
    return datetime.now().isoformat()


def finish(answer: str) -> str:
    """Call this tool when the task is complete with your final answer.

    Args:
        answer: The final answer to return to the user.
    """
    return answer


# --- Tool Registry ---
TOOLS = {
    "calculator": calculator,
    "get_current_time": get_current_time,
    "finish": finish,
}

TOOL_FUNCTIONS = [calculator, get_current_time, finish]

# --- Agent Loop ---

def run_agent(user_message: str, max_iterations: int = 10) -> str:
    """Run the agent loop until completion or max iterations."""
    print(f"\n{'='*50}")
    print(f"User: {user_message}")
    print(f"{'='*50}\n")

    config = types.GenerateContentConfig(
        tools=TOOL_FUNCTIONS,
        automatic_function_calling=types.AutomaticFunctionCallingConfig(
            disable=True
        ),
        system_instruction="You are a helpful assistant. Use your tools to solve problems step by step. When you have the final answer, use the finish tool to return it.",
    )

    contents = [types.Content(role="user", parts=[types.Part.from_text(text=user_message)])]

    for iteration in range(max_iterations):
        print(f"--- Iteration {iteration + 1} ---")

        response = client.models.generate_content(
            model=MODEL,
            contents=contents,
            config=config,
        )

        # Check if the model wants to call a function
        if response.candidates[0].content.parts[0].function_call:
            fc = response.candidates[0].content.parts[0].function_call
            tool_name = fc.name
            tool_args = dict(fc.args) if fc.args else {}

            print(f"Tool call: {tool_name}({tool_args})")

            # Check for termination
            if tool_name == "finish":
                print(f"\nAgent finished: {tool_args.get('answer', '')}")
                return tool_args.get("answer", "")

            # Execute the tool
            tool_fn = TOOLS.get(tool_name)
            if tool_fn:
                result = tool_fn(**tool_args)
            else:
                result = f"Error: Unknown tool '{tool_name}'"

            print(f"Result: {result}\n")

            # Add assistant message and tool result to history
            contents.append(response.candidates[0].content)
            contents.append(
                types.Content(
                    role="user",
                    parts=[
                        types.Part.from_function_response(
                            name=tool_name,
                            response={"result": result},
                        )
                    ],
                )
            )
        else:
            # Model returned text without a tool call
            text = response.candidates[0].content.parts[0].text
            print(f"\nAgent response: {text}")
            return text

    return "Error: Max iterations reached without finishing."


# --- Run It ---
if __name__ == "__main__":
    result = run_agent(
        "What is the current hour multiplied by 7, plus 42? Show your work."
    )
    print(f"\nFinal answer: {result}")

Secret mission

Add a Wikipedia Search Tool

Your agent can do math and tell time, but it can't look anything up. What if it could fetch real facts from the web and combine them with calculations to answer questions no single tool could handle alone?

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 with a free-tier API key, so there are no ongoing costs.

Resources you used:

  • agent.py file (your local Python script).
  • google-genai pip package.
  • GEMINI_API_KEY environment variable.
  • Google AI Studio API key.

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your agent.

  • Your agent.py file stays on your machine, ready to run anytime.
  • Your GEMINI_API_KEY environment variable remains set in your current terminal session.
  • The google-genai package stays installed and available for other Python projects.
  • Your Google AI Studio API key remains active on the free tier.

Pause - I'll come back to this later

Shut down running processes and clean up temporary state. Your files and installed packages stay in place for later.

  • Close any terminal sessions you used for this project.
  • Your GEMINI_API_KEY environment variable only persists in the terminal session where you set it. Opening a new terminal requires you to export it again.
  • Your agent.py file and google-genai package remain on your machine.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete your agent.py file by running:
rm agent.py
  • Uninstall the google-genai package by running:
pip uninstall google-genai
  • Remove the environment variable from your current session by running:
unset GEMINI_API_KEY
  • Revoke your API key in Google AI Studio:
  • Go to aistudio.google.com.
  • Click Get API key in the left sidebar.
  • Find your key in the list and click the trash icon to delete it.

Nice Work!

Nice Work!

You just built an AI agent from scratch in Python. Your agent can reason about problems, call tools autonomously, observe results, and decide when it has a final answer.

You've learned how to:

  • Built a complete agent loop using the ReAct pattern (Reason, Act, Observe) to solve multi-step problems.
  • Created custom tool functions with schemas that an LLM can invoke autonomously, and wired up memory (message history) and explicit termination (finish tool).
  • Understood exactly what frameworks like LangChain, CrewAI, and AutoGen abstract away behind their APIs.
  • Added a real web-connected tool (Wikipedia search) that lets your agent fetch live data and combine it with calculation.

Ready to quiz yourself?