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.
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/types/integration_name.py +1 -0
- vellum/workflows/inputs/dataset_row.py +9 -7
- vellum/workflows/nodes/displayable/final_output_node/node.py +4 -0
- vellum/workflows/nodes/displayable/final_output_node/tests/test_node.py +28 -0
- vellum/workflows/nodes/displayable/set_state_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/set_state_node/node.py +71 -0
- vellum/workflows/nodes/displayable/set_state_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/set_state_node/tests/test_node.py +212 -0
- vellum/workflows/sandbox.py +13 -3
- vellum/workflows/tests/test_dataset_row.py +20 -0
- vellum/workflows/tests/test_sandbox.py +40 -0
- vellum/workflows/tests/triggers/{test_vellum_integration_trigger.py → test_integration_trigger.py} +22 -22
- vellum/workflows/triggers/__init__.py +1 -2
- vellum/workflows/triggers/base.py +10 -0
- vellum/workflows/triggers/integration.py +168 -49
- vellum/workflows/triggers/tests/test_integration.py +49 -20
- vellum/workflows/workflows/base.py +44 -0
- {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/METADATA +1 -1
- {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/RECORD +28 -25
- vellum_ee/workflows/display/tests/workflow_serialization/{test_vellum_integration_trigger_serialization.py → test_integration_trigger_serialization.py} +8 -8
- vellum_ee/workflows/display/workflows/base_workflow_display.py +5 -5
- vellum_ee/workflows/server/virtual_file_loader.py +36 -8
- vellum_ee/workflows/tests/test_server.py +81 -2
- vellum_ee/workflows/tests/test_virtual_files.py +3 -6
- vellum/workflows/triggers/vellum_integration.py +0 -189
- {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/LICENSE +0 -0
- {vellum_ai-1.8.6.dist-info → vellum_ai-1.9.0.dist-info}/WHEEL +0 -0
- {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,
|
|
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
|
|
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
|
|
542
|
+
# Check if it's an IntegrationTrigger subclass
|
|
543
543
|
|
|
544
|
-
if issubclass(trigger_class,
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
assert spec
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|