From 05c6b1f5e2d9db0fac1b3954cfb560178b7902c8 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 14 Oct 2025 20:41:55 +0500 Subject: [PATCH 1/9] fix: Tool Name check in name_dict --- libs/core/langchain_core/output_parsers/openai_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/output_parsers/openai_tools.py b/libs/core/langchain_core/output_parsers/openai_tools.py index 5f44b916ae298..47e269470c24f 100644 --- a/libs/core/langchain_core/output_parsers/openai_tools.py +++ b/libs/core/langchain_core/output_parsers/openai_tools.py @@ -340,7 +340,8 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> An ) raise ValueError(msg) try: - pydantic_objects.append(name_dict[res["type"]](**res["args"])) + if res["type"] in name_dict: + pydantic_objects.append(name_dict[res["type"]](**res["args"])) except (ValidationError, ValueError): if partial: continue From 2f36de529eb4c5c554a4adb7f776942b1d44acd6 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 14 Oct 2025 21:07:46 +0500 Subject: [PATCH 2/9] chore: Support for both Pydantic V1 and V2 --- .../langchain_core/output_parsers/openai_tools.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/core/langchain_core/output_parsers/openai_tools.py b/libs/core/langchain_core/output_parsers/openai_tools.py index 47e269470c24f..085cb33f24e90 100644 --- a/libs/core/langchain_core/output_parsers/openai_tools.py +++ b/libs/core/langchain_core/output_parsers/openai_tools.py @@ -16,6 +16,7 @@ from langchain_core.outputs import ChatGeneration, Generation from langchain_core.utils.json import parse_partial_json from langchain_core.utils.pydantic import TypeBaseModel +from langchain_core.utils.pydantic import is_pydantic_v2_subclass, is_pydantic_v1_subclass logger = logging.getLogger(__name__) @@ -328,7 +329,15 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> An return None if self.first_tool_only else [] json_results = [json_results] if self.first_tool_only else json_results - name_dict = {tool.__name__: tool for tool in self.tools} + name_dict_v2 = { + tool.model_config.get("title", tool.__name__): tool for tool in self.tools + if is_pydantic_v2_subclass(tool) + } + name_dict_v1 = { + tool.model_config.get("title", tool.__name__): tool for tool in self.tools + if is_pydantic_v1_subclass(tool) + } + name_dict = {**name_dict_v2, **name_dict_v1} pydantic_objects = [] for res in json_results: if not isinstance(res["args"], dict): @@ -340,8 +349,7 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> An ) raise ValueError(msg) try: - if res["type"] in name_dict: - pydantic_objects.append(name_dict[res["type"]](**res["args"])) + pydantic_objects.append(name_dict[res["type"]](**res["args"])) except (ValidationError, ValueError): if partial: continue From d1e31b4a0f8ab6fd480a5824aa53877077724fe6 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 14 Oct 2025 21:09:26 +0500 Subject: [PATCH 3/9] chore: Support for both Pydantic V1 and V2 --- .../langchain_core/output_parsers/openai_tools.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/core/langchain_core/output_parsers/openai_tools.py b/libs/core/langchain_core/output_parsers/openai_tools.py index 085cb33f24e90..562b3f7e0c1e8 100644 --- a/libs/core/langchain_core/output_parsers/openai_tools.py +++ b/libs/core/langchain_core/output_parsers/openai_tools.py @@ -15,8 +15,11 @@ from langchain_core.output_parsers.transform import BaseCumulativeTransformOutputParser from langchain_core.outputs import ChatGeneration, Generation from langchain_core.utils.json import parse_partial_json -from langchain_core.utils.pydantic import TypeBaseModel -from langchain_core.utils.pydantic import is_pydantic_v2_subclass, is_pydantic_v1_subclass +from langchain_core.utils.pydantic import ( + TypeBaseModel, + is_pydantic_v1_subclass, + is_pydantic_v2_subclass, +) logger = logging.getLogger(__name__) @@ -330,11 +333,13 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> An json_results = [json_results] if self.first_tool_only else json_results name_dict_v2 = { - tool.model_config.get("title", tool.__name__): tool for tool in self.tools + tool.model_config.get("title", tool.__name__): tool + for tool in self.tools if is_pydantic_v2_subclass(tool) } name_dict_v1 = { - tool.model_config.get("title", tool.__name__): tool for tool in self.tools + tool.model_config.get("title", tool.__name__): tool + for tool in self.tools if is_pydantic_v1_subclass(tool) } name_dict = {**name_dict_v2, **name_dict_v1} From f764b13c91fe11d2c8f00eb86a0af26a1cb7ab62 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 14 Oct 2025 21:29:31 +0500 Subject: [PATCH 4/9] chore: typing for name_dict --- .../langchain_core/output_parsers/openai_tools.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libs/core/langchain_core/output_parsers/openai_tools.py b/libs/core/langchain_core/output_parsers/openai_tools.py index 562b3f7e0c1e8..043bc284b69e5 100644 --- a/libs/core/langchain_core/output_parsers/openai_tools.py +++ b/libs/core/langchain_core/output_parsers/openai_tools.py @@ -332,17 +332,15 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> An return None if self.first_tool_only else [] json_results = [json_results] if self.first_tool_only else json_results - name_dict_v2 = { - tool.model_config.get("title", tool.__name__): tool + name_dict_v2: dict[str, TypeBaseModel] = { + tool.model_config.get("title") or tool.__name__: tool for tool in self.tools if is_pydantic_v2_subclass(tool) } - name_dict_v1 = { - tool.model_config.get("title", tool.__name__): tool - for tool in self.tools - if is_pydantic_v1_subclass(tool) + name_dict_v1: dict[str, TypeBaseModel] = { + tool.__name__: tool for tool in self.tools if is_pydantic_v1_subclass(tool) } - name_dict = {**name_dict_v2, **name_dict_v1} + name_dict: dict[str, TypeBaseModel] = {**name_dict_v2, **name_dict_v1} pydantic_objects = [] for res in json_results: if not isinstance(res["args"], dict): From 5a4e193b503af07cda53c217728029b1bd652127 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Wed, 15 Oct 2025 16:36:49 +0500 Subject: [PATCH 5/9] test: Added Unit Tests --- .../output_parsers/test_openai_tools.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py index 7af420b326eb1..57e25a840c617 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py +++ b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py @@ -886,3 +886,146 @@ def test_max_tokens_error(caplog: Any) -> None: "`max_tokens` stop reason" in msg and record.levelname == "ERROR" for record, msg in zip(caplog.records, caplog.messages, strict=False) ) + + +def test_pydantic_tools_parser_with_mixed_pydantic_versions() -> None: + """Test PydanticToolsParser with both Pydantic v1 and v2 models.""" + + class WeatherV1(pydantic.v1.BaseModel): + """Weather information using Pydantic v1.""" + + temperature: int + conditions: str + + class LocationV2(BaseModel): + """Location information using Pydantic v2.""" + + city: str + country: str + + # Test with Pydantic v1 model + parser_v1 = PydanticToolsParser(tools=[WeatherV1]) + message_v1 = AIMessage( + content="", + tool_calls=[ + { + "id": "call_weather", + "name": "WeatherV1", + "args": {"temperature": 25, "conditions": "sunny"}, + } + ], + ) + generation_v1 = ChatGeneration(message=message_v1) + result_v1 = parser_v1.parse_result([generation_v1]) + + assert len(result_v1) == 1 + assert isinstance(result_v1[0], WeatherV1) + assert result_v1[0].temperature == 25 + assert result_v1[0].conditions == "sunny" + + # Test with Pydantic v2 model + parser_v2 = PydanticToolsParser(tools=[LocationV2]) + message_v2 = AIMessage( + content="", + tool_calls=[ + { + "id": "call_location", + "name": "LocationV2", + "args": {"city": "Paris", "country": "France"}, + } + ], + ) + generation_v2 = ChatGeneration(message=message_v2) + result_v2 = parser_v2.parse_result([generation_v2]) + + assert len(result_v2) == 1 + assert isinstance(result_v2[0], LocationV2) + assert result_v2[0].city == "Paris" + assert result_v2[0].country == "France" + + # Test with both v1 and v2 models + parser_mixed = PydanticToolsParser(tools=[WeatherV1, LocationV2]) + message_mixed = AIMessage( + content="", + tool_calls=[ + { + "id": "call_weather", + "name": "WeatherV1", + "args": {"temperature": 20, "conditions": "cloudy"}, + }, + { + "id": "call_location", + "name": "LocationV2", + "args": {"city": "London", "country": "UK"}, + }, + ], + ) + generation_mixed = ChatGeneration(message=message_mixed) + result_mixed = parser_mixed.parse_result([generation_mixed]) + + assert len(result_mixed) == 2 + assert isinstance(result_mixed[0], WeatherV1) + assert result_mixed[0].temperature == 20 + assert isinstance(result_mixed[1], LocationV2) + assert result_mixed[1].city == "London" + + +def test_pydantic_tools_parser_with_custom_title() -> None: + """Test PydanticToolsParser with Pydantic v2 model using custom title.""" + + class CustomTitleTool(BaseModel): + """Tool with custom title in model config.""" + + model_config = {"title": "MyCustomToolName"} + + value: int + description: str + + # Test with custom title - tool should be callable by custom name + parser = PydanticToolsParser(tools=[CustomTitleTool]) + message = AIMessage( + content="", + tool_calls=[ + { + "id": "call_custom", + "name": "MyCustomToolName", + "args": {"value": 42, "description": "test"}, + } + ], + ) + generation = ChatGeneration(message=message) + result = parser.parse_result([generation]) + + assert len(result) == 1 + assert isinstance(result[0], CustomTitleTool) + assert result[0].value == 42 + assert result[0].description == "test" + + +def test_pydantic_tools_parser_name_dict_fallback() -> None: + """Test that name_dict properly falls back to __name__ when title is None.""" + + class ToolWithoutTitle(BaseModel): + """Tool without explicit title.""" + + data: str + + # Ensure model_config doesn't have a title or it's None + # (This is the default behavior) + parser = PydanticToolsParser(tools=[ToolWithoutTitle]) + message = AIMessage( + content="", + tool_calls=[ + { + "id": "call_no_title", + "name": "ToolWithoutTitle", + "args": {"data": "test_data"}, + } + ], + ) + generation = ChatGeneration(message=message) + result = parser.parse_result([generation]) + + assert len(result) == 1 + assert isinstance(result[0], ToolWithoutTitle) + assert result[0].data == "test_data" From a7e99e3749713ba4d2ec9d98d0c95b5726c7033c Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 21 Oct 2025 16:26:26 +0500 Subject: [PATCH 6/9] tests: More Tests --- .../output_parsers/test_openai_tools.py | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) diff --git a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py index 57e25a840c617..c9055664bb86a 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py +++ b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py @@ -1029,3 +1029,284 @@ class ToolWithoutTitle(BaseModel): assert len(result) == 1 assert isinstance(result[0], ToolWithoutTitle) assert result[0].data == "test_data" + + +def test_pydantic_tools_parser_with_nested_models() -> None: + """Test PydanticToolsParser with nested Pydantic v1 and v2 models.""" + + # Nested v1 models + class AddressV1(pydantic.v1.BaseModel): + """Address using Pydantic v1.""" + + street: str + city: str + zip_code: str + + class PersonV1(pydantic.v1.BaseModel): + """Person with nested address using Pydantic v1.""" + + name: str + age: int + address: AddressV1 + + # Nested v2 models + class CoordinatesV2(BaseModel): + """Coordinates using Pydantic v2.""" + + latitude: float + longitude: float + + class LocationV2(BaseModel): + """Location with nested coordinates using Pydantic v2.""" + + name: str + coordinates: CoordinatesV2 + + # Test with nested Pydantic v1 model + parser_v1 = PydanticToolsParser(tools=[PersonV1]) + message_v1 = AIMessage( + content="", + tool_calls=[ + { + "id": "call_person", + "name": "PersonV1", + "args": { + "name": "Alice", + "age": 30, + "address": { + "street": "123 Main St", + "city": "Springfield", + "zip_code": "12345", + }, + }, + } + ], + ) + generation_v1 = ChatGeneration(message=message_v1) + result_v1 = parser_v1.parse_result([generation_v1]) + + assert len(result_v1) == 1 + assert isinstance(result_v1[0], PersonV1) + assert result_v1[0].name == "Alice" + assert result_v1[0].age == 30 + assert isinstance(result_v1[0].address, AddressV1) + assert result_v1[0].address.street == "123 Main St" + assert result_v1[0].address.city == "Springfield" + + # Test with nested Pydantic v2 model + parser_v2 = PydanticToolsParser(tools=[LocationV2]) + message_v2 = AIMessage( + content="", + tool_calls=[ + { + "id": "call_location", + "name": "LocationV2", + "args": { + "name": "Eiffel Tower", + "coordinates": {"latitude": 48.8584, "longitude": 2.2945}, + }, + } + ], + ) + generation_v2 = ChatGeneration(message=message_v2) + result_v2 = parser_v2.parse_result([generation_v2]) + + assert len(result_v2) == 1 + assert isinstance(result_v2[0], LocationV2) + assert result_v2[0].name == "Eiffel Tower" + assert isinstance(result_v2[0].coordinates, CoordinatesV2) + assert result_v2[0].coordinates.latitude == 48.8584 + assert result_v2[0].coordinates.longitude == 2.2945 + + # Test with both nested models in one message + parser_mixed = PydanticToolsParser(tools=[PersonV1, LocationV2]) + message_mixed = AIMessage( + content="", + tool_calls=[ + { + "id": "call_person", + "name": "PersonV1", + "args": { + "name": "Bob", + "age": 25, + "address": { + "street": "456 Oak Ave", + "city": "Portland", + "zip_code": "97201", + }, + }, + }, + { + "id": "call_location", + "name": "LocationV2", + "args": { + "name": "Golden Gate Bridge", + "coordinates": {"latitude": 37.8199, "longitude": -122.4783}, + }, + }, + ], + ) + generation_mixed = ChatGeneration(message=message_mixed) + result_mixed = parser_mixed.parse_result([generation_mixed]) + + assert len(result_mixed) == 2 + assert isinstance(result_mixed[0], PersonV1) + assert result_mixed[0].name == "Bob" + assert result_mixed[0].address.city == "Portland" + assert isinstance(result_mixed[1], LocationV2) + assert result_mixed[1].name == "Golden Gate Bridge" + assert result_mixed[1].coordinates.latitude == 37.8199 + + +def test_pydantic_tools_parser_with_optional_fields() -> None: + """Test PydanticToolsParser with optional fields in v1 and v2 models.""" + + # v1 model with optional fields + class ProductV1(pydantic.v1.BaseModel): + """Product with optional fields using Pydantic v1.""" + + name: str + price: float + description: str | None = None + stock: int = 0 + + # v2 model with optional fields + class UserV2(BaseModel): + """User with optional fields using Pydantic v2.""" + + username: str + email: str + bio: str | None = None + age: int | None = None + + # Test v1 with all fields provided + parser_v1_full = PydanticToolsParser(tools=[ProductV1]) + message_v1_full = AIMessage( + content="", + tool_calls=[ + { + "id": "call_product_full", + "name": "ProductV1", + "args": { + "name": "Laptop", + "price": 999.99, + "description": "High-end laptop", + "stock": 50, + }, + } + ], + ) + generation_v1_full = ChatGeneration(message=message_v1_full) + result_v1_full = parser_v1_full.parse_result([generation_v1_full]) + + assert len(result_v1_full) == 1 + assert isinstance(result_v1_full[0], ProductV1) + assert result_v1_full[0].name == "Laptop" + assert result_v1_full[0].price == 999.99 + assert result_v1_full[0].description == "High-end laptop" + assert result_v1_full[0].stock == 50 + + # Test v1 with only required fields + parser_v1_minimal = PydanticToolsParser(tools=[ProductV1]) + message_v1_minimal = AIMessage( + content="", + tool_calls=[ + { + "id": "call_product_minimal", + "name": "ProductV1", + "args": {"name": "Mouse", "price": 29.99}, + } + ], + ) + generation_v1_minimal = ChatGeneration(message=message_v1_minimal) + result_v1_minimal = parser_v1_minimal.parse_result([generation_v1_minimal]) + + assert len(result_v1_minimal) == 1 + assert isinstance(result_v1_minimal[0], ProductV1) + assert result_v1_minimal[0].name == "Mouse" + assert result_v1_minimal[0].price == 29.99 + assert result_v1_minimal[0].description is None + assert result_v1_minimal[0].stock == 0 + + # Test v2 with all fields provided + parser_v2_full = PydanticToolsParser(tools=[UserV2]) + message_v2_full = AIMessage( + content="", + tool_calls=[ + { + "id": "call_user_full", + "name": "UserV2", + "args": { + "username": "john_doe", + "email": "john@example.com", + "bio": "Software developer", + "age": 28, + }, + } + ], + ) + generation_v2_full = ChatGeneration(message=message_v2_full) + result_v2_full = parser_v2_full.parse_result([generation_v2_full]) + + assert len(result_v2_full) == 1 + assert isinstance(result_v2_full[0], UserV2) + assert result_v2_full[0].username == "john_doe" + assert result_v2_full[0].email == "john@example.com" + assert result_v2_full[0].bio == "Software developer" + assert result_v2_full[0].age == 28 + + # Test v2 with only required fields + parser_v2_minimal = PydanticToolsParser(tools=[UserV2]) + message_v2_minimal = AIMessage( + content="", + tool_calls=[ + { + "id": "call_user_minimal", + "name": "UserV2", + "args": {"username": "jane_smith", "email": "jane@example.com"}, + } + ], + ) + generation_v2_minimal = ChatGeneration(message=message_v2_minimal) + result_v2_minimal = parser_v2_minimal.parse_result([generation_v2_minimal]) + + assert len(result_v2_minimal) == 1 + assert isinstance(result_v2_minimal[0], UserV2) + assert result_v2_minimal[0].username == "jane_smith" + assert result_v2_minimal[0].email == "jane@example.com" + assert result_v2_minimal[0].bio is None + assert result_v2_minimal[0].age is None + + # Test mixed v1 and v2 with partial optional fields + parser_mixed = PydanticToolsParser(tools=[ProductV1, UserV2]) + message_mixed = AIMessage( + content="", + tool_calls=[ + { + "id": "call_product", + "name": "ProductV1", + "args": {"name": "Keyboard", "price": 79.99, "stock": 100}, + }, + { + "id": "call_user", + "name": "UserV2", + "args": { + "username": "alice", + "email": "alice@example.com", + "age": 35, + }, + }, + ], + ) + generation_mixed = ChatGeneration(message=message_mixed) + result_mixed = parser_mixed.parse_result([generation_mixed]) + + assert len(result_mixed) == 2 + assert isinstance(result_mixed[0], ProductV1) + assert result_mixed[0].name == "Keyboard" + assert result_mixed[0].description is None + assert result_mixed[0].stock == 100 + assert isinstance(result_mixed[1], UserV2) + assert result_mixed[1].username == "alice" + assert result_mixed[1].bio is None + assert result_mixed[1].age == 35 From fa15fece92e340d489ab31320c002d43ed48c60d Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 21 Oct 2025 16:44:04 +0500 Subject: [PATCH 7/9] tests: fix --- .../output_parsers/test_openai_tools.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py index c9055664bb86a..a4cb6ff91a0ae 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py +++ b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py @@ -894,8 +894,8 @@ def test_pydantic_tools_parser_with_mixed_pydantic_versions() -> None: class WeatherV1(pydantic.v1.BaseModel): """Weather information using Pydantic v1.""" - temperature: int - conditions: str + temperature: int = pydantic.v1.Field() + conditions: str = pydantic.v1.Field() class LocationV2(BaseModel): """Location information using Pydantic v2.""" @@ -1038,16 +1038,16 @@ def test_pydantic_tools_parser_with_nested_models() -> None: class AddressV1(pydantic.v1.BaseModel): """Address using Pydantic v1.""" - street: str - city: str - zip_code: str + street: str = pydantic.v1.Field() + city: str = pydantic.v1.Field() + zip_code: str = pydantic.v1.Field() class PersonV1(pydantic.v1.BaseModel): """Person with nested address using Pydantic v1.""" - name: str - age: int - address: AddressV1 + name: str = pydantic.v1.Field() + age: int = pydantic.v1.Field() + address: AddressV1 = pydantic.v1.Field() # Nested v2 models class CoordinatesV2(BaseModel): @@ -1165,10 +1165,10 @@ def test_pydantic_tools_parser_with_optional_fields() -> None: class ProductV1(pydantic.v1.BaseModel): """Product with optional fields using Pydantic v1.""" - name: str - price: float - description: str | None = None - stock: int = 0 + name: str = pydantic.v1.Field() + price: float = pydantic.v1.Field() + description: str | None = pydantic.v1.Field(default=None) + stock: int = pydantic.v1.Field(default=0) # v2 model with optional fields class UserV2(BaseModel): From 97a8c68340f6e2d9d886ded117f19b632d47add4 Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 21 Oct 2025 16:58:06 +0500 Subject: [PATCH 8/9] tests: fix --- .../output_parsers/test_openai_tools.py | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py index a4cb6ff91a0ae..9be604a0538ca 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py +++ b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py @@ -1,3 +1,4 @@ +import sys from collections.abc import AsyncIterator, Iterator from typing import Any @@ -890,12 +891,21 @@ def test_max_tokens_error(caplog: Any) -> None: def test_pydantic_tools_parser_with_mixed_pydantic_versions() -> None: """Test PydanticToolsParser with both Pydantic v1 and v2 models.""" + # For Python 3.14+ compatibility, use create_model for Pydantic v1 + if sys.version_info >= (3, 14): + WeatherV1 = pydantic.v1.create_model( # noqa: N806 + "WeatherV1", + __doc__="Weather information using Pydantic v1.", + temperature=(int, ...), + conditions=(str, ...), + ) + else: - class WeatherV1(pydantic.v1.BaseModel): - """Weather information using Pydantic v1.""" + class WeatherV1(pydantic.v1.BaseModel): + """Weather information using Pydantic v1.""" - temperature: int = pydantic.v1.Field() - conditions: str = pydantic.v1.Field() + temperature: int + conditions: str class LocationV2(BaseModel): """Location information using Pydantic v2.""" @@ -1033,21 +1043,37 @@ class ToolWithoutTitle(BaseModel): def test_pydantic_tools_parser_with_nested_models() -> None: """Test PydanticToolsParser with nested Pydantic v1 and v2 models.""" - # Nested v1 models - class AddressV1(pydantic.v1.BaseModel): - """Address using Pydantic v1.""" + if sys.version_info >= (3, 14): + AddressV1 = pydantic.v1.create_model( # noqa: N806 + "AddressV1", + __doc__="Address using Pydantic v1.", + street=(str, ...), + city=(str, ...), + zip_code=(str, ...), + ) + PersonV1 = pydantic.v1.create_model( # noqa: N806 + "PersonV1", + __doc__="Person with nested address using Pydantic v1.", + name=(str, ...), + age=(int, ...), + address=(AddressV1, ...), + ) + else: + + class AddressV1(pydantic.v1.BaseModel): + """Address using Pydantic v1.""" - street: str = pydantic.v1.Field() - city: str = pydantic.v1.Field() - zip_code: str = pydantic.v1.Field() + street: str + city: str + zip_code: str - class PersonV1(pydantic.v1.BaseModel): - """Person with nested address using Pydantic v1.""" + class PersonV1(pydantic.v1.BaseModel): + """Person with nested address using Pydantic v1.""" - name: str = pydantic.v1.Field() - age: int = pydantic.v1.Field() - address: AddressV1 = pydantic.v1.Field() + name: str + age: int + address: AddressV1 # Nested v2 models class CoordinatesV2(BaseModel): @@ -1160,15 +1186,24 @@ class LocationV2(BaseModel): def test_pydantic_tools_parser_with_optional_fields() -> None: """Test PydanticToolsParser with optional fields in v1 and v2 models.""" + if sys.version_info >= (3, 14): + ProductV1 = pydantic.v1.create_model( # noqa: N806 + "ProductV1", + __doc__="Product with optional fields using Pydantic v1.", + name=(str, ...), + price=(float, ...), + description=(str | None, None), + stock=(int, 0), + ) + else: - # v1 model with optional fields - class ProductV1(pydantic.v1.BaseModel): - """Product with optional fields using Pydantic v1.""" + class ProductV1(pydantic.v1.BaseModel): + """Product with optional fields using Pydantic v1.""" - name: str = pydantic.v1.Field() - price: float = pydantic.v1.Field() - description: str | None = pydantic.v1.Field(default=None) - stock: int = pydantic.v1.Field(default=0) + name: str + price: float + description: str | None = None + stock: int = 0 # v2 model with optional fields class UserV2(BaseModel): From b5c8275bd2c2d8ee4fdf95a58d6778524d86a3ad Mon Sep 17 00:00:00 2001 From: keenborder786 <21110290@lums.edu.pk> Date: Tue, 21 Oct 2025 17:05:53 +0500 Subject: [PATCH 9/9] tests: fix --- .../output_parsers/test_openai_tools.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py index 9be604a0538ca..1ae19b8b556a7 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py +++ b/libs/core/tests/unit_tests/output_parsers/test_openai_tools.py @@ -930,8 +930,8 @@ class LocationV2(BaseModel): assert len(result_v1) == 1 assert isinstance(result_v1[0], WeatherV1) - assert result_v1[0].temperature == 25 - assert result_v1[0].conditions == "sunny" + assert result_v1[0].temperature == 25 # type: ignore[attr-defined,unused-ignore] + assert result_v1[0].conditions == "sunny" # type: ignore[attr-defined,unused-ignore] # Test with Pydantic v2 model parser_v2 = PydanticToolsParser(tools=[LocationV2]) @@ -975,7 +975,7 @@ class LocationV2(BaseModel): assert len(result_mixed) == 2 assert isinstance(result_mixed[0], WeatherV1) - assert result_mixed[0].temperature == 20 + assert result_mixed[0].temperature == 20 # type: ignore[attr-defined,unused-ignore] assert isinstance(result_mixed[1], LocationV2) assert result_mixed[1].city == "London" @@ -1113,11 +1113,11 @@ class LocationV2(BaseModel): assert len(result_v1) == 1 assert isinstance(result_v1[0], PersonV1) - assert result_v1[0].name == "Alice" - assert result_v1[0].age == 30 - assert isinstance(result_v1[0].address, AddressV1) - assert result_v1[0].address.street == "123 Main St" - assert result_v1[0].address.city == "Springfield" + assert result_v1[0].name == "Alice" # type: ignore[attr-defined,unused-ignore] + assert result_v1[0].age == 30 # type: ignore[attr-defined,unused-ignore] + assert isinstance(result_v1[0].address, AddressV1) # type: ignore[attr-defined,unused-ignore] + assert result_v1[0].address.street == "123 Main St" # type: ignore[attr-defined,unused-ignore] + assert result_v1[0].address.city == "Springfield" # type: ignore[attr-defined,unused-ignore] # Test with nested Pydantic v2 model parser_v2 = PydanticToolsParser(tools=[LocationV2]) @@ -1177,8 +1177,8 @@ class LocationV2(BaseModel): assert len(result_mixed) == 2 assert isinstance(result_mixed[0], PersonV1) - assert result_mixed[0].name == "Bob" - assert result_mixed[0].address.city == "Portland" + assert result_mixed[0].name == "Bob" # type: ignore[attr-defined,unused-ignore] + assert result_mixed[0].address.city == "Portland" # type: ignore[attr-defined,unused-ignore] assert isinstance(result_mixed[1], LocationV2) assert result_mixed[1].name == "Golden Gate Bridge" assert result_mixed[1].coordinates.latitude == 37.8199 @@ -1236,10 +1236,10 @@ class UserV2(BaseModel): assert len(result_v1_full) == 1 assert isinstance(result_v1_full[0], ProductV1) - assert result_v1_full[0].name == "Laptop" - assert result_v1_full[0].price == 999.99 - assert result_v1_full[0].description == "High-end laptop" - assert result_v1_full[0].stock == 50 + assert result_v1_full[0].name == "Laptop" # type: ignore[attr-defined,unused-ignore] + assert result_v1_full[0].price == 999.99 # type: ignore[attr-defined,unused-ignore] + assert result_v1_full[0].description == "High-end laptop" # type: ignore[attr-defined,unused-ignore] + assert result_v1_full[0].stock == 50 # type: ignore[attr-defined,unused-ignore] # Test v1 with only required fields parser_v1_minimal = PydanticToolsParser(tools=[ProductV1]) @@ -1258,10 +1258,10 @@ class UserV2(BaseModel): assert len(result_v1_minimal) == 1 assert isinstance(result_v1_minimal[0], ProductV1) - assert result_v1_minimal[0].name == "Mouse" - assert result_v1_minimal[0].price == 29.99 - assert result_v1_minimal[0].description is None - assert result_v1_minimal[0].stock == 0 + assert result_v1_minimal[0].name == "Mouse" # type: ignore[attr-defined,unused-ignore] + assert result_v1_minimal[0].price == 29.99 # type: ignore[attr-defined,unused-ignore] + assert result_v1_minimal[0].description is None # type: ignore[attr-defined,unused-ignore] + assert result_v1_minimal[0].stock == 0 # type: ignore[attr-defined,unused-ignore] # Test v2 with all fields provided parser_v2_full = PydanticToolsParser(tools=[UserV2]) @@ -1338,9 +1338,9 @@ class UserV2(BaseModel): assert len(result_mixed) == 2 assert isinstance(result_mixed[0], ProductV1) - assert result_mixed[0].name == "Keyboard" - assert result_mixed[0].description is None - assert result_mixed[0].stock == 100 + assert result_mixed[0].name == "Keyboard" # type: ignore[attr-defined,unused-ignore] + assert result_mixed[0].description is None # type: ignore[attr-defined,unused-ignore] + assert result_mixed[0].stock == 100 # type: ignore[attr-defined,unused-ignore] assert isinstance(result_mixed[1], UserV2) assert result_mixed[1].username == "alice" assert result_mixed[1].bio is None