vellum-ai 1.7.6__py3-none-any.whl → 1.7.8__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 (34) hide show
  1. vellum/client/core/client_wrapper.py +2 -2
  2. vellum/client/reference.md +16 -0
  3. vellum/client/resources/ad_hoc/raw_client.py +2 -2
  4. vellum/client/resources/integration_providers/client.py +20 -0
  5. vellum/client/resources/integration_providers/raw_client.py +20 -0
  6. vellum/client/types/integration_name.py +1 -0
  7. vellum/client/types/workflow_execution_fulfilled_body.py +1 -0
  8. vellum/workflows/nodes/bases/base_adornment_node.py +53 -1
  9. vellum/workflows/nodes/core/map_node/node.py +10 -0
  10. vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +49 -1
  11. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +3 -1
  12. vellum/workflows/nodes/tests/test_utils.py +7 -1
  13. vellum/workflows/nodes/utils.py +1 -1
  14. vellum/workflows/references/__init__.py +2 -0
  15. vellum/workflows/references/trigger.py +83 -0
  16. vellum/workflows/runner/runner.py +17 -15
  17. vellum/workflows/state/base.py +49 -1
  18. vellum/workflows/triggers/__init__.py +2 -1
  19. vellum/workflows/triggers/base.py +140 -3
  20. vellum/workflows/triggers/integration.py +31 -26
  21. vellum/workflows/triggers/slack.py +101 -0
  22. vellum/workflows/triggers/tests/test_integration.py +55 -31
  23. vellum/workflows/triggers/tests/test_slack.py +180 -0
  24. {vellum_ai-1.7.6.dist-info → vellum_ai-1.7.8.dist-info}/METADATA +1 -1
  25. {vellum_ai-1.7.6.dist-info → vellum_ai-1.7.8.dist-info}/RECORD +34 -30
  26. vellum_ee/workflows/display/base.py +3 -0
  27. vellum_ee/workflows/display/nodes/base_node_display.py +1 -1
  28. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +16 -0
  29. vellum_ee/workflows/display/tests/workflow_serialization/test_slack_trigger_serialization.py +167 -0
  30. vellum_ee/workflows/display/utils/expressions.py +11 -11
  31. vellum_ee/workflows/display/workflows/base_workflow_display.py +22 -6
  32. {vellum_ai-1.7.6.dist-info → vellum_ai-1.7.8.dist-info}/LICENSE +0 -0
  33. {vellum_ai-1.7.6.dist-info → vellum_ai-1.7.8.dist-info}/WHEEL +0 -0
  34. {vellum_ai-1.7.6.dist-info → vellum_ai-1.7.8.dist-info}/entry_points.txt +0 -0
@@ -1,11 +1,58 @@
1
1
  from abc import ABC, ABCMeta
2
- from typing import TYPE_CHECKING, Any, Type, cast
2
+ import inspect
3
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Tuple, Type, cast, get_origin
3
4
 
4
5
  if TYPE_CHECKING:
5
6
  from vellum.workflows.graph.graph import Graph, GraphTarget
7
+ from vellum.workflows.state.base import BaseState
8
+
9
+ from vellum.workflows.references.trigger import TriggerAttributeReference
10
+ from vellum.workflows.types.utils import get_class_attr_names, infer_types
11
+ from vellum.workflows.utils.uuids import uuid4_from_hash
12
+
13
+
14
+ def _is_annotated(cls: Type, name: str) -> bool:
15
+ annotations = getattr(cls, "__annotations__", {})
16
+ annotation = annotations.get(name)
17
+ if annotation is not None:
18
+ if get_origin(annotation) is ClassVar:
19
+ return False
20
+ return True
21
+
22
+ for base in cls.__bases__:
23
+ if _is_annotated(base, name):
24
+ return True
25
+
26
+ return False
6
27
 
7
28
 
8
29
  class BaseTriggerMeta(ABCMeta):
30
+ def __new__(mcs, name: str, bases: Tuple[Type, ...], dct: Dict[str, Any]) -> Any:
31
+ cls = super().__new__(mcs, name, bases, dct)
32
+ trigger_cls = cast(Type["BaseTrigger"], cls)
33
+
34
+ attribute_ids: Dict[str, Any] = {}
35
+ for base in bases:
36
+ base_ids = getattr(base, "__trigger_attribute_ids__", None)
37
+ if isinstance(base_ids, dict):
38
+ attribute_ids.update(base_ids)
39
+
40
+ annotations = getattr(trigger_cls, "__annotations__", {})
41
+ for attr_name, annotation in annotations.items():
42
+ if attr_name.startswith("_"):
43
+ continue
44
+ if attr_name in trigger_cls.__dict__:
45
+ continue
46
+ if get_origin(annotation) is ClassVar:
47
+ continue
48
+ attribute_ids[attr_name] = uuid4_from_hash(
49
+ f"{trigger_cls.__module__}|{trigger_cls.__qualname__}|{attr_name}"
50
+ )
51
+
52
+ trigger_cls.__trigger_attribute_ids__ = attribute_ids
53
+ trigger_cls.__trigger_attribute_cache__ = {}
54
+ return trigger_cls
55
+
9
56
  """
10
57
  Metaclass for BaseTrigger that enables class-level >> operator.
11
58
 
@@ -13,6 +60,68 @@ class BaseTriggerMeta(ABCMeta):
13
60
  ManualTrigger >> MyNode # Class-level, no instantiation
14
61
  """
15
62
 
63
+ def __getattribute__(cls, name: str) -> Any:
64
+ trigger_cls = cast(Type["BaseTrigger"], cls)
65
+ if name.startswith("_"):
66
+ return super().__getattribute__(name)
67
+
68
+ try:
69
+ attribute = super().__getattribute__(name)
70
+ except AttributeError as exc:
71
+ if _is_annotated(cls, name):
72
+ attribute = None
73
+ else:
74
+ raise exc
75
+
76
+ if inspect.isroutine(attribute) or isinstance(attribute, (property, classmethod, staticmethod)):
77
+ return attribute
78
+
79
+ if isinstance(attribute, TriggerAttributeReference):
80
+ return attribute
81
+
82
+ if name in cls.__dict__:
83
+ return attribute
84
+
85
+ if not _is_annotated(cls, name):
86
+ return attribute
87
+
88
+ cache = getattr(cls, "__trigger_attribute_cache__", {})
89
+ if name in cache:
90
+ return cache[name]
91
+
92
+ for base in cls.__mro__[1:]:
93
+ base_cache = getattr(base, "__trigger_attribute_cache__", None)
94
+ if base_cache and name in base_cache:
95
+ cache[name] = base_cache[name]
96
+ return base_cache[name]
97
+
98
+ types = infer_types(cls, name)
99
+ reference = TriggerAttributeReference(name=name, types=types, instance=attribute, trigger_class=trigger_cls)
100
+ cache[name] = reference
101
+ cls.__trigger_attribute_cache__ = cache
102
+ return reference
103
+
104
+ def __iter__(cls) -> Iterator[TriggerAttributeReference]:
105
+ cache = getattr(cls, "__trigger_attribute_cache__", {})
106
+ seen: Dict[str, TriggerAttributeReference] = dict(cache)
107
+
108
+ for base in cls.__mro__[1:]:
109
+ base_cache = getattr(base, "__trigger_attribute_cache__", None)
110
+ if not base_cache:
111
+ continue
112
+ for key, value in base_cache.items():
113
+ seen.setdefault(key, value)
114
+
115
+ for attr_name in get_class_attr_names(cls):
116
+ if attr_name in seen:
117
+ yield seen[attr_name]
118
+ continue
119
+
120
+ attr_value = getattr(cls, attr_name)
121
+ if isinstance(attr_value, TriggerAttributeReference):
122
+ seen[attr_name] = attr_value
123
+ yield attr_value
124
+
16
125
  def __rshift__(cls, other: "GraphTarget") -> "Graph": # type: ignore[misc]
17
126
  """
18
127
  Enable Trigger class >> Node syntax (class-level only).
@@ -33,7 +142,7 @@ class BaseTriggerMeta(ABCMeta):
33
142
  from vellum.workflows.nodes.bases.base import BaseNode as BaseNodeClass
34
143
 
35
144
  # Cast cls to the proper type for TriggerEdge
36
- trigger_cls = cast("Type[BaseTrigger]", cls)
145
+ trigger_cls = cast(Type["BaseTrigger"], cls)
37
146
 
38
147
  if isinstance(other, set):
39
148
  # Trigger >> {NodeA, NodeB}
@@ -122,4 +231,32 @@ class BaseTrigger(ABC, metaclass=BaseTriggerMeta):
122
231
  Like nodes, triggers work at the class level only. Do not instantiate triggers.
123
232
  """
124
233
 
125
- pass
234
+ __trigger_attribute_ids__: Dict[str, Any]
235
+ __trigger_attribute_cache__: Dict[str, "TriggerAttributeReference[Any]"]
236
+
237
+ @classmethod
238
+ def attribute_references(cls) -> Dict[str, "TriggerAttributeReference[Any]"]:
239
+ """Return class-level trigger attribute descriptors keyed by attribute name."""
240
+
241
+ return {reference.name: reference for reference in cls}
242
+
243
+ def to_trigger_attribute_values(self) -> Dict["TriggerAttributeReference[Any]", Any]:
244
+ """Materialize attribute descriptor/value pairs for this trigger instance."""
245
+
246
+ attribute_values: Dict["TriggerAttributeReference[Any]", Any] = {}
247
+ for reference in type(self):
248
+ if hasattr(self, reference.name):
249
+ attribute_values[reference] = getattr(self, reference.name)
250
+ elif type(None) in reference.types:
251
+ attribute_values[reference] = None
252
+ else:
253
+ raise AttributeError(
254
+ f"{type(self).__name__} did not populate required trigger attribute '{reference.name}'"
255
+ )
256
+
257
+ return attribute_values
258
+
259
+ def bind_to_state(self, state: "BaseState") -> None:
260
+ """Persist this trigger's attribute values onto the provided state."""
261
+
262
+ state.meta.trigger_attributes.update(self.to_trigger_attribute_values())
@@ -1,7 +1,6 @@
1
1
  from abc import ABC
2
2
  from typing import ClassVar, Optional
3
3
 
4
- from vellum.workflows.outputs.base import BaseOutputs
5
4
  from vellum.workflows.triggers.base import BaseTrigger
6
5
 
7
6
 
@@ -11,52 +10,58 @@ class IntegrationTrigger(BaseTrigger, ABC):
11
10
 
12
11
  Integration triggers:
13
12
  - Are initiated by external events (webhooks, API calls)
14
- - Produce outputs that downstream nodes can reference
13
+ - Produce attributes that downstream nodes can reference
15
14
  - Require configuration (auth, webhooks, etc.)
16
15
 
17
16
  Examples:
18
17
  # Define an integration trigger
19
18
  class MyIntegrationTrigger(IntegrationTrigger):
20
- class Outputs(IntegrationTrigger.Outputs):
21
- data: str
19
+ data: str
22
20
 
23
- @classmethod
24
- def process_event(cls, event_data: dict):
25
- return cls.Outputs(data=event_data.get("data", ""))
21
+ def __init__(self, event_data: dict):
22
+ super().__init__(event_data)
23
+ self.data = event_data.get("data", "")
26
24
 
27
25
  # Use in workflow
28
26
  class MyWorkflow(BaseWorkflow):
29
27
  graph = MyIntegrationTrigger >> ProcessNode
30
28
 
29
+ # Reference trigger attributes in nodes
30
+ class ProcessNode(BaseNode):
31
+ class Outputs(BaseNode.Outputs):
32
+ result = MyIntegrationTrigger.data
33
+
31
34
  Note:
32
- Unlike ManualTrigger, integration triggers provide structured outputs
33
- that downstream nodes can reference directly via Outputs.
35
+ Unlike ManualTrigger, integration triggers provide structured attributes
36
+ that downstream nodes can reference directly.
34
37
  """
35
38
 
36
- class Outputs(BaseOutputs):
37
- """Base outputs for integration triggers."""
38
-
39
- pass
40
-
41
39
  # Configuration that can be set at runtime
42
40
  config: ClassVar[Optional[dict]] = None
43
41
 
44
- @classmethod
45
- def process_event(cls, event_data: dict) -> "IntegrationTrigger.Outputs":
42
+ def __init__(self, event_data: dict):
46
43
  """
47
- Process incoming webhook/event data and return trigger outputs.
44
+ Initialize trigger with event data from external system.
48
45
 
49
- This method should be implemented by subclasses to parse external
50
- event payloads (e.g., Slack webhooks, email notifications) into
51
- structured trigger outputs.
46
+ Subclasses should override this method to parse external
47
+ event payloads (e.g., Slack webhooks, email notifications) and
48
+ populate trigger attributes.
52
49
 
53
50
  Args:
54
51
  event_data: Raw event data from the external system
55
52
 
56
- Returns:
57
- Trigger outputs containing parsed event data
58
-
59
- Raises:
60
- NotImplementedError: If subclass doesn't implement this method
53
+ Examples:
54
+ >>> class MyTrigger(IntegrationTrigger):
55
+ ... data: str
56
+ ...
57
+ ... def __init__(self, event_data: dict):
58
+ ... super().__init__(event_data)
59
+ ... self.data = event_data.get("data", "")
60
+ >>>
61
+ >>> trigger = MyTrigger({"data": "hello"})
62
+ >>> state = workflow.get_default_state()
63
+ >>> trigger.bind_to_state(state)
64
+ >>> MyTrigger.data.resolve(state)
65
+ 'hello'
61
66
  """
62
- raise NotImplementedError(f"{cls.__name__} must implement process_event() method to handle external events")
67
+ self._event_data = event_data
@@ -0,0 +1,101 @@
1
+ from typing import Optional
2
+
3
+ from vellum.workflows.triggers.integration import IntegrationTrigger
4
+
5
+
6
+ class SlackTrigger(IntegrationTrigger):
7
+ """
8
+ Trigger for Slack events (messages, mentions, reactions, etc.).
9
+
10
+ SlackTrigger enables workflows to be initiated from Slack events such as
11
+ messages in channels, direct messages, app mentions, or reactions. The trigger
12
+ parses Slack webhook payloads and provides structured attributes that downstream
13
+ nodes can reference.
14
+
15
+ Examples:
16
+ Basic Slack message trigger:
17
+ >>> class MyWorkflow(BaseWorkflow):
18
+ ... graph = SlackTrigger >> ProcessMessageNode
19
+
20
+ Access trigger attributes in nodes:
21
+ >>> class ProcessMessageNode(BaseNode):
22
+ ... class Outputs(BaseNode.Outputs):
23
+ ... response = SlackTrigger.message
24
+
25
+ Multiple entry points:
26
+ >>> class MyWorkflow(BaseWorkflow):
27
+ ... graph = {
28
+ ... SlackTrigger >> ProcessSlackNode,
29
+ ... ManualTrigger >> ProcessManualNode,
30
+ ... }
31
+
32
+ Attributes:
33
+ message: str
34
+ The message text from Slack
35
+ channel: str
36
+ Slack channel ID where message was sent
37
+ user: str
38
+ User ID who sent the message
39
+ timestamp: str
40
+ Message timestamp (ts field from Slack)
41
+ thread_ts: Optional[str]
42
+ Thread timestamp if message is in a thread, None otherwise
43
+ event_type: str
44
+ Type of Slack event (e.g., "message", "app_mention")
45
+
46
+ Note:
47
+ The trigger expects Slack Event API webhook payloads. For details on
48
+ the payload structure, see: https://api.slack.com/apis/connections/events-api
49
+ """
50
+
51
+ message: str
52
+ channel: str
53
+ user: str
54
+ timestamp: str
55
+ thread_ts: Optional[str]
56
+ event_type: str
57
+
58
+ def __init__(self, event_data: dict):
59
+ """
60
+ Initialize SlackTrigger with Slack webhook payload.
61
+
62
+ Args:
63
+ event_data: Slack event payload from webhook. Expected structure:
64
+ {
65
+ "event": {
66
+ "type": "message",
67
+ "text": "Hello world",
68
+ "channel": "C123456",
69
+ "user": "U123456",
70
+ "ts": "1234567890.123456",
71
+ "thread_ts": "1234567890.123456" # optional
72
+ }
73
+ }
74
+
75
+ Examples:
76
+ >>> slack_payload = {
77
+ ... "event": {
78
+ ... "type": "message",
79
+ ... "text": "Hello!",
80
+ ... "channel": "C123",
81
+ ... "user": "U456",
82
+ ... "ts": "1234567890.123456"
83
+ ... }
84
+ ... }
85
+ >>> trigger = SlackTrigger(slack_payload)
86
+ >>> trigger.message
87
+ 'Hello!'
88
+ """
89
+ # Call parent init
90
+ super().__init__(event_data)
91
+
92
+ # Extract from Slack's event structure
93
+ event = event_data.get("event", {})
94
+
95
+ # Populate trigger attributes
96
+ self.message = event.get("text", "")
97
+ self.channel = event.get("channel", "")
98
+ self.user = event.get("user", "")
99
+ self.timestamp = event.get("ts", "")
100
+ self.thread_ts = event.get("thread_ts")
101
+ self.event_type = event.get("type", "message")
@@ -1,24 +1,29 @@
1
1
  """Tests for IntegrationTrigger base class."""
2
2
 
3
- import pytest
4
-
5
3
  from vellum.workflows.nodes.bases.base import BaseNode
4
+ from vellum.workflows.references.trigger import TriggerAttributeReference
5
+ from vellum.workflows.state.base import BaseState
6
6
  from vellum.workflows.triggers.integration import IntegrationTrigger
7
7
 
8
8
 
9
- def test_integration_trigger__is_abstract():
10
- """IntegrationTrigger cannot be instantiated directly (ABC)."""
11
- # WHEN we try to call process_event on IntegrationTrigger directly
12
- # THEN it raises NotImplementedError
13
- with pytest.raises(NotImplementedError, match="must implement process_event"):
14
- IntegrationTrigger.process_event({})
9
+ def test_integration_trigger__can_be_instantiated_as_base():
10
+ """IntegrationTrigger can be instantiated as a base class."""
11
+ # WHEN we instantiate IntegrationTrigger directly
12
+ trigger = IntegrationTrigger({"test": "data"})
15
13
 
14
+ # THEN it creates an instance with event data stored
15
+ assert trigger._event_data == {"test": "data"}
16
16
 
17
- def test_integration_trigger__outputs_class_exists():
18
- """IntegrationTrigger has Outputs class."""
19
- # GIVEN IntegrationTrigger
20
- # THEN it has an Outputs class
21
- assert hasattr(IntegrationTrigger, "Outputs")
17
+
18
+ def test_integration_trigger__can_be_instantiated():
19
+ """IntegrationTrigger can be instantiated for testing."""
20
+
21
+ # GIVEN IntegrationTrigger with concrete implementation
22
+ class TestTrigger(IntegrationTrigger):
23
+ pass
24
+
25
+ # THEN it can be instantiated (even though base is ABC, concrete subclasses work)
26
+ assert TestTrigger is not None
22
27
 
23
28
 
24
29
  def test_integration_trigger__can_be_subclassed():
@@ -26,31 +31,51 @@ def test_integration_trigger__can_be_subclassed():
26
31
 
27
32
  # GIVEN a concrete implementation of IntegrationTrigger
28
33
  class TestTrigger(IntegrationTrigger):
29
- class Outputs(IntegrationTrigger.Outputs):
30
- data: str
34
+ data: str
31
35
 
32
- @classmethod
33
- def process_event(cls, event_data: dict):
34
- return cls.Outputs(data=event_data.get("data", ""))
36
+ def __init__(self, event_data: dict):
37
+ super().__init__(event_data)
38
+ self.data = event_data.get("data", "")
35
39
 
36
- # WHEN we process an event
37
- result = TestTrigger.process_event({"data": "test"})
40
+ # WHEN we create a trigger instance
41
+ result = TestTrigger({"data": "test"})
38
42
 
39
- # THEN it returns the expected outputs
43
+ # THEN it returns the expected trigger instance with populated attributes
40
44
  assert result.data == "test"
41
45
 
42
46
 
47
+ def test_integration_trigger__attribute_reference():
48
+ """Trigger annotations expose TriggerAttributeReference descriptors."""
49
+
50
+ class TestTrigger(IntegrationTrigger):
51
+ value: str
52
+
53
+ def __init__(self, event_data: dict):
54
+ super().__init__(event_data)
55
+ self.value = event_data.get("value", "")
56
+
57
+ reference = TestTrigger.value
58
+ assert isinstance(reference, TriggerAttributeReference)
59
+ assert reference.name == "value"
60
+ assert TestTrigger.value is reference
61
+ assert reference is TestTrigger.attribute_references()["value"]
62
+
63
+ state = BaseState()
64
+ trigger = TestTrigger({"value": "data"})
65
+ trigger.bind_to_state(state)
66
+ assert reference.resolve(state) == "data"
67
+
68
+
43
69
  def test_integration_trigger__graph_syntax():
44
70
  """IntegrationTrigger can be used in graph syntax."""
45
71
 
46
72
  # GIVEN a concrete trigger and a node
47
73
  class TestTrigger(IntegrationTrigger):
48
- class Outputs(IntegrationTrigger.Outputs):
49
- value: str
74
+ value: str
50
75
 
51
- @classmethod
52
- def process_event(cls, event_data: dict):
53
- return cls.Outputs(value=event_data.get("value", ""))
76
+ def __init__(self, event_data: dict):
77
+ super().__init__(event_data)
78
+ self.value = event_data.get("value", "")
54
79
 
55
80
  class TestNode(BaseNode):
56
81
  pass
@@ -70,12 +95,11 @@ def test_integration_trigger__multiple_entrypoints():
70
95
 
71
96
  # GIVEN a trigger and multiple nodes
72
97
  class TestTrigger(IntegrationTrigger):
73
- class Outputs(IntegrationTrigger.Outputs):
74
- msg: str
98
+ msg: str
75
99
 
76
- @classmethod
77
- def process_event(cls, event_data: dict):
78
- return cls.Outputs(msg=event_data.get("msg", ""))
100
+ def __init__(self, event_data: dict):
101
+ super().__init__(event_data)
102
+ self.msg = event_data.get("msg", "")
79
103
 
80
104
  class NodeA(BaseNode):
81
105
  pass
@@ -0,0 +1,180 @@
1
+ """Tests for SlackTrigger."""
2
+
3
+ from typing import cast
4
+
5
+ from vellum.workflows.nodes.bases.base import BaseNode
6
+ from vellum.workflows.references.trigger import TriggerAttributeReference
7
+ from vellum.workflows.state.base import BaseState
8
+ from vellum.workflows.triggers.slack import SlackTrigger
9
+
10
+
11
+ def test_slack_trigger__basic():
12
+ """SlackTrigger parses Slack payload correctly."""
13
+ # GIVEN a Slack event payload
14
+ slack_payload = {
15
+ "event": {
16
+ "type": "message",
17
+ "text": "Hello world!",
18
+ "channel": "C123456",
19
+ "user": "U123456",
20
+ "ts": "1234567890.123456",
21
+ }
22
+ }
23
+
24
+ # WHEN we create a trigger instance
25
+ trigger = SlackTrigger(slack_payload)
26
+
27
+ # THEN trigger attributes contain the correct data
28
+ assert trigger.message == "Hello world!"
29
+ assert trigger.channel == "C123456"
30
+ assert trigger.user == "U123456"
31
+ assert trigger.timestamp == "1234567890.123456"
32
+ assert trigger.thread_ts is None
33
+ assert trigger.event_type == "message"
34
+
35
+ # AND the trigger can bind values to workflow state
36
+ state = BaseState()
37
+ trigger.bind_to_state(state)
38
+ stored = state.meta.trigger_attributes
39
+ message_ref = cast(TriggerAttributeReference[str], SlackTrigger.message)
40
+ assert message_ref in stored
41
+ assert stored[message_ref] == "Hello world!"
42
+
43
+
44
+ def test_slack_trigger__with_thread():
45
+ """SlackTrigger handles threaded messages."""
46
+ # GIVEN a Slack event payload with thread_ts
47
+ slack_payload = {
48
+ "event": {
49
+ "type": "message",
50
+ "text": "Reply in thread",
51
+ "channel": "C123456",
52
+ "user": "U123456",
53
+ "ts": "1234567891.123456",
54
+ "thread_ts": "1234567890.123456",
55
+ }
56
+ }
57
+
58
+ # WHEN we create a trigger instance
59
+ trigger = SlackTrigger(slack_payload)
60
+
61
+ # THEN thread_ts is populated
62
+ assert trigger.thread_ts == "1234567890.123456"
63
+
64
+
65
+ def test_slack_trigger__empty_payload():
66
+ """SlackTrigger handles empty payload gracefully."""
67
+ # GIVEN an empty payload
68
+ slack_payload: dict[str, str] = {}
69
+
70
+ # WHEN we create a trigger instance
71
+ trigger = SlackTrigger(slack_payload)
72
+
73
+ # THEN it returns empty strings for required fields
74
+ assert trigger.message == ""
75
+ assert trigger.channel == ""
76
+ assert trigger.user == ""
77
+ assert trigger.timestamp == ""
78
+ assert trigger.thread_ts is None
79
+ assert trigger.event_type == "message" # default value
80
+
81
+
82
+ def test_slack_trigger__app_mention():
83
+ """SlackTrigger handles app_mention events."""
84
+ # GIVEN an app_mention event
85
+ slack_payload = {
86
+ "event": {
87
+ "type": "app_mention",
88
+ "text": "<@U0LAN0Z89> is it everything a river should be?",
89
+ "channel": "C123456",
90
+ "user": "U123456",
91
+ "ts": "1234567890.123456",
92
+ }
93
+ }
94
+
95
+ # WHEN we create a trigger instance
96
+ trigger = SlackTrigger(slack_payload)
97
+
98
+ # THEN event_type is app_mention
99
+ assert trigger.event_type == "app_mention"
100
+ assert trigger.message == "<@U0LAN0Z89> is it everything a river should be?"
101
+
102
+
103
+ def test_slack_trigger__attributes():
104
+ """SlackTrigger has correct attributes."""
105
+ # GIVEN SlackTrigger class
106
+ # THEN it exposes attribute references for annotated fields
107
+ reference = SlackTrigger.message
108
+ assert isinstance(reference, TriggerAttributeReference)
109
+ assert reference.name == "message"
110
+ assert SlackTrigger.message is reference # cache returns same reference
111
+
112
+ annotations = SlackTrigger.__annotations__
113
+ assert set(annotations) >= {"message", "channel", "user", "timestamp", "thread_ts", "event_type"}
114
+
115
+ # AND references resolve when present on state
116
+ state = BaseState()
117
+ state.meta.trigger_attributes[reference] = "Hello"
118
+ assert reference.resolve(state) == "Hello"
119
+
120
+
121
+ def test_slack_trigger__graph_syntax():
122
+ """SlackTrigger can be used in graph syntax."""
123
+
124
+ # GIVEN a node
125
+ class TestNode(BaseNode):
126
+ pass
127
+
128
+ # WHEN we use SlackTrigger >> Node syntax
129
+ graph = SlackTrigger >> TestNode
130
+
131
+ # THEN a graph is created with trigger edge
132
+ assert graph is not None
133
+ trigger_edges = list(graph.trigger_edges)
134
+ assert len(trigger_edges) == 1
135
+ assert trigger_edges[0].trigger_class == SlackTrigger
136
+ assert trigger_edges[0].to_node == TestNode
137
+
138
+
139
+ def test_slack_trigger__multiple_entrypoints():
140
+ """SlackTrigger works with multiple entry points."""
141
+
142
+ # GIVEN multiple nodes
143
+ class NodeA(BaseNode):
144
+ pass
145
+
146
+ class NodeB(BaseNode):
147
+ pass
148
+
149
+ # WHEN we use SlackTrigger >> {NodeA, NodeB} syntax
150
+ graph = SlackTrigger >> {NodeA, NodeB}
151
+
152
+ # THEN both nodes have trigger edges
153
+ trigger_edges = list(graph.trigger_edges)
154
+ assert len(trigger_edges) == 2
155
+ target_nodes = {edge.to_node for edge in trigger_edges}
156
+ assert target_nodes == {NodeA, NodeB}
157
+
158
+
159
+ def test_slack_trigger__trigger_then_graph():
160
+ """SlackTrigger works with trigger >> node >> node syntax."""
161
+
162
+ # GIVEN two nodes
163
+ class StartNode(BaseNode):
164
+ pass
165
+
166
+ class EndNode(BaseNode):
167
+ pass
168
+
169
+ # WHEN we create SlackTrigger >> StartNode >> EndNode
170
+ graph = SlackTrigger >> StartNode >> EndNode
171
+
172
+ # THEN the graph has one trigger edge and one regular edge
173
+ trigger_edges = list(graph.trigger_edges)
174
+ assert len(trigger_edges) == 1
175
+ assert trigger_edges[0].trigger_class == SlackTrigger
176
+ assert trigger_edges[0].to_node == StartNode
177
+
178
+ edges = list(graph.edges)
179
+ assert len(edges) == 1
180
+ assert edges[0].to_node == EndNode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.7.6
3
+ Version: 1.7.8
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0