Building a Pelton Workout Recommendation Agent with PydanticAI

December 21, 2024 (1d ago)

PydanticAI, a new framework combining Pydantic's data validation with LLM-powered agents, was released recently. I saw it on Bluesky last week and was intrigued by its intuitive approach to building AI workflows. Instead of writing compled chains and prompts, PydanticAI lets you create agents using familiar Python patterns - classes and decorators.

To test PydanticAI I built a simple agent to help generate personalized Peloton workouts. Instead of having to think about what my workout is going to be every day I want to give an agent my workout preferences and access to the Peloton API to automatically generate the best workout to keep my fitness goals. Basically I want a set of well-rounded workouts each week, no skipping leg day (or core day in my case!)

Following the agent recommendations I've maintained a good mix of workouts. There have been a few pleasant surprises where a class I'd normally overlook was challenging and fun. To be fair there have also been a couple misses, but on the all it's been a great experience powered by PydanticAI!

All the code is available in the GitHub Repo to follow along.

Building the Peloton Agent

The Agent class is the primary interface with LLMs with PydanticAI. Inside the Agent definition you can define the type of LLM model to use, your system prompt, function tools, the expected output structure, and any kind of dependencies the agent might need.

The agent I defined was simple. First I define the LLM model to use, which is gpt-4o-mini. PydanticAI supports different kinds of LLM providers, Gemini, Ollama etc. with support for others like Anthropic and Mistral on the way. So you can bring a variety of LLM providers to PydanticAI, OpenAI is just used as an example here. The system prompt contains instructions for generating the recommended workout.

With a couple lines the agent for this workout recommendation system is defined:

peloton_agent = Agent(
    'openai:gpt-4o-mini',
    system_prompt=AGENT_SYSTEM_MSG
)

The agent has access to a handful of tools for generating a recommendation. These tools primarily interact with the (unofficial) Peloton API for retrieving recent user classes, available classes on the platform, and adding classes to the user stack. The @peloton_agent.tool_plain decorator is used on functions in the agent.py file to register the tool with the agent.

Since this is a chat application it's important to be able to access the conversation history in the chat. PydanticAI makes this pretty easy. Each RunResult from the Agent has two methods, new_messages() to get only the messages from the current run and all_messages() returning all messages from all runs of the agent. If you have multiple conversation turns and want to include the conversation history all you have to do is set the message_history parameter with the all_messages() response from the last agent run and the agent will utilize the conversation history. No extra classes to configure it all works as part of the standard agent interface.

In the code I'm using the message history in this custom handler to run the agent. I'm tracking the last agent response in a Streamlit session state variable, last_response. If the variable exists then I set the message_history parameter in the agent run call with the previous conversation messages.

async def run_agent():
    if st.session_state["last_response"]:
            output = st.session_state["agent"].run_sync(user_input, message_history=st.session_state["last_response"].all_messages())
    else:
        output = st.session_state["agent"].run_sync(user_input)
    
    return output

Challenges

Peloton's API is not officially supported. So fair warning this could stop working at anytime. Inside peloton.py there is a PelotonAPI class for interacting with the endpoints we'll need. Being an unofficial API I can't promise these integrations are 100% robust. For example, I don't have the tread or rower, so it's possible the API responses for those activities could be different. I had to do some trial and error so find the structure of different responses for different class types from the API.

Prompting was another challenge. Specifically getting the prompt to enforce the workout duration was a challenge. I had to iterate on the system prompt to get the agent to respect the user duration preference.

Wrapping Up / Next Steps

There are so many features inside PydanticAI. Specifically the multi-agent flows and result validators are features I want to dig into. For a first go building this simple agent I'm pretty impressed.

One element that's particularly impressive is the documentation. I found the explanations to be very clean with a bunch of exaples to reference. I recommend taking a look!

The next time I build an agent PydanticAI is going to be what I'm starting with!