vellum-ai 0.14.46__py3-none-any.whl → 0.14.48__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 (152) hide show
  1. vellum/client/README.md +2 -2
  2. vellum/client/__init__.py +72 -6
  3. vellum/client/core/client_wrapper.py +1 -1
  4. vellum/client/core/file.py +13 -8
  5. vellum/client/core/http_client.py +26 -14
  6. vellum/client/core/pydantic_utilities.py +2 -2
  7. vellum/client/core/request_options.py +3 -0
  8. vellum/client/resources/ad_hoc/client.py +14 -2
  9. vellum/client/resources/container_images/client.py +6 -0
  10. vellum/client/resources/deployments/client.py +12 -0
  11. vellum/client/resources/document_indexes/client.py +18 -0
  12. vellum/client/resources/documents/client.py +6 -0
  13. vellum/client/resources/folder_entities/client.py +6 -0
  14. vellum/client/resources/metric_definitions/client.py +6 -0
  15. vellum/client/resources/prompts/client.py +6 -0
  16. vellum/client/resources/sandboxes/client.py +12 -0
  17. vellum/client/resources/test_suite_runs/client.py +6 -0
  18. vellum/client/resources/test_suites/client.py +2 -2
  19. vellum/client/resources/workflow_deployments/client.py +6 -0
  20. vellum/client/resources/workflow_sandboxes/client.py +6 -0
  21. vellum/client/resources/workflows/client.py +6 -4
  22. vellum/client/resources/workspace_secrets/client.py +6 -0
  23. vellum/client/types/api_request_parent_context.py +0 -6
  24. vellum/client/types/array_input.py +0 -5
  25. vellum/client/types/code_execution_node_array_result.py +0 -5
  26. vellum/client/types/code_execution_node_result.py +0 -5
  27. vellum/client/types/code_execution_node_result_data.py +0 -5
  28. vellum/client/types/code_executor_response.py +0 -5
  29. vellum/client/types/create_test_suite_test_case_request.py +0 -5
  30. vellum/client/types/deployment_history_item.py +0 -5
  31. vellum/client/types/deployment_read.py +0 -5
  32. vellum/client/types/execute_workflow_response.py +0 -5
  33. vellum/client/types/execution_array_vellum_value.py +0 -5
  34. vellum/client/types/external_test_case_execution.py +0 -5
  35. vellum/client/types/external_test_case_execution_request.py +0 -5
  36. vellum/client/types/fulfilled_execute_workflow_workflow_result_event.py +0 -7
  37. vellum/client/types/fulfilled_workflow_node_result_event.py +0 -5
  38. vellum/client/types/initiated_workflow_node_result_event.py +0 -5
  39. vellum/client/types/metadata_filter_config_request.py +0 -5
  40. vellum/client/types/metric_definition_execution.py +0 -5
  41. vellum/client/types/metric_definition_history_item.py +0 -5
  42. vellum/client/types/named_test_case_array_variable_value.py +0 -5
  43. vellum/client/types/named_test_case_array_variable_value_request.py +0 -7
  44. vellum/client/types/node_execution_fulfilled_event.py +0 -11
  45. vellum/client/types/node_execution_initiated_event.py +0 -11
  46. vellum/client/types/node_execution_paused_event.py +0 -11
  47. vellum/client/types/node_execution_rejected_event.py +0 -11
  48. vellum/client/types/node_execution_resumed_event.py +0 -11
  49. vellum/client/types/node_execution_span.py +0 -11
  50. vellum/client/types/node_execution_streaming_event.py +0 -11
  51. vellum/client/types/node_input_compiled_array_value.py +0 -5
  52. vellum/client/types/node_output_compiled_array_value.py +0 -5
  53. vellum/client/types/node_parent_context.py +0 -6
  54. vellum/client/types/paginated_slim_deployment_read_list.py +0 -5
  55. vellum/client/types/paginated_slim_workflow_deployment_list.py +0 -5
  56. vellum/client/types/paginated_test_suite_run_execution_list.py +0 -5
  57. vellum/client/types/paginated_test_suite_test_case_list.py +0 -5
  58. vellum/client/types/prompt_deployment_parent_context.py +0 -6
  59. vellum/client/types/prompt_exec_config.py +0 -6
  60. vellum/client/types/rejected_workflow_node_result_event.py +0 -5
  61. vellum/client/types/replace_test_suite_test_case_request.py +0 -5
  62. vellum/client/types/search_filters_request.py +0 -7
  63. vellum/client/types/search_request_options_request.py +0 -7
  64. vellum/client/types/slim_deployment_read.py +0 -5
  65. vellum/client/types/slim_workflow_deployment.py +0 -5
  66. vellum/client/types/slim_workflow_execution_read.py +0 -12
  67. vellum/client/types/span_link.py +0 -6
  68. vellum/client/types/streaming_workflow_node_result_event.py +0 -5
  69. vellum/client/types/templating_node_array_result.py +0 -5
  70. vellum/client/types/templating_node_result.py +0 -5
  71. vellum/client/types/templating_node_result_data.py +0 -5
  72. vellum/client/types/terminal_node_array_result.py +0 -5
  73. vellum/client/types/terminal_node_result.py +0 -5
  74. vellum/client/types/terminal_node_result_data.py +0 -5
  75. vellum/client/types/test_case_array_variable_value.py +0 -5
  76. vellum/client/types/test_suite_run_execution.py +0 -5
  77. vellum/client/types/test_suite_run_execution_array_output.py +0 -5
  78. vellum/client/types/test_suite_run_execution_metric_result.py +0 -5
  79. vellum/client/types/test_suite_run_external_exec_config.py +0 -5
  80. vellum/client/types/test_suite_run_external_exec_config_data.py +0 -5
  81. vellum/client/types/test_suite_run_external_exec_config_data_request.py +0 -7
  82. vellum/client/types/test_suite_run_external_exec_config_request.py +0 -7
  83. vellum/client/types/test_suite_run_metric_array_output.py +0 -5
  84. vellum/client/types/test_suite_run_read.py +0 -5
  85. vellum/client/types/test_suite_test_case.py +0 -5
  86. vellum/client/types/test_suite_test_case_create_bulk_operation_request.py +0 -7
  87. vellum/client/types/test_suite_test_case_replace_bulk_operation_request.py +0 -7
  88. vellum/client/types/test_suite_test_case_upsert_bulk_operation_request.py +0 -7
  89. vellum/client/types/upsert_test_suite_test_case_request.py +0 -5
  90. vellum/client/types/vellum_value_logical_condition_group_request.py +0 -3
  91. vellum/client/types/vellum_value_logical_condition_request.py +0 -5
  92. vellum/client/types/vellum_variable.py +0 -5
  93. vellum/client/types/workflow_deployment_event_executions_response.py +0 -26
  94. vellum/client/types/workflow_deployment_history_item.py +0 -5
  95. vellum/client/types/workflow_deployment_parent_context.py +0 -6
  96. vellum/client/types/workflow_deployment_read.py +0 -5
  97. vellum/client/types/workflow_deployment_release.py +0 -5
  98. vellum/client/types/workflow_deployment_release_workflow_version.py +0 -5
  99. vellum/client/types/workflow_event_execution_read.py +0 -12
  100. vellum/client/types/workflow_execution_actual.py +0 -5
  101. vellum/client/types/workflow_execution_fulfilled_event.py +0 -11
  102. vellum/client/types/workflow_execution_initiated_event.py +0 -11
  103. vellum/client/types/workflow_execution_node_result_event.py +0 -5
  104. vellum/client/types/workflow_execution_paused_event.py +0 -11
  105. vellum/client/types/workflow_execution_rejected_event.py +0 -11
  106. vellum/client/types/workflow_execution_resumed_event.py +0 -11
  107. vellum/client/types/workflow_execution_snapshotted_event.py +0 -13
  108. vellum/client/types/workflow_execution_span.py +0 -11
  109. vellum/client/types/workflow_execution_streaming_event.py +0 -11
  110. vellum/client/types/workflow_execution_view_online_eval_metric_result.py +0 -7
  111. vellum/client/types/workflow_execution_workflow_result_event.py +0 -5
  112. vellum/client/types/workflow_output_array.py +0 -5
  113. vellum/client/types/workflow_parent_context.py +0 -6
  114. vellum/client/types/workflow_result_event.py +0 -5
  115. vellum/client/types/workflow_result_event_output_data_array.py +0 -5
  116. vellum/client/types/workflow_sandbox_parent_context.py +0 -6
  117. vellum/workflows/nodes/bases/base.py +26 -6
  118. vellum/workflows/nodes/bases/tests/test_base_node.py +30 -0
  119. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +50 -0
  120. vellum/workflows/nodes/utils.py +5 -1
  121. vellum/workflows/types/code_execution_node_wrappers.py +5 -0
  122. {vellum_ai-0.14.46.dist-info → vellum_ai-0.14.48.dist-info}/METADATA +1 -1
  123. {vellum_ai-0.14.46.dist-info → vellum_ai-0.14.48.dist-info}/RECORD +152 -152
  124. vellum_cli/__init__.py +3 -2
  125. vellum_cli/image_push.py +15 -3
  126. vellum_cli/tests/test_image_push.py +109 -0
  127. vellum_ee/workflows/display/nodes/base_node_display.py +25 -9
  128. vellum_ee/workflows/display/nodes/get_node_display_class.py +4 -5
  129. vellum_ee/workflows/display/nodes/vellum/api_node.py +11 -0
  130. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +5 -0
  131. vellum_ee/workflows/display/nodes/vellum/error_node.py +22 -16
  132. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +2 -0
  133. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +47 -13
  134. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +2 -0
  135. vellum_ee/workflows/display/nodes/vellum/map_node.py +2 -0
  136. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +2 -0
  137. vellum_ee/workflows/display/nodes/vellum/search_node.py +8 -0
  138. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +1 -0
  139. vellum_ee/workflows/display/nodes/vellum/templating_node.py +2 -0
  140. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +1 -1
  141. vellum_ee/workflows/display/nodes/vellum/tests/test_error_node.py +4 -0
  142. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_deployment_node.py +4 -3
  143. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +67 -2
  144. vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py +5 -4
  145. vellum_ee/workflows/display/nodes/vellum/tests/test_templating_node.py +1 -1
  146. vellum_ee/workflows/display/tests/test_base_workflow_display.py +44 -0
  147. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +2 -4
  148. vellum_ee/workflows/display/utils/expressions.py +31 -4
  149. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +162 -0
  150. {vellum_ai-0.14.46.dist-info → vellum_ai-0.14.48.dist-info}/LICENSE +0 -0
  151. {vellum_ai-0.14.46.dist-info → vellum_ai-0.14.48.dist-info}/WHEEL +0 -0
  152. {vellum_ai-0.14.46.dist-info → vellum_ai-0.14.48.dist-info}/entry_points.txt +0 -0
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):
@@ -9,6 +9,7 @@ from typing import (
9
9
  ForwardRef,
10
10
  Generic,
11
11
  Optional,
12
+ Set,
12
13
  Tuple,
13
14
  Type,
14
15
  TypeVar,
@@ -24,6 +25,7 @@ from vellum.workflows.nodes.bases.base import BaseNode
24
25
  from vellum.workflows.nodes.utils import get_unadorned_node, get_wrapped_node
25
26
  from vellum.workflows.ports import Port
26
27
  from vellum.workflows.references import OutputReference
28
+ from vellum.workflows.references.node import NodeReference
27
29
  from vellum.workflows.types.core import JsonArray, JsonObject
28
30
  from vellum.workflows.types.generics import NodeType
29
31
  from vellum.workflows.types.utils import get_original_base
@@ -33,7 +35,7 @@ from vellum.workflows.utils.vellum_variables import primitive_type_to_vellum_var
33
35
  from vellum_ee.workflows.display.editor.types import NodeDisplayComment, NodeDisplayData
34
36
  from vellum_ee.workflows.display.nodes.get_node_display_class import get_node_display_class
35
37
  from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay, PortDisplay, PortDisplayOverrides
36
- from vellum_ee.workflows.display.utils.expressions import serialize_condition, serialize_value
38
+ from vellum_ee.workflows.display.utils.expressions import serialize_value
37
39
  from vellum_ee.workflows.display.utils.registry import register_node_display_class
38
40
 
39
41
  if TYPE_CHECKING:
@@ -98,11 +100,18 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
98
100
  # Default values set by the metaclass
99
101
  output_display: Dict[OutputReference, NodeOutputDisplay]
100
102
  port_displays: Dict[Port, PortDisplayOverrides] = {}
101
- node_input_ids_by_name: ClassVar[Dict[str, UUID]] = {}
103
+ attribute_ids_by_name: ClassVar[Dict[str, UUID]] = {}
102
104
 
105
+ # START: Attributes for backwards compatible serialization
103
106
  # Used to explicitly set the target handle id for a node
104
107
  # Once all nodes are Generic Nodes, we may replace this with a trigger_id or trigger attribute
105
108
  target_handle_id: ClassVar[Optional[UUID]] = None
109
+ # Used to explicitly set the input ids for each node input
110
+ node_input_ids_by_name: ClassVar[Dict[str, UUID]] = {}
111
+ # Used by each class extending BaseNodeDisplay to specify which attributes are meant to be serialized
112
+ # as the former `"inputs"` field
113
+ __serializable_inputs__: Set[NodeReference] = set()
114
+ # END: Attributes for backwards compatible serialization
106
115
 
107
116
  def serialize(self, display_context: "WorkflowDisplayContext", **kwargs: Any) -> JsonObject:
108
117
  node = self._node
@@ -114,7 +123,11 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
114
123
  # We don't need to serialize generic node attributes containing a subworkflow
115
124
  continue
116
125
 
117
- id = str(uuid4_from_hash(f"{node_id}|{attribute.name}"))
126
+ id = (
127
+ str(self.attribute_ids_by_name[attribute.name])
128
+ if self.attribute_ids_by_name
129
+ else str(uuid4_from_hash(f"{node_id}|{attribute.name}"))
130
+ )
118
131
  try:
119
132
  attributes.append(
120
133
  {
@@ -191,9 +204,7 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
191
204
  "id": id,
192
205
  "name": port.name,
193
206
  "type": port._condition_type.value,
194
- "expression": (
195
- serialize_condition(display_context, port._condition) if port._condition else None
196
- ),
207
+ "expression": (serialize_value(display_context, port._condition) if port._condition else None),
197
208
  }
198
209
  )
199
210
  else:
@@ -249,10 +260,15 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
249
260
 
250
261
  def get_source_handle_id(self, port_displays: Dict[Port, PortDisplay]) -> UUID:
251
262
  unadorned_node = get_unadorned_node(self._node)
252
- 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
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.")
253
270
 
254
- default_port_display = port_displays[default_port]
255
- return default_port_display.id
271
+ return port_displays[first_port].id
256
272
 
257
273
  def get_trigger_id(self) -> UUID:
258
274
  return self.get_target_handle_id()
@@ -2,7 +2,6 @@ import types
2
2
  from uuid import UUID
3
3
  from typing import TYPE_CHECKING, Any, Dict, Generic, Type, TypeVar
4
4
 
5
- from vellum.workflows.descriptors.base import BaseDescriptor
6
5
  from vellum.workflows.types.generics import NodeType
7
6
  from vellum.workflows.utils.uuids import uuid4_from_hash
8
7
  from vellum_ee.workflows.display.utils.registry import get_from_node_display_registry
@@ -30,14 +29,14 @@ def get_node_display_class(node_class: Type[NodeType]) -> Type["BaseNodeDisplay"
30
29
  node_input_ids_by_name.update(_get_node_input_ids_by_ref(f"{path}.{key}", value))
31
30
  return node_input_ids_by_name
32
31
 
33
- if isinstance(inst, BaseDescriptor):
34
- return {path: uuid4_from_hash(f"{node_class.__id__}|{path}")}
35
-
36
- return {}
32
+ return {path: uuid4_from_hash(f"{node_class.__id__}|{path}")}
37
33
 
38
34
  def exec_body(ns: Dict):
39
35
  node_input_ids_by_name: Dict[str, UUID] = {}
40
36
  for ref in node_class:
37
+ if ref not in base_node_display_class.__serializable_inputs__:
38
+ continue
39
+
41
40
  node_input_ids_by_name.update(_get_node_input_ids_by_ref(ref.name, ref.instance))
42
41
 
43
42
  if node_input_ids_by_name:
@@ -20,6 +20,17 @@ class BaseAPINodeDisplay(BaseNodeDisplay[_APINodeType], Generic[_APINodeType]):
20
20
  # A mapping between node input keys and their ids for inputs representing additional header values
21
21
  additional_header_value_input_ids: ClassVar[Optional[Dict[str, UUID]]] = None
22
22
 
23
+ __serializable_inputs__ = {
24
+ APINode.url,
25
+ APINode.method,
26
+ APINode.json,
27
+ APINode.headers,
28
+ APINode.api_key_header_key,
29
+ APINode.api_key_header_value,
30
+ APINode.bearer_token_value,
31
+ APINode.authorization_type,
32
+ }
33
+
23
34
  def serialize(
24
35
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs: Any
25
36
  ) -> JsonObject:
@@ -18,6 +18,11 @@ class BaseCodeExecutionNodeDisplay(BaseNodeDisplay[_CodeExecutionNodeType], Gene
18
18
  output_id: ClassVar[Optional[UUID]] = None
19
19
  log_output_id: ClassVar[Optional[UUID]] = None
20
20
 
21
+ __serializable_inputs__ = {
22
+ CodeExecutionNode.code,
23
+ CodeExecutionNode.code_inputs,
24
+ }
25
+
21
26
  def serialize(
22
27
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
23
28
  ) -> JsonObject:
@@ -1,5 +1,5 @@
1
1
  from uuid import UUID
2
- from typing import ClassVar, Generic, Optional, TypeVar
2
+ from typing import Any, ClassVar, Generic, Optional, TypeVar
3
3
 
4
4
  from vellum.workflows.nodes import ErrorNode
5
5
  from vellum.workflows.types.core import JsonObject
@@ -9,47 +9,53 @@ from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
9
9
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
10
10
 
11
11
  _ErrorNodeType = TypeVar("_ErrorNodeType", bound=ErrorNode)
12
+ LEGACY_INPUT_NAME = "error_source_input_id"
12
13
 
13
14
 
14
15
  class BaseErrorNodeDisplay(BaseNodeDisplay[_ErrorNodeType], Generic[_ErrorNodeType]):
16
+ # DEPRECATED: Remove in 0.15.0 once removed from the vellum-side
15
17
  error_output_id: ClassVar[Optional[UUID]] = None
18
+ # DEPRECATED: Remove in 0.15.0 once removed from the vellum-side
19
+ name: ClassVar[Optional[str]] = None
16
20
 
17
- name: ClassVar[str] = "error-node"
21
+ __serializable_inputs__ = {ErrorNode.error}
18
22
 
19
- def serialize(
20
- self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
21
- ) -> JsonObject:
23
+ def serialize(self, display_context: WorkflowDisplayContext, **kwargs) -> JsonObject:
22
24
  node_id = self.node_id
23
- error_source_input_id = self.node_input_ids_by_name.get("error_source_input_id")
25
+ error_source_input_id = self.node_input_ids_by_name.get(
26
+ ErrorNode.error.name,
27
+ ) or self.node_input_ids_by_name.get(LEGACY_INPUT_NAME)
24
28
 
25
29
  error_attribute = raise_if_descriptor(self._node.error)
26
- input_values_by_name = {
27
- "error_source_input_id": error_attribute,
28
- }
29
30
 
30
31
  node_inputs = [
31
32
  create_node_input(
32
33
  node_id=node_id,
33
- input_name=variable_name,
34
- value=variable_value,
34
+ input_name=LEGACY_INPUT_NAME,
35
+ value=error_attribute,
35
36
  display_context=display_context,
36
- input_id=self.node_input_ids_by_name.get(variable_name),
37
+ input_id=error_source_input_id,
37
38
  )
38
- for variable_name, variable_value in input_values_by_name.items()
39
39
  ]
40
40
 
41
- return {
41
+ node_data: dict[str, Any] = {
42
42
  "id": str(node_id),
43
43
  "type": "ERROR",
44
44
  "inputs": [node_input.dict() for node_input in node_inputs],
45
45
  "data": {
46
- "name": self.name,
47
46
  "label": self.label,
48
47
  "target_handle_id": str(self.get_target_handle_id()),
49
48
  "error_source_input_id": str(error_source_input_id),
50
- "error_output_id": str(self.error_output_id),
51
49
  },
52
50
  "display_data": self.get_display_data().dict(),
53
51
  "base": self.get_base().dict(),
54
52
  "definition": self.get_definition().dict(),
55
53
  }
54
+
55
+ if self.name:
56
+ node_data["data"]["name"] = self.name
57
+
58
+ if self.error_output_id:
59
+ node_data["data"]["error_output_id"] = str(self.error_output_id)
60
+
61
+ return node_data
@@ -12,6 +12,8 @@ _GuardrailNodeType = TypeVar("_GuardrailNodeType", bound=GuardrailNode)
12
12
 
13
13
 
14
14
  class BaseGuardrailNodeDisplay(BaseNodeDisplay[_GuardrailNodeType], Generic[_GuardrailNodeType]):
15
+ __serializable_inputs__ = {GuardrailNode.metric_inputs}
16
+
15
17
  def serialize(
16
18
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
17
19
  ) -> JsonObject:
@@ -10,6 +10,7 @@ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
10
10
  from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
11
11
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
12
12
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
13
+ from vellum_ee.workflows.display.utils.expressions import serialize_value
13
14
  from vellum_ee.workflows.display.utils.vellum import infer_vellum_variable_type
14
15
  from vellum_ee.workflows.display.vellum import NodeInput
15
16
 
@@ -17,6 +18,8 @@ _InlinePromptNodeType = TypeVar("_InlinePromptNodeType", bound=InlinePromptNode)
17
18
 
18
19
 
19
20
  class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generic[_InlinePromptNodeType]):
21
+ __serializable_inputs__ = {InlinePromptNode.prompt_inputs}
22
+
20
23
  def serialize(
21
24
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
22
25
  ) -> JsonObject:
@@ -26,12 +29,14 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
26
29
  node_inputs, prompt_inputs = self._generate_node_and_prompt_inputs(node_id, node, display_context)
27
30
  input_variable_id_by_name = {prompt_input.key: prompt_input.id for prompt_input in prompt_inputs}
28
31
 
29
- _, output_display = display_context.global_node_output_displays[node.Outputs.text]
30
- _, array_display = display_context.global_node_output_displays[node.Outputs.results]
31
- _, json_display = display_context.global_node_output_displays[node.Outputs.json]
32
- node_blocks = raise_if_descriptor(node.blocks)
32
+ output_display = self.output_display[node.Outputs.text]
33
+ array_display = self.output_display[node.Outputs.results]
34
+ json_display = self.output_display[node.Outputs.json]
35
+ node_blocks = raise_if_descriptor(node.blocks) or []
33
36
  function_definitions = raise_if_descriptor(node.functions)
34
37
 
38
+ ml_model = str(raise_if_descriptor(node.ml_model))
39
+
35
40
  blocks: list = [
36
41
  self._generate_prompt_block(block, input_variable_id_by_name, [i]) for i, block in enumerate(node_blocks)
37
42
  ]
@@ -42,7 +47,7 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
42
47
  )
43
48
  blocks.extend(functions)
44
49
 
45
- return {
50
+ serialized_node: JsonObject = {
46
51
  "id": str(node_id),
47
52
  "type": "PROMPT",
48
53
  "inputs": [node_input.dict() for node_input in node_inputs],
@@ -62,7 +67,7 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
62
67
  "blocks": blocks,
63
68
  },
64
69
  },
65
- "ml_model_name": raise_if_descriptor(node.ml_model),
70
+ "ml_model_name": ml_model,
66
71
  },
67
72
  "display_data": self.get_display_data().dict(),
68
73
  "base": self.get_base().dict(),
@@ -74,6 +79,10 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
74
79
  ],
75
80
  "ports": self.serialize_ports(display_context),
76
81
  }
82
+ attributes = self._serialize_attributes(display_context)
83
+ if attributes:
84
+ serialized_node["attributes"] = attributes
85
+ return serialized_node
77
86
 
78
87
  def _generate_node_and_prompt_inputs(
79
88
  self,
@@ -127,6 +136,7 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
127
136
  path: List[int],
128
137
  ) -> JsonObject:
129
138
  block: JsonObject
139
+ block_id = uuid4_from_hash(f"{self.node_id}-{prompt_block.block_type}-{'-'.join([str(i) for i in path])}")
130
140
  if prompt_block.block_type == "JINJA":
131
141
  block = {
132
142
  "block_type": "JINJA",
@@ -162,10 +172,21 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
162
172
  }
163
173
 
164
174
  elif prompt_block.block_type == "VARIABLE":
165
- block = {
166
- "block_type": "VARIABLE",
167
- "input_variable_id": input_variable_id_by_name[prompt_block.input_variable],
168
- }
175
+ input_variable_id = input_variable_id_by_name.get(prompt_block.input_variable)
176
+ if input_variable_id:
177
+ block = {
178
+ "block_type": "VARIABLE",
179
+ "input_variable_id": input_variable_id,
180
+ }
181
+ else:
182
+ # Even though this will likely fail in runtime, we want to allow serialization to succeed
183
+ # in case the block is work in progress or the node is not yet part of the graph
184
+ block = {
185
+ "block_type": "VARIABLE",
186
+ "input_variable_id": str(
187
+ uuid4_from_hash(f"{block_id}-input_variable-{prompt_block.input_variable}")
188
+ ),
189
+ }
169
190
 
170
191
  elif prompt_block.block_type == "PLAIN_TEXT":
171
192
  block = {
@@ -184,9 +205,7 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
184
205
  else:
185
206
  raise NotImplementedError(f"Serialization for prompt block type {prompt_block.block_type} not implemented")
186
207
 
187
- block["id"] = str(
188
- uuid4_from_hash(f"{self.node_id}-{prompt_block.block_type}-{'-'.join([str(i) for i in path])}")
189
- )
208
+ block["id"] = str(block_id)
190
209
  if prompt_block.cache_config:
191
210
  block["cache_config"] = prompt_block.cache_config.dict()
192
211
  else:
@@ -198,3 +217,18 @@ class BaseInlinePromptNodeDisplay(BaseNodeDisplay[_InlinePromptNodeType], Generi
198
217
  block["state"] = "ENABLED"
199
218
 
200
219
  return block
220
+
221
+ def _serialize_attributes(self, display_context: "WorkflowDisplayContext"):
222
+ attribute_instances_by_name = {}
223
+ for attribute in self._node:
224
+ if attribute.name in self.attribute_ids_by_name:
225
+ attribute_instances_by_name[attribute.name] = attribute.instance
226
+
227
+ return [
228
+ {
229
+ "id": str(attr_id),
230
+ "name": attr_name,
231
+ "value": serialize_value(display_context, attribute_instances_by_name[attr_name]),
232
+ }
233
+ for attr_name, attr_id in self.attribute_ids_by_name.items()
234
+ ]
@@ -21,6 +21,8 @@ class BaseInlineSubworkflowNodeDisplay(
21
21
  ):
22
22
  workflow_input_ids_by_name: ClassVar[Dict[str, UUID]] = {}
23
23
 
24
+ __serializable_inputs__ = {InlineSubworkflowNode.subworkflow_inputs}
25
+
24
26
  def serialize(
25
27
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
26
28
  ) -> JsonObject:
@@ -13,6 +13,8 @@ _MapNodeType = TypeVar("_MapNodeType", bound=MapNode)
13
13
 
14
14
 
15
15
  class BaseMapNodeDisplay(BaseAdornmentNodeDisplay[_MapNodeType], Generic[_MapNodeType]):
16
+ __serializable_inputs__ = {MapNode.items} # type: ignore[misc]
17
+
16
18
  def serialize(
17
19
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
18
20
  ) -> JsonObject:
@@ -13,6 +13,8 @@ _PromptDeploymentNodeType = TypeVar("_PromptDeploymentNodeType", bound=PromptDep
13
13
 
14
14
 
15
15
  class BasePromptDeploymentNodeDisplay(BaseNodeDisplay[_PromptDeploymentNodeType], Generic[_PromptDeploymentNodeType]):
16
+ __serializable_inputs__ = {PromptDeploymentNode.prompt_inputs}
17
+
16
18
  def serialize(
17
19
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
18
20
  ) -> JsonObject:
@@ -32,6 +32,14 @@ class BaseSearchNodeDisplay(BaseNodeDisplay[_SearchNodeType], Generic[_SearchNod
32
32
  # A mapping between the id of the operand (e.g. "lhs_variable_id" or "rhs_variable_id") and the id of the node input
33
33
  # that the operand is pointing to.
34
34
  metadata_filter_input_id_by_operand_id: Dict[UUID, UUID] = {}
35
+ __serializable_inputs__ = {
36
+ SearchNode.query,
37
+ SearchNode.document_index,
38
+ SearchNode.weights,
39
+ SearchNode.chunk_separator,
40
+ SearchNode.limit,
41
+ SearchNode.result_merging,
42
+ }
35
43
 
36
44
  def serialize(
37
45
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
@@ -14,6 +14,7 @@ _SubworkflowDeploymentNodeType = TypeVar("_SubworkflowDeploymentNodeType", bound
14
14
  class BaseSubworkflowDeploymentNodeDisplay(
15
15
  BaseNodeDisplay[_SubworkflowDeploymentNodeType], Generic[_SubworkflowDeploymentNodeType]
16
16
  ):
17
+ __serializable_inputs__ = {SubworkflowDeploymentNode.subworkflow_inputs}
17
18
 
18
19
  def serialize(
19
20
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
@@ -15,6 +15,8 @@ TEMPLATE_INPUT_NAME = TemplatingNode.template.name
15
15
 
16
16
 
17
17
  class BaseTemplatingNodeDisplay(BaseNodeDisplay[_TemplatingNodeType], Generic[_TemplatingNodeType]):
18
+ __serializable_inputs__ = {TemplatingNode.inputs}
19
+
18
20
  def serialize(
19
21
  self, display_context: WorkflowDisplayContext, error_output_id: Optional[UUID] = None, **kwargs
20
22
  ) -> JsonObject:
@@ -29,7 +29,7 @@ def _display_class_with_node_input_ids_by_name_with_inputs_prefix(Node: Type[Cod
29
29
  @pytest.mark.parametrize(
30
30
  ["GetDisplayClass", "expected_input_id"],
31
31
  [
32
- (_no_display_class, "e3cdb222-324e-4ad1-abb2-bdd7881b3a0e"),
32
+ (_no_display_class, "a5dbe403-0b00-4df6-b8f7-ed5f7794b003"),
33
33
  (_display_class_with_node_input_ids_by_name, "fba6a4d5-835a-4e99-afb7-f6a4aed15110"),
34
34
  (_display_class_with_node_input_ids_by_name_with_inputs_prefix, "fba6a4d5-835a-4e99-afb7-f6a4aed15110"),
35
35
  ],
@@ -41,3 +41,7 @@ def test_error_node_display__serialize_with_vellum_error() -> None:
41
41
  }
42
42
  ],
43
43
  }
44
+
45
+ # AND we serialize the DEPRECATED fields
46
+ assert "error_output_id" not in serialized_node["data"]
47
+ assert "name" not in serialized_node["data"]
@@ -5,6 +5,7 @@ from typing import Type
5
5
 
6
6
  from vellum.workflows import BaseWorkflow
7
7
  from vellum.workflows.nodes import PromptDeploymentNode
8
+ from vellum_ee.workflows.display.nodes.vellum.prompt_deployment_node import BasePromptDeploymentNodeDisplay
8
9
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
9
10
 
10
11
 
@@ -13,14 +14,14 @@ def _no_display_class(Node: Type[PromptDeploymentNode]): # type: ignore
13
14
 
14
15
 
15
16
  def _display_class_with_node_input_ids_by_name(Node: Type[PromptDeploymentNode]):
16
- class PromptDeploymentNodeDisplay(PromptDeploymentNode[Node]): # type: ignore[valid-type]
17
+ class PromptDeploymentNodeDisplay(BasePromptDeploymentNodeDisplay[Node]): # type: ignore[valid-type]
17
18
  node_input_ids_by_name = {"foo": UUID("6037747a-1d35-4094-b363-4369fc92c5d4")}
18
19
 
19
20
  return PromptDeploymentNodeDisplay
20
21
 
21
22
 
22
23
  def _display_class_with_node_input_ids_by_name_with_inputs_prefix(Node: Type[PromptDeploymentNode]):
23
- class PromptDeploymentNodeDisplay(PromptDeploymentNode[Node]): # type: ignore[valid-type]
24
+ class PromptDeploymentNodeDisplay(BasePromptDeploymentNodeDisplay[Node]): # type: ignore[valid-type]
24
25
  node_input_ids_by_name = {"prompt_inputs.foo": UUID("6037747a-1d35-4094-b363-4369fc92c5d4")}
25
26
 
26
27
  return PromptDeploymentNodeDisplay
@@ -51,7 +52,7 @@ def mock_fetch_deployment(mocker):
51
52
  @pytest.mark.parametrize(
52
53
  ["GetDisplayClass", "expected_input_id"],
53
54
  [
54
- (_no_display_class, "6037747a-1d35-4094-b363-4369fc92c5d4"),
55
+ (_no_display_class, "016187d6-2830-4256-a61d-e52f9bf6355e"),
55
56
  (_display_class_with_node_input_ids_by_name, "6037747a-1d35-4094-b363-4369fc92c5d4"),
56
57
  (_display_class_with_node_input_ids_by_name_with_inputs_prefix, "6037747a-1d35-4094-b363-4369fc92c5d4"),
57
58
  ],