vellum-ai 0.14.47__py3-none-any.whl → 0.14.49__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/client/core/client_wrapper.py +1 -1
- vellum/workflows/nodes/displayable/code_execution_node/node.py +6 -5
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +112 -1
- vellum/workflows/nodes/displayable/code_execution_node/utils.py +23 -2
- vellum/workflows/types/code_execution_node_wrappers.py +10 -2
- {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/METADATA +1 -1
- {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/RECORD +22 -22
- vellum_cli/__init__.py +3 -2
- vellum_cli/image_push.py +15 -3
- vellum_cli/tests/test_image_push.py +109 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +8 -3
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -2
- vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +1 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +43 -0
- vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +31 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +51 -5
- vellum_ee/workflows/display/utils/expressions.py +50 -4
- vellum_ee/workflows/display/utils/vellum.py +10 -4
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +346 -0
- {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/LICENSE +0 -0
- {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/WHEEL +0 -0
- {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.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.
|
21
|
+
"X-Fern-SDK-Version": "0.14.49",
|
22
22
|
}
|
23
23
|
headers["X-API-KEY"] = self.api_key
|
24
24
|
return headers
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import inspect
|
2
|
+
import os
|
2
3
|
from typing import Any, ClassVar, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, cast, get_args
|
3
4
|
|
4
5
|
from vellum import (
|
@@ -95,9 +96,9 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
|
|
95
96
|
|
96
97
|
def run(self) -> Outputs:
|
97
98
|
output_type = self.__class__.get_output_type()
|
98
|
-
code = self._resolve_code()
|
99
|
+
code, filepath = self._resolve_code()
|
99
100
|
if not self.packages and self.runtime == "PYTHON_3_11_6":
|
100
|
-
logs, result = run_code_inline(code, self.code_inputs, output_type)
|
101
|
+
logs, result = run_code_inline(code, self.code_inputs, output_type, filepath)
|
101
102
|
return self.Outputs(result=result, log=logs)
|
102
103
|
|
103
104
|
else:
|
@@ -210,7 +211,7 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
|
|
210
211
|
|
211
212
|
return compiled_inputs
|
212
213
|
|
213
|
-
def _resolve_code(self) -> str:
|
214
|
+
def _resolve_code(self) -> Tuple[str, str]:
|
214
215
|
if self.code and self.filepath:
|
215
216
|
raise NodeException(
|
216
217
|
message="Cannot specify both `code` and `filepath` for a CodeExecutionNode",
|
@@ -218,7 +219,7 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
|
|
218
219
|
)
|
219
220
|
|
220
221
|
if self.code:
|
221
|
-
return self.code
|
222
|
+
return self.code, f"{self.__class__.__name__}.code.py"
|
222
223
|
|
223
224
|
if not self.filepath:
|
224
225
|
raise NodeException(
|
@@ -235,4 +236,4 @@ class CodeExecutionNode(BaseNode[StateType], Generic[StateType, _OutputType], me
|
|
235
236
|
code=WorkflowErrorCode.INVALID_INPUTS,
|
236
237
|
)
|
237
238
|
|
238
|
-
return code
|
239
|
+
return code, os.path.join(os.path.dirname(root), self.filepath)
|
@@ -850,7 +850,16 @@ def main(arg1: list) -> str:
|
|
850
850
|
node.run()
|
851
851
|
|
852
852
|
# AND the result should be the correct output
|
853
|
-
assert
|
853
|
+
assert (
|
854
|
+
exc_info.value.message
|
855
|
+
== """\
|
856
|
+
Traceback (most recent call last):
|
857
|
+
File "ExampleCodeExecutionNode.code.py", line 2, in main
|
858
|
+
return arg1["invalid"]
|
859
|
+
|
860
|
+
AttributeError: dict has no key: 'invalid'
|
861
|
+
"""
|
862
|
+
)
|
854
863
|
|
855
864
|
|
856
865
|
def test_run_node__execute_code__value_key_access():
|
@@ -933,3 +942,105 @@ def main(output: list[str]) -> list[str]:
|
|
933
942
|
|
934
943
|
# THEN the node should successfully access the string value
|
935
944
|
assert outputs == {"result": ['{"foo": "bar"}', '{"foo2": "bar2"}'], "log": ""}
|
945
|
+
|
946
|
+
|
947
|
+
def test_run_node__string_key_access_still_works():
|
948
|
+
# GIVEN a node that accesses the '0' index of a string input
|
949
|
+
class ExampleCodeExecutionNode(CodeExecutionNode[BaseState, Any]):
|
950
|
+
code = """\
|
951
|
+
def main(input: str) -> str:
|
952
|
+
return input[0]
|
953
|
+
"""
|
954
|
+
code_inputs = {"input": "hello"}
|
955
|
+
runtime = "PYTHON_3_11_6"
|
956
|
+
|
957
|
+
# WHEN we run the node
|
958
|
+
node = ExampleCodeExecutionNode()
|
959
|
+
outputs = node.run()
|
960
|
+
|
961
|
+
# THEN the node should successfully access the string value
|
962
|
+
assert outputs == {"result": "h", "log": ""}
|
963
|
+
|
964
|
+
|
965
|
+
def test_run_node__iter_list():
|
966
|
+
# GIVEN a node that will return the first string in a list
|
967
|
+
class ExampleCodeExecutionNode(CodeExecutionNode[BaseState, str]):
|
968
|
+
code = """\
|
969
|
+
def main(
|
970
|
+
input_list: list
|
971
|
+
) -> str:
|
972
|
+
return next((
|
973
|
+
o.value for o in input_list if o.type == "STRING"
|
974
|
+
), None)
|
975
|
+
"""
|
976
|
+
runtime = "PYTHON_3_11_6"
|
977
|
+
code_inputs = {
|
978
|
+
"input_list": [
|
979
|
+
StringVellumValue(value="foo"),
|
980
|
+
NumberVellumValue(value=1),
|
981
|
+
]
|
982
|
+
}
|
983
|
+
|
984
|
+
# WHEN we run the node
|
985
|
+
node = ExampleCodeExecutionNode()
|
986
|
+
outputs = node.run()
|
987
|
+
|
988
|
+
# THEN the node should successfully access the string value
|
989
|
+
assert outputs == {"result": "foo", "log": ""}
|
990
|
+
|
991
|
+
|
992
|
+
def test_run_node__iter_dict():
|
993
|
+
# GIVEN a node that will return the first string in a list
|
994
|
+
class ExampleCodeExecutionNode(CodeExecutionNode[BaseState, list[str]]):
|
995
|
+
code = """\
|
996
|
+
def main(input_dict: dict) -> list[str]:
|
997
|
+
return [item.value for item in input_dict]
|
998
|
+
"""
|
999
|
+
runtime = "PYTHON_3_11_6"
|
1000
|
+
code_inputs = {
|
1001
|
+
"input_dict": {
|
1002
|
+
"foo": "bar",
|
1003
|
+
"baz": "qux",
|
1004
|
+
}
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
# WHEN we run the node
|
1008
|
+
node = ExampleCodeExecutionNode()
|
1009
|
+
outputs = node.run()
|
1010
|
+
|
1011
|
+
# THEN the node should successfully access the string value
|
1012
|
+
assert outputs == {"result": ["bar", "qux"], "log": ""}
|
1013
|
+
|
1014
|
+
|
1015
|
+
def test_run_node__show_clearer_runtime_error_message():
|
1016
|
+
# GIVEN a node that will return the first string in a list
|
1017
|
+
class ExampleCodeExecutionNode(CodeExecutionNode[BaseState, list[str]]):
|
1018
|
+
code = """\
|
1019
|
+
def main(items: list[str]) -> list[str]:
|
1020
|
+
return first(items)
|
1021
|
+
|
1022
|
+
# Helper function to prove out stack traces
|
1023
|
+
def first(items: list[str]) -> str:
|
1024
|
+
return items[0]
|
1025
|
+
"""
|
1026
|
+
runtime = "PYTHON_3_11_6"
|
1027
|
+
code_inputs = {"items": []}
|
1028
|
+
|
1029
|
+
# WHEN we run the node
|
1030
|
+
node = ExampleCodeExecutionNode()
|
1031
|
+
with pytest.raises(NodeException) as exc_info:
|
1032
|
+
node.run()
|
1033
|
+
|
1034
|
+
# THEN the node should successfully access the string value
|
1035
|
+
assert (
|
1036
|
+
exc_info.value.message
|
1037
|
+
== """\
|
1038
|
+
Traceback (most recent call last):
|
1039
|
+
File "ExampleCodeExecutionNode.code.py", line 2, in main
|
1040
|
+
return first(items)
|
1041
|
+
File "ExampleCodeExecutionNode.code.py", line 6, in first
|
1042
|
+
return items[0]
|
1043
|
+
|
1044
|
+
IndexError: list index out of range
|
1045
|
+
"""
|
1046
|
+
)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import io
|
2
2
|
import os
|
3
|
+
import sys
|
4
|
+
import traceback
|
3
5
|
from typing import Any, Optional, Tuple, Union
|
4
6
|
|
5
7
|
from pydantic import BaseModel
|
@@ -40,6 +42,7 @@ def run_code_inline(
|
|
40
42
|
code: str,
|
41
43
|
inputs: EntityInputsInterface,
|
42
44
|
output_type: Any,
|
45
|
+
filepath: str,
|
43
46
|
) -> Tuple[str, Any]:
|
44
47
|
log_buffer = io.StringIO()
|
45
48
|
|
@@ -75,11 +78,29 @@ def run_code_inline(
|
|
75
78
|
__arg__out = main({", ".join(run_args)})
|
76
79
|
"""
|
77
80
|
try:
|
78
|
-
|
81
|
+
compiled_code = compile(execution_code, filepath, "exec")
|
82
|
+
exec(compiled_code, exec_globals)
|
79
83
|
except Exception as e:
|
84
|
+
lines = code.splitlines()
|
85
|
+
_, _, tb = sys.exc_info()
|
86
|
+
tb_generator = traceback.walk_tb(tb)
|
87
|
+
stack = traceback.StackSummary.extract(tb_generator)
|
88
|
+
|
89
|
+
# Filter stack to only include frames from the user's code file, and omit the first one
|
90
|
+
filtered_stack = traceback.StackSummary.from_list([frame for frame in stack if frame.filename == filepath][1:])
|
91
|
+
for frame in filtered_stack:
|
92
|
+
if not frame.line and frame.lineno and frame.lineno <= len(lines):
|
93
|
+
# Mypy doesn't like us setting private attributes
|
94
|
+
frame._line = lines[frame.lineno - 1] # type: ignore[attr-defined]
|
95
|
+
|
96
|
+
error_message = f"""\
|
97
|
+
Traceback (most recent call last):
|
98
|
+
{''.join(filtered_stack.format())}
|
99
|
+
{e.__class__.__name__}: {e}
|
100
|
+
"""
|
80
101
|
raise NodeException(
|
81
102
|
code=WorkflowErrorCode.INVALID_CODE,
|
82
|
-
message=
|
103
|
+
message=error_message,
|
83
104
|
)
|
84
105
|
|
85
106
|
logs = log_buffer.getvalue()
|
@@ -2,7 +2,7 @@ class StringValueWrapper(str):
|
|
2
2
|
def __getitem__(self, key):
|
3
3
|
if key == "value":
|
4
4
|
return self
|
5
|
-
|
5
|
+
return super().__getitem__(key)
|
6
6
|
|
7
7
|
def __getattr__(self, attr):
|
8
8
|
if attr == "value":
|
@@ -23,6 +23,10 @@ class ListWrapper(list):
|
|
23
23
|
return self
|
24
24
|
raise AttributeError(f"'list' object has no attribute '{attr}'")
|
25
25
|
|
26
|
+
def __iter__(self):
|
27
|
+
for i in range(len(self)):
|
28
|
+
yield self[i]
|
29
|
+
|
26
30
|
|
27
31
|
class DictWrapper(dict):
|
28
32
|
"""
|
@@ -52,11 +56,15 @@ class DictWrapper(dict):
|
|
52
56
|
def __setattr__(self, name, value):
|
53
57
|
self[name] = value
|
54
58
|
|
59
|
+
def __iter__(self):
|
60
|
+
for key in super().keys():
|
61
|
+
yield self[key]
|
62
|
+
|
55
63
|
|
56
64
|
def clean_for_dict_wrapper(obj):
|
57
65
|
if isinstance(obj, dict):
|
58
66
|
wrapped = DictWrapper(obj)
|
59
|
-
for key in wrapped:
|
67
|
+
for key in wrapped.keys():
|
60
68
|
wrapped[key] = clean_for_dict_wrapper(wrapped[key])
|
61
69
|
|
62
70
|
return wrapped
|
@@ -1,9 +1,9 @@
|
|
1
1
|
vellum_cli/CONTRIBUTING.md,sha256=FtDC7BGxSeMnwCXAUssFsAIElXtmJE-O5Z7BpolcgvI,2935
|
2
2
|
vellum_cli/README.md,sha256=2NudRoLzWxNKqnuVy1JuQ7DerIaxWGYkrH8kMd-asIE,90
|
3
|
-
vellum_cli/__init__.py,sha256=
|
3
|
+
vellum_cli/__init__.py,sha256=sCNP_hmFCexEWp1oQdpj8QsIUiAbo4MIlTalZEFruD8,12398
|
4
4
|
vellum_cli/aliased_group.py,sha256=ugW498j0yv4ALJ8vS9MsO7ctDW7Jlir9j6nE_uHAP8c,3363
|
5
5
|
vellum_cli/config.py,sha256=v5BmZ-t_v4Jmqd7KVuQMZF2pRI-rbMspSkVYXIRoTmI,9448
|
6
|
-
vellum_cli/image_push.py,sha256=
|
6
|
+
vellum_cli/image_push.py,sha256=skFXf25ixMOX1yfcyAtii-RivYYv-_hsv-Z-bVB6m5Q,7380
|
7
7
|
vellum_cli/init.py,sha256=WpnMXPItPmh0f0bBGIer3p-e5gu8DUGwSArT_FuoMEw,5093
|
8
8
|
vellum_cli/logger.py,sha256=PuRFa0WCh4sAGFS5aqWB0QIYpS6nBWwPJrIXpWxugV4,1022
|
9
9
|
vellum_cli/ping.py,sha256=p_BCCRjgPhng6JktuECtkDQLbhopt6JpmrtGoLnLJT8,1161
|
@@ -12,7 +12,7 @@ vellum_cli/push.py,sha256=nWHLDi_w0LXycNkVv00CiNwY469BcTNBn7NphWpCA7E,9711
|
|
12
12
|
vellum_cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
vellum_cli/tests/conftest.py,sha256=AFYZryKA2qnUuCPBxBKmHLFoPiE0WhBFFej9tNwSHdc,1526
|
14
14
|
vellum_cli/tests/test_config.py,sha256=uvKGDc8BoVyT9_H0Z-g8469zVxomn6Oi3Zj-vK7O_wU,2631
|
15
|
-
vellum_cli/tests/test_image_push.py,sha256=
|
15
|
+
vellum_cli/tests/test_image_push.py,sha256=QM-JlR_aJappvwbCLteQZZf76sd7SE1sRj3armvFK-I,5706
|
16
16
|
vellum_cli/tests/test_init.py,sha256=8UOc_ThfouR4ja5cCl_URuLk7ohr9JXfCnG4yka1OUQ,18754
|
17
17
|
vellum_cli/tests/test_main.py,sha256=qDZG-aQauPwBwM6A2DIu1494n47v3pL28XakTbLGZ-k,272
|
18
18
|
vellum_cli/tests/test_ping.py,sha256=3ucVRThEmTadlV9LrJdCCrr1Ofj3rOjG6ue0BNR2UC0,2523
|
@@ -26,7 +26,7 @@ vellum_ee/workflows/display/base.py,sha256=EqlQFD56kpqMY02ZBJBQajzJKh33Dwi60Wo77
|
|
26
26
|
vellum_ee/workflows/display/editor/__init__.py,sha256=MSAgY91xCEg2neH5d8jXx5wRdR962ftZVa6vO9BGq9k,167
|
27
27
|
vellum_ee/workflows/display/editor/types.py,sha256=x-tOOCJ6CF4HmiKDfCmcc3bOVfc1EBlP5o6u5WEfLoY,567
|
28
28
|
vellum_ee/workflows/display/nodes/__init__.py,sha256=jI1aPBQf8DkmrYoZ4O-wR1duqZByOf5mDFmo_wFJPE4,307
|
29
|
-
vellum_ee/workflows/display/nodes/base_node_display.py,sha256=
|
29
|
+
vellum_ee/workflows/display/nodes/base_node_display.py,sha256=1VoNyAw9MzgGePI2wrOShsNccrbS4bTuu3bnTii0Wu4,15480
|
30
30
|
vellum_ee/workflows/display/nodes/get_node_display_class.py,sha256=gKAZfc7JBLzcwYPchnpHy2nMVMPmltAszOwLyXDrro0,2085
|
31
31
|
vellum_ee/workflows/display/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=Z4Mf7xLCNiblSbpKI0BrV5modQr-ZcFzhfir_OSyTTs,2997
|
@@ -35,12 +35,12 @@ vellum_ee/workflows/display/nodes/utils.py,sha256=sloya5TpXsnot1HURc9L51INwflRqU
|
|
35
35
|
vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=nUIgH2s0-7IbQRNrBhLPyRNe8YIrx3Yo9HeeW-aXXFk,1668
|
36
36
|
vellum_ee/workflows/display/nodes/vellum/api_node.py,sha256=ZiOJsSovftbH1eVFdU0-UE4DUliIHPQZkwrFcooN-Vk,8856
|
37
37
|
vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py,sha256=rJbHZBg9A_v2bjk-R6MfWzShcrS2gcKIOyYGoqwTx8s,6353
|
38
|
-
vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=
|
38
|
+
vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=L308T4U6eaL0WUJ_hQ0qrH-i8IuMBCm5XjRO2_b-Mro,4507
|
39
39
|
vellum_ee/workflows/display/nodes/vellum/conditional_node.py,sha256=MrvyiYD0qgQf3-ZYFcurQtin3FagAHGRoT7zYGiIao0,11150
|
40
40
|
vellum_ee/workflows/display/nodes/vellum/error_node.py,sha256=m2DmOXm9-jiiIl6zwkXHNfsYp5PTpBHEdt5xaIsabWo,2363
|
41
41
|
vellum_ee/workflows/display/nodes/vellum/final_output_node.py,sha256=jUDI2FwVaw0Or4zJL58J_g0S--i59Hzik60s_Es_M-8,3098
|
42
42
|
vellum_ee/workflows/display/nodes/vellum/guardrail_node.py,sha256=5_5D5PMzBOeUdVtRlANbfEsu7Gv3r37dLvpfjGAqYac,2330
|
43
|
-
vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py,sha256=
|
43
|
+
vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py,sha256=gVwQwycEPNtCs8tWbFyIMLpCA7zXnqcmuuhFqRWNxZM,10368
|
44
44
|
vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py,sha256=fQV5o83BPTwGX6o-ThN4r7BcIhySyqwpW1JGYWpvSJI,5625
|
45
45
|
vellum_ee/workflows/display/nodes/vellum/map_node.py,sha256=CiklGf5_tDbqE1XQm2mnbtoL01_2JYjcnB4FDTpMImQ,3824
|
46
46
|
vellum_ee/workflows/display/nodes/vellum/merge_node.py,sha256=yBWeN4T_lOsDVnNOKWRiT7JYKu0IR5Fx2z99iq6QKSA,3273
|
@@ -51,11 +51,11 @@ vellum_ee/workflows/display/nodes/vellum/search_node.py,sha256=3n1qa-zWIk0p_H94u
|
|
51
51
|
vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py,sha256=MWLZBXHsmj5vKgqOgI2HHcHAJzLS0sqybn6idhwug8Y,2669
|
52
52
|
vellum_ee/workflows/display/nodes/vellum/templating_node.py,sha256=J84_EUfwWwpeOfUFBdRorrD1Bod0jDBFdQ6xnRyp9Ho,3338
|
53
53
|
vellum_ee/workflows/display/nodes/vellum/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
-
vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py,sha256
|
54
|
+
vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py,sha256=ZasoqG8FmqnZDj2FfL5BGPq9fafOTJqV_4xjOKLi1gc,5434
|
55
55
|
vellum_ee/workflows/display/nodes/vellum/tests/test_error_node.py,sha256=540FoWMpJ3EN_DPjHsr9ODJWCRVcUa5hZBn-5T2GiHU,1665
|
56
56
|
vellum_ee/workflows/display/nodes/vellum/tests/test_note_node.py,sha256=uiMB0cOxKZzos7YKnj4ef4DFa2bOvZJWIv-hfbUV6Go,1218
|
57
57
|
vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_deployment_node.py,sha256=G-qJyTNJkpqJiEZ3kCJl86CXJINLeFyf2lM0bQHCCOs,3822
|
58
|
-
vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py,sha256=
|
58
|
+
vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py,sha256=RPpromm0y9y-MukL8cmxpl9hYaw-JuNo8vFDOcLI4V4,8801
|
59
59
|
vellum_ee/workflows/display/nodes/vellum/tests/test_retry_node.py,sha256=h93ysolmbo2viisyhRnXKHPxiDK0I_dSAbYoHFYIoO4,1953
|
60
60
|
vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py,sha256=BUzHJgjdWnPeZxjFjHfDBKnbFjYjnbXPjc-1hne1B2Y,3965
|
61
61
|
vellum_ee/workflows/display/nodes/vellum/tests/test_templating_node.py,sha256=LSk2gx9TpGXbAqKe8dggQW8yJZqj-Cf0EGJFeGGlEcw,3321
|
@@ -69,7 +69,7 @@ vellum_ee/workflows/display/tests/workflow_serialization/__init__.py,sha256=47DE
|
|
69
69
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
70
70
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py,sha256=XOQDDRiG46etxTC7-_RUEutoNumXc02fo7oho4GYM0c,1900
|
71
71
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=tsjGM-jOurPSkDIbrDFdjNLmkI1fPNPAw3J9-l9knCw,12848
|
72
|
-
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py,sha256=
|
72
|
+
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py,sha256=rV7_NOUIsyHUrdSJ0pJFt7ha-uIC1nSLqdYpRhE9Zfg,21430
|
73
73
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py,sha256=s6_mnk0pkztU59wYpSfOFpMhAJaRjmyfxM6WJGtnD4Y,6456
|
74
74
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py,sha256=PkSgghJDz0fpDB72HHPjLjo8LkZk-HpUkCQzRLX-iVw,40611
|
75
75
|
vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_trigger_serialization.py,sha256=dsJr8I9AdPwMOGszirfNDzZP2Ychd94aAKuPXAzknMk,4632
|
@@ -94,14 +94,14 @@ vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_n
|
|
94
94
|
vellum_ee/workflows/display/types.py,sha256=i4T7ElU5b5h-nA1i3scmEhO1BqmNDc4eJDHavATD88w,2821
|
95
95
|
vellum_ee/workflows/display/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
96
96
|
vellum_ee/workflows/display/utils/exceptions.py,sha256=LSwwxCYNxFkf5XMUcFkaZKpQ13OSrI7y_bpEUwbKVk0,169
|
97
|
-
vellum_ee/workflows/display/utils/expressions.py,sha256=
|
97
|
+
vellum_ee/workflows/display/utils/expressions.py,sha256=gbc_UDogJEI1juSNPfR3a-OXu2sIH7w3vLLw4YjOqDc,12416
|
98
98
|
vellum_ee/workflows/display/utils/registry.py,sha256=fWIm5Jj-10gNFjgn34iBu4RWv3Vd15ijtSN0V97bpW8,1513
|
99
|
-
vellum_ee/workflows/display/utils/vellum.py,sha256=
|
99
|
+
vellum_ee/workflows/display/utils/vellum.py,sha256=mtoXmSYwR7rvrq-d6CzCW_auaJXTct0Mi1F0xpRCiNQ,5627
|
100
100
|
vellum_ee/workflows/display/vellum.py,sha256=o7mq_vk2Yapu9DDKRz5l76h8EmCAypWGQYe6pryrbB8,3576
|
101
101
|
vellum_ee/workflows/display/workflows/__init__.py,sha256=kapXsC67VJcgSuiBMa86FdePG5A9kMB5Pi4Uy1O2ob4,207
|
102
102
|
vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=NuWlnGNe3Htcfh-l_8e37uitdUsy6WZNB7W7dYcCoUg,33355
|
103
103
|
vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=gxz76AeCqgAZ9D2lZeTiZzxY9eMgn3qOSfVgiqYcOh8,2028
|
104
|
-
vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=
|
104
|
+
vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=gTKddCTGzjWHJ8v4E0XqCLgxKuSXaFveoN2IT1LxrPw,29472
|
105
105
|
vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=aaKdmWrgEe5YyV4zuDY_4E3y-l59rIHQnNGiPj2OWxQ,359
|
106
106
|
vellum_ee/workflows/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
107
|
vellum_ee/workflows/server/virtual_file_loader.py,sha256=7JphJcSO3H85qiC2DpFfBWjC3JjrbRmoynBC6KKHVsA,2710
|
@@ -132,7 +132,7 @@ vellum/client/README.md,sha256=qmaVIP42MnxAu8jV7u-CsgVFfs3-pHQODrXdZdFxtaw,4749
|
|
132
132
|
vellum/client/__init__.py,sha256=PEnFl7LbXQcvAi3bVN2qyt5xm2FtVtq7xWKkcWM3Tg4,120166
|
133
133
|
vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
|
134
134
|
vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
|
135
|
-
vellum/client/core/client_wrapper.py,sha256=
|
135
|
+
vellum/client/core/client_wrapper.py,sha256=fgLMgzbjkk4zz3CHZ_Wdqz6Sz1JL4L0v7uiNheKIcZA,1869
|
136
136
|
vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
|
137
137
|
vellum/client/core/file.py,sha256=d4NNbX8XvXP32z8KpK2Xovv33nFfruIrpz0QWxlgpZk,2663
|
138
138
|
vellum/client/core/http_client.py,sha256=Z77OIxIbL4OAB2IDqjRq_sYa5yNYAWfmdhdCSSvh6Y4,19552
|
@@ -1587,12 +1587,12 @@ vellum/workflows/nodes/displayable/bases/tests/test_utils.py,sha256=eqdqbKNRWVMD
|
|
1587
1587
|
vellum/workflows/nodes/displayable/bases/types.py,sha256=C37B2Qh2YP7s7pUjd-EYKc2Zl1TbnCgI_mENuUSb8bo,1706
|
1588
1588
|
vellum/workflows/nodes/displayable/bases/utils.py,sha256=ckMUenSsNkiYmSw6FmjSMHYaCk8Y8_sUjL6lkFFEqts,5412
|
1589
1589
|
vellum/workflows/nodes/displayable/code_execution_node/__init__.py,sha256=0FLWMMktpzSnmBMizQglBpcPrP80fzVsoJwJgf822Cg,76
|
1590
|
-
vellum/workflows/nodes/displayable/code_execution_node/node.py,sha256=
|
1590
|
+
vellum/workflows/nodes/displayable/code_execution_node/node.py,sha256=U21jXW8XZoC51vP0pvbbUQzQidR6Ej2lMdGypIUyF3I,9708
|
1591
1591
|
vellum/workflows/nodes/displayable/code_execution_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1592
1592
|
vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1593
1593
|
vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/main.py,sha256=5QsbmkzSlSbcbWTG_JmIqcP-JNJzOPTKxGzdHos19W4,79
|
1594
|
-
vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py,sha256=
|
1595
|
-
vellum/workflows/nodes/displayable/code_execution_node/utils.py,sha256=
|
1594
|
+
vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py,sha256=N5SZeYyxEDrkbTagEvbnsTLgOeqrYEfIUOheM5qgjiU,31423
|
1595
|
+
vellum/workflows/nodes/displayable/code_execution_node/utils.py,sha256=VRTKms59vrSR9mDk99cojParZVAP4lzjEeDwDNXU1tk,3837
|
1596
1596
|
vellum/workflows/nodes/displayable/conditional_node/__init__.py,sha256=AS_EIqFdU1F9t8aLmbZU-rLh9ry6LCJ0uj0D8F0L5Uw,72
|
1597
1597
|
vellum/workflows/nodes/displayable/conditional_node/node.py,sha256=Qjfl33gZ3JEgxBA1EgzSUebboGvsARthIxxcQyvx5Gg,1152
|
1598
1598
|
vellum/workflows/nodes/displayable/conftest.py,sha256=K2kLM2JGAfcrmmd92u8DXInUO5klFdggPWblg5RVcx4,5729
|
@@ -1676,7 +1676,7 @@ vellum/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
1676
1676
|
vellum/workflows/tests/test_sandbox.py,sha256=JKwaluI-lODQo7Ek9sjDstjL_WTdSqUlVik6ZVTfVOA,1826
|
1677
1677
|
vellum/workflows/tests/test_undefined.py,sha256=zMCVliCXVNLrlC6hEGyOWDnQADJ2g83yc5FIM33zuo8,353
|
1678
1678
|
vellum/workflows/types/__init__.py,sha256=KxUTMBGzuRCfiMqzzsykOeVvrrkaZmTTo1a7SLu8gRM,68
|
1679
|
-
vellum/workflows/types/code_execution_node_wrappers.py,sha256=
|
1679
|
+
vellum/workflows/types/code_execution_node_wrappers.py,sha256=zPnfvhxpDS3vvM6hX1X6sPfvo1bu1kcPQfhNoJD9vtM,2362
|
1680
1680
|
vellum/workflows/types/core.py,sha256=kMQremh_I8egXpiKmtMQbB6e3OczAWiRnnTq5V6xlD0,928
|
1681
1681
|
vellum/workflows/types/definition.py,sha256=z81CL_u0FJol-9yUIqoXNTYAARtU8x__c6s-f4rb5c8,2335
|
1682
1682
|
vellum/workflows/types/generics.py,sha256=tKXz0LwWJGKw1YGudyl9_yFDrRgU6yYV1yJV1Zv-LTw,1430
|
@@ -1701,8 +1701,8 @@ vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnad
|
|
1701
1701
|
vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
1702
1702
|
vellum/workflows/workflows/tests/test_base_workflow.py,sha256=8P5YIsNMO78_CR1NNK6wkEdkMB4b3Q_Ni1qxh78OnHo,20481
|
1703
1703
|
vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
|
1704
|
-
vellum_ai-0.14.
|
1705
|
-
vellum_ai-0.14.
|
1706
|
-
vellum_ai-0.14.
|
1707
|
-
vellum_ai-0.14.
|
1708
|
-
vellum_ai-0.14.
|
1704
|
+
vellum_ai-0.14.49.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
|
1705
|
+
vellum_ai-0.14.49.dist-info/METADATA,sha256=UnFisKY5VZ2o5md6oV2OU-gTx6Y24oQd4u5Bv_rJfag,5484
|
1706
|
+
vellum_ai-0.14.49.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
1707
|
+
vellum_ai-0.14.49.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
|
1708
|
+
vellum_ai-0.14.49.dist-info/RECORD,,
|
vellum_cli/__init__.py
CHANGED
@@ -354,9 +354,10 @@ def images() -> None:
|
|
354
354
|
help="Tags the provided image inside of Vellum's repo. "
|
355
355
|
"This field does not push multiple local tags of the passed in image.",
|
356
356
|
)
|
357
|
-
|
357
|
+
@click.option("--workspace", type=str, help="The specific Workspace config to use when pushing")
|
358
|
+
def image_push(image: str, tag: Optional[List[str]] = None, workspace: Optional[str] = None) -> None:
|
358
359
|
"""Push Docker image to Vellum"""
|
359
|
-
image_push_command(image, tag)
|
360
|
+
image_push_command(image, tag, workspace)
|
360
361
|
|
361
362
|
|
362
363
|
@workflows.command(name="init")
|
vellum_cli/image_push.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
+
import os
|
3
4
|
import re
|
4
5
|
import subprocess
|
5
6
|
from typing import List, Optional
|
@@ -9,15 +10,26 @@ from docker import DockerClient
|
|
9
10
|
from dotenv import load_dotenv
|
10
11
|
|
11
12
|
from vellum.workflows.vellum_client import create_vellum_client, create_vellum_environment
|
13
|
+
from vellum_cli.config import DEFAULT_WORKSPACE_CONFIG, load_vellum_cli_config
|
12
14
|
from vellum_cli.logger import load_cli_logger
|
13
15
|
|
14
16
|
_SUPPORTED_ARCHITECTURE = "amd64"
|
15
17
|
|
16
18
|
|
17
|
-
def image_push_command(image: str, tags: Optional[List[str]] = None) -> None:
|
18
|
-
load_dotenv()
|
19
|
+
def image_push_command(image: str, tags: Optional[List[str]] = None, workspace: Optional[str] = None) -> None:
|
20
|
+
load_dotenv(dotenv_path=os.path.join(os.getcwd(), ".env"))
|
19
21
|
logger = load_cli_logger()
|
20
|
-
|
22
|
+
config = load_vellum_cli_config()
|
23
|
+
workspace_config = next((w for w in config.workspaces if w.name == workspace), DEFAULT_WORKSPACE_CONFIG)
|
24
|
+
|
25
|
+
api_key = os.getenv(workspace_config.api_key, None)
|
26
|
+
if not api_key:
|
27
|
+
raise ValueError(f"API key {workspace_config.api_key} for workspace {workspace} not found")
|
28
|
+
|
29
|
+
vellum_client = create_vellum_client(
|
30
|
+
api_key=api_key,
|
31
|
+
api_url=workspace_config.api_url,
|
32
|
+
)
|
21
33
|
|
22
34
|
# Check if we are self hosted by looking at our base url
|
23
35
|
api_url = create_vellum_environment().default
|
@@ -1,16 +1,37 @@
|
|
1
|
+
import pytest
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import shutil
|
1
5
|
import subprocess
|
6
|
+
import tempfile
|
2
7
|
from unittest.mock import MagicMock, patch
|
8
|
+
from uuid import uuid4
|
9
|
+
from typing import Generator
|
3
10
|
|
4
11
|
from click.testing import CliRunner
|
12
|
+
from httpx import Response
|
5
13
|
|
6
14
|
from vellum_cli import main as cli_main
|
7
15
|
|
8
16
|
|
17
|
+
@pytest.fixture
|
18
|
+
def mock_temp_dir() -> Generator[str, None, None]:
|
19
|
+
current_dir = os.getcwd()
|
20
|
+
temp_dir = tempfile.mkdtemp()
|
21
|
+
os.chdir(temp_dir)
|
22
|
+
|
23
|
+
yield temp_dir
|
24
|
+
|
25
|
+
os.chdir(current_dir)
|
26
|
+
shutil.rmtree(temp_dir)
|
27
|
+
|
28
|
+
|
9
29
|
@patch("subprocess.run")
|
10
30
|
@patch("docker.from_env")
|
11
31
|
def test_image_push__self_hosted_happy_path(mock_docker_from_env, mock_run, vellum_client, monkeypatch):
|
12
32
|
# GIVEN a self hosted vellum api URL env var
|
13
33
|
monkeypatch.setenv("VELLUM_API_URL", "mycompany.api.com")
|
34
|
+
monkeypatch.setenv("VELLUM_API_KEY", "123456abcdef")
|
14
35
|
|
15
36
|
# Mock Docker client
|
16
37
|
mock_docker_client = MagicMock()
|
@@ -35,6 +56,94 @@ def test_image_push__self_hosted_happy_path(mock_docker_from_env, mock_run, vell
|
|
35
56
|
assert "Image successfully pushed" in result.output
|
36
57
|
|
37
58
|
|
59
|
+
@patch("subprocess.run")
|
60
|
+
@patch("docker.from_env")
|
61
|
+
def test_image_push__self_hosted_happy_path__workspace_option(
|
62
|
+
mock_docker_from_env, mock_run, mock_httpx_transport, mock_temp_dir
|
63
|
+
):
|
64
|
+
# GIVEN a workspace config with a new env for url
|
65
|
+
with open(os.path.join(mock_temp_dir, "vellum.lock.json"), "w") as f:
|
66
|
+
f.write(
|
67
|
+
json.dumps(
|
68
|
+
{
|
69
|
+
"workspaces": [
|
70
|
+
{
|
71
|
+
"name": "my_workspace",
|
72
|
+
"api_url": "MY_WORKSPACE_VELLUM_API_URL",
|
73
|
+
"api_key": "MY_WORKSPACE_VELLUM_API_KEY",
|
74
|
+
}
|
75
|
+
]
|
76
|
+
}
|
77
|
+
)
|
78
|
+
)
|
79
|
+
|
80
|
+
# AND a .env file with the workspace api key and url
|
81
|
+
with open(os.path.join(mock_temp_dir, ".env"), "w") as f:
|
82
|
+
f.write(
|
83
|
+
"VELLUM_API_KEY=123456abcdef\n"
|
84
|
+
"VELLUM_API_URL=https://api.vellum.ai\n"
|
85
|
+
"MY_WORKSPACE_VELLUM_API_KEY=789012ghijkl\n"
|
86
|
+
"MY_WORKSPACE_VELLUM_API_URL=https://api.vellum.mycompany.ai\n"
|
87
|
+
)
|
88
|
+
|
89
|
+
# AND the Docker client returns the correct response
|
90
|
+
mock_docker_client = MagicMock()
|
91
|
+
mock_docker_from_env.return_value = mock_docker_client
|
92
|
+
|
93
|
+
mock_run.side_effect = [
|
94
|
+
subprocess.CompletedProcess(
|
95
|
+
args="", returncode=0, stdout=b'{"manifests": [{"platform": {"architecture": "amd64"}}]}'
|
96
|
+
),
|
97
|
+
subprocess.CompletedProcess(args="", returncode=0, stdout=b"sha256:hellosha"),
|
98
|
+
]
|
99
|
+
|
100
|
+
# AND the vellum client returns the correct response for
|
101
|
+
mock_httpx_transport.handle_request.side_effect = [
|
102
|
+
# First call to get the docker service token
|
103
|
+
Response(
|
104
|
+
status_code=200,
|
105
|
+
text=json.dumps(
|
106
|
+
{
|
107
|
+
"access_token": "345678mnopqr",
|
108
|
+
"organization_id": str(uuid4()),
|
109
|
+
"repository": "myrepo.net",
|
110
|
+
}
|
111
|
+
),
|
112
|
+
),
|
113
|
+
# Second call to push the image
|
114
|
+
Response(
|
115
|
+
status_code=200,
|
116
|
+
text=json.dumps(
|
117
|
+
{
|
118
|
+
"id": str(uuid4()),
|
119
|
+
"name": "myrepo.net/myimage",
|
120
|
+
"visibility": "PRIVATE",
|
121
|
+
"created": "2021-01-01T00:00:00Z",
|
122
|
+
"modified": "2021-01-01T00:00:00Z",
|
123
|
+
"repository": "myrepo.net",
|
124
|
+
"sha": "sha256:hellosha",
|
125
|
+
"tags": [],
|
126
|
+
}
|
127
|
+
),
|
128
|
+
),
|
129
|
+
]
|
130
|
+
|
131
|
+
# WHEN the user runs the image push command
|
132
|
+
runner = CliRunner()
|
133
|
+
result = runner.invoke(cli_main, ["image", "push", "myrepo.net/myimage:latest", "--workspace", "my_workspace"])
|
134
|
+
|
135
|
+
# THEN the command exits successfully
|
136
|
+
assert result.exit_code == 0, (result.output, str(result.exception))
|
137
|
+
|
138
|
+
# AND gives the success message
|
139
|
+
assert "Image successfully pushed" in result.output
|
140
|
+
|
141
|
+
# AND the vellum client was called with the correct api key and url
|
142
|
+
request = mock_httpx_transport.handle_request.call_args[0][0]
|
143
|
+
assert request.headers["X-API-KEY"] == "789012ghijkl", result.stdout
|
144
|
+
assert str(request.url) == "https://api.vellum.mycompany.ai/v1/container-images/push"
|
145
|
+
|
146
|
+
|
38
147
|
@patch("subprocess.run")
|
39
148
|
@patch("docker.from_env")
|
40
149
|
def test_image_push__self_hosted_blocks_repo(mock_docker_from_env, mock_run, vellum_client, monkeypatch):
|
@@ -260,10 +260,15 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
|
|
260
260
|
|
261
261
|
def get_source_handle_id(self, port_displays: Dict[Port, PortDisplay]) -> UUID:
|
262
262
|
unadorned_node = get_unadorned_node(self._node)
|
263
|
-
default_port = unadorned_node.Ports.default
|
263
|
+
default_port = next((port for port in unadorned_node.Ports if port.default), None)
|
264
|
+
if default_port in port_displays:
|
265
|
+
return port_displays[default_port].id
|
264
266
|
|
265
|
-
|
266
|
-
|
267
|
+
first_port = next((port for port in unadorned_node.Ports), None)
|
268
|
+
if not first_port:
|
269
|
+
raise ValueError(f"Node {self._node.__name__} must have at least one port.")
|
270
|
+
|
271
|
+
return port_displays[first_port].id
|
267
272
|
|
268
273
|
def get_trigger_id(self) -> UUID:
|
269
274
|
return self.get_target_handle_id()
|
@@ -79,8 +79,8 @@ class BaseCodeExecutionNodeDisplay(BaseNodeDisplay[_CodeExecutionNodeType], Gene
|
|
79
79
|
|
80
80
|
packages = raise_if_descriptor(node.packages)
|
81
81
|
|
82
|
-
|
83
|
-
|
82
|
+
output_display = self.output_display[node.Outputs.result]
|
83
|
+
log_output_display = self.output_display[node.Outputs.log]
|
84
84
|
|
85
85
|
output_type = primitive_type_to_vellum_variable_type(node.get_output_type())
|
86
86
|
|
@@ -32,7 +32,7 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
|
|
32
32
|
output_display = self.output_display[node.Outputs.text]
|
33
33
|
array_display = self.output_display[node.Outputs.results]
|
34
34
|
json_display = self.output_display[node.Outputs.json]
|
35
|
-
node_blocks = raise_if_descriptor(node.blocks)
|
35
|
+
node_blocks = raise_if_descriptor(node.blocks) or []
|
36
36
|
function_definitions = raise_if_descriptor(node.functions)
|
37
37
|
|
38
38
|
ml_model = str(raise_if_descriptor(node.ml_model))
|
@@ -2,7 +2,9 @@ import pytest
|
|
2
2
|
from uuid import UUID
|
3
3
|
from typing import Type
|
4
4
|
|
5
|
+
from vellum.client.core.api_error import ApiError
|
5
6
|
from vellum.workflows.nodes.displayable.code_execution_node.node import CodeExecutionNode
|
7
|
+
from vellum.workflows.references.vellum_secret import VellumSecretReference
|
6
8
|
from vellum.workflows.workflows.base import BaseWorkflow
|
7
9
|
from vellum_ee.workflows.display.nodes.vellum.code_execution_node import BaseCodeExecutionNodeDisplay
|
8
10
|
from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
|
@@ -110,3 +112,44 @@ def test_serialize_node__code_node_inputs(GetDisplayClass, expected_input_id):
|
|
110
112
|
},
|
111
113
|
},
|
112
114
|
]
|
115
|
+
|
116
|
+
|
117
|
+
def test_serialize_node__with_unresolved_secret_references(vellum_client):
|
118
|
+
# GIVEN a node has access to a secret reference
|
119
|
+
class MyNode(CodeExecutionNode):
|
120
|
+
code_inputs = {"api_key": VellumSecretReference("MY_API_KEY")}
|
121
|
+
|
122
|
+
# AND the secret is not found
|
123
|
+
vellum_client.workspace_secrets.retrieve.side_effect = ApiError(
|
124
|
+
status_code=404,
|
125
|
+
body="Secret not found",
|
126
|
+
)
|
127
|
+
|
128
|
+
# AND a workflow with the code node
|
129
|
+
class Workflow(BaseWorkflow):
|
130
|
+
graph = MyNode
|
131
|
+
|
132
|
+
# WHEN we serialize the workflow
|
133
|
+
workflow_display = get_workflow_display(workflow_class=Workflow)
|
134
|
+
data: dict = workflow_display.serialize()
|
135
|
+
|
136
|
+
# THEN the condition should be serialized correctly
|
137
|
+
node = next(node for node in data["workflow_raw_data"]["nodes"] if node["type"] == "CODE_EXECUTION")
|
138
|
+
assert node["inputs"][0]["value"] == {
|
139
|
+
"combinator": "OR",
|
140
|
+
"rules": [
|
141
|
+
{
|
142
|
+
"type": "WORKSPACE_SECRET",
|
143
|
+
"data": {
|
144
|
+
"type": "STRING",
|
145
|
+
"workspace_secret_id": None,
|
146
|
+
},
|
147
|
+
}
|
148
|
+
],
|
149
|
+
}
|
150
|
+
|
151
|
+
# AND we should have a warning of the invalid reference
|
152
|
+
# TODO: Come up with a proposal for how nodes should propagate warnings
|
153
|
+
# warnings = list(workflow_display.errors)
|
154
|
+
# assert len(warnings) == 1
|
155
|
+
# assert "Failed to resolve secret reference 'MY_API_KEY'" in str(warnings[0])
|
@@ -6,6 +6,7 @@ from vellum.client.types.variable_prompt_block import VariablePromptBlock
|
|
6
6
|
from vellum.workflows import BaseWorkflow
|
7
7
|
from vellum.workflows.nodes import BaseNode
|
8
8
|
from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
|
9
|
+
from vellum.workflows.ports.port import Port
|
9
10
|
from vellum.workflows.references.lazy import LazyReference
|
10
11
|
from vellum.workflows.state.base import BaseState
|
11
12
|
from vellum_ee.workflows.display.nodes.vellum.inline_prompt_node import BaseInlinePromptNodeDisplay
|
@@ -215,3 +216,33 @@ def test_serialize_node__unreferenced_variable_block__still_serializes():
|
|
215
216
|
# warnings = list(workflow_display.errors)
|
216
217
|
# assert len(warnings) == 1
|
217
218
|
# assert "Missing input variable 'foo' for prompt block 0" in str(warnings[0])
|
219
|
+
|
220
|
+
|
221
|
+
def test_serialize_node__port_groups():
|
222
|
+
# GIVEN a prompt node with ports
|
223
|
+
class MyPromptNode(InlinePromptNode):
|
224
|
+
class Ports(InlinePromptNode.Ports):
|
225
|
+
apple = Port.on_if(LazyReference(lambda: MyPromptNode.Outputs.text).equals("apple"))
|
226
|
+
banana = Port.on_if(LazyReference(lambda: MyPromptNode.Outputs.text).equals("banana"))
|
227
|
+
|
228
|
+
# AND a workflow with the prompt node
|
229
|
+
class MyWorkflow(BaseWorkflow):
|
230
|
+
graph = MyPromptNode
|
231
|
+
|
232
|
+
# WHEN the prompt node is serialized
|
233
|
+
workflow_display = get_workflow_display(workflow_class=MyWorkflow)
|
234
|
+
serialized_workflow: dict = workflow_display.serialize()
|
235
|
+
|
236
|
+
# THEN the node should have the ports serialized
|
237
|
+
my_prompt_node = next(
|
238
|
+
node for node in serialized_workflow["workflow_raw_data"]["nodes"] if node["id"] == str(MyPromptNode.__id__)
|
239
|
+
)
|
240
|
+
ports = my_prompt_node["ports"]
|
241
|
+
assert len(ports) == 2
|
242
|
+
assert ports[0]["id"] == "149d97a4-3da3-44a9-95f7-ea7b8d38b877"
|
243
|
+
assert ports[1]["id"] == "71f2d2b3-194f-4492-bc1c-a5ca1f60fb0a"
|
244
|
+
assert ports[0]["name"] == "apple"
|
245
|
+
assert ports[1]["name"] == "banana"
|
246
|
+
|
247
|
+
# AND the legacy source_handle_id should be the default port
|
248
|
+
assert my_prompt_node["data"]["source_handle_id"] == "149d97a4-3da3-44a9-95f7-ea7b8d38b877"
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import pytest
|
2
1
|
from uuid import uuid4
|
3
2
|
from typing import List
|
4
3
|
|
@@ -257,11 +256,58 @@ def test_serialize_node__workflow_input_as_nested_chat_history():
|
|
257
256
|
|
258
257
|
# WHEN the workflow is serialized
|
259
258
|
workflow_display = get_workflow_display(workflow_class=Workflow)
|
260
|
-
|
261
|
-
|
259
|
+
serialized_workflow: dict = workflow_display.serialize()
|
260
|
+
|
261
|
+
# THEN the node should properly serialize the attribute reference
|
262
|
+
generic_node = next(
|
263
|
+
node for node in serialized_workflow["workflow_raw_data"]["nodes"] if node["id"] == str(GenericNode.__id__)
|
264
|
+
)
|
262
265
|
|
263
|
-
|
264
|
-
|
266
|
+
assert not DeepDiff(
|
267
|
+
{
|
268
|
+
"id": "11be9d37-0069-4695-a317-14a3b6519d4e",
|
269
|
+
"label": "test_serialize_node__workflow_input_as_nested_chat_history.<locals>.GenericNode",
|
270
|
+
"type": "GENERIC",
|
271
|
+
"display_data": {"position": {"x": 0.0, "y": 0.0}},
|
272
|
+
"base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
|
273
|
+
"definition": {
|
274
|
+
"name": "GenericNode",
|
275
|
+
"module": [
|
276
|
+
"vellum_ee",
|
277
|
+
"workflows",
|
278
|
+
"display",
|
279
|
+
"tests",
|
280
|
+
"workflow_serialization",
|
281
|
+
"generic_nodes",
|
282
|
+
"test_attributes_serialization",
|
283
|
+
],
|
284
|
+
},
|
285
|
+
"trigger": {"id": "d4548468-85a4-449e-92e0-e2d8b8fd097c", "merge_behavior": "AWAIT_ATTRIBUTES"},
|
286
|
+
"ports": [{"id": "c4a9a57d-1380-4689-8500-e8a0b6477291", "name": "default", "type": "DEFAULT"}],
|
287
|
+
"adornments": None,
|
288
|
+
"attributes": [
|
289
|
+
{
|
290
|
+
"id": "e878bbc9-1231-461e-9e9d-947604da116e",
|
291
|
+
"name": "attr",
|
292
|
+
"value": {
|
293
|
+
"type": "DICTIONARY_REFERENCE",
|
294
|
+
"entries": [
|
295
|
+
{
|
296
|
+
"key": "hello",
|
297
|
+
"value": {
|
298
|
+
"type": "WORKFLOW_INPUT",
|
299
|
+
"input_variable_id": "f727c3f9-f27f-4ac9-abd7-12bf612a094e",
|
300
|
+
},
|
301
|
+
}
|
302
|
+
],
|
303
|
+
},
|
304
|
+
}
|
305
|
+
],
|
306
|
+
"outputs": [],
|
307
|
+
},
|
308
|
+
generic_node,
|
309
|
+
ignore_order=True,
|
310
|
+
)
|
265
311
|
|
266
312
|
|
267
313
|
def test_serialize_node__node_output(serialize_node):
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import TYPE_CHECKING, Any
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, cast
|
2
2
|
|
3
3
|
from vellum.client.types.logical_operator import LogicalOperator
|
4
4
|
from vellum.workflows.descriptors.base import BaseDescriptor
|
@@ -37,7 +37,7 @@ from vellum.workflows.references.output import OutputReference
|
|
37
37
|
from vellum.workflows.references.state_value import StateValueReference
|
38
38
|
from vellum.workflows.references.vellum_secret import VellumSecretReference
|
39
39
|
from vellum.workflows.references.workflow_input import WorkflowInputReference
|
40
|
-
from vellum.workflows.types.core import JsonObject
|
40
|
+
from vellum.workflows.types.core import JsonArray, JsonObject
|
41
41
|
from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
|
42
42
|
|
43
43
|
if TYPE_CHECKING:
|
@@ -235,8 +235,54 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
|
|
235
235
|
"node_id": str(node_class_display.node_id),
|
236
236
|
}
|
237
237
|
|
238
|
-
if isinstance(value,
|
239
|
-
|
238
|
+
if isinstance(value, list):
|
239
|
+
serialized_items = [serialize_value(display_context, item) for item in value]
|
240
|
+
if all(isinstance(item, dict) and item["type"] == "CONSTANT_VALUE" for item in serialized_items):
|
241
|
+
constant_values = []
|
242
|
+
for item in serialized_items:
|
243
|
+
item_dict = cast(Dict[str, Any], item)
|
244
|
+
value_inner = item_dict["value"]
|
245
|
+
|
246
|
+
if value_inner["type"] == "JSON" and "items" in value_inner:
|
247
|
+
# Nested JSON list
|
248
|
+
constant_values.append(value_inner["items"])
|
249
|
+
else:
|
250
|
+
constant_values.append(value_inner["value"])
|
251
|
+
|
252
|
+
return {
|
253
|
+
"type": "CONSTANT_VALUE",
|
254
|
+
"value": {
|
255
|
+
"type": "JSON",
|
256
|
+
"items": constant_values,
|
257
|
+
},
|
258
|
+
}
|
259
|
+
else:
|
260
|
+
return {
|
261
|
+
"type": "ARRAY_REFERENCE",
|
262
|
+
"items": cast(JsonArray, serialized_items), # list[JsonObject] -> JsonArray
|
263
|
+
}
|
264
|
+
|
265
|
+
if isinstance(value, dict):
|
266
|
+
serialized_entries = [
|
267
|
+
{"key": key, "value": serialize_value(display_context, val)} for key, val in value.items()
|
268
|
+
]
|
269
|
+
|
270
|
+
# Check if all entries have constant values
|
271
|
+
if all(entry["value"]["type"] == "CONSTANT_VALUE" for entry in serialized_entries):
|
272
|
+
constant_entries = {}
|
273
|
+
for entry in serialized_entries:
|
274
|
+
entry_value = entry["value"]["value"]
|
275
|
+
constant_entries[entry["key"]] = entry_value["value"]
|
276
|
+
|
277
|
+
return {
|
278
|
+
"type": "CONSTANT_VALUE",
|
279
|
+
"value": {
|
280
|
+
"type": "JSON",
|
281
|
+
"value": constant_entries,
|
282
|
+
},
|
283
|
+
}
|
284
|
+
else:
|
285
|
+
return {"type": "DICTIONARY_REFERENCE", "entries": cast(JsonArray, serialized_entries)}
|
240
286
|
|
241
287
|
if not isinstance(value, BaseDescriptor):
|
242
288
|
vellum_value = primitive_to_vellum_value(value)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
2
2
|
|
3
|
+
from vellum.client.core.api_error import ApiError
|
3
4
|
from vellum.client.core.pydantic_utilities import UniversalBaseModel
|
4
5
|
from vellum.client.types.array_vellum_value import ArrayVellumValue
|
5
6
|
from vellum.client.types.vellum_value import VellumValue
|
@@ -117,13 +118,18 @@ def create_node_input_value_pointer_rule(
|
|
117
118
|
workflow_input_display = display_context.global_workflow_input_displays[value]
|
118
119
|
return InputVariablePointer(data=InputVariableData(input_variable_id=str(workflow_input_display.id)))
|
119
120
|
if isinstance(value, VellumSecretReference):
|
120
|
-
|
121
|
-
|
122
|
-
|
121
|
+
try:
|
122
|
+
workspace_secret = display_context.client.workspace_secrets.retrieve(
|
123
|
+
id=value.name,
|
124
|
+
)
|
125
|
+
workspace_secret_id: Optional[str] = str(workspace_secret.id)
|
126
|
+
except ApiError:
|
127
|
+
workspace_secret_id = None
|
128
|
+
|
123
129
|
return WorkspaceSecretPointer(
|
124
130
|
data=WorkspaceSecretData(
|
125
131
|
type="STRING",
|
126
|
-
workspace_secret_id=
|
132
|
+
workspace_secret_id=workspace_secret_id,
|
127
133
|
),
|
128
134
|
)
|
129
135
|
if isinstance(value, ExecutionCountReference):
|
@@ -425,3 +425,349 @@ def test_serialize_workflow__nested_lazy_reference():
|
|
425
425
|
"node_id": str(OuterNode.__id__),
|
426
426
|
"node_output_id": str(OuterNode.__output_ids__["bar"]),
|
427
427
|
}
|
428
|
+
|
429
|
+
|
430
|
+
def test_serialize_workflow__array_values():
|
431
|
+
# GIVEN a node with array and nested array values
|
432
|
+
class MyNode(BaseNode):
|
433
|
+
class Outputs(BaseNode.Outputs):
|
434
|
+
array_value = ["item1", "item2", "item3"]
|
435
|
+
nested_array_value = [["item1", "item2", "item3"], ["item4", "item5", "item6"]]
|
436
|
+
mixed_array_value = [["item1"], "item2", "item3"]
|
437
|
+
|
438
|
+
# AND a workflow that uses these outputs
|
439
|
+
class MyWorkflow(BaseWorkflow):
|
440
|
+
graph = MyNode
|
441
|
+
|
442
|
+
class Outputs(BaseWorkflow.Outputs):
|
443
|
+
array_output = MyNode.Outputs.array_value
|
444
|
+
nested_array_output = MyNode.Outputs.nested_array_value
|
445
|
+
|
446
|
+
# WHEN we serialize it
|
447
|
+
workflow_display = get_workflow_display(workflow_class=MyWorkflow)
|
448
|
+
data = workflow_display.serialize()
|
449
|
+
|
450
|
+
# THEN it should properly serialize the array and dictionary values
|
451
|
+
assert isinstance(data["workflow_raw_data"], dict)
|
452
|
+
assert isinstance(data["workflow_raw_data"]["nodes"], list)
|
453
|
+
raw_nodes = data["workflow_raw_data"]["nodes"]
|
454
|
+
generic_nodes = [node for node in raw_nodes if isinstance(node, dict) and node["type"] == "GENERIC"]
|
455
|
+
assert len(generic_nodes) > 0
|
456
|
+
my_node = generic_nodes[0]
|
457
|
+
|
458
|
+
outputs = my_node["outputs"]
|
459
|
+
assert isinstance(outputs, list)
|
460
|
+
|
461
|
+
array_outputs = [val for val in outputs if isinstance(val, dict) and val["name"] == "array_value"]
|
462
|
+
assert len(array_outputs) > 0
|
463
|
+
array_output = array_outputs[0]
|
464
|
+
|
465
|
+
assert isinstance(array_output, dict)
|
466
|
+
assert "value" in array_output
|
467
|
+
assert array_output["value"] == {
|
468
|
+
"type": "CONSTANT_VALUE",
|
469
|
+
"value": {"type": "JSON", "items": ["item1", "item2", "item3"]},
|
470
|
+
}
|
471
|
+
|
472
|
+
nested_array_outputs = [val for val in outputs if isinstance(val, dict) and val["name"] == "nested_array_value"]
|
473
|
+
assert len(nested_array_outputs) > 0
|
474
|
+
nested_array_output = nested_array_outputs[0]
|
475
|
+
|
476
|
+
assert isinstance(nested_array_output, dict)
|
477
|
+
assert "value" in nested_array_output
|
478
|
+
assert nested_array_output["value"] == {
|
479
|
+
"type": "CONSTANT_VALUE",
|
480
|
+
"value": {"type": "JSON", "items": [["item1", "item2", "item3"], ["item4", "item5", "item6"]]},
|
481
|
+
}
|
482
|
+
|
483
|
+
mixed_array_outputs = [val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_array_value"]
|
484
|
+
assert len(mixed_array_outputs) > 0
|
485
|
+
mixed_array_output = mixed_array_outputs[0]
|
486
|
+
|
487
|
+
assert isinstance(mixed_array_output, dict)
|
488
|
+
assert "value" in mixed_array_output
|
489
|
+
assert mixed_array_output["value"] == {
|
490
|
+
"type": "CONSTANT_VALUE",
|
491
|
+
"value": {"type": "JSON", "items": [["item1"], "item2", "item3"]},
|
492
|
+
}
|
493
|
+
|
494
|
+
|
495
|
+
def test_serialize_workflow__array_reference():
|
496
|
+
# GIVEN a node with array containing non-constant values (node references)
|
497
|
+
class FirstNode(BaseNode):
|
498
|
+
class Outputs(BaseNode.Outputs):
|
499
|
+
value1: str
|
500
|
+
value2: str
|
501
|
+
|
502
|
+
class SecondNode(BaseNode):
|
503
|
+
class Outputs(BaseNode.Outputs):
|
504
|
+
# Array containing a mix of constants and node references
|
505
|
+
mixed_array = ["constant1", FirstNode.Outputs.value1, "constant2", FirstNode.Outputs.value2]
|
506
|
+
mixed_nested_array = [["constant1", FirstNode.Outputs.value1], ["constant2", FirstNode.Outputs.value2]]
|
507
|
+
|
508
|
+
# AND a workflow that uses these outputs
|
509
|
+
class MyWorkflow(BaseWorkflow):
|
510
|
+
graph = FirstNode >> SecondNode
|
511
|
+
|
512
|
+
class Outputs(BaseWorkflow.Outputs):
|
513
|
+
mixed_array_output = SecondNode.Outputs.mixed_array
|
514
|
+
mixed_nested_array_output = SecondNode.Outputs.mixed_nested_array
|
515
|
+
|
516
|
+
# WHEN we serialize it
|
517
|
+
workflow_display = get_workflow_display(workflow_class=MyWorkflow)
|
518
|
+
data = workflow_display.serialize()
|
519
|
+
|
520
|
+
# THEN it should serialize as an ARRAY_REFERENCE
|
521
|
+
assert isinstance(data["workflow_raw_data"], dict)
|
522
|
+
assert isinstance(data["workflow_raw_data"]["nodes"], list)
|
523
|
+
assert len(data["workflow_raw_data"]["nodes"]) == 5
|
524
|
+
second_node = data["workflow_raw_data"]["nodes"][2]
|
525
|
+
assert isinstance(second_node, dict)
|
526
|
+
|
527
|
+
assert "outputs" in second_node
|
528
|
+
assert isinstance(second_node["outputs"], list)
|
529
|
+
outputs = second_node["outputs"]
|
530
|
+
|
531
|
+
mixed_array_outputs = [val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_array"]
|
532
|
+
assert len(mixed_array_outputs) > 0
|
533
|
+
mixed_array_output = mixed_array_outputs[0]
|
534
|
+
|
535
|
+
assert isinstance(mixed_array_output, dict)
|
536
|
+
assert "value" in mixed_array_output
|
537
|
+
assert mixed_array_output["value"] == {
|
538
|
+
"type": "ARRAY_REFERENCE",
|
539
|
+
"items": [
|
540
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant1"}},
|
541
|
+
{
|
542
|
+
"type": "NODE_OUTPUT",
|
543
|
+
"node_id": "702a08b5-61e8-4a7a-a83d-77f49e39c5be",
|
544
|
+
"node_output_id": "419b6afa-fab5-493a-ba1e-4606f4641616",
|
545
|
+
},
|
546
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant2"}},
|
547
|
+
{
|
548
|
+
"type": "NODE_OUTPUT",
|
549
|
+
"node_id": "702a08b5-61e8-4a7a-a83d-77f49e39c5be",
|
550
|
+
"node_output_id": "d1cacc41-478d-49a3-a6b3-1ba2d51291e2",
|
551
|
+
},
|
552
|
+
],
|
553
|
+
}
|
554
|
+
|
555
|
+
mixed_nested_array_outputs = [
|
556
|
+
val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_nested_array"
|
557
|
+
]
|
558
|
+
assert len(mixed_nested_array_outputs) > 0
|
559
|
+
mixed_nested_array_output = mixed_nested_array_outputs[0]
|
560
|
+
|
561
|
+
assert isinstance(mixed_nested_array_output, dict)
|
562
|
+
assert "value" in mixed_nested_array_output
|
563
|
+
assert mixed_nested_array_output["value"] == {
|
564
|
+
"type": "ARRAY_REFERENCE",
|
565
|
+
"items": [
|
566
|
+
{
|
567
|
+
"type": "ARRAY_REFERENCE",
|
568
|
+
"items": [
|
569
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant1"}},
|
570
|
+
{
|
571
|
+
"type": "NODE_OUTPUT",
|
572
|
+
"node_id": "702a08b5-61e8-4a7a-a83d-77f49e39c5be",
|
573
|
+
"node_output_id": "419b6afa-fab5-493a-ba1e-4606f4641616",
|
574
|
+
},
|
575
|
+
],
|
576
|
+
},
|
577
|
+
{
|
578
|
+
"type": "ARRAY_REFERENCE",
|
579
|
+
"items": [
|
580
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant2"}},
|
581
|
+
{
|
582
|
+
"type": "NODE_OUTPUT",
|
583
|
+
"node_id": "702a08b5-61e8-4a7a-a83d-77f49e39c5be",
|
584
|
+
"node_output_id": "d1cacc41-478d-49a3-a6b3-1ba2d51291e2",
|
585
|
+
},
|
586
|
+
],
|
587
|
+
},
|
588
|
+
],
|
589
|
+
}
|
590
|
+
|
591
|
+
|
592
|
+
def test_serialize_workflow__dict_values():
|
593
|
+
# GIVEN a node with a dictionary value
|
594
|
+
class MyNode(BaseNode):
|
595
|
+
class Outputs(BaseNode.Outputs):
|
596
|
+
dict_value = {"key1": "value1", "key2": "value2"}
|
597
|
+
nested_dict_value = {
|
598
|
+
"key1": {"nested_key1": "value1", "nested_key2": "value2"},
|
599
|
+
"key2": {"nested_key1": "value1", "nested_key2": "value2"},
|
600
|
+
}
|
601
|
+
mixed_dict_value = {"key1": "value1", "key2": {"key3": "value3", "key4": "value4"}}
|
602
|
+
|
603
|
+
# AND a workflow that uses these outputs
|
604
|
+
class MyWorkflow(BaseWorkflow):
|
605
|
+
graph = MyNode
|
606
|
+
|
607
|
+
class Outputs(BaseWorkflow.Outputs):
|
608
|
+
dict_output = MyNode.Outputs.dict_value
|
609
|
+
|
610
|
+
# WHEN we serialize it
|
611
|
+
workflow_display = get_workflow_display(workflow_class=MyWorkflow)
|
612
|
+
data = workflow_display.serialize()
|
613
|
+
|
614
|
+
# THEN it should serialize as a CONSTANT_VALUE
|
615
|
+
assert isinstance(data["workflow_raw_data"], dict)
|
616
|
+
assert isinstance(data["workflow_raw_data"]["nodes"], list)
|
617
|
+
my_node = next(
|
618
|
+
node for node in data["workflow_raw_data"]["nodes"] if isinstance(node, dict) and node["type"] == "GENERIC"
|
619
|
+
)
|
620
|
+
|
621
|
+
assert isinstance(my_node["outputs"], list)
|
622
|
+
outputs = my_node["outputs"]
|
623
|
+
|
624
|
+
dict_output = next(val for val in outputs if isinstance(val, dict) and val["name"] == "dict_value")
|
625
|
+
assert isinstance(dict_output, dict)
|
626
|
+
assert "value" in dict_output
|
627
|
+
assert dict_output["value"] == {
|
628
|
+
"type": "CONSTANT_VALUE",
|
629
|
+
"value": {"type": "JSON", "value": {"key1": "value1", "key2": "value2"}},
|
630
|
+
}
|
631
|
+
|
632
|
+
nested_dict_output = next(val for val in outputs if isinstance(val, dict) and val["name"] == "nested_dict_value")
|
633
|
+
assert isinstance(nested_dict_output, dict)
|
634
|
+
assert "value" in nested_dict_output
|
635
|
+
assert nested_dict_output["value"] == {
|
636
|
+
"type": "CONSTANT_VALUE",
|
637
|
+
"value": {
|
638
|
+
"type": "JSON",
|
639
|
+
"value": {
|
640
|
+
"key1": {"nested_key1": "value1", "nested_key2": "value2"},
|
641
|
+
"key2": {"nested_key1": "value1", "nested_key2": "value2"},
|
642
|
+
},
|
643
|
+
},
|
644
|
+
}
|
645
|
+
|
646
|
+
mixed_dict_output = next(val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_dict_value")
|
647
|
+
assert isinstance(mixed_dict_output, dict)
|
648
|
+
assert "value" in mixed_dict_output
|
649
|
+
assert mixed_dict_output["value"] == {
|
650
|
+
"type": "CONSTANT_VALUE",
|
651
|
+
"value": {"type": "JSON", "value": {"key1": "value1", "key2": {"key3": "value3", "key4": "value4"}}},
|
652
|
+
}
|
653
|
+
|
654
|
+
|
655
|
+
def test_serialize_workflow__dict_reference():
|
656
|
+
# GIVEN a node with a dictionary containing non-constant values (node references)
|
657
|
+
class FirstNode(BaseNode):
|
658
|
+
class Outputs(BaseNode.Outputs):
|
659
|
+
value1: str
|
660
|
+
|
661
|
+
class SecondNode(BaseNode):
|
662
|
+
class Outputs(BaseNode.Outputs):
|
663
|
+
# Dictionary containing a mix of constants and node references
|
664
|
+
mixed_dict = {
|
665
|
+
"key1": "constant1",
|
666
|
+
"key2": FirstNode.Outputs.value1,
|
667
|
+
"key3": "constant2",
|
668
|
+
"key4": FirstNode.Outputs.value1,
|
669
|
+
}
|
670
|
+
mixed_nested_dict = {
|
671
|
+
"key1": {"key1": "constant1", "key2": FirstNode.Outputs.value1},
|
672
|
+
"key2": {"key1": "constant2", "key2": FirstNode.Outputs.value1},
|
673
|
+
}
|
674
|
+
|
675
|
+
# AND a workflow that uses these outputs
|
676
|
+
class MyWorkflow(BaseWorkflow):
|
677
|
+
graph = FirstNode >> SecondNode
|
678
|
+
|
679
|
+
class Outputs(BaseWorkflow.Outputs):
|
680
|
+
mixed_dict_output = SecondNode.Outputs.mixed_dict
|
681
|
+
mixed_nested_dict_output = SecondNode.Outputs.mixed_nested_dict
|
682
|
+
|
683
|
+
# WHEN we serialize it
|
684
|
+
workflow_display = get_workflow_display(workflow_class=MyWorkflow)
|
685
|
+
data = workflow_display.serialize()
|
686
|
+
|
687
|
+
# THEN it should serialize as a CONSTANT_VALUE
|
688
|
+
assert isinstance(data["workflow_raw_data"], dict)
|
689
|
+
assert isinstance(data["workflow_raw_data"]["nodes"], list)
|
690
|
+
second_node = data["workflow_raw_data"]["nodes"][2]
|
691
|
+
|
692
|
+
assert isinstance(second_node, dict)
|
693
|
+
assert "outputs" in second_node
|
694
|
+
assert isinstance(second_node["outputs"], list)
|
695
|
+
|
696
|
+
outputs = second_node["outputs"]
|
697
|
+
mixed_dict_output = next(val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_dict")
|
698
|
+
assert isinstance(mixed_dict_output, dict)
|
699
|
+
assert "value" in mixed_dict_output
|
700
|
+
assert mixed_dict_output["value"] == {
|
701
|
+
"type": "DICTIONARY_REFERENCE",
|
702
|
+
"entries": [
|
703
|
+
{"key": "key1", "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant1"}}},
|
704
|
+
{
|
705
|
+
"key": "key2",
|
706
|
+
"value": {
|
707
|
+
"type": "NODE_OUTPUT",
|
708
|
+
"node_id": "13b4f5c0-e6aa-4ef9-9a1a-79476bc32500",
|
709
|
+
"node_output_id": "50a6bc11-afb3-49f2-879c-b28f5e16d974",
|
710
|
+
},
|
711
|
+
},
|
712
|
+
{"key": "key3", "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant2"}}},
|
713
|
+
{
|
714
|
+
"key": "key4",
|
715
|
+
"value": {
|
716
|
+
"type": "NODE_OUTPUT",
|
717
|
+
"node_id": "13b4f5c0-e6aa-4ef9-9a1a-79476bc32500",
|
718
|
+
"node_output_id": "50a6bc11-afb3-49f2-879c-b28f5e16d974",
|
719
|
+
},
|
720
|
+
},
|
721
|
+
],
|
722
|
+
}
|
723
|
+
|
724
|
+
mixed_nested_dict_output = next(
|
725
|
+
val for val in outputs if isinstance(val, dict) and val["name"] == "mixed_nested_dict"
|
726
|
+
)
|
727
|
+
assert isinstance(mixed_nested_dict_output, dict)
|
728
|
+
assert "value" in mixed_nested_dict_output
|
729
|
+
assert mixed_nested_dict_output["value"] == {
|
730
|
+
"type": "DICTIONARY_REFERENCE",
|
731
|
+
"entries": [
|
732
|
+
{
|
733
|
+
"key": "key1",
|
734
|
+
"value": {
|
735
|
+
"type": "DICTIONARY_REFERENCE",
|
736
|
+
"entries": [
|
737
|
+
{
|
738
|
+
"key": "key1",
|
739
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant1"}},
|
740
|
+
},
|
741
|
+
{
|
742
|
+
"key": "key2",
|
743
|
+
"value": {
|
744
|
+
"type": "NODE_OUTPUT",
|
745
|
+
"node_id": "13b4f5c0-e6aa-4ef9-9a1a-79476bc32500",
|
746
|
+
"node_output_id": "50a6bc11-afb3-49f2-879c-b28f5e16d974",
|
747
|
+
},
|
748
|
+
},
|
749
|
+
],
|
750
|
+
},
|
751
|
+
},
|
752
|
+
{
|
753
|
+
"key": "key2",
|
754
|
+
"value": {
|
755
|
+
"type": "DICTIONARY_REFERENCE",
|
756
|
+
"entries": [
|
757
|
+
{
|
758
|
+
"key": "key1",
|
759
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "constant2"}},
|
760
|
+
},
|
761
|
+
{
|
762
|
+
"key": "key2",
|
763
|
+
"value": {
|
764
|
+
"type": "NODE_OUTPUT",
|
765
|
+
"node_id": "13b4f5c0-e6aa-4ef9-9a1a-79476bc32500",
|
766
|
+
"node_output_id": "50a6bc11-afb3-49f2-879c-b28f5e16d974",
|
767
|
+
},
|
768
|
+
},
|
769
|
+
],
|
770
|
+
},
|
771
|
+
},
|
772
|
+
],
|
773
|
+
}
|
File without changes
|
File without changes
|
File without changes
|