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
@@ -0,0 +1,643 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from qtype.base.exceptions import SemanticError
6
+ from qtype.base.types import PrimitiveTypeEnum
7
+ from qtype.dsl.domain_types import ChatMessage, RAGChunk, RAGDocument
8
+ from qtype.dsl.linker import QTypeValidationError
9
+ from qtype.dsl.model import AWSAuthProvider
10
+ from qtype.semantic.model import (
11
+ Agent,
12
+ Application,
13
+ BedrockReranker,
14
+ Decoder,
15
+ DocToTextConverter,
16
+ DocumentEmbedder,
17
+ DocumentSearch,
18
+ DocumentSource,
19
+ DocumentSplitter,
20
+ Echo,
21
+ FieldExtractor,
22
+ Flow,
23
+ IndexUpsert,
24
+ ListType,
25
+ LLMInference,
26
+ PromptTemplate,
27
+ SecretReference,
28
+ SQLSource,
29
+ Step,
30
+ VectorIndex,
31
+ VectorSearch,
32
+ )
33
+
34
+ #
35
+ # This file contains rules for the language that are evaluated after
36
+ # it is loaded into the semantic representation
37
+ #
38
+
39
+
40
+ class FlowHasNoStepsError(QTypeValidationError):
41
+ """Raised when a flow has no steps defined."""
42
+
43
+ def __init__(self, flow_id: str):
44
+ super().__init__(f"Flow {flow_id} has no steps defined.")
45
+
46
+
47
+ # Alias for backward compatibility and semantic clarity
48
+ QTypeSemanticError = SemanticError
49
+
50
+
51
+ # ---- Helper Functions for Common Validation Patterns ----
52
+
53
+
54
+ def _validate_exact_input_count(
55
+ step: Step, expected: int, input_type=None
56
+ ) -> None:
57
+ """
58
+ Validate a step has exactly the expected number of inputs.
59
+
60
+ Args:
61
+ step: The step to validate
62
+ expected: Expected number of inputs
63
+ input_type: Optional expected type for the inputs (PrimitiveTypeEnum or type)
64
+
65
+ Raises:
66
+ QTypeSemanticError: If validation fails
67
+ """
68
+ if len(step.inputs) != expected:
69
+ raise QTypeSemanticError(
70
+ (
71
+ f"{step.type} step '{step.id}' must have exactly "
72
+ f"{expected} input variable(s), found {len(step.inputs)}."
73
+ )
74
+ )
75
+
76
+ if input_type is not None and len(step.inputs) > 0:
77
+ actual_type = step.inputs[0].type
78
+ if actual_type != input_type:
79
+ type_name = (
80
+ input_type.__name__
81
+ if hasattr(input_type, "__name__")
82
+ else str(input_type)
83
+ )
84
+ raise QTypeSemanticError(
85
+ (
86
+ f"{step.type} step '{step.id}' input must be of type "
87
+ f"'{type_name}', found '{actual_type}'."
88
+ )
89
+ )
90
+
91
+
92
+ def _validate_exact_output_count(
93
+ step: Step, expected: int, output_type=None
94
+ ) -> None:
95
+ """
96
+ Validate a step has exactly the expected number of outputs.
97
+
98
+ Args:
99
+ step: The step to validate
100
+ expected: Expected number of outputs
101
+ output_type: Optional expected type for the outputs (PrimitiveTypeEnum or type)
102
+
103
+ Raises:
104
+ QTypeSemanticError: If validation fails
105
+ """
106
+ if len(step.outputs) != expected:
107
+ raise QTypeSemanticError(
108
+ (
109
+ f"{step.type} step '{step.id}' must have exactly "
110
+ f"{expected} output variable(s), found {len(step.outputs)}."
111
+ )
112
+ )
113
+
114
+ if output_type is not None and len(step.outputs) > 0:
115
+ actual_type = step.outputs[0].type
116
+ if actual_type != output_type:
117
+ type_name = (
118
+ output_type.__name__
119
+ if hasattr(output_type, "__name__")
120
+ else str(output_type)
121
+ )
122
+ raise QTypeSemanticError(
123
+ (
124
+ f"{step.type} step '{step.id}' output must be of type "
125
+ f"'{type_name}', found '{actual_type}'."
126
+ )
127
+ )
128
+
129
+
130
+ def _validate_min_output_count(step: Step, minimum: int) -> None:
131
+ """
132
+ Validate a step has at least the minimum number of outputs.
133
+
134
+ Args:
135
+ step: The step to validate
136
+ minimum: Minimum number of outputs required
137
+
138
+ Raises:
139
+ QTypeSemanticError: If validation fails
140
+ """
141
+ if len(step.outputs) < minimum:
142
+ raise QTypeSemanticError(
143
+ (
144
+ f"{step.type} step '{step.id}' must have at least "
145
+ f"{minimum} output variable(s)."
146
+ )
147
+ )
148
+
149
+
150
+ def _validate_input_output_types_match(
151
+ step: Step, input_type: type, output_type: type
152
+ ) -> None:
153
+ """
154
+ Validate a step has matching input and output types.
155
+
156
+ Args:
157
+ step: The step to validate
158
+ input_type: Expected input type
159
+ output_type: Expected output type
160
+
161
+ Raises:
162
+ QTypeSemanticError: If validation fails
163
+ """
164
+ _validate_exact_input_count(step, 1, input_type)
165
+ _validate_exact_output_count(step, 1, output_type)
166
+
167
+
168
+ def _validate_prompt_template(t: PromptTemplate) -> None:
169
+ """Validate PromptTemplate has exactly one text output."""
170
+ _validate_exact_output_count(t, 1, PrimitiveTypeEnum.text)
171
+
172
+
173
+ def _validate_aws_auth(a: AWSAuthProvider) -> None:
174
+ """Validate AWS authentication configuration."""
175
+ # At least one auth method must be specified
176
+ has_keys = a.access_key_id and a.secret_access_key
177
+ has_profile = a.profile_name
178
+ has_role = a.role_arn
179
+
180
+ if not (has_keys or has_profile or has_role):
181
+ raise ValueError(
182
+ "AWSAuthProvider must specify at least one authentication method: "
183
+ "access keys, profile name, or role ARN."
184
+ )
185
+
186
+ # If assuming a role, need either keys or profile for base credentials
187
+ if has_role and not (has_keys or has_profile):
188
+ raise ValueError(
189
+ "Role assumption requires base credentials (access keys or profile)."
190
+ )
191
+
192
+
193
+ def _validate_llm_inference(step: LLMInference) -> None:
194
+ """Validate LLMInference step has exactly one text output."""
195
+
196
+ _validate_exact_output_count(step, 1)
197
+ ALLOWED_TYPES = {PrimitiveTypeEnum.text, ChatMessage}
198
+ if step.outputs[0].type not in ALLOWED_TYPES:
199
+ raise QTypeSemanticError(
200
+ (
201
+ f"LLMInference step '{step.id}' output must be of type "
202
+ f"'PrimitiveTypeEnum.text' or 'ChatMessage', found "
203
+ f"'{step.outputs[0].type}'."
204
+ )
205
+ )
206
+
207
+
208
+ def _validate_agent(step: Agent) -> None:
209
+ """Validate Agent step has exactly one input and output with matching types.
210
+
211
+ Agent constraints:
212
+ - Must have exactly one input (text or ChatMessage)
213
+ - Must have exactly one output
214
+ - Output type must match input type
215
+ """
216
+ # Validate exactly one input
217
+ _validate_exact_input_count(step, 1)
218
+
219
+ # Validate exactly one output
220
+ _validate_exact_output_count(step, 1)
221
+
222
+ # Validate input type is text or ChatMessage
223
+ input_type = step.inputs[0].type
224
+ output_type = step.outputs[0].type
225
+
226
+ ALLOWED_TYPES = {PrimitiveTypeEnum.text, ChatMessage}
227
+ if input_type not in ALLOWED_TYPES:
228
+ raise QTypeSemanticError(
229
+ (
230
+ f"Agent step '{step.id}' input must be of type "
231
+ f"'text' or 'ChatMessage', found '{input_type}'."
232
+ )
233
+ )
234
+
235
+ # Validate output type matches input type
236
+ if input_type != output_type:
237
+ raise QTypeSemanticError(
238
+ (
239
+ f"Agent step '{step.id}' output type must match input type. "
240
+ f"Input type: '{input_type}', Output type: '{output_type}'."
241
+ )
242
+ )
243
+
244
+
245
+ def _validate_decoder(step: Decoder) -> None:
246
+ """Validate Decoder step has exactly one text input and at least one output."""
247
+ _validate_exact_input_count(step, 1, PrimitiveTypeEnum.text)
248
+ _validate_min_output_count(step, 1)
249
+
250
+
251
+ def _validate_echo(step: Echo) -> None:
252
+ """
253
+ Validate Echo step has matching input and output variable IDs.
254
+
255
+ The inputs and outputs must contain the same set of variable IDs,
256
+ though they can be in different order.
257
+
258
+ Args:
259
+ step: The Echo step to validate
260
+
261
+ Raises:
262
+ QTypeSemanticError: If inputs and outputs don't match
263
+ """
264
+ input_ids = {var.id for var in step.inputs}
265
+ output_ids = {var.id for var in step.outputs}
266
+
267
+ if input_ids != output_ids:
268
+ missing_in_outputs = input_ids - output_ids
269
+ extra_in_outputs = output_ids - input_ids
270
+
271
+ error_msg = (
272
+ f"Echo step '{step.id}' must have the same variable IDs "
273
+ f"in inputs and outputs (order can differ)."
274
+ )
275
+ if missing_in_outputs:
276
+ error_msg += f" Missing in outputs: {sorted(missing_in_outputs)}."
277
+ if extra_in_outputs:
278
+ error_msg += f" Extra in outputs: {sorted(extra_in_outputs)}."
279
+
280
+ raise QTypeSemanticError(error_msg)
281
+
282
+
283
+ def _validate_field_extractor(step: FieldExtractor) -> None:
284
+ """
285
+ Validate FieldExtractor step has exactly one input and one output.
286
+
287
+ Args:
288
+ step: The FieldExtractor step to validate
289
+
290
+ Raises:
291
+ QTypeSemanticError: If validation fails
292
+ """
293
+ _validate_exact_input_count(step, 1)
294
+ _validate_exact_output_count(step, 1)
295
+
296
+ # Validate json_path is not empty
297
+ if not step.json_path or not step.json_path.strip():
298
+ raise QTypeSemanticError(
299
+ f"FieldExtractor step '{step.id}' must have a non-empty json_path."
300
+ )
301
+
302
+
303
+ def _validate_sql_source(step: SQLSource) -> None:
304
+ """Validate SQLSource has output variables defined."""
305
+ _validate_min_output_count(step, 1)
306
+
307
+
308
+ def _validate_document_source(step: DocumentSource) -> None:
309
+ """Validate DocumentSource has exactly one RAGDocument output."""
310
+ _validate_exact_output_count(step, 1, RAGDocument)
311
+
312
+
313
+ def _validate_doc_to_text_converter(step: DocToTextConverter) -> None:
314
+ """Validate DocToTextConverter has exactly one RAGDocument input and output."""
315
+ _validate_input_output_types_match(step, RAGDocument, RAGDocument)
316
+
317
+
318
+ def _validate_document_splitter(step: DocumentSplitter) -> None:
319
+ """Validate DocumentSplitter has exactly one RAGDocument input and one RAGChunk output."""
320
+ _validate_input_output_types_match(step, RAGDocument, RAGChunk)
321
+
322
+
323
+ def _validate_document_embedder(step: DocumentEmbedder) -> None:
324
+ """Validate DocumentEmbedder has exactly one RAGChunk input and output."""
325
+ _validate_input_output_types_match(step, RAGChunk, RAGChunk)
326
+
327
+
328
+ def _validate_index_upsert(step: IndexUpsert) -> None:
329
+ if isinstance(step.index, VectorIndex):
330
+ # Validate IndexUpsert has exactly one input of type RAGChunk or RAGDocument.
331
+ _validate_exact_input_count(step, 1)
332
+ input_type = step.inputs[0].type
333
+ if input_type not in (RAGChunk, RAGDocument):
334
+ raise QTypeSemanticError(
335
+ (
336
+ f"IndexUpsert step '{step.id}' on Vector Index '{step.index.id}' input must be of type "
337
+ f"'RAGChunk' or 'RAGDocument', found '{input_type}'."
338
+ )
339
+ )
340
+ else:
341
+ # Document index upsert just stores all variables in the message
342
+ if len(step.inputs) < 1:
343
+ raise QTypeSemanticError(
344
+ (
345
+ f"IndexUpsert step '{step.id}' on Document Index '{step.index.id}' must have at least one input."
346
+ )
347
+ )
348
+
349
+
350
+ def _validate_vector_search(step: VectorSearch) -> None:
351
+ """Validate VectorSearch has exactly one text input and one list[RAGSearchResult] output."""
352
+
353
+ _validate_exact_input_count(step, 1, PrimitiveTypeEnum.text)
354
+ _validate_exact_output_count(
355
+ step, 1, ListType(element_type="RAGSearchResult")
356
+ )
357
+
358
+
359
+ def _validate_document_search(step: DocumentSearch) -> None:
360
+ """Validate DocumentSearch has exactly one text input for the query."""
361
+ from qtype.dsl.model import ListType
362
+
363
+ _validate_exact_input_count(step, 1, PrimitiveTypeEnum.text)
364
+ _validate_exact_output_count(
365
+ step, 1, ListType(element_type="SearchResult")
366
+ )
367
+ # TODO: Restore below when ready to decompose into RAG search results for hybrid search
368
+ # actual_type = step.outputs[0].type
369
+ # acceptable_types = set(
370
+ # [
371
+ # ListType(element_type="RAGSearchResult"),
372
+ # ListType(element_type="SearchResult"),
373
+ # ]
374
+ # )
375
+ # if actual_type not in acceptable_types:
376
+ # raise QTypeSemanticError(
377
+ # (
378
+ # f"DocumentSearch step '{step.id}' output must be of type "
379
+ # f"'list[RAGSearchResult]' or 'list[SearchResult]', found "
380
+ # f"'{actual_type}'."
381
+ # )
382
+ # )
383
+
384
+
385
+ def _validate_flow(flow: Flow) -> None:
386
+ """Validate Flow has more than one step."""
387
+ if len(flow.steps) < 1:
388
+ raise QTypeSemanticError(
389
+ f"Flow '{flow.id}' must have one or more steps, found {len(flow.steps)}."
390
+ )
391
+
392
+ # Crawl the steps and identify all input and output variables.
393
+ fullfilled_variables = {i.id for i in flow.inputs}
394
+
395
+ for step in flow.steps:
396
+ step_input_ids = {i.id for i in step.inputs}
397
+ not_fulfilled = step_input_ids - fullfilled_variables
398
+ if not_fulfilled:
399
+ raise QTypeSemanticError(
400
+ f"Flow '{flow.id}' step '{step.id}' has input variables that are not included in the flow or previous outputs: {not_fulfilled}."
401
+ )
402
+ fullfilled_variables = {
403
+ i.id for i in step.outputs
404
+ } | fullfilled_variables
405
+
406
+ if flow.interface:
407
+ if flow.interface.type == "Conversational":
408
+ # If it's a chat interface, there must be at least one ChatMessage input and one ChatMessage output
409
+ # in the spec. All non-ChatMessage inputs must be specified in the session_inputs part of the flowinterface.
410
+
411
+ # ensure there is one chatmessage input
412
+ chat_message_inputs = {
413
+ i.id for i in flow.inputs if i.type == ChatMessage
414
+ }
415
+ if not len(chat_message_inputs):
416
+ raise QTypeSemanticError(
417
+ f"Flow {flow.id} has a Conversational interface but no ChatMessage inputs."
418
+ )
419
+ if len(chat_message_inputs) > 1:
420
+ raise QTypeSemanticError(
421
+ f"Flow {flow.id} has a Conversational interface but multiple ChatMessage inputs."
422
+ )
423
+
424
+ # ensure non chatmessage inputs are listed as variables in session
425
+ non_chat_message_inputs = {
426
+ i.id for i in flow.inputs
427
+ } - chat_message_inputs
428
+ variables_in_session = {
429
+ i.id for i in flow.interface.session_inputs
430
+ }
431
+ not_in_session = non_chat_message_inputs - variables_in_session
432
+ if not_in_session:
433
+ not_in_session_str = ",".join(not_in_session)
434
+ raise QTypeSemanticError(
435
+ f"Flow {flow.id} is Conversational so {not_in_session_str} inputs must be listed in session_inputs"
436
+ )
437
+
438
+ # ensure there is one chat message output
439
+ chat_message_outputs = {
440
+ i.id for i in flow.outputs if i.type == ChatMessage
441
+ }
442
+ if len(chat_message_outputs) != 1:
443
+ raise QTypeSemanticError(
444
+ f"Flow {flow.id} has a Conversational interface so it must have one and only one ChatMessage output."
445
+ )
446
+
447
+ elif flow.interface.type == "Complete":
448
+ # ensure there is one input and it is text
449
+ prompt_input = [
450
+ i for i in flow.inputs if i.type == PrimitiveTypeEnum.text
451
+ ]
452
+ if len(prompt_input) != 1:
453
+ raise QTypeSemanticError(
454
+ f'Flow has a Complete interface but {len(prompt_input)} prompt inputs -- there should be one input with type text and id "prompt"'
455
+ )
456
+
457
+ # stream if there is at least one output of type text. All other outputs should be returned by the call but not streamed.
458
+ text_outputs = {
459
+ i.id for i in flow.outputs if i.type == PrimitiveTypeEnum.text
460
+ }
461
+ if len(text_outputs) != 1:
462
+ raise QTypeSemanticError(
463
+ f"Flow {flow.id} has a Complete interface but {len(text_outputs)} text outputs -- there should be 1."
464
+ )
465
+
466
+
467
+ def _has_secret_reference(obj: Any) -> bool:
468
+ """
469
+ Recursively check if an object contains any SecretReference instances.
470
+
471
+ This function traverses Pydantic models, lists, and dictionaries to find
472
+ any SecretReference instances that require a secret manager for resolution.
473
+
474
+ Args:
475
+ obj: Object to check - can be a Pydantic BaseModel, list, dict,
476
+ SecretReference, or any other Python object
477
+
478
+ Returns:
479
+ True if SecretReference is found anywhere in the object graph,
480
+ False otherwise
481
+
482
+ Examples:
483
+ >>> from qtype.semantic.model import SecretReference
484
+ >>> _has_secret_reference("plain string")
485
+ False
486
+ >>> _has_secret_reference(SecretReference(secret_name="my-secret"))
487
+ True
488
+ >>> _has_secret_reference({"key": SecretReference(secret_name="s")})
489
+ True
490
+ """
491
+ # Direct check - most common case
492
+ if isinstance(obj, SecretReference):
493
+ return True
494
+
495
+ # Check Pydantic models by iterating over field values
496
+ if isinstance(obj, BaseModel):
497
+ for field_name, field_value in obj:
498
+ if _has_secret_reference(field_value):
499
+ return True
500
+
501
+ # Check lists
502
+ elif isinstance(obj, list):
503
+ for item in obj:
504
+ if _has_secret_reference(item):
505
+ return True
506
+
507
+ # Check dictionaries
508
+ elif isinstance(obj, dict):
509
+ for value in obj.values():
510
+ if _has_secret_reference(value):
511
+ return True
512
+
513
+ return False
514
+
515
+
516
+ def _validate_application(application: Application) -> None:
517
+ """
518
+ Validate Application configuration.
519
+
520
+ Args:
521
+ application: The Application to validate
522
+
523
+ Raises:
524
+ QTypeSemanticError: If SecretReference is used but
525
+ secret_manager is not configured, or if secret_manager
526
+ configuration is invalid
527
+ """
528
+ if application.secret_manager is None:
529
+ # Check if any SecretReference is used in the application
530
+ if _has_secret_reference(application):
531
+ raise QTypeSemanticError(
532
+ (
533
+ f"Application '{application.id}' uses SecretReference "
534
+ "but does not have a secret_manager configured. "
535
+ "Please add a secret_manager to the application."
536
+ )
537
+ )
538
+ else:
539
+ # Validate secret_manager configuration
540
+ from qtype.semantic.model import AWSAuthProvider, AWSSecretManager
541
+
542
+ secret_mgr = application.secret_manager
543
+
544
+ # For AWSSecretManager, verify auth is AWSAuthProvider
545
+ # (linker ensures the reference exists, we just check the type)
546
+ if isinstance(secret_mgr, AWSSecretManager):
547
+ auth_provider = secret_mgr.auth
548
+ if not isinstance(auth_provider, AWSAuthProvider):
549
+ raise QTypeSemanticError(
550
+ (
551
+ f"AWSSecretManager '{secret_mgr.id}' requires an "
552
+ f"AWSAuthProvider but references '{auth_provider.id}' "
553
+ f"which is of type '{type(auth_provider).__name__}'"
554
+ )
555
+ )
556
+
557
+
558
+ def _validate_bedrock_reranker(reranker: BedrockReranker) -> None:
559
+ """Validate BedrockReranker configuration."""
560
+ _validate_exact_output_count(
561
+ reranker, 1, ListType(element_type="SearchResult")
562
+ )
563
+ _validate_exact_input_count(reranker, 2)
564
+ # Confirm at least one input is text (the query)
565
+ input_types = [inp.type for inp in reranker.inputs] # type: ignore
566
+ if PrimitiveTypeEnum.text not in input_types:
567
+ raise QTypeSemanticError(
568
+ (
569
+ f"BedrockReranker step '{reranker.id}' must have at least one "
570
+ f"input of type 'text' for the query, found input types: "
571
+ f"{input_types}."
572
+ )
573
+ )
574
+ # Confirm at least one input is list[SearchResult] (the results to rerank)
575
+ if ListType(element_type="SearchResult") not in input_types:
576
+ raise QTypeSemanticError(
577
+ (
578
+ f"BedrockReranker step '{reranker.id}' must have at least one "
579
+ f"input of type 'list[SearchResult]' for the results to rerank, "
580
+ f"found input types: {input_types}."
581
+ )
582
+ )
583
+
584
+
585
+ # Mapping of types to their validation functions
586
+ _VALIDATORS = {
587
+ Agent: _validate_agent,
588
+ Application: _validate_application,
589
+ AWSAuthProvider: _validate_aws_auth,
590
+ BedrockReranker: _validate_bedrock_reranker,
591
+ Decoder: _validate_decoder,
592
+ DocToTextConverter: _validate_doc_to_text_converter,
593
+ DocumentEmbedder: _validate_document_embedder,
594
+ DocumentSearch: _validate_document_search,
595
+ DocumentSource: _validate_document_source,
596
+ DocumentSplitter: _validate_document_splitter,
597
+ Echo: _validate_echo,
598
+ FieldExtractor: _validate_field_extractor,
599
+ Flow: _validate_flow,
600
+ IndexUpsert: _validate_index_upsert,
601
+ LLMInference: _validate_llm_inference,
602
+ PromptTemplate: _validate_prompt_template,
603
+ SQLSource: _validate_sql_source,
604
+ VectorSearch: _validate_vector_search,
605
+ }
606
+
607
+
608
+ def check(model: BaseModel) -> None:
609
+ """
610
+ Recursively validate a pydantic BaseModel and all its fields.
611
+
612
+ For each field, if its type has a registered validator, call that validator.
613
+ Then recursively validate the field value itself.
614
+
615
+ Args:
616
+ model: The pydantic BaseModel instance to validate
617
+
618
+ Raises:
619
+ QTypeSemanticError: If any validation rules are violated
620
+ """
621
+ # Check if this model type has a validator
622
+ model_type = type(model)
623
+ if model_type in _VALIDATORS:
624
+ _VALIDATORS[model_type](model)
625
+
626
+ # Recursively validate all fields
627
+ for field_name, field_value in model:
628
+ if field_value is None:
629
+ continue
630
+
631
+ # Handle lists
632
+ if isinstance(field_value, list):
633
+ for item in field_value:
634
+ if isinstance(item, BaseModel):
635
+ check(item)
636
+ # Handle dicts
637
+ elif isinstance(field_value, dict):
638
+ for value in field_value.values():
639
+ if isinstance(value, BaseModel):
640
+ check(value)
641
+ # Handle BaseModel instances
642
+ elif isinstance(field_value, BaseModel):
643
+ check(field_value)