Skip to content

The tool invocation fails to correctly recognize the Pydantic v2 schema. #32224

@liuxiahuiyi

Description

@liuxiahuiyi

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Example Code

time_fmt = "%Y-%m-%d %H:%M:%S"
time_pattern = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$"
class DataSoilDashboardQueryPayloadTimeShift(BaseModel):
    shiftInterval: list[PositiveInt] = Field(description="Each element in the array represents a time offset relative to the query timestamp for individual time comparison analysis. If time comparison analysis dose not described, keep it **VOID**.",max_length=2,default=[])
    timeUnit: Literal["DAY"] = Field(default="DAY",description="The unit of specific comparison time offset. This is the description about each value of unit:" +
        "Unit **DAY** represents one day."
    )
class DataSoilDashboardQueryPayloadQueryParamWhereFilter(BaseModel):
    field: str = Field(description="The dimension **CODE** in the selected dimension list that requires enums filtering or pattern filtering.")
    operator: Literal["IN", "NI", "LIKE", "NOT_LIKE"] = Field(description="Operators for enums filtering or pattern filtering. This is the description about each value of operator: " +
        "The **IN** operator represents enums filtering and can be described as sql 'WHERE <dimension> IN <enums>'. " +
        "The **NI** operator represents enums filtering and can be described as sql 'WHERE <dimension> NOT IN <enums>'. " +
        "The **LIKE** operator represents pattern filtering and can be described as sql 'WHERE <dimension> LIKE %<pattern>%'. " +
        "The **NOT_LIKE** operator represents pattern filtering and can be described as sql 'WHERE <dimension> NOT LIKE %<pattern>%'."
    )
    value: list[str] = Field(description="If for enums filtering, every element represents th practical enums of the dimension. Otherwise for pattern filtering, only **one** element is required and it represents a wildcard pattern.",min_length=1)
    @field_validator("field")
    def field_block(cls, v: str, info: ValidationInfo) -> str:
        if v == "dt":
            raise ValueError("Instruction: The time filtering should be described in 'time' field, not in the 'filters' field.")
        return v
    @field_validator("value")
    def value_block(cls, v: Optional[list[str]], info: ValidationInfo) -> Optional[list[str]]:
        if info.data["operator"] in {"LIKE", "NOT_LIKE"} and len(v) > 1:
            raise ValueError("Instruction: For pattern filtering, the size of 'value' in 'where' must be **ONE**.")
        return v
class DataSoilDashboardQueryPayloadQueryParamWhere(BaseModel):
    time: list[Union[str, int]] = Field(
        description="The target time range which is **left closed and right opened** for the query, **excluding** the comparison periods required for time comparison analysis. " +
            f"The first element is start time, precise to second, a string formatted as **{time_fmt}**. The final time interval **include** this value. " +
            f"The last element is end time, precise to second, a string formatted as **{time_fmt}**. The final time interval **exclude** this value.",
        min_length=2,
        max_length=2,
    )
    filters: list[DataSoilDashboardQueryPayloadQueryParamWhereFilter] = Field(description="Enums filtering or pattern filtering condition of dimension in the dimensions list. Metric's filtering and time's filtering is **forbidden**.")
    relation: Literal["AND"] = Field(description="Boolean relationships between filters across multiple dimensions. This is the description about each value of relation: " +
        "The **AND** represents boolean logic **and**."
    )
    @field_validator("time")
    def value_block(cls, v: list[Union[int, str]], info: ValidationInfo) -> list[Union[int, str]]:
        if isinstance(v[0], str) and not re.search(time_pattern, v[0]):
            raise ValueError(f"Instruction: the start time of time range must be formatted as **{time_fmt}**")
        if isinstance(v[1], str) and not re.search(time_pattern, v[1]):
            raise ValueError(f"Instruction: the end time of time range must be formatted as **{time_fmt}**")
        return v
class DataSoilDashboardQueryPayloadQueryParamOrderBy(BaseModel):
    field: str = Field(description="The metric **CODE** in the selected metric list that requires metric sorting.")
    direction: Literal["ASC", "DESC"] = Field(description="Sorting direction for specified metric. The **ASC** represents ascending order by metric values, while the **DESC** represents descending order by metric values.")
    shift: int = Field(default=0)
    limit: int = Field(description="The number of rows to return from the dataset after sorting by the designated metric.",default=50)
class DataSoilDashboardQueryPayloadQueryParamGroupBy(BaseModel):
    field: str = Field(description="The dimension **CODE** in the selected dimension list for dimension grouping analysis.")
    extendFields: list[str] = Field(default=[])
    orderBy: Optional[DataSoilDashboardQueryPayloadQueryParamOrderBy] = Field(description="Sorting config for query results based on a specific **metric**. Dimension sorting is forbidden.",default=None)
class DataSoilDashboardQueryPayloadQueryParam(BaseModel):
    queryType: Literal["DETAIL_TABLE"] = Field(description="This is the description about each value of queryType: " +
        "The **DETAIL_TABLE​​** displays metrics grouped by each unique dimension combination where the levels of all grouped dimensions are the same."
    )
    interval: DashboardInterval = Field(description="The time granularity for time-based grouping analysis. This is the description about each value of granularity: " +
        "The **BY_ONE_MINUTE** represents aggregating time at one-minute intervals. " +
        "The **BY_FIVE_MINUTE** represents aggregating time at five-minute intervals. " +
        "The **BY_HOUR** represents aggregating time at one-hour intervals. " +
        "The **BY_DAY** represents aggregating time at one-day intervals. " +
        "The **BY_WEEK** represents aggregating time at one-week intervals. " +
        "The **BY_MONTH** represents aggregating time at one-month intervals. " +
        "The **SUM** represents NOT aggregating time."
    )
    resultField: list[str] = Field(default=[])
    where: DataSoilDashboardQueryPayloadQueryParamWhere = Field(description="Filtering condition for dimensions. Including target time range and enums filtering or pattern filtering for non-time dimensions.")
    groupBy: list[DataSoilDashboardQueryPayloadQueryParamGroupBy] = Field(description="A list of dimensions grouping analysis info derived from the dimensions list. If these dimensions require roll-up, a sorting config for each dimension is also required, otherwise all sorting config should be **VOID**. The sorting config specifies the ordering of rolled-up dimensional data when the dimension is the lowest-level among all non-aggregated dimensions.")
    orderBy: DataSoilDashboardQueryPayloadQueryParamOrderBy = Field(description="Sorting config for query results based on a specific **metric**. Dimension sorting is forbidden.")
    heavyQuery: bool = Field(default=False)
    @field_validator("groupBy")
    def groupBy_block(cls, v: list[DataSoilDashboardQueryPayloadQueryParamGroupBy], info: ValidationInfo) -> list[DataSoilDashboardQueryPayloadQueryParamGroupBy]:
        if "dt" in {e.field for e in v}:
            if info.data["interval"] == "SUM":
                raise ValueError("Instruction: the interval can not be **SUM** when **time-based grouping is required**.")
        else:
            if info.data["interval"] != "SUM":
                raise ValueError("Instruction: the interval must be **SUM** when **time-based grouping is not required**.")
        return v

class DataSoilDashboardQueryPayload(BaseModel):
    model_config = ConfigDict(
        frozen=False,
    )
    apiCode: str = Field(default="")
    requestId: str = Field(default="")
    applicationCode: str = Field(default="")
    applicationToken: str = Field(default="")
    debug: bool = Field(default=False)
    timeShift: DataSoilDashboardQueryPayloadTimeShift = Field(description="Time comparison analysis config.",default=DataSoilDashboardQueryPayloadTimeShift())
    dynamicQueryParam: DataSoilDashboardQueryPayloadQueryParam
    forceFlush: bool = Field(default=False)


@tool(response_format="content_and_artifact")
def query_datasoil_data_tool(payload: DataSoilDashboardQueryPayload, state: Annotated[dict, InjectedState]) -> Tuple[str, dict]:
    <any code>

generation_llm_with_tools = generation_llm.bind_tools(
    tools=[DataSoilDashboardQueryPayload],
    tool_choice=DataSoilDashboardQueryPayload.__name__,
)

generation_llm_with_tools.invoke([HumanMessage(content="any message")])

Error Message and Stack Trace (if applicable)

No response

Description

When a BaseChatModel binds a tool with input parameters defined by a ​​nested Pydantic v2 model​​, the tool invocation fails to correctly recognize the Pydantic v2 schema. This results in the arg schema within AIMessage.tool_calls being arbitrarily generated.

System Info

ai-agent-common==1.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.13
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.9.0
asttokens==3.0.0
async-timeout==5.0.1
atomic==0.7.3
attrs==25.3.0
backoff==2.2.1
beautifulsoup4==4.13.4
charset-normalizer==3.4.2
click==8.2.1
cloudpickle==3.1.1
cramjam==2.10.0
cryptography==44.0.3
dataclasses-json==0.6.7
decorator==5.2.1
distro==1.9.0
dnspython==2.7.0
docutils==0.21.2
exceptiongroup==1.3.0
executing==2.2.0
id==1.5.0
idna==3.10
importlib_metadata==8.7.0
IPy==1.1
ipython==9.4.0
ipython_pygments_lexers==1.1.1
jaraco.classes==3.4.0
keyring==25.6.0
langchain==0.3.26
langchain-community==0.3.27
langchain-core==0.3.71
langchain-openai==0.3.28
langchain-text-splitters==0.3.8
langgraph==0.5.4
langgraph-api==0.2.99
langgraph-checkpoint==2.1.1
langgraph-cli==0.3.5
langgraph-prebuilt==0.5.2
langgraph-runtime-inmem==0.6.1
langgraph-sdk==0.1.74
langserve==0.3.1
langsmith==0.4.8
markdown-it-py==3.0.0
marshmallow==3.26.1
matplotlib-inline==0.1.7
Pygments==2.19.2
PyJWT==2.10.1
python-dotenv==1.1.1
python-snappy==0.6.1
PyYAML==6.0.2
readme_renderer==44.0
regex==2024.11.6
rfc3986==2.0.0
rich==14.0.0
setuptools==80.9.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
tqdm==4.67.1
traitlets==5.14.3
truststore==0.10.1
twine==6.1.0
typing-inspect==0.9.0
typing-inspection==0.4.1
typing_extensions==4.14.0
urllib3==2.5.0
uvicorn==0.35.0
watchfiles==1.1.0
wcwidth==0.2.13
wrapt==1.17.2
xxhash==3.5.0
yarl==1.20.1
zipp==3.23.0
zstandard==0.23.0
```[ai-agent-common](https://pypi.python.org/pypi/ai-agent-common)==1.0
[aiohappyeyeballs](https://pypi.python.org/pypi/aiohappyeyeballs)==2.6.1
[aiohttp](https://pypi.python.org/pypi/aiohttp)==3.12.13
[aiosignal](https://pypi.python.org/pypi/aiosignal)==1.3.2
[annotated-types](https://pypi.python.org/pypi/annotated-types)==0.7.0
[anyio](https://pypi.python.org/pypi/anyio)==4.9.0
[asttokens](https://pypi.python.org/pypi/asttokens)==3.0.0
[async-timeout](https://pypi.python.org/pypi/async-timeout)==5.0.1
[atomic](https://pypi.python.org/pypi/atomic)==0.7.3
[attrs](https://pypi.python.org/pypi/attrs)==25.3.0
[backoff](https://pypi.python.org/pypi/backoff)==2.2.1
[beautifulsoup4](https://pypi.python.org/pypi/beautifulsoup4)==4.13.4
[charset-normalizer](https://pypi.python.org/pypi/charset-normalizer)==3.4.2
[click](https://pypi.python.org/pypi/click)==8.2.1
[cloudpickle](https://pypi.python.org/pypi/cloudpickle)==3.1.1
[cramjam](https://pypi.python.org/pypi/cramjam)==2.10.0
[cryptography](https://pypi.python.org/pypi/cryptography)==44.0.3
[dataclasses-json](https://pypi.python.org/pypi/dataclasses-json)==0.6.7
[decorator](https://pypi.python.org/pypi/decorator)==5.2.1
[distro](https://pypi.python.org/pypi/distro)==1.9.0
[dnspython](https://pypi.python.org/pypi/dnspython)==2.7.0
[docutils](https://pypi.python.org/pypi/docutils)==0.21.2
[exceptiongroup](https://pypi.python.org/pypi/exceptiongroup)==1.3.0
[executing](https://pypi.python.org/pypi/executing)==2.2.0
[id](https://pypi.python.org/pypi/id)==1.5.0
[idna](https://pypi.python.org/pypi/idna)==3.10
[importlib_metadata](https://pypi.python.org/pypi/importlib_metadata)==8.7.0
[IPy](https://pypi.python.org/pypi/IPy)==1.1
[ipython](https://pypi.python.org/pypi/ipython)==9.4.0
[ipython_pygments_lexers](https://pypi.python.org/pypi/ipython_pygments_lexers)==1.1.1
[jaraco.classes](https://pypi.python.org/pypi/jaraco.classes)==3.4.0
[keyring](https://pypi.python.org/pypi/keyring)==25.6.0
[langchain](https://pypi.python.org/pypi/langchain)==0.3.26
[langchain-community](https://pypi.python.org/pypi/langchain-community)==0.3.27
[langchain-core](https://pypi.python.org/pypi/langchain-core)==0.3.71
[langchain-openai](https://pypi.python.org/pypi/langchain-openai)==0.3.28
[langchain-text-splitters](https://pypi.python.org/pypi/langchain-text-splitters)==0.3.8
[langgraph](https://pypi.python.org/pypi/langgraph)==0.5.4
[langgraph-api](https://pypi.python.org/pypi/langgraph-api)==0.2.99
[langgraph-checkpoint](https://pypi.python.org/pypi/langgraph-checkpoint)==2.1.1
[langgraph-cli](https://pypi.python.org/pypi/langgraph-cli)==0.3.5
[langgraph-prebuilt](https://pypi.python.org/pypi/langgraph-prebuilt)==0.5.2
[langgraph-runtime-inmem](https://pypi.python.org/pypi/langgraph-runtime-inmem)==0.6.1
[langgraph-sdk](https://pypi.python.org/pypi/langgraph-sdk)==0.1.74
[langserve](https://pypi.python.org/pypi/langserve)==0.3.1
[langsmith](https://pypi.python.org/pypi/langsmith)==0.4.8
[markdown-it-py](https://pypi.python.org/pypi/markdown-it-py)==3.0.0
[marshmallow](https://pypi.python.org/pypi/marshmallow)==3.26.1
[matplotlib-inline](https://pypi.python.org/pypi/matplotlib-inline)==0.1.7
[Pygments](https://pypi.python.org/pypi/Pygments)==2.19.2
[PyJWT](https://pypi.python.org/pypi/PyJWT)==2.10.1
[python-dotenv](https://pypi.python.org/pypi/python-dotenv)==1.1.1
[python-snappy](https://pypi.python.org/pypi/python-snappy)==0.6.1
[PyYAML](https://pypi.python.org/pypi/PyYAML)==6.0.2
[readme_renderer](https://pypi.python.org/pypi/readme_renderer)==44.0
[regex](https://pypi.python.org/pypi/regex)==2024.11.6
[rfc3986](https://pypi.python.org/pypi/rfc3986)==2.0.0
[rich](https://pypi.python.org/pypi/rich)==14.0.0
[setuptools](https://pypi.python.org/pypi/setuptools)==80.9.0
[six](https://pypi.python.org/pypi/six)==1.17.0
[sniffio](https://pypi.python.org/pypi/sniffio)==1.3.1
[soupsieve](https://pypi.python.org/pypi/soupsieve)==2.7
[SQLAlchemy](https://pypi.python.org/pypi/SQLAlchemy)==2.0.41
[tqdm](https://pypi.python.org/pypi/tqdm)==4.67.1
[traitlets](https://pypi.python.org/pypi/traitlets)==5.14.3
[truststore](https://pypi.python.org/pypi/truststore)==0.10.1
[twine](https://pypi.python.org/pypi/twine)==6.1.0
[typing-inspect](https://pypi.python.org/pypi/typing-inspect)==0.9.0
[typing-inspection](https://pypi.python.org/pypi/typing-inspection)==0.4.1
[typing_extensions](https://pypi.python.org/pypi/typing_extensions)==4.14.0
[urllib3](https://pypi.python.org/pypi/urllib3)==2.5.0
[uvicorn](https://pypi.python.org/pypi/uvicorn)==0.35.0
[watchfiles](https://pypi.python.org/pypi/watchfiles)==1.1.0
[wcwidth](https://pypi.python.org/pypi/wcwidth)==0.2.13
[wrapt](https://pypi.python.org/pypi/wrapt)==1.17.2
[xxhash](https://pypi.python.org/pypi/xxhash)==3.5.0
[yarl](https://pypi.python.org/pypi/yarl)==1.20.1
[zipp](https://pypi.python.org/pypi/zipp)==3.23.0
[zstandard](https://pypi.python.org/pypi/zstandard)==0.23.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugRelated to a bug, vulnerability, unexpected error with an existing feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions