soprano-sdk 0.2.1__tar.gz → 0.2.3__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.1 → soprano_sdk-0.2.3}/PKG-INFO +1 -1
  2. soprano_sdk-0.2.3/examples/concert_booking/__init__.py +6 -0
  3. soprano_sdk-0.2.3/examples/concert_booking/booking_helpers.py +88 -0
  4. soprano_sdk-0.2.3/examples/concert_booking/concert_ticket_booking.yaml +244 -0
  5. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/pyproject.toml +1 -1
  6. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/__init__.py +2 -0
  7. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/authenticators/mfa.py +33 -20
  8. soprano_sdk-0.2.3/soprano_sdk/core/constants.py +114 -0
  9. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/engine.py +44 -16
  10. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/state.py +1 -0
  11. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/call_function.py +1 -0
  12. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/collect_input.py +8 -1
  13. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/tools.py +9 -3
  14. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/validator.py +34 -17
  15. soprano_sdk-0.2.3/tests/test_mfa_scenarios.py +418 -0
  16. soprano_sdk-0.2.1/soprano_sdk/core/constants.py +0 -76
  17. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.github/workflows/test_build_and_publish.yaml +0 -0
  18. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.gitignore +0 -0
  19. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.python-version +0 -0
  20. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/CLAUDE.md +0 -0
  21. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/LICENSE +0 -0
  22. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/README.md +0 -0
  23. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/framework_example.yaml +0 -0
  24. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/greeting_functions.py +0 -0
  25. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/greeting_workflow.yaml +0 -0
  26. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/main.py +0 -0
  27. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/README.md +0 -0
  28. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/conversation_based.py +0 -0
  29. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/entity_based.py +0 -0
  30. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/mongodb_demo.py +0 -0
  31. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/return_functions.py +0 -0
  32. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/return_workflow.yaml +0 -0
  33. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/structured_output_example.yaml +0 -0
  34. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/README.md +0 -0
  35. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  36. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  37. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/__init__.py +0 -0
  38. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/crewai_tools.py +0 -0
  39. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/langgraph_tools.py +0 -0
  40. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/workflow_tools.py +0 -0
  41. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/tools/__init__.py +0 -0
  42. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/tools/address.py +0 -0
  43. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/validator.py +0 -0
  44. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_demo.py +0 -0
  45. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_selfloop_demo.py +0 -0
  46. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_v.py +0 -0
  47. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/main.py +0 -0
  48. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/return_fsm.excalidraw +0 -0
  49. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/return_state_machine.png +0 -0
  50. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/ui.py +0 -0
  51. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/visualize_workflow.py +0 -0
  52. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/workflow_demo.py +0 -0
  53. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/workflow_demo_ui.py +0 -0
  54. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/__init__.py +0 -0
  55. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/adaptor.py +0 -0
  56. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/factory.py +0 -0
  57. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/structured_output.py +0 -0
  58. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/authenticators/__init__.py +0 -0
  59. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/__init__.py +0 -0
  60. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/rollback_strategies.py +0 -0
  61. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/engine.py +0 -0
  62. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/__init__.py +0 -0
  63. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/base.py +0 -0
  64. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/factory.py +0 -0
  65. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/routing/__init__.py +0 -0
  66. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/routing/router.py +0 -0
  67. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/__init__.py +0 -0
  68. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/function.py +0 -0
  69. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/logger.py +0 -0
  70. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/template.py +0 -0
  71. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/tool.py +0 -0
  72. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/tracing.py +0 -0
  73. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/__init__.py +0 -0
  74. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/schema.py +0 -0
  75. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/debug_jinja2.py +0 -0
  76. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_agent_factory.py +0 -0
  77. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_collect_input_refactor.py +0 -0
  78. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_external_values.py +0 -0
  79. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_inputs_validation.py +0 -0
  80. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_jinja2_path.py +0 -0
  81. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_jinja2_standalone.py +0 -0
  82. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_persistence.py +0 -0
  83. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_structured_output.py +0 -0
  84. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_transition_routing.py +0 -0
  85. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/todo.md +0 -0
  86. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/uv.lock +0 -0
  87. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/.eslintrc.cjs +0 -0
  88. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/.gitignore +0 -0
  89. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/README.md +0 -0
  90. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/index.html +0 -0
  91. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/package-lock.json +0 -0
  92. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/package.json +0 -0
  93. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/App.jsx +0 -0
  94. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/CustomNode.jsx +0 -0
  95. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  96. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  97. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  98. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/assets/react.svg +0 -0
  99. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/main.jsx +0 -0
  100. {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/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.1
3
+ Version: 0.2.3
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,6 @@
1
+ """
2
+ Concert Ticket Booking Example
3
+
4
+ This example demonstrates MFA (Multi-Factor Authentication) functionality
5
+ in a concert ticket booking workflow.
6
+ """
@@ -0,0 +1,88 @@
1
+ """
2
+ Helper functions for concert ticket booking workflow
3
+ """
4
+ import random
5
+ import uuid
6
+
7
+
8
+ def initialize_prices(state):
9
+ """Initialize ticket prices"""
10
+ return {
11
+ 'vip_price': 5000,
12
+ 'premium_price': 3000,
13
+ 'general_price': 1500
14
+ }
15
+
16
+
17
+ def check_availability(state):
18
+ """Check if requested seats are available"""
19
+ seat_preference = state.get('seat_preference')
20
+ ticket_quantity = state.get('ticket_quantity', 1)
21
+
22
+ # Simulate availability check - 80% chance of success
23
+ available = random.random() > 0.2
24
+
25
+ print(f"Checking availability: {ticket_quantity} x {seat_preference} seats")
26
+ print(f"Result: {'Available' if available else 'Not Available'}")
27
+
28
+ return available
29
+
30
+
31
+ def process_payment(state):
32
+ """Process payment for tickets"""
33
+ customer_name = state.get('customer_name')
34
+ concert_name = state.get('concert_name')
35
+ seat_preference = state.get('seat_preference')
36
+ ticket_quantity = state.get('ticket_quantity', 1)
37
+
38
+ # Calculate total amount
39
+ vip_price = state.get('vip_price', 5000)
40
+ premium_price = state.get('premium_price', 3000)
41
+ general_price = state.get('general_price', 1500)
42
+
43
+ prices = {
44
+ 'VIP': vip_price,
45
+ 'Premium': premium_price,
46
+ 'General': general_price
47
+ }
48
+
49
+ price_per_ticket = prices.get(seat_preference, general_price)
50
+ total_amount = price_per_ticket * ticket_quantity
51
+
52
+ # Simulate payment processing - 90% success rate
53
+ payment_success = random.random() > 0.1
54
+
55
+ print(f"Processing payment for {customer_name}")
56
+ print(f"Concert: {concert_name}")
57
+ print(f"Tickets: {ticket_quantity} x {seat_preference}")
58
+ print(f"Total: ₹{total_amount}")
59
+ print(f"Payment Status: {'Success' if payment_success else 'Failed'}")
60
+
61
+ # Generate booking reference on success
62
+ if payment_success:
63
+ state['booking_reference'] = f"BK{uuid.uuid4().hex[:8].upper()}"
64
+
65
+ return payment_success
66
+
67
+
68
+ def send_confirmation(state):
69
+ """Send booking confirmation"""
70
+ customer_name = state.get('customer_name')
71
+ booking_reference = state.get('booking_reference')
72
+ concert_name = state.get('concert_name')
73
+
74
+ # Simulate sending confirmation - 95% success rate
75
+ confirmation_sent = random.random() > 0.05
76
+
77
+ print(f"Sending confirmation to {customer_name}")
78
+ print(f"Booking Reference: {booking_reference}")
79
+ print(f"Concert: {concert_name}")
80
+ print(f"Status: {'Sent' if confirmation_sent else 'Failed'}")
81
+
82
+ return confirmation_sent
83
+
84
+
85
+ def handle_payment_failure(state):
86
+ """Handle payment failure"""
87
+ print("Payment failed. Cleaning up...")
88
+ return False
@@ -0,0 +1,244 @@
1
+ name: Concert Ticket Booking
2
+ description: Manages concert ticket booking with seat selection, payment processing, and confirmation
3
+ version: '1.0.0'
4
+
5
+ data:
6
+ - name: bearer_token
7
+ type: text
8
+ description: User authentication bearer token for MFA
9
+ - name: customer_name
10
+ type: text
11
+ description: Customer's full name
12
+ - name: concert_name
13
+ type: text
14
+ description: Name of the concert to book
15
+ - name: seat_preference
16
+ type: text
17
+ description: Preferred seating section (VIP, Premium, General)
18
+ - name: ticket_quantity
19
+ type: number
20
+ description: Number of tickets to book
21
+ - name: seat_available
22
+ type: boolean
23
+ description: Whether requested seats are available
24
+ - name: payment_status
25
+ type: boolean
26
+ description: Payment transaction status
27
+ - name: confirmation_sent
28
+ type: boolean
29
+ description: Whether confirmation email was sent
30
+ - name: modification_requested
31
+ type: boolean
32
+ description: Whether customer wants to modify booking
33
+ - name: booking_reference
34
+ type: text
35
+ description: Unique booking reference number
36
+ - name: vip_price
37
+ type: number
38
+ description: VIP ticket price
39
+ - name: premium_price
40
+ type: number
41
+ description: Premium ticket price
42
+ - name: general_price
43
+ type: number
44
+ description: General ticket price
45
+
46
+ steps:
47
+ - id: initialize_pricing
48
+ description: Initialize ticket pricing
49
+ action: call_function
50
+ function: booking_helpers.initialize_prices
51
+ output: vip_price
52
+ next: collect_customer_name
53
+
54
+ - id: collect_customer_name
55
+ description: Collect customer name
56
+ action: collect_input_with_agent
57
+ field: customer_name
58
+ max_attempts: 3
59
+ agent:
60
+ name: Customer Name Collector
61
+ model: gpt-4o-mini
62
+ initial_message: "Welcome to Concert Ticket Booking! Please provide your full name."
63
+ instructions: >
64
+ Extract the customer's full name from their message.
65
+ Return: NAME_CAPTURED: [customer_name] or INVALID_INPUT:
66
+ transitions:
67
+ - next: collect_concert_details
68
+ pattern: "NAME_CAPTURED:"
69
+ - next: booking_cancelled
70
+ pattern: "INVALID_INPUT:"
71
+
72
+ - id: collect_concert_details
73
+ description: Collect concert and seat preferences
74
+ action: collect_input_with_agent
75
+ field: concert_name
76
+ max_attempts: 3
77
+ agent:
78
+ name: Concert Details Collector
79
+ model: gpt-4o-mini
80
+ initial_message: "Hi {{customer_name}}! Which concert would you like to attend? Please specify the concert name."
81
+ instructions: >
82
+ Extract concert name from the user's message.
83
+ Return: CONCERT_CAPTURED: [concert_name] or CANCEL_REQUEST:
84
+ transitions:
85
+ - next: collect_seat_preference
86
+ pattern: "CONCERT_CAPTURED:"
87
+ - next: booking_cancelled
88
+ pattern: "CANCEL_REQUEST:"
89
+
90
+ - id: collect_seat_preference
91
+ description: Collect seating preference
92
+ action: collect_input_with_agent
93
+ field: seat_preference
94
+ max_attempts: 3
95
+ agent:
96
+ name: Seat Preference Collector
97
+ model: gpt-4o-mini
98
+ initial_message: >
99
+ Please choose your seating preference for {{concert_name}}:
100
+ - VIP (₹{{vip_price}})
101
+ - Premium (₹{{premium_price}})
102
+ - General (₹{{general_price}})
103
+ instructions: >
104
+ Identify seating preference (VIP, Premium, or General).
105
+ Return: SEAT_CAPTURED: [seat_preference] or CANCEL_REQUEST:
106
+ transitions:
107
+ - next: collect_ticket_quantity
108
+ pattern: "SEAT_CAPTURED:"
109
+ - next: booking_cancelled
110
+ pattern: "CANCEL_REQUEST:"
111
+
112
+ - id: collect_ticket_quantity
113
+ description: Collect number of tickets
114
+ action: collect_input_with_agent
115
+ field: ticket_quantity
116
+ max_attempts: 3
117
+ agent:
118
+ name: Ticket Quantity Collector
119
+ model: gpt-4o-mini
120
+ initial_message: "How many {{seat_preference}} tickets would you like to book? (Maximum 8 per booking)"
121
+ instructions: >
122
+ Extract the number of tickets (1-8).
123
+ Return: QUANTITY_CAPTURED: [ticket_quantity] or INVALID_QUANTITY:
124
+ transitions:
125
+ - next: check_seat_availability
126
+ pattern: "QUANTITY_CAPTURED:"
127
+ - next: booking_cancelled
128
+ pattern: "INVALID_QUANTITY:"
129
+
130
+ - id: check_seat_availability
131
+ description: Check if requested seats are available
132
+ action: call_function
133
+ function: booking_helpers.check_availability
134
+ output: seat_available
135
+ transitions:
136
+ - next: process_payment
137
+ condition: true
138
+ - next: offer_alternative_seats
139
+ condition: false
140
+
141
+ - id: offer_alternative_seats
142
+ description: Offer alternative seat options
143
+ action: collect_input_with_agent
144
+ field: seat_preference
145
+ max_attempts: 3
146
+ agent:
147
+ name: Alternative Seat Offer
148
+ model: gpt-4o-mini
149
+ initial_message: >
150
+ Sorry {{customer_name}}, {{seat_preference}} seats are not available for {{concert_name}}.
151
+ Would you like to try a different section?
152
+ instructions: >
153
+ Ask if user wants alternative seats.
154
+ Return: ALTERNATIVE_ACCEPTED: [new_preference] or BOOKING_DECLINED:
155
+ transitions:
156
+ - next: check_seat_availability
157
+ pattern: "ALTERNATIVE_ACCEPTED:"
158
+ - next: booking_cancelled
159
+ pattern: "BOOKING_DECLINED:"
160
+
161
+ # MFA-PROTECTED STEP: Payment processing requires authentication
162
+ - id: process_payment
163
+ description: Process payment transaction with MFA
164
+ action: call_function
165
+ function: booking_helpers.process_payment
166
+ output: payment_status
167
+ mfa:
168
+ model: gpt-4o-mini
169
+ type: REST
170
+ payload:
171
+ transactionType: CONCERT_TICKET_PAYMENT
172
+ businessKey:
173
+ customerName: "{{customer_name}}"
174
+ concertName: "{{concert_name}}"
175
+ seatPreference: "{{seat_preference}}"
176
+ ticketQuantity: "{{ticket_quantity}}"
177
+ transitions:
178
+ - next: send_confirmation
179
+ condition: true
180
+ - next: payment_failed
181
+ condition: false
182
+
183
+ # MFA-PROTECTED STEP: Sending confirmation requires authentication
184
+ - id: send_confirmation
185
+ description: Send booking confirmation with MFA
186
+ action: call_function
187
+ function: booking_helpers.send_confirmation
188
+ output: confirmation_sent
189
+ mfa:
190
+ model: gpt-4o-mini
191
+ type: REST
192
+ payload:
193
+ transactionType: SEND_TICKET_CONFIRMATION
194
+ businessKey:
195
+ bookingReference: "{{booking_reference}}"
196
+ customerName: "{{customer_name}}"
197
+ next: ask_modification_needed
198
+
199
+ - id: ask_modification_needed
200
+ description: Ask if customer wants to modify booking
201
+ action: collect_input_with_agent
202
+ field: modification_requested
203
+ max_attempts: 3
204
+ agent:
205
+ name: Modification Request Collector
206
+ model: gpt-4o-mini
207
+ initial_message: >
208
+ Booking confirmed for {{concert_name}}, {{customer_name}}!
209
+ Reference: {{booking_reference}}
210
+ Seats: {{ticket_quantity}} x {{seat_preference}}
211
+
212
+ Would you like to modify your booking details? (Yes/No)
213
+ instructions: >
214
+ Determine if user wants to modify the booking.
215
+ Return: MODIFICATION_REQUESTED: or NO_MODIFICATION:
216
+ transitions:
217
+ - next: collect_seat_preference
218
+ pattern: "MODIFICATION_REQUESTED:"
219
+ - next: booking_success
220
+ pattern: "NO_MODIFICATION:"
221
+
222
+ - id: payment_failed
223
+ description: Handle payment failure
224
+ action: call_function
225
+ function: booking_helpers.handle_payment_failure
226
+ output: payment_status
227
+ next: booking_failed
228
+
229
+ outcomes:
230
+ - id: booking_success
231
+ type: success
232
+ message: "Thank you {{customer_name}}! Your {{ticket_quantity}} {{seat_preference}} tickets for {{concert_name}} have been confirmed. Booking reference: {{booking_reference}}"
233
+
234
+ - id: booking_cancelled
235
+ type: failure
236
+ message: "Booking cancelled. Feel free to start a new booking anytime!"
237
+
238
+ - id: booking_failed
239
+ type: failure
240
+ message: "We couldn't complete your booking. Please try again or contact support."
241
+
242
+ - id: payment_failed
243
+ type: failure
244
+ message: "Payment failed. Please check your payment method and try again."
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soprano-sdk"
7
- version = "0.2.1"
7
+ version = "0.2.3"
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"
@@ -1,4 +1,5 @@
1
1
  from .core.engine import WorkflowEngine, load_workflow
2
+ from .core.constants import MFAConfig
2
3
  from .tools import WorkflowTool
3
4
 
4
5
  __version__ = "0.1.0"
@@ -6,5 +7,6 @@ __version__ = "0.1.0"
6
7
  __all__ = [
7
8
  "WorkflowEngine",
8
9
  "load_workflow",
10
+ "MFAConfig",
9
11
  "WorkflowTool",
10
12
  ]
@@ -1,6 +1,6 @@
1
1
  import requests
2
- from typing import TypedDict, Literal, NotRequired
3
- from soprano_sdk.core.constants import MFARestAuthorizerEnv
2
+ from typing import TypedDict, Literal, NotRequired, Optional
3
+ from soprano_sdk.core.constants import MFAConfig
4
4
 
5
5
 
6
6
  class MFAChallenge(TypedDict):
@@ -28,15 +28,21 @@ def build_path(base_url: str, path: str):
28
28
  return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
29
29
 
30
30
 
31
- def enforce_mfa_if_required(state: dict):
31
+ def enforce_mfa_if_required(state: dict, mfa_config: Optional[MFAConfig] = None):
32
+ if mfa_config is None:
33
+ mfa_config = state.get('_mfa_config') or MFAConfig()
34
+
32
35
  _mfa : MFAState = state['_mfa']
33
36
  if _mfa['status'] == 'COMPLETED':
34
37
  return True
35
38
  generate_token_response = requests.post(
36
39
  build_path(
37
- base_url=MFARestAuthorizerEnv.GENERATE_TOKEN_BASE_URL.get_from_env(),
38
- path=MFARestAuthorizerEnv.GENERATE_TOKEN_PATH.get_from_env()
39
- ), json=_mfa['post_payload'], timeout=30, headers={"Authorization": f"Bearer {state['bearer_token']}"}
40
+ base_url=mfa_config.generate_token_base_url,
41
+ path=mfa_config.generate_token_path
42
+ ),
43
+ json=_mfa['post_payload'],
44
+ timeout=mfa_config.api_timeout,
45
+ headers={"Authorization": f"Bearer {state['bearer_token']}"}
40
46
  )
41
47
  _, error = get_response(generate_token_response)
42
48
 
@@ -50,7 +56,10 @@ def enforce_mfa_if_required(state: dict):
50
56
  return False
51
57
 
52
58
 
53
- def mfa_validate_user_input(**state: dict):
59
+ def mfa_validate_user_input(mfa_config: Optional[MFAConfig] = None, **state: dict):
60
+ if mfa_config is None:
61
+ mfa_config = state.get('_mfa_config') or MFAConfig()
62
+
54
63
  _mfa : MFAState = state['_mfa']
55
64
  input_field_name = state['_active_input_field']
56
65
  if not state[input_field_name]:
@@ -61,19 +70,19 @@ def mfa_validate_user_input(**state: dict):
61
70
  post_payload.update({challenge_field_name: {"value": state[input_field_name]}})
62
71
  validate_token_response = requests.post(
63
72
  build_path(
64
- base_url=MFARestAuthorizerEnv.VALIDATE_TOKEN_BASE_URL.get_from_env(),
65
- path=MFARestAuthorizerEnv.VALIDATE_TOKEN_PATH.get_from_env()
66
- ), json=post_payload, timeout=30, headers={"Authorization": f"Bearer {state['bearer_token']}"}
73
+ base_url=mfa_config.validate_token_base_url,
74
+ path=mfa_config.validate_token_path
75
+ ),
76
+ json=post_payload,
77
+ timeout=mfa_config.api_timeout,
78
+ headers={"Authorization": f"Bearer {state['bearer_token']}"}
67
79
  )
68
80
  _mfa['retry_count'] += 1
69
81
  response, error = get_response(validate_token_response)
70
82
  if error:
71
83
  if _mfa['retry_count'] == 1:
72
84
  _mfa['status'] = 'ERRORED'
73
- _mfa['message'] = (
74
- f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
75
- )
76
- return False
85
+ return False, f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
77
86
 
78
87
  if response and 'token' in response:
79
88
  token = response['token']
@@ -81,19 +90,19 @@ def mfa_validate_user_input(**state: dict):
81
90
 
82
91
  authorize = requests.post(
83
92
  build_path(
84
- base_url=MFARestAuthorizerEnv.AUTHORIZE_TOKEN_BASE_URL.get_from_env(),
85
- path=MFARestAuthorizerEnv.AUTHORIZE_TOKEN_PATH.get_from_env()
93
+ base_url=mfa_config.authorize_token_base_url,
94
+ path=mfa_config.authorize_token_path
86
95
  ),
87
96
  json=post_payload,
88
- timeout=30,
97
+ timeout=mfa_config.api_timeout,
89
98
  headers={"Authorization": f"Bearer {state['bearer_token']}"}
90
99
  )
91
100
  if authorize.status_code == 204:
92
101
  _mfa['status'] = 'COMPLETED'
93
- return True
102
+ return True, None
94
103
  else:
95
104
  _mfa['status'] = 'FAILED'
96
- return False
105
+ return False, f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
97
106
 
98
107
 
99
108
  class MFANodeConfig:
@@ -140,8 +149,12 @@ class MFANodeConfig:
140
149
  - Extract ONLY the OTP code value
141
150
  - Output in the exact format shown below
142
151
 
152
+ Examples:
153
+ * User says: "1234" → `MFA_CAPTURED:1223`
154
+ * User says: "2345e" → `MFA_CAPTURED:1223e
155
+
143
156
  **Output Format:**
144
- MFA_CAPTURED:
157
+ MFA_CAPTURED:<input_field_name>
145
158
 
146
159
  """),
147
160
  transitions=[
@@ -0,0 +1,114 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class WorkflowKeys:
8
+ STEP_ID = '_step_id'
9
+ STATUS = '_status'
10
+ OUTCOME_ID = '_outcome_id'
11
+ MESSAGES = '_messages'
12
+ CONVERSATIONS = '_conversations'
13
+ STATE_HISTORY = '_state_history'
14
+ COLLECTOR_NODES = '_collector_nodes'
15
+ ATTEMPT_COUNTS = '_attempt_counts'
16
+ NODE_EXECUTION_ORDER = '_node_execution_order'
17
+ NODE_FIELD_MAP = '_node_field_map'
18
+ COMPUTED_FIELDS = '_computed_fields'
19
+ ERROR = 'error'
20
+
21
+
22
+ class ActionType(Enum):
23
+ COLLECT_INPUT_WITH_AGENT = 'collect_input_with_agent'
24
+ CALL_FUNCTION = 'call_function'
25
+
26
+
27
+ class DataType(Enum):
28
+ TEXT = 'text'
29
+ NUMBER = 'number'
30
+ DOUBLE = 'double'
31
+ BOOLEAN = 'boolean'
32
+ LIST = 'list'
33
+ DICT = 'dict'
34
+ ANY = "any"
35
+
36
+
37
+ class OutcomeType(Enum):
38
+ SUCCESS = 'success'
39
+ FAILURE = 'failure'
40
+
41
+
42
+ class StatusPattern:
43
+ COLLECTING = '{step_id}_collecting'
44
+ MAX_ATTEMPTS = '{step_id}_max_attempts'
45
+ NEXT_STEP = '{step_id}_{next_step}'
46
+ SUCCESS = '{step_id}_success'
47
+ FAILED = '{step_id}_failed'
48
+ INTENT_CHANGE = '{step_id}_{target_node}'
49
+
50
+
51
+ class TransitionPattern:
52
+ CAPTURED = '{field}_CAPTURED:'
53
+ FAILED = '{field}_FAILED:'
54
+ INTENT_CHANGE = 'INTENT_CHANGE:'
55
+
56
+
57
+ DEFAULT_MAX_ATTEMPTS = 3
58
+ DEFAULT_MODEL = 'gpt-4o-mini'
59
+ DEFAULT_TIMEOUT = 300
60
+
61
+ MAX_ATTEMPTS_MESSAGE = "I'm having trouble understanding your {field}. Please contact customer service for assistance."
62
+ WORKFLOW_COMPLETE_MESSAGE = "Workflow completed."
63
+
64
+
65
+ class MFAConfig(BaseSettings):
66
+ """
67
+ Configuration for MFA REST API endpoints.
68
+
69
+ Values can be provided during initialization or will be automatically
70
+ loaded from environment variables with the same name (uppercase).
71
+
72
+ Example:
73
+ # Load from environment variables
74
+ config = MFAConfig()
75
+
76
+ # Or provide specific values
77
+ config = MFAConfig(
78
+ generate_token_base_url="https://api.example.com",
79
+ generate_token_path="/v1/mfa/generate"
80
+ )
81
+ """
82
+ generate_token_base_url: Optional[str] = Field(
83
+ default=None,
84
+ description="Base URL for the generate token endpoint"
85
+ )
86
+ generate_token_path: Optional[str] = Field(
87
+ default=None,
88
+ description="Path for the generate token endpoint"
89
+ )
90
+ validate_token_base_url: Optional[str] = Field(
91
+ default=None,
92
+ description="Base URL for the validate token endpoint"
93
+ )
94
+ validate_token_path: Optional[str] = Field(
95
+ default=None,
96
+ description="Path for the validate token endpoint"
97
+ )
98
+ authorize_token_base_url: Optional[str] = Field(
99
+ default=None,
100
+ description="Base URL for the authorize token endpoint"
101
+ )
102
+ authorize_token_path: Optional[str] = Field(
103
+ default=None,
104
+ description="Path for the authorize token endpoint"
105
+ )
106
+ api_timeout: int = Field(
107
+ default=30,
108
+ description="API request timeout in seconds"
109
+ )
110
+
111
+ model_config = SettingsConfigDict(
112
+ case_sensitive=False,
113
+ extra='ignore'
114
+ )