byllm 0.4.8__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
byllm/schema.jac ADDED
@@ -0,0 +1,283 @@
1
+ """Schema generation for OpenAI compatible APIs.
2
+
3
+ This module provides functionality to generate JSON schemas for classes and types
4
+ and to validate instances against these schemas.
5
+ """
6
+
7
+ import from dataclasses { is_dataclass }
8
+ import from enum { Enum }
9
+ import from types { FunctionType, MethodType, UnionType }
10
+ import from typing { Callable, Union, get_args, get_origin, get_type_hints }
11
+ import from pydantic { TypeAdapter }
12
+
13
+ glob _SCHEMA_OBJECT_WRAPPER = "schema_object_wrapper";
14
+ glob _SCHEMA_DICT_WRAPPER = "schema_dict_wrapper";
15
+
16
+ def _type_to_schema(ty: type, title: str = "", desc: str = "") -> dict {
17
+ title = title.replace("_", " ").title();
18
+ context = ({"title": title} if title else {}) | (
19
+ {"description": desc} if desc else {}
20
+ );
21
+
22
+ semstr: str = ty._jac_semstr if hasattr(ty, "_jac_semstr") else "";
23
+ semstr = semstr or (ty.__doc__ if hasattr(ty, "__doc__") else "") or ""; # type: ignore
24
+
25
+ semstr_inner: dict[str, str] = (
26
+ ty._jac_semstr_inner if hasattr(ty, "_jac_semstr_inner") else {}
27
+ );
28
+
29
+ # Raise on unsupported types
30
+ if ty in (list, dict, set, tuple) {
31
+ raise ValueError(
32
+ f"Untyped {ty.__name__} is not supported for schema generation. "f"Use {ty.__name__}[T, ...] instead."
33
+ ) ;
34
+ }
35
+
36
+ # Handle primitive types
37
+ if ty is type(None) {
38
+ return {"type": "null"} | context;
39
+ }
40
+ if ty is bool {
41
+ return {"type": "boolean"} | context;
42
+ }
43
+ if ty is int {
44
+ return {"type": "integer"} | context;
45
+ }
46
+ if ty is float {
47
+ return {"type": "number"} | context;
48
+ }
49
+ if ty is str {
50
+ return {"type": "string"} | context;
51
+ }
52
+
53
+ # Handle Union
54
+ if get_origin(ty) in (Union, UnionType) {
55
+ args = get_args(ty);
56
+ return {"anyOf": [_type_to_schema(arg) for arg in args], "title": title,} | context;
57
+ }
58
+
59
+ # Handle annotated list
60
+ if get_origin(ty) is list {
61
+ item_type: type = get_args(ty)[0];
62
+ return {"type": "array", "items": _type_to_schema(item_type),} | context;
63
+ }
64
+
65
+ # Handle annotated tuple/set
66
+ if get_origin(ty) in (tuple, set) {
67
+ origin = get_origin(ty).__name__; # type: ignore
68
+ args = get_args(ty);
69
+ if len(args) == 2 and args[1] is Ellipsis {
70
+ item_type = args[0];
71
+ return {"type": "array", "items": _type_to_schema(item_type),} | context;
72
+ }
73
+ raise ValueError(
74
+ f"Unsupported {origin} type for schema generation: {ty}. "f"Only {origin} of the form {origin}[T, ...] are supported."
75
+ ) ;
76
+ }
77
+
78
+ # Handle annotated dictionaries
79
+ if get_origin(ty) is dict {
80
+ return _convert_dict_to_schema(ty) | context;
81
+ }
82
+
83
+ # Handle dataclass
84
+ if is_dataclass(ty) {
85
+ fields: dict[str, type] = {
86
+ name: type
87
+ for (name, type) in get_type_hints(ty).items()
88
+ if not name.startswith("_")
89
+ };
90
+ properties = {
91
+ name: _type_to_schema(
92
+ type, name, semstr_inner.get(name, "")
93
+ ) # type: ignore
94
+ for (name, type) in fields.items()
95
+ };
96
+ return {
97
+ "title": title or ty.__name__,
98
+ "description": semstr,
99
+ "type": "object",
100
+ "properties": properties,
101
+ "required": list(fields.keys()),
102
+ "additionalProperties": False,
103
+
104
+ };
105
+ }
106
+
107
+ # Handle enums
108
+ if isinstance(ty, type) and issubclass(ty, Enum) {
109
+ enum_type = None;
110
+ enum_values = [];
111
+ for member in ty.__members__.values() {
112
+ enum_values.append(member.value);
113
+ if enum_type is None {
114
+ enum_type = type(member.value);
115
+ } elif type(member.value) is not enum_type {
116
+ raise ValueError(
117
+ f"Enum {ty.__name__} has mixed types. Not supported for schema generation."
118
+ ) ;
119
+ }
120
+ enum_type = enum_type or int;
121
+ }
122
+ enum_desc = f"\nThe value *should* be one in this list: {enum_values} where";
123
+ enum_desc += " the names are [" + ", ".join([e.name for e in ty]) + "].";
124
+ if enum_type not in (int, str) {
125
+ raise ValueError(
126
+ f"Enum {ty.__name__} has unsupported type {enum_type}. ""Only int and str enums are supported for schema generation."
127
+ ) ;
128
+ }
129
+ return {
130
+ "description": semstr + enum_desc,
131
+ "type": "integer" if enum_type is int else "string",
132
+
133
+ };
134
+ }
135
+
136
+ # Handle functions
137
+ if isinstance(ty, (FunctionType, MethodType)) {
138
+ hints = get_type_hints(ty);
139
+ hints.pop("return", None);
140
+ params = {
141
+ name: _type_to_schema(type, name, semstr_inner.get(name, ""))
142
+ for (name, type) in hints.items()
143
+ };
144
+ return {
145
+ "title": title or ty.__name__,
146
+ "type": "function",
147
+ "description": semstr,
148
+ "properties": params,
149
+ "required": list(params.keys()),
150
+ "additionalProperties": False,
151
+
152
+ };
153
+ }
154
+
155
+ raise ValueError(
156
+ f"Unsupported type for schema generation: {ty}. ""Only primitive types, dataclasses, and Union types are supported."
157
+ ) ;
158
+ }
159
+
160
+ def _name_of_type(ty: type) -> str {
161
+ if get_origin(ty) in (Union, UnionType) {
162
+ names = [_name_of_type(arg) for arg in get_args(ty)];
163
+ return "_or_".join(names);
164
+ }
165
+ if hasattr(ty, "__name__") {
166
+ return ty.__name__;
167
+ }
168
+ return "type";
169
+ }
170
+
171
+ """Convert a dictionary type to a schema."""
172
+ def _convert_dict_to_schema(ty_dict: type) -> dict {
173
+ if get_origin(ty_dict) is not dict {
174
+ raise ValueError(f"Expected a dictionary type, got {ty_dict}.") ;
175
+ }
176
+ (key_type, value_type) = get_args(ty_dict);
177
+ return {
178
+ "type": "object",
179
+ "title": _SCHEMA_DICT_WRAPPER,
180
+ "properties": {
181
+ _SCHEMA_DICT_WRAPPER: {
182
+ "type": "array",
183
+ "items": {
184
+ "type": "object",
185
+ "properties": {
186
+ "key": _type_to_schema(key_type),
187
+ "value": _type_to_schema(value_type),
188
+
189
+ },
190
+ "required": ["key", "value"],
191
+ "additionalProperties": False,
192
+
193
+ },
194
+
195
+ }
196
+ },
197
+ "additionalProperties": False,
198
+ "required": [_SCHEMA_DICT_WRAPPER],
199
+
200
+ };
201
+ }
202
+
203
+ """Decode a JSON dictionary to a Python dictionary."""
204
+ def _decode_dict(json_obj: dict) -> dict {
205
+ if not isinstance(json_obj, dict) {
206
+ return json_obj;
207
+ }
208
+ if _SCHEMA_DICT_WRAPPER in json_obj {
209
+ items = json_obj[_SCHEMA_DICT_WRAPPER];
210
+ return {item["key"]: _decode_dict(item["value"]) for item in items};
211
+ }
212
+ return {key: _decode_dict(value) for (key, value) in json_obj.items()};
213
+ }
214
+
215
+ """Wrap the schema in an object with a type."""
216
+ def _wrap_to_object(schema: dict[str, object]) -> dict[str, object] {
217
+ if "type" in schema and schema["type"] == "object" {
218
+ return schema;
219
+ }
220
+ return {
221
+ "type": "object",
222
+ "title": _SCHEMA_OBJECT_WRAPPER,
223
+ "properties": {_SCHEMA_OBJECT_WRAPPER: schema,},
224
+ "required": [_SCHEMA_OBJECT_WRAPPER],
225
+ "additionalProperties": False,
226
+
227
+ };
228
+ }
229
+
230
+ """Unwrap the schema from an object with a type."""
231
+ def _unwrap_from_object(json_obj: dict) -> dict {
232
+ if _SCHEMA_OBJECT_WRAPPER in json_obj {
233
+ return json_obj[_SCHEMA_OBJECT_WRAPPER];
234
+ }
235
+ return json_obj;
236
+ }
237
+
238
+ """Return the JSON schema for the response type."""
239
+ def type_to_schema(resp_type: type) -> dict[str, object] {
240
+ type_name = _name_of_type(resp_type);
241
+ schema = _type_to_schema(resp_type, type_name);
242
+ schema = _wrap_to_object(schema);
243
+ return {
244
+ "type": "json_schema",
245
+ "json_schema": {"name": type_name, "schema": schema, "strict": True,},
246
+
247
+ };
248
+ }
249
+
250
+ """Return the JSON schema for the tool type."""
251
+ def tool_to_schema(
252
+ func: Callable, description: str, params_desc: dict[str, str]
253
+ ) -> dict[str, object] {
254
+ schema = _type_to_schema(func); # type: ignore
255
+ properties: dict[str, object] = schema.get("properties", {}); # type: ignore
256
+ required: list[str] = schema.get("required", []); # type: ignore
257
+ for (param_name, param_info) in properties.items() {
258
+ param_info["description"] = params_desc.get(param_name, ""); # type: ignore
259
+ }
260
+ return {
261
+ "type": "function",
262
+ "function": {
263
+ "name": func.__name__,
264
+ "description": description,
265
+ "parameters": {
266
+ "type": "object",
267
+ "properties": properties,
268
+ "required": required,
269
+ "additionalProperties": False,
270
+
271
+ },
272
+
273
+ },
274
+
275
+ };
276
+ }
277
+
278
+ """Convert a JSON dictionary to an instance of the given type."""
279
+ def json_to_instance(json_obj: dict, ty: type) -> object {
280
+ json_obj = _unwrap_from_object(json_obj);
281
+ json_obj = _decode_dict(json_obj);
282
+ return TypeAdapter(ty).validate_python(json_obj);
283
+ }