soprano-sdk 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
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/__init__.py +2 -0
- soprano_sdk/authenticators/mfa.py +37 -16
- soprano_sdk/core/constants.py +53 -15
- soprano_sdk/core/engine.py +21 -7
- soprano_sdk/core/state.py +1 -0
- soprano_sdk/nodes/call_function.py +1 -0
- soprano_sdk/nodes/collect_input.py +8 -1
- soprano_sdk/tools.py +9 -3
- soprano_sdk/validation/schema.py +14 -0
- soprano_sdk/validation/validator.py +34 -17
- {soprano_sdk-0.2.2.dist-info → soprano_sdk-0.2.4.dist-info}/METADATA +1 -1
- {soprano_sdk-0.2.2.dist-info → soprano_sdk-0.2.4.dist-info}/RECORD +14 -14
- {soprano_sdk-0.2.2.dist-info → soprano_sdk-0.2.4.dist-info}/WHEEL +0 -0
- {soprano_sdk-0.2.2.dist-info → soprano_sdk-0.2.4.dist-info}/licenses/LICENSE +0 -0
soprano_sdk/__init__.py
CHANGED
|
@@ -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,9 +70,12 @@ 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)
|
|
@@ -78,11 +90,11 @@ def mfa_validate_user_input(**state: dict):
|
|
|
78
90
|
|
|
79
91
|
authorize = requests.post(
|
|
80
92
|
build_path(
|
|
81
|
-
base_url=
|
|
82
|
-
path=
|
|
93
|
+
base_url=mfa_config.authorize_token_base_url,
|
|
94
|
+
path=mfa_config.authorize_token_path
|
|
83
95
|
),
|
|
84
96
|
json=post_payload,
|
|
85
|
-
timeout=
|
|
97
|
+
timeout=mfa_config.api_timeout,
|
|
86
98
|
headers={"Authorization": f"Bearer {state['bearer_token']}"}
|
|
87
99
|
)
|
|
88
100
|
if authorize.status_code == 204:
|
|
@@ -116,14 +128,18 @@ class MFANodeConfig:
|
|
|
116
128
|
)
|
|
117
129
|
|
|
118
130
|
@classmethod
|
|
119
|
-
def get_validate_user_input(cls, source_node: str, next_node: str,
|
|
131
|
+
def get_validate_user_input(cls, source_node: str, next_node: str, mfa_config: dict):
|
|
132
|
+
model_name = mfa_config['model']
|
|
133
|
+
max_attempts = mfa_config.get('max_attempts', 3)
|
|
134
|
+
on_max_attempts_reached = mfa_config.get('on_max_attempts_reached')
|
|
135
|
+
|
|
120
136
|
input_field_name = f"{source_node}_mfa_input"
|
|
121
|
-
|
|
137
|
+
node_config = dict(
|
|
122
138
|
id=f"{source_node}_mfa_validate",
|
|
123
139
|
action="collect_input_with_agent",
|
|
124
140
|
description="Collect Input for MFA value",
|
|
125
141
|
field=input_field_name,
|
|
126
|
-
max_attempts=
|
|
142
|
+
max_attempts=max_attempts,
|
|
127
143
|
validator="soprano_sdk.authenticators.mfa.mfa_validate_user_input",
|
|
128
144
|
agent=dict(
|
|
129
145
|
name="MFA Input Data Collector",
|
|
@@ -152,3 +168,8 @@ class MFANodeConfig:
|
|
|
152
168
|
)
|
|
153
169
|
]
|
|
154
170
|
)
|
|
171
|
+
|
|
172
|
+
if on_max_attempts_reached:
|
|
173
|
+
node_config['on_max_attempts_reached'] = on_max_attempts_reached
|
|
174
|
+
|
|
175
|
+
return node_config
|
soprano_sdk/core/constants.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class WorkflowKeys:
|
|
@@ -60,17 +62,53 @@ MAX_ATTEMPTS_MESSAGE = "I'm having trouble understanding your {field}. Please co
|
|
|
60
62
|
WORKFLOW_COMPLETE_MESSAGE = "Workflow completed."
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
class
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
)
|
soprano_sdk/core/engine.py
CHANGED
|
@@ -7,7 +7,7 @@ from langgraph.constants import START
|
|
|
7
7
|
from langgraph.graph import StateGraph
|
|
8
8
|
from langgraph.graph.state import CompiledStateGraph
|
|
9
9
|
|
|
10
|
-
from .constants import WorkflowKeys
|
|
10
|
+
from .constants import WorkflowKeys, MFAConfig
|
|
11
11
|
from .state import create_state_model
|
|
12
12
|
from ..nodes.factory import NodeFactory
|
|
13
13
|
from ..routing.router import WorkflowRouter
|
|
@@ -19,7 +19,7 @@ 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
|
+
def __init__(self, yaml_path: str, configs: dict, mfa_config: Optional[MFAConfig] = None):
|
|
23
23
|
self.yaml_path = yaml_path
|
|
24
24
|
self.configs = configs or {}
|
|
25
25
|
logger.info(f"Loading workflow from: {yaml_path}")
|
|
@@ -29,7 +29,7 @@ class WorkflowEngine:
|
|
|
29
29
|
self.config = yaml.safe_load(f)
|
|
30
30
|
|
|
31
31
|
logger.info("Validating workflow configuration")
|
|
32
|
-
validate_workflow(self.config)
|
|
32
|
+
validate_workflow(self.config, mfa_config=mfa_config or MFAConfig())
|
|
33
33
|
|
|
34
34
|
self.workflow_name = self.config['name']
|
|
35
35
|
self.workflow_description = self.config['description']
|
|
@@ -37,6 +37,7 @@ class WorkflowEngine:
|
|
|
37
37
|
self.mfa_validator_steps: set[str] = set()
|
|
38
38
|
self.steps: list = self.load_steps()
|
|
39
39
|
self.step_map = {step['id']: step for step in self.steps}
|
|
40
|
+
self.mfa_config = (mfa_config or MFAConfig()) if self.mfa_validator_steps else None
|
|
40
41
|
self.data_fields = self.load_data()
|
|
41
42
|
|
|
42
43
|
self.outcomes = self.config['outcomes']
|
|
@@ -208,8 +209,9 @@ class WorkflowEngine:
|
|
|
208
209
|
|
|
209
210
|
if mfa_config := step.get('mfa'):
|
|
210
211
|
mfa_data_collector = MFANodeConfig.get_validate_user_input(
|
|
211
|
-
next_node=step_id,
|
|
212
|
-
source_node=step_id
|
|
212
|
+
next_node=step_id,
|
|
213
|
+
source_node=step_id,
|
|
214
|
+
mfa_config=mfa_config
|
|
213
215
|
)
|
|
214
216
|
mfa_start = MFANodeConfig.get_call_function_template(
|
|
215
217
|
source_node=step_id,
|
|
@@ -259,7 +261,7 @@ class WorkflowEngine:
|
|
|
259
261
|
return data
|
|
260
262
|
|
|
261
263
|
|
|
262
|
-
def load_workflow(yaml_path: str, checkpointer=None, config=None) -> Tuple[CompiledStateGraph, WorkflowEngine]:
|
|
264
|
+
def load_workflow(yaml_path: str, checkpointer=None, config=None, mfa_config: Optional[MFAConfig] = None) -> Tuple[CompiledStateGraph, WorkflowEngine]:
|
|
263
265
|
"""
|
|
264
266
|
Load a workflow from YAML configuration.
|
|
265
267
|
|
|
@@ -270,6 +272,8 @@ def load_workflow(yaml_path: str, checkpointer=None, config=None) -> Tuple[Compi
|
|
|
270
272
|
checkpointer: Optional checkpointer for state persistence.
|
|
271
273
|
Defaults to InMemorySaver() if not provided.
|
|
272
274
|
Example: MongoDBSaver for production persistence.
|
|
275
|
+
config: Optional configuration dictionary
|
|
276
|
+
mfa_config: Optional MFA configuration. If not provided, will load from environment variables.
|
|
273
277
|
|
|
274
278
|
Returns:
|
|
275
279
|
Tuple of (compiled_graph, engine) where:
|
|
@@ -278,11 +282,21 @@ def load_workflow(yaml_path: str, checkpointer=None, config=None) -> Tuple[Compi
|
|
|
278
282
|
|
|
279
283
|
Example:
|
|
280
284
|
```python
|
|
285
|
+
# Load with environment variables
|
|
281
286
|
graph, engine = load_workflow("workflow.yaml")
|
|
287
|
+
|
|
288
|
+
# Or provide MFA configuration explicitly
|
|
289
|
+
from soprano_sdk.core.constants import MFAConfig
|
|
290
|
+
mfa_config = MFAConfig(
|
|
291
|
+
generate_token_base_url="https://api.example.com",
|
|
292
|
+
generate_token_path="/v1/mfa/generate"
|
|
293
|
+
)
|
|
294
|
+
graph, engine = load_workflow("workflow.yaml", mfa_config=mfa_config)
|
|
295
|
+
|
|
282
296
|
result = graph.invoke({}, config={"configurable": {"thread_id": "123"}})
|
|
283
297
|
message = engine.get_outcome_message(result)
|
|
284
298
|
```
|
|
285
299
|
"""
|
|
286
|
-
engine = WorkflowEngine(yaml_path, configs=config)
|
|
300
|
+
engine = WorkflowEngine(yaml_path, configs=config, mfa_config=mfa_config)
|
|
287
301
|
graph = engine.build_graph(checkpointer=checkpointer)
|
|
288
302
|
return graph, engine
|
soprano_sdk/core/state.py
CHANGED
|
@@ -48,6 +48,7 @@ def create_state_model(data_fields: List[dict]):
|
|
|
48
48
|
fields['_computed_fields'] = Annotated[List[str], replace]
|
|
49
49
|
fields['error'] = Annotated[Optional[Dict[str, str]], replace]
|
|
50
50
|
fields['_mfa'] = Annotated[Optional[Dict[str, str]], replace]
|
|
51
|
+
fields['_mfa_config'] = Annotated[Optional[Any], replace]
|
|
51
52
|
fields['mfa_input'] = Annotated[Optional[Dict[str, str]], replace]
|
|
52
53
|
|
|
53
54
|
return types.new_class('WorkflowState', (TypedDict,), {}, lambda ns: ns.update({'__annotations__': fields}))
|
|
@@ -39,6 +39,7 @@ class CallFunctionStrategy(ActionStrategy):
|
|
|
39
39
|
if 'mfa' in self.step_config:
|
|
40
40
|
state['_mfa'] = state.get('_mfa', {})
|
|
41
41
|
state['_mfa']['post_payload'] = dict(transactionId=str(uuid.uuid4()))
|
|
42
|
+
state['_mfa_config'] = self.engine_context.mfa_config
|
|
42
43
|
template_loader = self.engine_context.get_config_value("template_loader", Environment())
|
|
43
44
|
for k, v in self.step_config['mfa']['payload'].items():
|
|
44
45
|
state['_mfa']['post_payload'][k] = compile_values(template_loader, state, v)
|
|
@@ -92,6 +92,7 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
92
92
|
self.field = step_config.get('field')
|
|
93
93
|
self.agent_config = step_config.get('agent', {})
|
|
94
94
|
self.max_attempts = step_config.get('retry_limit') or engine_context.get_config_value("max_retry_limit", DEFAULT_MAX_ATTEMPTS)
|
|
95
|
+
self.on_max_attempts_reached = step_config.get('on_max_attempts_reached')
|
|
95
96
|
self.transitions = self._get_transitions()
|
|
96
97
|
self.next_step = self.step_config.get("next", None)
|
|
97
98
|
self.is_structured_output = self.agent_config.get("structured_output", {}).get("enabled", False)
|
|
@@ -116,6 +117,9 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
116
117
|
|
|
117
118
|
def pre_execute(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
118
119
|
state['_active_input_field'] = self.step_config.get('field')
|
|
120
|
+
# Inject MFA config for MFA validator nodes
|
|
121
|
+
if self.step_id in self.engine_context.mfa_validator_steps:
|
|
122
|
+
state['_mfa_config'] = self.engine_context.mfa_config
|
|
119
123
|
|
|
120
124
|
@property
|
|
121
125
|
def _formatted_field_name(self) -> str:
|
|
@@ -269,7 +273,10 @@ class CollectInputStrategy(ActionStrategy):
|
|
|
269
273
|
def _handle_max_attempts(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
270
274
|
logger.warning(f"Max attempts reached for field '{self.field}'")
|
|
271
275
|
self._set_status(state, 'max_attempts')
|
|
272
|
-
|
|
276
|
+
if self.on_max_attempts_reached:
|
|
277
|
+
message = self.on_max_attempts_reached
|
|
278
|
+
else:
|
|
279
|
+
message = MAX_ATTEMPTS_MESSAGE.format(field=self.field)
|
|
273
280
|
state[WorkflowKeys.MESSAGES] = [message]
|
|
274
281
|
return state
|
|
275
282
|
|
soprano_sdk/tools.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Workflow Tools - Wraps workflows as callable tools for agent frameworks
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
from __future__ import annotations
|
|
5
5
|
import uuid
|
|
6
6
|
from typing import Optional, Dict, Any
|
|
7
7
|
from .utils.logger import logger
|
|
@@ -9,6 +9,7 @@ from .utils.logger import logger
|
|
|
9
9
|
from langfuse.langchain import CallbackHandler
|
|
10
10
|
|
|
11
11
|
from .core.engine import load_workflow
|
|
12
|
+
from .core.constants import MFAConfig
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class WorkflowTool:
|
|
@@ -25,7 +26,8 @@ class WorkflowTool:
|
|
|
25
26
|
name: str,
|
|
26
27
|
description: str,
|
|
27
28
|
checkpointer=None,
|
|
28
|
-
config: Optional[Dict]=None
|
|
29
|
+
config: Optional[Dict]=None,
|
|
30
|
+
mfa_config: Optional[MFAConfig] = None
|
|
29
31
|
):
|
|
30
32
|
"""Initialize workflow tool
|
|
31
33
|
|
|
@@ -39,9 +41,13 @@ class WorkflowTool:
|
|
|
39
41
|
self.name = name
|
|
40
42
|
self.description = description
|
|
41
43
|
self.checkpointer = checkpointer
|
|
44
|
+
self.mfa_config = mfa_config
|
|
42
45
|
|
|
43
46
|
# Load workflow
|
|
44
|
-
self.graph, self.engine = load_workflow(
|
|
47
|
+
self.graph, self.engine = load_workflow(
|
|
48
|
+
yaml_path, checkpointer=checkpointer,
|
|
49
|
+
config=config, mfa_config=mfa_config
|
|
50
|
+
)
|
|
45
51
|
|
|
46
52
|
def execute(
|
|
47
53
|
self,
|
soprano_sdk/validation/schema.py
CHANGED
|
@@ -90,6 +90,10 @@ WORKFLOW_SCHEMA = {
|
|
|
90
90
|
"maximum": 20,
|
|
91
91
|
"description": "Maximum attempts (for collect_input_with_agent)"
|
|
92
92
|
},
|
|
93
|
+
"on_max_attempts_reached": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Custom error message to display when max attempts are exhausted (for collect_input_with_agent)"
|
|
96
|
+
},
|
|
93
97
|
"agent": {
|
|
94
98
|
"type": "object",
|
|
95
99
|
"description": "Agent configuration (for collect_input_with_agent)",
|
|
@@ -245,6 +249,16 @@ WORKFLOW_SCHEMA = {
|
|
|
245
249
|
"type": "object",
|
|
246
250
|
"description": "MFA payload data that is posted to the RESTAPI, Apart from the properties provided transactionId is sent by the framework in the post payload as an additional property, transactionId is the same throughout the MFA process",
|
|
247
251
|
"additionalProperties": True
|
|
252
|
+
},
|
|
253
|
+
"max_attempts": {
|
|
254
|
+
"type": "integer",
|
|
255
|
+
"minimum": 1,
|
|
256
|
+
"maximum": 20,
|
|
257
|
+
"description": "Maximum number of attempts allowed for MFA validation (default: 3)"
|
|
258
|
+
},
|
|
259
|
+
"on_max_attempts_reached": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"description": "Custom error message to display when MFA max attempts are exhausted"
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
264
|
},
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from typing import List, Set
|
|
1
|
+
from typing import List, Set, Optional, TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
import jsonschema
|
|
4
4
|
|
|
5
5
|
from .schema import WORKFLOW_SCHEMA
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from soprano_sdk.core.constants import MFAConfig
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class ValidationResult:
|
|
@@ -21,8 +23,9 @@ class ValidationResult:
|
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class WorkflowValidator:
|
|
24
|
-
def __init__(self, config: dict):
|
|
26
|
+
def __init__(self, config: dict, mfa_config: Optional['MFAConfig'] = None):
|
|
25
27
|
self.config = config
|
|
28
|
+
self.mfa_config = mfa_config
|
|
26
29
|
self.errors: List[str] = []
|
|
27
30
|
|
|
28
31
|
def validate(self) -> ValidationResult:
|
|
@@ -110,20 +113,10 @@ class WorkflowValidator:
|
|
|
110
113
|
)
|
|
111
114
|
|
|
112
115
|
def _validate_authorizer(self, step):
|
|
113
|
-
|
|
114
|
-
def _validate_rest_fields():
|
|
115
|
-
if mfa_authorizer['type'] == 'REST':
|
|
116
|
-
for field in MFARestAuthorizerEnv:
|
|
117
|
-
is_present = field.get_from_env()
|
|
118
|
-
if not is_present:
|
|
119
|
-
self.errors.append(f"`{field.value}` needs to be set as Environment for REST MFA")
|
|
120
|
-
else:
|
|
121
|
-
self.errors.append(f"step({step['id']}) -> mfa -> type is unsupported")
|
|
122
|
-
|
|
123
116
|
mfa_authorizer = step.get("mfa", None)
|
|
124
117
|
if mfa_authorizer is None:
|
|
125
118
|
return
|
|
126
|
-
|
|
119
|
+
|
|
127
120
|
if mfa_authorizer and step['action'] != 'call_function':
|
|
128
121
|
self.errors.append(
|
|
129
122
|
f"MFA is enabled in step({step['id']}). MFA is supported only for `call_function` nodes"
|
|
@@ -133,7 +126,31 @@ class WorkflowValidator:
|
|
|
133
126
|
if not model:
|
|
134
127
|
self.errors.append(f"step({step['id']}) -> mfa -> model is missing")
|
|
135
128
|
|
|
136
|
-
|
|
129
|
+
mfa_type = mfa_authorizer.get('type')
|
|
130
|
+
if mfa_type and mfa_type != 'REST':
|
|
131
|
+
self.errors.append(f"step({step['id']}) -> mfa -> type '{mfa_type}' is unsupported. Only 'REST' is supported.")
|
|
132
|
+
|
|
133
|
+
# Validate mfa_config if provided
|
|
134
|
+
if self.mfa_config is not None:
|
|
135
|
+
missing_fields = []
|
|
136
|
+
if not self.mfa_config.generate_token_base_url:
|
|
137
|
+
missing_fields.append('generate_token_base_url')
|
|
138
|
+
if not self.mfa_config.generate_token_path:
|
|
139
|
+
missing_fields.append('generate_token_path')
|
|
140
|
+
if not self.mfa_config.validate_token_base_url:
|
|
141
|
+
missing_fields.append('validate_token_base_url')
|
|
142
|
+
if not self.mfa_config.validate_token_path:
|
|
143
|
+
missing_fields.append('validate_token_path')
|
|
144
|
+
if not self.mfa_config.authorize_token_base_url:
|
|
145
|
+
missing_fields.append('authorize_token_base_url')
|
|
146
|
+
if not self.mfa_config.authorize_token_path:
|
|
147
|
+
missing_fields.append('authorize_token_path')
|
|
148
|
+
|
|
149
|
+
if missing_fields:
|
|
150
|
+
self.errors.append(
|
|
151
|
+
f"MFA configuration is missing required fields: {', '.join(missing_fields)}. "
|
|
152
|
+
f"Either provide them via mfa_config parameter or set corresponding environment variables."
|
|
153
|
+
)
|
|
137
154
|
|
|
138
155
|
|
|
139
156
|
def _validate_data_fields(self):
|
|
@@ -200,8 +217,8 @@ class WorkflowValidator:
|
|
|
200
217
|
)
|
|
201
218
|
|
|
202
219
|
|
|
203
|
-
def validate_workflow(config: dict) -> ValidationResult:
|
|
204
|
-
validator = WorkflowValidator(config)
|
|
220
|
+
def validate_workflow(config: dict, mfa_config: Optional['MFAConfig'] = None) -> ValidationResult:
|
|
221
|
+
validator = WorkflowValidator(config, mfa_config=mfa_config)
|
|
205
222
|
result = validator.validate()
|
|
206
223
|
|
|
207
224
|
if not result.is_valid:
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
soprano_sdk/__init__.py,sha256=
|
|
1
|
+
soprano_sdk/__init__.py,sha256=YZVl_SwQ0C-E_5_f1AwUe_hPcbgCt8k7k4_WAHM8vjE,243
|
|
2
2
|
soprano_sdk/engine.py,sha256=EFK91iTHjp72otLN6Kg-yeLye2J3CAKN0QH4FI2taL8,14838
|
|
3
|
-
soprano_sdk/tools.py,sha256=
|
|
3
|
+
soprano_sdk/tools.py,sha256=v2QGEaXxKzCBZCTdmhZXMXiVcwccJivdQdCxETkp-lQ,7656
|
|
4
4
|
soprano_sdk/agents/__init__.py,sha256=Yzbtv6iP_ABRgZo0IUjy9vDofEvLFbOjuABw758176A,636
|
|
5
5
|
soprano_sdk/agents/adaptor.py,sha256=Cm02YKFclrESu-Qq4CTknCgU7KaA7Z_2FspnQDkEVfU,3214
|
|
6
6
|
soprano_sdk/agents/factory.py,sha256=Aucfz4rZVKCXMAQtbGAqp1JR8aYwa66mokRmKkKGhYA,6699
|
|
7
7
|
soprano_sdk/agents/structured_output.py,sha256=7DSVzfMPsZAqBwI3v6XL15qG5Gh4jJ-qddcVPaa3gdc,3326
|
|
8
8
|
soprano_sdk/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
soprano_sdk/authenticators/mfa.py,sha256=
|
|
9
|
+
soprano_sdk/authenticators/mfa.py,sha256=7aTueLu2jLxl213Q5_BkNQ534RgEwf6lodQa_PaJFWs,5980
|
|
10
10
|
soprano_sdk/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
soprano_sdk/core/constants.py,sha256
|
|
12
|
-
soprano_sdk/core/engine.py,sha256=
|
|
11
|
+
soprano_sdk/core/constants.py,sha256=F7gL3hkpq5mZgbgiBcEERmzxYJ4zZG2ZFx1YQp5Z580,3130
|
|
12
|
+
soprano_sdk/core/engine.py,sha256=UTFJimrgUzfdDZgc4rW5nvkOKevAEGW8C5Zr1D0tgcs,11270
|
|
13
13
|
soprano_sdk/core/rollback_strategies.py,sha256=NjDTtBCZlqyDql5PSwI9SMDLK7_BNlTxbW_cq_5gV0g,7783
|
|
14
|
-
soprano_sdk/core/state.py,sha256=
|
|
14
|
+
soprano_sdk/core/state.py,sha256=k8ojLfWgjES3p9XWMeGU5s4UK-Xa5T8mS4VtZzTrcDw,2961
|
|
15
15
|
soprano_sdk/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
soprano_sdk/nodes/base.py,sha256=idFyOGGPnjsASYnrOF_NIh7eFcSuJqw61EoVN_WCTaU,2360
|
|
17
|
-
soprano_sdk/nodes/call_function.py,sha256=
|
|
18
|
-
soprano_sdk/nodes/collect_input.py,sha256=
|
|
17
|
+
soprano_sdk/nodes/call_function.py,sha256=PdmSSVXkMT5ptxeUjmKqND-wvqbaXp1i61QzIzFOfgQ,5535
|
|
18
|
+
soprano_sdk/nodes/collect_input.py,sha256=lEltZOU5ALvc57q8I_4SjzzEapVejy9mS0E73Jf7-sk,23759
|
|
19
19
|
soprano_sdk/nodes/factory.py,sha256=l-Gysfgnao-o2dphhnbjjxcH3ojZanZNYN3CBH9dDbA,1624
|
|
20
20
|
soprano_sdk/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
soprano_sdk/routing/router.py,sha256=SrNciTIXXdC9bAbbO5bX7PN9mlRbITjr4RZdNm4jEVA,3450
|
|
@@ -26,9 +26,9 @@ soprano_sdk/utils/template.py,sha256=MG_B9TMx1ShpnSGo7s7TO-VfQzuFByuRNhJTvZ668kM
|
|
|
26
26
|
soprano_sdk/utils/tool.py,sha256=hWN826HIKmLdswLCTURLH8hWlb2WU0MB8nIUErbpB-8,1877
|
|
27
27
|
soprano_sdk/utils/tracing.py,sha256=gSHeBDLe-MbAZ9rkzpCoGFveeMdR9KLaA6tteB0IWjk,1991
|
|
28
28
|
soprano_sdk/validation/__init__.py,sha256=ImChmO86jYHU90xzTttto2-LmOUOmvY_ibOQaLRz5BA,262
|
|
29
|
-
soprano_sdk/validation/schema.py,sha256=
|
|
30
|
-
soprano_sdk/validation/validator.py,sha256=
|
|
31
|
-
soprano_sdk-0.2.
|
|
32
|
-
soprano_sdk-0.2.
|
|
33
|
-
soprano_sdk-0.2.
|
|
34
|
-
soprano_sdk-0.2.
|
|
29
|
+
soprano_sdk/validation/schema.py,sha256=7B60_8xIt5bLXTYEGEDwU2DwmQn2f4QFyyIca2FYM9s,15420
|
|
30
|
+
soprano_sdk/validation/validator.py,sha256=GaCvHvjwVe88Z8yatQsueiPnqtq1oo5uN75gogzpQT0,8940
|
|
31
|
+
soprano_sdk-0.2.4.dist-info/METADATA,sha256=W-e-Si4pewUkpuWewKz_pmkWvUeRVR8HPwQ-Z-7tpTQ,11297
|
|
32
|
+
soprano_sdk-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
soprano_sdk-0.2.4.dist-info/licenses/LICENSE,sha256=A1aBauSjPNtVehOXJe3WuvdU2xvM9H8XmigFMm6665s,1073
|
|
34
|
+
soprano_sdk-0.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|