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.
Files changed (137) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +476 -11
  3. qtype/application/converters/tools_from_module.py +38 -14
  4. qtype/application/converters/types.py +15 -30
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +102 -85
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +5 -1
  9. qtype/commands/convert.py +52 -6
  10. qtype/commands/generate.py +44 -4
  11. qtype/commands/run.py +78 -36
  12. qtype/commands/serve.py +74 -44
  13. qtype/commands/validate.py +37 -14
  14. qtype/commands/visualize.py +46 -25
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +86 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +751 -263
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +63 -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 +91 -0
  30. qtype/interpreter/base/factory.py +84 -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 +471 -22
  36. qtype/interpreter/converters.py +79 -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/bedrock_reranker_executor.py +195 -0
  41. qtype/interpreter/executors/decoder_executor.py +163 -0
  42. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  43. qtype/interpreter/executors/document_embedder_executor.py +107 -0
  44. qtype/interpreter/executors/document_search_executor.py +113 -0
  45. qtype/interpreter/executors/document_source_executor.py +118 -0
  46. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  47. qtype/interpreter/executors/echo_executor.py +63 -0
  48. qtype/interpreter/executors/field_extractor_executor.py +165 -0
  49. qtype/interpreter/executors/file_source_executor.py +101 -0
  50. qtype/interpreter/executors/file_writer_executor.py +110 -0
  51. qtype/interpreter/executors/index_upsert_executor.py +232 -0
  52. qtype/interpreter/executors/invoke_embedding_executor.py +92 -0
  53. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  54. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  55. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  56. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  57. qtype/interpreter/executors/sql_source_executor.py +106 -0
  58. qtype/interpreter/executors/vector_search_executor.py +91 -0
  59. qtype/interpreter/flow.py +173 -22
  60. qtype/interpreter/logging_progress.py +61 -0
  61. qtype/interpreter/metadata_api.py +115 -0
  62. qtype/interpreter/resource_cache.py +5 -4
  63. qtype/interpreter/rich_progress.py +225 -0
  64. qtype/interpreter/stream/chat/__init__.py +15 -0
  65. qtype/interpreter/stream/chat/converter.py +391 -0
  66. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  67. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  68. qtype/interpreter/stream/chat/vercel.py +609 -0
  69. qtype/interpreter/stream/utils/__init__.py +15 -0
  70. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  71. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  72. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  73. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  74. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  75. qtype/interpreter/telemetry.py +135 -8
  76. qtype/interpreter/tools/__init__.py +5 -0
  77. qtype/interpreter/tools/function_tool_helper.py +265 -0
  78. qtype/interpreter/types.py +330 -0
  79. qtype/interpreter/typing.py +83 -89
  80. qtype/interpreter/ui/404/index.html +1 -1
  81. qtype/interpreter/ui/404.html +1 -1
  82. qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  83. qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
  84. qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
  85. qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
  86. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  87. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  88. qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
  89. qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
  90. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  91. qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  92. qtype/interpreter/ui/icon.png +0 -0
  93. qtype/interpreter/ui/index.html +1 -1
  94. qtype/interpreter/ui/index.txt +5 -5
  95. qtype/semantic/checker.py +643 -0
  96. qtype/semantic/generate.py +268 -85
  97. qtype/semantic/loader.py +95 -0
  98. qtype/semantic/model.py +535 -163
  99. qtype/semantic/resolver.py +63 -19
  100. qtype/semantic/visualize.py +50 -35
  101. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/METADATA +21 -4
  102. qtype-0.1.3.dist-info/RECORD +137 -0
  103. qtype/dsl/base_types.py +0 -38
  104. qtype/dsl/validator.py +0 -464
  105. qtype/interpreter/batch/__init__.py +0 -0
  106. qtype/interpreter/batch/flow.py +0 -95
  107. qtype/interpreter/batch/sql_source.py +0 -95
  108. qtype/interpreter/batch/step.py +0 -63
  109. qtype/interpreter/batch/types.py +0 -41
  110. qtype/interpreter/batch/utils.py +0 -179
  111. qtype/interpreter/chat/chat_api.py +0 -237
  112. qtype/interpreter/chat/vercel.py +0 -314
  113. qtype/interpreter/exceptions.py +0 -10
  114. qtype/interpreter/step.py +0 -67
  115. qtype/interpreter/steps/__init__.py +0 -0
  116. qtype/interpreter/steps/agent.py +0 -114
  117. qtype/interpreter/steps/condition.py +0 -36
  118. qtype/interpreter/steps/decoder.py +0 -88
  119. qtype/interpreter/steps/llm_inference.py +0 -150
  120. qtype/interpreter/steps/prompt_template.py +0 -54
  121. qtype/interpreter/steps/search.py +0 -24
  122. qtype/interpreter/steps/tool.py +0 -53
  123. qtype/interpreter/streaming_helpers.py +0 -123
  124. qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
  125. qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
  126. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  127. qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
  128. qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
  129. qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
  130. qtype/interpreter/ui/favicon.ico +0 -0
  131. qtype/loader.py +0 -389
  132. qtype-0.0.12.dist-info/RECORD +0 -105
  133. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  134. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/WHEEL +0 -0
  135. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/entry_points.txt +0 -0
  136. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/licenses/LICENSE +0 -0
  137. {qtype-0.0.12.dist-info → qtype-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,355 @@
1
+ """Unified API endpoint for flow execution with streaming and REST support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, AsyncIterator
7
+
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from fastapi.responses import StreamingResponse
10
+
11
+ from qtype.dsl.domain_types import ChatMessage, MessageRole
12
+ from qtype.interpreter.base.executor_context import ExecutorContext
13
+ from qtype.interpreter.flow import run_flow
14
+ from qtype.interpreter.stream.chat import format_stream_events_as_sse
15
+ from qtype.interpreter.stream.chat.ui_request_to_domain_type import (
16
+ completion_request_to_input_model,
17
+ ui_request_to_domain_type,
18
+ )
19
+ from qtype.interpreter.stream.chat.vercel import ChatRequest, CompletionRequest
20
+ from qtype.interpreter.stream.utils import callback_to_async_iterator
21
+ from qtype.interpreter.types import FlowMessage, StreamEvent
22
+ from qtype.interpreter.typing import (
23
+ create_input_shape,
24
+ create_output_container_type,
25
+ create_output_shape,
26
+ flow_results_to_output_container,
27
+ request_to_flow_message,
28
+ )
29
+ from qtype.semantic.model import Flow
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ async def _execute_flow_with_streaming(
35
+ flow: Flow,
36
+ initial_message: FlowMessage,
37
+ context: ExecutorContext,
38
+ ) -> AsyncIterator[StreamEvent]:
39
+ """
40
+ Execute flow and yield StreamEvents as they occur.
41
+
42
+ This function converts run_flow's callback-based streaming to an
43
+ async iterator. Executors emit StreamEvents (including ErrorEvents)
44
+ through the on_stream_event callback.
45
+
46
+ Args:
47
+ flow: The flow to execute
48
+ initial_message: Initial FlowMessage with inputs
49
+
50
+ Yields:
51
+ StreamEvent instances from executors
52
+ """
53
+
54
+ async def execute_with_callback(callback): # type: ignore[no-untyped-def]
55
+ """Execute flow with streaming callback."""
56
+ # Update context with streaming callback
57
+ stream_context = ExecutorContext(
58
+ secret_manager=context.secret_manager,
59
+ on_stream_event=callback,
60
+ on_progress=context.on_progress,
61
+ tracer=context.tracer,
62
+ )
63
+ await run_flow(
64
+ flow,
65
+ initial_message,
66
+ context=stream_context,
67
+ )
68
+
69
+ # Convert callback-based streaming to async iterator
70
+ async for event in callback_to_async_iterator(execute_with_callback):
71
+ yield event
72
+
73
+
74
+ async def _stream_sse_response(
75
+ flow: Flow,
76
+ initial_message: FlowMessage,
77
+ context: ExecutorContext,
78
+ output_metadata: dict[str, Any] | None = None,
79
+ ) -> AsyncIterator[str]:
80
+ """
81
+ Execute flow and stream Server-Sent Events using Vercel AI SDK protocol.
82
+
83
+ This function orchestrates flow execution and formats the resulting
84
+ StreamEvents as SSE for the Vercel AI SDK frontend.
85
+
86
+ Args:
87
+ flow: The flow to execute
88
+ initial_message: Initial FlowMessage with inputs
89
+ output_metadata: Optional metadata to include in FinishChunk
90
+
91
+ Yields:
92
+ SSE formatted strings (data: {json}\\n\\n)
93
+ """
94
+ # Execute flow and get event stream
95
+ event_stream = _execute_flow_with_streaming(flow, initial_message, context)
96
+
97
+ # Format events as SSE with metadata
98
+ async for sse_line in format_stream_events_as_sse(
99
+ event_stream, output_metadata=output_metadata
100
+ ):
101
+ yield sse_line
102
+
103
+
104
+ def _create_chat_streaming_endpoint(
105
+ app: FastAPI, flow: Flow, context: ExecutorContext
106
+ ) -> None:
107
+ """
108
+ Create streaming endpoint for Conversational flows.
109
+
110
+ Args:
111
+ app: FastAPI application instance
112
+ flow: Flow with Conversational interface
113
+ secret_manager: Optional secret manager for resolving secrets
114
+ """
115
+ flow_id = flow.id
116
+
117
+ # Type narrowing for mypy
118
+ if flow.interface is None:
119
+ raise ValueError(f"Flow {flow_id} has no interface defined")
120
+ interface = flow.interface
121
+
122
+ async def stream_chat(request: ChatRequest) -> StreamingResponse:
123
+ """Stream conversational flow with Vercel AI SDK protocol."""
124
+ try:
125
+ # Convert Vercel ChatRequest to ChatMessages
126
+ messages = ui_request_to_domain_type(request)
127
+ if not messages:
128
+ raise ValueError("No input messages received")
129
+
130
+ current_input = messages.pop()
131
+ if current_input.role != MessageRole.user:
132
+ raise ValueError(
133
+ f"Expected user message, got {current_input.role}"
134
+ )
135
+
136
+ # Find ChatMessage input variable
137
+ chat_input_var = next(
138
+ (var for var in flow.inputs if var.type == ChatMessage),
139
+ None,
140
+ )
141
+ if not chat_input_var:
142
+ raise ValueError("No ChatMessage input found in flow inputs")
143
+
144
+ input_data = {chat_input_var.id: current_input}
145
+
146
+ # Add session_inputs from interface
147
+ for session_var in interface.session_inputs:
148
+ if session_var.value is not None:
149
+ input_data[session_var.id] = session_var.value
150
+
151
+ # Create a dynamic request model with the input data
152
+ RequestModel = create_input_shape(flow)
153
+ request_obj = RequestModel(**input_data)
154
+
155
+ initial_message = request_to_flow_message(
156
+ request=request_obj,
157
+ session_id=request.id,
158
+ conversation_history=messages,
159
+ )
160
+
161
+ return StreamingResponse(
162
+ _stream_sse_response(
163
+ flow,
164
+ initial_message,
165
+ output_metadata=None,
166
+ context=context,
167
+ ),
168
+ media_type="text/plain; charset=utf-8",
169
+ headers={
170
+ "Cache-Control": "no-cache",
171
+ "Connection": "keep-alive",
172
+ "X-Accel-Buffering": "no",
173
+ "x-vercel-ai-ui-message-stream": "v1",
174
+ },
175
+ )
176
+
177
+ except ValueError as ve:
178
+ logger.error(f"Validation error: {ve}")
179
+ raise HTTPException(status_code=400, detail=str(ve)) from ve
180
+ except Exception as e:
181
+ logger.error(
182
+ f"Flow streaming failed for {flow_id}: {e}", exc_info=True
183
+ )
184
+ raise HTTPException(
185
+ status_code=500,
186
+ detail=f"Flow streaming failed: {str(e)}",
187
+ ) from e
188
+
189
+ # Register streaming endpoint
190
+ app.post(
191
+ f"/flows/{flow_id}/stream",
192
+ tags=["flows"],
193
+ summary=f"Stream {flow_id} flow (Chat)",
194
+ description=(
195
+ flow.description or f"Stream the {flow_id} conversational flow"
196
+ ),
197
+ )(stream_chat)
198
+
199
+
200
+ def _create_completion_streaming_endpoint(
201
+ app: FastAPI, flow: Flow, context: ExecutorContext
202
+ ) -> None:
203
+ """
204
+ Create streaming endpoint for Complete flows.
205
+
206
+ Args:
207
+ app: FastAPI application instance
208
+ flow: Flow with Complete interface
209
+ secret_manager: Optional secret manager for resolving secrets
210
+ """
211
+ flow_id = flow.id
212
+
213
+ async def stream_completion(
214
+ request: CompletionRequest,
215
+ ) -> StreamingResponse:
216
+ """Stream completion flow with Vercel AI SDK protocol."""
217
+ try:
218
+ # Complete flows: convert CompletionRequest to input model
219
+ InputModel = create_input_shape(flow)
220
+ request_obj = completion_request_to_input_model(
221
+ request, InputModel
222
+ )
223
+ initial_message = request_to_flow_message(request=request_obj)
224
+
225
+ return StreamingResponse(
226
+ _stream_sse_response(
227
+ flow,
228
+ initial_message,
229
+ output_metadata=None,
230
+ context=context,
231
+ ),
232
+ media_type="text/plain; charset=utf-8",
233
+ headers={
234
+ "Cache-Control": "no-cache",
235
+ "Connection": "keep-alive",
236
+ "X-Accel-Buffering": "no",
237
+ "x-vercel-ai-ui-message-stream": "v1",
238
+ },
239
+ )
240
+
241
+ except ValueError as ve:
242
+ logger.error(f"Validation error: {ve}")
243
+ raise HTTPException(status_code=400, detail=str(ve)) from ve
244
+ except Exception as e:
245
+ logger.error(
246
+ f"Flow streaming failed for {flow_id}: {e}", exc_info=True
247
+ )
248
+ raise HTTPException(
249
+ status_code=500,
250
+ detail=f"Flow streaming failed: {str(e)}",
251
+ ) from e
252
+
253
+ # Register streaming endpoint
254
+ app.post(
255
+ f"/flows/{flow_id}/stream",
256
+ tags=["flows"],
257
+ summary=f"Stream {flow_id} flow (Complete)",
258
+ description=(
259
+ flow.description or f"Stream the {flow_id} completion flow"
260
+ ),
261
+ )(stream_completion)
262
+
263
+
264
+ def create_streaming_endpoint(
265
+ app: FastAPI, flow: Flow, context: ExecutorContext
266
+ ) -> None:
267
+ """
268
+ Create streaming endpoint for flow execution.
269
+
270
+ Args:
271
+ app: FastAPI application instance
272
+ flow: Flow to create endpoint for
273
+ secret_manager: Optional secret manager for resolving secrets
274
+ """
275
+ if flow.interface is None:
276
+ raise ValueError(f"Flow {flow.id} has no interface defined")
277
+
278
+ # Dispatch based on interface type
279
+ interface_type = flow.interface.type
280
+ if interface_type == "Conversational":
281
+ _create_chat_streaming_endpoint(app, flow, context)
282
+ elif interface_type == "Complete":
283
+ _create_completion_streaming_endpoint(app, flow, context)
284
+ else:
285
+ raise ValueError(
286
+ f"Unknown interface type for flow {flow.id}: {interface_type}"
287
+ )
288
+
289
+
290
+ def create_rest_endpoint(
291
+ app: FastAPI, flow: Flow, context: ExecutorContext
292
+ ) -> None:
293
+ """
294
+ Create only the REST endpoint for flow execution.
295
+
296
+ Args:
297
+ app: FastAPI application instance
298
+ flow: Flow to create endpoint for
299
+ secret_manager: Optional secret manager for resolving secrets
300
+ """
301
+ RequestModel = create_input_shape(flow)
302
+ ResultShape = create_output_shape(flow)
303
+ ResponseModel = create_output_container_type(flow)
304
+
305
+ async def execute_flow_rest(
306
+ body: RequestModel, # type: ignore[valid-type]
307
+ request: Request,
308
+ ) -> ResponseModel: # type: ignore[valid-type]
309
+ """Execute the flow and return JSON response."""
310
+ try:
311
+ # Only pass session_id if it's provided in headers
312
+ kwargs = {}
313
+ if "session_id" in request.headers:
314
+ kwargs["session_id"] = request.headers["session_id"]
315
+
316
+ initial_message = request_to_flow_message(request=body, **kwargs)
317
+
318
+ # Execute flow
319
+ results = await run_flow(flow, initial_message, context=context)
320
+
321
+ if not results:
322
+ raise HTTPException(
323
+ status_code=500, detail="No results returned"
324
+ )
325
+
326
+ return flow_results_to_output_container(
327
+ results,
328
+ output_shape=ResultShape,
329
+ output_container=ResponseModel,
330
+ )
331
+ except ValueError as ve:
332
+ logger.error(f"Validation error: {ve}")
333
+ raise HTTPException(status_code=400, detail=str(ve)) from ve
334
+ except Exception as e:
335
+ logger.error(
336
+ f"Flow execution failed for {flow.id}: {e}", exc_info=True
337
+ )
338
+ raise HTTPException(
339
+ status_code=500, detail=f"Flow execution failed: {str(e)}"
340
+ ) from e
341
+
342
+ # Set annotations for REST endpoint
343
+ execute_flow_rest.__annotations__ = {
344
+ "body": RequestModel,
345
+ "request": Request,
346
+ "return": ResponseModel,
347
+ }
348
+
349
+ # Register REST endpoint
350
+ app.post(
351
+ f"/flows/{flow.id}",
352
+ tags=["flows"],
353
+ description=flow.description or f"Execute the {flow.id} flow",
354
+ response_model=ResponseModel,
355
+ )(execute_flow_rest)
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, AsyncIterator, cast
5
+
6
+ from llama_index.core.agent import ReActAgent
7
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
8
+
9
+ from qtype.base.types import PrimitiveTypeEnum
10
+ from qtype.dsl.domain_types import ChatContent, ChatMessage, MessageRole
11
+ from qtype.interpreter.base.base_step_executor import StepExecutor
12
+ from qtype.interpreter.base.executor_context import ExecutorContext
13
+ from qtype.interpreter.conversions import to_chat_message, to_llm, to_memory
14
+ from qtype.interpreter.executors.invoke_tool_executor import ToolExecutionMixin
15
+ from qtype.interpreter.tools import FunctionToolHelper
16
+ from qtype.interpreter.types import FlowMessage
17
+ from qtype.semantic.model import Agent, APITool, PythonFunctionTool
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class AgentExecutor(StepExecutor, ToolExecutionMixin, FunctionToolHelper):
23
+ """Executor for Agent steps using LlamaIndex ReActAgent."""
24
+
25
+ # Agent execution should be marked as AGENT type (similar to LLM)
26
+ span_kind = OpenInferenceSpanKindValues.AGENT
27
+
28
+ def __init__(self, step: Agent, context: ExecutorContext, **dependencies):
29
+ super().__init__(step, context, **dependencies)
30
+ if not isinstance(step, Agent):
31
+ raise ValueError("AgentExecutor can only execute Agent steps.")
32
+ self.step: Agent = step
33
+ self._agent = self._create_agent()
34
+
35
+ def _create_agent(self) -> ReActAgent:
36
+ """Create the ReActAgent instance.
37
+
38
+ Returns:
39
+ Configured ReActAgent instance.
40
+ """
41
+ # Convert QType tools to LlamaIndex FunctionTools
42
+ llama_tools = [
43
+ self._create_function_tool(
44
+ cast(APITool | PythonFunctionTool, tool)
45
+ )
46
+ for tool in self.step.tools
47
+ ]
48
+
49
+ # Get the LLM for the agent
50
+ llm = to_llm(
51
+ self.step.model, self.step.system_message, self._secret_manager
52
+ )
53
+
54
+ # Create ReActAgent
55
+ return ReActAgent(
56
+ name=self.step.id,
57
+ description=f"Agent for step {self.step.id}",
58
+ system_prompt=self.step.system_message,
59
+ tools=llama_tools, # type: ignore[arg-type]
60
+ llm=llm,
61
+ )
62
+
63
+ async def process_message(
64
+ self,
65
+ message: FlowMessage,
66
+ ) -> AsyncIterator[FlowMessage]:
67
+ """Process a single FlowMessage for the Agent step.
68
+
69
+ Args:
70
+ message: The FlowMessage to process.
71
+
72
+ Yields:
73
+ FlowMessage with agent execution results.
74
+ """
75
+ # Get output variable info
76
+ output_variable_id = self.step.outputs[0].id
77
+ output_variable_type = self.step.outputs[0].type
78
+
79
+ try:
80
+ # Determine if this is a chat or completion inference
81
+ if output_variable_type == ChatMessage:
82
+ result_message = await self._process_chat(
83
+ message, output_variable_id
84
+ )
85
+ else:
86
+ result_message = await self._process_completion(
87
+ message, output_variable_id
88
+ )
89
+
90
+ yield result_message
91
+
92
+ except Exception as e:
93
+ logger.error(f"Agent execution failed: {e}", exc_info=True)
94
+ # Emit error event to stream so frontend can display it
95
+ await self.stream_emitter.error(str(e))
96
+ message.set_error(self.step.id, e)
97
+ yield message
98
+
99
+ async def _process_chat(
100
+ self,
101
+ message: FlowMessage,
102
+ output_variable_id: str,
103
+ ) -> FlowMessage:
104
+ """Process a chat-based agent request.
105
+
106
+ Args:
107
+ message: The FlowMessage to process.
108
+ output_variable_id: The ID of the output variable.
109
+
110
+ Returns:
111
+ FlowMessage with the agent chat response.
112
+ """
113
+ # Convert input variables to chat messages
114
+ inputs = []
115
+ for input_var in self.step.inputs:
116
+ value = message.variables.get(input_var.id)
117
+ if value and isinstance(value, ChatMessage):
118
+ inputs.append(to_chat_message(value))
119
+
120
+ # Get session ID for memory isolation
121
+ session_id = message.session.session_id
122
+
123
+ # Handle memory if configured
124
+ if self.step.memory:
125
+ memory = to_memory(session_id, self.step.memory)
126
+
127
+ # Add the inputs to the memory
128
+ await memory.aput_messages(inputs)
129
+ # Use the whole memory state as inputs
130
+ inputs = memory.get_all()
131
+ else:
132
+ # Use conversation history from session if no memory
133
+ conversation_history = getattr(
134
+ message.session, "conversation_history", []
135
+ )
136
+ if conversation_history:
137
+ inputs = [
138
+ to_chat_message(msg) for msg in conversation_history
139
+ ] + inputs
140
+
141
+ # Prepare the user query (last message in inputs)
142
+ if not inputs:
143
+ raise ValueError("No input messages provided to agent")
144
+
145
+ # Get the last user message as the query
146
+ user_msg = inputs[-1] if inputs else None
147
+ chat_hist = inputs[:-1] if len(inputs) > 1 else []
148
+
149
+ # Execute agent (ReActAgent.run returns a WorkflowHandler)
150
+ handler = self._agent.run(
151
+ user_msg=user_msg,
152
+ chat_history=chat_hist,
153
+ )
154
+
155
+ # Stream or await the result
156
+ agent_response = await self._execute_handler(handler, message)
157
+
158
+ # Store result in memory if configured
159
+ if self.step.memory:
160
+ memory = to_memory(session_id, self.step.memory)
161
+ # Convert agent response to chat message
162
+ assistant_message = ChatMessage(
163
+ role=MessageRole.assistant,
164
+ blocks=[
165
+ ChatContent(
166
+ type=PrimitiveTypeEnum.text, content=agent_response
167
+ )
168
+ ],
169
+ )
170
+ memory.put(to_chat_message(assistant_message))
171
+
172
+ # Convert result to ChatMessage
173
+ result_value = ChatMessage(
174
+ role=MessageRole.assistant,
175
+ blocks=[
176
+ ChatContent(
177
+ type=PrimitiveTypeEnum.text, content=agent_response
178
+ )
179
+ ],
180
+ )
181
+
182
+ return message.copy_with_variables({output_variable_id: result_value})
183
+
184
+ async def _process_completion(
185
+ self,
186
+ message: FlowMessage,
187
+ output_variable_id: str,
188
+ ) -> FlowMessage:
189
+ """Process a completion-based agent request.
190
+
191
+ Args:
192
+ message: The FlowMessage to process.
193
+ output_variable_id: The ID of the output variable.
194
+
195
+ Returns:
196
+ FlowMessage with the agent completion response.
197
+ """
198
+ # Get input value (expecting text)
199
+ input_value = message.variables.get(self.step.inputs[0].id)
200
+ if not isinstance(input_value, str):
201
+ input_value = str(input_value)
202
+
203
+ # Execute agent with the input as a simple message
204
+ handler = self._agent.run(user_msg=input_value)
205
+
206
+ # Stream or await the result
207
+ agent_response = await self._execute_handler(handler, message)
208
+
209
+ # Return result as text
210
+ return message.copy_with_variables(
211
+ {output_variable_id: agent_response}
212
+ )
213
+
214
+ async def _execute_handler(
215
+ self, handler: Any, message: FlowMessage
216
+ ) -> str:
217
+ """Execute the agent handler and return the response.
218
+
219
+ Args:
220
+ handler: The WorkflowHandler from ReActAgent.run
221
+ message: The FlowMessage for stream ID generation
222
+
223
+ Returns:
224
+ The agent's response as a string
225
+ """
226
+ if self.context.on_stream_event:
227
+ # Generate a unique stream ID for this inference
228
+ stream_id = f"agent-{self.step.id}-{id(message)}"
229
+
230
+ async with self.stream_emitter.text_stream(stream_id) as streamer:
231
+ # Stream the agent response
232
+ async for event in handler.stream_events():
233
+ if hasattr(event, "delta") and event.delta:
234
+ await streamer.delta(event.delta)
235
+
236
+ # Get the final result
237
+ result = await handler
238
+ else:
239
+ # Non-streaming execution
240
+ result = await handler
241
+
242
+ return str(result)
@@ -0,0 +1,93 @@
1
+ from typing import AsyncIterator
2
+
3
+ from qtype.dsl.domain_types import AggregateStats
4
+ from qtype.interpreter.base.batch_step_executor import BatchedStepExecutor
5
+ from qtype.interpreter.base.executor_context import ExecutorContext
6
+ from qtype.interpreter.types import FlowMessage, Session
7
+ from qtype.semantic.model import Aggregate
8
+
9
+
10
+ class AggregateExecutor(BatchedStepExecutor):
11
+ """
12
+ Executor for the Aggregate step.
13
+
14
+ This is a terminal, many-to-one operation that reduces an entire stream
15
+ to a single summary message containing counts of successful and failed
16
+ messages. It processes all messages without modification during the
17
+ processing phase, then emits a single aggregate summary during finalization.
18
+ """
19
+
20
+ def __init__(
21
+ self, step: Aggregate, context: ExecutorContext, **dependencies
22
+ ):
23
+ super().__init__(step, context, **dependencies)
24
+ if not isinstance(step, Aggregate):
25
+ raise ValueError(
26
+ "AggregateExecutor can only execute Aggregate steps."
27
+ )
28
+ self.step: Aggregate = step
29
+ self.last_session: Session | None = None
30
+
31
+ async def process_batch(
32
+ self,
33
+ batch: list[FlowMessage],
34
+ ) -> AsyncIterator[FlowMessage]:
35
+ """
36
+ Process messages by passing them through unchanged.
37
+
38
+ The aggregate step doesn't modify messages - it just passes them
39
+ through. All counting is handled by the base class's ProgressTracker,
40
+ which is updated as messages flow through execute(). The actual
41
+ aggregation happens in finalize(), which runs after all progress
42
+ tracking is complete.
43
+
44
+ Note: Failed messages are filtered out by the base class before
45
+ reaching this method, so all messages in the batch are successful.
46
+
47
+ Args:
48
+ batch: List of messages to process (all successful)
49
+
50
+ Yields:
51
+ Each message unchanged (pass-through)
52
+ """
53
+ for msg in batch:
54
+ # Track the last session seen for use in finalize
55
+ self.last_session = msg.session
56
+
57
+ # Pass message through unchanged
58
+ yield msg
59
+
60
+ async def finalize(
61
+ self,
62
+ ) -> AsyncIterator[FlowMessage]:
63
+ """
64
+ Emit a single summary message with aggregate statistics.
65
+
66
+ This runs after all messages have been processed and counted by the
67
+ base class's ProgressTracker. The summary includes counts of
68
+ successful, failed, and total messages.
69
+
70
+ Yields:
71
+ A single FlowMessage containing the aggregate statistics
72
+ """
73
+ if not self.last_session:
74
+ # If no messages were processed, create a summary with all zeros
75
+ # using a default session
76
+ session = Session(session_id="aggregate-no-input")
77
+ else:
78
+ session = self.last_session
79
+
80
+ # Create the aggregate stats output using counts from ProgressTracker.
81
+ # Since finalize() now runs AFTER all progress tracking is complete,
82
+ # self.progress has accurate counts of all messages processed.
83
+ variable_name = self.step.outputs[0].id
84
+ yield FlowMessage(
85
+ session=session,
86
+ variables={
87
+ variable_name: AggregateStats(
88
+ num_successful=self.progress.items_succeeded,
89
+ num_failed=self.progress.items_in_error,
90
+ num_total=self.progress.items_processed,
91
+ )
92
+ },
93
+ )