Tool errors: ModelRetry and crashes
A tool that raises an ordinary exception ends the whole run. Raise ModelRetry instead, and the model reads your message and can try something else.
The model function below always asks for order A-9999, which does not exist:
from pydantic_ai import Agent, ModelResponse, ModelRetry, TextPart, ToolCallPart
from pydantic_ai.models.function import FunctionModel
ORDERS = {"A-1001": "shipped"}
def asks(messages, info):
last = messages[-1].parts[-1]
if last.part_kind == "user-prompt":
return ModelResponse(parts=[ToolCallPart("lookup_order", {"order_id": "A-9999"})])
if last.part_kind == "retry-prompt":
return ModelResponse(parts=[TextPart(f"Sorry: {last.content}")])
return ModelResponse(parts=[TextPart(last.content)])agent = Agent(FunctionModel(asks))
@agent.tool_plain
def lookup_order(order_id: str) -> str:
"""Look up an order."""
return ORDERS[order_id]
agent.run_sync("Where is my order?")ORDERS["A-9999"] raised KeyError, and Pydantic AI let it through: the run stopped, and the model was never told. That is right for a bug. For something a model can recover from, tell it:
agent = Agent(FunctionModel(asks))
@agent.tool_plain
def lookup_order(order_id: str) -> str:
"""Look up an order."""
if order_id not in ORDERS:
raise ModelRetry(f"There is no order {order_id}. Ask the customer for their order id.")
return ORDERS[order_id]
result = agent.run_sync("Where is my order?")
print(result.output)
print([part.part_kind for message in result.all_messages() for part in message.parts])ModelRetry became a retry-prompt part, and last.content is your message. A real model would now ask the customer for the id, as the message says, or call the tool again with a different one.
Retries run out
def stubborn(messages, info):
return ModelResponse(parts=[ToolCallPart("lookup_order", {"order_id": "A-9999"})])
agent = Agent(FunctionModel(stubborn))
@agent.tool_plain
def lookup_order(order_id: str) -> str:
"""Look up an order."""
raise ModelRetry(f"There is no order {order_id}.")
agent.run_sync("Where is my order?")Each tool has its own retry count, one by default. A model that keeps sending the same bad id ends the run with UnexpectedModelBehavior. @agent.tool_plain(retries=3) allows more for one tool, and Agent(retries=...) sets the default.
| In the tool | What happens |
|---|---|
return value | The model gets the value. |
raise ModelRetry(message) | The model gets your message and can try again, while that tool has retries left. |
| Any other exception | The run stops and the exception reaches your code. |
- Catch
KeyErrorin the first version and raiseModelRetryfrom it. - Set
retries=2on the stubborn tool and count the requests in the error. - Return the string
"not found"instead of raising. What does the model get?
You understood something today that you didn't yesterday.