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,92 @@
|
|
|
1
|
+
from typing import AsyncIterator
|
|
2
|
+
|
|
3
|
+
from openinference.semconv.trace import OpenInferenceSpanKindValues
|
|
4
|
+
|
|
5
|
+
from qtype.base.types import PrimitiveTypeEnum
|
|
6
|
+
from qtype.dsl.domain_types import Embedding
|
|
7
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
8
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
9
|
+
from qtype.interpreter.conversions import to_embedding_model
|
|
10
|
+
from qtype.interpreter.types import FlowMessage
|
|
11
|
+
from qtype.semantic.model import InvokeEmbedding
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvokeEmbeddingExecutor(StepExecutor):
|
|
15
|
+
"""Executor for InvokeEmbedding steps."""
|
|
16
|
+
|
|
17
|
+
# Embedding operations should be marked as EMBEDDING type
|
|
18
|
+
span_kind = OpenInferenceSpanKindValues.EMBEDDING
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self, step: InvokeEmbedding, context: ExecutorContext, **dependencies
|
|
22
|
+
):
|
|
23
|
+
super().__init__(step, context, **dependencies)
|
|
24
|
+
if not isinstance(step, InvokeEmbedding):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
(
|
|
27
|
+
"InvokeEmbeddingExecutor can only execute "
|
|
28
|
+
"InvokeEmbedding steps."
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
self.step: InvokeEmbedding = step
|
|
32
|
+
# Initialize the embedding model once for the executor
|
|
33
|
+
self.embedding_model = to_embedding_model(self.step.model)
|
|
34
|
+
|
|
35
|
+
async def process_message(
|
|
36
|
+
self,
|
|
37
|
+
message: FlowMessage,
|
|
38
|
+
) -> AsyncIterator[FlowMessage]:
|
|
39
|
+
"""Process a single FlowMessage for the InvokeEmbedding step.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
message: The FlowMessage to process.
|
|
43
|
+
Yields:
|
|
44
|
+
FlowMessage with embedding.
|
|
45
|
+
"""
|
|
46
|
+
input_id = self.step.inputs[0].id
|
|
47
|
+
input_type = self.step.inputs[0].type
|
|
48
|
+
output_id = self.step.outputs[0].id
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Get the input value
|
|
52
|
+
input_value = message.variables.get(input_id)
|
|
53
|
+
|
|
54
|
+
if input_value is None:
|
|
55
|
+
raise ValueError(f"Input variable '{input_id}' is missing")
|
|
56
|
+
|
|
57
|
+
# Generate embedding based on input type
|
|
58
|
+
if input_type == PrimitiveTypeEnum.text:
|
|
59
|
+
if not isinstance(input_value, str):
|
|
60
|
+
input_value = str(input_value)
|
|
61
|
+
vector = self.embedding_model.get_text_embedding(
|
|
62
|
+
text=input_value
|
|
63
|
+
)
|
|
64
|
+
content = input_value
|
|
65
|
+
elif input_type == PrimitiveTypeEnum.image:
|
|
66
|
+
# For image embeddings
|
|
67
|
+
vector = self.embedding_model.get_image_embedding(
|
|
68
|
+
image_path=input_value
|
|
69
|
+
)
|
|
70
|
+
content = input_value
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
(
|
|
74
|
+
f"Unsupported input type for embedding: "
|
|
75
|
+
f"{input_type}. Must be 'text' or 'image'."
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create the Embedding object
|
|
80
|
+
embedding = Embedding(
|
|
81
|
+
vector=vector,
|
|
82
|
+
content=content,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Yield the result
|
|
86
|
+
yield message.copy_with_variables({output_id: embedding})
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# Emit error event to stream so frontend can display it
|
|
90
|
+
await self.stream_emitter.error(str(e))
|
|
91
|
+
message.set_error(self.step.id, e)
|
|
92
|
+
yield message
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import AsyncIterator
|
|
2
|
+
|
|
3
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
4
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
5
|
+
from qtype.interpreter.types import FlowMessage
|
|
6
|
+
from qtype.semantic.model import InvokeFlow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvokeFlowExecutor(StepExecutor):
|
|
10
|
+
"""Executor for InvokeFlow steps."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self, step: InvokeFlow, context: ExecutorContext, **dependencies
|
|
14
|
+
):
|
|
15
|
+
super().__init__(step, context, **dependencies)
|
|
16
|
+
if not isinstance(step, InvokeFlow):
|
|
17
|
+
raise ValueError(
|
|
18
|
+
("InvokeFlowExecutor can only execute InvokeFlow steps.")
|
|
19
|
+
)
|
|
20
|
+
self.step: InvokeFlow = step
|
|
21
|
+
|
|
22
|
+
async def process_message(
|
|
23
|
+
self, message: FlowMessage
|
|
24
|
+
) -> AsyncIterator[FlowMessage]:
|
|
25
|
+
"""Process a single FlowMessage for the InvokeFlow step.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
message: The FlowMessage to process.
|
|
29
|
+
Yields:
|
|
30
|
+
FlowMessage with results from the invoked flow.
|
|
31
|
+
"""
|
|
32
|
+
from qtype.interpreter.flow import run_flow
|
|
33
|
+
|
|
34
|
+
initial = message.copy_with_variables(
|
|
35
|
+
{
|
|
36
|
+
id: message.variables.get(var.id)
|
|
37
|
+
for var, id in self.step.input_bindings.items()
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
# Pass through context (already available as self.context)
|
|
41
|
+
result = await run_flow(
|
|
42
|
+
self.step.flow, [initial], context=self.context
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for msg in result:
|
|
46
|
+
yield msg.copy_with_variables(
|
|
47
|
+
{
|
|
48
|
+
var.id: msg.variables.get(id)
|
|
49
|
+
for var, id in self.step.output_bindings.items()
|
|
50
|
+
}
|
|
51
|
+
)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, AsyncIterator
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from openinference.semconv.trace import OpenInferenceSpanKindValues
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
14
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
15
|
+
from qtype.interpreter.base.stream_emitter import StreamEmitter
|
|
16
|
+
from qtype.interpreter.types import FlowMessage
|
|
17
|
+
from qtype.semantic.model import (
|
|
18
|
+
APITool,
|
|
19
|
+
BearerTokenAuthProvider,
|
|
20
|
+
InvokeTool,
|
|
21
|
+
PythonFunctionTool,
|
|
22
|
+
Step,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# HTTP methods that require request body instead of query parameters
|
|
28
|
+
HTTP_BODY_METHODS = frozenset(["POST", "PUT", "PATCH"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolExecutionMixin:
|
|
32
|
+
"""Mixin providing tool execution capabilities for Python and API tools.
|
|
33
|
+
|
|
34
|
+
This mixin can be used by any executor that needs to invoke tools,
|
|
35
|
+
allowing code reuse across InvokeToolExecutor and AgentExecutor.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
# These will be set by the concrete executor classes
|
|
41
|
+
self.stream_emitter: StreamEmitter
|
|
42
|
+
self.step: Step
|
|
43
|
+
self._resolve_secret: (
|
|
44
|
+
Any # Will be provided by StepExecutor base class
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def execute_python_tool(
|
|
48
|
+
self,
|
|
49
|
+
tool: PythonFunctionTool,
|
|
50
|
+
inputs: dict[str, Any],
|
|
51
|
+
original_inputs: dict[str, Any] | None = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Execute a Python function tool with proper streaming events.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tool: The Python function tool to execute.
|
|
57
|
+
inputs: Dictionary of input parameter names to values
|
|
58
|
+
(may contain parsed Python objects like datetime).
|
|
59
|
+
original_inputs: Optional dictionary of original JSON-serializable
|
|
60
|
+
inputs for streaming events. If not provided, uses inputs.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The result from the function call.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If the function cannot be found or executed.
|
|
67
|
+
"""
|
|
68
|
+
tool_call_id = str(uuid.uuid4())
|
|
69
|
+
|
|
70
|
+
# Use original inputs for streaming events if provided
|
|
71
|
+
stream_inputs = original_inputs if original_inputs else inputs
|
|
72
|
+
|
|
73
|
+
async with self.stream_emitter.tool_execution(
|
|
74
|
+
tool_call_id=tool_call_id,
|
|
75
|
+
tool_name=tool.function_name,
|
|
76
|
+
tool_input=stream_inputs,
|
|
77
|
+
) as tool_ctx:
|
|
78
|
+
try:
|
|
79
|
+
module = importlib.import_module(tool.module_path)
|
|
80
|
+
function = getattr(module, tool.function_name, None)
|
|
81
|
+
if function is None:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
(
|
|
84
|
+
f"Function '{tool.function_name}' not found in "
|
|
85
|
+
f"module '{tool.module_path}'"
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
result = function(**inputs)
|
|
90
|
+
await tool_ctx.complete(result)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
error_msg = (
|
|
95
|
+
f"Failed to execute function {tool.function_name}: {e}"
|
|
96
|
+
)
|
|
97
|
+
logger.error(error_msg, exc_info=True)
|
|
98
|
+
await tool_ctx.error(error_msg)
|
|
99
|
+
raise ValueError(error_msg) from e
|
|
100
|
+
|
|
101
|
+
def serialize_value(self, value: Any) -> Any:
|
|
102
|
+
"""Recursively serialize values for API requests.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value: The value to serialize.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Serialized value suitable for JSON encoding.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(value, dict):
|
|
111
|
+
return {k: self.serialize_value(v) for k, v in value.items()}
|
|
112
|
+
elif isinstance(value, list):
|
|
113
|
+
return [self.serialize_value(item) for item in value]
|
|
114
|
+
elif isinstance(value, BaseModel):
|
|
115
|
+
return value.model_dump()
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
async def execute_api_tool(
|
|
119
|
+
self,
|
|
120
|
+
tool: APITool,
|
|
121
|
+
inputs: dict[str, Any],
|
|
122
|
+
original_inputs: dict[str, Any] | None = None,
|
|
123
|
+
) -> Any:
|
|
124
|
+
"""Execute an API tool by making an HTTP request with proper streaming events.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
tool: The API tool to execute.
|
|
128
|
+
inputs: Dictionary of input parameter names to values
|
|
129
|
+
(may contain parsed Python objects like datetime).
|
|
130
|
+
original_inputs: Optional dictionary of original JSON-serializable
|
|
131
|
+
inputs for streaming events. If not provided, uses inputs.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The result from the API call.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If authentication fails or the request fails.
|
|
138
|
+
"""
|
|
139
|
+
tool_call_id = str(uuid.uuid4())
|
|
140
|
+
|
|
141
|
+
# Use original inputs for streaming events if provided
|
|
142
|
+
stream_inputs = original_inputs if original_inputs else inputs
|
|
143
|
+
|
|
144
|
+
async with self.stream_emitter.tool_execution(
|
|
145
|
+
tool_call_id=tool_call_id,
|
|
146
|
+
tool_name=f"{tool.method} {tool.endpoint}",
|
|
147
|
+
tool_input=stream_inputs,
|
|
148
|
+
) as tool_ctx:
|
|
149
|
+
try:
|
|
150
|
+
# Prepare headers - resolve any SecretReferences
|
|
151
|
+
# Note: ToolExecutionMixin users inherit from StepExecutor
|
|
152
|
+
# which provides _secret_manager
|
|
153
|
+
secret_manager = getattr(self, "_secret_manager")
|
|
154
|
+
context = f"tool '{tool.id}'"
|
|
155
|
+
headers = (
|
|
156
|
+
secret_manager.resolve_secrets_in_dict(
|
|
157
|
+
tool.headers, context
|
|
158
|
+
)
|
|
159
|
+
if tool.headers
|
|
160
|
+
else {}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Handle authentication
|
|
164
|
+
if tool.auth:
|
|
165
|
+
if isinstance(tool.auth, BearerTokenAuthProvider):
|
|
166
|
+
token = self._resolve_secret(tool.auth.token)
|
|
167
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
(
|
|
171
|
+
f"Unsupported auth provider: {type(tool.auth).__name__}"
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Serialize inputs for JSON
|
|
176
|
+
body = self.serialize_value(inputs)
|
|
177
|
+
|
|
178
|
+
# Determine if we're sending body or query params
|
|
179
|
+
is_body_method = tool.method.upper() in HTTP_BODY_METHODS
|
|
180
|
+
|
|
181
|
+
start_time = time.time()
|
|
182
|
+
|
|
183
|
+
response = requests.request(
|
|
184
|
+
method=tool.method.upper(),
|
|
185
|
+
url=tool.endpoint,
|
|
186
|
+
headers=headers,
|
|
187
|
+
params=None if is_body_method else inputs,
|
|
188
|
+
json=body if is_body_method else None,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
duration = time.time() - start_time
|
|
192
|
+
|
|
193
|
+
# Raise for HTTP errors
|
|
194
|
+
response.raise_for_status()
|
|
195
|
+
|
|
196
|
+
logger.debug(
|
|
197
|
+
f"Request completed in {duration:.2f}s with status "
|
|
198
|
+
f"{response.status_code}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result = response.json()
|
|
202
|
+
await tool_ctx.complete(result)
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
except requests.exceptions.RequestException as e:
|
|
206
|
+
error_msg = f"API request failed: {e}"
|
|
207
|
+
await tool_ctx.error(error_msg)
|
|
208
|
+
raise ValueError(error_msg) from e
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
error_msg = f"Failed to decode JSON response: {e}"
|
|
211
|
+
await tool_ctx.error(error_msg)
|
|
212
|
+
raise ValueError(error_msg) from e
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class InvokeToolExecutor(StepExecutor, ToolExecutionMixin):
|
|
216
|
+
"""Executor for InvokeTool steps."""
|
|
217
|
+
|
|
218
|
+
# Tool invocations should be marked as TOOL type
|
|
219
|
+
span_kind = OpenInferenceSpanKindValues.TOOL
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self, step: InvokeTool, context: ExecutorContext, **dependencies: Any
|
|
223
|
+
) -> None:
|
|
224
|
+
super().__init__(step, context, **dependencies)
|
|
225
|
+
if not isinstance(step, InvokeTool):
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"InvokeToolExecutor can only execute InvokeTool steps."
|
|
228
|
+
)
|
|
229
|
+
self.step: InvokeTool = step
|
|
230
|
+
|
|
231
|
+
def _prepare_tool_inputs(self, message: FlowMessage) -> dict[str, Any]:
|
|
232
|
+
"""Prepare tool inputs from message variables using input bindings.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
message: The FlowMessage containing input variables.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Dictionary mapping tool parameter names to values.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If required inputs are missing.
|
|
242
|
+
"""
|
|
243
|
+
tool_inputs = {}
|
|
244
|
+
|
|
245
|
+
for tool_param_name, step_var_id in self.step.input_bindings.items():
|
|
246
|
+
# Get tool parameter definition
|
|
247
|
+
tool_param = self.step.tool.inputs.get(tool_param_name)
|
|
248
|
+
if not tool_param:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Tool parameter '{tool_param_name}' not defined in tool"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Get value from message variables
|
|
254
|
+
value = message.variables.get(step_var_id)
|
|
255
|
+
|
|
256
|
+
# Handle missing values
|
|
257
|
+
if value is None:
|
|
258
|
+
if not tool_param.optional:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
(
|
|
261
|
+
f"Required input '{step_var_id}' for tool "
|
|
262
|
+
f"parameter '{tool_param_name}' is missing"
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
# Skip optional parameters that are missing
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
tool_inputs[tool_param_name] = value
|
|
269
|
+
|
|
270
|
+
return tool_inputs
|
|
271
|
+
|
|
272
|
+
def _extract_tool_outputs(self, result: Any) -> dict[str, Any]:
|
|
273
|
+
"""Extract output variables from tool result using output bindings.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
result: The result from tool execution.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dictionary mapping step variable IDs to their values.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ValueError: If required outputs are missing from result.
|
|
283
|
+
"""
|
|
284
|
+
output_vars = {}
|
|
285
|
+
|
|
286
|
+
for tool_param_name, step_var_id in self.step.output_bindings.items():
|
|
287
|
+
# Get tool parameter definition
|
|
288
|
+
tool_param = self.step.tool.outputs.get(tool_param_name)
|
|
289
|
+
if not tool_param:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Tool parameter '{tool_param_name}' not defined in tool"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Extract value from result
|
|
295
|
+
if isinstance(result, dict):
|
|
296
|
+
value = result.get(tool_param_name)
|
|
297
|
+
if value is None and not tool_param.optional:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
(
|
|
300
|
+
f"Required output '{tool_param_name}' not found "
|
|
301
|
+
f"in result. Available: {list(result.keys())}"
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
# Single output case - use entire result
|
|
306
|
+
value = result
|
|
307
|
+
|
|
308
|
+
if value is not None:
|
|
309
|
+
output_vars[step_var_id] = value
|
|
310
|
+
|
|
311
|
+
return output_vars
|
|
312
|
+
|
|
313
|
+
async def process_message(
|
|
314
|
+
self,
|
|
315
|
+
message: FlowMessage,
|
|
316
|
+
) -> AsyncIterator[FlowMessage]:
|
|
317
|
+
"""Process a single FlowMessage for the InvokeTool step.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
message: The FlowMessage to process.
|
|
321
|
+
Yields:
|
|
322
|
+
FlowMessage with tool execution results.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
# Prepare tool inputs from message variables
|
|
326
|
+
tool_inputs = self._prepare_tool_inputs(message)
|
|
327
|
+
|
|
328
|
+
# Execute the tool with proper tool execution context
|
|
329
|
+
# Dispatch to appropriate execution method based on tool type
|
|
330
|
+
if isinstance(self.step.tool, PythonFunctionTool):
|
|
331
|
+
result = await self.execute_python_tool(
|
|
332
|
+
self.step.tool, tool_inputs
|
|
333
|
+
)
|
|
334
|
+
elif isinstance(self.step.tool, APITool):
|
|
335
|
+
result = await self.execute_api_tool(
|
|
336
|
+
self.step.tool, tool_inputs
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"Unsupported tool type: {type(self.step.tool).__name__}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Extract outputs from result
|
|
344
|
+
output_vars = self._extract_tool_outputs(result)
|
|
345
|
+
|
|
346
|
+
# Yield the result
|
|
347
|
+
yield message.copy_with_variables(output_vars)
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
# Emit error event to stream so frontend can display it
|
|
351
|
+
await self.stream_emitter.error(str(e))
|
|
352
|
+
message.set_error(self.step.id, e)
|
|
353
|
+
yield message
|