qtype 0.0.16__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 +57 -136
- 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.16.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.16.dist-info/RECORD +0 -106
- /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/WHEEL +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import AsyncIterator
|
|
2
|
+
|
|
3
|
+
import boto3 # type: ignore[import-untyped]
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import sqlalchemy
|
|
6
|
+
from sqlalchemy import create_engine
|
|
7
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
8
|
+
|
|
9
|
+
from qtype.interpreter.auth.generic import auth
|
|
10
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
11
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
12
|
+
from qtype.interpreter.types import FlowMessage
|
|
13
|
+
from qtype.semantic.model import SQLSource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLSourceExecutor(StepExecutor):
|
|
17
|
+
"""Executor for SQLSource steps."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, step: SQLSource, context: ExecutorContext, **dependencies
|
|
21
|
+
):
|
|
22
|
+
super().__init__(step, context, **dependencies)
|
|
23
|
+
if not isinstance(step, SQLSource):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"SQLSourceExecutor can only execute SQLSource steps."
|
|
26
|
+
)
|
|
27
|
+
self.step: SQLSource = step
|
|
28
|
+
|
|
29
|
+
async def process_message(
|
|
30
|
+
self,
|
|
31
|
+
message: FlowMessage,
|
|
32
|
+
) -> AsyncIterator[FlowMessage]:
|
|
33
|
+
"""Process a single FlowMessage for the SQLSource step.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
message: The FlowMessage to process.
|
|
37
|
+
Yields:
|
|
38
|
+
FlowMessages with the results of SQL query execution.
|
|
39
|
+
"""
|
|
40
|
+
# Create a database engine - resolve connection string if it's a SecretReference
|
|
41
|
+
connection_string = self._resolve_secret(self.step.connection)
|
|
42
|
+
connect_args = {}
|
|
43
|
+
if self.step.auth:
|
|
44
|
+
with auth(self.step.auth) as creds:
|
|
45
|
+
if isinstance(creds, boto3.Session):
|
|
46
|
+
connect_args["session"] = creds
|
|
47
|
+
engine = create_engine(connection_string, connect_args=connect_args)
|
|
48
|
+
|
|
49
|
+
output_columns = {output.id for output in self.step.outputs}
|
|
50
|
+
step_inputs = {i.id for i in self.step.inputs}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Make a dictionary of column_name: value from message variables
|
|
54
|
+
params = {
|
|
55
|
+
col: message.variables.get(col)
|
|
56
|
+
for col in step_inputs
|
|
57
|
+
if col in message.variables
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await self.stream_emitter.status(
|
|
61
|
+
f"Executing SQL query with params: {params}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Execute the query and fetch the results into a DataFrame
|
|
65
|
+
with engine.connect() as connection:
|
|
66
|
+
result = connection.execute(
|
|
67
|
+
sqlalchemy.text(self.step.query),
|
|
68
|
+
parameters=params if params else None,
|
|
69
|
+
)
|
|
70
|
+
df = pd.DataFrame(
|
|
71
|
+
result.fetchall(), columns=list(result.keys())
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Confirm the outputs exist in the dataframe
|
|
75
|
+
columns = set(df.columns)
|
|
76
|
+
missing_columns = output_columns - columns
|
|
77
|
+
if missing_columns:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
(
|
|
80
|
+
f"SQL Result was missing expected columns: "
|
|
81
|
+
f"{', '.join(missing_columns)}, it has columns: "
|
|
82
|
+
f"{', '.join(columns)}"
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Emit one message per result row
|
|
87
|
+
for _, row in df.iterrows():
|
|
88
|
+
# Create a dict with only the output columns
|
|
89
|
+
row_dict = {
|
|
90
|
+
str(k): v
|
|
91
|
+
for k, v in row.to_dict().items()
|
|
92
|
+
if str(k) in output_columns
|
|
93
|
+
}
|
|
94
|
+
# Merge with original message variables
|
|
95
|
+
yield message.copy_with_variables(new_variables=row_dict)
|
|
96
|
+
|
|
97
|
+
await self.stream_emitter.status(
|
|
98
|
+
f"Emitted {len(df)} rows from SQL query"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
except SQLAlchemyError as e:
|
|
102
|
+
# Emit error event to stream so frontend can display it
|
|
103
|
+
await self.stream_emitter.error(str(e))
|
|
104
|
+
# Set error on the message and yield it
|
|
105
|
+
message.set_error(self.step.id, e)
|
|
106
|
+
yield message
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Vector search executor for retrieving relevant chunks from vector stores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
9
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
10
|
+
from qtype.interpreter.conversions import (
|
|
11
|
+
from_node_with_score,
|
|
12
|
+
to_llama_vector_store_and_retriever,
|
|
13
|
+
)
|
|
14
|
+
from qtype.interpreter.types import FlowMessage
|
|
15
|
+
from qtype.semantic.model import VectorIndex, VectorSearch
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VectorSearchExecutor(StepExecutor):
|
|
21
|
+
"""Executor for VectorSearch steps using LlamaIndex vector stores."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, step: VectorSearch, context: ExecutorContext, **dependencies
|
|
25
|
+
):
|
|
26
|
+
super().__init__(step, context, **dependencies)
|
|
27
|
+
if not isinstance(step, VectorSearch):
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"VectorSearchExecutor can only execute VectorSearch steps."
|
|
30
|
+
)
|
|
31
|
+
self.step: VectorSearch = step
|
|
32
|
+
|
|
33
|
+
if not isinstance(self.step.index, VectorIndex):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"VectorSearch step {self.step.id} must reference a VectorIndex."
|
|
36
|
+
)
|
|
37
|
+
self.index: VectorIndex = self.step.index
|
|
38
|
+
|
|
39
|
+
# Get the vector store and retriever
|
|
40
|
+
self._vector_store, self._retriever = (
|
|
41
|
+
to_llama_vector_store_and_retriever(
|
|
42
|
+
self.step.index, self.context.secret_manager
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def process_message(
|
|
47
|
+
self,
|
|
48
|
+
message: FlowMessage,
|
|
49
|
+
) -> AsyncIterator[FlowMessage]:
|
|
50
|
+
"""Process a single FlowMessage for the VectorSearch step.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: The FlowMessage to process.
|
|
54
|
+
|
|
55
|
+
Yields:
|
|
56
|
+
FlowMessage with search results.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
# Get the query from the input variable
|
|
60
|
+
# (validated to be exactly one text input)
|
|
61
|
+
input_var = self.step.inputs[0]
|
|
62
|
+
query = message.variables.get(input_var.id)
|
|
63
|
+
|
|
64
|
+
if not isinstance(query, str):
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"VectorSearch input must be text, got {type(query)}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Perform the vector search
|
|
70
|
+
logger.debug(f"Performing vector search with query: {query}")
|
|
71
|
+
nodes_with_scores = await self._retriever.aretrieve(query)
|
|
72
|
+
|
|
73
|
+
# Convert results to RAGSearchResult objects
|
|
74
|
+
search_results = [
|
|
75
|
+
from_node_with_score(node_with_score)
|
|
76
|
+
for node_with_score in nodes_with_scores
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Set the output variable (validated to be exactly one output
|
|
80
|
+
# of type list[RAGSearchResult])
|
|
81
|
+
output_var = self.step.outputs[0]
|
|
82
|
+
output_vars = {output_var.id: search_results}
|
|
83
|
+
|
|
84
|
+
yield message.copy_with_variables(output_vars)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Vector search failed: {e}", exc_info=True)
|
|
88
|
+
# Emit error event to stream so frontend can display it
|
|
89
|
+
await self.stream_emitter.error(str(e))
|
|
90
|
+
message.set_error(self.step.id, e)
|
|
91
|
+
yield message
|