The practical construction of LLM agent tools using Python is essential for effective agent development. There are two primary ways to structure custom tools: as straightforward Python functions or as more organized Python classes. The choice between these methods depends on the complexity of the task the tool needs to perform and whether it needs to remember information across multiple uses.Python's readability and extensive libraries make it an excellent choice for developing tools for LLM agents. Whether you're performing a simple calculation, querying a database, or interacting with a web service, Python provides the necessary building blocks.Tools as Python FunctionsFor many tasks, a simple Python function is all you need to create an effective tool. Functions are ideal for tools that are:Stateless: They don't need to remember any information from previous calls. Each invocation is independent.Focused: They perform a single, well-defined operation.Simple Input/Output: They take clear inputs and produce a predictable output.Think of a function-based tool as a specialist that does one thing very well and doesn't need to keep notes between jobs.Structure of a Function-Based ToolA well-structured function tool should include:A Clear Signature: Define your function with explicit parameter names and, crucially, Python type hints. Type hints improve code clarity and can be used by LLM agent frameworks to understand expected data types.A Detailed Docstring: This is critical. The docstring is often the primary source of information for the LLM (or the agent framework managing the LLM) to understand what the tool does, what arguments it expects, and what it returns. A good docstring for a tool should clearly describe:The overall purpose of the tool.Each parameter: its name, type, and what it represents.The return value: its type and what it represents.Core Logic: The actual Python code that performs the tool's action.A Well-Defined Return Value: The function should return data in a consistent and predictable format that the LLM can easily parse and use.Let's look at an example. Suppose we want a tool that can calculate the area of a rectangle.def calculate_rectangle_area(length: float, width: float) -> float: """ Calculates the area of a rectangle. Args: length (float): The length of the rectangle. Must be a positive number. width (float): The width of the rectangle. Must be a positive number. Returns: float: The calculated area of the rectangle. """ if length <= 0 or width <= 0: # It's good practice to handle invalid inputs, # though more error handling will be covered later. raise ValueError("Length and width must be positive numbers.") return length * width # Example usage (not part of the tool itself, but for demonstration) # area = calculate_rectangle_area(10.0, 5.0) # print(f"The area is: {area}")In this calculate_rectangle_area tool, the docstring clearly tells the LLM (and any human developer) its purpose, the expected length and width arguments (including their types and a constraint), and what it will return. The type hints (length: float, width: float, -> float) provide additional structural information.Advantages of Function-Based ToolsSimplicity: They are easy to write, understand, and test.Lightweight: Minimal boilerplate code is required.Directness: Good for tasks where the LLM needs a quick, direct action performed.LimitationsStatelessness: If a tool needs to maintain information or context across multiple invocations (e.g., remembering previous steps in a conversation or accumulating data), a simple function is not sufficient.Organization: For tools with multiple related operations or complex internal logic, a single function can become unwieldy.When your tool's requirements exceed these limitations, it's time to consider using Python classes.Tools as Python ClassesWhen you need to build tools that are stateful, group related functionalities, or require more complex setup, Python classes offer an organized approach. A class can encapsulate both data (state) and the methods (behaviors) that operate on that data.Think of a class-based tool as a more versatile worker who can remember past interactions, manage their own resources, and offer a suite of related services.When to Use Classes for ToolsState Management: The tool needs to remember information between calls. For example, a tool that connects to a database might maintain the connection object as part of its state.Grouping Related Operations: A class can offer multiple methods, each acting as a distinct (but related) capability of the tool. For instance, a FileManager tool might have methods to read_file, write_file, and list_directory_contents.Complex Initialization: If the tool requires significant setup (e.g., loading configuration, initializing external clients) before it can be used, the class constructor (__init__) is the natural place for this.Resource Management: Classes can manage resources (like network connections or file handles) more effectively using methods like __enter__ and __exit__ for context management, ensuring resources are properly acquired and released.Structure of a Class-Based ToolThe Class Definition: Start with the class keyword.The __init__ Method (Constructor): This method is called when an instance of the class is created. Use it for:Initializing any internal state (instance attributes).Performing one-time setup operations.Accepting configuration parameters that the tool might need.Methods as Tool Capabilities: Public methods of the class typically represent the specific actions the LLM can invoke. Each such method should:Have a clear signature with type hints.Include a detailed docstring explaining its specific purpose, arguments, and return value. This is what the LLM will "see" for that particular action.Class Docstring: Provide a docstring for the class itself, describing the overall purpose and capabilities of the tool suite it represents.Internal State: Store data that needs to persist across method calls as instance attributes (e.g., self.my_data).Let's consider a UserProfileTool that can store and retrieve simple user preferences.class UserProfileTool: """ Manages simple user profile information like name and preferred city. This tool allows setting and getting these preferences. """ def __init__(self): """Initializes an empty user profile.""" self._name: str | None = None self._preferred_city: str | None = None print("UserProfileTool initialized.") # For demonstration def set_preference(self, key: str, value: str) -> str: """ Sets a user preference. Currently supports 'name' and 'preferred_city'. Args: (str): The preference (e.g., 'name', 'preferred_city'). value (str): The value for the preference. Returns: str: A confirmation message. """ if key == "name": self._name = value return f"User name set to '{value}'." elif key == "preferred_city": self._preferred_city = value return f"Preferred city set to '{value}'." else: return f"Unknown preference: '{key}'. Supported keys are 'name', 'preferred_city'." def get_preference(self, key: str) -> str | None: """ Retrieves a user preference. Args: preference (str): The preference to retrieve (e.g., 'name', 'preferred_city'). Returns: str | None: The value of the preference, or None if not set or the preference is unknown. """ if key == "name": return self._name elif key == "preferred_city": return self._preferred_city else: # For unknown keys, agent frameworks might prefer an error or a specific message. # Here we return None, but you might also raise an error or return a message. print(f"Attempted to get unknown preference: {key}") return None # Example usage: # profile_tool = UserProfileTool() # print(profile_tool.set_preference("name", "Alex")) # print(profile_tool.set_preference("preferred_city", "New York")) # print(f"User's name: {profile_tool.get_preference('name')}") # print(f"User's city: {profile_tool.get_preference('preferred_city')}") # print(f"User's favorite_color: {profile_tool.get_preference('favorite_color')}")In this UserProfileTool, the __init__ method initializes _name and _preferred_city (internal state prefixed with an underscore by convention to indicate they are for internal use, though still accessible). The set_preference and get_preference methods operate on this state. Each method has its own docstring, clearly defining its function for the LLM agent. An LLM would be presented with set_preference and get_preference as available actions within the UserProfileTool.Advantages of Class-Based ToolsState Management: Classes are the natural way to build tools that need to maintain state.Encapsulation: They bundle data and methods that operate on that data, leading to cleaner, more maintainable code.Organization: Excellent for grouping related functionalities under a single tool umbrella.Reusability: Class instances can be created and used multiple times, potentially with different initial configurations.OverviewInstantiation: You need to create an instance of the class before its methods can be used as tools. Agent frameworks usually handle this instantiation based on your tool registration.Complexity: For very simple, stateless tasks, a class might be overkill compared to a standalone function.Choosing Your Approach: Functions vs. ClassesThe decision of whether to implement a tool as a Python function or a class hinges on its requirements, particularly around state and complexity.digraph G { rankdir=TB; fontname="Arial"; node [shape=box, style="filled", fillcolor="#e9ecef", fontname="Arial", margin="0.1,0.1"]; edge [fontname="Arial", fontsize=10]; start [label="Begin Tool Implementation", shape=ellipse, fillcolor="#4dabf7", style="filled,rounded", fontcolor="white"]; needs_state [label="Does the tool need\nto remember information\nbetween calls (state)?", shape=diamond, fillcolor="#ffe066", style="filled,rounded"]; multiple_ops [label="Does the tool offer\nmultiple related operations\nor require complex setup?", shape=diamond, fillcolor="#ffe066", style="filled,rounded"]; use_function [label="Implement as a\nPython Function", shape=box, fillcolor="#69db7c", style="filled,rounded", fontcolor="white", peripheries=2]; use_class [label="Implement as a\nPython Class", shape=box, fillcolor="#748ffc", style="filled,rounded", fontcolor="white", peripheries=2]; start -> needs_state; needs_state -> use_class [label=" Yes "]; needs_state -> multiple_ops [label=" No "]; multiple_ops -> use_class [label=" Yes "]; multiple_ops -> use_function [label=" No "]; }A decision guide for choosing between Python functions and classes for tool implementation. If a tool requires state or groups multiple related operations, a class is generally preferred. Otherwise, a simpler function may suffice.Here's a quick summary:Use a function if:The tool is stateless.It performs a single, atomic operation.Initialization is trivial.Use a class if:The tool needs to maintain state across calls.You want to group several related operations together (as methods).The tool requires non-trivial setup or resource management.The Critical Role of Docstrings and Type HintsRegardless of whether you choose a function or a class, two elements are consistently important for making your Python code usable as an LLM tool: docstrings and type hints.Docstrings: As emphasized earlier, LLM agent frameworks often parse these to understand what your tool does. The LLM itself relies on these descriptions to:Select the right tool: When faced with a user request, the LLM needs to match the request to the capabilities described in your tool's docstring.Determine arguments: The docstring should clearly explain what arguments are needed, their purpose, and their expected format.Understand output: Knowing what the tool returns helps the LLM process the result. A poorly written or missing docstring can lead to the LLM misusing the tool, failing to use it when appropriate, or providing incorrect arguments.Type Hints: Python's type hints (e.g., name: str, count: int, -> list[str]) serve multiple purposes:Developer Clarity: They make your code easier to read and understand for humans.Static Analysis: Tools like MyPy can check for type inconsistencies.LLM Frameworks: Many agent frameworks can leverage type hints to:Automatically generate schemas (like JSON Schema) that describe the tool's input and output structure. This is often more reliable than parsing natural language from docstrings alone.Perform validation on the data passed to and from the tool.# Good use of type hints and docstrings def search_knowledge_base(query: str, filters: list[str] | None = None) -> list[dict]: """ Searches the knowledge base for articles matching the query. Can optionally apply filters. Args: query (str): The search term or question. filters (list[str] | None, optional): A list of filter strings to apply (e.g., ['category:technical', 'tag:python']). Defaults to None (no filters). Returns: list[dict]: A list of search results, where each result is a dictionary containing 'title', 'summary', and 'url'. Returns an empty list if no results are found. """ # ... implementation details ... print(f"Searching for: {query} with filters: {filters}") # Placeholder if query == "python tools": return [ {"title": "Python Functions as Tools", "summary": "...", "url": "/ch2/functions"}, {"title": "Python Classes for Tools", "summary": "...", "url": "/ch2/classes"} ] return []In this example, the combination of query: str and its docstring explanation gives the LLM precise information. Similarly, -> list[dict] along with its description in the "Returns" section of the docstring prepares the LLM for the structure of the output.Connecting to LLM Agent FrameworksWhile we are focusing on writing the Python code for the tools themselves, it's useful to keep in mind how these functions and classes become "active" tools for an LLM. Typically, you'll use an LLM agent framework (such as LangChain, LlamaIndex, or even a custom-built one). These frameworks provide mechanisms to:Register your tools: You'll "tell" the framework about your Python functions or class methods that are intended to be tools.Expose tool descriptions: The framework often introspects your code (reading docstrings, type hints) to create a structured description of each tool that the LLM can understand.Handle tool invocation: When the LLM decides to use a tool, the framework manages the execution of your Python code, passing in the arguments provided by the LLM and returning the results.We won't go into the specifics of any single framework in this section, but understanding this general workflow helps contextualize why clear function/method signatures, comprehensive docstrings, and accurate type hints are so important. They are the bridge between your Python code and the LLM's decision-making process.By mastering the implementation of tools as both Python functions and classes, you gain the flexibility to create a wide range of capabilities for your LLM agents. As we proceed, we'll build on these foundation structures to add more advanced features like state management, external service interaction, and strong error handling.