soprano-sdk 0.2.19__tar.gz → 0.2.20__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 (115) hide show
  1. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/PKG-INFO +108 -1
  2. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/README.md +107 -0
  3. soprano_sdk-0.2.20/docs/framework_flow_diagrams.md +430 -0
  4. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/pyproject.toml +1 -1
  5. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/agents/structured_output.py +5 -1
  6. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/core/constants.py +2 -0
  7. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/core/engine.py +1 -0
  8. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/core/state.py +2 -2
  9. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/base.py +6 -1
  10. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/collect_input.py +80 -7
  11. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/factory.py +2 -0
  12. soprano_sdk-0.2.20/soprano_sdk/nodes/follow_up.py +346 -0
  13. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/routing/router.py +6 -2
  14. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/tools.py +18 -0
  15. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/validation/schema.py +4 -0
  16. soprano_sdk-0.2.20/tests/test_base_node.py +232 -0
  17. soprano_sdk-0.2.20/tests/test_engine_failure_message.py +335 -0
  18. soprano_sdk-0.2.20/tests/test_follow_up.py +473 -0
  19. soprano_sdk-0.2.20/tests/test_out_of_scope.py +529 -0
  20. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/.claude/settings.local.json +0 -0
  21. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/.github/workflows/test_build_and_publish.yaml +0 -0
  22. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/.gitignore +0 -0
  23. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/.python-version +0 -0
  24. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/CLAUDE.md +0 -0
  25. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/LICENSE +0 -0
  26. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/__init__.py +0 -0
  27. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/booking_helpers.py +0 -0
  28. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
  29. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/concert_tools.py +0 -0
  30. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/example_runner.py +0 -0
  31. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/concert_booking/tool.py +0 -0
  32. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/framework_example.yaml +0 -0
  33. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/greeting_functions.py +0 -0
  34. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/greeting_workflow.yaml +0 -0
  35. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/main.py +0 -0
  36. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/payment_async_functions.py +0 -0
  37. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/payment_async_workflow.yaml +0 -0
  38. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/persistence/README.md +0 -0
  39. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/persistence/conversation_based.py +0 -0
  40. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/persistence/entity_based.py +0 -0
  41. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/persistence/mongodb_demo.py +0 -0
  42. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/return_functions.py +0 -0
  43. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/return_workflow.yaml +0 -0
  44. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/structured_output_example.yaml +0 -0
  45. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/README.md +0 -0
  46. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  47. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  48. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/tools/__init__.py +0 -0
  49. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/tools/crewai_tools.py +0 -0
  50. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/tools/langgraph_tools.py +0 -0
  51. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/supervisors/workflow_tools.py +0 -0
  52. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/test_payment_async.py +0 -0
  53. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/tools/__init__.py +0 -0
  54. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/tools/address.py +0 -0
  55. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/examples/validator.py +0 -0
  56. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/langgraph_demo.py +0 -0
  57. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/langgraph_selfloop_demo.py +0 -0
  58. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/langgraph_v.py +0 -0
  59. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/main.py +0 -0
  60. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/return_fsm.excalidraw +0 -0
  61. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/return_state_machine.png +0 -0
  62. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/legacy/ui.py +0 -0
  63. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/scripts/visualize_workflow.py +0 -0
  64. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/scripts/workflow_demo.py +0 -0
  65. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/scripts/workflow_demo_ui.py +0 -0
  66. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/__init__.py +0 -0
  67. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/agents/__init__.py +0 -0
  68. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/agents/adaptor.py +0 -0
  69. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/agents/factory.py +0 -0
  70. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/authenticators/__init__.py +0 -0
  71. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/authenticators/mfa.py +0 -0
  72. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/core/__init__.py +0 -0
  73. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/core/rollback_strategies.py +0 -0
  74. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/engine.py +0 -0
  75. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/__init__.py +0 -0
  76. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/async_function.py +0 -0
  77. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/nodes/call_function.py +0 -0
  78. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/routing/__init__.py +0 -0
  79. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/__init__.py +0 -0
  80. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/function.py +0 -0
  81. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/logger.py +0 -0
  82. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/template.py +0 -0
  83. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/tool.py +0 -0
  84. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/utils/tracing.py +0 -0
  85. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/validation/__init__.py +0 -0
  86. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/soprano_sdk/validation/validator.py +0 -0
  87. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/debug_jinja2.py +0 -0
  88. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_adaptor_logging.py +0 -0
  89. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_agent_factory.py +0 -0
  90. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_async_function.py +0 -0
  91. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_collect_input_refactor.py +0 -0
  92. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_external_values.py +0 -0
  93. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_inputs_validation.py +0 -0
  94. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_jinja2_path.py +0 -0
  95. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_jinja2_standalone.py +0 -0
  96. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_mfa_scenarios.py +0 -0
  97. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_persistence.py +0 -0
  98. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_structured_output.py +0 -0
  99. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_transition_routing.py +0 -0
  100. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/tests/test_workflow_tool_context_update.py +0 -0
  101. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/todo.md +0 -0
  102. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/.eslintrc.cjs +0 -0
  103. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/.gitignore +0 -0
  104. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/README.md +0 -0
  105. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/index.html +0 -0
  106. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/package-lock.json +0 -0
  107. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/package.json +0 -0
  108. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/App.jsx +0 -0
  109. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/CustomNode.jsx +0 -0
  110. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  111. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  112. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  113. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/assets/react.svg +0 -0
  114. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/workflow-visualizer/src/main.jsx +0 -0
  115. {soprano_sdk-0.2.19 → soprano_sdk-0.2.20}/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.19
3
+ Version: 0.2.20
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -51,6 +51,9 @@ A YAML-driven workflow engine with AI agent integration for building conversatio
51
51
  - **External Context Injection**: Support for pre-populated fields from external orchestrators
52
52
  - **Pattern Matching**: Flexible transition logic based on patterns and conditions
53
53
  - **Visualization**: Generate workflow graphs as images or Mermaid diagrams
54
+ - **Follow-up Conversations**: Handle user follow-up questions with full workflow context
55
+ - **Intent Detection**: Route users between collector nodes based on detected intent
56
+ - **Out-of-Scope Detection**: Signal when user queries are unrelated to the current workflow
54
57
 
55
58
  ## Installation
56
59
 
@@ -268,6 +271,108 @@ Calls a Python function with workflow state.
268
271
  next: failure_step
269
272
  ```
270
273
 
274
+ ### call_async_function
275
+
276
+ Calls an async function that may return a pending status, triggering an interrupt until the async operation completes.
277
+
278
+ ```yaml
279
+ - id: verify_payment
280
+ action: call_async_function
281
+ function: "payments.start_verification"
282
+ output: verification_result
283
+ transitions:
284
+ - condition: "verified"
285
+ next: payment_approved
286
+ - condition: "failed"
287
+ next: payment_rejected
288
+ ```
289
+
290
+ ### follow_up
291
+
292
+ Handles follow-up questions from users. Unlike `collect_input_with_agent` where the agent asks first, here the **user initiates** by asking questions. The agent responds using full workflow context.
293
+
294
+ ```yaml
295
+ - id: handle_questions
296
+ action: follow_up
297
+ next: final_confirmation # Where to go when user says "done"
298
+ closure_patterns: # Optional: customize closure detection
299
+ - "ok"
300
+ - "thank you"
301
+ - "done"
302
+ agent:
303
+ name: "FollowUpAssistant"
304
+ model: "gpt-4o-mini"
305
+ description: "Answering questions about the order"
306
+ instructions: |
307
+ Help the user with any questions about their order.
308
+ Be concise and helpful.
309
+ detect_out_of_scope: true # Signal when user asks unrelated questions
310
+ transitions: # Optional: route based on patterns
311
+ - pattern: "ROUTE_TO_PAYMENT:"
312
+ next: payment_step
313
+ ```
314
+
315
+ **Key features:**
316
+ - **User initiates**: No initial prompt - waits for user to ask a question
317
+ - **Full state context**: Agent sees all collected workflow data
318
+ - **Closure detection**: Detects "ok", "thanks", "done" → proceeds to next step
319
+ - **Intent change**: Routes to collector nodes when user wants to change data
320
+ - **Out-of-scope**: Signals to parent orchestrator for unrelated queries
321
+
322
+ ## Interrupt Types
323
+
324
+ The workflow engine uses three interrupt types to pause execution and communicate with the caller:
325
+
326
+ | Type | Marker | Triggered By | Use Case |
327
+ |------|--------|--------------|----------|
328
+ | **USER_INPUT** | `__WORKFLOW_INTERRUPT__` | `collect_input_with_agent`, `follow_up` | Waiting for user input |
329
+ | **ASYNC** | `__ASYNC_INTERRUPT__` | `call_async_function` | Waiting for async operation callback |
330
+ | **OUT_OF_SCOPE** | `__OUT_OF_SCOPE_INTERRUPT__` | `collect_input_with_agent`, `follow_up` | User query unrelated to current task |
331
+
332
+ ### Handling Interrupts
333
+
334
+ ```python
335
+ result = graph.invoke({}, config=config)
336
+
337
+ if "__interrupt__" in result and result["__interrupt__"]:
338
+ interrupt_value = result["__interrupt__"][0].value
339
+
340
+ # Check interrupt type
341
+ if isinstance(interrupt_value, dict):
342
+ if interrupt_value.get("type") == "async":
343
+ # Async interrupt - wait for external callback
344
+ pending_metadata = interrupt_value.get("pending")
345
+ # ... handle async operation ...
346
+ result = graph.invoke(Command(resume=async_result), config=config)
347
+
348
+ elif interrupt_value.get("type") == "out_of_scope":
349
+ # Out-of-scope - user asking unrelated question
350
+ reason = interrupt_value.get("reason")
351
+ user_message = interrupt_value.get("user_message")
352
+ # ... route to different workflow or handle appropriately ...
353
+ else:
354
+ # User input interrupt - prompt is a string
355
+ prompt = interrupt_value
356
+ user_input = input(f"Bot: {prompt}\nYou: ")
357
+ result = graph.invoke(Command(resume=user_input), config=config)
358
+ ```
359
+
360
+ ### Out-of-Scope Detection
361
+
362
+ Data collector and follow-up nodes can detect when user queries are unrelated to the current task. This is useful for multi-workflow systems where a supervisor agent needs to route users to different SOPs.
363
+
364
+ **Configuration:**
365
+ ```yaml
366
+ agent:
367
+ detect_out_of_scope: true # Enabled by default
368
+ scope_description: "collecting order information for returns" # Optional
369
+ ```
370
+
371
+ **Response format:**
372
+ ```
373
+ __OUT_OF_SCOPE_INTERRUPT__|{thread_id}|{workflow_name}|{"reason":"...","user_message":"..."}
374
+ ```
375
+
271
376
  ## Examples
272
377
 
273
378
  See the `examples/` directory for complete workflow examples:
@@ -411,6 +516,8 @@ Contributions are welcome! Please open an issue or submit a pull request.
411
516
  - ✅ Database persistence (SqliteSaver, PostgresSaver supported)
412
517
  - ✅ Pluggable checkpointer system
413
518
  - ✅ Thread ID strategies and examples
519
+ - ✅ Follow-up node for conversational Q&A
520
+ - ✅ Out-of-scope detection for multi-workflow routing
414
521
  - Additional action types (webhook, conditional branching, parallel execution)
415
522
  - More workflow examples (customer onboarding, support ticketing, approval flows)
416
523
  - Workflow testing utilities
@@ -10,6 +10,9 @@ A YAML-driven workflow engine with AI agent integration for building conversatio
10
10
  - **External Context Injection**: Support for pre-populated fields from external orchestrators
11
11
  - **Pattern Matching**: Flexible transition logic based on patterns and conditions
12
12
  - **Visualization**: Generate workflow graphs as images or Mermaid diagrams
13
+ - **Follow-up Conversations**: Handle user follow-up questions with full workflow context
14
+ - **Intent Detection**: Route users between collector nodes based on detected intent
15
+ - **Out-of-Scope Detection**: Signal when user queries are unrelated to the current workflow
13
16
 
14
17
  ## Installation
15
18
 
@@ -227,6 +230,108 @@ Calls a Python function with workflow state.
227
230
  next: failure_step
228
231
  ```
229
232
 
233
+ ### call_async_function
234
+
235
+ Calls an async function that may return a pending status, triggering an interrupt until the async operation completes.
236
+
237
+ ```yaml
238
+ - id: verify_payment
239
+ action: call_async_function
240
+ function: "payments.start_verification"
241
+ output: verification_result
242
+ transitions:
243
+ - condition: "verified"
244
+ next: payment_approved
245
+ - condition: "failed"
246
+ next: payment_rejected
247
+ ```
248
+
249
+ ### follow_up
250
+
251
+ Handles follow-up questions from users. Unlike `collect_input_with_agent` where the agent asks first, here the **user initiates** by asking questions. The agent responds using full workflow context.
252
+
253
+ ```yaml
254
+ - id: handle_questions
255
+ action: follow_up
256
+ next: final_confirmation # Where to go when user says "done"
257
+ closure_patterns: # Optional: customize closure detection
258
+ - "ok"
259
+ - "thank you"
260
+ - "done"
261
+ agent:
262
+ name: "FollowUpAssistant"
263
+ model: "gpt-4o-mini"
264
+ description: "Answering questions about the order"
265
+ instructions: |
266
+ Help the user with any questions about their order.
267
+ Be concise and helpful.
268
+ detect_out_of_scope: true # Signal when user asks unrelated questions
269
+ transitions: # Optional: route based on patterns
270
+ - pattern: "ROUTE_TO_PAYMENT:"
271
+ next: payment_step
272
+ ```
273
+
274
+ **Key features:**
275
+ - **User initiates**: No initial prompt - waits for user to ask a question
276
+ - **Full state context**: Agent sees all collected workflow data
277
+ - **Closure detection**: Detects "ok", "thanks", "done" → proceeds to next step
278
+ - **Intent change**: Routes to collector nodes when user wants to change data
279
+ - **Out-of-scope**: Signals to parent orchestrator for unrelated queries
280
+
281
+ ## Interrupt Types
282
+
283
+ The workflow engine uses three interrupt types to pause execution and communicate with the caller:
284
+
285
+ | Type | Marker | Triggered By | Use Case |
286
+ |------|--------|--------------|----------|
287
+ | **USER_INPUT** | `__WORKFLOW_INTERRUPT__` | `collect_input_with_agent`, `follow_up` | Waiting for user input |
288
+ | **ASYNC** | `__ASYNC_INTERRUPT__` | `call_async_function` | Waiting for async operation callback |
289
+ | **OUT_OF_SCOPE** | `__OUT_OF_SCOPE_INTERRUPT__` | `collect_input_with_agent`, `follow_up` | User query unrelated to current task |
290
+
291
+ ### Handling Interrupts
292
+
293
+ ```python
294
+ result = graph.invoke({}, config=config)
295
+
296
+ if "__interrupt__" in result and result["__interrupt__"]:
297
+ interrupt_value = result["__interrupt__"][0].value
298
+
299
+ # Check interrupt type
300
+ if isinstance(interrupt_value, dict):
301
+ if interrupt_value.get("type") == "async":
302
+ # Async interrupt - wait for external callback
303
+ pending_metadata = interrupt_value.get("pending")
304
+ # ... handle async operation ...
305
+ result = graph.invoke(Command(resume=async_result), config=config)
306
+
307
+ elif interrupt_value.get("type") == "out_of_scope":
308
+ # Out-of-scope - user asking unrelated question
309
+ reason = interrupt_value.get("reason")
310
+ user_message = interrupt_value.get("user_message")
311
+ # ... route to different workflow or handle appropriately ...
312
+ else:
313
+ # User input interrupt - prompt is a string
314
+ prompt = interrupt_value
315
+ user_input = input(f"Bot: {prompt}\nYou: ")
316
+ result = graph.invoke(Command(resume=user_input), config=config)
317
+ ```
318
+
319
+ ### Out-of-Scope Detection
320
+
321
+ Data collector and follow-up nodes can detect when user queries are unrelated to the current task. This is useful for multi-workflow systems where a supervisor agent needs to route users to different SOPs.
322
+
323
+ **Configuration:**
324
+ ```yaml
325
+ agent:
326
+ detect_out_of_scope: true # Enabled by default
327
+ scope_description: "collecting order information for returns" # Optional
328
+ ```
329
+
330
+ **Response format:**
331
+ ```
332
+ __OUT_OF_SCOPE_INTERRUPT__|{thread_id}|{workflow_name}|{"reason":"...","user_message":"..."}
333
+ ```
334
+
230
335
  ## Examples
231
336
 
232
337
  See the `examples/` directory for complete workflow examples:
@@ -370,6 +475,8 @@ Contributions are welcome! Please open an issue or submit a pull request.
370
475
  - ✅ Database persistence (SqliteSaver, PostgresSaver supported)
371
476
  - ✅ Pluggable checkpointer system
372
477
  - ✅ Thread ID strategies and examples
478
+ - ✅ Follow-up node for conversational Q&A
479
+ - ✅ Out-of-scope detection for multi-workflow routing
373
480
  - Additional action types (webhook, conditional branching, parallel execution)
374
481
  - More workflow examples (customer onboarding, support ticketing, approval flows)
375
482
  - Workflow testing utilities
@@ -0,0 +1,430 @@
1
+ # Soprano SDK Framework Flow Diagrams
2
+
3
+ ## 1. High-Level Architecture
4
+
5
+ ```mermaid
6
+ graph TB
7
+ subgraph "Entry Layer"
8
+ WT[WorkflowTool]
9
+ YML[YAML Workflow Definition]
10
+ end
11
+
12
+ subgraph "Engine Layer"
13
+ WE[WorkflowEngine]
14
+ FR[FunctionRepository]
15
+ TR[ToolRepository]
16
+ CS[ContextStore]
17
+ end
18
+
19
+ subgraph "Graph Layer"
20
+ SG[LangGraph StateGraph]
21
+ CP[Checkpointer]
22
+ WR[WorkflowRouter]
23
+ end
24
+
25
+ subgraph "Node Execution Layer"
26
+ NF[NodeFactory]
27
+ CIS[CollectInputStrategy]
28
+ CFS[CallFunctionStrategy]
29
+ AFS[AsyncFunctionStrategy]
30
+ end
31
+
32
+ subgraph "Agent Layer"
33
+ AF[AgentFactory]
34
+ LA[LangGraph Adapter]
35
+ CA[CrewAI Adapter]
36
+ AA[Agno Adapter]
37
+ PA[PydanticAI Adapter]
38
+ end
39
+
40
+ YML --> WE
41
+ WT --> WE
42
+ WE --> FR
43
+ WE --> TR
44
+ WE --> CS
45
+ WE --> SG
46
+ SG --> CP
47
+ SG --> WR
48
+ WR --> NF
49
+ NF --> CIS
50
+ NF --> CFS
51
+ NF --> AFS
52
+ CIS --> AF
53
+ AF --> LA
54
+ AF --> CA
55
+ AF --> AA
56
+ AF --> PA
57
+ ```
58
+
59
+ ## 2. Workflow Execution Flow
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ START([Start]) --> CHECK{Check State}
64
+
65
+ CHECK -->|Fresh Start| INIT[Initialize State<br/>with initial_context]
66
+ CHECK -->|Resuming| FILTER[Filter Already<br/>Collected Fields]
67
+
68
+ INIT --> INVOKE[graph.invoke<br/>initial_context]
69
+ FILTER --> RESUME[graph.invoke<br/>Command resume=value]
70
+
71
+ INVOKE --> RESULT{Check Result}
72
+ RESUME --> RESULT
73
+
74
+ RESULT -->|Has __interrupt__| PARSE[Parse Interrupt Value]
75
+ RESULT -->|No Interrupt| COMPLETE[Get Outcome Message]
76
+
77
+ PARSE --> INT_TYPE{Interrupt Type?}
78
+
79
+ INT_TYPE -->|USER_INPUT| UI_INT["__WORKFLOW_INTERRUPT__|<br/>thread_id|workflow|prompt"]
80
+ INT_TYPE -->|ASYNC| ASYNC_INT["__ASYNC_INTERRUPT__|<br/>thread_id|workflow|metadata"]
81
+ INT_TYPE -->|OUT_OF_SCOPE| OOS_INT["__OUT_OF_SCOPE_INTERRUPT__|<br/>thread_id|workflow|payload"]
82
+
83
+ UI_INT --> RETURN_INT[Return to Caller]
84
+ ASYNC_INT --> RETURN_INT
85
+ OOS_INT --> RETURN_INT
86
+ COMPLETE --> END_SUCCESS([Workflow Complete])
87
+
88
+ RETURN_INT --> WAIT[Wait for Resume]
89
+ WAIT --> CHECK
90
+ ```
91
+
92
+ ## 3. Node Execution Flow (CollectInputStrategy)
93
+
94
+ ```mermaid
95
+ flowchart TD
96
+ NODE_START([Node Invoked]) --> INIT_STATE[Initialize State]
97
+ INIT_STATE --> CTX[Apply Context Values]
98
+
99
+ CTX --> PRE_POP{Field<br/>Pre-populated?}
100
+ PRE_POP -->|Yes| VALIDATE_PRE[Validate & Route]
101
+ PRE_POP -->|No| MAX_ATT{Max Attempts<br/>Reached?}
102
+
103
+ MAX_ATT -->|Yes| MAX_FAIL[Set max_attempts status<br/>Return error message]
104
+ MAX_ATT -->|No| CREATE_AGENT[Create Agent<br/>with Instructions]
105
+
106
+ CREATE_AGENT --> ENHANCE[Enhance Instructions:<br/>+ Intent Detection<br/>+ Out-of-Scope Detection]
107
+
108
+ ENHANCE --> GEN_PROMPT[Generate Prompt]
109
+ GEN_PROMPT --> INTERRUPT_UI[/"INTERRUPT<br/>(USER_INPUT)"/]
110
+
111
+ INTERRUPT_UI --> GET_INPUT[Get User Input]
112
+ GET_INPUT --> ADD_CONV[Add to Conversation]
113
+ ADD_CONV --> INVOKE_AGENT[Invoke Agent]
114
+
115
+ INVOKE_AGENT --> STRUCT{Structured<br/>Output?}
116
+
117
+ STRUCT -->|Yes| PARSE_STRUCT[Parse JSON Response]
118
+ STRUCT -->|No| CHECK_PATTERN[Check Response Pattern]
119
+
120
+ PARSE_STRUCT --> OOS_CHECK{out_of_scope<br/>set?}
121
+ OOS_CHECK -->|Yes| OOS_HANDLE[/"INTERRUPT<br/>(OUT_OF_SCOPE)"/]
122
+ OOS_CHECK -->|No| INTENT_CHECK{intent_change<br/>set?}
123
+
124
+ INTENT_CHECK -->|Yes| ROLLBACK[Rollback to<br/>Target Node]
125
+ INTENT_CHECK -->|No| BOT_RESP{bot_response<br/>set?}
126
+
127
+ BOT_RESP -->|Yes| SELF_LOOP[Add to conversation<br/>Self-loop]
128
+ BOT_RESP -->|No| MATCH_TRANS[Match Transitions]
129
+
130
+ CHECK_PATTERN --> OOS_PAT{Starts with<br/>OUT_OF_SCOPE:?}
131
+ OOS_PAT -->|Yes| OOS_HANDLE
132
+ OOS_PAT -->|No| INTENT_PAT{Starts with<br/>INTENT_CHANGE:?}
133
+
134
+ INTENT_PAT -->|Yes| ROLLBACK
135
+ INTENT_PAT -->|No| MATCH_TRANS
136
+
137
+ MATCH_TRANS --> TRANS_FOUND{Transition<br/>Matched?}
138
+ TRANS_FOUND -->|Yes| VALIDATE[Validate Input]
139
+ TRANS_FOUND -->|No| SELF_LOOP
140
+
141
+ VALIDATE --> VALID{Valid?}
142
+ VALID -->|Yes| STORE[Store Value<br/>Set Status]
143
+ VALID -->|No| VAL_FAIL[Validation Error<br/>Self-loop]
144
+
145
+ STORE --> ROUTE[Route to Next Step]
146
+ VALIDATE_PRE --> ROUTE
147
+ MAX_FAIL --> NODE_END([Return State])
148
+ ROUTE --> NODE_END
149
+ SELF_LOOP --> NODE_END
150
+ VAL_FAIL --> NODE_END
151
+ ROLLBACK --> NODE_END
152
+ ```
153
+
154
+ ## 4. Async Function Node Flow
155
+
156
+ ```mermaid
157
+ flowchart TD
158
+ ASYNC_START([AsyncFunction Node]) --> CHECK_PENDING{Status =<br/>pending?}
159
+
160
+ CHECK_PENDING -->|Yes| HAS_META{Has Pending<br/>Metadata?}
161
+ HAS_META -->|Yes| ASYNC_INT[/"INTERRUPT<br/>(ASYNC)"/]
162
+ HAS_META -->|No| CALL_FUNC[Call Async Function]
163
+
164
+ CHECK_PENDING -->|No| CALL_FUNC
165
+
166
+ CALL_FUNC --> RESULT{Result Status?}
167
+
168
+ RESULT -->|pending| STORE_META[Store Pending Metadata]
169
+ RESULT -->|completed| SYNC_COMPLETE[Handle Sync Completion]
170
+
171
+ STORE_META --> SET_PENDING[Set status = pending]
172
+ SET_PENDING --> ASYNC_INT
173
+
174
+ ASYNC_INT --> WAIT_RESUME[Wait for External Result]
175
+ WAIT_RESUME --> RESUME[Resume with Result]
176
+ RESUME --> CLEAN_META[Clean Pending Metadata]
177
+ CLEAN_META --> ROUTE_RESULT[Route Based on Result]
178
+
179
+ SYNC_COMPLETE --> STORE_RESULT[Store in Output Field]
180
+ STORE_RESULT --> ROUTE_RESULT
181
+
182
+ ROUTE_RESULT --> CHECK_TRANS{Match<br/>Transitions?}
183
+ CHECK_TRANS -->|Yes| NEXT_STEP[Route to Matched Step]
184
+ CHECK_TRANS -->|No| SIMPLE{Has next_step?}
185
+
186
+ SIMPLE -->|Yes| NEXT_SIMPLE[Route to next_step]
187
+ SIMPLE -->|No| ASYNC_END([END])
188
+
189
+ NEXT_STEP --> ASYNC_END
190
+ NEXT_SIMPLE --> ASYNC_END
191
+ ```
192
+
193
+ ## 5. Routing Logic
194
+
195
+ ```mermaid
196
+ flowchart TD
197
+ ROUTE_START([Routing Decision]) --> GET_STATUS[Get _status from State]
198
+ GET_STATUS --> PARSE[Parse: step_id_suffix]
199
+
200
+ PARSE --> COLLECT{suffix =<br/>collecting?}
201
+ COLLECT -->|Yes| SELF_LOOP[Self-Loop to Same Node]
202
+ COLLECT -->|No| PENDING{suffix =<br/>pending?}
203
+
204
+ PENDING -->|Yes| SELF_LOOP
205
+ PENDING -->|No| ERROR{suffix =<br/>error?}
206
+
207
+ ERROR -->|Yes| GO_END[Route to END]
208
+ ERROR -->|No| MAX{suffix =<br/>max_attempts?}
209
+
210
+ MAX -->|Yes| GO_END
211
+ MAX -->|No| CHECK_STEP{suffix in<br/>step_map?}
212
+
213
+ CHECK_STEP -->|Yes| ROUTE_STEP[Route to Target Step]
214
+ CHECK_STEP -->|No| CHECK_OUT{suffix in<br/>outcome_map?}
215
+
216
+ CHECK_OUT -->|Yes| GO_END
217
+ CHECK_OUT -->|No| GO_END
218
+
219
+ SELF_LOOP --> ROUTE_END([Return Route Target])
220
+ GO_END --> ROUTE_END
221
+ ROUTE_STEP --> ROUTE_END
222
+ ```
223
+
224
+ ## 6. Intent Change & Rollback Flow
225
+
226
+ ```mermaid
227
+ flowchart TD
228
+ INTENT_START([Intent Change Detected]) --> EXTRACT[Extract Target Node]
229
+ EXTRACT --> GET_STRATEGY[Get Rollback Strategy]
230
+
231
+ GET_STRATEGY --> STRAT_TYPE{Strategy Type?}
232
+
233
+ STRAT_TYPE -->|history_based| FIND_SNAP[Find Snapshot<br/>Before Target Node]
234
+ STRAT_TYPE -->|dependency_based| BUILD_DEPS[Build Dependency<br/>Graph]
235
+
236
+ FIND_SNAP --> RESTORE_SNAP[Restore State<br/>from Snapshot]
237
+ BUILD_DEPS --> CLEAR_DEPS[Clear Target Field<br/>& Dependents]
238
+
239
+ RESTORE_SNAP --> RESTORE_CTX[Restore Context Values]
240
+ CLEAR_DEPS --> RESTORE_CTX
241
+
242
+ RESTORE_CTX --> SET_STATUS[Set Status:<br/>step_id_target_node]
243
+ SET_STATUS --> INTENT_END([Return to<br/>Target Node])
244
+ ```
245
+
246
+ ## 7. Agent Creation Flow
247
+
248
+ ```mermaid
249
+ flowchart TD
250
+ AGENT_START([Create Agent]) --> GET_MODEL[Get Model Config]
251
+ GET_MODEL --> LOAD_TOOLS[Load Tools from<br/>ToolRepository]
252
+
253
+ LOAD_TOOLS --> GET_COLLECTORS[Get Collector Nodes<br/>for Intent Detection]
254
+ GET_COLLECTORS --> RENDER_INST[Render Instructions<br/>with Jinja2]
255
+
256
+ RENDER_INST --> HAS_COLLECTORS{Has Other<br/>Collectors?}
257
+ HAS_COLLECTORS -->|Yes| ADD_INTENT[Add Intent<br/>Detection Instructions]
258
+ HAS_COLLECTORS -->|No| CHECK_OOS{Out-of-Scope<br/>Enabled?}
259
+
260
+ ADD_INTENT --> CHECK_OOS
261
+
262
+ CHECK_OOS -->|Yes| ADD_OOS[Add Out-of-Scope<br/>Detection Instructions]
263
+ CHECK_OOS -->|No| CREATE_STRUCT{Structured<br/>Output?}
264
+
265
+ ADD_OOS --> CREATE_STRUCT
266
+
267
+ CREATE_STRUCT -->|Yes| BUILD_MODEL[Build Pydantic Model<br/>with bot_response,<br/>intent_change, out_of_scope]
268
+ CREATE_STRUCT -->|No| FACTORY[AgentFactory.create_agent]
269
+
270
+ BUILD_MODEL --> FACTORY
271
+
272
+ FACTORY --> FRAMEWORK{Framework?}
273
+
274
+ FRAMEWORK -->|langgraph| LG[LangGraphAgentAdapter]
275
+ FRAMEWORK -->|crewai| CR[CrewAIAgentAdapter]
276
+ FRAMEWORK -->|agno| AG[AgnoAgentAdapter]
277
+ FRAMEWORK -->|pydantic_ai| PA[PydanticAIAgentAdapter]
278
+
279
+ LG --> AGENT_END([Return Adapter])
280
+ CR --> AGENT_END
281
+ AG --> AGENT_END
282
+ PA --> AGENT_END
283
+ ```
284
+
285
+ ## 8. Complete Workflow Lifecycle
286
+
287
+ ```mermaid
288
+ sequenceDiagram
289
+ participant C as Client/Caller
290
+ participant WT as WorkflowTool
291
+ participant G as LangGraph
292
+ participant N as Node Strategy
293
+ participant A as Agent
294
+ participant E as External System
295
+
296
+ C->>WT: execute(thread_id, context)
297
+ WT->>G: invoke(initial_context)
298
+
299
+ loop For Each Node
300
+ G->>N: execute(state)
301
+
302
+ alt CollectInput Node
303
+ N->>N: Generate prompt
304
+ N-->>G: INTERRUPT (USER_INPUT)
305
+ G-->>WT: Result with __interrupt__
306
+ WT-->>C: __WORKFLOW_INTERRUPT__|...|prompt
307
+ C->>WT: resume(thread_id, user_input)
308
+ WT->>G: invoke(Command resume=input)
309
+ G->>N: continue execution
310
+ N->>A: invoke(conversation)
311
+ A-->>N: response
312
+
313
+ alt Out of Scope
314
+ N-->>G: INTERRUPT (OUT_OF_SCOPE)
315
+ G-->>WT: Result with __interrupt__
316
+ WT-->>C: __OUT_OF_SCOPE_INTERRUPT__|...|payload
317
+ else Intent Change
318
+ N->>N: Rollback to target
319
+ N-->>G: Route to target node
320
+ else Normal Flow
321
+ N->>N: Validate & Store
322
+ N-->>G: Route to next
323
+ end
324
+
325
+ else CallFunction Node
326
+ N->>N: Load & Execute Function
327
+ N-->>G: Route based on result
328
+
329
+ else AsyncFunction Node
330
+ N->>E: Call async function
331
+ E-->>N: {status: pending, ...}
332
+ N-->>G: INTERRUPT (ASYNC)
333
+ G-->>WT: Result with __interrupt__
334
+ WT-->>C: __ASYNC_INTERRUPT__|...|metadata
335
+
336
+ Note over C,E: External processing...
337
+
338
+ C->>WT: resume(thread_id, async_result)
339
+ WT->>G: invoke(Command resume=result)
340
+ G->>N: continue with result
341
+ N-->>G: Route based on result
342
+ end
343
+ end
344
+
345
+ G-->>WT: Final state (no interrupt)
346
+ WT->>WT: Get outcome message
347
+ WT-->>C: Workflow complete message
348
+ ```
349
+
350
+ ## 9. State Structure
351
+
352
+ ```mermaid
353
+ classDiagram
354
+ class WorkflowState {
355
+ +str _status
356
+ +str _outcome_id
357
+ +List~Dict~ _messages
358
+ +Dict _conversations
359
+ +List _state_history
360
+ +List _node_execution_order
361
+ +Dict _node_field_map
362
+ +Dict _collector_nodes
363
+ +Dict _computed_fields
364
+ +str error
365
+ +Any ...user_defined_fields
366
+ }
367
+
368
+ class Conversation {
369
+ +str field_conversation
370
+ +List~Message~ messages
371
+ }
372
+
373
+ class Message {
374
+ +str role
375
+ +str content
376
+ }
377
+
378
+ class StateSnapshot {
379
+ +str snapshot_id
380
+ +str node_about_to_execute
381
+ +int execution_index
382
+ +str timestamp
383
+ +Dict state
384
+ }
385
+
386
+ WorkflowState "1" *-- "*" Conversation
387
+ Conversation "1" *-- "*" Message
388
+ WorkflowState "1" *-- "*" StateSnapshot
389
+ ```
390
+
391
+ ## 10. Interrupt Types Summary
392
+
393
+ ```mermaid
394
+ graph LR
395
+ subgraph "USER_INPUT"
396
+ UI_T[Trigger: CollectInputStrategy]
397
+ UI_D["Data: prompt string"]
398
+ UI_R["Resume: user response"]
399
+ end
400
+
401
+ subgraph "ASYNC"
402
+ A_T[Trigger: AsyncFunctionStrategy]
403
+ A_D["Data: {type, step_id, pending}"]
404
+ A_R["Resume: async result"]
405
+ end
406
+
407
+ subgraph "OUT_OF_SCOPE"
408
+ O_T[Trigger: CollectInputStrategy]
409
+ O_D["Data: {type, step_id,<br/>reason, user_message}"]
410
+ O_R["Action: Escalate to<br/>parent orchestrator"]
411
+ end
412
+
413
+ UI_T --> UI_D --> UI_R
414
+ A_T --> A_D --> A_R
415
+ O_T --> O_D --> O_R
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Usage
421
+
422
+ These diagrams are written in Mermaid format. To view them:
423
+
424
+ 1. **GitHub/GitLab**: Markdown files with Mermaid are rendered automatically
425
+ 2. **VS Code**: Install "Markdown Preview Mermaid Support" extension
426
+ 3. **Online**: Use [Mermaid Live Editor](https://mermaid.live/)
427
+ 4. **Export to PNG/SVG**: Use mermaid-cli (`npm install -g @mermaid-js/mermaid-cli`)
428
+ ```bash
429
+ mmdc -i framework_flow_diagrams.md -o output.png
430
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soprano-sdk"
7
- version = "0.2.19"
7
+ version = "0.2.20"
8
8
  description = "YAML-driven workflow engine with AI agent integration for building conversational SOPs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"