soprano-sdk 0.2.7__tar.gz → 0.2.8__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.
Files changed (106) hide show
  1. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/PKG-INFO +1 -1
  2. soprano_sdk-0.2.8/examples/ASYNC_FUNCTIONS_README.md +414 -0
  3. soprano_sdk-0.2.8/examples/payment_async_functions.py +383 -0
  4. soprano_sdk-0.2.8/examples/payment_async_workflow.yaml +107 -0
  5. soprano_sdk-0.2.8/examples/test_payment_async.py +460 -0
  6. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/pyproject.toml +1 -1
  7. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/collect_input.py +17 -1
  8. soprano_sdk-0.2.8/tests/test_collect_input_refactor.py +184 -0
  9. soprano_sdk-0.2.7/tests/test_collect_input_refactor.py +0 -68
  10. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/.github/workflows/test_build_and_publish.yaml +0 -0
  11. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/.gitignore +0 -0
  12. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/.python-version +0 -0
  13. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/CLAUDE.md +0 -0
  14. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/LICENSE +0 -0
  15. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/README.md +0 -0
  16. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/concert_booking/__init__.py +0 -0
  17. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/concert_booking/booking_helpers.py +0 -0
  18. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
  19. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/framework_example.yaml +0 -0
  20. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/greeting_functions.py +0 -0
  21. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/greeting_workflow.yaml +0 -0
  22. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/main.py +0 -0
  23. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/persistence/README.md +0 -0
  24. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/persistence/conversation_based.py +0 -0
  25. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/persistence/entity_based.py +0 -0
  26. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/persistence/mongodb_demo.py +0 -0
  27. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/return_functions.py +0 -0
  28. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/return_workflow.yaml +0 -0
  29. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/structured_output_example.yaml +0 -0
  30. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/README.md +0 -0
  31. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  32. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  33. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/tools/__init__.py +0 -0
  34. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/tools/crewai_tools.py +0 -0
  35. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/tools/langgraph_tools.py +0 -0
  36. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/supervisors/workflow_tools.py +0 -0
  37. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/tools/__init__.py +0 -0
  38. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/tools/address.py +0 -0
  39. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/examples/validator.py +0 -0
  40. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/langgraph_demo.py +0 -0
  41. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/langgraph_selfloop_demo.py +0 -0
  42. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/langgraph_v.py +0 -0
  43. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/main.py +0 -0
  44. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/return_fsm.excalidraw +0 -0
  45. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/return_state_machine.png +0 -0
  46. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/legacy/ui.py +0 -0
  47. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/scripts/visualize_workflow.py +0 -0
  48. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/scripts/workflow_demo.py +0 -0
  49. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/scripts/workflow_demo_ui.py +0 -0
  50. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/__init__.py +0 -0
  51. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/agents/__init__.py +0 -0
  52. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/agents/adaptor.py +0 -0
  53. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/agents/factory.py +0 -0
  54. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/agents/structured_output.py +0 -0
  55. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/authenticators/__init__.py +0 -0
  56. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/authenticators/mfa.py +0 -0
  57. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/core/__init__.py +0 -0
  58. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/core/constants.py +0 -0
  59. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/core/engine.py +0 -0
  60. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/core/rollback_strategies.py +0 -0
  61. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/core/state.py +0 -0
  62. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/engine.py +0 -0
  63. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/__init__.py +0 -0
  64. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/async_function.py +0 -0
  65. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/base.py +0 -0
  66. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/call_function.py +0 -0
  67. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/nodes/factory.py +0 -0
  68. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/routing/__init__.py +0 -0
  69. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/routing/router.py +0 -0
  70. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/tools.py +0 -0
  71. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/__init__.py +0 -0
  72. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/function.py +0 -0
  73. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/logger.py +0 -0
  74. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/template.py +0 -0
  75. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/tool.py +0 -0
  76. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/utils/tracing.py +0 -0
  77. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/validation/__init__.py +0 -0
  78. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/validation/schema.py +0 -0
  79. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/soprano_sdk/validation/validator.py +0 -0
  80. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/debug_jinja2.py +0 -0
  81. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_agent_factory.py +0 -0
  82. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_async_function.py +0 -0
  83. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_external_values.py +0 -0
  84. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_inputs_validation.py +0 -0
  85. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_jinja2_path.py +0 -0
  86. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_jinja2_standalone.py +0 -0
  87. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_mfa_scenarios.py +0 -0
  88. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_persistence.py +0 -0
  89. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_structured_output.py +0 -0
  90. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/tests/test_transition_routing.py +0 -0
  91. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/todo.md +0 -0
  92. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/uv.lock +0 -0
  93. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/.eslintrc.cjs +0 -0
  94. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/.gitignore +0 -0
  95. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/README.md +0 -0
  96. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/index.html +0 -0
  97. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/package-lock.json +0 -0
  98. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/package.json +0 -0
  99. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/App.jsx +0 -0
  100. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/CustomNode.jsx +0 -0
  101. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  102. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  103. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  104. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/assets/react.svg +0 -0
  105. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/src/main.jsx +0 -0
  106. {soprano_sdk-0.2.7 → soprano_sdk-0.2.8}/workflow-visualizer/vite.config.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soprano-sdk
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -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