vellum-ai 0.14.12__py3-none-any.whl → 0.14.13__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 (26) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/workflows/descriptors/base.py +3 -0
  3. vellum/workflows/events/workflow.py +23 -0
  4. vellum/workflows/inputs/base.py +26 -18
  5. vellum/workflows/inputs/tests/test_inputs.py +1 -1
  6. vellum/workflows/nodes/bases/base.py +7 -0
  7. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +7 -0
  8. vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py +32 -0
  9. vellum/workflows/nodes/core/map_node/node.py +28 -7
  10. vellum/workflows/nodes/core/map_node/tests/test_node.py +31 -0
  11. vellum/workflows/nodes/core/try_node/node.py +7 -0
  12. vellum/workflows/nodes/core/try_node/tests/test_node.py +32 -0
  13. vellum/workflows/nodes/mocks.py +229 -2
  14. vellum/workflows/nodes/tests/__init__.py +0 -0
  15. vellum/workflows/nodes/tests/test_mocks.py +207 -0
  16. vellum/workflows/outputs/base.py +1 -1
  17. {vellum_ai-0.14.12.dist-info → vellum_ai-0.14.13.dist-info}/METADATA +1 -1
  18. {vellum_ai-0.14.12.dist-info → vellum_ai-0.14.13.dist-info}/RECORD +26 -24
  19. vellum_ee/workflows/display/nodes/base_node_display.py +20 -4
  20. vellum_ee/workflows/display/nodes/get_node_display_class.py +9 -0
  21. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +24 -1
  22. vellum_ee/workflows/display/workflows/base_workflow_display.py +2 -2
  23. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +20 -0
  24. {vellum_ai-0.14.12.dist-info → vellum_ai-0.14.13.dist-info}/LICENSE +0 -0
  25. {vellum_ai-0.14.12.dist-info → vellum_ai-0.14.13.dist-info}/WHEEL +0 -0
  26. {vellum_ai-0.14.12.dist-info → vellum_ai-0.14.13.dist-info}/entry_points.txt +0 -0
@@ -18,7 +18,7 @@ class BaseClientWrapper:
18
18
  headers: typing.Dict[str, str] = {
19
19
  "X-Fern-Language": "Python",
20
20
  "X-Fern-SDK-Name": "vellum-ai",
21
- "X-Fern-SDK-Version": "0.14.12",
21
+ "X-Fern-SDK-Version": "0.14.13",
22
22
  }
23
23
  headers["X_API_KEY"] = self.api_key
24
24
  return headers
@@ -71,6 +71,9 @@ class BaseDescriptor(Generic[_T]):
71
71
  def __hash__(self) -> int:
72
72
  return hash(self._name)
73
73
 
74
+ def __repr__(self) -> str:
75
+ return self._name
76
+
74
77
  @overload
75
78
  def __get__(self, instance: "BaseNode", owner: Type["BaseNode"]) -> _T: ...
76
79
 
@@ -1,5 +1,6 @@
1
1
  from uuid import UUID
2
2
  from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Iterable, Literal, Optional, Type, Union
3
+ from typing_extensions import TypeGuard
3
4
 
4
5
  from pydantic import field_serializer
5
6
 
@@ -182,3 +183,25 @@ WorkflowEvent = Union[
182
183
  ]
183
184
 
184
185
  WorkflowEventStream = Generator[WorkflowEvent, None, None]
186
+
187
+ WorkflowExecutionEvent = Union[
188
+ WorkflowExecutionInitiatedEvent,
189
+ WorkflowExecutionStreamingEvent,
190
+ WorkflowExecutionRejectedEvent,
191
+ WorkflowExecutionPausedEvent,
192
+ WorkflowExecutionResumedEvent,
193
+ WorkflowExecutionFulfilledEvent,
194
+ WorkflowExecutionSnapshottedEvent,
195
+ ]
196
+
197
+
198
+ def is_workflow_event(event: WorkflowEvent) -> TypeGuard[WorkflowExecutionEvent]:
199
+ return (
200
+ event.name == "workflow.execution.initiated"
201
+ or event.name == "workflow.execution.fulfilled"
202
+ or event.name == "workflow.execution.streaming"
203
+ or event.name == "workflow.execution.snapshotted"
204
+ or event.name == "workflow.execution.paused"
205
+ or event.name == "workflow.execution.resumed"
206
+ or event.name == "workflow.execution.rejected"
207
+ )
@@ -42,38 +42,46 @@ class BaseInputs(metaclass=_BaseInputsMeta):
42
42
  __parent_class__: Type = type(None)
43
43
 
44
44
  def __init__(self, **kwargs: Any) -> None:
45
+ """
46
+ Initialize BaseInputs with provided keyword arguments.
47
+
48
+ Validation logic:
49
+ 1. Ensures all required fields (non-Optional types) either:
50
+ - Have a value provided in kwargs, or
51
+ - Have a default value defined in the class
52
+ 2. Validates that no None values are provided for required fields
53
+ 3. Sets all provided values as attributes on the instance
54
+
55
+ Args:
56
+ **kwargs: Keyword arguments corresponding to the class's type annotations
57
+
58
+ Raises:
59
+ WorkflowInitializationException: If a required field is missing or None
60
+ """
45
61
  for name, field_type in self.__class__.__annotations__.items():
46
- if name not in kwargs and name not in vars(self.__class__):
62
+ # Get the value (either from kwargs or class default)
63
+ value = kwargs.get(name)
64
+ has_default = name in vars(self.__class__)
65
+
66
+ if value is None and not has_default:
67
+ # Check if field_type allows None
47
68
  origin = get_origin(field_type)
48
69
  args = get_args(field_type)
49
70
  if not (origin is Union and type(None) in args):
50
71
  raise WorkflowInitializationException(
51
- message="Required input variables should have defined value",
72
+ message=f"Required input variables {name} should have defined value",
52
73
  code=WorkflowErrorCode.INVALID_INPUTS,
53
74
  )
54
75
 
55
- for name, value in kwargs.items():
56
- field_type = self.__class__.__annotations__.get(name)
57
- if field_type:
58
- self._validate_input(value, field_type)
59
- setattr(self, name, value)
76
+ # If value provided in kwargs, set it on the instance
77
+ if name in kwargs:
78
+ setattr(self, name, value)
60
79
 
61
80
  def __iter__(self) -> Iterator[Tuple[InputReference, Any]]:
62
81
  for input_descriptor in self.__class__:
63
82
  if hasattr(self, input_descriptor.name):
64
83
  yield (input_descriptor, getattr(self, input_descriptor.name))
65
84
 
66
- def _validate_input(self, value: Any, field_type: Any) -> None:
67
- if value is None:
68
- # Check if field_type is Optional
69
- origin = get_origin(field_type)
70
- args = get_args(field_type)
71
- if not (origin is Union and type(None) in args):
72
- raise WorkflowInitializationException(
73
- message="Required input variables should have defined value",
74
- code=WorkflowErrorCode.INVALID_INPUTS,
75
- )
76
-
77
85
  @classmethod
78
86
  def __get_pydantic_core_schema__(
79
87
  cls, source_type: Type[Any], handler: GetCoreSchemaHandler
@@ -34,7 +34,7 @@ def test_base_inputs_empty_value():
34
34
 
35
35
  # THEN it should raise a NodeException with the correct error message and code
36
36
  assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
37
- assert "Required input variables should have defined value" in str(exc_info.value)
37
+ assert "Required input variables required_string should have defined value" == str(exc_info.value)
38
38
 
39
39
 
40
40
  def test_base_inputs_with_default():
@@ -19,6 +19,7 @@ from vellum.workflows.ports.port import Port
19
19
  from vellum.workflows.references import ExternalInputReference
20
20
  from vellum.workflows.references.execution_count import ExecutionCountReference
21
21
  from vellum.workflows.references.node import NodeReference
22
+ from vellum.workflows.references.output import OutputReference
22
23
  from vellum.workflows.state.base import BaseState
23
24
  from vellum.workflows.state.context import WorkflowContext
24
25
  from vellum.workflows.types.core import MergeBehavior
@@ -118,6 +119,11 @@ class BaseNodeMeta(type):
118
119
  node_class.Trigger.node_class = node_class
119
120
  node_class.ExternalInputs.__parent_class__ = node_class
120
121
  node_class.__id__ = uuid4_from_hash(node_class.__qualname__)
122
+ node_class.__output_ids__ = {
123
+ ref.name: uuid4_from_hash(f"{node_class.__id__}|{ref.name}")
124
+ for ref in node_class.Outputs
125
+ if isinstance(ref, OutputReference)
126
+ }
121
127
  return node_class
122
128
 
123
129
  @property
@@ -236,6 +242,7 @@ NodeRunResponse = Union[BaseOutputs, Iterator[BaseOutput]]
236
242
 
237
243
  class BaseNode(Generic[StateType], metaclass=BaseNodeMeta):
238
244
  __id__: UUID = uuid4_from_hash(__qualname__)
245
+ __output_ids__: Dict[str, UUID] = {}
239
246
  state: StateType
240
247
  _context: WorkflowContext
241
248
  _inputs: MappingProxyType[NodeReference, Any]
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterator, Option
3
3
  from vellum.workflows.constants import undefined
4
4
  from vellum.workflows.context import execution_context, get_parent_context
5
5
  from vellum.workflows.errors.types import WorkflowErrorCode
6
+ from vellum.workflows.events.workflow import is_workflow_event
6
7
  from vellum.workflows.exceptions import NodeException
7
8
  from vellum.workflows.inputs.base import BaseInputs
8
9
  from vellum.workflows.nodes.bases.base import BaseNode, BaseNodeMeta
@@ -86,6 +87,12 @@ class InlineSubworkflowNode(
86
87
 
87
88
  for event in subworkflow_stream:
88
89
  self._context._emit_subworkflow_event(event)
90
+
91
+ if not is_workflow_event(event):
92
+ continue
93
+ if event.workflow_definition != self.subworkflow:
94
+ continue
95
+
89
96
  if event.name == "workflow.execution.streaming":
90
97
  if event.output.is_fulfilled:
91
98
  fulfilled_output_names.add(event.output.name)
@@ -3,6 +3,7 @@ import pytest
3
3
  from vellum.workflows.inputs.base import BaseInputs
4
4
  from vellum.workflows.nodes.bases.base import BaseNode
5
5
  from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubworkflowNode
6
+ from vellum.workflows.nodes.core.try_node.node import TryNode
6
7
  from vellum.workflows.outputs.base import BaseOutput
7
8
  from vellum.workflows.state.base import BaseState
8
9
  from vellum.workflows.workflows.base import BaseWorkflow
@@ -55,3 +56,34 @@ def test_inline_subworkflow_node__support_inputs_as_attributes():
55
56
  assert events == [
56
57
  BaseOutput(name="out", value="bar"),
57
58
  ]
59
+
60
+
61
+ def test_inline_subworkflow_node__nested_try():
62
+ """
63
+ Ensure that the nested try node doesn't affect the subworkflow node's outputs
64
+ """
65
+
66
+ # GIVEN a nested try node
67
+ @TryNode.wrap()
68
+ class InnerNode(BaseNode):
69
+ class Outputs:
70
+ foo = "hello"
71
+
72
+ # AND a subworkflow
73
+ class Subworkflow(BaseWorkflow):
74
+ graph = InnerNode
75
+
76
+ class Outputs(BaseWorkflow.Outputs):
77
+ bar = InnerNode.Outputs.foo
78
+
79
+ # AND an outer try node referencing that subworkflow
80
+ class OuterNode(InlineSubworkflowNode):
81
+ subworkflow = Subworkflow
82
+
83
+ # WHEN we run the try node
84
+ stream = OuterNode().run()
85
+ events = list(stream)
86
+
87
+ # THEN we only have the outer node's outputs
88
+ valid_events = [e for e in events if e.name == "bar"]
89
+ assert len(valid_events) == len(events)
@@ -1,4 +1,5 @@
1
1
  from collections import defaultdict
2
+ import logging
2
3
  from queue import Empty, Queue
3
4
  from threading import Thread
4
5
  from typing import (
@@ -19,6 +20,7 @@ from typing import (
19
20
  from vellum.workflows.context import ExecutionContext, execution_context, get_execution_context
20
21
  from vellum.workflows.descriptors.base import BaseDescriptor
21
22
  from vellum.workflows.errors.types import WorkflowErrorCode
23
+ from vellum.workflows.events.workflow import is_workflow_event
22
24
  from vellum.workflows.exceptions import NodeException
23
25
  from vellum.workflows.inputs.base import BaseInputs
24
26
  from vellum.workflows.nodes.bases.base_adornment_node import BaseAdornmentNode
@@ -33,6 +35,8 @@ from vellum.workflows.workflows.event_filters import all_workflow_event_filter
33
35
  if TYPE_CHECKING:
34
36
  from vellum.workflows.events.workflow import WorkflowEvent
35
37
 
38
+ logger = logging.getLogger(__name__)
39
+
36
40
  MapNodeItemType = TypeVar("MapNodeItemType")
37
41
 
38
42
 
@@ -104,19 +108,36 @@ class MapNode(BaseAdornmentNode[StateType], Generic[StateType, MapNodeItemType])
104
108
  subworkflow_event = map_node_event[1]
105
109
  self._context._emit_subworkflow_event(subworkflow_event)
106
110
 
111
+ if not is_workflow_event(subworkflow_event):
112
+ continue
113
+
114
+ if subworkflow_event.workflow_definition != self.subworkflow:
115
+ continue
116
+
107
117
  if subworkflow_event.name == "workflow.execution.initiated":
108
118
  for output_name in mapped_items.keys():
109
119
  yield BaseOutput(name=output_name, delta=(None, index, "INITIATED"))
110
120
 
111
121
  elif subworkflow_event.name == "workflow.execution.fulfilled":
112
- workflow_output_vars = vars(subworkflow_event.outputs)
113
-
114
- for output_name in workflow_output_vars:
115
- output_mapped_items = mapped_items[output_name]
116
- output_mapped_items[index] = workflow_output_vars[output_name]
122
+ for output_reference, output_value in subworkflow_event.outputs:
123
+ if not isinstance(output_reference, OutputReference):
124
+ logger.error(
125
+ "Invalid key to map node's subworkflow event outputs",
126
+ extra={"output_reference_type": type(output_reference)},
127
+ )
128
+ continue
129
+
130
+ output_mapped_items = mapped_items[output_reference.name]
131
+ if index < 0 or index >= len(output_mapped_items):
132
+ logger.error(
133
+ "Invalid map node index", extra={"index": index, "output_name": output_reference.name}
134
+ )
135
+ continue
136
+
137
+ output_mapped_items[index] = output_value
117
138
  yield BaseOutput(
118
- name=output_name,
119
- delta=(output_mapped_items[index], index, "FULFILLED"),
139
+ name=output_reference.name,
140
+ delta=(output_value, index, "FULFILLED"),
120
141
  )
121
142
 
122
143
  fulfilled_iterations[index] = True
@@ -3,8 +3,10 @@ import time
3
3
  from vellum.workflows.inputs.base import BaseInputs
4
4
  from vellum.workflows.nodes.bases import BaseNode
5
5
  from vellum.workflows.nodes.core.map_node.node import MapNode
6
+ from vellum.workflows.nodes.core.try_node.node import TryNode
6
7
  from vellum.workflows.outputs.base import BaseOutput, BaseOutputs
7
8
  from vellum.workflows.state.base import BaseState, StateMeta
9
+ from vellum.workflows.workflows.base import BaseWorkflow
8
10
 
9
11
 
10
12
  def test_map_node__use_parent_inputs_and_state():
@@ -85,3 +87,32 @@ def test_map_node__empty_list():
85
87
  # THEN the node should return an empty output
86
88
  fulfilled_output = outputs[-1]
87
89
  assert fulfilled_output == BaseOutput(name="value", value=[])
90
+
91
+
92
+ def test_map_node__inner_try():
93
+ # GIVEN a try wrapped node
94
+ @TryNode.wrap()
95
+ class InnerNode(BaseNode):
96
+ class Outputs(BaseNode.Outputs):
97
+ foo: str
98
+
99
+ # AND a workflow using that node
100
+ class SimpleMapNodeWorkflow(BaseWorkflow[MapNode.SubworkflowInputs, BaseState]):
101
+ graph = InnerNode
102
+
103
+ class Outputs(BaseWorkflow.Outputs):
104
+ final_output = InnerNode.Outputs.foo
105
+
106
+ # AND a map node referencing that workflow
107
+ class SimpleMapNode(MapNode):
108
+ items = ["hello", "world"]
109
+ subworkflow = SimpleMapNodeWorkflow
110
+ max_concurrency = 4
111
+
112
+ # WHEN we run the workflow
113
+ stream = SimpleMapNode().run()
114
+ outputs = list(stream)
115
+
116
+ # THEN the workflow should succeed
117
+ assert outputs[-1].name == "final_output"
118
+ assert len(outputs[-1].value) == 2
@@ -2,6 +2,7 @@ from typing import Callable, Generic, Iterator, Optional, Set, Type
2
2
 
3
3
  from vellum.workflows.context import execution_context, get_parent_context
4
4
  from vellum.workflows.errors.types import WorkflowError, WorkflowErrorCode
5
+ from vellum.workflows.events.workflow import is_workflow_event
5
6
  from vellum.workflows.exceptions import NodeException
6
7
  from vellum.workflows.nodes.bases import BaseNode
7
8
  from vellum.workflows.nodes.bases.base_adornment_node import BaseAdornmentNode
@@ -47,6 +48,12 @@ class TryNode(BaseAdornmentNode[StateType], Generic[StateType]):
47
48
  if exception:
48
49
  continue
49
50
 
51
+ if not is_workflow_event(event):
52
+ continue
53
+
54
+ if event.workflow_definition != self.subworkflow:
55
+ continue
56
+
50
57
  if event.name == "workflow.execution.streaming":
51
58
  if event.output.is_fulfilled:
52
59
  fulfilled_output_names.add(event.output.name)
@@ -10,6 +10,7 @@ from vellum.workflows.outputs import BaseOutputs
10
10
  from vellum.workflows.outputs.base import BaseOutput
11
11
  from vellum.workflows.state.base import BaseState, StateMeta
12
12
  from vellum.workflows.state.context import WorkflowContext
13
+ from vellum.workflows.workflows.base import BaseWorkflow
13
14
 
14
15
 
15
16
  def test_try_node__on_error_code__successfully_caught():
@@ -126,3 +127,34 @@ def test_try_node__resolved_inputs():
126
127
  foo = State.counter
127
128
 
128
129
  assert MyNode.foo.types == (float,)
130
+
131
+
132
+ def test_try_node__nested_try():
133
+ """
134
+ Ensure that the nested try node doesn't affect the outer try node's outputs
135
+ """
136
+
137
+ # GIVEN a nested try node
138
+ @TryNode.wrap()
139
+ class InnerNode(BaseNode):
140
+ class Outputs:
141
+ foo = "hello"
142
+
143
+ # AND a subworkflow
144
+ class Subworkflow(BaseWorkflow):
145
+ graph = InnerNode
146
+
147
+ class Outputs(BaseWorkflow.Outputs):
148
+ bar = InnerNode.Outputs.foo
149
+
150
+ # AND an outer try node referencing that subworkflow
151
+ class OuterNode(TryNode):
152
+ subworkflow = Subworkflow
153
+
154
+ # WHEN we run the try node
155
+ stream = OuterNode().run()
156
+ events = list(stream)
157
+
158
+ # THEN we only have the outer node's outputs
159
+ valid_events = [e for e in events if e.name == "bar"]
160
+ assert len(valid_events) == len(events)
@@ -1,10 +1,102 @@
1
- from typing import Sequence, Union
1
+ from functools import reduce
2
+ from uuid import UUID
3
+ from typing import TYPE_CHECKING, Any, List, Literal, Optional, Sequence, Type, Union
2
4
 
3
- from pydantic import ConfigDict
5
+ from pydantic import ConfigDict, ValidationError
4
6
 
5
7
  from vellum.client.core.pydantic_utilities import UniversalBaseModel
8
+ from vellum.client.types.array_vellum_value import ArrayVellumValue
9
+ from vellum.client.types.vellum_value import VellumValue
6
10
  from vellum.workflows.descriptors.base import BaseDescriptor
11
+ from vellum.workflows.errors.types import WorkflowErrorCode
12
+ from vellum.workflows.exceptions import WorkflowInitializationException
7
13
  from vellum.workflows.outputs.base import BaseOutputs
14
+ from vellum.workflows.references.constant import ConstantValueReference
15
+
16
+ if TYPE_CHECKING:
17
+ from vellum.workflows import BaseWorkflow
18
+
19
+ import logging
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class _RawLogicalCondition(UniversalBaseModel):
25
+ type: Literal["LOGICAL_CONDITION"] = "LOGICAL_CONDITION"
26
+ lhs_variable_id: UUID
27
+ operator: Literal["==", ">", ">=", "<", "<=", "!="]
28
+ rhs_variable_id: UUID
29
+
30
+
31
+ class _RawLogicalConditionGroup(UniversalBaseModel):
32
+ type: Literal["LOGICAL_CONDITION_GROUP"] = "LOGICAL_CONDITION_GROUP"
33
+ conditions: List["_RawLogicalExpression"]
34
+ combinator: Literal["AND", "OR"]
35
+ negated: bool
36
+
37
+
38
+ _RawLogicalExpression = Union[_RawLogicalCondition, _RawLogicalConditionGroup]
39
+
40
+
41
+ class _RawLogicalExpressionVariable(UniversalBaseModel):
42
+ id: UUID
43
+
44
+
45
+ class _RawMockWorkflowNodeExecutionConstantValuePointer(_RawLogicalExpressionVariable):
46
+ type: Literal["CONSTANT_VALUE"] = "CONSTANT_VALUE"
47
+ variable_value: VellumValue
48
+
49
+
50
+ class _RawMockWorkflowNodeExecutionNodeExecutionCounterPointer(_RawLogicalExpressionVariable):
51
+ type: Literal["EXECUTION_COUNTER"] = "EXECUTION_COUNTER"
52
+ node_id: UUID
53
+
54
+
55
+ class _RawMockWorkflowNodeExecutionInputVariablePointer(_RawLogicalExpressionVariable):
56
+ type: Literal["INPUT_VARIABLE"] = "INPUT_VARIABLE"
57
+ input_variable_id: UUID
58
+
59
+
60
+ class _RawMockWorkflowNodeExecutionNodeOutputPointer(_RawLogicalExpressionVariable):
61
+ type: Literal["NODE_OUTPUT"] = "NODE_OUTPUT"
62
+ node_id: UUID
63
+ input_id: UUID
64
+
65
+
66
+ class _RawMockWorkflowNodeExecutionNodeInputPointer(_RawLogicalExpressionVariable):
67
+ type: Literal["NODE_INPUT"] = "NODE_INPUT"
68
+ node_id: UUID
69
+ input_id: UUID
70
+
71
+
72
+ _RawMockWorkflowNodeExecutionValuePointer = Union[
73
+ _RawMockWorkflowNodeExecutionConstantValuePointer,
74
+ _RawMockWorkflowNodeExecutionNodeExecutionCounterPointer,
75
+ _RawMockWorkflowNodeExecutionInputVariablePointer,
76
+ _RawMockWorkflowNodeExecutionNodeOutputPointer,
77
+ _RawMockWorkflowNodeExecutionNodeInputPointer,
78
+ ]
79
+
80
+
81
+ class _RawMockWorkflowNodeWhenCondition(UniversalBaseModel):
82
+ expression: _RawLogicalExpression
83
+ variables: List[_RawMockWorkflowNodeExecutionValuePointer]
84
+
85
+
86
+ class _RawMockWorkflowNodeThenOutput(UniversalBaseModel):
87
+ output_id: UUID
88
+ value: _RawMockWorkflowNodeExecutionValuePointer
89
+
90
+
91
+ class _RawMockWorkflowNodeExecution(UniversalBaseModel):
92
+ when_condition: _RawMockWorkflowNodeWhenCondition
93
+ then_outputs: List[_RawMockWorkflowNodeThenOutput]
94
+
95
+
96
+ class _RawMockWorkflowNodeConfig(UniversalBaseModel):
97
+ type: Literal["WORKFLOW_NODE_OUTPUT"] = "WORKFLOW_NODE_OUTPUT"
98
+ node_id: UUID
99
+ mock_executions: List[_RawMockWorkflowNodeExecution]
8
100
 
9
101
 
10
102
  class MockNodeExecution(UniversalBaseModel):
@@ -13,5 +105,140 @@ class MockNodeExecution(UniversalBaseModel):
13
105
 
14
106
  model_config = ConfigDict(arbitrary_types_allowed=True)
15
107
 
108
+ @staticmethod
109
+ def validate_all(
110
+ raw_mock_workflow_node_configs: Optional[List[Any]],
111
+ workflow: Type["BaseWorkflow"],
112
+ ) -> Optional[List["MockNodeExecution"]]:
113
+ if not raw_mock_workflow_node_configs:
114
+ return None
115
+
116
+ ArrayVellumValue.model_rebuild()
117
+ try:
118
+ mock_workflow_node_configs = [
119
+ _RawMockWorkflowNodeConfig.model_validate(raw_mock_workflow_node_config)
120
+ for raw_mock_workflow_node_config in raw_mock_workflow_node_configs
121
+ ]
122
+ except ValidationError as e:
123
+ raise WorkflowInitializationException(
124
+ message="Failed to validate mock node executions",
125
+ code=WorkflowErrorCode.INVALID_INPUTS,
126
+ ) from e
127
+
128
+ nodes = {node.__id__: node for node in workflow.get_nodes()}
129
+ node_output_name_by_id = {
130
+ node.__output_ids__[output.name]: output.name for node in workflow.get_nodes() for output in node.Outputs
131
+ }
132
+
133
+ # We need to support the old way that the Vellum App's WorkflowRunner used to define Node Mocks in order to
134
+ # avoid needing to update the mock resolution strategy that it and the frontend uses. The path towards
135
+ # cleaning this up will go as follows:
136
+ # 1. Release Mock support in SDK-Enabled Workflows
137
+ # 2. Deprecate Mock support in non-SDK enabled Workflows, encouraging users to migrate to SDK-enabled Workflows
138
+ # 3. Remove the old mock resolution strategy
139
+ # 4. Update this SDK to handle the new mock resolution strategy with WorkflowValueDescriptors
140
+ # 5. Cutover the Vellum App to the new mock resolution strategy
141
+ # 6. Remove the old mock resolution strategy from this SDK
142
+ def _translate_raw_logical_expression(
143
+ raw_logical_expression: _RawLogicalExpression,
144
+ raw_variables: List[_RawMockWorkflowNodeExecutionValuePointer],
145
+ ) -> BaseDescriptor:
146
+ if raw_logical_expression.type == "LOGICAL_CONDITION":
147
+ return _translate_raw_logical_condition(raw_logical_expression, raw_variables)
148
+ else:
149
+ return _translate_raw_logical_condition_group(raw_logical_expression, raw_variables)
150
+
151
+ def _translate_raw_logical_condition_group(
152
+ raw_logical_condition_group: _RawLogicalConditionGroup,
153
+ raw_variables: List[_RawMockWorkflowNodeExecutionValuePointer],
154
+ ) -> BaseDescriptor:
155
+ if not raw_logical_condition_group.conditions:
156
+ return ConstantValueReference(True)
157
+
158
+ conditions = [
159
+ _translate_raw_logical_expression(condition, raw_variables)
160
+ for condition in raw_logical_condition_group.conditions
161
+ ]
162
+ return reduce(
163
+ lambda acc, condition: (
164
+ acc and condition if raw_logical_condition_group.combinator == "AND" else acc or condition
165
+ ),
166
+ conditions,
167
+ )
168
+
169
+ def _translate_raw_logical_condition(
170
+ raw_logical_condition: _RawLogicalCondition,
171
+ raw_variables: List[_RawMockWorkflowNodeExecutionValuePointer],
172
+ ) -> BaseDescriptor:
173
+ variable_by_id = {v.id: v for v in raw_variables}
174
+ lhs = _translate_raw_logical_expression_variable(variable_by_id[raw_logical_condition.lhs_variable_id])
175
+ rhs = _translate_raw_logical_expression_variable(variable_by_id[raw_logical_condition.rhs_variable_id])
176
+ if raw_logical_condition.operator == ">":
177
+ return lhs.greater_than(rhs)
178
+ elif raw_logical_condition.operator == ">=":
179
+ return lhs.greater_than_or_equal_to(rhs)
180
+ elif raw_logical_condition.operator == "<":
181
+ return lhs.less_than(rhs)
182
+ elif raw_logical_condition.operator == "<=":
183
+ return lhs.less_than_or_equal_to(rhs)
184
+ elif raw_logical_condition.operator == "==":
185
+ return lhs.equals(rhs)
186
+ elif raw_logical_condition.operator == "!=":
187
+ return lhs.does_not_equal(rhs)
188
+ else:
189
+ raise WorkflowInitializationException(f"Unsupported logical operator: {raw_logical_condition.operator}")
190
+
191
+ def _translate_raw_logical_expression_variable(
192
+ raw_variable: _RawMockWorkflowNodeExecutionValuePointer,
193
+ ) -> BaseDescriptor:
194
+ if raw_variable.type == "CONSTANT_VALUE":
195
+ return ConstantValueReference(raw_variable.variable_value.value)
196
+ elif raw_variable.type == "EXECUTION_COUNTER":
197
+ node = nodes[raw_variable.node_id]
198
+ return node.Execution.count
199
+ else:
200
+ raise WorkflowInitializationException(f"Unsupported logical expression type: {raw_variable.type}")
201
+
202
+ mock_node_executions = []
203
+ for mock_workflow_node_config in mock_workflow_node_configs:
204
+ for mock_execution in mock_workflow_node_config.mock_executions:
205
+ try:
206
+ when_condition = _translate_raw_logical_expression(
207
+ mock_execution.when_condition.expression,
208
+ mock_execution.when_condition.variables,
209
+ )
210
+
211
+ then_outputs = nodes[mock_workflow_node_config.node_id].Outputs()
212
+ for then_output in mock_execution.then_outputs:
213
+ node_output_name = node_output_name_by_id.get(then_output.output_id)
214
+ if node_output_name is None:
215
+ raise WorkflowInitializationException(
216
+ f"Output {then_output.output_id} not found in node {mock_workflow_node_config.node_id}"
217
+ )
218
+
219
+ resolved_output_reference = _translate_raw_logical_expression_variable(then_output.value)
220
+ if isinstance(resolved_output_reference, ConstantValueReference):
221
+ setattr(
222
+ then_outputs,
223
+ node_output_name,
224
+ resolved_output_reference._value,
225
+ )
226
+ else:
227
+ raise WorkflowInitializationException(
228
+ f"Unsupported resolved output reference type: {type(resolved_output_reference)}"
229
+ )
230
+
231
+ mock_node_executions.append(
232
+ MockNodeExecution(
233
+ when_condition=when_condition,
234
+ then_outputs=then_outputs,
235
+ )
236
+ )
237
+ except Exception as e:
238
+ logger.exception("Failed to validate mock node execution", exc_info=e)
239
+ continue
240
+
241
+ return mock_node_executions
242
+
16
243
 
17
244
  MockNodeExecutionArg = Sequence[Union[BaseOutputs, MockNodeExecution]]
File without changes
@@ -0,0 +1,207 @@
1
+ import uuid
2
+
3
+ from vellum.client.types.string_vellum_value import StringVellumValue
4
+ from vellum.workflows import BaseWorkflow
5
+ from vellum.workflows.nodes import InlinePromptNode
6
+ from vellum.workflows.nodes.bases.base import BaseNode
7
+ from vellum.workflows.nodes.mocks import MockNodeExecution
8
+ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
9
+ from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay
10
+
11
+
12
+ def test_mocks__parse_from_app():
13
+ # GIVEN a PromptNode
14
+ class PromptNode(InlinePromptNode):
15
+ pass
16
+
17
+ # AND a workflow class with that PromptNode
18
+ class MyWorkflow(BaseWorkflow):
19
+ graph = PromptNode
20
+
21
+ # AND a mock workflow node execution from the app
22
+ raw_mock_workflow_node_execution = [
23
+ {
24
+ "type": "WORKFLOW_NODE_OUTPUT",
25
+ "node_id": str(PromptNode.__id__),
26
+ "mock_executions": [
27
+ {
28
+ "when_condition": {
29
+ "expression": {
30
+ "type": "LOGICAL_CONDITION_GROUP",
31
+ "combinator": "AND",
32
+ "negated": False,
33
+ "conditions": [
34
+ {
35
+ "type": "LOGICAL_CONDITION",
36
+ "lhs_variable_id": "e60902d5-6892-4916-80c1-f0130af52322",
37
+ "operator": ">=",
38
+ "rhs_variable_id": "5c1bbb24-c288-49cb-a9b7-0c6f38a86037",
39
+ }
40
+ ],
41
+ },
42
+ "variables": [
43
+ {
44
+ "type": "EXECUTION_COUNTER",
45
+ "node_id": str(PromptNode.__id__),
46
+ "id": "e60902d5-6892-4916-80c1-f0130af52322",
47
+ },
48
+ {
49
+ "type": "CONSTANT_VALUE",
50
+ "variable_value": {"type": "NUMBER", "value": 0},
51
+ "id": "5c1bbb24-c288-49cb-a9b7-0c6f38a86037",
52
+ },
53
+ ],
54
+ },
55
+ "then_outputs": [
56
+ {
57
+ "output_id": "9e6dc5d3-8ea0-4346-8a2a-7cce5495755b",
58
+ "value": {
59
+ "id": "27006b2a-fa81-430c-a0b2-c66a9351fc68",
60
+ "type": "CONSTANT_VALUE",
61
+ "variable_value": {"type": "STRING", "value": "Hello"},
62
+ },
63
+ },
64
+ {
65
+ "output_id": "60305ffd-60b0-42aa-b54e-4fdae0f8c28a",
66
+ "value": {
67
+ "id": "4559c778-6e27-4cfe-a460-734ba62a5082",
68
+ "type": "CONSTANT_VALUE",
69
+ "variable_value": {"type": "ARRAY", "value": [{"type": "STRING", "value": "Hello"}]},
70
+ },
71
+ },
72
+ ],
73
+ }
74
+ ],
75
+ }
76
+ ]
77
+
78
+ # WHEN we parse the mock workflow node execution
79
+ node_output_mocks = MockNodeExecution.validate_all(
80
+ raw_mock_workflow_node_execution,
81
+ MyWorkflow,
82
+ )
83
+
84
+ # THEN we get a list of MockNodeExecution objects
85
+ assert node_output_mocks
86
+ assert len(node_output_mocks) == 1
87
+ assert node_output_mocks[0] == MockNodeExecution(
88
+ when_condition=PromptNode.Execution.count.greater_than_or_equal_to(0.0),
89
+ then_outputs=PromptNode.Outputs(
90
+ text="Hello",
91
+ results=[
92
+ StringVellumValue(value="Hello"),
93
+ ],
94
+ ),
95
+ )
96
+
97
+
98
+ def test_mocks__parse_none_still_runs():
99
+ # GIVEN a Base Node
100
+ class StartNode(BaseNode):
101
+ class Outputs(BaseNode.Outputs):
102
+ foo: str
103
+
104
+ # AND a workflow class with that Node
105
+ class MyWorkflow(BaseWorkflow):
106
+ graph = StartNode
107
+
108
+ class Outputs(BaseWorkflow.Outputs):
109
+ final_value = StartNode.Outputs.foo
110
+
111
+ # AND we parsed `None` on `MockNodeExecution`
112
+ node_output_mocks = MockNodeExecution.validate_all(
113
+ None,
114
+ MyWorkflow,
115
+ )
116
+
117
+ # WHEN we run the workflow
118
+ workflow = MyWorkflow()
119
+ final_event = workflow.run(node_output_mocks=node_output_mocks)
120
+
121
+ # THEN it was successful
122
+ assert final_event.name == "workflow.execution.fulfilled"
123
+
124
+
125
+ def test_mocks__use_id_from_display():
126
+ # GIVEN a Base Node
127
+ class StartNode(BaseNode):
128
+ class Outputs(BaseNode.Outputs):
129
+ foo: str
130
+
131
+ # AND a workflow class with that Node
132
+ class MyWorkflow(BaseWorkflow):
133
+ graph = StartNode
134
+
135
+ class Outputs(BaseWorkflow.Outputs):
136
+ final_value = StartNode.Outputs.foo
137
+
138
+ # AND a display class on that Base Node
139
+ node_output_id = uuid.uuid4()
140
+
141
+ class StartNodeDisplay(BaseNodeDisplay[StartNode]):
142
+ output_display = {StartNode.Outputs.foo: NodeOutputDisplay(id=node_output_id, name="foo")}
143
+
144
+ # AND a mock workflow node execution from the app
145
+ raw_mock_workflow_node_execution = [
146
+ {
147
+ "type": "WORKFLOW_NODE_OUTPUT",
148
+ "node_id": str(StartNode.__id__),
149
+ "mock_executions": [
150
+ {
151
+ "when_condition": {
152
+ "expression": {
153
+ "type": "LOGICAL_CONDITION_GROUP",
154
+ "combinator": "AND",
155
+ "negated": False,
156
+ "conditions": [
157
+ {
158
+ "type": "LOGICAL_CONDITION",
159
+ "lhs_variable_id": "e60902d5-6892-4916-80c1-f0130af52322",
160
+ "operator": ">=",
161
+ "rhs_variable_id": "5c1bbb24-c288-49cb-a9b7-0c6f38a86037",
162
+ }
163
+ ],
164
+ },
165
+ "variables": [
166
+ {
167
+ "type": "EXECUTION_COUNTER",
168
+ "node_id": str(StartNode.__id__),
169
+ "id": "e60902d5-6892-4916-80c1-f0130af52322",
170
+ },
171
+ {
172
+ "type": "CONSTANT_VALUE",
173
+ "variable_value": {"type": "NUMBER", "value": 0},
174
+ "id": "5c1bbb24-c288-49cb-a9b7-0c6f38a86037",
175
+ },
176
+ ],
177
+ },
178
+ "then_outputs": [
179
+ {
180
+ "output_id": str(node_output_id),
181
+ "value": {
182
+ "id": "27006b2a-fa81-430c-a0b2-c66a9351fc68",
183
+ "type": "CONSTANT_VALUE",
184
+ "variable_value": {"type": "STRING", "value": "Hello"},
185
+ },
186
+ },
187
+ ],
188
+ }
189
+ ],
190
+ }
191
+ ]
192
+
193
+ # WHEN we parsed the raw data on `MockNodeExecution`
194
+ node_output_mocks = MockNodeExecution.validate_all(
195
+ raw_mock_workflow_node_execution,
196
+ MyWorkflow,
197
+ )
198
+
199
+ # THEN we get the expected list of MockNodeExecution objects
200
+ assert node_output_mocks
201
+ assert len(node_output_mocks) == 1
202
+ assert node_output_mocks[0] == MockNodeExecution(
203
+ when_condition=StartNode.Execution.count.greater_than_or_equal_to(0.0),
204
+ then_outputs=StartNode.Outputs(
205
+ foo="Hello",
206
+ ),
207
+ )
@@ -201,7 +201,7 @@ class BaseOutputs(metaclass=_BaseOutputsMeta):
201
201
  self._outputs_post_init(**kwargs)
202
202
 
203
203
  def __eq__(self, other: object) -> bool:
204
- if not isinstance(other, dict):
204
+ if not isinstance(other, (dict, BaseOutputs)):
205
205
  return super().__eq__(other)
206
206
 
207
207
  outputs = {ref.name: value for ref, value in self if value is not undefined}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.12
3
+ Version: 0.14.13
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -23,9 +23,9 @@ vellum_ee/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
23
23
  vellum_ee/workflows/display/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  vellum_ee/workflows/display/base.py,sha256=ak29FIsawhaFa9_paZUHThlZRFJ1xB486JWKuSt1PYY,1965
25
25
  vellum_ee/workflows/display/nodes/__init__.py,sha256=436iSAh_Ex5tC68oEYvNgPu05ZVIAVXnS4PKGrQeZ0Y,321
26
- vellum_ee/workflows/display/nodes/base_node_display.py,sha256=h4_JqwH_AsT92OK14S3kqssZDjL-uyXnJfo0XcLijK0,16492
26
+ vellum_ee/workflows/display/nodes/base_node_display.py,sha256=p6orKsqEo8u6PWDbpp60D_E4UtjWNzWI8aVe8aknpyc,17097
27
27
  vellum_ee/workflows/display/nodes/base_node_vellum_display.py,sha256=pLO0dORfRu--Ne9NgoyFT_CNjfpr5fGCsgbsMkUF5GM,2845
28
- vellum_ee/workflows/display/nodes/get_node_display_class.py,sha256=0S6ksPp53WXWh1RQVH71nj2bkCWBj4ZaFYhTxW3N2F4,1230
28
+ vellum_ee/workflows/display/nodes/get_node_display_class.py,sha256=67J_TGFNoISq8wZqOBCu5BNMY4kpQBq3Lt65FPH3Gt0,1594
29
29
  vellum_ee/workflows/display/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=QqR3Ly0RNrXwOeLdW5nERDFt0gRPf76n1bPES6o5UN4,1093
31
31
  vellum_ee/workflows/display/nodes/types.py,sha256=St1BB6no528OyELGiyRabWao0GGw6mLhstQAvEACbGk,247
@@ -61,7 +61,7 @@ vellum_ee/workflows/display/tests/test_vellum_workflow_display.py,sha256=cdpUoDN
61
61
  vellum_ee/workflows/display/tests/workflow_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py,sha256=A1-tIpC5KIKG9JA_rkd1nLS8zUG3Kb4QiVdvb3boFxE,2509
64
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=jw8keqLNwqtaRSAqStsMqSH_OuuaTQm2MvIx8NTWAa4,14905
64
+ vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=mFmxXfnUPrwndaBurW9E-VSBUQjF3cGv3JpRuNmwWh8,15475
65
65
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py,sha256=1cszL6N6FNGVm61MOa7AEiHnF0QjZWqDQuPOp4yiG94,18277
66
66
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py,sha256=-12ZkZb3f5gyoNASV2yeQtMo5HmNsVEo8nXwL6IC-I8,6261
67
67
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py,sha256=6th6kCwzql6lddjkTQx4Jbvvs4ChqtJwctW-B4QuBhI,37352
@@ -90,9 +90,9 @@ vellum_ee/workflows/display/utils/expressions.py,sha256=9FpOslDI-RCR5m4TgAu9KCHh
90
90
  vellum_ee/workflows/display/utils/vellum.py,sha256=UjK_RxnSEmlIu9klGCPWU5RAQBmgZ7cRbRdgxaTbubE,8081
91
91
  vellum_ee/workflows/display/vellum.py,sha256=7mqQaKZPPrLMcXSAQkPIxCy5x8HkKs5PbCu3GRaC2o8,8507
92
92
  vellum_ee/workflows/display/workflows/__init__.py,sha256=kapXsC67VJcgSuiBMa86FdePG5A9kMB5Pi4Uy1O2ob4,207
93
- vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=Z9s_rtqAH5IfBtXOw40gbiT0GZeN6JRdFwGgPT-DIYM,20143
93
+ vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=yDjMk2tml2TzitcFdP5M7PFXm4G_Bz6aYLYby7riXGQ,20137
94
94
  vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=kp0u8LN_2IwshLrhMImhpZx1hRyAcD5gXY-kDuuaGMQ,1269
95
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=STVSG0eL97mdnwBA5nOOgW8AuK8k-b8kWDyHKatNXIA,4259
95
+ vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=JRTTM8gX182pBMP5eI_-B_X7LaK0kiSS07KqDdIfrQc,4962
96
96
  vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=mbAzCpswOek34ITeTkesbVreCXpulj4NFjIg3RcdVZ8,18243
97
97
  vellum_ee/workflows/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  vellum_ee/workflows/server/virtual_file_loader.py,sha256=X_DdNK7MfyOjKWekk6YQpOSCT6klKcdjT6nVJcBH1sM,1481
@@ -123,7 +123,7 @@ vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
123
123
  vellum/client/__init__.py,sha256=tKtdM1_GqmGq1gpi9ydWD_T-MM7fPn8QdHh8ww19cNI,117564
124
124
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
125
125
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
126
- vellum/client/core/client_wrapper.py,sha256=MP_Nnw3gVGA7BJKaD9TBDDwVxta-T1ASzBcOv7dF4nU,1869
126
+ vellum/client/core/client_wrapper.py,sha256=rm58d4E5Lrfg-SwnN1AtE5vFfVb1Qy6a5GyVyEmNWvU,1869
127
127
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
128
128
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
129
129
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -1299,7 +1299,7 @@ vellum/workflows/__init__.py,sha256=CssPsbNvN6rDhoLuqpEv7MMKGa51vE6dvAh6U31Pcio,
1299
1299
  vellum/workflows/constants.py,sha256=2yg4_uo5gpqViy3ZLSwfC8qTybleYCtOnhA4Rj6bacM,1310
1300
1300
  vellum/workflows/context.py,sha256=DwSf8lO9NHABiqOoD3exgrjUoRuNsKtutaL5TgRbD-A,1441
1301
1301
  vellum/workflows/descriptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1302
- vellum/workflows/descriptors/base.py,sha256=jtT11I5oB-CPWlJBZmc1RcVe4_xOKjimBthiHQHQuT0,14597
1302
+ vellum/workflows/descriptors/base.py,sha256=bvF3MWsc4Xyw5Z2s1A0fbsfMCebIbPYcGvbQ9uoa_Pg,14655
1303
1303
  vellum/workflows/descriptors/exceptions.py,sha256=gUy4UD9JFUKSeQnQpeuDSLiRqWjWiIsxLahB7p_q3JY,54
1304
1304
  vellum/workflows/descriptors/tests/test_utils.py,sha256=xoojJMyG5WLG9xGtmUjirz3lDFCcDsAcxjrtbdG8dNE,6060
1305
1305
  vellum/workflows/descriptors/utils.py,sha256=gmVXJjf2yWmvlYey41J2FZHeSou0JuCHKb3826K_Jok,3838
@@ -1316,7 +1316,7 @@ vellum/workflows/events/node.py,sha256=uHT6If0esgZ3nLjrjmUPTKf3qbjGhoV_x5YKpjDBD
1316
1316
  vellum/workflows/events/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1317
1317
  vellum/workflows/events/tests/test_event.py,sha256=uRfMwSOqU-ROeZKCEngGvvJYlOZuxBhnC3qH5AGi3fM,15399
1318
1318
  vellum/workflows/events/types.py,sha256=AeTJaQt_fNHDLI4nyBzo7XrW9QQybRC09AKzu3kEYEE,3575
1319
- vellum/workflows/events/workflow.py,sha256=sLO29djAeHGVd4hLhaNtOQ48uwUjfl-DotZQt06PxQA,6033
1319
+ vellum/workflows/events/workflow.py,sha256=QoSHyIOpuVacbR7POf7h104miTOhCjtO2udnYximJGs,6851
1320
1320
  vellum/workflows/exceptions.py,sha256=NiBiR3ggfmPxBVqD-H1SqmjI-7mIn0EStSN1BqApvCM,1213
1321
1321
  vellum/workflows/expressions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1322
1322
  vellum/workflows/expressions/accessor.py,sha256=ItZF7fMLzVTqsdAiaXb5SiDupXmX0X9xbIus1W6hRds,1870
@@ -1356,13 +1356,13 @@ vellum/workflows/graph/graph.py,sha256=GGR8cGpSuNWPIiTWNWsj6l70upb5nIxAyFcn7VdaJ
1356
1356
  vellum/workflows/graph/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1357
1357
  vellum/workflows/graph/tests/test_graph.py,sha256=q0wxLvPPxc-6en4a_XuAZwVfLURMd9Ikvoreq4bpJ9s,11839
1358
1358
  vellum/workflows/inputs/__init__.py,sha256=AbFEteIYEvCb14fM3EK7bhM-40-6s494rSlIhQ4Dsss,62
1359
- vellum/workflows/inputs/base.py,sha256=Bj0TO72ucLmPFGgoKDr8LZZbxzk51C6P4Rod0Nv5PwQ,3733
1359
+ vellum/workflows/inputs/base.py,sha256=eVTjtn6GielKYogQ-LPA6n4iZO3-WIfyxWpBKkN-dPs,3959
1360
1360
  vellum/workflows/inputs/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1361
- vellum/workflows/inputs/tests/test_inputs.py,sha256=Haa0Px0obef7rgIddO6wwHF_bzmvz9Yp8OErdx2mQss,1759
1361
+ vellum/workflows/inputs/tests/test_inputs.py,sha256=g--YqWTNWzMk5Ktoj__gq988kvBReefc2tsyUl6H2kg,1775
1362
1362
  vellum/workflows/logging.py,sha256=_a217XogktV4Ncz6xKFz7WfYmZAzkfVRVuC0rWob8ls,437
1363
1363
  vellum/workflows/nodes/__init__.py,sha256=aVdQVv7Y3Ro3JlqXGpxwaU2zrI06plDHD2aumH5WUIs,1157
1364
1364
  vellum/workflows/nodes/bases/__init__.py,sha256=cniHuz_RXdJ4TQgD8CBzoiKDiPxg62ErdVpCbWICX64,58
1365
- vellum/workflows/nodes/bases/base.py,sha256=lVHODc1mjUFCorhZfK01zHohOWz9CYz7dnUzA-KENJ4,15036
1365
+ vellum/workflows/nodes/bases/base.py,sha256=Y5xv0tFSSDafKDhVsxC5xTNrH15sj93jok6POzgWO0E,15351
1366
1366
  vellum/workflows/nodes/bases/base_adornment_node.py,sha256=eFTgsPCYb3eyGS0-kw7C6crFnwFx437R5wh9-8bWYts,2905
1367
1367
  vellum/workflows/nodes/bases/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1368
1368
  vellum/workflows/nodes/bases/tests/test_base_node.py,sha256=4SOdZzvugVtN8CIuo5RrapAxSYGXnxUwQ77dXJ64oTU,6295
@@ -1370,13 +1370,13 @@ vellum/workflows/nodes/core/__init__.py,sha256=5zDMCmyt1v0HTJzlUBwq3U9L825yZGZhT
1370
1370
  vellum/workflows/nodes/core/error_node/__init__.py,sha256=g7RRnlHhqu4qByfLjBwCunmgGA8dI5gNsjS3h6TwlSI,60
1371
1371
  vellum/workflows/nodes/core/error_node/node.py,sha256=MFHU5vITYSK-L9CuMZ49In2ZeNLWnhZD0f8r5dWvb5Y,1270
1372
1372
  vellum/workflows/nodes/core/inline_subworkflow_node/__init__.py,sha256=nKNEH1QTl-1PcvmYoqSWEl0-t6gAur8GLTXHzklRQfM,84
1373
- vellum/workflows/nodes/core/inline_subworkflow_node/node.py,sha256=WE_N8PocCF-xc87nRwbQ_WTBqOQ75JJlUZCdlocp7u0,5953
1373
+ vellum/workflows/nodes/core/inline_subworkflow_node/node.py,sha256=B1ant-Pwg1AGFs5BYXynHf2i4rAen1bkr7nbLmiVwHo,6175
1374
1374
  vellum/workflows/nodes/core/inline_subworkflow_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1375
- vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py,sha256=n0-821Ov9ZfRFX_lbzLy5o2rX8fEw2qoxz0aFWCOxVg,1547
1375
+ vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py,sha256=6AG-oTyJaw5a1KWGQFNZaKl6_Pu_fW3nI_WA3XNRFWY,2439
1376
1376
  vellum/workflows/nodes/core/map_node/__init__.py,sha256=MXpZYmGfhsMJHqqlpd64WiJRtbAtAMQz-_3fCU_cLV0,56
1377
- vellum/workflows/nodes/core/map_node/node.py,sha256=Q-9DlfIJYT7duG_3Up3P5uA96YDnFH6ykwRS4Ugr3uA,8327
1377
+ vellum/workflows/nodes/core/map_node/node.py,sha256=dY27Xm11LHsqD7hnZnVYYDIazZ-XfL4_zatvWKTi6CU,9243
1378
1378
  vellum/workflows/nodes/core/map_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1379
- vellum/workflows/nodes/core/map_node/tests/test_node.py,sha256=8ZXs4IIYrXpa4pZz4BSx9t0fx_Usgk3_KF6r3kcy9tE,2547
1379
+ vellum/workflows/nodes/core/map_node/tests/test_node.py,sha256=uMR0AyIFn539LqTKHdwuBswnx1i-PHyqPpgtYrnmYMY,3496
1380
1380
  vellum/workflows/nodes/core/retry_node/__init__.py,sha256=lN2bIy5a3Uzhs_FYCrooADyYU6ZGShtvLKFWpelwPvo,60
1381
1381
  vellum/workflows/nodes/core/retry_node/node.py,sha256=Vt3fx4G-DRIb9a-IHIUfaAclgfbzOPEQVkcumwhl9HE,4355
1382
1382
  vellum/workflows/nodes/core/retry_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1385,9 +1385,9 @@ vellum/workflows/nodes/core/templating_node/__init__.py,sha256=GmyuYo81_A1_Bz6id
1385
1385
  vellum/workflows/nodes/core/templating_node/node.py,sha256=-JIqLUv6Xpx_LTVZt7whQ2X2VatgHDdTxjMrz64luEs,3721
1386
1386
  vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py,sha256=ldnmSASx0TfAnT3ZvU0AXtN0diZGrfySiXipuJIIzWU,9055
1387
1387
  vellum/workflows/nodes/core/try_node/__init__.py,sha256=JVD4DrldTIqFQQFrubs9KtWCCc0YCAc7Fzol5ZWIWeM,56
1388
- vellum/workflows/nodes/core/try_node/node.py,sha256=_0df2_6kim8pW4hB7IXUJOYS4fLduaeGDH4rRhYYvcg,4212
1388
+ vellum/workflows/nodes/core/try_node/node.py,sha256=5ux1l2HO12FBFFyhz6j-4yfBYVrqgT2maTAne_GnNDk,4434
1389
1389
  vellum/workflows/nodes/core/try_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1390
- vellum/workflows/nodes/core/try_node/tests/test_node.py,sha256=Wc2kLl-MkffsBxl3IiFaqLd16e2Iosxhk7qBnojPvQg,4092
1390
+ vellum/workflows/nodes/core/try_node/tests/test_node.py,sha256=h6eUc3SggvhzBWlOD0PrPUlkoCSQHwjqYn81VkxSIxU,4948
1391
1391
  vellum/workflows/nodes/displayable/__init__.py,sha256=6F_4DlSwvHuilWnIalp8iDjjDXl0Nmz4QzJV2PYe5RI,1023
1392
1392
  vellum/workflows/nodes/displayable/api_node/__init__.py,sha256=MoxdQSnidIj1Nf_d-hTxlOxcZXaZnsWFDbE-PkTK24o,56
1393
1393
  vellum/workflows/nodes/displayable/api_node/node.py,sha256=QdpsyGVxo5PcN8nwGZpcpW_YMKHr3_VvmbK1BlrdOFk,2547
@@ -1450,10 +1450,12 @@ vellum/workflows/nodes/experimental/README.md,sha256=eF6DfIL8t-HbF9-mcofOMymKrra
1450
1450
  vellum/workflows/nodes/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1451
1451
  vellum/workflows/nodes/experimental/openai_chat_completion_node/__init__.py,sha256=lsyD9laR9p7kx5-BXGH2gUTM242UhKy8SMV0SR6S2iE,90
1452
1452
  vellum/workflows/nodes/experimental/openai_chat_completion_node/node.py,sha256=1EGeiaT-Zoo6pttQFKKBcdf3dmhAbjKGaErYD5FFwlc,10185
1453
- vellum/workflows/nodes/mocks.py,sha256=gvM2tyoe-V84jFbFdhQsyGAPyQBzmjn_CkhT_yxccgY,499
1453
+ vellum/workflows/nodes/mocks.py,sha256=a1FjWEIocseMfjzM-i8DNozpUsaW0IONRpZmXBoWlyc,10455
1454
+ vellum/workflows/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1455
+ vellum/workflows/nodes/tests/test_mocks.py,sha256=mfPvrs75PKcsNsbJLQAN6PDFoVqs9TmQxpdyFKDdO60,7837
1454
1456
  vellum/workflows/nodes/utils.py,sha256=uaTPGYp4utenz_QDghqQ23Q1iCsGHQ40nNZh1g9H9WI,7117
1455
1457
  vellum/workflows/outputs/__init__.py,sha256=AyZ4pRh_ACQIGvkf0byJO46EDnSix1ZCAXfvh-ms1QE,94
1456
- vellum/workflows/outputs/base.py,sha256=W5KL9FaWfSbZuF7lOQ677giHO839Do-MXmuzkDuzPqk,8607
1458
+ vellum/workflows/outputs/base.py,sha256=b4Dnha1miKu3uFJYptKKflIHNuajPF2BNKy0BTt8Tjc,8622
1457
1459
  vellum/workflows/ports/__init__.py,sha256=bZuMt-R7z5bKwpu4uPW7LlJeePOQWmCcDSXe5frUY5g,101
1458
1460
  vellum/workflows/ports/node_ports.py,sha256=g4A-8iUAvEJSkaWppbvzAR8XU02R9U-qLN4rP2Kq4Aw,2743
1459
1461
  vellum/workflows/ports/port.py,sha256=eI2SOZPZ5rsC3jMsxW6Rbn0NpaYQsrR7AapiIbbiy8Q,3635
@@ -1509,8 +1511,8 @@ vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnad
1509
1511
  vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1510
1512
  vellum/workflows/workflows/tests/test_base_workflow.py,sha256=NRteiICyJvDM5zrtUfq2fZoXcGQVaWC9xmNlLLVW0cU,7979
1511
1513
  vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
1512
- vellum_ai-0.14.12.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1513
- vellum_ai-0.14.12.dist-info/METADATA,sha256=1EKavjMZSN9SKB-BwKDVMb4xfc-TnvoUWiYD7JMCqLk,5408
1514
- vellum_ai-0.14.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1515
- vellum_ai-0.14.12.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1516
- vellum_ai-0.14.12.dist-info/RECORD,,
1514
+ vellum_ai-0.14.13.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1515
+ vellum_ai-0.14.13.dist-info/METADATA,sha256=y44fYKH3DaYDbx8hhtZFAtWbGESUBWDUV-2W_JL-XiI,5408
1516
+ vellum_ai-0.14.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1517
+ vellum_ai-0.14.13.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1518
+ vellum_ai-0.14.13.dist-info/RECORD,,
@@ -59,11 +59,27 @@ _NodeDisplayAttrType = TypeVar("_NodeDisplayAttrType")
59
59
  class BaseNodeDisplayMeta(type):
60
60
  def __new__(mcs, name: str, bases: Tuple[Type, ...], dct: Dict[str, Any]) -> Any:
61
61
  cls = super().__new__(mcs, name, bases, dct)
62
- if isinstance(dct.get("node_id"), UUID):
62
+ base_node_display_class = cast(Type["BaseNodeDisplay"], cls)
63
+ node_class = base_node_display_class.infer_node_class()
64
+ if not issubclass(node_class, BaseNode):
65
+ return cls
66
+
67
+ display_node_id = dct.get("node_id")
68
+ if isinstance(display_node_id, UUID):
63
69
  # Display classes are able to override the id of the node class it's parameterized by
64
- base_node_display_class = cast(Type["BaseNodeDisplay"], cls)
65
- node_class = base_node_display_class.infer_node_class()
66
- node_class.__id__ = dct["node_id"]
70
+ node_class.__id__ = display_node_id
71
+
72
+ output_display = dct.get("output_display")
73
+ if isinstance(output_display, dict):
74
+ # And the node class' output ids
75
+ for reference, node_output_display in output_display.items():
76
+ if not isinstance(reference, OutputReference):
77
+ continue
78
+ if not isinstance(node_output_display, NodeOutputDisplay):
79
+ continue
80
+
81
+ node_class.__output_ids__[reference.name] = node_output_display.id
82
+
67
83
  return cls
68
84
 
69
85
 
@@ -2,6 +2,7 @@ import types
2
2
  from typing import TYPE_CHECKING, Optional, Type
3
3
 
4
4
  from vellum.workflows.types.generics import NodeType
5
+ from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from vellum_ee.workflows.display.types import NodeDisplayType
@@ -29,4 +30,12 @@ def get_node_display_class(
29
30
  f"{node_class.__name__}Display",
30
31
  bases=(NodeDisplayBaseClass,),
31
32
  )
33
+ output_display = {
34
+ ref: NodeOutputDisplay(id=node_class.__output_ids__[ref.name], name=ref.name)
35
+ for ref in node_class.Outputs
36
+ if ref.name in node_class.__output_ids__
37
+ }
38
+ if output_display:
39
+ setattr(NodeDisplayClass, "output_display", output_display)
40
+
32
41
  return NodeDisplayClass
@@ -115,7 +115,8 @@ def test_serialize_node__retry(serialize_node):
115
115
  )
116
116
 
117
117
 
118
- def test_serialize_node__retry__no_display(): # GIVEN an adornment node
118
+ def test_serialize_node__retry__no_display():
119
+ # GIVEN an adornment node
119
120
  @RetryNode.wrap(max_attempts=5)
120
121
  class StartNode(BaseNode):
121
122
  pass
@@ -212,6 +213,28 @@ def test_serialize_node__try(serialize_node):
212
213
  )
213
214
 
214
215
 
216
+ def test_serialize_node__try__no_display():
217
+ # GIVEN an adornment node
218
+ @TryNode.wrap()
219
+ class StartNode(BaseNode):
220
+ pass
221
+
222
+ # AND a workflow that uses the adornment node
223
+ class MyWorkflow(BaseWorkflow):
224
+ graph = StartNode
225
+
226
+ # WHEN we serialize the workflow
227
+ workflow_display = get_workflow_display(
228
+ base_display_class=VellumWorkflowDisplay,
229
+ workflow_class=MyWorkflow,
230
+ )
231
+
232
+ exec_config = workflow_display.serialize()
233
+
234
+ # THEN the workflow display is created successfully
235
+ assert exec_config is not None
236
+
237
+
215
238
  def test_serialize_node__stacked():
216
239
  @TryNode.wrap()
217
240
  @RetryNode.wrap(max_attempts=5)
@@ -411,7 +411,7 @@ class BaseWorkflowDisplay(
411
411
  input_display = {}
412
412
  if isinstance(current_node_display, BaseNodeVellumDisplay):
413
413
  input_display = current_node_display.node_input_ids_by_name
414
- node_display_meta = {
414
+ output_display = {
415
415
  output.name: current_node_display.output_display[output].id
416
416
  for output in current_node_display.output_display
417
417
  }
@@ -434,7 +434,7 @@ class BaseWorkflowDisplay(
434
434
 
435
435
  node_event_displays[node_id] = NodeEventDisplayContext(
436
436
  input_display=input_display,
437
- output_display=node_display_meta,
437
+ output_display=output_display,
438
438
  port_display=port_display_meta,
439
439
  subworkflow_display=subworkflow_display_context,
440
440
  )
@@ -95,6 +95,26 @@ def test_serialize_workflow__node_display_class_not_registered():
95
95
  assert data is not None
96
96
 
97
97
 
98
+ def test_get_event_display_context__node_display_filled_without_base_display():
99
+ # GIVEN a simple workflow
100
+ class StartNode(BaseNode):
101
+ class Outputs(BaseNode.Outputs):
102
+ foo: str
103
+
104
+ class MyWorkflow(BaseWorkflow):
105
+ graph = StartNode
106
+
107
+ # WHEN we gather the event display context
108
+ display_context = VellumWorkflowDisplay(MyWorkflow).get_event_display_context()
109
+
110
+ # THEN the node display should be included
111
+ assert str(StartNode.__id__) in display_context.node_displays
112
+ node_event_display = display_context.node_displays[str(StartNode.__id__)]
113
+
114
+ # AND so should their output ids
115
+ assert StartNode.__output_ids__ == node_event_display.output_display
116
+
117
+
98
118
  def test_get_event_display_context__node_display_to_include_subworkflow_display():
99
119
  # GIVEN a simple workflow
100
120
  class InnerNode(BaseNode):