vellum-ai 1.6.4__py3-none-any.whl → 1.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. vellum/__init__.py +2 -0
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/client/reference.md +81 -0
  4. vellum/client/resources/container_images/client.py +8 -2
  5. vellum/client/resources/container_images/raw_client.py +8 -0
  6. vellum/client/resources/workflows/client.py +81 -0
  7. vellum/client/resources/workflows/raw_client.py +85 -0
  8. vellum/client/types/__init__.py +2 -0
  9. vellum/client/types/workflow_resolved_state.py +31 -0
  10. vellum/types/workflow_resolved_state.py +3 -0
  11. vellum/workflows/descriptors/base.py +3 -0
  12. vellum/workflows/errors/types.py +1 -0
  13. vellum/workflows/inputs/base.py +4 -1
  14. vellum/workflows/inputs/tests/test_inputs.py +21 -0
  15. vellum/workflows/resolvers/resolver.py +13 -46
  16. vellum/workflows/resolvers/tests/test_resolver.py +27 -112
  17. vellum/workflows/runner/runner.py +16 -0
  18. vellum/workflows/workflows/base.py +2 -0
  19. {vellum_ai-1.6.4.dist-info → vellum_ai-1.7.1.dist-info}/METADATA +1 -1
  20. {vellum_ai-1.6.4.dist-info → vellum_ai-1.7.1.dist-info}/RECORD +39 -37
  21. vellum_ee/assets/node-definitions.json +203 -18
  22. vellum_ee/workflows/display/exceptions.py +2 -6
  23. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -1
  24. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +1 -1
  25. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +11 -4
  26. vellum_ee/workflows/display/nodes/vellum/search_node.py +4 -4
  27. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +11 -4
  28. vellum_ee/workflows/display/nodes/vellum/templating_node.py +1 -1
  29. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +1 -1
  30. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +30 -0
  31. vellum_ee/workflows/display/utils/exceptions.py +19 -0
  32. vellum_ee/workflows/display/utils/expressions.py +19 -11
  33. vellum_ee/workflows/display/utils/vellum.py +7 -1
  34. vellum_ee/workflows/display/workflows/base_workflow_display.py +11 -3
  35. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +54 -1
  36. vellum_ee/workflows/tests/test_server.py +41 -0
  37. {vellum_ai-1.6.4.dist-info → vellum_ai-1.7.1.dist-info}/LICENSE +0 -0
  38. {vellum_ai-1.6.4.dist-info → vellum_ai-1.7.1.dist-info}/WHEEL +0 -0
  39. {vellum_ai-1.6.4.dist-info → vellum_ai-1.7.1.dist-info}/entry_points.txt +0 -0
@@ -542,6 +542,208 @@
542
542
  }
543
543
  ]
544
544
  },
545
+ {
546
+ "id": "3ae7366b-28ce-4d6b-8ec1-c6366b1e1a8b",
547
+ "display_data": {
548
+ "position": {
549
+ "x": 0.0,
550
+ "y": 0.0
551
+ },
552
+ "comment": {
553
+ "value": "\n Used to execute a Workflow Deployment.\n\n subworkflow_inputs: EntityInputsInterface - The inputs for the Subworkflow\n deployment: Union[UUID, str] - Either the Workflow Deployment's UUID or its name.\n release_tag: str = LATEST_RELEASE_TAG - The release tag to use for the Workflow Execution\n external_id: Optional[str] = OMIT - Optionally include a unique identifier for tracking purposes.\n Must be unique within a given Workflow Deployment.\n expand_meta: Optional[WorkflowExpandMetaRequest] = OMIT - Expandable execution fields to include in the response\n metadata: Optional[Dict[str, Optional[Any]]] = OMIT - The metadata to use for the Workflow Execution\n request_options: Optional[RequestOptions] = None - The request options to use for the Workflow Execution\n ",
554
+ "expanded": true
555
+ }
556
+ },
557
+ "base": {
558
+ "name": "BaseNode",
559
+ "module": [
560
+ "vellum",
561
+ "workflows",
562
+ "nodes",
563
+ "bases",
564
+ "base"
565
+ ]
566
+ },
567
+ "definition": {
568
+ "name": "SubworkflowDeploymentNode",
569
+ "module": [
570
+ "vellum",
571
+ "workflows",
572
+ "nodes",
573
+ "displayable",
574
+ "subworkflow_deployment_node",
575
+ "node"
576
+ ]
577
+ },
578
+ "trigger": {
579
+ "id": "a6eecf28-5c21-4406-80bb-917c6de84050",
580
+ "merge_behavior": "AWAIT_ANY"
581
+ },
582
+ "ports": [
583
+ {
584
+ "id": "8fc8d25b-c9b5-4db7-a632-d4eb1eb3e9fd",
585
+ "name": "default",
586
+ "type": "DEFAULT"
587
+ }
588
+ ]
589
+ },
590
+ {
591
+ "id": "d2f9a8ec-3023-46bb-aca3-dd67ffb61e18",
592
+ "display_data": {
593
+ "position": {
594
+ "x": 0.0,
595
+ "y": 0.0
596
+ },
597
+ "comment": {
598
+ "value": "\n Used to execute a Prompt Deployment and surface a string output and json output if applicable for convenience.\n\n prompt_inputs: EntityInputsInterface - The inputs for the Prompt\n deployment: Union[UUID, str] - Either the Prompt Deployment's UUID or its name.\n release_tag: str - The release tag to use for the Prompt Execution\n external_id: Optional[str] - Optionally include a unique identifier for tracking purposes.\n Must be unique within a given Prompt Deployment.\n expand_meta: Optional[PromptDeploymentExpandMetaRequest] - Expandable execution fields to include in the response\n raw_overrides: Optional[RawPromptExecutionOverridesRequest] - The raw overrides to use for the Prompt Execution\n expand_raw: Optional[Sequence[str]] - Expandable raw fields to include in the response\n metadata: Optional[Dict[str, Optional[Any]]] - The metadata to use for the Prompt Execution\n request_options: Optional[RequestOptions] - The request options to use for the Prompt Execution\n ",
599
+ "expanded": true
600
+ }
601
+ },
602
+ "base": {
603
+ "name": "BasePromptDeploymentNode",
604
+ "module": [
605
+ "vellum",
606
+ "workflows",
607
+ "nodes",
608
+ "displayable",
609
+ "bases",
610
+ "prompt_deployment_node"
611
+ ]
612
+ },
613
+ "definition": {
614
+ "name": "PromptDeploymentNode",
615
+ "module": [
616
+ "vellum",
617
+ "workflows",
618
+ "nodes",
619
+ "displayable",
620
+ "prompt_deployment_node",
621
+ "node"
622
+ ]
623
+ },
624
+ "trigger": {
625
+ "id": "7d209a9c-14a5-4b74-949c-d3e681750049",
626
+ "merge_behavior": "AWAIT_ANY"
627
+ },
628
+ "ports": [
629
+ {
630
+ "id": "d51b7c72-3e1f-400c-ba1c-1125b1047d9a",
631
+ "name": "default",
632
+ "type": "DEFAULT"
633
+ }
634
+ ],
635
+ "outputs": [
636
+ {
637
+ "id": "09c6ba90-4d69-4848-aa0b-74bdfcdd5a97",
638
+ "name": "json",
639
+ "type": "JSON",
640
+ "value": null
641
+ },
642
+ {
643
+ "id": "5ef5e3c6-1578-46da-89f6-6da3dd01fb5f",
644
+ "name": "text",
645
+ "type": "STRING",
646
+ "value": null
647
+ },
648
+ {
649
+ "id": "48513658-a72b-4db4-ad49-75a48a6cd084",
650
+ "name": "results",
651
+ "type": "ARRAY",
652
+ "value": null
653
+ }
654
+ ]
655
+ },
656
+ {
657
+ "id": "0ac6bdba-dbae-4ce5-b815-3d670aae0572",
658
+ "display_data": {
659
+ "position": {
660
+ "x": 0.0,
661
+ "y": 0.0
662
+ },
663
+ "comment": {
664
+ "value": "\n Used to perform a hybrid search against a Document Index in Vellum.\n\n document_index: Union[UUID, str] - Either the UUID or name of the Vellum Document Index that you'd like to search\n against\n query: str - The query to search for\n options: Optional[SearchRequestOptionsRequest] = None - Runtime configuration for the search\n request_options: Optional[RequestOptions] = None - The request options to use for the search\n chunk_separator: str = \"\n\n#####\n\n\" - The separator to use when joining the text of each search result\n ",
665
+ "expanded": true
666
+ }
667
+ },
668
+ "base": {
669
+ "name": "BaseSearchNode",
670
+ "module": [
671
+ "vellum",
672
+ "workflows",
673
+ "nodes",
674
+ "displayable",
675
+ "bases",
676
+ "search_node"
677
+ ]
678
+ },
679
+ "definition": {
680
+ "name": "SearchNode",
681
+ "module": [
682
+ "vellum",
683
+ "workflows",
684
+ "nodes",
685
+ "displayable",
686
+ "search_node",
687
+ "node"
688
+ ]
689
+ },
690
+ "trigger": {
691
+ "id": "37e64cf0-e604-4045-b8a0-d17e68c037e1",
692
+ "merge_behavior": "AWAIT_ANY"
693
+ },
694
+ "ports": [
695
+ {
696
+ "id": "8dea61c3-107c-4f17-bcba-dbbdc1185360",
697
+ "name": "default",
698
+ "type": "DEFAULT"
699
+ }
700
+ ]
701
+ },
702
+ {
703
+ "id": "ee9b5234-247b-49d1-bed4-490312f18838",
704
+ "display_data": {
705
+ "position": {
706
+ "x": 0.0,
707
+ "y": 0.0
708
+ },
709
+ "comment": {
710
+ "value": "Used to render a Jinja template.\n\n Useful for lightweight data transformations and complex string templating.\n ",
711
+ "expanded": true
712
+ }
713
+ },
714
+ "base": {
715
+ "name": "BaseNode",
716
+ "module": [
717
+ "vellum",
718
+ "workflows",
719
+ "nodes",
720
+ "bases",
721
+ "base"
722
+ ]
723
+ },
724
+ "definition": {
725
+ "name": "TemplatingNode",
726
+ "module": [
727
+ "vellum",
728
+ "workflows",
729
+ "nodes",
730
+ "core",
731
+ "templating_node",
732
+ "node"
733
+ ]
734
+ },
735
+ "trigger": {
736
+ "id": "005e1cd2-5452-4e1f-be6a-ac9fe3c02b9b",
737
+ "merge_behavior": "AWAIT_ATTRIBUTES"
738
+ },
739
+ "ports": [
740
+ {
741
+ "id": "8f4460f0-717b-4972-a6f4-ac164e5e204e",
742
+ "name": "default",
743
+ "type": "DEFAULT"
744
+ }
745
+ ]
746
+ },
545
747
  {
546
748
  "id": "035842e0-34ed-43af-97de-a706e40912ae",
547
749
  "label": "Tool Calling Node",
@@ -853,22 +1055,5 @@
853
1055
  ]
854
1056
  }
855
1057
  ],
856
- "errors": [
857
- {
858
- "node": "SubworkflowDeploymentNode",
859
- "error": "ApiError: headers: {'server': 'gunicorn', 'date': 'Wed, 24 Sep 2025 04:58:53 GMT', 'content-type': 'application/json', 'allow': 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS', 'x-frame-options': 'DENY', 'content-length': '58', 'vary': 'Accept-Language, Origin', 'content-language': 'en', 'strict-transport-security': 'max-age=60; includeSubDomains; preload', 'x-content-type-options': 'nosniff', 'referrer-policy': 'same-origin', 'cross-origin-opener-policy': 'same-origin', 'via': '1.1 google', 'alt-svc': 'h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000'}, status_code: 403, body: {'detail': 'Authentication credentials were not provided.'}"
860
- },
861
- {
862
- "node": "PromptDeploymentNode",
863
- "error": "ApiError: headers: {'server': 'gunicorn', 'date': 'Wed, 24 Sep 2025 04:58:53 GMT', 'content-type': 'application/json', 'allow': 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS', 'x-frame-options': 'DENY', 'content-length': '58', 'vary': 'Accept-Language, Origin', 'content-language': 'en', 'strict-transport-security': 'max-age=60; includeSubDomains; preload', 'x-content-type-options': 'nosniff', 'referrer-policy': 'same-origin', 'cross-origin-opener-policy': 'same-origin', 'via': '1.1 google', 'alt-svc': 'h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000'}, status_code: 403, body: {'detail': 'Authentication credentials were not provided.'}"
864
- },
865
- {
866
- "node": "SearchNode",
867
- "error": "ValueError: Expected NodeReference query to have an instance"
868
- },
869
- {
870
- "node": "TemplatingNode",
871
- "error": "KeyError: TemplatingNode.Outputs.result"
872
- }
873
- ]
1058
+ "errors": []
874
1059
  }
@@ -1,7 +1,3 @@
1
- class NodeValidationError(Exception):
2
- """Exception raised when a node fails validation during workflow display serialization."""
1
+ from vellum_ee.workflows.display.utils.exceptions import NodeValidationError
3
2
 
4
- def __init__(self, message: str, node_class_name: str):
5
- self.message = message
6
- self.node_class_name = node_class_name
7
- super().__init__(f"Node validation error in {node_class_name}: {message}")
3
+ __all__ = ["NodeValidationError"]
@@ -6,11 +6,11 @@ from typing import ClassVar, Generic, Optional, TypeVar
6
6
  from vellum.workflows.nodes.displayable.code_execution_node import CodeExecutionNode
7
7
  from vellum.workflows.types.core import JsonObject
8
8
  from vellum.workflows.utils.vellum_variables import primitive_type_to_vellum_variable_type
9
- from vellum_ee.workflows.display.exceptions import NodeValidationError
10
9
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
11
10
  from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
12
11
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
13
12
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
13
+ from vellum_ee.workflows.display.utils.exceptions import NodeValidationError
14
14
  from vellum_ee.workflows.display.utils.expressions import virtual_open
15
15
 
16
16
  _CodeExecutionNodeType = TypeVar("_CodeExecutionNodeType", bound=CodeExecutionNode)
@@ -8,11 +8,11 @@ from vellum.workflows.nodes import InlineSubworkflowNode
8
8
  from vellum.workflows.nodes.displayable.bases.utils import primitive_to_vellum_value
9
9
  from vellum.workflows.types.core import JsonObject
10
10
  from vellum.workflows.workflows.base import BaseWorkflow
11
- from vellum_ee.workflows.display.exceptions import NodeValidationError
12
11
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
13
12
  from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
14
13
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
15
14
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
15
+ from vellum_ee.workflows.display.utils.exceptions import NodeValidationError
16
16
  from vellum_ee.workflows.display.utils.vellum import infer_vellum_variable_type
17
17
  from vellum_ee.workflows.display.vellum import NodeInput
18
18
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
@@ -3,6 +3,7 @@ from typing import Generic, Optional, TypeVar
3
3
 
4
4
  from vellum.workflows.nodes.displayable.prompt_deployment_node import PromptDeploymentNode
5
5
  from vellum.workflows.types.core import JsonObject
6
+ from vellum.workflows.utils.uuids import uuid4_from_hash
6
7
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
7
8
  from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
8
9
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
@@ -43,9 +44,15 @@ class BasePromptDeploymentNodeDisplay(BaseNodeDisplay[_PromptDeploymentNodeType]
43
44
  array_display = self.output_display[node.Outputs.results]
44
45
  json_display = self.output_display[node.Outputs.json]
45
46
 
46
- deployment = display_context.client.deployments.retrieve(
47
- id=str(raise_if_descriptor(node.deployment)),
48
- )
47
+ deployment_descriptor_id = str(raise_if_descriptor(node.deployment))
48
+ try:
49
+ deployment = display_context.client.deployments.retrieve(
50
+ id=deployment_descriptor_id,
51
+ )
52
+ deployment_id = str(deployment.id)
53
+ except Exception as e:
54
+ display_context.add_error(e)
55
+ deployment_id = str(uuid4_from_hash(deployment_descriptor_id))
49
56
  ml_model_fallbacks = raise_if_descriptor(node.ml_model_fallbacks)
50
57
 
51
58
  return {
@@ -60,7 +67,7 @@ class BasePromptDeploymentNodeDisplay(BaseNodeDisplay[_PromptDeploymentNodeType]
60
67
  "source_handle_id": str(self.get_source_handle_id(display_context.port_displays)),
61
68
  "target_handle_id": str(self.get_target_handle_id()),
62
69
  "variant": "DEPLOYMENT",
63
- "prompt_deployment_id": str(deployment.id),
70
+ "prompt_deployment_id": deployment_id,
64
71
  "release_tag": raise_if_descriptor(node.release_tag),
65
72
  "ml_model_fallbacks": list(ml_model_fallbacks) if ml_model_fallbacks else None,
66
73
  },
@@ -48,8 +48,8 @@ class BaseSearchNodeDisplay(BaseNodeDisplay[_SearchNodeType], Generic[_SearchNod
48
48
  node_id = self.node_id
49
49
  node_inputs = self._generate_search_node_inputs(node_id, node, display_context)
50
50
 
51
- results_output_display = display_context.global_node_output_displays[node.Outputs.results]
52
- text_output_display = display_context.global_node_output_displays[node.Outputs.text]
51
+ results_output_display = self.get_node_output_display(node.Outputs.results)
52
+ text_output_display = self.get_node_output_display(node.Outputs.text)
53
53
 
54
54
  return {
55
55
  "id": str(node_id),
@@ -111,8 +111,8 @@ class BaseSearchNodeDisplay(BaseNodeDisplay[_SearchNodeType], Generic[_SearchNod
111
111
  limit = raw_limit if raw_limit is not None else options.limit if options is not None else None
112
112
 
113
113
  node_input_names_and_values = [
114
- ("query", node.query),
115
- ("document_index_id", node.document_index),
114
+ ("query", raise_if_descriptor(node.query)),
115
+ ("document_index_id", raise_if_descriptor(node.document_index)),
116
116
  ("weights", weights.dict() if weights else None),
117
117
  ("limit", limit),
118
118
  ("separator", raise_if_descriptor(node.chunk_separator)),
@@ -4,6 +4,7 @@ from typing import Generic, Optional, TypeVar
4
4
  from vellum.workflows.inputs.base import BaseInputs
5
5
  from vellum.workflows.nodes import SubworkflowDeploymentNode
6
6
  from vellum.workflows.types.core import JsonObject
7
+ from vellum.workflows.utils.uuids import uuid4_from_hash
7
8
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
8
9
  from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
9
10
  from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
@@ -44,9 +45,15 @@ class BaseSubworkflowDeploymentNodeDisplay(
44
45
  for variable_name, variable_value in input_items
45
46
  ]
46
47
 
47
- deployment = display_context.client.workflow_deployments.retrieve(
48
- id=str(raise_if_descriptor(node.deployment)),
49
- )
48
+ deployment_descriptor_id = str(raise_if_descriptor(node.deployment))
49
+ try:
50
+ deployment = display_context.client.workflow_deployments.retrieve(
51
+ id=deployment_descriptor_id,
52
+ )
53
+ deployment_id = str(deployment.id)
54
+ except Exception as e:
55
+ display_context.add_error(e)
56
+ deployment_id = str(uuid4_from_hash(deployment_descriptor_id))
50
57
 
51
58
  return {
52
59
  "id": str(node_id),
@@ -58,7 +65,7 @@ class BaseSubworkflowDeploymentNodeDisplay(
58
65
  "source_handle_id": str(self.get_source_handle_id(display_context.port_displays)),
59
66
  "target_handle_id": str(self.get_target_handle_id()),
60
67
  "variant": "DEPLOYMENT",
61
- "workflow_deployment_id": str(deployment.id),
68
+ "workflow_deployment_id": deployment_id,
62
69
  "release_tag": raise_if_descriptor(node.release_tag),
63
70
  },
64
71
  **self.serialize_generic_fields(display_context),
@@ -50,7 +50,7 @@ class BaseTemplatingNodeDisplay(BaseNodeDisplay[_TemplatingNodeType], Generic[_T
50
50
  # Misc type ignore is due to `node.Outputs` being generic
51
51
  # https://app.shortcut.com/vellum/story/4784
52
52
  output_descriptor = node.Outputs.result # type: ignore [misc]
53
- output_display = display_context.global_node_output_displays[output_descriptor]
53
+ output_display = self.get_node_output_display(output_descriptor)
54
54
  inferred_output_type = primitive_type_to_vellum_variable_type(output_descriptor)
55
55
 
56
56
  return {
@@ -6,8 +6,8 @@ from vellum.client.core.api_error import ApiError
6
6
  from vellum.workflows.nodes.displayable.code_execution_node.node import CodeExecutionNode
7
7
  from vellum.workflows.references.vellum_secret import VellumSecretReference
8
8
  from vellum.workflows.workflows.base import BaseWorkflow
9
- from vellum_ee.workflows.display.exceptions import NodeValidationError
10
9
  from vellum_ee.workflows.display.nodes.vellum.code_execution_node import BaseCodeExecutionNodeDisplay
10
+ from vellum_ee.workflows.display.utils.exceptions import NodeValidationError
11
11
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
12
12
 
13
13
 
@@ -353,3 +353,33 @@ def test_serialize_node__adornment_order_matches_decorator_order():
353
353
  assert len(adornments) == 2
354
354
  assert adornments[0]["label"] == "Try Node"
355
355
  assert adornments[1]["label"] == "Retry Node"
356
+
357
+
358
+ def test_serialize_workflow__retry_node_edges():
359
+ """
360
+ Tests that both retry-adorned nodes are correctly serialized in the nodes array.
361
+ """
362
+
363
+ @RetryNode.wrap(max_attempts=3, delay=60)
364
+ class FirstNode(BaseNode):
365
+ class Outputs(BaseOutputs):
366
+ value: str
367
+
368
+ @RetryNode.wrap(max_attempts=5, delay=120)
369
+ class SecondNode(BaseNode):
370
+ class Outputs(BaseOutputs):
371
+ result: str
372
+
373
+ class MyWorkflow(BaseWorkflow):
374
+ graph = FirstNode >> SecondNode
375
+
376
+ workflow_display = get_workflow_display(workflow_class=MyWorkflow)
377
+ exec_config = cast(Dict[str, Any], workflow_display.serialize())
378
+
379
+ assert isinstance(exec_config["workflow_raw_data"], dict)
380
+ assert isinstance(exec_config["workflow_raw_data"]["nodes"], list)
381
+
382
+ nodes = cast(List[Dict[str, Any]], exec_config["workflow_raw_data"]["nodes"])
383
+
384
+ generic_nodes = [node for node in nodes if node["type"] == "GENERIC"]
385
+ assert len(generic_nodes) == 2
@@ -5,3 +5,22 @@ class UserFacingException(Exception):
5
5
 
6
6
  class UnsupportedSerializationException(UserFacingException):
7
7
  pass
8
+
9
+
10
+ class NodeValidationError(UserFacingException):
11
+ """Exception raised when a node fails validation during workflow display serialization."""
12
+
13
+ def __init__(self, message: str, node_class_name: str):
14
+ self.message = message
15
+ self.node_class_name = node_class_name
16
+ super().__init__(f"Node validation error in {node_class_name}: {message}")
17
+
18
+
19
+ class InvalidInputReferenceError(UserFacingException):
20
+ """Exception raised when a node references a non-existent workflow input."""
21
+
22
+ def __init__(self, message: str, inputs_class_name: str, attribute_name: str):
23
+ self.message = message
24
+ self.inputs_class_name = inputs_class_name
25
+ self.attribute_name = attribute_name
26
+ super().__init__(f"Invalid input reference in {inputs_class_name}.{attribute_name}: {message}")
@@ -56,7 +56,7 @@ from vellum.workflows.types.core import JsonArray, JsonObject
56
56
  from vellum.workflows.types.generics import is_workflow_class
57
57
  from vellum.workflows.utils.functions import compile_function_definition
58
58
  from vellum.workflows.utils.uuids import uuid4_from_hash
59
- from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
59
+ from vellum_ee.workflows.display.utils.exceptions import InvalidInputReferenceError, UnsupportedSerializationException
60
60
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileLoader
61
61
 
62
62
  if TYPE_CHECKING:
@@ -275,16 +275,24 @@ def serialize_value(executable_id: UUID, display_context: "WorkflowDisplayContex
275
275
  return serialize_value(executable_id, display_context, child_descriptor)
276
276
 
277
277
  if isinstance(value, WorkflowInputReference):
278
- try:
279
- workflow_input_display = display_context.global_workflow_input_displays[value]
280
- except KeyError:
281
- inputs_class_name = value.inputs_class.__name__
282
- workflow_class_name = display_context.workflow_display_class.infer_workflow_class().__name__
283
- raise UnsupportedSerializationException(
284
- f"Inputs class '{inputs_class_name}' referenced during serialization of '{workflow_class_name}' "
285
- f"without parameterizing this Workflow with this Inputs definition. Update your Workflow "
286
- f"definition to '{workflow_class_name}(BaseWorkflow[{inputs_class_name}, BaseState])'."
287
- )
278
+ if value not in display_context.global_workflow_input_displays:
279
+ workflow_inputs_class = display_context.workflow_display_class.infer_workflow_class().get_inputs_class()
280
+ if value.inputs_class != workflow_inputs_class:
281
+ inputs_class_name = value.inputs_class.__name__
282
+ workflow_class_name = display_context.workflow_display_class.infer_workflow_class().__name__
283
+ raise UnsupportedSerializationException(
284
+ f"Inputs class '{inputs_class_name}' referenced during serialization of "
285
+ f"'{workflow_class_name}' without parameterizing this Workflow with this Inputs definition. "
286
+ f"Update your Workflow definition to "
287
+ f"'{workflow_class_name}(BaseWorkflow[{inputs_class_name}, BaseState])'."
288
+ )
289
+ else:
290
+ raise InvalidInputReferenceError(
291
+ message=f"Inputs class '{value.inputs_class.__qualname__}' has no attribute '{value.name}'",
292
+ inputs_class_name=value.inputs_class.__qualname__,
293
+ attribute_name=value.name,
294
+ )
295
+ workflow_input_display = display_context.global_workflow_input_displays[value]
288
296
  return {
289
297
  "type": "WORKFLOW_INPUT",
290
298
  "input_variable_id": str(workflow_input_display.id),
@@ -15,7 +15,7 @@ from vellum.workflows.references.lazy import LazyReference
15
15
  from vellum.workflows.references.node import NodeReference
16
16
  from vellum.workflows.references.vellum_secret import VellumSecretReference
17
17
  from vellum.workflows.utils.vellum_variables import primitive_type_to_vellum_variable_type
18
- from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
18
+ from vellum_ee.workflows.display.utils.exceptions import InvalidInputReferenceError, UnsupportedSerializationException
19
19
  from vellum_ee.workflows.display.utils.expressions import get_child_descriptor
20
20
 
21
21
  if TYPE_CHECKING:
@@ -124,6 +124,12 @@ def create_node_input_value_pointer_rule(
124
124
  child_descriptor = get_child_descriptor(value, display_context)
125
125
  return create_node_input_value_pointer_rule(child_descriptor, display_context)
126
126
  if isinstance(value, WorkflowInputReference):
127
+ if value not in display_context.global_workflow_input_displays:
128
+ raise InvalidInputReferenceError(
129
+ message=f"Inputs class '{value.inputs_class.__qualname__}' has no attribute '{value.name}'",
130
+ inputs_class_name=value.inputs_class.__qualname__,
131
+ attribute_name=value.name,
132
+ )
127
133
  workflow_input_display = display_context.global_workflow_input_displays[value]
128
134
  return InputVariablePointer(data=InputVariableData(input_variable_id=str(workflow_input_display.id)))
129
135
  if isinstance(value, VellumSecretReference):
@@ -39,7 +39,6 @@ from vellum_ee.workflows.display.base import (
39
39
  WorkflowOutputDisplay,
40
40
  )
41
41
  from vellum_ee.workflows.display.editor.types import NodeDisplayData, NodeDisplayPosition
42
- from vellum_ee.workflows.display.exceptions import NodeValidationError
43
42
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
44
43
  from vellum_ee.workflows.display.nodes.get_node_display_class import get_node_display_class
45
44
  from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay, PortDisplay
@@ -57,6 +56,7 @@ from vellum_ee.workflows.display.types import (
57
56
  WorkflowOutputDisplays,
58
57
  )
59
58
  from vellum_ee.workflows.display.utils.auto_layout import auto_layout_nodes
59
+ from vellum_ee.workflows.display.utils.exceptions import UserFacingException
60
60
  from vellum_ee.workflows.display.utils.expressions import serialize_value
61
61
  from vellum_ee.workflows.display.utils.registry import register_workflow_display_class
62
62
  from vellum_ee.workflows.display.utils.vellum import infer_vellum_variable_type
@@ -203,12 +203,20 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
203
203
  self.display_context.add_validation_error(validation_error)
204
204
 
205
205
  serialized_node = node_display.serialize(self.display_context)
206
- except (NotImplementedError, NodeValidationError) as e:
206
+ except (NotImplementedError, UserFacingException) as e:
207
207
  self.display_context.add_error(e)
208
208
  self.display_context.add_invalid_node(node)
209
209
  continue
210
210
 
211
- serialized_nodes[node_display.node_id] = serialized_node
211
+ # Use wrapped node's ID as dict key for adornment wrappers to prevent overwrites
212
+ wrapped_node = get_wrapped_node(node)
213
+ if wrapped_node:
214
+ wrapped_node_display = self.display_context.node_displays[wrapped_node]
215
+ dict_key = wrapped_node_display.node_id
216
+ else:
217
+ dict_key = node_display.node_id
218
+
219
+ serialized_nodes[dict_key] = serialized_node
212
220
 
213
221
  synthetic_output_edges: JsonArray = []
214
222
  output_variables: JsonArray = []
@@ -14,7 +14,7 @@ from vellum.workflows.state.base import BaseState
14
14
  from vellum.workflows.types.core import JsonObject
15
15
  from vellum.workflows.workflows.base import BaseWorkflow
16
16
  from vellum_ee.workflows.display.editor.types import NodeDisplayData, NodeDisplayPosition
17
- from vellum_ee.workflows.display.nodes import BaseNodeDisplay
17
+ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
18
18
  from vellum_ee.workflows.display.nodes.vellum.retry_node import BaseRetryNodeDisplay
19
19
  from vellum_ee.workflows.display.nodes.vellum.try_node import BaseTryNodeDisplay
20
20
  from vellum_ee.workflows.display.types import WorkflowDisplayContext
@@ -997,3 +997,56 @@ def test_serialize_workflow__with_complete_node_failure_prunes_edges():
997
997
  node_types = [node["type"] for node in data["workflow_raw_data"]["nodes"]]
998
998
  assert "ENTRYPOINT" in node_types
999
999
  assert "GENERIC" in node_types # This is the WorkingNode that should still be serialized
1000
+
1001
+
1002
+ def test_serialize_workflow__node_with_invalid_input_reference():
1003
+ """Test that serialization captures errors when nodes reference a non-existent input attribute."""
1004
+
1005
+ # GIVEN a workflow with defined inputs
1006
+ class Inputs(BaseInputs):
1007
+ valid_input: str
1008
+
1009
+ # AND a templating node that references a non-existent input
1010
+ class MyTemplatingNode(TemplatingNode):
1011
+ class Outputs(TemplatingNode.Outputs):
1012
+ pass
1013
+
1014
+ template = "valid: {{ valid_input }}, invalid: {{ invalid_ref }}"
1015
+ inputs = {
1016
+ "valid_input": Inputs.valid_input,
1017
+ "invalid_ref": Inputs.invalid_ref,
1018
+ }
1019
+
1020
+ # AND a base node that also references the non-existent input
1021
+ class MyBaseNode(BaseNode):
1022
+ invalid_ref = Inputs.invalid_ref
1023
+
1024
+ class Outputs(BaseNode.Outputs):
1025
+ result: str
1026
+
1027
+ def run(self) -> BaseNode.Outputs:
1028
+ return self.Outputs(result="done")
1029
+
1030
+ class MyBaseNodeDisplay(BaseNodeDisplay[MyBaseNode]):
1031
+ __serializable_inputs__ = {MyBaseNode.invalid_ref}
1032
+
1033
+ # WHEN we create a workflow with both nodes and serialize with dry_run=True
1034
+ class MyWorkflow(BaseWorkflow[Inputs, BaseState]):
1035
+ graph = MyTemplatingNode >> MyBaseNode
1036
+
1037
+ workflow_display = get_workflow_display(workflow_class=MyWorkflow, dry_run=True)
1038
+ serialized = workflow_display.serialize()
1039
+
1040
+ # THEN the serialization should succeed without raising an exception
1041
+ assert serialized is not None
1042
+ assert "workflow_raw_data" in serialized
1043
+
1044
+ errors = list(workflow_display.display_context.errors)
1045
+ assert len(errors) > 0
1046
+
1047
+ # AND the error messages should reference the missing attribute
1048
+ error_messages = [str(e) for e in errors]
1049
+ assert any("invalid_ref" in msg for msg in error_messages)
1050
+
1051
+ invalid_nodes = list(workflow_display.display_context.invalid_nodes)
1052
+ assert len(invalid_nodes) >= 2