Skip to content

Commit

Permalink
feat: implement required or optional types and default values
Browse files Browse the repository at this point in the history
  • Loading branch information
jfaldanam committed Nov 7, 2023
1 parent 7c7d7cb commit 710dd8d
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 35 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Functions are defined as a json that is then transpiled to OpenAI or whatever ne
- `description`: Description of the parameter.
- `options`: List of options that the parameter accepts. Similar to a enum. Example: `["option1", "option2"]`. Can't be used along with `regex`.
- `regex`: For string parameters, a regex can be specified to validate the input. Can't be used along with `options`.
- `default`: Default value of the parameter. It is ignored if the parameter is required. Default is `null` or None.
- `required`: If the parameter is required or not. Default is `false`.
- `response`: Schema of the response of the function. Keys are parameter names and values are the type of the parameter.
Example function definition:
Expand All @@ -72,7 +74,9 @@ Example function definition:
"type": "str",
"description": "Name of whom to salute. o7",
"options": null,
"regex": null
"regex": null,
"default": null,
"required": true
}
],
"response": {
Expand Down
4 changes: 3 additions & 1 deletion functions/salute.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"type": "str",
"description": "Name of whom to salute. o7",
"options": null,
"regex": null
"regex": null,
"default": null,
"required": true
}
],
"response": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "eidos"
version = "0.99.1"
version = "0.99.2"
authors = [
{ name="José F. Aldana Martín", email="[email protected]" },
]
Expand Down
4 changes: 3 additions & 1 deletion src/eidos/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ def execute(function_name: str, arguments: dict) -> tuple[dict[str, Any], int]:

# Validate inputs
try:
validate_input_schema(arguments, schema=function_definition["parameters"])
arguments = validate_input_schema(
arguments, schema=function_definition["parameters"]
)
except (ValueError, TypeError) as e:
logger.error(f"Invalid input: {e}")
status = 400
Expand Down
46 changes: 30 additions & 16 deletions src/eidos/models/function.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import builtins

from pydantic import BaseModel, Field, create_model
from pydantic_core import PydanticUndefined

from eidos.models.parameter import AiParameter
from eidos.validation.type import split_type_from_generic
Expand All @@ -27,24 +28,37 @@ def load_model(function_: dict) -> BaseModel:
AiParameter.model_validate(parameter) for parameter in function_["parameters"]
]

parameters_dict = {}
for v in parameters:
json_schema_extra = {}
if v.options:
json_schema_extra["enum"] = v.options

type_ = getattr(
builtins,
split_type_from_generic(v.type)[
0
], # OpenAI does not support generic types, remove it
)

# PydanticUndefined is used to mark parameters as not required
default_ = PydanticUndefined
if not v.required:
default_ = v.default

parameters_dict[v.name] = (
type_,
Field(
default=default_, # Set a default value to mark them as not required
description=v.description,
pattern=v.regex,
json_schema_extra=json_schema_extra,
),
)

ai_function = create_model(
function_["name"],
**{
v.name: (
getattr(
builtins,
split_type_from_generic(v.type)[
0
], # OpenAI does not support generic types, remove it
),
Field(
description=v.description,
pattern=v.regex,
json_schema_extra={"enum": v.options} if v.options else {},
),
)
for v in parameters
},
**parameters_dict,
)

return ai_function
2 changes: 2 additions & 0 deletions src/eidos/models/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class AiParameter(BaseModel):
type: str
options: Optional[list[Any]]
regex: Optional[str]
required: Optional[bool] = True
default: Optional[Any] = None

@model_validator(mode="before")
def _check_types(cls, values: dict) -> dict:
Expand Down
42 changes: 27 additions & 15 deletions src/eidos/validation/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,42 @@ def validate_output_schema(result: Any, schema: dict[str, Any]) -> dict[str, Any
return validated_result


def validate_input_schema(arguments: dict[str,], schema: list[dict[str, Any]]) -> None:
"""Validate the arguments of a function against its schema.
def validate_input_schema(
arguments: dict[str, Any], schema: list[dict[str, Any]]
) -> dict[str, Any]:
"""Validate and add any optional arguments of a function against its schema.
Args:
arguments (dict): Arguments to validate.
schema (dict): Schema of the function arguments.
Returns:
None
dict[str, Any]: Validated and transformed arguments.
Raises:
ValueError: If the number of arguments does not match the number of parameters.
ValueError: If there is a missing argument
ValueError: If an argument is not found in the schema.
TypeError: If an argument is not of the correct type.
"""
types = [(parameter["name"], parameter["type"]) for parameter in schema]
schema_names = [parameter["name"] for parameter in schema]

# Check that the number of arguments matches the number of parameters
if len(arguments) != len(types):
raise ValueError(
f"Number of arguments ({len(arguments)}) does not match "
f"number of parameters ({len(types)})"
)
for key in arguments:
if key not in schema_names:
raise ValueError(f"Unknown agument {key}: not found in schema")

# If they match, check that the arguments are of the correct type
for schema_name, type_ in types:
if schema_name not in arguments:
raise ValueError(f"Argument {schema_name} not found in arguments")
for parameter in schema:
required = parameter["required"]
schema_name = parameter["name"]
type_ = parameter["type"]

if required is None or required: # If required is None, it is defined as True
if schema_name not in arguments:
raise ValueError(f"Argument {schema_name} not found in arguments")
else: # If required is False, add the default value
if schema_name not in arguments:
arguments[schema_name] = parameter["default"]
if not validate_type(arguments[schema_name], type_):
raise TypeError(f"Argument {schema_name} is not of type {type_}")
raise TypeError(
f"Argument {schema_name} is not of type {type_}. Either change the "
"default or provide an alternative value"
)

return arguments
20 changes: 20 additions & 0 deletions src/eidos/validation/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ def validate_type(value: Any, type_: str) -> bool:
return isinstance(value, type_class)


def get_type_as_string(x: Any) -> str:
"""Get the type of a variable as a string.
Args:
x (Any): Variable to get the type from.
Returns:
str: Type of the variable as a string.
"""
return str(type(x)).split("'")[1]


def check_ai_parameter_types(values: dict) -> dict:
"""Check that the type is one of the allowed ones and
that the options are of the correct type.
Expand All @@ -58,6 +70,14 @@ def check_ai_parameter_types(values: dict) -> dict:
if values["regex"] is not None and values["options"] is not None:
raise ValueError("Regex and options are mutually exclusive")

# Validate default value
if not values["required"]:
if values["default"] is not None:
if get_type_as_string(values["default"]) != main_type:
raise ValueError(
"Default value must be of the same type as the parameter"
)

# Validate regex
if values["regex"] is not None:
if main_type != "str":
Expand Down

0 comments on commit 710dd8d

Please sign in to comment.