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/__init__.py +24 -0
- byllm/lib.py +10 -0
- byllm/llm.jac +361 -0
- byllm/mtir.jac +200 -0
- byllm/plugin.py +55 -0
- byllm/schema.jac +283 -0
- byllm/types.jac +471 -0
- byllm-0.4.8.dist-info/METADATA +188 -0
- byllm-0.4.8.dist-info/RECORD +11 -0
- byllm-0.4.8.dist-info/WHEEL +4 -0
- byllm-0.4.8.dist-info/entry_points.txt +3 -0
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
|
+
}
|