soprano-sdk 0.2.7__tar.gz → 0.2.9__tar.gz
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-0.2.7 → soprano_sdk-0.2.9}/PKG-INFO +1 -1
- soprano_sdk-0.2.9/examples/ASYNC_FUNCTIONS_README.md +414 -0
- soprano_sdk-0.2.9/examples/payment_async_functions.py +383 -0
- soprano_sdk-0.2.9/examples/payment_async_workflow.yaml +107 -0
- soprano_sdk-0.2.9/examples/test_payment_async.py +460 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/pyproject.toml +1 -1
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/collect_input.py +10 -1
- soprano_sdk-0.2.9/tests/test_collect_input_refactor.py +184 -0
- soprano_sdk-0.2.7/tests/test_collect_input_refactor.py +0 -68
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/.github/workflows/test_build_and_publish.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/.gitignore +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/.python-version +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/CLAUDE.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/LICENSE +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/README.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/concert_booking/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/concert_booking/booking_helpers.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/framework_example.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/greeting_functions.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/greeting_workflow.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/main.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/persistence/README.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/persistence/conversation_based.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/persistence/entity_based.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/persistence/mongodb_demo.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/return_functions.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/return_workflow.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/structured_output_example.yaml +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/README.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/crewai_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/tools/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/tools/crewai_tools.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/tools/langgraph_tools.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/supervisors/workflow_tools.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/tools/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/tools/address.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/examples/validator.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/langgraph_demo.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/langgraph_selfloop_demo.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/langgraph_v.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/main.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/return_fsm.excalidraw +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/return_state_machine.png +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/legacy/ui.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/scripts/visualize_workflow.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/scripts/workflow_demo.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/scripts/workflow_demo_ui.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/agents/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/agents/adaptor.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/agents/factory.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/agents/structured_output.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/authenticators/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/authenticators/mfa.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/core/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/core/constants.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/core/engine.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/core/rollback_strategies.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/core/state.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/engine.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/async_function.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/base.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/call_function.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/nodes/factory.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/routing/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/routing/router.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/tools.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/function.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/logger.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/template.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/tool.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/utils/tracing.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/validation/__init__.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/validation/schema.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/soprano_sdk/validation/validator.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/debug_jinja2.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_agent_factory.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_async_function.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_external_values.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_inputs_validation.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_jinja2_path.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_jinja2_standalone.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_mfa_scenarios.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_persistence.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_structured_output.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/tests/test_transition_routing.py +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/todo.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/uv.lock +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/.eslintrc.cjs +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/.gitignore +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/README.md +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/index.html +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/package-lock.json +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/package.json +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/App.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/CustomNode.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/assets/react.svg +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/src/main.jsx +0 -0
- {soprano_sdk-0.2.7 → soprano_sdk-0.2.9}/workflow-visualizer/vite.config.js +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Async Function Examples - Complete Guide
|
|
2
|
+
|
|
3
|
+
This directory contains complete examples of how to use `call_async_function` with the interrupt/resume pattern in Soprano SDK workflows.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- **`payment_async_workflow.yaml`** - Complete workflow YAML demonstrating async functions
|
|
8
|
+
- **`payment_async_functions.py`** - Python implementation of async functions
|
|
9
|
+
- **`test_payment_async.py`** - Comprehensive test examples
|
|
10
|
+
- **`ASYNC_FUNCTIONS_README.md`** - This file
|
|
11
|
+
|
|
12
|
+
## What is an Async Function?
|
|
13
|
+
|
|
14
|
+
An async function allows your workflow to pause while waiting for an external system to complete processing. This is useful for:
|
|
15
|
+
|
|
16
|
+
- **Payment verification** - Wait for payment gateway to verify
|
|
17
|
+
- **Identity verification** - Wait for KYC/identity checks
|
|
18
|
+
- **Background jobs** - Wait for long-running computations
|
|
19
|
+
- **External APIs** - Wait for third-party service responses
|
|
20
|
+
- **Manual approvals** - Wait for human review
|
|
21
|
+
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
### Two-Phase Execution Pattern
|
|
25
|
+
|
|
26
|
+
#### Phase 1: Initial Call (Interrupt)
|
|
27
|
+
```python
|
|
28
|
+
def verify_payment(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
29
|
+
# Initiate async operation
|
|
30
|
+
job_id = start_external_verification(state["payment_amount"])
|
|
31
|
+
|
|
32
|
+
# Return "pending" to pause workflow
|
|
33
|
+
return {
|
|
34
|
+
"status": "pending",
|
|
35
|
+
"job_id": job_id,
|
|
36
|
+
"webhook_url": f"https://api.example.com/webhook/{job_id}",
|
|
37
|
+
"estimated_time": "5-10 seconds"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When you return `{"status": "pending", ...}`:
|
|
42
|
+
1. Workflow calls `interrupt()` with the pending metadata
|
|
43
|
+
2. Workflow pauses execution
|
|
44
|
+
3. External system processes asynchronously
|
|
45
|
+
4. Workflow state is persisted
|
|
46
|
+
|
|
47
|
+
#### Phase 2: Resume (Complete)
|
|
48
|
+
```python
|
|
49
|
+
# External system completes and calls your webhook
|
|
50
|
+
@app.post("/webhook/payment/{job_id}")
|
|
51
|
+
async def payment_webhook(job_id: str, result: Dict):
|
|
52
|
+
# Get workflow config for this job
|
|
53
|
+
config = get_workflow_config(job_id)
|
|
54
|
+
|
|
55
|
+
# Resume workflow with result
|
|
56
|
+
graph.update_state(config, Command(resume=result))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When you resume with `Command(resume=result)`:
|
|
60
|
+
1. The `interrupt()` call returns with `result`
|
|
61
|
+
2. Result is stored in the `output` field
|
|
62
|
+
3. Workflow evaluates `transitions` based on result
|
|
63
|
+
4. Workflow continues to next step
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
### 1. Define in Workflow YAML
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
steps:
|
|
71
|
+
- id: verify_payment
|
|
72
|
+
action: call_async_function
|
|
73
|
+
function: "payment_async_functions.verify_payment"
|
|
74
|
+
output: verification_result
|
|
75
|
+
transitions:
|
|
76
|
+
- condition: "verified"
|
|
77
|
+
ref: "status"
|
|
78
|
+
next: approved
|
|
79
|
+
- condition: "rejected"
|
|
80
|
+
ref: "status"
|
|
81
|
+
next: rejected
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Implement Async Function
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
def verify_payment(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Returns pending status to pause workflow.
|
|
90
|
+
External system will later resume with final result.
|
|
91
|
+
"""
|
|
92
|
+
payment_amount = state.get("payment_amount")
|
|
93
|
+
job_id = f"pay_{uuid.uuid4().hex[:8]}"
|
|
94
|
+
|
|
95
|
+
# Call external API to start verification
|
|
96
|
+
payment_gateway.verify_async(
|
|
97
|
+
amount=payment_amount,
|
|
98
|
+
callback_url=f"https://your-api.com/webhook/{job_id}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Return pending to interrupt workflow
|
|
102
|
+
return {
|
|
103
|
+
"status": "pending",
|
|
104
|
+
"job_id": job_id,
|
|
105
|
+
"webhook_url": f"https://your-api.com/webhook/{job_id}"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. Create Webhook Handler
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from langgraph.types import Command
|
|
113
|
+
|
|
114
|
+
@app.post("/webhook/payment/{job_id}")
|
|
115
|
+
async def payment_webhook(job_id: str, result: Dict):
|
|
116
|
+
"""
|
|
117
|
+
Receives callback from external payment gateway.
|
|
118
|
+
Resumes the workflow with the result.
|
|
119
|
+
"""
|
|
120
|
+
# Retrieve workflow config from database
|
|
121
|
+
config = db.get_workflow_config(job_id)
|
|
122
|
+
|
|
123
|
+
# Resume workflow with result
|
|
124
|
+
# The result will be:
|
|
125
|
+
# 1. Returned by interrupt()
|
|
126
|
+
# 2. Stored in the output field
|
|
127
|
+
# 3. Used for transition routing
|
|
128
|
+
graph.update_state(config, Command(resume=result))
|
|
129
|
+
|
|
130
|
+
return {"status": "resumed"}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Checking Pending Status
|
|
134
|
+
|
|
135
|
+
### Method 1: Check Function Return Value
|
|
136
|
+
```python
|
|
137
|
+
result = verify_payment(state)
|
|
138
|
+
|
|
139
|
+
# Check if function wants to pause
|
|
140
|
+
if result.get("status") == "pending":
|
|
141
|
+
print("Function is pending")
|
|
142
|
+
print(f"Job ID: {result['job_id']}")
|
|
143
|
+
print(f"Webhook: {result['webhook_url']}")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Method 2: Check Workflow State
|
|
147
|
+
```python
|
|
148
|
+
# After workflow execution
|
|
149
|
+
if state[WorkflowKeys.STATUS] == f"{step_id}_pending":
|
|
150
|
+
print("Workflow is pending")
|
|
151
|
+
|
|
152
|
+
# Or use the strategy method
|
|
153
|
+
if strategy._is_async_pending(state):
|
|
154
|
+
print("Async operation is pending")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Method 3: Check Stored Pending Metadata
|
|
158
|
+
```python
|
|
159
|
+
# Pending metadata is stored in state
|
|
160
|
+
pending_key = f"_async_pending_{step_id}"
|
|
161
|
+
if pending_key in state:
|
|
162
|
+
metadata = state[pending_key]
|
|
163
|
+
print(f"Pending: {metadata}")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Accessing Interrupt and Resume Data
|
|
167
|
+
|
|
168
|
+
### Accessing the Three Interrupt Values
|
|
169
|
+
|
|
170
|
+
When `interrupt()` is called, it receives:
|
|
171
|
+
```python
|
|
172
|
+
{
|
|
173
|
+
"type": "async", # Always "async" for async functions
|
|
174
|
+
"step_id": "verify_payment", # The step ID
|
|
175
|
+
"pending": { # Metadata from your function
|
|
176
|
+
"status": "pending",
|
|
177
|
+
"job_id": "pay_123",
|
|
178
|
+
"webhook_url": "...",
|
|
179
|
+
# ... any other data you returned
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Access these values:
|
|
185
|
+
```python
|
|
186
|
+
# During workflow execution
|
|
187
|
+
with patch('soprano_sdk.nodes.async_function.interrupt') as mock_interrupt:
|
|
188
|
+
strategy.execute(state)
|
|
189
|
+
|
|
190
|
+
interrupt_args = mock_interrupt.call_args[0][0]
|
|
191
|
+
|
|
192
|
+
print(interrupt_args["type"]) # "async"
|
|
193
|
+
print(interrupt_args["step_id"]) # "verify_payment"
|
|
194
|
+
print(interrupt_args["pending"]) # Your pending metadata
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Accessing Resume Data
|
|
198
|
+
|
|
199
|
+
When you resume with `Command(resume=result)`, the result is:
|
|
200
|
+
1. Returned by the `interrupt()` call
|
|
201
|
+
2. Stored in the `output` field
|
|
202
|
+
3. Used for transition routing
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# Resume with this data
|
|
206
|
+
async_result = {
|
|
207
|
+
"job_id": "pay_123",
|
|
208
|
+
"status": "verified",
|
|
209
|
+
"verification_id": "VRF_789",
|
|
210
|
+
"amount_verified": 100.00,
|
|
211
|
+
"verified_at": "2026-01-12T10:30:00Z"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
graph.update_state(config, Command(resume=async_result))
|
|
215
|
+
|
|
216
|
+
# After resume, access via output field
|
|
217
|
+
verification_result = state["verification_result"]
|
|
218
|
+
print(verification_result["status"]) # "verified"
|
|
219
|
+
print(verification_result["verification_id"]) # "VRF_789"
|
|
220
|
+
print(verification_result["amount_verified"]) # 100.00
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Accessing Nested Resume Data with 'ref'
|
|
224
|
+
|
|
225
|
+
Use `ref` in transitions to check nested fields:
|
|
226
|
+
```yaml
|
|
227
|
+
transitions:
|
|
228
|
+
- condition: "approved"
|
|
229
|
+
ref: "result.status" # Checks nested field
|
|
230
|
+
next: success
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Access nested data:
|
|
234
|
+
```python
|
|
235
|
+
async_result = {
|
|
236
|
+
"result": {
|
|
237
|
+
"status": "approved",
|
|
238
|
+
"verification": {
|
|
239
|
+
"score": 95,
|
|
240
|
+
"details": "All checks passed"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# After resume
|
|
246
|
+
result = state["verification_data"]
|
|
247
|
+
print(result["result"]["status"]) # "approved"
|
|
248
|
+
print(result["result"]["verification"]["score"]) # 95
|
|
249
|
+
print(result["result"]["verification"]["details"]) # "All checks passed"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Complete Example: Two-Phase Execution
|
|
253
|
+
|
|
254
|
+
### Phase 1: Initial Call
|
|
255
|
+
```python
|
|
256
|
+
# Execute workflow
|
|
257
|
+
state = {"payment_amount": 100.00}
|
|
258
|
+
result = workflow.execute(state)
|
|
259
|
+
|
|
260
|
+
# Workflow interrupts with pending
|
|
261
|
+
print(state[WorkflowKeys.STATUS]) # "verify_payment_pending"
|
|
262
|
+
print(state["_async_pending_verify_payment"]) # Pending metadata
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### External Processing
|
|
266
|
+
```python
|
|
267
|
+
# External payment gateway processes
|
|
268
|
+
# (This happens outside your workflow)
|
|
269
|
+
time.sleep(5) # Simulate processing
|
|
270
|
+
payment_gateway.verify(job_id="pay_123")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Phase 2: Resume
|
|
274
|
+
```python
|
|
275
|
+
# Payment gateway calls your webhook
|
|
276
|
+
result = {
|
|
277
|
+
"status": "verified",
|
|
278
|
+
"verification_id": "VRF_789",
|
|
279
|
+
"amount_verified": 100.00
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Resume workflow
|
|
283
|
+
graph.update_state(config, Command(resume=result))
|
|
284
|
+
|
|
285
|
+
# Workflow completes
|
|
286
|
+
print(state[WorkflowKeys.STATUS]) # "verify_payment_approved"
|
|
287
|
+
print(state["verification_result"]) # Full result data
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Synchronous Completion (No Pending)
|
|
291
|
+
|
|
292
|
+
If your function can complete immediately, just return the result directly:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
def verify_payment_sync(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
296
|
+
"""Completes immediately without pending."""
|
|
297
|
+
return {
|
|
298
|
+
"status": "verified",
|
|
299
|
+
"verification_id": "VRF_INSTANT",
|
|
300
|
+
"amount_verified": state["payment_amount"]
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
When you DON'T return `{"status": "pending"}`:
|
|
305
|
+
- No workflow interrupt
|
|
306
|
+
- Result immediately available
|
|
307
|
+
- Transitions evaluated right away
|
|
308
|
+
- Workflow continues to next step
|
|
309
|
+
|
|
310
|
+
## Running the Examples
|
|
311
|
+
|
|
312
|
+
### 1. View Function Implementation
|
|
313
|
+
```bash
|
|
314
|
+
cat examples/payment_async_functions.py
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 2. Run Demo Script
|
|
318
|
+
```bash
|
|
319
|
+
python3 examples/payment_async_functions.py
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### 3. Run Tests (requires dependencies)
|
|
323
|
+
```bash
|
|
324
|
+
cd ..
|
|
325
|
+
python -m pytest tests/test_async_function.py -v
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 4. Run Test Examples
|
|
329
|
+
```bash
|
|
330
|
+
python3 examples/test_payment_async.py
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Key Concepts Summary
|
|
334
|
+
|
|
335
|
+
| Concept | Description |
|
|
336
|
+
|---------|-------------|
|
|
337
|
+
| **Pending Status** | Return `{"status": "pending", ...}` to pause workflow |
|
|
338
|
+
| **Interrupt** | Workflow calls `interrupt()` with pending metadata |
|
|
339
|
+
| **Resume** | Call `Command(resume=result)` to continue workflow |
|
|
340
|
+
| **Output Field** | Resume result is stored in the configured output field |
|
|
341
|
+
| **Transitions** | Use result fields to route to next step |
|
|
342
|
+
| **State Keys** | Pending data stored at `_async_pending_{step_id}` |
|
|
343
|
+
| **Status Key** | Workflow status becomes `{step_id}_pending` |
|
|
344
|
+
|
|
345
|
+
## Common Patterns
|
|
346
|
+
|
|
347
|
+
### Pattern 1: External API Call
|
|
348
|
+
```python
|
|
349
|
+
def call_external_api(state):
|
|
350
|
+
job_id = external_api.start_job(state["data"])
|
|
351
|
+
return {"status": "pending", "job_id": job_id}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Pattern 2: Manual Review
|
|
355
|
+
```python
|
|
356
|
+
def request_manual_review(state):
|
|
357
|
+
review_id = create_review_task(state)
|
|
358
|
+
return {
|
|
359
|
+
"status": "pending",
|
|
360
|
+
"review_id": review_id,
|
|
361
|
+
"webhook_url": f"/webhook/review/{review_id}"
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Pattern 3: Polling (Not Recommended)
|
|
366
|
+
```python
|
|
367
|
+
# DON'T DO THIS - Use webhooks instead
|
|
368
|
+
def check_status(state):
|
|
369
|
+
if job_complete(state["job_id"]):
|
|
370
|
+
return {"status": "complete"}
|
|
371
|
+
return {"status": "pending"} # Will keep checking
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Best Practices
|
|
375
|
+
|
|
376
|
+
1. **Always use webhooks** - Don't poll for completion
|
|
377
|
+
2. **Include job_id** - Track operations across systems
|
|
378
|
+
3. **Store webhook URL** - Document where to send results
|
|
379
|
+
4. **Add timeout handling** - What if webhook never arrives?
|
|
380
|
+
5. **Include error cases** - Handle failures in transitions
|
|
381
|
+
6. **Log everything** - Track async operation lifecycle
|
|
382
|
+
7. **Persist state** - Use workflow persistence for reliability
|
|
383
|
+
|
|
384
|
+
## Troubleshooting
|
|
385
|
+
|
|
386
|
+
### Workflow Not Resuming
|
|
387
|
+
- Verify webhook URL is correct
|
|
388
|
+
- Check external system is calling webhook
|
|
389
|
+
- Confirm `Command(resume=result)` is being called
|
|
390
|
+
- Verify config matches original workflow
|
|
391
|
+
|
|
392
|
+
### Wrong Transition
|
|
393
|
+
- Check `ref` field matches result structure
|
|
394
|
+
- Verify `condition` matches exact value
|
|
395
|
+
- Use nested refs for complex data: `ref: "result.status"`
|
|
396
|
+
|
|
397
|
+
### Missing Data
|
|
398
|
+
- Confirm resume result includes all needed fields
|
|
399
|
+
- Check output field name matches workflow YAML
|
|
400
|
+
- Verify data structure matches expectations
|
|
401
|
+
|
|
402
|
+
## Additional Resources
|
|
403
|
+
|
|
404
|
+
- Main async function implementation: `soprano_sdk/nodes/async_function.py`
|
|
405
|
+
- Workflow engine: `soprano_sdk/core/engine.py`
|
|
406
|
+
- Test suite: `tests/test_async_function.py`
|
|
407
|
+
- LangGraph Command docs: https://langchain-ai.github.io/langgraph/
|
|
408
|
+
|
|
409
|
+
## Need Help?
|
|
410
|
+
|
|
411
|
+
Check the test files for comprehensive examples:
|
|
412
|
+
- `tests/test_async_function.py` - Unit tests
|
|
413
|
+
- `examples/test_payment_async.py` - Practical examples
|
|
414
|
+
- `examples/payment_async_functions.py` - Implementation patterns
|