From 867a4bb3b546b4355823f2407d57d5fa9f7a3535 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:18:38 -0700 Subject: [PATCH 01/19] Add function call signatures --- gpt_json/exceptions.py | 23 ++++++ gpt_json/fn_calling.py | 125 ++++++++++++++++++++++++++++++ gpt_json/gpt.py | 115 +++++++++++++++++++++++---- gpt_json/models.py | 4 +- gpt_json/tests/test_fn_calling.py | 80 +++++++++++++++++++ gpt_json/tests/test_gpt.py | 25 +++--- 6 files changed, 340 insertions(+), 32 deletions(-) create mode 100644 gpt_json/fn_calling.py create mode 100644 gpt_json/tests/test_fn_calling.py diff --git a/gpt_json/exceptions.py b/gpt_json/exceptions.py index 3f8351b..0bcfd6d 100644 --- a/gpt_json/exceptions.py +++ b/gpt_json/exceptions.py @@ -3,3 +3,26 @@ class UnexpectedGPTResponse(Exception): Raised when the GPT response does not conform to the expected json schema """ + + +class InvalidFunctionResponse(Exception): + """ + GPT passed an invalid function name back to the caller + + """ + + def __init__(self, invalid_function_name: str): + super().__init__(f"Invalid function name: {invalid_function_name}") + self.invalid_function_name = invalid_function_name + + +class InvalidFunctionParameters(Exception): + """ + GPT passed invalid function parameters back to the caller + + """ + + def __init__(self, invalid_function_name: str, invalid_parameters: str): + super().__init__(f"Invalid function parameters: {invalid_parameters}") + self.invalid_function_name = invalid_function_name + self.invalid_parameters = invalid_parameters diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py new file mode 100644 index 0000000..78ea1eb --- /dev/null +++ b/gpt_json/fn_calling.py @@ -0,0 +1,125 @@ +from dataclasses import dataclass +from enum import Enum +from inspect import getdoc, signature +from types import UnionType +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Type, + Union, + get_args, + get_origin, +) + +from pydantic import BaseModel + + +@dataclass +class FunctionResponse: + caller_content: str + internal_context: Any + + +def function_to_name(fn: Callable) -> str: + return fn.__name__ + + +def get_request_from_function(fn: Callable) -> Type[BaseModel]: + """ + Parse the pydantic schema that is used as the only argument + for the functions. + + """ + # Parse the model inputs + parameters = list(signature(fn).parameters.values()) + + # Determine if we only have one parameter + if len(parameters) != 1: + raise ValueError("Only one Pydantic object is allowed to LLM function") + + # Get the parameter type + parameter_type = parameters[0].annotation + if not issubclass(parameter_type, BaseModel): + raise ValueError("Only Pydantic objects are allowed to LLM function") + + return parameter_type + + +def parse_function(fn: Callable) -> Dict[str, Any]: + """ + Parse a python function into a JSON schema that can be used by OpenAPI. We use + the first line of the docstring as the description, and the rest of the docstring + is ignored. We also assume that functions only have one parameter, which is a Pydantic + model with the required typehints and description strings. + + API Reference: https://platform.openai.com/docs/api-reference/chat/create + + """ + docstring = getdoc(fn) or "" + lines = docstring.strip().split("\n") + description = lines[0] if lines else None + + parameter_type = get_request_from_function(fn) + + # Parse the parameter type into a JSON schema + parameter_schema = model_to_parameter_schema(parameter_type) + print("parameter_schema", parameter_schema) + return { + "name": function_to_name(fn), + "description": description, + "parameters": parameter_schema, + } + + +def get_base_type(field_type): + origin = get_origin(field_type) + args = get_args(field_type) + + if isinstance(field_type, UnionType): + non_none_types = [t for t in field_type.__args__ if t is not type(None)] + if len(non_none_types) == 1: + return non_none_types[0] + elif len(non_none_types) > 1: + raise ValueError("Polymorphic types not supported") + elif origin is Union: + non_none_args = [arg for arg in args if arg is not type(None)] + if len(non_none_args) == 1: + # We've got only one non-None type in the Union + return non_none_args[0] + elif len(non_none_args) > 1: + raise ValueError("We don't support polymorphic types") + elif origin is Optional: + return args[0] + + return field_type + + +def resolve_refs(schema, defs=None): + if defs is None: + defs = schema.get("$defs", {}) + + if isinstance(schema, dict): + if "$ref" in schema: + ref_key = schema["$ref"].split("/")[ + -1 + ] # assuming $ref format is like '#/$defs/UnitType' + return resolve_refs(defs[ref_key], defs) + + return {k: resolve_refs(v, defs) for k, v in schema.items()} + + if isinstance(schema, list): + return [resolve_refs(item, defs) for item in schema] + + return schema + + +def model_to_parameter_schema(model: Type[BaseModel]) -> Dict[str, Any]: + formatted_json = resolve_refs(model.model_json_schema()) + return { + "type": "object", + "properties": formatted_json["properties"], + "required": formatted_json["required"], + } diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index 5378471..cf3cc06 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -7,6 +7,7 @@ from typing import ( Any, AsyncIterator, + Callable, Generic, Type, TypeVar, @@ -19,9 +20,15 @@ import openai from openai.error import APIConnectionError, RateLimitError from openai.error import Timeout as OpenAITimeout -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError from tiktoken import encoding_for_model +from gpt_json.exceptions import InvalidFunctionParameters, InvalidFunctionResponse +from gpt_json.fn_calling import ( + function_to_name, + get_request_from_function, + parse_function, +) from gpt_json.generics import resolve_generic_model from gpt_json.models import ( FixTransforms, @@ -72,6 +79,17 @@ class ListResponse(Generic[SchemaType], BaseModel): ) +class RunResponse(Generic[SchemaType], BaseModel): + """ + Helper schema to wrap a single response alongside the extracted metadata + """ + + response: SchemaType | None + fix_transforms: FixTransforms | None + function_call: Callable[..., BaseModel] | None + function_arg: BaseModel | None + + class GPTJSON(Generic[SchemaType]): """ A wrapper over GPT that provides basic JSON parsing and response handling. @@ -87,6 +105,7 @@ def __init__( model: GPTModelVersion | str = GPTModelVersion.GPT_4, auto_trim: bool = False, auto_trim_response_overhead: int = 0, + functions: list[Callable[..., BaseModel]] | None = None, # For messages that are relatively deterministic temperature=0.2, timeout: int | None = None, @@ -112,6 +131,7 @@ def __init__( self.openai_max_retries = openai_max_retries self.openai_arguments = kwargs self.schema_model = self._cls_schema_model + self.functions = {function_to_name(fn): fn for fn in (functions or [])} self.__class__._cls_schema_model = None if not self.schema_model: @@ -162,7 +182,8 @@ async def run( max_response_tokens: int | None = None, format_variables: dict[str, Any] | None = None, truncation_options: TruncationOptions | None = None, - ) -> tuple[SchemaType, FixTransforms] | tuple[None, None]: + allow_functions: bool = True, + ) -> RunResponse[SchemaType]: """ :param messages: List of GPTMessage objects to send to the API :param max_response_tokens: Maximum number of tokens allowed in the response @@ -189,16 +210,61 @@ async def run( )(self.submit_request) response = await backoff_request_submission( - messages, max_response_tokens=max_response_tokens + messages, + max_response_tokens=max_response_tokens, + allow_functions=allow_functions, ) + logger.debug("------- RAW RESPONSE ----------") logger.debug(response["choices"]) logger.debug("------- END RAW RESPONSE ----------") - extracted_json, fixed_payload = self.extract_json(response, self.extract_type) + + # If the response requests a function call, prefer this over the main response + response_message = self.extract_response_message(response) + if response_message is None: + return RunResponse( + response=None, + fix_transforms=None, + function_call=None, + function_arg=None, + ) + + if response_message.get("function_call"): + function_name = response_message["function_call"]["name"] + function_args_string = response_message["function_call"]["arguments"] + if function_name not in self.functions: + raise InvalidFunctionResponse(function_name) + + function_call = self.functions[function_name] + function_request_model = get_request_from_function(function_call) + + # Parameters are formatted as raw json strings + try: + function_parsed = function_request_model.model_validate_json( + function_args_string + ) + except (ValueError, ValidationError): + raise InvalidFunctionParameters(function_name, function_args_string) + + return RunResponse( + response=None, + fix_transforms=None, + function_call=function_call, + function_arg=function_parsed, + ) + + extracted_json, fixed_payload = self.extract_json( + response_message, self.extract_type + ) # Cast to schema model if extracted_json is None: - return None, None + return RunResponse( + response=None, + fix_transforms=fixed_payload, + function_call=None, + function_arg=None, + ) if not self.schema_model: raise ValueError( @@ -206,7 +272,12 @@ async def run( ) # Allow pydantic to handle the validation - return self.schema_model(**extracted_json), fixed_payload + return RunResponse( + response=self.schema_model(**extracted_json), + fix_transforms=fixed_payload, + function_call=None, + function_arg=None, + ) async def stream( self, @@ -246,7 +317,10 @@ async def stream( )(self.submit_request) raw_responses = await backoff_request_submission( - messages, max_response_tokens=max_response_tokens, stream=True + messages, + max_response_tokens=max_response_tokens, + stream=True, + allow_functions=False, ) previous_partial = None @@ -285,17 +359,12 @@ async def stream( yield partial_response previous_partial = partial_response - def extract_json(self, completion_response, extract_type: ResponseType): + def extract_json(self, response_message, extract_type: ResponseType): """ Assumes one main block of results, either list of dictionary """ - choices = completion_response["choices"] - - if not choices: - logger.warning("No choices available, should report error...") - return None, None - full_response = choices[0]["message"]["content"] + full_response = response_message["content"] extracted_response = find_json_response(full_response, extract_type) if extracted_response is None: @@ -319,10 +388,20 @@ def extract_json(self, completion_response, extract_type: ResponseType): logger.error(f"JSON decode error, likely malformed json input: {e}") return None, fixed_payload + def extract_response_message(self, completion_response): + choices = completion_response["choices"] + + if not choices: + logger.warning("No choices available, should report error...") + return None + + return choices[0]["message"] + async def submit_request( self, messages: list[GPTMessage], max_response_tokens: int | None, + allow_functions: bool, stream: bool = False, ): """ @@ -335,11 +414,17 @@ async def submit_request( if self.auto_trim: messages = self.trim_messages(messages, self.max_tokens) - optional_parameters = {} + optional_parameters: dict[str, Any] = {} if max_response_tokens: optional_parameters["max_tokens"] = max_response_tokens + if allow_functions and self.functions: + optional_parameters["functions"] = [ + parse_function(fn) for fn in self.functions.values() + ] + optional_parameters["function_call"] = "auto" + execute_prediction = openai.ChatCompletion.acreate( model=self.model, messages=[self.message_to_dict(message) for message in messages], diff --git a/gpt_json/models.py b/gpt_json/models.py index 9064864..09596d1 100644 --- a/gpt_json/models.py +++ b/gpt_json/models.py @@ -34,8 +34,8 @@ class GPTMessageRole(EnumSuper): @unique class GPTModelVersion(EnumSuper): - GPT_3_5 = "gpt-3.5-turbo" - GPT_4 = "gpt-4-0314" + GPT_3_5 = "gpt-3.5-turbo-0613" + GPT_4 = "gpt-4-0613" @unique diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py new file mode 100644 index 0000000..5612067 --- /dev/null +++ b/gpt_json/tests/test_fn_calling.py @@ -0,0 +1,80 @@ +from enum import Enum +from typing import Callable, Optional, Union + +import pytest +from pydantic import BaseModel, Field + +from gpt_json.fn_calling import get_base_type, parse_function + + +class UnitType(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + + +class GetCurrentWeatherRequest(BaseModel): + location: str = Field(description="The city and state, e.g. San Francisco, CA") + unit: UnitType | None = None + + +def get_current_weather(request: GetCurrentWeatherRequest): + """ + Get the current weather in a given location + + The rest of the docstring should be omitted. + """ + + +def get_weather_additional_args(request: GetCurrentWeatherRequest, other_args: str): + pass + + +def get_weather_no_pydantic(other_args: str): + pass + + +@pytest.mark.parametrize( + "incorrect_fn", + [ + get_weather_additional_args, + get_weather_no_pydantic, + ], +) +def test_parse_function_incorrect_args(incorrect_fn: Callable): + with pytest.raises(ValueError): + parse_function(incorrect_fn) + + +def test_get_base_type(): + assert get_base_type(UnitType | None) == UnitType + assert get_base_type(Optional[UnitType]) == UnitType + assert get_base_type(Union[UnitType, None]) == UnitType + + +def test_parse_function(): + """ + Assert the formatted schema conforms to the expected JSON-Schema / GPT format. + """ + parse_function(get_current_weather) == { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + 'unit': { + 'anyOf': [ + { + 'enum': ['celsius', 'fahrenheit'], 'title': 'UnitType', 'type': 'string' + }, + {'type': 'null'} + ], + 'default': None + }, + }, + "required": ["location"], + }, + } diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index af9f43e..f497f08 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -173,7 +173,7 @@ async def test_acreate( mock_acreate.return_value = mock_response # Call the function and pass the expected parameters - response, transformations = await model.run(messages=messages) + response = await model.run(messages=messages) # Assert that the mock function was called with the expected parameters mock_acreate.assert_called_with( @@ -191,8 +191,9 @@ async def test_acreate( ) assert response - assert response.dict() == parsed.dict() - assert transformations == expected_transformations + assert response.response + assert response.response.dict() == parsed.dict() + assert response.fix_transforms == expected_transformations @pytest.mark.parametrize( @@ -287,10 +288,8 @@ async def test_extracted_json_is_None(): ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): - result, _ = await gpt.run( - [GPTMessage(GPTMessageRole.SYSTEM, "message content")] - ) - assert result is None + result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + assert result.response is None @pytest.mark.asyncio @@ -298,10 +297,8 @@ async def test_no_valid_results_from_remote_request(): gpt = GPTJSON[MySchema](None) with patch.object(gpt, "submit_request", return_value={"choices": []}): - result, _ = await gpt.run( - [GPTMessage(GPTMessageRole.SYSTEM, "message content")] - ) - assert result is None + result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + assert result.response is None @pytest.mark.asyncio @@ -315,10 +312,8 @@ async def test_unable_to_find_valid_json_payload(): ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): - result, _ = await gpt.run( - [GPTMessage(GPTMessageRole.SYSTEM, "message content")] - ) - assert result is None + result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + assert result.response is None @pytest.mark.asyncio From a056eea8c12403f091a7905eaecdb8486992bb3a Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:18:48 -0700 Subject: [PATCH 02/19] Upgrade basic examples --- examples/function_example.py | 62 +++++++++++ examples/hint_example.py | 4 +- examples/poetry.lock | 198 ++++++++++++++++++++++++--------- examples/template_example.py | 6 +- examples/truncation_example.py | 4 +- 5 files changed, 217 insertions(+), 57 deletions(-) create mode 100644 examples/function_example.py diff --git a/examples/function_example.py b/examples/function_example.py new file mode 100644 index 0000000..f02f7bf --- /dev/null +++ b/examples/function_example.py @@ -0,0 +1,62 @@ +import asyncio +from enum import Enum +from json import dumps as json_dumps +from os import getenv + +from dotenv import load_dotenv +from pydantic import BaseModel, Field + +from gpt_json import GPTJSON, GPTMessage, GPTMessageRole + +load_dotenv() +API_KEY = getenv("OPENAI_API_KEY") + + +class UnitType(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + + +class GetCurrentWeatherRequest(BaseModel): + location: str = Field(description="The city and state, e.g. San Francisco, CA") + unit: UnitType | None = None + + +class DataPayload(BaseModel): + data: str + + +def get_current_weather(request: GetCurrentWeatherRequest): + """ + Get the current weather in a given location + + The rest of the docstring should be omitted. + """ + weather_info = { + "location": request.location, + "temperature": "72", + "unit": request.unit, + "forecast": ["sunny", "windy"], + } + return json_dumps(weather_info) + + +async def runner(): + gpt_json = GPTJSON[DataPayload](API_KEY, functions=[get_current_weather]) + response = await gpt_json.run( + messages=[ + GPTMessage( + role=GPTMessageRole.USER, + content="What's the weather like in Boston, in F?", + ), + ], + ) + + print(response) + assert response.function_call == get_current_weather + assert response.function_arg == GetCurrentWeatherRequest( + location="Boston", unit=UnitType.FAHRENHEIT + ) + + +asyncio.run(runner()) diff --git a/examples/hint_example.py b/examples/hint_example.py index 1ac9807..a2d39de 100644 --- a/examples/hint_example.py +++ b/examples/hint_example.py @@ -37,8 +37,8 @@ async def runner(): ), ] ) - print(response) - print(f"Detected sentiment: {response.sentiment}") + print(response.response) + print(f"Detected sentiment: {response.response.sentiment}") asyncio.run(runner()) diff --git a/examples/poetry.lock b/examples/poetry.lock index 17003d3..315bbd7 100644 --- a/examples/poetry.lock +++ b/examples/poetry.lock @@ -29,6 +29,14 @@ python-versions = ">=3.7" [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "async-timeout" version = "4.0.2" @@ -94,17 +102,17 @@ python-versions = ">=3.7" [[package]] name = "gpt-json" -version = "0.1.0" -description = "" +version = "0.3.0" +description = "Structured and typehinted GPT responses in Python." category = "main" optional = false -python-versions = "^3.10" +python-versions = "^3.11" develop = true [package.dependencies] backoff = "^2.2.1" openai = "^0.27.6" -pydantic = "^1.10.7" +pydantic = ">1.10.7, <3.0.0" tiktoken = "^0.3.3" [package.source] @@ -162,18 +170,30 @@ wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1 [[package]] name = "pydantic" -version = "1.10.7" -description = "Data validation and settings management using python type hints" +version = "2.3.0" +description = "Data validation using Python type hints" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.6.3" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.6.3" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "python-dotenv" @@ -246,7 +266,7 @@ telegram = ["requests"] [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -279,8 +299,8 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" -python-versions = "^3.10" -content-hash = "5047746b8f3f80dafd6ae709ab7fb319e4d3148576cac092591455d2f131736d" +python-versions = "^3.11" +content-hash = "4410f950a683a79793fc2ccdc9c44ec96b9f0ee7b7801f5e0c78c6af61c2c3ff" [metadata.files] aiohttp = [ @@ -376,6 +396,10 @@ aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] +annotated-types = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, @@ -639,42 +663,116 @@ openai = [ {file = "openai-0.27.6.tar.gz", hash = "sha256:63ca9f6ac619daef8c1ddec6d987fe6aa1c87a9bfdce31ff253204d077222375"}, ] pydantic = [ - {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, - {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, - {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, - {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, - {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, - {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, - {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, - {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, - {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, - {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, + {file = "pydantic-2.3.0-py3-none-any.whl", hash = "sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81"}, + {file = "pydantic-2.3.0.tar.gz", hash = "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d"}, +] +pydantic-core = [ + {file = "pydantic_core-2.6.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad"}, + {file = "pydantic_core-2.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb"}, + {file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd"}, + {file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e"}, + {file = "pydantic_core-2.6.3-cp310-none-win32.whl", hash = "sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7"}, + {file = "pydantic_core-2.6.3-cp310-none-win_amd64.whl", hash = "sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad"}, + {file = "pydantic_core-2.6.3-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973"}, + {file = "pydantic_core-2.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b"}, + {file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6"}, + {file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50"}, + {file = "pydantic_core-2.6.3-cp311-none-win32.whl", hash = "sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8"}, + {file = "pydantic_core-2.6.3-cp311-none-win_amd64.whl", hash = "sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950"}, + {file = "pydantic_core-2.6.3-cp311-none-win_arm64.whl", hash = "sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2"}, + {file = "pydantic_core-2.6.3-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5"}, + {file = "pydantic_core-2.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282"}, + {file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d"}, + {file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa"}, + {file = "pydantic_core-2.6.3-cp312-none-win32.whl", hash = "sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1"}, + {file = "pydantic_core-2.6.3-cp312-none-win_amd64.whl", hash = "sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881"}, + {file = "pydantic_core-2.6.3-cp312-none-win_arm64.whl", hash = "sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6"}, + {file = "pydantic_core-2.6.3-cp37-none-win32.whl", hash = "sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b"}, + {file = "pydantic_core-2.6.3-cp37-none-win_amd64.whl", hash = "sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525"}, + {file = "pydantic_core-2.6.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170"}, + {file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec"}, + {file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb"}, + {file = "pydantic_core-2.6.3-cp38-none-win32.whl", hash = "sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc"}, + {file = "pydantic_core-2.6.3-cp38-none-win_amd64.whl", hash = "sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378"}, + {file = "pydantic_core-2.6.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465"}, + {file = "pydantic_core-2.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3"}, + {file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76"}, + {file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef"}, + {file = "pydantic_core-2.6.3-cp39-none-win32.whl", hash = "sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a"}, + {file = "pydantic_core-2.6.3-cp39-none-win_amd64.whl", hash = "sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149"}, + {file = "pydantic_core-2.6.3.tar.gz", hash = "sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7"}, ] python-dotenv = [ {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, @@ -810,8 +908,8 @@ tqdm = [ {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, ] typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] urllib3 = [ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, diff --git a/examples/template_example.py b/examples/template_example.py index f48e7f7..608b3de 100644 --- a/examples/template_example.py +++ b/examples/template_example.py @@ -23,7 +23,7 @@ class QuoteSchema(BaseModel): async def runner(): gpt_json = GPTJSON[QuoteSchema](API_KEY) - response, _ = await gpt_json.run( + response = await gpt_json.run( messages=[ GPTMessage( role=GPTMessageRole.SYSTEM, @@ -33,8 +33,8 @@ async def runner(): format_variables={"sentiment": "happy", "max_items": 5}, ) - print(response) - print(f"Quotes: {response.quotes}") + print(response.response) + print(f"Quotes: {response.response.quotes}") asyncio.run(runner()) diff --git a/examples/truncation_example.py b/examples/truncation_example.py index f46ea10..903f6f2 100644 --- a/examples/truncation_example.py +++ b/examples/truncation_example.py @@ -53,7 +53,7 @@ def few_shot_truncate_next( return "\n".join(text.split("\n")[:-2]) gpt_json = GPTJSON[SentimentSchema](API_KEY) - response, _ = await gpt_json.run( + response = await gpt_json.run( messages=[ GPTMessage( role=GPTMessageRole.SYSTEM, @@ -76,7 +76,7 @@ def few_shot_truncate_next( ), ) print(response) - print(f"Detected sentiment: {response.sentiment}") + print(f"Detected sentiment: {response.response.sentiment}") asyncio.run(runner()) From a59dc402b8d78b8529a49624973a5cb0cb660291 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:27:31 -0700 Subject: [PATCH 03/19] Add basic function call test --- gpt_json/fn_calling.py | 13 +----- gpt_json/tests/shared.py | 28 ++++++++++++ gpt_json/tests/test_fn_calling.py | 46 ++++++------------- gpt_json/tests/test_gpt.py | 76 ++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 46 deletions(-) diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index 78ea1eb..84ebe77 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -1,18 +1,7 @@ from dataclasses import dataclass -from enum import Enum from inspect import getdoc, signature from types import UnionType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Type, - Union, - get_args, - get_origin, -) +from typing import Any, Callable, Dict, Optional, Type, Union, get_args, get_origin from pydantic import BaseModel diff --git a/gpt_json/tests/shared.py b/gpt_json/tests/shared.py index 8eab01e..7ca85cc 100644 --- a/gpt_json/tests/shared.py +++ b/gpt_json/tests/shared.py @@ -1,3 +1,5 @@ +from enum import Enum + from pydantic import BaseModel, Field @@ -11,3 +13,29 @@ class MySchema(BaseModel): numerical: int | float sub_element: MySubSchema reason: bool = Field(description="Explanation") + + +class UnitType(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + + +class GetCurrentWeatherRequest(BaseModel): + location: str = Field(description="The city and state, e.g. San Francisco, CA") + unit: UnitType | None = None + + +def get_current_weather(request: GetCurrentWeatherRequest): + """ + Get the current weather in a given location + + The rest of the docstring should be omitted. + """ + + +def get_weather_additional_args(request: GetCurrentWeatherRequest, other_args: str): + pass + + +def get_weather_no_pydantic(other_args: str): + pass diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py index 5612067..2840704 100644 --- a/gpt_json/tests/test_fn_calling.py +++ b/gpt_json/tests/test_fn_calling.py @@ -1,36 +1,14 @@ -from enum import Enum from typing import Callable, Optional, Union import pytest -from pydantic import BaseModel, Field from gpt_json.fn_calling import get_base_type, parse_function - - -class UnitType(Enum): - CELSIUS = "celsius" - FAHRENHEIT = "fahrenheit" - - -class GetCurrentWeatherRequest(BaseModel): - location: str = Field(description="The city and state, e.g. San Francisco, CA") - unit: UnitType | None = None - - -def get_current_weather(request: GetCurrentWeatherRequest): - """ - Get the current weather in a given location - - The rest of the docstring should be omitted. - """ - - -def get_weather_additional_args(request: GetCurrentWeatherRequest, other_args: str): - pass - - -def get_weather_no_pydantic(other_args: str): - pass +from gpt_json.tests.shared import ( + UnitType, + get_current_weather, + get_weather_additional_args, + get_weather_no_pydantic, +) @pytest.mark.parametrize( @@ -65,14 +43,16 @@ def test_parse_function(): "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, - 'unit': { - 'anyOf': [ + "unit": { + "anyOf": [ { - 'enum': ['celsius', 'fahrenheit'], 'title': 'UnitType', 'type': 'string' + "enum": ["celsius", "fahrenheit"], + "title": "UnitType", + "type": "string", }, - {'type': 'null'} + {"type": "null"}, ], - 'default': None + "default": None, }, }, "required": ["location"], diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index f497f08..0e964c1 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -8,10 +8,17 @@ from openai.error import Timeout as OpenAITimeout from pydantic import BaseModel, Field +from gpt_json.fn_calling import parse_function from gpt_json.generics import resolve_generic_model from gpt_json.gpt import GPTJSON, ListResponse from gpt_json.models import FixTransforms, GPTMessage, GPTMessageRole, GPTModelVersion -from gpt_json.tests.shared import MySchema, MySubSchema +from gpt_json.tests.shared import ( + GetCurrentWeatherRequest, + MySchema, + MySubSchema, + UnitType, + get_current_weather, +) from gpt_json.transformations import JsonFixEnum @@ -196,6 +203,73 @@ async def test_acreate( assert response.fix_transforms == expected_transformations +@pytest.mark.asyncio +async def test_acreate_with_function_calls(): + model_version = GPTModelVersion.GPT_3_5 + messages = [ + GPTMessage( + role=GPTMessageRole.USER, + content="Input prompt", + ) + ] + + model = GPTJSON[MySchema]( + None, + model=model_version, + temperature=0.0, + timeout=60, + functions=[get_current_weather], + ) + + mock_response = { + "choices": [ + { + "message": { + "role": "assistant", + "content": "", + "function_call": { + "name": "get_current_weather", + "arguments": json_dumps({ + "location": "Boston", + "unit": "fahrenheit", + }), + }, + }, + "index": 0, + "finish_reason": "stop", + } + ] + } + + with patch.object(openai.ChatCompletion, "acreate") as mock_acreate: + mock_acreate.return_value = mock_response + + response = await model.run(messages=messages) + + mock_acreate.assert_called_with( + model=model_version.value, + messages=[ + { + "role": message.role.value, + "content": message.content, + } + for message in messages + ], + temperature=0.0, + api_key=None, + stream=False, + functions=[parse_function(get_current_weather)], + function_call="auto", + ) + + assert response + assert response.response is None + assert response.function_call == get_current_weather + assert response.function_arg == GetCurrentWeatherRequest( + location="Boston", unit=UnitType.FAHRENHEIT + ) + + @pytest.mark.parametrize( "input_messages,expected_output_messages", [ From 39d1139496666c16e85976db7e481eb9e860aec7 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:29:33 -0700 Subject: [PATCH 04/19] Lint --- gpt_json/fn_calling.py | 7 ------- gpt_json/tests/test_gpt.py | 10 ++++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index 84ebe77..2af7d9d 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from inspect import getdoc, signature from types import UnionType from typing import Any, Callable, Dict, Optional, Type, Union, get_args, get_origin @@ -6,12 +5,6 @@ from pydantic import BaseModel -@dataclass -class FunctionResponse: - caller_content: str - internal_context: Any - - def function_to_name(fn: Callable) -> str: return fn.__name__ diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index 0e964c1..cb87bdb 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -229,10 +229,12 @@ async def test_acreate_with_function_calls(): "content": "", "function_call": { "name": "get_current_weather", - "arguments": json_dumps({ - "location": "Boston", - "unit": "fahrenheit", - }), + "arguments": json_dumps( + { + "location": "Boston", + "unit": "fahrenheit", + } + ), }, }, "index": 0, From b558b0004dda73484383319e1ad8ffba3136eef8 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:40:12 -0700 Subject: [PATCH 05/19] Raise error on invalid pydantic version --- gpt_json/common.py | 6 +++ gpt_json/fn_calling.py | 88 +++++++++++++++++++++++--------------- gpt_json/gpt.py | 6 +-- gpt_json/tests/test_gpt.py | 4 ++ poetry.lock | 14 +++++- pyproject.toml | 1 + 6 files changed, 80 insertions(+), 39 deletions(-) diff --git a/gpt_json/common.py b/gpt_json/common.py index 4ceda29..d6610cf 100644 --- a/gpt_json/common.py +++ b/gpt_json/common.py @@ -6,9 +6,15 @@ from typing import Type +from pkg_resources import get_distribution from pydantic import BaseModel +def get_pydantic_version(): + version = get_distribution("pydantic").version + return int(version.split(".")[0]) + + def get_field_description(field): if hasattr(field, "description"): return field.description diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index 2af7d9d..a610ae9 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -4,30 +4,7 @@ from pydantic import BaseModel - -def function_to_name(fn: Callable) -> str: - return fn.__name__ - - -def get_request_from_function(fn: Callable) -> Type[BaseModel]: - """ - Parse the pydantic schema that is used as the only argument - for the functions. - - """ - # Parse the model inputs - parameters = list(signature(fn).parameters.values()) - - # Determine if we only have one parameter - if len(parameters) != 1: - raise ValueError("Only one Pydantic object is allowed to LLM function") - - # Get the parameter type - parameter_type = parameters[0].annotation - if not issubclass(parameter_type, BaseModel): - raise ValueError("Only Pydantic objects are allowed to LLM function") - - return parameter_type +from gpt_json.common import get_pydantic_version def parse_function(fn: Callable) -> Dict[str, Any]: @@ -40,15 +17,19 @@ def parse_function(fn: Callable) -> Dict[str, Any]: API Reference: https://platform.openai.com/docs/api-reference/chat/create """ + if get_pydantic_version() < 2: + raise ValueError( + f"Function calling is only supported with Pydantic > 2, found {get_pydantic_version()}" + ) + docstring = getdoc(fn) or "" lines = docstring.strip().split("\n") description = lines[0] if lines else None - parameter_type = get_request_from_function(fn) + parameter_type = get_argument_for_function(fn) # Parse the parameter type into a JSON schema parameter_schema = model_to_parameter_schema(parameter_type) - print("parameter_schema", parameter_schema) return { "name": function_to_name(fn), "description": description, @@ -56,7 +37,47 @@ def parse_function(fn: Callable) -> Dict[str, Any]: } +def model_to_parameter_schema(model: Type[BaseModel]) -> Dict[str, Any]: + formatted_json = resolve_refs(model.model_json_schema()) + return { + "type": "object", + "properties": formatted_json["properties"], + "required": formatted_json["required"], + } + + +def function_to_name(fn: Callable) -> str: + return fn.__name__ + + +def get_argument_for_function(fn: Callable) -> Type[BaseModel]: + """ + Function definitions are expected to have one argument, which is a pydantic BaseModel that captures + the input parameters and optional descriptions of the field values. This function + validates that the input function only has that one argument, and returns the type of that argument. + + """ + # Parse the model inputs + parameters = list(signature(fn).parameters.values()) + + # Determine if we only have one parameter + if len(parameters) != 1: + raise ValueError("Only one argument is allowed as the function input") + + # Get the parameter type + parameter_type = parameters[0].annotation + if not issubclass(parameter_type, BaseModel): + raise ValueError("Only Pydantic objects are allowed as function inputs") + + return parameter_type + + def get_base_type(field_type): + """ + Given a type annotation that might be a Union or Optional, return the base type. + For instance, if the type is Union[None, int], return int. + + """ origin = get_origin(field_type) args = get_args(field_type) @@ -80,6 +101,12 @@ def get_base_type(field_type): def resolve_refs(schema, defs=None): + """ + Given a JSON-Schema, resolve all $ref references to their definitions. This is supported + by the OpenAPI spec, but not by Pydantic. It makes for a cleaner API definition for use in + the GPT API. + + """ if defs is None: defs = schema.get("$defs", {}) @@ -96,12 +123,3 @@ def resolve_refs(schema, defs=None): return [resolve_refs(item, defs) for item in schema] return schema - - -def model_to_parameter_schema(model: Type[BaseModel]) -> Dict[str, Any]: - formatted_json = resolve_refs(model.model_json_schema()) - return { - "type": "object", - "properties": formatted_json["properties"], - "required": formatted_json["required"], - } diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index cf3cc06..6d51d93 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -26,7 +26,7 @@ from gpt_json.exceptions import InvalidFunctionParameters, InvalidFunctionResponse from gpt_json.fn_calling import ( function_to_name, - get_request_from_function, + get_argument_for_function, parse_function, ) from gpt_json.generics import resolve_generic_model @@ -236,11 +236,11 @@ async def run( raise InvalidFunctionResponse(function_name) function_call = self.functions[function_name] - function_request_model = get_request_from_function(function_call) + function_arg_model = get_argument_for_function(function_call) # Parameters are formatted as raw json strings try: - function_parsed = function_request_model.model_validate_json( + function_parsed = function_arg_model.model_validate_json( function_args_string ) except (ValueError, ValidationError): diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index cb87bdb..09db4be 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -8,6 +8,7 @@ from openai.error import Timeout as OpenAITimeout from pydantic import BaseModel, Field +from gpt_json.common import get_pydantic_version from gpt_json.fn_calling import parse_function from gpt_json.generics import resolve_generic_model from gpt_json.gpt import GPTJSON, ListResponse @@ -203,6 +204,9 @@ async def test_acreate( assert response.fix_transforms == expected_transformations +@pytest.mark.skipif( + get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" +) @pytest.mark.asyncio async def test_acreate_with_function_calls(): model_version = GPTModelVersion.GPT_3_5 diff --git a/poetry.lock b/poetry.lock index f9d32ae..ec0cc45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -371,6 +371,14 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "types-setuptools" +version = "68.1.0.0" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.5.0" @@ -407,7 +415,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.11" -content-hash = "dbedc595faddae7aa0146ed5d9323bf389ec89b470a9b247ab43ad3e5b25bd6c" +content-hash = "fcffa8087d0cca0ea2e98ac1e07f6dc008607218ddc41361c7dc9efc7528e67f" [metadata.files] aiohttp = [ @@ -1030,6 +1038,10 @@ tqdm = [ {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, ] +types-setuptools = [ + {file = "types-setuptools-68.1.0.0.tar.gz", hash = "sha256:2bc9b0c0818f77bdcec619970e452b320a423bb3ac074f5f8bc9300ac281c4ae"}, + {file = "types_setuptools-68.1.0.0-py3-none-any.whl", hash = "sha256:0c1618fb14850cb482adbec602bbb519c43f24942d66d66196bc7528320f33b1"}, +] typing-extensions = [ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, diff --git a/pyproject.toml b/pyproject.toml index 0a54535..61b0d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ pytest-asyncio = "^0.21.0" autoflake = "^2.1.1" black = "^23.3.0" mypy = "^1.3.0" +types-setuptools = "^68.1.0.0" [build-system] requires = ["poetry-core"] From dd1ca7a8d5941985cb02ec471c97944b6f859d8c Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:50:44 -0700 Subject: [PATCH 06/19] Update README --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index 78d6ac4..444a3f4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Specifically this library: - Includes retry logic for the most common API failures - Formats the JSON schema as a flexible prompt that can be added into any message - Supports templating of prompts to allow for dynamic content +- Enables typehinted function calls within the new GPT models, to better support agent creation ## Getting Started @@ -136,6 +137,62 @@ response, _ = await gpt_json.run( ) ``` +## Function Calls + +`gpt-3.5-turbo-0613` and `gpt-4-0613` were fine-tuned to support a specific syntax for function calls. We support this syntax in `gpt-json` as well. Here's an example of how to use it: + +```python +class UnitType(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + + +class GetCurrentWeatherRequest(BaseModel): + location: str = Field(description="The city and state, e.g. San Francisco, CA") + unit: UnitType | None = None + + +class DataPayload(BaseModel): + data: str + + +def get_current_weather(request: GetCurrentWeatherRequest): + """ + Get the current weather in a given location + """ + weather_info = { + "location": request.location, + "temperature": "72", + "unit": request.unit, + "forecast": ["sunny", "windy"], + } + return json_dumps(weather_info) + + +async def runner(): + gpt_json = GPTJSON[DataPayload](API_KEY, functions=[get_current_weather]) + response = await gpt_json.run( + messages=[ + GPTMessage( + role=GPTMessageRole.USER, + content="What's the weather like in Boston, in F?", + ), + ], + ) + + assert response.function_call == get_current_weather + assert response.function_arg == GetCurrentWeatherRequest( + location="Boston", unit=UnitType.FAHRENHEIT + ) +``` + +The response provides the original function alongside a formatted Pydantic object. If users want to execute the function, they can run response.function_call(response.function_arg). We will parse the get_current_weather function and the GetCurrentWeatherRequest parameter into the format that GPT expects, so it is more likely to return you a correct function execution. + +GPT makes no guarantees about the validity of the returned functions. They could hallucinate a function name or the function signature. To address these cases, the run() function may now throw two new exceptions: + +`InvalidFunctionResponse` - The function name is incorrect. +`InvalidFunctionParameters` - The function name is correct, but doesn't match the input schema that was provided. + ## Other Configurations The `GPTJSON` class supports other configuration parameters at initialization. From bc9cd1fb68e4374ace3e591a608c11f78227c87b Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:51:57 -0700 Subject: [PATCH 07/19] Update default pydantic --- poetry.lock | 188 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 143 insertions(+), 45 deletions(-) diff --git a/poetry.lock b/poetry.lock index ec0cc45..6dfb7b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,6 +29,14 @@ python-versions = ">=3.7" [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "async-timeout" version = "4.0.2" @@ -260,18 +268,30 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "1.10.12" -description = "Data validation and settings management using python type hints" +version = "2.3.0" +description = "Data validation using Python type hints" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.6.3" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.6.3" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyflakes" @@ -381,7 +401,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -415,7 +435,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.11" -content-hash = "fcffa8087d0cca0ea2e98ac1e07f6dc008607218ddc41361c7dc9efc7528e67f" +content-hash = "43622a1a014c7bd9d60c54210bdb519ec40aa550dc074cd5b50414186caa1961" [metadata.files] aiohttp = [ @@ -511,6 +531,10 @@ aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] +annotated-types = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, @@ -860,42 +884,116 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pydantic = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-2.3.0-py3-none-any.whl", hash = "sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81"}, + {file = "pydantic-2.3.0.tar.gz", hash = "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d"}, +] +pydantic-core = [ + {file = "pydantic_core-2.6.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad"}, + {file = "pydantic_core-2.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728"}, + {file = "pydantic_core-2.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb"}, + {file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd"}, + {file = "pydantic_core-2.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e"}, + {file = "pydantic_core-2.6.3-cp310-none-win32.whl", hash = "sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7"}, + {file = "pydantic_core-2.6.3-cp310-none-win_amd64.whl", hash = "sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad"}, + {file = "pydantic_core-2.6.3-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973"}, + {file = "pydantic_core-2.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da"}, + {file = "pydantic_core-2.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b"}, + {file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6"}, + {file = "pydantic_core-2.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50"}, + {file = "pydantic_core-2.6.3-cp311-none-win32.whl", hash = "sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8"}, + {file = "pydantic_core-2.6.3-cp311-none-win_amd64.whl", hash = "sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950"}, + {file = "pydantic_core-2.6.3-cp311-none-win_arm64.whl", hash = "sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2"}, + {file = "pydantic_core-2.6.3-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5"}, + {file = "pydantic_core-2.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5"}, + {file = "pydantic_core-2.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282"}, + {file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d"}, + {file = "pydantic_core-2.6.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa"}, + {file = "pydantic_core-2.6.3-cp312-none-win32.whl", hash = "sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1"}, + {file = "pydantic_core-2.6.3-cp312-none-win_amd64.whl", hash = "sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881"}, + {file = "pydantic_core-2.6.3-cp312-none-win_arm64.whl", hash = "sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58"}, + {file = "pydantic_core-2.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6"}, + {file = "pydantic_core-2.6.3-cp37-none-win32.whl", hash = "sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b"}, + {file = "pydantic_core-2.6.3-cp37-none-win_amd64.whl", hash = "sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525"}, + {file = "pydantic_core-2.6.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db"}, + {file = "pydantic_core-2.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170"}, + {file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec"}, + {file = "pydantic_core-2.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb"}, + {file = "pydantic_core-2.6.3-cp38-none-win32.whl", hash = "sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc"}, + {file = "pydantic_core-2.6.3-cp38-none-win_amd64.whl", hash = "sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378"}, + {file = "pydantic_core-2.6.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465"}, + {file = "pydantic_core-2.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f"}, + {file = "pydantic_core-2.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3"}, + {file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76"}, + {file = "pydantic_core-2.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef"}, + {file = "pydantic_core-2.6.3-cp39-none-win32.whl", hash = "sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a"}, + {file = "pydantic_core-2.6.3-cp39-none-win_amd64.whl", hash = "sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b"}, + {file = "pydantic_core-2.6.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7"}, + {file = "pydantic_core-2.6.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483"}, + {file = "pydantic_core-2.6.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d"}, + {file = "pydantic_core-2.6.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149"}, + {file = "pydantic_core-2.6.3.tar.gz", hash = "sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7"}, ] pyflakes = [ {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, @@ -1043,8 +1141,8 @@ types-setuptools = [ {file = "types_setuptools-68.1.0.0-py3-none-any.whl", hash = "sha256:0c1618fb14850cb482adbec602bbb519c43f24942d66d66196bc7528320f33b1"}, ] typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] urllib3 = [ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, From 680052ff62ce08be13f5451d2c9bd7b7eb7e4642 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 11:52:57 -0700 Subject: [PATCH 08/19] Make fn_calling tests optional --- README.md | 2 +- gpt_json/tests/test_fn_calling.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 444a3f4..f154aac 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Specifically this library: - Includes retry logic for the most common API failures - Formats the JSON schema as a flexible prompt that can be added into any message - Supports templating of prompts to allow for dynamic content -- Enables typehinted function calls within the new GPT models, to better support agent creation +- Validate typehinted function calls in the new GPT models, to better support agent creation ## Getting Started diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py index 2840704..7675958 100644 --- a/gpt_json/tests/test_fn_calling.py +++ b/gpt_json/tests/test_fn_calling.py @@ -2,6 +2,7 @@ import pytest +from gpt_json.common import get_pydantic_version from gpt_json.fn_calling import get_base_type, parse_function from gpt_json.tests.shared import ( UnitType, @@ -11,6 +12,9 @@ ) +@pytest.mark.skipif( + get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" +) @pytest.mark.parametrize( "incorrect_fn", [ @@ -29,6 +33,9 @@ def test_get_base_type(): assert get_base_type(Union[UnitType, None]) == UnitType +@pytest.mark.skipif( + get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" +) def test_parse_function(): """ Assert the formatted schema conforms to the expected JSON-Schema / GPT format. From 2692b8ac307713515a7e24f9de9b85ffc0e4ccad Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 12:07:41 -0700 Subject: [PATCH 09/19] Export ListResponse from main package --- gpt_json/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpt_json/__init__.py b/gpt_json/__init__.py index 6d08887..e734deb 100644 --- a/gpt_json/__init__.py +++ b/gpt_json/__init__.py @@ -1,2 +1,2 @@ -from gpt_json.gpt import GPTJSON # noqa +from gpt_json.gpt import GPTJSON, ListResponse # noqa from gpt_json.models import * # noqa From 8f7031226c079847ea9138fa0ccfad377c947e5b Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 12:19:16 -0700 Subject: [PATCH 10/19] Support functions outputs in message type --- gpt_json/gpt.py | 21 +++++++++------------ gpt_json/models.py | 17 +++++++++++++++-- gpt_json/tests/test_gpt.py | 14 ++++++++++---- gpt_json/tests/test_models.py | 22 ++++++++++++++++++++++ 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index 6d51d93..f64e1f8 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -1,7 +1,7 @@ import logging from asyncio import TimeoutError as AsyncTimeoutError from asyncio import wait_for -from dataclasses import replace +from copy import copy from json import loads as json_loads from json.decoder import JSONDecodeError from typing import ( @@ -545,10 +545,9 @@ def fill_message_template( # have been left in the pydantic field typehints content = content.format(**format_variables) - return replace( - message, - content=content, - ) + new_message = copy(message) + new_message.content = content + return new_message def message_to_dict(self, message: GPTMessage): return {"role": message.role.value, "content": message.content} @@ -590,13 +589,11 @@ def trim_messages(self, messages: list[GPTMessage], n: int): break # Recreate the messages with our new text - new_messages = [ - replace( - messages[i], - content=content, - ) - for i, content in enumerate(filtered_messages) - ] + new_messages = [] + for i, content in enumerate(filtered_messages): + new_message = copy(messages[i]) + new_message.content = content + new_messages.append(new_message) # Log a copy of the message array if we have to crop it if current_token_count != original_token_count: diff --git a/gpt_json/models.py b/gpt_json/models.py index 09596d1..b77de92 100644 --- a/gpt_json/models.py +++ b/gpt_json/models.py @@ -3,6 +3,8 @@ from enum import Enum, unique from typing import Callable +from pydantic import BaseModel, model_validator + if sys.version_info >= (3, 11): from enum import StrEnum @@ -30,6 +32,7 @@ class GPTMessageRole(EnumSuper): SYSTEM = "system" USER = "user" ASSISTANT = "assistant" + FUNCTION = "function" @unique @@ -56,8 +59,7 @@ class FixTransforms: fixed_bools: bool = False -@dataclass -class GPTMessage: +class GPTMessage(BaseModel): """ A single message in the chat sequence """ @@ -65,6 +67,17 @@ class GPTMessage: role: GPTMessageRole content: str + # Name is only supported if we're formatting a function message + name: str | None = None + + @model_validator(mode="after") + def check_name_if_function(self): + if self.role == GPTMessageRole.FUNCTION and self.name is None: + raise ValueError("Must provide a name for function messages") + if self.role != GPTMessageRole.FUNCTION and self.name is not None: + raise ValueError("Cannot provide a name for non-function messages") + return self + @dataclass class TruncationOptions: diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index 09db4be..39ccfde 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -368,7 +368,9 @@ async def test_extracted_json_is_None(): ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): - result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + result = await gpt.run( + [GPTMessage(role=GPTMessageRole.SYSTEM, content="message content")] + ) assert result.response is None @@ -377,7 +379,9 @@ async def test_no_valid_results_from_remote_request(): gpt = GPTJSON[MySchema](None) with patch.object(gpt, "submit_request", return_value={"choices": []}): - result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + result = await gpt.run( + [GPTMessage(role=GPTMessageRole.SYSTEM, content="message content")] + ) assert result.response is None @@ -392,7 +396,9 @@ async def test_unable_to_find_valid_json_payload(): ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): - result = await gpt.run([GPTMessage(GPTMessageRole.SYSTEM, "message content")]) + result = await gpt.run( + [GPTMessage(role=GPTMessageRole.SYSTEM, content="message content")] + ) assert result.response is None @@ -445,7 +451,7 @@ async def side_effect(*args, **kwargs): with pytest.raises(OpenAITimeout): await gpt.run( - [GPTMessage(GPTMessageRole.SYSTEM, "message content")], + [GPTMessage(role=GPTMessageRole.SYSTEM, content="message content")], ) end_time = time() diff --git a/gpt_json/tests/test_models.py b/gpt_json/tests/test_models.py index 35871f1..2b2b873 100644 --- a/gpt_json/tests/test_models.py +++ b/gpt_json/tests/test_models.py @@ -4,6 +4,7 @@ import pytest import gpt_json.models as models_file +from gpt_json.models import GPTMessage, GPTMessageRole @pytest.mark.parametrize("model_file", [models_file]) @@ -26,3 +27,24 @@ def test_string_enums(model_file): # Every file listed in pytest should have at least one enum assert found_enums > 0, f"No enums found in {model_file}" + + +def test_gpt_message_validates_function(): + with pytest.raises(ValueError): + GPTMessage( + role=GPTMessageRole.SYSTEM, + name="function_name", + content="function_content", + ) + + with pytest.raises(ValueError): + GPTMessage( + role=GPTMessageRole.FUNCTION, + content="function_content", + ) + + GPTMessage( + role=GPTMessageRole.FUNCTION, + name="function_name", + content="function_content", + ) From 0179f3538e7846df94185b1e0016229df719118c Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 12:34:44 -0700 Subject: [PATCH 11/19] Return parsed content alongside function --- gpt_json/fn_calling.py | 8 ++++++-- gpt_json/gpt.py | 20 +++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index a610ae9..e00b2d1 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -62,12 +62,16 @@ def get_argument_for_function(fn: Callable) -> Type[BaseModel]: # Determine if we only have one parameter if len(parameters) != 1: - raise ValueError("Only one argument is allowed as the function input") + raise ValueError( + f"Only one argument is allowed as the function input: {fn} {parameters}" + ) # Get the parameter type parameter_type = parameters[0].annotation if not issubclass(parameter_type, BaseModel): - raise ValueError("Only Pydantic objects are allowed as function inputs") + raise ValueError( + f"Only Pydantic objects are allowed as function inputs: {fn} {parameter_type}" + ) return parameter_type diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index f64e1f8..078abd6 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -229,6 +229,9 @@ async def run( function_arg=None, ) + function_call: Callable[..., BaseModel] | None = None + function_parsed: BaseModel | None = None + if response_message.get("function_call"): function_name = response_message["function_call"]["name"] function_args_string = response_message["function_call"]["arguments"] @@ -246,13 +249,6 @@ async def run( except (ValueError, ValidationError): raise InvalidFunctionParameters(function_name, function_args_string) - return RunResponse( - response=None, - fix_transforms=None, - function_call=function_call, - function_arg=function_parsed, - ) - extracted_json, fixed_payload = self.extract_json( response_message, self.extract_type ) @@ -262,8 +258,8 @@ async def run( return RunResponse( response=None, fix_transforms=fixed_payload, - function_call=None, - function_arg=None, + function_call=function_call, + function_arg=function_parsed, ) if not self.schema_model: @@ -275,8 +271,8 @@ async def run( return RunResponse( response=self.schema_model(**extracted_json), fix_transforms=fixed_payload, - function_call=None, - function_arg=None, + function_call=function_call, + function_arg=function_parsed, ) async def stream( @@ -365,6 +361,8 @@ def extract_json(self, response_message, extract_type: ResponseType): """ full_response = response_message["content"] + if not full_response: + return None, None extracted_response = find_json_response(full_response, extract_type) if extracted_response is None: From 54792ea308bb0aed5e490050619f9877c1ddc423 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 12:59:50 -0700 Subject: [PATCH 12/19] Return raw response in RunResponse --- gpt_json/common.py | 13 ++++++++++++- gpt_json/gpt.py | 5 +++++ gpt_json/models.py | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/gpt_json/common.py b/gpt_json/common.py index d6610cf..93d0cf2 100644 --- a/gpt_json/common.py +++ b/gpt_json/common.py @@ -4,11 +4,13 @@ """ -from typing import Type +from typing import Any, Type, TypeVar from pkg_resources import get_distribution from pydantic import BaseModel +T = TypeVar("T", bound=BaseModel) + def get_pydantic_version(): version = get_distribution("pydantic").version @@ -40,3 +42,12 @@ def get_model_field_infos(model: Type[BaseModel]): return {key: value.field_info for key, value in model.__fields__.items()} # type: ignore else: raise ValueError(f"Unknown pydantic field class structure: {model}") + + +def parse_obj_model(model: Type[T], obj: dict[str, Any]) -> T: + if hasattr(model, "parse_obj"): + return model.parse_obj(obj) + elif hasattr(model, "model_validate"): + return model.model_validate(obj) + else: + raise ValueError(f"Unknown pydantic field class structure: {model}") diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index 078abd6..b2de55d 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -23,6 +23,7 @@ from pydantic import BaseModel, Field, ValidationError from tiktoken import encoding_for_model +from gpt_json.common import parse_obj_model from gpt_json.exceptions import InvalidFunctionParameters, InvalidFunctionResponse from gpt_json.fn_calling import ( function_to_name, @@ -84,6 +85,7 @@ class RunResponse(Generic[SchemaType], BaseModel): Helper schema to wrap a single response alongside the extracted metadata """ + raw_response: GPTMessage | None response: SchemaType | None fix_transforms: FixTransforms | None function_call: Callable[..., BaseModel] | None @@ -223,6 +225,7 @@ async def run( response_message = self.extract_response_message(response) if response_message is None: return RunResponse( + raw_response=None, response=None, fix_transforms=None, function_call=None, @@ -256,6 +259,7 @@ async def run( # Cast to schema model if extracted_json is None: return RunResponse( + raw_response=parse_obj_model(GPTMessage, response_message), response=None, fix_transforms=fixed_payload, function_call=function_call, @@ -269,6 +273,7 @@ async def run( # Allow pydantic to handle the validation return RunResponse( + raw_response=parse_obj_model(GPTMessage, response_message), response=self.schema_model(**extracted_json), fix_transforms=fixed_payload, function_call=function_call, diff --git a/gpt_json/models.py b/gpt_json/models.py index b77de92..f2978e9 100644 --- a/gpt_json/models.py +++ b/gpt_json/models.py @@ -59,6 +59,11 @@ class FixTransforms: fixed_bools: bool = False +class FunctionCall(BaseModel): + arguments: str + name: str + + class GPTMessage(BaseModel): """ A single message in the chat sequence @@ -70,6 +75,9 @@ class GPTMessage(BaseModel): # Name is only supported if we're formatting a function message name: str | None = None + # Message from the server + function_call: FunctionCall | None = None + @model_validator(mode="after") def check_name_if_function(self): if self.role == GPTMessageRole.FUNCTION and self.name is None: From 0694734fbc116183de44b4246982d2e0814d2369 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 13:13:39 -0700 Subject: [PATCH 13/19] Ignore templating for raw return requests --- gpt_json/gpt.py | 12 +++++++++--- gpt_json/models.py | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index b2de55d..a692ffd 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -252,6 +252,9 @@ async def run( except (ValueError, ValidationError): raise InvalidFunctionParameters(function_name, function_args_string) + raw_response = parse_obj_model(GPTMessage, response_message) + raw_response.allow_templating = False + extracted_json, fixed_payload = self.extract_json( response_message, self.extract_type ) @@ -259,7 +262,7 @@ async def run( # Cast to schema model if extracted_json is None: return RunResponse( - raw_response=parse_obj_model(GPTMessage, response_message), + raw_response=raw_response, response=None, fix_transforms=fixed_payload, function_call=function_call, @@ -273,7 +276,7 @@ async def run( # Allow pydantic to handle the validation return RunResponse( - raw_response=parse_obj_model(GPTMessage, response_message), + raw_response=raw_response, response=self.schema_model(**extracted_json), fix_transforms=fixed_payload, function_call=function_call, @@ -538,6 +541,9 @@ def fill_message_template( SCHEMA_PROMPT_TEMPLATE_KEY: self.schema_prompt, } + if message.content is None or not message.allow_templating: + return message + # Regular quotes should passthrough to the next stage, except for our special keys content = message.content.replace("{", "{{").replace("}", "}}") for key in auto_format.keys(): @@ -567,7 +573,7 @@ def trim_messages(self, messages: list[GPTMessage], n: int): Returns: list: A list of messages with a total token count less than n tokens. """ - message_text = [message.content for message in messages] + message_text = [message.content for message in messages if message.content] enc = encoding_for_model("gpt-4") filtered_messages = [] diff --git a/gpt_json/models.py b/gpt_json/models.py index f2978e9..f1b19c1 100644 --- a/gpt_json/models.py +++ b/gpt_json/models.py @@ -70,7 +70,7 @@ class GPTMessage(BaseModel): """ role: GPTMessageRole - content: str + content: str | None # Name is only supported if we're formatting a function message name: str | None = None @@ -78,6 +78,10 @@ class GPTMessage(BaseModel): # Message from the server function_call: FunctionCall | None = None + # If enabled, gpt-json will attempt to format the message with the runtime variables + # Disable this in cases where you want the message to be formatted 1:1 with the input + allow_templating: bool = True + @model_validator(mode="after") def check_name_if_function(self): if self.role == GPTMessageRole.FUNCTION and self.name is None: From 5fce4fca9ad24ccbe6fa05ba94f532fcc90a3e13 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 13:40:13 -0700 Subject: [PATCH 14/19] Fix missing role in tests --- gpt_json/common.py | 9 +++++++++ gpt_json/gpt.py | 7 +++++-- gpt_json/tests/test_gpt.py | 8 ++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/gpt_json/common.py b/gpt_json/common.py index 93d0cf2..810ba00 100644 --- a/gpt_json/common.py +++ b/gpt_json/common.py @@ -51,3 +51,12 @@ def parse_obj_model(model: Type[T], obj: dict[str, Any]) -> T: return model.model_validate(obj) else: raise ValueError(f"Unknown pydantic field class structure: {model}") + + +def obj_to_json(model: T, **kwargs) -> str: + if hasattr(model, "json"): + return model.json(**kwargs) + elif hasattr(model, "model_dump_json"): + return model.model_dump_json(**kwargs) + else: + raise ValueError(f"Unknown pydantic field class structure: {model}") diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index a692ffd..e20e26c 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -23,7 +23,7 @@ from pydantic import BaseModel, Field, ValidationError from tiktoken import encoding_for_model -from gpt_json.common import parse_obj_model +from gpt_json.common import obj_to_json, parse_obj_model from gpt_json.exceptions import InvalidFunctionParameters, InvalidFunctionResponse from gpt_json.fn_calling import ( function_to_name, @@ -559,7 +559,10 @@ def fill_message_template( return new_message def message_to_dict(self, message: GPTMessage): - return {"role": message.role.value, "content": message.content} + # return {"role": message.role.value, "content": message.content} + obj = json_loads(obj_to_json(message, exclude_unset=True)) + obj.pop("allow_templating", None) + return obj def trim_messages(self, messages: list[GPTMessage], n: int): """ diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index 39ccfde..4713125 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -364,7 +364,9 @@ async def test_extracted_json_is_None(): with patch.object( gpt, "submit_request", - return_value={"choices": [{"message": {"content": "some content"}}]}, + return_value={ + "choices": [{"message": {"content": "some content", "role": "assistant"}}] + }, ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): @@ -392,7 +394,9 @@ async def test_unable_to_find_valid_json_payload(): with patch.object( gpt, "submit_request", - return_value={"choices": [{"message": {"content": "some content"}}]}, + return_value={ + "choices": [{"message": {"content": "some content", "role": "assistant"}}] + }, ), patch.object( gpt, "extract_json", return_value=(None, FixTransforms(None, False)) ): From 469bd7947c1196217007c28d260157e954d76909 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 13:58:45 -0700 Subject: [PATCH 15/19] Require pydantic2 --- .github/workflows/deploy.yml | 2 +- gpt_json/common.py | 62 ------------------------------- gpt_json/fn_calling.py | 7 ---- gpt_json/generics.py | 4 +- gpt_json/gpt.py | 6 +-- gpt_json/prompts.py | 12 +++--- gpt_json/streaming.py | 7 +--- gpt_json/tests/test_fn_calling.py | 7 ---- gpt_json/tests/test_gpt.py | 4 -- pyproject.toml | 2 +- 10 files changed, 13 insertions(+), 100 deletions(-) delete mode 100644 gpt_json/common.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 84f2e73..dba5751 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: python: ["3.11"] - pydantic: ["1.10.12", "2.1.1"] + pydantic: ["2.1.1"] steps: - uses: actions/checkout@v3 diff --git a/gpt_json/common.py b/gpt_json/common.py deleted file mode 100644 index 810ba00..0000000 --- a/gpt_json/common.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Pydantic V1 and V2 compatibility layer. Pydantic V2 has a better API for the type inspection -that we do in GPT-JSON, but we can easily bridge some of the concepts in V1. - -""" - -from typing import Any, Type, TypeVar - -from pkg_resources import get_distribution -from pydantic import BaseModel - -T = TypeVar("T", bound=BaseModel) - - -def get_pydantic_version(): - version = get_distribution("pydantic").version - return int(version.split(".")[0]) - - -def get_field_description(field): - if hasattr(field, "description"): - return field.description - elif hasattr(field, "field_info"): - return field.field_info.description - else: - raise ValueError(f"Unknown pydantic field class structure: {field}") - - -def get_model_fields(model: Type[BaseModel]): - if hasattr(model, "model_fields"): - return model.model_fields - elif hasattr(model, "__fields__"): - return model.__fields__ - else: - raise ValueError(f"Unknown pydantic field class structure: {model}") - - -def get_model_field_infos(model: Type[BaseModel]): - if hasattr(model, "model_fields"): - return model.model_fields - elif hasattr(model, "__fields__"): - return {key: value.field_info for key, value in model.__fields__.items()} # type: ignore - else: - raise ValueError(f"Unknown pydantic field class structure: {model}") - - -def parse_obj_model(model: Type[T], obj: dict[str, Any]) -> T: - if hasattr(model, "parse_obj"): - return model.parse_obj(obj) - elif hasattr(model, "model_validate"): - return model.model_validate(obj) - else: - raise ValueError(f"Unknown pydantic field class structure: {model}") - - -def obj_to_json(model: T, **kwargs) -> str: - if hasattr(model, "json"): - return model.json(**kwargs) - elif hasattr(model, "model_dump_json"): - return model.model_dump_json(**kwargs) - else: - raise ValueError(f"Unknown pydantic field class structure: {model}") diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index e00b2d1..185cec0 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -4,8 +4,6 @@ from pydantic import BaseModel -from gpt_json.common import get_pydantic_version - def parse_function(fn: Callable) -> Dict[str, Any]: """ @@ -17,11 +15,6 @@ def parse_function(fn: Callable) -> Dict[str, Any]: API Reference: https://platform.openai.com/docs/api-reference/chat/create """ - if get_pydantic_version() < 2: - raise ValueError( - f"Function calling is only supported with Pydantic > 2, found {get_pydantic_version()}" - ) - docstring = getdoc(fn) or "" lines = docstring.strip().split("\n") description = lines[0] if lines else None diff --git a/gpt_json/generics.py b/gpt_json/generics.py index f83d786..3df5944 100644 --- a/gpt_json/generics.py +++ b/gpt_json/generics.py @@ -2,8 +2,6 @@ from pydantic import BaseModel, Field, create_model -from gpt_json.common import get_model_field_infos - def get_typevar_mapping(t: Any) -> dict[TypeVar, Any]: origin = get_origin(t) @@ -52,7 +50,7 @@ def resolve_generic_model(t: Any) -> Type[BaseModel]: # Create a dict with all the fields from the original model fields = {} for name, type_ in base_model.__annotations__.items(): - original_field = get_model_field_infos(base_model).get(name) + original_field = base_model.model_fields.get(name) if original_field: fields[name] = (type_, original_field) else: diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index e20e26c..cbcef6a 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -23,7 +23,6 @@ from pydantic import BaseModel, Field, ValidationError from tiktoken import encoding_for_model -from gpt_json.common import obj_to_json, parse_obj_model from gpt_json.exceptions import InvalidFunctionParameters, InvalidFunctionResponse from gpt_json.fn_calling import ( function_to_name, @@ -252,7 +251,7 @@ async def run( except (ValueError, ValidationError): raise InvalidFunctionParameters(function_name, function_args_string) - raw_response = parse_obj_model(GPTMessage, response_message) + raw_response = GPTMessage.model_validate(response_message) raw_response.allow_templating = False extracted_json, fixed_payload = self.extract_json( @@ -559,8 +558,7 @@ def fill_message_template( return new_message def message_to_dict(self, message: GPTMessage): - # return {"role": message.role.value, "content": message.content} - obj = json_loads(obj_to_json(message, exclude_unset=True)) + obj = json_loads(message.model_dump_json(exclude_unset=True)) obj.pop("allow_templating", None) return obj diff --git a/gpt_json/prompts.py b/gpt_json/prompts.py index eca71f7..5947276 100644 --- a/gpt_json/prompts.py +++ b/gpt_json/prompts.py @@ -3,8 +3,6 @@ from pydantic import BaseModel -from gpt_json.common import get_field_description, get_model_fields - def generate_schema_prompt(schema: Type[BaseModel]) -> str: """ @@ -15,12 +13,14 @@ def generate_schema_prompt(schema: Type[BaseModel]) -> str: def generate_payload(model: Type[BaseModel]): payload = [] - for key, value in get_model_fields(model).items(): + for key, value in model.model_fields.items(): field_annotation = value.annotation annotation_origin = get_origin(field_annotation) annotation_arguments = get_args(field_annotation) - if annotation_origin in {list, List}: + if field_annotation is None: + continue + elif annotation_origin in {list, List}: if issubclass(annotation_arguments[0], BaseModel): payload.append( f'"{key}": {generate_payload(annotation_arguments[0])}[]' @@ -35,8 +35,8 @@ def generate_payload(model: Type[BaseModel]): payload.append(f'"{key}": {generate_payload(field_annotation)}') else: payload.append(f'"{key}": {field_annotation.__name__.lower()}') - if get_field_description(value): - payload[-1] += f" // {get_field_description(value)}" + if value.description: + payload[-1] += f" // {value.description}" # All brackets are double defined so they will passthrough a call to `.format()` where we # pass custom variables return "{{\n" + ",\n".join(payload) + "\n}}" diff --git a/gpt_json/streaming.py b/gpt_json/streaming.py index 898d394..a3ce37e 100644 --- a/gpt_json/streaming.py +++ b/gpt_json/streaming.py @@ -5,7 +5,6 @@ from pydantic import BaseModel -from gpt_json.common import get_model_fields from gpt_json.transformations import JsonFixEnum, fix_truncated_json SchemaType = TypeVar("SchemaType", bound=BaseModel) @@ -41,9 +40,7 @@ def _create_schema_from_partial( TODO: this is hacky. ideally we want pydantic to implement Partial[SchemaType] https://github.com/pydantic/pydantic/issues/1673 my fix is to create the schema object with all string values for now""" - cleaned_obj_data = { - field: "" for field, typ in get_model_fields(schema_model).items() - } + cleaned_obj_data = {field: "" for field, typ in schema_model.model_fields.items()} cleaned_obj_data.update({k: v for k, v in partial.items() if v is not None}) return schema_model(**cleaned_obj_data) @@ -69,7 +66,7 @@ def prepare_streaming_object( list(current_partial_raw.keys())[-1] if current_partial_raw else None ) updated_key = ( - raw_recent_key if raw_recent_key in get_model_fields(schema_model) else None + raw_recent_key if raw_recent_key in schema_model.model_fields else None ) event = proposed_event diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py index 7675958..2840704 100644 --- a/gpt_json/tests/test_fn_calling.py +++ b/gpt_json/tests/test_fn_calling.py @@ -2,7 +2,6 @@ import pytest -from gpt_json.common import get_pydantic_version from gpt_json.fn_calling import get_base_type, parse_function from gpt_json.tests.shared import ( UnitType, @@ -12,9 +11,6 @@ ) -@pytest.mark.skipif( - get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" -) @pytest.mark.parametrize( "incorrect_fn", [ @@ -33,9 +29,6 @@ def test_get_base_type(): assert get_base_type(Union[UnitType, None]) == UnitType -@pytest.mark.skipif( - get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" -) def test_parse_function(): """ Assert the formatted schema conforms to the expected JSON-Schema / GPT format. diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index 4713125..ea68ee0 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -8,7 +8,6 @@ from openai.error import Timeout as OpenAITimeout from pydantic import BaseModel, Field -from gpt_json.common import get_pydantic_version from gpt_json.fn_calling import parse_function from gpt_json.generics import resolve_generic_model from gpt_json.gpt import GPTJSON, ListResponse @@ -204,9 +203,6 @@ async def test_acreate( assert response.fix_transforms == expected_transformations -@pytest.mark.skipif( - get_pydantic_version() < 2, reason="Pydantic 2+ required for function calls" -) @pytest.mark.asyncio async def test_acreate_with_function_calls(): model_version = GPTModelVersion.GPT_3_5 diff --git a/pyproject.toml b/pyproject.toml index 61b0d9f..029b4d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ packages = [{include = "gpt_json"}] python = "^3.11" tiktoken = "^0.3.3" openai = "^0.27.6" -pydantic = ">1.10.7, <3.0.0" +pydantic = ">2.0.0, <3.0.0" backoff = "^2.2.1" From 53b8c872a39a878de64d005a3b489d7f3d054b9a Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 14:10:48 -0700 Subject: [PATCH 16/19] Allow templating of function names in prompt --- gpt_json/gpt.py | 5 +++++ gpt_json/tests/test_gpt.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index cbcef6a..dd77474 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -2,6 +2,7 @@ from asyncio import TimeoutError as AsyncTimeoutError from asyncio import wait_for from copy import copy +from json import dumps as json_dumps from json import loads as json_loads from json.decoder import JSONDecodeError from typing import ( @@ -64,6 +65,7 @@ def handle_backoff(details): SCHEMA_PROMPT_TEMPLATE_KEY = "json_schema" +SCHEMA_PROMPT_FUNCTION_KEY = "functions" SchemaType = TypeVar("SchemaType", bound=BaseModel) @@ -538,6 +540,9 @@ def fill_message_template( ): auto_format = { SCHEMA_PROMPT_TEMPLATE_KEY: self.schema_prompt, + SCHEMA_PROMPT_FUNCTION_KEY: json_dumps( + [function_to_name(fn) for fn in self.functions.values()] + ), } if message.content is None or not message.allow_templating: diff --git a/gpt_json/tests/test_gpt.py b/gpt_json/tests/test_gpt.py index ea68ee0..11a13a4 100644 --- a/gpt_json/tests/test_gpt.py +++ b/gpt_json/tests/test_gpt.py @@ -334,7 +334,7 @@ class TestSchema2(BaseModel): assert gptjson2.schema_model == TestSchema2 -def test_fill_message_template(): +def test_fill_message_schema_template(): class TestTemplateSchema(BaseModel): template_field: str = Field(description="Max length {max_length}") @@ -353,6 +353,23 @@ class TestTemplateSchema(BaseModel): ) +def test_fill_message_functions_template(): + class TestTemplateSchema(BaseModel): + template_field: str = Field(description="Max length {max_length}") + + gpt = GPTJSON[TestTemplateSchema](None, functions=[get_current_weather]) + assert gpt.fill_message_template( + GPTMessage( + role=GPTMessageRole.USER, + content="Here are the functions available: {functions}", + ), + format_variables=dict(), + ) == GPTMessage( + role=GPTMessageRole.USER, + content='Here are the functions available: ["get_current_weather"]', + ) + + @pytest.mark.asyncio async def test_extracted_json_is_None(): gpt = GPTJSON[MySchema](None) From c393b34491a06f1c49f94578523df36b1c7d0436 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 14:36:53 -0700 Subject: [PATCH 17/19] Update docs --- README.md | 14 +++++++------- gpt_json/fn_calling.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f154aac..2e177d6 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Respond with the following JSON schema: async def runner(): gpt_json = GPTJSON[SentimentSchema](API_KEY) - response, _ = await gpt_json.run( + payload = await gpt_json.run( messages=[ GPTMessage( role=GPTMessageRole.SYSTEM, @@ -51,8 +51,8 @@ async def runner(): ) ] ) - print(response) - print(f"Detected sentiment: {response.sentiment}") + print(payload.response) + print(f"Detected sentiment: {payload.response.sentiment}") asyncio.run(runner()) ``` @@ -102,7 +102,7 @@ Generate fictitious quotes that are {sentiment}. """ gpt_json = GPTJSON[QuoteSchema](API_KEY) -response, _ = await gpt_json.run( +response = await gpt_json.run( messages=[ GPTMessage( role=GPTMessageRole.SYSTEM, @@ -126,7 +126,7 @@ Generate fictitious quotes that are {sentiment}. """ gpt_json = GPTJSON[QuoteSchema](API_KEY) -response, _ = await gpt_json.run( +response = await gpt_json.run( messages=[ GPTMessage( role=GPTMessageRole.SYSTEM, @@ -214,9 +214,9 @@ GPT (especially GPT-4) is relatively good at formatting responses at JSON, but i When calling `gpt_json.run()`, we return a tuple of values: ```python -response, transformations = await gpt_json.run(...) +payload = await gpt_json.run(...) -print(transformations) +print(transformations.fix_transforms) ``` ```bash diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index 185cec0..f83e3d3 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -99,9 +99,15 @@ def get_base_type(field_type): def resolve_refs(schema, defs=None): """ - Given a JSON-Schema, resolve all $ref references to their definitions. This is supported - by the OpenAPI spec, but not by Pydantic. It makes for a cleaner API definition for use in - the GPT API. + Given a JSON-Schema, replace all $ref references to their definitions. + + When JSON Schemas are exported by pydantic, use of nested fields (like enums) will be defined + in a separate $defs lookup table. This is a valid OpenAPI schema but makes for a less-obvious + definition for the GPT API. Additionally, none of the docs use the $defs format, so conceivably + the model was not fine-tuned on this particular format. + + This function takes a schema complete with $defs and resolves all the references to their + full definition. The resulting payload is what we send to the GPT API. """ if defs is None: From c99c2ce1cdeeae63e7c6f4151cd885ab6d4a432b Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 15:17:12 -0700 Subject: [PATCH 18/19] Fix typehinting of functions --- gpt_json/gpt.py | 14 ++++++++++---- gpt_json/tests/shared.py | 3 ++- gpt_json/tests/test_fn_calling.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gpt_json/gpt.py b/gpt_json/gpt.py index dd77474..47f5072 100644 --- a/gpt_json/gpt.py +++ b/gpt_json/gpt.py @@ -89,7 +89,7 @@ class RunResponse(Generic[SchemaType], BaseModel): raw_response: GPTMessage | None response: SchemaType | None fix_transforms: FixTransforms | None - function_call: Callable[..., BaseModel] | None + function_call: Callable[[BaseModel], Any] | None function_arg: BaseModel | None @@ -108,7 +108,7 @@ def __init__( model: GPTModelVersion | str = GPTModelVersion.GPT_4, auto_trim: bool = False, auto_trim_response_overhead: int = 0, - functions: list[Callable[..., BaseModel]] | None = None, + functions: list[Callable[[Any], Any]] | None = None, # For messages that are relatively deterministic temperature=0.2, timeout: int | None = None, @@ -134,7 +134,13 @@ def __init__( self.openai_max_retries = openai_max_retries self.openai_arguments = kwargs self.schema_model = self._cls_schema_model - self.functions = {function_to_name(fn): fn for fn in (functions or [])} + self.functions = { + function_to_name(cast_fn): cast_fn + for fn in (functions or []) + # Use an explicit cast; Callable can't be typehinted with BaseModel directly + # because [BaseModel] is considered invariant with subclasses + for cast_fn in [cast(Callable[[BaseModel], Any], fn)] + } self.__class__._cls_schema_model = None if not self.schema_model: @@ -233,7 +239,7 @@ async def run( function_arg=None, ) - function_call: Callable[..., BaseModel] | None = None + function_call: Callable[[BaseModel], Any] | None = None function_parsed: BaseModel | None = None if response_message.get("function_call"): diff --git a/gpt_json/tests/shared.py b/gpt_json/tests/shared.py index 7ca85cc..7b60507 100644 --- a/gpt_json/tests/shared.py +++ b/gpt_json/tests/shared.py @@ -27,7 +27,8 @@ class GetCurrentWeatherRequest(BaseModel): def get_current_weather(request: GetCurrentWeatherRequest): """ - Get the current weather in a given location + Get the current weather in a given location. + Second line should also be included. The rest of the docstring should be omitted. """ diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py index 2840704..782010d 100644 --- a/gpt_json/tests/test_fn_calling.py +++ b/gpt_json/tests/test_fn_calling.py @@ -35,7 +35,7 @@ def test_parse_function(): """ parse_function(get_current_weather) == { "name": "get_current_weather", - "description": "Get the current weather in a given location", + "description": "Get the current weather in a given location. Second line should also be included.", "parameters": { "type": "object", "properties": { From 93f75e4af21393166aed6056473dd754db1e8ca9 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Thu, 24 Aug 2023 18:44:15 -0700 Subject: [PATCH 19/19] Fix multiline descriptions --- gpt_json/fn_calling.py | 31 +++++++++++++++++++++---- gpt_json/tests/shared.py | 9 ++++---- gpt_json/tests/test_fn_calling.py | 38 +++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/gpt_json/fn_calling.py b/gpt_json/fn_calling.py index f83e3d3..934719b 100644 --- a/gpt_json/fn_calling.py +++ b/gpt_json/fn_calling.py @@ -15,11 +15,8 @@ def parse_function(fn: Callable) -> Dict[str, Any]: API Reference: https://platform.openai.com/docs/api-reference/chat/create """ - docstring = getdoc(fn) or "" - lines = docstring.strip().split("\n") - description = lines[0] if lines else None - parameter_type = get_argument_for_function(fn) + description = get_function_description(fn) # Parse the parameter type into a JSON schema parameter_schema = model_to_parameter_schema(parameter_type) @@ -43,6 +40,32 @@ def function_to_name(fn: Callable) -> str: return fn.__name__ +def get_function_description(fn: Callable) -> str: + """ + The description of a function is everything before an empty linebreak. + + For instance: + + ``` + A + B + + C + ``` + + Would return "A B" + + """ + docstring = getdoc(fn) or "" + lines = docstring.strip().split("\n") + description_lines = [] + for line in lines: + if not line.strip(): + break + description_lines.append(line.strip()) + return " ".join(description_lines) + + def get_argument_for_function(fn: Callable) -> Type[BaseModel]: """ Function definitions are expected to have one argument, which is a pydantic BaseModel that captures diff --git a/gpt_json/tests/shared.py b/gpt_json/tests/shared.py index 7b60507..9676eb2 100644 --- a/gpt_json/tests/shared.py +++ b/gpt_json/tests/shared.py @@ -26,12 +26,11 @@ class GetCurrentWeatherRequest(BaseModel): def get_current_weather(request: GetCurrentWeatherRequest): - """ - Get the current weather in a given location. - Second line should also be included. + """Test description""" - The rest of the docstring should be omitted. - """ + +async def get_current_weather_async(request: GetCurrentWeatherRequest): + """Test description""" def get_weather_additional_args(request: GetCurrentWeatherRequest, other_args: str): diff --git a/gpt_json/tests/test_fn_calling.py b/gpt_json/tests/test_fn_calling.py index 782010d..291d0c2 100644 --- a/gpt_json/tests/test_fn_calling.py +++ b/gpt_json/tests/test_fn_calling.py @@ -2,15 +2,30 @@ import pytest -from gpt_json.fn_calling import get_base_type, parse_function +from gpt_json.fn_calling import get_base_type, get_function_description, parse_function from gpt_json.tests.shared import ( UnitType, get_current_weather, + get_current_weather_async, get_weather_additional_args, get_weather_no_pydantic, ) +def multi_line_description_fn(): + """ + Test + description + + Hidden + description + """ + + +def single_line_description_fn(): + """Test description""" + + @pytest.mark.parametrize( "incorrect_fn", [ @@ -29,18 +44,31 @@ def test_get_base_type(): assert get_base_type(Union[UnitType, None]) == UnitType -def test_parse_function(): +def test_get_function_description(): + assert get_function_description(multi_line_description_fn) == "Test description" + assert get_function_description(single_line_description_fn) == "Test description" + + +@pytest.mark.parametrize( + "function,expected_name", + [ + (get_current_weather, "get_current_weather"), + (get_current_weather_async, "get_current_weather_async"), + ], +) +def test_parse_function(function, expected_name: str): """ Assert the formatted schema conforms to the expected JSON-Schema / GPT format. """ - parse_function(get_current_weather) == { - "name": "get_current_weather", - "description": "Get the current weather in a given location. Second line should also be included.", + assert parse_function(function) == { + "name": expected_name, + "description": "Test description", "parameters": { "type": "object", "properties": { "location": { "type": "string", + "title": "Location", "description": "The city and state, e.g. San Francisco, CA", }, "unit": {