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.
Files changed (39) hide show
  1. vellum/client/core/client_wrapper.py +2 -2
  2. vellum/client/types/function_definition.py +5 -0
  3. vellum/client/types/scenario_input_audio_variable_value.py +1 -1
  4. vellum/client/types/scenario_input_document_variable_value.py +1 -1
  5. vellum/client/types/scenario_input_image_variable_value.py +1 -1
  6. vellum/client/types/scenario_input_video_variable_value.py +1 -1
  7. vellum/workflows/emitters/vellum_emitter.py +55 -9
  8. vellum/workflows/events/node.py +1 -1
  9. vellum/workflows/events/tests/test_event.py +1 -1
  10. vellum/workflows/events/workflow.py +1 -1
  11. vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -2
  12. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +21 -15
  13. vellum/workflows/resolvers/resolver.py +18 -2
  14. vellum/workflows/resolvers/tests/test_resolver.py +121 -0
  15. vellum/workflows/runner/runner.py +17 -17
  16. vellum/workflows/state/encoder.py +0 -37
  17. vellum/workflows/state/tests/test_state.py +14 -0
  18. vellum/workflows/types/code_execution_node_wrappers.py +3 -0
  19. vellum/workflows/utils/functions.py +35 -0
  20. vellum/workflows/utils/vellum_variables.py +11 -2
  21. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/METADATA +1 -1
  22. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/RECORD +39 -37
  23. vellum_cli/__init__.py +21 -0
  24. vellum_cli/move.py +56 -0
  25. vellum_cli/tests/test_move.py +154 -0
  26. vellum_ee/workflows/display/base.py +1 -0
  27. vellum_ee/workflows/display/editor/types.py +1 -0
  28. vellum_ee/workflows/display/nodes/base_node_display.py +1 -0
  29. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +18 -2
  30. vellum_ee/workflows/display/tests/test_base_workflow_display.py +52 -2
  31. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +17 -5
  32. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -0
  33. vellum_ee/workflows/display/utils/events.py +1 -0
  34. vellum_ee/workflows/display/utils/expressions.py +44 -0
  35. vellum_ee/workflows/display/utils/tests/test_events.py +11 -1
  36. vellum_ee/workflows/display/workflows/base_workflow_display.py +32 -22
  37. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/LICENSE +0 -0
  38. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/WHEEL +0 -0
  39. {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 = read_file_from_path(
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) == 1
50
+ assert len(output_variables) == 2
51
51
  assert not DeepDiff(
52
- [{"id": "15a0ab89-8ed4-43b9-afa2-3c0b29d4dc3e", "key": "results", "type": "JSON"}],
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"]) == 2
60
- assert len(workflow_raw_data["nodes"]) == 3
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": -50.0}},
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,
@@ -152,6 +152,7 @@ def test_serialize_workflow():
152
152
  },
153
153
  "required": ["location", "unit"],
154
154
  },
155
+ "inputs": None,
155
156
  "forced": None,
156
157
  "strict": None,
157
158
  },
@@ -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
- # AND workflow_version_exec_config is set to the expected config
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
- edges.append(
333
- {
334
- "id": str(entrypoint_display.edge_display.id),
335
- "source_node_id": str(entrypoint_node_id),
336
- "source_handle_id": str(entrypoint_node_source_handle_id),
337
- "target_node_id": str(target_node_display.node_id),
338
- "target_handle_id": str(target_node_display.get_trigger_id()),
339
- "type": "DEFAULT",
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
- edges.append(
357
- {
358
- "id": str(edge_display.id),
359
- "source_node_id": str(source_node_port_display.node_id),
360
- "source_handle_id": str(source_node_port_display.id),
361
- "target_node_id": str(target_node_display.node_id),
362
- "target_handle_id": str(
363
- target_node_display.get_target_handle_id_by_source_node_id(source_node_port_display.node_id)
364
- ),
365
- "type": "DEFAULT",
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]] = []