vellum-ai 1.3.2__py3-none-any.whl → 1.3.4__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/client/core/client_wrapper.py +2 -2
- vellum/client/types/function_definition.py +5 -0
- vellum/client/types/scenario_input_audio_variable_value.py +1 -1
- vellum/client/types/scenario_input_document_variable_value.py +1 -1
- vellum/client/types/scenario_input_image_variable_value.py +1 -1
- vellum/client/types/scenario_input_video_variable_value.py +1 -1
- vellum/workflows/emitters/vellum_emitter.py +55 -9
- vellum/workflows/events/node.py +1 -1
- vellum/workflows/events/tests/test_event.py +1 -1
- vellum/workflows/events/workflow.py +1 -1
- vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -2
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +21 -15
- vellum/workflows/resolvers/resolver.py +18 -2
- vellum/workflows/resolvers/tests/test_resolver.py +121 -0
- vellum/workflows/runner/runner.py +17 -17
- vellum/workflows/state/encoder.py +0 -37
- vellum/workflows/state/tests/test_state.py +14 -0
- vellum/workflows/types/code_execution_node_wrappers.py +3 -0
- vellum/workflows/utils/functions.py +35 -0
- vellum/workflows/utils/vellum_variables.py +11 -2
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/METADATA +1 -1
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/RECORD +39 -37
- vellum_cli/__init__.py +21 -0
- vellum_cli/move.py +56 -0
- vellum_cli/tests/test_move.py +154 -0
- vellum_ee/workflows/display/base.py +1 -0
- vellum_ee/workflows/display/editor/types.py +1 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +1 -0
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +18 -2
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +52 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +17 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -0
- vellum_ee/workflows/display/utils/events.py +1 -0
- vellum_ee/workflows/display/utils/expressions.py +44 -0
- vellum_ee/workflows/display/utils/tests/test_events.py +11 -1
- vellum_ee/workflows/display/workflows/base_workflow_display.py +32 -22
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/LICENSE +0 -0
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/WHEEL +0 -0
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,9 @@
|
|
1
1
|
import inspect
|
2
|
+
import os
|
2
3
|
from uuid import UUID
|
3
4
|
from typing import ClassVar, Generic, Optional, TypeVar
|
4
5
|
|
5
6
|
from vellum.workflows.nodes.displayable.code_execution_node import CodeExecutionNode
|
6
|
-
from vellum.workflows.nodes.displayable.code_execution_node.utils import read_file_from_path
|
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
9
|
from vellum_ee.workflows.display.exceptions import NodeValidationError
|
@@ -11,10 +11,26 @@ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
|
|
11
11
|
from vellum_ee.workflows.display.nodes.utils import raise_if_descriptor
|
12
12
|
from vellum_ee.workflows.display.nodes.vellum.utils import create_node_input
|
13
13
|
from vellum_ee.workflows.display.types import WorkflowDisplayContext
|
14
|
+
from vellum_ee.workflows.display.utils.expressions import virtual_open
|
14
15
|
|
15
16
|
_CodeExecutionNodeType = TypeVar("_CodeExecutionNodeType", bound=CodeExecutionNode)
|
16
17
|
|
17
18
|
|
19
|
+
def _read_file_from_path_with_virtual_support(node_filepath: str, script_filepath: str) -> Optional[str]:
|
20
|
+
"""
|
21
|
+
Read a file using virtual_open which handles VirtualFileFinder instances.
|
22
|
+
"""
|
23
|
+
node_filepath_dir = os.path.dirname(node_filepath)
|
24
|
+
normalized_script_filepath = script_filepath.lstrip("./")
|
25
|
+
full_filepath = os.path.join(node_filepath_dir, normalized_script_filepath)
|
26
|
+
|
27
|
+
try:
|
28
|
+
with virtual_open(full_filepath, "r") as file:
|
29
|
+
return file.read()
|
30
|
+
except (FileNotFoundError, IsADirectoryError):
|
31
|
+
return None
|
32
|
+
|
33
|
+
|
18
34
|
class BaseCodeExecutionNodeDisplay(BaseNodeDisplay[_CodeExecutionNodeType], Generic[_CodeExecutionNodeType]):
|
19
35
|
output_id: ClassVar[Optional[UUID]] = None
|
20
36
|
log_output_id: ClassVar[Optional[UUID]] = None
|
@@ -37,7 +53,7 @@ class BaseCodeExecutionNodeDisplay(BaseNodeDisplay[_CodeExecutionNodeType], Gene
|
|
37
53
|
code_value = raw_code
|
38
54
|
elif filepath:
|
39
55
|
node_file_path = inspect.getfile(node)
|
40
|
-
file_code =
|
56
|
+
file_code = _read_file_from_path_with_virtual_support(
|
41
57
|
node_filepath=node_file_path,
|
42
58
|
script_filepath=filepath,
|
43
59
|
)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from uuid import UUID
|
2
|
-
from typing import Dict
|
2
|
+
from typing import Any, Dict, List, cast
|
3
3
|
|
4
4
|
from vellum.workflows.inputs import BaseInputs
|
5
5
|
from vellum.workflows.nodes import BaseNode, InlineSubworkflowNode
|
@@ -8,7 +8,7 @@ from vellum.workflows.ports.port import Port
|
|
8
8
|
from vellum.workflows.references.lazy import LazyReference
|
9
9
|
from vellum.workflows.state import BaseState
|
10
10
|
from vellum.workflows.workflows.base import BaseWorkflow
|
11
|
-
from vellum_ee.workflows.display.base import WorkflowInputsDisplay
|
11
|
+
from vellum_ee.workflows.display.base import EdgeDisplay, WorkflowInputsDisplay
|
12
12
|
from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
|
13
13
|
from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
|
14
14
|
|
@@ -379,3 +379,53 @@ def test_global_propagation_deep_nested_subworkflows():
|
|
379
379
|
inner_global_names = {ref.name for ref in inner_display.display_context.global_workflow_input_displays.keys()}
|
380
380
|
|
381
381
|
assert inner_global_names == {"middle_param", "inner_param", "root_param"}
|
382
|
+
|
383
|
+
|
384
|
+
def test_serialize_workflow_with_edge_display_data():
|
385
|
+
"""
|
386
|
+
Tests that edges with z_index values serialize display_data correctly.
|
387
|
+
"""
|
388
|
+
|
389
|
+
# GIVEN a workflow with connected nodes
|
390
|
+
class StartNode(BaseNode):
|
391
|
+
class Outputs(BaseNode.Outputs):
|
392
|
+
result: str
|
393
|
+
|
394
|
+
class EndNode(BaseNode):
|
395
|
+
class Outputs(BaseNode.Outputs):
|
396
|
+
final: str
|
397
|
+
|
398
|
+
class TestWorkflow(BaseWorkflow):
|
399
|
+
graph = StartNode >> EndNode
|
400
|
+
|
401
|
+
class Outputs(BaseWorkflow.Outputs):
|
402
|
+
final_result = EndNode.Outputs.final
|
403
|
+
|
404
|
+
class TestWorkflowDisplay(BaseWorkflowDisplay[TestWorkflow]):
|
405
|
+
edge_displays = {
|
406
|
+
(StartNode.Ports.default, EndNode): EdgeDisplay(id=UUID("12345678-1234-5678-1234-567812345678"), z_index=5)
|
407
|
+
}
|
408
|
+
|
409
|
+
# WHEN we serialize the workflow with the custom display
|
410
|
+
display = get_workflow_display(
|
411
|
+
base_display_class=TestWorkflowDisplay,
|
412
|
+
workflow_class=TestWorkflow,
|
413
|
+
)
|
414
|
+
serialized_workflow = display.serialize()
|
415
|
+
|
416
|
+
# THEN the edge should include display_data with z_index
|
417
|
+
workflow_raw_data = cast(Dict[str, Any], serialized_workflow["workflow_raw_data"])
|
418
|
+
edges = cast(List[Dict[str, Any]], workflow_raw_data["edges"])
|
419
|
+
|
420
|
+
edge_with_display_data = None
|
421
|
+
for edge in edges:
|
422
|
+
if edge["id"] == "12345678-1234-5678-1234-567812345678":
|
423
|
+
edge_with_display_data = edge
|
424
|
+
break
|
425
|
+
|
426
|
+
assert edge_with_display_data is not None, "Edge with custom UUID not found"
|
427
|
+
assert edge_with_display_data["display_data"] == {"z_index": 5}
|
428
|
+
|
429
|
+
assert edge_with_display_data["type"] == "DEFAULT"
|
430
|
+
assert "source_node_id" in edge_with_display_data
|
431
|
+
assert "target_node_id" in edge_with_display_data
|
@@ -47,17 +47,20 @@ def test_serialize_workflow():
|
|
47
47
|
|
48
48
|
# AND its output variables should be what we expect
|
49
49
|
output_variables = serialized_workflow["output_variables"]
|
50
|
-
assert len(output_variables) ==
|
50
|
+
assert len(output_variables) == 2
|
51
51
|
assert not DeepDiff(
|
52
|
-
[
|
52
|
+
[
|
53
|
+
{"id": "15a0ab89-8ed4-43b9-afa2-3c0b29d4dc3e", "key": "results", "type": "JSON"},
|
54
|
+
{"id": "0ef1608e-1737-41cc-9b90-a8e124138f70", "key": "json", "type": "JSON"},
|
55
|
+
],
|
53
56
|
output_variables,
|
54
57
|
ignore_order=True,
|
55
58
|
)
|
56
59
|
|
57
60
|
# AND its raw data should be what we expect
|
58
61
|
workflow_raw_data = serialized_workflow["workflow_raw_data"]
|
59
|
-
assert len(workflow_raw_data["edges"]) ==
|
60
|
-
assert len(workflow_raw_data["nodes"]) ==
|
62
|
+
assert len(workflow_raw_data["edges"]) == 3
|
63
|
+
assert len(workflow_raw_data["nodes"]) == 4
|
61
64
|
|
62
65
|
# AND each node should be serialized correctly
|
63
66
|
entrypoint_node = workflow_raw_data["nodes"][0]
|
@@ -240,6 +243,7 @@ def test_serialize_workflow():
|
|
240
243
|
"name": "favorite_noun",
|
241
244
|
"description": "Returns the favorite noun of the user",
|
242
245
|
"parameters": {},
|
246
|
+
"inputs": None,
|
243
247
|
"forced": None,
|
244
248
|
"strict": None,
|
245
249
|
}
|
@@ -305,7 +309,7 @@ def test_serialize_workflow():
|
|
305
309
|
},
|
306
310
|
}
|
307
311
|
],
|
308
|
-
"display_data": {"position": {"x": 400.0, "y":
|
312
|
+
"display_data": {"position": {"x": 400.0, "y": 75.0}},
|
309
313
|
"base": {
|
310
314
|
"name": "FinalOutputNode",
|
311
315
|
"module": ["vellum", "workflows", "nodes", "displayable", "final_output_node", "node"],
|
@@ -336,6 +340,14 @@ def test_serialize_workflow():
|
|
336
340
|
"target_handle_id": "46c99277-2b4b-477d-851c-ea497aef6b16",
|
337
341
|
"type": "DEFAULT",
|
338
342
|
},
|
343
|
+
{
|
344
|
+
"id": "0b1a2960-4cd5-4045-844f-42b6c87487aa",
|
345
|
+
"source_node_id": "8450dd06-975a-41a4-a564-808ee8808fe6",
|
346
|
+
"source_handle_id": "d4a097ab-e22d-42f1-b6bc-2ed96856377a",
|
347
|
+
"target_node_id": "1f4e3b7b-6af1-42c8-ab33-05b0f01e2b62",
|
348
|
+
"target_handle_id": "7d94907f-c840-4ced-b813-ee3b17f2a8a9",
|
349
|
+
"type": "DEFAULT",
|
350
|
+
},
|
339
351
|
],
|
340
352
|
serialized_edges,
|
341
353
|
ignore_order=True,
|
@@ -36,6 +36,7 @@ def event_enricher(event: WorkflowExecutionInitiatedEvent) -> WorkflowExecutionI
|
|
36
36
|
dry_run=True,
|
37
37
|
)
|
38
38
|
register_workflow_display_context(event.span_id, workflow_display.display_context)
|
39
|
+
event.body.display_context = workflow_display.get_event_display_context()
|
39
40
|
|
40
41
|
if event.body.workflow_definition.is_dynamic or _should_mark_workflow_dynamic(event):
|
41
42
|
register_workflow_display_class(workflow_definition, workflow_display.__class__)
|
@@ -1,4 +1,7 @@
|
|
1
1
|
from dataclasses import asdict, is_dataclass
|
2
|
+
import inspect
|
3
|
+
from io import StringIO
|
4
|
+
import sys
|
2
5
|
from typing import TYPE_CHECKING, Any, Dict, List, cast
|
3
6
|
|
4
7
|
from pydantic import BaseModel
|
@@ -48,13 +51,31 @@ from vellum.workflows.references.workflow_input import WorkflowInputReference
|
|
48
51
|
from vellum.workflows.types.core import JsonArray, JsonObject
|
49
52
|
from vellum.workflows.types.definition import DeploymentDefinition
|
50
53
|
from vellum.workflows.types.generics import is_workflow_class
|
54
|
+
from vellum.workflows.utils.functions import compile_function_definition
|
51
55
|
from vellum.workflows.utils.uuids import uuid4_from_hash
|
52
56
|
from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
|
57
|
+
from vellum_ee.workflows.server.virtual_file_loader import VirtualFileLoader
|
53
58
|
|
54
59
|
if TYPE_CHECKING:
|
55
60
|
from vellum_ee.workflows.display.types import WorkflowDisplayContext
|
56
61
|
|
57
62
|
|
63
|
+
def virtual_open(file_path: str, mode: str = "r"):
|
64
|
+
"""
|
65
|
+
Open a file, checking VirtualFileFinder instances first before falling back to regular open().
|
66
|
+
"""
|
67
|
+
for finder in sys.meta_path:
|
68
|
+
if hasattr(finder, "loader") and isinstance(finder.loader, VirtualFileLoader):
|
69
|
+
namespace = finder.loader.namespace
|
70
|
+
if file_path.startswith(namespace + "/"):
|
71
|
+
relative_path = file_path[len(namespace) + 1 :]
|
72
|
+
content = finder.loader._get_code(relative_path)
|
73
|
+
if content is not None:
|
74
|
+
return StringIO(content)
|
75
|
+
|
76
|
+
return open(file_path, mode)
|
77
|
+
|
78
|
+
|
58
79
|
def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperator:
|
59
80
|
if isinstance(descriptor, EqualsExpression):
|
60
81
|
return "="
|
@@ -399,6 +420,29 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
|
|
399
420
|
dict_value = value.model_dump()
|
400
421
|
return serialize_value(display_context, dict_value)
|
401
422
|
|
423
|
+
if callable(value):
|
424
|
+
function_definition = compile_function_definition(value)
|
425
|
+
source_path = inspect.getsourcefile(value)
|
426
|
+
if source_path is not None:
|
427
|
+
with virtual_open(source_path) as f:
|
428
|
+
source_code = f.read()
|
429
|
+
else:
|
430
|
+
source_code = f"Source code not available for {value.__name__}"
|
431
|
+
|
432
|
+
return {
|
433
|
+
"type": "CONSTANT_VALUE",
|
434
|
+
"value": {
|
435
|
+
"type": "JSON",
|
436
|
+
"value": {
|
437
|
+
"type": "CODE_EXECUTION",
|
438
|
+
"name": function_definition.name,
|
439
|
+
"description": function_definition.description,
|
440
|
+
"definition": function_definition.model_dump(),
|
441
|
+
"src": source_code,
|
442
|
+
},
|
443
|
+
},
|
444
|
+
}
|
445
|
+
|
402
446
|
if not isinstance(value, BaseDescriptor):
|
403
447
|
vellum_value = primitive_to_vellum_value(value)
|
404
448
|
return {
|
@@ -67,9 +67,14 @@ def test_event_enricher_static_workflow(is_dynamic: bool, expected_config: Optio
|
|
67
67
|
# WHEN the event_enricher is called with mocked dependencies
|
68
68
|
event_enricher(event)
|
69
69
|
|
70
|
-
#
|
70
|
+
# THEN workflow_version_exec_config is set to the expected config
|
71
71
|
assert event.body.workflow_version_exec_config == expected_config
|
72
72
|
|
73
|
+
assert event.body.display_context is not None
|
74
|
+
assert hasattr(event.body.display_context, "node_displays")
|
75
|
+
assert hasattr(event.body.display_context, "workflow_inputs")
|
76
|
+
assert hasattr(event.body.display_context, "workflow_outputs")
|
77
|
+
|
73
78
|
|
74
79
|
def test_event_enricher_marks_subworkflow_deployment_as_dynamic():
|
75
80
|
"""Test that event_enricher treats subworkflow deployments as dynamic."""
|
@@ -109,3 +114,8 @@ def test_event_enricher_marks_subworkflow_deployment_as_dynamic():
|
|
109
114
|
|
110
115
|
assert hasattr(enriched_event.body, "workflow_version_exec_config")
|
111
116
|
assert enriched_event.body.workflow_version_exec_config is not None
|
117
|
+
|
118
|
+
assert enriched_event.body.display_context is not None
|
119
|
+
assert hasattr(enriched_event.body.display_context, "node_displays")
|
120
|
+
assert hasattr(enriched_event.body.display_context, "workflow_inputs")
|
121
|
+
assert hasattr(enriched_event.body.display_context, "workflow_outputs")
|
@@ -329,16 +329,18 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
|
|
329
329
|
continue
|
330
330
|
|
331
331
|
target_node_display = self.display_context.node_displays[unadorned_target_node]
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
332
|
+
entrypoint_edge_dict: Dict[str, Json] = {
|
333
|
+
"id": str(entrypoint_display.edge_display.id),
|
334
|
+
"source_node_id": str(entrypoint_node_id),
|
335
|
+
"source_handle_id": str(entrypoint_node_source_handle_id),
|
336
|
+
"target_node_id": str(target_node_display.node_id),
|
337
|
+
"target_handle_id": str(target_node_display.get_trigger_id()),
|
338
|
+
"type": "DEFAULT",
|
339
|
+
}
|
340
|
+
display_data = self._serialize_edge_display_data(entrypoint_display.edge_display)
|
341
|
+
if display_data is not None:
|
342
|
+
entrypoint_edge_dict["display_data"] = display_data
|
343
|
+
edges.append(entrypoint_edge_dict)
|
342
344
|
|
343
345
|
for (source_node_port, target_node), edge_display in self.display_context.edge_displays.items():
|
344
346
|
unadorned_source_node_port = get_unadorned_port(source_node_port)
|
@@ -353,18 +355,20 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
|
|
353
355
|
source_node_port_display = self.display_context.port_displays[unadorned_source_node_port]
|
354
356
|
target_node_display = self.display_context.node_displays[unadorned_target_node]
|
355
357
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
358
|
+
regular_edge_dict: Dict[str, Json] = {
|
359
|
+
"id": str(edge_display.id),
|
360
|
+
"source_node_id": str(source_node_port_display.node_id),
|
361
|
+
"source_handle_id": str(source_node_port_display.id),
|
362
|
+
"target_node_id": str(target_node_display.node_id),
|
363
|
+
"target_handle_id": str(
|
364
|
+
target_node_display.get_target_handle_id_by_source_node_id(source_node_port_display.node_id)
|
365
|
+
),
|
366
|
+
"type": "DEFAULT",
|
367
|
+
}
|
368
|
+
display_data = self._serialize_edge_display_data(edge_display)
|
369
|
+
if display_data is not None:
|
370
|
+
regular_edge_dict["display_data"] = display_data
|
371
|
+
edges.append(regular_edge_dict)
|
368
372
|
|
369
373
|
edges.extend(synthetic_output_edges)
|
370
374
|
|
@@ -405,6 +409,12 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
|
|
405
409
|
"output_variables": output_variables,
|
406
410
|
}
|
407
411
|
|
412
|
+
def _serialize_edge_display_data(self, edge_display: EdgeDisplay) -> Optional[JsonObject]:
|
413
|
+
"""Serialize edge display data, returning None if no display data is present."""
|
414
|
+
if edge_display.z_index is not None:
|
415
|
+
return {"z_index": edge_display.z_index}
|
416
|
+
return None
|
417
|
+
|
408
418
|
def _apply_auto_layout(self, nodes_dict_list: List[Dict[str, Any]], edges: List[Json]) -> None:
|
409
419
|
"""Apply auto-layout to nodes that are all positioned at (0,0)."""
|
410
420
|
nodes_for_layout: List[Tuple[str, NodeDisplayData]] = []
|
File without changes
|
File without changes
|
File without changes
|