Part 4: From toyaikit to PydanticAI
toyaikit is great for learning - it is interactive and small enough to read in a few hours. For production you want a framework with broader provider support, better tracing, and ongoing maintenance. We migrate the same agent to PydanticAI.
Installing pydantic-ai
Add the dependency to your project:
uv add pydantic-ai
Collecting the tools
PydanticAI takes a plain list of functions. The functions still need docstrings and type hints:
tools = [search_tools.search, search_tools.get_file]
Creating the agent
Import PydanticAI and wire up the same pieces:
from pydantic_ai import Agent
search_agent = Agent(
name="search",
model="openai:gpt-4o-mini",
instructions=instructions,
tools=tools,
)
The pieces map one-to-one to what we had with toyaikit:
model- the LLM (openai:gpt-4o-mini, but PydanticAI also supports Anthropic, Gemini, Groq, and others)instructions- the same three-iteration prompt from Part 3tools- the samesearchandget_filemethods
Running the agent
PydanticAI is async, so we use await. In Jupyter this works directly. From a .py file, wrap calls in asyncio.run(...).
query = (
"how do I use evidently to monitor "
"my machine learning models?"
)
result = await search_agent.run(query)
print(result.output)
The agent runs the same agentic search pattern: search, snippets, get_file, synthesize.
Inspecting messages
PydanticAI exposes structured messages so you can see what happened inside the agent:
def print_messages(messages):
for m in messages:
print(m.kind)
for p in m.parts:
part_kind = p.part_kind
if part_kind == "user-prompt":
print("USER:", p.content)
if part_kind == "tool-call":
print("TOOL CALL:", p.tool_name, p.args)
if part_kind == "tool-return":
print("TOOL RETURN:", p.tool_name)
if part_kind == "text":
print(p.content)
print()
Call the helper to see every tool call and response:
print_messages(result.all_messages())
Each message has a kind (request or response) and parts. Parts can be user-prompt, tool-call, tool-return, or text. You can also check usage:
result.usage()
Multi-turn conversations
To send a follow-up question, pass the previous messages as message_history:
messages = result.all_messages()
result2 = await search_agent.run(
"show me the code",
message_history=messages,
)
print(result2.output)
print_messages(result2.new_messages())
A simple Q&A loop
Putting it together into an interactive loop:
from pydantic_ai.usage import RunUsage
messages = []
usage = RunUsage()
while True:
user_prompt = input("You: ")
if user_prompt.lower().strip() == "stop":
break
result = await search_agent.run(
user_prompt, message_history=messages
)
usage = usage + result.usage()
print_messages(result.new_messages())
messages.extend(result.new_messages())
print(usage)
Same agent, same tools, same behavior - now on a framework you can ship. Continue with Where to go from here for where to take this next.