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.
Files changed (96) hide show
  1. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/PKG-INFO +1 -1
  2. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/pyproject.toml +1 -1
  3. soprano_sdk-0.2.0/soprano_sdk/authenticators/mfa.py +153 -0
  4. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/constants.py +17 -0
  5. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/engine.py +52 -4
  6. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/state.py +9 -1
  7. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/base.py +5 -0
  8. soprano_sdk-0.2.0/soprano_sdk/nodes/call_api.py +651 -0
  9. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/call_function.py +22 -2
  10. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/collect_input.py +3 -0
  11. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/tools.py +0 -2
  12. soprano_sdk-0.2.0/soprano_sdk/utils/__init__.py +0 -0
  13. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/schema.py +22 -1
  14. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/validator.py +38 -1
  15. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.github/workflows/test_build_and_publish.yaml +0 -0
  16. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.gitignore +0 -0
  17. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/.python-version +0 -0
  18. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/CLAUDE.md +0 -0
  19. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/LICENSE +0 -0
  20. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/README.md +0 -0
  21. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/framework_example.yaml +0 -0
  22. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/greeting_functions.py +0 -0
  23. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/greeting_workflow.yaml +0 -0
  24. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/main.py +0 -0
  25. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/README.md +0 -0
  26. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/conversation_based.py +0 -0
  27. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/entity_based.py +0 -0
  28. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/persistence/mongodb_demo.py +0 -0
  29. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/return_functions.py +0 -0
  30. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/return_workflow.yaml +0 -0
  31. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/structured_output_example.yaml +0 -0
  32. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/README.md +0 -0
  33. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/crewai_supervisor_ui.py +0 -0
  34. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/langgraph_supervisor_ui.py +0 -0
  35. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/__init__.py +0 -0
  36. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/crewai_tools.py +0 -0
  37. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/tools/langgraph_tools.py +0 -0
  38. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/supervisors/workflow_tools.py +0 -0
  39. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/tools/__init__.py +0 -0
  40. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/tools/address.py +0 -0
  41. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/examples/validator.py +0 -0
  42. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_demo.py +0 -0
  43. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_selfloop_demo.py +0 -0
  44. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/langgraph_v.py +0 -0
  45. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/main.py +0 -0
  46. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/return_fsm.excalidraw +0 -0
  47. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/return_state_machine.png +0 -0
  48. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/legacy/ui.py +0 -0
  49. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/visualize_workflow.py +0 -0
  50. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/workflow_demo.py +0 -0
  51. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/scripts/workflow_demo_ui.py +0 -0
  52. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/__init__.py +0 -0
  53. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/__init__.py +0 -0
  54. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/adaptor.py +0 -0
  55. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/factory.py +0 -0
  56. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/agents/structured_output.py +0 -0
  57. {soprano_sdk-0.1.100/soprano_sdk/core → soprano_sdk-0.2.0/soprano_sdk/authenticators}/__init__.py +0 -0
  58. {soprano_sdk-0.1.100/soprano_sdk/nodes → soprano_sdk-0.2.0/soprano_sdk/core}/__init__.py +0 -0
  59. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/core/rollback_strategies.py +0 -0
  60. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/engine.py +0 -0
  61. {soprano_sdk-0.1.100/soprano_sdk/routing → soprano_sdk-0.2.0/soprano_sdk/nodes}/__init__.py +0 -0
  62. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/nodes/factory.py +0 -0
  63. {soprano_sdk-0.1.100/soprano_sdk/utils → soprano_sdk-0.2.0/soprano_sdk/routing}/__init__.py +0 -0
  64. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/routing/router.py +0 -0
  65. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/function.py +0 -0
  66. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/logger.py +0 -0
  67. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/template.py +0 -0
  68. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/tool.py +0 -0
  69. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/utils/tracing.py +0 -0
  70. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/soprano_sdk/validation/__init__.py +0 -0
  71. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/debug_jinja2.py +0 -0
  72. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_agent_factory.py +0 -0
  73. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_collect_input_refactor.py +0 -0
  74. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_external_values.py +0 -0
  75. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_inputs_validation.py +0 -0
  76. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_jinja2_path.py +0 -0
  77. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_jinja2_standalone.py +0 -0
  78. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_persistence.py +0 -0
  79. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_structured_output.py +0 -0
  80. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/tests/test_transition_routing.py +0 -0
  81. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/todo.md +0 -0
  82. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/uv.lock +0 -0
  83. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/.eslintrc.cjs +0 -0
  84. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/.gitignore +0 -0
  85. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/README.md +0 -0
  86. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/index.html +0 -0
  87. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/package-lock.json +0 -0
  88. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/package.json +0 -0
  89. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/App.jsx +0 -0
  90. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/CustomNode.jsx +0 -0
  91. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/StepDetailsModal.jsx +0 -0
  92. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/WorkflowGraph.jsx +0 -0
  93. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/WorkflowInfoPanel.jsx +0 -0
  94. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/assets/react.svg +0 -0
  95. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/workflow-visualizer/src/main.jsx +0 -0
  96. {soprano_sdk-0.1.100 → soprano_sdk-0.2.0}/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.1.100
3
+ Version: 0.2.0
4
4
  Summary: YAML-driven workflow engine with AI agent integration for building conversational SOPs
5
5
  Author: Arvind Thangamani
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soprano-sdk"
7
- version = "0.1.100"
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.data_fields = self.config['data']
37
- self.steps = self.config['steps']
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}")