Most agent systems operate in isolation, restricted to their own infrastructure. In practice, this means that an agent built within a company or hosted on a cloud server cannot easily communicate with another agent created in a different environment.
The Agent2Agent Protocol (A2A) was designed to solve exactly this problem. It enables agents to connect securely, allowing them to discover each other, exchange messages, share artifacts, and collaborate on tasks — all while remaining modular and independent within their own ecosystems.
In this post, I’ll cover:
- Core Components of A2A
- Why is A2A important?
- Use Case: Communication between Agents Built with Different Frameworks
- Hands-on Practice: Creating Agents with the A2A Protocol
- Testing and Results 1: Data Insertion
- Testing and Results 2: Data Retrieval
- Testing and Results 3: Data Update
- Testing and Results 4: Data Deletion
- Testing and Results 5: Performing Calculations
Core Components of A2A
Just like the MCP protocol, A2A is built on a client-server architecture. In this model, the client — which could be a service, another agent, or even an application — sends requests to an A2A interface exposed by another agent acting as the server. The server then processes these requests and returns the results.
Here are the main components of the A2A protocol:
- Agent Card: a JSON metadata file exposed by each A2A agent, usually accessible at
/.well-known/agent.json. It describes details such as the agent’s name, description, capabilities (skills), available endpoints, version, and authentication requirements. In other words, the agent card works like a business card: it lets other clients or agents understand what the agent can do and how to interact with it. This process is called discovery. - Task: a stateful entity that represents the collaborative process between clients and remote agents to achieve a specific outcome. Each task has a unique ID and can transition through different states:
submitted,working,input-required,completed,failed, andcanceled. All communication takes place within the context of a task. This is where remote agents exchange messages, update progress, and deliver results in the form of artifacts. - Artifacts: structured outputs generated within the context of a task, which may represent a final answer or a file produced during the process. Each artifact is immutable, can be named, and may consist of different parts — such as text, image, audio, or video — and a single task can generate multiple artifacts.
- Message: any transient content exchanged within a task that is not an artifact. It may include an agent’s reasoning, user-provided context, instructions, errors, or status updates. In short, messages are used to coordinate execution: agents send messages to report progress or request information, while final results are delivered as artifacts.
- Part: the smallest unit of content exchanged between a client and a remote agent within a message or artifact. Each part has its own content and metadata, and can take different forms such as TextPart, FilePart, or DataPart.
Why is A2A important?
Imagine having 5 A2A agents distributed across 3 different systems, all working together to solve a business problem.

Notice that agent 1 appears in all systems and ends up being the most requested. In this scenario, the A2A protocol allows it to be easily scaled by adding new instances without major difficulties — as shown in the second image.

Therefore, in addition to facilitating communication between agents built in different environments, A2A also contributes to the maintenance and scalability of the application, ensuring that critical agents can be replicated in a simple and efficient way.
Use Case: Communication between Agents Built with Different Frameworks
The use case we’ll explore here demonstrates how the A2A protocol enables communication between agents built with 3 different frameworks: CrewAI, LangGraph, and Smolagents from Hugging Face.
The image below illustrates the flow of this use case. The client — in this case, the user — sends a request, which is first analyzed by the manager (CrewAI). The manager is responsible for delegating the request to the most suitable agent to handle it. In this scenario, there are 2 agents: the CRUD agent (LangGraph), responsible for create, read, update, and delete operations, and the metrics agent (Smolagents), responsible for performing metric calculations.

Hands-on Practice: Creating Agents with the A2A Protocol
Below are the Python version used and the requirements.txt file.
Before creating the agents, we need to set up the database that will support the operations in this use case. For this, we’ll use SQLite3.
In the terminal, run sqlite3 inventory.db (make sure SQLite3 is installed on your system) to create the database. Then, still in the terminal, execute the following command to create the products table:
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT,
price REAL NOT NULL,
stock_qty INTEGER NOT NULL DEFAULT 0,
launch_date TEXT,
is_active INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
To confirm the table was created successfully and inspect its details, run:
.tables
PRAGMA table_info(products);
Finally, exit the terminal with:
.exit
To create the agents with the A2A protocol, the python_a2a library was used. Below is the script (crud_agent.py) for the first agent, the CRUD agent, which is responsible for creating, retrieving, updating, and deleting information in the database.
from python_a2a import A2AServer, skill, agent, run_server, TaskStatus, TaskState
import sqlite3
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from typing import List, Tuple, Optional, Dict
from dotenv import load_dotenv
load_dotenv()
@agent(
name="CRUD Agent",
description=(
"An agent that manages product records, allowing creation, "
"retrieval, update, and deletion of products from the "
"inventory database."
),
version="1.0.0"
)
class CRUDAgent(A2AServer):
@skill(
name="Create Product",
description="Create a product",
tags=["create", "product"]
)
def create_product(self, product_name: str, category: str, price: float, stock_qty: int, launch_date: str, is_active: bool) -> str:
"""
Create a new product in the inventory database.
Args:
product_name (str): The name of the product.
category (str): The category of the product.
price (float): The price of the product.
stock_qty (int): The quantity of the product in stock.
launch_date (str): The launch date of the product. The format must be YYYY-MM-DD.
is_active (bool): Whether the product is active or not.
Returns:
A success message or an error message if the operation fails.
"""
try:
with sqlite3.connect("inventory.db") as con:
cur = con.cursor()
cur.execute(
"""
INSERT INTO products (name, category, price, stock_qty, launch_date, is_active)
VALUES (?, ?, ?, ?, ?, ?)
""",
(product_name, category, price, stock_qty, launch_date, is_active)
)
return "Product created successfully."
except sqlite3.Error as e:
return f"An error occurred: {e}"
@skill(
name="Get products",
description="Get products from the inventory database based on the provided fields.",
tags=["get", "products"]
)
def get_products(self, fields: Optional[Dict] = None, operator: Optional[str] = "=") -> List[Tuple] | str:
"""
Get products from the inventory database based on provided filters.
Args:
fields (Optional[Dict]): Dictionary of filters to apply. Supported keys include:
- name (str): The name of the product.
- category (str): The category of the product.
- price (float): The price of the product.
- stock_qty (int): The stock quantity.
- launch_date (str): The launch date in format YYYY-MM-DD.
- is_active (bool): Whether the product is active.
operator (Optional[str]): The comparison operator to use for filtering. Default is "=".
Supported operators include "=", "<", ">", "<=", ">=", "!=", "LIKE".
Examples:
- get_products(fields={"category": "Electronics", "is_active": True})
- get_products(fields={"price": 100.0})
- get_products(fields={"name": "Laptop"}, operator="LIKE")
- get_products()
Returns:
A list of tuples with the products that match the filters or an error message if the query fails.
"""
try:
with sqlite3.connect("inventory.db") as con:
cur = con.cursor()
query = "SELECT * FROM products"
if fields is not None:
query += " WHERE"
for column, value in fields.items():
if operator == "LIKE":
query += f" {column.lower()} {operator} '%{value.lower()}%' AND"
else:
query += f" {column} {operator} "
if type(value) == str:
query += f"'{value}' AND"
else:
query += f"{value} AND"
query = query[:-4]
cur.execute(query)
data = cur.fetchall()
return data
except sqlite3.Error as e:
return f"An error occurred: {e}"
@skill(
name="Delete a product",
description="Delete a product from the inventory database.",
tags=["delete", "product"]
)
def delete_product(self, product_id: Optional[int]=None, product_name: Optional[str]=None) -> str:
"""
Delete a product from the inventory database.
Args:
product_id (Optional[int]): The ID of the product to delete.
product_name (Optional[str]): The name of the product to delete.
Returns:
A success message or an error message if the operation fails.
"""
if product_id is None and product_name is None:
return "Please provide either a product ID or a product name to delete."
if product_id is not None and product_name is not None:
condition = f"id = {product_id} AND name = '{product_name}'"
elif product_id is not None:
condition = f"id = {product_id}"
else:
condition = f"name = '{product_name}'"
try:
with sqlite3.connect("inventory.db") as con:
cur = con.cursor()
cur.execute(f"DELETE FROM products WHERE {condition}")
return "Product deleted successfully."
except sqlite3.Error as e:
return f"An error occurred: {e}"
@skill(
name="Update a product",
description="Update a product in the inventory database.",
tags=["update", "product"]
)
def update_product(self, product_id: int, fields: Dict) -> str:
"""
Updates a product in the inventory database based on the provided fields.
Args:
product_id (int): The ID of the product to update.
fields (Dict): The fields to update. Supported keys include:
- name (str)
- category (str)
- price (float)
- stock_qty (int)
- launch_date (str, format YYYY-MM-DD)
- is_active (bool)
Example:
update_product(1, {"name": "New Product Name", "price": 99.99})
Returns:
A success message if the update is successful, or an error message if it fails.
"""
if product_id is None and not fields:
return "Please provide a product ID and at least one field to update."
elif product_id is None:
return "Please provide a product ID to update."
elif not fields:
return "Please provide at least one field to update."
try:
with sqlite3.connect("inventory.db") as con:
cur = con.cursor()
data = ", ".join(f"{k} = ?" for k in fields)
values = list(fields.values()) + [product_id]
cur.execute(f"UPDATE products SET {data} WHERE id = ?", values)
return "Product updated successfully."
except sqlite3.Error as e:
return f"An error occurred: {e}"
def handle_task(self, task):
message_data = task.message or {}
content = message_data.get("content", {})
text = content.get("text", "") if isinstance(content, dict) else ""
prompt = (
"You are an intelligent agent responsible for managing products in an inventory database.\n"
"Handle requests to create, retrieve, update, or delete products in a clear and helpful way.\n"
"Always perform the requested action and respond with a direct message indicating success or failure."
)
model = ChatOpenAI(
model="gpt-4.1-mini",
temperature=0.1
)
agent = create_react_agent(
model=model,
tools=[self.create_product, self.get_products, self.delete_product, self.update_product],
prompt=SystemMessage(content=prompt),
debug=True
)
resp = agent.invoke({"messages": text})
task.artifacts = [{
"parts": [{"type": "text", "text": resp['messages'][-1].content}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
return task
if __name__ == "__main__":
agent = CRUDAgent()
run_server(agent, port=5001)
Here’s an explanation of the code:
@agent: a decorator that indicates the class is an A2A protocol agent. You must provide the agent’s name, description, and version (since it acts as a server, versioning is possible). The class also inherits fromA2AServer.@skill: a decorator that marks class methods as agent skills (except forhandle_task). Each skill has a name, description, and tags, which act as metadata for discovery and external understanding (clients) of what the agent can do. Internally, the agent relies on the method name and docstring to decide which of its own skills to use.handle_task: the method responsible for processing tasks sent to the agent by the client. In this flow, the message is extracted from the task (task.message) and the corresponding text is retrieved. The agent then processes the request using its defined skills. Once the result is ready, the task is updated with an artifact of type TextPart and its status is set to COMPLETED. Finally, the task itself is returned.run_server: the function responsible for starting the agent as an A2A server. In this example, it runs on port 5001, meaning the agent will be available locally athttp://localhost:5001, allowing clients or other agents to connect to this endpoint.
The image below shows the details exposed by the agent when accessing the configured endpoint (http://localhost:5001). You can see the information defined in the script — such as version, protocol, endpoint, and registered skills — displayed in a structured way through the interface.

As mentioned in the Core Components of A2A section, you can access http://localhost:5001/.well-known/agent.json to view the agent card — the JSON metadata file exposed by the agent. Feel free to open this endpoint and explore the JSON.
Finally, it is worth noting that the CRUD agent was implemented using LangGraph.
Continuing with the agent creation, below is the code for the metrics agent. Unlike the CRUD agent, this one uses smolagents from Hugging Face, where the agent (CodeAgent) performs its actions through Python code. While an LLM is excellent at predicting the next word, it is not always reliable for mathematical calculations (which may lead to incorrect answers). This agent takes a different approach: instead of solving the calculation directly, it generates the Python code that executes the request, effectively avoiding such errors.
from python_a2a import A2AServer, agent, skill, run_server, TaskStatus, TaskState
import sqlite3
from pandas import DataFrame
from typing import Optional, Dict
from smolagents import CodeAgent, OpenAIServerModel, tool
from dotenv import load_dotenv
load_dotenv()
@tool
def get_products_tool(fields: Optional[Dict] = None, operator: Optional[str] = "=") -> DataFrame | str:
"""
Get products from the inventory database based on provided filters.
Args:
fields (Optional[Dict]): Dictionary of filters to apply. Supported keys include:
- name (str): The name of the product.
- category (str): The category of the product.
- price (float): The price of the product.
- stock_qty (int): The stock quantity.
- launch_date (str): The launch date in format YYYY-MM-DD.
- is_active (bool): Whether the product is active.
operator (Optional[str]): The comparison operator to use for filtering. Default is "=".
Supported operators include "=", "<", ">", "<=", ">=", "!=", "LIKE".
Examples:
- get_products(fields={"category": "Electronics", "is_active": True})
- get_products(fields={"price": 100.0})
- get_products(fields={"name": "Laptop"}, operator="LIKE")
- get_products()
Returns:
A list of tuples with the products that match the filters or an error message if the query fails.
"""
try:
with sqlite3.connect("inventory.db") as con:
cur = con.cursor()
query = "SELECT * FROM products"
if fields is not None:
query += " WHERE"
for column, value in fields.items():
if operator == "LIKE":
query += f" {column.lower()} {operator} '%{value.lower()}%' AND"
else:
query += f" {column} {operator} "
if type(value) == str:
query += f"'{value}' AND"
else:
query += f"{value} AND"
query = query[:-4]
cur.execute(query)
data = cur.fetchall()
columns = [desc[0] for desc in cur.description]
df = DataFrame(data, columns=columns)
return df
except sqlite3.Error as e:
return f"An error occurred: {e}"
@agent(
name="Metrics Agent",
description=(
"An agent that analyzes product data from the inventory database and returns relevant metrics, "
"such as averages, totals, counts, or performance insights based on price, stock levels, categories, "
"launch dates, and activity status."
),
version="1.0.0"
)
class MetricsAgent(A2AServer):
@skill(
name="Get products",
description="Get products from the inventory database based on the provided fields.",
tags=["get", "products"]
)
def get_products(self):
pass
def handle_task(self, task):
message_data = task.message or {}
content = message_data.get("content", {})
text = content.get("text", "") if isinstance(content, dict) else ""
prompt = (
"You are an intelligent agent responsible for generating metrics and insights based on product data from the inventory database.\n"
"When asked for any type of summary, total, average, or analytical information, respond with the correct calculation based on the available data.\n"
"Always perform the necessary computations and return concise, factual responses with the result of the analysis."
)
model = OpenAIServerModel(
model_id="gpt-4.1-mini",
temperature=0.1
)
agent = CodeAgent(
model=model,
max_steps=10,
tools=[get_products_tool],
additional_authorized_imports=["pandas", "numpy"],
verbosity_level=2
)
resp = agent.run(
task=prompt,
additional_args={"user_input": text}
)
task.artifacts = [{
"parts": [{"type": "text", "text": resp}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
return task
if __name__ == "__main__":
agent = MetricsAgent()
run_server(agent, port=5002)
Note that, unlike LangGraph, in smolagents it was necessary to create a function (get_products_tool) outside the agent class, using the library’s @tool decorator, so the agent could utilize the data retrieval tool. Then, for the manager agent (next agent) to recognize that the metrics agent has this capability, a get_products function was created with the @skill decorator. However, this function does absolutely nothing — its sole purpose is to expose the skill of the metrics agent, allowing the manager (or any other client/agent with access to the server) to be aware of this capability. To standardize all these tool calls, the best approach is to use Agent2Agent together with the Model Context Protocol (MCP), which will be covered in the next post.
Finally, here is the code for the last agent created: the manager, developed with CrewAI. It is responsible for interpreting the user’s input and orchestrating the use of the available agents to fulfill the request. Similar to the metrics agent, it also required creating a tool outside the class — in the case of the manager, this tool is responsible for delegating tasks.
from python_a2a import A2AServer, A2AClient, agent, skill, TaskStatus, TaskState, run_server
from crewai import Agent, Task, Crew, Process
from crewai.tools import tool
from dotenv import load_dotenv
load_dotenv()
AGENT_SERVERS = [
"http://localhost:5001",
"http://localhost:5002"
]
@tool("delegate_work")
def delegate_work_tool(url: str, user_input: str) -> str:
"""
Delegates work for a specific agent based on what was requested by the user
Args:
- url (str): url contained in the agent card
- user_input (str): user request
Returns:
- Agent response to task
"""
client = A2AClient(url)
response = client.ask(user_input)
return response
@agent(
name="Manager Agent",
description="Select the most suitable agent to fulfill the user's request.",
version="1.0.0"
)
class ManagerAgent(A2AServer):
def __init__(self):
super().__init__()
self.cards = []
for url in AGENT_SERVERS:
client = A2AClient(url)
self.cards.append(client.agent_card)
@skill(
name="Delegate work",
description="Delegate work to the most suitable agent based on the user's request.",
tags=["delegate", "work"]
)
def delegate_work(self):
pass
def handle_task(self, task):
message_data = task.message or {}
content = message_data.get("content", {})
text = content.get("text", "") if isinstance(content, dict) else ""
try:
manager = Agent(
role="Task Orchestrator Agent",
goal=(
"Understand the user's goal and delegate the request to the most suitable agent available, "
"ensuring the task is completed efficiently and accurately."
),
backstory=(
"You are an experienced orchestrator of intelligent agents. Your expertise lies in interpreting user inputs, "
"identifying the most capable specialized agent for each task, and delegating responsibilities effectively. "
"You are logical, decisive, and focused on matching problems to the right skills within your team. "
"You never perform the task yourself—you always assign it to the best-suited agent in your team."
),
tools=[delegate_work_tool],
llm="gpt-4.1",
verbose=True
)
except Exception as e:
print(f"Error creating manager agent: {e}")
manager_task = Task(
description=(
"User input: {user_input}\n\n"
"You are the orchestrator of a team of specialized agents. Your job is to understand the user's request, improve its clarity if needed, and delegate the execution to the right agent(s).\n\n"
"Instructions:\n"
"1. Carefully analyze the user's input and rewrite it in a clearer and more structured form for the subordinate agents.\n"
"2. If the user is requesting data retrieval, updates, or deletions:\n"
" a. Always perform a **validation step BEFORE executing the main task**.\n"
" b. Use the CRUD agent to retrieve available data (e.g., list of product names, categories, or IDs).\n"
" c. Try to match the user's input against this data — even if it's partially written, incomplete, or contains spelling variations.\n"
" d. If a clear match is found, **rewrite the user input accordingly** (e.g., change 'graphic tee urban' to 'Graphic Tee Urban Style').\n"
" e. Then, delegate the clarified request to the proper agent.\n\n"
"3. If the request is about creating/inserting a new item:\n"
" - Do NOT perform validations on whether the product already exists unless explicitly requested by the user.\n"
" - Assume the user is introducing a new product unless they say otherwise.\n"
"4. If the request involves metrics or calculations:\n"
" a. If the metric request is general (e.g., average price of all products), pass it directly to the metrics agent.\n"
" b. If the metric request contains ambiguous or fuzzy filters (e.g., 'books about Python'), first validate the referenced items using the CRUD agent.\n"
" c. Once you have confirmed the correct references (e.g., product names), **rewrite the request to include those exact values** as filters for the metrics agent.\n"
" d. Do not manually extract product data or replicate metric logic — the metrics agent will handle calculations.\n\n"
"5. If multiple agents are needed (e.g., validation + metrics), coordinate the steps.\n"
"6. Assign subtasks to the relevant agents using the available tools and skills.\n"
"7. Collect all outputs from the agents and formulate a final response that clearly and directly answers the user's request.\n\n"
"Available agents and their capabilities:\n"
f"{self.cards}\n\n"
"Important:\n"
"- You do not execute tasks yourself; you only orchestrate by delegating to other agents.\n"
"- Always rewrite the input in a more structured and helpful form before delegating.\n"
"- Always validate uncertain or fuzzy user input for GET/UPDATE/DELETE before execution.\n"
"- Your final answer must be clear and helpful based on the subordinate agents' outputs."
),
expected_output=(
"A clear and helpful response to the user's input, based on the output generated by the selected specialized agent."
),
agent=manager
)
crew = Crew(
agents=[manager],
tasks=[manager_task],
verbose=True,
process=Process.sequential
)
resp = crew.kickoff(inputs={"user_input": text})
task.artifacts = [{
"parts": [{"type": "text", "text": resp.raw}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
return task
if __name__ == "__main__":
agent = ManagerAgent()
run_server(agent, port=5000)
It is worth highlighting 2 important aspects of the manager script:
- To send requests to an agent, you need to instantiate a client using the
A2AClientclass. The request itself is made with theaskmethod. - Once the client is instantiated, it is also possible to access the agent’s metadata (agent card) through the
client.agent_cardproperty.
Testing and Results 1: Data Insertion
The goal of this section, and the following ones, is to verify, through log analysis, how the agents cooperate with each other to carry out the user’s request. The example below shows the code for a request where the user asks the system to insert a new product into the table.
from python_a2a import A2AClient
client = A2AClient("http://localhost:5000")
user_input = "Can you please add Wireless Earbuds Pro to the Electronics section? It costs 149.99, we have 50 in stock, launched on 2025-08-01, and it's active."
response = client.ask(user_input)
print(f"Response: {response}")
Note that the request is sent to the manager, since the server being used is http://localhost:5000. This means that the manager is the one responsible for orchestrating the agents to fulfill the request.
Below is a snippet from the manager, where it interprets the request, uses the task-delegation tool, and forwards the request to the CRUD agent via the URL http://localhost:5001.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The user wants to add a new product ("Wireless Earbuds Pro") to the Electronics section, with specified details. According to │
│ the instructions, for an insertion task, I should not validate if the product already exists; I should assume this is a new product. My next │
│ step is to delegate the structured creation request to the CRUD Agent. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Please create a new product with the following details: Name: Wireless Earbuds Pro, │
│ Category: Electronics, Price: 149.99, Stock: 50, Launch Date: 2025-08-01, Status: active.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Here is also a snippet from the CRUD agent log, where it uses the data insertion tool and reports back to the manager that the product was successfully added.
tool_calls=[{'name': 'create_product', 'args': {'product_name': 'Wireless Earbuds Pro', 'category': 'Electronics', 'price': 149.99, 'stock_qty': 50, 'launch_date': '2025-08-01', 'is_active': True}, 'id': 'call_fVyLZJWvm4o7aT8rTaPqWQcE', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='The product "Wireless Earbuds Pro" has been created successfully in the Electronics category with a price of 149.99, stock quantity of 50, and a launch date of 2025-08-01. The product is marked as active. Is there anything else you would like to do?' ...
To make the table more robust, I inserted 19 additional examples. [Click here to expand and view them all] ⬇️. Feel free to use them.
products = [
"I need to register a Classic White T-Shirt under Clothing, priced at 19.90. There are 100 units and it was launched on 2025-07-15. It's active.",
"Insert a product: The Art of War Book. It's from the Books category, price 9.99, stock 75, release date 2025-06-10, and it's active.",
"Please include Bluetooth Smartwatch X in Electronics, launched 2025-07-25. Price is 89.50, with 30 units available, and status should be active.",
"I'd like to create a new item: Men's Denim Jacket, category Clothing, 120.00 dollars, 15 in stock, launched 2025-08-10, marked active.",
"Register Digital Photography Guide under the Books category. It costs 24.00, stock is 40, launch date is 2025-05-20, and it's active.",
"Add this: Noise Cancelling Headphones, from Electronics. Launch: 2025-08-05, price: 249.90, quantity: 20, active: true.",
"We need a new product: Women's Summer Dress. Clothing category, costs 39.99, 80 units, released 2025-07-30, and it's active.",
"Create an entry for Python Programming for Beginners in Books. Price is 35.50, stock level is 60, launch 2025-06-01, active product.",
"Please log a new product: Portable Power Bank 10000mAh, Electronics, 29.90 dollars, 120 in stock, launched 2025-08-15, set as active.",
"I'd like to list a Cotton Hoodie Unisex under Clothing, launched 2025-07-05. Price: 54.99, 40 units, status: active.",
"Enter this book into the inventory: Modern Web Design Handbook. Category: Books, cost: 45.00, 25 available, launched 2025-06-18, mark active.",
"Add Wireless Keyboard & Mouse Combo as a new product in Electronics. Priced at 59.00, 90 items, release date: 2025-08-12, it's active.",
"New product alert: Slim Fit Chinos, goes into Clothing, cost is 64.90, stock 35, launch 2025-07-22, should be active.",
"Introduce Creative Writing Essentials into the Books category. Price is 27.75, stock count 55, date: 2025-05-28, active status.",
"I want to add Home Security Camera to the Electronics list. Set the price at 89.99, quantity 45, launched on 2025-08-17, and mark it as active.",
"Log Waterproof Rain Jacket in Clothing. Launch date is 2025-07-18, price: 99.50, 22 units, make sure it's active.",
"Put History of the Modern World in the Books section. 39.80 is the price, with 38 in stock, from 2025-06-06, active item.",
"Please insert 4K Action Camera under Electronics. Tag it with 119.00 price, 18 items, launch 2025-08-19, and it's active.",
"Could you add Graphic Tee Urban Style to Clothing? Set launch for 2025-08-03, priced 24.99, 70 units in stock, active = true."
]
Below is a screenshot of the products table after being populated.

Testing and Results 2: Data Retrieval
The goal of this section is to test data retrieval. To make it more challenging, the inputs were not provided exactly as they are written in the database. This means the system must interpret what the user wants, check how the information is stored, and then perform the task correctly.
The input used was: “What is the price and stock quantity of the wireless earbuds?”. Below is a snippet from the manager log. Notice that it first tries to check whether the product “wireless earbuds” matches any item in the table.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The user's request is to know the price and stock quantity of "wireless earbuds". Before proceeding, I need to validate if │
│ "wireless earbuds" matches any actual product in the inventory database by retrieving the product list using the CRUD Agent. I will then refine │
│ the request based on that data. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"List all products (with their names) in the inventory database to help validate the │
│ request for 'wireless earbuds'.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log for this first request made by the manager.
tool_calls=[{'name': 'get_products', 'args': {}, 'id': 'call_e8McYrZT9r3lRkk2pxRLiIT2', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='Here are the product names in the inventory database:\n\n1. Wireless Earbuds Pro\n2. Classic White T-Shirt\n3. The Art of War Book\n4. Bluetooth Smartwatch X\n5. Men\'s Denim Jacket\n6. Digital Photography Guide\n7. Noise Cancelling Headphones\n8. Women\'s Summer Dress\n9. Python Programming for Beginners\n10. Portable Power Bank 10000mAh\n11. Cotton Hoodie Unisex\n12. Modern Web Design Handbook\n13. Wireless Keyboard & Mouse Combo\n14. Slim Fit Chinos\n15. Creative Writing Essentials\n16. Home Security Camera\n17. Log Waterproof Rain Jacket\n18. History of the Modern World\n19. 4K Action Camera\n20. Graphic Tee Urban Style\n\nThe product "Wireless Earbuds Pro" is available in the inventory. How can I assist you further with this product?' ...
After retrieving the correct product name, the manager requests its price and stock quantity. Below is a snippet from the log.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The product matching "wireless earbuds" is "Wireless Earbuds Pro". The user wants the price and stock quantity for this item. │
│ I will now clearly structure the request and ask the CRUD Agent for the price and stock of "Wireless Earbuds Pro". │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Retrieve the price and stock quantity for the product named 'Wireless Earbuds Pro'.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log for this new request from the manager.
tool_calls=[{'name': 'get_products', 'args': {'fields': {'name': 'Wireless Earbuds Pro'}}, 'id': 'call_UP2A1kvgITe22h4v86ihZtuv', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='The price of the product "Wireless Earbuds Pro" is $149.99, and the stock quantity is 50.' ...
Finally, below is the final response returned by the system.
Response: The price of the "Wireless Earbuds Pro" is $149.99, and the stock quantity is 50.
Another test performed was: “Can you show me all the products available in the clothing category?”. Feel free to review the agents’ logs. Below is the system’s response.
Response: The available products in the category 'Clothing' are:
1. Classic White T-Shirt - $19.90, Stock: 100
2. Men's Denim Jacket - $120.00, Stock: 15
3. Women's Summer Dress - $39.99, Stock: 80
4. Cotton Hoodie Unisex - $54.99, Stock: 40
5. Slim Fit Chinos - $64.90, Stock: 35
6. Log Waterproof Rain Jacket - $99.50, Stock: 22
7. Graphic Tee Urban Style - $24.99, Stock: 70
If you need more details or want to take action on any of these clothing products, please let me know!
Testing and Results 3: Data Update
This section is analogous to the previous one, since once again the input uses a product name that differs from the one stored in the database. The input used was: “Change the category of the Python Programming book to ‘Education’ and mark it as inactive.”. Below is a snippet from the manager log.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The user is requesting to change the category of "Python Programming" book to 'Education' and mark it as inactive. Since this │
│ is an update operation, I must first validate and retrieve the exact product details using the CRUD Agent before proceeding. The validation │
│ step involves fetching products with a name matching "Python Programming" to ensure correct matching. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Get products where the name is similar to 'Python Programming'. Return the product │
│ name, category, ID, and is_active status.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log.
tool_calls=[{'name': 'get_products', 'args': {'fields': {'name': 'Python Programming'}, 'operator': 'LIKE'}, 'id': 'call_cOvmCd81YRizmxQ7ea20WTyt', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='I found a product with a name similar to "Python Programming." Here are the details:\n- Product Name: Python Programming for Beginners\n- Category: Books\n- ID: 9\n- Is Active: Yes\n\nIf you need more information or want to perform any actions on this product, please let me know.' ...
After identifying the correct product, the manager sends the update request. Below is the corresponding snippet from the log.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The product matching the user's intent is "Python Programming for Beginners" with the ID 9. The task requires updating this │
│ product's category to "Education" and setting it as inactive. I will now rewrite the update request in a clear and structured form referencing │
│ the exact product identity and delegate to the CRUD Agent. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Update the product with ID 9. Change category to 'Education' and set is_active to │
│ False.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log for this new request from the manager.
tool_calls=[{'name': 'update_product', 'args': {'product_id': 9, 'fields': {'category': 'Education'}}, 'id': 'call_ynMzXNTr7X1cjuw6K1Ha6wCo', 'type': 'tool_call'}, {'name': 'update_product', 'args': {'product_id': 9, 'fields': {'is_active': False}}, 'id': 'call_8YckGBqU8hGHEsxYkOYIRK9K', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content="The product with ID 9 has been updated. The category is now set to 'Education' and the product is marked as inactive." ...
To validate, by running the query SELECT * FROM products WHERE id = 9; you will see that the category is now “Education” and the product is no longer active.

To conclude, another test was performed with the input “Update the price of the noise cancelling headphones to 229.90 and set the stock to 100 units.”, and the system’s response was:
Response: The product "Noise Cancelling Headphones" has been successfully updated: the price is now 229.90, and the stock is set to 100 units.
Feel free to validate both the logs and the product in the database with the updated values.
Testing and Results 4: Data Deletion
This section is analogous to the previous two (data retrieval and data update). The first input used was: “Please delete the product called classic white T-Shirt from the inventory.”. Below is a snippet from the manager log.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The user wants to delete a product called "classic white T-Shirt" from the inventory. To follow protocol, I must first │
│ validate that this product exists and get its exact name or ID using the CRUD Agent's "Get products" skill, then delegate the deletion with │
│ precise details if a clear match is found. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Retrieve the list of all products, including their names and IDs, to validate and find │
│ the exact match for 'classic white T-Shirt'.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log for this first request from the manager.
tool_calls=[{'name': 'get_products', 'args': {}, 'id': 'call_dq74GJgQcr32PiqGCtrA8fCZ', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='The product "Classic White T-Shirt" is found in the inventory with the ID 2. How would you like to proceed with this product?' ...
Next, the manager requests the deletion of this product.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The product "Classic White T-Shirt" exists in the inventory with ID 2. The next step is to clearly instruct the CRUD Agent to │
│ delete this specific product by using its exact name and/or ID for unambiguous action. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"Delete the product with ID 2 (name: Classic White T-Shirt) from the inventory.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Below is a snippet from the CRUD agent log for the product deletion request made by the manager.
tool_calls=[{'name': 'delete_product', 'args': {'product_id': 2}, 'id': 'call_CBDuJwCj5eJGElprfRVKBaQO', 'type': 'tool_call'}]
{'agent': {'messages': [AIMessage(content='The product with ID 2 (Classic White T-Shirt) has been deleted from the inventory.' ...
In addition to this product deletion request, another one was made: “Remove that home security camera we added recently.”. The system’s response was:
Response: The "Home Security Camera" (ID: 16) has been successfully deleted from the inventory.
Feel free to validate the logs. To check directly in the database, run the query SELECT COUNT(*) AS total_products FROM products;. Initially, 20 products were inserted, but now only 18 remain.

Testing and Results 5: Performing Calculations
The goal of this section is to perform some calculations and observe how the manager orchestrates the agents, especially the metrics agent, which is specifically designed for this type of task. Since its actions involve executing Python code, it ensures more accurate results compared to other types of agents.
The first input was: “What’s the average price of products in the Electronics category?”. The manager’s first action was to ensure that the category “Electronics” is actually written that way in the database. To do this, it called the CRUD agent to perform the check. Feel free to review the log.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The user is requesting the average price of products in the "Electronics" category. To ensure accuracy, I need to validate if │
│ "Electronics" exactly matches a category in the database. First, I'll retrieve all product categories using the CRUD Agent. After confirming │
│ the precise matching category name, I will rephrase the request for the Metrics Agent, specifying the definite category filter. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5001\", \"user_input\": \"List all available product categories.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Output ───────────────────────────────────────────────────────────────────╮
│ │
│ The available product categories are: │
│ - Electronics │
│ - Books │
│ - Clothing │
│ - Education │
│ │
│ Let me know if you need information on products from any specific category. │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
After retrieving the category names from the database, the manager requests the calculation from the metrics agent, using the URL http://localhost:5002. Below is a snippet from the manager log showing this call.
╭───────────────────────────────────────────────────────────── 🔧 Agent Tool Execution ─────────────────────────────────────────────────────────────╮
│ │
│ Agent: Task Orchestrator Agent │
│ │
│ Thought: Thought: The category "Electronics" is a precise match from the available categories. I will now rephrase the task for the Metrics │
│ Agent as: "Calculate the average price of all products in the category 'Electronics'." I will delegate this structured request to the Metrics │
│ Agent. │
│ │
│ Using Tool: delegate_work │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────── Tool Input ────────────────────────────────────────────────────────────────────╮
│ │
│ "{\"url\": \"http://localhost:5002\", \"user_input\": \"Calculate the average price of all products in the category 'Electronics'.\"}" │
│ │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
To perform this calculation, the metrics agent first retrieves all products that belong to the specified category.
─ Executing parsed code: ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
products_electronics = get_products_tool(fields={"category": "Electronics"}, operator="=")
print(products_electronics)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Execution logs:
id name category price stock_qty launch_date is_active created_at
0 1 Wireless Earbuds Pro Electronics 149.99 50 2025-08-01 1 2025-09-01 10:15:54
1 4 Bluetooth Smartwatch X Electronics 89.50 30 2025-07-25 1 2025-09-01 10:27:55
2 7 Noise Cancelling Headphones Electronics 229.90 100 2025-08-05 1 2025-09-01 10:28:14
3 10 Portable Power Bank 10000mAh Electronics 29.90 120 2025-08-15 1 2025-09-01 10:28:33
4 13 Wireless Keyboard & Mouse Combo Electronics 59.00 90 2025-08-12 1 2025-09-01 10:28:54
5 19 4K Action Camera Electronics 119.00 18 2025-08-19 1 2025-09-01 10:29:35
Next, the metrics agent calculates the average.
─ Executing parsed code: ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
prices = [149.99, 89.50, 229.90, 29.90, 59.00, 119.00]
average_price = sum(prices) / len(prices)
final_answer(round(average_price, 2))
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Final answer: 112.88
Notice that the agent is not predicting the average, but rather generating Python code to perform the calculation. This approach ensures much greater accuracy compared to an estimate produced by an LLM model.
To validate in the database, run the query SELECT ROUND(AVG(price), 2) AS average_price FROM products WHERE category = 'Electronics';. Below is the execution result.

Another test: “What’s the total inventory value across all products?”. To validate, run the following query: SELECT SUM(price * stock_qty) AS total_inventory FROM products;. You will see that the result from the query and the system is the same: 65,626.
One important detail: the metrics agent may sometimes ignore the Python Programming book (which was marked as inactive during the update). In that case, the value returned by the system may be 63,496. Feel free to review the logs.
A final test was: “Can you calculate the weighted average price of all products based on stock quantity?”. To verify, run the query SELECT ROUND(SUM(price * stock_qty) / SUM(stock_qty), 2) AS weighted_average_price FROM products;. You will see that both results are 68.15.

Leave a comment