Workshops ... Part 4: From toyaikit to PydanticAI

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 3
  • tools - the same search and get_file methods

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.

Questions & Answers

Sign in to ask questions