vellum-ai 0.13.14__py3-none-any.whl → 0.13.18__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/client/core/client_wrapper.py +1 -1
- vellum/client/resources/workflows/client.py +0 -10
- vellum/workflows/nodes/core/templating_node/node.py +4 -47
- vellum/workflows/nodes/displayable/code_execution_node/node.py +29 -23
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +169 -5
- vellum/workflows/nodes/displayable/code_execution_node/utils.py +98 -1
- vellum/workflows/nodes/utils.py +50 -1
- vellum/workflows/references/external_input.py +14 -0
- vellum/workflows/state/base.py +7 -0
- vellum/workflows/state/tests/test_state.py +42 -0
- {vellum_ai-0.13.14.dist-info → vellum_ai-0.13.18.dist-info}/METADATA +1 -1
- {vellum_ai-0.13.14.dist-info → vellum_ai-0.13.18.dist-info}/RECORD +26 -25
- vellum_cli/__init__.py +11 -0
- vellum_cli/pull.py +57 -20
- vellum_cli/push.py +27 -10
- vellum_cli/tests/test_pull.py +115 -8
- vellum_cli/tests/test_push.py +148 -44
- vellum_ee/workflows/display/nodes/base_node_display.py +2 -2
- vellum_ee/workflows/display/nodes/get_node_display_class.py +16 -20
- vellum_ee/workflows/display/nodes/vellum/__init__.py +2 -0
- vellum_ee/workflows/display/nodes/vellum/retry_node.py +10 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +23 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +2 -2
- {vellum_ai-0.13.14.dist-info → vellum_ai-0.13.18.dist-info}/LICENSE +0 -0
- {vellum_ai-0.13.14.dist-info → vellum_ai-0.13.18.dist-info}/WHEEL +0 -0
- {vellum_ai-0.13.14.dist-info → vellum_ai-0.13.18.dist-info}/entry_points.txt +0 -0
vellum_cli/tests/test_pull.py
CHANGED
@@ -39,6 +39,7 @@ def test_pull(vellum_client, mock_module, base_command):
|
|
39
39
|
# GIVEN a module on the user's filesystem
|
40
40
|
temp_dir = mock_module.temp_dir
|
41
41
|
module = mock_module.module
|
42
|
+
workflow_sandbox_id = mock_module.workflow_sandbox_id
|
42
43
|
|
43
44
|
# AND the workflow pull API call returns a zip file
|
44
45
|
vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
|
@@ -56,6 +57,27 @@ def test_pull(vellum_client, mock_module, base_command):
|
|
56
57
|
with open(workflow_py) as f:
|
57
58
|
assert f.read() == "print('hello')"
|
58
59
|
|
60
|
+
# AND the vellum.lock.json file is created
|
61
|
+
vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
62
|
+
assert os.path.exists(vellum_lock_json)
|
63
|
+
with open(vellum_lock_json) as f:
|
64
|
+
lock_data = json.load(f)
|
65
|
+
assert lock_data == {
|
66
|
+
"version": "1.0",
|
67
|
+
"workflows": [
|
68
|
+
{
|
69
|
+
"module": module,
|
70
|
+
"workflow_sandbox_id": workflow_sandbox_id,
|
71
|
+
"container_image_name": None,
|
72
|
+
"container_image_tag": None,
|
73
|
+
"ignore": None,
|
74
|
+
"deployments": [],
|
75
|
+
"workspace": "default",
|
76
|
+
}
|
77
|
+
],
|
78
|
+
"workspaces": [],
|
79
|
+
}
|
80
|
+
|
59
81
|
|
60
82
|
def test_pull__second_module(vellum_client, mock_module):
|
61
83
|
# GIVEN a module on the user's filesystem
|
@@ -95,7 +117,13 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
|
|
95
117
|
workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
|
96
118
|
|
97
119
|
# AND the workflow pull API call returns a zip file
|
98
|
-
vellum_client.workflows.pull.return_value = iter(
|
120
|
+
vellum_client.workflows.pull.return_value = iter(
|
121
|
+
[
|
122
|
+
_zip_file_map(
|
123
|
+
{"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
|
124
|
+
)
|
125
|
+
]
|
126
|
+
)
|
99
127
|
|
100
128
|
# AND we are currently in a new directory
|
101
129
|
current_dir = os.getcwd()
|
@@ -112,7 +140,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
|
|
112
140
|
|
113
141
|
# AND the pull api is called with the workflow sandbox id
|
114
142
|
vellum_client.workflows.pull.assert_called_once()
|
115
|
-
workflow_py = os.path.join(temp_dir, "
|
143
|
+
workflow_py = os.path.join(temp_dir, "super_cool_workflow", "workflow.py")
|
116
144
|
assert os.path.exists(workflow_py)
|
117
145
|
with open(workflow_py) as f:
|
118
146
|
assert f.read() == "print('hello')"
|
@@ -127,7 +155,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
|
|
127
155
|
"workspaces": [],
|
128
156
|
"workflows": [
|
129
157
|
{
|
130
|
-
"module": "
|
158
|
+
"module": "super_cool_workflow",
|
131
159
|
"workflow_sandbox_id": "87654321-0000-0000-0000-000000000000",
|
132
160
|
"ignore": None,
|
133
161
|
"deployments": [],
|
@@ -147,7 +175,13 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
|
|
147
175
|
workflow_sandbox_id = "87654321-0000-0000-0000-000000000000"
|
148
176
|
|
149
177
|
# AND the workflow pull API call returns a zip file
|
150
|
-
vellum_client.workflows.pull.return_value = iter(
|
178
|
+
vellum_client.workflows.pull.return_value = iter(
|
179
|
+
[
|
180
|
+
_zip_file_map(
|
181
|
+
{"workflow.py": "print('hello')", "metadata.json": json.dumps({"label": "Super Cool Workflow"})}
|
182
|
+
)
|
183
|
+
]
|
184
|
+
)
|
151
185
|
|
152
186
|
# WHEN the user runs the pull command with the new workflow sandbox id
|
153
187
|
runner = CliRunner()
|
@@ -162,7 +196,7 @@ def test_pull__sandbox_id_with_other_workflow_configured(vellum_client, mock_mod
|
|
162
196
|
assert call_args[0] == workflow_sandbox_id
|
163
197
|
|
164
198
|
# AND the workflow.py file is written to the module directory
|
165
|
-
workflow_py = os.path.join(temp_dir, "
|
199
|
+
workflow_py = os.path.join(temp_dir, "super_cool_workflow", "workflow.py")
|
166
200
|
assert os.path.exists(workflow_py)
|
167
201
|
with open(workflow_py) as f:
|
168
202
|
assert f.read() == "print('hello')"
|
@@ -420,8 +454,12 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
|
|
420
454
|
_zip_file_map(
|
421
455
|
{
|
422
456
|
"workflow.py": "print('hello')",
|
423
|
-
"metadata.json":
|
424
|
-
|
457
|
+
"metadata.json": json.dumps(
|
458
|
+
{
|
459
|
+
"runner_config": {"container_image_name": "test", "container_image_tag": "1.0"},
|
460
|
+
"label": "Super Cool Workflow",
|
461
|
+
}
|
462
|
+
),
|
425
463
|
}
|
426
464
|
)
|
427
465
|
]
|
@@ -456,7 +494,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
|
|
456
494
|
"workspace": "default",
|
457
495
|
},
|
458
496
|
{
|
459
|
-
"module": "
|
497
|
+
"module": "super_cool_workflow",
|
460
498
|
"workflow_sandbox_id": new_workflow_sandbox_id,
|
461
499
|
"ignore": None,
|
462
500
|
"deployments": [],
|
@@ -539,3 +577,72 @@ def test_pull__include_sandbox(vellum_client, mock_module):
|
|
539
577
|
with open(lock_json) as f:
|
540
578
|
lock_data = json.load(f)
|
541
579
|
assert lock_data["workflows"][0]["ignore"] == "sandbox.py"
|
580
|
+
|
581
|
+
|
582
|
+
def test_pull__same_pull_twice__one_entry_in_lockfile(vellum_client, mock_module):
|
583
|
+
# GIVEN a module on the user's filesystem
|
584
|
+
module = mock_module.module
|
585
|
+
temp_dir = mock_module.temp_dir
|
586
|
+
workflow_sandbox_id = mock_module.workflow_sandbox_id
|
587
|
+
|
588
|
+
# AND the workflow pull API call returns a zip file both times
|
589
|
+
zip_contents = _zip_file_map({"workflow.py": "print('hello')"})
|
590
|
+
responses = iter([zip_contents, zip_contents])
|
591
|
+
|
592
|
+
def workflows_pull_side_effect(*args, **kwargs):
|
593
|
+
return iter([next(responses)])
|
594
|
+
|
595
|
+
vellum_client.workflows.pull.side_effect = workflows_pull_side_effect
|
596
|
+
|
597
|
+
# AND the user runs the pull command once
|
598
|
+
runner = CliRunner()
|
599
|
+
runner.invoke(cli_main, ["pull", module])
|
600
|
+
|
601
|
+
# WHEN the user runs the pull command again but with the workflow sandbox id
|
602
|
+
result = runner.invoke(cli_main, ["workflows", "pull", "--workflow-sandbox-id", workflow_sandbox_id])
|
603
|
+
|
604
|
+
# THEN the command returns successfully
|
605
|
+
assert result.exit_code == 0, (result.output, result.exception)
|
606
|
+
|
607
|
+
# AND the lockfile should only have one entry
|
608
|
+
lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
609
|
+
with open(lock_json) as f:
|
610
|
+
lock_data = json.load(f)
|
611
|
+
assert len(lock_data["workflows"]) == 1
|
612
|
+
|
613
|
+
|
614
|
+
def test_pull__module_not_in_config(vellum_client, mock_module):
|
615
|
+
# GIVEN a module on the user's filesystem
|
616
|
+
module = mock_module.module
|
617
|
+
temp_dir = mock_module.temp_dir
|
618
|
+
workflow_sandbox_id = mock_module.workflow_sandbox_id
|
619
|
+
set_pyproject_toml = mock_module.set_pyproject_toml
|
620
|
+
|
621
|
+
# AND the pyproject.toml does not have the module configured
|
622
|
+
set_pyproject_toml({"workflows": []})
|
623
|
+
|
624
|
+
# AND the workflow pull API call returns a zip file
|
625
|
+
vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
|
626
|
+
|
627
|
+
# WHEN the user runs the pull command again with the workflow sandbox id and module
|
628
|
+
runner = CliRunner()
|
629
|
+
result = runner.invoke(cli_main, ["workflows", "pull", module, "--workflow-sandbox-id", workflow_sandbox_id])
|
630
|
+
|
631
|
+
# THEN the command returns successfully
|
632
|
+
assert result.exit_code == 0, (result.output, result.exception)
|
633
|
+
|
634
|
+
# AND the lockfile should have the new entry
|
635
|
+
lock_json = os.path.join(temp_dir, "vellum.lock.json")
|
636
|
+
with open(lock_json) as f:
|
637
|
+
lock_data = json.load(f)
|
638
|
+
assert lock_data["workflows"] == [
|
639
|
+
{
|
640
|
+
"module": module,
|
641
|
+
"workflow_sandbox_id": workflow_sandbox_id,
|
642
|
+
"ignore": None,
|
643
|
+
"deployments": [],
|
644
|
+
"container_image_name": None,
|
645
|
+
"container_image_tag": None,
|
646
|
+
"workspace": "default",
|
647
|
+
}
|
648
|
+
]
|
vellum_cli/tests/test_push.py
CHANGED
@@ -29,6 +29,21 @@ def _extract_tar_gz(tar_gz_bytes: bytes) -> dict[str, str]:
|
|
29
29
|
return files
|
30
30
|
|
31
31
|
|
32
|
+
def _ensure_workflow_py(temp_dir: str, module: str) -> str:
|
33
|
+
base_dir = os.path.join(temp_dir, *module.split("."))
|
34
|
+
os.makedirs(base_dir, exist_ok=True)
|
35
|
+
workflow_py_file_content = """\
|
36
|
+
from vellum.workflows import BaseWorkflow
|
37
|
+
|
38
|
+
class ExampleWorkflow(BaseWorkflow):
|
39
|
+
pass
|
40
|
+
"""
|
41
|
+
with open(os.path.join(temp_dir, *module.split("."), "workflow.py"), "w") as f:
|
42
|
+
f.write(workflow_py_file_content)
|
43
|
+
|
44
|
+
return workflow_py_file_content
|
45
|
+
|
46
|
+
|
32
47
|
def test_push__no_config(mock_module):
|
33
48
|
# GIVEN no config file set
|
34
49
|
mock_module.set_pyproject_toml({"workflows": []})
|
@@ -89,16 +104,7 @@ def test_push__happy_path(mock_module, vellum_client, base_command):
|
|
89
104
|
module = mock_module.module
|
90
105
|
|
91
106
|
# AND a workflow exists in the module successfully
|
92
|
-
|
93
|
-
os.makedirs(base_dir, exist_ok=True)
|
94
|
-
workflow_py_file_content = """\
|
95
|
-
from vellum.workflows import BaseWorkflow
|
96
|
-
|
97
|
-
class ExampleWorkflow(BaseWorkflow):
|
98
|
-
pass
|
99
|
-
"""
|
100
|
-
with open(os.path.join(temp_dir, *module.split("."), "workflow.py"), "w") as f:
|
101
|
-
f.write(workflow_py_file_content)
|
107
|
+
workflow_py_file_content = _ensure_workflow_py(temp_dir, module)
|
102
108
|
|
103
109
|
# AND the push API call returns successfully
|
104
110
|
vellum_client.workflows.push.return_value = WorkflowPushResponse(
|
@@ -113,14 +119,12 @@ class ExampleWorkflow(BaseWorkflow):
|
|
113
119
|
assert result.exit_code == 0
|
114
120
|
|
115
121
|
# Get the last part of the module path and format it
|
116
|
-
expected_label = mock_module.module.split(".")[-1].replace("_", " ").title()
|
117
122
|
expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
|
118
123
|
|
119
124
|
# AND we should have called the push API with the correct args
|
120
125
|
vellum_client.workflows.push.assert_called_once()
|
121
126
|
call_args = vellum_client.workflows.push.call_args.kwargs
|
122
127
|
assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
|
123
|
-
assert call_args["label"] == expected_label
|
124
128
|
assert is_valid_uuid(call_args["workflow_sandbox_id"])
|
125
129
|
assert call_args["artifact"].name == expected_artifact_name
|
126
130
|
assert "deplyment_config" not in call_args
|
@@ -137,22 +141,142 @@ class ExampleWorkflow(BaseWorkflow):
|
|
137
141
|
],
|
138
142
|
ids=["push", "workflows_push"],
|
139
143
|
)
|
140
|
-
def
|
144
|
+
def test_push__workflow_sandbox_option__existing_id(mock_module, vellum_client, base_command):
|
141
145
|
# GIVEN a single workflow configured
|
142
146
|
temp_dir = mock_module.temp_dir
|
143
147
|
module = mock_module.module
|
148
|
+
existing_workflow_sandbox_id = mock_module.workflow_sandbox_id
|
144
149
|
|
145
150
|
# AND a workflow exists in the module successfully
|
146
|
-
|
147
|
-
os.makedirs(base_dir, exist_ok=True)
|
148
|
-
workflow_py_file_content = """\
|
149
|
-
from vellum.workflows import BaseWorkflow
|
151
|
+
workflow_py_file_content = _ensure_workflow_py(temp_dir, module)
|
150
152
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
# AND the push API call would return successfully
|
154
|
+
vellum_client.workflows.push.return_value = WorkflowPushResponse(
|
155
|
+
workflow_sandbox_id=existing_workflow_sandbox_id,
|
156
|
+
)
|
157
|
+
|
158
|
+
# WHEN calling `vellum push` with the workflow sandbox option on an existing config
|
159
|
+
runner = CliRunner()
|
160
|
+
result = runner.invoke(cli_main, base_command + [module, "--workflow-sandbox-id", existing_workflow_sandbox_id])
|
161
|
+
|
162
|
+
# THEN it should succeed
|
163
|
+
assert result.exit_code == 0
|
164
|
+
|
165
|
+
# Get the last part of the module path and format it
|
166
|
+
expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
|
167
|
+
|
168
|
+
# AND we should have called the push API with the correct args
|
169
|
+
vellum_client.workflows.push.assert_called_once()
|
170
|
+
call_args = vellum_client.workflows.push.call_args.kwargs
|
171
|
+
assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
|
172
|
+
assert call_args["workflow_sandbox_id"] == existing_workflow_sandbox_id
|
173
|
+
assert call_args["artifact"].name == expected_artifact_name
|
174
|
+
assert "deplyment_config" not in call_args
|
175
|
+
|
176
|
+
extracted_files = _extract_tar_gz(call_args["artifact"].read())
|
177
|
+
assert extracted_files["workflow.py"] == workflow_py_file_content
|
178
|
+
|
179
|
+
|
180
|
+
def test_push__workflow_sandbox_option__existing_no_module(mock_module, vellum_client):
|
181
|
+
# GIVEN a single workflow configured
|
182
|
+
temp_dir = mock_module.temp_dir
|
183
|
+
first_module = mock_module.module
|
184
|
+
second_module = f"{first_module}2"
|
185
|
+
first_workflow_sandbox_id = mock_module.workflow_sandbox_id
|
186
|
+
second_workflow_sandbox_id = str(uuid4())
|
187
|
+
|
188
|
+
# AND the pyproject.toml has two workflow sandboxes configured
|
189
|
+
mock_module.set_pyproject_toml(
|
190
|
+
{
|
191
|
+
"workflows": [
|
192
|
+
{"module": first_module, "workflow_sandbox_id": first_workflow_sandbox_id},
|
193
|
+
{"module": second_module, "workflow_sandbox_id": second_workflow_sandbox_id},
|
194
|
+
]
|
195
|
+
}
|
196
|
+
)
|
197
|
+
|
198
|
+
# AND a workflow exists for both modules
|
199
|
+
_ensure_workflow_py(temp_dir, first_module)
|
200
|
+
workflow_py_file_content = _ensure_workflow_py(temp_dir, second_module)
|
201
|
+
|
202
|
+
# AND the push API call would return successfully for the second module
|
203
|
+
vellum_client.workflows.push.return_value = WorkflowPushResponse(
|
204
|
+
workflow_sandbox_id=second_workflow_sandbox_id,
|
205
|
+
)
|
206
|
+
|
207
|
+
# WHEN calling `vellum push` with the workflow sandbox option on the second module
|
208
|
+
runner = CliRunner()
|
209
|
+
result = runner.invoke(cli_main, ["workflows", "push", "--workflow-sandbox-id", second_workflow_sandbox_id])
|
210
|
+
|
211
|
+
# THEN it should succeed
|
212
|
+
assert result.exit_code == 0
|
213
|
+
|
214
|
+
# Get the last part of the module path and format it
|
215
|
+
expected_artifact_name = f"{second_module.replace('.', '__')}.tar.gz"
|
216
|
+
|
217
|
+
# AND we should have called the push API with the correct args
|
218
|
+
vellum_client.workflows.push.assert_called_once()
|
219
|
+
call_args = vellum_client.workflows.push.call_args.kwargs
|
220
|
+
assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
|
221
|
+
assert call_args["workflow_sandbox_id"] == second_workflow_sandbox_id
|
222
|
+
assert call_args["artifact"].name == expected_artifact_name
|
223
|
+
assert "deplyment_config" not in call_args
|
224
|
+
|
225
|
+
extracted_files = _extract_tar_gz(call_args["artifact"].read())
|
226
|
+
assert extracted_files["workflow.py"] == workflow_py_file_content
|
227
|
+
|
228
|
+
|
229
|
+
def test_push__workflow_sandbox_option__existing_id_different_module(mock_module):
|
230
|
+
# GIVEN a single workflow configured
|
231
|
+
temp_dir = mock_module.temp_dir
|
232
|
+
module = mock_module.module
|
233
|
+
second_module = f"{module}2"
|
234
|
+
first_workflow_sandbox_id = mock_module.workflow_sandbox_id
|
235
|
+
second_workflow_sandbox_id = str(uuid4())
|
236
|
+
set_pyproject_toml = mock_module.set_pyproject_toml
|
237
|
+
|
238
|
+
# AND the pyproject.toml has two workflow sandboxes configured
|
239
|
+
set_pyproject_toml(
|
240
|
+
{
|
241
|
+
"workflows": [
|
242
|
+
{"module": module, "workflow_sandbox_id": first_workflow_sandbox_id},
|
243
|
+
{"module": second_module, "workflow_sandbox_id": second_workflow_sandbox_id},
|
244
|
+
]
|
245
|
+
}
|
246
|
+
)
|
247
|
+
|
248
|
+
# AND a workflow exists in both modules successfully
|
249
|
+
_ensure_workflow_py(temp_dir, module)
|
250
|
+
_ensure_workflow_py(temp_dir, second_module)
|
251
|
+
|
252
|
+
# WHEN calling `vellum push` with the first module and the second workflow sandbox id
|
253
|
+
runner = CliRunner()
|
254
|
+
result = runner.invoke(cli_main, ["workflows", "push", module, "--workflow-sandbox-id", second_workflow_sandbox_id])
|
255
|
+
|
256
|
+
# THEN it should fail
|
257
|
+
assert result.exit_code == 1
|
258
|
+
assert result.exception
|
259
|
+
assert (
|
260
|
+
str(result.exception)
|
261
|
+
== "Multiple workflows found in project to push. Pushing only a single workflow is supported."
|
262
|
+
)
|
263
|
+
|
264
|
+
|
265
|
+
@pytest.mark.parametrize(
|
266
|
+
"base_command",
|
267
|
+
[
|
268
|
+
["push"],
|
269
|
+
["workflows", "push"],
|
270
|
+
],
|
271
|
+
ids=["push", "workflows_push"],
|
272
|
+
)
|
273
|
+
def test_push__deployment(mock_module, vellum_client, base_command):
|
274
|
+
# GIVEN a single workflow configured
|
275
|
+
temp_dir = mock_module.temp_dir
|
276
|
+
module = mock_module.module
|
277
|
+
|
278
|
+
# AND a workflow exists in the module successfully
|
279
|
+
workflow_py_file_content = _ensure_workflow_py(temp_dir, module)
|
156
280
|
|
157
281
|
# AND the push API call returns successfully
|
158
282
|
vellum_client.workflows.push.return_value = WorkflowPushResponse(
|
@@ -167,14 +291,12 @@ class ExampleWorkflow(BaseWorkflow):
|
|
167
291
|
assert result.exit_code == 0
|
168
292
|
|
169
293
|
# Get the last part of the module path and format it
|
170
|
-
expected_label = mock_module.module.split(".")[-1].replace("_", " ").title()
|
171
294
|
expected_artifact_name = f"{mock_module.module.replace('.', '__')}.tar.gz"
|
172
295
|
|
173
296
|
# AND we should have called the push API with the correct args
|
174
297
|
vellum_client.workflows.push.assert_called_once()
|
175
298
|
call_args = vellum_client.workflows.push.call_args.kwargs
|
176
299
|
assert json.loads(call_args["exec_config"])["workflow_raw_data"]["definition"]["name"] == "ExampleWorkflow"
|
177
|
-
assert call_args["label"] == expected_label
|
178
300
|
assert is_valid_uuid(call_args["workflow_sandbox_id"])
|
179
301
|
assert call_args["artifact"].name == expected_artifact_name
|
180
302
|
assert call_args["deployment_config"] == "{}"
|
@@ -252,16 +374,7 @@ def test_push__strict_option_returns_diffs(mock_module, vellum_client):
|
|
252
374
|
module = mock_module.module
|
253
375
|
|
254
376
|
# AND a workflow exists in the module successfully
|
255
|
-
|
256
|
-
os.makedirs(base_dir, exist_ok=True)
|
257
|
-
workflow_py_file_content = """\
|
258
|
-
from vellum.workflows import BaseWorkflow
|
259
|
-
|
260
|
-
class ExampleWorkflow(BaseWorkflow):
|
261
|
-
pass
|
262
|
-
"""
|
263
|
-
with open(os.path.join(temp_dir, *module.split("."), "workflow.py"), "w") as f:
|
264
|
-
f.write(workflow_py_file_content)
|
377
|
+
_ensure_workflow_py(temp_dir, module)
|
265
378
|
|
266
379
|
# AND the push API call returns a 4xx response with diffs
|
267
380
|
vellum_client.workflows.push.side_effect = ApiError(
|
@@ -356,16 +469,7 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
|
|
356
469
|
)
|
357
470
|
|
358
471
|
# AND a workflow exists in the module successfully
|
359
|
-
|
360
|
-
os.makedirs(base_dir, exist_ok=True)
|
361
|
-
workflow_py_file_content = """\
|
362
|
-
from vellum.workflows import BaseWorkflow
|
363
|
-
|
364
|
-
class ExampleWorkflow(BaseWorkflow):
|
365
|
-
pass
|
366
|
-
"""
|
367
|
-
with open(os.path.join(temp_dir, *module.split("."), "workflow.py"), "w") as f:
|
368
|
-
f.write(workflow_py_file_content)
|
472
|
+
_ensure_workflow_py(temp_dir, module)
|
369
473
|
|
370
474
|
# AND the push API call returns a new workflow sandbox id
|
371
475
|
new_workflow_sandbox_id = str(uuid4())
|
@@ -208,8 +208,8 @@ class BaseNodeDisplay(Generic[NodeType], metaclass=BaseNodeDisplayMeta):
|
|
208
208
|
return uuid4_from_hash(f"{self.node_id}|trigger")
|
209
209
|
|
210
210
|
@classmethod
|
211
|
-
def get_from_node_display_registry(cls, node_class: Type[NodeType]) -> Type["BaseNodeDisplay"]:
|
212
|
-
return cls._node_display_registry
|
211
|
+
def get_from_node_display_registry(cls, node_class: Type[NodeType]) -> Optional[Type["BaseNodeDisplay"]]:
|
212
|
+
return cls._node_display_registry.get(node_class)
|
213
213
|
|
214
214
|
@classmethod
|
215
215
|
def infer_node_class(cls) -> Type[NodeType]:
|
@@ -10,27 +10,23 @@ if TYPE_CHECKING:
|
|
10
10
|
def get_node_display_class(
|
11
11
|
base_class: Type["NodeDisplayType"], node_class: Type[NodeType], root_node_class: Optional[Type[NodeType]] = None
|
12
12
|
) -> Type["NodeDisplayType"]:
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
base_class, node_class.__bases__[0], node_class if root_node_class is None else root_node_class
|
13
|
+
node_display_class = base_class.get_from_node_display_registry(node_class)
|
14
|
+
if node_display_class:
|
15
|
+
if not issubclass(node_display_class, base_class):
|
16
|
+
raise TypeError(
|
17
|
+
f"Expected to find a subclass of '{base_class.__name__}' for node class '{node_class.__name__}'"
|
19
18
|
)
|
20
19
|
|
21
|
-
|
22
|
-
NodeDisplayBaseClass = base_node_display_class[node_class] # type: ignore[index]
|
23
|
-
NodeDisplayClass = types.new_class(
|
24
|
-
f"{node_class.__name__}Display",
|
25
|
-
bases=(NodeDisplayBaseClass,),
|
26
|
-
)
|
27
|
-
return NodeDisplayClass
|
28
|
-
except IndexError:
|
29
|
-
return base_class
|
20
|
+
return node_display_class
|
30
21
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
)
|
22
|
+
base_node_display_class = get_node_display_class(
|
23
|
+
base_class, node_class.__bases__[0], node_class if root_node_class is None else root_node_class
|
24
|
+
)
|
35
25
|
|
36
|
-
|
26
|
+
# `base_node_display_class` is always a Generic class, so it's safe to index into it
|
27
|
+
NodeDisplayBaseClass = base_node_display_class[node_class] # type: ignore[index]
|
28
|
+
NodeDisplayClass = types.new_class(
|
29
|
+
f"{node_class.__name__}Display",
|
30
|
+
bases=(NodeDisplayBaseClass,),
|
31
|
+
)
|
32
|
+
return NodeDisplayClass
|
@@ -10,6 +10,7 @@ from .map_node import BaseMapNodeDisplay
|
|
10
10
|
from .merge_node import BaseMergeNodeDisplay
|
11
11
|
from .note_node import BaseNoteNodeDisplay
|
12
12
|
from .prompt_deployment_node import BasePromptDeploymentNodeDisplay
|
13
|
+
from .retry_node import BaseRetryNodeDisplay
|
13
14
|
from .search_node import BaseSearchNodeDisplay
|
14
15
|
from .subworkflow_deployment_node import BaseSubworkflowDeploymentNodeDisplay
|
15
16
|
from .templating_node import BaseTemplatingNodeDisplay
|
@@ -17,6 +18,7 @@ from .try_node import BaseTryNodeDisplay
|
|
17
18
|
|
18
19
|
# All node display classes must be imported here to be registered in BaseNodeDisplay's node display registry
|
19
20
|
__all__ = [
|
21
|
+
"BaseRetryNodeDisplay",
|
20
22
|
"BaseAPINodeDisplay",
|
21
23
|
"BaseCodeExecutionNodeDisplay",
|
22
24
|
"BaseConditionalNodeDisplay",
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from typing import Generic, TypeVar
|
2
|
+
|
3
|
+
from vellum.workflows.nodes.core.retry_node.node import RetryNode
|
4
|
+
from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
|
5
|
+
|
6
|
+
_RetryNodeType = TypeVar("_RetryNodeType", bound=RetryNode)
|
7
|
+
|
8
|
+
|
9
|
+
class BaseRetryNodeDisplay(BaseNodeDisplay[_RetryNodeType], Generic[_RetryNodeType]):
|
10
|
+
pass
|
@@ -7,10 +7,13 @@ from vellum.workflows.nodes.bases.base import BaseNode
|
|
7
7
|
from vellum.workflows.nodes.core.retry_node.node import RetryNode
|
8
8
|
from vellum.workflows.nodes.core.try_node.node import TryNode
|
9
9
|
from vellum.workflows.outputs.base import BaseOutputs
|
10
|
+
from vellum.workflows.workflows.base import BaseWorkflow
|
10
11
|
from vellum_ee.workflows.display.base import WorkflowInputsDisplay
|
11
12
|
from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
|
12
13
|
from vellum_ee.workflows.display.nodes.base_node_vellum_display import BaseNodeVellumDisplay
|
13
14
|
from vellum_ee.workflows.display.nodes.vellum.try_node import BaseTryNodeDisplay
|
15
|
+
from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
|
16
|
+
from vellum_ee.workflows.display.workflows.vellum_workflow_display import VellumWorkflowDisplay
|
14
17
|
|
15
18
|
|
16
19
|
class Inputs(BaseInputs):
|
@@ -110,6 +113,26 @@ def test_serialize_node__retry(serialize_node):
|
|
110
113
|
)
|
111
114
|
|
112
115
|
|
116
|
+
def test_serialize_node__retry__no_display(): # GIVEN an adornment node
|
117
|
+
@RetryNode.wrap(max_attempts=5)
|
118
|
+
class StartNode(BaseNode):
|
119
|
+
pass
|
120
|
+
|
121
|
+
# AND a workflow that uses the adornment node
|
122
|
+
class MyWorkflow(BaseWorkflow):
|
123
|
+
graph = StartNode
|
124
|
+
|
125
|
+
# WHEN we serialize the workflow
|
126
|
+
workflow_display = get_workflow_display(
|
127
|
+
base_display_class=VellumWorkflowDisplay,
|
128
|
+
workflow_class=MyWorkflow,
|
129
|
+
)
|
130
|
+
exec_config = workflow_display.serialize()
|
131
|
+
|
132
|
+
# THEN the workflow display is created successfully
|
133
|
+
assert exec_config is not None
|
134
|
+
|
135
|
+
|
113
136
|
@TryNode.wrap()
|
114
137
|
class InnerTryGenericNode(BaseNode):
|
115
138
|
input = Inputs.input
|
@@ -93,7 +93,7 @@ def test_serialize_workflow_with_filepath():
|
|
93
93
|
"code_input_id": "f2e8a4fa-b54e-41e9-b314-0e5443519ac7",
|
94
94
|
"runtime_input_id": "19d64948-f22b-4103-a7f5-3add184b31cc",
|
95
95
|
"output_type": "NUMBER",
|
96
|
-
"packages": [],
|
96
|
+
"packages": [{"name": "openai", "version": "1.0.0"}],
|
97
97
|
"output_id": "0fde9607-353f-42c2-85c4-20f720ebc1ec",
|
98
98
|
"log_output_id": "7cac05e3-b7c3-475e-8df8-422b496c3398",
|
99
99
|
},
|
@@ -566,7 +566,7 @@ def test_serialize_workflow__try_wrapped():
|
|
566
566
|
"code_input_id": "f2e8a4fa-b54e-41e9-b314-0e5443519ac7",
|
567
567
|
"runtime_input_id": "19d64948-f22b-4103-a7f5-3add184b31cc",
|
568
568
|
"output_type": "NUMBER",
|
569
|
-
"packages": [],
|
569
|
+
"packages": [{"name": "openai", "version": "1.0.0"}],
|
570
570
|
"output_id": "0fde9607-353f-42c2-85c4-20f720ebc1ec",
|
571
571
|
"log_output_id": "7cac05e3-b7c3-475e-8df8-422b496c3398",
|
572
572
|
},
|
File without changes
|
File without changes
|
File without changes
|