vellum-ai 1.7.4__py3-none-any.whl → 1.7.5__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/__init__.py +2 -0
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/reference.md +95 -0
- vellum/client/resources/workflow_deployments/client.py +111 -0
- vellum/client/resources/workflow_deployments/raw_client.py +121 -0
- vellum/client/types/__init__.py +2 -0
- vellum/client/types/paginated_workflow_deployment_release_list.py +30 -0
- vellum/client/types/vellum_error_code_enum.py +2 -0
- vellum/client/types/vellum_sdk_error_code_enum.py +2 -0
- vellum/client/types/workflow_execution_event_error_code.py +2 -0
- vellum/types/paginated_workflow_deployment_release_list.py +3 -0
- vellum/workflows/edges/__init__.py +2 -0
- vellum/workflows/edges/trigger_edge.py +67 -0
- vellum/workflows/events/tests/test_event.py +40 -0
- vellum/workflows/events/workflow.py +15 -3
- vellum/workflows/graph/graph.py +93 -0
- vellum/workflows/graph/tests/test_graph.py +167 -0
- vellum/workflows/nodes/displayable/tool_calling_node/node.py +1 -1
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +1 -1
- vellum/workflows/ports/port.py +11 -0
- vellum/workflows/runner/runner.py +5 -3
- vellum/workflows/triggers/__init__.py +4 -0
- vellum/workflows/triggers/base.py +125 -0
- vellum/workflows/triggers/manual.py +37 -0
- vellum/workflows/workflows/base.py +9 -9
- {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/METADATA +1 -1
- {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/RECORD +36 -29
- vellum_ee/assets/node-definitions.json +1 -1
- vellum_ee/workflows/display/base.py +26 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_manual_trigger_serialization.py +113 -0
- vellum_ee/workflows/display/workflows/base_workflow_display.py +63 -10
- {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/LICENSE +0 -0
- {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/WHEEL +0 -0
- {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/entry_points.txt +0 -0
vellum/workflows/graph/graph.py
CHANGED
@@ -3,11 +3,13 @@ from typing import TYPE_CHECKING, Iterator, List, Optional, Set, Type, Union
|
|
3
3
|
from orderly_set import OrderedSet
|
4
4
|
|
5
5
|
from vellum.workflows.edges.edge import Edge
|
6
|
+
from vellum.workflows.edges.trigger_edge import TriggerEdge
|
6
7
|
from vellum.workflows.types.generics import NodeType
|
7
8
|
|
8
9
|
if TYPE_CHECKING:
|
9
10
|
from vellum.workflows.nodes.bases.base import BaseNode
|
10
11
|
from vellum.workflows.ports.port import Port
|
12
|
+
from vellum.workflows.triggers.base import BaseTrigger
|
11
13
|
|
12
14
|
|
13
15
|
class NoPortsNode:
|
@@ -46,16 +48,19 @@ class Graph:
|
|
46
48
|
_entrypoints: Set[Union["Port", "NoPortsNode"]]
|
47
49
|
_edges: List[Edge]
|
48
50
|
_terminals: Set[Union["Port", "NoPortsNode"]]
|
51
|
+
_trigger_edges: List[TriggerEdge]
|
49
52
|
|
50
53
|
def __init__(
|
51
54
|
self,
|
52
55
|
entrypoints: Set[Union["Port", "NoPortsNode"]],
|
53
56
|
edges: List[Edge],
|
54
57
|
terminals: Set[Union["Port", "NoPortsNode"]],
|
58
|
+
trigger_edges: Optional[List[TriggerEdge]] = None,
|
55
59
|
):
|
56
60
|
self._edges = edges
|
57
61
|
self._entrypoints = entrypoints
|
58
62
|
self._terminals = terminals
|
63
|
+
self._trigger_edges = trigger_edges or []
|
59
64
|
|
60
65
|
@staticmethod
|
61
66
|
def from_port(port: "Port") -> "Graph":
|
@@ -96,12 +101,79 @@ class Graph:
|
|
96
101
|
def from_edge(edge: Edge) -> "Graph":
|
97
102
|
return Graph(entrypoints={edge.from_port}, edges=[edge], terminals={port for port in edge.to_node.Ports})
|
98
103
|
|
104
|
+
@staticmethod
|
105
|
+
def from_trigger_edge(edge: TriggerEdge) -> "Graph":
|
106
|
+
"""
|
107
|
+
Create a graph from a single TriggerEdge (Trigger >> Node).
|
108
|
+
|
109
|
+
Args:
|
110
|
+
edge: TriggerEdge connecting a trigger to a node
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
Graph with the trigger edge and the target node's ports as terminals
|
114
|
+
"""
|
115
|
+
ports = {port for port in edge.to_node.Ports}
|
116
|
+
if not ports:
|
117
|
+
no_ports_node = NoPortsNode(edge.to_node)
|
118
|
+
return Graph(
|
119
|
+
entrypoints={no_ports_node},
|
120
|
+
edges=[],
|
121
|
+
terminals={no_ports_node},
|
122
|
+
trigger_edges=[edge],
|
123
|
+
)
|
124
|
+
return Graph(
|
125
|
+
entrypoints=set(ports),
|
126
|
+
edges=[],
|
127
|
+
terminals=set(ports),
|
128
|
+
trigger_edges=[edge],
|
129
|
+
)
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def from_trigger_edges(edges: List[TriggerEdge]) -> "Graph":
|
133
|
+
"""
|
134
|
+
Create a graph from multiple TriggerEdges (e.g., Trigger >> {NodeA, NodeB}).
|
135
|
+
|
136
|
+
Args:
|
137
|
+
edges: List of TriggerEdges
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Graph with all trigger edges and target nodes' ports as entrypoints/terminals
|
141
|
+
"""
|
142
|
+
entrypoints: Set[Union["Port", NoPortsNode]] = set()
|
143
|
+
terminals: Set[Union["Port", NoPortsNode]] = set()
|
144
|
+
|
145
|
+
for edge in edges:
|
146
|
+
ports = {port for port in edge.to_node.Ports}
|
147
|
+
if not ports:
|
148
|
+
no_ports_node = NoPortsNode(edge.to_node)
|
149
|
+
entrypoints.add(no_ports_node)
|
150
|
+
terminals.add(no_ports_node)
|
151
|
+
else:
|
152
|
+
entrypoints.update(ports)
|
153
|
+
terminals.update(ports)
|
154
|
+
|
155
|
+
return Graph(
|
156
|
+
entrypoints=entrypoints,
|
157
|
+
edges=[],
|
158
|
+
terminals=terminals,
|
159
|
+
trigger_edges=edges,
|
160
|
+
)
|
161
|
+
|
99
162
|
@staticmethod
|
100
163
|
def empty() -> "Graph":
|
101
164
|
"""Create an empty graph with no entrypoints, edges, or terminals."""
|
102
165
|
return Graph(entrypoints=set(), edges=[], terminals=set())
|
103
166
|
|
104
167
|
def __rshift__(self, other: GraphTarget) -> "Graph":
|
168
|
+
# Check for trigger target (class-level only)
|
169
|
+
from vellum.workflows.triggers.base import BaseTrigger
|
170
|
+
|
171
|
+
if isinstance(other, type) and issubclass(other, BaseTrigger):
|
172
|
+
raise TypeError(
|
173
|
+
f"Cannot create edge targeting trigger {other.__name__}. "
|
174
|
+
f"Triggers must be at the start of a graph path, not as targets."
|
175
|
+
)
|
176
|
+
|
105
177
|
if not self._edges and not self._entrypoints:
|
106
178
|
raise ValueError("Graph instance can only create new edges from nodes within existing edges")
|
107
179
|
|
@@ -179,9 +251,30 @@ class Graph:
|
|
179
251
|
def edges(self) -> Iterator[Edge]:
|
180
252
|
return iter(self._edges)
|
181
253
|
|
254
|
+
@property
|
255
|
+
def trigger_edges(self) -> Iterator[TriggerEdge]:
|
256
|
+
"""Get all trigger edges in this graph."""
|
257
|
+
return iter(self._trigger_edges)
|
258
|
+
|
259
|
+
@property
|
260
|
+
def triggers(self) -> Iterator[Type["BaseTrigger"]]:
|
261
|
+
"""Get all unique trigger classes in this graph."""
|
262
|
+
seen_triggers = set()
|
263
|
+
for trigger_edge in self._trigger_edges:
|
264
|
+
if trigger_edge.trigger_class not in seen_triggers:
|
265
|
+
seen_triggers.add(trigger_edge.trigger_class)
|
266
|
+
yield trigger_edge.trigger_class
|
267
|
+
|
182
268
|
@property
|
183
269
|
def nodes(self) -> Iterator[Type["BaseNode"]]:
|
184
270
|
nodes = set()
|
271
|
+
|
272
|
+
# Include nodes from trigger edges
|
273
|
+
for trigger_edge in self._trigger_edges:
|
274
|
+
if trigger_edge.to_node not in nodes:
|
275
|
+
nodes.add(trigger_edge.to_node)
|
276
|
+
yield trigger_edge.to_node
|
277
|
+
|
185
278
|
if not self._edges:
|
186
279
|
for node in self.entrypoints:
|
187
280
|
if node not in nodes:
|
@@ -1,7 +1,10 @@
|
|
1
|
+
import pytest
|
2
|
+
|
1
3
|
from vellum.workflows.edges.edge import Edge
|
2
4
|
from vellum.workflows.graph.graph import Graph
|
3
5
|
from vellum.workflows.nodes.bases.base import BaseNode
|
4
6
|
from vellum.workflows.ports.port import Port
|
7
|
+
from vellum.workflows.triggers import ManualTrigger
|
5
8
|
|
6
9
|
|
7
10
|
def test_graph__empty():
|
@@ -617,3 +620,167 @@ def test_graph__from_node_with_empty_ports():
|
|
617
620
|
|
618
621
|
# THEN the graph should have exactly 1 node
|
619
622
|
assert len(list(graph.nodes)) == 1
|
623
|
+
|
624
|
+
|
625
|
+
def test_graph__manual_trigger_to_node():
|
626
|
+
# GIVEN a node
|
627
|
+
class MyNode(BaseNode):
|
628
|
+
pass
|
629
|
+
|
630
|
+
# WHEN we create graph with ManualTrigger >> Node (class-level, no instantiation)
|
631
|
+
graph = ManualTrigger >> MyNode
|
632
|
+
|
633
|
+
# THEN the graph has one trigger edge
|
634
|
+
trigger_edges = list(graph.trigger_edges)
|
635
|
+
assert len(trigger_edges) == 1
|
636
|
+
assert trigger_edges[0].trigger_class == ManualTrigger
|
637
|
+
assert trigger_edges[0].to_node == MyNode
|
638
|
+
|
639
|
+
# AND the graph has one trigger
|
640
|
+
triggers = list(graph.triggers)
|
641
|
+
assert len(triggers) == 1
|
642
|
+
assert triggers[0] == ManualTrigger
|
643
|
+
|
644
|
+
# AND the graph has one node
|
645
|
+
assert len(list(graph.nodes)) == 1
|
646
|
+
assert MyNode in list(graph.nodes)
|
647
|
+
|
648
|
+
|
649
|
+
def test_graph__manual_trigger_to_set_of_nodes():
|
650
|
+
# GIVEN two nodes
|
651
|
+
class NodeA(BaseNode):
|
652
|
+
pass
|
653
|
+
|
654
|
+
class NodeB(BaseNode):
|
655
|
+
pass
|
656
|
+
|
657
|
+
# WHEN we create graph with ManualTrigger >> {NodeA, NodeB}
|
658
|
+
graph = ManualTrigger >> {NodeA, NodeB}
|
659
|
+
|
660
|
+
# THEN the graph has two trigger edges
|
661
|
+
trigger_edges = list(graph.trigger_edges)
|
662
|
+
assert len(trigger_edges) == 2
|
663
|
+
|
664
|
+
# AND both edges connect to the same ManualTrigger class
|
665
|
+
assert all(edge.trigger_class == ManualTrigger for edge in trigger_edges)
|
666
|
+
|
667
|
+
# AND edges connect to both nodes
|
668
|
+
target_nodes = {edge.to_node for edge in trigger_edges}
|
669
|
+
assert target_nodes == {NodeA, NodeB}
|
670
|
+
|
671
|
+
# AND the graph has one unique trigger
|
672
|
+
triggers = list(graph.triggers)
|
673
|
+
assert len(triggers) == 1
|
674
|
+
|
675
|
+
# AND the graph has two nodes
|
676
|
+
assert len(list(graph.nodes)) == 2
|
677
|
+
|
678
|
+
|
679
|
+
def test_graph__manual_trigger_to_graph():
|
680
|
+
# GIVEN a graph of nodes
|
681
|
+
class NodeA(BaseNode):
|
682
|
+
pass
|
683
|
+
|
684
|
+
class NodeB(BaseNode):
|
685
|
+
pass
|
686
|
+
|
687
|
+
node_graph = NodeA >> NodeB
|
688
|
+
|
689
|
+
# WHEN we create graph with ManualTrigger >> Graph
|
690
|
+
graph = ManualTrigger >> node_graph
|
691
|
+
|
692
|
+
# THEN the graph has a trigger edge to the entrypoint
|
693
|
+
trigger_edges = list(graph.trigger_edges)
|
694
|
+
assert len(trigger_edges) == 1
|
695
|
+
assert trigger_edges[0].to_node == NodeA
|
696
|
+
|
697
|
+
# AND the graph preserves the original edges
|
698
|
+
edges = list(graph.edges)
|
699
|
+
assert len(edges) == 1
|
700
|
+
assert edges[0].to_node == NodeB
|
701
|
+
|
702
|
+
# AND the graph has both nodes
|
703
|
+
nodes = list(graph.nodes)
|
704
|
+
assert len(nodes) == 2
|
705
|
+
assert NodeA in nodes
|
706
|
+
assert NodeB in nodes
|
707
|
+
|
708
|
+
|
709
|
+
def test_graph__manual_trigger_to_set_of_graphs_preserves_edges():
|
710
|
+
# GIVEN two graphs of nodes
|
711
|
+
class NodeA(BaseNode):
|
712
|
+
pass
|
713
|
+
|
714
|
+
class NodeB(BaseNode):
|
715
|
+
pass
|
716
|
+
|
717
|
+
class NodeC(BaseNode):
|
718
|
+
pass
|
719
|
+
|
720
|
+
class NodeD(BaseNode):
|
721
|
+
pass
|
722
|
+
|
723
|
+
graph_one = NodeA >> NodeB
|
724
|
+
graph_two = NodeC >> NodeD
|
725
|
+
|
726
|
+
# WHEN we create a graph with ManualTrigger >> {Graph1, Graph2}
|
727
|
+
combined_graph = ManualTrigger >> {graph_one, graph_two}
|
728
|
+
|
729
|
+
# THEN the combined graph has trigger edges to both entrypoints
|
730
|
+
trigger_edges = list(combined_graph.trigger_edges)
|
731
|
+
assert len(trigger_edges) == 2
|
732
|
+
assert {edge.to_node for edge in trigger_edges} == {NodeA, NodeC}
|
733
|
+
|
734
|
+
# AND the combined graph preserves all downstream edges
|
735
|
+
edges = list(combined_graph.edges)
|
736
|
+
assert len(edges) == 2
|
737
|
+
assert {(edge.from_port.node_class, edge.to_node) for edge in edges} == {
|
738
|
+
(NodeA, NodeB),
|
739
|
+
(NodeC, NodeD),
|
740
|
+
}
|
741
|
+
|
742
|
+
# AND the combined graph still exposes all nodes
|
743
|
+
nodes = list(combined_graph.nodes)
|
744
|
+
assert {NodeA, NodeB, NodeC, NodeD}.issubset(nodes)
|
745
|
+
|
746
|
+
|
747
|
+
def test_graph__node_to_trigger_raises():
|
748
|
+
# GIVEN a node and trigger
|
749
|
+
class MyNode(BaseNode):
|
750
|
+
pass
|
751
|
+
|
752
|
+
# WHEN we try to create Node >> Trigger (class-level)
|
753
|
+
# THEN it raises TypeError
|
754
|
+
with pytest.raises(TypeError, match="Cannot create edge targeting trigger"):
|
755
|
+
MyNode >> ManualTrigger
|
756
|
+
|
757
|
+
# WHEN we try to create Node >> Trigger (instance-level)
|
758
|
+
# THEN it also raises TypeError
|
759
|
+
with pytest.raises(TypeError, match="Cannot create edge targeting trigger"):
|
760
|
+
MyNode >> ManualTrigger
|
761
|
+
|
762
|
+
|
763
|
+
def test_graph__trigger_then_graph_then_node():
|
764
|
+
# GIVEN a trigger, a node, and another node
|
765
|
+
class StartNode(BaseNode):
|
766
|
+
pass
|
767
|
+
|
768
|
+
class EndNode(BaseNode):
|
769
|
+
pass
|
770
|
+
|
771
|
+
# WHEN we create Trigger >> Node >> Node
|
772
|
+
graph = ManualTrigger >> StartNode >> EndNode
|
773
|
+
|
774
|
+
# THEN the graph has one trigger edge
|
775
|
+
trigger_edges = list(graph.trigger_edges)
|
776
|
+
assert len(trigger_edges) == 1
|
777
|
+
assert trigger_edges[0].to_node == StartNode
|
778
|
+
|
779
|
+
# AND the graph has one regular edge
|
780
|
+
edges = list(graph.edges)
|
781
|
+
assert len(edges) == 1
|
782
|
+
assert edges[0].to_node == EndNode
|
783
|
+
|
784
|
+
# AND the graph has both nodes
|
785
|
+
nodes = list(graph.nodes)
|
786
|
+
assert len(nodes) == 2
|
@@ -47,7 +47,7 @@ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
|
|
47
47
|
functions: ClassVar[List[Tool]] = []
|
48
48
|
prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
|
49
49
|
parameters: PromptParameters = DEFAULT_PROMPT_PARAMETERS
|
50
|
-
max_prompt_iterations: ClassVar[Optional[int]] =
|
50
|
+
max_prompt_iterations: ClassVar[Optional[int]] = 25
|
51
51
|
settings: ClassVar[Optional[Union[PromptSettings, Dict[str, Any]]]] = None
|
52
52
|
|
53
53
|
class Outputs(BaseOutputs):
|
@@ -89,7 +89,7 @@ class FunctionCallNodeMixin:
|
|
89
89
|
|
90
90
|
|
91
91
|
class ToolPromptNode(InlinePromptNode[ToolCallingState]):
|
92
|
-
max_prompt_iterations: Optional[int] =
|
92
|
+
max_prompt_iterations: Optional[int] = 25
|
93
93
|
|
94
94
|
class Trigger(InlinePromptNode.Trigger):
|
95
95
|
merge_behavior = MergeBehavior.AWAIT_ATTRIBUTES
|
vellum/workflows/ports/port.py
CHANGED
@@ -61,6 +61,17 @@ class Port:
|
|
61
61
|
return iter(self._edges)
|
62
62
|
|
63
63
|
def __rshift__(self, other: GraphTarget) -> Graph:
|
64
|
+
# Check for trigger target (class-level only)
|
65
|
+
from vellum.workflows.triggers.base import BaseTrigger
|
66
|
+
|
67
|
+
# Check if other is a trigger class
|
68
|
+
if isinstance(other, type) and issubclass(other, BaseTrigger):
|
69
|
+
raise TypeError(
|
70
|
+
f"Cannot create edge targeting trigger {other.__name__}. "
|
71
|
+
f"Triggers must be at the start of a graph path, not as targets. "
|
72
|
+
f"Did you mean: {other.__name__} >> {self.node_class.__name__}?"
|
73
|
+
)
|
74
|
+
|
64
75
|
if isinstance(other, set) or isinstance(other, Graph):
|
65
76
|
return Graph.from_port(self) >> other
|
66
77
|
|
@@ -74,6 +74,7 @@ from vellum.workflows.references import ExternalInputReference, OutputReference
|
|
74
74
|
from vellum.workflows.references.state_value import StateValueReference
|
75
75
|
from vellum.workflows.state.base import BaseState
|
76
76
|
from vellum.workflows.state.delta import StateDelta
|
77
|
+
from vellum.workflows.types.core import CancelSignal
|
77
78
|
from vellum.workflows.types.generics import InputsType, OutputsType, StateType
|
78
79
|
|
79
80
|
if TYPE_CHECKING:
|
@@ -103,7 +104,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
103
104
|
entrypoint_nodes: Optional[RunFromNodeArg] = None,
|
104
105
|
external_inputs: Optional[ExternalInputsArg] = None,
|
105
106
|
previous_execution_id: Optional[Union[str, UUID]] = None,
|
106
|
-
cancel_signal: Optional[
|
107
|
+
cancel_signal: Optional[CancelSignal] = None,
|
107
108
|
node_output_mocks: Optional[MockNodeExecutionArg] = None,
|
108
109
|
max_concurrency: Optional[int] = None,
|
109
110
|
init_execution_context: Optional[ExecutionContext] = None,
|
@@ -799,13 +800,14 @@ class WorkflowRunner(Generic[StateType]):
|
|
799
800
|
parent=self._execution_context.parent_context,
|
800
801
|
)
|
801
802
|
|
802
|
-
def _fulfill_workflow_event(self, outputs: OutputsType) -> WorkflowExecutionFulfilledEvent:
|
803
|
+
def _fulfill_workflow_event(self, outputs: OutputsType, final_state: StateType) -> WorkflowExecutionFulfilledEvent:
|
803
804
|
return WorkflowExecutionFulfilledEvent(
|
804
805
|
trace_id=self._execution_context.trace_id,
|
805
806
|
span_id=self._initial_state.meta.span_id,
|
806
807
|
body=WorkflowExecutionFulfilledBody(
|
807
808
|
workflow_definition=self.workflow.__class__,
|
808
809
|
outputs=outputs,
|
810
|
+
final_state=final_state,
|
809
811
|
),
|
810
812
|
parent=self._execution_context.parent_context,
|
811
813
|
)
|
@@ -961,7 +963,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
961
963
|
descriptor.instance.resolve(final_state),
|
962
964
|
)
|
963
965
|
|
964
|
-
self._workflow_event_outer_queue.put(self._fulfill_workflow_event(fulfilled_outputs))
|
966
|
+
self._workflow_event_outer_queue.put(self._fulfill_workflow_event(fulfilled_outputs, final_state))
|
965
967
|
|
966
968
|
def _run_background_thread(self) -> None:
|
967
969
|
state_class = self.workflow.get_state_class()
|
@@ -0,0 +1,125 @@
|
|
1
|
+
from abc import ABC, ABCMeta
|
2
|
+
from typing import TYPE_CHECKING, Any, Type, cast
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from vellum.workflows.graph.graph import Graph, GraphTarget
|
6
|
+
|
7
|
+
|
8
|
+
class BaseTriggerMeta(ABCMeta):
|
9
|
+
"""
|
10
|
+
Metaclass for BaseTrigger that enables class-level >> operator.
|
11
|
+
|
12
|
+
This allows triggers to be used at the class level, similar to nodes:
|
13
|
+
ManualTrigger >> MyNode # Class-level, no instantiation
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __rshift__(cls, other: "GraphTarget") -> "Graph": # type: ignore[misc]
|
17
|
+
"""
|
18
|
+
Enable Trigger class >> Node syntax (class-level only).
|
19
|
+
|
20
|
+
Args:
|
21
|
+
other: The target to connect to - can be a Node, Graph, or set of Nodes
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Graph: A graph object with trigger edges
|
25
|
+
|
26
|
+
Examples:
|
27
|
+
ManualTrigger >> MyNode
|
28
|
+
ManualTrigger >> {NodeA, NodeB}
|
29
|
+
ManualTrigger >> (NodeA >> NodeB)
|
30
|
+
"""
|
31
|
+
from vellum.workflows.edges.trigger_edge import TriggerEdge
|
32
|
+
from vellum.workflows.graph.graph import Graph
|
33
|
+
from vellum.workflows.nodes.bases.base import BaseNode as BaseNodeClass
|
34
|
+
|
35
|
+
# Cast cls to the proper type for TriggerEdge
|
36
|
+
trigger_cls = cast("Type[BaseTrigger]", cls)
|
37
|
+
|
38
|
+
if isinstance(other, set):
|
39
|
+
# Trigger >> {NodeA, NodeB}
|
40
|
+
trigger_edges = []
|
41
|
+
graph_items = []
|
42
|
+
for item in other:
|
43
|
+
if isinstance(item, type) and issubclass(item, BaseNodeClass):
|
44
|
+
trigger_edges.append(TriggerEdge(trigger_cls, item))
|
45
|
+
elif isinstance(item, Graph):
|
46
|
+
# Trigger >> {Graph1, Graph2}
|
47
|
+
graph_items.append(item)
|
48
|
+
for entrypoint in item.entrypoints:
|
49
|
+
trigger_edges.append(TriggerEdge(trigger_cls, entrypoint))
|
50
|
+
else:
|
51
|
+
raise TypeError(
|
52
|
+
f"Cannot connect trigger to {type(item).__name__}. " f"Expected BaseNode or Graph in set."
|
53
|
+
)
|
54
|
+
|
55
|
+
result_graph = Graph.from_trigger_edges(trigger_edges)
|
56
|
+
|
57
|
+
for graph_item in graph_items:
|
58
|
+
result_graph._extend_edges(graph_item.edges)
|
59
|
+
result_graph._terminals.update(graph_item._terminals)
|
60
|
+
for existing_trigger_edge in graph_item._trigger_edges:
|
61
|
+
if existing_trigger_edge not in result_graph._trigger_edges:
|
62
|
+
result_graph._trigger_edges.append(existing_trigger_edge)
|
63
|
+
|
64
|
+
return result_graph
|
65
|
+
|
66
|
+
elif isinstance(other, Graph):
|
67
|
+
# Trigger >> Graph
|
68
|
+
edges = [TriggerEdge(trigger_cls, entrypoint) for entrypoint in other.entrypoints]
|
69
|
+
result_graph = Graph.from_trigger_edges(edges)
|
70
|
+
# Also include the edges from the original graph
|
71
|
+
result_graph._extend_edges(other.edges)
|
72
|
+
result_graph._terminals = other._terminals
|
73
|
+
return result_graph
|
74
|
+
|
75
|
+
elif isinstance(other, type) and issubclass(other, BaseNodeClass):
|
76
|
+
# Trigger >> Node
|
77
|
+
edge = TriggerEdge(trigger_cls, other)
|
78
|
+
return Graph.from_trigger_edge(edge)
|
79
|
+
|
80
|
+
else:
|
81
|
+
raise TypeError(
|
82
|
+
f"Cannot connect trigger to {type(other).__name__}. " f"Expected BaseNode, Graph, or set of BaseNodes."
|
83
|
+
)
|
84
|
+
|
85
|
+
def __rrshift__(cls, other: Any) -> "Graph":
|
86
|
+
"""
|
87
|
+
Prevent Node >> Trigger class syntax.
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
TypeError: Always, as this operation is not allowed
|
91
|
+
"""
|
92
|
+
raise TypeError(
|
93
|
+
f"Cannot create edge targeting trigger {cls.__name__}. "
|
94
|
+
f"Triggers must be at the start of a graph path, not as targets. "
|
95
|
+
f"Did you mean: {cls.__name__} >> {other.__name__ if hasattr(other, '__name__') else other}?"
|
96
|
+
)
|
97
|
+
|
98
|
+
|
99
|
+
class BaseTrigger(ABC, metaclass=BaseTriggerMeta):
|
100
|
+
"""
|
101
|
+
Base class for workflow triggers - first-class graph elements.
|
102
|
+
|
103
|
+
Triggers define how and when a workflow execution is initiated. They are integrated
|
104
|
+
into the workflow graph using the >> operator and can connect to nodes at the class level.
|
105
|
+
|
106
|
+
Examples:
|
107
|
+
# Class-level usage (consistent with nodes)
|
108
|
+
ManualTrigger >> MyNode
|
109
|
+
ManualTrigger >> {NodeA, NodeB}
|
110
|
+
ManualTrigger >> (NodeA >> NodeB)
|
111
|
+
|
112
|
+
Subclass Hierarchy:
|
113
|
+
- ManualTrigger: Explicit workflow invocation (default)
|
114
|
+
- IntegrationTrigger: External service triggers (base for Slack, GitHub, etc.)
|
115
|
+
- ScheduledTrigger: Time-based triggers with cron/interval schedules
|
116
|
+
|
117
|
+
Important:
|
118
|
+
Triggers can only appear at the start of graph paths. Attempting to create
|
119
|
+
edges targeting triggers (Node >> Trigger) will raise a TypeError.
|
120
|
+
|
121
|
+
Note:
|
122
|
+
Like nodes, triggers work at the class level only. Do not instantiate triggers.
|
123
|
+
"""
|
124
|
+
|
125
|
+
pass
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from vellum.workflows.triggers.base import BaseTrigger
|
2
|
+
|
3
|
+
|
4
|
+
class ManualTrigger(BaseTrigger):
|
5
|
+
"""
|
6
|
+
Default trigger representing explicit workflow invocation.
|
7
|
+
|
8
|
+
ManualTrigger is used when workflows are explicitly invoked via:
|
9
|
+
- workflow.run() method calls
|
10
|
+
- workflow.stream() method calls
|
11
|
+
- API calls to execute the workflow
|
12
|
+
|
13
|
+
This is the default trigger for all workflows. When no trigger is specified
|
14
|
+
in a workflow's graph definition, ManualTrigger is implicitly added.
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
# Explicit ManualTrigger (equivalent to implicit)
|
18
|
+
class MyWorkflow(BaseWorkflow):
|
19
|
+
graph = ManualTrigger >> MyNode
|
20
|
+
|
21
|
+
# Implicit ManualTrigger (normalized to above)
|
22
|
+
class MyWorkflow(BaseWorkflow):
|
23
|
+
graph = MyNode
|
24
|
+
|
25
|
+
Characteristics:
|
26
|
+
- Provides no trigger-specific inputs
|
27
|
+
- Always ready to execute when invoked
|
28
|
+
- Simplest trigger type with no configuration
|
29
|
+
- Default behavior for backward compatibility
|
30
|
+
|
31
|
+
Comparison with other triggers:
|
32
|
+
- IntegrationTrigger: Responds to external events (webhooks, API calls)
|
33
|
+
- ScheduledTrigger: Executes based on time/schedule configuration
|
34
|
+
- ManualTrigger: Executes when explicitly called
|
35
|
+
"""
|
36
|
+
|
37
|
+
pass
|
@@ -4,7 +4,6 @@ from functools import lru_cache
|
|
4
4
|
import importlib
|
5
5
|
import inspect
|
6
6
|
import logging
|
7
|
-
from threading import Event as ThreadingEvent
|
8
7
|
from uuid import UUID, uuid4
|
9
8
|
from typing import (
|
10
9
|
Any,
|
@@ -76,6 +75,7 @@ from vellum.workflows.runner.runner import ExternalInputsArg, RunFromNodeArg
|
|
76
75
|
from vellum.workflows.state.base import BaseState, StateMeta
|
77
76
|
from vellum.workflows.state.context import WorkflowContext
|
78
77
|
from vellum.workflows.state.store import Store
|
78
|
+
from vellum.workflows.types import CancelSignal
|
79
79
|
from vellum.workflows.types.generics import InputsType, StateType
|
80
80
|
from vellum.workflows.types.utils import get_original_base
|
81
81
|
from vellum.workflows.utils.uuids import uuid4_from_hash
|
@@ -227,12 +227,12 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
|
|
227
227
|
WorkflowEvent = Union[ # type: ignore
|
228
228
|
GenericWorkflowEvent,
|
229
229
|
WorkflowExecutionInitiatedEvent[InputsType, StateType], # type: ignore[valid-type]
|
230
|
-
WorkflowExecutionFulfilledEvent[Outputs],
|
230
|
+
WorkflowExecutionFulfilledEvent[Outputs, StateType], # type: ignore[valid-type]
|
231
231
|
WorkflowExecutionSnapshottedEvent[StateType], # type: ignore[valid-type]
|
232
232
|
]
|
233
233
|
|
234
234
|
TerminalWorkflowEvent = Union[
|
235
|
-
WorkflowExecutionFulfilledEvent[Outputs],
|
235
|
+
WorkflowExecutionFulfilledEvent[Outputs, StateType], # type: ignore[valid-type]
|
236
236
|
WorkflowExecutionRejectedEvent,
|
237
237
|
WorkflowExecutionPausedEvent,
|
238
238
|
]
|
@@ -374,7 +374,7 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
|
|
374
374
|
entrypoint_nodes: Optional[RunFromNodeArg] = None,
|
375
375
|
external_inputs: Optional[ExternalInputsArg] = None,
|
376
376
|
previous_execution_id: Optional[Union[str, UUID]] = None,
|
377
|
-
cancel_signal: Optional[
|
377
|
+
cancel_signal: Optional[CancelSignal] = None,
|
378
378
|
node_output_mocks: Optional[MockNodeExecutionArg] = None,
|
379
379
|
max_concurrency: Optional[int] = None,
|
380
380
|
) -> TerminalWorkflowEvent:
|
@@ -402,8 +402,8 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
|
|
402
402
|
previous_execution_id: Optional[Union[str, UUID]] = None
|
403
403
|
The execution ID of the previous execution to resume from.
|
404
404
|
|
405
|
-
cancel_signal: Optional[
|
406
|
-
A
|
405
|
+
cancel_signal: Optional[CancelSignal] = None
|
406
|
+
A cancel signal that can be used to cancel the Workflow Execution.
|
407
407
|
|
408
408
|
node_output_mocks: Optional[MockNodeExecutionArg] = None
|
409
409
|
A list of Outputs to mock for Nodes during Workflow Execution. Each mock can include a `when_condition`
|
@@ -493,7 +493,7 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
|
|
493
493
|
entrypoint_nodes: Optional[RunFromNodeArg] = None,
|
494
494
|
external_inputs: Optional[ExternalInputsArg] = None,
|
495
495
|
previous_execution_id: Optional[Union[str, UUID]] = None,
|
496
|
-
cancel_signal: Optional[
|
496
|
+
cancel_signal: Optional[CancelSignal] = None,
|
497
497
|
node_output_mocks: Optional[MockNodeExecutionArg] = None,
|
498
498
|
max_concurrency: Optional[int] = None,
|
499
499
|
) -> WorkflowEventStream:
|
@@ -522,8 +522,8 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
|
|
522
522
|
previous_execution_id: Optional[Union[str, UUID]] = None
|
523
523
|
The execution ID of the previous execution to resume from.
|
524
524
|
|
525
|
-
cancel_signal: Optional[
|
526
|
-
A
|
525
|
+
cancel_signal: Optional[CancelSignal] = None
|
526
|
+
A cancel signal that can be used to cancel the Workflow Execution.
|
527
527
|
|
528
528
|
node_output_mocks: Optional[MockNodeExecutionArg] = None
|
529
529
|
A list of Outputs to mock for Nodes during Workflow Execution. Each mock can include a `when_condition`
|