Creating Your Own Atomic Tool: A Comprehensive Guide
November 25, 2025 · View on GitHub
Introduction
This guide aims to explain the anatomy of an Atomic Tool within the Atomic Agents framework and help you get started adding your own tools, either to your own project or to the repository to share with others. We'll walk through the entire process step-by-step, using a mock Pizza Ordering Tool as our running example.
Prerequisites
Set up the main Atomic Agents library for development by following the instructions in the development setup guide.
The Principles
An Atomic Tool should always be self-contained and modular. This means it should encapsulate specific functionality, such as a calculator, a YouTube transcript scraper, or, in our case, a pizza ordering service. Each tool should be runnable both standalone and by an AI Agent, allowing for easy integration, maintenance, and extension.
An Atomic Tool should always have / be:
- Single Responsibility: Focuses on one specific task.
- Modular and Reusable: Easily integrated into different agents or applications.
- Self-Contained: Includes all necessary components to function independently.
- Clear Interfaces: Defines explicit input and output schemas for consistent data handling.
- Configurable: Allows customization through configuration settings.
- Executable Independently: Can run on its own or as part of an Atomic Agent.
Anatomy of an Atomic Tool
Folder Structure
Each tool should be placed in its own folder with the following structure:
tool_name/
│ .coveragerc
│ pyproject.toml
│ README.md
│ requirements.txt
│ uv.lock
│
├── tool/
│ │ tool_name.py
│ │ some_util_file.py
│ │ another_util_file.py
│
└── tests/
│ test_tool_name.py
│ test_some_util_file.py
│ test_another_util_file.py
To keep things modular and organized, you should place your tool code in the tool/ folder and your test code in the tests/ folder. Let's go over the important files:
-
pyproject.toml: This is the Python project file that contains metadata about your project and its dependencies. It is used by uv to install dependencies and manage the tool. Remember to runuv syncbefore starting development to ensure you are working in a clean, stand-alone environment. -
README.md: This file contains information about your tool, including how to use it, its purpose, environment variables, etc. Be sure to look at existing READMEs for examples. -
requirements.txt: This file lists just the runtime dependencies for your tool. These should match exactly the non-development dependencies inpyproject.toml, excluding thepythonversion specification. You should create this file manually to ensure it is clean and contains only the necessary runtime dependencies. -
.coveragerc: This is the coverage configuration file that contains the configuration for the coverage tool. This file is the same across all tools and should always be included. -
uv.lock: This is the lock file that contains the exact versions of the dependencies that were installed whenuv syncwas last run. This file should be committed as well to ensure consistency across different environments.
Creating requirements.txt
It's recommended to create the requirements.txt file manually. Your requirements.txt should include all the non-development dependencies specified in your pyproject.toml, excluding the python version.
Example pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "pizza-ordering-tool"
version = "1.0.0"
description = "A tool for placing and processing pizza orders"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"atomic-agents",
"pydantic>=2.8.2,<3.0.0",
"requests>=2.28.0,<3.0.0",
]
[dependency-groups]
dev = [
"coverage>=7.0.0,<8.0.0",
"pytest>=8.0.0,<9.0.0",
"pytest-cov>=5.0.0,<6.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"rich>=13.7.0,<14.0.0",
]
[tool.uv.sources]
atomic-agents = { workspace = true }
Corresponding requirements.txt:
atomic-agents>=1.0.0,<2.0.0
pydantic>=2.8.2,<3.0.0
requests>=2.28.0,<3.0.0
Explanation:
- The
requirements.txtincludes all the runtime dependencies specified under[project.dependencies]inpyproject.toml, excludingpython. - It does not include any of the development dependencies specified under
[dependency-groups.dev]. - This manual approach ensures that
requirements.txtis clean and only contains the necessary packages for running your tool.
Inheritance and Base Classes
To ensure consistency and interoperability within the Atomic Agents framework, your tool's classes should inherit from specific base classes:
- Input and Output Schemas: Must inherit from
BaseIOSchema. - Configuration Class: Must inherit from
BaseToolConfig. - Main Tool Class: Must inherit from
BaseTool.
By adhering to these inheritance rules upfront, you guarantee that your tool aligns with the framework's expectations and can seamlessly integrate with other components.
Overriding Title and Description
The BaseToolConfig allows you to override the default title and description of your tool. By default, the title and description are derived from the input schema's title and description, so usually, you will not need to set them.
However, there are certain edge cases where you may want to override the title and description, such as when the default title and description are not descriptive or clear enough for your agent.
One example of this could be when you have a tool that performs a web search and another that performs a search in a vector DB filled with internal company documents. You may want to override the title and description of these tools to make it clearer to the LLM that if a question is asked about the company's internal documents, the vector search tool is the appropriate tool to use and not the web search tool.
Modularity
Each tool should be an independent module that can function on its own. Modularity ensures that tools can be developed, tested, and debugged in isolation, leading to more reliable and maintainable code.
Reusability
Design tools with reusability in mind. Avoid hardcoding values or making assumptions that limit where and how the tool can be used.
Single Responsibility Principle
Ensure that each tool does one thing and does it well. This makes your tools easier to understand, test, and maintain.
Clear Interfaces
Define explicit input and output schemas using Pydantic models. This ensures consistent data handling and makes your tools predictable and reliable.
Configurability
Provide configuration options to allow users to customize the tool's behavior without modifying the code. Use default values where appropriate to make tools easy to use out of the box.
Structure
An Atomic Tool in the Atomic Agents framework follows a highly standardized structure. This consistency is crucial for maintainability, interoperability, and ease of understanding.
The structure of an Atomic Tool includes the following sections, in this specific order:
- Imports
- Input Schema
- Output Schema(s)
- Configuration
- Main Tool & Logic
- Example Usage
Remember: The input and output schemas must always extend from BaseIOSchema, and the main tool class must always extend from BaseTool.
Note: Each section (except imports) should be clearly delineated with a comment block that includes the section's name. For example:
################
# Input Schema #
################
Let's explore each of these sections in detail, using our Pizza Ordering Tool as the example.
1. Imports
All necessary imports should be placed at the top of your script. This includes standard libraries, third-party packages, and modules from the Atomic Agents framework.
Example:
import os
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field
from atomic_agents import BaseIOSchema, BaseTool, BaseToolConfig
2. Input Schema
The Input Schema defines the structure and validation rules for the data your tool accepts. It uses Pydantic models to enforce data types, provide default values, and include descriptions.
Composing Input Schemas
Input schemas can be composed of multiple models, including Enums and other Pydantic models, to create complex and structured inputs.
Example:
################
# Input Schema #
################
class PizzaSize(Enum):
SMALL = "Small"
MEDIUM = "Medium"
LARGE = "Large"
class CrustType(Enum):
THIN = "Thin"
THICK = "Thick"
STUFFED = "Stuffed"
class Topping(BaseModel):
name: str = Field(..., description="Name of the topping.")
extra_cheese: bool = Field(False, description="Add extra cheese to this topping.")
class PizzaOrderInputSchema(BaseIOSchema):
"""
Captures customer details and order specifics for placing a pizza order.
"""
customer_name: str = Field(..., description="Name of the customer placing the order.")
pizza_type: str = Field(..., description="Type of pizza to order (e.g., Margherita, Pepperoni).")
size: PizzaSize = Field(..., description="Size of the pizza.")
crust: CrustType = Field(..., description="Type of crust for the pizza.")
toppings: Optional[List[Topping]] = Field(None, description="List of additional toppings.")
quantity: int = Field(..., description="Number of pizzas to order.")
Explanation:
- Enums:
PizzaSizeandCrustTypeare Enums that restrict input to predefined choices. - Nested Models:
Toppingis its own Pydantic model used withinPizzaOrderInputSchema. - Fields with Descriptions: Each field uses
Fieldwith a description for clarity. - Inheritance: The
PizzaOrderInputSchemainherits fromBaseIOSchema.
3. Output Schema(s)
The Output Schema(s) define the structure of the data your tool outputs. You can have multiple output schemas if your tool produces different types of outputs.
Example:
#####################
# Output Schema(s) #
#####################
class OrderStatus(Enum):
PENDING = "Pending"
CONFIRMED = "Confirmed"
DELIVERED = "Delivered"
class OrderConfirmationSchema(BaseIOSchema):
"""
Confirmation details of the placed order.
"""
order_id: str = Field(..., description="Unique identifier for the order.")
estimated_delivery_time: str = Field(..., description="Estimated time for order delivery.")
status: OrderStatus = Field(..., description="Current status of the order.")
class PaymentDetailsSchema(BaseIOSchema):
"""
Payment information for the order.
"""
amount: float = Field(..., description="Total amount to be paid.")
currency: str = Field("USD", description="Currency of the payment.")
payment_status: str = Field(..., description="Status of the payment (e.g., Paid, Pending).")
Explanation:
- Multiple Output Schemas: Separate schemas for order confirmation and payment details.
- Enums in Output:
OrderStatusEnum represents the status of the order. - Inheritance: Both output schemas inherit from
BaseIOSchema.
4. Configuration
The Configuration section allows you to define settings that can customize the tool's behavior.
Example:
#################
# Configuration #
#################
class PizzaOrderingToolConfig(BaseToolConfig):
"""
Configuration for the PizzaOrderingTool.
"""
api_endpoint: str = Field(
default="https://api.pizzaorders.com/v1/orders",
description="API endpoint for processing pizza orders."
)
supported_pizzas: List[str] = Field(
default=["Margherita", "Pepperoni", "Veggie", "Hawaiian"],
description="List of supported pizza types."
)
api_key: str = Field(
default=os.getenv("PIZZA_API_KEY"),
description="API key for authenticating with the pizza ordering service."
)
title: Optional[str] = Field(
default="Pizza Ordering Tool",
description="Override the default title of the tool."
)
description: Optional[str] = Field(
default="A tool to place pizza orders and process payments.",
description="Override the default description of the tool."
)
Explanation:
- Inheritance: The configuration class inherits from
BaseToolConfig. - Environment Variables: Sensitive information like
api_keyis retrieved from environment variables. - Title and Description Overrides: Using
titleanddescriptionfields to override default values.
5. Main Tool & Logic
This is the core of your tool, where you implement the main functionality. Your tool must always have a run method, which is the entry point for executing your tool's logic.
Example:
#####################
# Main Tool & Logic #
#####################
class PizzaOrderingTool(BaseTool[PizzaOrderInputSchema, OrderConfirmationSchema]):
"""
Tool for placing pizza orders through the Pizza Orders API.
"""
def __init__(self, config: PizzaOrderingToolConfig = PizzaOrderingToolConfig()):
"""
Initializes the PizzaOrderingTool with the provided configuration.
"""
super().__init__(config)
self.api_endpoint = config.api_endpoint
self.supported_pizzas = config.supported_pizzas
self.api_key = config.api_key
self.tool_name = config.title or self.input_schema.__name__
self.tool_description = config.description or self.__doc__
def run(self, params: PizzaOrderInputSchema) -> dict:
"""
Executes the tool's main logic to place an order and process payment.
"""
# Validate pizza type
if params.pizza_type not in self.supported_pizzas:
raise ValueError(f"Pizza type '{params.pizza_type}' is not supported.")
# Simulate placing the order
order_id = self.place_order(params)
estimated_time = self.get_estimated_delivery_time(order_id)
amount = self.calculate_payment(params)
payment_status = self.process_payment(order_id, amount)
# Prepare outputs
confirmation = OrderConfirmationSchema(
order_id=order_id,
estimated_delivery_time=estimated_time,
status=OrderStatus.CONFIRMED
)
payment = PaymentDetailsSchema(
amount=amount,
payment_status=payment_status
)
return {
"confirmation": confirmation,
"payment": payment
}
def place_order(self, params: PizzaOrderInputSchema) -> str:
"""
Simulates placing an order and returns an order ID.
"""
# Placeholder logic; in reality, this would involve an API call.
return "ORD123456"
def get_estimated_delivery_time(self, order_id: str) -> str:
"""
Simulates retrieving the estimated delivery time.
"""
# Placeholder logic.
return "30 minutes"
def calculate_payment(self, params: PizzaOrderInputSchema) -> float:
"""
Calculates the total amount to be paid.
"""
base_prices = {
"Margherita": 8.99,
"Pepperoni": 9.99,
"Veggie": 10.99,
"Hawaiian": 9.49
}
size_multipliers = {
PizzaSize.SMALL: 1.0,
PizzaSize.MEDIUM: 1.2,
PizzaSize.LARGE: 1.5
}
topping_price = 0.99 # Price per additional topping
base_price = base_prices[params.pizza_type]
size_multiplier = size_multipliers[params.size]
toppings_cost = sum(
topping_price + (0.5 if topping.extra_cheese else 0)
for topping in params.toppings
) if params.toppings else 0
total = (base_price * size_multiplier + toppings_cost) * params.quantity
return total
def process_payment(self, order_id: str, amount: float) -> str:
"""
Simulates payment processing.
"""
# Placeholder logic.
return "Paid"
Explanation:
- Inheritance: The tool class inherits from
BaseToolwith generic type parameters. - Run Method: The
runmethod is mandatory and serves as the entry point for your tool. - Title and Description Overrides: Uses the configuration's
titleanddescription. - Helper Methods: Break down complex logic into smaller methods for clarity and maintainability.
- Error Handling: Validates inputs and raises exceptions where necessary.
6. Example Usage
Finally, the Example Usage section demonstrates how to instantiate and run your tool. This is invaluable for testing and serves as documentation for users.
Example:
#################
# Example Usage #
#################
if __name__ == "__main__":
from rich.console import Console
console = Console()
pizza_tool = PizzaOrderingTool()
order_input = PizzaOrderInputSchema(
customer_name="Jane Smith",
pizza_type="Veggie",
size=PizzaSize.MEDIUM,
crust=CrustType.THIN,
toppings=[
Topping(name="Olives", extra_cheese=False),
Topping(name="Mushrooms", extra_cheese=True)
],
quantity=2
)
try:
outputs = pizza_tool.run(order_input)
console.print(outputs)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
Best Practices and Guidelines
Do's
- Keep Imports at the Top: Place all import statements at the beginning of your script without a section comment block.
- Follow the Standard Structure: Adhere to the prescribed order and format for consistency.
- Use Clear and Descriptive Names: Make your code self-explanatory.
- Document Your Code: Include docstrings and comments to explain the purpose and functionality.
- Validate Inputs Thoroughly: Use Pydantic's features to enforce data integrity.
- Handle Errors Gracefully: Provide informative error messages and handle exceptions where appropriate.
- Keep Functions Small and Focused: Break down complex logic into helper methods.
- Commit
uv.lock: Ensure you commit youruv.lockfile to maintain consistent dependencies across environments. - Manually Create
requirements.txt: Ensure yourrequirements.txtfile matches the non-development dependencies in yourpyproject.toml, excludingpython, and create it manually for clarity.
Don'ts
- Don't Hardcode Values: Use configuration parameters or environment variables instead.
- Avoid Global Variables: Keep the state within your tool's scope.
- Don't Overcomplicate: Stick to the tool's single responsibility; avoid adding unrelated features.
- Don't Ignore Security: Be cautious with sensitive information like API keys; use environment variables.
- Don't Neglect Performance: Optimize your code, especially when dealing with large datasets or external API calls.
- Don't Skip Input Validation: Never assume inputs are valid; always validate.
- Don't Auto-Generate
requirements.txt: Avoid using automated tools that may include unnecessary or development dependencies; manually specify only the required runtime dependencies.
Best of luck creating your own Atomic Tools!