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.
Files changed (245) hide show
  1. vellum/plugins/__init__.py +0 -0
  2. vellum/plugins/pydantic.py +74 -0
  3. vellum/plugins/utils.py +19 -0
  4. vellum/plugins/vellum_mypy.py +639 -3
  5. vellum/workflows/README.md +90 -0
  6. vellum/workflows/__init__.py +5 -0
  7. vellum/workflows/constants.py +43 -0
  8. vellum/workflows/descriptors/__init__.py +0 -0
  9. vellum/workflows/descriptors/base.py +339 -0
  10. vellum/workflows/descriptors/tests/test_utils.py +83 -0
  11. vellum/workflows/descriptors/utils.py +90 -0
  12. vellum/workflows/edges/__init__.py +5 -0
  13. vellum/workflows/edges/edge.py +23 -0
  14. vellum/workflows/emitters/__init__.py +5 -0
  15. vellum/workflows/emitters/base.py +14 -0
  16. vellum/workflows/environment/__init__.py +5 -0
  17. vellum/workflows/environment/environment.py +7 -0
  18. vellum/workflows/errors/__init__.py +6 -0
  19. vellum/workflows/errors/types.py +20 -0
  20. vellum/workflows/events/__init__.py +31 -0
  21. vellum/workflows/events/node.py +125 -0
  22. vellum/workflows/events/tests/__init__.py +0 -0
  23. vellum/workflows/events/tests/test_event.py +216 -0
  24. vellum/workflows/events/types.py +52 -0
  25. vellum/workflows/events/utils.py +5 -0
  26. vellum/workflows/events/workflow.py +139 -0
  27. vellum/workflows/exceptions.py +15 -0
  28. vellum/workflows/expressions/__init__.py +0 -0
  29. vellum/workflows/expressions/accessor.py +52 -0
  30. vellum/workflows/expressions/and_.py +32 -0
  31. vellum/workflows/expressions/begins_with.py +31 -0
  32. vellum/workflows/expressions/between.py +38 -0
  33. vellum/workflows/expressions/coalesce_expression.py +41 -0
  34. vellum/workflows/expressions/contains.py +30 -0
  35. vellum/workflows/expressions/does_not_begin_with.py +31 -0
  36. vellum/workflows/expressions/does_not_contain.py +30 -0
  37. vellum/workflows/expressions/does_not_end_with.py +31 -0
  38. vellum/workflows/expressions/does_not_equal.py +25 -0
  39. vellum/workflows/expressions/ends_with.py +31 -0
  40. vellum/workflows/expressions/equals.py +25 -0
  41. vellum/workflows/expressions/greater_than.py +33 -0
  42. vellum/workflows/expressions/greater_than_or_equal_to.py +33 -0
  43. vellum/workflows/expressions/in_.py +31 -0
  44. vellum/workflows/expressions/is_blank.py +24 -0
  45. vellum/workflows/expressions/is_not_blank.py +24 -0
  46. vellum/workflows/expressions/is_not_null.py +21 -0
  47. vellum/workflows/expressions/is_not_undefined.py +22 -0
  48. vellum/workflows/expressions/is_null.py +21 -0
  49. vellum/workflows/expressions/is_undefined.py +22 -0
  50. vellum/workflows/expressions/less_than.py +33 -0
  51. vellum/workflows/expressions/less_than_or_equal_to.py +33 -0
  52. vellum/workflows/expressions/not_between.py +38 -0
  53. vellum/workflows/expressions/not_in.py +31 -0
  54. vellum/workflows/expressions/or_.py +32 -0
  55. vellum/workflows/graph/__init__.py +3 -0
  56. vellum/workflows/graph/graph.py +131 -0
  57. vellum/workflows/graph/tests/__init__.py +0 -0
  58. vellum/workflows/graph/tests/test_graph.py +437 -0
  59. vellum/workflows/inputs/__init__.py +5 -0
  60. vellum/workflows/inputs/base.py +55 -0
  61. vellum/workflows/logging.py +14 -0
  62. vellum/workflows/nodes/__init__.py +46 -0
  63. vellum/workflows/nodes/bases/__init__.py +7 -0
  64. vellum/workflows/nodes/bases/base.py +332 -0
  65. vellum/workflows/nodes/bases/base_subworkflow_node/__init__.py +5 -0
  66. vellum/workflows/nodes/bases/base_subworkflow_node/node.py +10 -0
  67. vellum/workflows/nodes/bases/tests/__init__.py +0 -0
  68. vellum/workflows/nodes/bases/tests/test_base_node.py +125 -0
  69. vellum/workflows/nodes/core/__init__.py +16 -0
  70. vellum/workflows/nodes/core/error_node/__init__.py +5 -0
  71. vellum/workflows/nodes/core/error_node/node.py +26 -0
  72. vellum/workflows/nodes/core/inline_subworkflow_node/__init__.py +5 -0
  73. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +73 -0
  74. vellum/workflows/nodes/core/map_node/__init__.py +5 -0
  75. vellum/workflows/nodes/core/map_node/node.py +147 -0
  76. vellum/workflows/nodes/core/map_node/tests/__init__.py +0 -0
  77. vellum/workflows/nodes/core/map_node/tests/test_node.py +65 -0
  78. vellum/workflows/nodes/core/retry_node/__init__.py +5 -0
  79. vellum/workflows/nodes/core/retry_node/node.py +106 -0
  80. vellum/workflows/nodes/core/retry_node/tests/__init__.py +0 -0
  81. vellum/workflows/nodes/core/retry_node/tests/test_node.py +93 -0
  82. vellum/workflows/nodes/core/templating_node/__init__.py +5 -0
  83. vellum/workflows/nodes/core/templating_node/custom_filters.py +12 -0
  84. vellum/workflows/nodes/core/templating_node/exceptions.py +2 -0
  85. vellum/workflows/nodes/core/templating_node/node.py +123 -0
  86. vellum/workflows/nodes/core/templating_node/render.py +55 -0
  87. vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +21 -0
  88. vellum/workflows/nodes/core/try_node/__init__.py +5 -0
  89. vellum/workflows/nodes/core/try_node/node.py +110 -0
  90. vellum/workflows/nodes/core/try_node/tests/__init__.py +0 -0
  91. vellum/workflows/nodes/core/try_node/tests/test_node.py +82 -0
  92. vellum/workflows/nodes/displayable/__init__.py +31 -0
  93. vellum/workflows/nodes/displayable/api_node/__init__.py +5 -0
  94. vellum/workflows/nodes/displayable/api_node/node.py +44 -0
  95. vellum/workflows/nodes/displayable/bases/__init__.py +11 -0
  96. vellum/workflows/nodes/displayable/bases/api_node/__init__.py +5 -0
  97. vellum/workflows/nodes/displayable/bases/api_node/node.py +70 -0
  98. vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py +5 -0
  99. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +60 -0
  100. vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py +5 -0
  101. vellum/workflows/nodes/displayable/bases/inline_prompt_node/constants.py +13 -0
  102. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +118 -0
  103. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +98 -0
  104. vellum/workflows/nodes/displayable/bases/search_node.py +90 -0
  105. vellum/workflows/nodes/displayable/code_execution_node/__init__.py +5 -0
  106. vellum/workflows/nodes/displayable/code_execution_node/node.py +197 -0
  107. vellum/workflows/nodes/displayable/code_execution_node/tests/__init__.py +0 -0
  108. vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/__init__.py +0 -0
  109. vellum/workflows/nodes/displayable/code_execution_node/tests/fixtures/main.py +3 -0
  110. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +111 -0
  111. vellum/workflows/nodes/displayable/code_execution_node/utils.py +10 -0
  112. vellum/workflows/nodes/displayable/conditional_node/__init__.py +5 -0
  113. vellum/workflows/nodes/displayable/conditional_node/node.py +25 -0
  114. vellum/workflows/nodes/displayable/final_output_node/__init__.py +5 -0
  115. vellum/workflows/nodes/displayable/final_output_node/node.py +43 -0
  116. vellum/workflows/nodes/displayable/guardrail_node/__init__.py +5 -0
  117. vellum/workflows/nodes/displayable/guardrail_node/node.py +97 -0
  118. vellum/workflows/nodes/displayable/inline_prompt_node/__init__.py +5 -0
  119. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +41 -0
  120. vellum/workflows/nodes/displayable/merge_node/__init__.py +5 -0
  121. vellum/workflows/nodes/displayable/merge_node/node.py +10 -0
  122. vellum/workflows/nodes/displayable/prompt_deployment_node/__init__.py +5 -0
  123. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +45 -0
  124. vellum/workflows/nodes/displayable/search_node/__init__.py +5 -0
  125. vellum/workflows/nodes/displayable/search_node/node.py +26 -0
  126. vellum/workflows/nodes/displayable/subworkflow_deployment_node/__init__.py +5 -0
  127. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +156 -0
  128. vellum/workflows/nodes/displayable/tests/__init__.py +0 -0
  129. vellum/workflows/nodes/displayable/tests/test_inline_text_prompt_node.py +148 -0
  130. vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py +134 -0
  131. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +80 -0
  132. vellum/workflows/nodes/utils.py +27 -0
  133. vellum/workflows/outputs/__init__.py +6 -0
  134. vellum/workflows/outputs/base.py +196 -0
  135. vellum/workflows/ports/__init__.py +7 -0
  136. vellum/workflows/ports/node_ports.py +75 -0
  137. vellum/workflows/ports/port.py +75 -0
  138. vellum/workflows/ports/utils.py +40 -0
  139. vellum/workflows/references/__init__.py +17 -0
  140. vellum/workflows/references/environment_variable.py +20 -0
  141. vellum/workflows/references/execution_count.py +20 -0
  142. vellum/workflows/references/external_input.py +49 -0
  143. vellum/workflows/references/input.py +7 -0
  144. vellum/workflows/references/lazy.py +55 -0
  145. vellum/workflows/references/node.py +43 -0
  146. vellum/workflows/references/output.py +78 -0
  147. vellum/workflows/references/state_value.py +23 -0
  148. vellum/workflows/references/vellum_secret.py +15 -0
  149. vellum/workflows/references/workflow_input.py +41 -0
  150. vellum/workflows/resolvers/__init__.py +5 -0
  151. vellum/workflows/resolvers/base.py +15 -0
  152. vellum/workflows/runner/__init__.py +5 -0
  153. vellum/workflows/runner/runner.py +588 -0
  154. vellum/workflows/runner/types.py +18 -0
  155. vellum/workflows/state/__init__.py +5 -0
  156. vellum/workflows/state/base.py +327 -0
  157. vellum/workflows/state/context.py +18 -0
  158. vellum/workflows/state/encoder.py +57 -0
  159. vellum/workflows/state/store.py +28 -0
  160. vellum/workflows/state/tests/__init__.py +0 -0
  161. vellum/workflows/state/tests/test_state.py +113 -0
  162. vellum/workflows/types/__init__.py +0 -0
  163. vellum/workflows/types/core.py +91 -0
  164. vellum/workflows/types/generics.py +14 -0
  165. vellum/workflows/types/stack.py +39 -0
  166. vellum/workflows/types/tests/__init__.py +0 -0
  167. vellum/workflows/types/tests/test_utils.py +76 -0
  168. vellum/workflows/types/utils.py +164 -0
  169. vellum/workflows/utils/__init__.py +0 -0
  170. vellum/workflows/utils/names.py +13 -0
  171. vellum/workflows/utils/tests/__init__.py +0 -0
  172. vellum/workflows/utils/tests/test_names.py +15 -0
  173. vellum/workflows/utils/tests/test_vellum_variables.py +25 -0
  174. vellum/workflows/utils/vellum_variables.py +81 -0
  175. vellum/workflows/vellum_client.py +18 -0
  176. vellum/workflows/workflows/__init__.py +5 -0
  177. vellum/workflows/workflows/base.py +365 -0
  178. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/METADATA +2 -1
  179. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/RECORD +245 -7
  180. vellum_cli/__init__.py +72 -0
  181. vellum_cli/aliased_group.py +103 -0
  182. vellum_cli/config.py +96 -0
  183. vellum_cli/image_push.py +112 -0
  184. vellum_cli/logger.py +36 -0
  185. vellum_cli/pull.py +73 -0
  186. vellum_cli/push.py +121 -0
  187. vellum_cli/tests/test_config.py +100 -0
  188. vellum_cli/tests/test_pull.py +152 -0
  189. vellum_ee/workflows/__init__.py +0 -0
  190. vellum_ee/workflows/display/__init__.py +0 -0
  191. vellum_ee/workflows/display/base.py +73 -0
  192. vellum_ee/workflows/display/nodes/__init__.py +4 -0
  193. vellum_ee/workflows/display/nodes/base_node_display.py +116 -0
  194. vellum_ee/workflows/display/nodes/base_node_vellum_display.py +36 -0
  195. vellum_ee/workflows/display/nodes/get_node_display_class.py +25 -0
  196. vellum_ee/workflows/display/nodes/tests/__init__.py +0 -0
  197. vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +47 -0
  198. vellum_ee/workflows/display/nodes/types.py +18 -0
  199. vellum_ee/workflows/display/nodes/utils.py +33 -0
  200. vellum_ee/workflows/display/nodes/vellum/__init__.py +32 -0
  201. vellum_ee/workflows/display/nodes/vellum/api_node.py +205 -0
  202. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +71 -0
  203. vellum_ee/workflows/display/nodes/vellum/conditional_node.py +217 -0
  204. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +61 -0
  205. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +49 -0
  206. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +170 -0
  207. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +99 -0
  208. vellum_ee/workflows/display/nodes/vellum/map_node.py +100 -0
  209. vellum_ee/workflows/display/nodes/vellum/merge_node.py +48 -0
  210. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +68 -0
  211. vellum_ee/workflows/display/nodes/vellum/search_node.py +193 -0
  212. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +58 -0
  213. vellum_ee/workflows/display/nodes/vellum/templating_node.py +67 -0
  214. vellum_ee/workflows/display/nodes/vellum/tests/__init__.py +0 -0
  215. vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +106 -0
  216. vellum_ee/workflows/display/nodes/vellum/try_node.py +38 -0
  217. vellum_ee/workflows/display/nodes/vellum/utils.py +76 -0
  218. vellum_ee/workflows/display/tests/__init__.py +0 -0
  219. vellum_ee/workflows/display/tests/workflow_serialization/__init__.py +0 -0
  220. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +426 -0
  221. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +607 -0
  222. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +1175 -0
  223. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +235 -0
  224. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +511 -0
  225. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +372 -0
  226. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +272 -0
  227. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +289 -0
  228. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +354 -0
  229. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +123 -0
  230. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +84 -0
  231. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +233 -0
  232. vellum_ee/workflows/display/types.py +46 -0
  233. vellum_ee/workflows/display/utils/__init__.py +0 -0
  234. vellum_ee/workflows/display/utils/tests/__init__.py +0 -0
  235. vellum_ee/workflows/display/utils/tests/test_uuids.py +16 -0
  236. vellum_ee/workflows/display/utils/uuids.py +24 -0
  237. vellum_ee/workflows/display/utils/vellum.py +121 -0
  238. vellum_ee/workflows/display/vellum.py +357 -0
  239. vellum_ee/workflows/display/workflows/__init__.py +5 -0
  240. vellum_ee/workflows/display/workflows/base_workflow_display.py +302 -0
  241. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +32 -0
  242. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +386 -0
  243. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/LICENSE +0 -0
  244. {vellum_ai-0.9.16rc2.dist-info → vellum_ai-0.9.16rc4.dist-info}/WHEEL +0 -0
  245. {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)
@@ -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.")