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.
Files changed (22) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/workflows/nodes/displayable/code_execution_node/node.py +6 -5
  3. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +112 -1
  4. vellum/workflows/nodes/displayable/code_execution_node/utils.py +23 -2
  5. vellum/workflows/types/code_execution_node_wrappers.py +10 -2
  6. {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/METADATA +1 -1
  7. {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/RECORD +22 -22
  8. vellum_cli/__init__.py +3 -2
  9. vellum_cli/image_push.py +15 -3
  10. vellum_cli/tests/test_image_push.py +109 -0
  11. vellum_ee/workflows/display/nodes/base_node_display.py +8 -3
  12. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -2
  13. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +1 -1
  14. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +43 -0
  15. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +31 -0
  16. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +51 -5
  17. vellum_ee/workflows/display/utils/expressions.py +50 -4
  18. vellum_ee/workflows/display/utils/vellum.py +10 -4
  19. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +346 -0
  20. {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/LICENSE +0 -0
  21. {vellum_ai-0.14.47.dist-info → vellum_ai-0.14.49.dist-info}/WHEEL +0 -0
  22. {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.47",
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 exc_info.value.message == "dict has no key: 'invalid'"
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
- exec(execution_code, exec_globals)
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=str(e),
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
- raise KeyError(key)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.47
3
+ Version: 0.14.49
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -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=7aO9XFnaEVRiVshn86cFudebFUccT-gV8xIARJWqKYo,12257
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=8DDvRDJEZ-FukUCqGW1827bg1ybF4xBbx9WyqWYQE-g,6816
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=i3lJuW8nFRwL1M1OF6752IZYvGAFgKmkB2hd_rYlsmg,2028
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=FGoFpqawVf7mPbMFDZdL9p3qy951Y1RZq8No9NQ8p8Y,15213
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=yWYK3bOK1hNt5jvqzSwUy4RwCHCP3lHYSU1jum7zxO8,4561
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=mIwK1gwW7ljrN3dEVCsZlvaGVZhdlbS-hXCMgPf6Ebw,10362
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=-b09X3dgtRxnAuqMTp_q3T_tfE_3lRGkgohWmy8w9lE,3880
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=94ab2dL1adN4ScPxoX3TiYvMguBn0OmwFRq2oCzs7mQ,7468
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=m9KpVN55_bHiooZOJLm7doRewhcTK53weVYRiglCJX0,19578
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=voNX6ZLuLIVu8cCjqASrSnlCZuUL9TVbWkWD6zAMmnE,10644
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=HDe1vtIaU35OEg4oC1KC6WNwhw5Laf_mNURorzbeutQ,5418
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=U0LsUqjJ-5qrkqgFsvdWzqWX7OHEMzsDCWrXR1xyzp0,15599
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=2b05KSUjKpa8HkExXYFatDZrzBB_PJoTnvwdO0cZNvc,1869
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=Ko_Dy17AjfSx2A4u5Xno5R0KH2p5akEHq8L0rQkySGs,9576
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=Vl4huLXAQNNveDJTg1HF-scxbBVNuuElDUn-IRB6pkg,28343
1595
- vellum/workflows/nodes/displayable/code_execution_node/utils.py,sha256=oCI8HS_Y-hsNhvGIC8HgbIzKIZeWOkXMLaknDRf-qus,2928
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=li3EuMWq6s3QrZ63Z36kSsvJudWjIwTVpBrZ594LVJ8,2169
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.47.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1705
- vellum_ai-0.14.47.dist-info/METADATA,sha256=ljQBBvIKHzaM01hSSpxXWdIyQPl4GpYpLimvOm1lxCw,5484
1706
- vellum_ai-0.14.47.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1707
- vellum_ai-0.14.47.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1708
- vellum_ai-0.14.47.dist-info/RECORD,,
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
- def image_push(image: str, tag: Optional[List[str]] = None) -> None:
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
- vellum_client = create_vellum_client()
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
- default_port_display = port_displays[default_port]
266
- return default_port_display.id
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
- _, output_display = display_context.global_node_output_displays[node.Outputs.result]
83
- _, log_output_display = display_context.global_node_output_displays[node.Outputs.log]
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
- with pytest.raises(Exception) as exc_info:
261
- workflow_display.serialize()
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
- # THEN we should raise a user facing error
264
- assert str(exc_info.value) == "Failed to serialize attribute 'attr': Nested references are not supported."
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, dict) and any(isinstance(v, BaseDescriptor) for v in value.values()):
239
- raise ValueError("Nested references are not supported.")
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
- workspace_secret = display_context.client.workspace_secrets.retrieve(
121
- id=value.name,
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=str(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
+ }