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,92 @@
1
+ from typing import AsyncIterator
2
+
3
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
4
+
5
+ from qtype.base.types import PrimitiveTypeEnum
6
+ from qtype.dsl.domain_types import Embedding
7
+ from qtype.interpreter.base.base_step_executor import StepExecutor
8
+ from qtype.interpreter.base.executor_context import ExecutorContext
9
+ from qtype.interpreter.conversions import to_embedding_model
10
+ from qtype.interpreter.types import FlowMessage
11
+ from qtype.semantic.model import InvokeEmbedding
12
+
13
+
14
+ class InvokeEmbeddingExecutor(StepExecutor):
15
+ """Executor for InvokeEmbedding steps."""
16
+
17
+ # Embedding operations should be marked as EMBEDDING type
18
+ span_kind = OpenInferenceSpanKindValues.EMBEDDING
19
+
20
+ def __init__(
21
+ self, step: InvokeEmbedding, context: ExecutorContext, **dependencies
22
+ ):
23
+ super().__init__(step, context, **dependencies)
24
+ if not isinstance(step, InvokeEmbedding):
25
+ raise ValueError(
26
+ (
27
+ "InvokeEmbeddingExecutor can only execute "
28
+ "InvokeEmbedding steps."
29
+ )
30
+ )
31
+ self.step: InvokeEmbedding = step
32
+ # Initialize the embedding model once for the executor
33
+ self.embedding_model = to_embedding_model(self.step.model)
34
+
35
+ async def process_message(
36
+ self,
37
+ message: FlowMessage,
38
+ ) -> AsyncIterator[FlowMessage]:
39
+ """Process a single FlowMessage for the InvokeEmbedding step.
40
+
41
+ Args:
42
+ message: The FlowMessage to process.
43
+ Yields:
44
+ FlowMessage with embedding.
45
+ """
46
+ input_id = self.step.inputs[0].id
47
+ input_type = self.step.inputs[0].type
48
+ output_id = self.step.outputs[0].id
49
+
50
+ try:
51
+ # Get the input value
52
+ input_value = message.variables.get(input_id)
53
+
54
+ if input_value is None:
55
+ raise ValueError(f"Input variable '{input_id}' is missing")
56
+
57
+ # Generate embedding based on input type
58
+ if input_type == PrimitiveTypeEnum.text:
59
+ if not isinstance(input_value, str):
60
+ input_value = str(input_value)
61
+ vector = self.embedding_model.get_text_embedding(
62
+ text=input_value
63
+ )
64
+ content = input_value
65
+ elif input_type == PrimitiveTypeEnum.image:
66
+ # For image embeddings
67
+ vector = self.embedding_model.get_image_embedding(
68
+ image_path=input_value
69
+ )
70
+ content = input_value
71
+ else:
72
+ raise ValueError(
73
+ (
74
+ f"Unsupported input type for embedding: "
75
+ f"{input_type}. Must be 'text' or 'image'."
76
+ )
77
+ )
78
+
79
+ # Create the Embedding object
80
+ embedding = Embedding(
81
+ vector=vector,
82
+ content=content,
83
+ )
84
+
85
+ # Yield the result
86
+ yield message.copy_with_variables({output_id: embedding})
87
+
88
+ except Exception as e:
89
+ # Emit error event to stream so frontend can display it
90
+ await self.stream_emitter.error(str(e))
91
+ message.set_error(self.step.id, e)
92
+ yield message
@@ -0,0 +1,51 @@
1
+ from typing import AsyncIterator
2
+
3
+ from qtype.interpreter.base.base_step_executor import StepExecutor
4
+ from qtype.interpreter.base.executor_context import ExecutorContext
5
+ from qtype.interpreter.types import FlowMessage
6
+ from qtype.semantic.model import InvokeFlow
7
+
8
+
9
+ class InvokeFlowExecutor(StepExecutor):
10
+ """Executor for InvokeFlow steps."""
11
+
12
+ def __init__(
13
+ self, step: InvokeFlow, context: ExecutorContext, **dependencies
14
+ ):
15
+ super().__init__(step, context, **dependencies)
16
+ if not isinstance(step, InvokeFlow):
17
+ raise ValueError(
18
+ ("InvokeFlowExecutor can only execute InvokeFlow steps.")
19
+ )
20
+ self.step: InvokeFlow = step
21
+
22
+ async def process_message(
23
+ self, message: FlowMessage
24
+ ) -> AsyncIterator[FlowMessage]:
25
+ """Process a single FlowMessage for the InvokeFlow step.
26
+
27
+ Args:
28
+ message: The FlowMessage to process.
29
+ Yields:
30
+ FlowMessage with results from the invoked flow.
31
+ """
32
+ from qtype.interpreter.flow import run_flow
33
+
34
+ initial = message.copy_with_variables(
35
+ {
36
+ id: message.variables.get(var.id)
37
+ for var, id in self.step.input_bindings.items()
38
+ }
39
+ )
40
+ # Pass through context (already available as self.context)
41
+ result = await run_flow(
42
+ self.step.flow, [initial], context=self.context
43
+ )
44
+
45
+ for msg in result:
46
+ yield msg.copy_with_variables(
47
+ {
48
+ var.id: msg.variables.get(id)
49
+ for var, id in self.step.output_bindings.items()
50
+ }
51
+ )
@@ -0,0 +1,353 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import logging
5
+ import time
6
+ import uuid
7
+ from typing import Any, AsyncIterator
8
+
9
+ import requests
10
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
11
+ from pydantic import BaseModel
12
+
13
+ from qtype.interpreter.base.base_step_executor import StepExecutor
14
+ from qtype.interpreter.base.executor_context import ExecutorContext
15
+ from qtype.interpreter.base.stream_emitter import StreamEmitter
16
+ from qtype.interpreter.types import FlowMessage
17
+ from qtype.semantic.model import (
18
+ APITool,
19
+ BearerTokenAuthProvider,
20
+ InvokeTool,
21
+ PythonFunctionTool,
22
+ Step,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # HTTP methods that require request body instead of query parameters
28
+ HTTP_BODY_METHODS = frozenset(["POST", "PUT", "PATCH"])
29
+
30
+
31
+ class ToolExecutionMixin:
32
+ """Mixin providing tool execution capabilities for Python and API tools.
33
+
34
+ This mixin can be used by any executor that needs to invoke tools,
35
+ allowing code reuse across InvokeToolExecutor and AgentExecutor.
36
+ """
37
+
38
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
39
+ super().__init__(*args, **kwargs)
40
+ # These will be set by the concrete executor classes
41
+ self.stream_emitter: StreamEmitter
42
+ self.step: Step
43
+ self._resolve_secret: (
44
+ Any # Will be provided by StepExecutor base class
45
+ )
46
+
47
+ async def execute_python_tool(
48
+ self,
49
+ tool: PythonFunctionTool,
50
+ inputs: dict[str, Any],
51
+ original_inputs: dict[str, Any] | None = None,
52
+ ) -> Any:
53
+ """Execute a Python function tool with proper streaming events.
54
+
55
+ Args:
56
+ tool: The Python function tool to execute.
57
+ inputs: Dictionary of input parameter names to values
58
+ (may contain parsed Python objects like datetime).
59
+ original_inputs: Optional dictionary of original JSON-serializable
60
+ inputs for streaming events. If not provided, uses inputs.
61
+
62
+ Returns:
63
+ The result from the function call.
64
+
65
+ Raises:
66
+ ValueError: If the function cannot be found or executed.
67
+ """
68
+ tool_call_id = str(uuid.uuid4())
69
+
70
+ # Use original inputs for streaming events if provided
71
+ stream_inputs = original_inputs if original_inputs else inputs
72
+
73
+ async with self.stream_emitter.tool_execution(
74
+ tool_call_id=tool_call_id,
75
+ tool_name=tool.function_name,
76
+ tool_input=stream_inputs,
77
+ ) as tool_ctx:
78
+ try:
79
+ module = importlib.import_module(tool.module_path)
80
+ function = getattr(module, tool.function_name, None)
81
+ if function is None:
82
+ raise ValueError(
83
+ (
84
+ f"Function '{tool.function_name}' not found in "
85
+ f"module '{tool.module_path}'"
86
+ )
87
+ )
88
+
89
+ result = function(**inputs)
90
+ await tool_ctx.complete(result)
91
+ return result
92
+
93
+ except Exception as e:
94
+ error_msg = (
95
+ f"Failed to execute function {tool.function_name}: {e}"
96
+ )
97
+ logger.error(error_msg, exc_info=True)
98
+ await tool_ctx.error(error_msg)
99
+ raise ValueError(error_msg) from e
100
+
101
+ def serialize_value(self, value: Any) -> Any:
102
+ """Recursively serialize values for API requests.
103
+
104
+ Args:
105
+ value: The value to serialize.
106
+
107
+ Returns:
108
+ Serialized value suitable for JSON encoding.
109
+ """
110
+ if isinstance(value, dict):
111
+ return {k: self.serialize_value(v) for k, v in value.items()}
112
+ elif isinstance(value, list):
113
+ return [self.serialize_value(item) for item in value]
114
+ elif isinstance(value, BaseModel):
115
+ return value.model_dump()
116
+ return value
117
+
118
+ async def execute_api_tool(
119
+ self,
120
+ tool: APITool,
121
+ inputs: dict[str, Any],
122
+ original_inputs: dict[str, Any] | None = None,
123
+ ) -> Any:
124
+ """Execute an API tool by making an HTTP request with proper streaming events.
125
+
126
+ Args:
127
+ tool: The API tool to execute.
128
+ inputs: Dictionary of input parameter names to values
129
+ (may contain parsed Python objects like datetime).
130
+ original_inputs: Optional dictionary of original JSON-serializable
131
+ inputs for streaming events. If not provided, uses inputs.
132
+
133
+ Returns:
134
+ The result from the API call.
135
+
136
+ Raises:
137
+ ValueError: If authentication fails or the request fails.
138
+ """
139
+ tool_call_id = str(uuid.uuid4())
140
+
141
+ # Use original inputs for streaming events if provided
142
+ stream_inputs = original_inputs if original_inputs else inputs
143
+
144
+ async with self.stream_emitter.tool_execution(
145
+ tool_call_id=tool_call_id,
146
+ tool_name=f"{tool.method} {tool.endpoint}",
147
+ tool_input=stream_inputs,
148
+ ) as tool_ctx:
149
+ try:
150
+ # Prepare headers - resolve any SecretReferences
151
+ # Note: ToolExecutionMixin users inherit from StepExecutor
152
+ # which provides _secret_manager
153
+ secret_manager = getattr(self, "_secret_manager")
154
+ context = f"tool '{tool.id}'"
155
+ headers = (
156
+ secret_manager.resolve_secrets_in_dict(
157
+ tool.headers, context
158
+ )
159
+ if tool.headers
160
+ else {}
161
+ )
162
+
163
+ # Handle authentication
164
+ if tool.auth:
165
+ if isinstance(tool.auth, BearerTokenAuthProvider):
166
+ token = self._resolve_secret(tool.auth.token)
167
+ headers["Authorization"] = f"Bearer {token}"
168
+ else:
169
+ raise ValueError(
170
+ (
171
+ f"Unsupported auth provider: {type(tool.auth).__name__}"
172
+ )
173
+ )
174
+
175
+ # Serialize inputs for JSON
176
+ body = self.serialize_value(inputs)
177
+
178
+ # Determine if we're sending body or query params
179
+ is_body_method = tool.method.upper() in HTTP_BODY_METHODS
180
+
181
+ start_time = time.time()
182
+
183
+ response = requests.request(
184
+ method=tool.method.upper(),
185
+ url=tool.endpoint,
186
+ headers=headers,
187
+ params=None if is_body_method else inputs,
188
+ json=body if is_body_method else None,
189
+ )
190
+
191
+ duration = time.time() - start_time
192
+
193
+ # Raise for HTTP errors
194
+ response.raise_for_status()
195
+
196
+ logger.debug(
197
+ f"Request completed in {duration:.2f}s with status "
198
+ f"{response.status_code}"
199
+ )
200
+
201
+ result = response.json()
202
+ await tool_ctx.complete(result)
203
+ return result
204
+
205
+ except requests.exceptions.RequestException as e:
206
+ error_msg = f"API request failed: {e}"
207
+ await tool_ctx.error(error_msg)
208
+ raise ValueError(error_msg) from e
209
+ except ValueError as e:
210
+ error_msg = f"Failed to decode JSON response: {e}"
211
+ await tool_ctx.error(error_msg)
212
+ raise ValueError(error_msg) from e
213
+
214
+
215
+ class InvokeToolExecutor(StepExecutor, ToolExecutionMixin):
216
+ """Executor for InvokeTool steps."""
217
+
218
+ # Tool invocations should be marked as TOOL type
219
+ span_kind = OpenInferenceSpanKindValues.TOOL
220
+
221
+ def __init__(
222
+ self, step: InvokeTool, context: ExecutorContext, **dependencies: Any
223
+ ) -> None:
224
+ super().__init__(step, context, **dependencies)
225
+ if not isinstance(step, InvokeTool):
226
+ raise ValueError(
227
+ "InvokeToolExecutor can only execute InvokeTool steps."
228
+ )
229
+ self.step: InvokeTool = step
230
+
231
+ def _prepare_tool_inputs(self, message: FlowMessage) -> dict[str, Any]:
232
+ """Prepare tool inputs from message variables using input bindings.
233
+
234
+ Args:
235
+ message: The FlowMessage containing input variables.
236
+
237
+ Returns:
238
+ Dictionary mapping tool parameter names to values.
239
+
240
+ Raises:
241
+ ValueError: If required inputs are missing.
242
+ """
243
+ tool_inputs = {}
244
+
245
+ for tool_param_name, step_var_id in self.step.input_bindings.items():
246
+ # Get tool parameter definition
247
+ tool_param = self.step.tool.inputs.get(tool_param_name)
248
+ if not tool_param:
249
+ raise ValueError(
250
+ f"Tool parameter '{tool_param_name}' not defined in tool"
251
+ )
252
+
253
+ # Get value from message variables
254
+ value = message.variables.get(step_var_id)
255
+
256
+ # Handle missing values
257
+ if value is None:
258
+ if not tool_param.optional:
259
+ raise ValueError(
260
+ (
261
+ f"Required input '{step_var_id}' for tool "
262
+ f"parameter '{tool_param_name}' is missing"
263
+ )
264
+ )
265
+ # Skip optional parameters that are missing
266
+ continue
267
+
268
+ tool_inputs[tool_param_name] = value
269
+
270
+ return tool_inputs
271
+
272
+ def _extract_tool_outputs(self, result: Any) -> dict[str, Any]:
273
+ """Extract output variables from tool result using output bindings.
274
+
275
+ Args:
276
+ result: The result from tool execution.
277
+
278
+ Returns:
279
+ Dictionary mapping step variable IDs to their values.
280
+
281
+ Raises:
282
+ ValueError: If required outputs are missing from result.
283
+ """
284
+ output_vars = {}
285
+
286
+ for tool_param_name, step_var_id in self.step.output_bindings.items():
287
+ # Get tool parameter definition
288
+ tool_param = self.step.tool.outputs.get(tool_param_name)
289
+ if not tool_param:
290
+ raise ValueError(
291
+ f"Tool parameter '{tool_param_name}' not defined in tool"
292
+ )
293
+
294
+ # Extract value from result
295
+ if isinstance(result, dict):
296
+ value = result.get(tool_param_name)
297
+ if value is None and not tool_param.optional:
298
+ raise ValueError(
299
+ (
300
+ f"Required output '{tool_param_name}' not found "
301
+ f"in result. Available: {list(result.keys())}"
302
+ )
303
+ )
304
+ else:
305
+ # Single output case - use entire result
306
+ value = result
307
+
308
+ if value is not None:
309
+ output_vars[step_var_id] = value
310
+
311
+ return output_vars
312
+
313
+ async def process_message(
314
+ self,
315
+ message: FlowMessage,
316
+ ) -> AsyncIterator[FlowMessage]:
317
+ """Process a single FlowMessage for the InvokeTool step.
318
+
319
+ Args:
320
+ message: The FlowMessage to process.
321
+ Yields:
322
+ FlowMessage with tool execution results.
323
+ """
324
+ try:
325
+ # Prepare tool inputs from message variables
326
+ tool_inputs = self._prepare_tool_inputs(message)
327
+
328
+ # Execute the tool with proper tool execution context
329
+ # Dispatch to appropriate execution method based on tool type
330
+ if isinstance(self.step.tool, PythonFunctionTool):
331
+ result = await self.execute_python_tool(
332
+ self.step.tool, tool_inputs
333
+ )
334
+ elif isinstance(self.step.tool, APITool):
335
+ result = await self.execute_api_tool(
336
+ self.step.tool, tool_inputs
337
+ )
338
+ else:
339
+ raise ValueError(
340
+ f"Unsupported tool type: {type(self.step.tool).__name__}"
341
+ )
342
+
343
+ # Extract outputs from result
344
+ output_vars = self._extract_tool_outputs(result)
345
+
346
+ # Yield the result
347
+ yield message.copy_with_variables(output_vars)
348
+
349
+ except Exception as e:
350
+ # Emit error event to stream so frontend can display it
351
+ await self.stream_emitter.error(str(e))
352
+ message.set_error(self.step.id, e)
353
+ yield message