vellum-ai 0.13.18__py3-none-any.whl → 0.13.19__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.
@@ -18,7 +18,7 @@ class BaseClientWrapper:
18
18
  headers: typing.Dict[str, str] = {
19
19
  "X-Fern-Language": "Python",
20
20
  "X-Fern-SDK-Name": "vellum-ai",
21
- "X-Fern-SDK-Version": "0.13.18",
21
+ "X-Fern-SDK-Version": "0.13.19",
22
22
  }
23
23
  headers["X_API_KEY"] = self.api_key
24
24
  return headers
@@ -1,3 +1,4 @@
1
+ import time
1
2
  from typing import Callable, Generic, Optional, Type
2
3
 
3
4
  from vellum.workflows.descriptors.base import BaseDescriptor
@@ -17,11 +18,13 @@ class RetryNode(BaseAdornmentNode[StateType], Generic[StateType]):
17
18
  Used to retry a Subworkflow a specified number of times.
18
19
 
19
20
  max_attempts: int - The maximum number of attempts to retry the Subworkflow
21
+ delay: float - The number of seconds to wait between retries
20
22
  retry_on_error_code: Optional[VellumErrorCode] = None - The error code to retry on
21
23
  subworkflow: Type["BaseWorkflow[SubworkflowInputs, BaseState]"] - The Subworkflow to execute
22
24
  """
23
25
 
24
26
  max_attempts: int
27
+ delay: Optional[float] = None
25
28
  retry_on_error_code: Optional[WorkflowErrorCode] = None
26
29
  retry_on_condition: Optional[BaseDescriptor] = None
27
30
 
@@ -29,7 +32,9 @@ class RetryNode(BaseAdornmentNode[StateType], Generic[StateType]):
29
32
  attempt_number: int
30
33
 
31
34
  def run(self) -> BaseNode.Outputs:
32
- last_exception = Exception("max_attempts must be greater than 0")
35
+ if self.max_attempts <= 0:
36
+ raise Exception("max_attempts must be greater than 0")
37
+
33
38
  for index in range(self.max_attempts):
34
39
  attempt_number = index + 1
35
40
  context = WorkflowContext(vellum_client=self._context.vellum_client)
@@ -49,30 +54,29 @@ class RetryNode(BaseAdornmentNode[StateType], Generic[StateType]):
49
54
 
50
55
  return node_outputs
51
56
  elif terminal_event.name == "workflow.execution.paused":
52
- last_exception = NodeException(
57
+ raise NodeException(
53
58
  code=WorkflowErrorCode.INVALID_OUTPUTS,
54
59
  message=f"Subworkflow unexpectedly paused on attempt {attempt_number}",
55
60
  )
56
- break
57
61
  elif self.retry_on_error_code and self.retry_on_error_code != terminal_event.error.code:
58
- last_exception = NodeException(
62
+ raise NodeException(
59
63
  code=WorkflowErrorCode.INVALID_OUTPUTS,
60
64
  message=f"""Unexpected rejection on attempt {attempt_number}: {terminal_event.error.code.value}.
61
65
  Message: {terminal_event.error.message}""",
62
66
  )
63
- break
64
67
  elif self.retry_on_condition and not resolve_value(self.retry_on_condition, self.state):
65
- last_exception = NodeException(
68
+ raise NodeException(
66
69
  code=WorkflowErrorCode.INVALID_OUTPUTS,
67
70
  message=f"""Rejection failed on attempt {attempt_number}: {terminal_event.error.code.value}.
68
71
  Message: {terminal_event.error.message}""",
69
72
  )
70
- break
71
73
  else:
72
74
  last_exception = NodeException(
73
75
  terminal_event.error.message,
74
76
  code=terminal_event.error.code,
75
77
  )
78
+ if self.delay:
79
+ time.sleep(self.delay)
76
80
 
77
81
  raise last_exception
78
82
 
@@ -80,6 +84,7 @@ Message: {terminal_event.error.message}""",
80
84
  def wrap(
81
85
  cls,
82
86
  max_attempts: int,
87
+ delay: Optional[float] = None,
83
88
  retry_on_error_code: Optional[WorkflowErrorCode] = None,
84
89
  retry_on_condition: Optional[BaseDescriptor] = None,
85
90
  ) -> Callable[..., Type["RetryNode"]]:
@@ -87,6 +92,7 @@ Message: {terminal_event.error.message}""",
87
92
  cls,
88
93
  attributes={
89
94
  "max_attempts": max_attempts,
95
+ "delay": delay,
90
96
  "retry_on_error_code": retry_on_error_code,
91
97
  "retry_on_condition": retry_on_condition,
92
98
  },
@@ -6,6 +6,8 @@ from pydantic_core import core_schema
6
6
 
7
7
  from vellum.workflows.constants import UNDEF
8
8
  from vellum.workflows.descriptors.base import BaseDescriptor
9
+ from vellum.workflows.errors.types import WorkflowErrorCode
10
+ from vellum.workflows.exceptions import NodeException
9
11
  from vellum.workflows.references.output import OutputReference
10
12
  from vellum.workflows.types.utils import get_class_attr_names, infer_types
11
13
 
@@ -183,6 +185,15 @@ class _BaseOutputsMeta(type):
183
185
 
184
186
  class BaseOutputs(metaclass=_BaseOutputsMeta):
185
187
  def __init__(self, **kwargs: Any) -> None:
188
+ declared_fields = {descriptor.name for descriptor in self.__class__}
189
+ provided_fields = set(kwargs.keys())
190
+
191
+ if not provided_fields.issubset(declared_fields):
192
+ raise NodeException(
193
+ message=f"Unexpected outputs: {provided_fields - declared_fields}",
194
+ code=WorkflowErrorCode.INVALID_OUTPUTS,
195
+ )
196
+
186
197
  for name, value in kwargs.items():
187
198
  setattr(self, name, value)
188
199
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.13.18
3
+ Version: 0.13.19
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -2,7 +2,7 @@ vellum_cli/CONTRIBUTING.md,sha256=FtDC7BGxSeMnwCXAUssFsAIElXtmJE-O5Z7BpolcgvI,29
2
2
  vellum_cli/README.md,sha256=2NudRoLzWxNKqnuVy1JuQ7DerIaxWGYkrH8kMd-asIE,90
3
3
  vellum_cli/__init__.py,sha256=uEn2Nlt2Z0kBc79NcO4-rgOIE7H9nsMEEjWF6MLDlPo,10591
4
4
  vellum_cli/aliased_group.py,sha256=ugW498j0yv4ALJ8vS9MsO7ctDW7Jlir9j6nE_uHAP8c,3363
5
- vellum_cli/config.py,sha256=LVRB-SEJcpQYfg2QGcjKHmRSAijdSFADbS90gDY4AI8,6829
5
+ vellum_cli/config.py,sha256=Bsb3mnvKvv3oOTcCuxpgC7lWPMqt6eJhgRA6VEE-vL4,9266
6
6
  vellum_cli/image_push.py,sha256=SJwhwWJsLjwGNezNVd_oCVpFMfPsAB3dfLWmriZZUtw,4419
7
7
  vellum_cli/logger.py,sha256=PuRFa0WCh4sAGFS5aqWB0QIYpS6nBWwPJrIXpWxugV4,1022
8
8
  vellum_cli/ping.py,sha256=lWyJw6sziXjyTopTYRdFF5hV-sYPVDdX0yVbG5fzcY4,585
@@ -13,7 +13,7 @@ vellum_cli/tests/conftest.py,sha256=AFYZryKA2qnUuCPBxBKmHLFoPiE0WhBFFej9tNwSHdc,
13
13
  vellum_cli/tests/test_config.py,sha256=uvKGDc8BoVyT9_H0Z-g8469zVxomn6Oi3Zj-vK7O_wU,2631
14
14
  vellum_cli/tests/test_main.py,sha256=qDZG-aQauPwBwM6A2DIu1494n47v3pL28XakTbLGZ-k,272
15
15
  vellum_cli/tests/test_ping.py,sha256=QtbhYKMYn1DFnDyBij2mkQO32j9KOpZ5Pf0yek7k_Ao,1284
16
- vellum_cli/tests/test_pull.py,sha256=CEnVpb9i-_I0wOSabjgJxleRuvvAHFfDB0_f4CvpwMI,23742
16
+ vellum_cli/tests/test_pull.py,sha256=JURmgGs5lSnpzefSx4K13eF2swv7O8OF86-4df81Zjo,25241
17
17
  vellum_cli/tests/test_push.py,sha256=kcM8jp6vTRBwfk6cAC_tHa4Eliuy_zZ-X1DfIpSEIW0,18299
18
18
  vellum_ee/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  vellum_ee/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,7 +28,7 @@ vellum_ee/workflows/display/nodes/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
28
28
  vellum_ee/workflows/display/nodes/tests/test_base_node_display.py,sha256=QqR3Ly0RNrXwOeLdW5nERDFt0gRPf76n1bPES6o5UN4,1093
29
29
  vellum_ee/workflows/display/nodes/types.py,sha256=St1BB6no528OyELGiyRabWao0GGw6mLhstQAvEACbGk,247
30
30
  vellum_ee/workflows/display/nodes/utils.py,sha256=sloya5TpXsnot1HURc9L51INwflRqUzHxRVnCS9Cd-4,973
31
- vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=Moeuvm0u7c_6wtxUWOJqTJYbrnG4Rp_5mKRoYpfewvQ,1578
31
+ vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=VHx6wSs9wuuiFlZpSQSd3mhECz4SUy2wEBuTSv--_As,1578
32
32
  vellum_ee/workflows/display/nodes/vellum/api_node.py,sha256=hoV-cUtS6H9kmRQXHd2py95GRWI_dAnnaPwvlNBkDOQ,8571
33
33
  vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=z00Z3L0d4PsUQo4S8FRDTtOFLtjdi17TJbatNVF4nM8,4288
34
34
  vellum_ee/workflows/display/nodes/vellum/conditional_node.py,sha256=ybLIa4uclqVIy3VAQvI1ivg2tnK5Ug_1R5a69DFqL7E,11104
@@ -56,7 +56,7 @@ vellum_ee/workflows/display/tests/test_vellum_workflow_display.py,sha256=h4bE187
56
56
  vellum_ee/workflows/display/tests/workflow_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py,sha256=EenmEdBtHUFQ0OS-kE7Vboax3JnDdj-K4Qixt5oR0Po,2253
59
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=6wgLNd-Ae-33SPIYhOvQ9KJ8-5ach98v_Z3g7nrN_xs,8026
59
+ vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=KYdohS5pRgHM0DcUaK0tHa40f0gSvDKi2K5On0zNEU8,8305
60
60
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py,sha256=yPXhdZxEDunNl5LL5Obb0jeO34djt7F-GiaTJhp_fmo,16742
61
61
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py,sha256=-12ZkZb3f5gyoNASV2yeQtMo5HmNsVEo8nXwL6IC-I8,6261
62
62
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py,sha256=6th6kCwzql6lddjkTQx4Jbvvs4ChqtJwctW-B4QuBhI,37352
@@ -113,7 +113,7 @@ vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
113
113
  vellum/client/__init__.py,sha256=8nZt88C9SVwWanjLbIQMU3rzb32h5UZfFMBx3VPHB50,111887
114
114
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
115
115
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
116
- vellum/client/core/client_wrapper.py,sha256=JapYtCCqy-ryd-fk1i1krQS6K630bgNpUw4zE0Lct8s,1869
116
+ vellum/client/core/client_wrapper.py,sha256=rnVChhyob3hO3iXrfL4gsCAYGAOdglXQdXoLL07FKLg,1869
117
117
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
118
118
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
119
119
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -1323,7 +1323,7 @@ vellum/workflows/nodes/core/map_node/node.py,sha256=bjCVMAzkqJUvaLWVBObjskcutwLG
1323
1323
  vellum/workflows/nodes/core/map_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1324
1324
  vellum/workflows/nodes/core/map_node/tests/test_node.py,sha256=P7ImQyeLcK-aEJUYCX1gPQyuOxdxnSPpI3mNPpKQ62Y,1919
1325
1325
  vellum/workflows/nodes/core/retry_node/__init__.py,sha256=lN2bIy5a3Uzhs_FYCrooADyYU6ZGShtvLKFWpelwPvo,60
1326
- vellum/workflows/nodes/core/retry_node/node.py,sha256=QEpxhKOyxDkRoAn2b0PToZWtAGQetSQYVTpb9yCOLlw,4028
1326
+ vellum/workflows/nodes/core/retry_node/node.py,sha256=WD96o-eOj3dwEEe2nqxwBbmLTIyPRYB3Lk4T6XHRX74,4214
1327
1327
  vellum/workflows/nodes/core/retry_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1328
1328
  vellum/workflows/nodes/core/retry_node/tests/test_node.py,sha256=RM_OHwxrHwyxvlQQBJPqVBxpedFuWQ9h2-Xa3kP75sc,4399
1329
1329
  vellum/workflows/nodes/core/templating_node/__init__.py,sha256=GmyuYo81_A1_Bz6id69ozVFS6FKiuDsZTiA3I6MaL2U,70
@@ -1389,7 +1389,7 @@ vellum/workflows/nodes/experimental/openai_chat_completion_node/__init__.py,sha2
1389
1389
  vellum/workflows/nodes/experimental/openai_chat_completion_node/node.py,sha256=1EGeiaT-Zoo6pttQFKKBcdf3dmhAbjKGaErYD5FFwlc,10185
1390
1390
  vellum/workflows/nodes/utils.py,sha256=T7krLipDSI007JkkH2zsfyQ-tfOBH4hyFzQ0YJKYpuw,4025
1391
1391
  vellum/workflows/outputs/__init__.py,sha256=AyZ4pRh_ACQIGvkf0byJO46EDnSix1ZCAXfvh-ms1QE,94
1392
- vellum/workflows/outputs/base.py,sha256=a7W6rNSDSawwGAXYjNTF2iHb9lnZu7WFSOagZIyy__k,7976
1392
+ vellum/workflows/outputs/base.py,sha256=Wu48tqyQoxpflBUcwzKeZjSVp1LPKrBwuIvnx__9H90,8459
1393
1393
  vellum/workflows/ports/__init__.py,sha256=bZuMt-R7z5bKwpu4uPW7LlJeePOQWmCcDSXe5frUY5g,101
1394
1394
  vellum/workflows/ports/node_ports.py,sha256=g4A-8iUAvEJSkaWppbvzAR8XU02R9U-qLN4rP2Kq4Aw,2743
1395
1395
  vellum/workflows/ports/port.py,sha256=rc3GB7dDQCUs0IbY08a92-31YzJHQgBeww13brSJ2Js,3172
@@ -1442,8 +1442,8 @@ vellum/workflows/vellum_client.py,sha256=ODrq_TSl-drX2aezXegf7pizpWDVJuTXH-j6528
1442
1442
  vellum/workflows/workflows/__init__.py,sha256=KY45TqvavCCvXIkyCFMEc0dc6jTMOUci93U2DUrlZYc,66
1443
1443
  vellum/workflows/workflows/base.py,sha256=k0kUWWko4fHyCqLSU_1cBK_pXZpl9MXekWiG-bdOAo0,18353
1444
1444
  vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnadGsrSZGa7t7LpJA,2008
1445
- vellum_ai-0.13.18.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1446
- vellum_ai-0.13.18.dist-info/METADATA,sha256=aLUy-xu3bfn69UAWushKWy3YWWoMMruLabea-UEOO1o,5335
1447
- vellum_ai-0.13.18.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1448
- vellum_ai-0.13.18.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1449
- vellum_ai-0.13.18.dist-info/RECORD,,
1445
+ vellum_ai-0.13.19.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1446
+ vellum_ai-0.13.19.dist-info/METADATA,sha256=NzphP-OKn-7lL7hjSD1e1eT709mTax682Cs85AT0O_4,5335
1447
+ vellum_ai-0.13.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1448
+ vellum_ai-0.13.19.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1449
+ vellum_ai-0.13.19.dist-info/RECORD,,
vellum_cli/config.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from dataclasses import field
2
3
  import json
3
4
  import os
@@ -88,6 +89,50 @@ class WorkflowConfig(UniversalBaseModel):
88
89
  )
89
90
 
90
91
 
92
+ def merge_workflows_by_sandbox_id(
93
+ workflows: List[WorkflowConfig], other_workflows: List[WorkflowConfig]
94
+ ) -> List[WorkflowConfig]:
95
+ merged_workflows: List[WorkflowConfig] = []
96
+ for self_workflow in workflows:
97
+ if self_workflow.workflow_sandbox_id is None:
98
+ # If the user defines a workflow in the pyproject.toml without a sandbox_id,
99
+ # we merge the workflow with one of the ones in the lockfile.
100
+ other_workflow = next(
101
+ (
102
+ other_workflow
103
+ for other_workflow in other_workflows
104
+ if self_workflow.workspace == other_workflow.workspace
105
+ ),
106
+ None,
107
+ )
108
+ if other_workflow is not None:
109
+ merged_workflows.append(self_workflow.merge(other_workflow))
110
+ else:
111
+ merged_workflows.append(self_workflow)
112
+ else:
113
+ # If the user defines a workflow in the pyproject.toml with a sandbox_id,
114
+ # we merge the workflow with one of the ones in the lockfile with the same sandbox_id.
115
+ other_workflow = next(
116
+ (
117
+ other_workflow
118
+ for other_workflow in other_workflows
119
+ if self_workflow.workflow_sandbox_id == other_workflow.workflow_sandbox_id
120
+ ),
121
+ None,
122
+ )
123
+ if other_workflow is not None:
124
+ merged_workflows.append(self_workflow.merge(other_workflow))
125
+ else:
126
+ merged_workflows.append(self_workflow)
127
+
128
+ workflow_sandbox_ids_so_far = {workflow.workflow_sandbox_id for workflow in merged_workflows}
129
+ for other_workflow in other_workflows:
130
+ if other_workflow.workflow_sandbox_id not in workflow_sandbox_ids_so_far:
131
+ merged_workflows.append(other_workflow)
132
+
133
+ return merged_workflows
134
+
135
+
91
136
  class VellumCliConfig(UniversalBaseModel):
92
137
  version: Literal["1.0"] = "1.0"
93
138
  workflows: List[WorkflowConfig] = field(default_factory=list)
@@ -97,24 +142,31 @@ class VellumCliConfig(UniversalBaseModel):
97
142
  lockfile_path = os.path.join(os.getcwd(), LOCKFILE_PATH)
98
143
  with open(lockfile_path, "w") as f:
99
144
  json.dump(self.model_dump(), f, indent=2, cls=DefaultStateEncoder)
145
+ # Makes sure the file ends with a newline, consistent with most autoformatters
146
+ f.write("\n")
100
147
 
101
148
  def merge(self, other: "VellumCliConfig") -> "VellumCliConfig":
102
149
  if other.version != self.version:
103
150
  raise ValueError("Lockfile version mismatch")
104
151
 
105
- self_workflow_by_module = {workflow.module: workflow for workflow in self.workflows}
106
- other_workflow_by_module = {workflow.module: workflow for workflow in other.workflows}
107
- all_modules = sorted(set(self_workflow_by_module.keys()).union(set(other_workflow_by_module.keys())))
152
+ self_workflows_by_module = self.get_workflows_by_module_mapping()
153
+ other_workflows_by_module = other.get_workflows_by_module_mapping()
154
+ all_modules = sorted(set(self_workflows_by_module.keys()).union(set(other_workflows_by_module.keys())))
108
155
  merged_workflows = []
109
156
  for module in all_modules:
110
- self_workflow = self_workflow_by_module.get(module)
111
- other_workflow = other_workflow_by_module.get(module)
112
- if self_workflow and other_workflow:
113
- merged_workflows.append(self_workflow.merge(other_workflow))
114
- elif self_workflow:
115
- merged_workflows.append(self_workflow)
116
- elif other_workflow:
117
- merged_workflows.append(other_workflow)
157
+ self_workflows = self_workflows_by_module.get(module)
158
+ other_workflows = other_workflows_by_module.get(module)
159
+ if self_workflows and other_workflows:
160
+ merged_workflows.extend(
161
+ merge_workflows_by_sandbox_id(
162
+ self_workflows,
163
+ other_workflows,
164
+ )
165
+ )
166
+ elif self_workflows:
167
+ merged_workflows.extend(self_workflows)
168
+ elif other_workflows:
169
+ merged_workflows.extend(other_workflows)
118
170
 
119
171
  self_workspace_by_name = {workspace.name: workspace for workspace in self.workspaces}
120
172
  other_workspace_by_name = {workspace.name: workspace for workspace in other.workspaces}
@@ -132,6 +184,12 @@ class VellumCliConfig(UniversalBaseModel):
132
184
 
133
185
  return VellumCliConfig(workflows=merged_workflows, workspaces=merged_workspaces, version=self.version)
134
186
 
187
+ def get_workflows_by_module_mapping(self) -> Dict[str, List[WorkflowConfig]]:
188
+ workflows_by_module = defaultdict(list)
189
+ for workflow in self.workflows:
190
+ workflows_by_module[workflow.module].append(workflow)
191
+ return workflows_by_module
192
+
135
193
 
136
194
  def load_vellum_cli_config(root_dir: Optional[str] = None) -> VellumCliConfig:
137
195
  if root_dir is None:
@@ -428,7 +428,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
428
428
  "workflows": [
429
429
  {
430
430
  "module": module,
431
- "workflow_sandbox_id": "0edc07cd-45b9-43e8-99bc-1f181972a857",
431
+ "workflow_sandbox_id": workflow_sandbox_id,
432
432
  "ignore": "tests/*",
433
433
  "deployments": [
434
434
  {
@@ -646,3 +646,44 @@ def test_pull__module_not_in_config(vellum_client, mock_module):
646
646
  "workspace": "default",
647
647
  }
648
648
  ]
649
+
650
+
651
+ def test_pull__multiple_instances_of_same_module__keep_when_pulling_another_module(vellum_client, mock_module):
652
+ # GIVEN a module on the user's filesystem
653
+ module = mock_module.module
654
+ temp_dir = mock_module.temp_dir
655
+ workflow_sandbox_id = mock_module.workflow_sandbox_id
656
+
657
+ # AND the vellum lock file has two instances of some other module
658
+ lock_data = {
659
+ "workflows": [
660
+ {
661
+ "module": "some_other_module",
662
+ "workflow_sandbox_id": str(uuid4()),
663
+ "workspace": "default",
664
+ },
665
+ {
666
+ "module": "some_other_module",
667
+ "workflow_sandbox_id": str(uuid4()),
668
+ "workspace": "other",
669
+ },
670
+ ]
671
+ }
672
+ lock_json = os.path.join(temp_dir, "vellum.lock.json")
673
+ with open(lock_json, "w") as f:
674
+ json.dump(lock_data, f)
675
+
676
+ # AND the workflow pull API call returns a zip file
677
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
678
+
679
+ # WHEN the user runs the pull command on the new module
680
+ runner = CliRunner()
681
+ result = runner.invoke(cli_main, ["workflows", "pull", module, "--workflow-sandbox-id", workflow_sandbox_id])
682
+
683
+ # THEN the command returns successfully
684
+ assert result.exit_code == 0, (result.output, result.exception)
685
+
686
+ # AND the lockfile should have all three entries
687
+ with open(lock_json) as f:
688
+ lock_data = json.load(f)
689
+ assert len(lock_data["workflows"]) == 3
@@ -18,7 +18,6 @@ from .try_node import BaseTryNodeDisplay
18
18
 
19
19
  # All node display classes must be imported here to be registered in BaseNodeDisplay's node display registry
20
20
  __all__ = [
21
- "BaseRetryNodeDisplay",
22
21
  "BaseAPINodeDisplay",
23
22
  "BaseCodeExecutionNodeDisplay",
24
23
  "BaseConditionalNodeDisplay",
@@ -31,6 +30,7 @@ __all__ = [
31
30
  "BaseMergeNodeDisplay",
32
31
  "BaseNoteNodeDisplay",
33
32
  "BasePromptDeploymentNodeDisplay",
33
+ "BaseRetryNodeDisplay",
34
34
  "BaseSearchNodeDisplay",
35
35
  "BaseSubworkflowDeploymentNodeDisplay",
36
36
  "BaseTemplatingNodeDisplay",
@@ -80,6 +80,11 @@ def test_serialize_node__retry(serialize_node):
80
80
  "module": ["vellum", "workflows", "nodes", "core", "retry_node", "node"],
81
81
  },
82
82
  "attributes": [
83
+ {
84
+ "id": "8a07dc58-3fed-41d4-8ca6-31ee0bb86c61",
85
+ "name": "delay",
86
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
87
+ },
83
88
  {
84
89
  "id": "f388e93b-8c68-4f54-8577-bbd0c9091557",
85
90
  "name": "max_attempts",