Skip to content

Decimal Type Issue: Issue in generating a valid JSON schema for certain Pydantic types when preparing the request for Gemini #1464

@RahulPant1

Description

@RahulPant1

Initial Checks

Description

Running the below code which uses Geminin API with a little complex structured output parsing results in error::

2025-04-12 23:57:46,085 - INFO - Running THREE COMPLEX FIELDS test using ONLY pydantic-ai Agent...
2025-04-12 23:57:46,147 - INFO - pydantic-ai Agent initialized with model 'gemini-1.5-flash-latest' and 3 complex fields included
2025-04-12 23:57:46,147 - INFO - Calling agent.run_sync...
2025-04-12 23:57:47,672 - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent "HTTP/1.1 400 Bad Request"
2025-04-12 23:57:47,674 - ERROR - Unexpected error during Agent execution: ModelHTTPError: status_code: 400, model_name: gemini-1.5-flash-latest, body: {
"error": {
"code": 400,
"message": "Unable to submit request because final_result functionDeclaration parameters.payment_milestones.amount schema didn't specify the schema type field. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling",
"status": "INVALID_ARGUMENT"
}
}

2025-04-12 23:57:47,694 - ERROR - Traceback (most recent call last):
/contract_structured_output/verysimple.py", line 126, in
result = agent.run_sync(sample_contract_text)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/agent.py", line 578, in run_sync
return get_event_loop().run_until_complete(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
o/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/agent.py", line 329, in run
async for _ in agent_run:
/lib/python3.11/site-packages/pydantic_ai/agent.py", line 1454, in anext
next_node = await self._graph_run.anext()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_graph/graph.py", line 790, in anext
return await self.next(self._next_node)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_graph/graph.py", line 763, in next
self._next_node = await node.run(ctx)
^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 264, in run
return await self._make_request(ctx)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 317, in _make_request
model_response, request_usage = await ctx.deps.model.request(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/models/gemini.py", line 135, in request
async with self._make_request(
/lib/python3.11/contextlib.py", line 210, in aenter
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
/lib/python3.11/site-packages/pydantic_ai/models/gemini.py", line 242, in _make_request
raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=r.text)
pydantic_ai.exceptions.ModelHTTPError: status_code: 400, model_name: gemini-1.5-flash-latest, body: {
"error": {
"code": 400,
"message": "Unable to submit request because final_result functionDeclaration parameters.payment_milestones.amount schema didn't specify the schema type field. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling",
"status": "INVALID_ARGUMENT"
}
}

--- Extraction Failed (Unexpected Error) ---

Example Code

# test_pydantic_ai_agent_only_three_complex.py

import os
from pydantic_ai import Agent
from pydantic import (
    BaseModel, Field, ValidationError, field_validator
)
import decimal
from typing import List, Optional
from datetime import date
import logging

# --- Configuration ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
if not GOOGLE_API_KEY:
    logging.warning("GOOGLE_API_KEY environment variable not found...")


class AddressDetail(BaseModel):
    street: Optional[str] = None; city: Optional[str] = None; state_province: Optional[str] = None; postal_code: Optional[str] = None; country: Optional[str] = None
    def __repr__(self) -> str: parts = [f"{k}={v!r}" for k, v in self.model_dump().items() if v is not None]; return f"<AddressDetail({', '.join(parts)})>"

class PartyDetail(BaseModel):
    name: str = Field(...); role: Optional[str] = Field(None); address: Optional[AddressDetail] = Field(None); authorized_signatory_name: Optional[str] = Field(None)
    def __repr__(self) -> str: addr_repr = repr(self.address) if self.address else 'None'; return f"<PartyDetail(name={self.name!r}, role={self.role!r}, address={addr_repr}, signatory={self.authorized_signatory_name!r})>"


class SoftwareProductDetail(BaseModel):
    name: str = Field(..., description="Primary name of the software or product")
    version: Optional[str] = Field(None, description="Specific version identifier, if mentioned")
    modules_features: Optional[List[str]] = Field(default_factory=list, description="List of included modules or key features")
    description: Optional[str] = Field(None, description="Brief description of the software/product")
    def __repr__(self) -> str:
        num_features = len(self.modules_features) if self.modules_features else 0
        return f"<SoftwareProductDetail(name={self.name!r}, version={self.version!r}, num_features={num_features})>"

class PaymentMilestoneDetail(BaseModel):
    description: str = Field(..., description="What triggers or describes this payment")
    amount: Optional[decimal.Decimal] = Field(None, description="Monetary amount") # Using standard Decimal
    currency: Optional[str] = Field(None, description="Currency code (e.g., USD, EUR, INR)")
    due_date_description: Optional[str] = Field(None, description="When the payment is due (e.g., 'Net 30', 'Upon signing', specific date)")
    due_date: Optional[date] = Field(None, description="Specific calendar due date, if available")
    def __repr__(self) -> str: # Keep simple repr
        amt_repr = repr(self.amount) if self.amount is not None else 'None'
        curr_repr = self.currency or 'N/A'
        return f"<PaymentMilestoneDetail(desc={self.description!r}, amount={amt_repr} {curr_repr}, due={self.due_date_description or self.due_date or 'N/A'!r})>"

# --- PARTIALLY Simplified Pydantic Model ---
class ContractAnalysisResult(BaseModel):
    """Structure with parties, products, and payments added back."""

    # Keep basic fields
    contract_title: Optional[str] = Field(None, description="The main title of the contract document.")
    effective_date: Optional[str] = Field(None, description="The effective date (YYYY-MM-DD string).")
    contract_id_reference: Optional[str] = Field(None, description="Any unique identifier or reference number.")

    parties: List[PartyDetail] = Field(default_factory=list, description="List of all identified parties involved in the contract.")

    primary_software_products: List[SoftwareProductDetail] = Field(default_factory=list, description="Details of the main software/products being licensed or sold.")
    payment_milestones: List[PaymentMilestoneDetail] = Field(default_factory=list, description="Breakdown of payment amounts, schedules, and currencies.")

    force_majeure_clause_present: Optional[bool] = Field(None, description="Is there a Force Majeure clause?")
    governing_law: Optional[str] = Field(None, description="The governing law jurisdiction.")

    # Add back validator IF it only refers to existing fields
    @field_validator('parties')
    def check_at_least_two_parties(cls, v):
        # Keep print for warning, avoid raising error during debug
        if isinstance(v, list) and len(v) < 2:
            print(f"Warning: Fewer than two parties extracted ({len(v)} found).")
        return v

# --- End Model Definition ---


# --- Sample Contract Text (Remains the same) ---
sample_contract_text = """
MASTER SOFTWARE LICENSE AND SERVICES AGREEMENT - Ref: MSSA-2024-TS-SL
This Master Software License and Services Agreement ("Agreement") is effective as of July 15, 2024 ("Effective Date"),
by and between Quantum Dynamics Inc., a Delaware corporation with offices at 1 Quantum Leap, Palo Alto, CA 94301 ("Quantum"), and
Global Synergy Partners LLC, a New York limited liability company with its principal place of business at 789 World Ave, New York, NY 10001 ("Synergy").
RECITALS
A. Quantum develops and licenses proprietary software known as "FusionPlatform" version 3.0, including modules "DataCore" and "AnalyticsSuite".
B. Synergy wishes to license FusionPlatform and receive related support services.
AGREEMENT
LICENSE GRANT. Quantum grants Synergy a non-exclusive, worldwide, 3-year subscription license to use FusionPlatform v3.0 (DataCore & AnalyticsSuite modules) for up to 250 named users solely for Synergy's internal business operations. Sub-licensing is prohibited.
SERVICES. Quantum will provide Standard Support (8x5, Pacific Time) and basic implementation services as outlined in Exhibit A.
FEES. Synergy shall pay Quantum a total Subscription Fee of $150,000 USD annually, due Net 30 days from the start of each subscription year. Implementation Services fee is a one-time charge of $25,000 USD due upon signing. Late payments accrue 1.5% monthly interest.
TERM AND TERMINATION. This Agreement commences on the Effective Date and continues for three (3) years. It renews automatically for successive 1-year terms unless either party provides written notice of non-renewal at least 90 days prior to the end of the then-current term. Either party may terminate for material breach upon 30 days' written notice if the breach remains uncured.
CONFIDENTIALITY. Obligations last for 5 years post-termination.
GOVERNING LAW & JURISDICTION. This Agreement is governed by the laws of the State of California, without regard to conflict of law principles. Disputes shall be resolved exclusively in the state or federal courts located in Santa Clara County, California.
ACCEPTANCE. Use of the software constitutes acceptance. Delivery shall occur within 5 business days of Effective Date.
IN WITNESS WHEREOF... [Signatures]
Exhibit A: Services Description (Attached)
Amendment No. 1: Pricing Adjustment (Dated Aug 1, 2024 - Attached)
"""
# --- Main Execution using pydantic-ai Agent (Remains the same) ---
if __name__ == "__main__":
    logging.info("Running THREE COMPLEX FIELDS test using ONLY pydantic-ai Agent...")

    GEMINI_MODEL = "gemini-1.5-flash-latest"
    agent = None
    try:
        agent = Agent(
            model=GEMINI_MODEL,
            result_type=ContractAnalysisResult # Use the schema with 3 complex fields
            # api_key=GOOGLE_API_KEY # Add if necessary
        )
        logging.info(f"pydantic-ai Agent initialized with model '{GEMINI_MODEL}' and 3 complex fields included")

        logging.info("Calling agent.run_sync...")
        result = agent.run_sync(sample_contract_text)
        logging.info("agent.run_sync finished.")

        # ... (Result processing remains the same) ...
        if result and hasattr(result, 'data') and result.data:
            if isinstance(result.data, ContractAnalysisResult):
                print("\n--- Successfully Extracted Three Complex Fields Info (using pydantic-ai Agent) ---")
                print(result.data.model_dump_json(indent=2))
                print("--- End ---")
            # ...(rest of the result/error handling) ...
            else:
                 logging.error(f"Agent returned data, but it's not the expected ContractAnalysisResult type. Type: {type(result.data)}")
                 print("\n--- Extraction returned unexpected data type ---")
                 print(f"Returned data:\n{result.data}")
        elif result and hasattr(result, 'error') and result.error:
             logging.error(f"Agent run failed with error: {result.error}")
             print("\n--- Extraction Failed (Agent reported error) ---")
             print(f"Error: {result.error}")
        else:
            logging.error("Agent run did not return expected data or error structure.")
            print("\n--- Extraction Failed (Unexpected Agent result) ---")
            print(f"Raw agent result: {result}")

    # ... (Exception handling remains the same, including specific RecursionError catch) ...
    except RecursionError as e: # Catch RecursionError specifically
        logging.error(f"!!! RecursionError occurred with 3 complex fields added back: {e}")
        import traceback
        logging.error(traceback.format_exc())
        print("\n--- Extraction Failed (Recursion Error with 3 Complex Fields) ---")
    except Exception as e:
        logging.error(f"Unexpected error during Agent execution: {type(e).__name__}: {e}")
        import traceback
        logging.error(traceback.format_exc())
        print("\n--- Extraction Failed (Unexpected Error) ---")

Python, Pydantic AI & LLM client version

Python 3.11.9
google-generativeai                      0.8.4
jsonref                                  1.1.0
pydantic                                 2.11.3
pydantic-ai                              0.0.55
pydantic-ai-slim                         0.0.55
pydantic_core                            2.33.1
pydantic-evals                           0.0.55
pydantic-graph                           0.0.55
pydantic-settings                        2.6.1

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions