soprano-sdk 0.2.21__tar.gz → 0.2.22__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 (116) hide show
  1. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/PKG-INFO +1 -1
  2. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/pyproject.toml +1 -1
  3. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/tools.py +16 -1
  4. soprano_sdk-0.2.22/tests/test_initial_context_override_bug.py +114 -0
  5. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/.claude/settings.local.json +0 -0
  6. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/.github/workflows/test_build_and_publish.yaml +0 -0
  7. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/.gitignore +0 -0
  8. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/.python-version +0 -0
  9. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/CLAUDE.md +0 -0
  10. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/LICENSE +0 -0
  11. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/README.md +0 -0
  12. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/docs/framework_flow_diagrams.md +0 -0
  13. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/__init__.py +0 -0
  14. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/booking_helpers.py +0 -0
  15. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
  16. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/concert_tools.py +0 -0
  17. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/example_runner.py +0 -0
  18. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/concert_booking/tool.py +0 -0
  19. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/framework_example.yaml +0 -0
  20. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/greeting_functions.py +0 -0
  21. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/greeting_workflow.yaml +0 -0
  22. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/main.py +0 -0
  23. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/payment_async_functions.py +0 -0
  24. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/payment_async_workflow.yaml +0 -0
  25. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/persistence/README.md +0 -0
  26. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/persistence/conversation_based.py +0 -0
  27. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/persistence/entity_based.py +0 -0
  28. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/persistence/mongodb_demo.py +0 -0
  29. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/return_functions.py +0 -0
  30. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/return_workflow.yaml +0 -0
  31. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/structured_output_example.yaml +0 -0
  32. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/README.md +0 -0
  33. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  34. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  35. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/tools/__init__.py +0 -0
  36. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/tools/crewai_tools.py +0 -0
  37. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/tools/langgraph_tools.py +0 -0
  38. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/supervisors/workflow_tools.py +0 -0
  39. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/test_payment_async.py +0 -0
  40. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/tools/__init__.py +0 -0
  41. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/tools/address.py +0 -0
  42. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/examples/validator.py +0 -0
  43. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/langgraph_demo.py +0 -0
  44. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/langgraph_selfloop_demo.py +0 -0
  45. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/langgraph_v.py +0 -0
  46. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/main.py +0 -0
  47. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/return_fsm.excalidraw +0 -0
  48. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/return_state_machine.png +0 -0
  49. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/legacy/ui.py +0 -0
  50. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/scripts/visualize_workflow.py +0 -0
  51. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/scripts/workflow_demo.py +0 -0
  52. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/scripts/workflow_demo_ui.py +0 -0
  53. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/__init__.py +0 -0
  54. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/agents/__init__.py +0 -0
  55. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/agents/adaptor.py +0 -0
  56. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/agents/factory.py +0 -0
  57. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/agents/structured_output.py +0 -0
  58. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/authenticators/__init__.py +0 -0
  59. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/authenticators/mfa.py +0 -0
  60. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/core/__init__.py +0 -0
  61. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/core/constants.py +0 -0
  62. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/core/engine.py +0 -0
  63. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/core/rollback_strategies.py +0 -0
  64. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/core/state.py +0 -0
  65. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/engine.py +0 -0
  66. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/__init__.py +0 -0
  67. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/async_function.py +0 -0
  68. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/base.py +0 -0
  69. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/call_function.py +0 -0
  70. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/collect_input.py +0 -0
  71. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/factory.py +0 -0
  72. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/nodes/follow_up.py +0 -0
  73. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/routing/__init__.py +0 -0
  74. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/routing/router.py +0 -0
  75. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/__init__.py +0 -0
  76. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/function.py +0 -0
  77. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/logger.py +0 -0
  78. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/template.py +0 -0
  79. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/tool.py +0 -0
  80. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/utils/tracing.py +0 -0
  81. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/validation/__init__.py +0 -0
  82. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/validation/schema.py +0 -0
  83. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/soprano_sdk/validation/validator.py +0 -0
  84. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/debug_jinja2.py +0 -0
  85. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_adaptor_logging.py +0 -0
  86. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_agent_factory.py +0 -0
  87. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_async_function.py +0 -0
  88. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_base_node.py +0 -0
  89. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_collect_input_refactor.py +0 -0
  90. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_engine_failure_message.py +0 -0
  91. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_external_values.py +0 -0
  92. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_follow_up.py +0 -0
  93. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_inputs_validation.py +0 -0
  94. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_jinja2_path.py +0 -0
  95. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_jinja2_standalone.py +0 -0
  96. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_mfa_scenarios.py +0 -0
  97. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_out_of_scope.py +0 -0
  98. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_persistence.py +0 -0
  99. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_structured_output.py +0 -0
  100. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_transition_routing.py +0 -0
  101. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/tests/test_workflow_tool_context_update.py +0 -0
  102. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/todo.md +0 -0
  103. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/.eslintrc.cjs +0 -0
  104. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/.gitignore +0 -0
  105. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/README.md +0 -0
  106. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/index.html +0 -0
  107. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/package-lock.json +0 -0
  108. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/package.json +0 -0
  109. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/App.jsx +0 -0
  110. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/CustomNode.jsx +0 -0
  111. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  112. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  113. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  114. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/assets/react.svg +0 -0
  115. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/workflow-visualizer/src/main.jsx +0 -0
  116. {soprano_sdk-0.2.21 → soprano_sdk-0.2.22}/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.21
3
+ Version: 0.2.22
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soprano-sdk"
7
- version = "0.2.21"
7
+ version = "0.2.22"
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"
@@ -169,7 +169,8 @@ class WorkflowTool:
169
169
  initial_context: Optional[Dict[str, Any]]
170
170
  ) -> Dict[str, Any]:
171
171
  """
172
- Filter initial_context to exclude fields that have already been collected.
172
+ Filter initial_context to exclude fields that have already been collected
173
+ or are currently being collected.
173
174
 
174
175
  Args:
175
176
  current_state: Current workflow state
@@ -193,6 +194,20 @@ class WorkflowTool:
193
194
  if executed_node_id in node_to_field_map:
194
195
  collected_fields.add(node_to_field_map[executed_node_id])
195
196
 
197
+ # Also exclude fields that are currently being collected (actively collecting)
198
+ # This prevents initial_context from overriding user input
199
+ current_status = current_state.get(WorkflowKeys.STATUS, "")
200
+ if "collecting" in current_status:
201
+ # Status format: "node_id_collecting" (e.g., "get_name_collecting")
202
+ for node_id, field in node_to_field_map.items():
203
+ if f"{node_id}_collecting" == current_status:
204
+ collected_fields.add(field)
205
+ logger.info(
206
+ f"[WorkflowTool] Field '{field}' is currently being collected. "
207
+ f"Excluding from initial_context to preserve user input."
208
+ )
209
+ break
210
+
196
211
  # Filter initial_context to exclude already-collected fields
197
212
  filtered_context = {
198
213
  field: value
@@ -0,0 +1,114 @@
1
+ """
2
+ Unit test for initial_context override bug.
3
+
4
+ Bug: When a collector node has interrupted and is waiting for user input,
5
+ if the caller passes initial_context with the same field, it overrides
6
+ the user's input instead of respecting it.
7
+
8
+ This test currently FAILS (documents the bug).
9
+ After fixing the bug, this test should PASS.
10
+ """
11
+
12
+ import pytest
13
+ from unittest.mock import MagicMock, patch
14
+ from soprano_sdk.tools import WorkflowTool
15
+ from soprano_sdk.core.constants import WorkflowKeys
16
+
17
+
18
+ class TestInitialContextOverrideBug:
19
+ """
20
+ Test that initial_context should NOT override user input
21
+ when a field is actively being collected.
22
+ """
23
+
24
+ @patch('soprano_sdk.tools.load_workflow')
25
+ @patch('soprano_sdk.utils.tracing.trace_workflow_execution')
26
+ @patch('soprano_sdk.tools.CallbackHandler')
27
+ def test_initial_context_should_not_override_user_input_during_collection(
28
+ self, mock_callback_handler, mock_trace, mock_load_workflow
29
+ ):
30
+ """
31
+ Test that initial_context does NOT override user input for actively collecting field.
32
+
33
+ Scenario:
34
+ 1. Node "get_name" has interrupted, asking "What's your name?"
35
+ 2. User provides "John" via user_message
36
+ 3. Caller also passes initial_context={"name": "Alice", "age": 30}
37
+ 4. Expected: "name" should be "John" (from user), not "Alice" (from context)
38
+ 5. Expected: "age" should be 30 (from context, not collected yet)
39
+
40
+ Current behavior (BUG):
41
+ - Both "name" and "age" are passed to engine.update_context
42
+ - _apply_context_value overwrites state["name"] = "Alice"
43
+ - User input "John" is lost
44
+
45
+ Expected behavior (AFTER FIX):
46
+ - Only "age" should be passed to engine.update_context
47
+ - "name" should be filtered out (actively collecting)
48
+ - User input "John" is preserved
49
+ """
50
+ # Setup mocks
51
+ mock_graph = MagicMock()
52
+ mock_engine = MagicMock()
53
+ mock_load_workflow.return_value = (mock_graph, mock_engine)
54
+
55
+ # Mock trace context manager
56
+ mock_span = MagicMock()
57
+ mock_trace.return_value.__enter__.return_value = mock_span
58
+
59
+ # Simulate state when "get_name" node has interrupted
60
+ mock_state = MagicMock()
61
+ mock_state.next = ["get_name"] # Node is waiting for input
62
+ mock_state.values = {
63
+ WorkflowKeys.NODE_EXECUTION_ORDER: [], # Node hasn't completed yet
64
+ WorkflowKeys.STATUS: "get_name_collecting", # Actively collecting
65
+ "_active_input_field": "name", # Field being collected
66
+ "name": None # Field not collected yet
67
+ }
68
+ mock_graph.get_state.return_value = mock_state
69
+
70
+ # Mock collector_node_field_map (maps node_id -> field)
71
+ mock_engine.collector_node_field_map = {
72
+ "get_name": "name",
73
+ "get_age": "age"
74
+ }
75
+
76
+ # Mock result
77
+ mock_result = {"outcome": "success"}
78
+ mock_graph.invoke.return_value = mock_result
79
+ mock_engine.get_outcome_message.return_value = "Success"
80
+
81
+ # Create workflow tool
82
+ workflow = WorkflowTool(
83
+ yaml_path="test.yaml",
84
+ name="TestWorkflow",
85
+ description="Test workflow"
86
+ )
87
+
88
+ # Execute - resuming with user input "John" but initial_context has "Alice"
89
+ workflow.execute(
90
+ thread_id="test-thread",
91
+ user_message="John", # User's input
92
+ initial_context={"name": "Alice", "age": 30} # Context trying to override
93
+ )
94
+
95
+ # ASSERTION: Verify that "name" was filtered out from context update
96
+ mock_engine.update_context.assert_called_once()
97
+ called_context = mock_engine.update_context.call_args[0][0]
98
+
99
+ # The actively collecting field "name" should NOT be in the context
100
+ assert "name" not in called_context, (
101
+ f"Bug: 'name' should be filtered out because it's actively being collected. "
102
+ f"Got context: {called_context}"
103
+ )
104
+
105
+ # The uncollected field "age" SHOULD be in the context
106
+ assert "age" in called_context, (
107
+ f"Expected: 'age' should be in context for pre-population. "
108
+ f"Got context: {called_context}"
109
+ )
110
+ assert called_context["age"] == 30
111
+
112
+
113
+ if __name__ == "__main__":
114
+ pytest.main([__file__, "-v"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes