Hello! Welcome to the sixth lesson in our journey to build an agentic AI for web testing.
In our previous lessons, we've built up a solid foundation. We've learned how to handle data structures, parse JSON, and, most recently, how to make robust API calls that can gracefully handle network errors. These are all crucial skills, but so far, our code has been a series of functions in a single script. As we build our agent, this will quickly become messy and hard to manage.
This lesson directly addresses the learning outcome: Define and use Python classes to structure agent components. We will take the logic we've developed and organize it into clean, reusable, and logical building blocks using Object-Oriented Programming (OOP). This is how we start building the skeleton of our agent, preparing it to take on complex tasks.
Why Use Classes for an AI Agent?
Before we start writing code, let's understand why classes are so important for building agents. An agent can be conceptually broken down into several components that work together.

Python classes allow us to model each of these components as a self-contained object. This approach, known as Object-Oriented Programming, has several key benefits:
- Encapsulation: A class bundles related data (attributes) and the functions that operate on that data (methods) together. For example, an
Agentclass would hold its API key and model name, as well as thechat()method for communicating with the LLM. - State Management: Each object created from a class (an "instance") maintains its own internal state. This is perfect for an agent's memory. Each agent instance can have its own unique conversation history, separate from any others.
- Organization and Reusability: By defining classes for different parts of our system (e.g.,
Agent,BrowserTool,MemoryManager), we create a clean and organized project structure. These classes can then be easily reused and combined. This is a common practice in software development, as seen in projects with a clear separation of concerns into different files likeactions.pyormemory.py.
Building a Basic Agent Class
Let's put theory into practice by building our first component: the Agent class itself. This class will act as the "brain" or central controller. We will build it up in a few steps, following the excellent guide from the blog post "Building an AI agent from scratch in Python".
First, we will create a simple agent that can have a conversation but has no memory of past interactions.
Building an AI agent from scratch in Python
We'll use this article by Leonie Monigatti as a guide to build our Agent class from the ground up. It provides a clear, step-by-step approach that aligns with our goal of understanding the internal workings of an agent.
Please read the section 'Component 1: LLM and Instructions' and study the first version of the Agent class provided. Focus on understanding the __init__ and chat methods.
Let's break down the code from the article:
import os
import anthropic # Or openai, depending on your choice
# Ensure your API key is set as an environment variable
class Agent:
"""A simple AI agent that can answer questions"""
def __init__(self):
"""
This is the constructor method. It runs when a new Agent object is created.
'self' refers to the specific instance of the agent being created.
"""
# We assume you have the API key in your environment variables
self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.model = "claude-3-sonnet-20240229" # Example model
self.system_message = "You are a helpful assistant."
def chat(self, user_message):
"""Processes a user message and returns a response from the LLM."""
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_message,
messages=[
{"role": "user", "content": user_message}
]
)
return response.content[0].text
Key concepts here:
__init__(self): This special method is the constructor. It initializes the newAgentobject. We store the API client, model name, and system prompt as attributes of the instance usingself..self: This parameter refers to the specific instance of the class.self.modelbelongs only to this agent object. This allows us to create multiple agents, each with potentially different models or system prompts.chat(self, user_message): This is a method, a function that belongs to the class. It can access the attributes of the instance viaself.
Adding State: Giving the Agent Memory
If you test the agent above with a follow-up question, you'll notice it has no memory of the previous turn. Let's fix that by adding short-term memory. We'll add a new attribute, self.messages, to store the conversation history.
Building an AI agent from scratch in Python
Let's continue with the same blog post to see how to add memory to our class.
Read 'Component 2: (Conversation) Memory' and observe how the Agent class is modified. Pay close attention to how self.messages is initialized and how it's updated within the chat method.
Here's the updated Agent class with memory:
class Agent:
"""An AI agent that can hold a multi-turn conversation."""
def __init__(self):
self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.model = "claude-3-sonnet-20240229"
self.system_message = "You are a helpful assistant."
# Initialize an empty list to store the conversation history
self.messages = []
def chat(self, user_message):
# Add the user's message to our history
self.messages.append({"role": "user", "content": user_message})
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_message,
# Pass the entire conversation history to the API
messages=self.messages
)
# The API response gives us a list of content blocks. We extract the text.
assistant_response_text = response.content[0].text
# Add the assistant's response to our history
self.messages.append({"role": "assistant", "content": assistant_response_text})
return assistant_response_text
Now, every time we call agent.chat(), the conversation history is maintained within the agent object's self.messages attribute. The agent has state!
Test your understanding!
Add a new method to the Agent class called clear_memory(). This method should take no arguments (other than self) and should reset the conversation history by clearing the self.messages list.
Show answer
class Agent:
"""An AI agent that can hold a multi-turn conversation."""
def __init__(self):
self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.model = "claude-3-sonnet-20240229"
self.system_message = "You are a helpful assistant."
self.messages = []
def chat(self, user_message):
self.messages.append({"role": "user", "content": user_message})
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_message,
messages=self.messages
)
assistant_response_text = response.content[0].text
self.messages.append({"role": "assistant", "content": assistant_response_text})
return assistant_response_text
def clear_memory(self):
"""Resets the conversation history."""
print("--- Clearing agent memory ---")
self.messages = []
# --- Example Usage ---
agent = Agent()
print("Agent:", agent.chat("My name is Alex."))
print("Agent:", agent.chat("What is my name?"))
agent.clear_memory() # Reset the conversation
print("Agent:", agent.chat("What is my name?")) # The agent should not remember the name now
Structuring Tools as Classes
Our agent can now chat, but your goal is to build an agent that can act—specifically, one that can test web applications. To do this, we need to give it tools. We can define each tool as its own class. This separates the logic for each tool from the agent's main reasoning loop.
For now, we'll use a simple CalculatorTool as an example. In later lessons, we will replace this with PlaywrightTool classes for web automation.
Building an AI agent from scratch in Python
The final component we'll add today is tool use. The same article demonstrates how to create a CalculatorTool class and integrate it into our agent.
Read the section 'Component 3: Tool Use'. First, analyze the structure of the CalculatorTool class. Note its get_schema and execute methods. Then, look at the final version of the Agent class to see how it's initialized with a list of tools.
This introduces two important concepts:
-
A
ToolClass: TheCalculatorToolclass is responsible for one thing: calculations. It has two key methods:get_schema(): Provides a JSON description of the tool for the LLM. This tells the LLM what the tool is called, what it does, and what arguments it expects.execute(): Contains the actual Python code to run the tool.
-
Composition: The
Agentclass doesn't inherit from the tool; it uses it. This is called composition. TheAgentis initialized with a list of tool objects, and it contains the logic to decide when to call a tool'sexecutemethod.
A More Formal Structure: Interfaces
The CalculatorTool is a great start. In a larger project, you'd want to ensure all your tools follow a consistent structure. This is where the concept of an interface or abstract base class comes in.
An interface is like a contract. It defines the methods that any class implementing it must have. For our project, we could define a ToolInterface class that requires every tool to have a get_schema() and an execute() method.
The OpenAI Cookbook article on agentic structure provides a great example of this more formal approach.
Dynamic Tool Generation and Execution With o3-mini
This OpenAI cookbook entry shows a more advanced, object-oriented structure. We won't implement it all now, but it's valuable to see how a larger system is organized.
Please read the section that starts with 'To keep the Agents and Tools organized...' and introduces the core classes: BaseAgent, ChatMessages, ToolManager, and ToolInterface. You don't need to read the code files, just understand the purpose of each class from the description.
This professional structure introduces:
BaseAgent: A template for all agents, ensuring they have common capabilities.ToolInterface: A contract for all tools, ensuring they are interchangeable.ToolManager: A component responsible for keeping track of an agent's available tools.
We will adopt this thinking as we build our web testing agent, ensuring our components are modular and well-defined.
Test your understanding!
Thinking ahead to our goal of web testing, let's design a skeleton for a WebNavigatorTool. You don't need to write any Playwright code. Just define the Python class structure.
Your WebNavigatorTool class should:
- Have a
__init__method (it can be empty for now, justpass). - Have a
get_schema()method that returns a dictionary describing a tool namednavigate_to_urlwhich takes aurlstring as a parameter. - Have an
execute(self, url)method that, for now, just prints a message likeNavigating to {url}...and returns a success message.
This exercise mimics the structure of the CalculatorTool but applies it to our specific domain.
Show answer
class WebNavigatorTool:
"""A tool for navigating to a web page."""
def __init__(self):
pass
def get_schema(self):
"""Returns the schema for the LLM to understand the tool."""
return {
"name": "navigate_to_url",
"description": "Navigates a web browser to the specified URL.",
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The full URL to navigate to (e.g., 'https://www.google.com')."
}
},
"required": ["url"]
}
}
def execute(self, url):
"""Executes the tool's logic."""
print(f"Executing navigation to: {url}")
# In a real scenario, this is where we would call Playwright.
return {"status": "success", "message": f"Successfully navigated to {url}"}
# --- Example Usage ---
navigator = WebNavigatorTool()
# Get the tool's description
schema = navigator.get_schema()
print("Tool Schema:", schema)
# Execute the tool with a sample URL
result = navigator.execute(url="https://example.com")
print("Execution Result:", result)
Conclusion
In this lesson, we took a major step toward building a real agent. We've moved from writing disconnected functions to structuring our logic into classes, the fundamental building blocks of object-oriented programming.
Here are the key takeaways:
- Classes bundle data and behavior: They encapsulate the attributes (like
modelormessages) and methods (likechatorexecute) of a logical component. selfis the instance: It allows a method to access the specific object's own data.__init__is the constructor: It sets up the initial state of a new object.- Composition lets objects work together: An
Agentclass can use variousToolclasses to extend its capabilities. - Interfaces provide structure: Defining a base class or "interface" (like
ToolInterface) ensures that all your components are consistent and interchangeable.
We've now laid the architectural foundation. We have a blueprint for an Agent and its Tools. In our next lesson, we will begin Module 2: Web Automation with Python and Playwright. We'll start by learning how to initialize Playwright and manage browser contexts. This will give us the power to fill in the execute method of our WebNavigatorTool with real browser automation code.