Defining Tools¶
Tools are the functions an agent can call. exagent infers the JSON schema automatically from your type hints and docstring — you just write a normal Python function.
The @tool decorator¶
from exagent import tool
@tool
def search(query: str, max_results: int = 5) -> list:
"""Search the knowledge base and return matching entries."""
...
What the decorator does:
| Source | Used for |
|---|---|
| Function name | Tool name sent to the model |
| Docstring | Tool description the model reads |
| Type hints | JSON schema (types + required fields) |
| Default values | Marks parameters as optional |
Supported type hints¶
| Python type | JSON schema type |
|---|---|
str |
string |
int |
integer |
float |
number |
bool |
boolean |
list / list[T] |
array |
dict |
object |
Optional[T] |
unwrapped to T (optional parameter) |
Override name or description¶
Pass keyword arguments to @tool to override the inferred values:
@tool(name="lookup_price", description="Get the current price of a product by SKU.")
def price(sku: str) -> float:
...
This is useful when the function name is not descriptive enough for the model, or when you want a more detailed description than the docstring.
Registering tools¶
# One at a time
agent.add_tool(my_tool)
# Several at once
agent.add_tools([tool_a, tool_b, tool_c])
# Plain function — wrapped automatically
agent.add_tool(my_plain_function)
add_tool accepts both Tool instances and plain functions. Plain functions are wrapped with @tool on the fly.
Manual Tool construction¶
When you need full control over the schema — for example to add an enum constraint or a nested object — construct a Tool directly:
from exagent import Tool
sentiment_tool = Tool(
name="analyze_sentiment",
description="Classify the sentiment of a piece of text.",
parameters={
"type": "object",
"properties": {
"text": {"type": "string", "description": "The text to analyze."},
"language": {
"type": "string",
"enum": ["en", "fr", "de", "es"],
"description": "ISO language code.",
},
},
"required": ["text"],
},
handler=lambda text, language="en": classify(text, language),
)
agent.add_tool(sentiment_tool)
Returning values¶
Tool handlers can return any JSON-serialisable value. exagent converts the result to a string before feeding it back to the model:
str— passed through unchanged- anything else — serialised with
json.dumps - non-serialisable — converted with
str()
@tool
def get_user(user_id: str) -> dict:
"""Fetch a user record."""
return {"id": user_id, "name": "Alice", "role": "admin"}
# the model receives: '{"id": "u1", "name": "Alice", "role": "admin"}'
Error handling¶
If a tool handler raises an exception, exagent catches it and sends the error message back to the model as a tool_result with is_error: true. The model can then decide how to respond — usually by informing the user or trying a different approach.
@tool
def read_file(path: str) -> str:
"""Read a file and return its contents."""
with open(path) as f:
return f.read()
# If FileNotFoundError is raised, the model is told: "Error running tool 'read_file': ..."
Raise meaningful exceptions
Use descriptive exception messages. The model reads them and uses them to decide what to do next.