soprano-sdk 0.2.1__tar.gz → 0.2.2__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.
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/PKG-INFO +1 -1
- soprano_sdk-0.2.2/examples/concert_booking/README.md +108 -0
- soprano_sdk-0.2.2/examples/concert_booking/TEST_RESULTS.md +179 -0
- soprano_sdk-0.2.2/examples/concert_booking/__init__.py +6 -0
- soprano_sdk-0.2.2/examples/concert_booking/booking_helpers.py +88 -0
- soprano_sdk-0.2.2/examples/concert_booking/concert_ticket_booking.yaml +244 -0
- soprano_sdk-0.2.2/examples/debit_card_block.yaml +608 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/pyproject.toml +1 -1
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/authenticators/mfa.py +8 -7
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/core/engine.py +26 -11
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/nodes/collect_input.py +5 -1
- soprano_sdk-0.2.2/tests/test_mfa_error_handling.py +778 -0
- soprano_sdk-0.2.2/tests/test_mfa_scenarios.py +427 -0
- soprano_sdk-0.2.2/tests/test_mfa_validation.py +545 -0
- soprano_sdk-0.2.2/uv.lock +5165 -0
- soprano_sdk-0.2.1/uv.lock +0 -5163
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/.github/workflows/test_build_and_publish.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/.gitignore +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/.python-version +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/CLAUDE.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/LICENSE +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/framework_example.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/greeting_functions.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/greeting_workflow.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/main.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/persistence/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/persistence/conversation_based.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/persistence/entity_based.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/persistence/mongodb_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/return_functions.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/return_workflow.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/structured_output_example.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/crewai_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/tools/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/tools/crewai_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/tools/langgraph_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/supervisors/workflow_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/tools/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/tools/address.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/examples/validator.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/langgraph_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/langgraph_selfloop_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/langgraph_v.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/main.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/return_fsm.excalidraw +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/return_state_machine.png +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/legacy/ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/scripts/visualize_workflow.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/scripts/workflow_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/scripts/workflow_demo_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/agents/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/agents/adaptor.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/agents/factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/agents/structured_output.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/authenticators/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/core/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/core/constants.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/core/rollback_strategies.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/core/state.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/engine.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/nodes/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/nodes/base.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/nodes/call_function.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/nodes/factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/routing/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/routing/router.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/function.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/logger.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/template.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/tool.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/utils/tracing.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/validation/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/validation/schema.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/soprano_sdk/validation/validator.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/debug_jinja2.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_agent_factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_collect_input_refactor.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_external_values.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_inputs_validation.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_jinja2_path.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_jinja2_standalone.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_persistence.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_structured_output.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/tests/test_transition_routing.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/todo.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/.eslintrc.cjs +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/.gitignore +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/index.html +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/package-lock.json +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/package.json +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/App.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/CustomNode.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/assets/react.svg +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/src/main.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.2}/workflow-visualizer/vite.config.js +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Concert Ticket Booking - MFA Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates a concert ticket booking workflow with Multi-Factor Authentication (MFA) protection on critical operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The workflow allows users to book concert tickets through a conversational interface with the following features:
|
|
8
|
+
|
|
9
|
+
- **Customer name collection**
|
|
10
|
+
- **Concert selection**
|
|
11
|
+
- **Seat preference selection** (VIP, Premium, General)
|
|
12
|
+
- **Ticket quantity selection**
|
|
13
|
+
- **Seat availability checking**
|
|
14
|
+
- **Payment processing** (MFA-protected)
|
|
15
|
+
- **Booking confirmation** (MFA-protected)
|
|
16
|
+
- **Booking modification** (with loop-back to MFA)
|
|
17
|
+
|
|
18
|
+
## MFA-Protected Steps
|
|
19
|
+
|
|
20
|
+
Two critical operations are protected with MFA:
|
|
21
|
+
|
|
22
|
+
1. **`process_payment`** - Payment transaction processing
|
|
23
|
+
2. **`send_confirmation`** - Sending booking confirmation
|
|
24
|
+
|
|
25
|
+
## Key Features for MFA Testing
|
|
26
|
+
|
|
27
|
+
### 1. Forward MFA Flow
|
|
28
|
+
Regular workflow progression through MFA-protected steps:
|
|
29
|
+
```
|
|
30
|
+
collect_seat_preference → check_seat_availability → process_payment (MFA) → send_confirmation (MFA)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Loop Back Scenario
|
|
34
|
+
User can modify booking AFTER completion, which loops back through MFA:
|
|
35
|
+
```
|
|
36
|
+
ask_modification_needed → collect_seat_preference → ... → process_payment (MFA)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This tests that steps AFTER MFA-protected nodes can correctly reference them.
|
|
40
|
+
|
|
41
|
+
### 3. Multiple MFA Steps
|
|
42
|
+
Sequential MFA-protected operations:
|
|
43
|
+
```
|
|
44
|
+
process_payment (MFA) → send_confirmation (MFA)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 4. Alternative Paths
|
|
48
|
+
When seats are unavailable, alternative paths also go through MFA:
|
|
49
|
+
```
|
|
50
|
+
offer_alternative_seats → check_seat_availability → process_payment (MFA)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Files
|
|
54
|
+
|
|
55
|
+
- **`concert_ticket_booking.yaml`** - Workflow definition with MFA configuration
|
|
56
|
+
- **`booking_helpers.py`** - Helper functions for workflow logic
|
|
57
|
+
- **`README.md`** - This file
|
|
58
|
+
|
|
59
|
+
## MFA Configuration Example
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- id: process_payment
|
|
63
|
+
description: Process payment transaction with MFA
|
|
64
|
+
action: call_function
|
|
65
|
+
function: booking_helpers.process_payment
|
|
66
|
+
output: payment_status
|
|
67
|
+
mfa:
|
|
68
|
+
model: gpt-4o-mini
|
|
69
|
+
type: REST
|
|
70
|
+
payload:
|
|
71
|
+
transactionType: CONCERT_TICKET_PAYMENT
|
|
72
|
+
businessKey:
|
|
73
|
+
customerName: "{{customer_name}}"
|
|
74
|
+
concertName: "{{concert_name}}"
|
|
75
|
+
seatPreference: "{{seat_preference}}"
|
|
76
|
+
ticketQuantity: "{{ticket_quantity}}"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
Run the comprehensive MFA test suite:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python tests/test_mfa_scenarios.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The test suite includes:
|
|
88
|
+
|
|
89
|
+
1. **MFA Node Creation** - Verifies MFA nodes are properly inserted
|
|
90
|
+
2. **Previous Step Redirection** - Tests forward flow through MFA
|
|
91
|
+
3. **Loop Back Redirection** - Tests backward references to MFA steps (critical)
|
|
92
|
+
4. **Multiple MFA Steps** - Tests sequential MFA operations
|
|
93
|
+
5. **MFA Data Fields** - Validates data schema updates
|
|
94
|
+
6. **Alternative Paths** - Tests multiple paths to MFA steps
|
|
95
|
+
7. **Comprehensive Flow** - End-to-end validation
|
|
96
|
+
|
|
97
|
+
## Pricing
|
|
98
|
+
|
|
99
|
+
- **VIP**: ₹5,000 per ticket
|
|
100
|
+
- **Premium**: ₹3,000 per ticket
|
|
101
|
+
- **General**: ₹1,500 per ticket
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- Maximum 8 tickets per booking
|
|
106
|
+
- Seat availability is simulated (80% success rate)
|
|
107
|
+
- Payment processing is simulated (90% success rate)
|
|
108
|
+
- Confirmation sending is simulated (95% success rate)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# MFA Test Results - Concert Ticket Booking
|
|
2
|
+
|
|
3
|
+
## Test Execution Summary
|
|
4
|
+
|
|
5
|
+
**Date**: 2026-01-05
|
|
6
|
+
**Test Suite**: `tests/test_mfa_scenarios.py`
|
|
7
|
+
**Status**: ✅ **ALL TESTS PASSED** (7/7)
|
|
8
|
+
|
|
9
|
+
## Test Cases
|
|
10
|
+
|
|
11
|
+
### ✅ Test 1: MFA Node Creation
|
|
12
|
+
**Status**: PASSED
|
|
13
|
+
**Purpose**: Verify MFA nodes are properly created and injected into the workflow
|
|
14
|
+
|
|
15
|
+
**Validations**:
|
|
16
|
+
- MFA start nodes are inserted before MFA-protected steps
|
|
17
|
+
- MFA validate nodes are created for user input collection
|
|
18
|
+
- Original steps remain intact after MFA injection
|
|
19
|
+
- 2 MFA-protected steps → 4 MFA nodes created (2 per protected step)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### ✅ Test 2: Previous Step Redirection (Forward Flow)
|
|
24
|
+
**Status**: PASSED
|
|
25
|
+
**Purpose**: Verify steps BEFORE MFA-protected steps redirect correctly
|
|
26
|
+
|
|
27
|
+
**Validations**:
|
|
28
|
+
- `check_seat_availability` → `process_payment` becomes `check_seat_availability` → `process_payment_mfa_start`
|
|
29
|
+
- `process_payment` → `send_confirmation` becomes `process_payment` → `send_confirmation_mfa_start`
|
|
30
|
+
- All forward transitions to MFA-protected steps go through MFA authentication
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### ✅ Test 3: Loop Back Redirection (CRITICAL BUG FIX TEST)
|
|
35
|
+
**Status**: PASSED
|
|
36
|
+
**Purpose**: Verify steps AFTER MFA-protected steps that loop back redirect correctly
|
|
37
|
+
|
|
38
|
+
**Critical Scenario**:
|
|
39
|
+
```
|
|
40
|
+
User Flow:
|
|
41
|
+
1. Complete booking (goes through MFA for payment & confirmation)
|
|
42
|
+
2. Request modification (ask_modification_needed step)
|
|
43
|
+
3. Change seat preference (loops back to collect_seat_preference)
|
|
44
|
+
4. Re-process payment (must go through MFA again)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Validations**:
|
|
48
|
+
- `ask_modification_needed` comes AFTER MFA-protected steps
|
|
49
|
+
- Loop back to `collect_seat_preference` is preserved
|
|
50
|
+
- Path from `collect_seat_preference` eventually reaches `process_payment_mfa_start`
|
|
51
|
+
- No MFA bypass - modification requires re-authentication
|
|
52
|
+
|
|
53
|
+
**This test validates the fix implemented in** `soprano_sdk/core/engine.py:202-246`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### ✅ Test 4: Multiple MFA Steps in Sequence
|
|
58
|
+
**Status**: PASSED
|
|
59
|
+
**Purpose**: Verify multiple consecutive MFA-protected steps work correctly
|
|
60
|
+
|
|
61
|
+
**Scenario**:
|
|
62
|
+
```
|
|
63
|
+
process_payment (MFA) → send_confirmation (MFA)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Validations**:
|
|
67
|
+
- Each MFA step has independent start and validate nodes
|
|
68
|
+
- Flow between MFA steps is preserved
|
|
69
|
+
- Both MFA processes execute sequentially without interference
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### ✅ Test 5: MFA Data Fields Creation
|
|
74
|
+
**Status**: PASSED
|
|
75
|
+
**Purpose**: Verify MFA-specific data fields are automatically created
|
|
76
|
+
|
|
77
|
+
**Validations**:
|
|
78
|
+
- Data fields for MFA validate nodes are added to workflow schema
|
|
79
|
+
- Field names match the MFA validate node field names
|
|
80
|
+
- Data schema properly includes MFA authentication fields
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### ✅ Test 6: Alternative Paths to MFA Steps
|
|
85
|
+
**Status**: PASSED
|
|
86
|
+
**Purpose**: Verify alternative/conditional paths to MFA steps are redirected
|
|
87
|
+
|
|
88
|
+
**Scenario**:
|
|
89
|
+
```
|
|
90
|
+
offer_alternative_seats → check_seat_availability → process_payment (MFA)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Validations**:
|
|
94
|
+
- Alternative path (when seats unavailable) also goes through MFA
|
|
95
|
+
- Multiple entry points to MFA-protected steps all redirect correctly
|
|
96
|
+
- No bypass routes exist for MFA-protected operations
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### ✅ Test 7: Comprehensive MFA Flow Validation
|
|
101
|
+
**Status**: PASSED
|
|
102
|
+
**Purpose**: End-to-end validation of entire workflow structure
|
|
103
|
+
|
|
104
|
+
**Validations**:
|
|
105
|
+
- All 15 workflow steps validated
|
|
106
|
+
- No step directly references MFA-protected steps (all go through MFA start)
|
|
107
|
+
- No MFA bypass violations found
|
|
108
|
+
- Complete workflow integrity maintained
|
|
109
|
+
|
|
110
|
+
**Workflow Statistics**:
|
|
111
|
+
- Total steps: 15
|
|
112
|
+
- MFA-protected steps: 2
|
|
113
|
+
- MFA nodes created: 4
|
|
114
|
+
- Regular steps: 11
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Technical Details
|
|
119
|
+
|
|
120
|
+
### MFA Implementation
|
|
121
|
+
The framework automatically injects MFA authentication nodes for steps marked with `mfa` configuration:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- id: process_payment
|
|
125
|
+
action: call_function
|
|
126
|
+
function: booking_helpers.process_payment
|
|
127
|
+
mfa:
|
|
128
|
+
model: gpt-4o-mini
|
|
129
|
+
type: REST
|
|
130
|
+
payload:
|
|
131
|
+
transactionType: CONCERT_TICKET_PAYMENT
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Node Injection Pattern
|
|
135
|
+
For each MFA-protected step `X`, the framework creates:
|
|
136
|
+
1. `X_mfa_start` - Initiates MFA authentication flow
|
|
137
|
+
2. `X_mfa_validate` - Collects and validates user authentication input
|
|
138
|
+
3. Original step `X` - Executes only after successful MFA
|
|
139
|
+
|
|
140
|
+
### Bug Fix Validation
|
|
141
|
+
The critical test (Test 3) validates the fix in `core/engine.py` where:
|
|
142
|
+
- **Before**: Only steps BEFORE MFA-protected steps were updated
|
|
143
|
+
- **After**: ALL steps (both before AND after) that reference MFA-protected steps are updated
|
|
144
|
+
|
|
145
|
+
This ensures loop-back scenarios (like booking modifications) cannot bypass MFA authentication.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Running the Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Using pytest (recommended)
|
|
153
|
+
cd /path/to/soprano_sdk
|
|
154
|
+
uv run pytest tests/test_mfa_scenarios.py -v
|
|
155
|
+
|
|
156
|
+
# Using direct Python execution
|
|
157
|
+
cd /path/to/soprano_sdk
|
|
158
|
+
uv run python tests/test_mfa_scenarios.py
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Dependencies
|
|
162
|
+
|
|
163
|
+
The tests require:
|
|
164
|
+
- `pytest` for test execution
|
|
165
|
+
- Environment variables for MFA endpoints (set via pytest fixtures)
|
|
166
|
+
- Concert booking workflow YAML and helper functions
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Conclusion
|
|
171
|
+
|
|
172
|
+
All comprehensive MFA test cases pass successfully, validating:
|
|
173
|
+
1. Correct MFA node creation and injection
|
|
174
|
+
2. Proper redirection of all paths (forward and backward)
|
|
175
|
+
3. Support for multiple consecutive MFA steps
|
|
176
|
+
4. Automatic data field management
|
|
177
|
+
5. Complete workflow integrity with no MFA bypass routes
|
|
178
|
+
|
|
179
|
+
The implementation correctly handles both forward flows and loop-back scenarios, ensuring that MFA protection cannot be circumvented through any workflow path.
|
|
@@ -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."
|