vellum-ai 0.14.45__py3-none-any.whl → 0.14.47__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 (162) 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/core/try_node/node.py +3 -6
  120. vellum/workflows/nodes/core/try_node/tests/test_node.py +0 -24
  121. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +8 -14
  122. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +112 -0
  123. vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -54
  124. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +5 -6
  125. vellum/workflows/nodes/utils.py +8 -0
  126. vellum/workflows/types/code_execution_node_wrappers.py +69 -0
  127. vellum/workflows/vellum_client.py +19 -7
  128. {vellum_ai-0.14.45.dist-info → vellum_ai-0.14.47.dist-info}/METADATA +1 -1
  129. {vellum_ai-0.14.45.dist-info → vellum_ai-0.14.47.dist-info}/RECORD +162 -161
  130. vellum_cli/config.py +7 -2
  131. vellum_cli/push.py +5 -1
  132. vellum_cli/tests/test_push.py +192 -8
  133. vellum_ee/workflows/display/nodes/base_node_display.py +17 -6
  134. vellum_ee/workflows/display/nodes/get_node_display_class.py +4 -5
  135. vellum_ee/workflows/display/nodes/vellum/api_node.py +11 -0
  136. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +5 -0
  137. vellum_ee/workflows/display/nodes/vellum/error_node.py +22 -16
  138. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +2 -0
  139. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +46 -12
  140. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +2 -0
  141. vellum_ee/workflows/display/nodes/vellum/map_node.py +2 -0
  142. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +3 -5
  143. vellum_ee/workflows/display/nodes/vellum/search_node.py +8 -0
  144. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +2 -5
  145. vellum_ee/workflows/display/nodes/vellum/templating_node.py +2 -0
  146. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +1 -1
  147. vellum_ee/workflows/display/nodes/vellum/tests/test_error_node.py +4 -0
  148. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_deployment_node.py +4 -3
  149. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +36 -2
  150. vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py +5 -4
  151. vellum_ee/workflows/display/nodes/vellum/tests/test_templating_node.py +1 -1
  152. vellum_ee/workflows/display/tests/test_base_workflow_display.py +44 -0
  153. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +2 -4
  154. vellum_ee/workflows/display/types.py +3 -0
  155. vellum_ee/workflows/display/utils/expressions.py +3 -3
  156. vellum_ee/workflows/display/utils/vellum.py +1 -3
  157. vellum_ee/workflows/display/workflows/base_workflow_display.py +10 -0
  158. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +3 -0
  159. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +53 -0
  160. {vellum_ai-0.14.45.dist-info → vellum_ai-0.14.47.dist-info}/LICENSE +0 -0
  161. {vellum_ai-0.14.45.dist-info → vellum_ai-0.14.47.dist-info}/WHEEL +0 -0
  162. {vellum_ai-0.14.45.dist-info → vellum_ai-0.14.47.dist-info}/entry_points.txt +0 -0
vellum_cli/config.py CHANGED
@@ -16,10 +16,15 @@ PYPROJECT_TOML_PATH = "pyproject.toml"
16
16
 
17
17
  class WorkspaceConfig(UniversalBaseModel):
18
18
  name: str
19
- api_key: str
19
+ api_key: str = "VELLUM_API_KEY"
20
+ api_url: Optional[str] = None
20
21
 
21
22
  def merge(self, other: "WorkspaceConfig") -> "WorkspaceConfig":
22
- return WorkspaceConfig(name=self.name or other.name, api_key=self.api_key or other.api_key)
23
+ return WorkspaceConfig(
24
+ name=self.name or other.name,
25
+ api_key=self.api_key or other.api_key,
26
+ api_url=self.api_url or other.api_url,
27
+ )
23
28
 
24
29
 
25
30
  DEFAULT_WORKSPACE_CONFIG = WorkspaceConfig(name="default", api_key="VELLUM_API_KEY")
vellum_cli/push.py CHANGED
@@ -97,6 +97,7 @@ def push_command(
97
97
 
98
98
  client = create_vellum_client(
99
99
  api_key=api_key,
100
+ api_url=workspace_config.api_url,
100
101
  )
101
102
  sys.path.insert(0, os.getcwd())
102
103
 
@@ -105,6 +106,7 @@ def push_command(
105
106
  workflow = BaseWorkflow.load_from_module(workflow_config.module)
106
107
  workflow_display = get_workflow_display(
107
108
  workflow_class=workflow,
109
+ client=client,
108
110
  dry_run=dry_run or False,
109
111
  )
110
112
  exec_config = workflow_display.serialize()
@@ -234,9 +236,11 @@ def push_command(
234
236
  """
235
237
  ) # type: ignore[attr-defined]
236
238
  else:
239
+ default_api_url = client._client_wrapper._environment.default
240
+ base_url = default_api_url.split("/v1")[0].replace("//api.", "//app.")
237
241
  logger.info(
238
242
  f"""Successfully pushed {workflow_config.module} to Vellum!
239
- Visit at: https://app.vellum.ai/workflow-sandboxes/{response.workflow_sandbox_id}"""
243
+ Visit at: {base_url}/workflow-sandboxes/{response.workflow_sandbox_id}"""
240
244
  )
241
245
 
242
246
  if not workflow_config.workflow_sandbox_id:
@@ -7,6 +7,7 @@ from unittest import mock
7
7
  from uuid import uuid4
8
8
 
9
9
  from click.testing import CliRunner
10
+ from httpx import Response
10
11
 
11
12
  from vellum.client.core.api_error import ApiError
12
13
  from vellum.client.types.workflow_push_response import WorkflowPushResponse
@@ -29,19 +30,25 @@ def _extract_tar_gz(tar_gz_bytes: bytes) -> dict[str, str]:
29
30
  return files
30
31
 
31
32
 
32
- def _ensure_workflow_py(temp_dir: str, module: str) -> str:
33
- base_dir = os.path.join(temp_dir, *module.split("."))
33
+ def _ensure_file(temp_dir: str, module: str, file_name: str, content: str) -> str:
34
+ file_path = os.path.join(temp_dir, *module.split("."), file_name)
35
+ base_dir = os.path.dirname(file_path)
34
36
  os.makedirs(base_dir, exist_ok=True)
37
+
38
+ with open(file_path, "w") as f:
39
+ f.write(content)
40
+
41
+ return content
42
+
43
+
44
+ def _ensure_workflow_py(temp_dir: str, module: str) -> str:
35
45
  workflow_py_file_content = """\
36
46
  from vellum.workflows import BaseWorkflow
37
47
 
38
48
  class ExampleWorkflow(BaseWorkflow):
39
49
  pass
40
50
  """
41
- with open(os.path.join(temp_dir, *module.split("."), "workflow.py"), "w") as f:
42
- f.write(workflow_py_file_content)
43
-
44
- return workflow_py_file_content
51
+ return _ensure_file(temp_dir, module, "workflow.py", workflow_py_file_content)
45
52
 
46
53
 
47
54
  def test_push__no_config(mock_module):
@@ -133,6 +140,38 @@ def test_push__happy_path(mock_module, vellum_client, base_command):
133
140
  assert extracted_files["workflow.py"] == workflow_py_file_content
134
141
 
135
142
 
143
+ def test_push__verify_default_url_in_raw_httpx_transport(mock_module, mock_httpx_transport):
144
+ # GIVEN a single workflow configured
145
+ module = mock_module.module
146
+ temp_dir = mock_module.temp_dir
147
+ _ensure_workflow_py(temp_dir, module)
148
+
149
+ # AND the push API call returns successfully
150
+ mock_httpx_transport.handle_request.return_value = Response(
151
+ status_code=200,
152
+ text=json.dumps(
153
+ {
154
+ "workflow_sandbox_id": str(uuid4()),
155
+ }
156
+ ),
157
+ )
158
+
159
+ # WHEN calling `vellum push`
160
+ runner = CliRunner()
161
+ result = runner.invoke(cli_main, ["workflows", "push", module])
162
+
163
+ # THEN it should succeed
164
+ assert result.exit_code == 0
165
+
166
+ # AND we should have called the push API with the correct args
167
+ mock_httpx_transport.handle_request.assert_called_once()
168
+ request = mock_httpx_transport.handle_request.call_args[0][0]
169
+ assert str(request.url) == "https://api.vellum.ai/v1/workflows/push"
170
+
171
+ # AND the new URL is in the message at the end
172
+ assert "Visit at: https://app.vellum.ai/workflow-sandboxes/" in result.output
173
+
174
+
136
175
  def test_push__no_config__module_found(mock_module, vellum_client):
137
176
  # GIVEN no config file set
138
177
  temp_dir = mock_module.temp_dir
@@ -486,7 +525,74 @@ Files that were different between the original project and the generated artifac
486
525
  )
487
526
 
488
527
 
489
- def test_push__workspace_option__uses_different_api_key(mock_module, vellum_client_class):
528
+ @pytest.mark.parametrize(
529
+ "file_data",
530
+ [
531
+ {
532
+ "workflow.py": """\
533
+ from vellum.workflows import BaseWorkflow
534
+
535
+ class ExampleWorkflow(BaseWorkflow):
536
+ pass
537
+ """
538
+ },
539
+ {
540
+ "nodes/start_node.py": """\
541
+ from vellum.workflows.nodes import CodeExecutionNode
542
+ from vellum.workflows.references import VellumSecretReference
543
+
544
+ class StartNode(CodeExecutionNode):
545
+ code_inputs = {
546
+ "foo": VellumSecretReference("MY_SECRET_KEY"),
547
+ }
548
+ """,
549
+ "workflow.py": """\
550
+ from vellum.workflows import BaseWorkflow
551
+ from .nodes.start_node import StartNode
552
+
553
+ class ExampleWorkflow(BaseWorkflow):
554
+ graph = StartNode
555
+ """,
556
+ },
557
+ {
558
+ "nodes/start_node.py": """\
559
+ from vellum.workflows.nodes import PromptDeploymentNode
560
+
561
+ class StartNode(PromptDeploymentNode):
562
+ deployment = "my-deployment"
563
+ """,
564
+ "workflow.py": """\
565
+ from vellum.workflows import BaseWorkflow
566
+ from .nodes.start_node import StartNode
567
+
568
+ class ExampleWorkflow(BaseWorkflow):
569
+ graph = StartNode
570
+ """,
571
+ },
572
+ {
573
+ "nodes/start_node.py": """\
574
+ from vellum.workflows.nodes import SubworkflowDeploymentNode
575
+
576
+ class StartNode(SubworkflowDeploymentNode):
577
+ deployment = "my-deployment"
578
+ """,
579
+ "workflow.py": """\
580
+ from vellum.workflows import BaseWorkflow
581
+ from .nodes.start_node import StartNode
582
+
583
+ class ExampleWorkflow(BaseWorkflow):
584
+ graph = StartNode
585
+ """,
586
+ },
587
+ ],
588
+ ids=[
589
+ "base_case",
590
+ "with_secret_reference",
591
+ "with_prompt_deployment",
592
+ "with_subworkflow_deployment",
593
+ ],
594
+ )
595
+ def test_push__workspace_option__uses_different_api_key(mock_module, vellum_client_class, file_data):
490
596
  # GIVEN a single workflow configured
491
597
  temp_dir = mock_module.temp_dir
492
598
  module = mock_module.module
@@ -521,7 +627,8 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
521
627
  )
522
628
 
523
629
  # AND a workflow exists in the module successfully
524
- _ensure_workflow_py(temp_dir, module)
630
+ for file_name, content in file_data.items():
631
+ _ensure_file(temp_dir, module, file_name, content)
525
632
 
526
633
  # AND the push API call returns a new workflow sandbox id
527
634
  new_workflow_sandbox_id = str(uuid4())
@@ -564,6 +671,83 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
564
671
  }
565
672
 
566
673
 
674
+ def test_push__workspace_option__uses_different_api_url_env(mock_module, mock_httpx_transport):
675
+ # GIVEN a single workflow configured
676
+ temp_dir = mock_module.temp_dir
677
+ module = mock_module.module
678
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
679
+ set_pyproject_toml = mock_module.set_pyproject_toml
680
+
681
+ # AND a different workspace is set in the pyproject.toml
682
+ set_pyproject_toml(
683
+ {
684
+ "workflows": [
685
+ {
686
+ "module": module,
687
+ "workflow_sandbox_id": workflow_sandbox_id,
688
+ }
689
+ ],
690
+ "workspaces": [
691
+ {
692
+ "name": "my_other_workspace",
693
+ "api_url": "MY_OTHER_VELLUM_API_URL",
694
+ }
695
+ ],
696
+ }
697
+ )
698
+
699
+ # AND the .env file has the other api key stored
700
+ with open(os.path.join(temp_dir, ".env"), "w") as f:
701
+ f.write(
702
+ """\
703
+ VELLUM_API_KEY=abcdef123456
704
+ MY_OTHER_VELLUM_API_URL=https://app.aws-vpc-staging.vellum.ai
705
+ """
706
+ )
707
+
708
+ # AND a workflow exists in the module successfully
709
+ _ensure_workflow_py(temp_dir, module)
710
+
711
+ # AND the push API call returns a new workflow sandbox id
712
+ new_workflow_sandbox_id = str(uuid4())
713
+ mock_httpx_transport.handle_request.return_value = Response(
714
+ status_code=200,
715
+ text=json.dumps(
716
+ {
717
+ "workflow_sandbox_id": new_workflow_sandbox_id,
718
+ }
719
+ ),
720
+ )
721
+
722
+ # WHEN calling `vellum push` on strict mode
723
+ runner = CliRunner()
724
+ result = runner.invoke(cli_main, ["push", module, "--workspace", "my_other_workspace"])
725
+
726
+ # THEN it should succeed
727
+ assert result.exit_code == 0, result.output
728
+
729
+ # AND we should have called the push API once with the correct api url
730
+ request = mock_httpx_transport.handle_request.call_args[0][0]
731
+ assert str(request.url) == "https://app.aws-vpc-staging.vellum.ai/v1/workflows/push"
732
+
733
+ # AND the vellum lock file should have been updated with the correct workspace
734
+ with open(os.path.join(temp_dir, "vellum.lock.json")) as f:
735
+ lock_file_content = json.load(f)
736
+ assert lock_file_content["workflows"][1] == {
737
+ "module": module,
738
+ "workflow_sandbox_id": new_workflow_sandbox_id,
739
+ "workspace": "my_other_workspace",
740
+ "container_image_name": None,
741
+ "container_image_tag": None,
742
+ "deployments": [],
743
+ "ignore": None,
744
+ "target_directory": None,
745
+ }
746
+
747
+ # AND the new URL is in the message at the end
748
+ assert "Visit at: https://app.aws-vpc-staging.vellum.ai/workflow-sandboxes/" in result.output
749
+
750
+
567
751
  def test_push__workspace_option__both_options_already_configured(mock_module, vellum_client_class):
568
752
  # GIVEN a single workflow configured
569
753
  temp_dir = mock_module.temp_dir
@@ -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:
@@ -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
+ 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]
32
35
  node_blocks = raise_if_descriptor(node.blocks)
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: