Skip to content

How do we implement tools that "do something" #83

@samuelcolvin

Description

@samuelcolvin

Currently retrievers are tools that are expected to be benign, e.g. have no side effects, so you don't care if the models chooses to call them or not.

Technically there's nothing to stop you from having retrievers with side-effects, and you could even look in message history to see if they were called.

But what is our recommend way of using tools that do something?

I would suggest something like this:

from typing import Literal

from pydantic import BaseModel

from pydantic_ai import Agent


class CreateTicket(BaseModel):
    title: str
    description: str

    async def run(self):
        ...


class DeleteTicket(BaseModel):
    reason: str

    async def run(self):
        ...


class ChangeSeatTicket(BaseModel):
    row: int
    seat: Literal['A', 'B', 'C', 'D']

    async def run(self):
        ...


ticket_agent = Agent(result_type=CreateTicket | DeleteTicket | ChangeSeatTicket)


async def main():
    result = await ticket_agent.run('I would like to create a ticket')
    await result.data.run()

This looks like a bit more logic, but it has some nice advtanges:

  • you can be very clear about when/if the action is called
  • it can take more arguments including agents, or dependencies that let you call other agents
  • with this already, each pydantic base model is registered as a tool in the LLM, so it's just as easy for the model
  • you could introduce arbitrary extra logic and control flow to run without it being hidden in the "magic" of PydanticAI

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions