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.
- qtype/application/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +5 -5
- qtype/application/converters/tools_from_module.py +2 -2
- qtype/application/converters/types.py +14 -43
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +92 -71
- qtype/base/types.py +227 -7
- qtype/commands/convert.py +20 -8
- qtype/commands/generate.py +19 -27
- qtype/commands/run.py +54 -36
- qtype/commands/serve.py +74 -54
- qtype/commands/validate.py +34 -8
- qtype/commands/visualize.py +46 -22
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +65 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +612 -363
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +58 -135
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +429 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +74 -0
- qtype/interpreter/base/factory.py +117 -0
- qtype/interpreter/base/progress_tracker.py +75 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +73 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +455 -21
- qtype/interpreter/converters.py +73 -0
- qtype/interpreter/endpoints.py +355 -0
- qtype/interpreter/executors/agent_executor.py +242 -0
- qtype/interpreter/executors/aggregate_executor.py +93 -0
- qtype/interpreter/executors/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +75 -0
- qtype/interpreter/executors/document_search_executor.py +122 -0
- qtype/interpreter/executors/document_source_executor.py +118 -0
- qtype/interpreter/executors/document_splitter_executor.py +105 -0
- qtype/interpreter/executors/echo_executor.py +63 -0
- qtype/interpreter/executors/field_extractor_executor.py +160 -0
- qtype/interpreter/executors/file_source_executor.py +101 -0
- qtype/interpreter/executors/file_writer_executor.py +110 -0
- qtype/interpreter/executors/index_upsert_executor.py +228 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +353 -0
- qtype/interpreter/executors/llm_inference_executor.py +272 -0
- qtype/interpreter/executors/prompt_template_executor.py +78 -0
- qtype/interpreter/executors/sql_source_executor.py +106 -0
- qtype/interpreter/executors/vector_search_executor.py +91 -0
- qtype/interpreter/flow.py +147 -22
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/stream/chat/__init__.py +15 -0
- qtype/interpreter/stream/chat/converter.py +391 -0
- qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
- qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
- qtype/interpreter/stream/chat/vercel.py +609 -0
- qtype/interpreter/stream/utils/__init__.py +15 -0
- qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
- qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
- qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
- qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
- qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
- qtype/interpreter/telemetry.py +135 -8
- qtype/interpreter/tools/__init__.py +5 -0
- qtype/interpreter/tools/function_tool_helper.py +265 -0
- qtype/interpreter/types.py +328 -0
- qtype/interpreter/typing.py +83 -89
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
- qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
- qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +4 -4
- qtype/semantic/checker.py +583 -0
- qtype/semantic/generate.py +262 -83
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +436 -159
- qtype/semantic/resolver.py +59 -17
- qtype/semantic/visualize.py +28 -31
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/METADATA +16 -3
- qtype-0.1.0.dist-info/RECORD +134 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -465
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/file_sink_source.py +0 -162
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -92
- qtype/interpreter/batch/step.py +0 -74
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -178
- qtype/interpreter/chat/chat_api.py +0 -237
- qtype/interpreter/chat/vercel.py +0 -314
- qtype/interpreter/exceptions.py +0 -10
- qtype/interpreter/step.py +0 -67
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +0 -114
- qtype/interpreter/steps/condition.py +0 -36
- qtype/interpreter/steps/decoder.py +0 -88
- qtype/interpreter/steps/llm_inference.py +0 -171
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -219
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -390
- qtype-0.0.15.dist-info/RECORD +0 -106
- /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/WHEEL +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.15.dist-info → qtype-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|