vellum-ai 0.9.16rc2__py3-none-any.whl → 0.9.16rc4__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/plugins/__init__.py +0 -0
- vellum/plugins/pydantic.py +74 -0
- vellum/plugins/utils.py +19 -0
- vellum/plugins/vellum_mypy.py +639 -3
- vellum/workflows/README.md +90 -0
- vellum/workflows/__init__.py +5 -0
- vellum/workflows/constants.py +43 -0
- vellum/workflows/descriptors/__init__.py +0 -0
- vellum/workflows/descriptors/base.py +339 -0
- vellum/workflows/descriptors/tests/test_utils.py +83 -0
- vellum/workflows/descriptors/utils.py +90 -0
- vellum/workflows/edges/__init__.py +5 -0
- vellum/workflows/edges/edge.py +23 -0
- vellum/workflows/emitters/__init__.py +5 -0
- vellum/workflows/emitters/base.py +14 -0
- vellum/workflows/environment/__init__.py +5 -0
- vellum/workflows/environment/environment.py +7 -0
- vellum/workflows/errors/__init__.py +6 -0
- vellum/workflows/errors/types.py +20 -0
- vellum/workflows/events/__init__.py +31 -0
- vellum/workflows/events/node.py +125 -0
- vellum/workflows/events/tests/__init__.py +0 -0
- vellum/workflows/events/tests/test_event.py +216 -0
- vellum/workflows/events/types.py +52 -0
- vellum/workflows/events/utils.py +5 -0
- vellum/workflows/events/workflow.py +139 -0
- vellum/workflows/exceptions.py +15 -0
- vellum/workflows/expressions/__init__.py +0 -0
- vellum/workflows/expressions/accessor.py +52 -0
- vellum/workflows/expressions/and_.py +32 -0
- vellum/workflows/expressions/begins_with.py +31 -0
- vellum/workflows/expressions/between.py +38 -0
- vellum/workflows/expressions/coalesce_expression.py +41 -0
- vellum/workflows/expressions/contains.py +30 -0
- vellum/workflows/expressions/does_not_begin_with.py +31 -0
- vellum/workflows/expressions/does_not_contain.py +30 -0
- vellum/workflows/expressions/does_not_end_with.py +31 -0
- vellum/workflows/expressions/does_not_equal.py +25 -0
- vellum/workflows/expressions/ends_with.py +31 -0
- vellum/workflows/expressions/equals.py +25 -0
- vellum/workflows/expressions/greater_than.py +33 -0
- vellum/workflows/expressions/greater_than_or_equal_to.py +33 -0
- vellum/workflows/expressions/in_.py +31 -0
- vellum/workflows/expressions/is_blank.py +24 -0
- vellum/workflows/expressions/is_not_blank.py +24 -0
- vellum/workflows/expressions/is_not_null.py +21 -0
- vellum/workflows/expressions/is_not_undefined.py +22 -0
- vellum/workflows/expressions/is_null.py +21 -0
- vellum/workflows/expressions/is_undefined.py +22 -0
- vellum/workflows/expressions/less_than.py +33 -0
- vellum/workflows/expressions/less_than_or_equal_to.py +33 -0
- vellum/workflows/expressions/not_between.py +38 -0
- vellum/workflows/expressions/not_in.py +31 -0
- vellum/workflows/expressions/or_.py +32 -0
- vellum/workflows/graph/__init__.py +3 -0
- vellum/workflows/graph/graph.py +131 -0
- vellum/workflows/graph/tests/__init__.py +0 -0
- vellum/workflows/graph/tests/test_graph.py +437 -0
- vellum/workflows/inputs/__init__.py +5 -0
- vellum/workflows/inputs/base.py +55 -0
- vellum/workflows/logging.py +14 -0
- vellum/workflows/nodes/__init__.py +46 -0
- vellum/workflows/nodes/bases/__init__.py +7 -0
- vellum/workflows/nodes/bases/base.py +332 -0
- vellum/workflows/nodes/bases/base_subworkflow_node/__init__.py +5 -0
- vellum/workflows/nodes/bases/base_subworkflow_node/node.py +10 -0
- vellum/workflows/nodes/bases/tests/__init__.py +0 -0
- vellum/workflows/nodes/bases/tests/test_base_node.py +125 -0
- vellum/workflows/nodes/core/__init__.py +16 -0
- vellum/workflows/nodes/core/error_node/__init__.py +5 -0
- vellum/workflows/nodes/core/error_node/node.py +26 -0
- vellum/workflows/nodes/core/inline_subworkflow_node/__init__.py +5 -0
- vellum/workflows/nodes/core/inline_subworkflow_node/node.py +73 -0
- vellum/workflows/nodes/core/map_node/__init__.py +5 -0
- vellum/workflows/nodes/core/map_node/node.py +147 -0
- vellum/workflows/nodes/core/map_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/core/map_node/tests/test_node.py +65 -0
- vellum/workflows/nodes/core/retry_node/__init__.py +5 -0
- vellum/workflows/nodes/core/retry_node/node.py +106 -0
- vellum/workflows/nodes/core/retry_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/core/retry_node/tests/test_node.py +93 -0
- vellum/workflows/nodes/core/templating_node/__init__.py +5 -0
- vellum/workflows/nodes/core/templating_node/custom_filters.py +12 -0
- vellum/workflows/nodes/core/templating_node/exceptions.py +2 -0
- vellum/workflows/nodes/core/templating_node/node.py +123 -0
- vellum/workflows/nodes/core/templating_node/render.py +55 -0
- vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +21 -0
- vellum/workflows/nodes/core/try_node/__init__.py +5 -0
- vellum/workflows/nodes/core/try_node/node.py +110 -0
- vellum/workflows/nodes/core/try_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/core/try_node/tests/test_node.py +82 -0
- vellum/workflows/nodes/displayable/__init__.py +31 -0
- vellum/workflows/nodes/displayable/api_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/api_node/node.py +44 -0
- vellum/workflows/nodes/displayable/bases/__init__.py +11 -0
- vellum/workflows/nodes/displayable/bases/api_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +70 -0
- vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +60 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/constants.py +13 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +118 -0
- vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +98 -0
- vellum/workflows/nodes/displayable/bases/search_node.py +90 -0
- vellum/workflows/nodes/displayable/code_execution_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/code_execution_node/node.py +197 -0
- vellum/workflows/nodes/displayable/code_execution_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/__init__.py +0 -0
- vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/main.py +3 -0
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +111 -0
- vellum/workflows/nodes/displayable/code_execution_node/utils.py +10 -0
- vellum/workflows/nodes/displayable/conditional_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/conditional_node/node.py +25 -0
- vellum/workflows/nodes/displayable/final_output_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/final_output_node/node.py +43 -0
- vellum/workflows/nodes/displayable/guardrail_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/guardrail_node/node.py +97 -0
- vellum/workflows/nodes/displayable/inline_prompt_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/inline_prompt_node/node.py +41 -0
- vellum/workflows/nodes/displayable/merge_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/merge_node/node.py +10 -0
- vellum/workflows/nodes/displayable/prompt_deployment_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +45 -0
- vellum/workflows/nodes/displayable/search_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/search_node/node.py +26 -0
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/__init__.py +5 -0
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +156 -0
- vellum/workflows/nodes/displayable/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/tests/test_inline_text_prompt_node.py +148 -0
- vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py +134 -0
- vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +80 -0
- vellum/workflows/nodes/utils.py +27 -0
- vellum/workflows/outputs/__init__.py +6 -0
- vellum/workflows/outputs/base.py +196 -0
- vellum/workflows/ports/__init__.py +7 -0
- vellum/workflows/ports/node_ports.py +75 -0
- vellum/workflows/ports/port.py +75 -0
- vellum/workflows/ports/utils.py +40 -0
- vellum/workflows/references/__init__.py +17 -0
- vellum/workflows/references/environment_variable.py +20 -0
- vellum/workflows/references/execution_count.py +20 -0
- vellum/workflows/references/external_input.py +49 -0
- vellum/workflows/references/input.py +7 -0
- vellum/workflows/references/lazy.py +55 -0
- vellum/workflows/references/node.py +43 -0
- vellum/workflows/references/output.py +78 -0
- vellum/workflows/references/state_value.py +23 -0
- vellum/workflows/references/vellum_secret.py +15 -0
- vellum/workflows/references/workflow_input.py +41 -0
- vellum/workflows/resolvers/__init__.py +5 -0
- vellum/workflows/resolvers/base.py +15 -0
- vellum/workflows/runner/__init__.py +5 -0
- vellum/workflows/runner/runner.py +588 -0
- vellum/workflows/runner/types.py +18 -0
- vellum/workflows/state/__init__.py +5 -0
- vellum/workflows/state/base.py +327 -0
- vellum/workflows/state/context.py +18 -0
- vellum/workflows/state/encoder.py +57 -0
- vellum/workflows/state/store.py +28 -0
- vellum/workflows/state/tests/__init__.py +0 -0
- vellum/workflows/state/tests/test_state.py +113 -0
- vellum/workflows/types/__init__.py +0 -0
- vellum/workflows/types/core.py +91 -0
- vellum/workflows/types/generics.py +14 -0
- vellum/workflows/types/stack.py +39 -0
- vellum/workflows/types/tests/__init__.py +0 -0
- vellum/workflows/types/tests/test_utils.py +76 -0
- vellum/workflows/types/utils.py +164 -0
- vellum/workflows/utils/__init__.py +0 -0
- vellum/workflows/utils/names.py +13 -0
- vellum/workflows/utils/tests/__init__.py +0 -0
- vellum/workflows/utils/tests/test_names.py +15 -0
- vellum/workflows/utils/tests/test_vellum_variables.py +25 -0
- vellum/workflows/utils/vellum_variables.py +81 -0
- vellum/workflows/vellum_client.py +18 -0
- vellum/workflows/workflows/__init__.py +5 -0
- vellum/workflows/workflows/base.py +365 -0
- {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/METADATA +2 -1
- {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/RECORD +245 -7
- vellum_cli/__init__.py +72 -0
- vellum_cli/aliased_group.py +103 -0
- vellum_cli/config.py +96 -0
- vellum_cli/image_push.py +112 -0
- vellum_cli/logger.py +36 -0
- vellum_cli/pull.py +73 -0
- vellum_cli/push.py +121 -0
- vellum_cli/tests/test_config.py +100 -0
- vellum_cli/tests/test_pull.py +152 -0
- vellum_ee/workflows/__init__.py +0 -0
- vellum_ee/workflows/display/__init__.py +0 -0
- vellum_ee/workflows/display/base.py +73 -0
- vellum_ee/workflows/display/nodes/__init__.py +4 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +116 -0
- vellum_ee/workflows/display/nodes/base_node_vellum_display.py +36 -0
- vellum_ee/workflows/display/nodes/get_node_display_class.py +25 -0
- vellum_ee/workflows/display/nodes/tests/__init__.py +0 -0
- vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +47 -0
- vellum_ee/workflows/display/nodes/types.py +18 -0
- vellum_ee/workflows/display/nodes/utils.py +33 -0
- vellum_ee/workflows/display/nodes/vellum/__init__.py +32 -0
- vellum_ee/workflows/display/nodes/vellum/api_node.py +205 -0
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +71 -0
- vellum_ee/workflows/display/nodes/vellum/conditional_node.py +217 -0
- vellum_ee/workflows/display/nodes/vellum/final_output_node.py +61 -0
- vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +49 -0
- vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +170 -0
- vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +99 -0
- vellum_ee/workflows/display/nodes/vellum/map_node.py +100 -0
- vellum_ee/workflows/display/nodes/vellum/merge_node.py +48 -0
- vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +68 -0
- vellum_ee/workflows/display/nodes/vellum/search_node.py +193 -0
- vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +58 -0
- vellum_ee/workflows/display/nodes/vellum/templating_node.py +67 -0
- vellum_ee/workflows/display/nodes/vellum/tests/__init__.py +0 -0
- vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +106 -0
- vellum_ee/workflows/display/nodes/vellum/try_node.py +38 -0
- vellum_ee/workflows/display/nodes/vellum/utils.py +76 -0
- vellum_ee/workflows/display/tests/__init__.py +0 -0
- vellum_ee/workflows/display/tests/workflow_serialization/__init__.py +0 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +426 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +607 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +1175 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +235 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +511 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +372 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +272 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +289 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +354 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +123 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +84 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +233 -0
- vellum_ee/workflows/display/types.py +46 -0
- vellum_ee/workflows/display/utils/__init__.py +0 -0
- vellum_ee/workflows/display/utils/tests/__init__.py +0 -0
- vellum_ee/workflows/display/utils/tests/test_uuids.py +16 -0
- vellum_ee/workflows/display/utils/uuids.py +24 -0
- vellum_ee/workflows/display/utils/vellum.py +121 -0
- vellum_ee/workflows/display/vellum.py +357 -0
- vellum_ee/workflows/display/workflows/__init__.py +5 -0
- vellum_ee/workflows/display/workflows/base_workflow_display.py +302 -0
- vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +32 -0
- vellum_ee/workflows/display/workflows/vellum_workflow_display.py +386 -0
- {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/LICENSE +0 -0
- {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/WHEEL +0 -0
- {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/entry_points.txt +0 -0
vellum_cli/__init__.py
CHANGED
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from vellum_cli.aliased_group import ClickAliasedGroup
|
6
|
+
from vellum_cli.image_push import image_push_command
|
7
|
+
from vellum_cli.pull import pull_command
|
8
|
+
from vellum_cli.push import push_command
|
9
|
+
|
10
|
+
|
11
|
+
@click.group(cls=ClickAliasedGroup)
|
12
|
+
def main() -> None:
|
13
|
+
"""Vellum SDK CLI"""
|
14
|
+
pass
|
15
|
+
|
16
|
+
|
17
|
+
@main.command()
|
18
|
+
@click.argument("module", required=False)
|
19
|
+
@click.option("--deploy", is_flag=True, help="Deploy the workflow after pushing it to Vellum")
|
20
|
+
@click.option("--deployment-label", type=str, help="Label to use for the deployment")
|
21
|
+
@click.option("--deployment-name", type=str, help="Unique name for the deployment")
|
22
|
+
@click.option("--deployment-description", type=str, help="Description for the deployment")
|
23
|
+
@click.option("--release-tag", type=list, help="Release tag for the deployment", multiple=True)
|
24
|
+
def push(
|
25
|
+
module: Optional[str],
|
26
|
+
deploy: Optional[bool],
|
27
|
+
deployment_label: Optional[str],
|
28
|
+
deployment_name: Optional[str],
|
29
|
+
deployment_description: Optional[str],
|
30
|
+
release_tag: Optional[List[str]],
|
31
|
+
) -> None:
|
32
|
+
"""Push Workflow to Vellum"""
|
33
|
+
push_command(
|
34
|
+
module=module,
|
35
|
+
deploy=deploy,
|
36
|
+
deployment_label=deployment_label,
|
37
|
+
deployment_name=deployment_name,
|
38
|
+
deployment_description=deployment_description,
|
39
|
+
release_tags=release_tag,
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
@main.command()
|
44
|
+
@click.argument("module", required=False)
|
45
|
+
@click.option("--legacy-module", is_flag=True, help="Pull the workflow as a legacy module")
|
46
|
+
def pull(module: Optional[str], legacy_module: Optional[bool]) -> None:
|
47
|
+
"""Pull Workflow from Vellum"""
|
48
|
+
pull_command(module, legacy_module)
|
49
|
+
|
50
|
+
|
51
|
+
@main.group(aliases=["images", "image"])
|
52
|
+
def images() -> None:
|
53
|
+
"""Vellum Docker Images"""
|
54
|
+
pass
|
55
|
+
|
56
|
+
|
57
|
+
@images.command(name="push")
|
58
|
+
@click.argument("image", required=True)
|
59
|
+
@click.option(
|
60
|
+
"--tag",
|
61
|
+
"-t",
|
62
|
+
multiple=True,
|
63
|
+
help="Tags the provided image inside of Vellum's repo. "
|
64
|
+
"This field does not push multiple local tags of the passed in image.",
|
65
|
+
)
|
66
|
+
def image_push(image: str, tag: Optional[List[str]] = None) -> None:
|
67
|
+
"""Push Docker image to Vellum"""
|
68
|
+
image_push_command(image, tag)
|
69
|
+
|
70
|
+
|
71
|
+
if __name__ == "__main__":
|
72
|
+
main()
|
@@ -0,0 +1,103 @@
|
|
1
|
+
"""
|
2
|
+
Extension for the python ``click`` module
|
3
|
+
to provide a group or command with aliases.
|
4
|
+
From https://github.com/click-contrib/click-aliases
|
5
|
+
"""
|
6
|
+
|
7
|
+
import typing as t
|
8
|
+
|
9
|
+
import click
|
10
|
+
|
11
|
+
_click7 = click.__version__[0] >= "7"
|
12
|
+
|
13
|
+
|
14
|
+
class ClickAliasedGroup(click.Group):
|
15
|
+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
16
|
+
super().__init__(*args, **kwargs)
|
17
|
+
self._commands: t.Dict[str, list[str]] = {}
|
18
|
+
self._aliases: t.Dict[str, str] = {}
|
19
|
+
|
20
|
+
def add_command(self, *args: t.Any, **kwargs: t.Any) -> None:
|
21
|
+
aliases = kwargs.pop("aliases", [])
|
22
|
+
super().add_command(*args, **kwargs)
|
23
|
+
if aliases:
|
24
|
+
cmd = args[0]
|
25
|
+
name = args[1] if len(args) > 1 else None
|
26
|
+
name = name or cmd.name
|
27
|
+
if name is None:
|
28
|
+
raise TypeError("Command has no name.")
|
29
|
+
|
30
|
+
self._commands[name] = aliases
|
31
|
+
for alias in aliases:
|
32
|
+
self._aliases[alias] = name
|
33
|
+
|
34
|
+
def command(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
35
|
+
aliases = kwargs.pop("aliases", [])
|
36
|
+
decorator = super().command(*args, **kwargs)
|
37
|
+
if not aliases:
|
38
|
+
return decorator
|
39
|
+
|
40
|
+
def _decorator(f: t.Any) -> t.Any:
|
41
|
+
cmd = decorator(f)
|
42
|
+
if aliases:
|
43
|
+
self._commands[cmd.name] = aliases
|
44
|
+
for alias in aliases:
|
45
|
+
self._aliases[alias] = cmd.name
|
46
|
+
return cmd
|
47
|
+
|
48
|
+
return _decorator
|
49
|
+
|
50
|
+
def group(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
51
|
+
aliases = kwargs.pop("aliases", [])
|
52
|
+
decorator = super().group(*args, **kwargs)
|
53
|
+
if not aliases:
|
54
|
+
return decorator
|
55
|
+
|
56
|
+
def _decorator(f: t.Any) -> t.Any:
|
57
|
+
cmd = decorator(f)
|
58
|
+
if aliases:
|
59
|
+
self._commands[cmd.name] = aliases
|
60
|
+
for alias in aliases:
|
61
|
+
self._aliases[alias] = cmd.name
|
62
|
+
return cmd
|
63
|
+
|
64
|
+
return _decorator
|
65
|
+
|
66
|
+
def resolve_alias(self, cmd_name: str) -> str:
|
67
|
+
if cmd_name in self._aliases:
|
68
|
+
return self._aliases[cmd_name]
|
69
|
+
return cmd_name
|
70
|
+
|
71
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> t.Optional[click.Command]:
|
72
|
+
cmd_name = self.resolve_alias(cmd_name)
|
73
|
+
command = super().get_command(ctx, cmd_name)
|
74
|
+
if command:
|
75
|
+
return command
|
76
|
+
return None
|
77
|
+
|
78
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
79
|
+
rows = []
|
80
|
+
|
81
|
+
sub_commands = self.list_commands(ctx)
|
82
|
+
|
83
|
+
max_len = 0
|
84
|
+
if len(sub_commands) > 0:
|
85
|
+
max_len = max(len(cmd) for cmd in sub_commands)
|
86
|
+
|
87
|
+
limit = formatter.width - 6 - max_len
|
88
|
+
|
89
|
+
for sub_command in sub_commands:
|
90
|
+
cmd = self.get_command(ctx, sub_command)
|
91
|
+
if cmd is None:
|
92
|
+
continue
|
93
|
+
if hasattr(cmd, "hidden") and cmd.hidden:
|
94
|
+
continue
|
95
|
+
if sub_command in self._commands:
|
96
|
+
aliases = ",".join(sorted(self._commands[sub_command]))
|
97
|
+
sub_command = f"{sub_command} ({aliases})"
|
98
|
+
cmd_help = cmd.get_short_help_str(limit) if _click7 else cmd.short_help or ""
|
99
|
+
rows.append((sub_command, cmd_help))
|
100
|
+
|
101
|
+
if rows:
|
102
|
+
with formatter.section("Commands"):
|
103
|
+
formatter.write_dl(rows)
|
vellum_cli/config.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
from dataclasses import field
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from uuid import UUID
|
5
|
+
from typing import Dict, List, Literal, Optional, Union
|
6
|
+
|
7
|
+
import tomli
|
8
|
+
|
9
|
+
from vellum.core.pydantic_utilities import UniversalBaseModel
|
10
|
+
|
11
|
+
from vellum.workflows.state.encoder import DefaultStateEncoder
|
12
|
+
|
13
|
+
LOCKFILE_PATH = "vellum.lock.json"
|
14
|
+
PYPROJECT_TOML_PATH = "pyproject.toml"
|
15
|
+
|
16
|
+
|
17
|
+
class WorkflowDeploymentConfig(UniversalBaseModel):
|
18
|
+
id: Optional[UUID] = None
|
19
|
+
label: Optional[str] = None
|
20
|
+
name: Optional[str] = None
|
21
|
+
description: Optional[str] = None
|
22
|
+
release_tags: Optional[List[str]] = None
|
23
|
+
|
24
|
+
|
25
|
+
class WorkflowConfig(UniversalBaseModel):
|
26
|
+
module: str
|
27
|
+
workflow_sandbox_id: Optional[str] = None
|
28
|
+
ignore: Optional[Union[str, List[str]]] = None
|
29
|
+
deployments: List[WorkflowDeploymentConfig] = field(default_factory=list)
|
30
|
+
|
31
|
+
def merge(self, other: "WorkflowConfig") -> "WorkflowConfig":
|
32
|
+
return WorkflowConfig(
|
33
|
+
module=self.module,
|
34
|
+
workflow_sandbox_id=self.workflow_sandbox_id or other.workflow_sandbox_id,
|
35
|
+
ignore=self.ignore or other.ignore,
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
class VellumCliConfig(UniversalBaseModel):
|
40
|
+
version: Literal["1.0"] = "1.0"
|
41
|
+
workflows: List[WorkflowConfig] = field(default_factory=list)
|
42
|
+
|
43
|
+
def save(self) -> None:
|
44
|
+
lockfile_path = os.path.join(os.getcwd(), LOCKFILE_PATH)
|
45
|
+
with open(lockfile_path, "w") as f:
|
46
|
+
json.dump(self.model_dump(), f, indent=2, cls=DefaultStateEncoder)
|
47
|
+
|
48
|
+
def merge(self, other: "VellumCliConfig") -> "VellumCliConfig":
|
49
|
+
if other.version != self.version:
|
50
|
+
raise ValueError("Lockfile version mismatch")
|
51
|
+
|
52
|
+
self_workflow_by_module = {workflow.module: workflow for workflow in self.workflows}
|
53
|
+
other_workflow_by_module = {workflow.module: workflow for workflow in other.workflows}
|
54
|
+
all_modules = sorted(set(self_workflow_by_module.keys()).union(set(other_workflow_by_module.keys())))
|
55
|
+
merged_workflows = []
|
56
|
+
for module in all_modules:
|
57
|
+
self_workflow = self_workflow_by_module.get(module)
|
58
|
+
other_workflow = other_workflow_by_module.get(module)
|
59
|
+
if self_workflow and other_workflow:
|
60
|
+
merged_workflows.append(self_workflow.merge(other_workflow))
|
61
|
+
elif self_workflow:
|
62
|
+
merged_workflows.append(self_workflow)
|
63
|
+
elif other_workflow:
|
64
|
+
merged_workflows.append(other_workflow)
|
65
|
+
|
66
|
+
return VellumCliConfig(workflows=merged_workflows, version=self.version)
|
67
|
+
|
68
|
+
|
69
|
+
def load_vellum_cli_config(root_dir: Optional[str] = None) -> VellumCliConfig:
|
70
|
+
if root_dir is None:
|
71
|
+
root_dir = os.getcwd()
|
72
|
+
lockfile_path = os.path.join(root_dir, LOCKFILE_PATH)
|
73
|
+
if not os.path.exists(lockfile_path):
|
74
|
+
lockfile_data = {}
|
75
|
+
else:
|
76
|
+
with open(lockfile_path, "rb") as f:
|
77
|
+
lockfile_data = json.load(f)
|
78
|
+
lockfile_config = VellumCliConfig.model_validate(lockfile_data)
|
79
|
+
|
80
|
+
pyproject_toml_path = os.path.join(root_dir, PYPROJECT_TOML_PATH)
|
81
|
+
if not os.path.exists(pyproject_toml_path):
|
82
|
+
toml_vellum: Dict = {}
|
83
|
+
else:
|
84
|
+
with open(pyproject_toml_path, "rb") as f:
|
85
|
+
toml_loaded = tomli.load(f)
|
86
|
+
toml_tool = toml_loaded.get("tool", {})
|
87
|
+
if not isinstance(toml_tool, dict):
|
88
|
+
toml_vellum = {}
|
89
|
+
|
90
|
+
toml_vellum = toml_tool.get("vellum")
|
91
|
+
if not isinstance(toml_vellum, dict):
|
92
|
+
# Mypy is wrong. this is totally reachable.
|
93
|
+
toml_vellum = {} # type: ignore[unreachable]
|
94
|
+
toml_config = VellumCliConfig.model_validate(toml_vellum)
|
95
|
+
|
96
|
+
return toml_config.merge(lockfile_config)
|
vellum_cli/image_push.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import subprocess
|
4
|
+
from typing import List, Optional
|
5
|
+
|
6
|
+
import docker
|
7
|
+
from docker import DockerClient
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
|
10
|
+
from vellum_cli.logger import load_cli_logger
|
11
|
+
from vellum.workflows.vellum_client import create_vellum_client
|
12
|
+
|
13
|
+
_SUPPORTED_ARCHITECTURE = "amd64"
|
14
|
+
|
15
|
+
|
16
|
+
def image_push_command(image: str, tags: Optional[List[str]] = None) -> None:
|
17
|
+
load_dotenv()
|
18
|
+
logger = load_cli_logger()
|
19
|
+
vellum_client = create_vellum_client()
|
20
|
+
|
21
|
+
# We're using docker python SDK here instead of subprocess since it connects to the docker host directly
|
22
|
+
# instead of using the command line so it seemed like it would possibly be a little more robust since
|
23
|
+
# it might avoid peoples' wonky paths, unfortunately it doesn't support the manifest command which we need for
|
24
|
+
# listing all of the architectures of the image instead of just the one that matches the machine. We can fall back
|
25
|
+
# to using normal inspect which returns the machine image for this case though. And in the future we could figure
|
26
|
+
# out how to call the docker host directly to do this.
|
27
|
+
docker_client = docker.from_env()
|
28
|
+
check_architecture(docker_client, image, logger)
|
29
|
+
|
30
|
+
auth = vellum_client.container_images.docker_service_token()
|
31
|
+
|
32
|
+
docker_client.login(
|
33
|
+
username="oauth2accesstoken",
|
34
|
+
password=auth.access_token,
|
35
|
+
registry=auth.repository,
|
36
|
+
)
|
37
|
+
|
38
|
+
repo_split = image.split("/")
|
39
|
+
tag_split = repo_split[-1].split(":")
|
40
|
+
image_name = tag_split[0]
|
41
|
+
main_tag = tag_split[1] if len(tag_split) > 1 else "latest"
|
42
|
+
|
43
|
+
all_tags = [main_tag, *(tags or [])]
|
44
|
+
for tag in all_tags:
|
45
|
+
vellum_image_name = f"{auth.repository}/{image_name}:{tag}"
|
46
|
+
|
47
|
+
docker_client.api.tag(image, vellum_image_name)
|
48
|
+
|
49
|
+
push_result = docker_client.images.push(repository=vellum_image_name, stream=True)
|
50
|
+
|
51
|
+
# Here were trying to mime the output you would get from a normal docker push, which
|
52
|
+
# the python sdk makes as hard as possible.
|
53
|
+
for raw_line in push_result:
|
54
|
+
try:
|
55
|
+
for sub_line in raw_line.decode("utf-8").split("\r\n"):
|
56
|
+
line = json.loads(sub_line)
|
57
|
+
error_message = line.get("errorDetail", {}).get("message")
|
58
|
+
status = line.get("status")
|
59
|
+
id = line.get("id", "")
|
60
|
+
|
61
|
+
if error_message:
|
62
|
+
logger.error(error_message)
|
63
|
+
exit(1)
|
64
|
+
elif status == "Waiting":
|
65
|
+
continue
|
66
|
+
elif status:
|
67
|
+
logger.info(f"{id}{': ' if id else ''}{status}")
|
68
|
+
else:
|
69
|
+
logger.info(line)
|
70
|
+
except Exception:
|
71
|
+
continue
|
72
|
+
|
73
|
+
logger.info("Updating Vellum metadata and enforcing the first law of robotics...")
|
74
|
+
image_details = docker_client.api.inspect_image(image)
|
75
|
+
sha = image_details["Id"]
|
76
|
+
|
77
|
+
vellum_client.container_images.push_container_image(
|
78
|
+
name=image_name,
|
79
|
+
sha=sha,
|
80
|
+
tags=all_tags,
|
81
|
+
)
|
82
|
+
logger.info(f"Image successfully pushed as {image_name} to vellum with tags: {all_tags}.")
|
83
|
+
|
84
|
+
|
85
|
+
def check_architecture(docker_client: DockerClient, image: str, logger: logging.Logger) -> None:
|
86
|
+
result = subprocess.run(
|
87
|
+
["docker", "manifest", "inspect", image],
|
88
|
+
stdout=subprocess.PIPE,
|
89
|
+
stderr=subprocess.PIPE,
|
90
|
+
)
|
91
|
+
|
92
|
+
manifest_parse_failed = False
|
93
|
+
architectures = []
|
94
|
+
try:
|
95
|
+
manifest = json.loads(result.stdout)
|
96
|
+
architectures = [manifest_item["platform"]["architecture"] for manifest_item in manifest["manifests"]]
|
97
|
+
except Exception:
|
98
|
+
logger.warning("Error parsing manifest response")
|
99
|
+
manifest_parse_failed = True
|
100
|
+
|
101
|
+
# Fall back to inspect image if we errored out using docker command line
|
102
|
+
if result.returncode != 0 or manifest_parse_failed:
|
103
|
+
logger.warning(f"Error inspecting manifest: {result.stderr.decode('utf-8').strip()}")
|
104
|
+
image_details = docker_client.api.inspect_image(image)
|
105
|
+
|
106
|
+
if image_details["Architecture"] != _SUPPORTED_ARCHITECTURE:
|
107
|
+
logger.error(f"Image must be built for {_SUPPORTED_ARCHITECTURE} architecture.")
|
108
|
+
exit(1)
|
109
|
+
else:
|
110
|
+
if _SUPPORTED_ARCHITECTURE not in architectures:
|
111
|
+
logger.error(f"Image must be built for {_SUPPORTED_ARCHITECTURE} architecture.")
|
112
|
+
exit(1)
|
vellum_cli/logger.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
|
5
|
+
class CLIFormatter(logging.Formatter):
|
6
|
+
grey = "\x1b[38;20m"
|
7
|
+
yellow = "\x1b[33;20m"
|
8
|
+
red = "\x1b[31;20m"
|
9
|
+
bold_red = "\x1b[31;1m"
|
10
|
+
white = "\33[37m"
|
11
|
+
reset = "\x1b[0m"
|
12
|
+
message_format = "%(message)s"
|
13
|
+
|
14
|
+
FORMATS = {
|
15
|
+
logging.DEBUG: white + message_format + reset,
|
16
|
+
logging.INFO: grey + message_format + reset,
|
17
|
+
logging.WARNING: yellow + message_format + reset,
|
18
|
+
logging.ERROR: red + message_format + reset,
|
19
|
+
logging.CRITICAL: bold_red + message_format + reset,
|
20
|
+
}
|
21
|
+
|
22
|
+
def format(self, record: logging.LogRecord) -> str:
|
23
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
24
|
+
formatter = logging.Formatter(log_fmt)
|
25
|
+
return formatter.format(record)
|
26
|
+
|
27
|
+
|
28
|
+
def load_cli_logger() -> logging.Logger:
|
29
|
+
logger = logging.getLogger(__package__)
|
30
|
+
logger.setLevel(os.getenv("LOG_LEVEL", logging.INFO))
|
31
|
+
|
32
|
+
handler = logging.StreamHandler()
|
33
|
+
handler.setFormatter(CLIFormatter())
|
34
|
+
logger.addHandler(handler)
|
35
|
+
|
36
|
+
return logger
|
vellum_cli/pull.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
import io
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
import zipfile
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
from dotenv import load_dotenv
|
8
|
+
|
9
|
+
from vellum_cli.config import load_vellum_cli_config
|
10
|
+
from vellum_cli.logger import load_cli_logger
|
11
|
+
from vellum.workflows.vellum_client import create_vellum_client
|
12
|
+
|
13
|
+
|
14
|
+
def pull_command(module: Optional[str], legacy_module: Optional[bool] = None) -> None:
|
15
|
+
load_dotenv()
|
16
|
+
logger = load_cli_logger()
|
17
|
+
config = load_vellum_cli_config()
|
18
|
+
|
19
|
+
if not config.workflows:
|
20
|
+
raise ValueError("No Workflows found in project to pull.")
|
21
|
+
|
22
|
+
if len(config.workflows) > 1 and not module:
|
23
|
+
raise ValueError("Multiple workflows found in project to pull. Pulling only a single workflow is supported.")
|
24
|
+
|
25
|
+
workflow_config = next((w for w in config.workflows if w.module == module), None) if module else config.workflows[0]
|
26
|
+
if workflow_config is None:
|
27
|
+
raise ValueError(f"No workflow config for '{module}' found in project to push.")
|
28
|
+
|
29
|
+
if not workflow_config.workflow_sandbox_id:
|
30
|
+
raise ValueError("No workflow sandbox ID found in project to pull from.")
|
31
|
+
|
32
|
+
logger.info(f"Pulling workflow into {workflow_config.module}")
|
33
|
+
client = create_vellum_client()
|
34
|
+
response = client.workflows.pull(
|
35
|
+
workflow_config.workflow_sandbox_id,
|
36
|
+
request_options={"additional_query_parameters": {"legacyModule": legacy_module} if legacy_module else {}},
|
37
|
+
)
|
38
|
+
|
39
|
+
zip_bytes = b"".join(response)
|
40
|
+
zip_buffer = io.BytesIO(zip_bytes)
|
41
|
+
|
42
|
+
target_dir = os.path.join(os.getcwd(), *workflow_config.module.split("."))
|
43
|
+
with zipfile.ZipFile(zip_buffer) as zip_file:
|
44
|
+
# Delete files in target_dir that aren't in the zip file
|
45
|
+
if os.path.exists(target_dir):
|
46
|
+
ignore_patterns = (
|
47
|
+
workflow_config.ignore
|
48
|
+
if isinstance(workflow_config.ignore, list)
|
49
|
+
else [workflow_config.ignore] if isinstance(workflow_config.ignore, str) else []
|
50
|
+
)
|
51
|
+
existing_files = []
|
52
|
+
for root, _, files in os.walk(target_dir):
|
53
|
+
for file in files:
|
54
|
+
rel_path = os.path.relpath(os.path.join(root, file), target_dir)
|
55
|
+
existing_files.append(rel_path)
|
56
|
+
|
57
|
+
for file in existing_files:
|
58
|
+
if any(Path(file).match(ignore_pattern) for ignore_pattern in ignore_patterns):
|
59
|
+
continue
|
60
|
+
|
61
|
+
if file not in zip_file.namelist():
|
62
|
+
file_path = os.path.join(target_dir, file)
|
63
|
+
logger.info(f"Deleting {file_path}...")
|
64
|
+
os.remove(file_path)
|
65
|
+
|
66
|
+
for file_name in zip_file.namelist():
|
67
|
+
target_file = os.path.join(target_dir, file_name)
|
68
|
+
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
69
|
+
with zip_file.open(file_name) as source, open(target_file, "w") as target:
|
70
|
+
logger.info(f"Writing to {target_file}...")
|
71
|
+
target.write(source.read().decode("utf-8"))
|
72
|
+
|
73
|
+
logger.info(f"Successfully pulled Workflow into {workflow_config.module}")
|
vellum_cli/push.py
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
import tarfile
|
6
|
+
from uuid import UUID
|
7
|
+
from typing import List, Optional
|
8
|
+
|
9
|
+
from dotenv import load_dotenv
|
10
|
+
|
11
|
+
from vellum.resources.workflows.client import OMIT
|
12
|
+
from vellum.types import WorkflowPushDeploymentConfigRequest
|
13
|
+
|
14
|
+
from vellum_cli.config import WorkflowDeploymentConfig, load_vellum_cli_config
|
15
|
+
from vellum_cli.logger import load_cli_logger
|
16
|
+
from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
|
17
|
+
from vellum_ee.workflows.display.workflows.vellum_workflow_display import VellumWorkflowDisplay
|
18
|
+
from vellum.workflows.utils.names import snake_to_title_case
|
19
|
+
from vellum.workflows.vellum_client import create_vellum_client
|
20
|
+
from vellum.workflows.workflows.base import BaseWorkflow
|
21
|
+
|
22
|
+
|
23
|
+
def push_command(
|
24
|
+
module: Optional[str],
|
25
|
+
deploy: Optional[bool],
|
26
|
+
deployment_label: Optional[str],
|
27
|
+
deployment_name: Optional[str],
|
28
|
+
deployment_description: Optional[str],
|
29
|
+
release_tags: Optional[List[str]],
|
30
|
+
) -> None:
|
31
|
+
load_dotenv()
|
32
|
+
logger = load_cli_logger()
|
33
|
+
config = load_vellum_cli_config()
|
34
|
+
|
35
|
+
if not config.workflows:
|
36
|
+
raise ValueError("No Workflows found in project to push.")
|
37
|
+
|
38
|
+
if len(config.workflows) > 1 and not module:
|
39
|
+
raise ValueError("Multiple workflows found in project to push. Pushing only a single workflow is supported.")
|
40
|
+
|
41
|
+
workflow_config = next((w for w in config.workflows if w.module == module), None) if module else config.workflows[0]
|
42
|
+
if workflow_config is None:
|
43
|
+
raise ValueError(f"No workflow config for '{module}' found in project to push.")
|
44
|
+
|
45
|
+
logger.info(f"Loading workflow from {workflow_config.module}")
|
46
|
+
client = create_vellum_client()
|
47
|
+
sys.path.insert(0, os.getcwd())
|
48
|
+
|
49
|
+
# Remove this once we could serialize using the artifact in Vembda
|
50
|
+
# https://app.shortcut.com/vellum/story/5585
|
51
|
+
workflow = BaseWorkflow.load_from_module(workflow_config.module)
|
52
|
+
workflow_display = get_workflow_display(base_display_class=VellumWorkflowDisplay, workflow_class=workflow)
|
53
|
+
exec_config = workflow_display.serialize()
|
54
|
+
|
55
|
+
label = snake_to_title_case(workflow_config.module.split(".")[-1])
|
56
|
+
|
57
|
+
deployment_config: WorkflowPushDeploymentConfigRequest = OMIT
|
58
|
+
deployment_config_serialized: str = OMIT
|
59
|
+
if deploy:
|
60
|
+
cli_deployment_config = (
|
61
|
+
workflow_config.deployments[0] if workflow_config.deployments else WorkflowDeploymentConfig()
|
62
|
+
)
|
63
|
+
|
64
|
+
deployment_config = WorkflowPushDeploymentConfigRequest(
|
65
|
+
label=deployment_label or cli_deployment_config.label,
|
66
|
+
name=deployment_name or cli_deployment_config.name,
|
67
|
+
description=deployment_description or cli_deployment_config.description,
|
68
|
+
release_tags=release_tags or cli_deployment_config.release_tags,
|
69
|
+
)
|
70
|
+
|
71
|
+
# We should check with fern if we could auto-serialize typed fields for us
|
72
|
+
# https://app.shortcut.com/vellum/story/5568
|
73
|
+
deployment_config_serialized = json.dumps({k: v for k, v in deployment_config.dict().items() if v is not None})
|
74
|
+
|
75
|
+
artifact = io.BytesIO()
|
76
|
+
with tarfile.open(fileobj=artifact, mode="w:gz") as tar:
|
77
|
+
module_dir = workflow_config.module.replace(".", os.path.sep)
|
78
|
+
for root, _, files in os.walk(module_dir):
|
79
|
+
for filename in files:
|
80
|
+
file_path = os.path.join(root, filename)
|
81
|
+
# Get path relative to module_dir for tar archive
|
82
|
+
relative_path = os.path.relpath(file_path, module_dir)
|
83
|
+
content_bytes = open(file_path, "rb").read()
|
84
|
+
file_buffer = io.BytesIO(content_bytes)
|
85
|
+
|
86
|
+
tarinfo = tarfile.TarInfo(name=relative_path)
|
87
|
+
tarinfo.size = len(content_bytes)
|
88
|
+
|
89
|
+
tar.addfile(tarinfo, file_buffer)
|
90
|
+
|
91
|
+
artifact.seek(0)
|
92
|
+
artifact.name = f"{workflow_config.module.replace('.', '__')}.tar.gz"
|
93
|
+
|
94
|
+
response = client.workflows.push(
|
95
|
+
# Remove this once we could serialize using the artifact in Vembda
|
96
|
+
# https://app.shortcut.com/vellum/story/5585
|
97
|
+
exec_config=json.dumps(exec_config),
|
98
|
+
label=label,
|
99
|
+
workflow_sandbox_id=workflow_config.workflow_sandbox_id,
|
100
|
+
artifact=artifact,
|
101
|
+
# We should check with fern if we could auto-serialize typed object fields for us
|
102
|
+
# https://app.shortcut.com/vellum/story/5568
|
103
|
+
deployment_config=deployment_config_serialized, # type: ignore[arg-type]
|
104
|
+
)
|
105
|
+
logger.info(
|
106
|
+
f"""Successfully pushed {label} to Vellum!
|
107
|
+
Visit at: https://app.vellum.ai/workflow-sandboxes/{response.workflow_sandbox_id}"""
|
108
|
+
)
|
109
|
+
|
110
|
+
requires_save = False
|
111
|
+
if not workflow_config.workflow_sandbox_id:
|
112
|
+
workflow_config.workflow_sandbox_id = response.workflow_sandbox_id
|
113
|
+
requires_save = True
|
114
|
+
|
115
|
+
if not workflow_config.deployments and response.workflow_deployment_id:
|
116
|
+
workflow_config.deployments.append(WorkflowDeploymentConfig(id=UUID(response.workflow_deployment_id)))
|
117
|
+
requires_save = True
|
118
|
+
|
119
|
+
if requires_save:
|
120
|
+
config.save()
|
121
|
+
logger.info("Updated vellum.lock file.")
|