qtype 0.0.5__py3-none-any.whl → 0.0.7__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.
- qtype/commands/convert.py +18 -5
- qtype/commands/generate.py +16 -8
- qtype/commands/run.py +6 -83
- qtype/commands/serve.py +73 -0
- qtype/commands/validate.py +18 -8
- qtype/commands/visualize.py +87 -0
- qtype/commons/generate.py +9 -4
- qtype/converters/tools_from_module.py +69 -134
- qtype/converters/types.py +47 -1
- qtype/dsl/base_types.py +0 -1
- qtype/dsl/custom_types.py +73 -0
- qtype/dsl/document.py +27 -3
- qtype/dsl/domain_types.py +3 -0
- qtype/dsl/model.py +60 -73
- qtype/dsl/validator.py +20 -0
- qtype/interpreter/api.py +45 -12
- qtype/interpreter/chat/chat_api.py +237 -0
- qtype/interpreter/chat/file_conversions.py +57 -0
- qtype/interpreter/chat/vercel.py +314 -0
- qtype/interpreter/conversions.py +2 -0
- qtype/interpreter/steps/llm_inference.py +44 -19
- qtype/interpreter/streaming_helpers.py +123 -0
- qtype/interpreter/typing.py +29 -10
- qtype/interpreter/ui/404/index.html +1 -0
- qtype/interpreter/ui/404.html +1 -0
- qtype/interpreter/ui/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +36 -0
- qtype/interpreter/ui/_next/static/chunks/964-ed4ab073db645007.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/app/_not-found/page-e110d2a9d0a83d82.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/app/layout-f8f02d19bf177f87.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/main-6d261b6c5d6fb6c2.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/main-app-6fc6346bc8f7f163.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/webpack-ebad980006f664ca.js +1 -0
- qtype/interpreter/ui/_next/static/css/cd7a636f069c2cbd.css +3 -0
- qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
- qtype/interpreter/ui/_next/static/media/747892c23ea88013-s.woff2 +0 -0
- qtype/interpreter/ui/_next/static/media/8d697b304b401681-s.woff2 +0 -0
- qtype/interpreter/ui/_next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- qtype/interpreter/ui/_next/static/media/9610d9e46709d722-s.woff2 +0 -0
- qtype/interpreter/ui/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- qtype/interpreter/ui/_next/static/qlZrsNT9fTEe92CsYpHBB/_buildManifest.js +1 -0
- qtype/interpreter/ui/_next/static/qlZrsNT9fTEe92CsYpHBB/_ssgManifest.js +1 -0
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/interpreter/ui/file.svg +1 -0
- qtype/interpreter/ui/globe.svg +1 -0
- qtype/interpreter/ui/index.html +1 -0
- qtype/interpreter/ui/index.txt +22 -0
- qtype/interpreter/ui/next.svg +1 -0
- qtype/interpreter/ui/vercel.svg +1 -0
- qtype/interpreter/ui/window.svg +1 -0
- qtype/loader.py +57 -8
- qtype/semantic/generate.py +17 -5
- qtype/semantic/model.py +16 -24
- qtype/semantic/visualize.py +485 -0
- {qtype-0.0.5.dist-info → qtype-0.0.7.dist-info}/METADATA +28 -20
- qtype-0.0.7.dist-info/RECORD +91 -0
- qtype-0.0.5.dist-info/RECORD +0 -50
- {qtype-0.0.5.dist-info → qtype-0.0.7.dist-info}/WHEEL +0 -0
- {qtype-0.0.5.dist-info → qtype-0.0.7.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.5.dist-info → qtype-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.5.dist-info → qtype-0.0.7.dist-info}/top_level.txt +0 -0
|
@@ -1,44 +1,22 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
import inspect
|
|
3
|
-
from datetime import date, datetime, time
|
|
4
3
|
from typing import Any, Type, Union, get_args, get_origin
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel
|
|
7
6
|
|
|
7
|
+
from qtype.converters.types import PYTHON_TYPE_TO_PRIMITIVE_TYPE
|
|
8
|
+
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
8
9
|
from qtype.dsl.model import (
|
|
9
|
-
|
|
10
|
-
ObjectTypeDefinition,
|
|
11
|
-
PrimitiveTypeEnum,
|
|
10
|
+
CustomType,
|
|
12
11
|
PythonFunctionTool,
|
|
13
12
|
Variable,
|
|
14
13
|
VariableType,
|
|
15
14
|
)
|
|
16
15
|
|
|
17
|
-
VARIABLE_TO_TYPE = {
|
|
18
|
-
# VariableTypeEnum.audio: bytes, # TODO: Define a proper audio type
|
|
19
|
-
PrimitiveTypeEnum.boolean: bool, # No boolean type in enum, using Python's
|
|
20
|
-
PrimitiveTypeEnum.bytes: bytes,
|
|
21
|
-
PrimitiveTypeEnum.date: date,
|
|
22
|
-
PrimitiveTypeEnum.datetime: datetime,
|
|
23
|
-
PrimitiveTypeEnum.int: int,
|
|
24
|
-
# VariableTypeEnum.file: bytes, # TODO: Define a proper file type
|
|
25
|
-
# VariableTypeEnum.image: bytes, # TODO: Define a proper image type
|
|
26
|
-
PrimitiveTypeEnum.float: float,
|
|
27
|
-
PrimitiveTypeEnum.text: str,
|
|
28
|
-
PrimitiveTypeEnum.time: time,
|
|
29
|
-
# VariableTypeEnum.video: bytes, # TODO: Define a proper video type
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
TYPE_TO_VARIABLE = {v: k for k, v in VARIABLE_TO_TYPE.items()}
|
|
33
|
-
|
|
34
|
-
assert len(VARIABLE_TO_TYPE) == len(
|
|
35
|
-
TYPE_TO_VARIABLE
|
|
36
|
-
), "Variable to type mapping is not one-to-one"
|
|
37
|
-
|
|
38
16
|
|
|
39
17
|
def tools_from_module(
|
|
40
18
|
module_path: str,
|
|
41
|
-
) -> list[PythonFunctionTool]:
|
|
19
|
+
) -> tuple[list[PythonFunctionTool], list[CustomType]]:
|
|
42
20
|
"""
|
|
43
21
|
Load tools from a Python module by introspecting its functions.
|
|
44
22
|
|
|
@@ -64,11 +42,14 @@ def tools_from_module(
|
|
|
64
42
|
f"No public functions found in module '{module_path}'"
|
|
65
43
|
)
|
|
66
44
|
|
|
45
|
+
custom_types: dict[str, CustomType] = {}
|
|
46
|
+
|
|
67
47
|
# Create Tool instances from functions
|
|
68
|
-
|
|
69
|
-
_create_tool_from_function(func_name, func_info)
|
|
48
|
+
tools = [
|
|
49
|
+
_create_tool_from_function(func_name, func_info, custom_types)
|
|
70
50
|
for func_name, func_info in functions.items()
|
|
71
51
|
]
|
|
52
|
+
return (tools, list(custom_types.values()))
|
|
72
53
|
except ImportError as e:
|
|
73
54
|
raise ImportError(f"Cannot import module '{module_path}': {e}") from e
|
|
74
55
|
|
|
@@ -132,7 +113,9 @@ def _get_module_functions(
|
|
|
132
113
|
|
|
133
114
|
|
|
134
115
|
def _create_tool_from_function(
|
|
135
|
-
func_name: str,
|
|
116
|
+
func_name: str,
|
|
117
|
+
func_info: dict[str, Any],
|
|
118
|
+
custom_types: dict[str, CustomType],
|
|
136
119
|
) -> PythonFunctionTool:
|
|
137
120
|
"""
|
|
138
121
|
Convert function metadata into a Tool instance.
|
|
@@ -154,8 +137,8 @@ def _create_tool_from_function(
|
|
|
154
137
|
# Create input variables from function parameters
|
|
155
138
|
input_variables = [
|
|
156
139
|
Variable(
|
|
157
|
-
id=p["name"],
|
|
158
|
-
type=_map_python_type_to_variable_type(p["type"]),
|
|
140
|
+
id=func_name + "." + p["name"],
|
|
141
|
+
type=_map_python_type_to_variable_type(p["type"], custom_types),
|
|
159
142
|
)
|
|
160
143
|
for p in func_info["parameters"]
|
|
161
144
|
]
|
|
@@ -163,7 +146,9 @@ def _create_tool_from_function(
|
|
|
163
146
|
# Create output variable based on return type
|
|
164
147
|
tool_id = func_info["module"] + "." + func_name
|
|
165
148
|
|
|
166
|
-
output_type = _map_python_type_to_variable_type(
|
|
149
|
+
output_type = _map_python_type_to_variable_type(
|
|
150
|
+
func_info["return_type"], custom_types
|
|
151
|
+
)
|
|
167
152
|
|
|
168
153
|
output_variable = Variable(
|
|
169
154
|
id=f"{tool_id}.result",
|
|
@@ -181,89 +166,28 @@ def _create_tool_from_function(
|
|
|
181
166
|
)
|
|
182
167
|
|
|
183
168
|
|
|
184
|
-
def
|
|
185
|
-
pydantic_type: Type[Any], model_name: str
|
|
186
|
-
) -> VariableType | str:
|
|
187
|
-
"""
|
|
188
|
-
Recursively maps a Python/Pydantic type to a DSL Type Definition.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
pydantic_type: The type hint to map (e.g., str, list[int], MyPydanticModel).
|
|
192
|
-
model_name: The name of the model being processed, for generating unique IDs.
|
|
193
|
-
|
|
194
|
-
Returns:
|
|
195
|
-
A PrimitiveTypeEnum member, an ObjectTypeDefinition, an ArrayTypeDefinition,
|
|
196
|
-
or a string reference to another type.
|
|
197
|
-
"""
|
|
198
|
-
origin = get_origin(pydantic_type)
|
|
199
|
-
args = get_args(pydantic_type)
|
|
200
|
-
|
|
201
|
-
# --- Handle Lists ---
|
|
202
|
-
if origin in (list, list):
|
|
203
|
-
if not args:
|
|
204
|
-
raise TypeError(
|
|
205
|
-
"List types must be parameterized, e.g., list[str]."
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Recursively map the inner type of the list
|
|
209
|
-
inner_type = _map_type_to_dsl(args[0], model_name)
|
|
210
|
-
|
|
211
|
-
# Create a unique ID for this specific array definition
|
|
212
|
-
array_id = (
|
|
213
|
-
f"{model_name}.{getattr(inner_type, 'id', str(inner_type))}_Array"
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
return ArrayTypeDefinition(
|
|
217
|
-
id=array_id,
|
|
218
|
-
type=inner_type,
|
|
219
|
-
description=f"An array of {getattr(inner_type, 'id', inner_type)}.",
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# --- Handle Unions (specifically for Optional[T]) ---
|
|
223
|
-
if origin is Union:
|
|
224
|
-
# Filter out NoneType to handle Optional[T]
|
|
225
|
-
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
226
|
-
if len(non_none_args) == 1:
|
|
227
|
-
return _map_type_to_dsl(non_none_args[0], model_name)
|
|
228
|
-
else:
|
|
229
|
-
# For more complex unions, you might decide on a specific handling strategy.
|
|
230
|
-
# For now, we'll raise an error as it's ambiguous.
|
|
231
|
-
raise TypeError(
|
|
232
|
-
"Complex Union types are not supported for automatic conversion."
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
# --- Handle Nested Pydantic Models ---
|
|
236
|
-
if inspect.isclass(pydantic_type) and issubclass(pydantic_type, BaseModel):
|
|
237
|
-
# If it's a nested model, recursively call the main function.
|
|
238
|
-
# This returns a full definition for the nested object.
|
|
239
|
-
return pydantic_to_object_definition(pydantic_type)
|
|
240
|
-
|
|
241
|
-
# --- Handle Primitive Types ---
|
|
242
|
-
# This could be expanded with more sophisticated mapping.
|
|
243
|
-
|
|
244
|
-
if pydantic_type in TYPE_TO_VARIABLE:
|
|
245
|
-
return TYPE_TO_VARIABLE[pydantic_type]
|
|
246
|
-
|
|
247
|
-
raise TypeError(f"Unsupported type for DSL conversion: {pydantic_type}")
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def pydantic_to_object_definition(
|
|
169
|
+
def _pydantic_to_custom_types(
|
|
251
170
|
model_cls: Type[BaseModel],
|
|
252
|
-
|
|
171
|
+
custom_types: dict[str, CustomType],
|
|
172
|
+
) -> str:
|
|
253
173
|
"""
|
|
254
|
-
Converts a Pydantic BaseModel class into a QType
|
|
174
|
+
Converts a Pydantic BaseModel class into a QType CustomType.
|
|
175
|
+
This function extracts the model's fields and their types, converting them
|
|
176
|
+
into a CustomType definition.
|
|
255
177
|
|
|
256
|
-
|
|
257
|
-
into
|
|
178
|
+
If multiple nested types are found, they are recursively converted
|
|
179
|
+
into CustomType definitions.
|
|
258
180
|
|
|
259
181
|
Args:
|
|
260
182
|
model_cls: The Pydantic model class to convert.
|
|
261
183
|
|
|
262
184
|
Returns:
|
|
263
|
-
|
|
185
|
+
A dictionary mapping field names to their corresponding CustomType definitions.
|
|
264
186
|
"""
|
|
265
187
|
properties = {}
|
|
266
188
|
model_name = model_cls.__name__
|
|
189
|
+
if model_name in custom_types:
|
|
190
|
+
return model_name # Already processed
|
|
267
191
|
|
|
268
192
|
for field_name, field_info in model_cls.model_fields.items():
|
|
269
193
|
# Use the annotation (the type hint) for the field
|
|
@@ -272,19 +196,35 @@ def pydantic_to_object_definition(
|
|
|
272
196
|
raise TypeError(
|
|
273
197
|
f"Field '{field_name}' in '{model_name}' must have a type hint."
|
|
274
198
|
)
|
|
199
|
+
elif get_origin(field_type) is Union:
|
|
200
|
+
# Assume the union means it's optional
|
|
201
|
+
field_type = [
|
|
202
|
+
t for t in get_args(field_type) if t is not type(None)
|
|
203
|
+
][0]
|
|
204
|
+
rv = _map_python_type_to_type_str(field_type, custom_types)
|
|
205
|
+
properties[field_name] = f"{rv}?"
|
|
206
|
+
elif get_origin(field_type) is list:
|
|
207
|
+
inner_type = get_args(field_type)[0]
|
|
208
|
+
rv = _map_python_type_to_type_str(inner_type, custom_types)
|
|
209
|
+
properties[field_name] = f"list[{rv}]"
|
|
210
|
+
else:
|
|
211
|
+
properties[field_name] = _map_python_type_to_type_str(
|
|
212
|
+
field_type, custom_types
|
|
213
|
+
)
|
|
275
214
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return ObjectTypeDefinition(
|
|
215
|
+
# Add the custom type to the list
|
|
216
|
+
custom_types[model_name] = CustomType(
|
|
279
217
|
id=model_name,
|
|
280
|
-
description=model_cls.__doc__ or f"A definition for {model_name}.",
|
|
281
218
|
properties=properties,
|
|
219
|
+
description=model_cls.__doc__ or f"Custom type for {model_name}",
|
|
282
220
|
)
|
|
221
|
+
return model_name
|
|
283
222
|
|
|
284
223
|
|
|
285
224
|
def _map_python_type_to_variable_type(
|
|
286
|
-
python_type:
|
|
287
|
-
|
|
225
|
+
python_type: Any,
|
|
226
|
+
custom_types: dict[str, CustomType],
|
|
227
|
+
) -> str | VariableType:
|
|
288
228
|
"""
|
|
289
229
|
Map Python type annotations to QType VariableType.
|
|
290
230
|
|
|
@@ -295,32 +235,27 @@ def _map_python_type_to_variable_type(
|
|
|
295
235
|
VariableType compatible value.
|
|
296
236
|
"""
|
|
297
237
|
|
|
298
|
-
if python_type
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
except AttributeError:
|
|
307
|
-
pass
|
|
238
|
+
if python_type in PYTHON_TYPE_TO_PRIMITIVE_TYPE:
|
|
239
|
+
return PYTHON_TYPE_TO_PRIMITIVE_TYPE[python_type]
|
|
240
|
+
elif python_type in get_args(VariableType):
|
|
241
|
+
# If it's a domain type, return its name
|
|
242
|
+
return python_type
|
|
243
|
+
elif inspect.isclass(python_type) and issubclass(python_type, BaseModel):
|
|
244
|
+
# If it's a Pydantic model, create or retrieve its CustomType definition
|
|
245
|
+
return _pydantic_to_custom_types(python_type, custom_types)
|
|
308
246
|
raise ValueError(
|
|
309
247
|
f"Unsupported Python type '{python_type}' for VariableType mapping"
|
|
310
248
|
)
|
|
311
249
|
|
|
312
250
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
PrimitiveTypeEnum
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
PrimitiveTypeEnum.time: time,
|
|
325
|
-
# VariableTypeEnum.video: bytes, # TODO: Define a proper video type
|
|
326
|
-
}
|
|
251
|
+
def _map_python_type_to_type_str(
|
|
252
|
+
python_type: Any,
|
|
253
|
+
custom_types: dict[str, CustomType],
|
|
254
|
+
) -> str:
|
|
255
|
+
var_type = _map_python_type_to_variable_type(python_type, custom_types)
|
|
256
|
+
if isinstance(var_type, PrimitiveTypeEnum):
|
|
257
|
+
return var_type.value
|
|
258
|
+
elif inspect.isclass(python_type):
|
|
259
|
+
return python_type.__name__
|
|
260
|
+
else:
|
|
261
|
+
return str(python_type)
|
qtype/converters/types.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from datetime import date, datetime, time
|
|
2
|
+
|
|
1
3
|
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
2
4
|
|
|
3
5
|
"""
|
|
@@ -13,8 +15,52 @@ PRIMITIVE_TO_PYTHON_TYPE = {
|
|
|
13
15
|
PrimitiveTypeEnum.file: bytes, # Use bytes for file content
|
|
14
16
|
PrimitiveTypeEnum.float: float,
|
|
15
17
|
PrimitiveTypeEnum.image: bytes, # Use bytes for image data
|
|
16
|
-
PrimitiveTypeEnum.number: float, # Use float for number representation
|
|
17
18
|
PrimitiveTypeEnum.text: str,
|
|
18
19
|
PrimitiveTypeEnum.time: str, # Use str for time representation
|
|
19
20
|
PrimitiveTypeEnum.video: bytes, # Use bytes for video data
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
PYTHON_TYPE_TO_PRIMITIVE_TYPE = {
|
|
24
|
+
bytes: PrimitiveTypeEnum.file,
|
|
25
|
+
bool: PrimitiveTypeEnum.boolean,
|
|
26
|
+
str: PrimitiveTypeEnum.text,
|
|
27
|
+
int: PrimitiveTypeEnum.int,
|
|
28
|
+
float: PrimitiveTypeEnum.float,
|
|
29
|
+
date: PrimitiveTypeEnum.date,
|
|
30
|
+
datetime: PrimitiveTypeEnum.datetime,
|
|
31
|
+
time: PrimitiveTypeEnum.time,
|
|
32
|
+
# TODO: decide on internal representation for images, video, and audio, or use annotation/hinting
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# def create_custom_type(model_cls: Type[BaseModel],) -> CustomType:
|
|
36
|
+
# """
|
|
37
|
+
# Create a CustomType from a Pydantic BaseModel.
|
|
38
|
+
|
|
39
|
+
# Args:
|
|
40
|
+
# type: The Pydantic BaseModel class.
|
|
41
|
+
|
|
42
|
+
# Returns:
|
|
43
|
+
# A CustomType instance representing the model.
|
|
44
|
+
# """
|
|
45
|
+
|
|
46
|
+
# properties = {}
|
|
47
|
+
# for field_name, field_info in model_cls.model_fields.items():
|
|
48
|
+
# # Use the annotation (the type hint) for the field
|
|
49
|
+
# field_type = field_info.annotation
|
|
50
|
+
# if field_type is None:
|
|
51
|
+
# raise TypeError(
|
|
52
|
+
# f"Field '{field_name}' in '{model_name}' must have a type hint."
|
|
53
|
+
# )
|
|
54
|
+
# origin = get_origin(field_type)
|
|
55
|
+
|
|
56
|
+
# if origin is Union:
|
|
57
|
+
# # Assume the union means it's optional
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# return CustomType(
|
|
61
|
+
# id=type.__name__,
|
|
62
|
+
# properties={
|
|
63
|
+
# name: python_type_to_variable_type(field.type_)
|
|
64
|
+
# for name, field in type.__fields__.items()
|
|
65
|
+
# },
|
|
66
|
+
# )
|
qtype/dsl/base_types.py
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Any, ForwardRef, Type, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, create_model
|
|
4
|
+
|
|
5
|
+
from qtype.converters.types import PRIMITIVE_TO_PYTHON_TYPE
|
|
6
|
+
|
|
7
|
+
# --- This would be in your interpreter's logic ---
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_dynamic_types(
|
|
11
|
+
type_definitions: list[dict]
|
|
12
|
+
) -> dict[str, Type[BaseModel]]:
|
|
13
|
+
"""
|
|
14
|
+
Parses a list of simplified type definitions and dynamically creates
|
|
15
|
+
Pydantic BaseModel classes. It handles out-of-order definitions using
|
|
16
|
+
a two-pass approach with ForwardRef.
|
|
17
|
+
"""
|
|
18
|
+
created_models: dict[str, Type[BaseModel]] = {}
|
|
19
|
+
|
|
20
|
+
PRIMITIVE_MAP = {k.value: v for k, v in PRIMITIVE_TO_PYTHON_TYPE.items()}
|
|
21
|
+
|
|
22
|
+
def _parse_type_string(type_str: str) -> tuple[Any, bool]:
|
|
23
|
+
"""
|
|
24
|
+
Parses a type string and returns the resolved type and a boolean
|
|
25
|
+
indicating if the type is optional.
|
|
26
|
+
"""
|
|
27
|
+
is_optional = False
|
|
28
|
+
if type_str.endswith("?"):
|
|
29
|
+
is_optional = True
|
|
30
|
+
type_str = type_str[:-1]
|
|
31
|
+
|
|
32
|
+
if type_str.startswith("list[") and type_str.endswith("]"):
|
|
33
|
+
inner_type_name = type_str[5:-1]
|
|
34
|
+
inner_type, _ = _parse_type_string(inner_type_name)
|
|
35
|
+
resolved_type = list[inner_type]
|
|
36
|
+
elif type_str in PRIMITIVE_MAP:
|
|
37
|
+
resolved_type = PRIMITIVE_MAP[type_str]
|
|
38
|
+
elif type_str in created_models:
|
|
39
|
+
resolved_type = created_models[type_str]
|
|
40
|
+
else:
|
|
41
|
+
# If the type isn't defined yet, create a ForwardRef placeholder.
|
|
42
|
+
# This is the key to handling out-of-order definitions.
|
|
43
|
+
resolved_type = ForwardRef(type_str)
|
|
44
|
+
|
|
45
|
+
if is_optional:
|
|
46
|
+
return Union[resolved_type, None], True
|
|
47
|
+
|
|
48
|
+
return resolved_type, False
|
|
49
|
+
|
|
50
|
+
# --- Pass 1: Create all models, using ForwardRef for unresolved types ---
|
|
51
|
+
for type_def in type_definitions:
|
|
52
|
+
model_name = type_def["id"]
|
|
53
|
+
field_definitions = {}
|
|
54
|
+
|
|
55
|
+
if "properties" in type_def:
|
|
56
|
+
for field_name, type_str in type_def["properties"].items():
|
|
57
|
+
resolved_type, is_optional = _parse_type_string(type_str)
|
|
58
|
+
default_value = None if is_optional else ...
|
|
59
|
+
field_definitions[field_name] = (resolved_type, default_value)
|
|
60
|
+
|
|
61
|
+
# Pass the created_models dict as the local namespace for resolution
|
|
62
|
+
DynamicModel = create_model(
|
|
63
|
+
model_name, **field_definitions, __localns=created_models
|
|
64
|
+
)
|
|
65
|
+
created_models[model_name] = DynamicModel
|
|
66
|
+
|
|
67
|
+
# --- Pass 2: Resolve all ForwardRef placeholders ---
|
|
68
|
+
# This rebuilds the models, linking the placeholders to the actual classes.
|
|
69
|
+
# We pass `created_models` as the types_namespace for resolution in Pydantic V2.
|
|
70
|
+
for model in created_models.values():
|
|
71
|
+
model.model_rebuild(_types_namespace=created_models)
|
|
72
|
+
|
|
73
|
+
return created_models
|
qtype/dsl/document.py
CHANGED
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Any, Type, Union, get_args, get_origin
|
|
4
4
|
|
|
5
5
|
import qtype.dsl.model as dsl
|
|
6
|
+
from qtype.dsl.base_types import PrimitiveTypeEnum
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def _format_type_name(field_type: Any) -> str:
|
|
@@ -56,8 +57,13 @@ def generate_class_docstring(output_file: Path, cls: Type[Any]) -> None:
|
|
|
56
57
|
class_name = cls.__name__
|
|
57
58
|
docstring = cls.__doc__ or "No documentation available."
|
|
58
59
|
|
|
60
|
+
# new lines in the docstring often have indentation, which we want to remove
|
|
61
|
+
docstring = "\n".join(
|
|
62
|
+
line.strip() for line in docstring.splitlines() if line.strip()
|
|
63
|
+
)
|
|
64
|
+
|
|
59
65
|
with output_file.open("w", encoding="utf-8") as file:
|
|
60
|
-
file.write(f"
|
|
66
|
+
file.write(f"### {class_name}\n\n{docstring}\n\n")
|
|
61
67
|
|
|
62
68
|
# Handle Pydantic models by checking for model_fields
|
|
63
69
|
if hasattr(cls, "model_fields") and hasattr(cls, "__annotations__"):
|
|
@@ -93,8 +99,6 @@ def generate_class_docstring(output_file: Path, cls: Type[Any]) -> None:
|
|
|
93
99
|
)
|
|
94
100
|
file.write(f"- **{name}**: {member_doc}\n")
|
|
95
101
|
|
|
96
|
-
file.write("\n---\n")
|
|
97
|
-
|
|
98
102
|
|
|
99
103
|
def generate_documentation(output_prefix: Path) -> None:
|
|
100
104
|
"""Generates markdown documentation for all DSL classes.
|
|
@@ -102,7 +106,27 @@ def generate_documentation(output_prefix: Path) -> None:
|
|
|
102
106
|
Args:
|
|
103
107
|
output_prefix (Path): The directory where the documentation files will be saved.
|
|
104
108
|
"""
|
|
109
|
+
# erase everything in the output directory
|
|
110
|
+
output_prefix.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
for item in output_prefix.iterdir():
|
|
112
|
+
if item.is_dir():
|
|
113
|
+
for subitem in item.iterdir():
|
|
114
|
+
if subitem.is_file():
|
|
115
|
+
subitem.unlink()
|
|
116
|
+
item.rmdir()
|
|
117
|
+
elif item.is_file():
|
|
118
|
+
item.unlink()
|
|
119
|
+
|
|
105
120
|
# Get all classes from the DSL model module
|
|
106
121
|
for name, cls in inspect.getmembers(dsl, inspect.isclass): # noqa: F821
|
|
107
122
|
if cls.__module__ == dsl.__name__ and not name.startswith("_"):
|
|
108
123
|
generate_class_docstring(output_prefix / f"{name}.md", cls)
|
|
124
|
+
generate_class_docstring(
|
|
125
|
+
output_prefix / "PrimitiveTypeEnum.md", PrimitiveTypeEnum
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
for name, cls in inspect.getmembers(dsl.domain_types, inspect.isclass): # noqa: F821
|
|
129
|
+
if cls.__module__ == dsl.domain_types.__name__ and not name.startswith(
|
|
130
|
+
"_"
|
|
131
|
+
):
|
|
132
|
+
generate_class_docstring(output_prefix / f"{name}.md", cls)
|
qtype/dsl/domain_types.py
CHANGED
|
@@ -41,6 +41,9 @@ class ChatContent(StrictBaseModel):
|
|
|
41
41
|
...,
|
|
42
42
|
description="The actual content, which can be a string, image data, etc.",
|
|
43
43
|
)
|
|
44
|
+
mime_type: str | None = Field(
|
|
45
|
+
default=None, description="The MIME type of the content, if known."
|
|
46
|
+
)
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
class ChatMessage(StrictBaseModel):
|