Module 7: Project — Build a Mini Coding Agent

Setup: API and Tool Calling

Description

Before building the agent, you need three things: the dependencies installed, an API key configured, and a first successful API call. This capsule takes you from zero to "the LLM responds to me and can call tools."


Step 1: Create the Project

# Create the project directory
mkdir mini-agent
cd mini-agent

# Create a virtual environment
python3 -m venv venv
source venv/bin/activate  # On macOS/Linux
# venv\Scripts\activate   # On Windows

# Create the initial files
touch mini_agent.py
touch .env
touch requirements.txt

requirements.txt

Choose ONE option based on your provider:

# Option A: Anthropic (Claude)
anthropic>=0.40.0
python-dotenv>=1.0.0
# Option B: OpenAI (GPT)
openai>=1.50.0
python-dotenv>=1.0.0
# Install dependencies
pip install -r requirements.txt

Step 2: Configure the API Key

Get the API key

ANTHROPIC:
1. Go to https://console.anthropic.com/
2. Create an account if you don't have one
3. Go to "API Keys"
4. Create a new key
5. Copy it (it's only shown once)

OPENAI:
1. Go to https://platform.openai.com/
2. Create an account if you don't have one
3. Go to "API Keys"
4. Create a new key
5. Copy it

Save the key in .env

# .env (NEVER commit this file)

# Option A: Anthropic
ANTHROPIC_API_KEY=sk-ant-api03-...your-key-here...

# Option B: OpenAI
OPENAI_API_KEY=sk-...your-key-here...

Add .env to .gitignore

echo ".env" >> .gitignore
echo "venv/" >> .gitignore

Step 3: First API Call (without tools)

Before adding tools, verify that the API works. Create this in mini_agent.py:

Option A: With Anthropic (Claude)

import os
from dotenv import load_dotenv
from anthropic import Anthropic

load_dotenv()

client = Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Say 'Hello, I'm a mini agent' and nothing else."}
    ]
)

print(response.content[0].text)
print(f"\nTokens used - Input: {response.usage.input_tokens}, Output: {response.usage.output_tokens}")

Option B: With OpenAI (GPT)

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-mini",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Say 'Hello, I'm a mini agent' and nothing else."}
    ]
)

print(response.choices[0].message.content)
print(f"\nTokens used - Input: {response.usage.prompt_tokens}, Output: {response.usage.completion_tokens}")

Run and verify

python mini_agent.py
EXPECTED RESULT:
Hello, I'm a mini agent

Tokens used - Input: 22, Output: 10

IF YOU SEE THIS: ✅ The API works. Continue.
IF THERE'S AN ERROR: Check your API key in .env

Step 4: Understand Tool Definitions

What tool definitions are

Tool definitions tell the LLM which tools it has available. They're JSON objects that describe each tool: name, description, and parameters.

THE LLM RECEIVES THIS IN EACH REQUEST:

"You have these tools available:

1. read_file
   - Description: Reads the content of a file
   - Parameters: path (string, required)
   
2. list_directory
   - Description: Lists files in a directory
   - Parameters: path (string, optional, default '.')

And the LLM can RESPOND with:
→ Normal text (if it doesn't need tools)
→ A tool call: {name: 'read_file', args: {path: 'example.py'}}"

The JSON Schema format

LLM APIs use JSON Schema to define tools. Here's the format for Anthropic:

TOOLS = [
    {
        "name": "read_file",
        "description": "Read the contents of a file at the given path. Returns the file content as a string.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The path to the file to read"
                }
            },
            "required": ["path"]
        }
    },
    {
        "name": "list_directory",
        "description": "List all files and directories at the given path.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The directory path to list. Defaults to current directory.",
                    "default": "."
                }
            },
            "required": []
        }
    }
]

For OpenAI the format is slightly different (it uses "parameters" instead of "input_schema" and wraps it in {"type": "function", "function": {...}}), but the idea is identical.

Why the description matters

THE DESCRIPTION IS WHAT THE LLM "READS"
TO DECIDE WHETHER TO USE THE TOOL.

VAGUE DESCRIPTION:
"read_file: Reads a file"
→ The LLM doesn't know when to use it vs list_directory

PRECISE DESCRIPTION:
"read_file: Read the contents of a file at the given path.
 Returns the file content as a string. Use this when you
 need to see what's inside a specific file."
→ The LLM knows exactly when it's appropriate

REMEMBER FROM MODULE 03:
The LLM chooses the tool by PATTERN MATCHING.
Better descriptions = better decisions.

Step 5: First API Call with Tools

Now let's make it so the LLM can request tools (without executing them yet):

With Anthropic

import os
from dotenv import load_dotenv
from anthropic import Anthropic

load_dotenv()

client = Anthropic()

TOOLS = [
    {
        "name": "read_file",
        "description": "Read the contents of a file at the given path.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The path to the file to read"
                }
            },
            "required": ["path"]
        }
    },
    {
        "name": "list_directory",
        "description": "List files and directories at the given path.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Directory path. Defaults to '.'",
                    "default": "."
                }
            },
            "required": []
        }
    }
]

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=TOOLS,
    messages=[
        {"role": "user", "content": "What files are in the current directory?"}
    ]
)

for block in response.content:
    if block.type == "text":
        print(f"TEXT: {block.text}")
    elif block.type == "tool_use":
        print(f"TOOL CALL: {block.name}")
        print(f"ARGUMENTS: {block.input}")
        print(f"TOOL ID: {block.id}")

print(f"\nStop reason: {response.stop_reason}")

Expected result

TOOL CALL: list_directory
ARGUMENTS: {'path': '.'}
TOOL ID: toolu_01ABC123...

Stop reason: tool_use

WHAT HAPPENED?
→ The LLM received the question: "What files are there?"
→ It saw that it has the "list_directory" tool
→ Instead of generating text, it generated a TOOL CALL
→ The stop_reason is "tool_use" (not "end_turn")
→ The LLM is REQUESTING that we execute the tool
→ WE have to execute it and send the result back

THIS IS THE START OF THE AGENTIC LOOP:
→ The LLM requested an action
→ We execute it
→ We send it the result
→ The LLM keeps reasoning

Troubleshooting

Problem 1: AuthenticationError or Invalid API key

Cause: The API key isn't in .env or is copied incorrectly.

Solution:

# Check that .env exists and has content
cat .env

# Check that python-dotenv loads it
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print(os.getenv('ANTHROPIC_API_KEY', 'NOT FOUND')[:10])"

If it prints "NOT FOUND": your .env doesn't have the correct variable or load_dotenv() doesn't find it (check that you're running from the project directory).

Problem 2: ModuleNotFoundError: No module named 'anthropic'

Cause: The dependencies aren't installed or you're not in the virtual environment.

Solution:

# Check that you're in the venv
which python
# It should show something like: /path/mini-agent/venv/bin/python

# If not, activate the venv
source venv/bin/activate  # macOS/Linux

# Reinstall
pip install -r requirements.txt

Problem 3: RateLimitError or 429 Too Many Requests

Cause: You exceeded your plan's request limit.

Solution:

→ Wait 60 seconds and try again
→ If it persists, check your plan in the provider's console
→ For development, use cheaper models:
  - Anthropic: claude-haiku (cheaper than sonnet)
  - OpenAI: gpt-4o-mini (cheaper than gpt-4o)

Problem 4: Connection Error or Timeout

Cause: Network problems or the service is down.

Solution:

→ Check your internet connection
→ Check the provider's status page:
  - Anthropic: status.anthropic.com
  - OpenAI: status.openai.com
→ If you use a proxy or VPN, check that it doesn't block the API

What You Learned in This Capsule

1. SETUP:
   → Project created with venv and dependencies
   → API key configured in .env
   → .gitignore protects secrets

2. BASIC API CALL:
   → The LLM is an API that returns text
   → Each call has input_tokens and output_tokens
   → It's a function: input → output

3. TOOL DEFINITIONS:
   → They're JSON Schema that describe which tools there are
   → Name + description + parameters
   → The description guides the LLM's selection

4. TOOL CALLING:
   → The LLM can return tool_use instead of text
   → stop_reason: "tool_use" means "execute this tool"
   → We execute, the LLM doesn't execute anything

5. THE AGENTIC LOOP STARTS HERE:
   → The LLM requests → we execute → we give it the result
   → Capsule 03 implements this complete loop

Next capsule: 03 - Implement the agentic loop — the heart of the agent.