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.
Files changed (128) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +5 -5
  3. qtype/application/converters/tools_from_module.py +2 -2
  4. qtype/application/converters/types.py +14 -43
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +94 -73
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +4 -0
  9. qtype/commands/convert.py +20 -8
  10. qtype/commands/generate.py +19 -27
  11. qtype/commands/run.py +73 -36
  12. qtype/commands/serve.py +74 -54
  13. qtype/commands/validate.py +34 -8
  14. qtype/commands/visualize.py +46 -22
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +65 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +612 -363
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +57 -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 +74 -0
  30. qtype/interpreter/base/factory.py +117 -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 +462 -22
  36. qtype/interpreter/converters.py +77 -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/decoder_executor.py +163 -0
  41. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  42. qtype/interpreter/executors/document_embedder_executor.py +107 -0
  43. qtype/interpreter/executors/document_search_executor.py +122 -0
  44. qtype/interpreter/executors/document_source_executor.py +118 -0
  45. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  46. qtype/interpreter/executors/echo_executor.py +63 -0
  47. qtype/interpreter/executors/field_extractor_executor.py +160 -0
  48. qtype/interpreter/executors/file_source_executor.py +101 -0
  49. qtype/interpreter/executors/file_writer_executor.py +110 -0
  50. qtype/interpreter/executors/index_upsert_executor.py +228 -0
  51. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  52. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  53. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  54. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  55. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  56. qtype/interpreter/executors/sql_source_executor.py +106 -0
  57. qtype/interpreter/executors/vector_search_executor.py +91 -0
  58. qtype/interpreter/flow.py +159 -22
  59. qtype/interpreter/metadata_api.py +115 -0
  60. qtype/interpreter/resource_cache.py +5 -4
  61. qtype/interpreter/rich_progress.py +225 -0
  62. qtype/interpreter/stream/chat/__init__.py +15 -0
  63. qtype/interpreter/stream/chat/converter.py +391 -0
  64. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  65. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  66. qtype/interpreter/stream/chat/vercel.py +609 -0
  67. qtype/interpreter/stream/utils/__init__.py +15 -0
  68. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  69. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  70. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  71. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  72. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  73. qtype/interpreter/telemetry.py +135 -8
  74. qtype/interpreter/tools/__init__.py +5 -0
  75. qtype/interpreter/tools/function_tool_helper.py +265 -0
  76. qtype/interpreter/types.py +330 -0
  77. qtype/interpreter/typing.py +83 -89
  78. qtype/interpreter/ui/404/index.html +1 -1
  79. qtype/interpreter/ui/404.html +1 -1
  80. qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  81. qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
  82. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  83. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  84. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  85. qtype/interpreter/ui/icon.png +0 -0
  86. qtype/interpreter/ui/index.html +1 -1
  87. qtype/interpreter/ui/index.txt +4 -4
  88. qtype/semantic/checker.py +583 -0
  89. qtype/semantic/generate.py +262 -83
  90. qtype/semantic/loader.py +95 -0
  91. qtype/semantic/model.py +436 -159
  92. qtype/semantic/resolver.py +63 -19
  93. qtype/semantic/visualize.py +28 -31
  94. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/METADATA +16 -3
  95. qtype-0.1.1.dist-info/RECORD +135 -0
  96. qtype/dsl/base_types.py +0 -38
  97. qtype/dsl/validator.py +0 -465
  98. qtype/interpreter/batch/__init__.py +0 -0
  99. qtype/interpreter/batch/file_sink_source.py +0 -162
  100. qtype/interpreter/batch/flow.py +0 -95
  101. qtype/interpreter/batch/sql_source.py +0 -92
  102. qtype/interpreter/batch/step.py +0 -74
  103. qtype/interpreter/batch/types.py +0 -41
  104. qtype/interpreter/batch/utils.py +0 -178
  105. qtype/interpreter/chat/chat_api.py +0 -237
  106. qtype/interpreter/chat/vercel.py +0 -314
  107. qtype/interpreter/exceptions.py +0 -10
  108. qtype/interpreter/step.py +0 -67
  109. qtype/interpreter/steps/__init__.py +0 -0
  110. qtype/interpreter/steps/agent.py +0 -114
  111. qtype/interpreter/steps/condition.py +0 -36
  112. qtype/interpreter/steps/decoder.py +0 -88
  113. qtype/interpreter/steps/llm_inference.py +0 -171
  114. qtype/interpreter/steps/prompt_template.py +0 -54
  115. qtype/interpreter/steps/search.py +0 -24
  116. qtype/interpreter/steps/tool.py +0 -219
  117. qtype/interpreter/streaming_helpers.py +0 -123
  118. qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
  119. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  120. qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
  121. qtype/interpreter/ui/favicon.ico +0 -0
  122. qtype/loader.py +0 -390
  123. qtype-0.0.16.dist-info/RECORD +0 -106
  124. /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  125. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/WHEEL +0 -0
  126. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/entry_points.txt +0 -0
  127. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/licenses/LICENSE +0 -0
  128. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/top_level.txt +0 -0
@@ -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
- 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",
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 typing import Any, Literal
136
+ from functools import partial
137
+ from typing import Any, Literal, Union
121
138
 
122
- from pydantic import BaseModel, Field, model_validator
139
+ from pydantic import BaseModel, Field, RootModel
123
140
 
124
- # Import enums and type aliases from DSL
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 Flow class which _could_ be generated but we want a validator to update it's carndiality
157
- f.write("\n\n")
173
+ # Write the DocumentType
158
174
  f.write(
159
- dedent('''
160
- class Flow(Step):
161
- """Defines a flow of steps that can be executed in sequence or parallel.
162
- If input or output variables are not specified, they are inferred from
163
- the first and last step, respectively.
164
- """
165
-
166
- description: str | None = Field(
167
- None, description="Optional description of the flow."
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
- get_args(dsl.ToolType): "Tool",
206
- get_args(dsl.StepType): "Step",
207
- get_args(dsl.IndexType): "Index",
208
- 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",
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
- """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)
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
- # First see if this is a DSL-only union type
222
- # If so, just return the corresponding semantic type
223
- if args_without_str_none in DSL_ONLY_UNION_TYPES:
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, 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):
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
- # Check if this class inherits from another DSL class
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 base.__module__ == dsl.__name__
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
- # This class inherits from another DSL class
335
- 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]
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
- 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)}"
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, semantic_type, field_default, field_description
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
- 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 ...:
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 = [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
+
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
- 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)})"
444
623
 
445
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