vellum-ai 0.13.15__py3-none-any.whl → 0.13.19__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 (28) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/resources/workflows/client.py +0 -10
  3. vellum/workflows/nodes/core/retry_node/node.py +13 -7
  4. vellum/workflows/nodes/core/templating_node/node.py +4 -47
  5. vellum/workflows/nodes/displayable/code_execution_node/node.py +29 -23
  6. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +169 -5
  7. vellum/workflows/nodes/displayable/code_execution_node/utils.py +98 -1
  8. vellum/workflows/nodes/utils.py +50 -1
  9. vellum/workflows/outputs/base.py +11 -0
  10. vellum/workflows/references/external_input.py +14 -0
  11. vellum/workflows/state/base.py +7 -0
  12. vellum/workflows/state/tests/test_state.py +42 -0
  13. {vellum_ai-0.13.15.dist-info → vellum_ai-0.13.19.dist-info}/METADATA +1 -1
  14. {vellum_ai-0.13.15.dist-info → vellum_ai-0.13.19.dist-info}/RECORD +28 -27
  15. vellum_cli/config.py +69 -11
  16. vellum_cli/pull.py +57 -20
  17. vellum_cli/push.py +1 -5
  18. vellum_cli/tests/test_pull.py +157 -9
  19. vellum_cli/tests/test_push.py +0 -8
  20. vellum_ee/workflows/display/nodes/base_node_display.py +2 -2
  21. vellum_ee/workflows/display/nodes/get_node_display_class.py +16 -20
  22. vellum_ee/workflows/display/nodes/vellum/__init__.py +2 -0
  23. vellum_ee/workflows/display/nodes/vellum/retry_node.py +10 -0
  24. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +28 -0
  25. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +2 -2
  26. {vellum_ai-0.13.15.dist-info → vellum_ai-0.13.19.dist-info}/LICENSE +0 -0
  27. {vellum_ai-0.13.15.dist-info → vellum_ai-0.13.19.dist-info}/WHEEL +0 -0
  28. {vellum_ai-0.13.15.dist-info → vellum_ai-0.13.19.dist-info}/entry_points.txt +0 -0
vellum_cli/push.py CHANGED
@@ -12,7 +12,6 @@ from dotenv import load_dotenv
12
12
  from vellum.client.core.api_error import ApiError
13
13
  from vellum.resources.workflows.client import OMIT
14
14
  from vellum.types import WorkflowPushDeploymentConfigRequest
15
- from vellum.workflows.utils.names import snake_to_title_case
16
15
  from vellum.workflows.vellum_client import create_vellum_client
17
16
  from vellum.workflows.workflows.base import BaseWorkflow
18
17
  from vellum_cli.config import DEFAULT_WORKSPACE_CONFIG, WorkflowConfig, WorkflowDeploymentConfig, load_vellum_cli_config
@@ -111,8 +110,6 @@ def push_command(
111
110
  "container_image_name": workflow_config.container_image_name,
112
111
  }
113
112
 
114
- label = snake_to_title_case(workflow_config.module.split(".")[-1])
115
-
116
113
  deployment_config: WorkflowPushDeploymentConfigRequest = OMIT
117
114
  deployment_config_serialized: str = OMIT
118
115
  if deploy:
@@ -158,7 +155,6 @@ def push_command(
158
155
  # Remove this once we could serialize using the artifact in Vembda
159
156
  # https://app.shortcut.com/vellum/story/5585
160
157
  exec_config=json.dumps(exec_config),
161
- label=label,
162
158
  workflow_sandbox_id=workflow_config.workflow_sandbox_id or workflow_sandbox_id,
163
159
  artifact=artifact,
164
160
  # We should check with fern if we could auto-serialize typed object fields for us
@@ -230,7 +226,7 @@ def push_command(
230
226
  ) # type: ignore[attr-defined]
231
227
  else:
232
228
  logger.info(
233
- f"""Successfully pushed {label} to Vellum!
229
+ f"""Successfully pushed {workflow_config.module} to Vellum!
234
230
  Visit at: https://app.vellum.ai/workflow-sandboxes/{response.workflow_sandbox_id}"""
235
231
  )
236
232
 
@@ -39,6 +39,7 @@ def test_pull(vellum_client, mock_module, base_command):
39
39
  # GIVEN a module on the user's filesystem
40
40
  temp_dir = mock_module.temp_dir
41
41
  module = mock_module.module
42
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
42
43
 
43
44
  # AND the workflow pull API call returns a zip file
44
45
  vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
@@ -56,6 +57,27 @@ def test_pull(vellum_client, mock_module, base_command):
56
57
  with open(workflow_py) as f:
57
58
  assert f.read() == "print('hello')"
58
59
 
60
+ # AND the vellum.lock.json file is created
61
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
62
+ assert os.path.exists(vellum_lock_json)
63
+ with open(vellum_lock_json) as f:
64
+ lock_data = json.load(f)
65
+ assert lock_data == {
66
+ "version": "1.0",
67
+ "workflows": [
68
+ {
69
+ "module": module,
70
+ "workflow_sandbox_id": workflow_sandbox_id,
71
+ "container_image_name": None,
72
+ "container_image_tag": None,
73
+ "ignore": None,
74
+ "deployments": [],
75
+ "workspace": "default",
76
+ }
77
+ ],
78
+ "workspaces": [],
79
+ }
80
+
59
81
 
60
82
  def test_pull__second_module(vellum_client, mock_module):
61
83
  # GIVEN a module on the user's filesystem
@@ -95,7 +117,13 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
95
117
  workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
96
118
 
97
119
  # AND the workflow pull API call returns a zip file
98
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
120
+ vellum_client.workflows.pull.return_value = iter(
121
+ [
122
+ _zip_file_map(
123
+ {"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
124
+ )
125
+ ]
126
+ )
99
127
 
100
128
  # AND we are currently in a new directory
101
129
  current_dir = os.getcwd()
@@ -112,7 +140,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
112
140
 
113
141
  # AND the pull api is called with the workflow sandbox id
114
142
  vellum_client.workflows.pull.assert_called_once()
115
- workflow_py = os.path.join(temp_dir, "workflow_87654321", "workflow.py")
143
+ workflow_py = os.path.join(temp_dir, "super_cool_workflow", "workflow.py")
116
144
  assert os.path.exists(workflow_py)
117
145
  with open(workflow_py) as f:
118
146
  assert f.read() == "print('hello')"
@@ -127,7 +155,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
127
155
  "workspaces": [],
128
156
  "workflows": [
129
157
  {
130
- "module": "workflow_87654321",
158
+ "module": "super_cool_workflow",
131
159
  "workflow_sandbox_id": "87654321-0000-0000-0000-000000000000",
132
160
  "ignore": None,
133
161
  "deployments": [],
@@ -147,7 +175,13 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
147
175
  workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
148
176
 
149
177
  # AND the workflow pull API call returns a zip file
150
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
178
+ vellum_client.workflows.pull.return_value = iter(
179
+ [
180
+ _zip_file_map(
181
+ {"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
182
+ )
183
+ ]
184
+ )
151
185
 
152
186
  # WHEN the user runs the pull command with the new workflow sandbox id
153
187
  runner = CliRunner()
@@ -162,7 +196,7 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
162
196
  assert call_args[0] == workflow_sandbox_id
163
197
 
164
198
  # AND the workflow.py file is written to the module directory
165
- workflow_py = os.path.join(temp_dir, "workflow_87654321", "workflow.py")
199
+ workflow_py = os.path.join(temp_dir, "super_cool_workflow", "workflow.py")
166
200
  assert os.path.exists(workflow_py)
167
201
  with open(workflow_py) as f:
168
202
  assert f.read() == "print('hello')"
@@ -394,7 +428,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
394
428
  "workflows": [
395
429
  {
396
430
  "module": module,
397
- "workflow_sandbox_id": "0edc07cd-45b9-43e8-99bc-1f181972a857",
431
+ "workflow_sandbox_id": workflow_sandbox_id,
398
432
  "ignore": "tests/*",
399
433
  "deployments": [
400
434
  {
@@ -420,8 +454,12 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
420
454
  _zip_file_map(
421
455
  {
422
456
  "workflow.py": "print('hello')",
423
- "metadata.json": '{"runner_config": { "container_image_name": "test", '
424
- '"container_image_tag": "1.0" } }',
457
+ "metadata.json": json.dumps(
458
+ {
459
+ "runner_config": {"container_image_name": "test", "container_image_tag": "1.0"},
460
+ "label": "Super Cool Workflow",
461
+ }
462
+ ),
425
463
  }
426
464
  )
427
465
  ]
@@ -456,7 +494,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
456
494
  "workspace": "default",
457
495
  },
458
496
  {
459
- "module": "workflow_87654321",
497
+ "module": "super_cool_workflow",
460
498
  "workflow_sandbox_id": new_workflow_sandbox_id,
461
499
  "ignore": None,
462
500
  "deployments": [],
@@ -539,3 +577,113 @@ def test_pull__include_sandbox(vellum_client, mock_module):
539
577
  with open(lock_json) as f:
540
578
  lock_data = json.load(f)
541
579
  assert lock_data["workflows"][0]["ignore"] == "sandbox.py"
580
+
581
+
582
+ def test_pull__same_pull_twice__one_entry_in_lockfile(vellum_client, mock_module):
583
+ # GIVEN a module on the user's filesystem
584
+ module = mock_module.module
585
+ temp_dir = mock_module.temp_dir
586
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
587
+
588
+ # AND the workflow pull API call returns a zip file both times
589
+ zip_contents = _zip_file_map({"workflow.py": "print('hello')"})
590
+ responses = iter([zip_contents, zip_contents])
591
+
592
+ def workflows_pull_side_effect(*args, **kwargs):
593
+ return iter([next(responses)])
594
+
595
+ vellum_client.workflows.pull.side_effect = workflows_pull_side_effect
596
+
597
+ # AND the user runs the pull command once
598
+ runner = CliRunner()
599
+ runner.invoke(cli_main, ["pull", module])
600
+
601
+ # WHEN the user runs the pull command again but with the workflow sandbox id
602
+ result = runner.invoke(cli_main, ["workflows", "pull", "--workflow-sandbox-id", workflow_sandbox_id])
603
+
604
+ # THEN the command returns successfully
605
+ assert result.exit_code == 0, (result.output, result.exception)
606
+
607
+ # AND the lockfile should only have one entry
608
+ lock_json = os.path.join(temp_dir, "vellum.lock.json")
609
+ with open(lock_json) as f:
610
+ lock_data = json.load(f)
611
+ assert len(lock_data["workflows"]) == 1
612
+
613
+
614
+ def test_pull__module_not_in_config(vellum_client, mock_module):
615
+ # GIVEN a module on the user's filesystem
616
+ module = mock_module.module
617
+ temp_dir = mock_module.temp_dir
618
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
619
+ set_pyproject_toml = mock_module.set_pyproject_toml
620
+
621
+ # AND the pyproject.toml does not have the module configured
622
+ set_pyproject_toml({"workflows": []})
623
+
624
+ # AND the workflow pull API call returns a zip file
625
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
626
+
627
+ # WHEN the user runs the pull command again with the workflow sandbox id and module
628
+ runner = CliRunner()
629
+ result = runner.invoke(cli_main, ["workflows", "pull", module, "--workflow-sandbox-id", workflow_sandbox_id])
630
+
631
+ # THEN the command returns successfully
632
+ assert result.exit_code == 0, (result.output, result.exception)
633
+
634
+ # AND the lockfile should have the new entry
635
+ lock_json = os.path.join(temp_dir, "vellum.lock.json")
636
+ with open(lock_json) as f:
637
+ lock_data = json.load(f)
638
+ assert lock_data["workflows"] == [
639
+ {
640
+ "module": module,
641
+ "workflow_sandbox_id": workflow_sandbox_id,
642
+ "ignore": None,
643
+ "deployments": [],
644
+ "container_image_name": None,
645
+ "container_image_tag": None,
646
+ "workspace": "default",
647
+ }
648
+ ]
649
+
650
+
651
+ def test_pull__multiple_instances_of_same_module__keep_when_pulling_another_module(vellum_client, mock_module):
652
+ # GIVEN a module on the user's filesystem
653
+ module = mock_module.module
654
+ temp_dir = mock_module.temp_dir
655
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
656
+
657
+ # AND the vellum lock file has two instances of some other module
658
+ lock_data = {
659
+ "workflows": [
660
+ {
661
+ "module": "some_other_module",
662
+ "workflow_sandbox_id": str(uuid4()),
663
+ "workspace": "default",
664
+ },
665
+ {
666
+ "module": "some_other_module",
667
+ "workflow_sandbox_id": str(uuid4()),
668
+ "workspace": "other",
669
+ },
670
+ ]
671
+ }
672
+ lock_json = os.path.join(temp_dir, "vellum.lock.json")
673
+ with open(lock_json, "w") as f:
674
+ json.dump(lock_data, f)
675
+
676
+ # AND the workflow pull API call returns a zip file
677
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
678
+
679
+ # WHEN the user runs the pull command on the new module
680
+ runner = CliRunner()
681
+ result = runner.invoke(cli_main, ["workflows", "pull", module, "--workflow-sandbox-id", workflow_sandbox_id])
682
+
683
+ # THEN the command returns successfully
684
+ assert result.exit_code == 0, (result.output, result.exception)
685
+
686
+ # AND the lockfile should have all three entries
687
+ with open(lock_json) as f:
688
+ lock_data = json.load(f)
689
+ assert len(lock_data["workflows"]) == 3
@@ -119,14 +119,12 @@ def test_push__happy_path(mock_module, vellum_client, base_command):
119
119
  assert result.exit_code == 0
120
120
 
121
121
  # Get the last part of the module path and format it
122
- expected_label = mock_module.module.split(".")[-1].replace("_", " ").title()
123
122
  expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
124
123
 
125
124
  # AND we should have called the push API with the correct args
126
125
  vellum_client.workflows.push.assert_called_once()
127
126
  call_args = vellum_client.workflows.push.call_args.kwargs
128
127
  assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
129
- assert call_args["label"] == expected_label
130
128
  assert is_valid_uuid(call_args["workflow_sandbox_id"])
131
129
  assert call_args["artifact"].name == expected_artifact_name
132
130
  assert "deplyment_config" not in call_args
@@ -165,14 +163,12 @@ def test_push__workflow_sandbox_option__existing_id(mock_module, vellum_client,
165
163
  assert result.exit_code == 0
166
164
 
167
165
  # Get the last part of the module path and format it
168
- expected_label = mock_module.module.split(".")[-1].replace("_", " ").title()
169
166
  expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
170
167
 
171
168
  # AND we should have called the push API with the correct args
172
169
  vellum_client.workflows.push.assert_called_once()
173
170
  call_args = vellum_client.workflows.push.call_args.kwargs
174
171
  assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
175
- assert call_args["label"] == expected_label
176
172
  assert call_args["workflow_sandbox_id"] == existing_workflow_sandbox_id
177
173
  assert call_args["artifact"].name == expected_artifact_name
178
174
  assert "deplyment_config" not in call_args
@@ -216,14 +212,12 @@ def test_push__workflow_sandbox_option__existing_no_module(mock_module, vellum_c
216
212
  assert result.exit_code == 0
217
213
 
218
214
  # Get the last part of the module path and format it
219
- expected_label = second_module.split(".")[-1].replace("_", " ").title()
220
215
  expected_artifact_name = f"{second_module.replace('.', '__')}.tar.gz"
221
216
 
222
217
  # AND we should have called the push API with the correct args
223
218
  vellum_client.workflows.push.assert_called_once()
224
219
  call_args = vellum_client.workflows.push.call_args.kwargs
225
220
  assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
226
- assert call_args["label"] == expected_label
227
221
  assert call_args["workflow_sandbox_id"] == second_workflow_sandbox_id
228
222
  assert call_args["artifact"].name == expected_artifact_name
229
223
  assert "deplyment_config" not in call_args
@@ -297,14 +291,12 @@ def test_push__deployment(mock_module, vellum_client, base_command):
297
291
  assert result.exit_code == 0
298
292
 
299
293
  # Get the last part of the module path and format it
300
- expected_label = mock_module.module.split(".")[-1].replace("_", " ").title()
301
294
  expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
302
295
 
303
296
  # AND we should have called the push API with the correct args
304
297
  vellum_client.workflows.push.assert_called_once()
305
298
  call_args = vellum_client.workflows.push.call_args.kwargs
306
299
  assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
307
- assert call_args["label"] == expected_label
308
300
  assert is_valid_uuid(call_args["workflow_sandbox_id"])
309
301
  assert call_args["artifact"].name == expected_artifact_name
310
302
  assert call_args["deployment_config"] == "{}"
@@ -208,8 +208,8 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
208
208
  return uuid4_from_hash(f"{self.node_id}|trigger")
209
209
 
210
210
  @classmethod
211
- def get_from_node_display_registry(cls, node_class: Type[NodeType]) -> Type["BaseNodeDisplay"]:
212
- return cls._node_display_registry[node_class]
211
+ def get_from_node_display_registry(cls, node_class: Type[NodeType]) -> Optional[Type["BaseNodeDisplay"]]:
212
+ return cls._node_display_registry.get(node_class)
213
213
 
214
214
  @classmethod
215
215
  def infer_node_class(cls) -> Type[NodeType]:
@@ -10,27 +10,23 @@ if TYPE_CHECKING:
10
10
  def get_node_display_class(
11
11
  base_class: Type["NodeDisplayType"], node_class: Type[NodeType], root_node_class: Optional[Type[NodeType]] = None
12
12
  ) -> Type["NodeDisplayType"]:
13
- try:
14
- node_display_class = base_class.get_from_node_display_registry(node_class)
15
- except KeyError:
16
- try:
17
- base_node_display_class = get_node_display_class(
18
- base_class, node_class.__bases__[0], node_class if root_node_class is None else root_node_class
13
+ node_display_class = base_class.get_from_node_display_registry(node_class)
14
+ if node_display_class:
15
+ if not issubclass(node_display_class, base_class):
16
+ raise TypeError(
17
+ f"Expected to find a subclass of '{base_class.__name__}' for node class '{node_class.__name__}'"
19
18
  )
20
19
 
21
- # `base_node_display_class` is always a Generic class, so it's safe to index into it
22
- NodeDisplayBaseClass = base_node_display_class[node_class] # type: ignore[index]
23
- NodeDisplayClass = types.new_class(
24
- f"{node_class.__name__}Display",
25
- bases=(NodeDisplayBaseClass,),
26
- )
27
- return NodeDisplayClass
28
- except IndexError:
29
- return base_class
20
+ return node_display_class
30
21
 
31
- if not issubclass(node_display_class, base_class):
32
- raise TypeError(
33
- f"Expected to find a subclass of '{base_class.__name__}' for node class '{node_class.__name__}'"
34
- )
22
+ base_node_display_class = get_node_display_class(
23
+ base_class, node_class.__bases__[0], node_class if root_node_class is None else root_node_class
24
+ )
35
25
 
36
- return node_display_class
26
+ # `base_node_display_class` is always a Generic class, so it's safe to index into it
27
+ NodeDisplayBaseClass = base_node_display_class[node_class] # type: ignore[index]
28
+ NodeDisplayClass = types.new_class(
29
+ f"{node_class.__name__}Display",
30
+ bases=(NodeDisplayBaseClass,),
31
+ )
32
+ return NodeDisplayClass
@@ -10,6 +10,7 @@ from .map_node import BaseMapNodeDisplay
10
10
  from .merge_node import BaseMergeNodeDisplay
11
11
  from .note_node import BaseNoteNodeDisplay
12
12
  from .prompt_deployment_node import BasePromptDeploymentNodeDisplay
13
+ from .retry_node import BaseRetryNodeDisplay
13
14
  from .search_node import BaseSearchNodeDisplay
14
15
  from .subworkflow_deployment_node import BaseSubworkflowDeploymentNodeDisplay
15
16
  from .templating_node import BaseTemplatingNodeDisplay
@@ -29,6 +30,7 @@ __all__ = [
29
30
  "BaseMergeNodeDisplay",
30
31
  "BaseNoteNodeDisplay",
31
32
  "BasePromptDeploymentNodeDisplay",
33
+ "BaseRetryNodeDisplay",
32
34
  "BaseSearchNodeDisplay",
33
35
  "BaseSubworkflowDeploymentNodeDisplay",
34
36
  "BaseTemplatingNodeDisplay",
@@ -0,0 +1,10 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from vellum.workflows.nodes.core.retry_node.node import RetryNode
4
+ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
5
+
6
+ _RetryNodeType = TypeVar("_RetryNodeType", bound=RetryNode)
7
+
8
+
9
+ class BaseRetryNodeDisplay(BaseNodeDisplay[_RetryNodeType], Generic[_RetryNodeType]):
10
+ pass
@@ -7,10 +7,13 @@ from vellum.workflows.nodes.bases.base import BaseNode
7
7
  from vellum.workflows.nodes.core.retry_node.node import RetryNode
8
8
  from vellum.workflows.nodes.core.try_node.node import TryNode
9
9
  from vellum.workflows.outputs.base import BaseOutputs
10
+ from vellum.workflows.workflows.base import BaseWorkflow
10
11
  from vellum_ee.workflows.display.base import WorkflowInputsDisplay
11
12
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
12
13
  from vellum_ee.workflows.display.nodes.base_node_vellum_display import BaseNodeVellumDisplay
13
14
  from vellum_ee.workflows.display.nodes.vellum.try_node import BaseTryNodeDisplay
15
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
16
+ from vellum_ee.workflows.display.workflows.vellum_workflow_display import VellumWorkflowDisplay
14
17
 
15
18
 
16
19
  class Inputs(BaseInputs):
@@ -77,6 +80,11 @@ def test_serialize_node__retry(serialize_node):
77
80
  "module": ["vellum", "workflows", "nodes", "core", "retry_node", "node"],
78
81
  },
79
82
  "attributes": [
83
+ {
84
+ "id": "8a07dc58-3fed-41d4-8ca6-31ee0bb86c61",
85
+ "name": "delay",
86
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
87
+ },
80
88
  {
81
89
  "id": "f388e93b-8c68-4f54-8577-bbd0c9091557",
82
90
  "name": "max_attempts",
@@ -110,6 +118,26 @@ def test_serialize_node__retry(serialize_node):
110
118
  )
111
119
 
112
120
 
121
+ def test_serialize_node__retry__no_display(): # GIVEN an adornment node
122
+ @RetryNode.wrap(max_attempts=5)
123
+ class StartNode(BaseNode):
124
+ pass
125
+
126
+ # AND a workflow that uses the adornment node
127
+ class MyWorkflow(BaseWorkflow):
128
+ graph = StartNode
129
+
130
+ # WHEN we serialize the workflow
131
+ workflow_display = get_workflow_display(
132
+ base_display_class=VellumWorkflowDisplay,
133
+ workflow_class=MyWorkflow,
134
+ )
135
+ exec_config = workflow_display.serialize()
136
+
137
+ # THEN the workflow display is created successfully
138
+ assert exec_config is not None
139
+
140
+
113
141
  @TryNode.wrap()
114
142
  class InnerTryGenericNode(BaseNode):
115
143
  input = Inputs.input
@@ -93,7 +93,7 @@ def test_serialize_workflow_with_filepath():
93
93
  "code_input_id": "f2e8a4fa-b54e-41e9-b314-0e5443519ac7",
94
94
  "runtime_input_id": "19d64948-f22b-4103-a7f5-3add184b31cc",
95
95
  "output_type": "NUMBER",
96
- "packages": [],
96
+ "packages": [{"name": "openai", "version": "1.0.0"}],
97
97
  "output_id": "0fde9607-353f-42c2-85c4-20f720ebc1ec",
98
98
  "log_output_id": "7cac05e3-b7c3-475e-8df8-422b496c3398",
99
99
  },
@@ -566,7 +566,7 @@ def test_serialize_workflow__try_wrapped():
566
566
  "code_input_id": "f2e8a4fa-b54e-41e9-b314-0e5443519ac7",
567
567
  "runtime_input_id": "19d64948-f22b-4103-a7f5-3add184b31cc",
568
568
  "output_type": "NUMBER",
569
- "packages": [],
569
+ "packages": [{"name": "openai", "version": "1.0.0"}],
570
570
  "output_id": "0fde9607-353f-42c2-85c4-20f720ebc1ec",
571
571
  "log_output_id": "7cac05e3-b7c3-475e-8df8-422b496c3398",
572
572
  },