vellum-ai 0.14.3__py3-none-any.whl → 0.14.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.
Files changed (47) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/resources/document_indexes/client.py +4 -4
  3. vellum/client/resources/documents/client.py +0 -2
  4. vellum/client/resources/folder_entities/client.py +4 -8
  5. vellum/client/resources/test_suite_runs/client.py +0 -2
  6. vellum/client/types/deployment_read.py +5 -5
  7. vellum/client/types/deployment_release_tag_read.py +2 -2
  8. vellum/client/types/document_document_to_document_index.py +5 -5
  9. vellum/client/types/document_index_read.py +5 -5
  10. vellum/client/types/document_read.py +1 -1
  11. vellum/client/types/enriched_normalized_completion.py +3 -3
  12. vellum/client/types/generate_options_request.py +2 -2
  13. vellum/client/types/slim_deployment_read.py +5 -5
  14. vellum/client/types/slim_document.py +3 -3
  15. vellum/client/types/slim_document_document_to_document_index.py +5 -5
  16. vellum/client/types/slim_workflow_deployment.py +5 -5
  17. vellum/client/types/test_suite_run_read.py +5 -5
  18. vellum/client/types/workflow_deployment_read.py +5 -5
  19. vellum/client/types/workflow_release_tag_read.py +2 -2
  20. vellum/workflows/constants.py +9 -0
  21. vellum/workflows/context.py +8 -3
  22. vellum/workflows/nodes/core/map_node/node.py +1 -1
  23. vellum/workflows/nodes/core/retry_node/node.py +4 -3
  24. vellum/workflows/nodes/core/try_node/node.py +1 -1
  25. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +5 -0
  26. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +81 -1
  27. vellum/workflows/nodes/displayable/code_execution_node/utils.py +44 -20
  28. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +17 -10
  29. vellum/workflows/nodes/displayable/tests/test_inline_text_prompt_node.py +1 -0
  30. vellum/workflows/tests/test_undefined.py +12 -0
  31. vellum/workflows/workflows/base.py +76 -0
  32. vellum/workflows/workflows/tests/test_base_workflow.py +135 -0
  33. vellum/workflows/workflows/tests/test_context.py +60 -0
  34. {vellum_ai-0.14.3.dist-info → vellum_ai-0.14.5.dist-info}/METADATA +1 -1
  35. {vellum_ai-0.14.3.dist-info → vellum_ai-0.14.5.dist-info}/RECORD +47 -44
  36. vellum_ee/workflows/display/nodes/__init__.py +4 -0
  37. vellum_ee/workflows/display/nodes/vellum/__init__.py +2 -0
  38. vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py +39 -0
  39. vellum_ee/workflows/display/nodes/vellum/map_node.py +2 -2
  40. vellum_ee/workflows/display/nodes/vellum/retry_node.py +36 -4
  41. vellum_ee/workflows/display/nodes/vellum/try_node.py +43 -29
  42. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +25 -1
  43. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +14 -0
  44. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +19 -1
  45. {vellum_ai-0.14.3.dist-info → vellum_ai-0.14.5.dist-info}/LICENSE +0 -0
  46. {vellum_ai-0.14.3.dist-info → vellum_ai-0.14.5.dist-info}/WHEEL +0 -0
  47. {vellum_ai-0.14.3.dist-info → vellum_ai-0.14.5.dist-info}/entry_points.txt +0 -0
@@ -67,6 +67,49 @@ def _clean_for_dict_wrapper(obj):
67
67
  return obj
68
68
 
69
69
 
70
+ def _get_type_name(obj: Any) -> str:
71
+ if isinstance(obj, type):
72
+ return obj.__name__
73
+
74
+ if get_origin(obj) is Union:
75
+ children = [_get_type_name(child) for child in get_args(obj)]
76
+ return " | ".join(children)
77
+
78
+ return str(obj)
79
+
80
+
81
+ def _cast_to_output_type(result: Any, output_type: Any) -> Any:
82
+ is_valid_output_type = isinstance(output_type, type)
83
+ if get_origin(output_type) is Union:
84
+ allowed_types = get_args(output_type)
85
+ for allowed_type in allowed_types:
86
+ try:
87
+ return _cast_to_output_type(result, allowed_type)
88
+ except NodeException:
89
+ continue
90
+ elif get_origin(output_type) is list:
91
+ allowed_item_type = get_args(output_type)[0]
92
+ if isinstance(result, list):
93
+ return [_cast_to_output_type(item, allowed_item_type) for item in result]
94
+ elif is_valid_output_type and issubclass(output_type, BaseModel) and not isinstance(result, output_type):
95
+ try:
96
+ return output_type.model_validate(result)
97
+ except ValidationError as e:
98
+ raise NodeException(
99
+ code=WorkflowErrorCode.INVALID_OUTPUTS,
100
+ message=re.sub(r"\s+For further information visit [^\s]+", "", str(e)),
101
+ ) from e
102
+ elif is_valid_output_type and isinstance(result, output_type):
103
+ return result
104
+
105
+ output_type_name = _get_type_name(output_type)
106
+ result_type_name = _get_type_name(type(result))
107
+ raise NodeException(
108
+ code=WorkflowErrorCode.INVALID_OUTPUTS,
109
+ message=f"Expected an output of type '{output_type_name}', but received '{result_type_name}'",
110
+ )
111
+
112
+
70
113
  def run_code_inline(
71
114
  code: str,
72
115
  inputs: EntityInputsInterface,
@@ -112,25 +155,6 @@ __arg__out = main({", ".join(run_args)})
112
155
  result = exec_globals["__arg__out"]
113
156
 
114
157
  if output_type != Any:
115
- if get_origin(output_type) is Union:
116
- allowed_types = get_args(output_type)
117
- if not isinstance(result, allowed_types):
118
- raise NodeException(
119
- code=WorkflowErrorCode.INVALID_OUTPUTS,
120
- message=f"Expected output to be in types {allowed_types}, but received '{type(result).__name__}'",
121
- )
122
- elif issubclass(output_type, BaseModel) and not isinstance(result, output_type):
123
- try:
124
- result = output_type.model_validate(result)
125
- except ValidationError as e:
126
- raise NodeException(
127
- code=WorkflowErrorCode.INVALID_OUTPUTS,
128
- message=re.sub(r"\s+For further information visit [^\s]+", "", str(e)),
129
- ) from e
130
- elif not isinstance(result, output_type):
131
- raise NodeException(
132
- code=WorkflowErrorCode.INVALID_OUTPUTS,
133
- message=f"Expected an output of type '{output_type.__name__}', but received '{type(result).__name__}'",
134
- )
158
+ result = _cast_to_output_type(result, output_type)
135
159
 
136
160
  return logs, result
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import Iterator
2
3
 
3
4
  from vellum.workflows.errors import WorkflowErrorCode
@@ -44,13 +45,19 @@ class PromptDeploymentNode(BasePromptDeploymentNode[StateType]):
44
45
  code=WorkflowErrorCode.INTERNAL_ERROR,
45
46
  )
46
47
 
47
- string_output = next((output for output in outputs if output.type == "STRING"), None)
48
- if not string_output or string_output.value is None:
49
- output_types = {output.type for output in outputs}
50
- is_plural = len(output_types) > 1
51
- raise NodeException(
52
- message=f"Expected to receive a non-null string output from Prompt. Only found outputs of type{'s' if is_plural else ''}: {', '.join(output_types)}", # noqa: E501
53
- code=WorkflowErrorCode.INTERNAL_ERROR,
54
- )
55
-
56
- yield BaseOutput(name="text", value=string_output.value)
48
+ string_outputs = []
49
+ for output in outputs:
50
+ if output.value is None:
51
+ continue
52
+
53
+ if output.type == "STRING":
54
+ string_outputs.append(output.value)
55
+ elif output.type == "JSON":
56
+ string_outputs.append(json.dumps(output.value, indent=4))
57
+ elif output.type == "FUNCTION_CALL":
58
+ string_outputs.append(output.value.model_dump_json(indent=4))
59
+ else:
60
+ string_outputs.append(output.value.message)
61
+
62
+ value = "\n".join(string_outputs)
63
+ yield BaseOutput(name="text", value=value)
@@ -91,6 +91,7 @@ def test_inline_text_prompt_node__basic(vellum_adhoc_prompt_client):
91
91
  custom_parameters=None,
92
92
  ),
93
93
  request_options=mock.ANY,
94
+ settings=None,
94
95
  )
95
96
 
96
97
 
@@ -0,0 +1,12 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.constants import undefined
4
+
5
+
6
+ def test_undefined__ensure_sensible_error_messages():
7
+ # WHEN we invoke an invalid operation on `undefined`
8
+ with pytest.raises(Exception) as e:
9
+ len(undefined)
10
+
11
+ # THEN we get a sensible error message
12
+ assert str(e.value) == "object of type 'undefined' has no len()"
@@ -100,6 +100,30 @@ class _BaseWorkflowMeta(type):
100
100
  new_dct,
101
101
  )
102
102
 
103
+ def collect_nodes(graph_item: Union[GraphAttribute, Set[GraphAttribute]]) -> Set[Type[BaseNode]]:
104
+ nodes: Set[Type[BaseNode]] = set()
105
+ if isinstance(graph_item, Graph):
106
+ nodes.update(node for node in graph_item.nodes)
107
+ elif isinstance(graph_item, set):
108
+ for item in graph_item:
109
+ if isinstance(item, Graph):
110
+ nodes.update(node for node in item.nodes)
111
+ elif inspect.isclass(item) and issubclass(item, BaseNode):
112
+ nodes.add(item)
113
+ elif issubclass(graph_item, BaseNode):
114
+ nodes.add(graph_item)
115
+ else:
116
+ raise ValueError(f"Unexpected graph type: {graph_item.__class__}")
117
+ return nodes
118
+
119
+ graph_nodes = collect_nodes(dct.get("graph", set()))
120
+ unused_nodes = collect_nodes(dct.get("unused_graphs", set()))
121
+
122
+ overlap = graph_nodes & unused_nodes
123
+ if overlap:
124
+ node_names = [node.__name__ for node in overlap]
125
+ raise ValueError(f"Node(s) {', '.join(node_names)} cannot appear in both graph and unused_graphs")
126
+
103
127
  cls = super().__new__(mcs, name, bases, dct)
104
128
  workflow_class = cast(Type["BaseWorkflow"], cls)
105
129
  workflow_class.__id__ = uuid4_from_hash(workflow_class.__qualname__)
@@ -112,6 +136,7 @@ GraphAttribute = Union[Type[BaseNode], Graph, Set[Type[BaseNode]], Set[Graph]]
112
136
  class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
113
137
  __id__: UUID = uuid4_from_hash(__qualname__)
114
138
  graph: ClassVar[GraphAttribute]
139
+ unused_graphs: ClassVar[Set[GraphAttribute]] # nodes or graphs that are defined but not used in the graph
115
140
  emitters: List[BaseWorkflowEmitter]
116
141
  resolvers: List[BaseWorkflowResolver]
117
142
 
@@ -196,6 +221,57 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
196
221
  nodes.add(node)
197
222
  yield node
198
223
 
224
+ @classmethod
225
+ def get_unused_subgraphs(cls) -> List[Graph]:
226
+ """
227
+ Returns a list of subgraphs that are defined but not used in the graph
228
+ """
229
+ if not hasattr(cls, "unused_graphs"):
230
+ return []
231
+
232
+ graphs = []
233
+ for item in cls.unused_graphs:
234
+ if isinstance(item, Graph):
235
+ graphs.append(item)
236
+ elif isinstance(item, set):
237
+ for subitem in item:
238
+ if isinstance(subitem, Graph):
239
+ graphs.append(subitem)
240
+ elif issubclass(subitem, BaseNode):
241
+ graphs.append(Graph.from_node(subitem))
242
+ elif issubclass(item, BaseNode):
243
+ graphs.append(Graph.from_node(item))
244
+ return graphs
245
+
246
+ @classmethod
247
+ def get_unused_nodes(cls) -> Iterator[Type[BaseNode]]:
248
+ """
249
+ Returns an iterator over the nodes that are defined but not used in the graph.
250
+ """
251
+ if not hasattr(cls, "unused_graphs"):
252
+ yield from ()
253
+ else:
254
+ nodes = set()
255
+ subgraphs = cls.get_unused_subgraphs()
256
+ for subgraph in subgraphs:
257
+ for node in subgraph.nodes:
258
+ if node not in nodes:
259
+ nodes.add(node)
260
+ yield node
261
+
262
+ @classmethod
263
+ def get_unused_edges(cls) -> Iterator[Edge]:
264
+ """
265
+ Returns an iterator over edges that are defined but not used in the graph.
266
+ """
267
+ edges = set()
268
+ subgraphs = cls.get_unused_subgraphs()
269
+ for subgraph in subgraphs:
270
+ for edge in subgraph.edges:
271
+ if edge not in edges:
272
+ edges.add(edge)
273
+ yield edge
274
+
199
275
  @classmethod
200
276
  def get_entrypoints(cls) -> Iterable[Type[BaseNode]]:
201
277
  return iter({e for g in cls.get_subgraphs() for e in g.entrypoints})
@@ -1,3 +1,6 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.edges.edge import Edge
1
4
  from vellum.workflows.inputs.base import BaseInputs
2
5
  from vellum.workflows.nodes.bases.base import BaseNode
3
6
  from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubworkflowNode
@@ -78,3 +81,135 @@ def test_subworkflow__inherit_base_outputs():
78
81
  # TEST that the outputs are correct
79
82
  assert terminal_event.name == "workflow.execution.fulfilled", terminal_event
80
83
  assert terminal_event.outputs == {"output": "bar"}
84
+
85
+
86
+ def test_workflow__nodes_not_in_graph():
87
+ class NodeA(BaseNode):
88
+ pass
89
+
90
+ class NodeB(BaseNode):
91
+ pass
92
+
93
+ class NodeC(BaseNode):
94
+ pass
95
+
96
+ # WHEN we create a workflow with multiple unused nodes
97
+ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
98
+ graph = NodeA
99
+ unused_graphs = {NodeB, NodeC}
100
+
101
+ # TEST that all nodes from unused_graphs are collected
102
+ unused_graphs = set(TestWorkflow.get_unused_nodes())
103
+ assert unused_graphs == {NodeB, NodeC}
104
+
105
+
106
+ def test_workflow__unused_graphs():
107
+ class NodeA(BaseNode):
108
+ pass
109
+
110
+ class NodeB(BaseNode):
111
+ pass
112
+
113
+ class NodeC(BaseNode):
114
+ pass
115
+
116
+ class NodeD(BaseNode):
117
+ pass
118
+
119
+ class NodeE(BaseNode):
120
+ pass
121
+
122
+ class NodeF(BaseNode):
123
+ pass
124
+
125
+ # WHEN we create a workflow with unused nodes in a graph
126
+ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
127
+ graph = NodeA
128
+ unused_graphs = {NodeB >> {NodeC >> NodeD}, NodeE, NodeF}
129
+
130
+ # TEST that all nodes from unused_graphs are collected
131
+ unused_graphs = set(TestWorkflow.get_unused_nodes())
132
+ assert unused_graphs == {NodeB, NodeC, NodeD, NodeE, NodeF}
133
+
134
+
135
+ def test_workflow__no_unused_nodes():
136
+ class NodeA(BaseNode):
137
+ pass
138
+
139
+ class NodeB(BaseNode):
140
+ pass
141
+
142
+ # WHEN we create a workflow with no unused nodes
143
+ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
144
+ graph = NodeA >> NodeB
145
+
146
+ # TEST that nodes not in the graph are empty
147
+ nodes = set(TestWorkflow.get_unused_nodes())
148
+ assert nodes == set()
149
+
150
+
151
+ def test_workflow__node_in_both_graph_and_unused():
152
+ class NodeA(BaseNode):
153
+ pass
154
+
155
+ class NodeB(BaseNode):
156
+ pass
157
+
158
+ class NodeC(BaseNode):
159
+ pass
160
+
161
+ # WHEN we try to create a workflow where NodeA appears in both graph and unused
162
+ with pytest.raises(ValueError) as exc_info:
163
+
164
+ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
165
+ graph = NodeA >> NodeB
166
+ unused_graphs = {NodeA >> NodeC}
167
+
168
+ # THEN it should raise an error
169
+ assert "Node(s) NodeA cannot appear in both graph and unused_graphs" in str(exc_info.value)
170
+
171
+
172
+ def test_workflow__get_unused_edges():
173
+ """
174
+ Test that get_unused_edges correctly identifies edges that are defined but not used in the workflow graph.
175
+ """
176
+
177
+ class NodeA(BaseNode):
178
+ pass
179
+
180
+ class NodeB(BaseNode):
181
+ pass
182
+
183
+ class NodeC(BaseNode):
184
+ pass
185
+
186
+ class NodeD(BaseNode):
187
+ pass
188
+
189
+ class NodeE(BaseNode):
190
+ pass
191
+
192
+ class NodeF(BaseNode):
193
+ pass
194
+
195
+ class NodeG(BaseNode):
196
+ pass
197
+
198
+ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
199
+ graph = NodeA >> NodeB
200
+ unused_graphs = {NodeC >> {NodeD >> NodeE, NodeF} >> NodeG}
201
+
202
+ edge_c_to_d = Edge(from_port=NodeC.Ports.default, to_node=NodeD)
203
+ edge_c_to_f = Edge(from_port=NodeC.Ports.default, to_node=NodeF)
204
+ edge_d_to_e = Edge(from_port=NodeD.Ports.default, to_node=NodeE)
205
+ edge_e_to_g = Edge(from_port=NodeE.Ports.default, to_node=NodeG)
206
+ edge_f_to_g = Edge(from_port=NodeF.Ports.default, to_node=NodeG)
207
+
208
+ # Collect unused edges
209
+ unused_edges = set(TestWorkflow.get_unused_edges())
210
+
211
+ # Expected unused edges
212
+ expected_unused_edges = {edge_c_to_d, edge_c_to_f, edge_d_to_e, edge_e_to_g, edge_f_to_g}
213
+
214
+ # TEST that unused edges are correctly identified
215
+ assert unused_edges == expected_unused_edges, f"Expected {expected_unused_edges}, but got {unused_edges}"
@@ -0,0 +1,60 @@
1
+ from uuid import UUID, uuid4
2
+
3
+ from vellum.workflows import BaseWorkflow
4
+ from vellum.workflows.context import execution_context, get_execution_context
5
+ from vellum.workflows.events.types import NodeParentContext, WorkflowParentContext
6
+ from vellum.workflows.inputs import BaseInputs
7
+ from vellum.workflows.nodes import BaseNode
8
+ from vellum.workflows.references import VellumSecretReference
9
+ from vellum.workflows.state import BaseState
10
+
11
+
12
+ class MockInputs(BaseInputs):
13
+ foo: str
14
+
15
+
16
+ class MockNode(BaseNode):
17
+ node_foo = MockInputs.foo
18
+ node_secret = VellumSecretReference("secret")
19
+
20
+ class Outputs(BaseNode.Outputs):
21
+ example: str
22
+
23
+
24
+ class MockWorkflow(BaseWorkflow[MockInputs, BaseState]):
25
+ graph = MockNode
26
+
27
+
28
+ def test_context_trace_and_parent():
29
+ trace_id = uuid4()
30
+ parent_context = NodeParentContext(
31
+ node_definition=MockNode,
32
+ span_id=UUID("123e4567-e89b-12d3-a456-426614174000"),
33
+ parent=WorkflowParentContext(
34
+ workflow_definition=MockWorkflow,
35
+ span_id=UUID("123e4567-e89b-12d3-a456-426614174000"),
36
+ ),
37
+ )
38
+ second_parent_context = WorkflowParentContext(
39
+ workflow_definition=MockWorkflow, span_id=uuid4(), parent=parent_context
40
+ )
41
+ # When using execution context , if we set trace id within
42
+ with execution_context(parent_context=parent_context, trace_id=trace_id):
43
+ test = get_execution_context()
44
+ assert test.trace_id == trace_id
45
+ assert test.parent_context == parent_context
46
+ with execution_context(parent_context=second_parent_context):
47
+ test1 = get_execution_context()
48
+ assert test1.trace_id == trace_id
49
+ assert test1.parent_context == second_parent_context
50
+ # then we can assume trace id will not change
51
+ with execution_context(trace_id=uuid4()):
52
+ test3 = get_execution_context()
53
+ assert test3.trace_id == trace_id
54
+ with execution_context(parent_context=parent_context, trace_id=uuid4()):
55
+ test3 = get_execution_context()
56
+ assert test3.trace_id == trace_id
57
+ # and if we have a new context, the trace will differ
58
+ with execution_context(parent_context=parent_context, trace_id=uuid4()):
59
+ test = get_execution_context()
60
+ assert test.trace_id != trace_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.3
3
+ Version: 0.14.5
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0