Skip to main content

Command Palette

Search for a command to run...

Building an AI Agent From Scratch Using the Anthropic API: A Complete Guide

Published
12 min read
Building an AI Agent From Scratch Using the Anthropic API: A Complete Guide
J

Senior AI Engineer | Building & Telling Stories about AI/ML Systems | Software Engineer

The Power Is In Your Hands

Most developers spend weeks wrestling with bloated agent frameworks, burning through $2,000+ in API costs while debugging black-box abstractions they don't understand.

Here's the truth: you don't need complex frameworks to build a production-ready AI agent.

In this guide, you'll build a fully functional agent from scratch in under 200 lines of Python—complete with tool integration, memory management, and multi-step reasoning. No expensive platforms. No mysterious abstractions. Just clean, extensible code you control entirely.

By the end, you'll have an autonomous agent that breaks down complex problems, uses external tools, and maintains conversation context—all while understanding exactly how every component works. You'll learn the core patterns for tool design, the agentic loop that enables autonomous behavior, and battle-tested strategies for memory management and error handling.

Let's build something real.

Setting Up Your Development Environment

Let's get your workspace ready for agent development. You'll need Python 3.8 or higher and a few essential libraries. First, install the required packages and set up your API credentials.

import os
import json
from typing import List, Dict, Any, Optional

from dotenv import load_dotenv

import anthropic

load_dotenv()

Create a .env file in your project root with your Anthropic API key:

ANTHROPIC_API_KEY=your_api_key_here

You can obtain your API key from the Anthropic Console at console.anthropic.com. Keep this key secure and never commit it to version control.

Building a Modular Tool System

Tools are the hands and eyes of your agent—they allow it to interact with the world beyond text generation.

Let's build a flexible tool system that can be easily extended with new capabilities.

We'll create a weather information tool that demonstrates real-world utility.

Designing the Tool Interface

Every tool needs a consistent interface so the agent can discover and use it effectively. We'll define a base structure that all tools must implement.

class BaseTool:
    """Base class for all agent tools"""

    def get_name(self) -> str:
        """Returns the tool name"""
        raise NotImplementedError

    def get_description(self) -> str:
        """Returns what the tool does"""
        raise NotImplementedError

    def get_parameters(self) -> Dict[str, Any]:
        """Returns the parameter schema for the tool"""
        raise NotImplementedError

    def execute(self, **kwargs) -> Dict[str, Any]:
        """Executes the tool with given parameters"""
        raise NotImplementedError

This base class establishes a contract that every tool must fulfill.

The agent will call get_name(), get_description(), and get_parameters() to understand what the tool does.

When the agent decides to use a tool, it calls execute() with the appropriate parameters.

Implementing a Weather Information Tool

Now let's build a concrete tool implementation that provides weather information. This tool simulates weather data retrieval—in production, you'd connect to a real weather API.

class WeatherTool(BaseTool):
    """A tool for retrieving weather information"""

    def get_name(self) -> str:
        return "get_weather"

    def get_description(self) -> str:
        return "Retrieves current weather information for a specified city. Use this when users ask about weather conditions, temperature, or forecasts."

    def get_parameters(self) -> Dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "The city name (e.g., 'New York', 'London', 'Tokyo')"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit preference"
                }
            },
            "required": ["city"]
        }

    def execute(self, city: str, unit: str = "celsius") -> Dict[str, Any]:
        """
        Simulates weather data retrieval.
        In production, this would call a real weather API.
        """
        # Simulated weather data
        weather_database = {
            "new york": {"temp": 22, "condition": "Partly cloudy", "humidity": 65},
            "london": {"temp": 15, "condition": "Rainy", "humidity": 80},
            "tokyo": {"temp": 28, "condition": "Sunny", "humidity": 55},
            "paris": {"temp": 18, "condition": "Overcast", "humidity": 70},
        }

        city_key = city.lower()

        if city_key not in weather_database:
            return {
                "error": f"Weather data not available for {city}",
                "available_cities": list(weather_database.keys())
            }

        data = weather_database[city_key]
        temp = data["temp"]

        # Convert temperature if needed
        if unit == "fahrenheit":
            temp = (temp * 9/5) + 32

        return {
            "city": city,
            "temperature": round(temp, 1),
            "unit": unit,
            "condition": data["condition"],
            "humidity": data["humidity"]
        }

This tool demonstrates proper error handling and parameter validation. Notice how we provide detailed descriptions for each parameter—this helps Claude understand how to use the tool correctly.

Creating the Tool Registry

We need a system to manage multiple tools and convert them into the format Claude expects. Let's build a tool registry that handles this transformation.

def create_tool_schema(tool: BaseTool) -> Dict[str, Any]:
    """
    Converts a tool into Claude's expected schema format.

    Args:
        tool: The tool instance to convert

    Returns:
        A dictionary matching Claude's tool schema specification
    """
    return {
        "name": tool.get_name(),
        "description": tool.get_description(),
        "input_schema": tool.get_parameters()
    }

def register_tools(tools: List[BaseTool]) -> tuple[List[Dict], Dict[str, BaseTool]]:
    """
    Registers multiple tools and creates lookup structures.

    Args:
        tools: List of tool instances to register

    Returns:
        A tuple of (tool_schemas, tool_map) for agent use
    """
    tool_schemas = [create_tool_schema(tool) for tool in tools]
    tool_map = {tool.get_name(): tool for tool in tools}

    return tool_schemas, tool_map

These utility functions separate the concerns of tool definition and tool registration. The create_tool_schema() function transforms our Python classes into the JSON schema format that Claude understands. The register_tools() function creates both the schema list for API calls and a lookup dictionary for executing tools.

Building the Agent Core

Now we'll construct the agent's brain—the component that manages conversation flow and orchestrates tool usage.

The agent needs to maintain conversation history, handle API interactions, and manage the request-response cycle.

Initializing the Agent

Let's create the agent class with proper initialization and configuration.

class Agent:
    """
    An AI agent powered by Claude that can use tools to solve problems.
    """

    def __init__(
        self,
        tools: Optional[List[BaseTool]] = None,
        model: str = "claude-sonnet-4-20250514",
        system_prompt: Optional[str] = None
    ):
        """
        Initialize the agent with tools and configuration.

        Args:
            tools: List of tool instances the agent can use
            model: Claude model identifier
            system_prompt: Custom system instructions for the agent
        """
        self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
        self.model = model
        self.conversation_history: List[Dict[str, Any]] = []

        # Set default system prompt if none provided
        self.system_prompt = system_prompt or self._get_default_system_prompt()

        # Register tools
        if tools:
            self.tool_schemas, self.tool_map = register_tools(tools)
        else:
            self.tool_schemas, self.tool_map = [], {}

    def _get_default_system_prompt(self) -> str:
        """Returns the default system prompt for the agent"""
        return """You are a helpful AI assistant that solves problems systematically.

When faced with complex questions, break them down into smaller steps.
Use available tools when you need to retrieve information or perform actions.
Think through problems logically and explain your reasoning.
Always verify your answers before presenting them to the user."""

The initialization method sets up all the essential components. We store the API client, model configuration, conversation history, and tool registry. The system prompt defines the agent's behavior and personality—customize this to match your use case.

Implementing the Chat Interface

The chat method is where the magic happens—it sends messages to Claude and receives responses.

def send_message(self, user_message: str) -> anthropic.types.Message:
    """
    Sends a user message to Claude and returns the response.

    Args:
        user_message: The user's input message

    Returns:
        Claude's response message object
    """
    # Add user message to conversation history
    self.conversation_history.append({
        "role": "user",
        "content": user_message
    })

    # Prepare API call parameters
    api_params = {
        "model": self.model,
        "max_tokens": 4096,
        "system": self.system_prompt,
        "messages": self.conversation_history
    }

    # Add tools if available
    if self.tool_schemas:
        api_params["tools"] = self.tool_schemas

    # Call Claude API
    response = self.client.messages.create(**api_params)

    # Add assistant's response to conversation history
    self.conversation_history.append({
        "role": "assistant",
        "content": response.content
    })

    return response

This method handles the complete request-response cycle. We append the user's message to history, prepare the API parameters, make the call, and store Claude's response. Notice how we conditionally include tools only when they're available—this keeps the API call clean for tool-less agents.

Extracting Text Responses

Claude's responses can contain multiple content blocks, including text and tool use blocks. We need a helper function to extract just the text.

def get_text_response(response: anthropic.types.Message) -> str:
    """
    Extracts text content from Claude's response.

    Args:
        response: The message object from Claude

    Returns:
        Concatenated text from all text content blocks
    """
    text_parts = []

    for block in response.content:
        if block.type == "text":
            text_parts.append(block.text)

    return "\n".join(text_parts)

This function iterates through all content blocks and collects text. When Claude uses tools, it might include both thinking (text) and tool calls in the same response.

Implementing the Agentic Loop

The agentic loop is where your agent becomes truly autonomous. This loop allows the agent to use tools, evaluate results, and continue reasoning until it reaches a final answer.

Unlike a simple chatbot that responds once, an agent can perform multiple actions in sequence to solve complex problems.

Detecting Tool Usage

First, we need a function to identify when Claude wants to use tools.

def extract_tool_uses(response: anthropic.types.Message) -> List[Dict[str, Any]]:
    """
    Extracts all tool use blocks from Claude's response.

    Args:
        response: The message object from Claude

    Returns:
        List of dictionaries containing tool use information
    """
    tool_uses = []

    for block in response.content:
        if block.type == "tool_use":
            tool_uses.append({
                "id": block.id,
                "name": block.name,
                "input": block.input
            })

    return tool_uses

When Claude decides it needs a tool, it includes a tool_use block in its response. This function finds all such blocks and extracts the relevant information. Each tool use has an ID (for tracking), a name (which tool to use), and input (the parameters).

Executing Tools Safely

Tool execution is where your agent interacts with the outside world—this requires careful error handling.

def execute_tool_safely(
    tool: BaseTool,
    tool_name: str,
    tool_input: Dict[str, Any]
) -> Dict[str, Any]:
    """
    Executes a tool with error handling and logging.

    Args:
        tool: The tool instance to execute
        tool_name: Name of the tool (for logging)
        tool_input: Parameters to pass to the tool

    Returns:
        The tool's execution result or error information
    """
    try:
        print(f"  → Executing {tool_name} with input: {tool_input}")
        result = tool.execute(**tool_input)
        print(f"  ✓ Tool execution successful")
        return result

    except Exception as e:
        error_msg = f"Tool execution failed: {str(e)}"
        print(f"  ✗ {error_msg}")
        return {"error": error_msg}

This function wraps tool execution in try-except blocks. If something goes wrong (invalid parameters, network errors, etc.), we capture the error and return it as a result. The agent can then see what went wrong and potentially retry or adjust its approach.

Processing Tool Results

After executing tools, we need to format the results for Claude.

def create_tool_result_content(
    tool_uses: List[Dict[str, Any]],
    tool_map: Dict[str, BaseTool]
) -> List[Dict[str, Any]]:
    """
    Executes tools and formats results for Claude.

    Args:
        tool_uses: List of tool use requests from Claude
        tool_map: Mapping of tool names to tool instances

    Returns:
        List of tool result objects formatted for the API
    """
    tool_results = []

    for tool_use in tool_uses:
        tool_name = tool_use["name"]
        tool_id = tool_use["id"]
        tool_input = tool_use["input"]

        # Execute the tool
        tool = tool_map[tool_name]
        result = execute_tool_safely(tool, tool_name, tool_input)

        # Format result for Claude
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": tool_id,
            "content": json.dumps(result)
        })

    return tool_results

Claude expects tool results in a specific format. Each result must include the original tool use ID (so Claude knows which call it corresponds to) and the content as a JSON string.

Building the Main Agent Loop

Now we combine everything into a loop that handles multi-turn tool usage.

def run_agent_loop(
    agent: Agent,
    initial_message: str,
    max_iterations: int = 10,
    verbose: bool = True
) -> str:
    """
    Runs the agent in a loop, handling tool use until completion.

    Args:
        agent: The agent instance to run
        initial_message: The user's initial query
        max_iterations: Maximum iterations to prevent infinite loops
        verbose: Whether to print detailed execution logs

    Returns:
        The final text response from the agent
    """
    iteration = 0
    current_message = initial_message

    if verbose:
        print(f"\n{'='*80}")
        print(f"AGENT EXECUTION START")
        print(f"{'='*80}")
        print(f"User Query: {initial_message}\n")

    while iteration < max_iterations:
        iteration += 1

        if verbose:
            print(f"\n--- Iteration {iteration} ---")

        # Send message to Claude
        response = agent.send_message(current_message)

        # Check if Claude wants to use tools
        tool_uses = extract_tool_uses(response)

        if not tool_uses:
            # No tools needed - we have the final answer
            final_response = get_text_response(response)

            if verbose:
                print(f"\n{'='*80}")
                print(f"AGENT EXECUTION COMPLETE")
                print(f"{'='*80}")
                print(f"Final Response: {final_response}\n")

            return final_response

        # Execute tools and prepare results
        if verbose:
            print(f"Claude wants to use {len(tool_uses)} tool(s):")

        tool_results = create_tool_result_content(tool_uses, agent.tool_map)

        # Send tool results back to Claude
        current_message = tool_results

    return "Maximum iterations reached without completion."

This function orchestrates the entire agent execution. It loops until Claude provides a final answer (no tool use) or hits the iteration limit. Each iteration sends a message, checks for tool use, executes any requested tools, and feeds the results back.

Complete Implementation Example

Let's bring everything together into a working example that demonstrates the full capabilities of our agent system.

def create_weather_agent() -> Agent:
    """
    Factory function to create a weather-enabled agent.

    Returns:
        Configured agent instance with weather tool
    """
    weather_tool = WeatherTool()

    custom_system_prompt = """You are a helpful weather assistant.

When users ask about weather, use the get_weather tool to retrieve accurate information.
Present weather data in a clear, conversational manner.
If users don't specify a temperature unit, use celsius by default.
Always mention the weather condition along with the temperature."""

    return Agent(
        tools=[weather_tool],
        system_prompt=custom_system_prompt
    )

def main():
    """Main execution function demonstrating agent capabilities"""

    # Create agent with weather tool
    agent = create_weather_agent()

    # Test Case 1: Simple weather query
    print("\n" + "="*80)
    print("TEST 1: Simple Weather Query")
    print("="*80)
    response = run_agent_loop(
        agent,
        "What's the weather like in Tokyo?",
        verbose=True
    )
    print(response)

    # Test Case 2: Multi-city comparison
    print("\n" + "="*80)
    print("TEST 2: Multi-City Comparison")
    print("="*80)
    response = run_agent_loop(
        agent,
        "Compare the weather in London and Paris. Which city is warmer?",
        verbose=True
    )
    print(response)
    # Test Case 3: Temperature unit conversion
    print("\n" + "="*80)
    print("TEST 3: Temperature Unit Preference")
    print("="*80)
    response = run_agent_loop(
        agent,
        "What's the temperature in New York in Fahrenheit?",
        verbose=True
    )
    print(response)

if __name__ == "__main__":
    main()

This complete example shows how to create specialized agents, run them through various scenarios, and monitor their performance.

Conclusion

You've just built a complete AI agent system from scratch using the Anthropic API.

This agent can reason through complex problems, use tools to gather information, maintain conversation context, and provide verified answers.

The modular architecture we've created allows you to easily add new tools, customize behavior, and scale to production environments.

The key takeaways from this guide include understanding that agents are more than chatbots—they're autonomous systems that can take actions.

Tool design is crucial—well-designed tools with clear schemas enable Claude to use them effectively.

The agentic loop enables multi-step reasoning and complex problem-solving. Memory management keeps conversations efficient and cost-effective.

From here, you can extend your agent with additional tools like database queries, API integrations, file operations, or web searches.

You can implement streaming responses for better UX, add conversation summarization for long-running sessions, or build multi-agent systems where specialized agents collaborate.

The future of AI isn't just in better models—it's in better agent architectures that leverage those models effectively.

You now have the foundation to build production-ready AI agents that can solve real-world problems autonomously.

Start experimenting with your own tools and use cases. The possibilities are limitless.

PS:

If you like this article, share it with others ♻️

Would help a lot ❤️

And feel free to follow me for more content like this.

More from this blog

Juan C Olamendy

122 posts

🤖 Talk about AI/ML · AI-preneur 🛠️ Build AI tools 🚀 Share my journey 𓀙 🔗 pixela.io · 🛍️ shoppingbot.ai