vellum-ai 0.10.7__py3-none-any.whl → 0.10.9__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.
Files changed (41) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/types/logical_operator.py +2 -0
  3. vellum/workflows/descriptors/utils.py +27 -0
  4. vellum/workflows/events/__init__.py +0 -2
  5. vellum/workflows/events/tests/test_event.py +2 -1
  6. vellum/workflows/events/types.py +36 -30
  7. vellum/workflows/events/workflow.py +14 -7
  8. vellum/workflows/nodes/bases/base.py +100 -38
  9. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -0
  10. vellum/workflows/nodes/core/templating_node/node.py +5 -0
  11. vellum/workflows/nodes/core/try_node/node.py +22 -4
  12. vellum/workflows/nodes/core/try_node/tests/test_node.py +15 -0
  13. vellum/workflows/nodes/displayable/api_node/node.py +1 -1
  14. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +1 -2
  15. vellum/workflows/nodes/displayable/code_execution_node/node.py +1 -2
  16. vellum/workflows/nodes/displayable/code_execution_node/utils.py +13 -2
  17. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +10 -3
  18. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +6 -1
  19. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +1 -2
  20. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +1 -2
  21. vellum/workflows/runner/runner.py +141 -32
  22. vellum/workflows/state/base.py +55 -21
  23. vellum/workflows/state/context.py +26 -3
  24. vellum/workflows/types/__init__.py +5 -0
  25. vellum/workflows/types/core.py +1 -1
  26. vellum/workflows/workflows/base.py +51 -17
  27. vellum/workflows/workflows/event_filters.py +61 -0
  28. {vellum_ai-0.10.7.dist-info → vellum_ai-0.10.9.dist-info}/METADATA +1 -1
  29. {vellum_ai-0.10.7.dist-info → vellum_ai-0.10.9.dist-info}/RECORD +40 -38
  30. vellum_cli/__init__.py +23 -4
  31. vellum_cli/pull.py +28 -13
  32. vellum_cli/tests/test_pull.py +45 -2
  33. vellum_ee/workflows/display/nodes/base_node_display.py +1 -1
  34. vellum_ee/workflows/display/nodes/vellum/__init__.py +6 -4
  35. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +17 -2
  36. vellum_ee/workflows/display/nodes/vellum/error_node.py +49 -0
  37. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +203 -0
  38. vellum/workflows/events/utils.py +0 -5
  39. {vellum_ai-0.10.7.dist-info → vellum_ai-0.10.9.dist-info}/LICENSE +0 -0
  40. {vellum_ai-0.10.7.dist-info → vellum_ai-0.10.9.dist-info}/WHEEL +0 -0
  41. {vellum_ai-0.10.7.dist-info → vellum_ai-0.10.9.dist-info}/entry_points.txt +0 -0
vellum_cli/pull.py CHANGED
@@ -7,28 +7,38 @@ from typing import Optional
7
7
  from dotenv import load_dotenv
8
8
 
9
9
  from vellum.workflows.vellum_client import create_vellum_client
10
- from vellum_cli.config import load_vellum_cli_config
10
+ from vellum_cli.config import WorkflowConfig, load_vellum_cli_config
11
11
  from vellum_cli.logger import load_cli_logger
12
12
 
13
13
 
14
14
  def pull_command(
15
- module: Optional[str], legacy_module: Optional[bool] = None, include_json: Optional[bool] = None
15
+ module: Optional[str] = None,
16
+ workflow_sandbox_id: Optional[str] = None,
17
+ include_json: Optional[bool] = None,
18
+ exclude_code: Optional[bool] = None,
16
19
  ) -> None:
17
20
  load_dotenv()
18
21
  logger = load_cli_logger()
19
22
  config = load_vellum_cli_config()
20
23
 
21
- if not config.workflows:
22
- raise ValueError("No Workflows found in project to pull.")
23
-
24
- if len(config.workflows) > 1 and not module:
25
- raise ValueError("Multiple workflows found in project to pull. Pulling only a single workflow is supported.")
26
-
27
24
  workflow_config = (
28
- next((w for w in config.workflows if w.module == module), None) if module else config.workflows[0]
25
+ next((w for w in config.workflows if w.module == module), None)
26
+ if module
27
+ else (config.workflows[0] if config.workflows else None)
29
28
  )
29
+ save_lock_file = False
30
30
  if workflow_config is None:
31
- raise ValueError(f"No workflow config for '{module}' found in project to push.")
31
+ if module:
32
+ raise ValueError(f"No workflow config for '{module}' found in project to pull.")
33
+ elif workflow_sandbox_id:
34
+ workflow_config = WorkflowConfig(
35
+ workflow_sandbox_id=workflow_sandbox_id,
36
+ module=f"workflow_{workflow_sandbox_id.split('-')[0]}",
37
+ )
38
+ config.workflows.append(workflow_config)
39
+ save_lock_file = True
40
+ else:
41
+ raise ValueError("No workflow config found in project to pull from.")
32
42
 
33
43
  if not workflow_config.workflow_sandbox_id:
34
44
  raise ValueError("No workflow sandbox ID found in project to pull from.")
@@ -36,10 +46,10 @@ def pull_command(
36
46
  logger.info(f"Pulling workflow into {workflow_config.module}")
37
47
  client = create_vellum_client()
38
48
  query_parameters = {}
39
- if legacy_module:
40
- query_parameters["legacyModule"] = legacy_module
41
49
  if include_json:
42
50
  query_parameters["include_json"] = include_json
51
+ if exclude_code:
52
+ query_parameters["exclude_code"] = exclude_code
43
53
 
44
54
  response = client.workflows.pull(
45
55
  workflow_config.workflow_sandbox_id,
@@ -81,6 +91,11 @@ def pull_command(
81
91
  target.write(source.read().decode("utf-8"))
82
92
 
83
93
  if include_json:
84
- logger.warning("The pulled JSON representation of the Workflow should be used for debugging purposely only. Its schema should be considered unstable and subject to change at any time.")
94
+ logger.warning(
95
+ "The pulled JSON representation of the Workflow should be used for debugging purposely only. Its schema should be considered unstable and subject to change at any time."
96
+ )
97
+
98
+ if save_lock_file:
99
+ config.save()
85
100
 
86
101
  logger.info(f"Successfully pulled Workflow into {workflow_config.module}")
@@ -69,8 +69,33 @@ def test_pull(vellum_client, mock_module):
69
69
  pull_command(module)
70
70
 
71
71
  # THEN the workflow.py file is written to the module directory
72
- assert os.path.exists(os.path.join(temp_dir, *module.split("."), "workflow.py"))
73
- with open(os.path.join(temp_dir, *module.split("."), "workflow.py")) as f:
72
+ workflow_py = os.path.join(temp_dir, *module.split("."), "workflow.py")
73
+ assert os.path.exists(workflow_py)
74
+ with open(workflow_py) as f:
75
+ assert f.read() == "print('hello')"
76
+
77
+
78
+ def test_pull__sandbox_id_with_no_config(vellum_client):
79
+ # GIVEN a workflow sandbox id
80
+ workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
81
+
82
+ # AND the workflow pull API call returns a zip file
83
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
84
+
85
+ # AND we are currently in a new directory
86
+ current_dir = os.getcwd()
87
+ temp_dir = tempfile.mkdtemp()
88
+ os.chdir(temp_dir)
89
+
90
+ # WHEN the user runs the pull command with the workflow sandbox id and no module
91
+ pull_command(workflow_sandbox_id=workflow_sandbox_id)
92
+ os.chdir(current_dir)
93
+
94
+ # THEN the pull api is called with exclude_code=True
95
+ vellum_client.workflows.pull.assert_called_once()
96
+ workflow_py = os.path.join(temp_dir, "workflow_87654321", "workflow.py")
97
+ assert os.path.exists(workflow_py)
98
+ with open(workflow_py) as f:
74
99
  assert f.read() == "print('hello')"
75
100
 
76
101
 
@@ -168,3 +193,21 @@ def test_pull__include_json(vellum_client, mock_module):
168
193
  vellum_client.workflows.pull.assert_called_once()
169
194
  call_args = vellum_client.workflows.pull.call_args.kwargs
170
195
  assert call_args["request_options"]["additional_query_parameters"] == {"include_json": True}
196
+
197
+
198
+ def test_pull__exclude_code(vellum_client, mock_module):
199
+ # GIVEN a module on the user's filesystem
200
+ _, module = mock_module
201
+
202
+ # AND the workflow pull API call returns a zip file
203
+ vellum_client.workflows.pull.return_value = iter(
204
+ [zip_file_map({"workflow.py": "print('hello')", "workflow.json": "{}"})]
205
+ )
206
+
207
+ # WHEN the user runs the pull command
208
+ pull_command(module, exclude_code=True)
209
+
210
+ # THEN the pull api is called with exclude_code=True
211
+ vellum_client.workflows.pull.assert_called_once()
212
+ call_args = vellum_client.workflows.pull.call_args.kwargs
213
+ assert call_args["request_options"]["additional_query_parameters"] == {"exclude_code": True}
@@ -1,7 +1,7 @@
1
1
  from functools import cached_property
2
2
  import inspect
3
3
  from uuid import UUID
4
- from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, get_args, get_origin, cast
4
+ from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, cast, get_args, get_origin
5
5
 
6
6
  from vellum.workflows.nodes.bases.base import BaseNode
7
7
  from vellum.workflows.nodes.utils import get_wrapped_node, has_wrapped_node
@@ -1,6 +1,7 @@
1
1
  from .api_node import BaseAPINodeDisplay
2
2
  from .code_execution_node import BaseCodeExecutionNodeDisplay
3
3
  from .conditional_node import BaseConditionalNodeDisplay
4
+ from .error_node import BaseErrorNodeDisplay
4
5
  from .final_output_node import BaseFinalOutputNodeDisplay
5
6
  from .guardrail_node import BaseGuardrailNodeDisplay
6
7
  from .inline_prompt_node import BaseInlinePromptNodeDisplay
@@ -16,19 +17,20 @@ from .try_node import BaseTryNodeDisplay
16
17
 
17
18
  # All node display classes must be imported here to be registered in BaseNodeDisplay's node display registry
18
19
  __all__ = [
20
+ "BaseAPINodeDisplay",
19
21
  "BaseCodeExecutionNodeDisplay",
20
22
  "BaseConditionalNodeDisplay",
23
+ "BaseErrorNodeDisplay",
24
+ "BaseFinalOutputNodeDisplay",
21
25
  "BaseGuardrailNodeDisplay",
22
26
  "BaseInlinePromptNodeDisplay",
23
27
  "BaseInlineSubworkflowNodeDisplay",
24
- "BaseAPINodeDisplay",
25
28
  "BaseMapNodeDisplay",
26
29
  "BaseMergeNodeDisplay",
27
30
  "BaseNoteNodeDisplay",
31
+ "BasePromptDeploymentNodeDisplay",
28
32
  "BaseSearchNodeDisplay",
29
33
  "BaseSubworkflowDeploymentNodeDisplay",
30
34
  "BaseTemplatingNodeDisplay",
31
- "BasePromptDeploymentNodeDisplay",
32
- "BaseFinalOutputNodeDisplay",
33
- "BaseTryNodeDisplay",
35
+ "BaseTryNodeDisplay"
34
36
  ]
@@ -1,5 +1,5 @@
1
1
  from uuid import UUID
2
- from typing import ClassVar, Generic, Optional, TypeVar
2
+ from typing import ClassVar, Dict, Generic, Optional, TypeVar
3
3
 
4
4
  from vellum.workflows.nodes.displayable.code_execution_node import CodeExecutionNode
5
5
  from vellum.workflows.nodes.displayable.code_execution_node.utils import read_file_from_path
@@ -20,6 +20,8 @@ class BaseCodeExecutionNodeDisplay(BaseNodeVellumDisplay[_CodeExecutionNodeType]
20
20
  output_id: ClassVar[Optional[UUID]] = None
21
21
  log_output_id: ClassVar[Optional[UUID]] = None
22
22
 
23
+ node_input_ids_by_name: ClassVar[Dict[str, UUID]] = {}
24
+
23
25
  def serialize(
24
26
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
25
27
  ) -> JsonObject:
@@ -27,6 +29,19 @@ class BaseCodeExecutionNodeDisplay(BaseNodeVellumDisplay[_CodeExecutionNodeType]
27
29
  node_id = self.node_id
28
30
 
29
31
  code = read_file_from_path(raise_if_descriptor(node.filepath))
32
+ code_inputs = raise_if_descriptor(node.code_inputs)
33
+
34
+ inputs = [
35
+ create_node_input(
36
+ node_id=node_id,
37
+ input_name=variable_name,
38
+ value=variable_value,
39
+ display_context=display_context,
40
+ input_id=self.node_input_ids_by_name.get(variable_name),
41
+ )
42
+ for variable_name, variable_value in code_inputs.items()
43
+ ]
44
+
30
45
  code_node_input = create_node_input(
31
46
  node_id=node_id,
32
47
  input_name="code",
@@ -41,7 +56,7 @@ class BaseCodeExecutionNodeDisplay(BaseNodeVellumDisplay[_CodeExecutionNodeType]
41
56
  display_context=display_context,
42
57
  input_id=self.runtime_input_id,
43
58
  )
44
- inputs = [code_node_input, runtime_node_input]
59
+ inputs.extend([code_node_input, runtime_node_input])
45
60
 
46
61
  packages = raise_if_descriptor(node.packages)
47
62
 
@@ -0,0 +1,49 @@
1
+ from uuid import UUID
2
+ from typing import Any, ClassVar, Dict, Generic, Optional, TypeVar
3
+
4
+ from vellum.workflows.nodes import ErrorNode
5
+ from vellum.workflows.types.core import EntityInputsInterface, Json, JsonObject
6
+ from vellum_ee.workflows.display.nodes.base_node_vellum_display import BaseNodeVellumDisplay
7
+ from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
8
+ from vellum_ee.workflows.display.types import WorkflowDisplayContext
9
+
10
+ _ErrorNodeType = TypeVar("_ErrorNodeType", bound=ErrorNode)
11
+
12
+ class BaseErrorNodeDisplay(BaseNodeVellumDisplay[_ErrorNodeType], Generic[_ErrorNodeType]):
13
+ error_output_id: ClassVar[Optional[UUID]] = None
14
+ error_inputs_by_name: ClassVar[Dict[str, Any]] = {}
15
+ name: ClassVar[str] = "error-node"
16
+
17
+ def serialize(
18
+ self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
19
+ ) -> JsonObject:
20
+ node = self._node
21
+ node_id = self.node_id
22
+ error_source_input_id = self.node_input_ids_by_name.get("error_source_input_id")
23
+
24
+ node_inputs = [
25
+ create_node_input(
26
+ node_id=node_id,
27
+ input_name=variable_name,
28
+ value=variable_value,
29
+ display_context=display_context,
30
+ input_id=self.node_input_ids_by_name.get(variable_name),
31
+ )
32
+ for variable_name, variable_value in self.error_inputs_by_name.items()
33
+ ]
34
+
35
+ return {
36
+ "id": str(node_id),
37
+ "type": "ERROR",
38
+ "inputs": [node_input.dict() for node_input in node_inputs],
39
+ "data": {
40
+ "name": self.name,
41
+ "label": self.label,
42
+ "source_handle_id": str(self.get_source_handle_id(display_context.port_displays)),
43
+ "target_handle_id": str(self.get_target_handle_id()),
44
+ "error_source_input_id": str(error_source_input_id),
45
+ "error_output_id": str(self.error_output_id),
46
+ },
47
+ "display_data": self.get_display_data().dict(),
48
+ "definition": self.get_definition().dict(),
49
+ }
@@ -0,0 +1,203 @@
1
+ from unittest import mock
2
+
3
+ from deepdiff import DeepDiff
4
+
5
+ from vellum_ee.workflows.display.nodes.base_node_vellum_display import BaseNodeVellumDisplay
6
+ from vellum_ee.workflows.display.workflows import VellumWorkflowDisplay
7
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
8
+
9
+ from tests.workflows.basic_error_node.workflow import BasicErrorNodeWorkflow
10
+
11
+
12
+ def test_serialize_workflow():
13
+ # GIVEN a Workflow with an error node
14
+ # WHEN we serialize it
15
+ workflow_display = get_workflow_display(
16
+ base_display_class=VellumWorkflowDisplay, workflow_class=BasicErrorNodeWorkflow
17
+ )
18
+
19
+ # TODO: Support serialization of BaseNode
20
+ # https://app.shortcut.com/vellum/story/4871/support-serialization-of-base-node
21
+ with mock.patch.object(BaseNodeVellumDisplay, "serialize") as mocked_serialize:
22
+ mocked_serialize.return_value = {"type": "MOCKED"}
23
+ serialized_workflow: dict = workflow_display.serialize()
24
+
25
+ # THEN we should get a serialized representation of the Workflow
26
+ assert serialized_workflow.keys() == {
27
+ "workflow_raw_data",
28
+ "input_variables",
29
+ "output_variables",
30
+ }
31
+
32
+ # AND its input variables should be what we expect
33
+ input_variables = serialized_workflow["input_variables"]
34
+ assert len(input_variables) == 1
35
+ assert not DeepDiff(
36
+ [
37
+ {
38
+ "id": "5d9edd44-b35b-4bad-ad51-ccdfe8185ff5",
39
+ "key": "threshold",
40
+ "type": "NUMBER",
41
+ "default": None,
42
+ "required": True,
43
+ "extensions": {"color": None},
44
+ }
45
+ ],
46
+ input_variables,
47
+ ignore_order=True,
48
+ )
49
+
50
+ # AND its output variables should be what we expect
51
+ output_variables = serialized_workflow["output_variables"]
52
+ assert len(output_variables) == 1
53
+ assert not DeepDiff(
54
+ [
55
+ {
56
+ "id": "04c5c6be-f5e1-41b8-b668-39e179790d9e",
57
+ "key": "final_value",
58
+ "type": "NUMBER",
59
+ }
60
+ ],
61
+ output_variables,
62
+ ignore_order=True,
63
+ )
64
+
65
+ # AND its raw data should be what we expect
66
+ workflow_raw_data = serialized_workflow["workflow_raw_data"]
67
+ assert workflow_raw_data.keys() == {"edges", "nodes", "display_data", "definition"}
68
+ assert len(workflow_raw_data["edges"]) == 4
69
+ assert len(workflow_raw_data["nodes"]) == 5
70
+
71
+ # AND each node should be serialized correctly
72
+ entrypoint_node = workflow_raw_data["nodes"][0]
73
+ assert entrypoint_node == {
74
+ "id": "10e90662-e998-421d-a5c9-ec16e37a8de1",
75
+ "type": "ENTRYPOINT",
76
+ "inputs": [],
77
+ "data": {
78
+ "label": "Entrypoint Node",
79
+ "source_handle_id": "7d86498b-84ed-4feb-8e62-2188058c2c4e",
80
+ },
81
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
82
+ "definition": {
83
+ "name": "BaseNode",
84
+ "module": ["vellum", "workflows", "nodes", "bases", "base"],
85
+ "bases": [],
86
+ },
87
+ }
88
+
89
+ error_node, error_index = next(
90
+ (
91
+ (node, index)
92
+ for index, node in enumerate(workflow_raw_data["nodes"])
93
+ if node.get("data", {}).get("label") == "Fail Node"
94
+ ),
95
+ (None, None),
96
+ )
97
+ assert not DeepDiff(
98
+ {
99
+ "id": "5cf9c5e3-0eae-4daf-8d73-8b9536258eb9",
100
+ "type": "ERROR",
101
+ "inputs": [],
102
+ "data": {
103
+ "name": "error-node",
104
+ "label": "Fail Node",
105
+ "source_handle_id": "ca17d318-a0f5-4f7c-be6c-59c9dc1dd7ed",
106
+ "target_handle_id": "70c19f1c-309c-4a5d-ba65-664c0bb2fedf",
107
+ "error_source_input_id": "None",
108
+ "error_output_id": "None",
109
+ },
110
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
111
+ "definition": {
112
+ "name": "FailNode",
113
+ "module": ["tests", "workflows", "basic_error_node", "workflow"],
114
+ "bases": [
115
+ {
116
+ "name": "ErrorNode",
117
+ "module": [
118
+ "vellum",
119
+ "workflows",
120
+ "nodes",
121
+ "core",
122
+ "error_node",
123
+ "node",
124
+ ],
125
+ }
126
+ ],
127
+ },
128
+ },
129
+ error_node,
130
+ ignore_order=True,
131
+ )
132
+
133
+ mocked_base_nodes = [
134
+ node
135
+ for i, node in enumerate(workflow_raw_data["nodes"])
136
+ if i != error_index and i != 0 and i != len(workflow_raw_data["nodes"]) - 1
137
+ ]
138
+
139
+ assert not DeepDiff(
140
+ [
141
+ {
142
+ "type": "MOCKED",
143
+ },
144
+ {
145
+ "type": "MOCKED",
146
+ },
147
+ ],
148
+ mocked_base_nodes,
149
+ )
150
+
151
+ terminal_node = workflow_raw_data["nodes"][-1]
152
+ assert not DeepDiff(
153
+ {
154
+ "id": "e5fff999-80c7-4cbc-9d99-06c653f3ec77",
155
+ "type": "TERMINAL",
156
+ "data": {
157
+ "label": "Final Output",
158
+ "name": "final_value",
159
+ "target_handle_id": "b070e9bc-e9b7-46d3-8f5b-0b646bd25cf0",
160
+ "output_id": "04c5c6be-f5e1-41b8-b668-39e179790d9e",
161
+ "output_type": "NUMBER",
162
+ "node_input_id": "39ff42c9-eae8-432e-ad41-e208fba77027",
163
+ },
164
+ "inputs": [
165
+ {
166
+ "id": "39ff42c9-eae8-432e-ad41-e208fba77027",
167
+ "key": "node_input",
168
+ "value": {
169
+ "rules": [
170
+ {
171
+ "type": "NODE_OUTPUT",
172
+ "data": {
173
+ "node_id": "1eee9b4e-531f-45f2-a4b9-42207fac2c33",
174
+ "output_id": "c6b017a4-25e9-4296-8d81-6aa4b3dad171",
175
+ },
176
+ }
177
+ ],
178
+ "combinator": "OR",
179
+ },
180
+ }
181
+ ],
182
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
183
+ "definition": {
184
+ "name": "FinalOutputNode",
185
+ "module": [
186
+ "vellum",
187
+ "workflows",
188
+ "nodes",
189
+ "displayable",
190
+ "final_output_node",
191
+ "node",
192
+ ],
193
+ "bases": [
194
+ {
195
+ "name": "BaseNode",
196
+ "module": ["vellum", "workflows", "nodes", "bases", "base"],
197
+ "bases": [],
198
+ }
199
+ ],
200
+ },
201
+ },
202
+ terminal_node,
203
+ )
@@ -1,5 +0,0 @@
1
- from vellum.workflows.events.workflow import WorkflowEvent
2
-
3
-
4
- def is_terminal_event(event: WorkflowEvent) -> bool:
5
- return event.name in {"workflow.execution.fulfilled", "workflow.execution.rejected", "workflow.execution.paused"}