qtype 0.0.12__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +753 -264
  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 +495 -24
  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 +123 -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 +104 -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 +172 -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.7.dist-info}/METADATA +22 -5
  102. qtype-0.1.7.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.7.dist-info}/WHEEL +0 -0
  135. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
  136. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
  137. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/top_level.txt +0 -0
@@ -2,69 +2,96 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
8
- import pandas as pd
9
- from pydantic import BaseModel
10
-
11
- 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
- logger = get_logger("application.facade")
17
+ logger = logging.getLogger(__name__)
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
29
+ def telemetry(self, spec: SemanticDocumentType) -> None:
30
+ from qtype.interpreter.telemetry import register
35
31
 
36
- return load_document(Path(path).read_text(encoding="utf-8"))
32
+ if isinstance(spec, SemanticApplication) and spec.telemetry:
33
+ logger.info(
34
+ f"Telemetry enabled with endpoint: {spec.telemetry.endpoint}"
35
+ )
36
+ # Register telemetry if needed
37
+ register(spec.telemetry, self.secret_manager(spec), spec.id)
37
38
 
38
- def load_and_validate(self, path: PathLike) -> DocumentRootType:
39
- """Load and validate a document."""
40
- logger.info("Document loaded, proceeding to validation")
41
- root, _ = self.load_dsl_document(path)
42
- return root
39
+ def secret_manager(self, spec: SemanticDocumentType):
40
+ """
41
+ Create a secret manager based on the specification.
43
42
 
44
- def load_semantic_model(
45
- self, path: PathLike
46
- ) -> tuple[SemanticApplication, CustomTypeRegistry]:
47
- """Load a document and return the resolved semantic model."""
48
- from qtype.loader import load
43
+ Args:
44
+ spec: SemanticDocumentType specification
49
45
 
50
- content = Path(path).read_text(encoding="utf-8")
51
- return load(content)
46
+ Returns:
47
+ Secret manager instance
48
+ """
49
+ from qtype.interpreter.base.secrets import create_secret_manager
52
50
 
53
- def execute_workflow(
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(
54
59
  self,
55
60
  path: PathLike,
56
- inputs: dict | pd.DataFrame,
61
+ inputs: dict | Any,
57
62
  flow_name: str | None = None,
58
- batch_config: BatchConfig | None = None,
59
63
  **kwargs: Any,
60
- ) -> pd.DataFrame | list[Variable]:
61
- """Execute a complete workflow from document to results."""
62
- logger.info(f"Executing workflow from {path}")
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
+ from opentelemetry import trace
79
+
80
+ from qtype.interpreter.base.executor_context import ExecutorContext
81
+ from qtype.interpreter.converters import (
82
+ dataframe_to_flow_messages,
83
+ flow_messages_to_dataframe,
84
+ )
85
+ from qtype.interpreter.flow import run_flow
86
+ from qtype.interpreter.types import Session
87
+ from qtype.semantic.loader import load
63
88
 
64
89
  # Load the semantic application
65
- semantic_model, type_registry = self.load_semantic_model(path)
90
+ semantic_model, type_registry = load(Path(path))
91
+ assert isinstance(semantic_model, SemanticApplication)
92
+ self.telemetry(semantic_model)
66
93
 
67
- # Find the flow to execute (inlined from _find_flow)
94
+ # Find the flow to execute
68
95
  if flow_name:
69
96
  target_flow = None
70
97
  for flow in semantic_model.flows:
@@ -78,57 +105,47 @@ class QTypeFacade:
78
105
  target_flow = semantic_model.flows[0]
79
106
  else:
80
107
  raise ValueError("No flows found in application")
81
- if target_flow.cardinality == StepCardinality.many:
82
- if isinstance(inputs, dict):
83
- inputs = pd.DataFrame([inputs])
84
- if not isinstance(inputs, pd.DataFrame):
85
- raise ValueError(
86
- "Input must be a DataFrame for flows with 'many' cardinality"
87
- )
88
- from qtype.interpreter.batch.flow import batch_execute_flow
89
-
90
- batch_config = batch_config or BatchConfig()
91
- results, errors = batch_execute_flow(
92
- target_flow, inputs, batch_config, **kwargs
93
- ) # type: ignore
94
- return results
95
- else:
96
- from qtype.interpreter.flow import execute_flow
97
-
98
- args = {**kwargs, **inputs}
99
- return execute_flow(target_flow, **args)
100
108
 
101
- def visualize_application(self, path: PathLike) -> str:
102
- """Visualize an application as Mermaid diagram."""
103
- from qtype.semantic.visualize import visualize_application
109
+ logger.info(f"Executing flow {target_flow.id} from {path}")
104
110
 
105
- semantic_model, _ = self.load_semantic_model(path)
106
- return visualize_application(semantic_model)
111
+ # Convert inputs to DataFrame (normalize single dict to 1-row DataFrame)
107
112
 
108
- def convert_document(self, document: DocumentType) -> str:
109
- """Convert a document to YAML format."""
110
- # Wrap DSLApplication in Document if needed
111
- wrapped_document: BaseModel = document
112
- if isinstance(document, DSLApplication):
113
- from qtype.dsl.model import Document
114
-
115
- wrapped_document = Document(root=document)
116
-
117
- # Try to use pydantic_yaml first
118
- try:
119
- from pydantic_yaml import to_yaml_str
120
-
121
- return to_yaml_str(
122
- wrapped_document, exclude_unset=True, exclude_none=True
113
+ if isinstance(inputs, dict):
114
+ input_df = pd.DataFrame([inputs])
115
+ elif isinstance(inputs, pd.DataFrame):
116
+ input_df = inputs
117
+ else:
118
+ raise ValueError(
119
+ f"Inputs must be dict or DataFrame, got {type(inputs)}"
123
120
  )
124
- except ImportError:
125
- # Fallback to basic YAML if pydantic_yaml is not available
126
- import yaml
127
121
 
128
- document_dict = wrapped_document.model_dump(
129
- exclude_unset=True, exclude_none=True
130
- )
131
- return yaml.dump(document_dict, default_flow_style=False)
122
+ # Create session
123
+ session = Session(
124
+ session_id=kwargs.pop("session_id", "default"),
125
+ conversation_history=kwargs.pop("conversation_history", []),
126
+ )
127
+
128
+ # Convert DataFrame to FlowMessages
129
+ initial_messages = dataframe_to_flow_messages(input_df, session)
130
+
131
+ # Execute the flow
132
+ secret_manager = self.secret_manager(semantic_model)
133
+
134
+ context = ExecutorContext(
135
+ secret_manager=secret_manager,
136
+ tracer=trace.get_tracer(__name__),
137
+ )
138
+ results = await run_flow(
139
+ target_flow,
140
+ initial_messages,
141
+ context=context,
142
+ **kwargs,
143
+ )
144
+
145
+ # Convert results back to DataFrame
146
+ results_df = flow_messages_to_dataframe(results, target_flow)
147
+
148
+ return results_df
132
149
 
133
150
  def generate_aws_bedrock_models(self) -> list[dict[str, Any]]:
134
151
  """
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/cli.py CHANGED
@@ -7,6 +7,10 @@ import importlib
7
7
  import logging
8
8
  from pathlib import Path
9
9
 
10
+ from qtype.base.logging import get_logger
11
+
12
+ logger = get_logger("application.facade")
13
+
10
14
  try:
11
15
  from importlib.metadata import entry_points
12
16
  except ImportError:
@@ -131,7 +135,7 @@ def main() -> None:
131
135
  # Set logging level based on user input
132
136
  logging.basicConfig(
133
137
  level=getattr(logging, args.log_level),
134
- format="%(levelname)s: %(message)s",
138
+ format="%(asctime)s - %(levelname)s: %(message)s",
135
139
  )
136
140
 
137
141
  # Dispatch to the selected subcommand
qtype/commands/convert.py CHANGED
@@ -8,14 +8,62 @@ import argparse
8
8
  import logging
9
9
  from pathlib import Path
10
10
 
11
- from qtype.application.facade import QTypeFacade
11
+ from qtype.dsl.model import Application, Document, ToolList
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
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
+
16
31
  def convert_api(args: argparse.Namespace) -> None:
17
32
  """Convert API specification to qtype format."""
18
- raise NotImplementedError("API conversion is not implemented yet.")
33
+ from qtype.application.converters.tools_from_api import tools_from_api
34
+
35
+ try:
36
+ api_name, auths, tools, types = tools_from_api(args.api_spec)
37
+ if not tools:
38
+ raise ValueError(
39
+ f"No tools found from the API specification: {args.api_spec}"
40
+ )
41
+ if not auths and not types:
42
+ doc = ToolList(
43
+ root=list(tools),
44
+ )
45
+ else:
46
+ doc: Application | ToolList = Application(
47
+ id=api_name,
48
+ description=f"Tools created from API specification {args.api_spec}",
49
+ tools=list(tools),
50
+ types=types,
51
+ auths=auths,
52
+ )
53
+ # Convert to YAML format
54
+ content = _convert_to_yaml(doc)
55
+
56
+ # Write to file or stdout
57
+ if args.output:
58
+ output_path = Path(args.output)
59
+ output_path.write_text(content, encoding="utf-8")
60
+ logger.info(f"✅ Converted tools saved to {output_path}")
61
+ else:
62
+ print(content)
63
+
64
+ except Exception as e:
65
+ logger.error(f"❌ Conversion failed: {e}")
66
+ raise
19
67
 
20
68
 
21
69
  def convert_module(args: argparse.Namespace) -> None:
@@ -23,7 +71,6 @@ def convert_module(args: argparse.Namespace) -> None:
23
71
  from qtype.application.converters.tools_from_module import (
24
72
  tools_from_module,
25
73
  )
26
- from qtype.dsl.model import Application, ToolList
27
74
 
28
75
  try:
29
76
  tools, types = tools_from_module(args.module_path)
@@ -45,9 +92,8 @@ def convert_module(args: argparse.Namespace) -> None:
45
92
  root=list(tools),
46
93
  )
47
94
 
48
- # Use facade to convert to YAML format
49
- facade = QTypeFacade()
50
- content = facade.convert_document(doc)
95
+ # Convert to YAML format
96
+ content = _convert_to_yaml(doc)
51
97
 
52
98
  # Write to file or stdout
53
99
  if args.output:
@@ -84,8 +84,44 @@ def generate_schema(args: argparse.Namespace) -> None:
84
84
  'output' attribute specifying the output file path.
85
85
  """
86
86
  schema = Document.model_json_schema()
87
+
87
88
  # Add the $schema property to indicate JSON Schema version
88
89
  schema["$schema"] = "http://json-schema.org/draft-07/schema#"
90
+
91
+ # Add custom YAML tag definitions for QType loader features
92
+ if "$defs" not in schema:
93
+ schema["$defs"] = {}
94
+
95
+ # Note: Custom YAML tags (!include, !include_raw) and environment variable
96
+ # substitution (${VAR}) are handled by the QType YAML loader at parse time,
97
+ # not by JSON Schema validation. We define them in $defs for documentation
98
+ # purposes, but we don't apply them to string fields since:
99
+ # 1. They would cause false positives (e.g., "localhost" matching as valid)
100
+ # 2. The YAML loader processes these before schema validation occurs
101
+ # 3. After YAML loading, the schema sees the resolved/substituted values
102
+ #
103
+ # Schema validation happens on the post-processed document structure,
104
+ # so we don't need to (and shouldn't) validate the raw YAML tag syntax.
105
+
106
+ # Define custom YAML tags used by QType loader
107
+ schema["$defs"]["qtype_include_tag"] = {
108
+ "type": "string",
109
+ "pattern": "^!include\\s+.+",
110
+ "description": "Include external YAML file using QType's !include tag",
111
+ }
112
+
113
+ schema["$defs"]["qtype_include_raw_tag"] = {
114
+ "type": "string",
115
+ "pattern": "^!include_raw\\s+.+",
116
+ "description": "Include raw text file using QType's !include_raw tag",
117
+ }
118
+
119
+ schema["$defs"]["qtype_env_var"] = {
120
+ "type": "string",
121
+ "pattern": "^.*\\$\\{[^}:]+(?::[^}]*)?\\}.*$",
122
+ "description": "String with environment variable substitution using ${VAR_NAME} or ${VAR_NAME:default} syntax",
123
+ }
124
+
89
125
  output = json.dumps(schema, indent=2)
90
126
  output_path: Optional[str] = getattr(args, "output", None)
91
127
  if output_path:
@@ -150,6 +186,14 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
150
186
  import networkx # noqa: F401
151
187
  import ruff # type: ignore[import-untyped] # noqa: F401
152
188
 
189
+ has_semantic_deps = True
190
+ except ImportError:
191
+ logger.debug(
192
+ "NetworkX or Ruff is not installed. Skipping semantic model generation."
193
+ )
194
+ has_semantic_deps = False
195
+
196
+ if has_semantic_deps:
153
197
  from qtype.semantic.generate import generate_semantic_model
154
198
 
155
199
  semantic_parser = generate_subparsers.add_parser(
@@ -164,7 +208,3 @@ def parser(subparsers: argparse._SubParsersAction) -> None:
164
208
  help="Output file for the semantic model (default: stdout)",
165
209
  )
166
210
  semantic_parser.set_defaults(func=generate_semantic_model)
167
- except ImportError:
168
- logger.debug(
169
- "NetworkX or Ruff is not installed. Skipping semantic model generation."
170
- )