Build an Instagram Growth Agent in Python

Build a Python AI agent that analyzes Instagram data and recommends growth strategies.

Introduction

30 Second Summary

You post consistently on Instagram, but your reach is flat. You know the answer is buried somewhere in your analytics, but staring at numbers in a spreadsheet does not turn into a content strategy on its own.

In this project, you will build an AI agent in Python that reads your Instagram performance data, identifies patterns, and gives you actionable growth recommendations. Ask it questions in plain English and it figures out which analysis to run.

What You'll Build

A terminal-based AI agent powered by the Grok API that analyzes Instagram post data and delivers growth insights on demand.

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

  • A working Instagram growth agent that answers questions like "Which hashtags drive the most reach?" and "What content type performs best?".
  • Hands-on experience with the agent loop pattern and function calling using the Grok API.
  • Four custom analysis tools that calculate engagement rates, identify top hashtags, break down reach sources, and detect trending topics.
  • Secret Mission: connect your agent to live Instagram data using an MCP server for real-time insights.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, loops, and dictionaries). No prior AI or machine learning experience is needed. You also do not need an Instagram account to complete this project.

Set Up Your Python Environment

Before you can build an AI agent, you need a working Python environment with the right tools installed.

In this step, you will install Python, create an isolated project folder with its own virtual environment, and install the libraries your agent needs: the OpenAI Python SDK (which connects to Grok's API) and pandas for data analysis.

In this step, get ready to:
  • Verify Python 3.9+ is installed on your machine.
  • Create a project directory and virtual environment.
  • Install the OpenAI SDK, pandas, and python-dotenv.
Check your Python installation

Your agent script requires Python 3.9 or higher. Let's confirm you have it.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.9 or higher

You should see something like Python 3.11.4 or Python 3.12.1. Any version from 3.9 onwards works.

You are all set. Move on to the next substep.

ⓧ I see an older version

If your version is lower than 3.9 (for example Python 3.7.3), you need to install a newer version.

  • Visit python.org/downloads and download the latest Python installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH during installation.
  • Close and reopen your terminal after installation.
  • Run python3 --version again to confirm the new version appears.

macOS default Python

Some macOS versions ship with an older system Python. After installing from python.org, you may need to use the full path (/usr/local/bin/python3) or restart your terminal for the new version to take effect.

ⓧ Command not found

If you see an error like command not found, Python is not installed yet.

  • Visit python.org/downloads and download the latest Python installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH during installation.
  • Close and reopen your terminal after installation.
  • Run python3 --version again. You should now see a version number like Python 3.12.1.

Still seeing command not found?

On Windows, try running python --version (without the 3). Some Windows installs register the binary as python instead of python3.

If neither works, your PATH may not be set correctly. Reinstall Python and ensure the Add Python to PATH checkbox is selected.

ask me for help troubleshooting my Python installation

Create your project directory and virtual environment

A virtual environment is an isolated Python space for your project. It keeps your agent's dependencies separate from other Python projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir instagram-growth-agent
cd instagram-growth-agent
  • Confirm the folder exists by running:
ls

The folder will be empty for now. That is expected.

  • Create a virtual environment inside your project folder by running:
python3 -m venv venv

What does this command do?

python3 -m venv venv tells Python to create a new virtual environment in a folder called venv. This folder contains its own copy of Python and pip, so anything you install stays local to this project.

  • Activate the virtual environment:

macOS

  • Run this command to activate:
source venv/bin/activate

Windows

  • Run this command to activate:
venv\Scripts\activate

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

Don't see (venv) in your prompt?

Make sure you ran the activate command from inside the instagram-growth-agent folder. Run pwd (macOS) or cd (Windows) to check your current location.

On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned first, then try activating again.

help me activate my Python virtual environment

Install your project dependencies

Your agent needs three libraries: the OpenAI SDK to communicate with Grok's API, pandas to analyze the Instagram dataset, and python-dotenv to load your API key from a file.

  • Install all three packages by running this command:
pip install openai pandas python-dotenv

What are these packages?

openai is the SDK that talks to Grok's API. Grok uses an OpenAI-compatible endpoint, so the same library works for both. pandas loads and analyzes CSV data. python-dotenv reads environment variables from a .env file so you never hardcode secrets.

You should see output showing all three packages downloading and installing successfully.

  • Verify the installation by running:
pip show openai pandas python-dotenv

You should see details for all three packages including their names and versions. This confirms everything is installed and ready.

Installation failed?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, go back and run the activate command from the previous substep.

If you see a permissions error, do not use sudo. Instead, deactivate and recreate your virtual environment, then try again.

help me fix my pip install error

Your Python environment is ready to go. Next up, you will get your Grok API key so your agent can communicate with the AI model.

Get Your Claude API Key

Get Your Grok API Key

Your agent needs a way to communicate with Grok, the AI model from xAI. Every request your Python script sends goes through the Grok API, and the API requires a key to identify who is making the call.

In this step, you will create an xAI account, grab your free $25 API credits, generate an API key, and store it safely in a .env file.

In this step, get ready to:
  • Create an xAI account and claim your free credits.
  • Generate an API key.
  • Store the key in a .env file and verify the connection.
Create your xAI account

The xAI Console is where you manage API keys and monitor usage. New accounts receive $25 in free credits with no payment method required.

  • Open your browser and go to console.x.ai.
  • Click Create an account and sign up using your email (you do not need an X/Twitter account).
  • Complete any verification steps that appear during sign-up.
  • Once logged in, your $25 in free API credits should appear on the dashboard.

What are the free credits?

xAI gives every new account $25 in promotional API credits. These expire 30 days after account creation. This project uses the grok-4.3 model at $1.25 per million input tokens. Your $25 covers thousands of agent calls.

Generate an API key

An API key is a unique string that authenticates your script when it sends requests to Grok.

  • In the xAI Console, click API Keys in the left sidebar.
  • Click the button to create a new key.
  • Give your key a name like instagram-agent so you can identify it later.
  • Copy the key immediately and paste it somewhere safe (a password manager or a temporary text file).

Save your key now

xAI only shows the full key once at creation time. If you lose it, you will need to generate a new one. Do not share this key or commit it to version control.

Store your key and verify the connection

Instead of exporting the key in your terminal each time, you will store it in a .env file. The python-dotenv library reads this file automatically when your script starts.

  • Create a file called .env in your instagram-growth-agent folder with the following content (paste your own key in place of the placeholder):
XAI_API_KEY=your-api-key-here

Why a .env file?

Storing your key in a .env file keeps it out of your source code. If you ever push this project to GitHub, add .env to your .gitignore to prevent accidentally sharing it.

  • Create a test script to verify the connection. Create a file called test_connection.py with the following code:
from dotenv import load_dotenv
from openai import OpenAI
import os

# Load the API key from .env
load_dotenv()

# Create a client pointing to Grok's API
client = OpenAI(
    api_key=os.getenv("XAI_API_KEY"),
    base_url="https://api.x.ai/v1"
)

# Send a simple test message to Grok
response = client.chat.completions.create(
    model="grok-4.3",
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
    max_tokens=100
)

# Print the response
print(response.choices[0].message.content)

What does this code do?

This script uses the OpenAI SDK but points it at Grok's API by setting base_url to https://api.x.ai/v1. Grok's API is OpenAI-compatible, so the same library works for both.

If your key is valid, Grok will respond with a short greeting.

  • Run the test script:
python3 test_connection.py

You should see a short greeting from Grok printed in your terminal.

Seeing an authentication error?

  • Make sure your .env file is in the same folder where you run the script (instagram-growth-agent).
  • Double-check that you copied the full key with no extra spaces or missing characters.
  • Confirm your key starts with xai- and has no trailing whitespace.

Help me debug my Grok API authentication error.

Your environment is fully connected to Grok. Next up, you will download the Instagram dataset and load it into your project.

Define Your Agent's Tools

Load Your Instagram Dataset

Your API key is working and Grok can respond to messages. Now you need data for your agent to analyze.

In this step, you will download a real Instagram analytics dataset from Kaggle, place it in your project, and write a Python script that loads and explores the data using pandas.

In this step, get ready to:
  • Download the Instagram Reach dataset from Kaggle.
  • Load the CSV into pandas and explore its structure.
  • Understand what each column means for growth analysis.
Download the dataset

The dataset comes from a real Instagram creator account and contains 13 columns of post-level analytics including reach sources, engagement metrics, and hashtags.

  • Open your browser and go to kaggle.com/datasets/amankumarkharwal/instagram-reach-data.
  • Click Download to get the ZIP file (you may need to create a free Kaggle account).
  • Extract the ZIP file. You should see a file called Instagram data.csv.
  • Move Instagram data.csv into your instagram-growth-agent folder on the Desktop.
  • Confirm the file is in the right place by running:
ls

You should see Instagram data.csv, .env, and test_connection.py listed.

Load and explore the data with pandas

Before your agent can analyze this data, you need to understand its shape. A quick exploration script shows you what columns are available and what the numbers look like.

  • Create a file called explore_data.py with the following code:
import pandas as pd

# Load the Instagram dataset
df = pd.read_csv("Instagram data.csv")

# Show basic info about the dataset
print(f"Dataset shape: {df.shape[0]} posts, {df.shape[1]} columns")
print(f"\nColumns: {list(df.columns)}")
print(f"\nFirst 3 rows:")
print(df.head(3).to_string())
print(f"\nBasic stats:")
print(df[['Impressions', 'Likes', 'Comments', 'Shares', 'Saves']].describe().to_string())

What does this code do?

  • pd.read_csv() loads the CSV file into a pandas DataFrame (a table structure).
  • df.shape tells you how many rows (posts) and columns (metrics) exist.
  • df.describe() calculates summary statistics (mean, min, max) for the engagement columns.
  • Run the exploration script:
python3 explore_data.py

You should see the dataset shape, column names, sample rows, and summary statistics printed in your terminal.

What do these columns mean?

  • Impressions is the total number of times the post was seen (your reach).
  • From Home / From Hashtags / From Explore / From Other shows where that reach came from.
  • Likes, Comments, Shares, Saves are engagement actions people took on each post.
  • Profile Visits, Follows are growth metrics showing how many people visited your profile or followed you.
  • Caption, Hashtags contain the text content and hashtag strategy used on each post.

🙋‍♀️ Seeing a FileNotFoundError?

  • Make sure Instagram data.csv is in the same folder where you run the script. The filename has a space in it.
  • Confirm you extracted the ZIP file. The CSV should not still be inside an archive.
  • Check the exact filename with ls. Kaggle sometimes nests files inside a subfolder.

Help me fix a FileNotFoundError when loading my Instagram CSV

  • Add the tools list below your read_file function in agent.py:
tools = [
    {
        "name": "manage_todo",
        "description": "Manage a to-do list. Can add new tasks, list all tasks, or mark tasks as complete. Use this whenever the user wants to manage their tasks or to-do items.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["add", "list", "complete"],
                    "description": "The action to perform: 'add' to add a new task, 'list' to show all tasks, 'complete' to mark a task as done.",
                },
                "task": {
                    "type": "string",
                    "description": "The task description (required for 'add' action).",
                },
                "task_index": {
                    "type": "integer",
                    "description": "The task number to mark as complete (required for 'complete' action, 1-based index).",
                },
            },
            "required": ["action"],
        },
    },
    {
        "name": "check_weather",
        "description": "Check the current weather for a city. Returns temperature, condition, and humidity. Available cities: New York, London, Tokyo, Sydney.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city to check weather for.",
                }
            },
            "required": ["city"],
        },
    },
    {
        "name": "read_file",
        "description": "Read the contents of a local text file. Use this when the user asks about file contents or needs information from a file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "The name or path of the file to read.",
                }
            },
            "required": ["filename"],
        },
    },
]

What does this code do?

The tools list is what you pass to Claude when making an API call. Claude reads these definitions to understand what tools are available.

  • name must match the Python function you will call when Claude picks this tool.
  • description tells Claude when to use the tool. Write it like you are explaining to a colleague.
  • input_schema defines the parameters using JSON Schema. The required array tells Claude which arguments it must always provide.
  • Optional parameters (like task and task_index) are left out of required so Claude only sends them when relevant to the action.
  • Save agent.py.
  • Verify all three tool definitions loaded correctly by running:
python3 -c "from agent import tools; print(f'{len(tools)} tools defined: {[t[\"name\"] for t in tools]}')"

You should see: 3 tools defined: ['manage_todo', 'check_weather', 'read_file']

Seeing an ImportError or empty list?

  • Check that tools is defined at the module level (not indented inside a function).
  • Ensure there are no missing commas between the three dictionary entries in the list.
  • Look for unmatched braces or brackets. Each tool definition has several nested levels of { and }.

Need help? Help me fix an error when importing my tools list from agent.py

💡 Clean up the test to-do file

The earlier verify command created a todos.json file with a test task. Delete it so you start fresh when your agent runs.

rm todos.json

✔️ Awesome, I've got everything!

Great. Double check you have saved agent.py with all the imports, functions, and the tools list.

ⓧ I'd like to double check the full code

Here is the complete agent.py file so far. Make sure yours matches:

import anthropic
import json
import os

client = anthropic.Anthropic()

TODO_FILE = "todos.json"


def load_todos():
    if os.path.exists(TODO_FILE):
        with open(TODO_FILE, "r") as f:
            return json.load(f)
    return []


def save_todos(todos):
    with open(TODO_FILE, "w") as f:
        json.dump(todos, f, indent=2)


def manage_todo(action, task=None, task_index=None):
    todos = load_todos()

    if action == "add":
        todos.append({"task": task, "done": False})
        save_todos(todos)
        return f"Added task: '{task}'. You now have {len(todos)} task(s)."

    elif action == "list":
        if not todos:
            return "Your to-do list is empty."
        result = "Your to-do list:\n"
        for i, item in enumerate(todos):
            status = "done" if item["done"] else "pending"
            result += f"  {i + 1}. [{status}] {item['task']}\n"
        return result

    elif action == "complete":
        if task_index is None or task_index < 1 or task_index > len(todos):
            return f"Invalid task number. You have {len(todos)} task(s)."
        todos[task_index - 1]["done"] = True
        completed_task = todos[task_index - 1]["task"]
        save_todos(todos)
        return f"Marked task {task_index} as complete: '{completed_task}'"

    return "Unknown action. Use 'add', 'list', or 'complete'."


def check_weather(city):
    weather_data = {
        "new york": {"temp": 72, "condition": "sunny", "humidity": 45},
        "london": {"temp": 58, "condition": "rainy", "humidity": 80},
        "tokyo": {"temp": 68, "condition": "cloudy", "humidity": 60},
        "sydney": {"temp": 75, "condition": "sunny", "humidity": 50},
    }

    city_lower = city.lower()
    if city_lower in weather_data:
        data = weather_data[city_lower]
        return f"Weather in {city}: {data['temp']}°F, {data['condition']}, humidity {data['humidity']}%"
    return f"Weather data not available for '{city}'. Available cities: New York, London, Tokyo, Sydney."


def read_file(filename):
    try:
        with open(filename, "r") as f:
            content = f.read()
        return f"Contents of '{filename}':\n{content}"
    except FileNotFoundError:
        return f"File '{filename}' not found."
    except Exception as e:
        return f"Error reading '{filename}': {str(e)}"


tools = [
    {
        "name": "manage_todo",
        "description": "Manage a to-do list. Can add new tasks, list all tasks, or mark tasks as complete. Use this whenever the user wants to manage their tasks or to-do items.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["add", "list", "complete"],
                    "description": "The action to perform: 'add' to add a new task, 'list' to show all tasks, 'complete' to mark a task as done.",
                },
                "task": {
                    "type": "string",
                    "description": "The task description (required for 'add' action).",
                },
                "task_index": {
                    "type": "integer",
                    "description": "The task number to mark as complete (required for 'complete' action, 1-based index).",
                },
            },
            "required": ["action"],
        },
    },
    {
        "name": "check_weather",
        "description": "Check the current weather for a city. Returns temperature, condition, and humidity. Available cities: New York, London, Tokyo, Sydney.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city to check weather for.",
                }
            },
            "required": ["city"],
        },
    },
    {
        "name": "read_file",
        "description": "Read the contents of a local text file. Use this when the user asks about file contents or needs information from a file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "The name or path of the file to read.",
                }
            },
            "required": ["filename"],
        },
    },
]

You now have real Instagram data loaded and ready for analysis. Next up, you will build the tools your agent uses to extract growth insights from this data.

Build the Agent Loop

Your tools are defined and the Python functions behind them are ready. The missing piece is the brain that ties everything together: the agent loop.

The agent loop is the core pattern behind every AI agent framework. It sends your message to Claude along with the available tools, checks whether Claude wants to call a tool or respond directly, executes any requested tools, and feeds the results back until the task is complete.

In this step, get ready to:
  • Build a tool dispatcher that routes Claude's tool requests to the correct Python function.
  • Implement the agent loop that sends messages, handles tool calls, and knows when to stop.
  • Add an interactive terminal interface to chat with your agent.
Create the tool dispatcher

When Claude decides to use a tool, it sends back a tool name and the input arguments as JSON. Your code needs a function that receives those values and calls the correct Python function.

  • In agent.py, scroll to the bottom of the file (below the tools list) and add the following function:
def execute_tool(tool_name, tool_input):
    if tool_name == "manage_todo":
        return manage_todo(
            action=tool_input["action"],
            task=tool_input.get("task"),
            task_index=tool_input.get("task_index"),
        )
    elif tool_name == "check_weather":
        return check_weather(city=tool_input["city"])
    elif tool_name == "read_file":
        return read_file(filename=tool_input["filename"])
    return f"Unknown tool: {tool_name}"

What does this code do?

This is the dispatcher. It takes the tool name Claude requests and the input arguments, then routes to the matching Python function.

  • tool_input.get("task") uses .get() instead of bracket access because task and task_index are optional parameters that may not be present.
  • The fallback at the end returns an error message if Claude somehow requests a tool that doesn't exist.

You won't see a change yet. The next chunk uses this function inside the agent loop.

Implement the agent loop

The agent loop is where the magic happens. It follows a cycle: send a message to Claude with the available tools, check whether Claude wants to call a tool or is done, execute any tool calls, then feed the results back to Claude for the next iteration.

What is an agent loop?

Every AI agent framework uses the same fundamental cycle: perceive (receive input), plan (decide what to do), act (execute a tool), observe (read the result). This loop repeats until the agent decides it has enough information to respond.

Claude signals its decision through the stop_reason field. A value of tool_use means Claude wants to call a tool. A value of end_turn means Claude is done and ready to give its final answer.

  • Below the execute_tool function you just added, add the run_agent function:
def run_agent(user_message):
    print(f"\nYou: {user_message}")
    print("-" * 40)

    messages = [{"role": "user", "content": user_message}]

    system_prompt = (
        "You are a helpful personal task automation agent. "
        "You can manage to-do lists, check weather, and read files. "
        "Use the available tools to help the user with their requests. "
        "When you have completed the task, provide a clear summary of what you did. "
        "If a task requires multiple steps, complete all steps before responding."
    )

    max_iterations = 10
    iteration = 0

    while iteration < max_iterations:
        iteration += 1

        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1024,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    print(f"\nAgent: {block.text}")
            break

        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    print(f"  [Tool Call] {tool_name}({json.dumps(tool_input)})")

                    result = execute_tool(tool_name, tool_input)
                    print(f"  [Result] {result}")

                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result,
                        }
                    )

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

    if iteration >= max_iterations:
        print("\nAgent: I reached the maximum number of steps. Please try a simpler request.")

What does this code do?

This is the complete agent loop. Here is how each section works:

  • messages stores the conversation history. Claude needs to see the full context (your message, its tool calls, and the results) to decide what to do next.
  • system_prompt tells Claude its role and behaviour. It instructs the agent to complete multi-step tasks before responding.
  • max_iterations = 10 prevents a runaway loop. Without this, a confused agent could call tools forever.
  • response.stop_reason == "end_turn" detects when Claude is done and prints the final answer.
  • response.stop_reason == "tool_use" detects when Claude wants to call a tool. The code executes the tool, then appends both Claude's request and the tool results back into messages so the next iteration has full context.

💡 Why send tool results as a user message?

Claude's API requires tool results to be sent as a user role message containing tool_result content blocks. Each result includes the tool_use_id so Claude can match results to the specific tool call it made.

You won't be able to run the agent yet. The next chunk adds the interactive terminal interface.

Add the interactive terminal interface

The final piece is a simple read-eval-print loop (REPL) that lets you type messages and sends them to the agent.

  • Below the run_agent function, add the main function and entry point:
def main():
    print("Personal Task Automation Agent")
    print("=" * 40)
    print("Type your requests in natural language.")
    print("Type 'quit' to exit.\n")

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


if __name__ == "__main__":
    main()

What does this code do?

  • main() prints a welcome banner, then loops forever waiting for input.
  • Typing quit, exit, or q exits the program gracefully.
  • if __name__ == "__main__": ensures the agent only starts when you run the file directly, not when it is imported by another script.
  • Save agent.py.
  • Test that the file runs without errors by running this command:
python agent.py
  • You should see the welcome banner followed by a You: prompt waiting for your input.
  • Type quit and press **Enter** to exit for now.

Seeing an error when you run the agent?

  • If you see a SyntaxError, check that all parentheses and quotes are closed properly. The system_prompt string uses parentheses for line continuation.
  • If you see an AuthenticationError, make sure your ANTHROPIC_API_KEY environment variable is still set in your current terminal session.
  • If you see ModuleNotFoundError: No module named 'anthropic', make sure your virtual environment is activated.

Help me debug my agent startup error.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the complete agent.py file at the end of this step:

import anthropic
import json
import os

client = anthropic.Anthropic()

TODO_FILE = "todos.json"


def load_todos():
    if os.path.exists(TODO_FILE):
        with open(TODO_FILE, "r") as f:
            return json.load(f)
    return []


def save_todos(todos):
    with open(TODO_FILE, "w") as f:
        json.dump(todos, f, indent=2)


def manage_todo(action, task=None, task_index=None):
    todos = load_todos()

    if action == "add":
        todos.append({"task": task, "done": False})
        save_todos(todos)
        return f"Added task: '{task}'. You now have {len(todos)} task(s)."

    elif action == "list":
        if not todos:
            return "Your to-do list is empty."
        result = "Your to-do list:\n"
        for i, item in enumerate(todos):
            status = "done" if item["done"] else "pending"
            result += f"  {i + 1}. [{status}] {item['task']}\n"
        return result

    elif action == "complete":
        if task_index is None or task_index < 1 or task_index > len(todos):
            return f"Invalid task number. You have {len(todos)} task(s)."
        todos[task_index - 1]["done"] = True
        completed_task = todos[task_index - 1]["task"]
        save_todos(todos)
        return f"Marked task {task_index} as complete: '{completed_task}'"

    return "Unknown action. Use 'add', 'list', or 'complete'."


def check_weather(city):
    weather_data = {
        "new york": {"temp": 72, "condition": "sunny", "humidity": 45},
        "london": {"temp": 58, "condition": "rainy", "humidity": 80},
        "tokyo": {"temp": 68, "condition": "cloudy", "humidity": 60},
        "sydney": {"temp": 75, "condition": "sunny", "humidity": 50},
    }

    city_lower = city.lower()
    if city_lower in weather_data:
        data = weather_data[city_lower]
        return f"Weather in {city}: {data['temp']}°F, {data['condition']}, humidity {data['humidity']}%"
    return f"Weather data not available for '{city}'. Available cities: New York, London, Tokyo, Sydney."


def read_file(filename):
    try:
        with open(filename, "r") as f:
            content = f.read()
        return f"Contents of '{filename}':\n{content}"
    except FileNotFoundError:
        return f"File '{filename}' not found."
    except Exception as e:
        return f"Error reading '{filename}': {str(e)}"


tools = [
    {
        "name": "manage_todo",
        "description": "Manage a to-do list. Can add new tasks, list all tasks, or mark tasks as complete. Use this whenever the user wants to manage their tasks or to-do items.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["add", "list", "complete"],
                    "description": "The action to perform: 'add' to add a new task, 'list' to show all tasks, 'complete' to mark a task as done.",
                },
                "task": {
                    "type": "string",
                    "description": "The task description (required for 'add' action).",
                },
                "task_index": {
                    "type": "integer",
                    "description": "The task number to mark as complete (required for 'complete' action, 1-based index).",
                },
            },
            "required": ["action"],
        },
    },
    {
        "name": "check_weather",
        "description": "Check the current weather for a city. Returns temperature, condition, and humidity. Available cities: New York, London, Tokyo, Sydney.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city to check weather for.",
                }
            },
            "required": ["city"],
        },
    },
    {
        "name": "read_file",
        "description": "Read the contents of a local text file. Use this when the user asks about file contents or needs information from a file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "The name or path of the file to read.",
                }
            },
            "required": ["filename"],
        },
    },
]


def execute_tool(tool_name, tool_input):
    if tool_name == "manage_todo":
        return manage_todo(
            action=tool_input["action"],
            task=tool_input.get("task"),
            task_index=tool_input.get("task_index"),
        )
    elif tool_name == "check_weather":
        return check_weather(city=tool_input["city"])
    elif tool_name == "read_file":
        return read_file(filename=tool_input["filename"])
    return f"Unknown tool: {tool_name}"


def run_agent(user_message):
    print(f"\nYou: {user_message}")
    print("-" * 40)

    messages = [{"role": "user", "content": user_message}]

    system_prompt = (
        "You are a helpful personal task automation agent. "
        "You can manage to-do lists, check weather, and read files. "
        "Use the available tools to help the user with their requests. "
        "When you have completed the task, provide a clear summary of what you did. "
        "If a task requires multiple steps, complete all steps before responding."
    )

    max_iterations = 10
    iteration = 0

    while iteration < max_iterations:
        iteration += 1

        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1024,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    print(f"\nAgent: {block.text}")
            break

        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    print(f"  [Tool Call] {tool_name}({json.dumps(tool_input)})")

                    result = execute_tool(tool_name, tool_input)
                    print(f"  [Result] {result}")

                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result,
                        }
                    )

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

    if iteration >= max_iterations:
        print("\nAgent: I reached the maximum number of steps. Please try a simpler request.")


def main():
    print("Personal Task Automation Agent")
    print("=" * 40)
    print("Type your requests in natural language.")
    print("Type 'quit' to exit.\n")

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


if __name__ == "__main__":
    main()

Your agent is fully wired up and ready to take on tasks. Next, you will put it through its paces with single-step and multi-step requests to see the agent loop in action.

Test Your Agent with Multi-Step Tasks

Your agent is fully built, but untested. A single tool call is useful, but the real power of an AI agent shows when it chains multiple tools together autonomously to solve a complex request.

In this step, you'll create a test script that exercises your agent from simple single-tool calls all the way up to multi-step chained reasoning. You'll watch the agent decide which tools to call, execute them, and feed results back into the loop.

In this step, get ready to:
  • Run the agent with simple single-tool tasks.
  • Test multi-step reasoning where the agent chains multiple tool calls.
  • Try complex chained requests in interactive mode.
Create a test script for your agent

Instead of typing test prompts one at a time in interactive mode, you'll write a dedicated test script that runs multiple scenarios in sequence. This lets you see how the agent handles progressively harder tasks.

  • Create a new file called test_agent.py in your project folder (the same folder as agent.py).
  • Paste the following code into test_agent.py:
from agent import run_agent

print("=" * 50)
print("TEST 1: Add a task")
print("=" * 50)
run_agent("Add 'buy groceries' to my to-do list")

print("\n" + "=" * 50)
print("TEST 2: List tasks")
print("=" * 50)
run_agent("Show me my to-do list")

print("\n" + "=" * 50)
print("TEST 3: Check weather")
print("=" * 50)
run_agent("What's the weather like in London?")

print("\n" + "=" * 50)
print("TEST 4: Multi-step task")
print("=" * 50)
run_agent("Check the weather in London. If it's rainy, add 'bring umbrella' to my to-do list.")

What does this test script do?

This script imports the run_agent function directly from your agent.py file. Each test sends a different natural language prompt to your agent.

  • Tests 1-3 are single-tool tasks. The agent only needs to call one tool to complete the request.
  • Test 4 is a multi-step chained request. The agent must check the weather first, reason about the result, then conditionally add a task.
  • Save test_agent.py.
Run the tests and observe single-tool behavior

Your test script will call the Claude API four times. Each call costs a tiny fraction of a cent with the claude-haiku-4-5 model.

  • Run the test script by executing this command:
python3 test_agent.py

You should see output for all four tests scrolling through your terminal. Each test prints the tool calls the agent made and the results it received.

What should I see for tests 1-3?

For each single-tool test, you should see exactly one [Tool Call] line followed by one [Result] line, then the agent's final response.

  • Test 1 calls manage_todo with action add and creates a todos.json file.
  • Test 2 calls manage_todo with action list and shows the task you just added.
  • Test 3 calls check_weather for London and returns rainy, 58°F.

💡 What should I see for test 4?

Test 4 is where multi-step reasoning shines. You should see multiple [Tool Call] lines. The agent first calls check_weather for London, observes that it's rainy, then calls manage_todo to add "bring umbrella".

This is the agent loop in action. Claude receives the weather result, reasons about the condition, and autonomously decides to call a second tool. You never told it how to chain the calls.

🙋‍♀️ Seeing an error when running the tests?

  • If you see ModuleNotFoundError: No module named 'anthropic', make sure your virtual environment is activated.
  • If you see an authentication error, confirm your ANTHROPIC_API_KEY environment variable is still set in this terminal session.
  • If you see ModuleNotFoundError: No module named 'agent', make sure test_agent.py is in the same folder as agent.py.

Help me debug my test_agent.py errors

Try your own requests in interactive mode

The test script proves your agent works. Now try the interactive mode where you can type any request and watch the agent figure out which tools to use in real time.

  • Start the interactive agent by running this command:
python3 agent.py

You should see the agent greeting with "Personal Task Automation Agent" and a prompt waiting for your input.

  • Try typing a multi-step request like: Check my to-do list and tell me how many tasks are pending
  • Try a request that uses a different tool: What's the weather in Tokyo?
  • Try a chained request: Mark task 1 as complete and then show me my updated list
  • Type quit when you're done experimenting.

Why does the agent sometimes chain tools?

Claude reads your request, looks at the available tools, and plans a strategy. When a request requires information from one tool before it can use another, Claude returns a stop_reason of tool_use and your agent loop feeds the result back.

Claude then sees the result in context and decides whether to call another tool or respond with end_turn. This is exactly how LangChain, CrewAI, and other frameworks work under the hood.

  • Confirm that a todos.json file was created in your project folder by running:
cat todos.json

You should see your tasks stored as JSON, with "buy groceries" and "bring umbrella" from the test script (and any tasks you added in interactive mode).

Agent not chaining tool calls?

  • If the agent only calls one tool and responds without completing the full request, try rephrasing your prompt to be more explicit about both steps.
  • If the agent hits the maximum iteration limit, check that your execute_tool function returns a string result for every tool name.

Help me figure out why my agent isn't chaining tool calls

✔️ Awesome, I've got everything!

Great. Double check you've saved both files.

ⓧ I'd like to double check the full code

Here is the complete test_agent.py file:

from agent import run_agent

print("=" * 50)
print("TEST 1: Add a task")
print("=" * 50)
run_agent("Add 'buy groceries' to my to-do list")

print("\n" + "=" * 50)
print("TEST 2: List tasks")
print("=" * 50)
run_agent("Show me my to-do list")

print("\n" + "=" * 50)
print("TEST 3: Check weather")
print("=" * 50)
run_agent("What's the weather like in London?")

print("\n" + "=" * 50)
print("TEST 4: Multi-step task")
print("=" * 50)
run_agent("Check the weather in London. If it's rainy, add 'bring umbrella' to my to-do list.")

Your agent.py file should be unchanged from the previous step.

Secret mission

Build a Daily Briefing Tool

Your agent can check weather and manage tasks individually. But what if it could pull everything together into a single morning briefing? In this mission, you'll add a fourth tool that combines weather, to-dos, and calendar events into one summary that Claude formats for you.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the Claude API on a pay-per-use basis. There are no ongoing costs as long as you are not actively running the agent.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder (containing agent.py, agent_with_briefing.py, test_agent.py, todos.json, calendar.txt, and your virtual environment).
  • Anthropic API key (stored in your Anthropic Console account).
  • ANTHROPIC_API_KEY environment variable (set in your terminal session).

Keep everything running

No action needed. Choose this if you are still actively building or want to extend your agent with more tools.

  • Your project files stay on your Desktop.
  • Your virtual environment stays in the project folder.
  • Your API key remains active in the Anthropic Console. You are only charged when you run the agent and make API calls.
  • The ANTHROPIC_API_KEY environment variable will disappear when you close your terminal (unless you added it to a shell config file like .bashrc or .zshrc).

Pause - I'll come back to this later

Free up your terminal and deactivate the virtual environment. Your files and API key stay intact for when you return.

  • If your virtual environment is active, deactivate it by running:
deactivate
  • Close your terminal window. The ANTHROPIC_API_KEY environment variable will be cleared automatically (session-scoped variables do not persist).
  • Your project files and API key in the Anthropic Console remain untouched. When you come back, reactivate your virtual environment and re-export your API key.

Delete - I don't want to use this again

Remove all project files and revoke your API key so nothing lingers on your machine or in your Anthropic account.

  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

On Windows, use:

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • This removes all Python files, the virtual environment, todos.json, and calendar.txt.
  • Unset the environment variable in your current terminal session by running:
unset ANTHROPIC_API_KEY
  • If you added the export to a shell config file (.bashrc, .zshrc, or .bash_profile), open that file and remove the line containing ANTHROPIC_API_KEY.
  • To revoke your API key, log in to console.anthropic.com, navigate to the API keys section, and delete the key you created for this project.

Nice Work!

Nice Work!

You just built a personal task automation agent from scratch using Python and the Claude API. Your agent autonomously decides which tools to call, executes them, and chains multiple actions together to solve complex requests.

Here's what you learned:

  • Built a complete agent loop that sends messages to Claude, detects tool calls via stop_reason, executes tools, and feeds results back until the task is done.
  • Defined 3 custom tools with JSON Schema and wired them to Python functions that manage a to-do list, check weather, and read files.
  • Tested multi-step reasoning where the agent autonomously chains multiple tool calls to complete complex natural language requests.
  • Added a daily briefing tool that combines weather, to-do items, and calendar events into a personalized morning summary.

Ready to quiz yourself?