← Back to Blog

What You'll Build

If you've been searching for a real how to build AI agents tutorial — not theory, but actual working code — you're in the right place. By the end of this guide, you'll have a production-ready AI agent that uses Claude's tool-use API to answer questions, run calculations, and look up weather data using Python and the Anthropic SDK. The whole thing runs in under 100 lines of code and you can adapt it to your own tools in minutes.

This is the same foundation we use at Naples AI when building custom AI agents for local businesses — real estate offices, restaurants, car dealerships, and more. It's simple enough to understand in one sitting, and solid enough to build on.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and functions
  • The anthropic package installed (pip install anthropic)
  • A terminal or IDE you're comfortable using
📦 Full Source Code
The complete working agent is built step by step in the sections below. Each code block adds to the last, so by Step 4 you'll have the entire file ready to run. If you want to skip to the finished product, jump to Step 3: Create the Agent Loop — that section shows the complete class assembled.

Step 1: Set Up Claude API & Anthropic SDK

First, install the Anthropic SDK if you haven't already. One line in your terminal handles it.

terminal
pip install anthropic

Now create a file called agent.py and drop in your imports and API setup. We pull the API key from an environment variable so it never gets hardcoded into your source.

agent.py
import os
import json
import anthropic

# Pull key from environment so it stays out of your code
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

Set your API key in your terminal before running anything:

terminal
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
⚠️ Never commit your API key to Git. Use a .env file with python-dotenv or set it as a system environment variable. One accidental push and you'll be rotating keys.

Step 2: Define Your Tool Schemas

Tools are what turn a regular Claude chat into an actual agent. You define each tool as a JSON schema, and Claude decides when to call them based on the conversation. Think of tools as the hands your agent uses to interact with the outside world.

For this tutorial we're building three tools: a calculator, a weather lookup, and a simple text search. These are easy to swap out for real APIs once you get the pattern down.

agent.py — tool definitions
import os
import json
import anthropic

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

# Each tool needs a name, description, and input_schema.
# Claude reads the description to decide when to use the tool.
TOOLS = [
    {
        "name": "calculate",
        "description": (
            "Performs basic arithmetic calculations. "
            "Use this when the user asks you to compute a math expression."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "A valid Python math expression, e.g. '12 * 4 + 7'"
                }
            },
            "required": ["expression"]
        }
    },
    {
        "name": "get_weather",
        "description": (
            "Returns simulated current weather for a given city. "
            "Use this when the user asks about weather conditions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The name of the city, e.g. 'Naples, FL'"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "search_knowledge_base",
        "description": (
            "Searches a local knowledge base for answers about Naples AI services. "
            "Use this when the user asks about what Naples AI does or offers."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query string"
                }
            },
            "required": ["query"]
        }
    }
]

Writing clear tool descriptions is the most important thing you'll do here. Claude doesn't guess — it reads your description and decides. Vague descriptions mean wrong tool calls, so be specific about when each tool should be used.

Step 3: Create the Agent Loop

This is the core of the whole thing. The agent loop sends a message to Claude, checks if Claude wants to use a tool, runs that tool, then feeds the result back to Claude. It keeps going until Claude gives a final text response with no more tool calls.

Here's the complete agent class. Everything from Steps 1 and 2 lives in the same file above this class definition.

agent.py — main agent class & run loop
import os
import json
import anthropic

client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

TOOLS = [
    {
        "name": "calculate",
        "description": (
            "Performs basic arithmetic calculations. "
            "Use this when the user asks you to compute a math expression."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "A valid Python math expression, e.g. '12 * 4 + 7'"
                }
            },
            "required": ["expression"]
        }
    },
    {
        "name": "get_weather",
        "description": (
            "Returns simulated current weather for a given city. "
            "Use this when the user asks about weather conditions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The name of the city, e.g. 'Naples, FL'"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "search_knowledge_base",
        "description": (
            "Searches a local knowledge base for answers about Naples AI services. "
            "Use this when the user asks about what Naples AI does or offers."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query string"
                }
            },
            "required": ["query"]
        }
    }
]


class ClaudeAgent:
    def __init__(self, tools: list, system_prompt: str = ""):
        self.tools = tools
        self.system_prompt = system_prompt or (
            "You are a helpful AI assistant for Naples AI, "
            "a custom AI solutions agency in Southwest Florida. "
            "Use the available tools when needed to answer user questions accurately."
        )
        # Conversation history persists across turns in the same session
        self.messages: list = []

    def run(self, user_input: str) -> str:
        """Send a user message and return Claude's final text response."""
        self.messages.append({"role": "user", "content": user_input})

        # Keep looping until Claude stops requesting tool calls
        while True:
            response = client.messages.create(
                model=MODEL,
                max_tokens=4096,
                system=self.system_prompt,
                tools=self.tools,
                messages=self.messages
            )

            # Append Claude's full response to conversation history
            self.messages.append({"role": "assistant", "content": response.content})

            if response.stop_reason == "end_turn":
                # No tool calls — extract and return the final text
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text
                return ""

            if response.stop_reason == "tool_use":
                # One response can contain multiple tool calls — handle all of them
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        result = self._execute_tool(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result
                        })

                # Feed every tool result back in a single user message
                self.messages.append({"role": "user", "content": tool_results})

    def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
        """Route tool calls to their handler functions."""
        if tool_name == "calculate":
            return handle_calculate(tool_input)
        elif tool_name == "get_weather":
            return handle_weather(tool_input)
        elif tool_name == "search_knowledge_base":
            return handle_knowledge_base(tool_input)
        else:
            return f"Error: unknown tool '{tool_name}'"

Step 4: Handle Tool Calls & Responses

Now we write the actual functions that execute when Claude calls a tool. In a production system, these would hit real APIs or databases. For this tutorial they return realistic mock data so you can run the agent immediately without any external dependencies.

agent.py — tool handlers & entry point
def handle_calculate(tool_input: dict) -> str:
    """Safely evaluate a math expression and return the result."""
    expression = tool_input.get("expression", "")
    try:
        # eval is acceptable here because we restrict the namespace
        result = eval(expression, {"__builtins__": {}}, {})
        return f"The result of {expression} is {result}"
    except Exception as e:
        return f"Calculation error: {str(e)}"


def handle_weather(tool_input: dict) -> str:
    """Return simulated weather data for a city."""
    city = tool_input.get("city", "unknown city")
    # Swap this out for a real weather API like OpenWeatherMap in production
    weather_data = {
        "Naples, FL": "81°F, sunny, humidity 65%, light southeast breeze",
        "Miami, FL": "84°F, partly cloudy, humidity 72%, calm winds",
        "Fort Myers, FL": "79°F, mostly sunny, humidity 60%, light winds",
    }
    conditions = weather_data.get(city, f"75°F, clear skies, humidity 55% (simulated data for {city})")
    return f"Current weather in {city}: {conditions}"


def handle_knowledge_base(tool_input: dict) -> str:
    """Search a static knowledge base about Naples AI services."""
    query = tool_input.get("query", "").lower()
    # In production, replace this with a vector database or semantic search
    kb = {
        "services": (
            "Naples AI offers custom AI development, intelligent process automation, "
            "AI chatbots, predictive analytics, AI-powered SEO, computer vision quality "
            "control, AI knowledge base systems, and real estate listing automation."
        ),
        "industries": (
            "Naples AI works with real estate agencies, healthcare providers, "
            "restaurants, car dealerships, and manufacturing companies in Southwest Florida."
        ),
        "contact": (
            "Naples AI was founded by Chris Mejias. You can book a free consultation at "
            "https://calendly.com/chris-mejias-naplesaiagency/30min"
        ),
        "location": (
            "Naples AI is based in Naples, Florida, serving Southwest Florida businesses "
            "and remote clients nationwide."
        ),
    }
    for key, value in kb.items():
        if key in query or any(word in query for word in query.split()):
            return value
    return (
        "Naples AI is a Southwest Florida agency specializing in custom AI solutions "
        "for local businesses. Visit naplesai.agency for more information."
    )


if __name__ == "__main__":
    agent = ClaudeAgent(tools=TOOLS)

    test_queries = [
        "What is 847 multiplied by 23?",
        "What's the weather like in Naples, FL right now?",
        "What services does Naples AI offer?",
        "What is 15% of 3,400 and what industries does Naples AI serve?"
    ]

    for query in test_queries:
        print(f"\n{'='*60}")
        print(f"USER: {query}")
        print(f"{'='*60}")
        response = agent.run(query)
        print(f"AGENT: {response}")
        # Reset conversation between unrelated queries
        agent.messages = []

Example Output

Here's what you'll actually see when you run python agent.py in your terminal. The agent makes real tool calls and returns clean, contextual answers.

terminal output
============================================================
USER: What is 847 multiplied by 23?
============================================================
AGENT: 847 multiplied by 23 equals **19,481**.

============================================================
USER: What's the weather like in Naples, FL right now?
============================================================
AGENT: The current weather in Naples, FL is 81°F, sunny, with 65% humidity
and a light southeast breeze. Sounds like a great day to be in Southwest Florida!

============================================================
USER: What services does Naples AI offer?
============================================================
AGENT: Naples AI offers a comprehensive range of custom AI services, including:

- **Custom AI development** tailored to your business needs
- **Intelligent process automation** to eliminate repetitive manual work
- **AI chatbots and virtual assistants** for customer engagement
- **Predictive analytics and forecasting** for smarter decisions
- **AI-powered SEO content generation**
- **Computer vision quality control** for manufacturing
- **AI knowledge base systems**
- **Real estate listing automation**

They serve businesses across Southwest Florida in real estate, healthcare,
restaurants, car dealerships, and manufacturing.

============================================================
USER: What is 15% of 3,400 and what industries does Naples AI serve?
============================================================
AGENT: 15% of 3,400 is **510**.

As for Naples AI, they serve several industries across Southwest Florida:
real estate agencies, healthcare providers, restaurants, car dealerships,
and manufacturing companies — helping each one streamline operations and
reduce manual work with custom AI solutions.

How It Works

The magic here is in the loop inside the run() method. Claude doesn't just respond — it reasons about what tools it needs, calls them, reads the results, and then decides if it needs to call more tools or if it has enough to give you a final answer.

Here's the plain-English version of what happens on each turn:

  1. You send a message. It gets appended to the conversation history as a user message.
  2. Claude reads the message and your tool schemas. It decides whether it needs a tool or can answer directly.
  3. If Claude calls a tool (stop reason = tool_use), we extract the tool name and inputs from the response, run our Python handler function, and send the result back as a tool_result message.
  4. Claude reads the tool result and either calls another tool or produces its final answer (stop reason = end_turn).
  5. We return the final text and the conversation history stays intact for the next message.

The multi-tool example at the end — asking about 15% of 3,400 and Naples AI industries in one question — shows Claude calling both calculate and search_knowledge_base in the same turn, then combining both results into a single coherent answer. That's the power of the agentic loop.

Common Errors and Fixes

Error 1: AuthenticationError on startup

anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}

Fix: Your API key isn't set or isn't being read correctly. Run echo $ANTHROPIC_API_KEY in your terminal to confirm it's set. If it's empty, re-export it: export ANTHROPIC_API_KEY="sk-ant-your-key-here". Make sure there are no extra spaces or quote characters in the value.

Error 2: tool_use block missing from response loop

anthropic.BadRequestError: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"messages: tool_use block at index 1 does not have a corresponding tool_result block"}}

Fix: This happens when you append Claude's tool-use response to the history but forget to send back the tool result before making the next API call. Every tool_use block needs a matching tool_result in the very next user message. The agent loop in Step 3 handles this correctly — if you're seeing this error, check that you haven't modified the message-appending logic.

Error 3: max_tokens causes truncated tool calls

stop_reason: "max_tokens"
# Claude's response cuts off mid-tool-call, causing a JSON parse error downstream

Fix: Set max_tokens to at least 4096 when using tools. Tool call JSON can be verbose, and a low token limit will cut the response off mid-structure. If you're handling complex tasks with lots of tool results, bump it to 8192. Always check response.stop_reason — if it's "max_tokens" instead of "tool_use" or "end_turn", that's your culprit.

Next Steps

You've got a working agent. Here's how to make it actually useful for your specific situation:

  1. Connect real APIs. Swap the mock handle_weather function for a call to OpenWeatherMap, or replace handle_knowledge_base with a vector database like Pinecone or Supabase pgvector. The tool interface doesn't change — only the handler internals do.
  2. Add memory across sessions. Right now, self.messages resets when you restart the script. Serialize the conversation to a JSON file or a database to give your agent persistent memory across multiple sessions.
  3. Build a web interface. Wrap the ClaudeAgent class in a FastAPI endpoint and connect it to a simple HTML chat UI. You'll have a deployable chatbot in under an hour.
  4. Add human-in-the-loop approval. For high-stakes tools (like sending emails or making database writes), add a confirmation step before _execute_tool runs. This keeps the agent useful while preventing expensive mistakes.

FAQ

What is an AI agent vs a regular chatbot?

A regular chatbot just generates text based on your input. An AI agent can take actions — calling APIs, running code, searching databases, or triggering workflows — and then use the results to inform its next response. The tool-use loop in this tutorial is what makes Claude an agent rather than just a language model.

How many tools can a Claude agent use at once?

Claude can be given many tools in a single request — Anthropic doesn't publish a hard cap, but in practice you'll want to keep it under 20-30 tools for reliable tool selection. If you have dozens of capabilities, consider grouping them into categories or using a router agent that calls sub-agents. Claude can also call multiple tools in a single response turn when the task calls for it, as shown in the multi-tool example above.

Is claude-sonnet-4-6 good enough for production AI agents?

Yes — Claude Sonnet 4-6 hits the