qtype 0.0.15__py3-none-any.whl → 0.1.0__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 (126) 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 +92 -71
  7. qtype/base/types.py +227 -7
  8. qtype/commands/convert.py +20 -8
  9. qtype/commands/generate.py +19 -27
  10. qtype/commands/run.py +54 -36
  11. qtype/commands/serve.py +74 -54
  12. qtype/commands/validate.py +34 -8
  13. qtype/commands/visualize.py +46 -22
  14. qtype/dsl/__init__.py +6 -5
  15. qtype/dsl/custom_types.py +1 -1
  16. qtype/dsl/domain_types.py +65 -5
  17. qtype/dsl/linker.py +384 -0
  18. qtype/dsl/loader.py +315 -0
  19. qtype/dsl/model.py +612 -363
  20. qtype/dsl/parser.py +200 -0
  21. qtype/dsl/types.py +50 -0
  22. qtype/interpreter/api.py +58 -135
  23. qtype/interpreter/auth/aws.py +19 -9
  24. qtype/interpreter/auth/generic.py +93 -16
  25. qtype/interpreter/base/base_step_executor.py +429 -0
  26. qtype/interpreter/base/batch_step_executor.py +171 -0
  27. qtype/interpreter/base/exceptions.py +50 -0
  28. qtype/interpreter/base/executor_context.py +74 -0
  29. qtype/interpreter/base/factory.py +117 -0
  30. qtype/interpreter/base/progress_tracker.py +75 -0
  31. qtype/interpreter/base/secrets.py +339 -0
  32. qtype/interpreter/base/step_cache.py +73 -0
  33. qtype/interpreter/base/stream_emitter.py +469 -0
  34. qtype/interpreter/conversions.py +455 -21
  35. qtype/interpreter/converters.py +73 -0
  36. qtype/interpreter/endpoints.py +355 -0
  37. qtype/interpreter/executors/agent_executor.py +242 -0
  38. qtype/interpreter/executors/aggregate_executor.py +93 -0
  39. qtype/interpreter/executors/decoder_executor.py +163 -0
  40. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  41. qtype/interpreter/executors/document_embedder_executor.py +75 -0
  42. qtype/interpreter/executors/document_search_executor.py +122 -0
  43. qtype/interpreter/executors/document_source_executor.py +118 -0
  44. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  45. qtype/interpreter/executors/echo_executor.py +63 -0
  46. qtype/interpreter/executors/field_extractor_executor.py +160 -0
  47. qtype/interpreter/executors/file_source_executor.py +101 -0
  48. qtype/interpreter/executors/file_writer_executor.py +110 -0
  49. qtype/interpreter/executors/index_upsert_executor.py +228 -0
  50. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  51. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  52. qtype/interpreter/executors/invoke_tool_executor.py +353 -0
  53. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  54. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  55. qtype/interpreter/executors/sql_source_executor.py +106 -0
  56. qtype/interpreter/executors/vector_search_executor.py +91 -0
  57. qtype/interpreter/flow.py +147 -22
  58. qtype/interpreter/metadata_api.py +115 -0
  59. qtype/interpreter/resource_cache.py +5 -4
  60. qtype/interpreter/stream/chat/__init__.py +15 -0
  61. qtype/interpreter/stream/chat/converter.py +391 -0
  62. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  63. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  64. qtype/interpreter/stream/chat/vercel.py +609 -0
  65. qtype/interpreter/stream/utils/__init__.py +15 -0
  66. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  67. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  68. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  69. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  70. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  71. qtype/interpreter/telemetry.py +135 -8
  72. qtype/interpreter/tools/__init__.py +5 -0
  73. qtype/interpreter/tools/function_tool_helper.py +265 -0
  74. qtype/interpreter/types.py +328 -0
  75. qtype/interpreter/typing.py +83 -89
  76. qtype/interpreter/ui/404/index.html +1 -1
  77. qtype/interpreter/ui/404.html +1 -1
  78. qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  79. qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
  80. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  81. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  82. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  83. qtype/interpreter/ui/icon.png +0 -0
  84. qtype/interpreter/ui/index.html +1 -1
  85. qtype/interpreter/ui/index.txt +4 -4
  86. qtype/semantic/checker.py +583 -0
  87. qtype/semantic/generate.py +262 -83
  88. qtype/semantic/loader.py +95 -0
  89. qtype/semantic/model.py +436 -159
  90. qtype/semantic/resolver.py +59 -17
  91. qtype/semantic/visualize.py +28 -31
  92. {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/METADATA +16 -3
  93. qtype-0.1.0.dist-info/RECORD +134 -0
  94. qtype/dsl/base_types.py +0 -38
  95. qtype/dsl/validator.py +0 -465
  96. qtype/interpreter/batch/__init__.py +0 -0
  97. qtype/interpreter/batch/file_sink_source.py +0 -162
  98. qtype/interpreter/batch/flow.py +0 -95
  99. qtype/interpreter/batch/sql_source.py +0 -92
  100. qtype/interpreter/batch/step.py +0 -74
  101. qtype/interpreter/batch/types.py +0 -41
  102. qtype/interpreter/batch/utils.py +0 -178
  103. qtype/interpreter/chat/chat_api.py +0 -237
  104. qtype/interpreter/chat/vercel.py +0 -314
  105. qtype/interpreter/exceptions.py +0 -10
  106. qtype/interpreter/step.py +0 -67
  107. qtype/interpreter/steps/__init__.py +0 -0
  108. qtype/interpreter/steps/agent.py +0 -114
  109. qtype/interpreter/steps/condition.py +0 -36
  110. qtype/interpreter/steps/decoder.py +0 -88
  111. qtype/interpreter/steps/llm_inference.py +0 -171
  112. qtype/interpreter/steps/prompt_template.py +0 -54
  113. qtype/interpreter/steps/search.py +0 -24
  114. qtype/interpreter/steps/tool.py +0 -219
  115. qtype/interpreter/streaming_helpers.py +0 -123
  116. qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
  117. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  118. qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
  119. qtype/interpreter/ui/favicon.ico +0 -0
  120. qtype/loader.py +0 -390
  121. qtype-0.0.15.dist-info/RECORD +0 -106
  122. /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  123. {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/WHEEL +0 -0
  124. {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/entry_points.txt +0 -0
  125. {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/licenses/LICENSE +0 -0
  126. {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/top_level.txt +0 -0
@@ -180,7 +180,7 @@ def parse_duration_string(duration: str) -> int:
180
180
 
181
181
  def format_datetime(timestamp: datetime, format_string: str) -> str:
182
182
  """
183
- Format a timestamp using a custom format string.
183
+ Format a timestamp using a custom format string that can be passed to strftime.
184
184
 
185
185
  Args:
186
186
  timestamp: Datetime object to format.
@@ -17,7 +17,7 @@ from openapi_parser.specification import (
17
17
  Security,
18
18
  )
19
19
 
20
- from qtype.dsl.base_types import PrimitiveTypeEnum
20
+ from qtype.base.types import PrimitiveTypeEnum
21
21
  from qtype.dsl.model import (
22
22
  APIKeyAuthProvider,
23
23
  APITool,
@@ -344,9 +344,9 @@ def to_api_tool(
344
344
  endpoint=endpoint,
345
345
  method=operation.method.value.upper(),
346
346
  auth=auth.id if auth else None, # Use auth ID string instead of object
347
- inputs=inputs if inputs else None,
348
- outputs=outputs if outputs else None,
349
- parameters=parameters if parameters else None,
347
+ inputs=inputs,
348
+ outputs=outputs,
349
+ parameters=parameters,
350
350
  )
351
351
 
352
352
 
@@ -394,7 +394,7 @@ def to_authorization_provider(
394
394
  }
395
395
  )
396
396
  if any(flow.scopes for flow in security.flows.values())
397
- else None,
397
+ else [],
398
398
  )
399
399
  case _:
400
400
  raise ValueError(
@@ -5,7 +5,7 @@ from typing import Any, Type, Union, get_args, get_origin
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from qtype.application.converters.types import PYTHON_TYPE_TO_PRIMITIVE_TYPE
8
- from qtype.dsl.base_types import PrimitiveTypeEnum
8
+ from qtype.base.types import PrimitiveTypeEnum
9
9
  from qtype.dsl.model import (
10
10
  CustomType,
11
11
  ListType,
@@ -159,7 +159,7 @@ def _create_tool_from_function(
159
159
  module_path=func_info["module"],
160
160
  function_name=func_name,
161
161
  description=description,
162
- inputs=inputs if inputs else None,
162
+ inputs=inputs,
163
163
  outputs=outputs,
164
164
  )
165
165
 
@@ -1,47 +1,18 @@
1
- from datetime import date, datetime, time
2
-
3
- from qtype.dsl.base_types import PrimitiveTypeEnum
4
-
5
- """
6
- Mapping of QType primitive types to Python types for internal representations.
7
1
  """
8
- PRIMITIVE_TO_PYTHON_TYPE = {
9
- PrimitiveTypeEnum.audio: bytes,
10
- PrimitiveTypeEnum.boolean: bool,
11
- PrimitiveTypeEnum.bytes: bytes,
12
- PrimitiveTypeEnum.date: date,
13
- PrimitiveTypeEnum.datetime: datetime,
14
- PrimitiveTypeEnum.int: int,
15
- PrimitiveTypeEnum.file: bytes, # Use bytes for file content
16
- PrimitiveTypeEnum.float: float,
17
- PrimitiveTypeEnum.image: bytes, # Use bytes for image data
18
- PrimitiveTypeEnum.text: str,
19
- PrimitiveTypeEnum.time: time, # Use time for time representation
20
- PrimitiveTypeEnum.video: bytes, # Use bytes for video data
21
- }
2
+ Re-export type mappings from DSL layer for backward compatibility.
22
3
 
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
-
36
- def python_type_for_list(element_type: PrimitiveTypeEnum) -> type:
37
- """
38
- Get the Python list type for a given QType primitive element type.
4
+ This module maintains the application layer's interface while delegating
5
+ to the DSL layer where these mappings are now defined.
6
+ """
39
7
 
40
- Args:
41
- element_type: The primitive type of the list elements
8
+ from qtype.dsl.types import (
9
+ PRIMITIVE_TO_PYTHON_TYPE,
10
+ PYTHON_TYPE_TO_PRIMITIVE_TYPE,
11
+ python_type_for_list,
12
+ )
42
13
 
43
- Returns:
44
- The corresponding Python list type (e.g., list[str] for text elements)
45
- """
46
- element_python_type = PRIMITIVE_TO_PYTHON_TYPE[element_type]
47
- return list[element_python_type]
14
+ __all__ = [
15
+ "PRIMITIVE_TO_PYTHON_TYPE",
16
+ "PYTHON_TYPE_TO_PRIMITIVE_TYPE",
17
+ "python_type_for_list",
18
+ ]
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from typing import Any, Type, Union, get_args, get_origin
6
6
 
7
7
  import qtype.dsl.model as dsl
8
- from qtype.dsl.base_types import PrimitiveTypeEnum
8
+ from qtype.base.types import PrimitiveTypeEnum
9
9
 
10
10
 
11
11
  def _format_type_name(field_type: Any) -> str:
@@ -5,71 +5,87 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
- import pandas as pd
9
- from pydantic import BaseModel
10
-
11
8
  from qtype.base.logging import get_logger
12
- from qtype.base.types import CustomTypeRegistry, DocumentRootType, PathLike
13
- from qtype.dsl.base_types import StepCardinality
14
- from qtype.dsl.model import Application as DSLApplication
15
- from qtype.dsl.model import DocumentType
16
- from qtype.interpreter.batch.types import BatchConfig
9
+ from qtype.base.types import PathLike
17
10
  from qtype.semantic.model import Application as SemanticApplication
18
- from qtype.semantic.model import Variable
11
+ from qtype.semantic.model import DocumentType as SemanticDocumentType
12
+
13
+ # Note: There should be _zero_ imports here at the top that import qtype.interpreter.
14
+ # That's the whole point of this facade - to avoid importing optional
15
+ # dependencies unless these methods are called.
19
16
 
20
17
  logger = get_logger("application.facade")
21
18
 
22
19
 
23
20
  class QTypeFacade:
24
21
  """
25
- Simplified interface for all qtype operations.
22
+ Simplified interface for qtype operations.
26
23
 
27
- This facade hides the complexity of coordinating between DSL, semantic,
28
- and interpreter layers, providing a clean API for common operations.
24
+ This facade provides lazy-loading wrappers for operations that require
25
+ optional dependencies (interpreter package), allowing base qtype to work
26
+ without those dependencies installed.
29
27
  """
30
28
 
31
- def load_dsl_document(
32
- self, path: PathLike
33
- ) -> tuple[DocumentRootType, CustomTypeRegistry]:
34
- from qtype.loader import load_document
35
-
36
- return load_document(Path(path).read_text(encoding="utf-8"))
37
-
38
- def telemetry(self, spec: SemanticApplication) -> None:
39
- if spec.telemetry:
29
+ def telemetry(self, spec: SemanticDocumentType) -> None:
30
+ if isinstance(spec, SemanticApplication) and spec.telemetry:
40
31
  logger.info(
41
32
  f"Telemetry enabled with endpoint: {spec.telemetry.endpoint}"
42
33
  )
43
34
  # Register telemetry if needed
44
35
  from qtype.interpreter.telemetry import register
45
36
 
46
- register(spec.telemetry, spec.id)
37
+ register(spec.telemetry, self.secret_manager(spec), spec.id)
47
38
 
48
- def load_semantic_model(
49
- self, path: PathLike
50
- ) -> tuple[SemanticApplication, CustomTypeRegistry]:
51
- """Load a document and return the resolved semantic model."""
52
- from qtype.loader import load
39
+ def secret_manager(self, spec: SemanticDocumentType):
40
+ """
41
+ Create a secret manager based on the specification.
53
42
 
54
- content = Path(path).read_text(encoding="utf-8")
55
- return load(content)
43
+ Args:
44
+ spec: SemanticDocumentType specification
56
45
 
57
- def execute_workflow(
46
+ Returns:
47
+ Secret manager instance
48
+ """
49
+ from qtype.interpreter.base.secrets import create_secret_manager
50
+
51
+ if isinstance(spec, SemanticApplication):
52
+ return create_secret_manager(spec.secret_manager)
53
+ else:
54
+ raise ValueError(
55
+ "Can't create secret manager for non-Application spec"
56
+ )
57
+
58
+ async def execute_workflow(
58
59
  self,
59
60
  path: PathLike,
60
- inputs: dict | pd.DataFrame,
61
+ inputs: dict | Any,
61
62
  flow_name: str | None = None,
62
- batch_config: BatchConfig | None = None,
63
63
  **kwargs: Any,
64
- ) -> pd.DataFrame | list[Variable]:
65
- """Execute a complete workflow from document to results."""
64
+ ) -> Any:
65
+ """
66
+ Execute a complete workflow from document to results.
67
+
68
+ Args:
69
+ path: Path to the QType specification file
70
+ inputs: Dictionary of input values or DataFrame for batch
71
+ flow_name: Optional name of flow to execute
72
+ **kwargs: Additional dependencies for execution
73
+
74
+ Returns:
75
+ DataFrame with results (one row per input)
76
+ """
77
+ import pandas as pd
78
+
79
+ from qtype.semantic.loader import load
80
+
66
81
  logger.info(f"Executing workflow from {path}")
67
82
 
68
83
  # Load the semantic application
69
- semantic_model, type_registry = self.load_semantic_model(path)
84
+ semantic_model, type_registry = load(Path(path))
85
+ assert isinstance(semantic_model, SemanticApplication)
70
86
  self.telemetry(semantic_model)
71
87
 
72
- # Find the flow to execute (inlined from _find_flow)
88
+ # Find the flow to execute
73
89
  if flow_name:
74
90
  target_flow = None
75
91
  for flow in semantic_model.flows:
@@ -83,49 +99,54 @@ class QTypeFacade:
83
99
  target_flow = semantic_model.flows[0]
84
100
  else:
85
101
  raise ValueError("No flows found in application")
86
- if target_flow.cardinality == StepCardinality.many:
87
- if isinstance(inputs, dict):
88
- inputs = pd.DataFrame([inputs])
89
- if not isinstance(inputs, pd.DataFrame):
90
- raise ValueError(
91
- "Input must be a DataFrame for flows with 'many' cardinality"
92
- )
93
- from qtype.interpreter.batch.flow import batch_execute_flow
94
-
95
- batch_config = batch_config or BatchConfig()
96
- results, errors = batch_execute_flow(
97
- target_flow, inputs, batch_config, **kwargs
98
- ) # type: ignore
99
- return results
102
+
103
+ # Convert inputs to DataFrame (normalize single dict to 1-row DataFrame)
104
+ if isinstance(inputs, dict):
105
+ input_df = pd.DataFrame([inputs])
106
+ elif isinstance(inputs, pd.DataFrame):
107
+ input_df = inputs
100
108
  else:
101
- from qtype.interpreter.flow import execute_flow
109
+ raise ValueError(
110
+ f"Inputs must be dict or DataFrame, got {type(inputs)}"
111
+ )
102
112
 
103
- for var in target_flow.inputs:
104
- if var.id in inputs:
105
- var.value = inputs[var.id]
106
- args = {**kwargs, **inputs}
107
- return execute_flow(target_flow, **args)
113
+ # Create session
114
+ from qtype.interpreter.converters import (
115
+ dataframe_to_flow_messages,
116
+ flow_messages_to_dataframe,
117
+ )
118
+ from qtype.interpreter.types import Session
108
119
 
109
- def visualize_application(self, path: PathLike) -> str:
110
- """Visualize an application as Mermaid diagram."""
111
- from qtype.semantic.visualize import visualize_application
120
+ session = Session(
121
+ session_id=kwargs.pop("session_id", "default"),
122
+ conversation_history=kwargs.pop("conversation_history", []),
123
+ )
112
124
 
113
- semantic_model, _ = self.load_semantic_model(path)
114
- return visualize_application(semantic_model)
125
+ # Convert DataFrame to FlowMessages
126
+ initial_messages = dataframe_to_flow_messages(input_df, session)
115
127
 
116
- def convert_document(self, document: DocumentType) -> str:
117
- """Convert a document to YAML format."""
118
- # Wrap DSLApplication in Document if needed
119
- wrapped_document: BaseModel = document
120
- if isinstance(document, DSLApplication):
121
- from qtype.dsl.model import Document
128
+ # Execute the flow
129
+ from opentelemetry import trace
122
130
 
123
- wrapped_document = Document(root=document)
124
- from pydantic_yaml import to_yaml_str
131
+ from qtype.interpreter.base.executor_context import ExecutorContext
132
+ from qtype.interpreter.flow import run_flow
125
133
 
126
- return to_yaml_str(
127
- wrapped_document, exclude_unset=True, exclude_none=True
134
+ secret_manager = self.secret_manager(semantic_model)
135
+ context = ExecutorContext(
136
+ secret_manager=secret_manager,
137
+ tracer=trace.get_tracer(__name__),
128
138
  )
139
+ results = await run_flow(
140
+ target_flow,
141
+ initial_messages,
142
+ context=context,
143
+ **kwargs,
144
+ )
145
+
146
+ # Convert results back to DataFrame
147
+ results_df = flow_messages_to_dataframe(results, target_flow)
148
+
149
+ return results_df
129
150
 
130
151
  def generate_aws_bedrock_models(self) -> list[dict[str, Any]]:
131
152
  """
qtype/base/types.py CHANGED
@@ -3,11 +3,24 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pathlib
6
- from typing import Any, Type, Union
6
+ import types
7
+ import typing
8
+ from enum import Enum
9
+ from typing import (
10
+ Any,
11
+ Generic,
12
+ Literal,
13
+ Optional,
14
+ Type,
15
+ TypeVar,
16
+ Union,
17
+ get_args,
18
+ get_origin,
19
+ )
7
20
 
8
21
  from pydantic import BaseModel
9
-
10
- from qtype.dsl import model as dsl
22
+ from pydantic import ConfigDict as PydanticConfigDict
23
+ from pydantic import Field, model_validator
11
24
 
12
25
  # JSON-serializable value types
13
26
  JSONValue = Union[
@@ -20,10 +33,217 @@ JSONValue = Union[
20
33
  list["JSONValue"],
21
34
  ]
22
35
 
23
- # Configuration dictionary type
24
- ConfigDict = dict[str, Any]
25
-
26
36
  # Path-like type (string or Path object)
27
37
  PathLike = Union[str, pathlib.Path]
38
+
28
39
  CustomTypeRegistry = dict[str, Type[BaseModel]]
29
- DocumentRootType = dsl.Agent | dsl.Application | dsl.Flow | list
40
+ # Configuration dictionary type
41
+ ConfigDict = dict[str, Any]
42
+
43
+
44
+ # ---------------- Shared Base Types and Enums ----------------
45
+
46
+
47
+ class PrimitiveTypeEnum(str, Enum):
48
+ """Represents the type of data a user or system input can accept within the DSL."""
49
+
50
+ audio = "audio"
51
+ boolean = "boolean"
52
+ bytes = "bytes"
53
+ citation_document = "citation_document"
54
+ citation_url = "citation_url"
55
+ date = "date"
56
+ datetime = "datetime"
57
+ int = "int"
58
+ file = "file"
59
+ float = "float"
60
+ image = "image"
61
+ text = "text"
62
+ time = "time"
63
+ video = "video"
64
+ thinking = "thinking"
65
+
66
+
67
+ class StepCardinality(str, Enum):
68
+ """Does this step emit 1 (one) or 0...N (many) items?"""
69
+
70
+ one = "one"
71
+ many = "many"
72
+
73
+
74
+ ReferenceT = TypeVar("ReferenceT")
75
+
76
+
77
+ class Reference(BaseModel, Generic[ReferenceT]):
78
+ """Represents a reference to another component by its ID."""
79
+
80
+ # model_config = PydanticConfigDict(extra="forbid")
81
+
82
+ ref: str = Field(..., alias="$ref")
83
+
84
+
85
+ def _contains_reference_and_str(type_hint: Any) -> bool:
86
+ """Check if type contains both Reference and str in a union."""
87
+ # Get union args (handles Union, | syntax, and Optional)
88
+ origin = get_origin(type_hint)
89
+ if origin not in (Union, None) and not isinstance(
90
+ type_hint, types.UnionType
91
+ ):
92
+ return False
93
+
94
+ args = get_args(type_hint)
95
+ if not args:
96
+ return False
97
+
98
+ has_str = str in args
99
+ has_ref = any(
100
+ get_origin(arg) is Reference
101
+ or (hasattr(arg, "__mro__") and Reference in arg.__mro__)
102
+ for arg in args
103
+ )
104
+ return has_str and has_ref
105
+
106
+
107
+ def _should_transform_field(type_hint: Any) -> tuple[bool, bool]:
108
+ """
109
+ Check if field should be transformed.
110
+ Returns: (should_transform, is_list)
111
+ """
112
+ # Check direct union: Reference[T] | str
113
+ if _contains_reference_and_str(type_hint):
114
+ return True, False
115
+
116
+ # Check list of union: list[Reference[T] | str]
117
+ origin = get_origin(type_hint)
118
+ if origin is list:
119
+ args = get_args(type_hint)
120
+ if args and _contains_reference_and_str(args[0]):
121
+ return True, True
122
+
123
+ # Check optional list: list[Reference[T] | str] | None
124
+ if origin is Union or isinstance(type_hint, types.UnionType):
125
+ for arg in get_args(type_hint):
126
+ if get_origin(arg) is list:
127
+ inner_args = get_args(arg)
128
+ if inner_args and _contains_reference_and_str(inner_args[0]):
129
+ return True, True
130
+
131
+ return False, False
132
+
133
+
134
+ class StrictBaseModel(BaseModel):
135
+ """Base model with extra fields forbidden."""
136
+
137
+ model_config = PydanticConfigDict(extra="forbid")
138
+
139
+ @model_validator(mode="before")
140
+ @classmethod
141
+ def normalize_string_references(cls, data: Any) -> Any:
142
+ """
143
+ Normalize string references to Reference objects before validation.
144
+
145
+ Transforms:
146
+ - `field: "ref_id"` -> `field: {"$ref": "ref_id"}`
147
+ - `field: ["ref1", "ref2"]` -> `field: [{"$ref": "ref1"}, {"$ref": "ref2"}]`
148
+
149
+ Only applies to fields typed as `Reference[T] | str` or `list[Reference[T] | str]`.
150
+ """
151
+ if not isinstance(data, dict):
152
+ return data
153
+
154
+ # Get type hints (evaluates ForwardRefs)
155
+ hints = typing.get_type_hints(cls)
156
+
157
+ # Transform fields
158
+ for field_name, field_value in data.items():
159
+ if field_name == "type" or field_name not in hints:
160
+ continue
161
+
162
+ should_transform, is_list = _should_transform_field(
163
+ hints[field_name]
164
+ )
165
+ if not should_transform:
166
+ continue
167
+
168
+ if is_list and isinstance(field_value, list):
169
+ data[field_name] = [
170
+ {"$ref": item} if isinstance(item, str) else item
171
+ for item in field_value
172
+ ]
173
+ elif not is_list and isinstance(field_value, str):
174
+ data[field_name] = {"$ref": field_value}
175
+
176
+ return data
177
+
178
+
179
+ class BatchConfig(BaseModel):
180
+ """Configuration for batch execution.
181
+
182
+ Attributes:
183
+ num_workers: Number of async workers for batch operations.
184
+ """
185
+
186
+ batch_size: int = Field(
187
+ default=25,
188
+ description="Max number of rows to send to a step at a time",
189
+ gt=0,
190
+ )
191
+
192
+
193
+ class ConcurrencyConfig(BaseModel):
194
+ """Configuration for concurrent processing.
195
+
196
+ Attributes:
197
+ num_workers: Number of async workers for batch operations.
198
+ """
199
+
200
+ num_workers: int = Field(
201
+ default=1,
202
+ description="Number of async workers for batch operations",
203
+ gt=0,
204
+ )
205
+
206
+
207
+ class BatchableStepMixin(BaseModel):
208
+ """A mixin for steps that support concurrent batch processing."""
209
+
210
+ batch_config: BatchConfig = Field(
211
+ default_factory=BatchConfig,
212
+ description="Configuration for processing the input stream in batches. If omitted, the step processes items one by one.",
213
+ )
214
+
215
+
216
+ class CacheConfig(BaseModel):
217
+ directory: PathLike = Field(
218
+ default=pathlib.Path("./.qtype-cache"),
219
+ description="Base cache directory.",
220
+ )
221
+ namespace: Optional[str] = Field(
222
+ default=None, description="Logical namespace for cache keys."
223
+ )
224
+ on_error: Literal["Cache", "Drop"] = "Drop"
225
+ version: str = Field(
226
+ default="1.0", description="Bump to invalidate old cache."
227
+ )
228
+ compress: bool = Field(default=False, description="Compress stored data.")
229
+ ttl: Optional[int] = Field(
230
+ default=None, description="Optional time-to-live in seconds."
231
+ )
232
+
233
+
234
+ class CachedStepMixin(BaseModel):
235
+ """A mixin for steps that support caching."""
236
+
237
+ cache_config: CacheConfig | None = Field(
238
+ default=None,
239
+ description="Configuration for caching step outputs. If omitted, caching is disabled.",
240
+ )
241
+
242
+
243
+ class ConcurrentStepMixin(BaseModel):
244
+ """A mixin for steps that support concurrent processing."""
245
+
246
+ concurrency_config: ConcurrencyConfig = Field(
247
+ default_factory=ConcurrencyConfig,
248
+ description="Configuration for processing the input stream concurrently. If omitted, the step processes items sequentially.",
249
+ )
qtype/commands/convert.py CHANGED
@@ -8,12 +8,26 @@ import argparse
8
8
  import logging
9
9
  from pathlib import Path
10
10
 
11
- from qtype.application.facade import QTypeFacade
12
- from qtype.dsl.model import Application, ToolList
11
+ from qtype.dsl.model import Application, Document, ToolList
13
12
 
14
13
  logger = logging.getLogger(__name__)
15
14
 
16
15
 
16
+ def _convert_to_yaml(doc: Application | ToolList) -> str:
17
+ """Convert a document to YAML format."""
18
+ from pydantic_yaml import to_yaml_str
19
+
20
+ # Wrap in Document if needed
21
+ if isinstance(doc, Application):
22
+ wrapped = Document(root=doc)
23
+ else:
24
+ wrapped = doc
25
+
26
+ # NOTE: We use exclude_none but NOT exclude_unset because discriminator
27
+ # fields like 'type' have default values and must be included in output
28
+ return to_yaml_str(wrapped, exclude_none=True)
29
+
30
+
17
31
  def convert_api(args: argparse.Namespace) -> None:
18
32
  """Convert API specification to qtype format."""
19
33
  from qtype.application.converters.tools_from_api import tools_from_api
@@ -36,9 +50,8 @@ def convert_api(args: argparse.Namespace) -> None:
36
50
  types=types,
37
51
  auths=auths,
38
52
  )
39
- # Use facade to convert to YAML format
40
- facade = QTypeFacade()
41
- content = facade.convert_document(doc)
53
+ # Convert to YAML format
54
+ content = _convert_to_yaml(doc)
42
55
 
43
56
  # Write to file or stdout
44
57
  if args.output:
@@ -79,9 +92,8 @@ def convert_module(args: argparse.Namespace) -> None:
79
92
  root=list(tools),
80
93
  )
81
94
 
82
- # Use facade to convert to YAML format
83
- facade = QTypeFacade()
84
- content = facade.convert_document(doc)
95
+ # Convert to YAML format
96
+ content = _convert_to_yaml(doc)
85
97
 
86
98
  # Write to file or stdout
87
99
  if args.output: