Skip to content

Tools on objects and from data

Give the agent tools showed @tool on a function. This page covers the other ways to make a tool.

Tools on an object

@tool works on methods. Pass the object to give the Agent every @tool method, or pass one method:

from pathlib import Path

from alpineagents import Agent, tool


class Workspace:
    def __init__(self, root: str):
        self.root = Path(root)

    @tool
    def read_file(self, path: str) -> str:
        """Read a file in the workspace"""
        return (self.root / path).read_text()

    @tool
    def list_files(self) -> list[str]:
        """List the files in the workspace"""
        return sorted(p.name for p in self.root.iterdir())


workspace = Workspace("my-repo")
agent = Agent(model="claude-sonnet-5", tools=[workspace])
reader = Agent(model="claude-sonnet-5", tools=[workspace.read_file])
  1. workspace in tools= gives the model read_file and list_files.
  2. workspace.read_file gives it only that one.
  3. The model does not see self.

Use an object when tools share settings, such as a root folder or a client.

Tools from data

@tool needs a Python function with type hints. For a tool whose name, description and input schema come as data, such as rows in a database or an OpenAPI spec, subclass Tool:

import httpx

from alpineagents import Agent, State, Tool, ToolError, ToolInputError

rows = [  # for example from a database or an OpenAPI spec
    {
        "name": "create_ticket",
        "description": "Create a support ticket",
        "schema": {
            "type": "object",
            "properties": {"title": {"type": "string", "description": "A short title"}},
            "required": ["title"],
        },
        "url": "https://example.com/hooks/tickets",
    },
]


class Webhook(Tool):
    def __init__(self, name: str, description: str, schema: dict, url: str):
        super().__init__(name=name, description=description, input_schema=schema, open_world=True)
        self.url = url

    def run(self, args: dict, state: State) -> dict:
        if "title" not in args:
            raise ToolInputError("title is required")
        response = httpx.post(self.url, json=args)
        if response.status_code >= 400:
            raise ToolError(f"HTTP {response.status_code}")
        return response.json()


agent = Agent(model="claude-sonnet-5", tools=[Webhook(**row) for row in rows])
print(agent.run("Open a ticket: the login page is down"))
  1. super().__init__ takes name, description, input_schema (an object schema, left out for no arguments), parallel and the hints.
  2. run(args, state) gets the model's arguments as a dict. They are not checked against input_schema, so check what you rely on and raise ToolInputError when it is wrong.
  3. What run returns and raises works as for @tool: see Tool calls. run can be async def.

@tool makes a FunctionTool and an MCP server's tool is an MCPTool. Both are Tool subclasses.

Images from a tool

Return an Image for a tool whose result the model should look at, such as a screenshot or a chart. Put it in a list to send text with it:

from alpineagents import Agent, Image, tool


@tool
def show_chart(path: str) -> list:
    """Look at a chart image"""
    return [f"The chart in {path}", Image.from_path(path)]


agent = Agent(model="claude-sonnet-5", tools=[show_chart])
print(agent.run("What does sales.png show?"))
  • Image(data) takes the file's bytes. Image.from_path("chart.png") reads a file, and Image.from_base64(text) decodes base64 text. PNG, JPEG, GIF and WebP work. The type is read from the bytes, or pass media_type="image/png".
  • In the list, a str is text, and anything else that is not an Image is sent as JSON text.
  • The model must read images. Anthropic takes them in the tool result. OpenAICompatible sends them in a user message right after it.
  • An image counts as 1,600 tokens, so state.clear_tool_results() clears old ones with the text results (Keep the context small). History and a store keep them.
  • A Reporter gets the result as a tuple of blocks. result_text(result) from alpineagents.types makes it one line.
  • To send an image from the person, see Keep the conversation.

Tools that must not overlap

A reply can ask for several calls. Tools with parallel=True (the default) run at the same time. Then tools with parallel=False run one at a time, in the order the model asked. Use parallel=False for two writes to the same file:

from alpineagents import tool


@tool(parallel=False)
def append_line(path: str, line: str) -> None:
    """Add a line to the end of a file"""
    with open(path, "a") as file:
        file.write(line + "\n")

Test a tool alone

A @tool function runs as a plain function: read_file("missing.py") raises its ToolError. A Tool subclass has run(args, state): Webhook(**row).run({"title": "x"}, State()). To test a tool inside a run, see Test your agent.