soprano-sdk 0.2.4__py3-none-any.whl → 0.2.6__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.
- soprano_sdk/authenticators/mfa.py +11 -3
- soprano_sdk/core/constants.py +7 -0
- soprano_sdk/nodes/async_function.py +237 -0
- soprano_sdk/nodes/call_function.py +6 -0
- soprano_sdk/nodes/factory.py +2 -0
- soprano_sdk/routing/router.py +6 -1
- soprano_sdk/tools.py +35 -13
- soprano_sdk/validation/schema.py +1 -1
- {soprano_sdk-0.2.4.dist-info → soprano_sdk-0.2.6.dist-info}/METADATA +1 -1
- {soprano_sdk-0.2.4.dist-info → soprano_sdk-0.2.6.dist-info}/RECORD +12 -11
- {soprano_sdk-0.2.4.dist-info → soprano_sdk-0.2.6.dist-info}/WHEEL +0 -0
- {soprano_sdk-0.2.4.dist-info → soprano_sdk-0.2.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,6 +10,7 @@ class MFAChallenge(TypedDict):
|
|
|
10
10
|
class MFAState(TypedDict):
|
|
11
11
|
challengeType: Literal['OTP', 'dob']
|
|
12
12
|
post_payload: dict[str, str]
|
|
13
|
+
post_headers: NotRequired[dict[str, str]]
|
|
13
14
|
otpValue: NotRequired[str]
|
|
14
15
|
status: Literal['IN_PROGRESS', 'COMPLETED', 'ERRORED', 'FAILED'] | None
|
|
15
16
|
message: str
|
|
@@ -35,6 +36,10 @@ def enforce_mfa_if_required(state: dict, mfa_config: Optional[MFAConfig] = None)
|
|
|
35
36
|
_mfa : MFAState = state['_mfa']
|
|
36
37
|
if _mfa['status'] == 'COMPLETED':
|
|
37
38
|
return True
|
|
39
|
+
|
|
40
|
+
# Use custom headers if provided, otherwise empty dict
|
|
41
|
+
headers = _mfa.get('post_headers', {})
|
|
42
|
+
|
|
38
43
|
generate_token_response = requests.post(
|
|
39
44
|
build_path(
|
|
40
45
|
base_url=mfa_config.generate_token_base_url,
|
|
@@ -42,7 +47,7 @@ def enforce_mfa_if_required(state: dict, mfa_config: Optional[MFAConfig] = None)
|
|
|
42
47
|
),
|
|
43
48
|
json=_mfa['post_payload'],
|
|
44
49
|
timeout=mfa_config.api_timeout,
|
|
45
|
-
headers=
|
|
50
|
+
headers=headers
|
|
46
51
|
)
|
|
47
52
|
_, error = get_response(generate_token_response)
|
|
48
53
|
|
|
@@ -65,6 +70,9 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
|
|
|
65
70
|
if not state[input_field_name]:
|
|
66
71
|
return False
|
|
67
72
|
|
|
73
|
+
# Use custom headers if provided, otherwise empty dict
|
|
74
|
+
headers = _mfa.get('post_headers', {})
|
|
75
|
+
|
|
68
76
|
post_payload = _mfa['post_payload']
|
|
69
77
|
challenge_field_name = f"{_mfa['challengeType'].lower()}Challenge"
|
|
70
78
|
post_payload.update({challenge_field_name: {"value": state[input_field_name]}})
|
|
@@ -75,7 +83,7 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
|
|
|
75
83
|
),
|
|
76
84
|
json=post_payload,
|
|
77
85
|
timeout=mfa_config.api_timeout,
|
|
78
|
-
headers=
|
|
86
|
+
headers=headers
|
|
79
87
|
)
|
|
80
88
|
_mfa['retry_count'] += 1
|
|
81
89
|
response, error = get_response(validate_token_response)
|
|
@@ -95,7 +103,7 @@ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dic
|
|
|
95
103
|
),
|
|
96
104
|
json=post_payload,
|
|
97
105
|
timeout=mfa_config.api_timeout,
|
|
98
|
-
headers=
|
|
106
|
+
headers=headers
|
|
99
107
|
)
|
|
100
108
|
if authorize.status_code == 204:
|
|
101
109
|
_mfa['status'] = 'COMPLETED'
|
soprano_sdk/core/constants.py
CHANGED
|
@@ -22,6 +22,13 @@ class WorkflowKeys:
|
|
|
22
22
|
class ActionType(Enum):
|
|
23
23
|
COLLECT_INPUT_WITH_AGENT = 'collect_input_with_agent'
|
|
24
24
|
CALL_FUNCTION = 'call_function'
|
|
25
|
+
CALL_ASYNC_FUNCTION = 'call_async_function'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InterruptType:
|
|
29
|
+
"""Interrupt type markers for workflow pauses"""
|
|
30
|
+
USER_INPUT = '__WORKFLOW_INTERRUPT__'
|
|
31
|
+
ASYNC = '__ASYNC_INTERRUPT__'
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
class DataType(Enum):
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async Function Strategy - Handles asynchronous function calls with interrupt/resume pattern.
|
|
3
|
+
|
|
4
|
+
This strategy allows workflows to pause while waiting for an external async operation
|
|
5
|
+
to complete. The async function initiates an operation and returns a "pending" status.
|
|
6
|
+
The workflow then interrupts, and resumes when the external system calls back with the result.
|
|
7
|
+
|
|
8
|
+
Example YAML:
|
|
9
|
+
- id: verify_identity
|
|
10
|
+
action: call_async_function
|
|
11
|
+
function: "services.identity.start_verification"
|
|
12
|
+
output: verification_result
|
|
13
|
+
transitions:
|
|
14
|
+
- condition: "verified"
|
|
15
|
+
next: approved
|
|
16
|
+
- condition: "failed"
|
|
17
|
+
next: rejected
|
|
18
|
+
|
|
19
|
+
The async function should return:
|
|
20
|
+
- {"status": "pending", ...metadata} to trigger interrupt and wait for callback
|
|
21
|
+
- Any other dict for synchronous completion (no interrupt)
|
|
22
|
+
|
|
23
|
+
On resume, the async result is passed via Command(resume=async_result) and stored
|
|
24
|
+
in the output field, then transitions are evaluated.
|
|
25
|
+
"""
|
|
26
|
+
from typing import Dict, Any
|
|
27
|
+
|
|
28
|
+
from langgraph.types import interrupt
|
|
29
|
+
|
|
30
|
+
from .base import ActionStrategy
|
|
31
|
+
from ..core.state import set_state_value, get_state_value
|
|
32
|
+
from ..core.constants import WorkflowKeys
|
|
33
|
+
from ..utils.logger import logger
|
|
34
|
+
from ..utils.template import get_nested_value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncFunctionStrategy(ActionStrategy):
|
|
38
|
+
"""Strategy for executing async functions with interrupt/resume pattern."""
|
|
39
|
+
|
|
40
|
+
# Key for storing pending metadata in state
|
|
41
|
+
PENDING_KEY_PREFIX = '_async_pending_'
|
|
42
|
+
|
|
43
|
+
def __init__(self, step_config: Dict[str, Any], engine_context: Any):
|
|
44
|
+
super().__init__(step_config, engine_context)
|
|
45
|
+
self.function_path = step_config.get('function')
|
|
46
|
+
self.output_field = step_config.get('output')
|
|
47
|
+
self.transitions = self._get_transitions()
|
|
48
|
+
self.next_step = self._get_next_step()
|
|
49
|
+
|
|
50
|
+
if not self.function_path:
|
|
51
|
+
raise RuntimeError(f"Step '{self.step_id}' missing required 'function' property")
|
|
52
|
+
|
|
53
|
+
if not self.output_field:
|
|
54
|
+
raise RuntimeError(f"Step '{self.step_id}' missing required 'output' property")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def _pending_key(self) -> str:
|
|
58
|
+
"""State key for storing pending operation metadata."""
|
|
59
|
+
return f"{self.PENDING_KEY_PREFIX}{self.step_id}"
|
|
60
|
+
|
|
61
|
+
def _is_async_pending(self, state: Dict[str, Any]) -> bool:
|
|
62
|
+
"""Check if this node is waiting for async operation to complete."""
|
|
63
|
+
return state.get(WorkflowKeys.STATUS) == f'{self.step_id}_pending'
|
|
64
|
+
|
|
65
|
+
def pre_execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
66
|
+
"""Pre-execution hook."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
from ..utils.tracing import trace_node_execution
|
|
71
|
+
|
|
72
|
+
with trace_node_execution(
|
|
73
|
+
node_id=self.step_id,
|
|
74
|
+
node_type="call_async_function",
|
|
75
|
+
function=self.function_path,
|
|
76
|
+
output_field=self.output_field
|
|
77
|
+
) as span:
|
|
78
|
+
# Check if we're resuming from a pending async operation
|
|
79
|
+
if self._is_async_pending(state):
|
|
80
|
+
span.add_event("async.resuming")
|
|
81
|
+
pending_metadata = state.get(self._pending_key, {})
|
|
82
|
+
else:
|
|
83
|
+
# First invocation - call the async function
|
|
84
|
+
result = self._call_function(state, span)
|
|
85
|
+
|
|
86
|
+
if self._is_pending_result(result):
|
|
87
|
+
# Async operation started - store metadata and prepare to interrupt
|
|
88
|
+
span.add_event("async.pending", {"metadata": str(result)})
|
|
89
|
+
self._set_status(state, "pending")
|
|
90
|
+
state[self._pending_key] = result
|
|
91
|
+
pending_metadata = result
|
|
92
|
+
else:
|
|
93
|
+
# Synchronous completion - no interrupt needed
|
|
94
|
+
span.add_event("async.sync_complete", {"result": str(result)})
|
|
95
|
+
return self._handle_sync_completion(state, result, span)
|
|
96
|
+
|
|
97
|
+
# Interrupt with pending metadata
|
|
98
|
+
# On resume, interrupt() returns the async result from Command(resume=...)
|
|
99
|
+
async_result = interrupt({
|
|
100
|
+
"type": "async",
|
|
101
|
+
"step_id": self.step_id,
|
|
102
|
+
"pending": pending_metadata
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Clean up pending state
|
|
106
|
+
if self._pending_key in state:
|
|
107
|
+
del state[self._pending_key]
|
|
108
|
+
|
|
109
|
+
span.add_event("async.resumed", {"result": str(async_result)})
|
|
110
|
+
|
|
111
|
+
# Store result and handle routing
|
|
112
|
+
return self._handle_async_completion(state, async_result, span)
|
|
113
|
+
|
|
114
|
+
def _call_function(self, state: Dict[str, Any], span) -> Any:
|
|
115
|
+
"""Load and execute the async function."""
|
|
116
|
+
try:
|
|
117
|
+
logger.info(f"Loading async function: {self.function_path}")
|
|
118
|
+
func = self.engine_context.function_repository.load(self.function_path)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
span.set_attribute("error", True)
|
|
121
|
+
span.set_attribute("error.type", "LoadError")
|
|
122
|
+
span.set_attribute("error.message", str(e))
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
f"Failed to load function '{self.function_path}' in step '{self.step_id}': {e}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
logger.info(f"Calling async function: {self.function_path}")
|
|
129
|
+
result = func(state)
|
|
130
|
+
logger.info(f"Async function {self.function_path} returned: {result}")
|
|
131
|
+
return result
|
|
132
|
+
except Exception as e:
|
|
133
|
+
span.set_attribute("error", True)
|
|
134
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
135
|
+
span.set_attribute("error.message", str(e))
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
f"Function '{self.function_path}' failed in step '{self.step_id}': {e}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _is_pending_result(self, result: Any) -> bool:
|
|
141
|
+
"""Check if the function result indicates a pending async operation."""
|
|
142
|
+
if not isinstance(result, dict):
|
|
143
|
+
return False
|
|
144
|
+
return result.get("status") == "pending"
|
|
145
|
+
|
|
146
|
+
def _handle_sync_completion(
|
|
147
|
+
self,
|
|
148
|
+
state: Dict[str, Any],
|
|
149
|
+
result: Any,
|
|
150
|
+
span
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Handle synchronous function completion (no async wait needed)."""
|
|
153
|
+
set_state_value(state, self.output_field, result)
|
|
154
|
+
self._track_computed_field(state)
|
|
155
|
+
return self._handle_routing(state, result, span)
|
|
156
|
+
|
|
157
|
+
def _handle_async_completion(
|
|
158
|
+
self,
|
|
159
|
+
state: Dict[str, Any],
|
|
160
|
+
async_result: Any,
|
|
161
|
+
span
|
|
162
|
+
) -> Dict[str, Any]:
|
|
163
|
+
"""Handle async operation completion after resume."""
|
|
164
|
+
set_state_value(state, self.output_field, async_result)
|
|
165
|
+
self._track_computed_field(state)
|
|
166
|
+
return self._handle_routing(state, async_result, span)
|
|
167
|
+
|
|
168
|
+
def _track_computed_field(self, state: Dict[str, Any]):
|
|
169
|
+
"""Track this field as computed for rollback purposes."""
|
|
170
|
+
computed_fields = get_state_value(state, WorkflowKeys.COMPUTED_FIELDS, [])
|
|
171
|
+
if self.output_field not in computed_fields:
|
|
172
|
+
computed_fields.append(self.output_field)
|
|
173
|
+
set_state_value(state, WorkflowKeys.COMPUTED_FIELDS, computed_fields)
|
|
174
|
+
|
|
175
|
+
def _handle_routing(
|
|
176
|
+
self,
|
|
177
|
+
state: Dict[str, Any],
|
|
178
|
+
result: Any,
|
|
179
|
+
span
|
|
180
|
+
) -> Dict[str, Any]:
|
|
181
|
+
"""Determine next step based on transitions or default routing."""
|
|
182
|
+
if self.transitions:
|
|
183
|
+
return self._handle_transition_routing(state, result, span)
|
|
184
|
+
return self._handle_simple_routing(state, span)
|
|
185
|
+
|
|
186
|
+
def _handle_transition_routing(
|
|
187
|
+
self,
|
|
188
|
+
state: Dict[str, Any],
|
|
189
|
+
result: Any,
|
|
190
|
+
span
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""Route based on transition conditions matching the result."""
|
|
193
|
+
for transition in self.transitions:
|
|
194
|
+
check_value = result
|
|
195
|
+
|
|
196
|
+
# Support nested field references
|
|
197
|
+
if 'ref' in transition:
|
|
198
|
+
check_value = get_nested_value(result, transition['ref'])
|
|
199
|
+
|
|
200
|
+
condition = transition['condition']
|
|
201
|
+
|
|
202
|
+
# Support list of conditions
|
|
203
|
+
if isinstance(condition, list):
|
|
204
|
+
if check_value not in condition:
|
|
205
|
+
continue
|
|
206
|
+
elif check_value != condition:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
next_dest = transition['next']
|
|
210
|
+
logger.info(f"Async function matched transition, routing to {next_dest}")
|
|
211
|
+
span.add_event("transition.matched", {"next": next_dest})
|
|
212
|
+
self._set_status(state, next_dest)
|
|
213
|
+
|
|
214
|
+
if next_dest in self.engine_context.outcome_map:
|
|
215
|
+
self._set_outcome(state, next_dest)
|
|
216
|
+
|
|
217
|
+
return state
|
|
218
|
+
|
|
219
|
+
logger.warning(
|
|
220
|
+
f"No matching transition for async result '{result}' in step '{self.step_id}'"
|
|
221
|
+
)
|
|
222
|
+
span.add_event("transition.no_match", {"result": str(result)})
|
|
223
|
+
self._set_status(state, 'failed')
|
|
224
|
+
return state
|
|
225
|
+
|
|
226
|
+
def _handle_simple_routing(self, state: Dict[str, Any], span) -> Dict[str, Any]:
|
|
227
|
+
"""Route to next step when no transitions are defined."""
|
|
228
|
+
self._set_status(state, 'success')
|
|
229
|
+
|
|
230
|
+
if self.next_step:
|
|
231
|
+
self._set_status(state, self.next_step)
|
|
232
|
+
span.add_event("routing.next_step", {"next": self.next_step})
|
|
233
|
+
|
|
234
|
+
if self.next_step in self.engine_context.outcome_map:
|
|
235
|
+
self._set_outcome(state, self.next_step)
|
|
236
|
+
|
|
237
|
+
return state
|
|
@@ -39,11 +39,17 @@ class CallFunctionStrategy(ActionStrategy):
|
|
|
39
39
|
if 'mfa' in self.step_config:
|
|
40
40
|
state['_mfa'] = state.get('_mfa', {})
|
|
41
41
|
state['_mfa']['post_payload'] = dict(transactionId=str(uuid.uuid4()))
|
|
42
|
+
state['_mfa']['post_headers'] = {}
|
|
42
43
|
state['_mfa_config'] = self.engine_context.mfa_config
|
|
43
44
|
template_loader = self.engine_context.get_config_value("template_loader", Environment())
|
|
44
45
|
for k, v in self.step_config['mfa']['payload'].items():
|
|
45
46
|
state['_mfa']['post_payload'][k] = compile_values(template_loader, state, v)
|
|
46
47
|
|
|
48
|
+
# Process headers if provided
|
|
49
|
+
if 'headers' in self.step_config['mfa']:
|
|
50
|
+
for k, v in self.step_config['mfa']['headers'].items():
|
|
51
|
+
state['_mfa']['post_headers'][k] = compile_values(template_loader, state, v)
|
|
52
|
+
|
|
47
53
|
def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
48
54
|
from ..utils.tracing import trace_node_execution
|
|
49
55
|
|
soprano_sdk/nodes/factory.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Dict, Any, Type, Callable
|
|
|
3
3
|
from .base import ActionStrategy
|
|
4
4
|
from .call_function import CallFunctionStrategy
|
|
5
5
|
from .collect_input import CollectInputStrategy
|
|
6
|
+
from .async_function import AsyncFunctionStrategy
|
|
6
7
|
from ..core.constants import ActionType
|
|
7
8
|
from ..utils.logger import logger
|
|
8
9
|
|
|
@@ -44,3 +45,4 @@ class NodeFactory:
|
|
|
44
45
|
|
|
45
46
|
NodeFactory.register(ActionType.COLLECT_INPUT_WITH_AGENT.value, CollectInputStrategy)
|
|
46
47
|
NodeFactory.register(ActionType.CALL_FUNCTION.value, CallFunctionStrategy)
|
|
48
|
+
NodeFactory.register(ActionType.CALL_ASYNC_FUNCTION.value, AsyncFunctionStrategy)
|
soprano_sdk/routing/router.py
CHANGED
|
@@ -32,6 +32,10 @@ class WorkflowRouter:
|
|
|
32
32
|
logger.info(f"Self-loop: {self.step_id} (collecting)")
|
|
33
33
|
return self.step_id
|
|
34
34
|
|
|
35
|
+
if status == f'{self.step_id}_pending':
|
|
36
|
+
logger.info(f"Self-loop: {self.step_id} (async pending)")
|
|
37
|
+
return self.step_id
|
|
38
|
+
|
|
35
39
|
if status == f'{self.step_id}_error' :
|
|
36
40
|
logger.info(f"Error encountered in {self.step_id}, ending workflow")
|
|
37
41
|
return END
|
|
@@ -73,7 +77,8 @@ class WorkflowRouter:
|
|
|
73
77
|
def get_routing_map(self, collector_nodes: List[str]) -> Dict[str, str]:
|
|
74
78
|
routing_map = {}
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
# Self-loop for nodes that can interrupt (agent input or async)
|
|
81
|
+
if self.action in ('collect_input_with_agent', 'call_async_function'):
|
|
77
82
|
routing_map[self.step_id] = self.step_id
|
|
78
83
|
|
|
79
84
|
for transition in self.transitions:
|
soprano_sdk/tools.py
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
Workflow Tools - Wraps workflows as callable tools for agent frameworks
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
|
+
import json
|
|
5
6
|
import uuid
|
|
6
|
-
from typing import Optional, Dict, Any
|
|
7
|
+
from typing import Optional, Dict, Any, Union
|
|
7
8
|
from .utils.logger import logger
|
|
8
9
|
|
|
9
10
|
from langfuse.langchain import CallbackHandler
|
|
10
11
|
|
|
11
12
|
from .core.engine import load_workflow
|
|
12
|
-
from .core.constants import MFAConfig
|
|
13
|
+
from .core.constants import MFAConfig, InterruptType
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class WorkflowTool:
|
|
@@ -103,35 +104,56 @@ class WorkflowTool:
|
|
|
103
104
|
if not final_state.next and self.checkpointer:
|
|
104
105
|
self.checkpointer.delete_thread(thread_id)
|
|
105
106
|
|
|
106
|
-
# If workflow needs user input, return structured interrupt data
|
|
107
|
+
# If workflow needs user input or async operation, return structured interrupt data
|
|
107
108
|
if "__interrupt__" in result and result["__interrupt__"]:
|
|
109
|
+
interrupt_value = result["__interrupt__"][0].value
|
|
110
|
+
|
|
111
|
+
# Check if this is an async interrupt
|
|
112
|
+
if isinstance(interrupt_value, dict) and interrupt_value.get("type") == "async":
|
|
113
|
+
span.set_attribute("workflow.status", "async_interrupted")
|
|
114
|
+
span.set_attribute("async.step_id", interrupt_value.get("step_id", ""))
|
|
115
|
+
pending_metadata = json.dumps(interrupt_value.get("pending", {}))
|
|
116
|
+
return f"{InterruptType.ASYNC}|{thread_id}|{self.name}|{pending_metadata}"
|
|
117
|
+
|
|
118
|
+
# User input interrupt (existing behavior)
|
|
108
119
|
span.set_attribute("workflow.status", "interrupted")
|
|
109
|
-
prompt =
|
|
110
|
-
return f"
|
|
120
|
+
prompt = interrupt_value
|
|
121
|
+
return f"{InterruptType.USER_INPUT}|{thread_id}|{self.name}|{prompt}"
|
|
111
122
|
|
|
112
123
|
# Workflow completed without interrupting
|
|
113
124
|
span.set_attribute("workflow.status", "completed")
|
|
114
125
|
return self.engine.get_outcome_message(result)
|
|
115
126
|
|
|
116
|
-
def resume(
|
|
117
|
-
|
|
127
|
+
def resume(
|
|
128
|
+
self,
|
|
129
|
+
thread_id: str,
|
|
130
|
+
resume_value: Union[str, Dict[str, Any]]
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Resume an interrupted workflow with user input or async result
|
|
118
133
|
|
|
119
134
|
Args:
|
|
120
135
|
thread_id: Thread ID of the interrupted workflow
|
|
121
|
-
|
|
136
|
+
resume_value: User's response (str) or async operation result (dict)
|
|
122
137
|
|
|
123
138
|
Returns:
|
|
124
|
-
Either another interrupt prompt or final outcome message
|
|
139
|
+
Either another interrupt prompt/async metadata or final outcome message
|
|
125
140
|
"""
|
|
126
141
|
from langgraph.types import Command
|
|
127
142
|
|
|
128
143
|
config = {"configurable": {"thread_id": thread_id}}
|
|
129
|
-
result = self.graph.invoke(Command(resume=
|
|
144
|
+
result = self.graph.invoke(Command(resume=resume_value), config=config)
|
|
130
145
|
|
|
131
|
-
# Check if workflow needs more input
|
|
146
|
+
# Check if workflow needs more input or has another async operation
|
|
132
147
|
if "__interrupt__" in result and result["__interrupt__"]:
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
interrupt_value = result["__interrupt__"][0].value
|
|
149
|
+
|
|
150
|
+
# Check if this is an async interrupt
|
|
151
|
+
if isinstance(interrupt_value, dict) and interrupt_value.get("type") == "async":
|
|
152
|
+
pending_metadata = json.dumps(interrupt_value.get("pending", {}))
|
|
153
|
+
return f"{InterruptType.ASYNC}|{thread_id}|{self.name}|{pending_metadata}"
|
|
154
|
+
|
|
155
|
+
# User input interrupt
|
|
156
|
+
return f"{InterruptType.USER_INPUT}|{thread_id}|{self.name}|{interrupt_value}"
|
|
135
157
|
|
|
136
158
|
# Workflow completed
|
|
137
159
|
return self.engine.get_outcome_message(result)
|
soprano_sdk/validation/schema.py
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
soprano_sdk/__init__.py,sha256=YZVl_SwQ0C-E_5_f1AwUe_hPcbgCt8k7k4_WAHM8vjE,243
|
|
2
2
|
soprano_sdk/engine.py,sha256=EFK91iTHjp72otLN6Kg-yeLye2J3CAKN0QH4FI2taL8,14838
|
|
3
|
-
soprano_sdk/tools.py,sha256=
|
|
3
|
+
soprano_sdk/tools.py,sha256=xsJbY1hZzhXbNRTAVj9M_2saU0oa7J8O9DaRGGuPf30,8832
|
|
4
4
|
soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
|
|
5
5
|
soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU,3214
|
|
6
6
|
soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
|
|
7
7
|
soprano_sdk/agents/structured_output.py,sha256=7DSVzfMPsZAqBwI3v6XL15qG5Gh4jJ-qddcVPaa3gdc,3326
|
|
8
8
|
soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
soprano_sdk/authenticators/mfa.py,sha256=
|
|
9
|
+
soprano_sdk/authenticators/mfa.py,sha256=Zl1dcwCmuwsAbruFMqguJ4lY0PPnC6v2EZ-xTPULX04,6098
|
|
10
10
|
soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
soprano_sdk/core/constants.py,sha256=
|
|
11
|
+
soprano_sdk/core/constants.py,sha256=21ful8skHgDeDJJMdB_oJDw7Xq3lkbO6KWOhHuuLIa0,3330
|
|
12
12
|
soprano_sdk/core/engine.py,sha256=UTFJimrgUzfdDZgc4rW5nvkOKevAEGW8C5Zr1D0tgcs,11270
|
|
13
13
|
soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
|
|
14
14
|
soprano_sdk/core/state.py,sha256=k8ojLfWgjES3p9XWMeGU5s4UK-Xa5T8mS4VtZzTrcDw,2961
|
|
15
15
|
soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
soprano_sdk/nodes/async_function.py,sha256=v6WujLKm8NXX2iAkJ7Gz_QIVCtWFrpC6nnPyyfuCxXs,9354
|
|
16
17
|
soprano_sdk/nodes/base.py,sha256=idFyOGGPnjsASYnrOF_NIh7eFcSuJqw61EoVN_WCTaU,2360
|
|
17
|
-
soprano_sdk/nodes/call_function.py,sha256=
|
|
18
|
+
soprano_sdk/nodes/call_function.py,sha256=afYBmj5Aditbkvb_7gD3CsXBEEUohcsC1_cdHfcOunE,5847
|
|
18
19
|
soprano_sdk/nodes/collect_input.py,sha256=lEltZOU5ALvc57q8I_4SjzzEapVejy9mS0E73Jf7-sk,23759
|
|
19
|
-
soprano_sdk/nodes/factory.py,sha256=
|
|
20
|
+
soprano_sdk/nodes/factory.py,sha256=IbBzT4FKBnYw5PuSo7uDONV3HSFtoyqjBQQtXtUY2IY,1756
|
|
20
21
|
soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
soprano_sdk/routing/router.py,sha256=
|
|
22
|
+
soprano_sdk/routing/router.py,sha256=Z218r4BMbmlL9282ombutAoKsIs1WHZ2d5YHnbCeet8,3698
|
|
22
23
|
soprano_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
soprano_sdk/utils/function.py,sha256=yqkY4MlHOenv-Q3NciiovK1lamyrGQljpy6Q41wviy8,1216
|
|
24
25
|
soprano_sdk/utils/logger.py,sha256=hMYaNHt5syGOXRkglTUKzkgfSbWerix_pHQntcYyep8,157
|
|
@@ -26,9 +27,9 @@ soprano_sdk/utils/template.py,sha256=MG_B9TMx1ShpnSGo7s7TO-VfQzuFByuRNhJTvZ668kM
|
|
|
26
27
|
soprano_sdk/utils/tool.py,sha256=hWN826HIKmLdswLCTURLH8hWlb2WU0MB8nIUErbpB-8,1877
|
|
27
28
|
soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,1991
|
|
28
29
|
soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
|
|
29
|
-
soprano_sdk/validation/schema.py,sha256=
|
|
30
|
+
soprano_sdk/validation/schema.py,sha256=SlC4sq-ueEg0p_8Uox_cgPj9S-0AEEiOOlA1Vsu0DsE,15443
|
|
30
31
|
soprano_sdk/validation/validator.py,sha256=GaCvHvjwVe88Z8yatQsueiPnqtq1oo5uN75gogzpQT0,8940
|
|
31
|
-
soprano_sdk-0.2.
|
|
32
|
-
soprano_sdk-0.2.
|
|
33
|
-
soprano_sdk-0.2.
|
|
34
|
-
soprano_sdk-0.2.
|
|
32
|
+
soprano_sdk-0.2.6.dist-info/METADATA,sha256=Ykmgc9OD9Q5nBvGSD-z67oPU7Cy4jynpvhjZkwdPxzQ,11297
|
|
33
|
+
soprano_sdk-0.2.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
soprano_sdk-0.2.6.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
|
|
35
|
+
soprano_sdk-0.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|