vellum-ai 0.14.6__py3-none-any.whl → 0.14.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.
- vellum/__init__.py +14 -0
- vellum/client/core/client_wrapper.py +1 -1
- vellum/client/types/__init__.py +14 -0
- vellum/client/types/array_chat_message_content_item.py +6 -1
- vellum/client/types/array_chat_message_content_item_request.py +2 -0
- vellum/client/types/chat_message_content.py +2 -0
- vellum/client/types/chat_message_content_request.py +2 -0
- vellum/client/types/document_chat_message_content.py +25 -0
- vellum/client/types/document_chat_message_content_request.py +25 -0
- vellum/client/types/document_prompt_block.py +29 -0
- vellum/client/types/document_vellum_value.py +25 -0
- vellum/client/types/document_vellum_value_request.py +25 -0
- vellum/client/types/prompt_block.py +2 -0
- vellum/client/types/vellum_document.py +20 -0
- vellum/client/types/vellum_document_request.py +20 -0
- vellum/client/types/vellum_value.py +2 -0
- vellum/client/types/vellum_value_request.py +2 -0
- vellum/client/types/vellum_variable_type.py +1 -0
- vellum/types/document_chat_message_content.py +3 -0
- vellum/types/document_chat_message_content_request.py +3 -0
- vellum/types/document_prompt_block.py +3 -0
- vellum/types/document_vellum_value.py +3 -0
- vellum/types/document_vellum_value_request.py +3 -0
- vellum/types/vellum_document.py +3 -0
- vellum/types/vellum_document_request.py +3 -0
- vellum/workflows/exceptions.py +18 -0
- vellum/workflows/inputs/base.py +27 -1
- vellum/workflows/inputs/tests/__init__.py +0 -0
- vellum/workflows/inputs/tests/test_inputs.py +49 -0
- vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -1
- vellum/workflows/nodes/core/map_node/node.py +7 -7
- vellum/workflows/nodes/core/try_node/node.py +1 -1
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +33 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
- vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +2 -2
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +5 -3
- vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +5 -4
- vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py +4 -4
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +39 -15
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/tests/test_node.py +142 -0
- vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +3 -1
- vellum/workflows/outputs/base.py +1 -1
- vellum/workflows/runner/runner.py +16 -10
- vellum/workflows/state/context.py +7 -7
- vellum/workflows/workflows/base.py +16 -5
- vellum/workflows/workflows/tests/test_base_workflow.py +131 -40
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/METADATA +1 -1
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/RECORD +65 -47
- vellum_cli/__init__.py +43 -0
- vellum_cli/config.py +1 -0
- vellum_cli/init.py +132 -0
- vellum_cli/pull.py +7 -3
- vellum_cli/tests/test_init.py +473 -0
- vellum_cli/tests/test_pull.py +135 -0
- vellum_cli/tests/test_push.py +1 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +4 -4
- vellum_ee/workflows/display/tests/test_vellum_workflow_display.py +83 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +118 -3
- vellum_ee/workflows/display/vellum.py +0 -4
- vellum_ee/workflows/display/workflows/base_workflow_display.py +46 -13
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +29 -0
- vellum_ee/workflows/display/workflows/vellum_workflow_display.py +12 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/LICENSE +0 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/WHEEL +0 -0
- {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
from uuid import UUID
|
2
|
+
from typing import Dict
|
2
3
|
|
3
4
|
from vellum.workflows.inputs import BaseInputs
|
4
5
|
from vellum.workflows.nodes import BaseNode
|
@@ -145,3 +146,85 @@ def test_vellum_workflow_display_serialize_valid_handle_ids_for_base_nodes():
|
|
145
146
|
assert (
|
146
147
|
node["trigger"]["id"] in edge_target_handle_ids
|
147
148
|
), f"Trigger {node['trigger']['id']} from node {node['label']} not found in edge target handle ids"
|
149
|
+
|
150
|
+
|
151
|
+
def test_vellum_workflow_display__serialize_with_unused_nodes_and_edges():
|
152
|
+
# GIVEN a workflow with active and unused nodes
|
153
|
+
class NodeA(BaseNode):
|
154
|
+
class Outputs(BaseNode.Outputs):
|
155
|
+
result: str
|
156
|
+
|
157
|
+
class NodeB(BaseNode):
|
158
|
+
pass
|
159
|
+
|
160
|
+
class NodeC(BaseNode):
|
161
|
+
pass
|
162
|
+
|
163
|
+
# AND A workflow that uses them correctly
|
164
|
+
class Workflow(BaseWorkflow):
|
165
|
+
graph = NodeA
|
166
|
+
unused_graphs = {NodeB >> NodeC}
|
167
|
+
|
168
|
+
class Outputs(BaseWorkflow.Outputs):
|
169
|
+
final = NodeA.Outputs.result
|
170
|
+
|
171
|
+
# WHEN we serialize it
|
172
|
+
workflow_display = get_workflow_display(
|
173
|
+
base_display_class=VellumWorkflowDisplay,
|
174
|
+
workflow_class=Workflow,
|
175
|
+
)
|
176
|
+
|
177
|
+
# WHEN we serialize the workflow
|
178
|
+
exec_config = workflow_display.serialize()
|
179
|
+
|
180
|
+
# THEN the serialized workflow contains the expected nodes and edges
|
181
|
+
raw_data = exec_config["workflow_raw_data"]
|
182
|
+
assert isinstance(raw_data, dict)
|
183
|
+
|
184
|
+
nodes = raw_data["nodes"]
|
185
|
+
edges = raw_data["edges"]
|
186
|
+
|
187
|
+
assert isinstance(nodes, list)
|
188
|
+
assert isinstance(edges, list)
|
189
|
+
|
190
|
+
# Find nodes by their definition name
|
191
|
+
node_ids: Dict[str, str] = {}
|
192
|
+
|
193
|
+
for node in nodes:
|
194
|
+
assert isinstance(node, dict)
|
195
|
+
definition = node.get("definition")
|
196
|
+
if definition is None:
|
197
|
+
continue
|
198
|
+
|
199
|
+
assert isinstance(definition, dict)
|
200
|
+
name = definition.get("name")
|
201
|
+
if not isinstance(name, str):
|
202
|
+
continue
|
203
|
+
|
204
|
+
if name in ["NodeA", "NodeB", "NodeC"]:
|
205
|
+
node_id = node.get("id")
|
206
|
+
if isinstance(node_id, str):
|
207
|
+
node_ids[name] = node_id
|
208
|
+
|
209
|
+
# Verify all nodes are present
|
210
|
+
assert "NodeA" in node_ids, "Active node NodeA not found in serialized output"
|
211
|
+
assert "NodeB" in node_ids, "Unused node NodeB not found in serialized output"
|
212
|
+
assert "NodeC" in node_ids, "Unused node NodeC not found in serialized output"
|
213
|
+
|
214
|
+
# Verify the edge between NodeB and NodeC is present
|
215
|
+
edge_found = False
|
216
|
+
for edge in edges:
|
217
|
+
assert isinstance(edge, dict)
|
218
|
+
source_id = edge.get("source_node_id")
|
219
|
+
target_id = edge.get("target_node_id")
|
220
|
+
|
221
|
+
if (
|
222
|
+
isinstance(source_id, str)
|
223
|
+
and isinstance(target_id, str)
|
224
|
+
and source_id == node_ids["NodeB"]
|
225
|
+
and target_id == node_ids["NodeC"]
|
226
|
+
):
|
227
|
+
edge_found = True
|
228
|
+
break
|
229
|
+
|
230
|
+
assert edge_found, "Edge between unused nodes NodeB and NodeC not found in serialized output"
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import pytest
|
2
1
|
from uuid import uuid4
|
3
2
|
|
4
3
|
from deepdiff import DeepDiff
|
@@ -217,7 +216,6 @@ def test_serialize_node__try(serialize_node):
|
|
217
216
|
)
|
218
217
|
|
219
218
|
|
220
|
-
@pytest.mark.skip(reason="Not implemented")
|
221
219
|
def test_serialize_node__stacked():
|
222
220
|
@TryNode.wrap()
|
223
221
|
@RetryNode.wrap(max_attempts=5)
|
@@ -236,4 +234,121 @@ def test_serialize_node__stacked():
|
|
236
234
|
exec_config = workflow_display.serialize()
|
237
235
|
|
238
236
|
# THEN the workflow display is created successfully
|
239
|
-
assert
|
237
|
+
assert not DeepDiff(
|
238
|
+
{
|
239
|
+
"workflow_raw_data": {
|
240
|
+
"nodes": [
|
241
|
+
{
|
242
|
+
"id": "c14c1c9b-a7a4-4d2c-84fb-c940cfb09525",
|
243
|
+
"type": "ENTRYPOINT",
|
244
|
+
"inputs": [],
|
245
|
+
"data": {
|
246
|
+
"label": "Entrypoint Node",
|
247
|
+
"source_handle_id": "51a5eb25-af14-4bee-9ced-d2aa534ea8e9",
|
248
|
+
},
|
249
|
+
"display_data": {"position": {"x": 0.0, "y": 0.0}},
|
250
|
+
"base": None,
|
251
|
+
"definition": None,
|
252
|
+
},
|
253
|
+
{
|
254
|
+
"id": "074833b0-e142-4bbc-8dec-209a35e178a3",
|
255
|
+
"label": "test_serialize_node__stacked.<locals>.InnerStackedGenericNode",
|
256
|
+
"type": "GENERIC",
|
257
|
+
"display_data": {"position": {"x": 0.0, "y": 0.0}},
|
258
|
+
"base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
|
259
|
+
"definition": {
|
260
|
+
"name": "InnerStackedGenericNode",
|
261
|
+
"module": [
|
262
|
+
"vellum_ee",
|
263
|
+
"workflows",
|
264
|
+
"display",
|
265
|
+
"tests",
|
266
|
+
"workflow_serialization",
|
267
|
+
"generic_nodes",
|
268
|
+
"test_adornments_serialization",
|
269
|
+
],
|
270
|
+
},
|
271
|
+
"trigger": {"id": "f206358d-04a5-41c9-beee-0871a074fa48", "merge_behavior": "AWAIT_ATTRIBUTES"},
|
272
|
+
"ports": [{"id": "408cd5fb-3a3e-4eb2-9889-61111bd6a129", "name": "default", "type": "DEFAULT"}],
|
273
|
+
"adornments": [
|
274
|
+
{
|
275
|
+
"id": "5be7d260-74f7-4734-b31b-a46a94539586",
|
276
|
+
"label": "RetryNode",
|
277
|
+
"base": {
|
278
|
+
"name": "RetryNode",
|
279
|
+
"module": ["vellum", "workflows", "nodes", "core", "retry_node", "node"],
|
280
|
+
},
|
281
|
+
"attributes": [
|
282
|
+
{
|
283
|
+
"id": "c91782e3-140f-4938-9c23-d2a7b85dcdd8",
|
284
|
+
"name": "retry_on_error_code",
|
285
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
|
286
|
+
},
|
287
|
+
{
|
288
|
+
"id": "f388e93b-8c68-4f54-8577-bbd0c9091557",
|
289
|
+
"name": "max_attempts",
|
290
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 5}},
|
291
|
+
},
|
292
|
+
{
|
293
|
+
"id": "8a07dc58-3fed-41d4-8ca6-31ee0bb86c61",
|
294
|
+
"name": "delay",
|
295
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
|
296
|
+
},
|
297
|
+
{
|
298
|
+
"id": "73a02e62-4535-4e1f-97b5-1264ca8b1d71",
|
299
|
+
"name": "retry_on_condition",
|
300
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
|
301
|
+
},
|
302
|
+
],
|
303
|
+
},
|
304
|
+
{
|
305
|
+
"id": "3344083c-a32c-4a32-920b-0fb5093448fa",
|
306
|
+
"label": "TryNode",
|
307
|
+
"base": {
|
308
|
+
"name": "TryNode",
|
309
|
+
"module": ["vellum", "workflows", "nodes", "core", "try_node", "node"],
|
310
|
+
},
|
311
|
+
"attributes": [
|
312
|
+
{
|
313
|
+
"id": "ab2fbab0-e2a0-419b-b1ef-ce11ecf11e90",
|
314
|
+
"name": "on_error_code",
|
315
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
|
316
|
+
}
|
317
|
+
],
|
318
|
+
},
|
319
|
+
],
|
320
|
+
"attributes": [],
|
321
|
+
"outputs": [],
|
322
|
+
},
|
323
|
+
],
|
324
|
+
"edges": [
|
325
|
+
{
|
326
|
+
"id": "e8bd50dd-37a0-49b0-8b7b-f1dd8eb478b9",
|
327
|
+
"source_node_id": "c14c1c9b-a7a4-4d2c-84fb-c940cfb09525",
|
328
|
+
"source_handle_id": "51a5eb25-af14-4bee-9ced-d2aa534ea8e9",
|
329
|
+
"target_node_id": "074833b0-e142-4bbc-8dec-209a35e178a3",
|
330
|
+
"target_handle_id": "f206358d-04a5-41c9-beee-0871a074fa48",
|
331
|
+
"type": "DEFAULT",
|
332
|
+
}
|
333
|
+
],
|
334
|
+
"display_data": {"viewport": {"x": 0.0, "y": 0.0, "zoom": 1.0}},
|
335
|
+
"definition": {
|
336
|
+
"name": "StackedWorkflow",
|
337
|
+
"module": [
|
338
|
+
"vellum_ee",
|
339
|
+
"workflows",
|
340
|
+
"display",
|
341
|
+
"tests",
|
342
|
+
"workflow_serialization",
|
343
|
+
"generic_nodes",
|
344
|
+
"test_adornments_serialization",
|
345
|
+
],
|
346
|
+
},
|
347
|
+
},
|
348
|
+
"input_variables": [],
|
349
|
+
"state_variables": [],
|
350
|
+
"output_variables": [],
|
351
|
+
},
|
352
|
+
exec_config,
|
353
|
+
ignore_order=True,
|
354
|
+
)
|
@@ -41,10 +41,6 @@ class NodeDisplayData(UniversalBaseModel):
|
|
41
41
|
comment: Optional[NodeDisplayComment] = None
|
42
42
|
|
43
43
|
|
44
|
-
class GenericNodeDisplayData(UniversalBaseModel):
|
45
|
-
position: NodeDisplayPosition = Field(default_factory=NodeDisplayPosition)
|
46
|
-
|
47
|
-
|
48
44
|
class CodeResourceDefinition(UniversalBaseModel):
|
49
45
|
name: str
|
50
46
|
module: List[str]
|
@@ -217,23 +217,30 @@ class BaseWorkflowDisplay(
|
|
217
217
|
# TODO: We should still serialize nodes that are in the workflow's directory but aren't used in the graph.
|
218
218
|
# https://app.shortcut.com/vellum/story/5394
|
219
219
|
for node in self._workflow.get_nodes():
|
220
|
-
|
220
|
+
extracted_node_displays = self._extract_node_displays(node)
|
221
221
|
|
222
|
-
|
223
|
-
|
222
|
+
for extracted_node, extracted_node_display in extracted_node_displays.items():
|
223
|
+
if extracted_node not in node_displays:
|
224
|
+
node_displays[extracted_node] = extracted_node_display
|
224
225
|
|
225
|
-
|
226
|
-
|
226
|
+
if extracted_node not in global_node_displays:
|
227
|
+
global_node_displays[extracted_node] = extracted_node_display
|
227
228
|
|
228
|
-
|
229
|
-
|
230
|
-
if inner_node:
|
231
|
-
inner_node_display = self._get_node_display(inner_node)
|
232
|
-
node_displays[inner_node] = inner_node_display
|
233
|
-
global_node_displays[inner_node] = inner_node_display
|
229
|
+
self._enrich_global_node_output_displays(node, extracted_node_displays[node], global_node_output_displays)
|
230
|
+
self._enrich_node_port_displays(node, extracted_node_displays[node], port_displays)
|
234
231
|
|
235
|
-
|
236
|
-
self.
|
232
|
+
for node in self._workflow.get_unused_nodes():
|
233
|
+
extracted_node_displays = self._extract_node_displays(node)
|
234
|
+
|
235
|
+
for extracted_node, extracted_node_display in extracted_node_displays.items():
|
236
|
+
if extracted_node not in node_displays:
|
237
|
+
node_displays[extracted_node] = extracted_node_display
|
238
|
+
|
239
|
+
if extracted_node not in global_node_displays:
|
240
|
+
global_node_displays[extracted_node] = extracted_node_display
|
241
|
+
|
242
|
+
self._enrich_global_node_output_displays(node, extracted_node_displays[node], global_node_output_displays)
|
243
|
+
self._enrich_node_port_displays(node, extracted_node_displays[node], port_displays)
|
237
244
|
|
238
245
|
workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = {}
|
239
246
|
# If we're dealing with a nested workflow, then it should have access to the inputs of its parents.
|
@@ -280,6 +287,15 @@ class BaseWorkflowDisplay(
|
|
280
287
|
edge, node_displays, port_displays, overrides=edge_display_overrides
|
281
288
|
)
|
282
289
|
|
290
|
+
for edge in self._workflow.get_unused_edges():
|
291
|
+
if edge in edge_displays:
|
292
|
+
continue
|
293
|
+
|
294
|
+
edge_display_overrides = self.edge_displays.get((edge.from_port, edge.to_node))
|
295
|
+
edge_displays[(edge.from_port, edge.to_node)] = self._generate_edge_display(
|
296
|
+
edge, node_displays, port_displays, overrides=edge_display_overrides
|
297
|
+
)
|
298
|
+
|
283
299
|
workflow_output_displays: Dict[BaseDescriptor, WorkflowOutputDisplay] = {}
|
284
300
|
for workflow_output in self._workflow.Outputs:
|
285
301
|
if workflow_output in workflow_output_displays:
|
@@ -409,3 +425,20 @@ class BaseWorkflowDisplay(
|
|
409
425
|
node_displays=temp_node_displays,
|
410
426
|
)
|
411
427
|
return display_meta
|
428
|
+
|
429
|
+
def _extract_node_displays(self, node: Type[BaseNode]) -> Dict[Type[BaseNode], NodeDisplayType]:
|
430
|
+
node_display = self._get_node_display(node)
|
431
|
+
additional_node_displays: Dict[Type[BaseNode], NodeDisplayType] = {
|
432
|
+
node: node_display,
|
433
|
+
}
|
434
|
+
|
435
|
+
# Nodes wrapped in a decorator need to be in our node display dictionary for later retrieval
|
436
|
+
inner_node = get_wrapped_node(node)
|
437
|
+
if inner_node:
|
438
|
+
inner_node_displays = self._extract_node_displays(inner_node)
|
439
|
+
|
440
|
+
for node, display in inner_node_displays.items():
|
441
|
+
if node not in additional_node_displays:
|
442
|
+
additional_node_displays[node] = display
|
443
|
+
|
444
|
+
return additional_node_displays
|
@@ -2,6 +2,8 @@ import pytest
|
|
2
2
|
|
3
3
|
from vellum.workflows.nodes.bases.base import BaseNode
|
4
4
|
from vellum.workflows.workflows.base import BaseWorkflow
|
5
|
+
from vellum_ee.workflows.display.nodes import BaseNodeDisplay
|
6
|
+
from vellum_ee.workflows.display.vellum import NodeDisplayData, NodeDisplayPosition
|
5
7
|
from vellum_ee.workflows.display.workflows import VellumWorkflowDisplay
|
6
8
|
from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
|
7
9
|
|
@@ -63,3 +65,30 @@ def test_serialize_workflow__workflow_outputs_reference_non_node_outputs():
|
|
63
65
|
== """Failed to serialize output 'final': Reference to outputs \
|
64
66
|
'test_serialize_workflow__workflow_outputs_reference_non_node_outputs.<locals>.FirstWorkflow.Outputs' is invalid."""
|
65
67
|
)
|
68
|
+
|
69
|
+
|
70
|
+
def test_serialize_workflow__node_display_class_not_registered():
|
71
|
+
# GIVEN a workflow with a node that has a display class referencing display data
|
72
|
+
class StartNode(BaseNode):
|
73
|
+
class Outputs(BaseNode.Outputs):
|
74
|
+
result: str
|
75
|
+
|
76
|
+
class StartNodeDisplay(BaseNodeDisplay[StartNode]):
|
77
|
+
node_input_ids_by_name = {}
|
78
|
+
display_data = NodeDisplayData(position=NodeDisplayPosition(x=0, y=0), width=None, height=None)
|
79
|
+
|
80
|
+
class MyWorkflow(BaseWorkflow):
|
81
|
+
graph = StartNode
|
82
|
+
|
83
|
+
class Outputs(BaseWorkflow.Outputs):
|
84
|
+
answer = StartNode.Outputs.result
|
85
|
+
|
86
|
+
# WHEN we serialize it
|
87
|
+
workflow_display = get_workflow_display(
|
88
|
+
base_display_class=VellumWorkflowDisplay,
|
89
|
+
workflow_class=MyWorkflow,
|
90
|
+
)
|
91
|
+
data = workflow_display.serialize()
|
92
|
+
|
93
|
+
# THEN it should should succeed
|
94
|
+
assert data is not None
|
@@ -126,6 +126,18 @@ class VellumWorkflowDisplay(
|
|
126
126
|
|
127
127
|
nodes.append(serialized_node)
|
128
128
|
|
129
|
+
# Add all unused nodes in the workflow
|
130
|
+
for node in self._workflow.get_unused_nodes():
|
131
|
+
node_display = self.display_context.node_displays[node]
|
132
|
+
|
133
|
+
try:
|
134
|
+
serialized_node = node_display.serialize(self.display_context)
|
135
|
+
except NotImplementedError as e:
|
136
|
+
self.add_error(e)
|
137
|
+
continue
|
138
|
+
|
139
|
+
nodes.append(serialized_node)
|
140
|
+
|
129
141
|
synthetic_output_edges: JsonArray = []
|
130
142
|
output_variables: JsonArray = []
|
131
143
|
final_output_nodes = [
|
File without changes
|
File without changes
|
File without changes
|