From 8fe2ec52cf80e1e21804dcdf193bc26bfd6b32e3 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Thu, 7 Mar 2024 15:03:48 -0600 Subject: [PATCH 01/12] fix function calling schema for pydantic v2 --- guardrails/utils/pydantic_utils/v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index e8e75d598..eb4d122e0 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -116,7 +116,7 @@ def is_enum(type_annotation: Any) -> bool: return False -def _create_bare_model(model: Type[BaseModel]) -> Type[BaseModel]: +def _create_bare_model(model: BaseModel) -> Type[BaseModel]: class BareModel(BaseModel): __annotations__ = getattr(model, "__annotations__", {}) @@ -133,7 +133,7 @@ def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: OpenAI function paramters. """ - bare_model = _create_bare_model(type(model)) + bare_model = _create_bare_model(model) # Convert Pydantic model to JSON schema json_schema = bare_model.model_json_schema() From 43d6db18abadeb79ed08e73984c24e365e58eede Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Fri, 8 Mar 2024 09:20:09 -0600 Subject: [PATCH 02/12] add tests --- guardrails/utils/pydantic_utils/v1.py | 1 + guardrails/utils/pydantic_utils/v2.py | 1 + tests/unit_tests/utils/pydantic_utils/v1.py | 48 +++++++++++ tests/unit_tests/utils/pydantic_utils/v2.py | 93 +++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 tests/unit_tests/utils/pydantic_utils/v1.py create mode 100644 tests/unit_tests/utils/pydantic_utils/v2.py diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index 00bf5103c..4974be8e7 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -244,6 +244,7 @@ class BareModel(BaseModel): # Convert Pydantic model to JSON schema json_schema = BareModel.schema() + json_schema["title"] = model.__name__ # Create OpenAI function parameters fn_params = { diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index eb4d122e0..2c6fb47ef 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -137,6 +137,7 @@ def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: # Convert Pydantic model to JSON schema json_schema = bare_model.model_json_schema() + json_schema["title"] = model.__name__ # Create OpenAI function parameters fn_params = { diff --git a/tests/unit_tests/utils/pydantic_utils/v1.py b/tests/unit_tests/utils/pydantic_utils/v1.py new file mode 100644 index 000000000..6f640b330 --- /dev/null +++ b/tests/unit_tests/utils/pydantic_utils/v1.py @@ -0,0 +1,48 @@ +import pydantic.version +import pytest +from pydantic import BaseModel, Field + +from guardrails.utils.pydantic_utils.v1 import convert_pydantic_model_to_openai_fn + +PYDANTIC_VERSION = pydantic.version.VERSION + + +# This test is descriptive, not prescriptive. +@pytest.mark.skipif( + not PYDANTIC_VERSION.startswith("1"), + reason="Tests function calling syntax for Pydantic v1", +) +def test_convert_pydantic_model_to_openai_fn(): + class Foo(BaseModel): + bar: str = Field(description="some string value") + + # fmt: off + expected_schema = { + "title": "Foo", + "type": "object", + "properties": { + "bar": { + "title": "Bar", + "description": "some string value", + "type": "string" + } + }, + "required": [ + "bar" + ] + } + # fmt: on + + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_fn_params = { + "name": "Foo", + "parameters": expected_schema + } + # fmt: on + + actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) + + assert actual_fn_params == expected_fn_params diff --git a/tests/unit_tests/utils/pydantic_utils/v2.py b/tests/unit_tests/utils/pydantic_utils/v2.py new file mode 100644 index 000000000..cb410bf5d --- /dev/null +++ b/tests/unit_tests/utils/pydantic_utils/v2.py @@ -0,0 +1,93 @@ +from copy import deepcopy + +import pydantic.version +import pytest +from pydantic import BaseModel, Field + +from guardrails.utils.pydantic_utils.v2 import ( + _create_bare_model, + convert_pydantic_model_to_openai_fn, +) + +PYDANTIC_VERSION = pydantic.version.VERSION + + +class Foo(BaseModel): + bar: str = Field(description="some string value") + + +# fmt: off +foo_schema = { + "title": "Foo", + "type": "object", + "properties": { + "bar": { + "title": "Bar", + "description": "some string value", + "type": "string" + } + }, + "required": [ + "bar" + ] +} +# fmt: on + + +# This test is descriptive, not prescriptive. +@pytest.mark.skipif( + not PYDANTIC_VERSION.startswith("2"), + reason="Tests function calling syntax for Pydantic v2", +) +def test_convert_pydantic_model_to_openai_fn(): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_fn_params = { + "name": "Foo", + "parameters": expected_schema + } + # fmt: on + + actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) + + assert actual_fn_params == expected_fn_params + + +# These tests demonstrate the issue fixed in PR #616 +@pytest.mark.skipif( + not PYDANTIC_VERSION.startswith("2"), + reason="Tests function calling syntax for Pydantic v2", +) +class TestCreateBareModel: + def test_with_model_type(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # Current logic + bare_model = _create_bare_model(Foo) + + # Convert Pydantic model to JSON schema + json_schema = bare_model.model_json_schema() + json_schema["title"] = Foo.__name__ + + assert json_schema == expected_schema + + def test_with_type_of_model_type(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + empty_schema = {"properties": {}, "title": "BareModel", "type": "object"} + + # Previous logic + bare_model = _create_bare_model(type(Foo)) + + # Convert Pydantic model to JSON schema + json_schema = bare_model.model_json_schema() + + assert json_schema != expected_schema + assert json_schema == empty_schema From dedee9b8af5190ae85dbd2f7a6481298b9d18a6b Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Fri, 8 Mar 2024 10:04:11 -0600 Subject: [PATCH 03/12] add list typing and tests --- guardrails/llm_providers.py | 21 ++++- guardrails/utils/pydantic_utils/v1.py | 38 +++++++- guardrails/utils/pydantic_utils/v2.py | 43 +++++++-- tests/unit_tests/utils/pydantic_utils/v1.py | 99 ++++++++++++++------- tests/unit_tests/utils/pydantic_utils/v2.py | 50 ++++++++--- 5 files changed, 193 insertions(+), 58 deletions(-) diff --git a/guardrails/llm_providers.py b/guardrails/llm_providers.py index 8bae2bc89..545d14a9b 100644 --- a/guardrails/llm_providers.py +++ b/guardrails/llm_providers.py @@ -1,4 +1,15 @@ -from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional, cast +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + List, + Optional, + Type, + Union, + cast, +) from pydantic import BaseModel @@ -148,7 +159,9 @@ def _invoke_llm( model: str = "gpt-3.5-turbo", instructions: Optional[str] = None, msg_history: Optional[List[Dict]] = None, - base_model: Optional[BaseModel] = None, + base_model: Optional[ + Union[Type[BaseModel], Type[List[Type[BaseModel]]]] + ] = None, function_call: Optional[Any] = None, *args, **kwargs, @@ -678,7 +691,9 @@ async def invoke_llm( model: str = "gpt-3.5-turbo", instructions: Optional[str] = None, msg_history: Optional[List[Dict]] = None, - base_model: Optional[BaseModel] = None, + base_model: Optional[ + Union[Type[BaseModel], Type[List[Type[BaseModel]]]] + ] = None, function_call: Optional[Any] = None, *args, **kwargs, diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index 4974be8e7..b487c5735 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -4,7 +4,17 @@ from copy import deepcopy from datetime import date, time from enum import Enum -from typing import Any, Callable, Dict, Optional, Type, Union, get_args, get_origin +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Type, + Union, + get_args, + get_origin, +) from pydantic import BaseModel, validator from pydantic.fields import ModelField @@ -22,6 +32,7 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType +from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -228,7 +239,9 @@ def process_validators(vals, fld): return model_fields -def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: +def convert_pydantic_model_to_openai_fn( + model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] +) -> Dict: """Convert a Pydantic BaseModel to an OpenAI function. Args: @@ -238,13 +251,30 @@ def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: OpenAI function paramters. """ + schema_model = model + + type_origin = get_origin(model) + if type_origin == list: + item_types = get_args(model) + if len(item_types) > 1: + raise ValueError("List data type must have exactly one child.") + # No List[List] support; we've already declared that in our types + schema_model = safe_get(item_types, 0) + # Create a bare model with no extra fields class BareModel(BaseModel): - __annotations__ = model.__annotations__ + __annotations__ = schema_model.__annotations__ # Convert Pydantic model to JSON schema json_schema = BareModel.schema() - json_schema["title"] = model.__name__ + json_schema["title"] = schema_model.__name__ + + if type_origin == list: + json_schema = { + "title": f"List<{json_schema.get('title')}>", + "type": "array", + "items": json_schema, + } # Create OpenAI function parameters fn_params = { diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index 2c6fb47ef..cb3ee6a28 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -3,7 +3,18 @@ from copy import deepcopy from datetime import date, time from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, get_args +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Type, + TypeVar, + Union, + get_args, + get_origin, +) from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator from pydantic.fields import FieldInfo @@ -23,6 +34,7 @@ from guardrails.datatypes import PythonCode as PythonCodeDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType +from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -116,14 +128,18 @@ def is_enum(type_annotation: Any) -> bool: return False -def _create_bare_model(model: BaseModel) -> Type[BaseModel]: +def _create_bare_model( + model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] +) -> Type[BaseModel]: class BareModel(BaseModel): __annotations__ = getattr(model, "__annotations__", {}) return BareModel -def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: +def convert_pydantic_model_to_openai_fn( + model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] +) -> Dict: """Convert a Pydantic BaseModel to an OpenAI function. Args: @@ -133,11 +149,28 @@ def convert_pydantic_model_to_openai_fn(model: BaseModel) -> Dict: OpenAI function paramters. """ - bare_model = _create_bare_model(model) + schema_model = model + + type_origin = get_origin(model) + if type_origin == list: + item_types = get_args(model) + if len(item_types) > 1: + raise ValueError("List data type must have exactly one child.") + # No List[List] support; we've already declared that in our types + schema_model = safe_get(item_types, 0) + + bare_model = _create_bare_model(schema_model) # Convert Pydantic model to JSON schema json_schema = bare_model.model_json_schema() - json_schema["title"] = model.__name__ + json_schema["title"] = schema_model.__name__ + + if type_origin == list: + json_schema = { + "title": f"List<{json_schema.get('title')}>", + "type": "array", + "items": json_schema, + } # Create OpenAI function parameters fn_params = { diff --git a/tests/unit_tests/utils/pydantic_utils/v1.py b/tests/unit_tests/utils/pydantic_utils/v1.py index 6f640b330..0c25c1cac 100644 --- a/tests/unit_tests/utils/pydantic_utils/v1.py +++ b/tests/unit_tests/utils/pydantic_utils/v1.py @@ -1,3 +1,6 @@ +from copy import deepcopy +from typing import List + import pydantic.version import pytest from pydantic import BaseModel, Field @@ -7,42 +10,70 @@ PYDANTIC_VERSION = pydantic.version.VERSION +class Foo(BaseModel): + bar: str = Field(description="some string value") + + +# fmt: off +foo_schema = { + "title": "Foo", + "type": "object", + "properties": { + "bar": { + "title": "Bar", + "description": "some string value", + "type": "string" + } + }, + "required": [ + "bar" + ] +} +# fmt: on + + # This test is descriptive, not prescriptive. @pytest.mark.skipif( not PYDANTIC_VERSION.startswith("1"), reason="Tests function calling syntax for Pydantic v1", ) -def test_convert_pydantic_model_to_openai_fn(): - class Foo(BaseModel): - bar: str = Field(description="some string value") - - # fmt: off - expected_schema = { - "title": "Foo", - "type": "object", - "properties": { - "bar": { - "title": "Bar", - "description": "some string value", - "type": "string" - } - }, - "required": [ - "bar" - ] - } - # fmt: on - - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] - - # fmt: off - expected_fn_params = { - "name": "Foo", - "parameters": expected_schema - } - # fmt: on - - actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) - - assert actual_fn_params == expected_fn_params +class TestConvertPydanticModelToOpenaiFn: + def test_object_schema(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_fn_params = { + "name": "Foo", + "parameters": expected_schema + } + # fmt: on + + actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) + + assert actual_fn_params == expected_fn_params + + def test_list_schema(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_schema = { + "title": f"List<{expected_schema.get('title')}>", + "type": "array", + "items": expected_schema + } + # fmt: on + + # fmt: off + expected_fn_params = { + "name": "List", + "parameters": expected_schema + } + # fmt: on + + actual_fn_params = convert_pydantic_model_to_openai_fn(List[Foo]) + + assert actual_fn_params == expected_fn_params diff --git a/tests/unit_tests/utils/pydantic_utils/v2.py b/tests/unit_tests/utils/pydantic_utils/v2.py index cb410bf5d..a594fb014 100644 --- a/tests/unit_tests/utils/pydantic_utils/v2.py +++ b/tests/unit_tests/utils/pydantic_utils/v2.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import List import pydantic.version import pytest @@ -39,21 +40,46 @@ class Foo(BaseModel): not PYDANTIC_VERSION.startswith("2"), reason="Tests function calling syntax for Pydantic v2", ) -def test_convert_pydantic_model_to_openai_fn(): - expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] +class TestConvertPydanticModelToOpenaiFn: + def test_object_schema(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_fn_params = { + "name": "Foo", + "parameters": expected_schema + } + # fmt: on + + actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) + + assert actual_fn_params == expected_fn_params - # fmt: off - expected_fn_params = { - "name": "Foo", - "parameters": expected_schema - } - # fmt: on + def test_list_schema(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # fmt: off + expected_schema = { + "title": f"List<{expected_schema.get('title')}>", + "type": "array", + "items": expected_schema + } + # fmt: on + + # fmt: off + expected_fn_params = { + "name": "List", + "parameters": expected_schema + } + # fmt: on - actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) + actual_fn_params = convert_pydantic_model_to_openai_fn(List[Foo]) - assert actual_fn_params == expected_fn_params + assert actual_fn_params == expected_fn_params # These tests demonstrate the issue fixed in PR #616 From 41223d57368320d0056b26dc19180e7a97f1b9a5 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Tue, 12 Mar 2024 08:21:12 -0500 Subject: [PATCH 04/12] refactor _create_bare_model to shared method --- guardrails/utils/pydantic_utils/common.py | 12 ++++ guardrails/utils/pydantic_utils/v1.py | 6 +- guardrails/utils/pydantic_utils/v2.py | 10 +-- .../unit_tests/utils/pydantic_utils/common.py | 69 +++++++++++++++++++ tests/unit_tests/utils/pydantic_utils/v2.py | 42 +---------- 5 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 guardrails/utils/pydantic_utils/common.py create mode 100644 tests/unit_tests/utils/pydantic_utils/common.py diff --git a/guardrails/utils/pydantic_utils/common.py b/guardrails/utils/pydantic_utils/common.py new file mode 100644 index 000000000..3d20ae0ee --- /dev/null +++ b/guardrails/utils/pydantic_utils/common.py @@ -0,0 +1,12 @@ +from typing import List, Type, Union + +from pydantic import BaseModel + + +def _create_bare_model( + model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] +) -> Type[BaseModel]: + class BareModel(BaseModel): + __annotations__ = getattr(model, "__annotations__", {}) + + return BareModel diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index b487c5735..a858a96a2 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -32,6 +32,7 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType +from guardrails.utils.pydantic_utils.common import _create_bare_model from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -262,11 +263,10 @@ def convert_pydantic_model_to_openai_fn( schema_model = safe_get(item_types, 0) # Create a bare model with no extra fields - class BareModel(BaseModel): - __annotations__ = schema_model.__annotations__ + bare_model = _create_bare_model(schema_model) # Convert Pydantic model to JSON schema - json_schema = BareModel.schema() + json_schema = bare_model.schema() json_schema["title"] = schema_model.__name__ if type_origin == list: diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index 10c41ce49..cb630310b 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -32,6 +32,7 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType +from guardrails.utils.pydantic_utils.common import _create_bare_model from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -126,15 +127,6 @@ def is_enum(type_annotation: Any) -> bool: return False -def _create_bare_model( - model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] -) -> Type[BaseModel]: - class BareModel(BaseModel): - __annotations__ = getattr(model, "__annotations__", {}) - - return BareModel - - def convert_pydantic_model_to_openai_fn( model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] ) -> Dict: diff --git a/tests/unit_tests/utils/pydantic_utils/common.py b/tests/unit_tests/utils/pydantic_utils/common.py new file mode 100644 index 000000000..50646f08d --- /dev/null +++ b/tests/unit_tests/utils/pydantic_utils/common.py @@ -0,0 +1,69 @@ +from copy import deepcopy + +import pydantic +from pydantic import BaseModel, Field + +from guardrails.utils.pydantic_utils.common import _create_bare_model + +PYDANTIC_VERSION = pydantic.version.VERSION + + +class Foo(BaseModel): + bar: str = Field(description="some string value") + + +# fmt: off +foo_schema = { + "title": "Foo", + "type": "object", + "properties": { + "bar": { + "title": "Bar", + "description": "some string value", + "type": "string" + } + }, + "required": [ + "bar" + ] +} +# fmt: on + + +# These tests demonstrate the issue fixed in PR #616 +class TestCreateBareModel: + def test_with_model_type(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + # Current logic + bare_model = _create_bare_model(Foo) + + # Convert Pydantic model to JSON schema + if PYDANTIC_VERSION.startswith("1"): + json_schema = bare_model.schema() + else: + json_schema = bare_model.model_json_schema() + json_schema["title"] = Foo.__name__ + + assert json_schema == expected_schema + + def test_with_type_of_model_type(self): + expected_schema = deepcopy(foo_schema) + # When pushed through BareModel it loses the description on any properties. + del expected_schema["properties"]["bar"]["description"] + + empty_schema = {"properties": {}, "title": "BareModel", "type": "object"} + + # Previous logic + bare_model = _create_bare_model(type(Foo)) + + # Convert Pydantic model to JSON schema + if PYDANTIC_VERSION.startswith("1"): + json_schema = bare_model.schema() + else: + json_schema = bare_model.model_json_schema() + + assert json_schema != expected_schema + assert json_schema == empty_schema diff --git a/tests/unit_tests/utils/pydantic_utils/v2.py b/tests/unit_tests/utils/pydantic_utils/v2.py index a594fb014..593050434 100644 --- a/tests/unit_tests/utils/pydantic_utils/v2.py +++ b/tests/unit_tests/utils/pydantic_utils/v2.py @@ -5,10 +5,7 @@ import pytest from pydantic import BaseModel, Field -from guardrails.utils.pydantic_utils.v2 import ( - _create_bare_model, - convert_pydantic_model_to_openai_fn, -) +from guardrails.utils.pydantic_utils.v2 import convert_pydantic_model_to_openai_fn PYDANTIC_VERSION = pydantic.version.VERSION @@ -80,40 +77,3 @@ def test_list_schema(self): actual_fn_params = convert_pydantic_model_to_openai_fn(List[Foo]) assert actual_fn_params == expected_fn_params - - -# These tests demonstrate the issue fixed in PR #616 -@pytest.mark.skipif( - not PYDANTIC_VERSION.startswith("2"), - reason="Tests function calling syntax for Pydantic v2", -) -class TestCreateBareModel: - def test_with_model_type(self): - expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] - - # Current logic - bare_model = _create_bare_model(Foo) - - # Convert Pydantic model to JSON schema - json_schema = bare_model.model_json_schema() - json_schema["title"] = Foo.__name__ - - assert json_schema == expected_schema - - def test_with_type_of_model_type(self): - expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] - - empty_schema = {"properties": {}, "title": "BareModel", "type": "object"} - - # Previous logic - bare_model = _create_bare_model(type(Foo)) - - # Convert Pydantic model to JSON schema - json_schema = bare_model.model_json_schema() - - assert json_schema != expected_schema - assert json_schema == empty_schema From 940db4d1efaa8c18d6794d18843b6099b33149d0 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 10:40:34 -0500 Subject: [PATCH 05/12] fix serialization for pydantic 2 --- guardrails/utils/pydantic_utils/common.py | 51 +++++++++++++++++-- guardrails/utils/pydantic_utils/v1.py | 4 +- guardrails/utils/pydantic_utils/v2.py | 6 +-- guardrails/validator_base.py | 4 +- .../unit_tests/utils/pydantic_utils/common.py | 8 +-- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/guardrails/utils/pydantic_utils/common.py b/guardrails/utils/pydantic_utils/common.py index 3d20ae0ee..8df42d507 100644 --- a/guardrails/utils/pydantic_utils/common.py +++ b/guardrails/utils/pydantic_utils/common.py @@ -1,12 +1,55 @@ -from typing import List, Type, Union +from typing import Any, Dict, List, Type, Union, get_args, get_origin from pydantic import BaseModel +from guardrails.utils.safe_get import safe_get -def _create_bare_model( - model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] -) -> Type[BaseModel]: + +def create_bare_model(model: Type[BaseModel]): class BareModel(BaseModel): __annotations__ = getattr(model, "__annotations__", {}) return BareModel + + +def reduce_to_annotations(type_annotation: Any) -> Type[Any]: + if ( + type_annotation + and isinstance(type_annotation, type) + and issubclass(type_annotation, BaseModel) + ): + return create_bare_model(type_annotation) + return type_annotation + + +def find_models_in_type(type_annotation: Any) -> Type[Any]: + type_origin = get_origin(type_annotation) + inner_types = get_args(type_annotation) + if type_origin == Union: + data_types = tuple([find_models_in_type(t) for t in inner_types]) + return Type[Union[data_types]] # type: ignore + elif type_origin == list: + if len(inner_types) > 1: + raise ValueError("List data type must have exactly one child.") + # No List[List] support; we've already declared that in our types + item_type = safe_get(inner_types, 0) + return Type[List[find_models_in_type(item_type)]] + elif type_origin == dict: + # First arg is key which must be primitive + # Second arg is potentially a model + key_value_type = safe_get(inner_types, 1) + value_value_type = safe_get(inner_types, 1) + return Type[Dict[key_value_type, find_models_in_type(value_value_type)]] + else: + return reduce_to_annotations(type_annotation) + + +def schema_to_bare_model(model: Type[BaseModel]) -> Type[BaseModel]: + root_model = reduce_to_annotations(model) + + for key in root_model.__annotations__: + value = root_model.__annotations__.get(key) + value_type = find_models_in_type(value) + root_model.__annotations__[key] = value_type + + return root_model diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index a858a96a2..5f5ec33c2 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -32,7 +32,7 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType -from guardrails.utils.pydantic_utils.common import _create_bare_model +from guardrails.utils.pydantic_utils.common import schema_to_bare_model from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -263,7 +263,7 @@ def convert_pydantic_model_to_openai_fn( schema_model = safe_get(item_types, 0) # Create a bare model with no extra fields - bare_model = _create_bare_model(schema_model) + bare_model = schema_to_bare_model(schema_model) # Convert Pydantic model to JSON schema json_schema = bare_model.schema() diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index cb630310b..f2d191438 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -12,6 +12,7 @@ Type, TypeVar, Union, + cast, get_args, get_origin, ) @@ -32,7 +33,6 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType -from guardrails.utils.pydantic_utils.common import _create_bare_model from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -149,10 +149,10 @@ def convert_pydantic_model_to_openai_fn( # No List[List] support; we've already declared that in our types schema_model = safe_get(item_types, 0) - bare_model = _create_bare_model(schema_model) + schema_model = cast(Type[BaseModel], schema_model) # Convert Pydantic model to JSON schema - json_schema = bare_model.model_json_schema() + json_schema = schema_model.model_json_schema() json_schema["title"] = schema_model.__name__ if type_origin == list: diff --git a/guardrails/validator_base.py b/guardrails/validator_base.py index 225a04eae..2376f990c 100644 --- a/guardrails/validator_base.py +++ b/guardrails/validator_base.py @@ -1,6 +1,7 @@ import inspect from collections import defaultdict from copy import deepcopy +from dataclasses import dataclass from string import Template from typing import ( Any, @@ -231,10 +232,11 @@ class FailResult(ValidationResult): fix_value: Optional[Any] = None +@dataclass class Validator(Runnable): """Base class for validators.""" - rail_alias: str + rail_alias: str = "" run_in_separate_process = False override_value_on_pass = False diff --git a/tests/unit_tests/utils/pydantic_utils/common.py b/tests/unit_tests/utils/pydantic_utils/common.py index 50646f08d..1c3434f26 100644 --- a/tests/unit_tests/utils/pydantic_utils/common.py +++ b/tests/unit_tests/utils/pydantic_utils/common.py @@ -3,7 +3,7 @@ import pydantic from pydantic import BaseModel, Field -from guardrails.utils.pydantic_utils.common import _create_bare_model +from guardrails.utils.pydantic_utils.common import schema_to_bare_model PYDANTIC_VERSION = pydantic.version.VERSION @@ -31,14 +31,14 @@ class Foo(BaseModel): # These tests demonstrate the issue fixed in PR #616 -class TestCreateBareModel: +class TestSchemaToBareModel: def test_with_model_type(self): expected_schema = deepcopy(foo_schema) # When pushed through BareModel it loses the description on any properties. del expected_schema["properties"]["bar"]["description"] # Current logic - bare_model = _create_bare_model(Foo) + bare_model = schema_to_bare_model(Foo) # Convert Pydantic model to JSON schema if PYDANTIC_VERSION.startswith("1"): @@ -57,7 +57,7 @@ def test_with_type_of_model_type(self): empty_schema = {"properties": {}, "title": "BareModel", "type": "object"} # Previous logic - bare_model = _create_bare_model(type(Foo)) + bare_model = schema_to_bare_model(type(Foo)) # Convert Pydantic model to JSON schema if PYDANTIC_VERSION.startswith("1"): From 49b38d5ed4b91fca032f4ef8c7f918fc857f4c37 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 13:45:42 -0500 Subject: [PATCH 06/12] turn off function calling for pydantic 1.x --- guardrails/llm_providers.py | 28 +++--- guardrails/utils/dataclass.py | 8 ++ guardrails/utils/pydantic_utils/v1.py | 139 +++++++++++++++++++------- guardrails/validator_base.py | 4 +- 4 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 guardrails/utils/dataclass.py diff --git a/guardrails/llm_providers.py b/guardrails/llm_providers.py index 0d68956a2..9964085d6 100644 --- a/guardrails/llm_providers.py +++ b/guardrails/llm_providers.py @@ -197,13 +197,15 @@ def _invoke_llm( ) # Configure function calling if applicable (only for non-streaming) + fn_kwargs = {} if base_model and not kwargs.get("stream", False): - function_params = [convert_pydantic_model_to_openai_fn(base_model)] - if function_call is None: - function_call = {"name": function_params[0]["name"]} - fn_kwargs = {"functions": function_params, "function_call": function_call} - else: - fn_kwargs = {} + function_params = convert_pydantic_model_to_openai_fn(base_model) + if function_call is None and function_params: + function_call = {"name": function_params["name"]} + fn_kwargs = { + "functions": [function_params], + "function_call": function_call, + } # Call OpenAI if "api_key" in kwargs: @@ -733,13 +735,15 @@ async def invoke_llm( ) # Configure function calling if applicable + fn_kwargs = {} if base_model: - function_params = [convert_pydantic_model_to_openai_fn(base_model)] - if function_call is None: - function_call = {"name": function_params[0]["name"]} - fn_kwargs = {"functions": function_params, "function_call": function_call} - else: - fn_kwargs = {} + function_params = convert_pydantic_model_to_openai_fn(base_model) + if function_call is None and function_params: + function_call = {"name": function_params["name"]} + fn_kwargs = { + "functions": [function_params], + "function_call": function_call, + } # Call OpenAI if "api_key" in kwargs: diff --git a/guardrails/utils/dataclass.py b/guardrails/utils/dataclass.py new file mode 100644 index 000000000..f2a0591c6 --- /dev/null +++ b/guardrails/utils/dataclass.py @@ -0,0 +1,8 @@ +import pydantic.version + +PYDANTIC_VERSION = pydantic.version.VERSION + +if PYDANTIC_VERSION.startswith("1"): + from pydantic.dataclasses import dataclass # type: ignore +else: + from dataclasses import dataclass # type: ignore # noqa diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index 5f5ec33c2..29d81cb39 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -32,7 +32,6 @@ from guardrails.datatypes import Object as ObjectDataType from guardrails.datatypes import String as StringDataType from guardrails.datatypes import Time as TimeDataType -from guardrails.utils.pydantic_utils.common import schema_to_bare_model from guardrails.utils.safe_get import safe_get from guardrails.validator_base import Validator from guardrails.validatorsattr import ValidatorsAttr @@ -240,6 +239,75 @@ def process_validators(vals, fld): return model_fields +def create_bare_model(model: Type[BaseModel]): + class BareModel(BaseModel): + __annotations__ = getattr(model, "__annotations__", {}) + + return BareModel + + +def reduce_to_annotations(type_annotation: Any) -> Type[Any]: + if ( + type_annotation + and isinstance(type_annotation, type) + and issubclass(type_annotation, BaseModel) + ): + return create_bare_model(type_annotation) + return type_annotation + + +def find_models_in_type(type_annotation: Any) -> Type[Any]: + type_origin = get_origin(type_annotation) + inner_types = get_args(type_annotation) + if type_origin == Union: + data_types = tuple([find_models_in_type(t) for t in inner_types]) + return Type[Union[data_types]] # type: ignore + elif type_origin == list: + if len(inner_types) > 1: + raise ValueError("List data type must have exactly one child.") + # No List[List] support; we've already declared that in our types + item_type = safe_get(inner_types, 0) + return Type[List[find_models_in_type(item_type)]] + elif type_origin == dict: + # First arg is key which must be primitive + # Second arg is potentially a model + key_value_type = safe_get(inner_types, 1) + value_value_type = safe_get(inner_types, 1) + return Type[Dict[key_value_type, find_models_in_type(value_value_type)]] + else: + return reduce_to_annotations(type_annotation) + + +def schema_to_bare_model(model: Type[BaseModel]) -> Type[BaseModel]: + copy = deepcopy(model) + for field_key in copy.__fields__: + field = copy.__fields__.get(field_key) + if field: + extras = field.field_info.extra + if "validators" in extras: + extras["format"] = list( + v.to_prompt() + for v in extras.pop("validators", []) + if hasattr(v, "to_prompt") + ) + + field.field_info.extra = extras + + value_type = find_models_in_type(field.annotation) + field.annotation = value_type + copy.__fields__[field_key] = field + + # root_model = reduce_to_annotations(model) + + # for key in root_model.__annotations__: + # value = root_model.__annotations__.get(key) + # print("value.field_info: ", value.field_info) + # value_type = find_models_in_type(value) + # root_model.__annotations__[key] = value_type + + return copy + + def convert_pydantic_model_to_openai_fn( model: Union[Type[BaseModel], Type[List[Type[BaseModel]]]] ) -> Dict: @@ -251,40 +319,41 @@ def convert_pydantic_model_to_openai_fn( Returns: OpenAI function paramters. """ - - schema_model = model - - type_origin = get_origin(model) - if type_origin == list: - item_types = get_args(model) - if len(item_types) > 1: - raise ValueError("List data type must have exactly one child.") - # No List[List] support; we've already declared that in our types - schema_model = safe_get(item_types, 0) - - # Create a bare model with no extra fields - bare_model = schema_to_bare_model(schema_model) - - # Convert Pydantic model to JSON schema - json_schema = bare_model.schema() - json_schema["title"] = schema_model.__name__ - - if type_origin == list: - json_schema = { - "title": f"List<{json_schema.get('title')}>", - "type": "array", - "items": json_schema, - } - - # Create OpenAI function parameters - fn_params = { - "name": json_schema["title"], - "parameters": json_schema, - } - if "description" in json_schema and json_schema["description"] is not None: - fn_params["description"] = json_schema["description"] - - return fn_params + return {} + + # schema_model = model + + # type_origin = get_origin(model) + # if type_origin == list: + # item_types = get_args(model) + # if len(item_types) > 1: + # raise ValueError("List data type must have exactly one child.") + # # No List[List] support; we've already declared that in our types + # schema_model = safe_get(item_types, 0) + + # # Create a bare model with no extra fields + # bare_model = schema_to_bare_model(schema_model) + + # # Convert Pydantic model to JSON schema + # json_schema = bare_model.schema() + # json_schema["title"] = schema_model.__name__ + + # if type_origin == list: + # json_schema = { + # "title": f"List<{json_schema.get('title')}>", + # "type": "array", + # "items": json_schema, + # } + + # # Create OpenAI function parameters + # fn_params = { + # "name": json_schema["title"], + # "parameters": json_schema, + # } + # if "description" in json_schema and json_schema["description"] is not None: + # fn_params["description"] = json_schema["description"] + + # return fn_params def field_to_datatype(field: Union[ModelField, Type]) -> Type[DataType]: diff --git a/guardrails/validator_base.py b/guardrails/validator_base.py index 2376f990c..d78571692 100644 --- a/guardrails/validator_base.py +++ b/guardrails/validator_base.py @@ -1,7 +1,6 @@ import inspect from collections import defaultdict from copy import deepcopy -from dataclasses import dataclass from string import Template from typing import ( Any, @@ -23,6 +22,7 @@ from guardrails.classes import InputType from guardrails.constants import hub from guardrails.errors import ValidationError +from guardrails.utils.dataclass import dataclass class Filter: @@ -232,7 +232,7 @@ class FailResult(ValidationResult): fix_value: Optional[Any] = None -@dataclass +@dataclass # type: ignore class Validator(Runnable): """Base class for validators.""" From 96d3e542e6f5186adacca72e31d62b65ec7fd24c Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 16:33:25 -0500 Subject: [PATCH 07/12] remove common since it's no longer needed --- guardrails/utils/pydantic_utils/common.py | 55 ----------------------- 1 file changed, 55 deletions(-) delete mode 100644 guardrails/utils/pydantic_utils/common.py diff --git a/guardrails/utils/pydantic_utils/common.py b/guardrails/utils/pydantic_utils/common.py deleted file mode 100644 index 8df42d507..000000000 --- a/guardrails/utils/pydantic_utils/common.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Any, Dict, List, Type, Union, get_args, get_origin - -from pydantic import BaseModel - -from guardrails.utils.safe_get import safe_get - - -def create_bare_model(model: Type[BaseModel]): - class BareModel(BaseModel): - __annotations__ = getattr(model, "__annotations__", {}) - - return BareModel - - -def reduce_to_annotations(type_annotation: Any) -> Type[Any]: - if ( - type_annotation - and isinstance(type_annotation, type) - and issubclass(type_annotation, BaseModel) - ): - return create_bare_model(type_annotation) - return type_annotation - - -def find_models_in_type(type_annotation: Any) -> Type[Any]: - type_origin = get_origin(type_annotation) - inner_types = get_args(type_annotation) - if type_origin == Union: - data_types = tuple([find_models_in_type(t) for t in inner_types]) - return Type[Union[data_types]] # type: ignore - elif type_origin == list: - if len(inner_types) > 1: - raise ValueError("List data type must have exactly one child.") - # No List[List] support; we've already declared that in our types - item_type = safe_get(inner_types, 0) - return Type[List[find_models_in_type(item_type)]] - elif type_origin == dict: - # First arg is key which must be primitive - # Second arg is potentially a model - key_value_type = safe_get(inner_types, 1) - value_value_type = safe_get(inner_types, 1) - return Type[Dict[key_value_type, find_models_in_type(value_value_type)]] - else: - return reduce_to_annotations(type_annotation) - - -def schema_to_bare_model(model: Type[BaseModel]) -> Type[BaseModel]: - root_model = reduce_to_annotations(model) - - for key in root_model.__annotations__: - value = root_model.__annotations__.get(key) - value_type = find_models_in_type(value) - root_model.__annotations__[key] = value_type - - return root_model From 681b22afd1a511ccc24f6a56e710e12ce556abce Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 16:37:21 -0500 Subject: [PATCH 08/12] remove test for deleted file --- .../unit_tests/utils/pydantic_utils/common.py | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 tests/unit_tests/utils/pydantic_utils/common.py diff --git a/tests/unit_tests/utils/pydantic_utils/common.py b/tests/unit_tests/utils/pydantic_utils/common.py deleted file mode 100644 index 1c3434f26..000000000 --- a/tests/unit_tests/utils/pydantic_utils/common.py +++ /dev/null @@ -1,69 +0,0 @@ -from copy import deepcopy - -import pydantic -from pydantic import BaseModel, Field - -from guardrails.utils.pydantic_utils.common import schema_to_bare_model - -PYDANTIC_VERSION = pydantic.version.VERSION - - -class Foo(BaseModel): - bar: str = Field(description="some string value") - - -# fmt: off -foo_schema = { - "title": "Foo", - "type": "object", - "properties": { - "bar": { - "title": "Bar", - "description": "some string value", - "type": "string" - } - }, - "required": [ - "bar" - ] -} -# fmt: on - - -# These tests demonstrate the issue fixed in PR #616 -class TestSchemaToBareModel: - def test_with_model_type(self): - expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] - - # Current logic - bare_model = schema_to_bare_model(Foo) - - # Convert Pydantic model to JSON schema - if PYDANTIC_VERSION.startswith("1"): - json_schema = bare_model.schema() - else: - json_schema = bare_model.model_json_schema() - json_schema["title"] = Foo.__name__ - - assert json_schema == expected_schema - - def test_with_type_of_model_type(self): - expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] - - empty_schema = {"properties": {}, "title": "BareModel", "type": "object"} - - # Previous logic - bare_model = schema_to_bare_model(type(Foo)) - - # Convert Pydantic model to JSON schema - if PYDANTIC_VERSION.startswith("1"): - json_schema = bare_model.schema() - else: - json_schema = bare_model.model_json_schema() - - assert json_schema != expected_schema - assert json_schema == empty_schema From 089f3b5005ee37925c717040c8ebfae4c80aa4f2 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 16:47:29 -0500 Subject: [PATCH 09/12] update tests --- tests/unit_tests/utils/pydantic_utils/v1.py | 9 +++++++-- tests/unit_tests/utils/pydantic_utils/v2.py | 4 ---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/utils/pydantic_utils/v1.py b/tests/unit_tests/utils/pydantic_utils/v1.py index 0c25c1cac..8ec643c52 100644 --- a/tests/unit_tests/utils/pydantic_utils/v1.py +++ b/tests/unit_tests/utils/pydantic_utils/v1.py @@ -1,5 +1,6 @@ from copy import deepcopy from typing import List +from warnings import warn import pydantic.version import pytest @@ -52,7 +53,9 @@ def test_object_schema(self): actual_fn_params = convert_pydantic_model_to_openai_fn(Foo) - assert actual_fn_params == expected_fn_params + # assert actual_fn_params == expected_fn_params + warn("Function calling is disabled for pydantic 1.x") + assert actual_fn_params == {} def test_list_schema(self): expected_schema = deepcopy(foo_schema) @@ -76,4 +79,6 @@ def test_list_schema(self): actual_fn_params = convert_pydantic_model_to_openai_fn(List[Foo]) - assert actual_fn_params == expected_fn_params + # assert actual_fn_params == expected_fn_params + warn("Function calling is disabled for pydantic 1.x") + assert actual_fn_params == {} diff --git a/tests/unit_tests/utils/pydantic_utils/v2.py b/tests/unit_tests/utils/pydantic_utils/v2.py index 593050434..d5c66677f 100644 --- a/tests/unit_tests/utils/pydantic_utils/v2.py +++ b/tests/unit_tests/utils/pydantic_utils/v2.py @@ -40,8 +40,6 @@ class Foo(BaseModel): class TestConvertPydanticModelToOpenaiFn: def test_object_schema(self): expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] # fmt: off expected_fn_params = { @@ -56,8 +54,6 @@ def test_object_schema(self): def test_list_schema(self): expected_schema = deepcopy(foo_schema) - # When pushed through BareModel it loses the description on any properties. - del expected_schema["properties"]["bar"]["description"] # fmt: off expected_schema = { From 950f579f06dc4f8f5f87588c2406d9be9d1b185d Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 17:13:40 -0500 Subject: [PATCH 10/12] stub dataclass for pydantic 1.x --- guardrails/utils/dataclass.py | 4 +++- tests/unit_tests/utils/pydantic_utils/v1.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/guardrails/utils/dataclass.py b/guardrails/utils/dataclass.py index f2a0591c6..49f6d98cb 100644 --- a/guardrails/utils/dataclass.py +++ b/guardrails/utils/dataclass.py @@ -3,6 +3,8 @@ PYDANTIC_VERSION = pydantic.version.VERSION if PYDANTIC_VERSION.startswith("1"): - from pydantic.dataclasses import dataclass # type: ignore + def dataclass(cls): # type: ignore + return cls + else: from dataclasses import dataclass # type: ignore # noqa diff --git a/tests/unit_tests/utils/pydantic_utils/v1.py b/tests/unit_tests/utils/pydantic_utils/v1.py index 8ec643c52..a80c9b5cb 100644 --- a/tests/unit_tests/utils/pydantic_utils/v1.py +++ b/tests/unit_tests/utils/pydantic_utils/v1.py @@ -45,7 +45,7 @@ def test_object_schema(self): del expected_schema["properties"]["bar"]["description"] # fmt: off - expected_fn_params = { + expected_fn_params = { # noqa "name": "Foo", "parameters": expected_schema } @@ -71,7 +71,7 @@ def test_list_schema(self): # fmt: on # fmt: off - expected_fn_params = { + expected_fn_params = { # noqa "name": "List", "parameters": expected_schema } From cf3952ebe9803513d2154b971a1492075aeae9af Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 17:22:52 -0500 Subject: [PATCH 11/12] lint fix --- guardrails/utils/dataclass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/guardrails/utils/dataclass.py b/guardrails/utils/dataclass.py index 49f6d98cb..92bdb81ce 100644 --- a/guardrails/utils/dataclass.py +++ b/guardrails/utils/dataclass.py @@ -3,6 +3,7 @@ PYDANTIC_VERSION = pydantic.version.VERSION if PYDANTIC_VERSION.startswith("1"): + def dataclass(cls): # type: ignore return cls From 82500dae114241a5171fcb738902cab486b51a29 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Wed, 13 Mar 2024 17:55:03 -0500 Subject: [PATCH 12/12] title List -> Array --- guardrails/utils/pydantic_utils/v1.py | 2 +- guardrails/utils/pydantic_utils/v2.py | 2 +- tests/unit_tests/utils/pydantic_utils/v1.py | 4 ++-- tests/unit_tests/utils/pydantic_utils/v2.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guardrails/utils/pydantic_utils/v1.py b/guardrails/utils/pydantic_utils/v1.py index 29d81cb39..9e29466af 100644 --- a/guardrails/utils/pydantic_utils/v1.py +++ b/guardrails/utils/pydantic_utils/v1.py @@ -340,7 +340,7 @@ def convert_pydantic_model_to_openai_fn( # if type_origin == list: # json_schema = { - # "title": f"List<{json_schema.get('title')}>", + # "title": f"Array<{json_schema.get('title')}>", # "type": "array", # "items": json_schema, # } diff --git a/guardrails/utils/pydantic_utils/v2.py b/guardrails/utils/pydantic_utils/v2.py index f2d191438..5c135caca 100644 --- a/guardrails/utils/pydantic_utils/v2.py +++ b/guardrails/utils/pydantic_utils/v2.py @@ -157,7 +157,7 @@ def convert_pydantic_model_to_openai_fn( if type_origin == list: json_schema = { - "title": f"List<{json_schema.get('title')}>", + "title": f"Array<{json_schema.get('title')}>", "type": "array", "items": json_schema, } diff --git a/tests/unit_tests/utils/pydantic_utils/v1.py b/tests/unit_tests/utils/pydantic_utils/v1.py index a80c9b5cb..a9322c8d2 100644 --- a/tests/unit_tests/utils/pydantic_utils/v1.py +++ b/tests/unit_tests/utils/pydantic_utils/v1.py @@ -64,7 +64,7 @@ def test_list_schema(self): # fmt: off expected_schema = { - "title": f"List<{expected_schema.get('title')}>", + "title": f"Array<{expected_schema.get('title')}>", "type": "array", "items": expected_schema } @@ -72,7 +72,7 @@ def test_list_schema(self): # fmt: off expected_fn_params = { # noqa - "name": "List", + "name": "Array", "parameters": expected_schema } # fmt: on diff --git a/tests/unit_tests/utils/pydantic_utils/v2.py b/tests/unit_tests/utils/pydantic_utils/v2.py index d5c66677f..32f6e98d9 100644 --- a/tests/unit_tests/utils/pydantic_utils/v2.py +++ b/tests/unit_tests/utils/pydantic_utils/v2.py @@ -57,7 +57,7 @@ def test_list_schema(self): # fmt: off expected_schema = { - "title": f"List<{expected_schema.get('title')}>", + "title": f"Array<{expected_schema.get('title')}>", "type": "array", "items": expected_schema } @@ -65,7 +65,7 @@ def test_list_schema(self): # fmt: off expected_fn_params = { - "name": "List", + "name": "Array", "parameters": expected_schema } # fmt: on