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