soprano-sdk 0.1.100__tar.gz → 0.2.0__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.1.100 → soprano_sdk-0.2.0}/PKG-INFO +1 -1
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/pyproject.toml +1 -1
- soprano_sdk-0.2.0/soprano_sdk/authenticators/mfa.py +153 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/constants.py +17 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/engine.py +52 -4
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/state.py +9 -1
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/base.py +5 -0
- soprano_sdk-0.2.0/soprano_sdk/nodes/call_api.py +651 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/call_function.py +22 -2
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/collect_input.py +3 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/tools.py +0 -2
- soprano_sdk-0.2.0/soprano_sdk/utils/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/schema.py +22 -1
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/validator.py +38 -1
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.github/workflows/test_build_and_publish.yaml +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.gitignore +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.python-version +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/CLAUDE.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/LICENSE +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/README.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/framework_example.yaml +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/greeting_functions.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/greeting_workflow.yaml +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/main.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/README.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/conversation_based.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/entity_based.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/mongodb_demo.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/return_functions.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/return_workflow.yaml +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/structured_output_example.yaml +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/README.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/crewai_supervisor_ui.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/crewai_tools.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/langgraph_tools.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/workflow_tools.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/tools/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/tools/address.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/validator.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_demo.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_selfloop_demo.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_v.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/main.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/return_fsm.excalidraw +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/return_state_machine.png +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/ui.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/visualize_workflow.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/workflow_demo.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/workflow_demo_ui.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/adaptor.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/factory.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/structured_output.py +0 -0
- {soprano_sdk-0.1.100/soprano_sdk/core → soprano_sdk-0.2.0/soprano_sdk/authenticators}/__init__.py +0 -0
- {soprano_sdk-0.1.100/soprano_sdk/nodes → soprano_sdk-0.2.0/soprano_sdk/core}/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/rollback_strategies.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/engine.py +0 -0
- {soprano_sdk-0.1.100/soprano_sdk/routing → soprano_sdk-0.2.0/soprano_sdk/nodes}/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/factory.py +0 -0
- {soprano_sdk-0.1.100/soprano_sdk/utils → soprano_sdk-0.2.0/soprano_sdk/routing}/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/routing/router.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/function.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/logger.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/template.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/tool.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/tracing.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/__init__.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/debug_jinja2.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_agent_factory.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_collect_input_refactor.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_external_values.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_inputs_validation.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_jinja2_path.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_jinja2_standalone.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_persistence.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_structured_output.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_transition_routing.py +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/todo.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/uv.lock +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/.eslintrc.cjs +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/.gitignore +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/README.md +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/index.html +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/package-lock.json +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/package.json +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/App.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/CustomNode.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/assets/react.svg +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/main.jsx +0 -0
- {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/vite.config.js +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soprano-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import TypedDict, Literal, NotRequired
|
|
3
|
+
from soprano_sdk.core.constants import MFARestAuthorizerEnv
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MFAChallenge(TypedDict):
|
|
7
|
+
value: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MFAState(TypedDict):
|
|
11
|
+
challengeType: Literal['OTP', 'dob']
|
|
12
|
+
post_payload: dict[str, str]
|
|
13
|
+
otpValue: NotRequired[str]
|
|
14
|
+
status: Literal['IN_PROGRESS', 'COMPLETED', 'ERRORED', 'FAILED'] | None
|
|
15
|
+
message: str
|
|
16
|
+
retry_count: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_response(response: requests.Response):
|
|
21
|
+
if response.ok:
|
|
22
|
+
return response.json(), None
|
|
23
|
+
else:
|
|
24
|
+
return None, response.json()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_path(base_url: str, path: str):
|
|
28
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def enforce_mfa_if_required(state: dict):
|
|
32
|
+
_mfa : MFAState = state['_mfa']
|
|
33
|
+
if _mfa['status'] == 'COMPLETED':
|
|
34
|
+
return True
|
|
35
|
+
generate_token_response = requests.post(
|
|
36
|
+
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
|
+
)
|
|
41
|
+
_, error = get_response(generate_token_response)
|
|
42
|
+
|
|
43
|
+
challenge_type = error['additionalData']['challengeType']
|
|
44
|
+
_mfa['challengeType'] = challenge_type
|
|
45
|
+
_mfa['status'] = 'IN_PROGRESS'
|
|
46
|
+
_mfa['retry_count'] = 0
|
|
47
|
+
_mfa['message'] = f"Please enter the {challenge_type}"
|
|
48
|
+
if delivery_methods := error['additionalData'].get(f"{challenge_type.lower()}SentTo"):
|
|
49
|
+
_mfa['message'] += f" sent via {','.join(delivery_methods)}"
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def mfa_validate_user_input(**state: dict):
|
|
54
|
+
_mfa : MFAState = state['_mfa']
|
|
55
|
+
input_field_name = state['_active_input_field']
|
|
56
|
+
if not state[input_field_name]:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
post_payload = _mfa['post_payload']
|
|
60
|
+
challenge_field_name = f"{_mfa['challengeType'].lower()}Challenge"
|
|
61
|
+
post_payload.update({challenge_field_name: {"value": state[input_field_name]}})
|
|
62
|
+
validate_token_response = requests.post(
|
|
63
|
+
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']}"}
|
|
67
|
+
)
|
|
68
|
+
_mfa['retry_count'] += 1
|
|
69
|
+
response, error = get_response(validate_token_response)
|
|
70
|
+
if error:
|
|
71
|
+
if _mfa['retry_count'] == 1:
|
|
72
|
+
_mfa['status'] = 'ERRORED'
|
|
73
|
+
_mfa['message'] = (
|
|
74
|
+
f"You Have Entered Invalid {_mfa['challengeType']}. {_mfa['message']}"
|
|
75
|
+
)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
if response and 'token' in response:
|
|
79
|
+
token = response['token']
|
|
80
|
+
post_payload['token'] = token
|
|
81
|
+
|
|
82
|
+
authorize = requests.post(
|
|
83
|
+
build_path(
|
|
84
|
+
base_url=MFARestAuthorizerEnv.AUTHORIZE_TOKEN_BASE_URL.get_from_env(),
|
|
85
|
+
path=MFARestAuthorizerEnv.AUTHORIZE_TOKEN_PATH.get_from_env()
|
|
86
|
+
),
|
|
87
|
+
json=post_payload,
|
|
88
|
+
timeout=30,
|
|
89
|
+
headers={"Authorization": f"Bearer {state['bearer_token']}"}
|
|
90
|
+
)
|
|
91
|
+
if authorize.status_code == 204:
|
|
92
|
+
_mfa['status'] = 'COMPLETED'
|
|
93
|
+
return True
|
|
94
|
+
else:
|
|
95
|
+
_mfa['status'] = 'FAILED'
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MFANodeConfig:
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def get_call_function_template(cls, source_node: str, next_node: str, mfa: dict):
|
|
103
|
+
return dict(
|
|
104
|
+
id=f"{source_node}_mfa_start",
|
|
105
|
+
action="call_function",
|
|
106
|
+
function="conversational_sop.authenticators.mfa.enforce_mfa_if_required",
|
|
107
|
+
output=f"{source_node}_mfa_start",
|
|
108
|
+
mfa=mfa,
|
|
109
|
+
transitions=[
|
|
110
|
+
dict(
|
|
111
|
+
condition=True,
|
|
112
|
+
next=source_node,
|
|
113
|
+
),
|
|
114
|
+
dict(
|
|
115
|
+
condition=False,
|
|
116
|
+
next=next_node,
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_validate_user_input(cls, source_node: str, next_node: str, model_name: str):
|
|
123
|
+
input_field_name = f"{source_node}_mfa_input"
|
|
124
|
+
return dict(
|
|
125
|
+
id=f"{source_node}_mfa_validate",
|
|
126
|
+
action="collect_input_with_agent",
|
|
127
|
+
description="Collect Input for MFA value",
|
|
128
|
+
field=input_field_name,
|
|
129
|
+
max_attempts=3,
|
|
130
|
+
validator="conversational_sop.authenticators.mfa.mfa_validate_user_input",
|
|
131
|
+
agent=dict(
|
|
132
|
+
name="MFA Input Data Collector",
|
|
133
|
+
model=model_name,
|
|
134
|
+
initial_message="{{_mfa.message}}",
|
|
135
|
+
instructions="""
|
|
136
|
+
You are an authentication value extractor. Your job is to identify and extract MFA codes from user input.
|
|
137
|
+
|
|
138
|
+
**Task:**
|
|
139
|
+
- Read the user's message
|
|
140
|
+
- Extract ONLY the OTP code value
|
|
141
|
+
- Output in the exact format shown below
|
|
142
|
+
|
|
143
|
+
**Output Format:**
|
|
144
|
+
MFA_CAPTURED:
|
|
145
|
+
|
|
146
|
+
"""),
|
|
147
|
+
transitions=[
|
|
148
|
+
dict(
|
|
149
|
+
pattern="MFA_CAPTURED:",
|
|
150
|
+
next=next_node
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from enum import Enum
|
|
2
3
|
|
|
3
4
|
|
|
@@ -57,3 +58,19 @@ DEFAULT_TIMEOUT = 300
|
|
|
57
58
|
|
|
58
59
|
MAX_ATTEMPTS_MESSAGE = "I'm having trouble understanding your {field}. Please contact customer service for assistance."
|
|
59
60
|
WORKFLOW_COMPLETE_MESSAGE = "Workflow completed."
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MFARestAuthorizerEnv(Enum):
|
|
64
|
+
GENERATE_TOKEN_BASE_URL = 'GENERATE_TOKEN_BASE_URL'
|
|
65
|
+
GENERATE_TOKEN_PATH = 'GENERATE_TOKEN_PATH'
|
|
66
|
+
VALIDATE_TOKEN_BASE_URL = 'VALIDATE_TOKEN_BASE_URL'
|
|
67
|
+
VALIDATE_TOKEN_PATH = 'VALIDATE_TOKEN_PATH'
|
|
68
|
+
AUTHORIZE_TOKEN_BASE_URL = 'AUTHORIZE_TOKEN_BASE_URL'
|
|
69
|
+
AUTHORIZE_TOKEN_PATH = 'AUTHORIZE_TOKEN_PATH'
|
|
70
|
+
|
|
71
|
+
API_TIMEOUT = 'API_TIMEOUT'
|
|
72
|
+
|
|
73
|
+
def get_from_env(self):
|
|
74
|
+
if self == MFARestAuthorizerEnv.API_TIMEOUT:
|
|
75
|
+
return int(os.getenv(self.value, '30'))
|
|
76
|
+
return os.getenv(self.value)
|
|
@@ -15,9 +15,10 @@ from ..utils.function import FunctionRepository
|
|
|
15
15
|
from ..utils.logger import logger
|
|
16
16
|
from ..utils.tool import ToolRepository
|
|
17
17
|
from ..validation import validate_workflow
|
|
18
|
-
|
|
18
|
+
from soprano_sdk.authenticators.mfa import MFANodeConfig
|
|
19
19
|
|
|
20
20
|
class WorkflowEngine:
|
|
21
|
+
|
|
21
22
|
def __init__(self, yaml_path: str, configs: dict):
|
|
22
23
|
self.yaml_path = yaml_path
|
|
23
24
|
self.configs = configs or {}
|
|
@@ -33,14 +34,16 @@ class WorkflowEngine:
|
|
|
33
34
|
self.workflow_name = self.config['name']
|
|
34
35
|
self.workflow_description = self.config['description']
|
|
35
36
|
self.workflow_version = self.config['version']
|
|
36
|
-
self.
|
|
37
|
-
self.steps = self.
|
|
37
|
+
self.mfa_node_indexes: list[int] = []
|
|
38
|
+
self.steps: list = self.load_steps()
|
|
39
|
+
self.step_map = {step['id']: step for step in self.steps}
|
|
40
|
+
self.data_fields = self.load_data()
|
|
41
|
+
|
|
38
42
|
self.outcomes = self.config['outcomes']
|
|
39
43
|
self.metadata = self.config.get('metadata', {})
|
|
40
44
|
|
|
41
45
|
self.StateType = create_state_model(self.data_fields)
|
|
42
46
|
|
|
43
|
-
self.step_map = {step['id']: step for step in self.steps}
|
|
44
47
|
self.outcome_map = {outcome['id']: outcome for outcome in self.outcomes}
|
|
45
48
|
|
|
46
49
|
self.function_repository = FunctionRepository()
|
|
@@ -196,6 +199,51 @@ class WorkflowEngine:
|
|
|
196
199
|
return tool_config.get('usage_policy')
|
|
197
200
|
|
|
198
201
|
|
|
202
|
+
def load_steps(self):
|
|
203
|
+
prepared_steps: list = []
|
|
204
|
+
previous_step: dict | None = None
|
|
205
|
+
for step in self.config['steps']:
|
|
206
|
+
step_id = step['id']
|
|
207
|
+
|
|
208
|
+
if mfa_config := step.get('mfa'):
|
|
209
|
+
mfa_data_collector = MFANodeConfig.get_validate_user_input(
|
|
210
|
+
next_node=step_id, model_name=mfa_config['model'],
|
|
211
|
+
source_node=step_id
|
|
212
|
+
)
|
|
213
|
+
mfa_start = MFANodeConfig.get_call_function_template(
|
|
214
|
+
source_node=step_id,
|
|
215
|
+
next_node=mfa_data_collector['id'],
|
|
216
|
+
mfa=mfa_config
|
|
217
|
+
)
|
|
218
|
+
prepared_steps.append(mfa_start)
|
|
219
|
+
prepared_steps.append(mfa_data_collector)
|
|
220
|
+
self.mfa_node_indexes.append(len(prepared_steps) - 1)
|
|
221
|
+
|
|
222
|
+
if previous_step and previous_step.get('transitions'):
|
|
223
|
+
for transition in previous_step.get('transitions'):
|
|
224
|
+
if transition.get('next') == step_id:
|
|
225
|
+
transition['next'] = mfa_start['id']
|
|
226
|
+
|
|
227
|
+
del step['mfa']
|
|
228
|
+
|
|
229
|
+
prepared_steps.append(step)
|
|
230
|
+
previous_step = step
|
|
231
|
+
return prepared_steps
|
|
232
|
+
|
|
233
|
+
def load_data(self):
|
|
234
|
+
data: list = self.config['data']
|
|
235
|
+
for mfa_node_index in self.mfa_node_indexes:
|
|
236
|
+
data.append(
|
|
237
|
+
dict(
|
|
238
|
+
name=f'{self.steps[mfa_node_index]['field']}',
|
|
239
|
+
type='text',
|
|
240
|
+
description='Input Recieved from the user during MFA'
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return data
|
|
245
|
+
|
|
246
|
+
|
|
199
247
|
def load_workflow(yaml_path: str, checkpointer=None, config=None) -> Tuple[CompiledStateGraph, WorkflowEngine]:
|
|
200
248
|
"""
|
|
201
249
|
Load a workflow from YAML configuration.
|
|
@@ -47,6 +47,8 @@ def create_state_model(data_fields: List[dict]):
|
|
|
47
47
|
fields['_node_field_map'] = Annotated[Dict[str, str], replace]
|
|
48
48
|
fields['_computed_fields'] = Annotated[List[str], replace]
|
|
49
49
|
fields['error'] = Annotated[Optional[Dict[str, str]], replace]
|
|
50
|
+
fields['_mfa'] = Annotated[Optional[Dict[str, str]], replace]
|
|
51
|
+
fields['mfa_input'] = Annotated[Optional[Dict[str, str]], replace]
|
|
50
52
|
|
|
51
53
|
return types.new_class('WorkflowState', (TypedDict,), {}, lambda ns: ns.update({'__annotations__': fields}))
|
|
52
54
|
|
|
@@ -61,7 +63,13 @@ def initialize_state(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
61
63
|
'_node_execution_order': [],
|
|
62
64
|
'_node_field_map': {},
|
|
63
65
|
'_computed_fields': [],
|
|
64
|
-
'error': None
|
|
66
|
+
'error': None,
|
|
67
|
+
'_mfa': {
|
|
68
|
+
'retry_count': 0,
|
|
69
|
+
'challengeType': None,
|
|
70
|
+
'status': None,
|
|
71
|
+
'message': None,
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
for field_name, default_value in fields_to_initialize.items():
|
|
@@ -18,10 +18,15 @@ class ActionStrategy(ABC):
|
|
|
18
18
|
def execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
19
|
pass
|
|
20
20
|
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def pre_execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
23
|
+
pass
|
|
24
|
+
|
|
21
25
|
def get_node_function(self) -> Callable[[Dict[str, Any]], Dict[str, Any]]:
|
|
22
26
|
def node_fn(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
23
27
|
logger.info(f"Executing node: {self.step_id} (action: {self.action})")
|
|
24
28
|
try:
|
|
29
|
+
self.pre_execute(state)
|
|
25
30
|
result = self.execute(state)
|
|
26
31
|
logger.info(f"Node {self.step_id} completed successfully")
|
|
27
32
|
logger.info(f"Result: {result}")
|