-
Notifications
You must be signed in to change notification settings - Fork 3.5k
/
Copy pathkernel_json_schema_builder.py
215 lines (186 loc) · 8.5 KB
/
kernel_json_schema_builder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# Copyright (c) Microsoft. All rights reserved.
import types
from enum import Enum
from typing import Any, Union, get_args, get_origin, get_type_hints
from semantic_kernel.const import PARSED_ANNOTATION_UNION_DELIMITER
from semantic_kernel.exceptions.function_exceptions import FunctionInvalidParameterConfiguration
from semantic_kernel.kernel_pydantic import KernelBaseModel
TYPE_MAPPING = {
int: "integer",
str: "string",
bool: "boolean",
float: "number",
list: "array",
dict: "object",
set: "array",
tuple: "array",
"int": "integer",
"str": "string",
"bool": "boolean",
"float": "number",
"list": "array",
"dict": "object",
"set": "array",
"tuple": "array",
"object": "object",
"array": "array",
}
class KernelJsonSchemaBuilder:
"""Kernel JSON schema builder."""
@classmethod
def build(cls, parameter_type: type | str, description: str | None = None) -> dict[str, Any]:
"""Builds the JSON schema for a given parameter type and description.
Args:
parameter_type (type | str): The parameter type.
description (str, optional): The description of the parameter. Defaults to None.
Returns:
dict[str, Any]: The JSON schema for the parameter type.
"""
if isinstance(parameter_type, str):
return cls.build_from_type_name(parameter_type, description)
if isinstance(parameter_type, KernelBaseModel):
return cls.build_model_schema(parameter_type, description)
if isinstance(parameter_type, type) and issubclass(parameter_type, Enum):
return cls.build_enum_schema(parameter_type, description)
if hasattr(parameter_type, "__annotations__"):
return cls.build_model_schema(parameter_type, description)
if hasattr(parameter_type, "__args__"):
return cls.handle_complex_type(parameter_type, description)
schema = cls.get_json_schema(parameter_type)
if description:
schema["description"] = description
return schema
@classmethod
def build_model_schema(cls, model: type, description: str | None = None) -> dict[str, Any]:
"""Builds the JSON schema for a given model and description.
Args:
model (type): The model type.
description (str, optional): The description of the model. Defaults to None.
Returns:
dict[str, Any]: The JSON schema for the model.
"""
# TODO (moonbox3): add support for handling forward references, which is not currently tested
# https://github.com/microsoft/semantic-kernel/issues/6464
properties = {}
required = []
hints = get_type_hints(model, globals(), locals())
for field_name, field_type in hints.items():
field_description = None
if hasattr(model, "model_fields") and field_name in model.model_fields:
field_info = model.model_fields[field_name]
if isinstance(field_info.metadata, dict):
field_description = field_info.metadata.get("description")
elif isinstance(field_info.metadata, list) and field_info.metadata:
field_description = field_info.metadata[0]
elif hasattr(field_info, "description"):
field_description = field_info.description
if not cls._is_optional(field_type):
required.append(field_name)
properties[field_name] = cls.build(field_type, field_description)
schema = {"type": "object", "properties": properties}
if required:
schema["required"] = required
if description:
schema["description"] = description
return schema
@classmethod
def _is_optional(cls, field_type: Any) -> bool:
return get_origin(field_type) in {types.UnionType, Union} and type(None) in get_args(field_type)
@classmethod
def build_from_type_name(cls, parameter_type: str, description: str | None = None) -> dict[str, Any]:
"""Builds the JSON schema for a given parameter type name and description.
Args:
parameter_type (str): The parameter type name.
description (str, optional): The description of the parameter. Defaults to None.
Returns:
dict[str, Any]: The JSON schema for the parameter type.
"""
schema: dict[str, Any] = {}
if PARSED_ANNOTATION_UNION_DELIMITER in parameter_type:
# this means it is a Union or | so need to build with "anyOf"
types = parameter_type.split(PARSED_ANNOTATION_UNION_DELIMITER)
schemas = [cls.build_from_type_name(t.strip(), description) for t in types]
schema["anyOf"] = schemas
else:
type_name = TYPE_MAPPING.get(parameter_type, "object")
schema["type"] = type_name
if description:
schema["description"] = description
return schema
@classmethod
def get_json_schema(cls, parameter_type: type) -> dict[str, Any]:
"""Gets JSON schema for a given parameter type.
Args:
parameter_type (type): The parameter type.
Returns:
dict[str, Any]: The JSON schema for the parameter type.
"""
type_name = TYPE_MAPPING.get(parameter_type, "object")
return {"type": type_name}
@classmethod
def handle_complex_type(cls, parameter_type: type, description: str | None = None) -> dict[str, Any]:
"""Handles building the JSON schema for complex types.
Args:
parameter_type (type): The parameter type.
description (str, optional): The description of the parameter. Defaults to None.
Returns:
dict[str, Any]: The JSON schema for the parameter type.
"""
origin = get_origin(parameter_type)
args = get_args(parameter_type)
schema: dict[str, Any] = {}
if origin is list or origin is set:
item_type = args[0]
schema = {"type": "array", "items": cls.build(item_type)}
if description:
schema["description"] = description
return schema
if origin is dict:
_, value_type = args
additional_properties = cls.build(value_type)
if additional_properties == {"type": "object"}:
additional_properties["properties"] = {} # Account for differences in Python 3.10 dict
schema = {"type": "object", "additionalProperties": additional_properties}
if description:
schema["description"] = description
return schema
if origin is tuple:
items = [cls.build(arg) for arg in args]
schema = {"type": "array", "items": items}
if description:
schema["description"] = description
return schema
if origin in {Union, types.UnionType}:
# Handle Optional[T] (Union[T, None]) by making schema nullable
if len(args) == 2 and type(None) in args:
non_none_type = args[0] if args[1] is type(None) else args[1]
schema = cls.build(non_none_type)
schema["type"] = [schema["type"], "null"]
if description:
schema["description"] = description
return schema
schemas = [cls.build(arg, description) for arg in args]
return {"anyOf": schemas}
schema = cls.get_json_schema(parameter_type)
if description:
schema["description"] = description
return schema
@classmethod
def build_enum_schema(cls, enum_type: type, description: str | None = None) -> dict[str, Any]:
"""Builds the JSON schema for an enum type.
Args:
enum_type (type): The enum type.
description (str, optional): The description of the enum. Defaults to None.
Returns:
dict[str, Any]: The JSON schema for the enum type.
"""
if not issubclass(enum_type, Enum):
raise FunctionInvalidParameterConfiguration(f"{enum_type} is not a valid Enum type")
try:
enum_values = [item.value for item in enum_type]
except TypeError as ex:
raise FunctionInvalidParameterConfiguration(f"Failed to get enum values for {enum_type}") from ex
schema = {"type": TYPE_MAPPING.get(type(enum_values[0]), "string"), "enum": enum_values}
if description:
schema["description"] = description
return schema