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.
- vellum/__init__.py +8 -0
- vellum/client/core/client_wrapper.py +1 -1
- vellum/client/types/__init__.py +8 -0
- vellum/client/types/build_status_enum.py +5 -0
- vellum/client/types/container_image_build_config.py +20 -0
- vellum/client/types/container_image_read.py +4 -0
- vellum/client/types/execute_api_response.py +2 -2
- vellum/client/types/folder_entity.py +2 -0
- vellum/client/types/folder_entity_dataset.py +26 -0
- vellum/client/types/folder_entity_dataset_data.py +25 -0
- vellum/client/types/secret_type_enum.py +3 -1
- vellum/types/build_status_enum.py +3 -0
- vellum/types/container_image_build_config.py +3 -0
- vellum/types/folder_entity_dataset.py +3 -0
- vellum/types/folder_entity_dataset_data.py +3 -0
- vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -1
- vellum/workflows/nodes/displayable/api_node/node.py +2 -0
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +43 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +6 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +30 -4
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +43 -3
- vellum/workflows/nodes/displayable/bases/utils.py +2 -0
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +68 -58
- vellum/workflows/nodes/experimental/tool_calling_node/node.py +15 -11
- vellum/workflows/nodes/experimental/tool_calling_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/experimental/tool_calling_node/tests/test_node.py +13 -0
- vellum/workflows/nodes/experimental/tool_calling_node/tests/test_utils.py +49 -0
- vellum/workflows/nodes/experimental/tool_calling_node/utils.py +78 -7
- vellum/workflows/ports/utils.py +26 -6
- vellum/workflows/runner/runner.py +35 -3
- vellum/workflows/state/encoder.py +2 -0
- vellum/workflows/types/core.py +12 -0
- vellum/workflows/types/definition.py +6 -0
- vellum/workflows/utils/functions.py +12 -12
- vellum/workflows/utils/pydantic_schema.py +38 -0
- vellum/workflows/utils/tests/test_functions.py +18 -18
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/METADATA +1 -1
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/RECORD +74 -61
- vellum_cli/push.py +6 -8
- vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +36 -7
- vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +8 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +102 -0
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +91 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +5 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +12 -12
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +10 -10
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +20 -9
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +120 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +8 -8
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +6 -6
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +8 -8
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +4 -4
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +12 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +8 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py +62 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +2 -2
- vellum_ee/workflows/display/utils/auto_layout.py +1 -1
- vellum_ee/workflows/display/utils/expressions.py +33 -2
- vellum_ee/workflows/display/workflows/base_workflow_display.py +199 -10
- vellum_ee/workflows/tests/test_display_meta.py +41 -0
- vellum_ee/workflows/tests/test_serialize_module.py +47 -0
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/LICENSE +0 -0
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/WHEEL +0 -0
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.74.dist-info}/entry_points.txt +0 -0
vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py
CHANGED
@@ -61,7 +61,7 @@ def test_serialize_workflow__missing_final_output_node():
|
|
61
61
|
},
|
62
62
|
}
|
63
63
|
],
|
64
|
-
"display_data": {"position": {"x":
|
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":
|
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
|
-
|
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
|
-
"
|
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
|
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":
|
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
|
-
|
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
|
614
|
-
|
615
|
-
|
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
|
-
|
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"]
|
File without changes
|
File without changes
|
File without changes
|