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.
Files changed (65) hide show
  1. vellum/__init__.py +14 -0
  2. vellum/client/core/client_wrapper.py +1 -1
  3. vellum/client/types/__init__.py +14 -0
  4. vellum/client/types/array_chat_message_content_item.py +6 -1
  5. vellum/client/types/array_chat_message_content_item_request.py +2 -0
  6. vellum/client/types/chat_message_content.py +2 -0
  7. vellum/client/types/chat_message_content_request.py +2 -0
  8. vellum/client/types/document_chat_message_content.py +25 -0
  9. vellum/client/types/document_chat_message_content_request.py +25 -0
  10. vellum/client/types/document_prompt_block.py +29 -0
  11. vellum/client/types/document_vellum_value.py +25 -0
  12. vellum/client/types/document_vellum_value_request.py +25 -0
  13. vellum/client/types/prompt_block.py +2 -0
  14. vellum/client/types/vellum_document.py +20 -0
  15. vellum/client/types/vellum_document_request.py +20 -0
  16. vellum/client/types/vellum_value.py +2 -0
  17. vellum/client/types/vellum_value_request.py +2 -0
  18. vellum/client/types/vellum_variable_type.py +1 -0
  19. vellum/types/document_chat_message_content.py +3 -0
  20. vellum/types/document_chat_message_content_request.py +3 -0
  21. vellum/types/document_prompt_block.py +3 -0
  22. vellum/types/document_vellum_value.py +3 -0
  23. vellum/types/document_vellum_value_request.py +3 -0
  24. vellum/types/vellum_document.py +3 -0
  25. vellum/types/vellum_document_request.py +3 -0
  26. vellum/workflows/exceptions.py +18 -0
  27. vellum/workflows/inputs/base.py +27 -1
  28. vellum/workflows/inputs/tests/__init__.py +0 -0
  29. vellum/workflows/inputs/tests/test_inputs.py +49 -0
  30. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -1
  31. vellum/workflows/nodes/core/map_node/node.py +7 -7
  32. vellum/workflows/nodes/core/try_node/node.py +1 -1
  33. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +33 -0
  34. vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
  35. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +2 -2
  36. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +5 -3
  37. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +5 -4
  38. vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py +4 -4
  39. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +39 -15
  40. vellum/workflows/nodes/displayable/subworkflow_deployment_node/tests/test_node.py +142 -0
  41. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +3 -1
  42. vellum/workflows/outputs/base.py +1 -1
  43. vellum/workflows/runner/runner.py +16 -10
  44. vellum/workflows/state/context.py +7 -7
  45. vellum/workflows/workflows/base.py +16 -5
  46. vellum/workflows/workflows/tests/test_base_workflow.py +131 -40
  47. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/METADATA +1 -1
  48. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/RECORD +65 -47
  49. vellum_cli/__init__.py +43 -0
  50. vellum_cli/config.py +1 -0
  51. vellum_cli/init.py +132 -0
  52. vellum_cli/pull.py +7 -3
  53. vellum_cli/tests/test_init.py +473 -0
  54. vellum_cli/tests/test_pull.py +135 -0
  55. vellum_cli/tests/test_push.py +1 -0
  56. vellum_ee/workflows/display/nodes/base_node_display.py +4 -4
  57. vellum_ee/workflows/display/tests/test_vellum_workflow_display.py +83 -0
  58. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +118 -3
  59. vellum_ee/workflows/display/vellum.py +0 -4
  60. vellum_ee/workflows/display/workflows/base_workflow_display.py +46 -13
  61. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +29 -0
  62. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +12 -0
  63. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/LICENSE +0 -0
  64. {vellum_ai-0.14.6.dist-info → vellum_ai-0.14.8.dist-info}/WHEEL +0 -0
  65. {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 exec_config is not None
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
- node_display = self._get_node_display(node)
220
+ extracted_node_displays = self._extract_node_displays(node)
221
221
 
222
- if node not in node_displays:
223
- node_displays[node] = node_display
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
- if node not in global_node_displays:
226
- global_node_displays[node] = node_display
226
+ if extracted_node not in global_node_displays:
227
+ global_node_displays[extracted_node] = extracted_node_display
227
228
 
228
- # Nodes wrapped in a decorator need to be in our node display dictionary for later retrieval
229
- inner_node = get_wrapped_node(node)
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
- self._enrich_global_node_output_displays(node, node_display, global_node_output_displays)
236
- self._enrich_node_port_displays(node, node_display, port_displays)
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 = [