vellum-ai 0.14.44__py3-none-any.whl → 0.14.45__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 (46) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/core/pydantic_utilities.py +7 -1
  3. vellum/workflows/nodes/bases/base.py +1 -0
  4. vellum/workflows/nodes/bases/tests/test_base_node.py +20 -0
  5. vellum/workflows/nodes/core/try_node/node.py +6 -3
  6. vellum/workflows/nodes/core/try_node/tests/test_node.py +24 -0
  7. vellum/workflows/ports/port.py +13 -3
  8. vellum/workflows/types/tests/test_utils.py +3 -3
  9. vellum/workflows/types/utils.py +31 -10
  10. {vellum_ai-0.14.44.dist-info → vellum_ai-0.14.45.dist-info}/METADATA +1 -1
  11. {vellum_ai-0.14.44.dist-info → vellum_ai-0.14.45.dist-info}/RECORD +46 -44
  12. vellum_ee/workflows/display/nodes/base_node_display.py +4 -173
  13. vellum_ee/workflows/display/nodes/vellum/conditional_node.py +1 -1
  14. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +2 -1
  15. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +4 -1
  16. vellum_ee/workflows/display/nodes/vellum/retry_node.py +3 -3
  17. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +4 -1
  18. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_deployment_node.py +106 -0
  19. vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py +109 -0
  20. vellum_ee/workflows/display/nodes/vellum/try_node.py +3 -3
  21. vellum_ee/workflows/display/tests/test_base_workflow_display.py +1 -0
  22. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +73 -111
  23. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +0 -1
  24. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +0 -3
  25. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +0 -4
  26. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +0 -1
  27. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +0 -1
  28. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +0 -1
  29. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +0 -1
  30. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +18 -2
  31. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +10 -1
  32. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +0 -1
  33. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +2 -3
  34. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +0 -1
  35. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +2 -3
  36. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +0 -1
  37. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +1 -2
  38. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +0 -1
  39. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +5 -55
  40. vellum_ee/workflows/display/utils/expressions.py +221 -1
  41. vellum_ee/workflows/display/utils/vellum.py +0 -76
  42. vellum_ee/workflows/display/workflows/base_workflow_display.py +49 -37
  43. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +45 -0
  44. {vellum_ai-0.14.44.dist-info → vellum_ai-0.14.45.dist-info}/LICENSE +0 -0
  45. {vellum_ai-0.14.44.dist-info → vellum_ai-0.14.45.dist-info}/WHEEL +0 -0
  46. {vellum_ai-0.14.44.dist-info → vellum_ai-0.14.45.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,98 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
+ from vellum.client.types.logical_operator import LogicalOperator
3
4
  from vellum.workflows.descriptors.base import BaseDescriptor
5
+ from vellum.workflows.expressions.accessor import AccessorExpression
6
+ from vellum.workflows.expressions.and_ import AndExpression
7
+ from vellum.workflows.expressions.begins_with import BeginsWithExpression
8
+ from vellum.workflows.expressions.between import BetweenExpression
9
+ from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
10
+ from vellum.workflows.expressions.contains import ContainsExpression
11
+ from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
12
+ from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
13
+ from vellum.workflows.expressions.does_not_end_with import DoesNotEndWithExpression
14
+ from vellum.workflows.expressions.does_not_equal import DoesNotEqualExpression
15
+ from vellum.workflows.expressions.ends_with import EndsWithExpression
16
+ from vellum.workflows.expressions.equals import EqualsExpression
17
+ from vellum.workflows.expressions.greater_than import GreaterThanExpression
18
+ from vellum.workflows.expressions.greater_than_or_equal_to import GreaterThanOrEqualToExpression
19
+ from vellum.workflows.expressions.in_ import InExpression
20
+ from vellum.workflows.expressions.is_nil import IsNilExpression
21
+ from vellum.workflows.expressions.is_not_nil import IsNotNilExpression
22
+ from vellum.workflows.expressions.is_not_null import IsNotNullExpression
23
+ from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
24
+ from vellum.workflows.expressions.is_null import IsNullExpression
25
+ from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
26
+ from vellum.workflows.expressions.less_than import LessThanExpression
27
+ from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
28
+ from vellum.workflows.expressions.not_between import NotBetweenExpression
29
+ from vellum.workflows.expressions.not_in import NotInExpression
30
+ from vellum.workflows.expressions.or_ import OrExpression
31
+ from vellum.workflows.expressions.parse_json import ParseJsonExpression
32
+ from vellum.workflows.nodes.displayable.bases.utils import primitive_to_vellum_value
33
+ from vellum.workflows.references.constant import ConstantValueReference
34
+ from vellum.workflows.references.execution_count import ExecutionCountReference
4
35
  from vellum.workflows.references.lazy import LazyReference
36
+ from vellum.workflows.references.output import OutputReference
37
+ from vellum.workflows.references.state_value import StateValueReference
38
+ from vellum.workflows.references.vellum_secret import VellumSecretReference
39
+ from vellum.workflows.references.workflow_input import WorkflowInputReference
40
+ from vellum.workflows.types.core import JsonObject
41
+ from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
5
42
 
6
43
  if TYPE_CHECKING:
7
44
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
8
45
 
9
46
 
47
+ def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperator:
48
+ if isinstance(descriptor, EqualsExpression):
49
+ return "="
50
+ elif isinstance(descriptor, DoesNotEqualExpression):
51
+ return "!="
52
+ elif isinstance(descriptor, LessThanExpression):
53
+ return "<"
54
+ elif isinstance(descriptor, GreaterThanExpression):
55
+ return ">"
56
+ elif isinstance(descriptor, LessThanOrEqualToExpression):
57
+ return "<="
58
+ elif isinstance(descriptor, GreaterThanOrEqualToExpression):
59
+ return ">="
60
+ elif isinstance(descriptor, ContainsExpression):
61
+ return "contains"
62
+ elif isinstance(descriptor, BeginsWithExpression):
63
+ return "beginsWith"
64
+ elif isinstance(descriptor, EndsWithExpression):
65
+ return "endsWith"
66
+ elif isinstance(descriptor, DoesNotContainExpression):
67
+ return "doesNotContain"
68
+ elif isinstance(descriptor, DoesNotBeginWithExpression):
69
+ return "doesNotBeginWith"
70
+ elif isinstance(descriptor, DoesNotEndWithExpression):
71
+ return "doesNotEndWith"
72
+ elif isinstance(descriptor, (IsNullExpression, IsNilExpression, IsUndefinedExpression)):
73
+ return "null"
74
+ elif isinstance(descriptor, (IsNotNullExpression, IsNotNilExpression, IsNotUndefinedExpression)):
75
+ return "notNull"
76
+ elif isinstance(descriptor, InExpression):
77
+ return "in"
78
+ elif isinstance(descriptor, NotInExpression):
79
+ return "notIn"
80
+ elif isinstance(descriptor, BetweenExpression):
81
+ return "between"
82
+ elif isinstance(descriptor, NotBetweenExpression):
83
+ return "notBetween"
84
+ elif isinstance(descriptor, AndExpression):
85
+ return "and"
86
+ elif isinstance(descriptor, OrExpression):
87
+ return "or"
88
+ elif isinstance(descriptor, CoalesceExpression):
89
+ return "coalesce"
90
+ elif isinstance(descriptor, ParseJsonExpression):
91
+ return "parseJson"
92
+ else:
93
+ raise ValueError(f"Unsupported descriptor type: {descriptor}")
94
+
95
+
10
96
  def get_child_descriptor(value: LazyReference, display_context: "WorkflowDisplayContext") -> BaseDescriptor:
11
97
  if isinstance(value._get, str):
12
98
  reference_parts = value._get.split(".")
@@ -28,3 +114,137 @@ def get_child_descriptor(value: LazyReference, display_context: "WorkflowDisplay
28
114
  raise Exception(f"Failed to parse lazy reference: {value._get}")
29
115
 
30
116
  return value._get()
117
+
118
+
119
+ def serialize_condition(display_context: "WorkflowDisplayContext", condition: BaseDescriptor) -> JsonObject:
120
+ if isinstance(
121
+ condition,
122
+ (
123
+ IsNullExpression,
124
+ IsNotNullExpression,
125
+ IsNilExpression,
126
+ IsNotNilExpression,
127
+ IsUndefinedExpression,
128
+ IsNotUndefinedExpression,
129
+ ParseJsonExpression,
130
+ ),
131
+ ):
132
+ lhs = serialize_value(display_context, condition._expression)
133
+ return {
134
+ "type": "UNARY_EXPRESSION",
135
+ "lhs": lhs,
136
+ "operator": convert_descriptor_to_operator(condition),
137
+ }
138
+ elif isinstance(condition, (BetweenExpression, NotBetweenExpression)):
139
+ base = serialize_value(display_context, condition._value)
140
+ lhs = serialize_value(display_context, condition._start)
141
+ rhs = serialize_value(display_context, condition._end)
142
+
143
+ return {
144
+ "type": "TERNARY_EXPRESSION",
145
+ "base": base,
146
+ "operator": convert_descriptor_to_operator(condition),
147
+ "lhs": lhs,
148
+ "rhs": rhs,
149
+ }
150
+ elif isinstance(
151
+ condition,
152
+ (
153
+ AndExpression,
154
+ BeginsWithExpression,
155
+ CoalesceExpression,
156
+ ContainsExpression,
157
+ DoesNotBeginWithExpression,
158
+ DoesNotContainExpression,
159
+ DoesNotEndWithExpression,
160
+ DoesNotEqualExpression,
161
+ EndsWithExpression,
162
+ EqualsExpression,
163
+ GreaterThanExpression,
164
+ GreaterThanOrEqualToExpression,
165
+ InExpression,
166
+ LessThanExpression,
167
+ LessThanOrEqualToExpression,
168
+ NotInExpression,
169
+ OrExpression,
170
+ ),
171
+ ):
172
+ lhs = serialize_value(display_context, condition._lhs)
173
+ rhs = serialize_value(display_context, condition._rhs)
174
+
175
+ return {
176
+ "type": "BINARY_EXPRESSION",
177
+ "lhs": lhs,
178
+ "operator": convert_descriptor_to_operator(condition),
179
+ "rhs": rhs,
180
+ }
181
+ elif isinstance(condition, AccessorExpression):
182
+ return {
183
+ "type": "BINARY_EXPRESSION",
184
+ "lhs": serialize_value(display_context, condition._base),
185
+ "operator": "accessField",
186
+ "rhs": serialize_value(display_context, condition._field),
187
+ }
188
+
189
+ raise UnsupportedSerializationException(f"Unsupported condition type: {condition.__class__.__name__}")
190
+
191
+
192
+ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> JsonObject:
193
+ if isinstance(value, ConstantValueReference):
194
+ return serialize_value(display_context, value._value)
195
+
196
+ if isinstance(value, LazyReference):
197
+ child_descriptor = get_child_descriptor(value, display_context)
198
+ return serialize_value(display_context, child_descriptor)
199
+
200
+ if isinstance(value, WorkflowInputReference):
201
+ workflow_input_display = display_context.global_workflow_input_displays[value]
202
+ return {
203
+ "type": "WORKFLOW_INPUT",
204
+ "input_variable_id": str(workflow_input_display.id),
205
+ }
206
+
207
+ if isinstance(value, StateValueReference):
208
+ state_value_display = display_context.global_state_value_displays[value]
209
+ return {
210
+ "type": "STATE_VALUE",
211
+ "state_variable_id": str(state_value_display.id),
212
+ }
213
+
214
+ if isinstance(value, OutputReference):
215
+ upstream_node, output_display = display_context.global_node_output_displays[value]
216
+ upstream_node_display = display_context.global_node_displays[upstream_node]
217
+
218
+ return {
219
+ "type": "NODE_OUTPUT",
220
+ "node_id": str(upstream_node_display.node_id),
221
+ "node_output_id": str(output_display.id),
222
+ }
223
+
224
+ if isinstance(value, VellumSecretReference):
225
+ return {
226
+ "type": "VELLUM_SECRET",
227
+ "vellum_secret_name": value.name,
228
+ }
229
+
230
+ if isinstance(value, ExecutionCountReference):
231
+ node_class_display = display_context.global_node_displays[value.node_class]
232
+
233
+ return {
234
+ "type": "EXECUTION_COUNTER",
235
+ "node_id": str(node_class_display.node_id),
236
+ }
237
+
238
+ if isinstance(value, dict) and any(isinstance(v, BaseDescriptor) for v in value.values()):
239
+ raise ValueError("Nested references are not supported.")
240
+
241
+ if not isinstance(value, BaseDescriptor):
242
+ vellum_value = primitive_to_vellum_value(value)
243
+ return {
244
+ "type": "CONSTANT_VALUE",
245
+ "value": vellum_value.dict(),
246
+ }
247
+
248
+ # If it's not any of the references we know about,
249
+ # then try to serialize it as a nested value
250
+ return serialize_condition(display_context, value)
@@ -2,36 +2,9 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
2
2
 
3
3
  from vellum.client.core.pydantic_utilities import UniversalBaseModel
4
4
  from vellum.client.types.array_vellum_value import ArrayVellumValue
5
- from vellum.client.types.logical_operator import LogicalOperator
6
5
  from vellum.client.types.vellum_value import VellumValue
7
6
  from vellum.client.types.vellum_variable_type import VellumVariableType
8
7
  from vellum.workflows.descriptors.base import BaseDescriptor
9
- from vellum.workflows.expressions.and_ import AndExpression
10
- from vellum.workflows.expressions.begins_with import BeginsWithExpression
11
- from vellum.workflows.expressions.between import BetweenExpression
12
- from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
13
- from vellum.workflows.expressions.contains import ContainsExpression
14
- from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
15
- from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
16
- from vellum.workflows.expressions.does_not_end_with import DoesNotEndWithExpression
17
- from vellum.workflows.expressions.does_not_equal import DoesNotEqualExpression
18
- from vellum.workflows.expressions.ends_with import EndsWithExpression
19
- from vellum.workflows.expressions.equals import EqualsExpression
20
- from vellum.workflows.expressions.greater_than import GreaterThanExpression
21
- from vellum.workflows.expressions.greater_than_or_equal_to import GreaterThanOrEqualToExpression
22
- from vellum.workflows.expressions.in_ import InExpression
23
- from vellum.workflows.expressions.is_nil import IsNilExpression
24
- from vellum.workflows.expressions.is_not_nil import IsNotNilExpression
25
- from vellum.workflows.expressions.is_not_null import IsNotNullExpression
26
- from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
27
- from vellum.workflows.expressions.is_null import IsNullExpression
28
- from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
29
- from vellum.workflows.expressions.less_than import LessThanExpression
30
- from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
31
- from vellum.workflows.expressions.not_between import NotBetweenExpression
32
- from vellum.workflows.expressions.not_in import NotInExpression
33
- from vellum.workflows.expressions.or_ import OrExpression
34
- from vellum.workflows.expressions.parse_json import ParseJsonExpression
35
8
  from vellum.workflows.nodes.bases.base import BaseNode
36
9
  from vellum.workflows.nodes.displayable.bases.utils import primitive_to_vellum_value
37
10
  from vellum.workflows.references import OutputReference, WorkflowInputReference
@@ -166,52 +139,3 @@ def create_node_input_value_pointer_rule(
166
139
  return ConstantValuePointer(type="CONSTANT_VALUE", data=vellum_value)
167
140
 
168
141
  raise UnsupportedSerializationException(f"Unsupported descriptor type: {value.__class__.__name__}")
169
-
170
-
171
- def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperator:
172
- if isinstance(descriptor, EqualsExpression):
173
- return "="
174
- elif isinstance(descriptor, DoesNotEqualExpression):
175
- return "!="
176
- elif isinstance(descriptor, LessThanExpression):
177
- return "<"
178
- elif isinstance(descriptor, GreaterThanExpression):
179
- return ">"
180
- elif isinstance(descriptor, LessThanOrEqualToExpression):
181
- return "<="
182
- elif isinstance(descriptor, GreaterThanOrEqualToExpression):
183
- return ">="
184
- elif isinstance(descriptor, ContainsExpression):
185
- return "contains"
186
- elif isinstance(descriptor, BeginsWithExpression):
187
- return "beginsWith"
188
- elif isinstance(descriptor, EndsWithExpression):
189
- return "endsWith"
190
- elif isinstance(descriptor, DoesNotContainExpression):
191
- return "doesNotContain"
192
- elif isinstance(descriptor, DoesNotBeginWithExpression):
193
- return "doesNotBeginWith"
194
- elif isinstance(descriptor, DoesNotEndWithExpression):
195
- return "doesNotEndWith"
196
- elif isinstance(descriptor, (IsNullExpression, IsNilExpression, IsUndefinedExpression)):
197
- return "null"
198
- elif isinstance(descriptor, (IsNotNullExpression, IsNotNilExpression, IsNotUndefinedExpression)):
199
- return "notNull"
200
- elif isinstance(descriptor, InExpression):
201
- return "in"
202
- elif isinstance(descriptor, NotInExpression):
203
- return "notIn"
204
- elif isinstance(descriptor, BetweenExpression):
205
- return "between"
206
- elif isinstance(descriptor, NotBetweenExpression):
207
- return "notBetween"
208
- elif isinstance(descriptor, AndExpression):
209
- return "and"
210
- elif isinstance(descriptor, OrExpression):
211
- return "or"
212
- elif isinstance(descriptor, CoalesceExpression):
213
- return "coalesce"
214
- elif isinstance(descriptor, ParseJsonExpression):
215
- return "parseJson"
216
- else:
217
- raise ValueError(f"Unsupported descriptor type: {descriptor}")
@@ -46,6 +46,7 @@ from vellum_ee.workflows.display.types import (
46
46
  WorkflowInputsDisplays,
47
47
  WorkflowOutputDisplays,
48
48
  )
49
+ from vellum_ee.workflows.display.utils.expressions import serialize_value
49
50
  from vellum_ee.workflows.display.utils.registry import register_workflow_display_class
50
51
  from vellum_ee.workflows.display.utils.vellum import infer_vellum_variable_type
51
52
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
@@ -124,26 +125,24 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
124
125
  }
125
126
  )
126
127
 
127
- nodes: JsonArray = []
128
+ serialized_nodes: Dict[UUID, JsonObject] = {}
128
129
  edges: JsonArray = []
129
130
 
130
131
  # Add a single synthetic node for the workflow entrypoint
131
132
  entrypoint_node_id = self.display_context.workflow_display.entrypoint_node_id
132
133
  entrypoint_node_source_handle_id = self.display_context.workflow_display.entrypoint_node_source_handle_id
133
- nodes.append(
134
- {
135
- "id": str(entrypoint_node_id),
136
- "type": "ENTRYPOINT",
137
- "inputs": [],
138
- "data": {
139
- "label": "Entrypoint Node",
140
- "source_handle_id": str(entrypoint_node_source_handle_id),
141
- },
142
- "display_data": self.display_context.workflow_display.entrypoint_node_display.dict(),
143
- "base": None,
144
- "definition": None,
134
+ serialized_nodes[entrypoint_node_id] = {
135
+ "id": str(entrypoint_node_id),
136
+ "type": "ENTRYPOINT",
137
+ "inputs": [],
138
+ "data": {
139
+ "label": "Entrypoint Node",
140
+ "source_handle_id": str(entrypoint_node_source_handle_id),
145
141
  },
146
- )
142
+ "display_data": self.display_context.workflow_display.entrypoint_node_display.dict(),
143
+ "base": None,
144
+ "definition": None,
145
+ }
147
146
 
148
147
  # Add all the nodes in the workflow
149
148
  for node in self._workflow.get_nodes():
@@ -155,7 +154,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
155
154
  self.add_error(e)
156
155
  continue
157
156
 
158
- nodes.append(serialized_node)
157
+ serialized_nodes[node_display.node_id] = serialized_node
159
158
 
160
159
  # Add all unused nodes in the workflow
161
160
  for node in self._workflow.get_unused_nodes():
@@ -167,10 +166,11 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
167
166
  self.add_error(e)
168
167
  continue
169
168
 
170
- nodes.append(serialized_node)
169
+ serialized_nodes[node_display.node_id] = serialized_node
171
170
 
172
171
  synthetic_output_edges: JsonArray = []
173
172
  output_variables: JsonArray = []
173
+ output_values: JsonArray = []
174
174
  final_output_nodes = [
175
175
  node for node in self.display_context.node_displays.keys() if issubclass(node, FinalOutputNode)
176
176
  ]
@@ -185,9 +185,9 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
185
185
  for workflow_output, workflow_output_display in self.display_context.workflow_output_displays.items():
186
186
  final_output_node_id = uuid4_from_hash(f"{self.workflow_id}|node_id|{workflow_output.name}")
187
187
  inferred_type = infer_vellum_variable_type(workflow_output)
188
-
189
188
  # Remove the terminal node output from the unreferenced set
190
- unreferenced_final_output_node_outputs.discard(cast(OutputReference, workflow_output.instance))
189
+ if isinstance(workflow_output.instance, OutputReference):
190
+ unreferenced_final_output_node_outputs.discard(workflow_output.instance)
191
191
 
192
192
  if workflow_output.instance not in final_output_node_outputs:
193
193
  # Create a synthetic terminal node only if there is no terminal node for this output
@@ -220,24 +220,22 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
220
220
  )
221
221
  synthetic_display_data = NodeDisplayData().dict()
222
222
  synthetic_node_label = "Final Output"
223
- nodes.append(
224
- {
225
- "id": str(final_output_node_id),
226
- "type": "TERMINAL",
227
- "data": {
228
- "label": synthetic_node_label,
229
- "name": workflow_output_display.name,
230
- "target_handle_id": synthetic_target_handle_id,
231
- "output_id": str(workflow_output_display.id),
232
- "output_type": inferred_type,
233
- "node_input_id": str(node_input.id),
234
- },
235
- "inputs": [node_input.dict()],
236
- "display_data": synthetic_display_data,
237
- "base": final_output_node_base,
238
- "definition": None,
239
- }
240
- )
223
+ serialized_nodes[final_output_node_id] = {
224
+ "id": str(final_output_node_id),
225
+ "type": "TERMINAL",
226
+ "data": {
227
+ "label": synthetic_node_label,
228
+ "name": workflow_output_display.name,
229
+ "target_handle_id": synthetic_target_handle_id,
230
+ "output_id": str(workflow_output_display.id),
231
+ "output_type": inferred_type,
232
+ "node_input_id": str(node_input.id),
233
+ },
234
+ "inputs": [node_input.dict()],
235
+ "display_data": synthetic_display_data,
236
+ "base": final_output_node_base,
237
+ "definition": None,
238
+ }
241
239
 
242
240
  if source_node_display:
243
241
  source_handle_id = source_node_display.get_source_handle_id(
@@ -255,6 +253,19 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
255
253
  }
256
254
  )
257
255
 
256
+ elif isinstance(workflow_output.instance, OutputReference):
257
+ terminal_node_id = workflow_output.instance.outputs_class._node_class.__id__
258
+ serialized_terminal_node = serialized_nodes.get(terminal_node_id)
259
+ if serialized_terminal_node and isinstance(serialized_terminal_node["data"], dict):
260
+ serialized_terminal_node["data"]["name"] = workflow_output_display.name
261
+
262
+ output_values.append(
263
+ {
264
+ "output_variable_id": str(workflow_output_display.id),
265
+ "value": serialize_value(self.display_context, workflow_output.instance),
266
+ }
267
+ )
268
+
258
269
  output_variables.append(
259
270
  {
260
271
  "id": str(workflow_output_display.id),
@@ -309,13 +320,14 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
309
320
 
310
321
  return {
311
322
  "workflow_raw_data": {
312
- "nodes": nodes,
323
+ "nodes": list(serialized_nodes.values()),
313
324
  "edges": edges,
314
325
  "display_data": self.display_context.workflow_display.display_data.dict(),
315
326
  "definition": {
316
327
  "name": self._workflow.__name__,
317
328
  "module": cast(JsonArray, self._workflow.__module__.split(".")),
318
329
  },
330
+ "output_values": output_values,
319
331
  },
320
332
  "input_variables": input_variables,
321
333
  "state_variables": state_variables,
@@ -6,6 +6,7 @@ from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubwo
6
6
  from vellum.workflows.nodes.core.retry_node.node import RetryNode
7
7
  from vellum.workflows.nodes.core.templating_node.node import TemplatingNode
8
8
  from vellum.workflows.nodes.core.try_node.node import TryNode
9
+ from vellum.workflows.nodes.displayable.final_output_node.node import FinalOutputNode
9
10
  from vellum.workflows.workflows.base import BaseWorkflow
10
11
  from vellum_ee.workflows.display.editor.types import NodeDisplayData, NodeDisplayPosition
11
12
  from vellum_ee.workflows.display.nodes import BaseNodeDisplay
@@ -327,3 +328,47 @@ def test_serialize_workflow__inherited_workflow_display_class_not_registered():
327
328
 
328
329
  # THEN it should should succeed
329
330
  assert data is not None
331
+
332
+
333
+ def test_serialize_workflow__terminal_node_mismatches_workflow_output_name():
334
+ # GIVEN a node
335
+ class ExitNode(FinalOutputNode):
336
+ class Outputs(FinalOutputNode.Outputs):
337
+ value = "hello"
338
+
339
+ # AND a workflow that uses the node
340
+ class MyWorkflow(BaseWorkflow):
341
+ graph = ExitNode
342
+
343
+ class Outputs(BaseWorkflow.Outputs):
344
+ answer = ExitNode.Outputs.value
345
+
346
+ # WHEN we serialize it
347
+ workflow_display = get_workflow_display(workflow_class=MyWorkflow)
348
+ data = workflow_display.serialize()
349
+
350
+ # THEN it should have an output name that matches the workflow output
351
+ assert isinstance(data["workflow_raw_data"], dict)
352
+ assert isinstance(data["workflow_raw_data"]["nodes"], list)
353
+ terminal_node = [
354
+ node for node in data["workflow_raw_data"]["nodes"] if isinstance(node, dict) and node["type"] == "TERMINAL"
355
+ ][0]
356
+ assert isinstance(terminal_node["data"], dict)
357
+ assert terminal_node["data"]["name"] == "answer"
358
+
359
+ # AND the output variable should have the correct name
360
+ assert isinstance(data["output_variables"], list)
361
+ assert isinstance(data["output_variables"][0], dict)
362
+ assert data["output_variables"][0]["key"] == "answer"
363
+ assert data["output_variables"][0]["type"] == "STRING"
364
+
365
+ # AND the output value should have the correct name
366
+ output_variable_id = data["output_variables"][0]["id"]
367
+ assert isinstance(data["workflow_raw_data"]["output_values"], list)
368
+ assert isinstance(data["workflow_raw_data"]["output_values"][0], dict)
369
+ assert data["workflow_raw_data"]["output_values"][0]["output_variable_id"] == output_variable_id
370
+ assert data["workflow_raw_data"]["output_values"][0]["value"] == {
371
+ "type": "NODE_OUTPUT",
372
+ "node_id": str(ExitNode.__id__),
373
+ "node_output_id": str(ExitNode.__output_ids__["value"]),
374
+ }