vellum-ai 1.1.5__py3-none-any.whl → 1.2.1__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 (61) hide show
  1. vellum/__init__.py +18 -1
  2. vellum/client/__init__.py +3 -0
  3. vellum/client/core/client_wrapper.py +2 -2
  4. vellum/client/errors/__init__.py +10 -1
  5. vellum/client/errors/too_many_requests_error.py +11 -0
  6. vellum/client/errors/unauthorized_error.py +11 -0
  7. vellum/client/reference.md +94 -0
  8. vellum/client/resources/__init__.py +2 -0
  9. vellum/client/resources/events/__init__.py +4 -0
  10. vellum/client/resources/events/client.py +165 -0
  11. vellum/client/resources/events/raw_client.py +207 -0
  12. vellum/client/types/__init__.py +6 -0
  13. vellum/client/types/error_detail_response.py +22 -0
  14. vellum/client/types/event_create_response.py +26 -0
  15. vellum/client/types/execution_thinking_vellum_value.py +1 -1
  16. vellum/client/types/thinking_vellum_value.py +1 -1
  17. vellum/client/types/thinking_vellum_value_request.py +1 -1
  18. vellum/client/types/workflow_event.py +33 -0
  19. vellum/errors/too_many_requests_error.py +3 -0
  20. vellum/errors/unauthorized_error.py +3 -0
  21. vellum/resources/events/__init__.py +3 -0
  22. vellum/resources/events/client.py +3 -0
  23. vellum/resources/events/raw_client.py +3 -0
  24. vellum/types/error_detail_response.py +3 -0
  25. vellum/types/event_create_response.py +3 -0
  26. vellum/types/workflow_event.py +3 -0
  27. vellum/workflows/nodes/displayable/bases/api_node/node.py +4 -0
  28. vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +26 -0
  29. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +6 -1
  30. vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +22 -0
  31. vellum/workflows/sandbox.py +28 -8
  32. vellum/workflows/state/encoder.py +19 -1
  33. vellum/workflows/utils/hmac.py +44 -0
  34. {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/METADATA +1 -1
  35. {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/RECORD +61 -43
  36. vellum_ee/workflows/display/nodes/base_node_display.py +2 -2
  37. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +37 -7
  38. vellum_ee/workflows/display/nodes/vellum/retry_node.py +1 -1
  39. vellum_ee/workflows/display/nodes/vellum/tests/test_retry_node.py +1 -1
  40. vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +314 -2
  41. vellum_ee/workflows/display/nodes/vellum/try_node.py +1 -1
  42. vellum_ee/workflows/display/tests/test_base_workflow_display.py +53 -1
  43. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +9 -9
  44. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +9 -9
  45. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py +3 -3
  46. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py +14 -15
  47. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py +58 -3
  48. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +1 -1
  49. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +4 -0
  50. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +1 -1
  51. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +2 -2
  52. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
  53. vellum_ee/workflows/display/utils/expressions.py +9 -1
  54. vellum_ee/workflows/display/utils/registry.py +46 -0
  55. vellum_ee/workflows/display/workflows/base_workflow_display.py +21 -1
  56. vellum_ee/workflows/tests/test_registry.py +169 -0
  57. vellum_ee/workflows/tests/test_serialize_module.py +31 -0
  58. vellum_ee/workflows/tests/test_server.py +72 -0
  59. {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/LICENSE +0 -0
  60. {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/WHEEL +0 -0
  61. {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/entry_points.txt +0 -0
@@ -20,11 +20,10 @@ def test_serialize_node__basic(serialize_node):
20
20
  pass
21
21
 
22
22
  serialized_node = serialize_node(BasicGenericNode)
23
-
24
23
  assert not DeepDiff(
25
24
  {
26
25
  "id": "8d7cbfe4-72ca-4367-a401-8d28723d2f00",
27
- "label": "test_serialize_node__basic.<locals>.BasicGenericNode",
26
+ "label": "Basic Generic Node",
28
27
  "type": "GENERIC",
29
28
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
30
29
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -70,7 +69,7 @@ def test_serialize_node__if(serialize_node):
70
69
  assert not DeepDiff(
71
70
  {
72
71
  "id": "bba4b15a-dea0-48c9-a79b-4e12e99db00f",
73
- "label": "test_serialize_node__if.<locals>.IfGenericNode",
72
+ "label": "If Generic Node",
74
73
  "type": "GENERIC",
75
74
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
76
75
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -132,7 +131,7 @@ def test_serialize_node__if_else(serialize_node):
132
131
  assert not DeepDiff(
133
132
  {
134
133
  "id": "25c9c3f1-4014-47ac-90cf-5216de10d05c",
135
- "label": "test_serialize_node__if_else.<locals>.IfElseGenericNode",
134
+ "label": "If Else Generic Node",
136
135
  "type": "GENERIC",
137
136
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
138
137
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -201,7 +200,7 @@ def test_serialize_node__if_elif_else(serialize_node):
201
200
  assert not DeepDiff(
202
201
  {
203
202
  "id": "7b2b9cfc-12aa-432c-940d-cbe53e71de9c",
204
- "label": "test_serialize_node__if_elif_else.<locals>.IfElifElseGenericNode",
203
+ "label": "If Elif Else Generic Node",
205
204
  "type": "GENERIC",
206
205
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
207
206
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -300,7 +299,7 @@ def test_serialize_node__node_output_reference(serialize_node):
300
299
  assert not DeepDiff(
301
300
  {
302
301
  "id": "ac067acc-6a6f-44b1-ae84-428e965ce691",
303
- "label": "test_serialize_node__node_output_reference.<locals>.GenericNodeReferencingOutput",
302
+ "label": "Generic Node Referencing Output",
304
303
  "type": "GENERIC",
305
304
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
306
305
  "definition": {
@@ -363,7 +362,7 @@ def test_serialize_node__vellum_secret_reference(serialize_node):
363
362
  assert not DeepDiff(
364
363
  {
365
364
  "id": "feb4b331-e25f-4a5c-9840-c5575b1efd5c",
366
- "label": "test_serialize_node__vellum_secret_reference.<locals>.GenericNodeReferencingSecret",
365
+ "label": "Generic Node Referencing Secret",
367
366
  "type": "GENERIC",
368
367
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
369
368
  "definition": {
@@ -429,7 +428,7 @@ def test_serialize_node__execution_count_reference(serialize_node):
429
428
  assert not DeepDiff(
430
429
  {
431
430
  "id": "0b4fe8a6-6d0c-464e-9372-10110e2b0e13",
432
- "label": "test_serialize_node__execution_count_reference.<locals>.GenericNodeReferencingExecutions",
431
+ "label": "Generic Node Referencing Executions",
433
432
  "type": "GENERIC",
434
433
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
435
434
  "definition": {
@@ -490,7 +489,7 @@ def test_serialize_node__null(serialize_node):
490
489
  assert not DeepDiff(
491
490
  {
492
491
  "id": "1838ce1f-9c07-4fd0-9fd4-2a3a841ea402",
493
- "label": "test_serialize_node__null.<locals>.NullGenericNode",
492
+ "label": "Null Generic Node",
494
493
  "type": "GENERIC",
495
494
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
496
495
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -548,7 +547,7 @@ def test_serialize_node__between(serialize_node):
548
547
  assert not DeepDiff(
549
548
  {
550
549
  "id": "f2f5a1f2-a12d-4ce0-bfe9-42190ffe5328",
551
- "label": "test_serialize_node__between.<locals>.BetweenGenericNode",
550
+ "label": "Between Generic Node",
552
551
  "type": "GENERIC",
553
552
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
554
553
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -617,7 +616,7 @@ def test_serialize_node__or(serialize_node):
617
616
  assert not DeepDiff(
618
617
  {
619
618
  "id": "5386abad-3378-4378-b3a8-831b4b77dc23",
620
- "label": "test_serialize_node__or.<locals>.OrGenericNode",
619
+ "label": "Or Generic Node",
621
620
  "type": "GENERIC",
622
621
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
623
622
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -700,7 +699,7 @@ def test_serialize_node__and_then_or(serialize_node):
700
699
  assert not DeepDiff(
701
700
  {
702
701
  "id": "4d3995b1-437b-48d9-8878-9f57a8b725f1",
703
- "label": "test_serialize_node__and_then_or.<locals>.AndThenOrGenericNode",
702
+ "label": "And Then Or Generic Node",
704
703
  "type": "GENERIC",
705
704
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
706
705
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -802,7 +801,7 @@ def test_serialize_node__parenthesized_and_then_or(serialize_node):
802
801
  assert not DeepDiff(
803
802
  {
804
803
  "id": "223864c9-0088-4c05-9b7d-e5b1c9ec936d",
805
- "label": "test_serialize_node__parenthesized_and_then_or.<locals>.ParenthesizedAndThenOrGenericNode",
804
+ "label": "Parenthesized And Then Or Generic Node",
806
805
  "type": "GENERIC",
807
806
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
808
807
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -904,7 +903,7 @@ def test_serialize_node__or_then_and(serialize_node):
904
903
  assert not DeepDiff(
905
904
  {
906
905
  "id": "a946342e-4ede-4e96-8e3d-f396748d9f7c",
907
- "label": "test_serialize_node__or_then_and.<locals>.OrThenAndGenericNode",
906
+ "label": "Or Then And Generic Node",
908
907
  "type": "GENERIC",
909
908
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
910
909
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -1005,7 +1004,7 @@ def test_serialize_node__parse_json(serialize_node):
1005
1004
  assert not DeepDiff(
1006
1005
  {
1007
1006
  "id": "bfc3f81b-242a-4f43-9e1c-648223d77768",
1008
- "label": "test_serialize_node__parse_json.<locals>.ParseJsonGenericNode",
1007
+ "label": "Parse Json Generic Node",
1009
1008
  "type": "GENERIC",
1010
1009
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
1011
1010
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -2,7 +2,11 @@ from deepdiff import DeepDiff
2
2
 
3
3
  from vellum.workflows.inputs.base import BaseInputs
4
4
  from vellum.workflows.nodes.bases.base import BaseNode
5
+ from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
6
+ from vellum.workflows.state.base import BaseState
5
7
  from vellum.workflows.types.core import MergeBehavior
8
+ from vellum.workflows.workflows.base import BaseWorkflow
9
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
6
10
 
7
11
 
8
12
  class Inputs(BaseInputs):
@@ -17,7 +21,7 @@ def test_serialize_node__basic(serialize_node):
17
21
  assert not DeepDiff(
18
22
  {
19
23
  "id": "8d7cbfe4-72ca-4367-a401-8d28723d2f00",
20
- "label": "test_serialize_node__basic.<locals>.BasicGenericNode",
24
+ "label": "Basic Generic Node",
21
25
  "type": "GENERIC",
22
26
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
23
27
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -59,7 +63,7 @@ def test_serialize_node__await_any(serialize_node):
59
63
  assert not DeepDiff(
60
64
  {
61
65
  "id": "42e17f0e-8496-415f-9c72-f85250ba6f0b",
62
- "label": "test_serialize_node__await_any.<locals>.AwaitAnyGenericNode",
66
+ "label": "Await Any Generic Node",
63
67
  "type": "GENERIC",
64
68
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
65
69
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -101,7 +105,7 @@ def test_serialize_node__await_all(serialize_node):
101
105
  assert not DeepDiff(
102
106
  {
103
107
  "id": "b3e1145a-5f41-456b-9382-6d0a1e828c2f",
104
- "label": "test_serialize_node__await_all.<locals>.AwaitAllGenericNode",
108
+ "label": "Await All Generic Node",
105
109
  "type": "GENERIC",
106
110
  "display_data": {"position": {"x": 0.0, "y": 0.0}},
107
111
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -132,3 +136,54 @@ def test_serialize_node__await_all(serialize_node):
132
136
  serialized_node,
133
137
  ignore_order=True,
134
138
  )
139
+
140
+
141
+ def test_serialize_node__inline_prompt_await_all():
142
+ """
143
+ Tests that InlinePromptNode with AWAIT_ALL merge behavior can be defined and serializes without errors.
144
+ """
145
+
146
+ # GIVEN an InlinePromptNode with AWAIT_ALL merge behavior
147
+ class AwaitAllInlinePromptNode(InlinePromptNode):
148
+ ml_model = "gpt-4o"
149
+ blocks = []
150
+
151
+ class Trigger(InlinePromptNode.Trigger):
152
+ merge_behavior = MergeBehavior.AWAIT_ALL
153
+
154
+ class TestWorkflow(BaseWorkflow[Inputs, BaseState]):
155
+ graph = AwaitAllInlinePromptNode
156
+
157
+ # WHEN we serialize the workflow containing the node
158
+ workflow_display = get_workflow_display(workflow_class=TestWorkflow)
159
+ serialized = workflow_display.serialize()
160
+
161
+ # THEN the workflow should serialize successfully
162
+ assert "workflow_raw_data" in serialized # type: ignore
163
+ assert "nodes" in serialized["workflow_raw_data"] # type: ignore
164
+
165
+ # AND the workflow should contain the InlinePromptNode
166
+ nodes = serialized["workflow_raw_data"]["nodes"] # type: ignore
167
+ prompt_nodes = [node for node in nodes if node["type"] == "PROMPT"] # type: ignore
168
+ assert len(prompt_nodes) == 1
169
+
170
+ prompt_node = prompt_nodes[0]
171
+
172
+ # AND the node should have the correct type and base
173
+ assert prompt_node["type"] == "PROMPT" # type: ignore
174
+ assert prompt_node["base"]["name"] == "InlinePromptNode" # type: ignore
175
+ assert prompt_node["base"]["module"] == [ # type: ignore
176
+ "vellum",
177
+ "workflows",
178
+ "nodes",
179
+ "displayable",
180
+ "inline_prompt_node",
181
+ "node",
182
+ ]
183
+
184
+ # AND the node should have the expected structure (InlinePromptNode doesn't serialize trigger info)
185
+ assert "data" in prompt_node # type: ignore
186
+ assert "ml_model_name" in prompt_node["data"] # type: ignore
187
+ assert prompt_node["data"]["ml_model_name"] == "gpt-4o" # type: ignore
188
+
189
+ assert prompt_node["trigger"]["merge_behavior"] == "AWAIT_ALL" # type: ignore
@@ -591,7 +591,7 @@ def test_serialize_workflow__try_wrapped():
591
591
  "adornments": [
592
592
  {
593
593
  "id": "3344083c-a32c-4a32-920b-0fb5093448fa",
594
- "label": "TryNode",
594
+ "label": "Try Node",
595
595
  "base": {"name": "TryNode", "module": ["vellum", "workflows", "nodes", "core", "try_node", "node"]},
596
596
  "attributes": [
597
597
  {
@@ -165,6 +165,10 @@ def test_serialize_workflow():
165
165
  "name": "ExampleBaseInlinePromptNodeWithFunctions",
166
166
  "module": ["tests", "workflows", "basic_inline_prompt_node_with_functions", "workflow"],
167
167
  },
168
+ "trigger": {
169
+ "id": "c2dccecb-8a41-40a8-95af-325d3ab8bfe5",
170
+ "merge_behavior": "AWAIT_ANY",
171
+ },
168
172
  "outputs": [
169
173
  {"id": "9557bd86-702d-4b45-b8c1-c3980bffe28f", "name": "json", "type": "JSON", "value": None},
170
174
  {"id": "ead0ccb5-092f-4d9b-a9ec-5eb83d498188", "name": "text", "type": "STRING", "value": None},
@@ -129,7 +129,7 @@ def test_serialize_workflow():
129
129
  },
130
130
  {
131
131
  "id": "1381c078-efa2-4255-89a1-7b4cb742c7fc",
132
- "label": "StartNode",
132
+ "label": "Start Node",
133
133
  "type": "GENERIC",
134
134
  "display_data": {"position": {"x": 200.0, "y": -50.0}},
135
135
  "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
@@ -40,7 +40,7 @@ def test_serialize_workflow():
40
40
  tool_calling_node = workflow_raw_data["nodes"][1]
41
41
  assert tool_calling_node == {
42
42
  "id": "21f29cac-da87-495f-bba1-093d423f4e46",
43
- "label": "GetCurrentWeatherNode",
43
+ "label": "Get Current Weather Node",
44
44
  "type": "GENERIC",
45
45
  "display_data": {
46
46
  "position": {"x": 200.0, "y": -50.0},
@@ -153,7 +153,7 @@ def test_serialize_workflow():
153
153
  },
154
154
  {
155
155
  "id": "1381c078-efa2-4255-89a1-7b4cb742c7fc",
156
- "label": "StartNode",
156
+ "label": "Start Node",
157
157
  "type": "GENERIC",
158
158
  "display_data": {"position": {"x": 200.0, "y": -50.0}},
159
159
  "base": {
@@ -40,7 +40,7 @@ def test_serialize_workflow():
40
40
  tool_calling_node = workflow_raw_data["nodes"][1]
41
41
  assert tool_calling_node == {
42
42
  "id": "21f29cac-da87-495f-bba1-093d423f4e46",
43
- "label": "GetCurrentWeatherNode",
43
+ "label": "Get Current Weather Node",
44
44
  "type": "GENERIC",
45
45
  "display_data": {
46
46
  "position": {"x": 200.0, "y": -50.0},
@@ -11,6 +11,7 @@ from vellum.workflows.expressions.and_ import AndExpression
11
11
  from vellum.workflows.expressions.begins_with import BeginsWithExpression
12
12
  from vellum.workflows.expressions.between import BetweenExpression
13
13
  from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
14
+ from vellum.workflows.expressions.concat import ConcatExpression
14
15
  from vellum.workflows.expressions.contains import ContainsExpression
15
16
  from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
16
17
  from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
@@ -105,6 +106,8 @@ def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperato
105
106
  return "+"
106
107
  elif isinstance(descriptor, MinusExpression):
107
108
  return "-"
109
+ elif isinstance(descriptor, ConcatExpression):
110
+ return "concat"
108
111
  else:
109
112
  raise ValueError(f"Unsupported descriptor type: {descriptor}")
110
113
 
@@ -171,6 +174,7 @@ def _serialize_condition(display_context: "WorkflowDisplayContext", condition: B
171
174
  AndExpression,
172
175
  BeginsWithExpression,
173
176
  CoalesceExpression,
177
+ ConcatExpression,
174
178
  ContainsExpression,
175
179
  DoesNotBeginWithExpression,
176
180
  DoesNotContainExpression,
@@ -349,7 +353,11 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
349
353
  if is_workflow_class(value):
350
354
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
351
355
 
352
- workflow_display = get_workflow_display(workflow_class=value)
356
+ # Pass the parent display context so the subworkflow can resolve parent workflow inputs
357
+ workflow_display = get_workflow_display(
358
+ workflow_class=value,
359
+ parent_display_context=display_context,
360
+ )
353
361
  serialized_value: dict = workflow_display.serialize()
354
362
  name = serialized_value["workflow_raw_data"]["definition"]["name"]
355
363
  description = value.__doc__ or ""
@@ -1,10 +1,14 @@
1
+ from uuid import UUID
1
2
  from typing import TYPE_CHECKING, Dict, Optional, Type
2
3
 
4
+ from vellum.workflows.events.types import BaseEvent
3
5
  from vellum.workflows.nodes import BaseNode
4
6
  from vellum.workflows.workflows.base import BaseWorkflow
5
7
 
6
8
  if TYPE_CHECKING:
9
+ from vellum.workflows.events.types import ParentContext
7
10
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
11
+ from vellum_ee.workflows.display.types import WorkflowDisplayContext
8
12
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
9
13
 
10
14
 
@@ -14,6 +18,9 @@ _workflow_display_registry: Dict[Type[BaseWorkflow], Type["BaseWorkflowDisplay"]
14
18
  # Used to store the mapping between node types and their display classes
15
19
  _node_display_registry: Dict[Type[BaseNode], Type["BaseNodeDisplay"]] = {}
16
20
 
21
+ # Registry to store active workflow display contexts by span ID for nested workflow inheritance
22
+ _active_workflow_display_contexts: Dict[UUID, "WorkflowDisplayContext"] = {}
23
+
17
24
 
18
25
  def get_from_workflow_display_registry(workflow_class: Type[BaseWorkflow]) -> Optional[Type["BaseWorkflowDisplay"]]:
19
26
  return _workflow_display_registry.get(workflow_class)
@@ -35,3 +42,42 @@ def get_from_node_display_registry(node_class: Type[BaseNode]) -> Optional[Type[
35
42
 
36
43
  def register_node_display_class(node_class: Type[BaseNode], node_display_class: Type["BaseNodeDisplay"]) -> None:
37
44
  _node_display_registry[node_class] = node_display_class
45
+
46
+
47
+ def register_workflow_display_context(span_id: UUID, display_context: "WorkflowDisplayContext") -> None:
48
+ """Register a workflow display context by span ID for nested workflow inheritance."""
49
+ _active_workflow_display_contexts[span_id] = display_context
50
+
51
+
52
+ def _get_parent_display_context_for_span(span_id: UUID) -> Optional["WorkflowDisplayContext"]:
53
+ """Get the parent display context for a given span ID."""
54
+ return _active_workflow_display_contexts.get(span_id)
55
+
56
+
57
+ def get_parent_display_context_from_event(event: BaseEvent) -> Optional["WorkflowDisplayContext"]:
58
+ """Extract parent display context from an event by traversing the parent chain.
59
+
60
+ This function traverses up the parent chain starting from the event's parent,
61
+ looking for workflow parents and attempting to get their display context.
62
+
63
+ Args:
64
+ event: The event to extract parent display context from
65
+
66
+ Returns:
67
+ The parent workflow display context if found, None otherwise
68
+ """
69
+ if not event.parent:
70
+ return None
71
+
72
+ current_parent: Optional["ParentContext"] = event.parent
73
+ while current_parent:
74
+ if current_parent.type == "WORKFLOW":
75
+ # Found a parent workflow, try to get its display context
76
+ parent_span_id = current_parent.span_id
77
+ parent_display_context = _get_parent_display_context_for_span(parent_span_id)
78
+ if parent_display_context:
79
+ return parent_display_context
80
+ # Move up the parent chain
81
+ current_parent = current_parent.parent
82
+
83
+ return None
@@ -3,6 +3,7 @@ import fnmatch
3
3
  from functools import cached_property
4
4
  import importlib
5
5
  import inspect
6
+ import json
6
7
  import logging
7
8
  import os
8
9
  from uuid import UUID
@@ -15,12 +16,14 @@ from vellum.workflows.constants import undefined
15
16
  from vellum.workflows.descriptors.base import BaseDescriptor
16
17
  from vellum.workflows.edges import Edge
17
18
  from vellum.workflows.events.workflow import NodeEventDisplayContext, WorkflowEventDisplayContext
19
+ from vellum.workflows.inputs.base import BaseInputs
18
20
  from vellum.workflows.nodes.bases import BaseNode
19
21
  from vellum.workflows.nodes.displayable.bases.utils import primitive_to_vellum_value
20
22
  from vellum.workflows.nodes.displayable.final_output_node.node import FinalOutputNode
21
23
  from vellum.workflows.nodes.utils import get_unadorned_node, get_unadorned_port, get_wrapped_node
22
24
  from vellum.workflows.ports import Port
23
25
  from vellum.workflows.references import OutputReference, WorkflowInputReference
26
+ from vellum.workflows.state.encoder import DefaultStateEncoder
24
27
  from vellum.workflows.types.core import Json, JsonArray, JsonObject
25
28
  from vellum.workflows.types.generics import WorkflowType
26
29
  from vellum.workflows.types.utils import get_original_base
@@ -72,6 +75,7 @@ IGNORE_PATTERNS = [
72
75
  class WorkflowSerializationResult(UniversalBaseModel):
73
76
  exec_config: Dict[str, Any]
74
77
  errors: List[str]
78
+ dataset: Optional[List[Dict[str, Any]]] = None
75
79
 
76
80
 
77
81
  class BaseWorkflowDisplay(Generic[WorkflowType]):
@@ -524,7 +528,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
524
528
  workflow_input_displays: WorkflowInputsDisplays = {}
525
529
  # If we're dealing with a nested workflow, then it should have access to the inputs of its parents.
526
530
  global_workflow_input_displays = (
527
- copy(self._parent_display_context.workflow_input_displays) if self._parent_display_context else {}
531
+ copy(self._parent_display_context.global_workflow_input_displays) if self._parent_display_context else {}
528
532
  )
529
533
  for workflow_input in self._workflow.get_inputs_class():
530
534
  workflow_input_display_overrides = self.inputs_display.get(workflow_input)
@@ -892,9 +896,25 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
892
896
  if additional_files:
893
897
  exec_config["module_data"] = {"additional_files": cast(JsonObject, additional_files)}
894
898
 
899
+ dataset = None
900
+ try:
901
+ sandbox_module_path = f"{module}.sandbox"
902
+ sandbox_module = importlib.import_module(sandbox_module_path)
903
+ if hasattr(sandbox_module, "dataset"):
904
+ dataset_attr = getattr(sandbox_module, "dataset")
905
+ if dataset_attr and isinstance(dataset_attr, list):
906
+ dataset = []
907
+ for i, inputs_obj in enumerate(dataset_attr):
908
+ if isinstance(inputs_obj, BaseInputs):
909
+ serialized_inputs = json.loads(json.dumps(inputs_obj, cls=DefaultStateEncoder))
910
+ dataset.append({"label": f"Scenario {i + 1}", "inputs": serialized_inputs})
911
+ except (ImportError, AttributeError):
912
+ pass
913
+
895
914
  return WorkflowSerializationResult(
896
915
  exec_config=exec_config,
897
916
  errors=[str(error) for error in workflow_display.display_context.errors],
917
+ dataset=dataset,
898
918
  )
899
919
 
900
920
  def _gather_additional_module_files(self, module_path: str) -> Dict[str, str]:
@@ -0,0 +1,169 @@
1
+ from datetime import datetime, timezone
2
+ from uuid import uuid4
3
+
4
+ from vellum.workflows.events.types import NodeParentContext, WorkflowParentContext
5
+ from vellum.workflows.events.workflow import WorkflowExecutionInitiatedBody, WorkflowExecutionInitiatedEvent
6
+ from vellum.workflows.inputs.base import BaseInputs
7
+ from vellum.workflows.nodes import BaseNode
8
+ from vellum.workflows.state.base import BaseState
9
+ from vellum.workflows.workflows.base import BaseWorkflow
10
+ from vellum_ee.workflows.display.utils.registry import (
11
+ get_parent_display_context_from_event,
12
+ register_workflow_display_context,
13
+ )
14
+
15
+
16
+ class MockInputs(BaseInputs):
17
+ pass
18
+
19
+
20
+ class MockState(BaseState):
21
+ pass
22
+
23
+
24
+ class MockNode(BaseNode):
25
+ pass
26
+
27
+
28
+ class MockWorkflow(BaseWorkflow[MockInputs, MockState]):
29
+ pass
30
+
31
+
32
+ class MockWorkflowDisplayContext:
33
+ pass
34
+
35
+
36
+ def test_get_parent_display_context_from_event__no_parent():
37
+ """Test event with no parent returns None"""
38
+ # GIVEN a workflow execution initiated event with no parent
39
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
40
+ id=uuid4(),
41
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
42
+ trace_id=uuid4(),
43
+ span_id=uuid4(),
44
+ body=WorkflowExecutionInitiatedBody(
45
+ workflow_definition=MockWorkflow,
46
+ inputs=MockInputs(),
47
+ ),
48
+ parent=None, # No parent
49
+ )
50
+
51
+ # WHEN getting parent display context
52
+ result = get_parent_display_context_from_event(event)
53
+
54
+ # THEN it should return None
55
+ assert result is None
56
+
57
+
58
+ def test_get_parent_display_context_from_event__non_workflow_parent():
59
+ """Test event with non-workflow parent continues traversal"""
60
+ # GIVEN an event with a non-workflow parent (NodeParentContext)
61
+ non_workflow_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=None)
62
+
63
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
64
+ id=uuid4(),
65
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
66
+ trace_id=uuid4(),
67
+ span_id=uuid4(),
68
+ body=WorkflowExecutionInitiatedBody(
69
+ workflow_definition=MockWorkflow,
70
+ inputs=MockInputs(),
71
+ ),
72
+ parent=non_workflow_parent,
73
+ )
74
+
75
+ # WHEN getting parent display context
76
+ result = get_parent_display_context_from_event(event)
77
+
78
+ # THEN it should return None (no workflow parent found)
79
+ assert result is None
80
+
81
+
82
+ def test_get_parent_display_context_from_event__nested_workflow_parents():
83
+ """Test event with nested workflow parents traverses correctly"""
84
+ # GIVEN a chain of nested contexts:
85
+ # Event -> WorkflowParent -> NodeParent -> MiddleWorkflowParent -> NodeParent
86
+
87
+ # Top level workflow parent
88
+ top_workflow_span_id = uuid4()
89
+ top_context = MockWorkflowDisplayContext()
90
+ register_workflow_display_context(top_workflow_span_id, top_context) # type: ignore[arg-type]
91
+
92
+ top_workflow_parent = WorkflowParentContext(
93
+ workflow_definition=MockWorkflow, span_id=top_workflow_span_id, parent=None
94
+ )
95
+
96
+ top_node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=top_workflow_parent)
97
+
98
+ # AND middle workflow parent (no display context)
99
+ middle_workflow_span_id = uuid4()
100
+ middle_workflow_parent = WorkflowParentContext(
101
+ workflow_definition=MockWorkflow, span_id=middle_workflow_span_id, parent=top_node_parent
102
+ )
103
+
104
+ # AND node parent between middle workflow and event
105
+ node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=middle_workflow_parent)
106
+
107
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
108
+ id=uuid4(),
109
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
110
+ trace_id=uuid4(),
111
+ span_id=uuid4(),
112
+ body=WorkflowExecutionInitiatedBody(
113
+ workflow_definition=MockWorkflow,
114
+ inputs=MockInputs(),
115
+ ),
116
+ parent=node_parent,
117
+ )
118
+
119
+ # WHEN getting parent display context
120
+ result = get_parent_display_context_from_event(event)
121
+
122
+ # THEN it should find the top-level workflow context
123
+ assert result == top_context
124
+
125
+
126
+ def test_get_parent_display_context_from_event__middle_workflow_has_context():
127
+ """Test event returns middle workflow context when it's the first one with registered context"""
128
+ # GIVEN a chain of nested contexts:
129
+ # Event -> WorkflowParent -> NodeParent -> MiddleWorkflowParent -> NodeParent
130
+
131
+ top_workflow_span_id = uuid4()
132
+ top_context = MockWorkflowDisplayContext()
133
+ register_workflow_display_context(top_workflow_span_id, top_context) # type: ignore[arg-type]
134
+
135
+ top_workflow_parent = WorkflowParentContext(
136
+ workflow_definition=MockWorkflow, span_id=top_workflow_span_id, parent=None
137
+ )
138
+
139
+ # AND node parent between top workflow and middle workflow
140
+ top_node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=top_workflow_parent)
141
+
142
+ # AND middle workflow parent
143
+ middle_workflow_span_id = uuid4()
144
+ middle_context = MockWorkflowDisplayContext()
145
+ register_workflow_display_context(middle_workflow_span_id, middle_context) # type: ignore[arg-type]
146
+
147
+ middle_workflow_parent = WorkflowParentContext(
148
+ workflow_definition=MockWorkflow, span_id=middle_workflow_span_id, parent=top_node_parent
149
+ )
150
+
151
+ node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=middle_workflow_parent)
152
+
153
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
154
+ id=uuid4(),
155
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
156
+ trace_id=uuid4(),
157
+ span_id=uuid4(),
158
+ body=WorkflowExecutionInitiatedBody(
159
+ workflow_definition=MockWorkflow,
160
+ inputs=MockInputs(),
161
+ ),
162
+ parent=node_parent,
163
+ )
164
+
165
+ # WHEN getting parent display context
166
+ result = get_parent_display_context_from_event(event)
167
+
168
+ # THEN it should find the MIDDLE workflow context
169
+ assert result == middle_context
@@ -1,6 +1,37 @@
1
1
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
2
2
 
3
3
 
4
+ def test_serialize_module_with_dataset():
5
+ """Test that serialize_module correctly serializes dataset from sandbox modules."""
6
+ module_path = "tests.workflows.basic_inputs_and_outputs"
7
+
8
+ result = BaseWorkflowDisplay.serialize_module(module_path)
9
+
10
+ assert hasattr(result, "dataset")
11
+
12
+ assert result.dataset is None
13
+
14
+
15
+ def test_serialize_module_with_actual_dataset():
16
+ """Test that serialize_module correctly serializes dataset when sandbox has dataset attribute."""
17
+ module_path = "tests.workflows.test_dataset_serialization"
18
+
19
+ result = BaseWorkflowDisplay.serialize_module(module_path)
20
+
21
+ assert hasattr(result, "dataset")
22
+
23
+ assert result.dataset is not None
24
+ assert isinstance(result.dataset, list)
25
+ assert len(result.dataset) == 2
26
+
27
+ for i, item in enumerate(result.dataset):
28
+ assert "label" in item
29
+ assert "inputs" in item
30
+ assert item["label"] == f"Scenario {i + 1}"
31
+ assert isinstance(item["inputs"], dict)
32
+ assert "message" in item["inputs"]
33
+
34
+
4
35
  def test_serialize_module_happy_path():
5
36
  """Test that serialize_module works with a valid module path."""
6
37
  module_path = "tests.workflows.trivial"