Extending Pydantic AI Agents with Function Tools — Reading and Updating Project Data in Python


In the previous part of this series, we extended our Pydantic AI agent with dependencies, allowing it to use contextual information such as the user and project name when generating tasks.

In this part, we’ll further enhance our agent by enabling it to access and modify project data through Function Tools in PydanticAI.

This functionality allows the agent to perform controlled interactions with external data, such as retrieving project details or updating records. By introducing Function Tools, we make our agent more autonomous, structured, and adaptable for real-world applications.

In real-world software systems, an AI agent often needs to interact with its environment. It might query project information, update task counts, or verify user permissions before executing an action.

To achieve this reliably, we need a structured interface between the agent and the systems it operates in. PydanticAI Function Tools provide exactly that interface. They allow the agent to call predefined Python functions in a type-safe and predictable way, ensuring consistent behavior across multiple runs. This makes Function Tools an essential component for integrating AI agents into production-grade Python applications.

Disclaimer:

For demonstration purposes, this tutorial uses a Python dictionary to simulate a database. In a real application, these operations would typically be implemented through actual database queries or API requests.

data.py

PROJECTS = {
    "LearnPydanticAI": {"group": "dev_team", "open_tasks": 2},
    "InternalResearch": {"group": "data_team", "open_tasks": 4}
}
Enter fullscreen mode

Exit fullscreen mode

This dictionary represents our data source. Each project includes information about the assigned group and the current number of open tasks.

Next, we will extend our TaskModel of the agent to include additional fields that reflect the operation’s outcome. With this extension we enable the agent to communicate whether the operation was successful and how many open tasks remain for a given project.

model.py

from pydantic import BaseModel, Field

class TaskModel(BaseModel):
    task: str = Field(description="The title of the task.")
    description: str = Field(description="A brief description of the task specifying its steps.")
    priority: int = Field(description="The priority of the task on a scale from 1 (high) to 5 (low).")
    project: str = Field(description="The project the task belongs to.")
    created_by: str = Field(description="The name of the person who created the task.")
    number_of_open_tasks: int = Field(description="The number of open tasks for the project after creating this task.")
    success: bool = Field(description="Indicates if the task was created successfully.")

Enter fullscreen mode

Exit fullscreen mode

Now, let’s update our agent to use two function tools and interact with the simulated data source. The first retrieves project information, while the second updates the number of open tasks after a new task is created.

agent.py

from pydantic_ai.models.mistral import MistralModel
from pydantic_ai.providers.mistral import MistralProvider
from pydantic_ai import Agent, RunContext

from LearnPydanticAI.dependencies import TaskDependency
from LearnPydanticAI.model import TaskModel
from LearnPydanticAI.projects import PROJECTS

import os

class TaskAgent:
    def __init__(self):
        self.agent = self._init_agent()

    def _init_agent(self) -> Agent:
        agent = Agent(
            model=MistralModel(
                model_name=os.getenv("LLM_MISTRAL_MODEL"),
                provider=MistralProvider(api_key=os.getenv("MISTRAL_API_KEY"))
            ),
            output_type=TaskModel,
            deps_type=TaskDependency,
        )

        @agent.system_prompt
        def create_system_prompt(ctx: RunContext[TaskDependency]) -> str:
            return (f"""
                    Create a brief task from the user's request.
                    The task is created by {ctx.deps.username}.
                    The task belongs to the project {ctx.deps.project}.
                    Before creating the task, verify that the project {ctx.deps.project} exists by retrieving its details.
                    If you verified that it exists, update the number of open tasks for this project.
                    If the project does not exist, mark the task creation as failed and leave the task and its description empty.
                    """)

        @agent.tool_plain
        def get_project_info(project_name: str) -> str:
            """Retrieves project information."""
            project = PROJECTS.get(project_name, None)
            if not project:
                return f"Project {project_name} not found."
            return project

        @agent.tool_plain
        def update_open_tasks(project_name: str, new_open_tasks: int) -> str:
            """Updates the number of open tasks for a given project."""
            project = PROJECTS.get(project_name, None)
            if not project:
                return f"Updating open tasks failed for project {project_name}. Project does not exist."
            project["open_tasks"] = new_open_tasks
            return f"Successfully updated open tasks for project {project_name} to {project['open_tasks']}."

        return agent

    def run(self, query: str) -> str:
        deps = TaskDependency(username="sudo", project="LearnPydanticAI")
        result = self.agent.run_sync(query, deps=deps)
        return result.output

Enter fullscreen mode

Exit fullscreen mode

This setup allows the agent to retrieve project data and update it when creating new tasks. Both functions are registered using the @agent.tool_plain decorator.

In addition to this, PydanticAI also provides the @agent.tool decorator. Both decorators register Function Tools, but they serve different purposes. Let’s take a closer look at how they differ and when to use each of them.



@agent.tool_plain decorator

The @agent.tool_plain decorator registers a synchronous function as a tool that can be directly executed by the agent. These function tools typically perform simple operations, like retrieving values from memory, performing calculations or extending the agents capabilities in another way.

In this example, both get_project_info() and update_open_tasks() are synchronous, and their execution does not depend on external network requests.



@agent.tool decorator

In contrast, the @agent.tool decorator is designed for asynchronous operations, such as API calls. When using @agent.tool, the function also receives a RunContext object, which allows access to dependency values like API hosts, authentication keys and database operations.



Example 1: External API Call

@agent.tool
async def get_project_info(ctx: RunContext[TaskDependency], project_name: str) -> str:
    """Retrieve project info from an external API."""
    api_host = ctx.deps.api_host
    api_key = ctx.deps.api_key
    response = await httpx.get(f"{api_host}/projects/{project_name}", headers={"Authorization": f"Bearer {api_key}"})
    return response.text
Enter fullscreen mode

Exit fullscreen mode



Example 2: Internal Database Call

@agent.tool
async def get_project_info(ctx: RunContext[DBDependencies], project_name: str) -> str:
    if ctx.deps is None:
        raise ValueError("Dependencies (ctx.deps) are not set.")

    db = ctx.deps.db
    projects = db.get_project_info(project_name)
    return f"{projects}"
Enter fullscreen mode

Exit fullscreen mode

This approach is essential when connecting your AI agent to real-world data sources, where security, authentication, and asynchronous execution are required.

When we execute the agent with this configuration, it validates whether the project exists, creates a new task, and updates the number of open tasks.

% poetry run python src/LearnPydanticAI/app.py
task='Learn about Pydantic AI'
description='1. Research about Pydantic AI. 2. Understand its core concepts and applications. 3. Experiment with simple examples and exercises.'
priority=2
project='LearnPydanticAI'
created_by='sudo'
number_of_open_tasks=3
success=True
Enter fullscreen mode

Exit fullscreen mode

We can validate the agents behavior by I changing the dependencies handed to the agent, to include a project that does not exist.

agent.py

...
deps = TaskDependency(username="sudo", project="UnknownProject")
result = self.agent.run_sync(query, deps=deps)
return result.output
...
Enter fullscreen mode

Exit fullscreen mode

After doing so, the agent responds accordingly.

% poetry run python src/LearnPydanticAI/app.py
task=''
description=''
priority=1
project='UnknownProject'
created_by='sudo'
number_of_open_tasks=0
success=False
Enter fullscreen mode

Exit fullscreen mode

This demonstrates how Function Tools enhance the agent’s ability to make context-aware decisions and update its environment accordingly.

When working with Function Tools, it is often helpful to analyze how the agent interacts with them during execution. PydanticAI provides a simple way to inspect this process through the all_messages() method of the result object.

This method returns the complete internal message history — including system prompts, tool calls, intermediate reasoning, and final responses. It allows developers to understand how the agent decides when and why to call a tool, which can be invaluable when debugging or optimizing complex workflows.

agent.py

...
# Bonus: inspecting function tool calls
for message in result.all_messages():
    print(message)
...
Enter fullscreen mode

Exit fullscreen mode

The output reveals each step of the conversation, such as:

  • The system prompt created for the current run
  • The function call the agent decided to make (get_project_info or update_open_tasks)
  • The parameters passed to each call
  • The resulting updates before the final structured output

By inspecting these internal messages, developers can gain deeper insight into how the PydanticAI runtime orchestrates reasoning and function execution. This kind of introspection is especially valuable when designing complex, multi-tool agents or optimizing prompt behavior for production environments.

Function Tools are a critical feature when integrating AI agents into real-world systems.

They allow the agent to go beyond generating text and to perform meaningful actions such as fetching data, validating information, and modifying state.

In practice, agents often need to:

  • Validate input or synchronize state
  • Fetch data from APIs or internal services
  • Update records in databases
  • Interact with external environments or data stores

By implementing these patterns through PydanticAI Function Tools, developers gain both the flexibility of AI and the reliability of typed, structured interfaces.

With Function Tools implemented, our agent can now access, validate, and update contextual project data.

In the next part of this PydanticAI tutorial, we will explore how to integrate real external APIs and securely manage dependencies for more advanced automation workflows.

💻 Code on GitHub: hamluk/LearnPydanticAI/part-3

📘 Read Part 2: Extending Pydantic AI Agents with Dependencies



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *