A custom AI agent framework implementation using Anthropic's Claude API. This enables Claude to automatically execute tools (functions) in a conversation loop, creating truly autonomous agents.
- What Are Tools?
- Architecture Overview
- Understanding
toolloop - How Tools Are Called
- Complete Flow Example
- Setup & Usage
- Project Structure
Tools are Python functions that Claude can call to perform actions. For example:
def get_order(order_id: str):
"""Get order details by order ID"""
return orders.get(order_id, "Order not found")This function becomes a "tool" that Claude can invoke during conversations.
- Manages conversations with Claude
- Maintains conversation history (
self.h) - Registers and executes tools (functions that Claude can call)
- Handles the back-and-forth between user, Claude, and tool execution
- Sample data for demos (customers and orders)
- Used to demonstrate tool capabilities
toolloop (chat.py:120-179) is the core method that implements an agentic loop:
User sends message
β
Claude responds (may request tool calls)
β
Tools are executed automatically
β
Results sent back to Claude
β
Claude responds with results (may request more tools)
β
Loop continues until Claude is done or max_steps reached
- Initial message (line 132): Sends your message to Claude
- Loop execution (line 135): Runs up to
max_stepstimes - Check for tool calls (line 141): Extracts any
tool_useblocks from Claude's response - Execute tools (lines 146-158): Runs each requested tool with the provided parameters
- Send results back (lines 161-164): Adds tool results to conversation history
- Get next response (lines 167-172): Claude receives results and continues reasoning
- Yield responses (line 179): Returns each response to the caller
Tool Schema Generation (chat.py:16-52)
generate_tool_schema(func) # Converts Python function to JSON schemaThis inspects the function and creates a schema like:
{
"name": "get_order",
"description": "Get order details by order ID",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}Registration (chat.py:87-91)
self.tools[func.__name__] = func # Store the actual function
self.tool_schemas.append(schema) # Store the schema for APISimple Call (chat.py:93-118)
chat("What's the weather?")Internally:
-
Add to history (line 104):
self.h.append({"role": "user", "content": user_message})
-
API call (lines 106-111):
response = self.c.messages.create( model=self.model, max_tokens=4096, messages=self.h, # Full conversation history tools=self.tool_schemas # Available tools schemas )
-
Save response (lines 113-116): Add Claude's response to history
Claude's response contains content blocks:
response.content = [
TextBlock(text="Let me check that order..."),
ToolUseBlock(
id="toolu_123",
name="get_order",
input={"order_id": "O1"}
)
]In toolloop (chat.py:141-158):
tool_calls = [block for block in response.content if block.type == "tool_use"]
# Finds all ToolUseBlock objectsfor tool_call in tool_calls:
tool_name = tool_call.name # "get_order"
tool_input = tool_call.input # {"order_id": "O1"}
if tool_name in self.tools:
result = self.tools[tool_name](**tool_input) # Actual function call!
# This is like calling: get_order(order_id="O1")
tool_results.append(mk_toolres(tool_call.id, result))self.h.append({
"role": "user", # Results come back as "user" messages
"content": tool_results # List of tool_result objects
})response = self.c.messages.create(...) # Claude sees the results
# Claude can now respond with findings or call more toolschat = Chat(tools=[get_order])
for response in chat.toolloop("What's in order O1?"):
print(chat.get_text(response))- Sends:
[{"role": "user", "content": "What's in order O1?"}] - Claude thinks: "I need to use get_order tool"
- Returns:
[TextBlock("Let me check..."), ToolUseBlock(name="get_order", input={"order_id": "O1"})]
- Python executes:
get_order(order_id="O1") - Returns:
{"id": "O1", "product": "Laptop", "quantity": 1, "price": 1200, ...}
- Sends:
[..previous messages.., {"role": "user", "content": [tool_result]}] - Claude sees the laptop data
- Returns:
[TextBlock("Order O1 is for a Laptop, quantity 1, price $1200...")]
- No more
tool_useblocks detected - Final response displayed to user
-
Install dependencies:
pip install -r requirements.txt
-
Set your Anthropic API key:
export ANTHROPIC_API_KEY='your-api-key-here'
Or create a
.envfile:ANTHROPIC_API_KEY=your-api-key-here
To run the demo with full logging of all API interactions:
./venv/Scripts/python demo.pyOr on Windows Command Prompt/PowerShell:
venv\Scripts\python demo.pyThis will:
- Run three demos: simple weather, tool loop, and orders management
- Generate complete cycle logs with full HTTP and API details
- Show tool executions with inputs/outputs
- Track timing for each request-response cycle
Logs are saved to:
logs/
βββ claude_complete.log (Everything with detailed view)
βββ session_YYYYMMDD_HHMMSS.log (Current session only)
βββ cycles/
β βββ complete_cycles.log (Complete request-response cycles)
βββ tools/
β βββ tool_executions.log (Tool executions only)
βββ errors/
βββ errors.log (Errors only)
Each cycle log includes:
- HTTP Request (method, URL, headers, body)
- API Request (model, messages, tools, parameters)
- HTTP Response (status code, duration, body)
- API Response (content blocks, token usage)
- Tool Executions (inputs, outputs, errors)
- Total cycle duration
from chat import Chat
def get_weather(city: str):
"""Get the weather for a city"""
return f"It's sunny in {city}!"
chat = Chat(tools=[get_weather])
response = chat("What's the weather in Paris?")
print(chat.get_text(response))from chat import Chat
def get_order(order_id: str):
"""Get order details by order ID"""
return orders.get(order_id, "Order not found")
def get_customer(customer_id: str):
"""Get customer details by customer ID"""
return customers.get(customer_id, "Customer not found")
chat = Chat(tools=[get_order, get_customer])
# Claude will automatically call tools as needed
for response in chat.toolloop("Show me order O1 and its customer details"):
print(chat.get_text(response))def calculate(expression: str):
"""Evaluate a mathematical expression"""
return eval(expression)
def search_database(query: str):
"""Search the database for records matching the query"""
# Your implementation
return results
chat = Chat(tools=[calculate, search_database])def my_stop_condition(response):
# Custom logic to determine if loop should continue
text = chat.get_text(response)
return "DONE" not in text
for response in chat.toolloop("message", cont_func=my_stop_condition):
print(chat.get_text(response))# Limit tool execution rounds
for response in chat.toolloop("complex task", max_steps=5):
print(chat.get_text(response))AnthropicAgent/
βββ chat.py # Main Chat class implementation
βββ logger.py # Comprehensive API logging system
βββ demo.py # Demo with three example scenarios
βββ tools.py # Example tool functions
βββ orders.py # Sample data for demos
βββ requirements.txt # Python dependencies
βββ .env # API key (create this)
βββ logs/ # Generated log files
β βββ claude_complete.log
β βββ session_*.log
β βββ cycles/complete_cycles.log
β βββ tools/tool_executions.log
β βββ errors/errors.log
βββ README.md # This file
- Tools aren't "sent" to Claude - only their schemas are sent
- Claude decides which tools to call based on the schemas
- Your code executes the actual Python functions
- Results go back to Claude as special "user" messages
- This creates a conversation loop where Claude can reason β act β observe β reason again
The beauty is that Claude orchestrates everything - it decides when to use tools, what parameters to pass, and when it has enough information to give you a final answer!
- Python 3.7+
- anthropic >= 0.40.0
- python-dotenv
MIT