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.
- vellum/__init__.py +18 -1
- vellum/client/__init__.py +3 -0
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/errors/__init__.py +10 -1
- vellum/client/errors/too_many_requests_error.py +11 -0
- vellum/client/errors/unauthorized_error.py +11 -0
- vellum/client/reference.md +94 -0
- vellum/client/resources/__init__.py +2 -0
- vellum/client/resources/events/__init__.py +4 -0
- vellum/client/resources/events/client.py +165 -0
- vellum/client/resources/events/raw_client.py +207 -0
- vellum/client/types/__init__.py +6 -0
- vellum/client/types/error_detail_response.py +22 -0
- vellum/client/types/event_create_response.py +26 -0
- vellum/client/types/execution_thinking_vellum_value.py +1 -1
- vellum/client/types/thinking_vellum_value.py +1 -1
- vellum/client/types/thinking_vellum_value_request.py +1 -1
- vellum/client/types/workflow_event.py +33 -0
- vellum/errors/too_many_requests_error.py +3 -0
- vellum/errors/unauthorized_error.py +3 -0
- vellum/resources/events/__init__.py +3 -0
- vellum/resources/events/client.py +3 -0
- vellum/resources/events/raw_client.py +3 -0
- vellum/types/error_detail_response.py +3 -0
- vellum/types/event_create_response.py +3 -0
- vellum/types/workflow_event.py +3 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +4 -0
- vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +26 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +6 -1
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +22 -0
- vellum/workflows/sandbox.py +28 -8
- vellum/workflows/state/encoder.py +19 -1
- vellum/workflows/utils/hmac.py +44 -0
- {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/METADATA +1 -1
- {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/RECORD +61 -43
- vellum_ee/workflows/display/nodes/base_node_display.py +2 -2
- vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +37 -7
- vellum_ee/workflows/display/nodes/vellum/retry_node.py +1 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_retry_node.py +1 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +314 -2
- vellum_ee/workflows/display/nodes/vellum/try_node.py +1 -1
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +53 -1
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +9 -9
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +9 -9
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py +14 -15
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py +58 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +4 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +2 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
- vellum_ee/workflows/display/utils/expressions.py +9 -1
- vellum_ee/workflows/display/utils/registry.py +46 -0
- vellum_ee/workflows/display/workflows/base_workflow_display.py +21 -1
- vellum_ee/workflows/tests/test_registry.py +169 -0
- vellum_ee/workflows/tests/test_serialize_module.py +31 -0
- vellum_ee/workflows/tests/test_server.py +72 -0
- {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/LICENSE +0 -0
- {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/WHEEL +0 -0
- {vellum_ai-1.1.5.dist-info → vellum_ai-1.2.1.dist-info}/entry_points.txt +0 -0
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py
CHANGED
@@ -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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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"]},
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py
CHANGED
@@ -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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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": "
|
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
|
-
|
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.
|
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"
|