qtype 0.0.12__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qtype/application/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +476 -11
- qtype/application/converters/tools_from_module.py +38 -14
- qtype/application/converters/types.py +15 -30
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +102 -85
- qtype/base/types.py +227 -7
- qtype/cli.py +5 -1
- qtype/commands/convert.py +52 -6
- qtype/commands/generate.py +44 -4
- qtype/commands/run.py +78 -36
- qtype/commands/serve.py +74 -44
- qtype/commands/validate.py +37 -14
- qtype/commands/visualize.py +46 -25
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +86 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +751 -263
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +63 -136
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +436 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +91 -0
- qtype/interpreter/base/factory.py +84 -0
- qtype/interpreter/base/progress_tracker.py +110 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +74 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +471 -22
- qtype/interpreter/converters.py +79 -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/bedrock_reranker_executor.py +195 -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 +107 -0
- qtype/interpreter/executors/document_search_executor.py +113 -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 +165 -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 +232 -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 +358 -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 +173 -22
- qtype/interpreter/logging_progress.py +61 -0
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/rich_progress.py +225 -0
- 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 +330 -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/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
- qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
- 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/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
- qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +5 -5
- qtype/semantic/checker.py +643 -0
- qtype/semantic/generate.py +268 -85
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +535 -163
- qtype/semantic/resolver.py +63 -19
- qtype/semantic/visualize.py +50 -35
- {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/METADATA +21 -4
- qtype-0.1.3.dist-info/RECORD +137 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -464
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -95
- qtype/interpreter/batch/step.py +0 -63
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -179
- 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 -150
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -53
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
- qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
- qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
- qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -389
- qtype-0.0.12.dist-info/RECORD +0 -105
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.3.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
|