diff --git a/guardrails/decorators/experimental.py b/guardrails/decorators/experimental.py new file mode 100644 index 000000000..440d27f9a --- /dev/null +++ b/guardrails/decorators/experimental.py @@ -0,0 +1,15 @@ +import functools +from guardrails.logger import logger + + +def experimental(func): + """Decorator to mark a function as experimental.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + logger.warn( + f"The function '{func.__name__}' is experimental and subject to change." + ) + return func(*args, **kwargs) + + return wrapper diff --git a/guardrails/guard.py b/guardrails/guard.py index 75d1644b3..1560ba2e1 100644 --- a/guardrails/guard.py +++ b/guardrails/guard.py @@ -86,10 +86,13 @@ ValidatorMap, ) -from guardrails.utils.tools_utils import ( +from guardrails.utils.structured_data_utils import ( # Prevent duplicate declaration in the docs json_function_calling_tool as json_function_calling_tool_util, + output_format_json_schema as output_format_json_schema, ) +from guardrails.decorators.experimental import experimental + from guardrails.settings import settings @@ -1331,6 +1334,10 @@ def to_dict(self) -> Dict[str, Any]: return i_guard.to_dict() + @experimental + def response_format_json_schema(self) -> Dict[str, Any]: + return output_format_json_schema(schema=self._base_model) # type: ignore + def json_function_calling_tool( self, tools: Optional[list] = None, diff --git a/guardrails/utils/structured_data_utils.py b/guardrails/utils/structured_data_utils.py new file mode 100644 index 000000000..d63d0ee9a --- /dev/null +++ b/guardrails/utils/structured_data_utils.py @@ -0,0 +1,75 @@ +from typing import List, Optional +from guardrails.logger import logger +from guardrails.classes.schema.processed_schema import ProcessedSchema +from guardrails.types.pydantic import ModelOrListOfModels + + +# takes processed schema and converts it to a openai tool object +def schema_to_tool(schema) -> dict: + tool = { + "type": "function", + "function": { + "name": "gd_response_tool", + "description": "A tool for generating responses to guardrails." + " It must be called last in every response.", + "parameters": schema, + "required": schema["required"] or [], + }, + } + return tool + + +def set_additional_properties_false_iteratively(schema): + stack = [schema] + while stack: + current = stack.pop() + if isinstance(current, dict): + if "properties" in current: + current["required"] = list( + current["properties"].keys() + ) # this has to be set + if "maximum" in current: + logger.warn("Property maximum is not supported." " Dropping") + current.pop("maximum") # the api does not like these set + if "minimum" in current: + logger.warn("Property maximum is not supported." " Dropping") + current.pop("minimum") # the api does not like these set + if "default" in current: + logger.warn("Property default is not supported. Marking field Required") + current.pop("default") # the api does not like these set + for prop in current.values(): + stack.append(prop) + elif isinstance(current, list): + for prop in current: + stack.append(prop) + if ( + isinstance(current, dict) + and "additionalProperties" not in current + and "type" in current + and current["type"] == "object" + ): + current["additionalProperties"] = False # the api needs these set + + +def json_function_calling_tool( + schema: ProcessedSchema, + tools: Optional[List] = None, +) -> List: + tools = tools or [] + tools.append(schema_to_tool(schema)) # type: ignore + return tools + + +def output_format_json_schema(schema: ModelOrListOfModels) -> dict: + parsed_schema = schema.model_json_schema() # type: ignore + + set_additional_properties_false_iteratively(parsed_schema) + + return { + "type": "json_schema", + "json_schema": { + "name": parsed_schema["title"], + "schema": parsed_schema, + "strict": True, + }, # type: ignore + } diff --git a/guardrails/utils/tools_utils.py b/guardrails/utils/tools_utils.py deleted file mode 100644 index da0386a8a..000000000 --- a/guardrails/utils/tools_utils.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List, Optional - -from guardrails.classes.schema.processed_schema import ProcessedSchema - - -# takes processed schema and converts it to a openai tool object -def schema_to_tool(schema) -> dict: - tool = { - "type": "function", - "function": { - "name": "gd_response_tool", - "description": "A tool for generating responses to guardrails." - " It must be called last in every response.", - "parameters": schema, - "required": schema["required"] or [], - }, - } - return tool - - -def json_function_calling_tool( - schema: ProcessedSchema, - tools: Optional[List] = None, -) -> List: - tools = tools or [] - tools.append(schema_to_tool(schema)) # type: ignore - return tools diff --git a/tests/unit_tests/utils/test_tools_utils.py b/tests/unit_tests/utils/test_structured_data_utils.py similarity index 60% rename from tests/unit_tests/utils/test_tools_utils.py rename to tests/unit_tests/utils/test_structured_data_utils.py index 4bdd7fc38..3397e50c4 100644 --- a/tests/unit_tests/utils/test_tools_utils.py +++ b/tests/unit_tests/utils/test_structured_data_utils.py @@ -3,7 +3,11 @@ from guardrails.schema.pydantic_schema import pydantic_model_to_schema -from guardrails.utils.tools_utils import json_function_calling_tool, schema_to_tool +from guardrails.utils.structured_data_utils import ( + json_function_calling_tool, + schema_to_tool, + output_format_json_schema, +) class Delivery(BaseModel): @@ -141,3 +145,89 @@ def test_json_function_calling_tool(): }, } ] + + +def test_output_format_json_schema(): + schema = output_format_json_schema(Schedule) + assert schema == { + "type": "json_schema", + "json_schema": { + "name": "Schedule", + "schema": { + "additionalProperties": False, + "$defs": { + "Delivery": { + "additionalProperties": False, + "properties": { + "customer": { + "description": "customer name", + "title": "Customer", + "type": "string", + }, + "pickup_time": { + "description": "date and time of pickup", + "title": "Pickup Time", + "type": "string", + }, + "pickup_location": { + "description": "address of pickup", + "title": "Pickup Location", + "type": "string", + }, + "dropoff_time": { + "description": "date and time of dropoff", + "title": "Dropoff Time", + "type": "string", + }, + "dropoff_location": { + "description": "address of dropoff", + "title": "Dropoff Location", + "type": "string", + }, + "price": { + "description": "price of delivery with" + " currency symbol included", + "title": "Price", + "type": "string", + }, + "items": { + "description": "items for pickup/delivery typically" + " something a single person can carry on a bike", + "title": "Items", + "type": "string", + }, + "number_items": { + "description": "number of items", + "title": "Number Items", + "type": "integer", + }, + }, + "required": [ + "customer", + "pickup_time", + "pickup_location", + "dropoff_time", + "dropoff_location", + "price", + "items", + "number_items", + ], + "title": "Delivery", + "type": "object", + } + }, + "properties": { + "deliveries": { + "description": "deliveries for messenger", + "items": {"$ref": "#/$defs/Delivery"}, + "title": "Deliveries", + "type": "array", + } + }, + "required": ["deliveries"], + "title": "Schedule", + "type": "object", + }, + "strict": True, + }, + }