FunctionModel: write a stand-in model
FunctionModel turns a Python function into a model. The agent sends it the same messages a real model gets, and your function writes the response.
from pydantic_ai import Agent, ModelResponse, TextPart
from pydantic_ai.models.function import AgentInfo, FunctionModel
def reply(messages, info: AgentInfo) -> ModelResponse:
ticket = messages[-1].parts[-1].content
return ModelResponse(parts=[TextPart(f"You wrote {len(ticket.split())} words.")])
agent = Agent(FunctionModel(reply))
print(agent.run_sync("I was charged twice for one order").output)A model function takes two arguments. messages is the list from lesson 2, so messages[-1].parts[-1] is the newest part: the ticket. info describes what the agent offers the model. The function returns a ModelResponse with the parts a model would send, here one TextPart.
def peek(messages, info: AgentInfo) -> ModelResponse:
print("instructions:", info.instructions)
print("tools:", [tool.name for tool in info.function_tools])
print("text allowed:", info.allow_text_output)
return ModelResponse(parts=[TextPart("ok")])
agent = Agent(FunctionModel(peek), instructions="Be short.")
agent.run_sync("hello")info is how a stand-in model knows what the agent wants. A real model gets the same things, as JSON in the API request.
The stand-in for this course
The rest of the course uses one model function, in shop_model.py. It is a few keyword rules, not a language model, but it answers through the same parts a real model would:
import re
from pydantic_ai import ModelResponse, TextPart, ToolCallPart
from pydantic_ai.models.function import AgentInfo, FunctionModel
def sort_ticket(text):
text = text.lower()
if "charged" in text or "refund" in text:
return "billing", 4
if "parcel" in text or "arrived" in text:
return "shipping", 3
return "other", 1
def shop_reply(messages, info: AgentInfo) -> ModelResponse:
prompts = [p.content for m in messages for p in m.parts if p.part_kind == "user-prompt"]
ticket = prompts[-1]
last = messages[-1].parts[-1]
order = re.search(r"A-\d{4}", ticket)
# 1. The ticket names an order and the agent has a tool: ask for it.
if order and info.function_tools and last.part_kind == "user-prompt":
tool = info.function_tools[0].name
return ModelResponse(parts=[ToolCallPart(tool, {"order_id": order.group()})])
# 2. A tool answered: write the reply from what it said.
if last.part_kind == "tool-return" and info.allow_text_output:
return ModelResponse(parts=[TextPart(f"Order {order.group()}: {last.content}.")])
# 3. The agent wants a typed answer: fill in its output tool.
category, priority = sort_ticket(ticket)
if info.output_tools:
args = {"category": category, "priority": priority}
return ModelResponse(parts=[ToolCallPart(info.output_tools[0].name, args)])
# 4. Otherwise, plain text.
return ModelResponse(parts=[TextPart(f"Sorted as {category}.")])
shop_model = FunctionModel(shop_reply, model_name="shop")sort_ticketguesses a category and a priority from keywords.- Rule 1: a ticket with an order id like
A-1001, sent to an agent that has a tool, gets aToolCallPartasking for that tool. - Rule 2: when the newest part is a
tool-return, it writes the answer from the tool's result. - Rule 3: an agent that wants typed output, lesson 6, gets its output tool called with the category and priority.
- Rule 4: anything else gets a sentence.
from pydantic_ai import Agent
from shop_model import shop_model
agent = Agent(shop_model)
for ticket in ["I was charged twice for one order", "My parcel never arrived", "How do I change my email?"]:
print(agent.run_sync(ticket).output)- Add a rule to
sort_ticketfor tickets about passwords, with the categoryaccount. - Make
replyanswer in capital letters. - In
peek, printlen(messages).
Slow is fine. Stopping is the only problem.