qtype 0.0.12__py3-none-any.whl → 0.1.7__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 +753 -264
- 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 +495 -24
- 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 +123 -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 +104 -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 +172 -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.7.dist-info}/METADATA +22 -5
- qtype-0.1.7.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.7.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.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
|
+
)
|