qtype 0.0.16__py3-none-any.whl → 0.1.1__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/application/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +5 -5
- qtype/application/converters/tools_from_module.py +2 -2
- qtype/application/converters/types.py +14 -43
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +94 -73
- qtype/base/types.py +227 -7
- qtype/cli.py +4 -0
- qtype/commands/convert.py +20 -8
- qtype/commands/generate.py +19 -27
- qtype/commands/run.py +73 -36
- qtype/commands/serve.py +74 -54
- qtype/commands/validate.py +34 -8
- qtype/commands/visualize.py +46 -22
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +65 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +612 -363
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +57 -136
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +436 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +74 -0
- qtype/interpreter/base/factory.py +117 -0
- qtype/interpreter/base/progress_tracker.py +110 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +74 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +462 -22
- qtype/interpreter/converters.py +77 -0
- qtype/interpreter/endpoints.py +355 -0
- qtype/interpreter/executors/agent_executor.py +242 -0
- qtype/interpreter/executors/aggregate_executor.py +93 -0
- qtype/interpreter/executors/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +107 -0
- qtype/interpreter/executors/document_search_executor.py +122 -0
- qtype/interpreter/executors/document_source_executor.py +118 -0
- qtype/interpreter/executors/document_splitter_executor.py +105 -0
- qtype/interpreter/executors/echo_executor.py +63 -0
- qtype/interpreter/executors/field_extractor_executor.py +160 -0
- qtype/interpreter/executors/file_source_executor.py +101 -0
- qtype/interpreter/executors/file_writer_executor.py +110 -0
- qtype/interpreter/executors/index_upsert_executor.py +228 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +358 -0
- qtype/interpreter/executors/llm_inference_executor.py +272 -0
- qtype/interpreter/executors/prompt_template_executor.py +78 -0
- qtype/interpreter/executors/sql_source_executor.py +106 -0
- qtype/interpreter/executors/vector_search_executor.py +91 -0
- qtype/interpreter/flow.py +159 -22
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/rich_progress.py +225 -0
- qtype/interpreter/stream/chat/__init__.py +15 -0
- qtype/interpreter/stream/chat/converter.py +391 -0
- qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
- qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
- qtype/interpreter/stream/chat/vercel.py +609 -0
- qtype/interpreter/stream/utils/__init__.py +15 -0
- qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
- qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
- qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
- qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
- qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
- qtype/interpreter/telemetry.py +135 -8
- qtype/interpreter/tools/__init__.py +5 -0
- qtype/interpreter/tools/function_tool_helper.py +265 -0
- qtype/interpreter/types.py +330 -0
- qtype/interpreter/typing.py +83 -89
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
- qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
- qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +4 -4
- qtype/semantic/checker.py +583 -0
- qtype/semantic/generate.py +262 -83
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +436 -159
- qtype/semantic/resolver.py +63 -19
- qtype/semantic/visualize.py +28 -31
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/METADATA +16 -3
- qtype-0.1.1.dist-info/RECORD +135 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -465
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/file_sink_source.py +0 -162
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -92
- qtype/interpreter/batch/step.py +0 -74
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -178
- qtype/interpreter/chat/chat_api.py +0 -237
- qtype/interpreter/chat/vercel.py +0 -314
- qtype/interpreter/exceptions.py +0 -10
- qtype/interpreter/step.py +0 -67
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +0 -114
- qtype/interpreter/steps/condition.py +0 -36
- qtype/interpreter/steps/decoder.py +0 -88
- qtype/interpreter/steps/llm_inference.py +0 -171
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -219
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -390
- qtype-0.0.16.dist-info/RECORD +0 -106
- /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/WHEEL +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/top_level.txt +0 -0
qtype/semantic/generate.py
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import inspect
|
|
3
3
|
import subprocess
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from functools import partial
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from textwrap import dedent
|
|
6
|
-
from typing import Any, Literal, Union, get_args, get_origin
|
|
8
|
+
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
7
9
|
|
|
8
10
|
import networkx as nx
|
|
9
11
|
|
|
12
|
+
import qtype.base.types as base_types
|
|
10
13
|
import qtype.dsl.model as dsl
|
|
11
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_dsl_type(type_obj: Any) -> bool:
|
|
17
|
+
"""Check if a type is a DSL type that should be converted to semantic."""
|
|
18
|
+
if not hasattr(type_obj, "__name__"):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
# Check if it's defined in the DSL module
|
|
22
|
+
return (
|
|
23
|
+
hasattr(type_obj, "__module__")
|
|
24
|
+
and (
|
|
25
|
+
type_obj.__module__ == dsl.__name__
|
|
26
|
+
or type_obj.__module__ == base_types.__name__
|
|
27
|
+
)
|
|
28
|
+
and not type_obj.__name__.startswith("_")
|
|
29
|
+
)
|
|
30
|
+
|
|
12
31
|
|
|
13
32
|
FIELDS_TO_IGNORE = {"Application.references"}
|
|
14
33
|
TYPES_TO_IGNORE = {
|
|
15
34
|
"CustomType",
|
|
16
35
|
"DecoderFormat",
|
|
17
36
|
"Document",
|
|
18
|
-
"Flow",
|
|
19
37
|
"ListType",
|
|
20
38
|
"PrimitiveTypeEnum",
|
|
21
39
|
"StrictBaseModel",
|
|
22
|
-
"StructuralTypeEnum",
|
|
23
40
|
"TypeDefinition",
|
|
24
41
|
"ToolParameter",
|
|
25
42
|
"Variable",
|
|
@@ -78,7 +95,6 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
|
|
|
78
95
|
cls.__module__ == dsl.__name__
|
|
79
96
|
and not name.startswith("_")
|
|
80
97
|
and name not in TYPES_TO_IGNORE
|
|
81
|
-
and not name.endswith("List")
|
|
82
98
|
):
|
|
83
99
|
dsl_classes.append((name, cls))
|
|
84
100
|
|
|
@@ -117,18 +133,19 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
|
|
|
117
133
|
dedent("""
|
|
118
134
|
from __future__ import annotations
|
|
119
135
|
|
|
120
|
-
from
|
|
136
|
+
from functools import partial
|
|
137
|
+
from typing import Any, Literal, Union
|
|
121
138
|
|
|
122
|
-
from pydantic import BaseModel, Field,
|
|
139
|
+
from pydantic import BaseModel, Field, RootModel
|
|
123
140
|
|
|
124
|
-
# Import enums and type aliases
|
|
141
|
+
# Import enums, mixins, and type aliases
|
|
142
|
+
from qtype.base.types import BatchableStepMixin, BatchConfig, CachedStepMixin, ConcurrencyConfig, ConcurrentStepMixin # noqa: F401
|
|
125
143
|
from qtype.dsl.model import ( # noqa: F401
|
|
126
144
|
CustomType,
|
|
127
145
|
DecoderFormat,
|
|
128
146
|
ListType,
|
|
129
147
|
PrimitiveTypeEnum,
|
|
130
148
|
StepCardinality,
|
|
131
|
-
StructuralTypeEnum,
|
|
132
149
|
ToolParameter
|
|
133
150
|
)
|
|
134
151
|
from qtype.dsl.model import Variable as DSLVariable # noqa: F401
|
|
@@ -153,37 +170,18 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
|
|
|
153
170
|
# Write classes
|
|
154
171
|
f.write("\n\n".join(generated))
|
|
155
172
|
|
|
156
|
-
# Write the
|
|
157
|
-
f.write("\n\n")
|
|
173
|
+
# Write the DocumentType
|
|
158
174
|
f.write(
|
|
159
|
-
dedent(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
cardinality: StepCardinality = Field(
|
|
170
|
-
StepCardinality.auto,
|
|
171
|
-
description="The cardinality of the flow, inferred from its steps when set to 'auto'.",
|
|
172
|
-
)
|
|
173
|
-
mode: Literal["Complete", "Chat"] = Field("Complete")
|
|
174
|
-
steps: list[Step] = Field(..., description="List of steps or step IDs.")
|
|
175
|
-
|
|
176
|
-
@model_validator(mode="after")
|
|
177
|
-
def infer_cardinality(self) -> "Flow":
|
|
178
|
-
if self.cardinality == StepCardinality.auto:
|
|
179
|
-
self.cardinality = StepCardinality.one
|
|
180
|
-
for step in self.steps:
|
|
181
|
-
if step.cardinality == StepCardinality.many:
|
|
182
|
-
self.cardinality = StepCardinality.many
|
|
183
|
-
break
|
|
184
|
-
return self
|
|
185
|
-
|
|
186
|
-
''').lstrip()
|
|
175
|
+
dedent("""\n\n
|
|
176
|
+
DocumentType = Union[
|
|
177
|
+
Application,
|
|
178
|
+
AuthorizationProviderList,
|
|
179
|
+
ModelList,
|
|
180
|
+
ToolList,
|
|
181
|
+
TypeList,
|
|
182
|
+
VariableList,
|
|
183
|
+
]
|
|
184
|
+
""")
|
|
187
185
|
)
|
|
188
186
|
|
|
189
187
|
# Format the file with Ruff
|
|
@@ -201,51 +199,142 @@ def format_with_ruff(file_path: str) -> None:
|
|
|
201
199
|
subprocess.run(["isort", file_path], check=True)
|
|
202
200
|
|
|
203
201
|
|
|
202
|
+
def _get_union_args(type_annotation):
|
|
203
|
+
"""Extract union args from a type, handling Annotated types."""
|
|
204
|
+
if get_origin(type_annotation) is Annotated:
|
|
205
|
+
# For Annotated[Union[...], ...], get the Union part
|
|
206
|
+
union_type = get_args(type_annotation)[0]
|
|
207
|
+
return get_args(union_type)
|
|
208
|
+
else:
|
|
209
|
+
return get_args(type_annotation)
|
|
210
|
+
|
|
211
|
+
|
|
204
212
|
DSL_ONLY_UNION_TYPES = {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
_get_union_args(dsl.ToolType): "Tool",
|
|
214
|
+
_get_union_args(dsl.StepType): "Step",
|
|
215
|
+
_get_union_args(dsl.AuthProviderType): "AuthorizationProvider",
|
|
216
|
+
_get_union_args(dsl.SecretManagerType): "SecretManager",
|
|
217
|
+
_get_union_args(dsl.SourceType): "Source",
|
|
218
|
+
_get_union_args(dsl.IndexType): "Index",
|
|
219
|
+
_get_union_args(dsl.ModelType): "Model",
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
|
|
223
|
+
def _is_dsl_only_union(args_without_str_none: tuple) -> tuple[bool, str]:
|
|
224
|
+
"""
|
|
225
|
+
Check if union represents a DSL-only type pattern.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
args_without_str_none: Union args with str and None filtered out
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Tuple of (is_dsl_only, semantic_type_name)
|
|
232
|
+
"""
|
|
233
|
+
if args_without_str_none and args_without_str_none in DSL_ONLY_UNION_TYPES:
|
|
234
|
+
return True, DSL_ONLY_UNION_TYPES[args_without_str_none]
|
|
235
|
+
return False, ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _resolve_optional_collection(args: tuple, has_none: bool) -> str | None:
|
|
239
|
+
"""
|
|
240
|
+
Handle list|None -> list pattern (empty collection = None).
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
args: Union type arguments
|
|
244
|
+
has_none: Whether None is in the union
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Resolved type name if pattern matches, None otherwise
|
|
248
|
+
"""
|
|
249
|
+
if len(args) == 2 and has_none:
|
|
250
|
+
collection_types = [
|
|
251
|
+
arg for arg in args if get_origin(arg) in {list, dict}
|
|
252
|
+
]
|
|
253
|
+
if collection_types:
|
|
254
|
+
return dsl_to_semantic_type_name(collection_types[0])
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _is_id_reference_pattern(
|
|
259
|
+
args: tuple, has_str: bool, has_secret_ref: bool
|
|
260
|
+
) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Check if union represents an ID reference pattern (str | Type).
|
|
263
|
+
|
|
264
|
+
ID references allow DSL to use string IDs that get resolved to objects
|
|
265
|
+
in the semantic model. Exception: str | SecretReference stays as-is.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
args: Union type arguments
|
|
269
|
+
has_str: Whether str is in the union
|
|
270
|
+
has_secret_ref: Whether SecretReference is in the union
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if this is an ID reference pattern
|
|
274
|
+
"""
|
|
275
|
+
return (
|
|
276
|
+
any(_is_dsl_type(arg) for arg in args)
|
|
277
|
+
and has_str
|
|
278
|
+
and not has_secret_ref
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _strip_str_from_union(args: tuple) -> tuple:
|
|
283
|
+
"""
|
|
284
|
+
Remove str component from union for ID reference pattern.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
args: Union type arguments
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Args with str filtered out
|
|
291
|
+
"""
|
|
292
|
+
return tuple(arg for arg in args if arg is not str)
|
|
293
|
+
|
|
294
|
+
|
|
212
295
|
def _transform_union_type(args: tuple) -> str:
|
|
213
|
-
"""
|
|
296
|
+
"""
|
|
297
|
+
Transform Union types, handling string ID references and special cases.
|
|
298
|
+
|
|
299
|
+
This function handles the semantic type generation for union types,
|
|
300
|
+
with special handling for:
|
|
301
|
+
- DSL-only types (e.g., ToolType -> Tool)
|
|
302
|
+
- ID references (str | SomeType -> SomeType)
|
|
303
|
+
- SecretReference (str | SecretReference stays as-is)
|
|
304
|
+
- Optional types (Type | None)
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
args: Tuple of types in the union
|
|
214
308
|
|
|
309
|
+
Returns:
|
|
310
|
+
String representation of the semantic type
|
|
311
|
+
"""
|
|
312
|
+
# Import SecretReference for direct type comparison
|
|
313
|
+
from qtype.dsl.model import SecretReference
|
|
314
|
+
|
|
315
|
+
# Pre-compute type characteristics
|
|
215
316
|
args_without_str_none = tuple(
|
|
216
317
|
arg for arg in args if arg is not str and arg is not type(None)
|
|
217
318
|
)
|
|
218
319
|
has_none = any(arg is type(None) for arg in args)
|
|
219
320
|
has_str = any(arg is str for arg in args)
|
|
321
|
+
has_secret_ref = any(arg is SecretReference for arg in args)
|
|
220
322
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
if
|
|
224
|
-
if has_none
|
|
225
|
-
# If we have a DSL type and None, we return the DSL type with None
|
|
226
|
-
return DSL_ONLY_UNION_TYPES[args_without_str_none] + " | None"
|
|
227
|
-
else:
|
|
228
|
-
# Note we don't handle the case where we have a DSL type and str,
|
|
229
|
-
# because that would indicate a reference to an ID, which we handle separately.
|
|
230
|
-
return DSL_ONLY_UNION_TYPES[args_without_str_none]
|
|
231
|
-
|
|
232
|
-
# Handle the case where we have a list | None, which in the dsl is needed, but here we will just have an empty list.
|
|
233
|
-
if len(args) == 2:
|
|
234
|
-
list_elems = [
|
|
235
|
-
arg for arg in args if get_origin(arg) in set([list, dict])
|
|
236
|
-
]
|
|
237
|
-
if len(list_elems) > 0 and has_none:
|
|
238
|
-
# If we have a list and None, we return the list type
|
|
239
|
-
# This is to handle cases like List[SomeType] | None
|
|
240
|
-
# which in the DSL is needed, but here we will just have an empty list.
|
|
241
|
-
return dsl_to_semantic_type_name(list_elems[0])
|
|
242
|
-
|
|
243
|
-
# If the union contains a DSL type and a str, we need to drop the str
|
|
244
|
-
if any(_is_dsl_type(arg) for arg in args) and has_str:
|
|
245
|
-
# There is a DSL type and a str, which indicates something that can reference an ID.
|
|
246
|
-
# drop the str
|
|
247
|
-
args = tuple(arg for arg in args if arg is not str)
|
|
323
|
+
# Handle DSL-only union types (e.g., ToolType -> Tool)
|
|
324
|
+
is_dsl_only, dsl_semantic_name = _is_dsl_only_union(args_without_str_none)
|
|
325
|
+
if is_dsl_only:
|
|
326
|
+
return dsl_semantic_name + " | None" if has_none else dsl_semantic_name
|
|
248
327
|
|
|
328
|
+
# Handle list | None -> list (empty list is equivalent to None)
|
|
329
|
+
if resolved_collection := _resolve_optional_collection(args, has_none):
|
|
330
|
+
return resolved_collection
|
|
331
|
+
|
|
332
|
+
# Handle ID references: str | SomeType -> SomeType
|
|
333
|
+
# Exception: str | SecretReference should remain as-is
|
|
334
|
+
if _is_id_reference_pattern(args, has_str, has_secret_ref):
|
|
335
|
+
args = _strip_str_from_union(args)
|
|
336
|
+
|
|
337
|
+
# Convert remaining types to semantic type names
|
|
249
338
|
return " | ".join(dsl_to_semantic_type_name(a) for a in args)
|
|
250
339
|
|
|
251
340
|
|
|
@@ -263,6 +352,27 @@ def dsl_to_semantic_type_name(field_type: Any) -> str:
|
|
|
263
352
|
origin = get_origin(field_type)
|
|
264
353
|
args = get_args(field_type)
|
|
265
354
|
|
|
355
|
+
# Handle Reference types - unwrap to get the actual type
|
|
356
|
+
# Reference[T] is a Pydantic generic model, so get_origin returns None
|
|
357
|
+
# Instead, check if Reference is in the MRO
|
|
358
|
+
if (
|
|
359
|
+
hasattr(field_type, "__mro__")
|
|
360
|
+
and base_types.Reference in field_type.__mro__
|
|
361
|
+
):
|
|
362
|
+
# Reference[T] becomes just T in semantic model
|
|
363
|
+
# The actual type parameter is in __pydantic_generic_metadata__ for Pydantic generic models
|
|
364
|
+
if hasattr(field_type, "__pydantic_generic_metadata__"):
|
|
365
|
+
metadata = field_type.__pydantic_generic_metadata__
|
|
366
|
+
if "args" in metadata and metadata["args"]:
|
|
367
|
+
return dsl_to_semantic_type_name(metadata["args"][0])
|
|
368
|
+
return "Any" # Fallback for untyped Reference
|
|
369
|
+
|
|
370
|
+
# Handle Annotated types - extract the underlying type
|
|
371
|
+
if origin is Annotated:
|
|
372
|
+
# For Annotated[SomeType, ...], we want to process SomeType
|
|
373
|
+
if args:
|
|
374
|
+
return dsl_to_semantic_type_name(args[0])
|
|
375
|
+
|
|
266
376
|
if origin is Union or (
|
|
267
377
|
hasattr(field_type, "__class__")
|
|
268
378
|
and field_type.__class__.__name__ == "UnionType"
|
|
@@ -274,7 +384,13 @@ def dsl_to_semantic_type_name(field_type: Any) -> str:
|
|
|
274
384
|
# Format literal values
|
|
275
385
|
literal_values = []
|
|
276
386
|
for arg in args:
|
|
277
|
-
if isinstance(arg,
|
|
387
|
+
if isinstance(arg, Enum):
|
|
388
|
+
# Keep the enum reference for semantic models (e.g., StepCardinality.one)
|
|
389
|
+
# not the string value
|
|
390
|
+
enum_class_name = arg.__class__.__name__
|
|
391
|
+
enum_value_name = arg.name
|
|
392
|
+
literal_values.append(f"{enum_class_name}.{enum_value_name}")
|
|
393
|
+
elif isinstance(arg, str):
|
|
278
394
|
literal_values.append(f'"{arg}"')
|
|
279
395
|
else:
|
|
280
396
|
literal_values.append(str(arg))
|
|
@@ -323,16 +439,35 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
|
|
|
323
439
|
if inspect.isabstract(cls):
|
|
324
440
|
inheritance += ", ABC"
|
|
325
441
|
|
|
326
|
-
#
|
|
442
|
+
# Collect all base classes from DSL and base_types modules
|
|
443
|
+
base_classes = []
|
|
327
444
|
for base in cls.__bases__:
|
|
328
445
|
if (
|
|
329
446
|
hasattr(base, "__module__")
|
|
330
|
-
and
|
|
447
|
+
and (
|
|
448
|
+
base.__module__ == dsl.__name__
|
|
449
|
+
or base.__module__ == base_types.__name__
|
|
450
|
+
)
|
|
331
451
|
and base.__name__ not in TYPES_TO_IGNORE
|
|
332
452
|
and not base.__name__.startswith("_")
|
|
333
453
|
):
|
|
334
|
-
|
|
335
|
-
|
|
454
|
+
base_classes.append(base)
|
|
455
|
+
|
|
456
|
+
# Build inheritance string
|
|
457
|
+
if base_classes:
|
|
458
|
+
# Process DSL classes first, then mixins
|
|
459
|
+
dsl_bases = [
|
|
460
|
+
b.__name__ for b in base_classes if b.__module__ == dsl.__name__
|
|
461
|
+
]
|
|
462
|
+
mixin_bases = [
|
|
463
|
+
b.__name__
|
|
464
|
+
for b in base_classes
|
|
465
|
+
if b.__module__ == base_types.__name__
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
if dsl_bases:
|
|
469
|
+
# Inherit from the DSL class
|
|
470
|
+
semantic_base = dsl_bases[0]
|
|
336
471
|
if inspect.isabstract(cls):
|
|
337
472
|
inheritance = f"ABC, {semantic_base}"
|
|
338
473
|
else:
|
|
@@ -340,7 +475,15 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
|
|
|
340
475
|
if semantic_name == "Tool":
|
|
341
476
|
# Tools should inherit from Step and be immutable
|
|
342
477
|
inheritance = f"{semantic_base}, ImmutableModel"
|
|
343
|
-
|
|
478
|
+
|
|
479
|
+
# Add mixins to the inheritance - must come BEFORE BaseModel for correct MRO
|
|
480
|
+
if mixin_bases:
|
|
481
|
+
if inheritance == "BaseModel":
|
|
482
|
+
# Mixins must come before BaseModel
|
|
483
|
+
inheritance = f"{', '.join(mixin_bases)}, BaseModel"
|
|
484
|
+
else:
|
|
485
|
+
# If we have other bases, append mixins
|
|
486
|
+
inheritance = f"{inheritance}, {', '.join(mixin_bases)}"
|
|
344
487
|
|
|
345
488
|
# Get field information from the class - only fields defined on this class, not inherited
|
|
346
489
|
fields = []
|
|
@@ -354,6 +497,7 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
|
|
|
354
497
|
field_info = cls.model_fields[field_name]
|
|
355
498
|
field_type = field_info.annotation
|
|
356
499
|
field_default = field_info.default
|
|
500
|
+
field_default_factory = field_info.default_factory
|
|
357
501
|
field_description = getattr(field_info, "description", None)
|
|
358
502
|
|
|
359
503
|
# Transform the field type
|
|
@@ -369,7 +513,11 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
|
|
|
369
513
|
|
|
370
514
|
# Create field definition
|
|
371
515
|
field_def = create_field_definition(
|
|
372
|
-
field_name,
|
|
516
|
+
field_name,
|
|
517
|
+
semantic_type,
|
|
518
|
+
field_default,
|
|
519
|
+
field_default_factory,
|
|
520
|
+
field_description,
|
|
373
521
|
)
|
|
374
522
|
fields.append(field_def)
|
|
375
523
|
|
|
@@ -391,6 +539,7 @@ def create_field_definition(
|
|
|
391
539
|
field_name: str,
|
|
392
540
|
field_type: str,
|
|
393
541
|
field_default: Any,
|
|
542
|
+
field_default_factory: Any,
|
|
394
543
|
field_description: str | None,
|
|
395
544
|
) -> str:
|
|
396
545
|
"""Create a field definition string."""
|
|
@@ -401,11 +550,29 @@ def create_field_definition(
|
|
|
401
550
|
|
|
402
551
|
# Handle default values
|
|
403
552
|
# Check for PydanticUndefined (required field)
|
|
404
|
-
from enum import Enum
|
|
405
|
-
|
|
406
553
|
from pydantic_core import PydanticUndefined
|
|
407
554
|
|
|
408
|
-
|
|
555
|
+
# Check if there's a default_factory
|
|
556
|
+
if field_default_factory is not None:
|
|
557
|
+
# Handle default_factory - check if it's a partial
|
|
558
|
+
if isinstance(field_default_factory, partial):
|
|
559
|
+
# For partial, we need to serialize it properly
|
|
560
|
+
func_name = field_default_factory.func.__name__
|
|
561
|
+
# Get the keyword arguments from the partial
|
|
562
|
+
kwargs_str = ", ".join(
|
|
563
|
+
f"{k}={v}" if not isinstance(v, str) else f'{k}="{v}"'
|
|
564
|
+
for k, v in field_default_factory.keywords.items()
|
|
565
|
+
)
|
|
566
|
+
default_part = (
|
|
567
|
+
f"default_factory=partial({func_name}, {kwargs_str})"
|
|
568
|
+
)
|
|
569
|
+
else:
|
|
570
|
+
# Regular factory function
|
|
571
|
+
factory_name = getattr(
|
|
572
|
+
field_default_factory, "__name__", str(field_default_factory)
|
|
573
|
+
)
|
|
574
|
+
default_part = f"default_factory={factory_name}"
|
|
575
|
+
elif field_default is PydanticUndefined or field_default is ...:
|
|
409
576
|
default_part = "..."
|
|
410
577
|
elif field_default is None:
|
|
411
578
|
default_part = "None"
|
|
@@ -430,7 +597,12 @@ def create_field_definition(
|
|
|
430
597
|
default_part = str(field_default)
|
|
431
598
|
|
|
432
599
|
# Create Field definition
|
|
433
|
-
field_parts
|
|
600
|
+
# If using default_factory, don't include it in field_parts list initially
|
|
601
|
+
if field_default_factory is not None:
|
|
602
|
+
field_parts = []
|
|
603
|
+
else:
|
|
604
|
+
field_parts = [default_part]
|
|
605
|
+
|
|
434
606
|
if field_description:
|
|
435
607
|
# Escape quotes and handle multiline descriptions
|
|
436
608
|
escaped_desc = field_description.replace('"', '\\"').replace(
|
|
@@ -440,6 +612,13 @@ def create_field_definition(
|
|
|
440
612
|
if alias_part:
|
|
441
613
|
field_parts.append(alias_part.lstrip(", "))
|
|
442
614
|
|
|
443
|
-
|
|
615
|
+
# Handle default_factory in the Field() call
|
|
616
|
+
if field_default_factory is not None:
|
|
617
|
+
if field_parts:
|
|
618
|
+
field_def = f"Field({default_part}, {', '.join(field_parts)})"
|
|
619
|
+
else:
|
|
620
|
+
field_def = f"Field({default_part})"
|
|
621
|
+
else:
|
|
622
|
+
field_def = f"Field({', '.join(field_parts)})"
|
|
444
623
|
|
|
445
624
|
return f" {field_name}: {field_type} = {field_def}"
|
qtype/semantic/loader.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic model loading and resolution.
|
|
3
|
+
|
|
4
|
+
This is the main entry point for loading QType specifications.
|
|
5
|
+
Coordinates the pipeline: load → parse → link → resolve → check
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from qtype.base.types import CustomTypeRegistry
|
|
13
|
+
from qtype.dsl.linker import link
|
|
14
|
+
from qtype.dsl.loader import load_yaml_file, load_yaml_string
|
|
15
|
+
from qtype.dsl.parser import parse_document
|
|
16
|
+
from qtype.semantic.checker import check
|
|
17
|
+
from qtype.semantic.model import DocumentType
|
|
18
|
+
from qtype.semantic.resolver import resolve
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load(
|
|
22
|
+
source: str | Path,
|
|
23
|
+
) -> tuple[DocumentType, CustomTypeRegistry]:
|
|
24
|
+
"""
|
|
25
|
+
Load a QType YAML file, validate it, and return resolved semantic model.
|
|
26
|
+
|
|
27
|
+
This function coordinates the complete loading pipeline:
|
|
28
|
+
1. Load YAML (with env vars and includes)
|
|
29
|
+
2. Parse into DSL models (with custom type building)
|
|
30
|
+
3. Link references (resolve IDs to objects)
|
|
31
|
+
4. Resolve to semantic models (DSL → IR)
|
|
32
|
+
5. Check semantic rules
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
source: File path (str or Path) to load. Use load_from_string()
|
|
36
|
+
for raw YAML content.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (SemanticDocumentType, CustomTypeRegistry)
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
YAMLLoadError: If YAML parsing fails
|
|
43
|
+
ValueError: If validation fails
|
|
44
|
+
DuplicateComponentError: If duplicate IDs found
|
|
45
|
+
ReferenceNotFoundError: If reference resolution fails
|
|
46
|
+
QTypeSemanticError: If semantic rules violated
|
|
47
|
+
"""
|
|
48
|
+
# Load from file path
|
|
49
|
+
if isinstance(source, Path):
|
|
50
|
+
yaml_data = load_yaml_file(source)
|
|
51
|
+
else:
|
|
52
|
+
# Assume str is a file path
|
|
53
|
+
yaml_data = load_yaml_file(source)
|
|
54
|
+
|
|
55
|
+
dsl_doc, types = parse_document(yaml_data)
|
|
56
|
+
linked_doc = link(dsl_doc)
|
|
57
|
+
semantic_doc = resolve(linked_doc)
|
|
58
|
+
check(semantic_doc)
|
|
59
|
+
return semantic_doc, types
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_from_string(
|
|
63
|
+
content: str, base_path: str | Path | None = None
|
|
64
|
+
) -> tuple[DocumentType, CustomTypeRegistry]:
|
|
65
|
+
"""
|
|
66
|
+
Load a QType YAML from string content.
|
|
67
|
+
|
|
68
|
+
This function coordinates the complete loading pipeline:
|
|
69
|
+
1. Load YAML (with env vars and includes)
|
|
70
|
+
2. Parse into DSL models (with custom type building)
|
|
71
|
+
3. Link references (resolve IDs to objects)
|
|
72
|
+
4. Resolve to semantic models (DSL → IR)
|
|
73
|
+
5. Check semantic rules
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
content: Raw YAML content as string
|
|
77
|
+
base_path: Base path for resolving relative includes (default: cwd)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (SemanticDocumentType, CustomTypeRegistry)
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
YAMLLoadError: If YAML parsing fails
|
|
84
|
+
ValueError: If validation fails
|
|
85
|
+
DuplicateComponentError: If duplicate IDs found
|
|
86
|
+
ReferenceNotFoundError: If reference resolution fails
|
|
87
|
+
QTypeSemanticError: If semantic rules violated
|
|
88
|
+
"""
|
|
89
|
+
yaml_data = load_yaml_string(content, base_path=base_path)
|
|
90
|
+
|
|
91
|
+
dsl_doc, types = parse_document(yaml_data)
|
|
92
|
+
linked_doc = link(dsl_doc)
|
|
93
|
+
semantic_doc = resolve(linked_doc)
|
|
94
|
+
check(semantic_doc)
|
|
95
|
+
return semantic_doc, types
|