stepflow-py 0.2.1__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.
- stepflow_py/__init__.py +58 -0
- stepflow_py/context.py +182 -0
- stepflow_py/exceptions.py +161 -0
- stepflow_py/flow_builder.py +376 -0
- stepflow_py/generated_protocol.py +628 -0
- stepflow_py/http_server.py +359 -0
- stepflow_py/main.py +59 -0
- stepflow_py/message_decoder.py +246 -0
- stepflow_py/py.typed +0 -0
- stepflow_py/server.py +356 -0
- stepflow_py/stdio_server.py +213 -0
- stepflow_py/udf.py +221 -0
- stepflow_py/value.py +298 -0
- stepflow_py-0.2.1.dist-info/METADATA +127 -0
- stepflow_py-0.2.1.dist-info/RECORD +17 -0
- stepflow_py-0.2.1.dist-info/WHEEL +4 -0
- stepflow_py-0.2.1.dist-info/entry_points.txt +2 -0
stepflow_py/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
|
|
2
|
+
# license agreements. See the NOTICE file distributed with this work for
|
|
3
|
+
# additional information regarding copyright ownership. The ASF licenses this
|
|
4
|
+
# file to you under the Apache License, Version 2.0 (the "License"); you may not
|
|
5
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
6
|
+
# the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
13
|
+
# License for the specific language governing permissions and limitations under
|
|
14
|
+
# the License.
|
|
15
|
+
|
|
16
|
+
from .context import StepflowContext
|
|
17
|
+
from .flow_builder import FlowBuilder, StepHandle
|
|
18
|
+
from .generated_protocol import (
|
|
19
|
+
ErrorAction,
|
|
20
|
+
OnErrorDefault,
|
|
21
|
+
OnErrorFail,
|
|
22
|
+
OnErrorRetry,
|
|
23
|
+
OnErrorSkip,
|
|
24
|
+
OnSkipDefault,
|
|
25
|
+
OnSkipSkip,
|
|
26
|
+
SkipAction,
|
|
27
|
+
)
|
|
28
|
+
from .stdio_server import StepflowStdioServer
|
|
29
|
+
from .value import JsonPath, StepReference, Valuable, Value, WorkflowInput
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Core classes
|
|
33
|
+
"StepflowStdioServer",
|
|
34
|
+
"StepflowContext",
|
|
35
|
+
"FlowBuilder",
|
|
36
|
+
# Value API for cleaner workflow definitions
|
|
37
|
+
"Value",
|
|
38
|
+
"Valuable",
|
|
39
|
+
# Helper classes for type hints and intermediate objects
|
|
40
|
+
"JsonPath",
|
|
41
|
+
"StepHandle",
|
|
42
|
+
"StepReference",
|
|
43
|
+
"WorkflowInput",
|
|
44
|
+
# Error and Skip Action types
|
|
45
|
+
"ErrorAction",
|
|
46
|
+
"OnErrorFail",
|
|
47
|
+
"OnErrorSkip",
|
|
48
|
+
"OnErrorRetry",
|
|
49
|
+
"OnErrorDefault",
|
|
50
|
+
"SkipAction",
|
|
51
|
+
"OnSkipSkip",
|
|
52
|
+
"OnSkipDefault",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
from . import main
|
|
57
|
+
|
|
58
|
+
main.main()
|
stepflow_py/context.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
|
|
2
|
+
# license agreements. See the NOTICE file distributed with this work for
|
|
3
|
+
# additional information regarding copyright ownership. The ASF licenses this
|
|
4
|
+
# file to you under the Apache License, Version 2.0 (the "License"); you may not
|
|
5
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
6
|
+
# the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
13
|
+
# License for the specific language governing permissions and limitations under
|
|
14
|
+
# the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Any, TypeVar
|
|
21
|
+
from uuid import uuid4
|
|
22
|
+
|
|
23
|
+
from stepflow_py.generated_protocol import (
|
|
24
|
+
EvaluateFlowResult,
|
|
25
|
+
Flow,
|
|
26
|
+
FlowResultFailed,
|
|
27
|
+
FlowResultSkipped,
|
|
28
|
+
FlowResultSuccess,
|
|
29
|
+
GetBlobResult,
|
|
30
|
+
Message,
|
|
31
|
+
Method,
|
|
32
|
+
MethodError,
|
|
33
|
+
MethodSuccess,
|
|
34
|
+
PutBlobResult,
|
|
35
|
+
)
|
|
36
|
+
from stepflow_py.message_decoder import MessageDecoder
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
Context API for stepflow components to interact with the runtime.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
T = TypeVar("T")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StepflowContext:
|
|
46
|
+
"""Context for stepflow components to make calls back to the runtime.
|
|
47
|
+
|
|
48
|
+
This allows components to store/retrieve blobs and perform other
|
|
49
|
+
runtime operations through bidirectional communication.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
outgoing_queue: asyncio.Queue,
|
|
55
|
+
message_decoder: MessageDecoder[asyncio.Future[Message]],
|
|
56
|
+
session_id: str | None = None,
|
|
57
|
+
):
|
|
58
|
+
self._outgoing_queue = outgoing_queue
|
|
59
|
+
self._message_decoder = message_decoder
|
|
60
|
+
self._session_id = session_id
|
|
61
|
+
|
|
62
|
+
async def _send_request(
|
|
63
|
+
self, method: Method, params: Any, result_type: type[T]
|
|
64
|
+
) -> T:
|
|
65
|
+
"""Send a request to the stepflow runtime and wait for response."""
|
|
66
|
+
request_id = str(uuid4())
|
|
67
|
+
request = {
|
|
68
|
+
"jsonrpc": "2.0",
|
|
69
|
+
"id": request_id,
|
|
70
|
+
"method": method.value,
|
|
71
|
+
"params": params,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Create future for response
|
|
75
|
+
future: asyncio.Future[Message] = asyncio.Future()
|
|
76
|
+
|
|
77
|
+
# Register pending request with message decoder using method registration
|
|
78
|
+
self._message_decoder.register_request_for_method(request_id, method, future)
|
|
79
|
+
|
|
80
|
+
# Send request via queue
|
|
81
|
+
await self._outgoing_queue.put(request)
|
|
82
|
+
|
|
83
|
+
# Wait for response - the MessageDecoder will resolve this future
|
|
84
|
+
# when the response is received
|
|
85
|
+
response_message = await future
|
|
86
|
+
|
|
87
|
+
# Extract the result from the response message
|
|
88
|
+
if isinstance(response_message, MethodSuccess):
|
|
89
|
+
result = response_message.result
|
|
90
|
+
assert isinstance(result, result_type), (
|
|
91
|
+
f"Expected {result_type}, got {type(result)}"
|
|
92
|
+
)
|
|
93
|
+
return result
|
|
94
|
+
elif isinstance(response_message, MethodError):
|
|
95
|
+
# Handle error case
|
|
96
|
+
raise Exception(f"Request failed: {response_message.error}")
|
|
97
|
+
else:
|
|
98
|
+
raise Exception(
|
|
99
|
+
f"Unexpected response type: {type(response_message)} {response_message}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def put_blob(self, data: Any) -> str:
|
|
103
|
+
"""Store JSON data as a blob and return its content-based ID.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
data: The JSON-serializable data to store
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The blob ID (SHA-256 hash) for the stored data
|
|
110
|
+
"""
|
|
111
|
+
params = {"data": data}
|
|
112
|
+
response = await self._send_request(Method.blobs_put, params, PutBlobResult)
|
|
113
|
+
return response.blob_id
|
|
114
|
+
|
|
115
|
+
async def get_blob(self, blob_id: str) -> Any:
|
|
116
|
+
"""Retrieve JSON data by blob ID.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
blob_id: The blob ID to retrieve
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The JSON data associated with the blob ID
|
|
123
|
+
"""
|
|
124
|
+
params = {"blob_id": blob_id}
|
|
125
|
+
response = await self._send_request(Method.blobs_get, params, GetBlobResult)
|
|
126
|
+
return response.data
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def session_id(self) -> str | None:
|
|
130
|
+
"""Get the session ID for HTTP mode, or None for STDIO mode."""
|
|
131
|
+
return self._session_id
|
|
132
|
+
|
|
133
|
+
async def evaluate_flow(self, flow: Flow | dict, input: Any) -> Any:
|
|
134
|
+
"""Evaluate a flow with the given input.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
flow: The flow definition (as a Flow object or dictionary)
|
|
138
|
+
input: The input to provide to the flow
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The result value on success
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
StepflowSkipped: If the flow execution was skipped
|
|
145
|
+
StepflowFailed: If the flow execution failed with a business logic error
|
|
146
|
+
Exception: For system/runtime errors
|
|
147
|
+
"""
|
|
148
|
+
from stepflow_py.exceptions import StepflowFailed, StepflowSkipped
|
|
149
|
+
|
|
150
|
+
# Convert Flow object to dict if needed
|
|
151
|
+
if isinstance(flow, Flow):
|
|
152
|
+
import msgspec
|
|
153
|
+
|
|
154
|
+
flow_dict = msgspec.to_builtins(flow)
|
|
155
|
+
else:
|
|
156
|
+
flow_dict = flow
|
|
157
|
+
|
|
158
|
+
params = {"flow": flow_dict, "input": input}
|
|
159
|
+
|
|
160
|
+
evaluate_result = await self._send_request(
|
|
161
|
+
Method.flows_evaluate, params, EvaluateFlowResult
|
|
162
|
+
)
|
|
163
|
+
flow_result = evaluate_result.result
|
|
164
|
+
|
|
165
|
+
# Check the outcome and either return the result or raise appropriate exception
|
|
166
|
+
if isinstance(flow_result, FlowResultSuccess):
|
|
167
|
+
return flow_result.result
|
|
168
|
+
elif isinstance(flow_result, FlowResultSkipped):
|
|
169
|
+
raise StepflowSkipped("Flow execution was skipped")
|
|
170
|
+
elif isinstance(flow_result, FlowResultFailed):
|
|
171
|
+
error = flow_result.error
|
|
172
|
+
raise StepflowFailed(
|
|
173
|
+
error_code=error.code,
|
|
174
|
+
message=error.message,
|
|
175
|
+
data=error.data,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
raise Exception(f"Unexpected flow result type: {type(flow_result)}")
|
|
179
|
+
|
|
180
|
+
def log(self, message):
|
|
181
|
+
"""Log a message."""
|
|
182
|
+
print(f"PYTHON: {message}", file=sys.stderr)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
|
|
2
|
+
# license agreements. See the NOTICE file distributed with this work for
|
|
3
|
+
# additional information regarding copyright ownership. The ASF licenses this
|
|
4
|
+
# file to you under the Apache License, Version 2.0 (the "License"); you may not
|
|
5
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
6
|
+
# the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
13
|
+
# License for the specific language governing permissions and limitations under
|
|
14
|
+
# the License.
|
|
15
|
+
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorCode(IntEnum):
|
|
21
|
+
GENERIC_ERROR = -32000
|
|
22
|
+
NOT_INITIALIZED = -32001
|
|
23
|
+
|
|
24
|
+
INVALID_REQUEST = -32600
|
|
25
|
+
COMPONENT_ERROR = -32001
|
|
26
|
+
VALIDATION_ERROR = -32002
|
|
27
|
+
EXECUTION_ERROR = -32003
|
|
28
|
+
RUNTIME_ERROR = -32004
|
|
29
|
+
VALUE_ERROR = -32005
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StepflowError(Exception):
|
|
33
|
+
"""Base exception for all StepFlow SDK errors."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str, code: ErrorCode = None, data: dict = None):
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.message = message
|
|
38
|
+
self.code = code or self.default_code
|
|
39
|
+
self.data = data or {}
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def default_code(self) -> ErrorCode:
|
|
43
|
+
"""Default error code for this exception type."""
|
|
44
|
+
return ErrorCode.GENERIC_ERROR
|
|
45
|
+
|
|
46
|
+
def to_json_rpc_error(self) -> dict:
|
|
47
|
+
"""Convert to JSON-RPC error format."""
|
|
48
|
+
return {"code": self.code.value, "message": self.message, "data": self.data}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StepflowProtocolError(StepflowError):
|
|
52
|
+
"""Errors related to JSON-RPC protocol violations."""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def default_code(self) -> ErrorCode:
|
|
56
|
+
return ErrorCode.INVALID_REQUEST # Invalid Request
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StepflowComponentError(StepflowError):
|
|
60
|
+
"""Errors related to component operations."""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def default_code(self) -> ErrorCode:
|
|
64
|
+
return ErrorCode.COMPONENT_ERROR
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class StepflowValidationError(StepflowError):
|
|
68
|
+
"""Errors related to input/output validation."""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def default_code(self) -> ErrorCode:
|
|
72
|
+
return ErrorCode.VALIDATION_ERROR
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class StepflowValueError(StepflowError):
|
|
76
|
+
"""Errors related to invalid values."""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def default_code(self) -> ErrorCode:
|
|
80
|
+
return ErrorCode.VALUE_ERROR
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StepflowExecutionError(StepflowError):
|
|
84
|
+
"""Errors during component execution."""
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def default_code(self) -> ErrorCode:
|
|
88
|
+
return ErrorCode.EXECUTION_ERROR
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class StepflowRuntimeError(StepflowError):
|
|
92
|
+
"""Errors from the StepFlow runtime."""
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def default_code(self) -> ErrorCode:
|
|
96
|
+
return ErrorCode.RUNTIME_ERROR
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ComponentNotFoundError(StepflowComponentError):
|
|
100
|
+
"""Component was not found."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, component_name: str):
|
|
103
|
+
super().__init__(f"Component '{component_name}' not found")
|
|
104
|
+
self.component_name = component_name
|
|
105
|
+
self.data = {"component": component_name}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ServerNotInitializedError(StepflowProtocolError):
|
|
109
|
+
"""Server hasn't been initialized yet."""
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
super().__init__("Server not initialized")
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def default_code(self) -> ErrorCode:
|
|
116
|
+
return ErrorCode.NOT_INITIALIZED
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class InputValidationError(StepflowValidationError):
|
|
120
|
+
"""Input validation failed."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, validation_error: str, input_data: dict | None = None):
|
|
123
|
+
super().__init__(f"Input validation failed: {validation_error}")
|
|
124
|
+
if input_data:
|
|
125
|
+
self.data = {"input": input_data}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class BlobNotFoundError(StepflowRuntimeError):
|
|
129
|
+
"""Blob was not found in storage."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, blob_id: str):
|
|
132
|
+
super().__init__(f"Blob '{blob_id}' not found")
|
|
133
|
+
self.blob_id = blob_id
|
|
134
|
+
self.data = {"blob_id": blob_id}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CodeCompilationError(StepflowExecutionError):
|
|
138
|
+
"""User code compilation failed."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, compilation_error: str, code: str | None = None):
|
|
141
|
+
super().__init__(f"Code compilation failed: {compilation_error}")
|
|
142
|
+
if code:
|
|
143
|
+
self.data = {"code": code}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class StepflowSkipped(Exception):
|
|
147
|
+
"""Exception raised when a step or flow is skipped."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, message: str = "Flow execution was skipped"):
|
|
150
|
+
super().__init__(message)
|
|
151
|
+
self.message = message
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StepflowFailed(Exception):
|
|
155
|
+
"""Exception raised when a step or flow fails with a business logic error."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, error_code: int, message: str, data: Any = None):
|
|
158
|
+
super().__init__(message)
|
|
159
|
+
self.error_code = error_code
|
|
160
|
+
self.message = message
|
|
161
|
+
self.data = data
|