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.
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 +57 -136
  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.16.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.16.dist-info/RECORD +0 -106
  122. /qtype/interpreter/ui/_next/static/{nUaw6_IwRwPqkzwe5s725 → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  123. {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/WHEEL +0 -0
  124. {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/entry_points.txt +0 -0
  125. {qtype-0.0.16.dist-info → qtype-0.1.0.dist-info}/licenses/LICENSE +0 -0
  126. {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