qtype 0.0.12__py3-none-any.whl → 0.1.3__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.
Files changed (137) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +476 -11
  3. qtype/application/converters/tools_from_module.py +38 -14
  4. qtype/application/converters/types.py +15 -30
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +102 -85
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +5 -1
  9. qtype/commands/convert.py +52 -6
  10. qtype/commands/generate.py +44 -4
  11. qtype/commands/run.py +78 -36
  12. qtype/commands/serve.py +74 -44
  13. qtype/commands/validate.py +37 -14
  14. qtype/commands/visualize.py +46 -25
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +86 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +751 -263
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +63 -136
  24. qtype/interpreter/auth/aws.py +19 -9
  25. qtype/interpreter/auth/generic.py +93 -16
  26. qtype/interpreter/base/base_step_executor.py +436 -0
  27. qtype/interpreter/base/batch_step_executor.py +171 -0
  28. qtype/interpreter/base/exceptions.py +50 -0
  29. qtype/interpreter/base/executor_context.py +91 -0
  30. qtype/interpreter/base/factory.py +84 -0
  31. qtype/interpreter/base/progress_tracker.py +110 -0
  32. qtype/interpreter/base/secrets.py +339 -0
  33. qtype/interpreter/base/step_cache.py +74 -0
  34. qtype/interpreter/base/stream_emitter.py +469 -0
  35. qtype/interpreter/conversions.py +471 -22
  36. qtype/interpreter/converters.py +79 -0
  37. qtype/interpreter/endpoints.py +355 -0
  38. qtype/interpreter/executors/agent_executor.py +242 -0
  39. qtype/interpreter/executors/aggregate_executor.py +93 -0
  40. qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
  41. qtype/interpreter/executors/decoder_executor.py +163 -0
  42. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  43. qtype/interpreter/executors/document_embedder_executor.py +107 -0
  44. qtype/interpreter/executors/document_search_executor.py +113 -0
  45. qtype/interpreter/executors/document_source_executor.py +118 -0
  46. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  47. qtype/interpreter/executors/echo_executor.py +63 -0
  48. qtype/interpreter/executors/field_extractor_executor.py +165 -0
  49. qtype/interpreter/executors/file_source_executor.py +101 -0
  50. qtype/interpreter/executors/file_writer_executor.py +110 -0
  51. qtype/interpreter/executors/index_upsert_executor.py +232 -0
  52. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  53. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  54. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  55. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  56. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  57. qtype/interpreter/executors/sql_source_executor.py +106 -0
  58. qtype/interpreter/executors/vector_search_executor.py +91 -0
  59. qtype/interpreter/flow.py +173 -22
  60. qtype/interpreter/logging_progress.py +61 -0
  61. qtype/interpreter/metadata_api.py +115 -0
  62. qtype/interpreter/resource_cache.py +5 -4
  63. qtype/interpreter/rich_progress.py +225 -0
  64. qtype/interpreter/stream/chat/__init__.py +15 -0
  65. qtype/interpreter/stream/chat/converter.py +391 -0
  66. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  67. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  68. qtype/interpreter/stream/chat/vercel.py +609 -0
  69. qtype/interpreter/stream/utils/__init__.py +15 -0
  70. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  71. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  72. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  73. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  74. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  75. qtype/interpreter/telemetry.py +135 -8
  76. qtype/interpreter/tools/__init__.py +5 -0
  77. qtype/interpreter/tools/function_tool_helper.py +265 -0
  78. qtype/interpreter/types.py +330 -0
  79. qtype/interpreter/typing.py +83 -89
  80. qtype/interpreter/ui/404/index.html +1 -1
  81. qtype/interpreter/ui/404.html +1 -1
  82. qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  83. qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
  84. qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
  85. qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
  86. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  87. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  88. qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
  89. qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
  90. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  91. qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  92. qtype/interpreter/ui/icon.png +0 -0
  93. qtype/interpreter/ui/index.html +1 -1
  94. qtype/interpreter/ui/index.txt +5 -5
  95. qtype/semantic/checker.py +643 -0
  96. qtype/semantic/generate.py +268 -85
  97. qtype/semantic/loader.py +95 -0
  98. qtype/semantic/model.py +535 -163
  99. qtype/semantic/resolver.py +63 -19
  100. qtype/semantic/visualize.py +50 -35
  101. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/METADATA +21 -4
  102. qtype-0.1.3.dist-info/RECORD +137 -0
  103. qtype/dsl/base_types.py +0 -38
  104. qtype/dsl/validator.py +0 -464
  105. qtype/interpreter/batch/__init__.py +0 -0
  106. qtype/interpreter/batch/flow.py +0 -95
  107. qtype/interpreter/batch/sql_source.py +0 -95
  108. qtype/interpreter/batch/step.py +0 -63
  109. qtype/interpreter/batch/types.py +0 -41
  110. qtype/interpreter/batch/utils.py +0 -179
  111. qtype/interpreter/chat/chat_api.py +0 -237
  112. qtype/interpreter/chat/vercel.py +0 -314
  113. qtype/interpreter/exceptions.py +0 -10
  114. qtype/interpreter/step.py +0 -67
  115. qtype/interpreter/steps/__init__.py +0 -0
  116. qtype/interpreter/steps/agent.py +0 -114
  117. qtype/interpreter/steps/condition.py +0 -36
  118. qtype/interpreter/steps/decoder.py +0 -88
  119. qtype/interpreter/steps/llm_inference.py +0 -150
  120. qtype/interpreter/steps/prompt_template.py +0 -54
  121. qtype/interpreter/steps/search.py +0 -24
  122. qtype/interpreter/steps/tool.py +0 -53
  123. qtype/interpreter/streaming_helpers.py +0 -123
  124. qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
  125. qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
  126. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  127. qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
  128. qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
  129. qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
  130. qtype/interpreter/ui/favicon.ico +0 -0
  131. qtype/loader.py +0 -389
  132. qtype-0.0.12.dist-info/RECORD +0 -105
  133. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  134. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/WHEEL +0 -0
  135. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/entry_points.txt +0 -0
  136. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/licenses/LICENSE +0 -0
  137. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,45 @@
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
- from qtype.dsl.validator import _is_dsl_type
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",
37
+ "ListType",
19
38
  "PrimitiveTypeEnum",
20
39
  "StrictBaseModel",
21
- "StructuralTypeEnum",
22
40
  "TypeDefinition",
41
+ "ToolParameter",
23
42
  "Variable",
24
- "VariableType",
25
43
  }
26
44
 
27
45
  FROZEN_TYPES = {
@@ -31,6 +49,7 @@ FROZEN_TYPES = {
31
49
  "Index",
32
50
  "Memory",
33
51
  "Model",
52
+ "Tool",
34
53
  "VectorIndex",
35
54
  }
36
55
 
@@ -76,7 +95,6 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
76
95
  cls.__module__ == dsl.__name__
77
96
  and not name.startswith("_")
78
97
  and name not in TYPES_TO_IGNORE
79
- and not name.endswith("List")
80
98
  ):
81
99
  dsl_classes.append((name, cls))
82
100
 
@@ -115,20 +133,23 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
115
133
  dedent("""
116
134
  from __future__ import annotations
117
135
 
118
- from typing import Any, Literal
136
+ from functools import partial
137
+ from typing import Any, Literal, Union
119
138
 
120
- from pydantic import BaseModel, Field, model_validator
139
+ from pydantic import BaseModel, Field, RootModel
121
140
 
122
- # Import enums and type aliases from DSL
123
- from qtype.dsl.model import VariableType # noqa: F401
141
+ # Import enums, mixins, and type aliases
142
+ from qtype.base.types import BatchableStepMixin, BatchConfig, CachedStepMixin, ConcurrencyConfig, ConcurrentStepMixin # noqa: F401
124
143
  from qtype.dsl.model import ( # noqa: F401
125
144
  CustomType,
126
145
  DecoderFormat,
146
+ ListType,
127
147
  PrimitiveTypeEnum,
128
148
  StepCardinality,
129
- StructuralTypeEnum,
149
+ ToolParameter
130
150
  )
131
151
  from qtype.dsl.model import Variable as DSLVariable # noqa: F401
152
+ from qtype.dsl.model import VariableType # noqa: F401
132
153
  from qtype.semantic.base_types import ImmutableModel
133
154
 
134
155
  """).lstrip()
@@ -149,37 +170,18 @@ def generate_semantic_model(args: argparse.Namespace) -> None:
149
170
  # Write classes
150
171
  f.write("\n\n".join(generated))
151
172
 
152
- # Write the Flow class which _could_ be generated but we want a validator to update it's carndiality
153
- f.write("\n\n")
173
+ # Write the DocumentType
154
174
  f.write(
155
- dedent('''
156
- class Flow(Step):
157
- """Defines a flow of steps that can be executed in sequence or parallel.
158
- If input or output variables are not specified, they are inferred from
159
- the first and last step, respectively.
160
- """
161
-
162
- description: str | None = Field(
163
- None, description="Optional description of the flow."
164
- )
165
- cardinality: StepCardinality = Field(
166
- StepCardinality.auto,
167
- description="The cardinality of the flow, inferred from its steps when set to 'auto'.",
168
- )
169
- mode: Literal["Complete", "Chat"] = Field("Complete")
170
- steps: list[Step] = Field(..., description="List of steps or step IDs.")
171
-
172
- @model_validator(mode="after")
173
- def infer_cardinality(self) -> "Flow":
174
- if self.cardinality == StepCardinality.auto:
175
- self.cardinality = StepCardinality.one
176
- for step in self.steps:
177
- if step.cardinality == StepCardinality.many:
178
- self.cardinality = StepCardinality.many
179
- break
180
- return self
181
-
182
- ''').lstrip()
175
+ dedent("""\n\n
176
+ DocumentType = Union[
177
+ Application,
178
+ AuthorizationProviderList,
179
+ ModelList,
180
+ ToolList,
181
+ TypeList,
182
+ VariableList,
183
+ ]
184
+ """)
183
185
  )
184
186
 
185
187
  # Format the file with Ruff
@@ -197,51 +199,142 @@ def format_with_ruff(file_path: str) -> None:
197
199
  subprocess.run(["isort", file_path], check=True)
198
200
 
199
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
+
200
212
  DSL_ONLY_UNION_TYPES = {
201
- get_args(dsl.ToolType): "Tool",
202
- get_args(dsl.StepType): "Step",
203
- get_args(dsl.IndexType): "Index",
204
- get_args(dsl.ModelType): "Model",
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",
205
220
  }
206
221
 
207
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
+
208
295
  def _transform_union_type(args: tuple) -> str:
209
- """Transform Union types, handling string ID references."""
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)
210
305
 
306
+ Args:
307
+ args: Tuple of types in the union
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
211
316
  args_without_str_none = tuple(
212
317
  arg for arg in args if arg is not str and arg is not type(None)
213
318
  )
214
319
  has_none = any(arg is type(None) for arg in args)
215
320
  has_str = any(arg is str for arg in args)
321
+ has_secret_ref = any(arg is SecretReference for arg in args)
216
322
 
217
- # First see if this is a DSL-only union type
218
- # If so, just return the corresponding semantic type
219
- if args_without_str_none in DSL_ONLY_UNION_TYPES:
220
- if has_none:
221
- # If we have a DSL type and None, we return the DSL type with None
222
- return DSL_ONLY_UNION_TYPES[args_without_str_none] + " | None"
223
- else:
224
- # Note we don't handle the case where we have a DSL type and str,
225
- # because that would indicate a reference to an ID, which we handle separately.
226
- return DSL_ONLY_UNION_TYPES[args_without_str_none]
227
-
228
- # Handle the case where we have a list | None, which in the dsl is needed, but here we will just have an empty list.
229
- if len(args) == 2:
230
- list_elems = [
231
- arg for arg in args if get_origin(arg) in set([list, dict])
232
- ]
233
- if len(list_elems) > 0 and has_none:
234
- # If we have a list and None, we return the list type
235
- # This is to handle cases like List[SomeType] | None
236
- # which in the DSL is needed, but here we will just have an empty list.
237
- return dsl_to_semantic_type_name(list_elems[0])
238
-
239
- # If the union contains a DSL type and a str, we need to drop the str
240
- if any(_is_dsl_type(arg) for arg in args) and has_str:
241
- # There is a DSL type and a str, which indicates something that can reference an ID.
242
- # drop the str
243
- 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
244
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
245
338
  return " | ".join(dsl_to_semantic_type_name(a) for a in args)
246
339
 
247
340
 
@@ -259,6 +352,27 @@ def dsl_to_semantic_type_name(field_type: Any) -> str:
259
352
  origin = get_origin(field_type)
260
353
  args = get_args(field_type)
261
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
+
262
376
  if origin is Union or (
263
377
  hasattr(field_type, "__class__")
264
378
  and field_type.__class__.__name__ == "UnionType"
@@ -270,7 +384,13 @@ def dsl_to_semantic_type_name(field_type: Any) -> str:
270
384
  # Format literal values
271
385
  literal_values = []
272
386
  for arg in args:
273
- if isinstance(arg, str):
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):
274
394
  literal_values.append(f'"{arg}"')
275
395
  else:
276
396
  literal_values.append(str(arg))
@@ -319,16 +439,35 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
319
439
  if inspect.isabstract(cls):
320
440
  inheritance += ", ABC"
321
441
 
322
- # Check if this class inherits from another DSL class
442
+ # Collect all base classes from DSL and base_types modules
443
+ base_classes = []
323
444
  for base in cls.__bases__:
324
445
  if (
325
446
  hasattr(base, "__module__")
326
- and base.__module__ == dsl.__name__
447
+ and (
448
+ base.__module__ == dsl.__name__
449
+ or base.__module__ == base_types.__name__
450
+ )
327
451
  and base.__name__ not in TYPES_TO_IGNORE
328
452
  and not base.__name__.startswith("_")
329
453
  ):
330
- # This class inherits from another DSL class
331
- semantic_base = f"{base.__name__}"
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]
332
471
  if inspect.isabstract(cls):
333
472
  inheritance = f"ABC, {semantic_base}"
334
473
  else:
@@ -336,7 +475,15 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
336
475
  if semantic_name == "Tool":
337
476
  # Tools should inherit from Step and be immutable
338
477
  inheritance = f"{semantic_base}, ImmutableModel"
339
- break
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)}"
340
487
 
341
488
  # Get field information from the class - only fields defined on this class, not inherited
342
489
  fields = []
@@ -350,6 +497,7 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
350
497
  field_info = cls.model_fields[field_name]
351
498
  field_type = field_info.annotation
352
499
  field_default = field_info.default
500
+ field_default_factory = field_info.default_factory
353
501
  field_description = getattr(field_info, "description", None)
354
502
 
355
503
  # Transform the field type
@@ -365,7 +513,11 @@ def generate_semantic_class(class_name: str, cls: type) -> str:
365
513
 
366
514
  # Create field definition
367
515
  field_def = create_field_definition(
368
- field_name, semantic_type, field_default, field_description
516
+ field_name,
517
+ semantic_type,
518
+ field_default,
519
+ field_default_factory,
520
+ field_description,
369
521
  )
370
522
  fields.append(field_def)
371
523
 
@@ -387,6 +539,7 @@ def create_field_definition(
387
539
  field_name: str,
388
540
  field_type: str,
389
541
  field_default: Any,
542
+ field_default_factory: Any,
390
543
  field_description: str | None,
391
544
  ) -> str:
392
545
  """Create a field definition string."""
@@ -397,11 +550,29 @@ def create_field_definition(
397
550
 
398
551
  # Handle default values
399
552
  # Check for PydanticUndefined (required field)
400
- from enum import Enum
401
-
402
553
  from pydantic_core import PydanticUndefined
403
554
 
404
- if field_default is PydanticUndefined or field_default is ...:
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 ...:
405
576
  default_part = "..."
406
577
  elif field_default is None:
407
578
  default_part = "None"
@@ -426,7 +597,12 @@ def create_field_definition(
426
597
  default_part = str(field_default)
427
598
 
428
599
  # Create Field definition
429
- field_parts = [default_part]
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
+
430
606
  if field_description:
431
607
  # Escape quotes and handle multiline descriptions
432
608
  escaped_desc = field_description.replace('"', '\\"').replace(
@@ -436,6 +612,13 @@ def create_field_definition(
436
612
  if alias_part:
437
613
  field_parts.append(alias_part.lstrip(", "))
438
614
 
439
- field_def = f"Field({', '.join(field_parts)})"
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)})"
440
623
 
441
624
  return f" {field_name}: {field_type} = {field_def}"
@@ -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