Define and customize tools

You can create custom tools in Letta using the Python SDK, as well as via the ADE tool builder.

For your agent to call a tool, Letta constructs an OpenAI tool schema (contained in json_schema field) from the function you define. Letta can either parse this automatically from a properly formatting docstring, or you can pass in the schema explicitly by providing a Pydantic object that defines the argument schema.

Creating a custom tool

Specifying tools via Pydantic models

To create a custom tool, you can extend the BaseTool class and specify the following:

  • name - The name of the tool
  • args_schema - A Pydantic model that defines the arguments for the tool
  • description - A description of the tool
  • tags - (Optional) A list of tags for the tool to query You must also define a run(..) method for the tool code that takes in the fields from the args_schema.

Below is an example of how to create a tool by extending BaseTool:

python
1from letta_client import Letta
2from letta_client.client import BaseTool
3from pydantic import BaseModel
4from typing import List, Type
5
6class InventoryItem(BaseModel):
7 sku: str # Unique product identifier
8 name: str # Product name
9 price: float # Current price
10 category: str # Product category (e.g., "Electronics", "Clothing")
11
12class InventoryEntry(BaseModel):
13 timestamp: int # Unix timestamp of the transaction
14 item: InventoryItem # The product being updated
15 transaction_id: str # Unique identifier for this inventory update
16
17class InventoryEntryData(BaseModel):
18 data: InventoryEntry
19 quantity_change: int # Change in quantity (positive for additions, negative for removals)
20
21
22class ManageInventoryTool(BaseTool):
23 name: str = "manage_inventory"
24 args_schema: Type[BaseModel] = InventoryEntryData
25 description: str = "Update inventory catalogue with a new data entry"
26 tags: List[str] = ["inventory", "shop"]
27
28 def run(self, data: InventoryEntry, quantity_change: int) -> bool:
29 print(f"Updated inventory for {data.item.name} with a quantity change of {quantity_change}")
30 return True
31
32# create a client to connect to your local Letta Server
33client = Letta(
34 base_url="http://localhost:8283"
35)
36# create the tool
37tool_from_class = client.tools.add(
38 tool=ManageInventoryTool(),
39)

Specifying tools via function docstrings

You can create a tool by passing in a function with a properly formatting docstring specifying the arguments and description of the tool:

python
1# install letta_client with `pip install letta-client`
2from letta_client import Letta
3
4# create a client to connect to your local Letta Server
5client = Letta(
6 base_url="http://localhost:8283"
7)
8
9# define a function with a docstring
10def roll_dice() -> str:
11 """
12 Simulate the roll of a 20-sided die (d20).
13
14 This function generates a random integer between 1 and 20, inclusive,
15 which represents the outcome of a single roll of a d20.
16
17 Returns:
18 str: The result of the die roll.
19 """
20 import random
21
22 dice_role_outcome = random.randint(1, 20)
23 output_string = f"You rolled a {dice_role_outcome}"
24 return output_string
25
26# create the tool
27tool = client.tools.create_from_function(
28 func=roll_dice
29)

The tool creation will return a Tool object. You can update the tool with client.tools.upsert_from_function(...).

Specifying arguments via Pydantic models

To specify the arguments for a complex tool, you can use the args_schema parameter.

python
1# install letta_client with `pip install letta-client`
2from letta_client import Letta
3
4class Step(BaseModel):
5 name: str = Field(
6 ...,
7 description="Name of the step.",
8 )
9 description: str = Field(
10 ...,
11 description="An exhaustic description of what this step is trying to achieve and accomplish.",
12 )
13
14
15class StepsList(BaseModel):
16 steps: list[Step] = Field(
17 ...,
18 description="List of steps to add to the task plan.",
19 )
20 explanation: str = Field(
21 ...,
22 description="Explanation for the list of steps.",
23 )
24
25def create_task_plan(steps, explanation):
26 """ Creates a task plan for the current task. """
27 return steps
28
29
30tool = client.tools.upsert_from_function(
31 func=create_task_plan,
32 args_schema=StepsList
33)

Note: this path for updating tools is currently only supported in Python.

Creating a tool from a file

You can also define a tool from a file that contains source code. For example, you may have the following file:

custom_tool.py
1from typing import List, Optional
2from pydantic import BaseModel, Field
3
4
5class Order(BaseModel):
6 order_number: int = Field(
7 ...,
8 description="The order number to check on.",
9 )
10 customer_name: str = Field(
11 ...,
12 description="The customer name to check on.",
13 )
14
15def check_order_status(
16 orders: List[Order]
17):
18 """
19 Check status of a provided list of orders
20
21 Args:
22 orders (List[Order]): List of orders to check
23
24 Returns:
25 str: The status of the order (e.g. cancelled, refunded, processed, processing, shipping).
26 """
27 # TODO: implement
28 return "ok"

Then, you can define the tool in Letta via the source_code parameter:

python
1tool = client.tools.create(
2 source_code = open("custom_tool.py", "r").read()
3)

(Advanced) Accessing Agent State

Tools that use agent_state currently do not work in the ADE live tool tester (they will error when you press “Run”), however if the tool is correct it will work once you attach it to an agent.

If you need to directly access the state of an agent inside a tool, you can use the reserved agent_state keyword argument, for example:

python
1def get_agent_id(agent_state: "AgentState") -> str:
2 """
3 A custom tool that returns the agent ID
4
5 Returns:
6 str: The agent ID
7 """
8 return agent_state.id