vellum-ai 1.3.4__py3-none-any.whl → 1.3.6__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 (31) hide show
  1. vellum/client/README.md +16 -0
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/plugins/vellum_mypy.py +12 -9
  4. vellum/workflows/executable.py +9 -0
  5. vellum/workflows/graph/graph.py +5 -0
  6. vellum/workflows/graph/tests/test_graph.py +17 -0
  7. vellum/workflows/nodes/bases/base.py +4 -8
  8. vellum/workflows/nodes/core/map_node/node.py +8 -1
  9. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +11 -4
  10. vellum/workflows/nodes/displayable/guardrail_node/node.py +8 -3
  11. vellum/workflows/nodes/displayable/tool_calling_node/node.py +6 -0
  12. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +9 -1
  13. vellum/workflows/outputs/base.py +19 -10
  14. vellum/workflows/references/output.py +4 -6
  15. vellum/workflows/utils/names.py +12 -4
  16. vellum/workflows/utils/tests/test_names.py +9 -0
  17. vellum/workflows/workflows/base.py +9 -3
  18. {vellum_ai-1.3.4.dist-info → vellum_ai-1.3.6.dist-info}/METADATA +1 -1
  19. {vellum_ai-1.3.4.dist-info → vellum_ai-1.3.6.dist-info}/RECORD +31 -28
  20. vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +17 -0
  21. vellum_ee/workflows/display/tests/test_base_workflow_display.py +47 -0
  22. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +1 -1
  23. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_parent_input.py +89 -0
  24. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
  25. vellum_ee/workflows/display/tests/workflow_serialization/test_final_output_node_map_reference_serialization.py +88 -0
  26. vellum_ee/workflows/display/utils/expressions.py +45 -12
  27. vellum_ee/workflows/display/utils/vellum.py +3 -1
  28. vellum_ee/workflows/display/workflows/base_workflow_display.py +3 -5
  29. {vellum_ai-1.3.4.dist-info → vellum_ai-1.3.6.dist-info}/LICENSE +0 -0
  30. {vellum_ai-1.3.4.dist-info → vellum_ai-1.3.6.dist-info}/WHEEL +0 -0
  31. {vellum_ai-1.3.4.dist-info → vellum_ai-1.3.6.dist-info}/entry_points.txt +0 -0
vellum/client/README.md CHANGED
@@ -232,3 +232,19 @@ client = Vellum(
232
232
  )
233
233
  ```
234
234
 
235
+ ## Contributing
236
+
237
+ While we value open-source contributions to this SDK, this library is generated programmatically.
238
+ Additions made directly to this library would have to be moved over to our generation code,
239
+ otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
240
+ a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
241
+ an issue first to discuss with us!
242
+
243
+ On the other hand, contributions to the README are always very welcome!
244
+ ## Open-Source vs. Paid
245
+
246
+ This repo is available under the [MIT expat license](https://github.com/vellum-ai/vellum-python-sdks/blob/main/LICENSE), except
247
+ for the `ee` directory (which has its [license here](https://github.com/vellum-ai/vellum-python-sdks/blob/main/ee/LICENSE)) if applicable.
248
+
249
+ To learn more, [book a demo](https://www.vellum.ai/landing-pages/request-demo) or see our [pricing page](https://www.vellum.ai/pricing).
250
+
@@ -27,10 +27,10 @@ class BaseClientWrapper:
27
27
 
28
28
  def get_headers(self) -> typing.Dict[str, str]:
29
29
  headers: typing.Dict[str, str] = {
30
- "User-Agent": "vellum-ai/1.3.4",
30
+ "User-Agent": "vellum-ai/1.3.6",
31
31
  "X-Fern-Language": "Python",
32
32
  "X-Fern-SDK-Name": "vellum-ai",
33
- "X-Fern-SDK-Version": "1.3.4",
33
+ "X-Fern-SDK-Version": "1.3.6",
34
34
  **(self.get_custom_headers() or {}),
35
35
  }
36
36
  if self._api_version is not None:
@@ -20,6 +20,7 @@ from mypy.types import AnyType, CallableType, FunctionLike, Instance, Type as My
20
20
 
21
21
  TypeResolver = Callable[[str, List[MypyType]], MypyType]
22
22
  NODE_ATTRIBUTE_REGEX = r"^[a-z].*$"
23
+ BASE_NODE_FULLNAME = "vellum.workflows.nodes.bases.base.BaseNode"
23
24
 
24
25
  DESCRIPTOR_PATHS: list[tuple[str, str, str]] = [
25
26
  (
@@ -28,12 +29,12 @@ DESCRIPTOR_PATHS: list[tuple[str, str, str]] = [
28
29
  r"^[^_].*$",
29
30
  ),
30
31
  (
31
- "vellum.workflows.nodes.bases.base.BaseNode.ExternalInputs",
32
+ f"{BASE_NODE_FULLNAME}.ExternalInputs",
32
33
  "vellum.workflows.references.external_input.ExternalInputReference",
33
34
  r"^[^_].*$",
34
35
  ),
35
36
  (
36
- "vellum.workflows.nodes.bases.base.BaseNode.Execution",
37
+ f"{BASE_NODE_FULLNAME}.Execution",
37
38
  "vellum.workflows.references.execution_count.ExecutionCountReference",
38
39
  r"^count$",
39
40
  ),
@@ -48,7 +49,7 @@ DESCRIPTOR_PATHS: list[tuple[str, str, str]] = [
48
49
  r"^[^_].*$",
49
50
  ),
50
51
  (
51
- "vellum.workflows.nodes.bases.base.BaseNode",
52
+ BASE_NODE_FULLNAME,
52
53
  "vellum.workflows.references.node.NodeReference",
53
54
  NODE_ATTRIBUTE_REGEX,
54
55
  ),
@@ -163,7 +164,7 @@ class VellumMypyPlugin(Plugin):
163
164
  elif _is_subclass(ctx.cls.info, "vellum.workflows.nodes.displayable.final_output_node.node.FinalOutputNode"):
164
165
  self._dynamic_output_node_class_hook(ctx, "value")
165
166
 
166
- if _is_subclass(ctx.cls.info, "vellum.workflows.nodes.bases.base.BaseNode"):
167
+ if _is_subclass(ctx.cls.info, BASE_NODE_FULLNAME):
167
168
  return self._base_node_class_hook(ctx)
168
169
 
169
170
  if _is_subclass(ctx.cls.info, "vellum.workflows.workflows.base.BaseWorkflow"):
@@ -241,6 +242,9 @@ class VellumMypyPlugin(Plugin):
241
242
  """
242
243
 
243
244
  for key, sym in ctx.cls.info.names.items():
245
+ if ctx.cls.info.fullname == BASE_NODE_FULLNAME:
246
+ continue
247
+
244
248
  if not isinstance(sym.node, Var):
245
249
  continue
246
250
 
@@ -250,6 +254,7 @@ class VellumMypyPlugin(Plugin):
250
254
  type_ = sym.node.type
251
255
  if not type_:
252
256
  continue
257
+
253
258
  sym.node.type = self._get_resolvable_type(
254
259
  lambda fullname, types: ctx.api.named_type(fullname, types), type_
255
260
  )
@@ -301,7 +306,7 @@ class VellumMypyPlugin(Plugin):
301
306
  and isinstance(arg.expr, MemberExpr)
302
307
  and isinstance(arg.expr.expr, NameExpr)
303
308
  and isinstance(arg.expr.expr.node, TypeInfo)
304
- and _is_subclass(arg.expr.expr.node, "vellum.workflows.nodes.bases.base.BaseNode")
309
+ and _is_subclass(arg.expr.expr.node, BASE_NODE_FULLNAME)
305
310
  and arg.expr.name == "Outputs"
306
311
  ):
307
312
  self._calls_with_nested_descriptor_expressions.add(call_expr)
@@ -548,7 +553,7 @@ class VellumMypyPlugin(Plugin):
548
553
  if not isinstance(ctx.default_return_type, Instance):
549
554
  return ctx.default_return_type
550
555
 
551
- if not _is_subclass(ctx.default_return_type.type, "vellum.workflows.nodes.bases.base.BaseNode.Outputs"):
556
+ if not _is_subclass(ctx.default_return_type.type, f"{BASE_NODE_FULLNAME}.Outputs"):
552
557
  return ctx.default_return_type
553
558
 
554
559
  outputs_node = self._get_node_outputs_type_info(ctx)
@@ -677,9 +682,7 @@ class VellumMypyPlugin(Plugin):
677
682
 
678
683
  expr = ctx.context.callee.expr
679
684
  instance = ctx.api.get_expression_type(expr)
680
- if not isinstance(instance, Instance) or not _is_subclass(
681
- instance.type, "vellum.workflows.nodes.bases.base.BaseNode"
682
- ):
685
+ if not isinstance(instance, Instance) or not _is_subclass(instance.type, BASE_NODE_FULLNAME):
683
686
  return None
684
687
 
685
688
  outputs_node = instance.type.names.get("Outputs")
@@ -0,0 +1,9 @@
1
+ from uuid import UUID
2
+ from typing import Dict
3
+
4
+ from vellum.workflows.utils.uuids import uuid4_from_hash
5
+
6
+
7
+ class BaseExecutable:
8
+ __id__: UUID = uuid4_from_hash(__qualname__)
9
+ __output_ids__: Dict[str, UUID] = {}
@@ -96,6 +96,11 @@ class Graph:
96
96
  def from_edge(edge: Edge) -> "Graph":
97
97
  return Graph(entrypoints={edge.from_port}, edges=[edge], terminals={port for port in edge.to_node.Ports})
98
98
 
99
+ @staticmethod
100
+ def empty() -> "Graph":
101
+ """Create an empty graph with no entrypoints, edges, or terminals."""
102
+ return Graph(entrypoints=set(), edges=[], terminals=set())
103
+
99
104
  def __rshift__(self, other: GraphTarget) -> "Graph":
100
105
  if not self._edges and not self._entrypoints:
101
106
  raise ValueError("Graph instance can only create new edges from nodes within existing edges")
@@ -4,6 +4,23 @@ from vellum.workflows.nodes.bases.base import BaseNode
4
4
  from vellum.workflows.ports.port import Port
5
5
 
6
6
 
7
+ def test_graph__empty():
8
+ # WHEN we create an empty graph
9
+ graph = Graph.empty()
10
+
11
+ # THEN the graph has no entrypoints
12
+ assert len(list(graph.entrypoints)) == 0
13
+
14
+ # AND no nodes
15
+ assert len(list(graph.nodes)) == 0
16
+
17
+ # AND no edges
18
+ assert len(list(graph.edges)) == 0
19
+
20
+ # AND string representation indicates empty graph
21
+ assert str(graph) == "Graph(empty)"
22
+
23
+
7
24
  def test_graph__from_node():
8
25
  # GIVEN a node
9
26
  class MyNode(BaseNode):
@@ -13,6 +13,7 @@ from vellum.workflows.descriptors.utils import is_unresolved, resolve_value
13
13
  from vellum.workflows.errors.types import WorkflowErrorCode
14
14
  from vellum.workflows.events.node import NodeExecutionStreamingEvent
15
15
  from vellum.workflows.exceptions import NodeException
16
+ from vellum.workflows.executable import BaseExecutable
16
17
  from vellum.workflows.graph import Graph
17
18
  from vellum.workflows.graph.graph import GraphTarget
18
19
  from vellum.workflows.inputs.base import BaseInputs
@@ -120,7 +121,7 @@ class BaseNodeMeta(ABCMeta):
120
121
  cls = super().__new__(mcs, name, bases, dct)
121
122
  node_class = cast(Type["BaseNode"], cls)
122
123
 
123
- node_class.Outputs._node_class = node_class
124
+ node_class.Outputs.__parent_class__ = node_class
124
125
 
125
126
  # Add cls to relevant nested classes, since python should've been doing this by default
126
127
  for port in node_class.Ports:
@@ -260,9 +261,7 @@ class _BaseNodeExecutionMeta(type):
260
261
  NodeRunResponse = Union[BaseOutputs, Iterator[BaseOutput]]
261
262
 
262
263
 
263
- class BaseNode(Generic[StateType], ABC, metaclass=BaseNodeMeta):
264
- __id__: UUID = uuid4_from_hash(__qualname__)
265
- __output_ids__: Dict[str, UUID] = {}
264
+ class BaseNode(Generic[StateType], ABC, BaseExecutable, metaclass=BaseNodeMeta):
266
265
  state: StateType
267
266
  _context: WorkflowContext
268
267
  _inputs: MappingProxyType[NodeReference, Any]
@@ -270,11 +269,8 @@ class BaseNode(Generic[StateType], ABC, metaclass=BaseNodeMeta):
270
269
  class ExternalInputs(BaseInputs):
271
270
  __descriptor_class__ = ExternalInputReference
272
271
 
273
- # TODO: Consider using metaclasses to prevent the need for users to specify that the
274
- # "Outputs" class inherits from "BaseOutputs" and do so automatically.
275
- # https://app.shortcut.com/vellum/story/4008/auto-inherit-basenodeoutputs-in-outputs-classes
276
272
  class Outputs(BaseOutputs):
277
- _node_class: Type["BaseNode"] = field(init=False)
273
+ __parent_class__: Type["BaseNode"] = field(init=False)
278
274
 
279
275
  class Ports(NodePorts):
280
276
  default = Port(default=True)
@@ -27,6 +27,7 @@ from vellum.workflows.nodes.bases.base_adornment_node import BaseAdornmentNode
27
27
  from vellum.workflows.nodes.utils import create_adornment
28
28
  from vellum.workflows.outputs import BaseOutputs
29
29
  from vellum.workflows.outputs.base import BaseOutput
30
+ from vellum.workflows.references.node import NodeReference
30
31
  from vellum.workflows.references.output import OutputReference
31
32
  from vellum.workflows.state.context import WorkflowContext
32
33
  from vellum.workflows.types.generics import StateType
@@ -217,5 +218,11 @@ class MapNode(BaseAdornmentNode[StateType], Generic[StateType, MapNodeItemType])
217
218
  # value: List[str]
218
219
  outputs_class.__annotations__ = {**previous_annotations, reference.name: annotation}
219
220
 
220
- output_id = uuid4_from_hash(f"{cls.__id__}|{reference.name}")
221
+ subworkflow_class = cls.subworkflow.instance if isinstance(cls.subworkflow, NodeReference) else None
222
+ if subworkflow_class:
223
+ output_id = subworkflow_class.__output_ids__.get(reference.name) or uuid4_from_hash(
224
+ f"{cls.__id__}|{reference.name}"
225
+ )
226
+ else:
227
+ output_id = uuid4_from_hash(f"{cls.__id__}|{reference.name}")
221
228
  cls.__output_ids__[reference.name] = output_id
@@ -100,7 +100,8 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
100
100
  execution_context = get_execution_context()
101
101
  request_options = self.request_options or RequestOptions()
102
102
 
103
- processed_parameters = self._process_parameters(self.parameters)
103
+ processed_parameters = self.process_parameters(self.parameters)
104
+ processed_blocks = self.process_blocks(self.blocks)
104
105
 
105
106
  request_options["additional_body_parameters"] = {
106
107
  "execution_context": execution_context.model_dump(mode="json"),
@@ -139,7 +140,7 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
139
140
  input_values=input_values,
140
141
  input_variables=input_variables,
141
142
  parameters=processed_parameters,
142
- blocks=self.blocks,
143
+ blocks=processed_blocks,
143
144
  settings=self.settings,
144
145
  functions=normalized_functions,
145
146
  expand_meta=self.expand_meta,
@@ -152,7 +153,7 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
152
153
  input_values=input_values,
153
154
  input_variables=input_variables,
154
155
  parameters=processed_parameters,
155
- blocks=self.blocks,
156
+ blocks=processed_blocks,
156
157
  settings=self.settings,
157
158
  functions=normalized_functions,
158
159
  expand_meta=self.expand_meta,
@@ -300,7 +301,7 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
300
301
 
301
302
  return input_variables, input_values
302
303
 
303
- def _process_parameters(self, parameters: PromptParameters) -> PromptParameters:
304
+ def process_parameters(self, parameters: PromptParameters) -> PromptParameters:
304
305
  """
305
306
  Process parameters to recursively convert any Pydantic models to JSON schema dictionaries.
306
307
  """
@@ -310,3 +311,9 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
310
311
  processed_custom_params = normalize_json(parameters.custom_parameters)
311
312
 
312
313
  return parameters.model_copy(update={"custom_parameters": processed_custom_params})
314
+
315
+ def process_blocks(self, blocks: List[PromptBlock]) -> List[PromptBlock]:
316
+ """
317
+ Override this method to process the blocks before they are executed.
318
+ """
319
+ return blocks
@@ -8,7 +8,6 @@ from vellum.workflows.constants import LATEST_RELEASE_TAG
8
8
  from vellum.workflows.errors.types import WorkflowErrorCode
9
9
  from vellum.workflows.exceptions import NodeException
10
10
  from vellum.workflows.nodes.bases import BaseNode
11
- from vellum.workflows.outputs.base import BaseOutputs
12
11
  from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior
13
12
  from vellum.workflows.types.generics import StateType
14
13
 
@@ -33,7 +32,7 @@ class GuardrailNode(BaseNode[StateType], Generic[StateType]):
33
32
  class Trigger(BaseNode.Trigger):
34
33
  merge_behavior = MergeBehavior.AWAIT_ANY
35
34
 
36
- class Outputs(BaseOutputs):
35
+ class Outputs(BaseNode.Outputs):
37
36
  score: float
38
37
  normalized_score: Optional[float]
39
38
  log: Optional[str]
@@ -98,7 +97,13 @@ class GuardrailNode(BaseNode[StateType], Generic[StateType]):
98
97
  else:
99
98
  reason = None
100
99
 
101
- return self.Outputs(score=score, normalized_score=normalized_score, log=log, reason=reason, **metric_outputs)
100
+ return self.Outputs(
101
+ score=score,
102
+ normalized_score=normalized_score,
103
+ log=log,
104
+ reason=reason,
105
+ **metric_outputs, # type: ignore [arg-type]
106
+ )
102
107
 
103
108
  def _compile_metric_inputs(self) -> List[MetricDefinitionInput]:
104
109
  # TODO: We may want to consolidate with prompt deployment input compilation
@@ -138,6 +138,10 @@ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
138
138
  )
139
139
 
140
140
  def _build_graph(self) -> None:
141
+ # Get the process_parameters method if it exists on this class
142
+ process_parameters_method = getattr(self.__class__, "process_parameters", None)
143
+ process_blocks_method = getattr(self.__class__, "process_blocks", None)
144
+
141
145
  self.tool_prompt_node = create_tool_prompt_node(
142
146
  ml_model=self.ml_model,
143
147
  blocks=self.blocks,
@@ -145,6 +149,8 @@ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
145
149
  prompt_inputs=self.prompt_inputs,
146
150
  parameters=self.parameters,
147
151
  max_prompt_iterations=self.max_prompt_iterations,
152
+ process_parameters_method=process_parameters_method,
153
+ process_blocks_method=process_blocks_method,
148
154
  )
149
155
 
150
156
  # Create the router node (handles routing logic only)
@@ -14,6 +14,7 @@ from vellum.client.types.prompt_output import PromptOutput
14
14
  from vellum.client.types.prompt_parameters import PromptParameters
15
15
  from vellum.client.types.string_chat_message_content import StringChatMessageContent
16
16
  from vellum.client.types.variable_prompt_block import VariablePromptBlock
17
+ from vellum.workflows.descriptors.base import BaseDescriptor
17
18
  from vellum.workflows.errors.types import WorkflowErrorCode
18
19
  from vellum.workflows.exceptions import NodeException
19
20
  from vellum.workflows.expressions.concat import ConcatExpression
@@ -329,6 +330,8 @@ def create_tool_prompt_node(
329
330
  prompt_inputs: Optional[EntityInputsInterface],
330
331
  parameters: PromptParameters,
331
332
  max_prompt_iterations: Optional[int] = None,
333
+ process_parameters_method: Optional[Callable] = None,
334
+ process_blocks_method: Optional[Callable] = None,
332
335
  ) -> Type[ToolPromptNode]:
333
336
  if functions and len(functions) > 0:
334
337
  prompt_functions: List[Union[Tool, FunctionDefinition]] = []
@@ -398,6 +401,8 @@ def create_tool_prompt_node(
398
401
  "prompt_inputs": node_prompt_inputs,
399
402
  "parameters": parameters,
400
403
  "max_prompt_iterations": max_prompt_iterations,
404
+ **({"process_parameters": process_parameters_method} if process_parameters_method is not None else {}),
405
+ **({"process_blocks": process_blocks_method} if process_blocks_method is not None else {}),
401
406
  "__module__": __name__,
402
407
  },
403
408
  ),
@@ -527,7 +532,10 @@ def create_function_node(
527
532
  inputs = getattr(func, "__vellum_inputs__", {})
528
533
  if inputs:
529
534
  for param_name, param_ref in inputs.items():
530
- resolved_value = param_ref.resolve(self.state)
535
+ if isinstance(param_ref, BaseDescriptor):
536
+ resolved_value = param_ref.resolve(self.state)
537
+ else:
538
+ resolved_value = param_ref
531
539
  merged_kwargs[param_name] = resolved_value
532
540
 
533
541
  return func(**merged_kwargs)
@@ -1,5 +1,6 @@
1
+ from dataclasses import field
1
2
  import inspect
2
- from typing import TYPE_CHECKING, Any, Generic, Iterator, Set, Tuple, Type, TypeVar, Union, cast
3
+ from typing import Any, Dict, Generic, Iterator, Set, Tuple, Type, TypeVar, Union, cast
3
4
  from typing_extensions import dataclass_transform
4
5
 
5
6
  from pydantic import GetCoreSchemaHandler
@@ -9,13 +10,11 @@ from vellum.workflows.constants import undefined
9
10
  from vellum.workflows.descriptors.base import BaseDescriptor
10
11
  from vellum.workflows.errors.types import WorkflowErrorCode
11
12
  from vellum.workflows.exceptions import NodeException
13
+ from vellum.workflows.executable import BaseExecutable
12
14
  from vellum.workflows.references.output import OutputReference
13
15
  from vellum.workflows.types.generics import is_node_instance
14
16
  from vellum.workflows.types.utils import get_class_attr_names, infer_types
15
17
 
16
- if TYPE_CHECKING:
17
- from vellum.workflows.nodes.bases.base import BaseNode
18
-
19
18
  _Delta = TypeVar("_Delta")
20
19
  _Accumulated = TypeVar("_Accumulated")
21
20
 
@@ -101,6 +100,10 @@ class BaseOutput(Generic[_Delta, _Accumulated]):
101
100
 
102
101
  @dataclass_transform(kw_only_default=True)
103
102
  class _BaseOutputsMeta(type):
103
+ def __new__(cls, name: str, bases: Tuple[Type, ...], dct: Dict[str, Any]) -> Any:
104
+ dct["__parent_class__"] = type(None)
105
+ return super().__new__(cls, name, bases, dct)
106
+
104
107
  def __eq__(cls, other: Any) -> bool:
105
108
  """
106
109
  We need to include custom eq logic to prevent infinite loops during ipython reloading.
@@ -112,17 +115,21 @@ class _BaseOutputsMeta(type):
112
115
  if not cls.__qualname__.endswith(".Outputs") or not other.__qualname__.endswith(".Outputs"):
113
116
  return super().__eq__(other)
114
117
 
115
- self_outputs_class = cast(Type["BaseNode.Outputs"], cls)
116
- other_outputs_class = cast(Type["BaseNode.Outputs"], other)
118
+ self_outputs_class = cast(Type["BaseOutputs"], cls)
119
+ other_outputs_class = cast(Type["BaseOutputs"], other)
117
120
 
118
- if not hasattr(self_outputs_class, "_node_class") or not hasattr(other_outputs_class, "_node_class"):
121
+ if not hasattr(self_outputs_class, "__parent_class__") or not hasattr(other_outputs_class, "__parent_class__"):
119
122
  return super().__eq__(other)
120
123
 
121
- if self_outputs_class._node_class is None or other_outputs_class._node_class is None:
124
+ if self_outputs_class.__parent_class__ is None or other_outputs_class.__parent_class__ is None:
122
125
  return super().__eq__(other)
123
126
 
124
- return getattr(self_outputs_class._node_class, "__qualname__") == getattr(
125
- other_outputs_class._node_class, "__qualname__"
127
+ if self_outputs_class.__parent_class__ is type(None) or other_outputs_class.__parent_class__ is type(None):
128
+ return super().__eq__(other)
129
+
130
+ return (
131
+ self_outputs_class.__parent_class__.__qualname__ == other_outputs_class.__parent_class__.__qualname__
132
+ and self_outputs_class.__parent_class__.__module__ == other_outputs_class.__parent_class__.__module__
126
133
  )
127
134
 
128
135
  def __setattr__(cls, name: str, value: Any) -> None:
@@ -187,6 +194,8 @@ class _BaseOutputsMeta(type):
187
194
 
188
195
 
189
196
  class BaseOutputs(metaclass=_BaseOutputsMeta):
197
+ __parent_class__: Type[BaseExecutable] = field(init=False)
198
+
190
199
  def __init__(self, **kwargs: Any) -> None:
191
200
  declared_fields = {descriptor.name for descriptor in self.__class__}
192
201
  provided_fields = set(kwargs.keys())
@@ -35,13 +35,11 @@ class OutputReference(BaseDescriptor[_OutputType], Generic[_OutputType]):
35
35
 
36
36
  @cached_property
37
37
  def id(self) -> UUID:
38
- self._outputs_class = self._outputs_class
39
-
40
- node_class = getattr(self._outputs_class, "_node_class", None)
41
- if not node_class:
38
+ parent_class = self._outputs_class.__parent_class__
39
+ if not parent_class:
42
40
  return uuid4()
43
41
 
44
- output_ids = getattr(node_class, "__output_ids__", {})
42
+ output_ids = getattr(parent_class, "__output_ids__", {})
45
43
  if not isinstance(output_ids, dict):
46
44
  return uuid4()
47
45
 
@@ -78,7 +76,7 @@ class OutputReference(BaseDescriptor[_OutputType], Generic[_OutputType]):
78
76
  def __eq__(self, other: object) -> bool:
79
77
  if not isinstance(other, type(self)):
80
78
  return False
81
- return super().__eq__(other) and id(self._outputs_class) == id(other._outputs_class)
79
+ return super().__eq__(other) and self._outputs_class == other._outputs_class
82
80
 
83
81
  def __hash__(self) -> int:
84
82
  return hash((self._outputs_class, self._name))
@@ -2,11 +2,19 @@ import re
2
2
 
3
3
 
4
4
  def pascal_to_title_case(pascal_str: str) -> str:
5
- # Insert space before each capital letter (except the first one)
6
- title_case_str = re.sub(r"(?<!^)(?=[A-Z])", " ", pascal_str)
5
+ title_case_str = re.sub(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", pascal_str)
7
6
 
8
- # Return the result in title case
9
- return title_case_str.title()
7
+ words = title_case_str.split()
8
+ result_words = []
9
+
10
+ for word in words:
11
+ if word.isupper():
12
+ result_words.append(word)
13
+ # Otherwise, apply title case
14
+ else:
15
+ result_words.append(word.capitalize())
16
+
17
+ return " ".join(result_words)
10
18
 
11
19
 
12
20
  def snake_to_title_case(snake_str: str) -> str:
@@ -8,6 +8,15 @@ from vellum.workflows.utils.names import pascal_to_title_case
8
8
  [
9
9
  ("MyPascalCaseString", "My Pascal Case String"),
10
10
  ("AnotherPascalCaseString", "Another Pascal Case String"),
11
+ ("FetchDeploymentScheme", "Fetch Deployment Scheme"),
12
+ ("CheckDeploymentExists", "Check Deployment Exists"),
13
+ ("APINode", "API Node"),
14
+ ("HTTPSConnection", "HTTPS Connection"),
15
+ ("XMLParser", "XML Parser"),
16
+ ("SimpleWord", "Simple Word"),
17
+ ("A", "A"),
18
+ ("AB", "AB"),
19
+ ("ABCDef", "ABC Def"),
11
20
  ],
12
21
  )
13
22
  def test_pascal_to_title_case(input_str, expected):
@@ -1,3 +1,4 @@
1
+ from dataclasses import field
1
2
  from datetime import datetime
2
3
  from functools import lru_cache
3
4
  import importlib
@@ -61,6 +62,7 @@ from vellum.workflows.events.workflow import (
61
62
  WorkflowExecutionStreamingBody,
62
63
  WorkflowExecutionStreamingEvent,
63
64
  )
65
+ from vellum.workflows.executable import BaseExecutable
64
66
  from vellum.workflows.graph import Graph
65
67
  from vellum.workflows.inputs.base import BaseInputs
66
68
  from vellum.workflows.nodes.bases import BaseNode
@@ -199,14 +201,18 @@ class _BaseWorkflowMeta(type):
199
201
  if inputs_class is not BaseInputs and inputs_class.__parent_class__ is type(None):
200
202
  inputs_class.__parent_class__ = workflow_class
201
203
 
204
+ workflow_class.Outputs.__parent_class__ = workflow_class
205
+ workflow_class.__output_ids__ = {
206
+ ref.name: uuid4_from_hash(f"{workflow_class.__id__}|id|{ref.name}") for ref in workflow_class.Outputs
207
+ }
208
+
202
209
  return workflow_class
203
210
 
204
211
 
205
212
  GraphAttribute = Union[Type[BaseNode], Graph, Set[Type[BaseNode]], Set[Graph]]
206
213
 
207
214
 
208
- class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
209
- __id__: UUID = uuid4_from_hash(__qualname__)
215
+ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_BaseWorkflowMeta):
210
216
  graph: ClassVar[GraphAttribute]
211
217
  unused_graphs: ClassVar[Set[GraphAttribute]] # nodes or graphs that are defined but not used in the graph
212
218
  emitters: List[BaseWorkflowEmitter]
@@ -214,7 +220,7 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
214
220
  is_dynamic: ClassVar[bool] = False
215
221
 
216
222
  class Outputs(BaseOutputs):
217
- pass
223
+ __parent_class__: Type["BaseWorkflow"] = field(init=False)
218
224
 
219
225
  WorkflowEvent = Union[ # type: ignore
220
226
  GenericWorkflowEvent,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -33,7 +33,7 @@ vellum_ee/workflows/display/nodes/__init__.py,sha256=jI1aPBQf8DkmrYoZ4O-wR1duqZB
33
33
  vellum_ee/workflows/display/nodes/base_node_display.py,sha256=98vszulLRvYz0kAh7ZEOeWOqzMtlpnOZM2lAViOUieA,18393
34
34
  vellum_ee/workflows/display/nodes/get_node_display_class.py,sha256=jI_kUi9LnNLDpY63QtlC4TfN8P571VN4LpzH0I1ZtLk,1149
35
35
  vellum_ee/workflows/display/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=wBoCqULS4XO3s9Vwhd9v4g10opfBFqeZgRqB8CoFz0c,3015
36
+ vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=Cg5Ker3amAn2wraeNW4t_puvIZdvRB-P8XIL-p4GGL0,3571
37
37
  vellum_ee/workflows/display/nodes/types.py,sha256=St1BB6no528OyELGiyRabWao0GGw6mLhstQAvEACbGk,247
38
38
  vellum_ee/workflows/display/nodes/utils.py,sha256=sloya5TpXsnot1HURc9L51INwflRqUzHxRVnCS9Cd-4,973
39
39
  vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=nUIgH2s0-7IbQRNrBhLPyRNe8YIrx3Yo9HeeW-aXXFk,1668
@@ -72,7 +72,7 @@ vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py,sha256=rHybfUAWwa0L
72
72
  vellum_ee/workflows/display/nodes/vellum/try_node.py,sha256=47fOnSCEFnY8th9m2yTYlgnoUuzgyRZdjg-SXwn--lk,4079
73
73
  vellum_ee/workflows/display/nodes/vellum/utils.py,sha256=oICunzyaXPs0tYnW5zH1r93Bx35MSH7mcD-n0DEWRok,4978
74
74
  vellum_ee/workflows/display/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- vellum_ee/workflows/display/tests/test_base_workflow_display.py,sha256=KUxFl04uZv1xPfnLHsMOjPKygwJkf6fRvPj0JvV8Des,14190
75
+ vellum_ee/workflows/display/tests/test_base_workflow_display.py,sha256=fh-F7TmzOSiWEBwhtd3Nm_Ix9v5W4LhMNhiIxT7G100,15990
76
76
  vellum_ee/workflows/display/tests/workflow_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py,sha256=Y-ajeT65b5varmrZCw6L3hir4hJCFq-eO0jZfRcrs7g,1886
@@ -81,7 +81,7 @@ vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attr
81
81
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py,sha256=O1CIKMmRTaMaT3IhjwCAiMz1ZThPg9lAUbSiZkx_E8c,6321
82
82
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py,sha256=fPXcX-tWQ0UMuEyOFAylgi7pWiE7lZWk9vrShlTExik,40053
83
83
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py,sha256=FLvcD8eoABHUPv08oSpIp_P-65sw2gl4whMXEJJj4f8,6785
84
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py,sha256=ygNirKPjErhjTkDIgFZdaLrsvxDmUEionlmYFMJju3w,16338
84
+ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py,sha256=D_ZGMOgr5Ek33ZQagv08b8up1U0Lc0dCgAjnO8ZEpzQ,16336
85
85
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py,sha256=arl-tho6f0qstUM2ejONEO4ru_6TxCPbni3FS-UZouQ,30108
86
86
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py,sha256=-BiFVw3JUx_79isQNgAtZB2362oByRcuAuUVK9uzCJI,54204
87
87
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py,sha256=E1ww97BFoGbnRkxf84TScat5NQhP8nLVrdaOlpGnSKU,8615
@@ -100,10 +100,12 @@ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_nod
100
100
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py,sha256=oVXCjkU0G56QJmqnd_xIwF3D9bhJwALFibM2wmRhwUk,3739
101
101
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py,sha256=Sg82qV5NCzQDy-RD90hA6QaHgFHOq6ESNkbWXygsnNw,26367
102
102
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py,sha256=QhQbijeCnFeX1i3SMjHJg2WVAEt5JEO3dhFRv-mofdA,2458
103
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py,sha256=cvjHGrgN1LbRvuSKq2U7eLrRFDK4Rb6VJt8L2HIC8HY,10137
103
+ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_parent_input.py,sha256=__LX4cuzbyZp_1wc-SI8X_J0tnhOkCEmRVUWLKI5aQM,4578
104
+ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py,sha256=cKaxme5vUIvKa8aBU7xdeFxXF9wVZ5fW3T5Ie5vToU0,10152
104
105
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py,sha256=XIZZr5POo2NLn2uEWm9EC3rejeBMoO4X-JtzTH6mvp4,4074
105
106
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py,sha256=pLCyMScV88DTBXRH7jXaXOEA1GBq8NIipCUFwIAWnwI,2771
106
107
  vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py,sha256=exT7U-axwtYgFylagScflSQLJEND51qIAx2UATju6JM,6023
108
+ vellum_ee/workflows/display/tests/workflow_serialization/test_final_output_node_map_reference_serialization.py,sha256=vl3pxUJlrYRA8zzFJ-gRm7fe-5fviLNSIsUC7imnMqk,3502
107
109
  vellum_ee/workflows/display/tests/workflow_serialization/test_web_search_node_serialization.py,sha256=vbDFBrWUPeeW7cxjNA6SXrsHlYcbOAhlQ4C45Vdnr1c,3428
108
110
  vellum_ee/workflows/display/tests/workflow_serialization/test_workflow_input_parameterization_error.py,sha256=vAdmn3YTBDpo55znbydQxsgg9ASqHcvsUPwiBR_7wfo,1461
109
111
  vellum_ee/workflows/display/types.py,sha256=cyZruu4sXAdHjwuFc7dydM4DcFNf-pp_CmulXItxac4,3679
@@ -111,15 +113,15 @@ vellum_ee/workflows/display/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
111
113
  vellum_ee/workflows/display/utils/auto_layout.py,sha256=f4GiLn_LazweupfqTpubcdtdfE_vrOcmZudSsnYIY9E,3906
112
114
  vellum_ee/workflows/display/utils/events.py,sha256=MEG2BT6GgAzkbv1dMaFpov5OShtaAZeAb1-g3xDFsAM,1826
113
115
  vellum_ee/workflows/display/utils/exceptions.py,sha256=LSwwxCYNxFkf5XMUcFkaZKpQ13OSrI7y_bpEUwbKVk0,169
114
- vellum_ee/workflows/display/utils/expressions.py,sha256=5DwluPppoJSsMf0pfbRafPIisZHZ4VFueXR1YeLcRwY,18593
116
+ vellum_ee/workflows/display/utils/expressions.py,sha256=80TcSWhT249cvT_NRobwelWCLWKgeFQEpXCvjFq0fwY,19743
115
117
  vellum_ee/workflows/display/utils/registry.py,sha256=1qXiBTdsnro6FeCX0FGBEK7CIf6wa--Jt50iZ_nEp_M,3460
116
118
  vellum_ee/workflows/display/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
119
  vellum_ee/workflows/display/utils/tests/test_auto_layout.py,sha256=vfXI769418s9vda5Gb5NFBH747WMOwSgHRXeLCTLVm8,2356
118
120
  vellum_ee/workflows/display/utils/tests/test_events.py,sha256=Qze6wEmFJx23_sKQhX-i329apWgMeS9zTptWlRca6Ko,4528
119
- vellum_ee/workflows/display/utils/vellum.py,sha256=mtoXmSYwR7rvrq-d6CzCW_auaJXTct0Mi1F0xpRCiNQ,5627
121
+ vellum_ee/workflows/display/utils/vellum.py,sha256=sZwU0KdmZZTKWW62SyxJTl2tC8tN6p_BpZ-lDoinV-U,5670
120
122
  vellum_ee/workflows/display/vellum.py,sha256=J2mdJZ1sdLW535DDUkq_Vm8Z572vhuxHxVZF9deKSdk,391
121
123
  vellum_ee/workflows/display/workflows/__init__.py,sha256=JTB9ObEV3l4gGGdtfBHwVJtTTKC22uj-a-XjTVwXCyA,148
122
- vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=o1yKDaBv3dECPcJnEtjDZw8Nt9Y5kjijGc7FQi5IhOQ,43745
124
+ vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=HEwhGlQ6AllU_SpGSVCYgRc0RXev4iuHxXX3Rl18Xzs,43674
123
125
  vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=gxz76AeCqgAZ9D2lZeTiZzxY9eMgn3qOSfVgiqYcOh8,2028
124
126
  vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=pb7BTH-ivRnya1LQU3j-MApWk_m8POpPNOdD0oEK82A,37847
125
127
  vellum_ee/workflows/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -149,11 +151,11 @@ vellum_ee/workflows/tests/test_serialize_module.py,sha256=lk-4dVnG2HcxxywBXxDR1i
149
151
  vellum_ee/workflows/tests/test_server.py,sha256=dXFBraU99Y6cKp2aBhLFXQTScSRcE9WaWjo1z9piqdU,23344
150
152
  vellum_ee/workflows/tests/test_virtual_files.py,sha256=TJEcMR0v2S8CkloXNmCHA0QW0K6pYNGaIjraJz7sFvY,2762
151
153
  vellum/__init__.py,sha256=lv4OTbPgTegugVOpRy5xJPPwMvC1Zqrg4oFMEwY1KSg,47428
152
- vellum/client/README.md,sha256=b6XKeYBBbhQx0v1sHWfM0gIJeJhUFF-aqL2ig7ADa08,5564
154
+ vellum/client/README.md,sha256=flqu57ubZNTfpq60CdLtJC9gp4WEkyjb_n_eZ4OYf9w,6497
153
155
  vellum/client/__init__.py,sha256=T5Ht_w-Mk_9nzGqdadhQB8V20M0vYj7am06ut0A3P1o,73401
154
156
  vellum/client/core/__init__.py,sha256=lTcqUPXcx4112yLDd70RAPeqq6tu3eFMe1pKOqkW9JQ,1562
155
157
  vellum/client/core/api_error.py,sha256=44vPoTyWN59gonCIZMdzw7M1uspygiLnr3GNFOoVL2Q,614
156
- vellum/client/core/client_wrapper.py,sha256=7aJQmDvTC5-vD5Qkr7IPOm7wV5Tj_nzD5SlG1cAUg9g,2840
158
+ vellum/client/core/client_wrapper.py,sha256=i5oGjE3DOmkirKwi-u3hUXa3ZSnCYWb06u5EdpjKLMM,2840
157
159
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
158
160
  vellum/client/core/file.py,sha256=d4NNbX8XvXP32z8KpK2Xovv33nFfruIrpz0QWxlgpZk,2663
159
161
  vellum/client/core/force_multipart.py,sha256=awxh5MtcRYe74ehY8U76jzv6fYM_w_D3Rur7KQQzSDk,429
@@ -948,7 +950,7 @@ vellum/plugins/pydantic.py,sha256=SamPlRZ8V9kuxEfMkOPKjhMMLa5Q3wYJ3Z-F_IfKtvA,39
948
950
  vellum/plugins/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
949
951
  vellum/plugins/tests/test_pydantic.py,sha256=S6bLqs3Y5DGA012QV_7f6hk4e6Bz-iJ9py9DEKuo4fM,746
950
952
  vellum/plugins/utils.py,sha256=cPmxE9R2CK1bki2jKE8rB-G9zMf2pzHjSPDHFPXwd3Q,878
951
- vellum/plugins/vellum_mypy.py,sha256=hfjC2rIxNdQuJa6fIN4PDgODnQ3YaUszyaV2eNbLJlE,27952
953
+ vellum/plugins/vellum_mypy.py,sha256=awSjop_Vn7VRCQZCy5rNUMq8n_ueNjGJ0wHVN-rTVJY,27916
952
954
  vellum/prompts/__init__.py,sha256=kn-btvQOMNnnBuyQiUpie48_VBJAk7tFFU-ul5dwK60,107
953
955
  vellum/prompts/blocks/__init__.py,sha256=Zwvncjd8sUCPmT-8pFpgLYsKJl0xB6td1GTQzjV9hYA,108
954
956
  vellum/prompts/blocks/compilation.py,sha256=mCGfqJXcKMkv565Ye5y9MsPdVAmkCGOnqQBRa3nJVg8,10754
@@ -1738,6 +1740,7 @@ vellum/workflows/events/tests/test_event.py,sha256=V26TNmbo2aL4sDvcY3nPxCWgcoS4e
1738
1740
  vellum/workflows/events/types.py,sha256=mVrqAH9Hs9SpXm08Hcxdyap_ImQPwGsxRr56rSNMP34,5043
1739
1741
  vellum/workflows/events/workflow.py,sha256=kLSWFXiDZH0TELWoDjQ_kHVomFnw8MVVUPDGIzgyosw,8997
1740
1742
  vellum/workflows/exceptions.py,sha256=NiBiR3ggfmPxBVqD-H1SqmjI-7mIn0EStSN1BqApvCM,1213
1743
+ vellum/workflows/executable.py,sha256=um-gLJMVYfGJwGJfZIPlCRHhHIYm6pn8PUEfeqrNx5k,218
1741
1744
  vellum/workflows/expressions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1742
1745
  vellum/workflows/expressions/accessor.py,sha256=3lu1-_-dBfZdJvtX-q66jbmRAZtqIfdsh_3_JNuzg1E,4462
1743
1746
  vellum/workflows/expressions/add.py,sha256=Rr1O83nksL5Z0kam4eaQOokvDrEwlUg7LqWnXzGUW40,1226
@@ -1783,9 +1786,9 @@ vellum/workflows/expressions/tests/test_length.py,sha256=pQA1tYSwqxE6euclboY024N
1783
1786
  vellum/workflows/expressions/tests/test_minus.py,sha256=pq7dvdRGNhSSn95LGNzRErsYUsFk5SpOKHDcSR5QToc,1632
1784
1787
  vellum/workflows/expressions/tests/test_parse_json.py,sha256=zpB_qE5_EwWQL7ULQUJm0o1PRSfWZdAqZNW6Ah13oJE,1059
1785
1788
  vellum/workflows/graph/__init__.py,sha256=3sHlay5d_-uD7j3QJXiGl0WHFZZ_QScRvgyDhN2GhHY,74
1786
- vellum/workflows/graph/graph.py,sha256=kUKNMcxw9jtcnHbLdMPgY7t-lxIml6HbFy1uFkgPcRw,11088
1789
+ vellum/workflows/graph/graph.py,sha256=vkpteMc2a61IFGHlrA50J4ntVj6m3agcyWrXGQEbjHc,11280
1787
1790
  vellum/workflows/graph/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1788
- vellum/workflows/graph/tests/test_graph.py,sha256=04FUSFk38fRwO7NZANaS34WkDowqQ21cLwamRSJlel8,15045
1791
+ vellum/workflows/graph/tests/test_graph.py,sha256=0Pov0sCsxjzUDL9wy7xy9jFD-F2GsMJnZVEVFXzQGdM,15433
1789
1792
  vellum/workflows/inputs/__init__.py,sha256=AbFEteIYEvCb14fM3EK7bhM-40-6s494rSlIhQ4Dsss,62
1790
1793
  vellum/workflows/inputs/base.py,sha256=w3owT5B3rLBmIj-v-jL2l-HD4yd3hXK9RmHVd557BpA,5126
1791
1794
  vellum/workflows/inputs/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1797,7 +1800,7 @@ vellum/workflows/integrations/tests/test_mcp_service.py,sha256=q_DYrDkIqI4sQBNgI
1797
1800
  vellum/workflows/logging.py,sha256=_a217XogktV4Ncz6xKFz7WfYmZAzkfVRVuC0rWob8ls,437
1798
1801
  vellum/workflows/nodes/__init__.py,sha256=zymtc3_iW2rFmMR-sayTLuN6ZsAw8VnJweWPsjQk2-Q,1197
1799
1802
  vellum/workflows/nodes/bases/__init__.py,sha256=cniHuz_RXdJ4TQgD8CBzoiKDiPxg62ErdVpCbWICX64,58
1800
- vellum/workflows/nodes/bases/base.py,sha256=9vOPHP6K6oK36bEby1KFRXpMevMwq6slwuSRhC-bWFc,20752
1803
+ vellum/workflows/nodes/bases/base.py,sha256=i4tuPo_KyFI1ZJh__AI-oxRkDGch9pNTmOqt69HTcE0,20478
1801
1804
  vellum/workflows/nodes/bases/base_adornment_node.py,sha256=hrgzuTetM4ynPd9YGHoK8Vwwn4XITi3aZZ_OCnQrq4Y,3433
1802
1805
  vellum/workflows/nodes/bases/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1803
1806
  vellum/workflows/nodes/bases/tests/test_base_adornment_node.py,sha256=fXZI9KqpS4XMBrBnIEkK3foHaBVvyHwYcQWWDKay7ic,1148
@@ -1810,7 +1813,7 @@ vellum/workflows/nodes/core/inline_subworkflow_node/node.py,sha256=TCmO0wPbt7kc8
1810
1813
  vellum/workflows/nodes/core/inline_subworkflow_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1811
1814
  vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py,sha256=RK2g1h2ib-ruQZ9A2_2L-B9WBdHV44WZj75rkDNL0cE,5766
1812
1815
  vellum/workflows/nodes/core/map_node/__init__.py,sha256=MXpZYmGfhsMJHqqlpd64WiJRtbAtAMQz-_3fCU_cLV0,56
1813
- vellum/workflows/nodes/core/map_node/node.py,sha256=gyOUJw2EhuI5FYT0782moEFlMKG1ajk3ukTiDCtFlxw,9678
1816
+ vellum/workflows/nodes/core/map_node/node.py,sha256=kSyQmIWk4v-KSt4WBf3d-0_QueKYkjtrEmQPGTbMryw,10054
1814
1817
  vellum/workflows/nodes/core/map_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1815
1818
  vellum/workflows/nodes/core/map_node/tests/test_node.py,sha256=Xc2xZY5ShSy-bsIQe41JbvIjq3TE95duS-ygaELRVkk,9320
1816
1819
  vellum/workflows/nodes/core/retry_node/__init__.py,sha256=lN2bIy5a3Uzhs_FYCrooADyYU6ZGShtvLKFWpelwPvo,60
@@ -1837,7 +1840,7 @@ vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py,sha256=5C59
1837
1840
  vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py,sha256=Org3xTvgp1pA0uUXFfnJr29D3HzCey2lEdYF4zbIUgo,70
1838
1841
  vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py,sha256=ea20icDM1HB942wkH-XtXNSNCBDcjeOiN3vowkHL4fs,4477
1839
1842
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py,sha256=Hl35IAoepRpE-j4cALaXVJIYTYOF3qszyVbxTj4kS1s,82
1840
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py,sha256=cW0fgmferJVm6LctqC5kCFu4Qg9udMQEb0IISLS7CTo,13795
1843
+ vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py,sha256=qxABS2tl7gXfU350-0geBMoL25QrgyqR6-vhUAQt4Qk,14065
1841
1844
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1842
1845
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py,sha256=Hk_u2IxLIeeqL_s0RTgoyL5QGYwY9VllKT8z5_JHiDU,24956
1843
1846
  vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py,sha256=0a40fkkZkFMmZN0CsWf6EP_y1H6x36EGa3WcfVNyOsM,9797
@@ -1861,7 +1864,7 @@ vellum/workflows/nodes/displayable/final_output_node/node.py,sha256=6SMaGeBlHQ5r
1861
1864
  vellum/workflows/nodes/displayable/final_output_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1862
1865
  vellum/workflows/nodes/displayable/final_output_node/tests/test_node.py,sha256=E6LQ74qZjY4Xi4avx2qdOCgGhF8pEcNLBh8cqYRkzMI,709
1863
1866
  vellum/workflows/nodes/displayable/guardrail_node/__init__.py,sha256=Ab5eXmOoBhyV4dMWdzh32HLUmnPIBEK_zFCT38C4Fng,68
1864
- vellum/workflows/nodes/displayable/guardrail_node/node.py,sha256=GRWRDCVfPbL03hGbyZP4FAUtAjg_8K3XrU0xJ_lIFeA,5820
1867
+ vellum/workflows/nodes/displayable/guardrail_node/node.py,sha256=axYUojar_kdB3gi4LG3g9euJ8VkOxNtiFxJNI46v-SQ,5869
1865
1868
  vellum/workflows/nodes/displayable/guardrail_node/test_node.py,sha256=SAGv6hSFcBwQkudn1VxtaKNsXSXWWELl3eK05zM6tS0,5410
1866
1869
  vellum/workflows/nodes/displayable/guardrail_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1867
1870
  vellum/workflows/nodes/displayable/guardrail_node/tests/test_node.py,sha256=X2pd6TI8miYxIa7rgvs1pHTEreyWcf77EyR0_Jsa700,2055
@@ -1891,13 +1894,13 @@ vellum/workflows/nodes/displayable/tests/test_search_node_error_handling.py,sha2
1891
1894
  vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py,sha256=VepO5z1277c1y5N6LLIC31nnWD1aak2m5oPFplfJHHs,6935
1892
1895
  vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py,sha256=Bjv-wZyFgNaVZb9KEMMZd9lFoLzbPEPjEMpANizMZw4,2413
1893
1896
  vellum/workflows/nodes/displayable/tool_calling_node/__init__.py,sha256=3n0-ysmFKsr40CVxPthc0rfJgqVJeZuUEsCmYudLVRg,117
1894
- vellum/workflows/nodes/displayable/tool_calling_node/node.py,sha256=diAm1hJ4ZywdRmwZAYhBq_TjmCsUt_qc6ew7tywV1rs,7840
1897
+ vellum/workflows/nodes/displayable/tool_calling_node/node.py,sha256=J4RNOggTx5nzovC0354SPGV-NkRpYnV51PMTYQ7aIQ8,8202
1895
1898
  vellum/workflows/nodes/displayable/tool_calling_node/state.py,sha256=CcBVb_YtwfSSka4ze678k6-qwmzMSfjfVP8_Y95feSo,302
1896
1899
  vellum/workflows/nodes/displayable/tool_calling_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1897
1900
  vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py,sha256=in1fbEz5x1tx3uKv9YXdvOncsHucNL8Ro6Go7lBuuOQ,8962
1898
1901
  vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py,sha256=GZoeybB9uM7ai8sBLAtUMHrMVgh-WrJDWrKZci6feDs,11892
1899
1902
  vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py,sha256=SIu5GCj4tIE4fz-cAcdULtQfqZIhrcc3Doo6TWLXBws,8804
1900
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py,sha256=Y2MlODE0lRQ4Fci-G3eYZBpI7CQYe5cIykTLcF4jrbE,23602
1903
+ vellum/workflows/nodes/displayable/tool_calling_node/utils.py,sha256=c0fJA-de3yJKGTzKfjyZOkbVhIndSAhZqBZp_DpU1fg,24158
1901
1904
  vellum/workflows/nodes/displayable/web_search_node/__init__.py,sha256=8FOnEP-n-U68cvxTlJW9wphIAGHq5aqjzLM-DoSSXnU,61
1902
1905
  vellum/workflows/nodes/displayable/web_search_node/node.py,sha256=NQYux2bOtuBF5E4tn-fXi5y3btURPRrNqMSM9MAZYI4,5091
1903
1906
  vellum/workflows/nodes/displayable/web_search_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1912,7 +1915,7 @@ vellum/workflows/nodes/tests/test_mocks.py,sha256=mfPvrs75PKcsNsbJLQAN6PDFoVqs9T
1912
1915
  vellum/workflows/nodes/tests/test_utils.py,sha256=BUugAHx2C9YuCwTlsTXV1Glxca0kW3St6T9o_QFatSU,5649
1913
1916
  vellum/workflows/nodes/utils.py,sha256=wCvf8K5qruT5GwtvnHcQ-LMllktTD8aaFmAGpKQy--c,10720
1914
1917
  vellum/workflows/outputs/__init__.py,sha256=AyZ4pRh_ACQIGvkf0byJO46EDnSix1ZCAXfvh-ms1QE,94
1915
- vellum/workflows/outputs/base.py,sha256=PUn0zhGzYCSZL34JXtXg9zALlXS_cqxZldLilPxDzb8,9614
1918
+ vellum/workflows/outputs/base.py,sha256=zy02zr9DmG3j7Xp3Q8xiOiXFF_c7uNh76jf2LiMS-qE,10132
1916
1919
  vellum/workflows/ports/__init__.py,sha256=bZuMt-R7z5bKwpu4uPW7LlJeePOQWmCcDSXe5frUY5g,101
1917
1920
  vellum/workflows/ports/node_ports.py,sha256=SM9uLAaoaE1HwR-Uqwf2v5zZK5iFnphKs6mE5Ls7ldE,2877
1918
1921
  vellum/workflows/ports/port.py,sha256=PYhmzEHgJyjUjSFkPIQ38cNIKpcXrhYiZlj7nZC5yCk,3989
@@ -1925,7 +1928,7 @@ vellum/workflows/references/external_input.py,sha256=c_4SojTpykCSbGS1Pjmx9FfquyY
1925
1928
  vellum/workflows/references/input.py,sha256=3INu-TLTi4dziWmva6LO3WvgDlPzsjayUx61cVvqLJA,325
1926
1929
  vellum/workflows/references/lazy.py,sha256=jgUYmgt-yAybzPf_R-74MzdU8VuNwMYI8EQqrj9lVR0,2948
1927
1930
  vellum/workflows/references/node.py,sha256=LP854wDVs-9I_aZ7-nkbwXqL2H7W2_3LED2e9FixNS8,1418
1928
- vellum/workflows/references/output.py,sha256=Odpjqnw2uY6lbmt49sUwDclBPZMndYxgtFAGn1iKj8k,3387
1931
+ vellum/workflows/references/output.py,sha256=qVa3QmlOyJhyIMOHwOmi1RgHPAOryhdPr2DRj97aEvU,3321
1929
1932
  vellum/workflows/references/state_value.py,sha256=bInUF0A3Pt4-zhA0f6LdSuyv8tz7n5QRkHAEn4gsmqI,711
1930
1933
  vellum/workflows/references/tests/test_lazy.py,sha256=0s50-LizMTlSTBQahpK0fg_xqCucA8YTp6QmIMqPvMk,919
1931
1934
  vellum/workflows/references/vellum_secret.py,sha256=Od4d19a5yletWMqNfJR5d_mZQUkVcFzj29mE-T9J7yE,480
@@ -1962,11 +1965,11 @@ vellum/workflows/types/utils.py,sha256=mTctHITBybpt4855x32oCKALBEcMNLn-9cCmfEKgJ
1962
1965
  vellum/workflows/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1963
1966
  vellum/workflows/utils/functions.py,sha256=6WRRMb_XbxtvhUKOJq5ZChy0KKvlBaQCBiPhvecXT7I,10029
1964
1967
  vellum/workflows/utils/hmac.py,sha256=JJCczc6pyV6DuE1Oa0QVfYPUN_of3zEYmGFib3OZnrE,1135
1965
- vellum/workflows/utils/names.py,sha256=QLUqfJ1tmSEeUwBKTTiv_Qk3QGbInC2RSmlXfGXc8Wo,380
1968
+ vellum/workflows/utils/names.py,sha256=QtHquoaGqRseu5gg2OcVGI2d_CMcEOvjb9KspwH4C-A,552
1966
1969
  vellum/workflows/utils/pydantic_schema.py,sha256=eR_bBtY-T0pttJP-ARwagSdCOnwPUtiT3cegm2lzDTQ,1310
1967
1970
  vellum/workflows/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1968
1971
  vellum/workflows/utils/tests/test_functions.py,sha256=TlNhD0OBJ-aeYn4qUqmApuajkx9sJx0lIQRMfT8Xf0w,23688
1969
- vellum/workflows/utils/tests/test_names.py,sha256=aOqpyvMsOEK_9mg_-yaNxQDW7QQfwqsYs37PseyLhxw,402
1972
+ vellum/workflows/utils/tests/test_names.py,sha256=DnRRnuORxQXx9ESegCzkxiWcHy2_bBi7lXgbFi3FZK8,757
1970
1973
  vellum/workflows/utils/tests/test_uuids.py,sha256=i77ABQ0M3S-aFLzDXHJq_yr5FPkJEWCMBn1HJ3DObrE,437
1971
1974
  vellum/workflows/utils/tests/test_vellum_variables.py,sha256=vbnKgm41aB5OXlO-ZIPbhQ6xDiZkT8KMxCLqz4zocWY,1791
1972
1975
  vellum/workflows/utils/uuids.py,sha256=IaZQANz7fhF7la0_J1O50Y6D2PIYv_taRDTRzBT9aWw,1284
@@ -1974,13 +1977,13 @@ vellum/workflows/utils/vellum_variables.py,sha256=YHLNiQGWDNssGH1FQoG9Z1jUFZ-zYe
1974
1977
  vellum/workflows/utils/zip.py,sha256=HVg_YZLmBOTXKaDV3Xhaf3V6sYnfqqZXQ8CpuafkbPY,1181
1975
1978
  vellum/workflows/vellum_client.py,sha256=xkfoucodxNK5JR2-lbRqZx3xzDgExWkP6kySrpi_Ubc,1079
1976
1979
  vellum/workflows/workflows/__init__.py,sha256=KY45TqvavCCvXIkyCFMEc0dc6jTMOUci93U2DUrlZYc,66
1977
- vellum/workflows/workflows/base.py,sha256=PT7Hveate5A4Nt1UQoS6wwFehxgZQ5fuYjS1vLnlkl0,28008
1980
+ vellum/workflows/workflows/base.py,sha256=4_-kEebUBCn7d4N0TxF-sQejM2gAF8N6FbGabV2t6Pw,28346
1978
1981
  vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnadGsrSZGa7t7LpJA,2008
1979
1982
  vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1980
1983
  vellum/workflows/workflows/tests/test_base_workflow.py,sha256=ptMntHzVyy8ZuzNgeTuk7hREgKQ5UBdgq8VJFSGaW4Y,20832
1981
1984
  vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
1982
- vellum_ai-1.3.4.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1983
- vellum_ai-1.3.4.dist-info/METADATA,sha256=b4vvPio8YVqk9c6RUtdneolPn9VxgV12Es1eAIxG0qM,5547
1984
- vellum_ai-1.3.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1985
- vellum_ai-1.3.4.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1986
- vellum_ai-1.3.4.dist-info/RECORD,,
1985
+ vellum_ai-1.3.6.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1986
+ vellum_ai-1.3.6.dist-info/METADATA,sha256=zNFEQuOlfP7i6yEReP2nVPoXRzJ6wvJWmK_OAs8oz1Q,5547
1987
+ vellum_ai-1.3.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1988
+ vellum_ai-1.3.6.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1989
+ vellum_ai-1.3.6.dist-info/RECORD,,
@@ -100,3 +100,20 @@ def test_serialize_display_data():
100
100
  "position": {"x": 0.0, "y": 0.0},
101
101
  "comment": {"expanded": True, "value": "I hope this works"},
102
102
  }
103
+
104
+
105
+ def test_serialize_node_label_with_pascal_case():
106
+ """
107
+ Tests that a node with PascalCase name serializes with proper title case label.
108
+ """
109
+
110
+ # GIVEN a node with a PascalCase name that includes common patterns
111
+ class MyCustomNode(BaseNode):
112
+ pass
113
+
114
+ # WHEN we serialize the node
115
+ node_display_class = get_node_display_class(MyCustomNode)
116
+ data = node_display_class().serialize(WorkflowDisplayContext())
117
+
118
+ # THEN the label should be converted to proper title case with spaces
119
+ assert data["label"] == "My Custom Node"
@@ -9,6 +9,8 @@ from vellum.workflows.references.lazy import LazyReference
9
9
  from vellum.workflows.state import BaseState
10
10
  from vellum.workflows.workflows.base import BaseWorkflow
11
11
  from vellum_ee.workflows.display.base import EdgeDisplay, WorkflowInputsDisplay
12
+ from vellum_ee.workflows.display.editor.types import NodeDisplayData, NodeDisplayPosition
13
+ from vellum_ee.workflows.display.nodes import BaseNodeDisplay
12
14
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
13
15
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
14
16
 
@@ -429,3 +431,48 @@ def test_serialize_workflow_with_edge_display_data():
429
431
  assert edge_with_display_data["type"] == "DEFAULT"
430
432
  assert "source_node_id" in edge_with_display_data
431
433
  assert "target_node_id" in edge_with_display_data
434
+
435
+
436
+ def test_serialize_workflow_with_node_display_data():
437
+ """
438
+ Tests that nodes with z_index values serialize display_data correctly.
439
+ """
440
+
441
+ # GIVEN a workflow with a node that has custom display data
442
+ class TestNode(BaseNode):
443
+ class Outputs(BaseNode.Outputs):
444
+ result: str
445
+
446
+ class TestWorkflow(BaseWorkflow):
447
+ graph = TestNode
448
+
449
+ class Outputs(BaseWorkflow.Outputs):
450
+ final_result = TestNode.Outputs.result
451
+
452
+ class TestNodeDisplay(BaseNodeDisplay[TestNode]):
453
+ display_data = NodeDisplayData(position=NodeDisplayPosition(x=100, y=200), z_index=10, width=300, height=150)
454
+
455
+ class TestWorkflowDisplay(BaseWorkflowDisplay[TestWorkflow]):
456
+ pass
457
+
458
+ # WHEN we serialize the workflow with the custom node display
459
+ display = get_workflow_display(
460
+ base_display_class=TestWorkflowDisplay,
461
+ workflow_class=TestWorkflow,
462
+ )
463
+ serialized_workflow = display.serialize()
464
+
465
+ # THEN the node should include display_data with z_index
466
+ workflow_raw_data = cast(Dict[str, Any], serialized_workflow["workflow_raw_data"])
467
+ nodes = cast(List[Dict[str, Any]], workflow_raw_data["nodes"])
468
+
469
+ test_node = None
470
+ for node in nodes:
471
+ if node.get("type") == "GENERIC":
472
+ definition = node.get("definition")
473
+ if isinstance(definition, dict) and definition.get("name") == "TestNode":
474
+ test_node = node
475
+ break
476
+
477
+ assert test_node is not None, "TestNode not found in serialized nodes"
478
+ assert test_node["display_data"] == {"position": {"x": 100, "y": 200}, "z_index": 10, "width": 300, "height": 150}
@@ -173,7 +173,7 @@ def test_serialize_workflow(vellum_client):
173
173
  },
174
174
  ],
175
175
  "data": {
176
- "label": "Simple A P I Node",
176
+ "label": "Simple API Node",
177
177
  "error_output_id": None,
178
178
  "source_handle_id": "7c33b4d3-9204-4bd5-9371-80ee34f83073",
179
179
  "target_handle_id": "14b538a5-aedb-41f3-b579-039956b7c7ed",
@@ -0,0 +1,89 @@
1
+ from deepdiff import DeepDiff
2
+
3
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
4
+
5
+ from tests.workflows.basic_tool_calling_node_parent_input.workflow import BasicToolCallingNodeParentInputWorkflow
6
+
7
+
8
+ def test_serialize_workflow():
9
+ # GIVEN a Workflow that uses a generic node
10
+ # WHEN we serialize it
11
+ workflow_display = get_workflow_display(workflow_class=BasicToolCallingNodeParentInputWorkflow)
12
+
13
+ serialized_workflow: dict = workflow_display.serialize()
14
+ # THEN we should get a serialized representation of the Workflow
15
+ assert serialized_workflow.keys() == {
16
+ "workflow_raw_data",
17
+ "input_variables",
18
+ "state_variables",
19
+ "output_variables",
20
+ }
21
+
22
+ # AND its input variables should be what we expect
23
+ input_variables = serialized_workflow["input_variables"]
24
+ assert len(input_variables) == 1
25
+
26
+ # AND its output variables should be what we expect
27
+ output_variables = serialized_workflow["output_variables"]
28
+ assert len(output_variables) == 2
29
+ assert not DeepDiff(
30
+ [
31
+ {"id": "e2e36cfc-cf24-42fd-ba8f-cce39c53d47b", "key": "text", "type": "STRING"},
32
+ {"id": "08ca9519-e421-47e8-a42d-44f49f6aab16", "key": "chat_history", "type": "CHAT_HISTORY"},
33
+ ],
34
+ output_variables,
35
+ ignore_order=True,
36
+ )
37
+
38
+ # AND its raw data should be what we expect
39
+ workflow_raw_data = serialized_workflow["workflow_raw_data"]
40
+ tool_calling_node = workflow_raw_data["nodes"][2]
41
+
42
+ attributes = tool_calling_node["attributes"]
43
+ function_attributes = next(attribute for attribute in attributes if attribute["name"] == "functions")
44
+ assert function_attributes == {
45
+ "id": "cec9f5f2-7bb0-42e4-9c56-f215f07c5569",
46
+ "name": "functions",
47
+ "value": {
48
+ "type": "CONSTANT_VALUE",
49
+ "value": {
50
+ "type": "JSON",
51
+ "value": [
52
+ {
53
+ "type": "CODE_EXECUTION",
54
+ "name": "get_string",
55
+ "description": "\n Get a string with the parent input, dummy input, and the populated input.\n ", # noqa: E501
56
+ "definition": {
57
+ "state": None,
58
+ "cache_config": None,
59
+ "name": "get_string",
60
+ "description": "\n Get a string with the parent input, dummy input, and the populated input.\n ", # noqa: E501
61
+ "parameters": {
62
+ "type": "object",
63
+ "properties": {"populated_input": {"type": "string"}},
64
+ "required": ["populated_input"],
65
+ },
66
+ "inputs": {
67
+ "parent_input": {
68
+ "type": "WORKFLOW_INPUT",
69
+ "input_variable_id": "4bf1f0e7-76c6-4204-9f8c-bd9c3b73a8db",
70
+ },
71
+ "dummy_input": {
72
+ "type": "NODE_OUTPUT",
73
+ "node_id": "8e89ae10-a709-45ec-89f8-242f92e4a83f",
74
+ "node_output_id": "0cd09f0a-c142-4d5d-acc7-b93cd30ca58d",
75
+ },
76
+ "constant_input": {
77
+ "type": "CONSTANT_VALUE",
78
+ "value": {"type": "STRING", "value": "constant_input"},
79
+ },
80
+ },
81
+ "forced": None,
82
+ "strict": None,
83
+ },
84
+ "src": 'from vellum.workflows.utils.functions import use_tool_inputs\n\nfrom .inputs import ParentInputs\nfrom .nodes.dummy_node import DummyNode\n\n\n@use_tool_inputs(\n parent_input=ParentInputs.parent_input,\n dummy_input=DummyNode.Outputs.text,\n constant_input="constant_input",\n)\ndef get_string(parent_input: str, dummy_input: str, constant_input: str, populated_input: str) -> str:\n """\n Get a string with the parent input, dummy input, and the populated input.\n """\n return f"parent input: {parent_input}, dummy input: {dummy_input}, constant input: {constant_input}, populated input: {populated_input}" # noqa: E501\n', # noqa: E501
85
+ }
86
+ ],
87
+ },
88
+ },
89
+ }
@@ -195,7 +195,7 @@ def test_serialize_workflow():
195
195
  "frequency_penalty": 0.0,
196
196
  "presence_penalty": 0.0,
197
197
  "logit_bias": None,
198
- "custom_parameters": None,
198
+ "custom_parameters": {"mode": "initial"},
199
199
  },
200
200
  },
201
201
  },
@@ -0,0 +1,88 @@
1
+ from typing import List
2
+
3
+ from vellum.workflows import BaseWorkflow
4
+ from vellum.workflows.inputs.base import BaseInputs
5
+ from vellum.workflows.nodes import MapNode
6
+ from vellum.workflows.nodes.bases import BaseNode
7
+ from vellum.workflows.nodes.displayable import FinalOutputNode
8
+ from vellum.workflows.outputs import BaseOutputs
9
+ from vellum.workflows.state import BaseState
10
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
11
+
12
+
13
+ class TestInputs(BaseInputs):
14
+ items: List[str]
15
+
16
+
17
+ class TestIteration(BaseNode):
18
+ item = MapNode.SubworkflowInputs.item
19
+ index = MapNode.SubworkflowInputs.index
20
+
21
+ class Outputs(BaseOutputs):
22
+ processed: str
23
+
24
+ def run(self) -> Outputs:
25
+ return self.Outputs(processed=f"processed_{self.item}_{self.index}")
26
+
27
+
28
+ class TestIterationSubworkflow(BaseWorkflow[MapNode.SubworkflowInputs, BaseState]):
29
+ graph = TestIteration
30
+
31
+ class Outputs(BaseOutputs):
32
+ processed = TestIteration.Outputs.processed
33
+
34
+
35
+ class TestMapNode(MapNode):
36
+ items = TestInputs.items
37
+ subworkflow = TestIterationSubworkflow
38
+
39
+
40
+ class TestFinalOutputNode(FinalOutputNode[BaseState, List[str]]):
41
+ class Outputs(FinalOutputNode.Outputs):
42
+ value = TestMapNode.Outputs.processed
43
+
44
+
45
+ class TestWorkflowWithFinalOutputReferencingMap(BaseWorkflow[TestInputs, BaseState]):
46
+ graph = TestMapNode >> TestFinalOutputNode
47
+
48
+ class Outputs(BaseOutputs):
49
+ final_result = TestFinalOutputNode.Outputs.value
50
+
51
+
52
+ def test_serialize_workflow__final_output_node_referencing_map_node():
53
+ """
54
+ Test that final output nodes referencing map node outputs have correct outputs structure.
55
+
56
+ This test verifies that when a FinalOutputNode references a MapNode output,
57
+ the serialized output contains proper NODE_OUTPUT references instead of None values.
58
+ This addresses the Agent Builder issue where final outputs showed value=None in the UI.
59
+ """
60
+ workflow_display = get_workflow_display(workflow_class=TestWorkflowWithFinalOutputReferencingMap)
61
+
62
+ # WHEN we serialize it
63
+ serialized_workflow: dict = workflow_display.serialize()
64
+
65
+ # THEN the final output node should have the correct outputs structure
66
+ workflow_raw_data = serialized_workflow["workflow_raw_data"]
67
+ map_node = next(node for node in workflow_raw_data["nodes"] if node["type"] == "MAP")
68
+ final_output_node = next(node for node in workflow_raw_data["nodes"] if node["type"] == "TERMINAL")
69
+
70
+ # AND the map node's subworkflow should have the one output variable
71
+ output_variable = next(iter(map_node["data"]["output_variables"]))
72
+ map_node_output_id = output_variable["id"]
73
+
74
+ # AND the final output node should have an outputs array with proper structure
75
+ assert "outputs" in final_output_node
76
+ outputs = final_output_node["outputs"]
77
+ assert len(outputs) == 1
78
+
79
+ output = outputs[0]
80
+ # AND the output should have the correct structure with NODE_OUTPUT reference instead of None
81
+ assert output["name"] == "value"
82
+ assert output["type"] == "JSON"
83
+
84
+ # AND the value should be a NODE_OUTPUT reference, not None
85
+ assert output["value"] is not None, f"Expected NODE_OUTPUT reference but got None. Full output: {output}"
86
+ assert output["value"]["type"] == "NODE_OUTPUT", f"Expected NODE_OUTPUT type but got {output['value']['type']}"
87
+ assert "node_id" in output["value"], f"Missing node_id in output value: {output['value']}"
88
+ assert output["value"]["node_output_id"] == map_node_output_id
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, cast
7
7
  from pydantic import BaseModel
8
8
 
9
9
  from vellum.client.types.logical_operator import LogicalOperator
10
+ from vellum.workflows.constants import undefined
10
11
  from vellum.workflows.descriptors.base import BaseDescriptor
11
12
  from vellum.workflows.expressions.accessor import AccessorExpression
12
13
  from vellum.workflows.expressions.add import AddExpression
@@ -243,7 +244,14 @@ def serialize_key(key: Any) -> str:
243
244
  return str(key)
244
245
 
245
246
 
247
+ # Sentinel value to indicate a value should be omitted from serialization
248
+ _UNDEFINED_SENTINEL: JsonObject = {"__undefined__": True}
249
+
250
+
246
251
  def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> JsonObject:
252
+ if value is undefined:
253
+ return _UNDEFINED_SENTINEL
254
+
247
255
  if isinstance(value, ConstantValueReference):
248
256
  return serialize_value(display_context, value._value)
249
257
 
@@ -314,7 +322,12 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
314
322
  }
315
323
 
316
324
  if isinstance(value, list):
317
- serialized_items = [serialize_value(display_context, item) for item in value]
325
+ serialized_items = []
326
+ for item in value:
327
+ serialized_item = serialize_value(display_context, item)
328
+ if serialized_item != _UNDEFINED_SENTINEL:
329
+ serialized_items.append(serialized_item)
330
+
318
331
  if all(isinstance(item, dict) and item["type"] == "CONSTANT_VALUE" for item in serialized_items):
319
332
  constant_values = []
320
333
  for item in serialized_items:
@@ -345,14 +358,17 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
345
358
  return serialize_value(display_context, dict_value)
346
359
 
347
360
  if isinstance(value, dict):
348
- serialized_entries: List[Dict[str, Any]] = [
349
- {
350
- "id": str(uuid4_from_hash(f"{key}|{val}")),
351
- "key": serialize_key(key),
352
- "value": serialize_value(display_context, val),
353
- }
354
- for key, val in value.items()
355
- ]
361
+ serialized_entries: List[Dict[str, Any]] = []
362
+ for key, val in value.items():
363
+ serialized_val = serialize_value(display_context, val)
364
+ if serialized_val != _UNDEFINED_SENTINEL:
365
+ serialized_entries.append(
366
+ {
367
+ "id": str(uuid4_from_hash(f"{key}|{val}")),
368
+ "key": serialize_key(key),
369
+ "value": serialized_val,
370
+ }
371
+ )
356
372
 
357
373
  # Check if all entries have constant values
358
374
  if all(entry["value"]["type"] == "CONSTANT_VALUE" for entry in serialized_entries):
@@ -422,6 +438,23 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
422
438
 
423
439
  if callable(value):
424
440
  function_definition = compile_function_definition(value)
441
+
442
+ name = function_definition.name
443
+ description = function_definition.description or ""
444
+
445
+ inputs = getattr(value, "__vellum_inputs__", {})
446
+
447
+ if inputs:
448
+ serialized_inputs = {}
449
+ for param_name, input_ref in inputs.items():
450
+ serialized_inputs[param_name] = serialize_value(display_context, input_ref)
451
+
452
+ model_data = function_definition.model_dump()
453
+ model_data["inputs"] = serialized_inputs
454
+ function_definition_data = model_data
455
+ else:
456
+ function_definition_data = function_definition.model_dump()
457
+
425
458
  source_path = inspect.getsourcefile(value)
426
459
  if source_path is not None:
427
460
  with virtual_open(source_path) as f:
@@ -435,9 +468,9 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
435
468
  "type": "JSON",
436
469
  "value": {
437
470
  "type": "CODE_EXECUTION",
438
- "name": function_definition.name,
439
- "description": function_definition.description,
440
- "definition": function_definition.model_dump(),
471
+ "name": name,
472
+ "description": description,
473
+ "definition": function_definition_data,
441
474
  "src": source_code,
442
475
  },
443
476
  },
@@ -102,7 +102,9 @@ def create_node_input_value_pointer_rule(
102
102
  if isinstance(value, OutputReference):
103
103
  if value not in display_context.global_node_output_displays:
104
104
  if issubclass(value.outputs_class, BaseNode.Outputs):
105
- raise ValueError(f"Reference to node '{value.outputs_class._node_class.__name__}' not found in graph.")
105
+ raise ValueError(
106
+ f"Reference to node '{value.outputs_class.__parent_class__.__name__}' not found in graph."
107
+ )
106
108
 
107
109
  raise ValueError(f"Reference to outputs '{value.outputs_class.__qualname__}' is invalid.")
108
110
 
@@ -294,7 +294,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
294
294
  )
295
295
 
296
296
  elif isinstance(workflow_output.instance, OutputReference):
297
- terminal_node_id = workflow_output.instance.outputs_class._node_class.__id__
297
+ terminal_node_id = workflow_output.instance.outputs_class.__parent_class__.__id__
298
298
  serialized_terminal_node = serialized_nodes.get(terminal_node_id)
299
299
  if serialized_terminal_node and isinstance(serialized_terminal_node["data"], dict):
300
300
  serialized_terminal_node["data"]["name"] = workflow_output_display.name
@@ -688,10 +688,8 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
688
688
 
689
689
  return EntrypointDisplay(id=entrypoint_id, edge_display=edge_display)
690
690
 
691
- def _generate_workflow_output_display(self, output: BaseDescriptor) -> WorkflowOutputDisplay:
692
- output_id = uuid4_from_hash(f"{self.workflow_id}|id|{output.name}")
693
-
694
- return WorkflowOutputDisplay(id=output_id, name=output.name)
691
+ def _generate_workflow_output_display(self, output: OutputReference) -> WorkflowOutputDisplay:
692
+ return WorkflowOutputDisplay(id=output.id, name=output.name)
695
693
 
696
694
  def __init_subclass__(cls, **kwargs: Any) -> None:
697
695
  super().__init_subclass__(**kwargs)