vellum-ai 0.12.15__py3-none-any.whl → 0.12.17__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/plugins/vellum_mypy.py +80 -11
  3. vellum/prompts/blocks/compilation.py +43 -0
  4. vellum/utils/templating/render.py +3 -0
  5. vellum/workflows/nodes/bases/base.py +4 -0
  6. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +10 -2
  7. vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py +16 -0
  8. vellum/workflows/nodes/core/retry_node/node.py +24 -3
  9. vellum/workflows/nodes/core/retry_node/tests/test_node.py +40 -0
  10. vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +20 -1
  11. vellum/workflows/nodes/displayable/api_node/node.py +3 -3
  12. {vellum_ai-0.12.15.dist-info → vellum_ai-0.12.17.dist-info}/METADATA +10 -8
  13. {vellum_ai-0.12.15.dist-info → vellum_ai-0.12.17.dist-info}/RECORD +48 -45
  14. vellum_cli/__init__.py +14 -0
  15. vellum_cli/pull.py +7 -4
  16. vellum_cli/push.py +26 -4
  17. vellum_cli/tests/conftest.py +4 -2
  18. vellum_cli/tests/test_push.py +75 -4
  19. vellum_ee/workflows/display/nodes/vellum/api_node.py +3 -3
  20. vellum_ee/workflows/display/nodes/vellum/base_node.py +17 -0
  21. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -2
  22. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +2 -2
  23. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +20 -6
  24. vellum_ee/workflows/display/nodes/vellum/map_node.py +1 -0
  25. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +2 -2
  26. vellum_ee/workflows/display/nodes/vellum/search_node.py +4 -2
  27. vellum_ee/workflows/display/nodes/vellum/templating_node.py +1 -1
  28. vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +3 -3
  29. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py +0 -0
  30. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py +28 -0
  31. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py +123 -0
  32. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +2 -11
  33. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +2 -14
  34. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +1 -7
  35. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +17 -1
  36. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +17 -1
  37. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +1 -9
  38. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +1 -1
  39. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +1 -4
  40. vellum_ee/workflows/display/types.py +5 -1
  41. vellum_ee/workflows/display/utils/vellum.py +3 -3
  42. vellum_ee/workflows/display/vellum.py +4 -0
  43. vellum_ee/workflows/display/workflows/base_workflow_display.py +44 -16
  44. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +3 -0
  45. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +7 -8
  46. {vellum_ai-0.12.15.dist-info → vellum_ai-0.12.17.dist-info}/LICENSE +0 -0
  47. {vellum_ai-0.12.15.dist-info → vellum_ai-0.12.17.dist-info}/WHEEL +0 -0
  48. {vellum_ai-0.12.15.dist-info → vellum_ai-0.12.17.dist-info}/entry_points.txt +0 -0
@@ -91,18 +91,18 @@ def test_create_node_input_value_pointer_rules(
91
91
  entrypoint_node_source_handle_id=uuid4(),
92
92
  entrypoint_node_display=NodeDisplayData(),
93
93
  ),
94
- workflow_input_displays={
94
+ global_workflow_input_displays={
95
95
  cast(WorkflowInputReference, Inputs.example_workflow_input): WorkflowInputsVellumDisplayOverrides(
96
96
  id=UUID("a154c29d-fac0-4cd0-ba88-bc52034f5470"),
97
97
  ),
98
98
  },
99
- node_output_displays={
99
+ global_node_output_displays={
100
100
  cast(OutputReference, MyNodeA.Outputs.output): (
101
101
  MyNodeA,
102
102
  NodeOutputDisplay(id=UUID("4b16a629-11a1-4b3f-a965-a57b872d13b8"), name="output"),
103
103
  ),
104
104
  },
105
- node_displays={
105
+ global_node_displays={
106
106
  MyNodeA: MyNodeADisplay(),
107
107
  },
108
108
  ),
@@ -0,0 +1,28 @@
1
+ import pytest
2
+ from uuid import uuid4
3
+
4
+ from vellum_ee.workflows.display.nodes.get_node_display_class import get_node_display_class
5
+ from vellum_ee.workflows.display.nodes.vellum.base_node import BaseNodeDisplay
6
+ from vellum_ee.workflows.display.types import WorkflowDisplayContext
7
+ from vellum_ee.workflows.display.vellum import NodeDisplayData, WorkflowMetaVellumDisplay
8
+ from vellum_ee.workflows.display.workflows.vellum_workflow_display import VellumWorkflowDisplay
9
+
10
+
11
+ @pytest.fixture()
12
+ def serialize_node():
13
+ def _serialize_node(node_class) -> dict:
14
+ node_display_class = get_node_display_class(BaseNodeDisplay, node_class)
15
+ node_display = node_display_class()
16
+
17
+ context: WorkflowDisplayContext = WorkflowDisplayContext(
18
+ workflow_display_class=VellumWorkflowDisplay,
19
+ workflow_display=WorkflowMetaVellumDisplay(
20
+ entrypoint_node_id=uuid4(),
21
+ entrypoint_node_source_handle_id=uuid4(),
22
+ entrypoint_node_display=NodeDisplayData(),
23
+ ),
24
+ node_displays={node_class: node_display},
25
+ )
26
+ return node_display.serialize(context)
27
+
28
+ return _serialize_node
@@ -0,0 +1,123 @@
1
+ from deepdiff import DeepDiff
2
+
3
+ from vellum.workflows.inputs.base import BaseInputs
4
+ from vellum.workflows.nodes.bases.base import BaseNode
5
+ from vellum.workflows.types.core import MergeBehavior
6
+
7
+
8
+ class Inputs(BaseInputs):
9
+ input: str
10
+
11
+
12
+ class BasicGenericNode(BaseNode):
13
+ class Outputs(BaseNode.Outputs):
14
+ output = Inputs.input
15
+
16
+
17
+ class AwaitAnyGenericNode(BaseNode):
18
+ class Outputs(BaseNode.Outputs):
19
+ output = Inputs.input
20
+
21
+ class Trigger(BaseNode.Trigger):
22
+ merge_behavior = MergeBehavior.AWAIT_ANY
23
+
24
+
25
+ class AwaitAllGenericNode(BaseNode):
26
+ class Outputs(BaseNode.Outputs):
27
+ output = Inputs.input
28
+
29
+ class Trigger(BaseNode.Trigger):
30
+ merge_behavior = MergeBehavior.AWAIT_ALL
31
+
32
+
33
+ def test_serialize_node__basic(serialize_node):
34
+ serialized_node = serialize_node(BasicGenericNode)
35
+ assert not DeepDiff(
36
+ {
37
+ "id": "c2ed23f7-f6cb-4a56-a91c-2e5f9d8fda7f",
38
+ "label": "BasicGenericNode",
39
+ "type": "GENERIC",
40
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
41
+ "definition": {
42
+ "name": "BasicGenericNode",
43
+ "module": [
44
+ "vellum_ee",
45
+ "workflows",
46
+ "display",
47
+ "tests",
48
+ "workflow_serialization",
49
+ "generic_nodes",
50
+ "test_trigger_serialization",
51
+ ],
52
+ "bases": [{"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]}],
53
+ },
54
+ "trigger": {"id": "9d3a1b3d-4a38-4f2e-bbf1-dd8be152bce8", "merge_behavior": "AWAIT_ANY"},
55
+ "ports": [],
56
+ "adornments": None,
57
+ "attributes": [],
58
+ },
59
+ serialized_node,
60
+ ignore_order=True,
61
+ )
62
+
63
+
64
+ def test_serialize_node__await_any(serialize_node):
65
+ serialized_node = serialize_node(AwaitAnyGenericNode)
66
+ assert not DeepDiff(
67
+ {
68
+ "id": "0ba67f76-aaff-4bd4-a20f-73a32ef5810d",
69
+ "label": "AwaitAnyGenericNode",
70
+ "type": "GENERIC",
71
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
72
+ "definition": {
73
+ "name": "AwaitAnyGenericNode",
74
+ "module": [
75
+ "vellum_ee",
76
+ "workflows",
77
+ "display",
78
+ "tests",
79
+ "workflow_serialization",
80
+ "generic_nodes",
81
+ "test_trigger_serialization",
82
+ ],
83
+ "bases": [{"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]}],
84
+ },
85
+ "trigger": {"id": "ffa72187-9a18-453f-ae55-b77aad332630", "merge_behavior": "AWAIT_ANY"},
86
+ "ports": [],
87
+ "adornments": None,
88
+ "attributes": [],
89
+ },
90
+ serialized_node,
91
+ ignore_order=True,
92
+ )
93
+
94
+
95
+ def test_serialize_node__await_all(serialize_node):
96
+ serialized_node = serialize_node(AwaitAllGenericNode)
97
+ assert not DeepDiff(
98
+ {
99
+ "id": "09d06cd3-06ea-40cc-afd8-17ad88542271",
100
+ "label": "AwaitAllGenericNode",
101
+ "type": "GENERIC",
102
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
103
+ "definition": {
104
+ "name": "AwaitAllGenericNode",
105
+ "module": [
106
+ "vellum_ee",
107
+ "workflows",
108
+ "display",
109
+ "tests",
110
+ "workflow_serialization",
111
+ "generic_nodes",
112
+ "test_trigger_serialization",
113
+ ],
114
+ "bases": [{"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]}],
115
+ },
116
+ "trigger": {"id": "62074276-c817-476d-b59d-da523ae3f218", "merge_behavior": "AWAIT_ALL"},
117
+ "ports": [],
118
+ "adornments": None,
119
+ "attributes": [],
120
+ },
121
+ serialized_node,
122
+ ignore_order=True,
123
+ )
@@ -448,17 +448,8 @@ def test_serialize_workflow():
448
448
  ignore_order=True,
449
449
  )
450
450
 
451
- assert not DeepDiff(
452
- [
453
- {"id": "148c61bd-e8b0-4d4b-8734-b043a72b90ed", "type": "GENERIC"},
454
- {"id": "ed7caf01-9ae7-47a3-b15a-16697abaf486", "type": "GENERIC"},
455
- {"id": "0d959311-c836-4641-a867-58f63df9dfea", "type": "GENERIC"},
456
- {"id": "8df781b1-ff28-48a5-98a2-d7d796b932b0", "type": "GENERIC"},
457
- {"id": "68c02b7c-5077-4087-803d-841474a8081f", "type": "GENERIC"},
458
- ],
459
- workflow_raw_data["nodes"][2:7],
460
- ignore_order=True,
461
- )
451
+ passthrough_nodes = [node for node in workflow_raw_data["nodes"] if node["type"] == "GENERIC"]
452
+ assert len(passthrough_nodes) == 5
462
453
 
463
454
  assert not DeepDiff(
464
455
  [
@@ -122,20 +122,8 @@ def test_serialize_workflow():
122
122
  ignore_order=True,
123
123
  )
124
124
 
125
- mocked_base_nodes = [
126
- node
127
- for i, node in enumerate(workflow_raw_data["nodes"])
128
- if i != error_index and i != 0 and i != len(workflow_raw_data["nodes"]) - 1
129
- ]
130
-
131
- assert not DeepDiff(
132
- [
133
- {"id": "1381c078-efa2-4255-89a1-7b4cb742c7fc", "type": "GENERIC"},
134
- {"id": "1eee9b4e-531f-45f2-a4b9-42207fac2c33", "type": "GENERIC"},
135
- ],
136
- mocked_base_nodes,
137
- ignore_order=True,
138
- )
125
+ passthrough_nodes = [node for node in workflow_raw_data["nodes"] if node["type"] == "GENERIC"]
126
+ assert len(passthrough_nodes) == 2
139
127
 
140
128
  terminal_node = workflow_raw_data["nodes"][-1]
141
129
  assert not DeepDiff(
@@ -79,13 +79,7 @@ def test_serialize_workflow(vellum_client):
79
79
  }
80
80
 
81
81
  api_node = workflow_raw_data["nodes"][1]
82
- assert not DeepDiff(
83
- {
84
- "id": "c2ed23f7-f6cb-4a56-a91c-2e5f9d8fda7f",
85
- "type": "GENERIC",
86
- },
87
- api_node,
88
- )
82
+ assert api_node["id"] == "c2ed23f7-f6cb-4a56-a91c-2e5f9d8fda7f"
89
83
 
90
84
  final_output_node = workflow_raw_data["nodes"][2]
91
85
  assert not DeepDiff(
@@ -148,7 +148,23 @@ def test_serialize_workflow():
148
148
  "bases": [],
149
149
  },
150
150
  },
151
- {"id": "1381c078-efa2-4255-89a1-7b4cb742c7fc", "type": "GENERIC"},
151
+ {
152
+ "id": "1381c078-efa2-4255-89a1-7b4cb742c7fc",
153
+ "label": "StartNode",
154
+ "type": "GENERIC",
155
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
156
+ "definition": {
157
+ "name": "StartNode",
158
+ "module": ["tests", "workflows", "basic_inline_subworkflow", "workflow"],
159
+ "bases": [
160
+ {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]}
161
+ ],
162
+ },
163
+ "trigger": {"id": "a95a34f2-e894-4fb6-a2c9-15d12c1e3135", "merge_behavior": "AWAIT_ANY"},
164
+ "ports": [],
165
+ "adornments": None,
166
+ "attributes": [],
167
+ },
152
168
  {
153
169
  "id": "a773c3a5-78cb-4250-8d29-7282e8a579d3",
154
170
  "type": "TERMINAL",
@@ -133,7 +133,23 @@ def test_serialize_workflow():
133
133
  "bases": [],
134
134
  },
135
135
  },
136
- {"id": "baf6d316-dc75-41e8-96c0-015aede96309", "type": "GENERIC"},
136
+ {
137
+ "id": "baf6d316-dc75-41e8-96c0-015aede96309",
138
+ "label": "Iteration",
139
+ "type": "GENERIC",
140
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
141
+ "definition": {
142
+ "name": "Iteration",
143
+ "module": ["tests", "workflows", "basic_map_node", "workflow"],
144
+ "bases": [
145
+ {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]}
146
+ ],
147
+ },
148
+ "trigger": {"id": "01324747-9bc0-4ecd-a8ab-40dca5a94e2e", "merge_behavior": "AWAIT_ANY"},
149
+ "ports": [],
150
+ "adornments": None,
151
+ "attributes": [],
152
+ },
137
153
  {
138
154
  "id": "6f4883b2-70b1-4e1c-ae15-7d0f5aec810b",
139
155
  "type": "TERMINAL",
@@ -69,15 +69,7 @@ def test_serialize_workflow__await_all():
69
69
  }
70
70
 
71
71
  passthrough_nodes = [node for node in workflow_raw_data["nodes"] if node["type"] == "GENERIC"]
72
- assert not DeepDiff(
73
- [
74
- {"id": "127ef456-91bc-43c6-bd8b-1772db5e3cb5", "type": "GENERIC"},
75
- {"id": "59243c65-053f-4ea6-9157-3f3edb1477bf", "type": "GENERIC"},
76
- {"id": "634f0202-9ea9-4c62-b152-1a58c595cffb", "type": "GENERIC"},
77
- ],
78
- passthrough_nodes,
79
- ignore_order=True,
80
- )
72
+ assert len(passthrough_nodes) == 3
81
73
 
82
74
  merge_node = next(node for node in workflow_raw_data["nodes"] if node["type"] == "MERGE")
83
75
  assert not DeepDiff(
@@ -76,4 +76,4 @@ def test_serialize_workflow():
76
76
  }
77
77
 
78
78
  try_node = workflow_raw_data["nodes"][1]
79
- assert try_node == {"id": "1381c078-efa2-4255-89a1-7b4cb742c7fc", "type": "GENERIC"}
79
+ assert try_node["id"] == "1381c078-efa2-4255-89a1-7b4cb742c7fc"
@@ -91,10 +91,7 @@ def test_serialize_workflow__missing_final_output_node():
91
91
  }
92
92
 
93
93
  passthrough_node = next(node for node in workflow_raw_data["nodes"] if node["type"] == "GENERIC")
94
- assert passthrough_node == {
95
- "id": "32d88cab-e9fa-4a56-9bc2-fb6e1fd0897f",
96
- "type": "GENERIC",
97
- }
94
+ assert passthrough_node["id"] == "32d88cab-e9fa-4a56-9bc2-fb6e1fd0897f"
98
95
 
99
96
  final_output_nodes = [node for node in workflow_raw_data["nodes"] if node["type"] == "TERMINAL"]
100
97
  assert not DeepDiff(
@@ -36,8 +36,12 @@ class WorkflowDisplayContext(
36
36
  workflow_display_class: Type["BaseWorkflowDisplay"]
37
37
  workflow_display: WorkflowMetaDisplayType
38
38
  workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = field(default_factory=dict)
39
+ global_workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = field(
40
+ default_factory=dict
41
+ )
39
42
  node_displays: Dict[Type[BaseNode], "NodeDisplayType"] = field(default_factory=dict)
40
- node_output_displays: Dict[OutputReference, Tuple[Type[BaseNode], "NodeOutputDisplay"]] = field(
43
+ global_node_displays: Dict[Type[BaseNode], NodeDisplayType] = field(default_factory=dict)
44
+ global_node_output_displays: Dict[OutputReference, Tuple[Type[BaseNode], "NodeOutputDisplay"]] = field(
41
45
  default_factory=dict
42
46
  )
43
47
  entrypoint_displays: Dict[Type[BaseNode], EntrypointDisplayType] = field(default_factory=dict)
@@ -58,13 +58,13 @@ def create_node_input_value_pointer_rule(
58
58
  value: Any, display_context: WorkflowDisplayContext
59
59
  ) -> NodeInputValuePointerRule:
60
60
  if isinstance(value, OutputReference):
61
- upstream_node, output_display = display_context.node_output_displays[value]
62
- upstream_node_display = display_context.node_displays[upstream_node]
61
+ upstream_node, output_display = display_context.global_node_output_displays[value]
62
+ upstream_node_display = display_context.global_node_displays[upstream_node]
63
63
  return NodeOutputPointer(
64
64
  data=NodeOutputData(node_id=str(upstream_node_display.node_id), output_id=str(output_display.id)),
65
65
  )
66
66
  if isinstance(value, WorkflowInputReference):
67
- workflow_input_display = display_context.workflow_input_displays[value]
67
+ workflow_input_display = display_context.global_workflow_input_displays[value]
68
68
  return InputVariablePointer(data=InputVariableData(input_variable_id=str(workflow_input_display.id)))
69
69
  if isinstance(value, VellumSecretReference):
70
70
  # TODO: Pass through the name instead of retrieving the ID
@@ -38,6 +38,10 @@ class NodeDisplayData(UniversalBaseModel):
38
38
  comment: Optional[NodeDisplayComment] = None
39
39
 
40
40
 
41
+ class GenericNodeDisplayData(UniversalBaseModel):
42
+ position: NodeDisplayPosition = Field(default_factory=NodeDisplayPosition)
43
+
44
+
41
45
  class CodeResourceDefinition(UniversalBaseModel):
42
46
  name: str
43
47
  module: List[str]
@@ -1,9 +1,9 @@
1
1
  from abc import abstractmethod
2
- from copy import deepcopy
2
+ from copy import copy
3
3
  from functools import cached_property
4
4
  import logging
5
5
  from uuid import UUID
6
- from typing import Any, Dict, Generic, Optional, Tuple, Type, get_args
6
+ from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, Type, get_args
7
7
 
8
8
  from vellum.workflows.descriptors.base import BaseDescriptor
9
9
  from vellum.workflows.edges import Edge
@@ -71,6 +71,10 @@ class BaseWorkflowDisplay(
71
71
  # Used to store the mapping between workflows and their display classes
72
72
  _workflow_display_registry: Dict[Type[WorkflowType], Type["BaseWorkflowDisplay"]] = {}
73
73
 
74
+ _errors: List[Exception]
75
+
76
+ _dry_run: bool
77
+
74
78
  def __init__(
75
79
  self,
76
80
  workflow: Type[WorkflowType],
@@ -85,12 +89,15 @@ class BaseWorkflowDisplay(
85
89
  EdgeDisplayType,
86
90
  ]
87
91
  ] = None,
92
+ dry_run: bool = False,
88
93
  ):
89
94
  self._workflow = workflow
90
95
  self._parent_display_context = parent_display_context
96
+ self._errors: List[Exception] = []
97
+ self._dry_run = dry_run
91
98
 
92
99
  @abstractmethod
93
- def serialize(self, raise_errors: bool = True) -> JsonObject:
100
+ def serialize(self) -> JsonObject:
94
101
  pass
95
102
 
96
103
  @classmethod
@@ -110,7 +117,18 @@ class BaseWorkflowDisplay(
110
117
  def node_display_base_class(self) -> Type[NodeDisplayType]:
111
118
  pass
112
119
 
113
- def _enrich_node_output_displays(
120
+ def add_error(self, error: Exception) -> None:
121
+ if self._dry_run:
122
+ self._errors.append(error)
123
+ return
124
+
125
+ raise error
126
+
127
+ @property
128
+ def errors(self) -> Iterator[Exception]:
129
+ return iter(self._errors)
130
+
131
+ def _enrich_global_node_output_displays(
114
132
  self,
115
133
  node: Type[BaseNode],
116
134
  node_display: NodeDisplayType,
@@ -126,7 +144,7 @@ class BaseWorkflowDisplay(
126
144
  inner_node = get_wrapped_node(node)
127
145
  if inner_node._is_wrapped_node:
128
146
  inner_node_display = self._get_node_display(inner_node)
129
- self._enrich_node_output_displays(inner_node, inner_node_display, node_output_displays)
147
+ self._enrich_global_node_output_displays(inner_node, inner_node_display, node_output_displays)
130
148
 
131
149
  # TODO: Make sure this output ID matches the workflow output ID of the subworkflow node's workflow
132
150
  # https://app.shortcut.com/vellum/story/5660/fix-output-id-in-subworkflow-nodes
@@ -174,19 +192,26 @@ class BaseWorkflowDisplay(
174
192
  ]:
175
193
  workflow_display = self._generate_workflow_meta_display()
176
194
 
177
- # If we're dealing with a nested workflow, then it should have access to the outputs of all nodes
178
- node_output_displays: Dict[OutputReference, Tuple[Type[BaseNode], NodeOutputDisplay]] = (
179
- deepcopy(self._parent_display_context.node_output_displays) if self._parent_display_context else {}
195
+ global_node_output_displays: Dict[OutputReference, Tuple[Type[BaseNode], NodeOutputDisplay]] = (
196
+ copy(self._parent_display_context.global_node_output_displays) if self._parent_display_context else {}
180
197
  )
181
198
 
182
199
  node_displays: Dict[Type[BaseNode], NodeDisplayType] = {}
200
+
201
+ global_node_displays: Dict[Type[BaseNode], NodeDisplayType] = (
202
+ copy(self._parent_display_context.global_node_displays) if self._parent_display_context else {}
203
+ )
204
+
183
205
  port_displays: Dict[Port, PortDisplay] = {}
184
206
 
185
207
  # TODO: We should still serialize nodes that are in the workflow's directory but aren't used in the graph.
186
208
  # https://app.shortcut.com/vellum/story/5394
187
209
  for node in self._workflow.get_nodes():
210
+ if node in global_node_displays:
211
+ continue
188
212
  node_display = self._get_node_display(node)
189
213
  node_displays[node] = node_display
214
+ global_node_displays[node] = node_display
190
215
 
191
216
  # Nodes wrapped in a decorator need to be in our node display dictionary for later retrieval
192
217
  if has_wrapped_node(node):
@@ -195,22 +220,23 @@ class BaseWorkflowDisplay(
195
220
 
196
221
  if inner_node._is_wrapped_node:
197
222
  node_displays[inner_node] = inner_node_display
223
+ global_node_displays[inner_node] = inner_node_display
198
224
 
199
- self._enrich_node_output_displays(node, node_display, node_output_displays)
225
+ self._enrich_global_node_output_displays(node, node_display, global_node_output_displays)
200
226
  self._enrich_node_port_displays(node, node_display, port_displays)
201
227
 
228
+ workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = {}
202
229
  # If we're dealing with a nested workflow, then it should have access to the inputs of its parents.
203
- workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = (
204
- deepcopy(self._parent_display_context.workflow_input_displays) if self._parent_display_context else {}
230
+ global_workflow_input_displays = (
231
+ copy(self._parent_display_context.workflow_input_displays) if self._parent_display_context else {}
205
232
  )
206
233
  for workflow_input in self._workflow.get_inputs_class():
207
- if workflow_input in workflow_input_displays:
208
- continue
209
-
210
234
  workflow_input_display_overrides = self.inputs_display.get(workflow_input)
211
- workflow_input_displays[workflow_input] = self._generate_workflow_input_display(
235
+ input_display = self._generate_workflow_input_display(
212
236
  workflow_input, overrides=workflow_input_display_overrides
213
237
  )
238
+ workflow_input_displays[workflow_input] = input_display
239
+ global_workflow_input_displays[workflow_input] = input_display
214
240
 
215
241
  entrypoint_displays: Dict[Type[BaseNode], EntrypointDisplayType] = {}
216
242
  for entrypoint in self._workflow.get_entrypoints():
@@ -253,8 +279,10 @@ class BaseWorkflowDisplay(
253
279
  return WorkflowDisplayContext(
254
280
  workflow_display=workflow_display,
255
281
  workflow_input_displays=workflow_input_displays,
282
+ global_workflow_input_displays=global_workflow_input_displays,
256
283
  node_displays=node_displays,
257
- node_output_displays=node_output_displays,
284
+ global_node_output_displays=global_node_output_displays,
285
+ global_node_displays=global_node_displays,
258
286
  entrypoint_displays=entrypoint_displays,
259
287
  workflow_output_displays=workflow_output_displays,
260
288
  edge_displays=edge_displays,
@@ -10,6 +10,7 @@ def get_workflow_display(
10
10
  workflow_class: Type[WorkflowType],
11
11
  root_workflow_class: Optional[Type[WorkflowType]] = None,
12
12
  parent_display_context: Optional[WorkflowDisplayContext] = None,
13
+ dry_run: bool = False,
13
14
  ) -> WorkflowDisplayType:
14
15
  try:
15
16
  workflow_display_class = base_display_class.get_from_workflow_display_registry(workflow_class)
@@ -20,6 +21,7 @@ def get_workflow_display(
20
21
  workflow_class=workflow_class.__bases__[0],
21
22
  root_workflow_class=workflow_class if root_workflow_class is None else root_workflow_class,
22
23
  parent_display_context=parent_display_context,
24
+ dry_run=dry_run,
23
25
  )
24
26
  except IndexError:
25
27
  return base_display_class(workflow_class)
@@ -27,4 +29,5 @@ def get_workflow_display(
27
29
  return workflow_display_class( # type: ignore[return-value]
28
30
  workflow_class,
29
31
  parent_display_context=parent_display_context,
32
+ dry_run=dry_run,
30
33
  )
@@ -53,7 +53,7 @@ class VellumWorkflowDisplay(
53
53
  ):
54
54
  node_display_base_class = BaseNodeVellumDisplay
55
55
 
56
- def serialize(self, raise_errors: bool = True) -> JsonObject:
56
+ def serialize(self) -> JsonObject:
57
57
  input_variables: JsonArray = []
58
58
  for workflow_input, workflow_input_display in self.display_context.workflow_input_displays.items():
59
59
  default = primitive_to_vellum_value(workflow_input.instance) if workflow_input.instance else None
@@ -106,12 +106,9 @@ class VellumWorkflowDisplay(
106
106
 
107
107
  try:
108
108
  serialized_node = node_display.serialize(self.display_context)
109
- except NotImplementedError:
110
- logger.warning("Unable to serialize node", extra={"node": node.__name__})
111
- if raise_errors:
112
- raise
113
- else:
114
- continue
109
+ except NotImplementedError as e:
110
+ self.add_error(e)
111
+ continue
115
112
 
116
113
  nodes.append(serialized_node)
117
114
 
@@ -205,7 +202,9 @@ class VellumWorkflowDisplay(
205
202
  # If there are terminal nodes with no workflow output reference,
206
203
  # raise a serialization error
207
204
  if len(unreferenced_final_output_node_outputs) > 0:
208
- raise ValueError("Unable to serialize terminal nodes that are not referenced by workflow outputs.")
205
+ self.add_error(
206
+ ValueError("Unable to serialize terminal nodes that are not referenced by workflow outputs.")
207
+ )
209
208
 
210
209
  # Add an edge for each edge in the workflow
211
210
  all_edge_displays: List[EdgeVellumDisplay] = [