vellum-ai 0.14.7__py3-none-any.whl → 0.14.9__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 (44) hide show
  1. vellum/__init__.py +2 -0
  2. vellum/client/core/client_wrapper.py +1 -1
  3. vellum/client/types/__init__.py +2 -0
  4. vellum/client/types/document_prompt_block.py +29 -0
  5. vellum/client/types/prompt_block.py +2 -0
  6. vellum/types/document_prompt_block.py +3 -0
  7. vellum/workflows/descriptors/base.py +6 -0
  8. vellum/workflows/descriptors/tests/test_utils.py +14 -0
  9. vellum/workflows/events/tests/test_event.py +40 -0
  10. vellum/workflows/events/workflow.py +20 -1
  11. vellum/workflows/expressions/greater_than.py +15 -8
  12. vellum/workflows/expressions/greater_than_or_equal_to.py +14 -8
  13. vellum/workflows/expressions/less_than.py +14 -8
  14. vellum/workflows/expressions/less_than_or_equal_to.py +14 -8
  15. vellum/workflows/expressions/parse_json.py +30 -0
  16. vellum/workflows/expressions/tests/__init__.py +0 -0
  17. vellum/workflows/expressions/tests/test_expressions.py +310 -0
  18. vellum/workflows/expressions/tests/test_parse_json.py +31 -0
  19. vellum/workflows/nodes/bases/base.py +5 -2
  20. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +34 -2
  21. vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
  22. vellum/workflows/nodes/displayable/code_execution_node/node.py +18 -8
  23. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +53 -0
  24. vellum/workflows/runner/runner.py +33 -4
  25. vellum/workflows/state/encoder.py +2 -1
  26. {vellum_ai-0.14.7.dist-info → vellum_ai-0.14.9.dist-info}/METADATA +1 -1
  27. {vellum_ai-0.14.7.dist-info → vellum_ai-0.14.9.dist-info}/RECORD +44 -38
  28. vellum_cli/__init__.py +9 -2
  29. vellum_cli/config.py +1 -0
  30. vellum_cli/init.py +6 -2
  31. vellum_cli/pull.py +1 -0
  32. vellum_cli/tests/test_init.py +194 -76
  33. vellum_cli/tests/test_pull.py +8 -0
  34. vellum_cli/tests/test_push.py +1 -0
  35. vellum_ee/workflows/display/nodes/base_node_display.py +4 -0
  36. vellum_ee/workflows/display/tests/test_vellum_workflow_display.py +114 -0
  37. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +118 -3
  38. vellum_ee/workflows/display/types.py +1 -14
  39. vellum_ee/workflows/display/workflows/base_workflow_display.py +48 -19
  40. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +12 -0
  41. vellum_ee/workflows/tests/test_server.py +1 -0
  42. {vellum_ai-0.14.7.dist-info → vellum_ai-0.14.9.dist-info}/LICENSE +0 -0
  43. {vellum_ai-0.14.7.dist-info → vellum_ai-0.14.9.dist-info}/WHEEL +0 -0
  44. {vellum_ai-0.14.7.dist-info → vellum_ai-0.14.9.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
1
- import pytest
2
1
  import io
3
2
  import json
4
3
  import os
@@ -33,14 +32,7 @@ class MockTemplate:
33
32
  self.label = label
34
33
 
35
34
 
36
- @pytest.mark.parametrize(
37
- "base_command",
38
- [
39
- ["workflows", "init"],
40
- ],
41
- ids=["workflows_init"],
42
- )
43
- def test_init_command(vellum_client, mock_module, base_command):
35
+ def test_init_command(vellum_client, mock_module):
44
36
  # GIVEN a module on the user's filesystem
45
37
  temp_dir = mock_module.temp_dir
46
38
  mock_module.set_pyproject_toml({"workflows": []})
@@ -52,18 +44,11 @@ def test_init_command(vellum_client, mock_module, base_command):
52
44
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
53
45
 
54
46
  # AND the workflow pull API call returns a zip file
55
- vellum_client.workflows.pull.return_value = iter(
56
- [
57
- _zip_file_map(
58
- {
59
- "workflow.py": "print('hello')",
60
- }
61
- )
62
- ]
63
- )
47
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
48
+
64
49
  # WHEN the user runs the `init` command and selects the first template
65
50
  runner = CliRunner()
66
- result = runner.invoke(cli_main, base_command, input="1\n")
51
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
67
52
 
68
53
  # THEN the command returns successfully
69
54
  assert result.exit_code == 0
@@ -94,18 +79,12 @@ def test_init_command(vellum_client, mock_module, base_command):
94
79
  "container_image_name": None,
95
80
  "container_image_tag": None,
96
81
  "workspace": "default",
82
+ "target_directory": None,
97
83
  }
98
84
  ]
99
85
 
100
86
 
101
- @pytest.mark.parametrize(
102
- "base_command",
103
- [
104
- ["workflows", "init"],
105
- ],
106
- ids=["workflows_init"],
107
- )
108
- def test_init_command__invalid_template_id(vellum_client, mock_module, base_command):
87
+ def test_init_command__invalid_template_id(vellum_client, mock_module):
109
88
  # GIVEN a module on the user's filesystem
110
89
  temp_dir = mock_module.temp_dir
111
90
  mock_module.set_pyproject_toml({"workflows": []})
@@ -121,7 +100,7 @@ def test_init_command__invalid_template_id(vellum_client, mock_module, base_comm
121
100
  # Mock click.prompt to raise a KeyboardInterrupt (simulating Ctrl+C)
122
101
  with patch("click.prompt", side_effect=KeyboardInterrupt):
123
102
  runner = CliRunner()
124
- result = runner.invoke(cli_main, base_command)
103
+ result = runner.invoke(cli_main, ["workflows", "init"])
125
104
 
126
105
  # THEN the command is aborted
127
106
  assert result.exit_code != 0
@@ -142,14 +121,7 @@ def test_init_command__invalid_template_id(vellum_client, mock_module, base_comm
142
121
  assert lock_data["workflows"] == []
143
122
 
144
123
 
145
- @pytest.mark.parametrize(
146
- "base_command",
147
- [
148
- ["workflows", "init"],
149
- ],
150
- ids=["workflows_init"],
151
- )
152
- def test_init_command__no_templates(vellum_client, mock_module, base_command):
124
+ def test_init_command__no_templates(vellum_client, mock_module):
153
125
  # GIVEN a module on the user's filesystem
154
126
  temp_dir = mock_module.temp_dir
155
127
  mock_module.set_pyproject_toml({"workflows": []})
@@ -158,7 +130,7 @@ def test_init_command__no_templates(vellum_client, mock_module, base_command):
158
130
 
159
131
  # WHEN the user runs the `init` command
160
132
  runner = CliRunner()
161
- result = runner.invoke(cli_main, base_command)
133
+ result = runner.invoke(cli_main, ["workflows", "init"])
162
134
 
163
135
  # THEN the command gracefully exits
164
136
  assert result.exit_code == 0
@@ -179,14 +151,7 @@ def test_init_command__no_templates(vellum_client, mock_module, base_command):
179
151
  assert lock_data["workflows"] == []
180
152
 
181
153
 
182
- @pytest.mark.parametrize(
183
- "base_command",
184
- [
185
- ["workflows", "init"],
186
- ],
187
- ids=["workflows_init"],
188
- )
189
- def test_init_command_target_directory_exists(vellum_client, mock_module, base_command):
154
+ def test_init_command_target_directory_exists(vellum_client, mock_module):
190
155
  """
191
156
  GIVEN a target directory already exists
192
157
  WHEN the user tries to run the `init` command
@@ -208,19 +173,11 @@ def test_init_command_target_directory_exists(vellum_client, mock_module, base_c
208
173
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
209
174
 
210
175
  # AND the workflow pull API call returns a zip file
211
- vellum_client.workflows.pull.return_value = iter(
212
- [
213
- _zip_file_map(
214
- {
215
- "workflow.py": "print('hello')",
216
- }
217
- )
218
- ]
219
- )
176
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
220
177
 
221
178
  # WHEN the user runs the `init` command and selects the template
222
179
  runner = CliRunner()
223
- result = runner.invoke(cli_main, base_command, input="1\n")
180
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
224
181
 
225
182
  # THEN the command should detect the existing directory and abort
226
183
  assert result.exit_code == 0
@@ -244,14 +201,7 @@ def test_init_command_target_directory_exists(vellum_client, mock_module, base_c
244
201
  assert lock_data["workflows"] == []
245
202
 
246
203
 
247
- @pytest.mark.parametrize(
248
- "base_command",
249
- [
250
- ["workflows", "init"],
251
- ],
252
- ids=["workflows_init"],
253
- )
254
- def test_init_command_with_template_name(vellum_client, mock_module, base_command):
204
+ def test_init_command_with_template_name(vellum_client, mock_module):
255
205
  # GIVEN a module on the user's filesystem
256
206
  temp_dir = mock_module.temp_dir
257
207
  mock_module.set_pyproject_toml({"workflows": []})
@@ -264,14 +214,12 @@ def test_init_command_with_template_name(vellum_client, mock_module, base_comman
264
214
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
265
215
 
266
216
  # AND the workflow pull API call returns a zip file
267
- vellum_client.workflows.pull.return_value = iter(
268
- [_zip_file_map({"workflow.py": "print('hello')", "README.md": "# Another Workflow\nThis is a test template."})]
269
- )
217
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
270
218
 
271
219
  # WHEN the user runs the `init` command with a specific template name
272
220
  template_name = snake_case("Another Workflow")
273
221
  runner = CliRunner()
274
- result = runner.invoke(cli_main, base_command + [template_name])
222
+ result = runner.invoke(cli_main, ["workflows", "init", template_name])
275
223
 
276
224
  # THEN the command returns successfully
277
225
  assert result.exit_code == 0
@@ -305,18 +253,12 @@ def test_init_command_with_template_name(vellum_client, mock_module, base_comman
305
253
  "container_image_name": None,
306
254
  "container_image_tag": None,
307
255
  "workspace": "default",
256
+ "target_directory": None,
308
257
  }
309
258
  ]
310
259
 
311
260
 
312
- @pytest.mark.parametrize(
313
- "base_command",
314
- [
315
- ["workflows", "init"],
316
- ],
317
- ids=["workflows_init"],
318
- )
319
- def test_init_command_with_nonexistent_template_name(vellum_client, mock_module, base_command):
261
+ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module):
320
262
  # GIVEN a module on the user's filesystem
321
263
  temp_dir = mock_module.temp_dir
322
264
  mock_module.set_pyproject_toml({"workflows": []})
@@ -331,7 +273,7 @@ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module,
331
273
  # WHEN the user runs the `init` command with a non-existent template name
332
274
  nonexistent_template = "nonexistent_template"
333
275
  runner = CliRunner()
334
- result = runner.invoke(cli_main, base_command + [nonexistent_template])
276
+ result = runner.invoke(cli_main, ["workflows", "init", nonexistent_template])
335
277
 
336
278
  # THEN the command should indicate the template was not found
337
279
  assert result.exit_code == 0
@@ -353,3 +295,179 @@ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module,
353
295
  with open(vellum_lock_json) as f:
354
296
  lock_data = json.load(f)
355
297
  assert lock_data["workflows"] == []
298
+
299
+
300
+ def test_init__with_target_dir(vellum_client, mock_module):
301
+ # GIVEN a module on the user's filesystem
302
+ temp_dir = mock_module.temp_dir
303
+ mock_module.set_pyproject_toml({"workflows": []})
304
+
305
+ # GIVEN the vellum client returns a list of template workflows
306
+ fake_templates = [
307
+ MockTemplate(id="template-1", label="Example Workflow"),
308
+ ]
309
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
310
+
311
+ # AND the workflow pull API call returns a zip file
312
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
313
+
314
+ # AND a target directory
315
+ target_dir = os.path.join(temp_dir, "dir")
316
+ os.makedirs(target_dir, exist_ok=True)
317
+
318
+ # WHEN the user runs the init command with target-dir
319
+ runner = CliRunner()
320
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", target_dir], input="1\n")
321
+
322
+ # THEN the command returns successfully
323
+ assert result.exit_code == 0
324
+
325
+ # AND the `workflow.py` file should be created in the target directory
326
+ module_path = os.path.join(target_dir, "example_workflow")
327
+ workflow_py = os.path.join(module_path, "workflow.py")
328
+ assert os.path.exists(workflow_py)
329
+ with open(workflow_py) as f:
330
+ assert f.read() == "print('hello')"
331
+
332
+ # AND the files are not in the default module directory
333
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
334
+ assert not os.path.exists(default_module_path)
335
+
336
+ # AND the vellum.lock.json file should be created in the original directory
337
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
338
+ assert os.path.exists(vellum_lock_json)
339
+ with open(vellum_lock_json) as f:
340
+ lock_data = json.load(f)
341
+ assert lock_data["workflows"] == [
342
+ {
343
+ "module": "example_workflow",
344
+ "workflow_sandbox_id": "template-1",
345
+ "ignore": None,
346
+ "deployments": [],
347
+ "container_image_name": None,
348
+ "container_image_tag": None,
349
+ "workspace": "default",
350
+ "target_directory": module_path,
351
+ }
352
+ ]
353
+
354
+
355
+ def test_init__with_nested_target_dir(vellum_client, mock_module):
356
+ # GIVEN a module on the user's filesystem
357
+ temp_dir = mock_module.temp_dir
358
+ mock_module.set_pyproject_toml({"workflows": []})
359
+
360
+ # GIVEN the vellum client returns a list of template workflows
361
+ fake_templates = [
362
+ MockTemplate(id="template-1", label="Example Workflow"),
363
+ ]
364
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
365
+
366
+ # AND the workflow pull API call returns a zip file
367
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
368
+
369
+ # AND a nested target directory that doesn't exist yet
370
+ nested_target_dir = os.path.join(temp_dir, "dir-1", "dir-2")
371
+
372
+ # WHEN the user runs the init command with nested target-dir
373
+ runner = CliRunner()
374
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", nested_target_dir], input="1\n")
375
+
376
+ # THEN the command returns successfully
377
+ assert result.exit_code == 0
378
+
379
+ # AND the nested directory with module subdirectory should be created
380
+ module_path = os.path.join(nested_target_dir, "example_workflow")
381
+ assert os.path.exists(module_path)
382
+
383
+ # AND the workflow.py file is written to the nested target directory
384
+ workflow_py = os.path.join(module_path, "workflow.py")
385
+ assert os.path.exists(workflow_py)
386
+ with open(workflow_py) as f:
387
+ assert f.read() == "print('hello')"
388
+
389
+ # AND the files are not in the default module directory
390
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
391
+ assert not os.path.exists(default_module_path)
392
+
393
+ # AND the vellum.lock.json file is still updated
394
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
395
+ assert os.path.exists(vellum_lock_json)
396
+ with open(vellum_lock_json) as f:
397
+ lock_data = json.load(f)
398
+ assert lock_data["workflows"] == [
399
+ {
400
+ "module": "example_workflow",
401
+ "workflow_sandbox_id": "template-1",
402
+ "ignore": None,
403
+ "deployments": [],
404
+ "container_image_name": None,
405
+ "container_image_tag": None,
406
+ "workspace": "default",
407
+ "target_directory": module_path,
408
+ }
409
+ ]
410
+
411
+
412
+ def test_init__with_template_name_and_target_dir(vellum_client, mock_module):
413
+ # GIVEN a module on the user's filesystem
414
+ temp_dir = mock_module.temp_dir
415
+ mock_module.set_pyproject_toml({"workflows": []})
416
+
417
+ # GIVEN the vellum client returns a list of template workflows
418
+ fake_templates = [
419
+ MockTemplate(id="template-1", label="Example Workflow"),
420
+ MockTemplate(id="template-2", label="Another Workflow"),
421
+ ]
422
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
423
+
424
+ # AND the workflow pull API call returns a zip file
425
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
426
+
427
+ # AND a target directory
428
+ target_dir = os.path.join(temp_dir, "dir")
429
+ os.makedirs(target_dir, exist_ok=True)
430
+
431
+ # WHEN the user runs the init command with a specific template name and target-dir
432
+ template_name = snake_case("Another Workflow")
433
+ runner = CliRunner()
434
+ result = runner.invoke(cli_main, ["workflows", "init", template_name, "--target-dir", target_dir])
435
+
436
+ # THEN the command returns successfully
437
+ assert result.exit_code == 0
438
+
439
+ # AND `vellum_client.workflows.pull` is called with the correct template ID
440
+ vellum_client.workflows.pull.assert_called_once_with(
441
+ "template-2", # ID of "Another Workflow"
442
+ request_options={"additional_query_parameters": {"include_sandbox": True}},
443
+ )
444
+
445
+ # AND the workflow files should be created in the target directory with the correct module subdirectory
446
+ module_path = os.path.join(target_dir, "another_workflow")
447
+ workflow_py = os.path.join(module_path, "workflow.py")
448
+ assert os.path.exists(workflow_py)
449
+ with open(workflow_py) as f:
450
+ assert f.read() == "print('hello')"
451
+
452
+ # AND the files are not in the default module directory
453
+ default_module_path = os.path.join(temp_dir, "another_workflow", "workflow.py")
454
+ assert not os.path.exists(default_module_path)
455
+
456
+ # AND the vellum.lock.json file should be created with the correct data
457
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
458
+ assert os.path.exists(vellum_lock_json)
459
+
460
+ with open(vellum_lock_json) as f:
461
+ lock_data = json.load(f)
462
+ assert lock_data["workflows"] == [
463
+ {
464
+ "module": "another_workflow",
465
+ "workflow_sandbox_id": "template-2",
466
+ "ignore": None,
467
+ "deployments": [],
468
+ "container_image_name": None,
469
+ "container_image_tag": None,
470
+ "workspace": "default",
471
+ "target_directory": module_path,
472
+ }
473
+ ]
@@ -73,6 +73,7 @@ def test_pull(vellum_client, mock_module, base_command):
73
73
  "ignore": None,
74
74
  "deployments": [],
75
75
  "workspace": "default",
76
+ "target_directory": None,
76
77
  }
77
78
  ],
78
79
  "workspaces": [],
@@ -167,6 +168,7 @@ def test_pull__with_target_dir(vellum_client, mock_module, base_command):
167
168
  "ignore": None,
168
169
  "deployments": [],
169
170
  "workspace": "default",
171
+ "target_directory": module_path,
170
172
  }
171
173
  ],
172
174
  "workspaces": [],
@@ -233,6 +235,7 @@ def test_pull__with_nested_target_dir(vellum_client, mock_module, base_command):
233
235
  "ignore": None,
234
236
  "deployments": [],
235
237
  "workspace": "default",
238
+ "target_directory": module_path,
236
239
  }
237
240
  ],
238
241
  "workspaces": [],
@@ -289,6 +292,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
289
292
  "container_image_tag": None,
290
293
  "container_image_name": None,
291
294
  "workspace": "default",
295
+ "target_directory": None,
292
296
  }
293
297
  ],
294
298
  }
@@ -372,6 +376,7 @@ def test_pull__workflow_deployment_with_no_config(vellum_client):
372
376
  "container_image_tag": None,
373
377
  "container_image_name": None,
374
378
  "workspace": "default",
379
+ "target_directory": None,
375
380
  }
376
381
  ],
377
382
  "workspaces": [],
@@ -619,6 +624,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
619
624
  "container_image_name": None,
620
625
  "container_image_tag": None,
621
626
  "workspace": "default",
627
+ "target_directory": None,
622
628
  },
623
629
  {
624
630
  "module": "super_cool_workflow",
@@ -628,6 +634,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
628
634
  "container_image_name": "test",
629
635
  "container_image_tag": "1.0",
630
636
  "workspace": "default",
637
+ "target_directory": None,
631
638
  },
632
639
  ]
633
640
 
@@ -771,6 +778,7 @@ def test_pull__module_not_in_config(vellum_client, mock_module):
771
778
  "container_image_name": None,
772
779
  "container_image_tag": None,
773
780
  "workspace": "default",
781
+ "target_directory": None,
774
782
  }
775
783
  ]
776
784
 
@@ -508,6 +508,7 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
508
508
  "container_image_tag": None,
509
509
  "deployments": [],
510
510
  "ignore": None,
511
+ "target_directory": None,
511
512
  }
512
513
 
513
514
 
@@ -27,6 +27,7 @@ from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpressi
27
27
  from vellum.workflows.expressions.is_null import IsNullExpression
28
28
  from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
29
29
  from vellum.workflows.expressions.not_between import NotBetweenExpression
30
+ from vellum.workflows.expressions.parse_json import ParseJsonExpression
30
31
  from vellum.workflows.nodes.bases.base import BaseNode
31
32
  from vellum.workflows.nodes.utils import get_wrapped_node
32
33
  from vellum.workflows.ports import Port
@@ -386,6 +387,9 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
386
387
  "node_id": str(node_class_display.node_id),
387
388
  }
388
389
 
390
+ if isinstance(value, ParseJsonExpression):
391
+ raise ValueError("ParseJsonExpression is not supported in the UI")
392
+
389
393
  if not isinstance(value, BaseDescriptor):
390
394
  vellum_value = primitive_to_vellum_value(value)
391
395
  return {
@@ -1,4 +1,6 @@
1
+ import pytest
1
2
  from uuid import UUID
3
+ from typing import Dict
2
4
 
3
5
  from vellum.workflows.inputs import BaseInputs
4
6
  from vellum.workflows.nodes import BaseNode
@@ -145,3 +147,115 @@ def test_vellum_workflow_display_serialize_valid_handle_ids_for_base_nodes():
145
147
  assert (
146
148
  node["trigger"]["id"] in edge_target_handle_ids
147
149
  ), f"Trigger {node['trigger']['id']} from node {node['label']} not found in edge target handle ids"
150
+
151
+
152
+ def test_vellum_workflow_display__serialize_with_unused_nodes_and_edges():
153
+ # GIVEN a workflow with active and unused nodes
154
+ class NodeA(BaseNode):
155
+ class Outputs(BaseNode.Outputs):
156
+ result: str
157
+
158
+ class NodeB(BaseNode):
159
+ pass
160
+
161
+ class NodeC(BaseNode):
162
+ pass
163
+
164
+ # AND A workflow that uses them correctly
165
+ class Workflow(BaseWorkflow):
166
+ graph = NodeA
167
+ unused_graphs = {NodeB >> NodeC}
168
+
169
+ class Outputs(BaseWorkflow.Outputs):
170
+ final = NodeA.Outputs.result
171
+
172
+ # WHEN we serialize it
173
+ workflow_display = get_workflow_display(
174
+ base_display_class=VellumWorkflowDisplay,
175
+ workflow_class=Workflow,
176
+ )
177
+
178
+ # WHEN we serialize the workflow
179
+ exec_config = workflow_display.serialize()
180
+
181
+ # THEN the serialized workflow contains the expected nodes and edges
182
+ raw_data = exec_config["workflow_raw_data"]
183
+ assert isinstance(raw_data, dict)
184
+
185
+ nodes = raw_data["nodes"]
186
+ edges = raw_data["edges"]
187
+
188
+ assert isinstance(nodes, list)
189
+ assert isinstance(edges, list)
190
+
191
+ # Find nodes by their definition name
192
+ node_ids: Dict[str, str] = {}
193
+
194
+ for node in nodes:
195
+ assert isinstance(node, dict)
196
+ definition = node.get("definition")
197
+ if definition is None:
198
+ continue
199
+
200
+ assert isinstance(definition, dict)
201
+ name = definition.get("name")
202
+ if not isinstance(name, str):
203
+ continue
204
+
205
+ if name in ["NodeA", "NodeB", "NodeC"]:
206
+ node_id = node.get("id")
207
+ if isinstance(node_id, str):
208
+ node_ids[name] = node_id
209
+
210
+ # Verify all nodes are present
211
+ assert "NodeA" in node_ids, "Active node NodeA not found in serialized output"
212
+ assert "NodeB" in node_ids, "Unused node NodeB not found in serialized output"
213
+ assert "NodeC" in node_ids, "Unused node NodeC not found in serialized output"
214
+
215
+ # Verify the edge between NodeB and NodeC is present
216
+ edge_found = False
217
+ for edge in edges:
218
+ assert isinstance(edge, dict)
219
+ source_id = edge.get("source_node_id")
220
+ target_id = edge.get("target_node_id")
221
+
222
+ if (
223
+ isinstance(source_id, str)
224
+ and isinstance(target_id, str)
225
+ and source_id == node_ids["NodeB"]
226
+ and target_id == node_ids["NodeC"]
227
+ ):
228
+ edge_found = True
229
+ break
230
+
231
+ assert edge_found, "Edge between unused nodes NodeB and NodeC not found in serialized output"
232
+
233
+
234
+ def test_parse_json_not_supported_in_ui():
235
+ """
236
+ Test that verifies ParseJsonExpression is not yet supported in the UI.
237
+ This test should fail once UI support is added, at which point it should be updated.
238
+ """
239
+ # GIVEN a workflow that uses the parse_json function
240
+ from vellum.workflows.references.constant import ConstantValueReference
241
+
242
+ class JsonNode(BaseNode):
243
+ class Outputs(BaseNode.Outputs):
244
+ json_result = ConstantValueReference('{"key": "value"}').parse_json()
245
+
246
+ class Workflow(BaseWorkflow):
247
+ graph = JsonNode
248
+
249
+ class Outputs(BaseWorkflow.Outputs):
250
+ final = JsonNode.Outputs.json_result
251
+
252
+ # WHEN we attempt to serialize it
253
+ workflow_display = get_workflow_display(
254
+ base_display_class=VellumWorkflowDisplay,
255
+ workflow_class=Workflow,
256
+ )
257
+
258
+ with pytest.raises(ValueError) as exc_info:
259
+ workflow_display.serialize()
260
+
261
+ assert "ParseJsonExpression is not supported in the UI" == str(exc_info.value)