vellum-ai 1.2.4__py3-none-any.whl → 1.3.0__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 (105) hide show
  1. vellum/__init__.py +56 -0
  2. vellum/client/README.md +1 -1
  3. vellum/client/core/client_wrapper.py +2 -2
  4. vellum/client/reference.md +0 -9
  5. vellum/client/resources/workflow_sandboxes/client.py +0 -12
  6. vellum/client/resources/workflow_sandboxes/raw_client.py +2 -10
  7. vellum/client/resources/workflows/client.py +20 -0
  8. vellum/client/resources/workflows/raw_client.py +20 -0
  9. vellum/client/types/__init__.py +56 -0
  10. vellum/client/types/audio_input.py +30 -0
  11. vellum/client/types/code_executor_input.py +8 -0
  12. vellum/client/types/deployment_read.py +5 -5
  13. vellum/client/types/document_input.py +30 -0
  14. vellum/client/types/image_input.py +30 -0
  15. vellum/client/types/named_scenario_input_audio_variable_value_request.py +22 -0
  16. vellum/client/types/named_scenario_input_document_variable_value_request.py +22 -0
  17. vellum/client/types/named_scenario_input_image_variable_value_request.py +22 -0
  18. vellum/client/types/named_scenario_input_request.py +8 -0
  19. vellum/client/types/named_scenario_input_video_variable_value_request.py +22 -0
  20. vellum/client/types/named_test_case_audio_variable_value.py +26 -0
  21. vellum/client/types/named_test_case_audio_variable_value_request.py +26 -0
  22. vellum/client/types/named_test_case_document_variable_value.py +22 -0
  23. vellum/client/types/named_test_case_document_variable_value_request.py +22 -0
  24. vellum/client/types/named_test_case_image_variable_value.py +22 -0
  25. vellum/client/types/named_test_case_image_variable_value_request.py +22 -0
  26. vellum/client/types/named_test_case_variable_value.py +8 -0
  27. vellum/client/types/named_test_case_variable_value_request.py +8 -0
  28. vellum/client/types/named_test_case_video_variable_value.py +22 -0
  29. vellum/client/types/named_test_case_video_variable_value_request.py +22 -0
  30. vellum/client/types/node_execution_span_attributes.py +1 -0
  31. vellum/client/types/scenario_input.py +11 -1
  32. vellum/client/types/scenario_input_audio_variable_value.py +22 -0
  33. vellum/client/types/scenario_input_document_variable_value.py +22 -0
  34. vellum/client/types/scenario_input_image_variable_value.py +22 -0
  35. vellum/client/types/scenario_input_video_variable_value.py +22 -0
  36. vellum/client/types/slim_deployment_read.py +5 -5
  37. vellum/client/types/slim_workflow_deployment.py +5 -5
  38. vellum/client/types/span_link.py +1 -1
  39. vellum/client/types/span_link_type_enum.py +1 -1
  40. vellum/client/types/test_case_audio_variable_value.py +27 -0
  41. vellum/client/types/test_case_document_variable_value.py +27 -0
  42. vellum/client/types/test_case_image_variable_value.py +27 -0
  43. vellum/client/types/test_case_variable_value.py +8 -0
  44. vellum/client/types/test_case_video_variable_value.py +27 -0
  45. vellum/client/types/video_input.py +30 -0
  46. vellum/client/types/workflow_deployment_read.py +5 -5
  47. vellum/client/types/workflow_push_deployment_config_request.py +1 -0
  48. vellum/client/types/workflow_request_audio_input_request.py +30 -0
  49. vellum/client/types/workflow_request_document_input_request.py +30 -0
  50. vellum/client/types/workflow_request_image_input_request.py +30 -0
  51. vellum/client/types/workflow_request_input_request.py +8 -0
  52. vellum/client/types/workflow_request_video_input_request.py +30 -0
  53. vellum/types/audio_input.py +3 -0
  54. vellum/types/document_input.py +3 -0
  55. vellum/types/image_input.py +3 -0
  56. vellum/types/named_scenario_input_audio_variable_value_request.py +3 -0
  57. vellum/types/named_scenario_input_document_variable_value_request.py +3 -0
  58. vellum/types/named_scenario_input_image_variable_value_request.py +3 -0
  59. vellum/types/named_scenario_input_video_variable_value_request.py +3 -0
  60. vellum/types/named_test_case_audio_variable_value.py +3 -0
  61. vellum/types/named_test_case_audio_variable_value_request.py +3 -0
  62. vellum/types/named_test_case_document_variable_value.py +3 -0
  63. vellum/types/named_test_case_document_variable_value_request.py +3 -0
  64. vellum/types/named_test_case_image_variable_value.py +3 -0
  65. vellum/types/named_test_case_image_variable_value_request.py +3 -0
  66. vellum/types/named_test_case_video_variable_value.py +3 -0
  67. vellum/types/named_test_case_video_variable_value_request.py +3 -0
  68. vellum/types/scenario_input_audio_variable_value.py +3 -0
  69. vellum/types/scenario_input_document_variable_value.py +3 -0
  70. vellum/types/scenario_input_image_variable_value.py +3 -0
  71. vellum/types/scenario_input_video_variable_value.py +3 -0
  72. vellum/types/test_case_audio_variable_value.py +3 -0
  73. vellum/types/test_case_document_variable_value.py +3 -0
  74. vellum/types/test_case_image_variable_value.py +3 -0
  75. vellum/types/test_case_video_variable_value.py +3 -0
  76. vellum/types/video_input.py +3 -0
  77. vellum/types/workflow_request_audio_input_request.py +3 -0
  78. vellum/types/workflow_request_document_input_request.py +3 -0
  79. vellum/types/workflow_request_image_input_request.py +3 -0
  80. vellum/types/workflow_request_video_input_request.py +3 -0
  81. vellum/workflows/events/types.py +6 -1
  82. vellum/workflows/integrations/tests/test_mcp_service.py +106 -1
  83. vellum/workflows/nodes/__init__.py +2 -0
  84. vellum/workflows/nodes/displayable/__init__.py +2 -0
  85. vellum/workflows/nodes/displayable/web_search_node/__init__.py +3 -0
  86. vellum/workflows/nodes/displayable/web_search_node/node.py +133 -0
  87. vellum/workflows/resolvers/base.py +19 -1
  88. vellum/workflows/resolvers/resolver.py +97 -0
  89. vellum/workflows/resolvers/tests/test_resolver.py +131 -0
  90. vellum/workflows/resolvers/types.py +11 -0
  91. vellum/workflows/runner/runner.py +49 -1
  92. vellum/workflows/state/context.py +41 -7
  93. vellum/workflows/utils/zip.py +46 -0
  94. vellum/workflows/workflows/base.py +13 -0
  95. {vellum_ai-1.2.4.dist-info → vellum_ai-1.3.0.dist-info}/METADATA +1 -1
  96. {vellum_ai-1.2.4.dist-info → vellum_ai-1.3.0.dist-info}/RECORD +105 -43
  97. vellum_cli/tests/test_init.py +7 -24
  98. vellum_cli/tests/test_pull.py +27 -52
  99. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +7 -33
  100. vellum_ee/workflows/display/utils/events.py +19 -1
  101. vellum_ee/workflows/display/utils/tests/test_events.py +42 -0
  102. vellum_ee/workflows/tests/test_server.py +115 -0
  103. {vellum_ai-1.2.4.dist-info → vellum_ai-1.3.0.dist-info}/LICENSE +0 -0
  104. {vellum_ai-1.2.4.dist-info → vellum_ai-1.3.0.dist-info}/WHEEL +0 -0
  105. {vellum_ai-1.2.4.dist-info → vellum_ai-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,34 +1,17 @@
1
1
  import pytest
2
- import io
3
2
  import json
4
3
  import os
5
4
  import tempfile
6
5
  from unittest import mock
7
6
  from uuid import uuid4
8
- import zipfile
9
7
 
10
8
  from click.testing import CliRunner
11
9
 
12
10
  from vellum.client.core.api_error import ApiError
11
+ from vellum.workflows.utils.zip import zip_file_map
13
12
  from vellum_cli import main as cli_main
14
13
 
15
14
 
16
- def _zip_file_map(file_map: dict[str, str]) -> bytes:
17
- # Create an in-memory bytes buffer to store the zip
18
- zip_buffer = io.BytesIO()
19
-
20
- # Create zip file and add files from file_map
21
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
22
- for filename, content in file_map.items():
23
- zip_file.writestr(filename, content)
24
-
25
- # Get the bytes from the buffer
26
- zip_bytes = zip_buffer.getvalue()
27
- zip_buffer.close()
28
-
29
- return zip_bytes
30
-
31
-
32
15
  @pytest.mark.parametrize(
33
16
  "base_command",
34
17
  [
@@ -44,7 +27,7 @@ def test_pull(vellum_client, mock_module, base_command):
44
27
  workflow_sandbox_id = mock_module.workflow_sandbox_id
45
28
 
46
29
  # AND the workflow pull API call returns a zip file
47
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
30
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
48
31
 
49
32
  # WHEN the user runs the pull command
50
33
  runner = CliRunner()
@@ -89,7 +72,7 @@ def test_pull__second_module(vellum_client, mock_module):
89
72
  set_pyproject_toml = mock_module.set_pyproject_toml
90
73
 
91
74
  # AND the workflow pull API call returns a zip file
92
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
75
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
93
76
 
94
77
  # AND the module we're about to pull is configured second
95
78
  set_pyproject_toml(
@@ -134,7 +117,7 @@ def test_pull__with_target_dir(vellum_client, mock_module, base_command):
134
117
  os.makedirs(target_dir, exist_ok=True)
135
118
 
136
119
  # AND the workflow pull API call returns a zip file
137
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
120
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
138
121
 
139
122
  # WHEN the user runs the pull command with target-dir
140
123
  runner = CliRunner()
@@ -195,7 +178,7 @@ def test_pull__with_nested_target_dir(vellum_client, mock_module, base_command):
195
178
  nested_target_dir = os.path.join(temp_dir, "dir-1", "dir-2")
196
179
 
197
180
  # AND the workflow pull API call returns a zip file
198
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
181
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
199
182
 
200
183
  # WHEN the user runs the pull command with nested target-dir
201
184
  runner = CliRunner()
@@ -250,11 +233,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
250
233
 
251
234
  # AND the workflow pull API call returns a zip file
252
235
  vellum_client.workflows.pull.return_value = iter(
253
- [
254
- _zip_file_map(
255
- {"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
256
- )
257
- ]
236
+ [zip_file_map({"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})})]
258
237
  )
259
238
 
260
239
  # AND we are currently in a new directory
@@ -309,11 +288,7 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
309
288
 
310
289
  # AND the workflow pull API call returns a zip file
311
290
  vellum_client.workflows.pull.return_value = iter(
312
- [
313
- _zip_file_map(
314
- {"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
315
- )
316
- ]
291
+ [zip_file_map({"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})})]
317
292
  )
318
293
 
319
294
  # WHEN the user runs the pull command with the new workflow sandbox id
@@ -343,7 +318,7 @@ def test_pull__workflow_deployment_with_no_config(vellum_client):
343
318
  # AND the workflow pull API call returns a zip file
344
319
  vellum_client.workflows.pull.return_value = iter(
345
320
  [
346
- _zip_file_map(
321
+ zip_file_map(
347
322
  {
348
323
  "workflow.py": "print('hello')",
349
324
  "metadata.json": json.dumps(
@@ -414,7 +389,7 @@ def test_pull__both_workflow_sandbox_id_and_deployment(vellum_client):
414
389
  workflow_deployment = "my-deployment"
415
390
 
416
391
  # AND the workflow pull API call returns a zip file
417
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
392
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
418
393
 
419
394
  # AND we are currently in a new directory
420
395
  current_dir = os.getcwd()
@@ -447,7 +422,7 @@ def test_pull__remove_missing_files(vellum_client, mock_module):
447
422
  module = mock_module.module
448
423
 
449
424
  # AND the workflow pull API call returns a zip file
450
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
425
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
451
426
 
452
427
  # AND there is already a different file in the module directory
453
428
  other_file_path = os.path.join(temp_dir, *module.split("."), "other_file.py")
@@ -478,7 +453,7 @@ def test_pull__remove_missing_files__ignore_pattern(vellum_client, mock_module):
478
453
  set_pyproject_toml = mock_module.set_pyproject_toml
479
454
 
480
455
  # AND the workflow pull API call returns a zip file
481
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
456
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
482
457
 
483
458
  # AND there is already a different file in the module directory
484
459
  other_file_path = os.path.join(temp_dir, *module.split("."), "other_file.py")
@@ -530,7 +505,7 @@ def test_pull__include_json(vellum_client, mock_module):
530
505
 
531
506
  # AND the workflow pull API call returns a zip file
532
507
  vellum_client.workflows.pull.return_value = iter(
533
- [_zip_file_map({"workflow.py": "print('hello')", "workflow.json": "{}"})]
508
+ [zip_file_map({"workflow.py": "print('hello')", "workflow.json": "{}"})]
534
509
  )
535
510
 
536
511
  # WHEN the user runs the pull command
@@ -552,7 +527,7 @@ def test_pull__exclude_code(vellum_client, mock_module):
552
527
 
553
528
  # AND the workflow pull API call returns a zip file
554
529
  vellum_client.workflows.pull.return_value = iter(
555
- [_zip_file_map({"workflow.py": "print('hello')", "workflow.json": "{}"})]
530
+ [zip_file_map({"workflow.py": "print('hello')", "workflow.json": "{}"})]
556
531
  )
557
532
 
558
533
  # WHEN the user runs the pull command
@@ -606,7 +581,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
606
581
  # AND the workflow pull API call returns a zip file
607
582
  vellum_client.workflows.pull.return_value = iter(
608
583
  [
609
- _zip_file_map(
584
+ zip_file_map(
610
585
  {
611
586
  "workflow.py": "print('hello')",
612
587
  "metadata.json": json.dumps(
@@ -671,7 +646,7 @@ def test_pull__handle_error_log(vellum_client, mock_module):
671
646
 
672
647
  # AND the workflow pull API call returns a zip file with an error log
673
648
  vellum_client.workflows.pull.return_value = iter(
674
- [_zip_file_map({"workflow.py": "print('hello')", "error.log": "test error"})]
649
+ [zip_file_map({"workflow.py": "print('hello')", "error.log": "test error"})]
675
650
  )
676
651
 
677
652
  # WHEN the user runs the pull command with the new workflow sandbox id
@@ -693,7 +668,7 @@ def test_pull__strict__with_error(vellum_client, mock_module):
693
668
  workflow_sandbox_id = mock_module.workflow_sandbox_id
694
669
 
695
670
  # AND the workflow pull API call returns a zip file
696
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
671
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
697
672
 
698
673
  # WHEN the user runs the pull command with the new workflow sandbox id
699
674
  runner = CliRunner()
@@ -715,7 +690,7 @@ def test_pull__include_sandbox(vellum_client, mock_module):
715
690
 
716
691
  # AND the workflow pull API call returns a zip file
717
692
  vellum_client.workflows.pull.return_value = iter(
718
- [_zip_file_map({"workflow.py": "print('hello')", "sandbox.py": "print('hello')"})]
693
+ [zip_file_map({"workflow.py": "print('hello')", "sandbox.py": "print('hello')"})]
719
694
  )
720
695
 
721
696
  # WHEN the user runs the pull command
@@ -744,7 +719,7 @@ def test_pull__same_pull_twice__one_entry_in_lockfile(vellum_client, mock_module
744
719
  workflow_sandbox_id = mock_module.workflow_sandbox_id
745
720
 
746
721
  # AND the workflow pull API call returns a zip file both times
747
- zip_contents = _zip_file_map({"workflow.py": "print('hello')"})
722
+ zip_contents = zip_file_map({"workflow.py": "print('hello')"})
748
723
  responses = iter([zip_contents, zip_contents])
749
724
 
750
725
  def workflows_pull_side_effect(*_args, **_kwargs):
@@ -780,7 +755,7 @@ def test_pull__module_not_in_config(vellum_client, mock_module):
780
755
  set_pyproject_toml({"workflows": []})
781
756
 
782
757
  # AND the workflow pull API call returns a zip file
783
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
758
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
784
759
 
785
760
  # WHEN the user runs the pull command again with the workflow sandbox id and module
786
761
  runner = CliRunner()
@@ -833,7 +808,7 @@ def test_pull__multiple_instances_of_same_module__keep_when_pulling_another_modu
833
808
  json.dump(lock_data, f)
834
809
 
835
810
  # AND the workflow pull API call returns a zip file
836
- vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
811
+ vellum_client.workflows.pull.return_value = iter([zip_file_map({"workflow.py": "print('hello')"})])
837
812
 
838
813
  # WHEN the user runs the pull command on the new module
839
814
  runner = CliRunner()
@@ -856,7 +831,7 @@ def test_pull__module_name_from_deployment_name(vellum_client):
856
831
  deployment_name = "Test Deployment"
857
832
  vellum_client.workflows.pull.return_value = iter(
858
833
  [
859
- _zip_file_map(
834
+ zip_file_map(
860
835
  {
861
836
  "workflow.py": "print('hello')",
862
837
  "metadata.json": json.dumps({"deployment_name": deployment_name, "label": "Some Label"}),
@@ -995,7 +970,7 @@ def test_pull__workflow_deployment_adds_deployment_to_config(vellum_client, work
995
970
  # AND the workflow pull API call returns a zip file with metadata
996
971
  vellum_client.workflows.pull.return_value = iter(
997
972
  [
998
- _zip_file_map(
973
+ zip_file_map(
999
974
  {
1000
975
  "workflow.py": "print('hello')",
1001
976
  "metadata.json": json.dumps(
@@ -1082,7 +1057,7 @@ def test_pull__workflow_deployment_name_is_uuid(vellum_client):
1082
1057
  updated_label = "Updated Label"
1083
1058
  vellum_client.workflows.pull.return_value = iter(
1084
1059
  [
1085
- _zip_file_map(
1060
+ zip_file_map(
1086
1061
  {
1087
1062
  "workflow.py": "print('hello')",
1088
1063
  "metadata.json": json.dumps(
@@ -1170,7 +1145,7 @@ def test_pull__workflow_deployment_updates_existing_deployment(vellum_client, ge
1170
1145
  updated_label = "Updated Label"
1171
1146
  vellum_client.workflows.pull.return_value = iter(
1172
1147
  [
1173
- _zip_file_map(
1148
+ zip_file_map(
1174
1149
  {
1175
1150
  "workflow.py": "print('hello')",
1176
1151
  "metadata.json": json.dumps(
@@ -1218,7 +1193,7 @@ def test_pull__workflow_deployment_with_name_and_id(vellum_client):
1218
1193
  # AND the workflow pull API call returns a zip file with metadata
1219
1194
  vellum_client.workflows.pull.return_value = iter(
1220
1195
  [
1221
- _zip_file_map(
1196
+ zip_file_map(
1222
1197
  {
1223
1198
  "workflow.py": "print('hello')",
1224
1199
  "metadata.json": json.dumps(
@@ -1264,7 +1239,7 @@ def test_pull__workflow_deployment_with_name_and_id(vellum_client):
1264
1239
  # AND pull with name will not add a new deployment to the config
1265
1240
  vellum_client.workflows.pull.return_value = iter(
1266
1241
  [
1267
- _zip_file_map(
1242
+ zip_file_map(
1268
1243
  {
1269
1244
  "workflow.py": "print('hello')",
1270
1245
  "metadata.json": json.dumps(
@@ -1323,7 +1298,7 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
1323
1298
 
1324
1299
  # AND the workflow pull API call returns a zip file
1325
1300
  vellum_client_class.return_value.workflows.pull.return_value = iter(
1326
- [_zip_file_map({"workflow.py": "print('hello')"})]
1301
+ [zip_file_map({"workflow.py": "print('hello')"})]
1327
1302
  )
1328
1303
 
1329
1304
  # WHEN calling `vellum pull` with --workspace
@@ -142,41 +142,15 @@ def test_serialize_node__lazy_reference(serialize_node):
142
142
  attr: str = LazyReference(lambda: ConstantValueReference("hello"))
143
143
 
144
144
  serialized_node = serialize_node(LazyReferenceGenericNode)
145
+ attributes = serialized_node["attributes"]
145
146
 
146
- assert not DeepDiff(
147
+ assert attributes == [
147
148
  {
148
- "id": "3d6bfe3b-263a-40a6-8a05-98288e9559a4",
149
- "label": "Lazy Reference Generic Node",
150
- "type": "GENERIC",
151
- "display_data": {"position": {"x": 0.0, "y": 0.0}},
152
- "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
153
- "definition": {
154
- "name": "LazyReferenceGenericNode",
155
- "module": [
156
- "vellum_ee",
157
- "workflows",
158
- "display",
159
- "tests",
160
- "workflow_serialization",
161
- "generic_nodes",
162
- "test_attributes_serialization",
163
- ],
164
- },
165
- "trigger": {"id": "14ec4d19-13e5-4db3-94fa-4e15274bffc7", "merge_behavior": "AWAIT_ATTRIBUTES"},
166
- "ports": [{"id": "2dba7224-a376-4780-8414-2b50601f9283", "name": "default", "type": "DEFAULT"}],
167
- "adornments": None,
168
- "attributes": [
169
- {
170
- "id": "7ae37eb4-18c8-49e1-b5ac-6369ce7ed5dd",
171
- "name": "attr",
172
- "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "hello"}},
173
- }
174
- ],
175
- "outputs": [],
176
- },
177
- serialized_node,
178
- ignore_order=True,
179
- )
149
+ "id": "7ae37eb4-18c8-49e1-b5ac-6369ce7ed5dd",
150
+ "name": "attr",
151
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "hello"}},
152
+ }
153
+ ]
180
154
 
181
155
 
182
156
  def test_serialize_node__lazy_reference_with_string():
@@ -7,6 +7,24 @@ from vellum_ee.workflows.display.utils.registry import (
7
7
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
8
8
 
9
9
 
10
+ def _should_mark_workflow_dynamic(event: WorkflowExecutionInitiatedEvent) -> bool:
11
+ """
12
+ Check if workflow should be marked as dynamic based on execution context.
13
+ Returns True if parent.type == WORKFLOW_RELEASE_TAG and parent.parent.type == WORKFLOW_NODE.
14
+ """
15
+ if not event.parent:
16
+ return False
17
+
18
+ parent = event.parent
19
+ if parent.type != "WORKFLOW_RELEASE_TAG":
20
+ return False
21
+
22
+ if not parent.parent or parent.parent.type != "WORKFLOW_NODE":
23
+ return False
24
+
25
+ return True
26
+
27
+
10
28
  def event_enricher(event: WorkflowExecutionInitiatedEvent) -> WorkflowExecutionInitiatedEvent:
11
29
  workflow_definition = event.body.workflow_definition
12
30
  workflow_display = get_workflow_display(
@@ -16,7 +34,7 @@ def event_enricher(event: WorkflowExecutionInitiatedEvent) -> WorkflowExecutionI
16
34
  )
17
35
  register_workflow_display_context(event.span_id, workflow_display.display_context)
18
36
 
19
- if event.body.workflow_definition.is_dynamic:
37
+ if event.body.workflow_definition.is_dynamic or _should_mark_workflow_dynamic(event):
20
38
  register_workflow_display_class(workflow_definition, workflow_display.__class__)
21
39
  workflow_version_exec_config = workflow_display.serialize()
22
40
  setattr(event.body, "workflow_version_exec_config", workflow_version_exec_config)
@@ -2,8 +2,10 @@ import pytest
2
2
  from uuid import uuid4
3
3
  from typing import Optional
4
4
 
5
+ from vellum.workflows.events.types import NodeParentContext, WorkflowDeploymentParentContext
5
6
  from vellum.workflows.events.workflow import WorkflowExecutionInitiatedBody, WorkflowExecutionInitiatedEvent
6
7
  from vellum.workflows.inputs.base import BaseInputs
8
+ from vellum.workflows.outputs.base import BaseOutputs
7
9
  from vellum.workflows.workflows.base import BaseWorkflow
8
10
  from vellum_ee.workflows.display.utils.events import event_enricher
9
11
 
@@ -67,3 +69,43 @@ def test_event_enricher_static_workflow(is_dynamic: bool, expected_config: Optio
67
69
 
68
70
  # AND workflow_version_exec_config is set to the expected config
69
71
  assert event.body.workflow_version_exec_config == expected_config
72
+
73
+
74
+ def test_event_enricher_marks_subworkflow_deployment_as_dynamic():
75
+ """Test that event_enricher treats subworkflow deployments as dynamic."""
76
+
77
+ class TestWorkflow(BaseWorkflow):
78
+ is_dynamic = False
79
+
80
+ class Outputs(BaseOutputs):
81
+ pass
82
+
83
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
84
+ trace_id=uuid4(),
85
+ span_id=uuid4(),
86
+ parent=WorkflowDeploymentParentContext(
87
+ span_id=uuid4(),
88
+ deployment_id=uuid4(),
89
+ deployment_name="test-deployment",
90
+ deployment_history_item_id=uuid4(),
91
+ release_tag_id=uuid4(),
92
+ release_tag_name="test-tag",
93
+ workflow_version_id=uuid4(),
94
+ external_id=None,
95
+ metadata=None,
96
+ parent=NodeParentContext(
97
+ span_id=uuid4(),
98
+ node_definition=TestWorkflow,
99
+ parent=None,
100
+ ),
101
+ ),
102
+ body=WorkflowExecutionInitiatedBody(
103
+ workflow_definition=TestWorkflow,
104
+ inputs=BaseInputs(),
105
+ ),
106
+ )
107
+
108
+ enriched_event = event_enricher(event)
109
+
110
+ assert hasattr(enriched_event.body, "workflow_version_exec_config")
111
+ assert enriched_event.body.workflow_version_exec_config is not None
@@ -9,6 +9,7 @@ from vellum.workflows import BaseWorkflow
9
9
  from vellum.workflows.nodes import BaseNode
10
10
  from vellum.workflows.state.context import WorkflowContext
11
11
  from vellum.workflows.utils.uuids import generate_workflow_deployment_prefix
12
+ from vellum.workflows.utils.zip import zip_file_map
12
13
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
13
14
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
14
15
 
@@ -661,3 +662,117 @@ __all__ = ["TestNode"]
661
662
 
662
663
  # AND the method should return a workflow (not None) - this will pass once implemented
663
664
  assert event.name == "workflow.execution.fulfilled"
665
+
666
+
667
+ def test_resolve_workflow_deployment__uses_pull_api_with_inputs_deployment_name(vellum_client):
668
+ """
669
+ Test that resolve_workflow_deployment uses the pull API to fetch subworkflow files
670
+ when the deployment name comes from Inputs.deployment_name.
671
+ """
672
+ # GIVEN a deployment name and release tag
673
+ deployment_name = "test_deployment"
674
+ release_tag = "LATEST"
675
+
676
+ test_node_code = """
677
+ from vellum.workflows.nodes.bases.base import BaseNode
678
+ from vellum.workflows.outputs import BaseOutputs
679
+
680
+ class TestNode(BaseNode):
681
+ template = "Hello"
682
+
683
+ class Outputs(BaseOutputs):
684
+ result: str
685
+
686
+ def run(self):
687
+ return self.Outputs(result="Hello, {template}")
688
+ """
689
+
690
+ mock_workflow_code = """
691
+ from vellum.workflows import BaseWorkflow
692
+ from .nodes.test_node import TestNode
693
+
694
+ class ResolvedWorkflow(BaseWorkflow):
695
+ graph = TestNode
696
+ """
697
+
698
+ inputs_code = """
699
+ from vellum.workflows.inputs import BaseInputs
700
+
701
+ class Inputs(BaseInputs):
702
+ deployment_name: str
703
+ """
704
+
705
+ parent_workflow_code = """
706
+ from vellum.workflows import BaseWorkflow
707
+ from .inputs import Inputs
708
+ from .nodes.subworkflow_deployment_node import TestSubworkflowDeploymentNode
709
+ from vellum.workflows.state import BaseState
710
+
711
+ class ParentWorkflow(BaseWorkflow[Inputs, BaseState]):
712
+ graph = TestSubworkflowDeploymentNode
713
+ """
714
+
715
+ parent_node_code = """
716
+ from vellum.workflows.nodes import SubworkflowDeploymentNode
717
+ from vellum.workflows.outputs import BaseOutputs
718
+ from ..inputs import Inputs
719
+
720
+ class TestSubworkflowDeploymentNode(SubworkflowDeploymentNode):
721
+ deployment = Inputs.deployment_name
722
+
723
+ class Outputs(BaseOutputs):
724
+ result: str
725
+
726
+ subworkflow_inputs = {"message": "test"}
727
+ """
728
+
729
+ subworkflow_files = {
730
+ "__init__.py": "",
731
+ "workflow.py": mock_workflow_code,
732
+ "nodes/__init__.py": """
733
+ from .test_node import TestNode
734
+
735
+ __all__ = ["TestNode"]
736
+ """,
737
+ "nodes/test_node.py": test_node_code,
738
+ }
739
+
740
+ parent_files = {
741
+ "__init__.py": "",
742
+ "inputs.py": inputs_code,
743
+ "workflow.py": parent_workflow_code,
744
+ "nodes/__init__.py": """
745
+ from .subworkflow_deployment_node import TestSubworkflowDeploymentNode
746
+
747
+ __all__ = ["TestSubworkflowDeploymentNode"]
748
+ """,
749
+ "nodes/subworkflow_deployment_node.py": parent_node_code,
750
+ }
751
+
752
+ namespace = str(uuid4())
753
+
754
+ # AND the virtual file loader is registered for the parent workflow
755
+ finder = VirtualFileFinder(parent_files, namespace)
756
+ sys.meta_path.append(finder)
757
+
758
+ vellum_client.workflows.pull.return_value = iter([zip_file_map(subworkflow_files)])
759
+
760
+ # WHEN we execute the root workflow with the mocked client
761
+ Workflow = BaseWorkflow.load_from_module(namespace)
762
+ Inputs = Workflow.get_inputs_class()
763
+
764
+ workflow = Workflow(context=WorkflowContext(namespace=namespace, generated_files=parent_files))
765
+ final_event = workflow.run(inputs=Inputs(deployment_name=deployment_name))
766
+
767
+ # THEN the method should return a workflow (not None) - this will pass once implemented
768
+ assert final_event.name == "workflow.execution.fulfilled", final_event
769
+
770
+ # AND the pull API should have been called with the correct deployment name, release tag, and version
771
+ args, kwargs = vellum_client.workflows.pull.call_args
772
+ assert args[0] == deployment_name
773
+ assert kwargs["release_tag"] == release_tag
774
+ assert kwargs["version"].startswith(">=")
775
+ assert ".0.0,<=" in kwargs["version"]
776
+
777
+ # AND the X-Vellum-Always-Success header should be included for graceful error handling
778
+ assert kwargs["request_options"]["additional_headers"]["X-Vellum-Always-Success"] == "true"