qtype 0.0.16__py3-none-any.whl → 0.1.1__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 (128) 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 +94 -73
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +4 -0
  9. qtype/commands/convert.py +20 -8
  10. qtype/commands/generate.py +19 -27
  11. qtype/commands/run.py +73 -36
  12. qtype/commands/serve.py +74 -54
  13. qtype/commands/validate.py +34 -8
  14. qtype/commands/visualize.py +46 -22
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +65 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +612 -363
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +57 -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 +74 -0
  30. qtype/interpreter/base/factory.py +117 -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 +462 -22
  36. qtype/interpreter/converters.py +77 -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/decoder_executor.py +163 -0
  41. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  42. qtype/interpreter/executors/document_embedder_executor.py +107 -0
  43. qtype/interpreter/executors/document_search_executor.py +122 -0
  44. qtype/interpreter/executors/document_source_executor.py +118 -0
  45. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  46. qtype/interpreter/executors/echo_executor.py +63 -0
  47. qtype/interpreter/executors/field_extractor_executor.py +160 -0
  48. qtype/interpreter/executors/file_source_executor.py +101 -0
  49. qtype/interpreter/executors/file_writer_executor.py +110 -0
  50. qtype/interpreter/executors/index_upsert_executor.py +228 -0
  51. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  52. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  53. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  54. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  55. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  56. qtype/interpreter/executors/sql_source_executor.py +106 -0
  57. qtype/interpreter/executors/vector_search_executor.py +91 -0
  58. qtype/interpreter/flow.py +159 -22
  59. qtype/interpreter/metadata_api.py +115 -0
  60. qtype/interpreter/resource_cache.py +5 -4
  61. qtype/interpreter/rich_progress.py +225 -0
  62. qtype/interpreter/stream/chat/__init__.py +15 -0
  63. qtype/interpreter/stream/chat/converter.py +391 -0
  64. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  65. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  66. qtype/interpreter/stream/chat/vercel.py +609 -0
  67. qtype/interpreter/stream/utils/__init__.py +15 -0
  68. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  69. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  70. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  71. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  72. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  73. qtype/interpreter/telemetry.py +135 -8
  74. qtype/interpreter/tools/__init__.py +5 -0
  75. qtype/interpreter/tools/function_tool_helper.py +265 -0
  76. qtype/interpreter/types.py +330 -0
  77. qtype/interpreter/typing.py +83 -89
  78. qtype/interpreter/ui/404/index.html +1 -1
  79. qtype/interpreter/ui/404.html +1 -1
  80. qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  81. qtype/interpreter/ui/_next/static/chunks/{393-8fd474427f8e19ce.js → 434-b2112d19f25c44ff.js} +3 -3
  82. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  83. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  84. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  85. qtype/interpreter/ui/icon.png +0 -0
  86. qtype/interpreter/ui/index.html +1 -1
  87. qtype/interpreter/ui/index.txt +4 -4
  88. qtype/semantic/checker.py +583 -0
  89. qtype/semantic/generate.py +262 -83
  90. qtype/semantic/loader.py +95 -0
  91. qtype/semantic/model.py +436 -159
  92. qtype/semantic/resolver.py +63 -19
  93. qtype/semantic/visualize.py +28 -31
  94. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/METADATA +16 -3
  95. qtype-0.1.1.dist-info/RECORD +135 -0
  96. qtype/dsl/base_types.py +0 -38
  97. qtype/dsl/validator.py +0 -465
  98. qtype/interpreter/batch/__init__.py +0 -0
  99. qtype/interpreter/batch/file_sink_source.py +0 -162
  100. qtype/interpreter/batch/flow.py +0 -95
  101. qtype/interpreter/batch/sql_source.py +0 -92
  102. qtype/interpreter/batch/step.py +0 -74
  103. qtype/interpreter/batch/types.py +0 -41
  104. qtype/interpreter/batch/utils.py +0 -178
  105. qtype/interpreter/chat/chat_api.py +0 -237
  106. qtype/interpreter/chat/vercel.py +0 -314
  107. qtype/interpreter/exceptions.py +0 -10
  108. qtype/interpreter/step.py +0 -67
  109. qtype/interpreter/steps/__init__.py +0 -0
  110. qtype/interpreter/steps/agent.py +0 -114
  111. qtype/interpreter/steps/condition.py +0 -36
  112. qtype/interpreter/steps/decoder.py +0 -88
  113. qtype/interpreter/steps/llm_inference.py +0 -171
  114. qtype/interpreter/steps/prompt_template.py +0 -54
  115. qtype/interpreter/steps/search.py +0 -24
  116. qtype/interpreter/steps/tool.py +0 -219
  117. qtype/interpreter/streaming_helpers.py +0 -123
  118. qtype/interpreter/ui/_next/static/chunks/app/page-7e26b6156cfb55d3.js +0 -1
  119. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  120. qtype/interpreter/ui/_next/static/css/b40532b0db09cce3.css +0 -3
  121. qtype/interpreter/ui/favicon.ico +0 -0
  122. qtype/loader.py +0 -390
  123. qtype-0.0.16.dist-info/RECORD +0 -106
  124. /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  125. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/WHEEL +0 -0
  126. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/entry_points.txt +0 -0
  127. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/licenses/LICENSE +0 -0
  128. {qtype-0.0.16.dist-info → qtype-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,358 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import importlib
5
+ import inspect
6
+ import logging
7
+ import time
8
+ import uuid
9
+ from typing import Any, AsyncIterator
10
+
11
+ import requests
12
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
13
+ from pydantic import BaseModel
14
+
15
+ from qtype.interpreter.base.base_step_executor import StepExecutor
16
+ from qtype.interpreter.base.executor_context import ExecutorContext
17
+ from qtype.interpreter.base.stream_emitter import StreamEmitter
18
+ from qtype.interpreter.types import FlowMessage
19
+ from qtype.semantic.model import (
20
+ APITool,
21
+ BearerTokenAuthProvider,
22
+ InvokeTool,
23
+ PythonFunctionTool,
24
+ Step,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # HTTP methods that require request body instead of query parameters
30
+ HTTP_BODY_METHODS = frozenset(["POST", "PUT", "PATCH"])
31
+
32
+
33
+ class ToolExecutionMixin:
34
+ """Mixin providing tool execution capabilities for Python and API tools.
35
+
36
+ This mixin can be used by any executor that needs to invoke tools,
37
+ allowing code reuse across InvokeToolExecutor and AgentExecutor.
38
+ """
39
+
40
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
41
+ super().__init__(*args, **kwargs)
42
+ # These will be set by the concrete executor classes
43
+ self.stream_emitter: StreamEmitter
44
+ self.step: Step
45
+ self._resolve_secret: (
46
+ Any # Will be provided by StepExecutor base class
47
+ )
48
+
49
+ async def execute_python_tool(
50
+ self,
51
+ tool: PythonFunctionTool,
52
+ inputs: dict[str, Any],
53
+ original_inputs: dict[str, Any] | None = None,
54
+ ) -> Any:
55
+ """Execute a Python function tool with proper streaming events.
56
+
57
+ Args:
58
+ tool: The Python function tool to execute.
59
+ inputs: Dictionary of input parameter names to values
60
+ (may contain parsed Python objects like datetime).
61
+ original_inputs: Optional dictionary of original JSON-serializable
62
+ inputs for streaming events. If not provided, uses inputs.
63
+
64
+ Returns:
65
+ The result from the function call.
66
+
67
+ Raises:
68
+ ValueError: If the function cannot be found or executed.
69
+ """
70
+ tool_call_id = str(uuid.uuid4())
71
+
72
+ # Use original inputs for streaming events if provided
73
+ stream_inputs = original_inputs if original_inputs else inputs
74
+
75
+ async with self.stream_emitter.tool_execution(
76
+ tool_call_id=tool_call_id,
77
+ tool_name=tool.function_name,
78
+ tool_input=stream_inputs,
79
+ ) as tool_ctx:
80
+ try:
81
+ module = importlib.import_module(tool.module_path)
82
+ function = getattr(module, tool.function_name, None)
83
+ if function is None:
84
+ raise ValueError(
85
+ (
86
+ f"Function '{tool.function_name}' not found in "
87
+ f"module '{tool.module_path}'"
88
+ )
89
+ )
90
+
91
+ if inspect.iscoroutinefunction(function):
92
+ result = await function(**inputs)
93
+ else:
94
+ result = await asyncio.to_thread(function, **inputs)
95
+ await tool_ctx.complete(result)
96
+ return result
97
+
98
+ except Exception as e:
99
+ error_msg = (
100
+ f"Failed to execute function {tool.function_name}: {e}"
101
+ )
102
+ logger.error(error_msg, exc_info=True)
103
+ await tool_ctx.error(error_msg)
104
+ raise ValueError(error_msg) from e
105
+
106
+ def serialize_value(self, value: Any) -> Any:
107
+ """Recursively serialize values for API requests.
108
+
109
+ Args:
110
+ value: The value to serialize.
111
+
112
+ Returns:
113
+ Serialized value suitable for JSON encoding.
114
+ """
115
+ if isinstance(value, dict):
116
+ return {k: self.serialize_value(v) for k, v in value.items()}
117
+ elif isinstance(value, list):
118
+ return [self.serialize_value(item) for item in value]
119
+ elif isinstance(value, BaseModel):
120
+ return value.model_dump()
121
+ return value
122
+
123
+ async def execute_api_tool(
124
+ self,
125
+ tool: APITool,
126
+ inputs: dict[str, Any],
127
+ original_inputs: dict[str, Any] | None = None,
128
+ ) -> Any:
129
+ """Execute an API tool by making an HTTP request with proper streaming events.
130
+
131
+ Args:
132
+ tool: The API tool to execute.
133
+ inputs: Dictionary of input parameter names to values
134
+ (may contain parsed Python objects like datetime).
135
+ original_inputs: Optional dictionary of original JSON-serializable
136
+ inputs for streaming events. If not provided, uses inputs.
137
+
138
+ Returns:
139
+ The result from the API call.
140
+
141
+ Raises:
142
+ ValueError: If authentication fails or the request fails.
143
+ """
144
+ tool_call_id = str(uuid.uuid4())
145
+
146
+ # Use original inputs for streaming events if provided
147
+ stream_inputs = original_inputs if original_inputs else inputs
148
+
149
+ async with self.stream_emitter.tool_execution(
150
+ tool_call_id=tool_call_id,
151
+ tool_name=f"{tool.method} {tool.endpoint}",
152
+ tool_input=stream_inputs,
153
+ ) as tool_ctx:
154
+ try:
155
+ # Prepare headers - resolve any SecretReferences
156
+ # Note: ToolExecutionMixin users inherit from StepExecutor
157
+ # which provides _secret_manager
158
+ secret_manager = getattr(self, "_secret_manager")
159
+ context = f"tool '{tool.id}'"
160
+ headers = (
161
+ secret_manager.resolve_secrets_in_dict(
162
+ tool.headers, context
163
+ )
164
+ if tool.headers
165
+ else {}
166
+ )
167
+
168
+ # Handle authentication
169
+ if tool.auth:
170
+ if isinstance(tool.auth, BearerTokenAuthProvider):
171
+ token = self._resolve_secret(tool.auth.token)
172
+ headers["Authorization"] = f"Bearer {token}"
173
+ else:
174
+ raise ValueError(
175
+ (
176
+ f"Unsupported auth provider: {type(tool.auth).__name__}"
177
+ )
178
+ )
179
+
180
+ # Serialize inputs for JSON
181
+ body = self.serialize_value(inputs)
182
+
183
+ # Determine if we're sending body or query params
184
+ is_body_method = tool.method.upper() in HTTP_BODY_METHODS
185
+
186
+ start_time = time.time()
187
+
188
+ response = requests.request(
189
+ method=tool.method.upper(),
190
+ url=tool.endpoint,
191
+ headers=headers,
192
+ params=None if is_body_method else inputs,
193
+ json=body if is_body_method else None,
194
+ )
195
+
196
+ duration = time.time() - start_time
197
+
198
+ # Raise for HTTP errors
199
+ response.raise_for_status()
200
+
201
+ logger.debug(
202
+ f"Request completed in {duration:.2f}s with status "
203
+ f"{response.status_code}"
204
+ )
205
+
206
+ result = response.json()
207
+ await tool_ctx.complete(result)
208
+ return result
209
+
210
+ except requests.exceptions.RequestException as e:
211
+ error_msg = f"API request failed: {e}"
212
+ await tool_ctx.error(error_msg)
213
+ raise ValueError(error_msg) from e
214
+ except ValueError as e:
215
+ error_msg = f"Failed to decode JSON response: {e}"
216
+ await tool_ctx.error(error_msg)
217
+ raise ValueError(error_msg) from e
218
+
219
+
220
+ class InvokeToolExecutor(StepExecutor, ToolExecutionMixin):
221
+ """Executor for InvokeTool steps."""
222
+
223
+ # Tool invocations should be marked as TOOL type
224
+ span_kind = OpenInferenceSpanKindValues.TOOL
225
+
226
+ def __init__(
227
+ self, step: InvokeTool, context: ExecutorContext, **dependencies: Any
228
+ ) -> None:
229
+ super().__init__(step, context, **dependencies)
230
+ if not isinstance(step, InvokeTool):
231
+ raise ValueError(
232
+ "InvokeToolExecutor can only execute InvokeTool steps."
233
+ )
234
+ self.step: InvokeTool = step
235
+
236
+ def _prepare_tool_inputs(self, message: FlowMessage) -> dict[str, Any]:
237
+ """Prepare tool inputs from message variables using input bindings.
238
+
239
+ Args:
240
+ message: The FlowMessage containing input variables.
241
+
242
+ Returns:
243
+ Dictionary mapping tool parameter names to values.
244
+
245
+ Raises:
246
+ ValueError: If required inputs are missing.
247
+ """
248
+ tool_inputs = {}
249
+
250
+ for tool_param_name, step_var_id in self.step.input_bindings.items():
251
+ # Get tool parameter definition
252
+ tool_param = self.step.tool.inputs.get(tool_param_name)
253
+ if not tool_param:
254
+ raise ValueError(
255
+ f"Tool parameter '{tool_param_name}' not defined in tool"
256
+ )
257
+
258
+ # Get value from message variables
259
+ value = message.variables.get(step_var_id)
260
+
261
+ # Handle missing values
262
+ if value is None:
263
+ if not tool_param.optional:
264
+ raise ValueError(
265
+ (
266
+ f"Required input '{step_var_id}' for tool "
267
+ f"parameter '{tool_param_name}' is missing"
268
+ )
269
+ )
270
+ # Skip optional parameters that are missing
271
+ continue
272
+
273
+ tool_inputs[tool_param_name] = value
274
+
275
+ return tool_inputs
276
+
277
+ def _extract_tool_outputs(self, result: Any) -> dict[str, Any]:
278
+ """Extract output variables from tool result using output bindings.
279
+
280
+ Args:
281
+ result: The result from tool execution.
282
+
283
+ Returns:
284
+ Dictionary mapping step variable IDs to their values.
285
+
286
+ Raises:
287
+ ValueError: If required outputs are missing from result.
288
+ """
289
+ output_vars = {}
290
+
291
+ for tool_param_name, step_var_id in self.step.output_bindings.items():
292
+ # Get tool parameter definition
293
+ tool_param = self.step.tool.outputs.get(tool_param_name)
294
+ if not tool_param:
295
+ raise ValueError(
296
+ f"Tool parameter '{tool_param_name}' not defined in tool"
297
+ )
298
+
299
+ # Extract value from result
300
+ if isinstance(result, dict):
301
+ value = result.get(tool_param_name)
302
+ if value is None and not tool_param.optional:
303
+ raise ValueError(
304
+ (
305
+ f"Required output '{tool_param_name}' not found "
306
+ f"in result. Available: {list(result.keys())}"
307
+ )
308
+ )
309
+ else:
310
+ # Single output case - use entire result
311
+ value = result
312
+
313
+ if value is not None:
314
+ output_vars[step_var_id] = value
315
+
316
+ return output_vars
317
+
318
+ async def process_message(
319
+ self,
320
+ message: FlowMessage,
321
+ ) -> AsyncIterator[FlowMessage]:
322
+ """Process a single FlowMessage for the InvokeTool step.
323
+
324
+ Args:
325
+ message: The FlowMessage to process.
326
+ Yields:
327
+ FlowMessage with tool execution results.
328
+ """
329
+ try:
330
+ # Prepare tool inputs from message variables
331
+ tool_inputs = self._prepare_tool_inputs(message)
332
+
333
+ # Execute the tool with proper tool execution context
334
+ # Dispatch to appropriate execution method based on tool type
335
+ if isinstance(self.step.tool, PythonFunctionTool):
336
+ result = await self.execute_python_tool(
337
+ self.step.tool, tool_inputs
338
+ )
339
+ elif isinstance(self.step.tool, APITool):
340
+ result = await self.execute_api_tool(
341
+ self.step.tool, tool_inputs
342
+ )
343
+ else:
344
+ raise ValueError(
345
+ f"Unsupported tool type: {type(self.step.tool).__name__}"
346
+ )
347
+
348
+ # Extract outputs from result
349
+ output_vars = self._extract_tool_outputs(result)
350
+
351
+ # Yield the result
352
+ yield message.copy_with_variables(output_vars)
353
+
354
+ except Exception as e:
355
+ # Emit error event to stream so frontend can display it
356
+ await self.stream_emitter.error(str(e))
357
+ message.set_error(self.step.id, e)
358
+ yield message
@@ -0,0 +1,272 @@
1
+ from typing import AsyncIterator
2
+
3
+ from llama_cloud import MessageRole as LlamaMessageRole
4
+ from llama_index.core.base.llms.types import ChatResponse, CompletionResponse
5
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
6
+
7
+ from qtype.base.types import PrimitiveTypeEnum
8
+ from qtype.dsl.domain_types import ChatContent, ChatMessage, MessageRole
9
+ from qtype.interpreter.base.base_step_executor import StepExecutor
10
+ from qtype.interpreter.base.executor_context import ExecutorContext
11
+ from qtype.interpreter.conversions import (
12
+ from_chat_message,
13
+ to_chat_message,
14
+ to_llm,
15
+ to_memory,
16
+ variable_to_chat_message,
17
+ )
18
+ from qtype.interpreter.types import FlowMessage
19
+ from qtype.semantic.model import LLMInference
20
+
21
+
22
+ class LLMInferenceExecutor(StepExecutor):
23
+ """Executor for LLMInference steps."""
24
+
25
+ # LLM inference spans should be marked as LLM type
26
+ span_kind = OpenInferenceSpanKindValues.LLM
27
+
28
+ def __init__(
29
+ self, step: LLMInference, context: ExecutorContext, **dependencies
30
+ ):
31
+ super().__init__(step, context, **dependencies)
32
+ if not isinstance(step, LLMInference):
33
+ raise ValueError(
34
+ "LLMInferenceExecutor can only execute LLMInference steps."
35
+ )
36
+ self.step: LLMInference = step
37
+
38
+ def __extract_stream_reasoning_(self, response):
39
+ raw = response.raw
40
+ content_block_delta = raw.get("contentBlockDelta")
41
+ block_index = (
42
+ content_block_delta.get("contentBlockIndex")
43
+ if isinstance(content_block_delta, dict)
44
+ else None
45
+ )
46
+
47
+ reasoning_text = None
48
+ if block_index == 0:
49
+ reasoning_text = (
50
+ content_block_delta.get("delta", {})
51
+ .get("reasoningContent", {})
52
+ .get("text")
53
+ )
54
+
55
+ return reasoning_text
56
+
57
+ async def process_message(
58
+ self,
59
+ message: FlowMessage,
60
+ ) -> AsyncIterator[FlowMessage]:
61
+ """Process a single FlowMessage for the LLMInference step.
62
+
63
+ Args:
64
+ message: The FlowMessage to process.
65
+
66
+ Yields:
67
+ FlowMessage with the results of LLM inference.
68
+ """
69
+ # Get output variable info
70
+ output_variable_id = self.step.outputs[0].id
71
+ output_variable_type = self.step.outputs[0].type
72
+
73
+ try:
74
+ # Determine if this is a chat or completion inference
75
+ if output_variable_type == ChatMessage:
76
+ result_message = await self._process_chat(
77
+ message, output_variable_id
78
+ )
79
+ else:
80
+ result_message = await self._process_completion(
81
+ message, output_variable_id
82
+ )
83
+
84
+ yield result_message
85
+
86
+ except Exception as e:
87
+ # Emit error event to stream so frontend can display it
88
+ await self.stream_emitter.error(str(e))
89
+ message.set_error(self.step.id, e)
90
+ yield message
91
+
92
+ async def _process_chat(
93
+ self,
94
+ message: FlowMessage,
95
+ output_variable_id: str,
96
+ ) -> FlowMessage:
97
+ """Process a chat inference request.
98
+
99
+ Args:
100
+ message: The FlowMessage to process.
101
+ output_variable_id: The ID of the output variable.
102
+
103
+ Returns:
104
+ FlowMessage with the chat response.
105
+ """
106
+ model = to_llm(
107
+ self.step.model, self.step.system_message, self._secret_manager
108
+ )
109
+
110
+ # Convert input variables to chat messages
111
+ inputs = []
112
+ for input_var in self.step.inputs:
113
+ value = message.variables.get(input_var.id)
114
+ # Convert any value type to ChatMessage, then to LlamaChatMessage
115
+ chat_msg = variable_to_chat_message(value, input_var)
116
+ inputs.append(to_chat_message(chat_msg))
117
+
118
+ # Get session ID for memory isolation
119
+ session_id = message.session.session_id
120
+
121
+ # If memory is defined, use it
122
+ if self.step.memory:
123
+ memory = to_memory(session_id, self.step.memory)
124
+
125
+ # Add the inputs to the memory
126
+ from llama_index.core.async_utils import asyncio_run
127
+
128
+ asyncio_run(memory.aput_messages(inputs))
129
+ # Use the whole memory state as inputs to the llm
130
+ inputs = memory.get_all()
131
+ else:
132
+ # If memory is not defined, use conversation history from session
133
+ conversation_history = (
134
+ message.session.conversation_history
135
+ if hasattr(message.session, "conversation_history")
136
+ else []
137
+ )
138
+ if conversation_history:
139
+ inputs = [
140
+ to_chat_message(msg) for msg in conversation_history
141
+ ] + inputs
142
+
143
+ # Add system message if needed
144
+ if (
145
+ self.step.system_message
146
+ and inputs
147
+ and inputs[0].role != LlamaMessageRole.SYSTEM
148
+ ):
149
+ system_message = ChatMessage(
150
+ role=MessageRole.system,
151
+ blocks=[
152
+ ChatContent(
153
+ type=PrimitiveTypeEnum.text,
154
+ content=self.step.system_message,
155
+ )
156
+ ],
157
+ )
158
+ inputs = [to_chat_message(system_message)] + inputs
159
+
160
+ chat_result: ChatResponse
161
+ if self.context.on_stream_event:
162
+ # Generate a unique stream ID for this inference
163
+ stream_id = f"llm-{self.step.id}-{id(message)}"
164
+ async with self.stream_emitter.reasoning_stream(
165
+ f"llm-{self.step.id}-{id(message)}-reasoning"
166
+ ) as reasoning:
167
+ generator = await model.astream_chat(
168
+ messages=inputs,
169
+ **(
170
+ self.step.model.inference_params
171
+ if self.step.model.inference_params
172
+ else {}
173
+ ),
174
+ )
175
+ async for complete_response in generator:
176
+ reasoning_text = self.__extract_stream_reasoning_(
177
+ complete_response
178
+ )
179
+ if reasoning_text:
180
+ await reasoning.delta(reasoning_text)
181
+
182
+ async with self.stream_emitter.text_stream(stream_id) as streamer:
183
+ generator = await model.astream_chat(
184
+ messages=inputs,
185
+ **(
186
+ self.step.model.inference_params
187
+ if self.step.model.inference_params
188
+ else {}
189
+ ),
190
+ )
191
+ async for chat_response in generator:
192
+ chat_text = chat_response.delta
193
+ if chat_text.strip() != "":
194
+ await streamer.delta(chat_response.delta)
195
+ # Get the final result
196
+ chat_result = chat_response
197
+ else:
198
+ chat_result = model.chat(
199
+ messages=inputs,
200
+ **(
201
+ self.step.model.inference_params
202
+ if self.step.model.inference_params
203
+ else {}
204
+ ),
205
+ )
206
+
207
+ # Store result in memory if configured
208
+ if self.step.memory:
209
+ memory.put(chat_result.message)
210
+
211
+ # Convert result and return
212
+ result_value = from_chat_message(chat_result.message)
213
+ return message.copy_with_variables({output_variable_id: result_value})
214
+
215
+ async def _process_completion(
216
+ self,
217
+ message: FlowMessage,
218
+ output_variable_id: str,
219
+ ) -> FlowMessage:
220
+ """Process a completion inference request.
221
+
222
+ Args:
223
+ message: The FlowMessage to process.
224
+ output_variable_id: The ID of the output variable.
225
+
226
+ Returns:
227
+ FlowMessage with the completion response.
228
+ """
229
+ model = to_llm(
230
+ self.step.model, self.step.system_message, self._secret_manager
231
+ )
232
+
233
+ # Get input value
234
+ input_value = message.variables.get(self.step.inputs[0].id)
235
+ if not isinstance(input_value, str):
236
+ input_value = str(input_value)
237
+
238
+ # Perform inference with streaming if callback provided
239
+ complete_result: CompletionResponse
240
+ if self.context.on_stream_event:
241
+ # Generate a unique stream ID for this inference
242
+ stream_id = f"llm-{self.step.id}-{id(message)}"
243
+
244
+ async with self.stream_emitter.text_stream(stream_id) as streamer:
245
+ generator = await model.astream_complete(
246
+ prompt=input_value,
247
+ **(
248
+ self.step.model.inference_params
249
+ if self.step.model.inference_params
250
+ else {}
251
+ ),
252
+ )
253
+
254
+ async for complete_response in generator:
255
+ text = complete_response.delta
256
+ if complete_response.text.strip() != "":
257
+ await streamer.delta(text)
258
+
259
+ complete_result = complete_response
260
+ else:
261
+ complete_result = model.complete(
262
+ prompt=input_value,
263
+ **(
264
+ self.step.model.inference_params
265
+ if self.step.model.inference_params
266
+ else {}
267
+ ),
268
+ )
269
+
270
+ response: dict[str, str] = {output_variable_id: complete_result.text}
271
+
272
+ return message.copy_with_variables(response)
@@ -0,0 +1,78 @@
1
+ import string
2
+ from typing import AsyncIterator
3
+
4
+ from qtype.interpreter.base.base_step_executor import StepExecutor
5
+ from qtype.interpreter.base.executor_context import ExecutorContext
6
+ from qtype.interpreter.types import FlowMessage
7
+ from qtype.semantic.model import PromptTemplate
8
+
9
+
10
+ def get_format_arguments(format_string: str) -> set[str]:
11
+ formatter = string.Formatter()
12
+ arguments = []
13
+ for literal_text, field_name, format_spec, conversion in formatter.parse(
14
+ format_string
15
+ ):
16
+ if field_name:
17
+ arguments.append(field_name)
18
+ return set(arguments)
19
+
20
+
21
+ class PromptTemplateExecutor(StepExecutor):
22
+ """Executor for PromptTemplate steps."""
23
+
24
+ def __init__(
25
+ self, step: PromptTemplate, context: ExecutorContext, **dependencies
26
+ ):
27
+ super().__init__(step, context, **dependencies)
28
+ if not isinstance(step, PromptTemplate):
29
+ raise ValueError(
30
+ (
31
+ "PromptTemplateExecutor can only execute "
32
+ "PromptTemplate steps."
33
+ )
34
+ )
35
+ self.step: PromptTemplate = step
36
+
37
+ async def process_message(
38
+ self,
39
+ message: FlowMessage,
40
+ ) -> AsyncIterator[FlowMessage]:
41
+ """Process a single FlowMessage for the PromptTemplate step.
42
+
43
+ Args:
44
+ message: The FlowMessage to process.
45
+ Yields:
46
+ FlowMessage with the results of processing.
47
+ """
48
+ format_args = get_format_arguments(self.step.template)
49
+ try:
50
+ # Read input values from FlowMessage.variables
51
+ input_map = {}
52
+ for var in self.step.inputs:
53
+ if var.id in format_args:
54
+ value = message.variables.get(var.id)
55
+ if value is not None:
56
+ input_map[var.id] = value
57
+
58
+ missing = format_args - input_map.keys()
59
+ if missing:
60
+ raise ValueError(
61
+ (
62
+ "The following fields are in the prompt template "
63
+ f"but not in the inputs: {missing}"
64
+ )
65
+ )
66
+ result = self.step.template.format(**input_map)
67
+ output_var_id = self.step.outputs[0].id
68
+
69
+ await self.stream_emitter.status(
70
+ (f"Processed message with PromptTemplate step {self.step.id}"),
71
+ )
72
+ yield message.copy_with_variables({output_var_id: result})
73
+
74
+ except Exception as e:
75
+ # Emit error event to stream so frontend can display it
76
+ await self.stream_emitter.error(str(e))
77
+ message.set_error(self.step.id, e)
78
+ yield message