vellum-ai 0.14.72__py3-none-any.whl → 0.14.74__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 (74) hide show
  1. vellum/__init__.py +8 -0
  2. vellum/client/core/client_wrapper.py +1 -1
  3. vellum/client/types/__init__.py +8 -0
  4. vellum/client/types/build_status_enum.py +5 -0
  5. vellum/client/types/container_image_build_config.py +20 -0
  6. vellum/client/types/container_image_read.py +4 -0
  7. vellum/client/types/execute_api_response.py +2 -2
  8. vellum/client/types/folder_entity.py +2 -0
  9. vellum/client/types/folder_entity_dataset.py +26 -0
  10. vellum/client/types/folder_entity_dataset_data.py +25 -0
  11. vellum/client/types/secret_type_enum.py +3 -1
  12. vellum/types/build_status_enum.py +3 -0
  13. vellum/types/container_image_build_config.py +3 -0
  14. vellum/types/folder_entity_dataset.py +3 -0
  15. vellum/types/folder_entity_dataset_data.py +3 -0
  16. vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -1
  17. vellum/workflows/nodes/displayable/api_node/node.py +2 -0
  18. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +43 -0
  19. vellum/workflows/nodes/displayable/bases/api_node/node.py +6 -0
  20. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +30 -4
  21. vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +43 -3
  22. vellum/workflows/nodes/displayable/bases/utils.py +2 -0
  23. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +68 -58
  24. vellum/workflows/nodes/experimental/tool_calling_node/node.py +15 -11
  25. vellum/workflows/nodes/experimental/tool_calling_node/tests/__init__.py +0 -0
  26. vellum/workflows/nodes/experimental/tool_calling_node/tests/test_node.py +13 -0
  27. vellum/workflows/nodes/experimental/tool_calling_node/tests/test_utils.py +49 -0
  28. vellum/workflows/nodes/experimental/tool_calling_node/utils.py +78 -7
  29. vellum/workflows/ports/utils.py +26 -6
  30. vellum/workflows/runner/runner.py +35 -3
  31. vellum/workflows/state/encoder.py +2 -0
  32. vellum/workflows/types/core.py +12 -0
  33. vellum/workflows/types/definition.py +6 -0
  34. vellum/workflows/utils/functions.py +12 -12
  35. vellum/workflows/utils/pydantic_schema.py +38 -0
  36. vellum/workflows/utils/tests/test_functions.py +18 -18
  37. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/METADATA +1 -1
  38. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/RECORD +74 -61
  39. vellum_cli/push.py +6 -8
  40. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +36 -7
  41. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +8 -1
  42. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +102 -0
  43. vellum_ee/workflows/display/tests/test_base_workflow_display.py +1 -1
  44. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +1 -1
  45. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +91 -1
  46. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +5 -5
  47. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +12 -12
  48. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +10 -10
  49. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +3 -3
  50. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +3 -3
  51. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +20 -9
  52. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +3 -3
  53. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +120 -3
  54. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +8 -8
  55. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +6 -6
  56. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -3
  57. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +8 -8
  58. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +3 -3
  59. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +4 -4
  60. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +3 -3
  61. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -2
  62. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +12 -5
  63. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +8 -1
  64. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py +62 -0
  65. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +1 -1
  66. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +2 -2
  67. vellum_ee/workflows/display/utils/auto_layout.py +1 -1
  68. vellum_ee/workflows/display/utils/expressions.py +33 -2
  69. vellum_ee/workflows/display/workflows/base_workflow_display.py +199 -10
  70. vellum_ee/workflows/tests/test_display_meta.py +41 -0
  71. vellum_ee/workflows/tests/test_serialize_module.py +47 -0
  72. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/LICENSE +0 -0
  73. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/WHEEL +0 -0
  74. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/entry_points.txt +0 -0
@@ -63,7 +63,7 @@ def test_serialize_workflow():
63
63
  "source_handle_id": "04da0bb6-5b42-4dd1-a4e4-08f3ab03e1a3",
64
64
  },
65
65
  "display_data": {
66
- "position": {"x": 0.0, "y": 0.0},
66
+ "position": {"x": 0.0, "y": -50.0},
67
67
  },
68
68
  }
69
69
 
@@ -61,7 +61,7 @@ def test_serialize_workflow__missing_final_output_node():
61
61
  },
62
62
  }
63
63
  ],
64
- "display_data": {"position": {"x": 0.0, "y": 0.0}},
64
+ "display_data": {"position": {"x": 200.0, "y": 75.0}},
65
65
  "base": {
66
66
  "name": "FinalOutputNode",
67
67
  "module": ["vellum", "workflows", "nodes", "displayable", "final_output_node", "node"],
@@ -111,7 +111,7 @@ def test_serialize_workflow__missing_final_output_node():
111
111
  },
112
112
  }
113
113
  ],
114
- "display_data": {"position": {"x": 0.0, "y": 0.0}},
114
+ "display_data": {"position": {"x": 400.0, "y": -50.0}},
115
115
  "base": {
116
116
  "name": "FinalOutputNode",
117
117
  "module": ["vellum", "workflows", "nodes", "displayable", "final_output_node", "node"],
@@ -102,7 +102,7 @@ def _topological_sort_layers(
102
102
  if neighbor in remaining_nodes:
103
103
  in_degree[neighbor] -= 1
104
104
 
105
- return layers
105
+ return [sorted(layer) for layer in layers]
106
106
 
107
107
 
108
108
  def _calculate_layer_height(layer_nodes: List[Tuple[str, NodeDisplayData]], node_spacing: float) -> float:
@@ -1,5 +1,8 @@
1
+ from dataclasses import asdict, is_dataclass
1
2
  from typing import TYPE_CHECKING, Any, Dict, List, cast
2
3
 
4
+ from pydantic import BaseModel
5
+
3
6
  from vellum.client.types.logical_operator import LogicalOperator
4
7
  from vellum.workflows.descriptors.base import BaseDescriptor
5
8
  from vellum.workflows.expressions.accessor import AccessorExpression
@@ -39,6 +42,7 @@ from vellum.workflows.references.state_value import StateValueReference
39
42
  from vellum.workflows.references.vellum_secret import VellumSecretReference
40
43
  from vellum.workflows.references.workflow_input import WorkflowInputReference
41
44
  from vellum.workflows.types.core import JsonArray, JsonObject
45
+ from vellum.workflows.types.definition import DeploymentDefinition
42
46
  from vellum.workflows.types.generics import is_workflow_class
43
47
  from vellum.workflows.utils.uuids import uuid4_from_hash
44
48
  from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
@@ -290,6 +294,10 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
290
294
  "items": cast(JsonArray, serialized_items), # list[JsonObject] -> JsonArray
291
295
  }
292
296
 
297
+ if is_dataclass(value) and not isinstance(value, type):
298
+ dict_value = asdict(value)
299
+ return serialize_value(display_context, dict_value)
300
+
293
301
  if isinstance(value, dict):
294
302
  serialized_entries: List[Dict[str, Any]] = [
295
303
  {
@@ -321,18 +329,41 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
321
329
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
322
330
 
323
331
  workflow_display = get_workflow_display(workflow_class=value)
324
- value = workflow_display.serialize()
332
+ serialized_value: dict = workflow_display.serialize()
333
+ name = serialized_value["workflow_raw_data"]["definition"]["name"]
334
+ description = value.__doc__ or ""
325
335
  return {
326
336
  "type": "CONSTANT_VALUE",
327
337
  "value": {
328
338
  "type": "JSON",
329
339
  "value": {
330
340
  "type": "INLINE_WORKFLOW",
331
- "exec_config": value,
341
+ "name": name,
342
+ "description": description,
343
+ "exec_config": serialized_value,
344
+ },
345
+ },
346
+ }
347
+
348
+ if isinstance(value, DeploymentDefinition):
349
+ return {
350
+ "type": "CONSTANT_VALUE",
351
+ "value": {
352
+ "type": "JSON",
353
+ "value": {
354
+ "type": "WORKFLOW_DEPLOYMENT",
355
+ "name": value.deployment,
356
+ "description": f"Workflow deployment for {value.deployment}",
357
+ "deployment": value.deployment,
358
+ "release_tag": value.release_tag,
332
359
  },
333
360
  },
334
361
  }
335
362
 
363
+ if isinstance(value, BaseModel):
364
+ dict_value = value.model_dump()
365
+ return serialize_value(display_context, dict_value)
366
+
336
367
  if not isinstance(value, BaseDescriptor):
337
368
  vellum_value = primitive_to_vellum_value(value)
338
369
  return {
@@ -1,12 +1,15 @@
1
1
  from copy import copy
2
+ import fnmatch
2
3
  from functools import cached_property
3
4
  import importlib
4
5
  import inspect
5
6
  import logging
7
+ import os
6
8
  from uuid import UUID
7
9
  from typing import Any, Dict, ForwardRef, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union, cast, get_args
8
10
 
9
11
  from vellum.client import Vellum as VellumClient
12
+ from vellum.core.pydantic_utilities import UniversalBaseModel
10
13
  from vellum.workflows import BaseWorkflow
11
14
  from vellum.workflows.constants import undefined
12
15
  from vellum.workflows.descriptors.base import BaseDescriptor
@@ -18,7 +21,7 @@ from vellum.workflows.nodes.displayable.final_output_node.node import FinalOutpu
18
21
  from vellum.workflows.nodes.utils import get_unadorned_node, get_unadorned_port, get_wrapped_node
19
22
  from vellum.workflows.ports import Port
20
23
  from vellum.workflows.references import OutputReference, WorkflowInputReference
21
- from vellum.workflows.types.core import JsonArray, JsonObject
24
+ from vellum.workflows.types.core import Json, JsonArray, JsonObject
22
25
  from vellum.workflows.types.generics import WorkflowType
23
26
  from vellum.workflows.types.utils import get_original_base
24
27
  from vellum.workflows.utils.uuids import uuid4_from_hash
@@ -31,7 +34,7 @@ from vellum_ee.workflows.display.base import (
31
34
  WorkflowMetaDisplay,
32
35
  WorkflowOutputDisplay,
33
36
  )
34
- from vellum_ee.workflows.display.editor.types import NodeDisplayData
37
+ from vellum_ee.workflows.display.editor.types import NodeDisplayData, NodeDisplayPosition
35
38
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
36
39
  from vellum_ee.workflows.display.nodes.get_node_display_class import get_node_display_class
37
40
  from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay, PortDisplay
@@ -48,6 +51,7 @@ from vellum_ee.workflows.display.types import (
48
51
  WorkflowInputsDisplays,
49
52
  WorkflowOutputDisplays,
50
53
  )
54
+ from vellum_ee.workflows.display.utils.auto_layout import auto_layout_nodes
51
55
  from vellum_ee.workflows.display.utils.expressions import serialize_value
52
56
  from vellum_ee.workflows.display.utils.registry import register_workflow_display_class
53
57
  from vellum_ee.workflows.display.utils.vellum import infer_vellum_variable_type
@@ -55,6 +59,19 @@ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class imp
55
59
 
56
60
  logger = logging.getLogger(__name__)
57
61
 
62
+ IGNORE_PATTERNS = [
63
+ "*.pyc",
64
+ "__pycache__",
65
+ ".*",
66
+ "node_modules/*",
67
+ "*.log",
68
+ ]
69
+
70
+
71
+ class WorkflowSerializationResult(UniversalBaseModel):
72
+ exec_config: Dict[str, Any]
73
+ errors: List[str]
74
+
58
75
 
59
76
  class BaseWorkflowDisplay(Generic[WorkflowType]):
60
77
  # Used to specify the display data for a workflow.
@@ -80,6 +97,8 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
80
97
 
81
98
  _errors: List[Exception]
82
99
 
100
+ _serialized_files: List[str]
101
+
83
102
  _dry_run: bool
84
103
 
85
104
  def __init__(
@@ -96,10 +115,20 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
96
115
  if self._parent_display_context
97
116
  else create_vellum_client()
98
117
  )
99
- self._errors: List[Exception] = []
118
+ self._errors = []
119
+ self._serialized_files = []
100
120
  self._dry_run = dry_run
101
121
 
102
122
  def serialize(self) -> JsonObject:
123
+ self._serialized_files = [
124
+ "__init__.py",
125
+ "display/*",
126
+ "inputs.py",
127
+ "nodes/*",
128
+ "state.py",
129
+ "workflow.py",
130
+ ]
131
+
103
132
  input_variables: JsonArray = []
104
133
  for workflow_input_reference, workflow_input_display in self.display_context.workflow_input_displays.items():
105
134
  default = (
@@ -320,9 +349,30 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
320
349
 
321
350
  edges.extend(synthetic_output_edges)
322
351
 
352
+ nodes_list = list(serialized_nodes.values())
353
+ nodes_dict_list = [cast(Dict[str, Any], node) for node in nodes_list if isinstance(node, dict)]
354
+
355
+ all_nodes_at_zero = all(
356
+ (
357
+ isinstance(node.get("display_data"), dict)
358
+ and isinstance(node["display_data"].get("position"), dict)
359
+ and node["display_data"]["position"].get("x", 0) == 0.0
360
+ and node["display_data"]["position"].get("y", 0) == 0.0
361
+ )
362
+ for node in nodes_dict_list
363
+ )
364
+
365
+ should_apply_auto_layout = all_nodes_at_zero and len(nodes_dict_list) > 0
366
+
367
+ if should_apply_auto_layout:
368
+ try:
369
+ self._apply_auto_layout(nodes_dict_list, edges)
370
+ except Exception as e:
371
+ self.add_error(e)
372
+
323
373
  return {
324
374
  "workflow_raw_data": {
325
- "nodes": list(serialized_nodes.values()),
375
+ "nodes": cast(JsonArray, nodes_dict_list),
326
376
  "edges": edges,
327
377
  "display_data": self.display_context.workflow_display.display_data.dict(),
328
378
  "definition": {
@@ -336,6 +386,54 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
336
386
  "output_variables": output_variables,
337
387
  }
338
388
 
389
+ def _apply_auto_layout(self, nodes_dict_list: List[Dict[str, Any]], edges: List[Json]) -> None:
390
+ """Apply auto-layout to nodes that are all positioned at (0,0)."""
391
+ nodes_for_layout: List[Tuple[str, NodeDisplayData]] = []
392
+ for node_dict in nodes_dict_list:
393
+ if isinstance(node_dict.get("id"), str) and isinstance(node_dict.get("display_data"), dict):
394
+ display_data = node_dict["display_data"]
395
+ position = display_data.get("position", {})
396
+ if isinstance(position, dict):
397
+ nodes_for_layout.append(
398
+ (
399
+ str(node_dict["id"]),
400
+ NodeDisplayData(
401
+ position=NodeDisplayPosition(
402
+ x=float(position.get("x", 0.0)), y=float(position.get("y", 0.0))
403
+ ),
404
+ width=display_data.get("width"),
405
+ height=display_data.get("height"),
406
+ comment=display_data.get("comment"),
407
+ ),
408
+ )
409
+ )
410
+
411
+ edges_for_layout: List[Tuple[str, str, EdgeDisplay]] = []
412
+ for edge in edges:
413
+ if isinstance(edge, dict):
414
+ edge_dict = cast(Dict[str, Any], edge)
415
+ edge_source_node_id: Optional[Any] = edge_dict.get("source_node_id")
416
+ edge_target_node_id: Optional[Any] = edge_dict.get("target_node_id")
417
+ edge_id_raw: Optional[Any] = edge_dict.get("id")
418
+ if (
419
+ isinstance(edge_source_node_id, str)
420
+ and isinstance(edge_target_node_id, str)
421
+ and isinstance(edge_id_raw, str)
422
+ ):
423
+ edges_for_layout.append(
424
+ (edge_source_node_id, edge_target_node_id, EdgeDisplay(id=UUID(edge_id_raw)))
425
+ )
426
+
427
+ positioned_nodes = auto_layout_nodes(nodes_for_layout, edges_for_layout)
428
+
429
+ for node_id, positioned_data in positioned_nodes:
430
+ for node_dict in nodes_dict_list:
431
+ node_id_val = node_dict.get("id")
432
+ display_data = node_dict.get("display_data")
433
+ if isinstance(node_id_val, str) and node_id_val == node_id and isinstance(display_data, dict):
434
+ display_data_dict = cast(Dict[str, Any], display_data)
435
+ display_data_dict["position"] = positioned_data.position.dict()
436
+
339
437
  @cached_property
340
438
  def workflow_id(self) -> UUID:
341
439
  """Can be overridden as a class attribute to specify a custom workflow id."""
@@ -592,8 +690,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
592
690
  try:
593
691
  display_module = importlib.import_module(full_workflow_display_module_path)
594
692
  except ModuleNotFoundError:
595
- logger.exception("Failed to import workflow display module: %s", full_workflow_display_module_path)
596
- return None
693
+ return BaseWorkflowDisplay._gather_event_display_context_from_workflow_crawling(module_path, workflow_class)
597
694
 
598
695
  WorkflowDisplayClass: Optional[Type[BaseWorkflowDisplay]] = None
599
696
  for name, definition in display_module.__dict__.items():
@@ -610,11 +707,26 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
610
707
  WorkflowDisplayClass = definition
611
708
  break
612
709
 
613
- if not WorkflowDisplayClass:
614
- logger.exception("No workflow display class found in module: %s", full_workflow_display_module_path)
615
- return None
710
+ if WorkflowDisplayClass:
711
+ return WorkflowDisplayClass().get_event_display_context()
712
+
713
+ return BaseWorkflowDisplay._gather_event_display_context_from_workflow_crawling(module_path, workflow_class)
714
+
715
+ @staticmethod
716
+ def _gather_event_display_context_from_workflow_crawling(
717
+ module_path: str,
718
+ workflow_class: Optional[Type[BaseWorkflow]] = None,
719
+ ) -> Union[WorkflowEventDisplayContext, None]:
720
+ try:
721
+ if workflow_class is None:
722
+ workflow_class = BaseWorkflow.load_from_module(module_path)
616
723
 
617
- return WorkflowDisplayClass().get_event_display_context()
724
+ workflow_display = get_workflow_display(workflow_class=workflow_class)
725
+ return workflow_display.get_event_display_context()
726
+
727
+ except ModuleNotFoundError:
728
+ logger.exception("Failed to load workflow from module %s", module_path)
729
+ return None
618
730
 
619
731
  def get_event_display_context(self):
620
732
  display_context = self.display_context
@@ -744,5 +856,82 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
744
856
  def _workflow(self) -> Type[WorkflowType]:
745
857
  return cast(Type[WorkflowType], self.__class__.infer_workflow_class())
746
858
 
859
+ @staticmethod
860
+ def serialize_module(
861
+ module: str,
862
+ *,
863
+ client: Optional[VellumClient] = None,
864
+ dry_run: bool = False,
865
+ ) -> WorkflowSerializationResult:
866
+ """
867
+ Load a workflow from a module and serialize it to JSON.
868
+
869
+ Args:
870
+ module: The module path to load the workflow from
871
+ client: Optional Vellum client to use for serialization
872
+ dry_run: Whether to run in dry-run mode
873
+
874
+ Returns:
875
+ WorkflowSerializationResult containing exec_config and errors
876
+ """
877
+ workflow = BaseWorkflow.load_from_module(module)
878
+ workflow_display = get_workflow_display(
879
+ workflow_class=workflow,
880
+ client=client,
881
+ dry_run=dry_run,
882
+ )
883
+
884
+ exec_config = workflow_display.serialize()
885
+ additional_files = workflow_display._gather_additional_module_files(module)
886
+
887
+ if additional_files:
888
+ exec_config["module_data"] = {"additional_files": cast(JsonObject, additional_files)}
889
+
890
+ return WorkflowSerializationResult(
891
+ exec_config=exec_config,
892
+ errors=[str(error) for error in workflow_display.errors],
893
+ )
894
+
895
+ def _gather_additional_module_files(self, module_path: str) -> Dict[str, str]:
896
+ workflow_module_path = f"{module_path}.workflow"
897
+ workflow_module = importlib.import_module(workflow_module_path)
898
+
899
+ workflow_file_path = workflow_module.__file__
900
+ if not workflow_file_path:
901
+ return {}
902
+
903
+ module_dir = os.path.dirname(workflow_file_path)
904
+ additional_files = {}
905
+
906
+ for root, _, filenames in os.walk(module_dir):
907
+ for filename in filenames:
908
+ file_path = os.path.join(root, filename)
909
+ relative_path = os.path.relpath(file_path, start=module_dir)
910
+
911
+ should_ignore = False
912
+ for ignore_pattern in IGNORE_PATTERNS:
913
+ if fnmatch.fnmatch(filename, ignore_pattern) or fnmatch.fnmatch(relative_path, ignore_pattern):
914
+ should_ignore = True
915
+ break
916
+
917
+ if not should_ignore:
918
+ for serialized_pattern in self._serialized_files:
919
+ if fnmatch.fnmatch(relative_path, serialized_pattern) or fnmatch.fnmatch(
920
+ filename, serialized_pattern
921
+ ):
922
+ should_ignore = True
923
+ break
924
+
925
+ if should_ignore:
926
+ continue
927
+
928
+ try:
929
+ with open(file_path, encoding="utf-8") as f:
930
+ additional_files[relative_path] = f.read()
931
+ except (UnicodeDecodeError, PermissionError):
932
+ continue
933
+
934
+ return additional_files
935
+
747
936
 
748
937
  register_workflow_display_class(workflow_class=BaseWorkflow, workflow_display_class=BaseWorkflowDisplay)
@@ -4,6 +4,7 @@ import sys
4
4
  from uuid import uuid4
5
5
 
6
6
  from vellum.workflows import BaseWorkflow
7
+ from vellum.workflows.events.workflow import WorkflowEventDisplayContext
7
8
  from vellum_ee.workflows.display.workflows import BaseWorkflowDisplay
8
9
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
9
10
 
@@ -102,3 +103,43 @@ class MyCustomWorkflowDisplay(BaseWorkflowDisplay[MyCustomWorkflow]):
102
103
  assert display_meta.workflow_outputs == {
103
104
  "answer": workflow_output_id,
104
105
  }
106
+
107
+
108
+ def test_gather_event_display_context__workflow_crawling_without_display_module():
109
+ # GIVEN a workflow module without a display module
110
+ files = {
111
+ "__init__.py": "",
112
+ "workflow.py": """\
113
+ from vellum.workflows import BaseWorkflow
114
+ from vellum.workflows.nodes import BaseNode
115
+
116
+ class TestNode(BaseNode):
117
+ class Outputs(BaseNode.Outputs):
118
+ result: str
119
+
120
+ class TestWorkflow(BaseWorkflow):
121
+ graph = TestNode
122
+
123
+ class Outputs(BaseWorkflow.Outputs):
124
+ final_result = TestNode.Outputs.result
125
+ """,
126
+ }
127
+
128
+ namespace = str(uuid4())
129
+
130
+ # AND the virtual file loader is registered
131
+ sys.meta_path.append(VirtualFileFinder(files, namespace))
132
+
133
+ # WHEN the workflow display context is gathered
134
+ display_meta = BaseWorkflowDisplay.gather_event_display_context(namespace)
135
+
136
+ # THEN the workflow display context should be successfully created via workflow crawling
137
+ assert display_meta is not None
138
+ assert isinstance(display_meta, WorkflowEventDisplayContext)
139
+
140
+ # AND the node displays should be populated with the correct node structure
141
+ assert len(display_meta.node_displays) == 1
142
+ node_display = list(display_meta.node_displays.values())[0]
143
+ assert "result" in node_display.output_display
144
+
145
+ assert "final_result" in display_meta.workflow_outputs
@@ -0,0 +1,47 @@
1
+ from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
2
+
3
+
4
+ def test_serialize_module_happy_path():
5
+ """Test that serialize_module works with a valid module path."""
6
+ module_path = "tests.workflows.trivial"
7
+
8
+ result = BaseWorkflowDisplay.serialize_module(module_path)
9
+
10
+ assert hasattr(result, "exec_config")
11
+ assert hasattr(result, "errors")
12
+ assert isinstance(result.exec_config, dict)
13
+ assert isinstance(result.errors, list)
14
+ assert "workflow_raw_data" in result.exec_config
15
+ assert "input_variables" in result.exec_config
16
+ assert "output_variables" in result.exec_config
17
+
18
+
19
+ def test_serialize_module_includes_additional_files():
20
+ """Test that serialize_module includes additional files from the module directory."""
21
+ module_path = "tests.workflows.module_with_additional_files"
22
+
23
+ result = BaseWorkflowDisplay.serialize_module(module_path)
24
+
25
+ assert hasattr(result, "exec_config")
26
+ assert hasattr(result, "errors")
27
+ assert isinstance(result.exec_config, dict)
28
+ assert isinstance(result.errors, list)
29
+
30
+ assert "module_data" in result.exec_config
31
+ assert "additional_files" in result.exec_config["module_data"]
32
+
33
+ additional_files = result.exec_config["module_data"]["additional_files"]
34
+ assert isinstance(additional_files, dict)
35
+
36
+ assert "helper.py" in additional_files
37
+ assert "data.txt" in additional_files
38
+ assert "utils/constants.py" in additional_files
39
+
40
+ assert "workflow.py" not in additional_files
41
+ assert "__init__.py" not in additional_files
42
+ assert "utils/__init__.py" not in additional_files
43
+ assert "nodes/test_node.py" not in additional_files
44
+
45
+ assert "def helper_function():" in additional_files["helper.py"]
46
+ assert "sample data file" in additional_files["data.txt"]
47
+ assert "CONSTANT_VALUE" in additional_files["utils/constants.py"]