Building Real-World AI Agents with PydanticAI and Unstract

Table of Contents

Introduction

In recent year(s), we’ve witnessed a significant shift in how businesses interact with data.

No longer limited to rigid scripts or dashboards, organizations are increasingly turning to AI agents to automate complex, multi-step workflows that are all triggered by natural language commands.

At the core of this shift is the concept of natural language orchestration, which is enabling users to describe what they want in plain English and letting AI agents coordinate the necessary tools and actions.

This has tremendous implications for modern data pipelines, where collecting, transforming, and storing structured data often requires integrating multiple systems and tools.

To explore this new paradigm, we’ll walk through a sample prompt similar to this one:

“Process the economic report, calculate the sum and averages before 2025 and save to the database”

This sentence, while simple to a human, implies a multi-stage pipeline:

  • Extract structured data from a PDF.
  • Parse and process that data with logic like grouping and aggregation.
  • Persist the result into a relational database.

Using PydanticAI, we can define an AI agent that intelligently chooses which tools to use, from document parsing with Unstract, to data processing with Pandas, and finally storage in PostgreSQL.

This article will walk you through how to build such an agent, showing how natural language can drive sophisticated data workflows that are accurate, efficient, and with minimal developer friction.


What is PydanticAI and Why Use It?

PydanticAI is a lightweight Python framework designed to bridge the gap between large language models (LLMs) and real-world software tools.

At its core, PydanticAI enables the creation of AI agents that can reason over a set of user-defined functions, which are called tools, and execute them in a structured, validated, and context-aware manner.

Agent-Based Design

The framework is built around the concept of an agent: a stateful orchestration engine that communicates with an LLM (like MistralAI or GPT-4o) and determines how to respond to user input by invoking the right tools in the right order.

Tools in PydanticAI are regular Python functions, but with a twist: their input and output parameters are validated using Pydantic models.

This agent-based approach makes it possible to construct complex workflows where the LLM isn’t just generating text, but it’s actively deciding how to move data between structured tools, how to handle errors, and how to produce final outputs.

Typed, Validated Tool Definitions

By leveraging Pydantic for tool I/O, every function exposed to the AI agent has strict type definitions.

This offers several major benefits:

  • Safety: Agents cannot call tools with invalid or malformed inputs.
  • Clarity: Tool definitions serve as both documentation and a contract, making it easier for both developers and the AI to understand what each function does.
  • Debuggability: Validation errors provide clear feedback when something goes wrong, which is a huge improvement over unclear LLM hallucinations.

Where LLMs + Structured Tools Shine

By combining the reasoning power of LLMs with strictly-typed tools, PydanticAI unlocks a range of use cases that would be difficult using either traditional scripting or pure LLM prompting:

  • Dynamic ETL Pipelines: Users can describe what kind of aggregation, transformation, or storage they want, and the agent chooses the right functions.
  • Natural Language APIs: Turn plain English queries into live function calls, with no need to expose raw endpoints.
  • Autonomous Data Workflows: Process documents, extract data, run computations, and store results, all orchestrated by a language model that understands context.

In short, PydanticAI brings structure, safety, and composability to AI-powered systems, which is  enabling developers to move beyond static scripts and towards intelligent, conversational workflows.

Here is the Github repository where you will find all the codes written for this article.


Define Your Unstract API with Prompt Studio

Unstract’s Prompt Studio is a no-code interface for designing and refining AI prompts to extract structured data from unstructured sources like PDFs, scanned reports, and documents.

In this walkthrough, we’ll focus on creating prompts to extract relevant fields from an economic report, such as the New Zealand economic report: 

To begin, visit the Unstract website and create an account. The sign-up process is simple and provides full access to Unstract’s core tools, including Prompt Studio and LLMWhisperer.

All new accounts include a 14-day trial and $10 worth of LLM tokens, so you can start building and testing your data extraction pipeline immediately.

Setting Up Prompts in Prompt Studio

Start by opening the Prompt Studio interface in Unstract and creating a new project tailored to your specific document type, like for example, an economic report.

Next, upload the document you want to process using the Manage Documents section. This is where you’ll attach the file and begin crafting your prompts.

Prompts guide the AI to extract targeted fields from the document. Here’s an example prompt designed to extract the National Accounts section data.

Prompt:


“Extract each of the items under the heading ‘National Accounts’, where the JSON key is the item and a list of values for each year, with the key being the year. For example: [{item: [{year:value}]}]”

Note: Be sure to set the output format to JSON for structured results.

Running the prompt, we get the following JSON: 

Prompt:


“Extract each of the items under the heading ‘Labour Market’, where the JSON key is the item and a list of values for each year, with the key being the year. For example: [{item: [{year:value}]}]”

Note: Be sure to set the output format to JSON for structured results.

Running the prompt, we get the following JSON:

Output Format:

The extracted data is organized into structured JSON, as mentioned.

The combined output of the different prompts is, for example:

{
  "national_accounts": [
    {
      "Real GDP (production)": [
        {
          "2020": -1.4
        },
        {
          "2021": 5.6
        },
        {
          "2022": 2.4
        },
        {
          "2023": 0.6
        },
        {
          "2024": 1
        },
        {
          "2025": 2
        },
        {
          "2026": 2.4
        },
        {
          "2027": 2.4
        },
        {
          "2028": 2.4
        },
        {
          "2029": 2.4
        }
      ]
    },
    ...
  ],
  "labor_market": [
    {
      "Employment": [
        {
          "2020": 1.3
        },
        {
          "2021": 2.2
        },
        {
          "2022": 1.7
        },
        {
          "2023": 3.1
        },
        {
          "2024": 1.1
        },
        {
          "2025": 1.1
        },
        {
          "2026": 1.6
        },
        {
          "2027": 1.7
        },
        {
          "2028": 1.7
        },
        {
          "2029": 1.6
        }
      ]
    },
	...
  ]
}

Note: JSON output abbreviated for clarity.

Deploying as an API:

Once you’ve set up your Prompt Studio project and fine-tuned your prompts for precise data extraction, the next step is to deploy your Unstract solution as an API.

This deployment enables you to integrate the parsing functionality directly into your scripts, to support real-time processing and scalable operations.

Creating a Tool:

Begin by converting your project into a tool that can be incorporated into a workflow. In your Prompt Studio project, click the Export as tool icon located at the top right corner.

This action will transform your project into a ready-to-use tool.

Creating a Workflow:

Next, create a new workflow:

  • Navigate to BUILD → Workflows.
  • Click on + New Workflow to start a new workflow.

Then, in the Tools section on the right, locate the tool you just created (e.g., “Economic Reports”) and drag and drop it into the workflow editor on the left side:

Creating an API:

Now that your workflow is ready, you can transform it into an API.

Begin by navigating to MANAGE → API Deployments and clicking on the + API Deployment button to create a new API deployment by selecting the created workflow.

Once the API is set up, you can use the Actions links to manage different aspects of the API.

For example, you can manage the API keys or download a Postman collection for testing:

Also note down the API Endpoint URL, you will need to fill that information in the script.


Setting Up Postgres and MistralAI

Now that we have the Unstract API ready, we need to prepare the other requirements needed for the AI agent and the custom tools.

The script will use MistralAI as the LLM and a Neon Postgres database as the database storage, that can be invoked by a custom tool.

Creating a Neon Postgres Database

Sign up for a free NeonDB project to create a dedicated database for storing the processed economic data.

Create a new project:

And you will be redirected to the Dashboard, where you can click on ‘Connect to your database’:

Select ‘Connection string’ and click on ‘Copy snippet’, you will need this value later to connect the Python script to the Postgres database.

MistralAI

The LLM model we will use in this example is the latest model from MistralAI, which we will be accessing through their API.

You can get an API key from the Mistral Console.


Architecture

The core architecture of this workflow combines Unstract’s document parsing capabilities with PydanticAI’s validation and orchestration layer, enabling a clean, traceable, and fully automated pipeline:

pydantic ai and unstract

Main Components

The AI agent architecture is composed of three core components that work together to efficiently handle, process, and store the economic data extracted from the PDF.

Input & Extraction

  • A user defines a PDF file containing economic data.
  • The Unstract API is used to extract clean, structured JSON from the PDF.
  • This approach avoids the pitfalls of OCR or regex by maintaining nested data integrity.

Processing & Validation

  • The structured JSON is fed into a Pandas-based function that aggregates metrics (sum or average) around a configurable split_year.
  • PydanticAI validates the schema, ensuring robust handling and retry logic if the data doesn’t conform.

Storage & Output

  • Aggregated records are saved to a PostgreSQL database using psycopg, with separate tables for sum and avg actions.
  • The system returns a final JSON response enabling easy logging and traceability.

Creating an AI Agent with Custom Tools using PydanticAI

In this section, we will go trough the Python code necessary to build the custom tools and for building and connecting the AI agent to these tools.

A note regarding Python version, due to the use of psycopg (to connect to Postgres), the recommended version is 3.11.x.

The source code is available at: https://github.com/nunombispo/PydanticAI-Unstract

Let’s start with the necessary requirements. Create a requirements.txt file:

pydantic
pydantic-ai
requests
python-dotenv
pandas
psycopg[binary,pool]

Which can be installed with:

pip install -r requirements.txt

Before starting to create a script, you will need to define an .env file to store the API keys and connection string to Neon Postgres:

MISTRAL_API_KEY=<YOUR_MISTRAL_API_KEY>
MISTRAL_MODEL=mistral:mistral-large-latest
UNSTRACT_API_KEY=<YOUR_UNSTRACT_API_KEY>
UNSTRACT_API_URL=<YOUR_UNSTRACT_API_URL>
NEON_DATABASE_URL=<YOUR_NEON_DATABASE_URL>

Then you can create a pdf_extractor_agent.py file to contain the script and start by placing the imports:

import json
import os
import time
from typing import Dict, Any
import pandas as pd
from dotenv import load_dotenv
from pydantic_ai import Agent, BinaryContent, RunContext
from pathlib import Path
import requests
import asyncio
import numpy as np  
import psycopg
from datetime import datetime, UTC


# Load the environment variables
load_dotenv()

This code snippet also loads the environment variables.

Tool 1: extract_pdf_text

Next let’s place the code to create the custom tool to invoke the Unstract API:

# Define the function to extract text from the PDF file
def extract_pdf_text(ctx: RunContext[str]) -> str:
    """Extract text from PDF binary data."""
    print("extract_pdf_text called")

    try:
        # Get the binary data from the context
        pdf_binary = ctx.deps.data
        # Write the pdf file to a temporary file
        filepath = 'temp.pdf'
        with open(filepath, 'wb') as f:
            f.write(pdf_binary)

        # Define the API URL and headers
        api_url = os.getenv('UNSTRACT_API_URL')
        headers = {
            'Authorization': 'Bearer ' + os.getenv('UNSTRACT_API_KEY')
        }
        # Define the payload
        payload = {'timeout': 300, 'include_metadata': False}
        # Define the files
        files=[('files',('file',open(filepath,'rb'),'application/octet-stream'))]
        # Make the request
        response = requests.post(api_url, headers=headers, data=payload, files=files)
        # Return the response
        return response.json()['message']['result'][0]['result']['output']
    except Exception as e:
        print(e)
        return "Error extracting text from PDF"

This function, extract_pdf_text, extracts text from a PDF file provided as binary data. The binary data is provided from the AI agent run context (ctx.deps.data).

It saves the PDF temporarily, then sends it to the Unstract API for processing.

The extracted text is returned as a string. If an error occurs, it returns an error message.

Tool 2: process_dataframe

Now, you can place the code for the custom tool to process the extracted data from the PDF with Pandas:

# Define the function to process the data using Pandas
def process_dataframe(data: Dict[str, Any], action: str = 'sum', split_year: int = 2025) -> str:
    """Process any nested dictionary data using pandas and return a processed DataFrame with aggregated values.
    
    Args:
        data (Dict[str, Any]): The input data dictionary
        action (str): The aggregation action to perform ('sum' or 'avg')
        split_year (int): The year to split the data before and after (default: 2025)
    
    Returns:
        JSON string containing the aggregated data
    """
    print(f"process_dataframe called with action={action}, split_year={split_year}")
    
    # Validate action parameter
    if action not in ['sum', 'avg']:
        raise ValueError("Action must be either 'sum' or 'avg'")
    
    # Parse JSON if data is a string
    if isinstance(data, str):
        try:
            raw_data = json.loads(data)
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON string: {e}")
    else:
        raw_data = data
    
    # Initialize lists to store the data
    metrics = []
    categories = []
    years = []
    values = []
    
    # Process each category (labor_market, national_accounts)
    for category, metrics_list in raw_data.items():
        # Process each metric dictionary in the list
        for metric_dict in metrics_list:
            # Each metric_dict has one key-value pair
            for metric_name, year_list in metric_dict.items():
                # Process each year dictionary in the list
                for year_dict in year_list:
                    # Each year_dict has one key-value pair
                    for year, value in year_dict.items():
                        metrics.append(metric_name)
                        categories.append(category)
                        years.append(int(year))  # Convert year to integer
                        values.append(float(value))  # Convert value to float
    
    # Create DataFrame
    df = pd.DataFrame({
        'Category': categories,
        'Metric': metrics,
        'Year': years,
        'Value': values
    })
    
    # Create separate DataFrames for before and after split_year
    df_before = df[df['Year'] < split_year].groupby(['Category', 'Metric'])['Value']
    df_after = df[df['Year'] >= split_year].groupby(['Category', 'Metric'])['Value']
    
    # Apply the specified action
    if action == 'sum':
        df_before = df_before.sum().reset_index()
        df_after = df_after.sum().reset_index()
        before_col = f'Sum_Before_{split_year}'
        after_col = f'Sum_After_{split_year}'
    else:  # action == 'avg'
        df_before = df_before.mean().reset_index()
        df_after = df_after.mean().reset_index()
        before_col = f'Avg_Before_{split_year}'
        after_col = f'Avg_After_{split_year}'
    
    # Rename the Value columns
    df_before = df_before.rename(columns={'Value': before_col})
    df_after = df_after.rename(columns={'Value': after_col})
    
    # Merge the two DataFrames
    result_df = pd.merge(df_before, df_after, on=['Category', 'Metric'])
    
    # Sort by Category and Metric
    result_df = result_df.sort_values(['Category', 'Metric'])
    
    # Round numeric columns to 2 decimal places
    numeric_cols = result_df.select_dtypes(include=['float64']).columns
    result_df[numeric_cols] = result_df[numeric_cols].round(2)
    
    # Return a JSON of the DataFrame
    return result_df.to_json(orient='records', indent=2)

The process_dataframe function takes nested dictionary data (from the economic report), flattens and transforms it into a Pandas DataFrame, and then aggregates values by category and metric.

It splits the data into two periods (before and after a given year) and calculates either the sum or average for each period, returning the result as a JSON string.

Tool 3: save_data_to_database

To save the data to Neon Postgres database, let’s place the code for the custom tool:

# Define the function to save the data to the database
def _save_data_to_database(data: Dict[str, Any], action: str = 'sum', split_year: int = 2025) -> Dict[str, Any]:
    """Save the processed data to the database using direct psycopg2 connection.
    
    Args:
        data (Dict[str, Any]): The processed data to save
        action (str): The aggregation action ('sum' or 'avg')
        split_year (int): The year used for splitting the data
        
    Returns:
        Dict[str, Any]: Result of the database operation including status and inserted records
    """
    print("save_data_to_database called")
    try:
        # Parse JSON if data is a string
        if isinstance(data, str):
            try:
                data = json.loads(data)
            except json.JSONDecodeError as e:
                raise ValueError(f"Invalid JSON string: {e}")

        # Get the connection string from the environment variable
        connection_string = os.getenv('NEON_DATABASE_URL')
        
        with psycopg.connect(connection_string) as conn:
            # Open a cursor to perform database operations
            with conn.cursor() as cur:
                # Create tables if they don't exist
                if action == 'sum':
                    cur.execute("""
                        CREATE TABLE IF NOT EXISTS economic_report_sum (
                            id SERIAL PRIMARY KEY,
                            category VARCHAR NOT NULL,
                            metric VARCHAR NOT NULL,
                            sum_before FLOAT NOT NULL,
                            sum_after FLOAT NOT NULL,
                            split_year INTEGER NOT NULL,
                            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                        )
                    """)
                else:  # action == 'avg'
                    cur.execute("""
                        CREATE TABLE IF NOT EXISTS economic_report_avg (
                            id SERIAL PRIMARY KEY,
                            category VARCHAR NOT NULL,
                            metric VARCHAR NOT NULL,
                            avg_before FLOAT NOT NULL,
                            avg_after FLOAT NOT NULL,
                            split_year INTEGER NOT NULL,
                            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                        )
                    """)
        
                # Prepare data for insertion
                records_inserted = 0
                for record in data['records']:
                    if action == 'sum':
                        category = record['Category']
                        metric = record['Metric']
                        sum_before = record[f'{action.title()}_Before_{split_year}']
                        sum_after = record[f'{action.title()}_After_{split_year}']
                        created_at = datetime.now(UTC)
                        
                        insert_query = """
                            INSERT INTO economic_report_sum 
                            (category, metric, sum_before, sum_after, split_year, created_at)
                            VALUES (%s, %s, %s, %s, %s, %s)
                        """
                        cur.execute(insert_query, (category, metric, sum_before, sum_after, split_year, created_at))
                    else:  # action == 'avg'
                        category = record['Category']
                        metric = record['Metric']
                        avg_before = record[f'{action.title()}_Before_{split_year}']
                        avg_after = record[f'{action.title()}_After_{split_year}']
                        created_at = datetime.now(UTC)
                        
                        insert_query = """
                            INSERT INTO economic_report_avg 
                            (category, metric, avg_before, avg_after, split_year, created_at)
                            VALUES (%s, %s, %s, %s, %s, %s)
                        """
                        cur.execute(insert_query, (category, metric, avg_before, avg_after, split_year, created_at))
                    
                    # Increment the number of records inserted
                    records_inserted += 1
                    
                # Commit the transaction
                conn.commit()
                
                # Prepare result
                result = {
                    'status': 'success',
                    'action': action,
                    'split_year': split_year,
                    'records_inserted': records_inserted,
                    'timestamp': datetime.now(UTC).isoformat()
                }
                
                return result
        
    except Exception as e:
        # Rollback in case of error
        if 'conn' in locals() and conn is not None:
            conn.rollback()
        return {
            'status': 'error',
            'error': str(e),
            'action': action,
            'split_year': split_year,
            'timestamp': datetime.now(UTC).isoformat()
        }
    finally:
        # Close the cursor and connection if they exist
        if 'cur' in locals() and cur is not None:
            cur.close()
        if 'conn' in locals() and conn is not None:
            conn.close()

# Define the function to save the data to the database for action 'sum'
def save_data_to_database_sum(data: Dict[str, Any], split_year: int = 2025) -> None:
    """Save the data to the database for action 'sum'."""
    _save_data_to_database(data, 'sum', split_year)


# Define the function to save the data to the database for action 'avg'
def save_data_to_database_avg(data: Dict[str, Any], split_year: int = 2025) -> None:
    """Save the data to the database for action 'avg'."""
    _save_data_to_database(data, 'avg', split_year)

This code defines a set of functions to store aggregated economic data in a PostgreSQL database.

The _save_data_to_database function handles both “sum” and “avg” actions by creating the appropriate table if it doesn’t exist and inserting records with values before and after a given year (split_year).

Two helper functions, save_data_to_database_sum and save_data_to_database_avg, provide convenient wrappers for saving data with either aggregation method and define the tools available to the AI agent.

Defining the Agent

With all the custom tools defined, we can now prepare the code to the AI agent:

# Define the async function to run the agent
async def run_agent(messages, deps, agent):
    # Begin an AgentRun, which is an async-iterable over the nodes of the agent's graph
    print("-" * 100)
    start_time = time.time()
    print("Running agent")
    print("-" * 100)
    async with agent.iter(messages, deps=deps) as agent_run:
        async for node in agent_run:
            # Each node represents a step in the agent's execution
            print(node)
            print("\n")
    end_time = time.time()
    print("-" * 100)
    print(f"Agent run completed in {end_time - start_time} seconds")
    print("-" * 100)
    # Print the result
    print("\n")
    print(agent_run.result.output)

This asynchronous function run_agent executes an agent’s workflow using an async iterator.

It processes each step (node) of the agent’s execution graph, logs the output for each node, and prints the final result along with the total runtime.

Running the Agent

To run the agent, we need to prepare the system message and the prompt for the LLM, as well as define the PDF file to be processed:

# Usage example
if __name__ == "__main__":
    # Get the path to the PDF file
    pdf_path = Path('new-zealand-economic-report-1.pdf')

    # Define the system prompt
    system_prompt=(
            """
                You are a helpful assistant that has access to a PDF file and can process data using Pandas.
                Make sure to use the tool 'extract_pdf_text' to extract the text from the PDF file.
                You can also use 'process_dataframe' to process data using Pandas.
                You can also use 'save_data_to_database_sum' to save the data to the database for action 'sum'.
                You can also use 'save_data_to_database_avg' to save the data to the database for action 'avg'.
            """
        )

    # Define the messages to send to the agent
    messages = [
            "You have tools available if you need to extract the text from the PDF file.",
            "You have tools available if you need to process data using Pandas.",
            "You have tools available if you need to save the data to the database for action 'sum'.",
            "You have tools available if you need to save the data to the database for action 'avg'.",
            "Extract the text from the PDF file and process the data using Pandas to return the average of year 2025 and save the result to the database (make sure to pass a list with records).",
            "Return the result in a JSON format."
        ]

    # Define the dependencies to send to the agent
    deps = BinaryContent(data=pdf_path.read_bytes(), media_type='application/pdf')

    # Define the agent
    agent = Agent(
        os.getenv('MISTRAL_MODEL'),  
        api_key=os.getenv('MISTRAL_API_KEY'),
        tools=[extract_pdf_text, process_dataframe, save_data_to_database_sum, save_data_to_database_avg], 
        system_prompt=system_prompt
    )

    # Run the async function
    asyncio.run(run_agent(messages, deps, agent))

This script demonstrates how to use an AI agent to extract, process, and store data from a PDF economic report.

It sets up a system prompt, defines task instructions (messages), provides the PDF as binary input, and equips the agent with tools for text extraction, data processing (using Pandas), and database saving.

It then runs the agent asynchronously to perform the complete workflow and return the result in JSON format.


Executing the Script

With all the code now prepared, we can run the script to process the PDF, process the data with Pandas and save the result to a Postgres database.

All of this will be orchestrated by the AI agent.

Run the script as a normal Python script:

python pdf_extractor_agent.py

For the previously defined code, it will process the average of the values, splitting it in the year 2025.

As the AI agent executes, you can see the following node executions:

----------------------------------------------------------------------------------------------------
Running agent
----------------------------------------------------------------------------------------------------
UserPromptNode(user_prompt=['You have tools available if you need to extract the text from the PDF file.', 'You have tools available if you need to process data using Pandas.', "You have tools available if you need to save the data to the database for action 'sum'.", "You have tools available if you need to save the data to the database for action 'avg'.", 'Extract the text from the PDF file and process the data using Pandas to return the average of year 2025 and save the result to the database (make sure to pass a list with records).', 'Return the result in a JSON format.'], instructions=None, instructions_functions=[], system_prompts=("\n                You are a helpful assistant that has access to a PDF file and can process data using Pandas.\n                Make sure to use the tool 'extract_pdf_text' to extract the text from the PDF file.\n                You can also use 'process_dataframe' to process data using Pandas.\n                You can also use 'save_data_to_database_sum' to save the data to the database for action 'sum'.\n                You can also use 'save_data_to_database_avg' to save the data to the database for action 'avg'.\n            ",), system_prompt_functions=[], system_prompt_dynamic_functions={})


ModelRequestNode(request=ModelRequest(parts=[SystemPromptPart(content="\n                You are a helpful assistant that has access to a PDF file and can process data using Pandas.\n                Make sure to use the tool 'extract_pdf_text' to extract the text from the PDF file.\n                You can also use 'process_dataframe' to process data using Pandas.\n                You can also use 'save_data_to_database_sum' to save the data to the database for action 'sum'.\n                You can also use 'save_data_to_database_avg' to save the data to the database for action 'avg'.\n            ", timestamp=datetime.datetime(2025, 6, 16, 10, 1, 47, 504817, tzinfo=datetime.timezone.utc)), UserPromptPart(content=['You have tools available if you need to extract the text from the PDF file.', 'You have tools available if you need to process data using Pandas.', "You have tools available if you need to save the data to the database for action 'sum'.", "You have tools available if you need to save the data to the database for action 'avg'.", 'Extract the text from the PDF file and process the data using Pandas to return the average of year 2025 and save the result to the database (make sure to pass a list with records).', 'Return the result in a JSON format.'], timestamp=datetime.datetime(2025, 6, 16, 10, 1, 47, 504817, tzinfo=datetime.timezone.utc))]))       


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='extract_pdf_text', args='{}', tool_call_id='KFNl3Fcxr')], usage=Usage(requests=1, request_tokens=685, response_tokens=19, total_tokens=704), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 10, 1, 48, tzinfo=TzInfo(UTC)), vendor_id='07310f55dd104cb4818c562ffff62be6'))


extract_pdf_text called
ModelRequestNode(request=ModelRequest(parts=[ToolReturnPart(tool_name='extract_pdf_text', content={'labor_market': [{'Employment': [{'2020': 1.3}, {'2021': 2.2}, {'2022': 1.7}, {'2023': 3.1}, {'2024': 1.1}, {'2025': 1.1}, {'2026': 1.6}, {'2027': 1.7}, {'2028': 1.7}, {'2029': 1.6}]}, {'Unemployment (percent of labor force, ann. average)': [{'2020': 4.6}, {'2021': 3.8}, {'2022': 3.3}, {'2023': 3.7}, {'2024': 5.0}, {'2025': 5.4}, {'2026': 5.2}, {'2027': 5.0}, {'2028': 4.7}, {'2029': 4.5}]}, {'Wages (nominal percent change)': [{'2020': 3.8}, {'2021': 3.8}, {'2022': 6.5}, {'2023': 7.0}, {'2024': 4.8}, {'2025': 3.9}, {'2026': 3.7}, {'2027': 3.2}, {'2028': 3.0}, {'2029': 3.0}]}], 'national_accounts': [{'Real GDP (production)': [{'2020': -1.4}, {'2021': 5.6}, {'2022': 2.4}, {'2023': 0.6}, {'2024': 1.0}, {'2025': 2.0}, {'2026': 2.4}, {'2027': 2.4}, {'2028': 2.4}, {'2029': 2.4}]}, {'Domestic demand': [{'2020': -1.7}, {'2021': 10.1}, {'2022': 3.4}, {'2023': -1.5}, {'2024': -0.4}, {'2025': 1.5}, {'2026': 2.0}, {'2027': 2.1}, {'2028': 2.2}, {'2029': 2.1}]}, {'Private consumption': [{'2020': -1.7}, {'2021': 7.7}, {'2022': 3.2}, {'2023': 0.3}, {'2024': -1.6}, {'2025': 2.0}, {'2026': 2.1}, {'2027': 2.3}, {'2028': 2.4}, {'2029': 2.3}]}, {'Public consumption': [{'2020': 6.7}, {'2021': 7.8}, {'2022': 4.9}, {'2023': -1.1}, {'2024': -1.1}, {'2025': 0.0}, {'2026': 0.6}, {'2027': 0.4}, {'2028': 0.4}, {'2029': 0.4}]}, {'Investment': [{'2020': -7.8}, {'2021': 18.1}, {'2022': 2.0}, {'2023': -5.1}, {'2024': 0.3}, {'2025': 1.5}, {'2026': 3.0}, {'2027': 3.0}, {'2028': 3.0}, {'2029': 2.9}]}, {'Public': [{'2020': 4.0}, {'2021': 7.9}, {'2022': -6.4}, {'2023': 4.9}, {'2024': 2.5}, {'2025': 1.3}, {'2026': 2.3}, {'2027': 2.5}, {'2028': 2.8}, {'2029': 2.8}]}, {'Private': [{'2020': -7.4}, {'2021': 13.5}, {'2022': 6.3}, {'2023': -2.6}, {'2024': -4.1}, {'2025': 1.5}, {'2026': 3.2}, {'2027': 3.1}, {'2028': 3.1}, {'2029': 2.9}]}, {'Private business': [{'2020': -9.3}, {'2021': 15.7}, {'2022': 9.6}, {'2023': -1.9}, {'2024': -4.5}, {'2025': 1.4}, {'2026': 3.4}, {'2027': 3.4}, {'2028': 3.4}, {'2029': 3.1}]}, {'Dwelling': [{'2020': -3.1}, {'2021': 9.0}, {'2022': -0.9}, {'2023': -4.2}, {'2024': -3.0}, {'2025': 1.9}, {'2026': 2.8}, {'2027': 2.4}, {'2028': 2.4}, {'2029': 2.4}]}, {'Inventories (contribution to growth, percent)': [{'2020': -0.8}, {'2021': 1.4}, {'2022': -0.4}, {'2023': -1.1}, {'2024': 0.7}, {'2025': 0.0}, {'2026': 0.0}, {'2027': 0.0}, {'2028': 0.0}, {'2029': 0.0}]}, {'Net exports (contribution to growth, percent)': [{'2020': 1.5}, {'2021': -4.8}, {'2022': -1.5}, {'2023': 2.2}, {'2024': 1.5}, {'2025': 0.3}, {'2026': 0.3}, {'2027': 0.1}, {'2028': 0.1}, {'2029': 0.1}]}, {'Real gross domestic income': [{'2020': -0.7}, {'2021': 5.1}, {'2022': 1.3}, {'2023': 0.0}, {'2024': 1.4}, {'2025': 2.1}, {'2026': 2.5}, {'2027': 2.5}, {'2028': 2.5}, {'2029': 2.5}]}, {'Investment (percent of GDP)': [{'2020': 22.1}, {'2021': 25.0}, {'2022': 26.0}, {'2023': 24.4}, {'2024': 24.3}, {'2025': 24.2}, {'2026': 24.4}, {'2027': 24.4}, {'2028': 24.4}, {'2029': 24.5}]}, {'Public': [{'2020': 5.5}, {'2021': 5.7}, {'2022': 5.4}, {'2023': 5.7}, {'2024': 5.7}, {'2025': 5.7}, {'2026': 5.7}, {'2027': 5.6}, {'2028': 5.6}, {'2029': 5.6}]}, {'Private': [{'2020': 16.6}, {'2021': 19.4}, {'2022': 20.6}, {'2023': 18.7}, {'2024': 18.6}, {'2025': 18.6}, {'2026': 18.7}, {'2027': 18.7}, {'2028': 18.8}, {'2029': 18.9}]}, {'Savings (gross, percent of GDP)': [{'2020': 21.1}, {'2021': 19.2}, {'2022': 17.2}, {'2023': 17.5}, {'2024': 18.3}, {'2025': 18.9}, {'2026': 19.6}, {'2027': 20.0}, {'2028': 20.3}, {'2029': 20.8}]}, {'Public': [{'2020': -4.3}, {'2021': -3.2}, {'2022': -3.5}, {'2023': -3.5}, {'2024': -3.5}, {'2025': -2.6}, {'2026': -1.7}, {'2027': -1.1}, {'2028': -0.4}, {'2029': -0.1}]}, {'Private': [{'2020': 25.5}, {'2021': 22.4}, {'2022': 20.7}, {'2023': 21.0}, {'2024': 21.8}, {'2025': 21.4}, {'2026': 21.3}, {'2027': 21.0}, {'2028': 20.7}, {'2029': 20.9}]}, {'Potential output': [{'2020': 1.6}, {'2021': 1.5}, {'2022': 1.9}, {'2023': 2.1}, {'2024': 2.3}, {'2025': 2.3}, {'2026': 2.2}, {'2027': 2.2}, {'2028': 2.2}, {'2029': 2.2}]}, {'Output gap (percent of potential)': [{'2020': -2.3}, {'2021': 1.7}, {'2022': 2.1}, {'2023': 0.6}, {'2024': -0.5}, {'2025': -0.9}, {'2026': -0.7}, {'2027': -0.5}, {'2028': -0.2}, {'2029': 0.0}]}]}, tool_call_id='KFNl3Fcxr', timestamp=datetime.datetime(2025, 6, 16, 10, 2, 36, 802005, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='process_dataframe', args='{"action": "avg", "data": {"labor_market": [{"Employment": [{"2020": 1.3}, {"2021": 2.2}, {"2022": 1.7}, {"2023": 3.1}, {"2024": 1.1}, {"2025": 1.1}, {"2026": 1.6}, {"2027": 1.7}, {"2028": 1.7}, {"2029": 1.6}]}, {"Unemployment (percent of labor force, ann. average)": [{"2020": 4.6}, {"2021": 3.8}, {"2022": 3.3}, {"2023": 3.7}, {"2024": 5}, {"2025": 5.4}, {"2026": 5.2}, {"2027": 5}, {"2028": 4.7}, {"2029": 4.5}]}, {"Wages (nominal percent change)": [{"2020": 3.8}, {"2021": 3.8}, {"2022": 6.5}, {"2023": 7}, {"2024": 4.8}, {"2025": 3.9}, {"2026": 3.7}, {"2027": 3.2}, {"2028": 3}, {"2029": 3}]}], "national_accounts": [{"Real GDP (production)": [{"2020": -1.4}, {"2021": 5.6}, {"2022": 2.4}, {"2023": 0.6}, {"2024": 1}, {"2025": 2}, {"2026": 2.4}, {"2027": 2.4}, {"2028": 2.4}, {"2029": 2.4}]}, {"Domestic demand": [{"2020": -1.7}, {"2021": 10.1}, {"2022": 3.4}, {"2023": -1.5}, {"2024": -0.4}, {"2025": 1.5}, {"2026": 2}, {"2027": 2.1}, {"2028": 2.2}, {"2029": 2.1}]}, {"Private consumption": [{"2020": -1.7}, {"2021": 7.7}, {"2022": 3.2}, {"2023": 0.3}, {"2024": -1.6}, {"2025": 2}, {"2026": 2.1}, {"2027": 2.3}, {"2028": 2.4}, {"2029": 2.3}]}, {"Public consumption": [{"2020": 6.7}, {"2021": 7.8}, {"2022": 4.9}, {"2023": -1.1}, {"2024": -1.1}, {"2025": 0}, {"2026": 0.6}, {"2027": 0.4}, {"2028": 0.4}, {"2029": 0.4}]}, {"Investment": [{"2020": -7.8}, {"2021": 18.1}, {"2022": 2}, {"2023": -5.1}, {"2024": 0.3}, {"2025": 1.5}, {"2026": 3}, {"2027": 3}, {"2028": 3}, {"2029": 2.9}]}, {"Public": [{"2020": 4}, {"2021": 7.9}, {"2022": -6.4}, {"2023": 4.9}, {"2024": 2.5}, {"2025": 1.3}, {"2026": 2.3}, {"2027": 2.5}, {"2028": 2.8}, {"2029": 2.8}]}, {"Private": [{"2020": -7.4}, {"2021": 13.5}, {"2022": 6.3}, {"2023": -2.6}, {"2024": -4.1}, {"2025": 1.5}, {"2026": 3.2}, {"2027": 3.1}, {"2028": 3.1}, {"2029": 2.9}]}, {"Private business": [{"2020": -9.3}, {"2021": 15.7}, {"2022": 9.6}, {"2023": -1.9}, {"2024": -4.5}, {"2025": 1.4}, {"2026": 3.4}, {"2027": 3.4}, {"2028": 3.4}, {"2029": 3.1}]}, {"Dwelling": [{"2020": -3.1}, {"2021": 9}, {"2022": -0.9}, {"2023": -4.2}, {"2024": -3}, {"2025": 1.9}, {"2026": 2.8}, {"2027": 2.4}, {"2028": 2.4}, {"2029": 2.4}]}, {"Inventories (contribution to growth, percent)": [{"2020": -0.8}, {"2021": 1.4}, {"2022": -0.4}, {"2023": -1.1}, {"2024": 0.7}, {"2025": 0}, {"2026": 0}, {"2027": 0}, {"2028": 0}, {"2029": 0}]}, {"Net exports (contribution to growth, percent)": [{"2020": 1.5}, {"2021": -4.8}, {"2022": -1.5}, {"2023": 2.2}, {"2024": 1.5}, {"2025": 0.3}, {"2026": 0.3}, {"2027": 0.1}, {"2028": 0.1}, {"2029": 0.1}]}, {"Real gross domestic income": [{"2020": -0.7}, {"2021": 5.1}, {"2022": 1.3}, {"2023": 0}, {"2024": 1.4}, {"2025": 2.1}, {"2026": 2.5}, {"2027": 2.5}, {"2028": 2.5}, {"2029": 2.5}]}, {"Investment (percent of GDP)": [{"2020": 22.1}, {"2021": 25}, {"2022": 26}, {"2023": 24.4}, {"2024": 24.3}, {"2025": 24.2}, {"2026": 24.4}, {"2027": 24.4}, {"2028": 24.4}, {"2029": 24.5}]}, {"Public": [{"2020": 5.5}, {"2021": 5.7}, {"2022": 5.4}, {"2023": 5.7}, {"2024": 5.7}, {"2025": 5.7}, {"2026": 5.7}, {"2027": 5.6}, {"2028": 5.6}, {"2029": 5.6}]}, {"Private": [{"2020": 16.6}, {"2021": 19.4}, {"2022": 20.6}, {"2023": 18.7}, {"2024": 18.6}, {"2025": 18.6}, {"2026": 18.7}, {"2027": 18.7}, {"2028": 18.8}, {"2029": 18.9}]}, {"Savings (gross, percent of GDP)": [{"2020": 21.1}, {"2021": 19.2}, {"2022": 17.2}, {"2023": 17.5}, {"2024": 18.3}, {"2025": 18.9}, {"2026": 19.6}, {"2027": 20}, {"2028": 20.3}, {"2029": 20.8}]}, {"Public": [{"2020": -4.3}, {"2021": -3.2}, {"2022": -3.5}, {"2023": -3.5}, {"2024": -3.5}, {"2025": -2.6}, {"2026": -1.7}, {"2027": -1.1}, {"2028": -0.4}, {"2029": -0.1}]}, {"Private": [{"2020": 25.5}, {"2021": 22.4}, {"2022": 20.7}, {"2023": 21}, {"2024": 21.8}, {"2025": 21.4}, {"2026": 21.3}, {"2027": 21}, {"2028": 20.7}, {"2029": 20.9}]}, {"Potential output": [{"2020": 1.6}, {"2021": 1.5}, {"2022": 1.9}, {"2023": 2.1}, {"2024": 2.3}, {"2025": 2.3}, {"2026": 2.2}, {"2027": 2.2}, {"2028": 2.2}, {"2029": 2.2}]}, {"Output gap (percent of potential)": [{"2020": -2.3}, {"2021": 1.7}, {"2022": 2.1}, {"2023": 0.6}, {"2024": -0.5}, {"2025": -0.9}, {"2026": -0.7}, {"2027": -0.5}, {"2028": -0.2}, {"2029": 0}]}]}}', tool_call_id='U1sg70wOk')], usage=Usage(requests=1, request_tokens=3330, response_tokens=2760, total_tokens=6090), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 10, 2, 37, tzinfo=TzInfo(UTC)), vendor_id='8561c34f99ad456e92245c1aff991255'))


process_dataframe called with action=avg, split_year=2025
ModelRequestNode(request=ModelRequest(parts=[ToolReturnPart(tool_name='process_dataframe', content='[\n  {\n    "Category":"labor_market",\n    "Metric":"Employment",\n    "Avg_Before_2025":1.88,\n    "Avg_After_2025":1.54\n  },\n  {\n    "Category":"labor_market",\n    "Metric":"Unemployment (percent of labor force, ann. average)",\n    "Avg_Before_2025":4.08,\n    "Avg_After_2025":4.96\n  },\n  {\n    "Category":"labor_market",\n    "Metric":"Wages (nominal percent change)",\n    "Avg_Before_2025":5.18,\n    "Avg_After_2025":3.36\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Domestic demand",\n    "Avg_Before_2025":1.98,\n    "Avg_After_2025":1.98\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Dwelling",\n    "Avg_Before_2025":-0.44,\n    "Avg_After_2025":2.38\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Inventories (contribution to growth, percent)",\n    "Avg_Before_2025":-0.04,\n    "Avg_After_2025":0.0\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Investment",\n    "Avg_Before_2025":1.5,\n    "Avg_After_2025":2.68\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Investment (percent of GDP)",\n    "Avg_Before_2025":24.36,\n    "Avg_After_2025":24.38\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Net exports (contribution to growth, percent)",\n    "Avg_Before_2025":-0.22,\n    "Avg_After_2025":0.18\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Output gap (percent of potential)",\n    "Avg_Before_2025":0.32,\n    "Avg_After_2025":-0.46\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Potential output",\n    "Avg_Before_2025":1.88,\n    "Avg_After_2025":2.22\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private",\n    "Avg_Before_2025":14.07,\n    "Avg_After_2025":14.19\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private business",\n    "Avg_Before_2025":1.92,\n    "Avg_After_2025":2.94\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private consumption",\n    "Avg_Before_2025":1.58,\n    "Avg_After_2025":2.22\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public",\n    "Avg_Before_2025":1.53,\n    "Avg_After_2025":2.27\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public consumption",\n    "Avg_Before_2025":3.44,\n    "Avg_After_2025":0.36\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Real GDP (production)",\n    "Avg_Before_2025":1.64,\n    "Avg_After_2025":2.32\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Real gross domestic income",\n    "Avg_Before_2025":1.42,\n    "Avg_After_2025":2.42\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Savings (gross, percent of GDP)",\n    "Avg_Before_2025":18.66,\n    "Avg_After_2025":19.92\n  }\n]', tool_call_id='U1sg70wOk', timestamp=datetime.datetime(2025, 6, 16, 10, 4, 55, 891820, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='save_data_to_database_avg', args='{"data": {"records": [{"Category": "labor_market", "Metric": "Employment", "Avg_Before_2025": 1.88, "Avg_After_2025": 1.54}, {"Category": "labor_market", "Metric": "Unemployment (percent of labor force, ann. average)", "Avg_Before_2025": 4.08, "Avg_After_2025": 4.96}, {"Category": "labor_market", "Metric": "Wages (nominal percent change)", "Avg_Before_2025": 5.18, "Avg_After_2025": 3.36}, {"Category": "national_accounts", "Metric": "Domestic demand", "Avg_Before_2025": 1.98, "Avg_After_2025": 1.98}, {"Category": "national_accounts", "Metric": "Dwelling", "Avg_Before_2025": -0.44, "Avg_After_2025": 2.38}, {"Category": "national_accounts", "Metric": "Inventories (contribution to growth, percent)", "Avg_Before_2025": -0.04, "Avg_After_2025": 0}, {"Category": "national_accounts", "Metric": "Investment", "Avg_Before_2025": 1.5, "Avg_After_2025": 2.68}, {"Category": "national_accounts", "Metric": "Investment (percent of GDP)", "Avg_Before_2025": 24.36, "Avg_After_2025": 24.38}, {"Category": "national_accounts", "Metric": "Net exports (contribution to growth, percent)", "Avg_Before_2025": -0.22, "Avg_After_2025": 0.18}, {"Category": "national_accounts", "Metric": "Output gap (percent of potential)", "Avg_Before_2025": 0.32, "Avg_After_2025": -0.46}, {"Category": "national_accounts", "Metric": "Potential output", "Avg_Before_2025": 1.88, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Private", "Avg_Before_2025": 14.07, "Avg_After_2025": 14.19}, {"Category": "national_accounts", "Metric": "Private business", "Avg_Before_2025": 1.92, "Avg_After_2025": 2.94}, {"Category": "national_accounts", "Metric": "Private consumption", "Avg_Before_2025": 1.58, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Public", "Avg_Before_2025": 1.53, "Avg_After_2025": 2.27}, {"Category": "national_accounts", "Metric": "Public consumption", "Avg_Before_2025": 3.44, "Avg_After_2025": 0.36}, {"Category": "national_accounts", "Metric": "Real GDP (production)", "Avg_Before_2025": 1.64, "Avg_After_2025": 2.32}, {"Category": "national_accounts", "Metric": "Real gross domestic income", "Avg_Before_2025": 1.42, "Avg_After_2025": 2.42}, {"Category": "national_accounts", "Metric": "Savings (gross, percent of GDP)", "Avg_Before_2025": 18.66, "Avg_After_2025": 19.92}]}, "split_year": 2025}', tool_call_id='e92DZ8MpW')], usage=Usage(requests=1, request_tokens=7319, response_tokens=1067, total_tokens=8386), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 10, 5, 41, tzinfo=TzInfo(UTC)), vendor_id='9a1233fcef03435cba6aec23eea92c78'))


save_data_to_database called
ModelRequestNode(request=ModelRequest(parts=[ToolReturnPart(tool_name='save_data_to_database_avg', content=None, tool_call_id='e92DZ8MpW', timestamp=datetime.datetime(2025, 6, 16, 10, 5, 58, 1210, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[TextPart(content='[{"name": "save_data_to_database_avg", "arguments": {"data": {"records": [{"Category": "labor_market", "Metric": "Employment", "Avg_Before_2025": 1.88, "Avg_After_2025": 1.54}, {"Category": "labor_market", "Metric": "Unemployment (percent of labor force, ann. average)", "Avg_Before_2025": 4.08, "Avg_After_2025": 4.96}, {"Category": "labor_market", "Metric": "Wages (nominal percent change)", "Avg_Before_2025": 5.18, "Avg_After_2025": 3.36}, {"Category": "national_accounts", "Metric": "Domestic demand", "Avg_Before_2025": 1.98, "Avg_After_2025": 1.98}, {"Category": "national_accounts", "Metric": "Dwelling", "Avg_Before_2025": -0.44, "Avg_After_2025": 2.38}, {"Category": "national_accounts", "Metric": "Inventories (contribution to growth, percent)", "Avg_Before_2025": -0.04, "Avg_After_2025": 0}, {"Category": "national_accounts", "Metric": "Investment", "Avg_Before_2025": 1.5, "Avg_After_2025": 2.68}, {"Category": "national_accounts", "Metric": "Investment (percent of GDP)", "Avg_Before_2025": 24.36, "Avg_After_2025": 24.38}, {"Category": "national_accounts", "Metric": "Net exports (contribution to growth, percent)", "Avg_Before_2025": -0.22, "Avg_After_2025": 0.18}, {"Category": "national_accounts", "Metric": "Output gap (percent of potential)", "Avg_Before_2025": 0.32, "Avg_After_2025": -0.46}, {"Category": "national_accounts", "Metric": "Potential output", "Avg_Before_2025": 1.88, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Private", "Avg_Before_2025": 14.07, "Avg_After_2025": 14.19}, {"Category": "national_accounts", "Metric": "Private business", "Avg_Before_2025": 1.92, "Avg_After_2025": 2.94}, {"Category": "national_accounts", "Metric": "Private consumption", "Avg_Before_2025": 1.58, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Public", "Avg_Before_2025": 1.53, "Avg_After_2025": 2.27}, {"Category": "national_accounts", "Metric": "Public consumption", "Avg_Before_2025": 3.44, "Avg_After_2025": 0.36}, {"Category": "national_accounts", "Metric": "Real GDP (production)", "Avg_Before_2025": 1.64, "Avg_After_2025": 2.32}, {"Category": "national_accounts", "Metric": "Real gross domestic income", "Avg_Before_2025": 1.42, "Avg_After_2025": 2.42}, {"Category": "national_accounts", "Metric": "Savings (gross, percent of GDP)", "Avg_Before_2025": 18.66, "Avg_After_2025": 19.92}]}, "split_year": 2025}}]')], usage=Usage(requests=1, request_tokens=8413, response_tokens=1066, total_tokens=9479), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 10, 5, 58, tzinfo=TzInfo(UTC)), vendor_id='c8bdf3a1d21f4cb68cedd1b9ba89e7f0'))


End(data=FinalResult(output='[{"name": "save_data_to_database_avg", "arguments": {"data": {"records": [{"Category": "labor_market", "Metric": "Employment", "Avg_Before_2025": 1.88, "Avg_After_2025": 1.54}, {"Category": "labor_market", "Metric": "Unemployment (percent of labor force, ann. average)", "Avg_Before_2025": 4.08, "Avg_After_2025": 4.96}, {"Category": "labor_market", "Metric": "Wages (nominal percent change)", "Avg_Before_2025": 5.18, "Avg_After_2025": 3.36}, {"Category": "national_accounts", "Metric": "Domestic demand", "Avg_Before_2025": 1.98, "Avg_After_2025": 1.98}, {"Category": "national_accounts", "Metric": "Dwelling", "Avg_Before_2025": -0.44, "Avg_After_2025": 2.38}, {"Category": "national_accounts", "Metric": "Inventories (contribution to growth, percent)", "Avg_Before_2025": -0.04, "Avg_After_2025": 0}, {"Category": "national_accounts", "Metric": "Investment", "Avg_Before_2025": 1.5, "Avg_After_2025": 2.68}, {"Category": "national_accounts", "Metric": "Investment (percent of GDP)", "Avg_Before_2025": 24.36, "Avg_After_2025": 24.38}, {"Category": "national_accounts", "Metric": "Net exports (contribution to growth, percent)", "Avg_Before_2025": -0.22, "Avg_After_2025": 0.18}, {"Category": "national_accounts", "Metric": "Output gap (percent of potential)", "Avg_Before_2025": 0.32, "Avg_After_2025": -0.46}, {"Category": "national_accounts", "Metric": "Potential output", "Avg_Before_2025": 1.88, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Private", "Avg_Before_2025": 14.07, "Avg_After_2025": 14.19}, {"Category": "national_accounts", "Metric": "Private business", "Avg_Before_2025": 1.92, "Avg_After_2025": 2.94}, {"Category": "national_accounts", "Metric": "Private consumption", "Avg_Before_2025": 1.58, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Public", "Avg_Before_2025": 1.53, "Avg_After_2025": 2.27}, {"Category": "national_accounts", "Metric": "Public consumption", "Avg_Before_2025": 3.44, "Avg_After_2025": 0.36}, {"Category": "national_accounts", "Metric": "Real GDP (production)", "Avg_Before_2025": 1.64, "Avg_After_2025": 2.32}, {"Category": "national_accounts", "Metric": "Real gross domestic income", "Avg_Before_2025": 1.42, "Avg_After_2025": 2.42}, {"Category": "national_accounts", "Metric": "Savings (gross, percent of GDP)", "Avg_Before_2025": 18.66, "Avg_After_2025": 19.92}]}, "split_year": 2025}}]'))


----------------------------------------------------------------------------------------------------
Agent run completed in 293.2259724140167 seconds
----------------------------------------------------------------------------------------------------


[{"name": "save_data_to_database_avg", "arguments": {"data": {"records": [{"Category": "labor_market", "Metric": "Employment", "Avg_Before_2025": 1.88, "Avg_After_2025": 1.54}, {"Category": "labor_market", "Metric": "Unemployment (percent of labor force, ann. average)", "Avg_Before_2025": 4.08, "Avg_After_2025": 4.96}, {"Category": "labor_market", "Metric": "Wages (nominal percent change)", "Avg_Before_2025": 5.18, "Avg_After_2025": 3.36}, {"Category": "national_accounts", "Metric": "Domestic demand", "Avg_Before_2025": 1.98, "Avg_After_2025": 1.98}, {"Category": "national_accounts", "Metric": "Dwelling", "Avg_Before_2025": -0.44, "Avg_After_2025": 2.38}, {"Category": "national_accounts", "Metric": "Inventories (contribution to growth, percent)", "Avg_Before_2025": -0.04, "Avg_After_2025": 0}, {"Category": "national_accounts", "Metric": "Investment", "Avg_Before_2025": 1.5, "Avg_After_2025": 2.68}, {"Category": "national_accounts", "Metric": "Investment (percent of GDP)", "Avg_Before_2025": 24.36, "Avg_After_2025": 24.38}, {"Category": "national_accounts", "Metric": "Net exports (contribution to growth, percent)", "Avg_Before_2025": -0.22, "Avg_After_2025": 0.18}, {"Category": "national_accounts", "Metric": "Output gap (percent of potential)", "Avg_Before_2025": 0.32, "Avg_After_2025": -0.46}, {"Category": "national_accounts", "Metric": "Potential output", "Avg_Before_2025": 1.88, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Private", "Avg_Before_2025": 14.07, "Avg_After_2025": 14.19}, {"Category": "national_accounts", "Metric": "Private business", "Avg_Before_2025": 1.92, "Avg_After_2025": 2.94}, {"Category": "national_accounts", "Metric": "Private consumption", "Avg_Before_2025": 1.58, "Avg_After_2025": 2.22}, {"Category": "national_accounts", "Metric": "Public", "Avg_Before_2025": 1.53, "Avg_After_2025": 2.27}, {"Category": "national_accounts", "Metric": "Public consumption", "Avg_Before_2025": 3.44, "Avg_After_2025": 0.36}, {"Category": "national_accounts", "Metric": "Real GDP (production)", "Avg_Before_2025": 1.64, "Avg_After_2025": 2.32}, {"Category": "national_accounts", "Metric": "Real gross domestic income", "Avg_Before_2025": 1.42, "Avg_After_2025": 2.42}, {"Category": "national_accounts", "Metric": "Savings (gross, percent of GDP)", "Avg_Before_2025": 18.66, "Avg_After_2025": 19.92}]}, "split_year": 2025}}]

You can see how the AI agent, executes the appropriate tools, passing the correct values to each of them and using the output of each tool as the input of the next tool.

Checking the Neon Postgres database, you can see that the records were inserted:

building an ai agent

If you want to run the code for processing the sum of values (instead of the average, as defined), then the prompt to the LLM needs to specify that, meaning you will need to change part of the previous code to this prompt:

# Define the messages to send to the agent
    messages = [
            "You have tools available if you need to extract the text from the PDF file.",
            "You have tools available if you need to process data using Pandas.",
            "You have tools available if you need to save the data to the database for action 'sum'.",
            "You have tools available if you need to save the data to the database for action 'avg'.",
            "Extract the text from the PDF file and process the data using Pandas to return the sum of year 2025 and save the result to the database (make sure to pass a list with records).",
            "Return the result in a JSON format."
        ]

Then you can again observe the AI agent execution by following node executions:

----------------------------------------------------------------------------------------------------
Running agent
----------------------------------------------------------------------------------------------------
UserPromptNode(user_prompt=['You have tools available if you need to extract the text from the PDF file.', 'You have tools available if you need to process data using Pandas.', "You have tools available if you need to save the data to the database for action 'sum'.", "You have tools available if you need to save the data to the database for action 'avg'.", 'Extract the text from the PDF file and process the data using Pandas to return the sum of year 2025 and save the result to the database (make sure to pass a list with records).', 'Return the result in a JSON format.'], instructions=None, instructions_functions=[], system_prompts=("\n                You are a helpful assistant that has access to a PDF file and can process data using Pandas.\n                Make sure to use the tool 'extract_pdf_text' to extract the text from the PDF file.\n                You can also use 'process_dataframe' to process data using Pandas.\n                You can also use 'save_data_to_database_sum' to save the data to the database for action 'sum'.\n                You can also use 'save_data_to_database_avg' to save the data to the database for action 'avg'.\n            ",), system_prompt_functions=[], system_prompt_dynamic_functions={})


ModelRequestNode(request=ModelRequest(parts=[SystemPromptPart(content="\n                You are a helpful assistant that has access to a PDF file and can process data using Pandas.\n                Make sure to use the tool 'extract_pdf_text' to extract the text from the PDF file.\n                You can also use 'process_dataframe' to process data using Pandas.\n                You can also use 'save_data_to_database_sum' to save the data to the database for action 'sum'.\n                You can also use 'save_data_to_database_avg' to save the data to the database for action 'avg'.\n            ", timestamp=datetime.datetime(2025, 6, 16, 9, 50, 55, 930042, tzinfo=datetime.timezone.utc)), UserPromptPart(content=['You have tools available if you need to extract the text from the PDF file.', 'You have tools available if you need to process data using Pandas.', "You have tools available if you need to save the data to the database for action 'sum'.", "You have tools available if you need to save the data to the database for action 'avg'.", 'Extract the text from the PDF file and process the data using Pandas to return the sum of year 2025 and save the result to the database (make sure to pass a list with records).', 'Return the result in a JSON format.'], timestamp=datetime.datetime(2025, 6, 16, 9, 50, 55, 930042, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='extract_pdf_text', args='{}', tool_call_id='jTCCD3q6K')], usage=Usage(requests=1, request_tokens=685, response_tokens=19, total_tokens=704), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 9, 50, 56, tzinfo=TzInfo(UTC)), vendor_id='689e7b098c7940d29a079d1626c4a2a8'))


extract_pdf_text called
ModelRequestNode(request=ModelRequest(parts=[ToolReturnPart(tool_name='extract_pdf_text', content={'labor_market': [{'Employment': [{'2020': 1.3}, {'2021': 2.2}, {'2022': 1.7}, {'2023': 3.1}, {'2024': 1.1}, {'2025': 1.1}, {'2026': 1.6}, {'2027': 1.7}, {'2028': 1.7}, {'2029': 1.6}]}, {'Unemployment (percent of labor force, ann. average)': [{'2020': 4.6}, {'2021': 3.8}, {'2022': 3.3}, {'2023': 3.7}, {'2024': 5.0}, {'2025': 5.4}, {'2026': 5.2}, {'2027': 5.0}, {'2028': 4.7}, {'2029': 4.5}]}, {'Wages (nominal percent change)': [{'2020': 3.8}, {'2021': 3.8}, {'2022': 6.5}, {'2023': 7.0}, {'2024': 4.8}, {'2025': 3.9}, {'2026': 3.7}, {'2027': 3.2}, {'2028': 3.0}, {'2029': 3.0}]}], 'national_accounts': [{'Real GDP (production)': [{'2020': -1.4}, {'2021': 5.6}, {'2022': 2.4}, {'2023': 0.6}, {'2024': 1.0}, {'2025': 2.0}, {'2026': 2.4}, {'2027': 2.4}, {'2028': 2.4}, {'2029': 2.4}]}, {'Domestic demand': [{'2020': -1.7}, {'2021': 10.1}, {'2022': 3.4}, {'2023': -1.5}, {'2024': -0.4}, {'2025': 1.5}, {'2026': 2.0}, {'2027': 2.1}, {'2028': 2.2}, {'2029': 2.1}]}, {'Private consumption': [{'2020': -1.7}, {'2021': 7.7}, {'2022': 3.2}, {'2023': 0.3}, {'2024': -1.6}, {'2025': 2.0}, {'2026': 2.1}, {'2027': 2.3}, {'2028': 2.4}, {'2029': 2.3}]}, {'Public consumption': [{'2020': 6.7}, {'2021': 7.8}, {'2022': 4.9}, {'2023': -1.1}, {'2024': -1.1}, {'2025': 0.0}, {'2026': 0.6}, {'2027': 0.4}, {'2028': 0.4}, {'2029': 0.4}]}, {'Investment': [{'2020': -7.8}, {'2021': 18.1}, {'2022': 2.0}, {'2023': -5.1}, {'2024': 0.3}, {'2025': 1.5}, {'2026': 3.0}, {'2027': 3.0}, {'2028': 3.0}, {'2029': 2.9}]}, {'Public': [{'2020': 4.0}, {'2021': 7.9}, {'2022': -6.4}, {'2023': 4.9}, {'2024': 2.5}, {'2025': 1.3}, {'2026': 2.3}, {'2027': 2.5}, {'2028': 2.8}, {'2029': 2.8}]}, {'Private': [{'2020': -7.4}, {'2021': 13.5}, {'2022': 6.3}, {'2023': -2.6}, {'2024': -4.1}, {'2025': 1.5}, {'2026': 3.2}, {'2027': 3.1}, {'2028': 3.1}, {'2029': 2.9}]}, {'Private business': [{'2020': -9.3}, {'2021': 15.7}, {'2022': 9.6}, {'2023': -1.9}, {'2024': -4.5}, {'2025': 1.4}, {'2026': 3.4}, {'2027': 3.4}, {'2028': 3.4}, {'2029': 3.1}]}, {'Dwelling': [{'2020': -3.1}, {'2021': 9.0}, {'2022': -0.9}, {'2023': -4.2}, {'2024': -3.0}, {'2025': 1.9}, {'2026': 2.8}, {'2027': 2.4}, {'2028': 2.4}, {'2029': 2.4}]}, {'Inventories (contribution to growth, percent)': [{'2020': -0.8}, {'2021': 1.4}, {'2022': -0.4}, {'2023': -1.1}, {'2024': 0.7}, {'2025': 0.0}, {'2026': 0.0}, {'2027': 0.0}, {'2028': 0.0}, {'2029': 0.0}]}, {'Net exports (contribution to growth, percent)': [{'2020': 1.5}, {'2021': -4.8}, {'2022': -1.5}, {'2023': 2.2}, {'2024': 1.5}, {'2025': 0.3}, {'2026': 0.3}, {'2027': 0.1}, {'2028': 0.1}, {'2029': 0.1}]}, {'Real gross domestic income': [{'2020': -0.7}, {'2021': 5.1}, {'2022': 1.3}, {'2023': 0.0}, {'2024': 1.4}, {'2025': 2.1}, {'2026': 2.5}, {'2027': 2.5}, {'2028': 2.5}, {'2029': 2.5}]}, {'Investment (percent of GDP)': [{'2020': 22.1}, {'2021': 25.0}, {'2022': 26.0}, {'2023': 24.4}, {'2024': 24.3}, {'2025': 24.2}, {'2026': 24.4}, {'2027': 24.4}, {'2028': 24.4}, {'2029': 24.5}]}, {'Public (percent of GDP)': [{'2020': 5.5}, {'2021': 5.7}, {'2022': 5.4}, {'2023': 5.7}, {'2024': 5.7}, {'2025': 5.7}, {'2026': 5.7}, {'2027': 5.6}, {'2028': 5.6}, {'2029': 5.6}]}, {'Private (percent of GDP)': [{'2020': 16.6}, {'2021': 19.4}, {'2022': 20.6}, {'2023': 18.7}, {'2024': 18.6}, {'2025': 18.6}, {'2026': 18.7}, {'2027': 18.7}, {'2028': 18.8}, {'2029': 18.9}]}, {'Savings (gross, percent of GDP)': [{'2020': 21.1}, {'2021': 19.2}, {'2022': 17.2}, {'2023': 17.5}, {'2024': 18.3}, {'2025': 18.9}, {'2026': 19.6}, {'2027': 20.0}, {'2028': 20.3}, {'2029': 20.8}]}, {'Public savings (percent of GDP)': [{'2020': -4.3}, {'2021': -3.2}, {'2022': -3.5}, {'2023': -3.5}, {'2024': -3.5}, {'2025': -2.6}, {'2026': -1.7}, {'2027': -1.1}, {'2028': -0.4}, {'2029': -0.1}]}, {'Private savings (percent of GDP)': [{'2020': 25.5}, {'2021': 22.4}, {'2022': 20.7}, {'2023': 21.0}, {'2024': 21.8}, {'2025': 21.4}, {'2026': 21.3}, {'2027': 21.0}, {'2028': 20.7}, {'2029': 20.9}]}, {'Potential output': [{'2020': 1.6}, {'2021': 1.5}, {'2022': 1.9}, {'2023': 2.1}, {'2024': 2.3}, {'2025': 2.3}, {'2026': 2.2}, {'2027': 2.2}, {'2028': 2.2}, {'2029': 2.2}]}, {'Output gap (percent of potential)': [{'2020': -2.3}, {'2021': 1.7}, {'2022': 2.1}, {'2023': 0.6}, {'2024': -0.5}, {'2025': -0.9}, {'2026': -0.7}, {'2027': -0.5}, {'2028': -0.2}, {'2029': 0.0}]}]}, tool_call_id='jTCCD3q6K', timestamp=datetime.datetime(2025, 6, 16, 9, 51, 45, 361451, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='process_dataframe', args='{"data": {"labor_market": [{"Employment": [{"2020": 1.3}, {"2021": 2.2}, {"2022": 1.7}, {"2023": 3.1}, {"2024": 1.1}, {"2025": 1.1}, {"2026": 1.6}, {"2027": 1.7}, {"2028": 1.7}, {"2029": 1.6}]}, {"Unemployment (percent of labor force, ann. average)": [{"2020": 4.6}, {"2021": 3.8}, {"2022": 3.3}, {"2023": 3.7}, {"2024": 5}, {"2025": 5.4}, {"2026": 5.2}, {"2027": 5}, {"2028": 4.7}, {"2029": 4.5}]}, {"Wages (nominal percent change)": [{"2020": 3.8}, {"2021": 3.8}, {"2022": 6.5}, {"2023": 7}, {"2024": 4.8}, {"2025": 3.9}, {"2026": 3.7}, {"2027": 3.2}, {"2028": 3}, {"2029": 3}]}], "national_accounts": [{"Real GDP (production)": [{"2020": -1.4}, {"2021": 5.6}, {"2022": 2.4}, {"2023": 0.6}, {"2024": 1}, {"2025": 2}, {"2026": 2.4}, {"2027": 2.4}, {"2028": 2.4}, {"2029": 2.4}]}, {"Domestic demand": [{"2020": -1.7}, {"2021": 10.1}, {"2022": 3.4}, {"2023": -1.5}, {"2024": -0.4}, {"2025": 1.5}, {"2026": 2}, {"2027": 2.1}, {"2028": 2.2}, {"2029": 2.1}]}, {"Private consumption": [{"2020": -1.7}, {"2021": 7.7}, {"2022": 3.2}, {"2023": 0.3}, {"2024": -1.6}, {"2025": 2}, {"2026": 2.1}, {"2027": 2.3}, {"2028": 2.4}, {"2029": 2.3}]}, {"Public consumption": [{"2020": 6.7}, {"2021": 7.8}, {"2022": 4.9}, {"2023": -1.1}, {"2024": -1.1}, {"2025": 0}, {"2026": 0.6}, {"2027": 0.4}, {"2028": 0.4}, {"2029": 0.4}]}, {"Investment": [{"2020": -7.8}, {"2021": 18.1}, {"2022": 2}, {"2023": -5.1}, {"2024": 0.3}, {"2025": 1.5}, {"2026": 3}, {"2027": 3}, {"2028": 3}, {"2029": 2.9}]}, {"Public": [{"2020": 4}, {"2021": 7.9}, {"2022": -6.4}, {"2023": 4.9}, {"2024": 2.5}, {"2025": 1.3}, {"2026": 2.3}, {"2027": 2.5}, {"2028": 2.8}, {"2029": 2.8}]}, {"Private": [{"2020": -7.4}, {"2021": 13.5}, {"2022": 6.3}, {"2023": -2.6}, {"2024": -4.1}, {"2025": 1.5}, {"2026": 3.2}, {"2027": 3.1}, {"2028": 3.1}, {"2029": 2.9}]}, {"Private business": [{"2020": -9.3}, {"2021": 15.7}, {"2022": 9.6}, {"2023": -1.9}, {"2024": -4.5}, {"2025": 1.4}, {"2026": 3.4}, {"2027": 3.4}, {"2028": 3.4}, {"2029": 3.1}]}, {"Dwelling": [{"2020": -3.1}, {"2021": 9}, {"2022": -0.9}, {"2023": -4.2}, {"2024": -3}, {"2025": 1.9}, {"2026": 2.8}, {"2027": 2.4}, {"2028": 2.4}, {"2029": 2.4}]}, {"Inventories (contribution to growth, percent)": [{"2020": -0.8}, {"2021": 1.4}, {"2022": -0.4}, {"2023": -1.1}, {"2024": 0.7}, {"2025": 0}, {"2026": 0}, {"2027": 0}, {"2028": 0}, {"2029": 0}]}, {"Net exports (contribution to growth, percent)": [{"2020": 1.5}, {"2021": -4.8}, {"2022": -1.5}, {"2023": 2.2}, {"2024": 1.5}, {"2025": 0.3}, {"2026": 0.3}, {"2027": 0.1}, {"2028": 0.1}, {"2029": 0.1}]}, {"Real gross domestic income": [{"2020": -0.7}, {"2021": 5.1}, {"2022": 1.3}, {"2023": 0}, {"2024": 1.4}, {"2025": 2.1}, {"2026": 2.5}, {"2027": 2.5}, {"2028": 2.5}, {"2029": 2.5}]}, {"Investment (percent of GDP)": [{"2020": 22.1}, {"2021": 25}, {"2022": 26}, {"2023": 24.4}, {"2024": 24.3}, {"2025": 24.2}, {"2026": 24.4}, {"2027": 24.4}, {"2028": 24.4}, {"2029": 24.5}]}, {"Public (percent of GDP)": [{"2020": 5.5}, {"2021": 5.7}, {"2022": 5.4}, {"2023": 5.7}, {"2024": 5.7}, {"2025": 5.7}, {"2026": 5.7}, {"2027": 5.6}, {"2028": 5.6}, {"2029": 5.6}]}, {"Private (percent of GDP)": [{"2020": 16.6}, {"2021": 19.4}, {"2022": 20.6}, {"2023": 18.7}, {"2024": 18.6}, {"2025": 18.6}, {"2026": 18.7}, {"2027": 18.7}, {"2028": 18.8}, {"2029": 18.9}]}, {"Savings (gross, percent of GDP)": [{"2020": 21.1}, {"2021": 19.2}, {"2022": 17.2}, {"2023": 17.5}, {"2024": 18.3}, {"2025": 18.9}, {"2026": 19.6}, {"2027": 20}, {"2028": 20.3}, {"2029": 20.8}]}, {"Public savings (percent of GDP)": [{"2020": -4.3}, {"2021": -3.2}, {"2022": -3.5}, {"2023": -3.5}, {"2024": -3.5}, {"2025": -2.6}, {"2026": -1.7}, {"2027": -1.1}, {"2028": -0.4}, {"2029": -0.1}]}, {"Private savings (percent of GDP)": [{"2020": 25.5}, {"2021": 22.4}, {"2022": 20.7}, {"2023": 21}, {"2024": 21.8}, {"2025": 21.4}, {"2026": 21.3}, {"2027": 21}, {"2028": 20.7}, {"2029": 20.9}]}, {"Potential output": [{"2020": 1.6}, {"2021": 1.5}, {"2022": 1.9}, {"2023": 2.1}, {"2024": 2.3}, {"2025": 2.3}, {"2026": 2.2}, {"2027": 2.2}, {"2028": 2.2}, {"2029": 2.2}]}, {"Output gap (percent of potential)": [{"2020": -2.3}, {"2021": 1.7}, {"2022": 2.1}, {"2023": 0.6}, {"2024": -0.5}, {"2025": -0.9}, {"2026": -0.7}, {"2027": -0.5}, {"2028": -0.2}, {"2029": 0}]}]}, "action": "sum", "split_year": 2025}', tool_call_id='nsEYyAWpq')], usage=Usage(requests=1, request_tokens=3358, response_tokens=2797, total_tokens=6155), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 9, 51, 45, tzinfo=TzInfo(UTC)), vendor_id='c4b1158e3e474679835dd3dfff7ad775'))


process_dataframe called with action=sum, split_year=2025
ModelRequestNode(request=ModelRequest(parts=[ToolReturnPart(tool_name='process_dataframe', content='[\n  {\n    "Category":"labor_market",\n    "Metric":"Employment",\n    "Sum_Before_2025":9.4,\n    "Sum_After_2025":7.7\n  },\n  {\n    "Category":"labor_market",\n    "Metric":"Unemployment (percent of labor force, ann. average)",\n    "Sum_Before_2025":20.4,\n    "Sum_After_2025":24.8\n  },\n  {\n    "Category":"labor_market",\n    "Metric":"Wages (nominal percent change)",\n    "Sum_Before_2025":25.9,\n    "Sum_After_2025":16.8\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Domestic demand",\n    "Sum_Before_2025":9.9,\n    "Sum_After_2025":9.9\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Dwelling",\n    "Sum_Before_2025":-2.2,\n    "Sum_After_2025":11.9\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Inventories (contribution to growth, percent)",\n    "Sum_Before_2025":-0.2,\n    "Sum_After_2025":0.0\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Investment",\n    "Sum_Before_2025":7.5,\n    "Sum_After_2025":13.4\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Investment (percent of GDP)",\n    "Sum_Before_2025":121.8,\n    "Sum_After_2025":121.9\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Net exports (contribution to growth, percent)",\n    "Sum_Before_2025":-1.1,\n    "Sum_After_2025":0.9\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Output gap (percent of potential)",\n    "Sum_Before_2025":1.6,\n    "Sum_After_2025":-2.3\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Potential output",\n    "Sum_Before_2025":9.4,\n    "Sum_After_2025":11.1\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private",\n    "Sum_Before_2025":5.7,\n    "Sum_After_2025":13.8\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private (percent of GDP)",\n    "Sum_Before_2025":93.9,\n    "Sum_After_2025":93.7\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private business",\n    "Sum_Before_2025":9.6,\n    "Sum_After_2025":14.7\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private consumption",\n    "Sum_Before_2025":7.9,\n    "Sum_After_2025":11.1\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Private savings (percent of GDP)",\n    "Sum_Before_2025":111.4,\n    "Sum_After_2025":105.3\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public",\n    "Sum_Before_2025":12.9,\n    "Sum_After_2025":11.7\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public (percent of GDP)",\n    "Sum_Before_2025":28.0,\n    "Sum_After_2025":28.2\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public consumption",\n    "Sum_Before_2025":17.2,\n    "Sum_After_2025":1.8\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Public savings (percent of GDP)",\n    "Sum_Before_2025":-18.0,\n    "Sum_After_2025":-5.9\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Real GDP (production)",\n    "Sum_Before_2025":8.2,\n    "Sum_After_2025":11.6\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Real gross domestic income",\n    "Sum_Before_2025":7.1,\n    "Sum_After_2025":12.1\n  },\n  {\n    "Category":"national_accounts",\n    "Metric":"Savings (gross, percent of GDP)",\n    "Sum_Before_2025":93.3,\n    "Sum_After_2025":99.6\n  }\n]', tool_call_id='nsEYyAWpq', timestamp=datetime.datetime(2025, 6, 16, 9, 53, 2, 119817, tzinfo=datetime.timezone.utc))]))


CallToolsNode(model_response=ModelResponse(parts=[ToolCallPart(tool_name='save_data_to_database_sum', args='{"data": {"records": [{"Category": "labor_market", "Metric": "Employment", "Sum_Before_2025": 9.4, "Sum_After_2025": 7.7}, {"Category": "labor_market", "Metric": "Unemployment (percent of labor force, ann. average)", "Sum_Before_2025": 20.4, "Sum_After_2025": 24.8}, {"Category": "labor_market", "Metric": "Wages (nominal percent change)", "Sum_Before_2025": 25.9, "Sum_After_2025": 16.8}, {"Category": "national_accounts", "Metric": "Domestic demand", "Sum_Before_2025": 9.9, "Sum_After_2025": 9.9}, {"Category": "national_accounts", "Metric": "Dwelling", "Sum_Before_2025": -2.2, "Sum_After_2025": 11.9}, {"Category": "national_accounts", "Metric": "Inventories (contribution to growth, percent)", "Sum_Before_2025": -0.2, "Sum_After_2025": 0}, {"Category": "national_accounts", "Metric": "Investment", "Sum_Before_2025": 7.5, "Sum_After_2025": 13.4}, {"Category": "national_accounts", "Metric": "Investment (percent of GDP)", "Sum_Before_2025": 121.8, "Sum_After_2025": 121.9}, {"Category": "national_accounts", "Metric": "Net exports (contribution to growth, percent)", "Sum_Before_2025": -1.1, "Sum_After_2025": 0.9}, {"Category": "national_accounts", "Metric": "Output gap (percent of potential)", "Sum_Before_2025": 1.6, "Sum_After_2025": -2.3}, {"Category": "national_accounts", "Metric": "Potential output", "Sum_Before_2025": 9.4, "Sum_After_2025": 11.1}, {"Category": "national_accounts", "Metric": "Private", "Sum_Before_2025": 5.7, "Sum_After_2025": 13.8}, {"Category": "national_accounts", "Metric": "Private (percent of GDP)", "Sum_Before_2025": 93.9, "Sum_After_2025": 93.7}, {"Category": "national_accounts", "Metric": "Private business", "Sum_Before_2025": 9.6, "Sum_After_2025": 14.7}, {"Category": "national_accounts", "Metric": "Private consumption", "Sum_Before_2025": 7.9, "Sum_After_2025": 11.1}, {"Category": "national_accounts", "Metric": "Private savings (percent of GDP)", "Sum_Before_2025": 111.4, "Sum_After_2025": 105.3}, {"Category": "national_accounts", "Metric": "Public", "Sum_Before_2025": 12.9, "Sum_After_2025": 11.7}, {"Category": "national_accounts", "Metric": "Public (percent of GDP)", "Sum_Before_2025": 28, "Sum_After_2025": 28.2}, {"Category": "national_accounts", "Metric": "Public consumption", "Sum_Before_2025": 17.2, "Sum_After_2025": 1.8}, {"Category": "national_accounts", "Metric": "Public savings (percent of GDP)", "Sum_Before_2025": -18, "Sum_After_2025": -5.9}, {"Category": "national_accounts", "Metric": "Real GDP (production)", "Sum_Before_2025": 8.2, "Sum_After_2025": 11.6}, {"Category": "national_accounts", "Metric": "Real gross domestic income", "Sum_Before_2025": 7.1, "Sum_After_2025": 12.1}, {"Category": "national_accounts", "Metric": "Savings (gross, percent of GDP)", "Sum_Before_2025": 93.3, "Sum_After_2025": 99.6}], "split_year": 2025}}', tool_call_id='cplg1h8i0')], usage=Usage(requests=1, request_tokens=7576, response_tokens=1219, total_tokens=8795), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 9, 53, 2, tzinfo=TzInfo(UTC)), vendor_id='200569af03b8488e92ef1d6758ae0920'))


CallToolsNode(model_response=ModelResponse(parts=[TextPart(content='The data was saved successfully to the database.')], usage=Usage(requests=1, request_tokens=8820, response_tokens=10, total_tokens=8830), model_name='mistral-large-latest', timestamp=datetime.datetime(2025, 6, 16, 9, 53, 39, tzinfo=TzInfo(UTC)), vendor_id='e9ae5fb14336413e854758d58601128a'))


End(data=FinalResult(output='The data was saved successfully to the database.'))


----------------------------------------------------------------------------------------------------
Agent run completed in 165.5586588382721 seconds
----------------------------------------------------------------------------------------------------


The data was saved successfully to the database.

Checking the Neon Postgres database, we can see that the records were inserted:

Why Unstract Matters in This Workflow

Unstract plays a pivotal role in transforming unstructured documents into reliable, structured data, which is an essential step in any automation pipeline that involves having a good source of data to work with.

Unstract has the following advantages:

Reliable and Accurate Output: Unlike basic OCR or rule-based methods, Unstract uses AI-driven prompt engineering to extract data with high accuracy. It understands context and structure, reducing the risk of misinterpretation or incorrect parsing.

Beyond Naive Extraction: Traditional approaches like plain text extraction or OCR combined with regex often result in error-prone workflows. These methods struggle with varying document formats, inconsistent layouts, and missing context, leading to unreliable results that require manual clean-up.

Structured JSON for Seamless Processing: Unstract returns clean, nested JSON that mirrors the logical structure of the document. This format is ideal for downstream processing with tools like Pandas or for storing in databases. It enables seamless integration into analytics pipelines, APIs, and automation platforms without the need for additional data processing.


Conclusion

Integrating Unstract and PydanticAI creates a powerful, modular workflow for extracting and validating structured data from unstructured documents.

This approach brings automation, traceability, and flexibility to document processing tasks, whether you’re working with tax forms, economic reports, or complex business documents.

PydanticAI streamlines the process of interacting with LLMs by combining prompt execution, validation, and retry logic in a clean, maintainable way.

When paired with Unstract’s precise document parsing and structured output, it enables robust and scalable AI-powered pipelines.

Try incorporating Unstract and PydanticAI into your own workflows to simplify complex data extraction tasks.

Whether you’re building internal tools or production-grade APIs, this combination will help you move faster and with more confidence in your results.


Get started with Unstract

Unstract is an open-source, no-code platform for automating document-heavy workflows. It converts unstructured data—like invoices, ID cards, or contracts—into clean JSON for easy integration and automation.