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.
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/PKG-INFO +1 -1
- soprano_sdk-0.2.3/examples/concert_booking/__init__.py +6 -0
- soprano_sdk-0.2.3/examples/concert_booking/booking_helpers.py +88 -0
- soprano_sdk-0.2.3/examples/concert_booking/concert_ticket_booking.yaml +244 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/pyproject.toml +1 -1
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/__init__.py +2 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/authenticators/mfa.py +33 -20
- soprano_sdk-0.2.3/soprano_sdk/core/constants.py +114 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/engine.py +44 -16
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/state.py +1 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/call_function.py +1 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/collect_input.py +8 -1
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/tools.py +9 -3
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/validator.py +34 -17
- soprano_sdk-0.2.3/tests/test_mfa_scenarios.py +418 -0
- soprano_sdk-0.2.1/soprano_sdk/core/constants.py +0 -76
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.github/workflows/test_build_and_publish.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.gitignore +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/.python-version +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/CLAUDE.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/LICENSE +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/framework_example.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/greeting_functions.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/greeting_workflow.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/main.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/conversation_based.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/entity_based.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/persistence/mongodb_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/return_functions.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/return_workflow.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/structured_output_example.yaml +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/crewai_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/crewai_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/tools/langgraph_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/supervisors/workflow_tools.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/tools/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/tools/address.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/examples/validator.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_selfloop_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/langgraph_v.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/main.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/return_fsm.excalidraw +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/return_state_machine.png +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/legacy/ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/visualize_workflow.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/workflow_demo.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/scripts/workflow_demo_ui.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/adaptor.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/agents/structured_output.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/authenticators/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/core/rollback_strategies.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/engine.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/base.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/nodes/factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/routing/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/routing/router.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/function.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/logger.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/template.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/tool.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/utils/tracing.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/__init__.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/soprano_sdk/validation/schema.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/debug_jinja2.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_agent_factory.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_collect_input_refactor.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_external_values.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_inputs_validation.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_jinja2_path.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_jinja2_standalone.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_persistence.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_structured_output.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/tests/test_transition_routing.py +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/todo.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/uv.lock +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/.eslintrc.cjs +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/.gitignore +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/README.md +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/index.html +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/package-lock.json +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/package.json +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/App.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/CustomNode.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/assets/react.svg +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/src/main.jsx +0 -0
- {soprano_sdk-0.2.1 → soprano_sdk-0.2.3}/workflow-visualizer/vite.config.js +0 -0
|
@@ -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.
|
|
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
|
|
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=
|
|
38
|
-
path=
|
|
39
|
-
),
|
|
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=
|
|
65
|
-
path=
|
|
66
|
-
),
|
|
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
|
-
|
|
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=
|
|
85
|
-
path=
|
|
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=
|
|
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
|
+
)
|