vellum-ai 1.8.6__py3-none-any.whl → 1.9.0__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 (29) hide show
  1. vellum/client/core/client_wrapper.py +2 -2
  2. vellum/client/types/integration_name.py +1 -0
  3. vellum/workflows/inputs/dataset_row.py +9 -7
  4. vellum/workflows/nodes/displayable/final_output_node/node.py +4 -0
  5. vellum/workflows/nodes/displayable/final_output_node/tests/test_node.py +28 -0
  6. vellum/workflows/nodes/displayable/set_state_node/__init__.py +5 -0
  7. vellum/workflows/nodes/displayable/set_state_node/node.py +71 -0
  8. vellum/workflows/nodes/displayable/set_state_node/tests/__init__.py +0 -0
  9. vellum/workflows/nodes/displayable/set_state_node/tests/test_node.py +212 -0
  10. vellum/workflows/sandbox.py +13 -3
  11. vellum/workflows/tests/test_dataset_row.py +20 -0
  12. vellum/workflows/tests/test_sandbox.py +40 -0
  13. vellum/workflows/tests/triggers/{test_vellum_integration_trigger.py → test_integration_trigger.py} +22 -22
  14. vellum/workflows/triggers/__init__.py +1 -2
  15. vellum/workflows/triggers/base.py +10 -0
  16. vellum/workflows/triggers/integration.py +168 -49
  17. vellum/workflows/triggers/tests/test_integration.py +49 -20
  18. vellum/workflows/workflows/base.py +44 -0
  19. {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/METADATA +1 -1
  20. {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/RECORD +28 -25
  21. vellum_ee/workflows/display/tests/workflow_serialization/{test_vellum_integration_trigger_serialization.py → test_integration_trigger_serialization.py} +8 -8
  22. vellum_ee/workflows/display/workflows/base_workflow_display.py +5 -5
  23. vellum_ee/workflows/server/virtual_file_loader.py +36 -8
  24. vellum_ee/workflows/tests/test_server.py +81 -2
  25. vellum_ee/workflows/tests/test_virtual_files.py +3 -6
  26. vellum/workflows/triggers/vellum_integration.py +0 -189
  27. {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/LICENSE +0 -0
  28. {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/WHEEL +0 -0
  29. {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/entry_points.txt +0 -0
@@ -25,8 +25,8 @@ from vellum.workflows.nodes.utils import get_unadorned_node, get_unadorned_port,
25
25
  from vellum.workflows.ports import Port
26
26
  from vellum.workflows.references import OutputReference, WorkflowInputReference
27
27
  from vellum.workflows.state.encoder import DefaultStateEncoder
28
+ from vellum.workflows.triggers.integration import IntegrationTrigger
28
29
  from vellum.workflows.triggers.manual import ManualTrigger
29
- from vellum.workflows.triggers.vellum_integration import VellumIntegrationTrigger
30
30
  from vellum.workflows.types.core import Json, JsonArray, JsonObject
31
31
  from vellum.workflows.types.generics import WorkflowType
32
32
  from vellum.workflows.types.utils import get_original_base
@@ -190,7 +190,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
190
190
  # For IntegrationTrigger only: no ENTRYPOINT node (use trigger ID directly in edges)
191
191
  manual_trigger_edges = [edge for edge in trigger_edges if issubclass(edge.trigger_class, ManualTrigger)]
192
192
  integration_trigger_edges = [
193
- edge for edge in trigger_edges if issubclass(edge.trigger_class, VellumIntegrationTrigger)
193
+ edge for edge in trigger_edges if issubclass(edge.trigger_class, IntegrationTrigger)
194
194
  ]
195
195
 
196
196
  has_manual_trigger = len(manual_trigger_edges) > 0
@@ -536,12 +536,12 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
536
536
  f"{edge.trigger_class.__name__} in the same workflow."
537
537
  )
538
538
 
539
- # Get the trigger type from the mapping, or check if it's a VellumIntegrationTrigger subclass
539
+ # Get the trigger type from the mapping, or check if it's an IntegrationTrigger subclass
540
540
  trigger_type = trigger_type_mapping.get(trigger_class)
541
541
  if trigger_type is None:
542
- # Check if it's a VellumIntegrationTrigger subclass
542
+ # Check if it's an IntegrationTrigger subclass
543
543
 
544
- if issubclass(trigger_class, VellumIntegrationTrigger):
544
+ if issubclass(trigger_class, IntegrationTrigger):
545
545
  trigger_type = WorkflowTriggerType.INTEGRATION
546
546
  else:
547
547
  raise ValueError(
@@ -85,13 +85,11 @@ class VirtualFileLoader(importlib.abc.Loader):
85
85
 
86
86
  def _is_package_directory(self, fullname: str) -> bool:
87
87
  """Check if directory contains .py files that should be treated as a package."""
88
- directory_prefix = fullname.replace(".", "/") + "/"
89
-
90
- # Exclude top-level display directory from auto-generation as it typically has
91
- # specific __init__.py content that shouldn't be replaced with empty files.
92
- if directory_prefix == "display/":
93
- return False
88
+ if fullname == "":
89
+ # This is the root namespace, so it's a package directory
90
+ return True
94
91
 
92
+ directory_prefix = fullname.replace(".", "/") + "/"
95
93
  for file_path in self.files.keys():
96
94
  if file_path.startswith(directory_prefix):
97
95
  if file_path.endswith(".py") and not file_path.endswith("__init__.py"):
@@ -104,10 +102,40 @@ class VirtualFileLoader(importlib.abc.Loader):
104
102
 
105
103
  def _generate_init_content(self, fullname: str) -> tuple[str, str]:
106
104
  """Auto-generate empty __init__.py content to mark directory as a package."""
105
+ if fullname == "":
106
+ if any(file_path.startswith("display/") for file_path in self.files.keys()):
107
+ return (
108
+ "__init__.py",
109
+ """\
110
+ from .display import *
111
+ """,
112
+ )
113
+ else:
114
+ return "__init__.py", ""
115
+
107
116
  directory_prefix = fullname.replace(".", "/") + "/"
108
117
  file_path = directory_prefix + "__init__.py"
109
118
 
110
- code = ""
119
+ if not fullname.startswith("display"):
120
+ return "__init__.py", ""
121
+
122
+ # We have to go through the effort of exhaustively importing all display modules, because that's how
123
+ # dynamically annotate our Workflow graph.
124
+ all_nested_files = [
125
+ file_path[len(directory_prefix) :]
126
+ for file_path in self.files.keys()
127
+ if file_path.startswith(directory_prefix)
128
+ ]
129
+
130
+ top_level_items = {file_path.split("/")[0] for file_path in all_nested_files}
131
+
132
+ top_level_modules = [
133
+ re.sub(r"\.py$", "", file_path)
134
+ for file_path in top_level_items
135
+ if file_path.endswith(".py") or "." not in file_path
136
+ ]
137
+
138
+ code = "\n".join([f"from .{file_path} import *" for file_path in top_level_modules])
111
139
 
112
140
  return file_path, code
113
141
 
@@ -126,7 +154,7 @@ class VirtualFileFinder(BaseWorkflowFinder):
126
154
 
127
155
  def find_spec(self, fullname: str, path, target=None):
128
156
  module_info = self.loader._resolve_module(fullname)
129
- if module_info:
157
+ if isinstance(module_info, tuple) and len(module_info) == 2:
130
158
  file_path, _ = module_info
131
159
  is_package = file_path.endswith("__init__.py")
132
160
  return importlib.machinery.ModuleSpec(
@@ -1,17 +1,19 @@
1
1
  import pytest
2
2
  import sys
3
- from uuid import uuid4
3
+ from uuid import UUID, uuid4
4
4
  from typing import Type, cast
5
5
 
6
6
  from vellum.client.core.pydantic_utilities import UniversalBaseModel
7
7
  from vellum.client.types.code_executor_response import CodeExecutorResponse
8
8
  from vellum.client.types.number_vellum_value import NumberVellumValue
9
9
  from vellum.workflows import BaseWorkflow
10
+ from vellum.workflows.events.workflow import WorkflowExecutionInitiatedBody
10
11
  from vellum.workflows.exceptions import WorkflowInitializationException
11
12
  from vellum.workflows.nodes import BaseNode
12
13
  from vellum.workflows.state.context import WorkflowContext
13
14
  from vellum.workflows.utils.uuids import generate_workflow_deployment_prefix
14
15
  from vellum.workflows.utils.zip import zip_file_map
16
+ from vellum_ee.workflows.display.utils.events import event_enricher
15
17
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
16
18
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
17
19
 
@@ -27,7 +29,6 @@ def test_load_workflow_event_display_context():
27
29
  def test_load_from_module__lazy_reference_in_file_loader():
28
30
  # GIVEN a workflow module with a node containing a lazy reference
29
31
  files = {
30
- "__init__.py": "",
31
32
  "workflow.py": """\
32
33
  from vellum.workflows import BaseWorkflow
33
34
  from .nodes.start_node import StartNode
@@ -902,3 +903,81 @@ class TestSubworkflowDeploymentNode(SubworkflowDeploymentNode):
902
903
 
903
904
  # AND the X-Vellum-Always-Success header should be included for graceful error handling
904
905
  assert kwargs["request_options"]["additional_headers"]["X-Vellum-Always-Success"] == "true"
906
+
907
+
908
+ def test_workflow_initiated_event_includes_display_context_with_output_display_name():
909
+ """
910
+ Tests that workflow initiated events include display context with annotated node and output information.
911
+ """
912
+ # GIVEN a workflow with a single node that has a single output
913
+ files = {
914
+ "workflow.py": """\
915
+ from vellum.workflows import BaseWorkflow
916
+ from .nodes.start_node import StartNode
917
+
918
+ class Workflow(BaseWorkflow):
919
+ graph = StartNode
920
+
921
+ class Outputs(BaseWorkflow.Outputs):
922
+ final_result = StartNode.Outputs.result
923
+ """,
924
+ "nodes/start_node.py": """\
925
+ from vellum.workflows.nodes import BaseNode
926
+
927
+ class StartNode(BaseNode):
928
+ class Outputs(BaseNode.Outputs):
929
+ result: str = "test output"
930
+ """,
931
+ "display/nodes/__init__.py": """\
932
+ from .start_node import StartNodeDisplay
933
+ """,
934
+ "display/nodes/start_node.py": """\
935
+ from uuid import UUID
936
+ from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
937
+ from vellum_ee.workflows.display.nodes.types import NodeOutputDisplay
938
+ from ...nodes.start_node import StartNode
939
+
940
+ class StartNodeDisplay(BaseNodeDisplay[StartNode]):
941
+ node_id = UUID("11111111-1111-1111-1111-111111111111")
942
+ output_display = {
943
+ StartNode.Outputs.result: NodeOutputDisplay(
944
+ id=UUID("22222222-2222-2222-2222-222222222222"),
945
+ name="Pretty Result"
946
+ )
947
+ }
948
+ """,
949
+ "display/workflow.py": """\
950
+ """,
951
+ }
952
+
953
+ namespace = str(uuid4())
954
+ finder = VirtualFileFinder(files, namespace)
955
+ sys.meta_path.append(finder)
956
+
957
+ try:
958
+ # WHEN we stream the workflow and enrich the initiated event with display context
959
+ Workflow = BaseWorkflow.load_from_module(namespace)
960
+ workflow = Workflow()
961
+ initiated_event = list(workflow.stream())[0]
962
+ enriched_event = event_enricher(initiated_event)
963
+
964
+ # THEN the initiated event should have display context
965
+ assert isinstance(enriched_event.body, WorkflowExecutionInitiatedBody)
966
+ body = enriched_event.body
967
+ assert body.display_context is not None
968
+
969
+ # AND the display context should contain the annotated node
970
+ node_displays = body.display_context.node_displays
971
+ annotated_node_id = UUID("11111111-1111-1111-1111-111111111111")
972
+ assert annotated_node_id in node_displays
973
+
974
+ # AND the node's output display should contain the output with the display name "Pretty Result"
975
+ node_display = node_displays[annotated_node_id]
976
+ assert "result" in node_display.output_display
977
+
978
+ # AND the output should map to the annotated output id
979
+ annotated_output_id = UUID("22222222-2222-2222-2222-222222222222")
980
+ assert node_display.output_display["result"] == annotated_output_id
981
+ finally:
982
+ if finder in sys.meta_path:
983
+ sys.meta_path.remove(finder)
@@ -121,12 +121,9 @@ class WorkflowDisplay(BaseWorkflowDisplay):
121
121
 
122
122
  spec = importlib.util.find_spec(f"{namespace}.display")
123
123
 
124
- # THEN the spec should be None because we don't want to auto-generate display/__init__.py
125
- # If the spec exists, it means an empty __init__.py was auto-generated (BAD)
126
- assert spec is None, (
127
- "display directory should NOT have auto-generated __init__.py. "
128
- "Display directories require specific __init__.py content that shouldn't be empty."
129
- )
124
+ # THEN the spec should be found because we now support dynamic display module imports
125
+ assert spec
126
+ assert spec.origin == "display/__init__.py"
130
127
 
131
128
  finally:
132
129
  # Clean up
@@ -1,189 +0,0 @@
1
- from typing import Any, ClassVar, Dict
2
-
3
- from vellum.workflows.constants import VellumIntegrationProviderType
4
- from vellum.workflows.references.trigger import TriggerAttributeReference
5
- from vellum.workflows.triggers.base import BaseTriggerMeta
6
- from vellum.workflows.triggers.integration import IntegrationTrigger
7
-
8
-
9
- class VellumIntegrationTriggerMeta(BaseTriggerMeta):
10
- """
11
- Custom metaclass for VellumIntegrationTrigger.
12
-
13
- This metaclass extends BaseTriggerMeta to automatically convert type annotations
14
- into TriggerAttributeReference objects during class creation. This enables trigger
15
- attributes to be referenced in workflow graphs while maintaining type safety.
16
- """
17
-
18
- def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) -> "VellumIntegrationTriggerMeta":
19
- """Create a new trigger class and set up attribute references."""
20
- cls = super().__new__(mcs, name, bases, namespace, **kwargs)
21
-
22
- # Process __annotations__ to create TriggerAttributeReference for each attribute
23
- # Only process if class has Config and annotations
24
- has_config = hasattr(cls, "Config") and "Config" in namespace
25
- if has_config and hasattr(cls, "__annotations__"):
26
- # Create TriggerAttributeReference for each annotated attribute
27
- for attr_name, attr_type in cls.__annotations__.items():
28
- # Skip special attributes and Config
29
- if attr_name.startswith("_") or attr_name == "Config":
30
- continue
31
-
32
- # Create reference with proper type
33
- reference = TriggerAttributeReference(
34
- name=attr_name, types=(attr_type,), instance=None, trigger_class=cls
35
- )
36
- # Set as class attribute so it's directly accessible
37
- setattr(cls, attr_name, reference)
38
-
39
- return cls
40
-
41
-
42
- class VellumIntegrationTrigger(IntegrationTrigger, metaclass=VellumIntegrationTriggerMeta):
43
- """
44
- Base class for Vellum-managed integration triggers.
45
-
46
- Subclasses define two types of attributes:
47
- 1. **Config class**: Specifies how the trigger is configured (provider, integration_name, slug)
48
- - These are configuration details users shouldn't need to interact with directly
49
- 2. **Top-level type annotations**: Define the webhook event payload structure (message, user, channel, etc.)
50
- - These become TriggerAttributeReference that can be referenced in workflow nodes
51
-
52
- Examples:
53
- Create a Slack trigger:
54
- >>> class SlackNewMessageTrigger(VellumIntegrationTrigger):
55
- ... # Event attributes (webhook payload structure)
56
- ... message: str
57
- ... user: str
58
- ... channel: str
59
- ... timestamp: float
60
- ...
61
- ... # Configuration (how trigger is set up)
62
- ... class Config(VellumIntegrationTrigger.Config):
63
- ... provider = VellumIntegrationProviderType.COMPOSIO
64
- ... integration_name = "SLACK"
65
- ... slug = "slack_new_message"
66
-
67
- Use in workflow graph:
68
- >>> class MyWorkflow(BaseWorkflow):
69
- ... graph = SlackNewMessageTrigger >> ProcessMessageNode
70
-
71
- Reference trigger attributes in nodes:
72
- >>> class ProcessNode(BaseNode):
73
- ... class Outputs(BaseNode.Outputs):
74
- ... text = SlackNewMessageTrigger.message
75
- ... channel = SlackNewMessageTrigger.channel
76
-
77
- Instantiate for testing:
78
- >>> trigger = SlackNewMessageTrigger(event_data={
79
- ... "message": "Hello world",
80
- ... "channel": "C123456",
81
- ... "user": "U123",
82
- ... "timestamp": 1234567890.0,
83
- ... })
84
- >>> trigger.message
85
- 'Hello world'
86
- """
87
-
88
- class Config:
89
- """
90
- Configuration for VellumIntegrationTrigger subclasses.
91
-
92
- Defines how the trigger connects to the integration provider. These settings
93
- specify which integration and which specific trigger type to use.
94
- """
95
-
96
- provider: ClassVar[VellumIntegrationProviderType]
97
- integration_name: ClassVar[str]
98
- slug: ClassVar[str]
99
-
100
- def __init_subclass__(cls, **kwargs: Any) -> None:
101
- """Validate that subclasses define required Config class with all required fields."""
102
- super().__init_subclass__(**kwargs)
103
-
104
- # Skip validation for the base class itself
105
- if cls.__name__ == "VellumIntegrationTrigger":
106
- return
107
-
108
- # Require Config class with required fields
109
- if not hasattr(cls, "Config") or cls.Config is VellumIntegrationTrigger.Config:
110
- raise TypeError(
111
- f"{cls.__name__} must define a nested Config class. "
112
- f"Example:\n"
113
- f" class {cls.__name__}(VellumIntegrationTrigger):\n"
114
- f" message: str\n"
115
- f" class Config(VellumIntegrationTrigger.Config):\n"
116
- f" provider = VellumIntegrationProviderType.COMPOSIO\n"
117
- f" integration_name = 'SLACK'\n"
118
- f" slug = 'slack_new_message'"
119
- )
120
-
121
- # Validate Config class has required fields
122
- config_cls = cls.Config
123
- required_fields = ["provider", "integration_name", "slug"]
124
- for field in required_fields:
125
- if not hasattr(config_cls, field):
126
- raise TypeError(
127
- f"{cls.__name__}.Config must define '{field}'. " f"Required fields: {', '.join(required_fields)}"
128
- )
129
-
130
- def __init__(self, event_data: dict):
131
- """
132
- Initialize trigger with event data from the integration.
133
-
134
- The trigger dynamically populates its attributes based on the event_data
135
- dictionary keys. Any key in event_data becomes an accessible attribute.
136
-
137
- Args:
138
- event_data: Raw event data from the integration. Keys become trigger attributes.
139
-
140
- Examples:
141
- >>> class SlackTrigger(VellumIntegrationTrigger):
142
- ... message: str
143
- ... channel: str
144
- ... user: str
145
- ...
146
- ... class Config(VellumIntegrationTrigger.Config):
147
- ... provider = VellumIntegrationProviderType.COMPOSIO
148
- ... integration_name = "SLACK"
149
- ... slug = "slack_new_message"
150
- >>> trigger = SlackTrigger(event_data={
151
- ... "message": "Hello",
152
- ... "channel": "C123",
153
- ... "user": "U456"
154
- ... })
155
- >>> trigger.message
156
- 'Hello'
157
- >>> trigger.channel
158
- 'C123'
159
- """
160
- super().__init__(event_data)
161
-
162
- # Dynamically populate instance attributes from event_data.
163
- # This allows any key in event_data to become an accessible attribute:
164
- # event_data={"message": "Hi"} → trigger.message == "Hi"
165
- for key, value in event_data.items():
166
- setattr(self, key, value)
167
-
168
- def to_trigger_attribute_values(self) -> Dict["TriggerAttributeReference[Any]", Any]:
169
- """
170
- Materialize attribute descriptor/value pairs for this trigger instance.
171
-
172
- For VellumIntegrationTrigger, this includes all dynamic attributes from event_data.
173
- """
174
- attribute_values: Dict["TriggerAttributeReference[Any]", Any] = {}
175
-
176
- # Unlike the base class which iterates over type(self) (predefined annotations),
177
- # we iterate over event_data keys since our attributes are discovered dynamically
178
- # from the actual event data received during workflow execution.
179
- # The base class approach: for reference in type(self)
180
- # Our approach: for attr_name in self._event_data.keys()
181
- for attr_name in self._event_data.keys():
182
- # Get the class-level reference for this attribute (created by __new__ from annotations)
183
- # Unknown keys can appear in webhook payloads, so gracefully skip them if the
184
- # trigger class doesn't expose a corresponding reference.
185
- reference = getattr(type(self), attr_name, None)
186
- if isinstance(reference, TriggerAttributeReference):
187
- attribute_values[reference] = getattr(self, attr_name)
188
-
189
- return attribute_values