soprano-sdk 0.2.3__tar.gz → 0.2.4__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 (100) hide show
  1. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/PKG-INFO +1 -1
  2. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/pyproject.toml +1 -1
  3. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/authenticators/mfa.py +12 -3
  4. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/core/engine.py +3 -2
  5. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/nodes/collect_input.py +5 -1
  6. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/validation/schema.py +14 -0
  7. soprano_sdk-0.2.4/tests/test_collect_input_refactor.py +66 -0
  8. soprano_sdk-0.2.3/tests/test_collect_input_refactor.py +0 -27
  9. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/.github/workflows/test_build_and_publish.yaml +0 -0
  10. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/.gitignore +0 -0
  11. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/.python-version +0 -0
  12. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/CLAUDE.md +0 -0
  13. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/LICENSE +0 -0
  14. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/README.md +0 -0
  15. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/concert_booking/__init__.py +0 -0
  16. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/concert_booking/booking_helpers.py +0 -0
  17. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/concert_booking/concert_ticket_booking.yaml +0 -0
  18. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/framework_example.yaml +0 -0
  19. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/greeting_functions.py +0 -0
  20. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/greeting_workflow.yaml +0 -0
  21. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/main.py +0 -0
  22. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/persistence/README.md +0 -0
  23. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/persistence/conversation_based.py +0 -0
  24. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/persistence/entity_based.py +0 -0
  25. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/persistence/mongodb_demo.py +0 -0
  26. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/return_functions.py +0 -0
  27. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/return_workflow.yaml +0 -0
  28. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/structured_output_example.yaml +0 -0
  29. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/README.md +0 -0
  30. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  31. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  32. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/tools/__init__.py +0 -0
  33. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/tools/crewai_tools.py +0 -0
  34. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/tools/langgraph_tools.py +0 -0
  35. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/supervisors/workflow_tools.py +0 -0
  36. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/tools/__init__.py +0 -0
  37. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/tools/address.py +0 -0
  38. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/examples/validator.py +0 -0
  39. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/langgraph_demo.py +0 -0
  40. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/langgraph_selfloop_demo.py +0 -0
  41. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/langgraph_v.py +0 -0
  42. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/main.py +0 -0
  43. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/return_fsm.excalidraw +0 -0
  44. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/return_state_machine.png +0 -0
  45. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/legacy/ui.py +0 -0
  46. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/scripts/visualize_workflow.py +0 -0
  47. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/scripts/workflow_demo.py +0 -0
  48. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/scripts/workflow_demo_ui.py +0 -0
  49. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/__init__.py +0 -0
  50. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/agents/__init__.py +0 -0
  51. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/agents/adaptor.py +0 -0
  52. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/agents/factory.py +0 -0
  53. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/agents/structured_output.py +0 -0
  54. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/authenticators/__init__.py +0 -0
  55. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/core/__init__.py +0 -0
  56. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/core/constants.py +0 -0
  57. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/core/rollback_strategies.py +0 -0
  58. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/core/state.py +0 -0
  59. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/engine.py +0 -0
  60. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/nodes/__init__.py +0 -0
  61. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/nodes/base.py +0 -0
  62. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/nodes/call_function.py +0 -0
  63. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/nodes/factory.py +0 -0
  64. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/routing/__init__.py +0 -0
  65. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/routing/router.py +0 -0
  66. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/tools.py +0 -0
  67. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/__init__.py +0 -0
  68. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/function.py +0 -0
  69. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/logger.py +0 -0
  70. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/template.py +0 -0
  71. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/tool.py +0 -0
  72. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/utils/tracing.py +0 -0
  73. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/validation/__init__.py +0 -0
  74. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/soprano_sdk/validation/validator.py +0 -0
  75. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/debug_jinja2.py +0 -0
  76. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_agent_factory.py +0 -0
  77. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_external_values.py +0 -0
  78. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_inputs_validation.py +0 -0
  79. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_jinja2_path.py +0 -0
  80. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_jinja2_standalone.py +0 -0
  81. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_mfa_scenarios.py +0 -0
  82. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_persistence.py +0 -0
  83. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_structured_output.py +0 -0
  84. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/tests/test_transition_routing.py +0 -0
  85. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/todo.md +0 -0
  86. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/uv.lock +0 -0
  87. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/.eslintrc.cjs +0 -0
  88. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/.gitignore +0 -0
  89. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/README.md +0 -0
  90. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/index.html +0 -0
  91. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/package-lock.json +0 -0
  92. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/package.json +0 -0
  93. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/App.jsx +0 -0
  94. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/CustomNode.jsx +0 -0
  95. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  96. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  97. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  98. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/assets/react.svg +0 -0
  99. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/workflow-visualizer/src/main.jsx +0 -0
  100. {soprano_sdk-0.2.3 → soprano_sdk-0.2.4}/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.3
3
+ Version: 0.2.4
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.3"
7
+ version = "0.2.4"
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"
@@ -128,14 +128,18 @@ class MFANodeConfig:
128
128
  )
129
129
 
130
130
  @classmethod
131
- def get_validate_user_input(cls, source_node: str, next_node: str, model_name: str):
131
+ def get_validate_user_input(cls, source_node: str, next_node: str, mfa_config: dict):
132
+ model_name = mfa_config['model']
133
+ max_attempts = mfa_config.get('max_attempts', 3)
134
+ on_max_attempts_reached = mfa_config.get('on_max_attempts_reached')
135
+
132
136
  input_field_name = f"{source_node}_mfa_input"
133
- return dict(
137
+ node_config = dict(
134
138
  id=f"{source_node}_mfa_validate",
135
139
  action="collect_input_with_agent",
136
140
  description="Collect Input for MFA value",
137
141
  field=input_field_name,
138
- max_attempts=3,
142
+ max_attempts=max_attempts,
139
143
  validator="soprano_sdk.authenticators.mfa.mfa_validate_user_input",
140
144
  agent=dict(
141
145
  name="MFA Input Data Collector",
@@ -164,3 +168,8 @@ class MFANodeConfig:
164
168
  )
165
169
  ]
166
170
  )
171
+
172
+ if on_max_attempts_reached:
173
+ node_config['on_max_attempts_reached'] = on_max_attempts_reached
174
+
175
+ return node_config
@@ -209,8 +209,9 @@ class WorkflowEngine:
209
209
 
210
210
  if mfa_config := step.get('mfa'):
211
211
  mfa_data_collector = MFANodeConfig.get_validate_user_input(
212
- next_node=step_id, model_name=mfa_config['model'],
213
- source_node=step_id
212
+ next_node=step_id,
213
+ source_node=step_id,
214
+ mfa_config=mfa_config
214
215
  )
215
216
  mfa_start = MFANodeConfig.get_call_function_template(
216
217
  source_node=step_id,
@@ -92,6 +92,7 @@ class CollectInputStrategy(ActionStrategy):
92
92
  self.field = step_config.get('field')
93
93
  self.agent_config = step_config.get('agent', {})
94
94
  self.max_attempts = step_config.get('retry_limit') or engine_context.get_config_value("max_retry_limit", DEFAULT_MAX_ATTEMPTS)
95
+ self.on_max_attempts_reached = step_config.get('on_max_attempts_reached')
95
96
  self.transitions = self._get_transitions()
96
97
  self.next_step = self.step_config.get("next", None)
97
98
  self.is_structured_output = self.agent_config.get("structured_output", {}).get("enabled", False)
@@ -272,7 +273,10 @@ class CollectInputStrategy(ActionStrategy):
272
273
  def _handle_max_attempts(self, state: Dict[str, Any]) -> Dict[str, Any]:
273
274
  logger.warning(f"Max attempts reached for field '{self.field}'")
274
275
  self._set_status(state, 'max_attempts')
275
- message = MAX_ATTEMPTS_MESSAGE.format(field=self.field)
276
+ if self.on_max_attempts_reached:
277
+ message = self.on_max_attempts_reached
278
+ else:
279
+ message = MAX_ATTEMPTS_MESSAGE.format(field=self.field)
276
280
  state[WorkflowKeys.MESSAGES] = [message]
277
281
  return state
278
282
 
@@ -90,6 +90,10 @@ WORKFLOW_SCHEMA = {
90
90
  "maximum": 20,
91
91
  "description": "Maximum attempts (for collect_input_with_agent)"
92
92
  },
93
+ "on_max_attempts_reached": {
94
+ "type": "string",
95
+ "description": "Custom error message to display when max attempts are exhausted (for collect_input_with_agent)"
96
+ },
93
97
  "agent": {
94
98
  "type": "object",
95
99
  "description": "Agent configuration (for collect_input_with_agent)",
@@ -245,6 +249,16 @@ WORKFLOW_SCHEMA = {
245
249
  "type": "object",
246
250
  "description": "MFA payload data that is posted to the RESTAPI, Apart from the properties provided transactionId is sent by the framework in the post payload as an additional property, transactionId is the same throughout the MFA process",
247
251
  "additionalProperties": True
252
+ },
253
+ "max_attempts": {
254
+ "type": "integer",
255
+ "minimum": 1,
256
+ "maximum": 20,
257
+ "description": "Maximum number of attempts allowed for MFA validation (default: 3)"
258
+ },
259
+ "on_max_attempts_reached": {
260
+ "type": "string",
261
+ "description": "Custom error message to display when MFA max attempts are exhausted"
248
262
  }
249
263
  }
250
264
  },
@@ -0,0 +1,66 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from soprano_sdk.nodes.collect_input import CollectInputStrategy, _get_agent_response
4
+ from soprano_sdk.core.constants import WorkflowKeys
5
+
6
+ class TestCollectInputStrategyRefactor:
7
+ @pytest.fixture
8
+ def strategy(self):
9
+ step_config = {
10
+ "field": "test_field",
11
+ "agent": {"name": "test_agent"}
12
+ }
13
+ engine_context = MagicMock()
14
+ engine_context.get_config_value.return_value = "history_based"
15
+ return CollectInputStrategy(step_config, engine_context)
16
+
17
+ def test_get_agent_response_success(self, strategy):
18
+ agent = MagicMock()
19
+ conversation = [{"role": "user", "content": "hello"}]
20
+
21
+ mock_response = "response content"
22
+ agent.invoke.return_value = mock_response
23
+
24
+ response = _get_agent_response(agent, conversation)
25
+
26
+ assert response == "response content"
27
+ assert conversation[-1]["role"] == "assistant"
28
+ assert conversation[-1]["content"] == "response content"
29
+
30
+ def test_handle_max_attempts_default_message(self):
31
+ """Test that default error message is used when on_max_attempts_reached is not provided"""
32
+ step_config = {
33
+ "field": "customer_name",
34
+ "agent": {"name": "test_agent"}
35
+ }
36
+ engine_context = MagicMock()
37
+ engine_context.get_config_value.return_value = "history_based"
38
+
39
+ strategy = CollectInputStrategy(step_config, engine_context)
40
+ state = {}
41
+
42
+ result = strategy._handle_max_attempts(state)
43
+
44
+ assert result[WorkflowKeys.STATUS] == "max_attempts"
45
+ assert "customer_name" in result[WorkflowKeys.MESSAGES][0]
46
+ assert "customer service" in result[WorkflowKeys.MESSAGES][0].lower()
47
+
48
+ def test_handle_max_attempts_custom_message(self):
49
+ """Test that custom error message is used when on_max_attempts_reached is provided"""
50
+ custom_message = "Custom error: Too many attempts. Please call 1-800-SUPPORT."
51
+ step_config = {
52
+ "field": "customer_name",
53
+ "agent": {"name": "test_agent"},
54
+ "on_max_attempts_reached": custom_message
55
+ }
56
+ engine_context = MagicMock()
57
+ engine_context.get_config_value.return_value = "history_based"
58
+
59
+ strategy = CollectInputStrategy(step_config, engine_context)
60
+ state = {}
61
+
62
+ result = strategy._handle_max_attempts(state)
63
+
64
+ assert result[WorkflowKeys.STATUS] == "max_attempts"
65
+ assert result[WorkflowKeys.MESSAGES][0] == custom_message
66
+ assert "1-800-SUPPORT" in result[WorkflowKeys.MESSAGES][0]
@@ -1,27 +0,0 @@
1
- import pytest
2
- from unittest.mock import MagicMock
3
- from soprano_sdk.nodes.collect_input import CollectInputStrategy, _get_agent_response
4
-
5
- class TestCollectInputStrategyRefactor:
6
- @pytest.fixture
7
- def strategy(self):
8
- step_config = {
9
- "field": "test_field",
10
- "agent": {"name": "test_agent"}
11
- }
12
- engine_context = MagicMock()
13
- engine_context.get_config_value.return_value = "history_based"
14
- return CollectInputStrategy(step_config, engine_context)
15
-
16
- def test_get_agent_response_success(self, strategy):
17
- agent = MagicMock()
18
- conversation = [{"role": "user", "content": "hello"}]
19
-
20
- mock_response = "response content"
21
- agent.invoke.return_value = mock_response
22
-
23
- response = _get_agent_response(agent, conversation)
24
-
25
- assert response == "response content"
26
- assert conversation[-1]["role"] == "assistant"
27
- assert conversation[-1]["content"] == "response content"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes