This masterclass is designed to guide you through building a simple, yet functional, coding assistant using Pydantic AI. Over the course of the exercises, you will gain hands-on experience with the core concepts behind agentic systems, including how to define and structure an agent, write instructions (prompts), and enable tool-calling capabilities. You will also explore how execution hooks can be used to customize and control agent behavior.
By the end of this masterclass, you will have a working prototype and a solid understanding of foundational patterns to develop agentic applications.
In this first exercise, the goal is to establish a minimal working setup and make your first call to a language model. You will configure the model provider, define the model that you will use for your agent, and connect everything through a Pydantic AI agent.
-
Configure the model provider:
We will use an OpenAI Responses API via a gateway. This means that you need to define a provider that specifies the endpoint of the API (base_url) and your authentication key (api_key).Create an
OpenAIProviderinstance and configure it with the appropriate values for your environment. -
Configure the model:
Next, define the model usingOpenAIResponsesModel. This requires the name of the model that you want to use (model_name) and the provider instance that you just created (provider). -
Create the agent:
Now, create the Agent instance by passing the configured model (model) and providing instructions (instructions).The instructions define how the assistant should behave. For this exercise, keep it simple (e.g.,
"You are a Python coding assistant. Write clear, correct, and minimal Python code. ..."). -
Run the agent:
Create the basic user interaction. Prompt the user for input usingconsole.input(">> "). Pass the user input to the agents using the run method. Print the output to the console usingconsole.print(result.output).
In the previous exercise, you made a single call to the agent. A natural next step is to wrap this in a loop to create an interactive assistant. However, this will not work as expected out of the box.
If you repeatedly call the agent with only the last user input, the model has no memory of the previous interactions. Each call is treated as an isolated request, which means that the assistant cannot maintain context across turns.
To build a conversational agent, you need to explicitly pass the message history between calls.
- Capture the message history:
After each agent run, you can retrieve the full conversation history usingresult.all_messages(). This returns the message history, including both user inputs and model responses. - Reuse message history:
When making the next call to the agent, pass this history back using themessage_historyargument in therun()method.
You now have a stateful agent that can handle multi-turn conversations coherently.
So far, our agent can respond to user prompts, but it cannot take actions. In this exercise, we will give the agent basic file system capabilities through tools. This allows the agent to read files, write files, search for files, and delete files.
A tool is a simple Python function that the agent can call. Each tool should have a clear docstring describing what the tool does, what its parameters mean, and what its return value represents. This information is exposed to the model and helps it decide when and how to use the tool correctly.
- Implement the four tools:
Implement theread_fileandwrite_filetools. - Create the file operations capability:
Once the functions are in place, expose them to the agent through a capability. Create aFileOperationsclass that inherits from Pydantic AI’sAbstractCapability[Any]class and override theget_toolset()method to return aFunctionToolsetcontaining your tools (add the functions using theadd_function()method). - Pass the capability to the agent:
Finally, pass
[FileOperations()]as the agent’scapabilitywhen configuring the agent.
At this point, our agent can act on files, but as a user we get very little insight into what is happening while it works. Files may appear or change on disk, yet the console remains quiet until the final response from the agent is printed. In this exercise, we will improve the user experience by adding logging to the FileOperations capability.
Pydantic AI processes many internal events while an agent runs, and hooks allow us to tap into those events.
-
Configure the agent’s dependencies:
To make it possible for the capability to write to the console, we need to inject a console instance. Set thedeps_typeof the agent toAgentDeps(defined indeps.py), create a dependencies object, and pass it in therun()method. -
Implement the hook:
Use the before_tool_execute hook to display a message whenever the agent is about to call a tool.async def before_tool_execute( self, ctx: RunContext[Any], *, call: ToolCallPart, tool_def: ToolDefinition, args: dict[str, Any], ) -> dict[str, Any]: ...
You only need to use
call.tool_namefor this exercise, the other parameters can be ignored.
By default, the model uses a medium level of reasoning effort. However, not every task needs the same amount of work. For simple tasks, we may want to reduce the reasoning effort to low for faster, cheaper execution. For more difficult tasks, we may want to increase it to high.
Pydantic AI allows you to control the reasoning effort, and other model-related
parameters, through the ModelSettings. In this exercise, you will implement a
capability that dynamically selects the reasoning effort based on the user’s
request.
-
Create the reasoning effort capability:
Create aReasoningEffortclass that inherits fromAbstractCapability[Any]. Inside it, implement theget_model_settingsmethod. This method should return aCallablethat acceptsRunContext[Any]and returns the appropriateModelSettingsinstance.Here is the method signature:
def get_model_settings(self) -> Callable[[RunContext[Any]], ModelSettings]: def _set_reasoning_effort( ctx: RunContext[Any] ) -> ModelSettings: ... return _set_reasoning_effort
-
Set the reasoning effort:
Usectx.promptto inspect the original user instructions passed to the agent viarun(). Choose between"low","medium", and"high"reasoning effort.Use
ModelSettingsand set thethinkingfield accordingly. -
Pass the capability to the agent:
Add theReasoningEffort()capability to the list of capabilities in the agent constructor.
So far, all of our agent’s behavior has been directly defined in Python. In this exercise, we will make the agent extensible by allowing users to provide skills. Skills are Markdown files that the agent can load dynamically at runtime.
In skills.py, you will find a partial implementation of a Skills capability.
It already contains a load_skill tool as well as the Skills capability
itself. However, simply exposing the load_skill tool is not enough. The agent
also needs to know which skills are available, so that it can decide when to
load and use them.
-
Extend the system instructions:
Implement theget_instructionsmethod on the capability. This method should read all Markdown files in theskillsdirectory, extract their metadata, and return an additional instruction block listing the available skills.For each skills file, use:
skill = frontmatter.load(filename)
The metadata you need is available as
skill.metadata.get("name")andskill.metadata.get("description"). -
Pass the capability to the agent:
Add theSkills()capability to the list of capabilities in the agent constructor.