vellum-ai 0.14.7__py3-none-any.whl → 0.14.8__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/__init__.py CHANGED
@@ -83,6 +83,7 @@ from .types import (
83
83
  DocumentIndexIndexingConfigRequest,
84
84
  DocumentIndexRead,
85
85
  DocumentProcessingState,
86
+ DocumentPromptBlock,
86
87
  DocumentRead,
87
88
  DocumentStatus,
88
89
  DocumentVellumValue,
@@ -623,6 +624,7 @@ __all__ = [
623
624
  "DocumentIndexRead",
624
625
  "DocumentIndexesListRequestStatus",
625
626
  "DocumentProcessingState",
627
+ "DocumentPromptBlock",
626
628
  "DocumentRead",
627
629
  "DocumentStatus",
628
630
  "DocumentVellumValue",
@@ -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.14.7",
21
+ "X-Fern-SDK-Version": "0.14.8",
22
22
  }
23
23
  headers["X_API_KEY"] = self.api_key
24
24
  return headers
@@ -87,6 +87,7 @@ from .document_index_indexing_config import DocumentIndexIndexingConfig
87
87
  from .document_index_indexing_config_request import DocumentIndexIndexingConfigRequest
88
88
  from .document_index_read import DocumentIndexRead
89
89
  from .document_processing_state import DocumentProcessingState
90
+ from .document_prompt_block import DocumentPromptBlock
90
91
  from .document_read import DocumentRead
91
92
  from .document_status import DocumentStatus
92
93
  from .document_vellum_value import DocumentVellumValue
@@ -611,6 +612,7 @@ __all__ = [
611
612
  "DocumentIndexIndexingConfigRequest",
612
613
  "DocumentIndexRead",
613
614
  "DocumentProcessingState",
615
+ "DocumentPromptBlock",
614
616
  "DocumentRead",
615
617
  "DocumentStatus",
616
618
  "DocumentVellumValue",
@@ -0,0 +1,29 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ from ..core.pydantic_utilities import UniversalBaseModel
4
+ import typing
5
+ from .prompt_block_state import PromptBlockState
6
+ from .ephemeral_prompt_cache_config import EphemeralPromptCacheConfig
7
+ from ..core.pydantic_utilities import IS_PYDANTIC_V2
8
+ import pydantic
9
+
10
+
11
+ class DocumentPromptBlock(UniversalBaseModel):
12
+ """
13
+ A block that represents a document in a prompt template.
14
+ """
15
+
16
+ block_type: typing.Literal["DOCUMENT"] = "DOCUMENT"
17
+ state: typing.Optional[PromptBlockState] = None
18
+ cache_config: typing.Optional[EphemeralPromptCacheConfig] = None
19
+ src: str
20
+ metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None
21
+
22
+ if IS_PYDANTIC_V2:
23
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
24
+ else:
25
+
26
+ class Config:
27
+ frozen = True
28
+ smart_union = True
29
+ extra = pydantic.Extra.allow
@@ -8,6 +8,7 @@ from .rich_text_prompt_block import RichTextPromptBlock
8
8
  from .audio_prompt_block import AudioPromptBlock
9
9
  from .function_call_prompt_block import FunctionCallPromptBlock
10
10
  from .image_prompt_block import ImagePromptBlock
11
+ from .document_prompt_block import DocumentPromptBlock
11
12
  import typing
12
13
 
13
14
  if typing.TYPE_CHECKING:
@@ -20,4 +21,5 @@ PromptBlock = typing.Union[
20
21
  AudioPromptBlock,
21
22
  FunctionCallPromptBlock,
22
23
  ImagePromptBlock,
24
+ DocumentPromptBlock,
23
25
  ]
@@ -0,0 +1,3 @@
1
+ # WARNING: This file will be removed in a future release. Please import from "vellum.client" instead.
2
+
3
+ from vellum.client.types.document_prompt_block import *
@@ -1,5 +1,9 @@
1
+ import pytest
2
+
1
3
  from vellum import ExecuteApiResponse, VellumSecret as ClientVellumSecret
4
+ from vellum.client.core.api_error import ApiError
2
5
  from vellum.workflows.constants import APIRequestMethod, AuthorizationType
6
+ from vellum.workflows.exceptions import NodeException
3
7
  from vellum.workflows.nodes import APINode
4
8
  from vellum.workflows.state import BaseState
5
9
  from vellum.workflows.types.core import VellumSecret
@@ -32,3 +36,32 @@ def test_run_workflow__secrets(vellum_client):
32
36
  bearer_token = vellum_client.execute_api.call_args.kwargs["bearer_token"]
33
37
  assert bearer_token == ClientVellumSecret(name="secret")
34
38
  assert terminal.headers == {"X-Response-Header": "bar"}
39
+
40
+
41
+ def test_api_node_raises_error_when_api_call_fails(vellum_client):
42
+ # Mock the vellum_client to raise an ApiError
43
+ vellum_client.execute_api.side_effect = ApiError(status_code=400, body="API Error")
44
+
45
+ class SimpleAPINode(APINode):
46
+ method = APIRequestMethod.GET
47
+ authorization_type = AuthorizationType.BEARER_TOKEN
48
+ url = "https://api.vellum.ai"
49
+ body = {
50
+ "key": "value",
51
+ }
52
+ headers = {
53
+ "X-Test-Header": "foo",
54
+ }
55
+ bearer_token_value = VellumSecret(name="api_key")
56
+
57
+ node = SimpleAPINode(state=BaseState())
58
+
59
+ # Assert that the NodeException is raised
60
+ with pytest.raises(NodeException) as excinfo:
61
+ node.run()
62
+
63
+ # Verify that the exception contains some error message
64
+ assert "Failed to prepare HTTP request" in str(excinfo.value)
65
+
66
+ # Verify the vellum_client was called
67
+ assert vellum_client.execute_api.call_count == 1
@@ -89,7 +89,7 @@ class BaseAPINode(BaseNode, Generic[StateType]):
89
89
  url=url, method=method.value, body=data, headers=headers, bearer_token=client_vellum_secret
90
90
  )
91
91
  except ApiError as e:
92
- NodeException(f"Failed to prepare HTTP request: {e}", code=WorkflowErrorCode.NODE_EXECUTION)
92
+ raise NodeException(f"Failed to prepare HTTP request: {e}", code=WorkflowErrorCode.NODE_EXECUTION)
93
93
 
94
94
  return self.Outputs(
95
95
  json=vellum_response.json_,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.7
3
+ Version: 0.14.8
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -1,22 +1,22 @@
1
1
  vellum_cli/CONTRIBUTING.md,sha256=FtDC7BGxSeMnwCXAUssFsAIElXtmJE-O5Z7BpolcgvI,2935
2
2
  vellum_cli/README.md,sha256=2NudRoLzWxNKqnuVy1JuQ7DerIaxWGYkrH8kMd-asIE,90
3
- vellum_cli/__init__.py,sha256=iZzKPaB279edn4xR1CM31OfEL6eHSpYEx9XiuOSgxcU,11923
3
+ vellum_cli/__init__.py,sha256=7aO9XFnaEVRiVshn86cFudebFUccT-gV8xIARJWqKYo,12257
4
4
  vellum_cli/aliased_group.py,sha256=ugW498j0yv4ALJ8vS9MsO7ctDW7Jlir9j6nE_uHAP8c,3363
5
- vellum_cli/config.py,sha256=Bsb3mnvKvv3oOTcCuxpgC7lWPMqt6eJhgRA6VEE-vL4,9266
5
+ vellum_cli/config.py,sha256=aKnhvM5B8QdPA4cQC5Sqg7ImP-vNcVdSkZmk_OBpQTw,9309
6
6
  vellum_cli/image_push.py,sha256=SJwhwWJsLjwGNezNVd_oCVpFMfPsAB3dfLWmriZZUtw,4419
7
- vellum_cli/init.py,sha256=PSLr7YNSijaY7L8O5aSZCAoPCK5jLl7AjsGj0J2jd1A,4787
7
+ vellum_cli/init.py,sha256=WpnMXPItPmh0f0bBGIer3p-e5gu8DUGwSArT_FuoMEw,5093
8
8
  vellum_cli/logger.py,sha256=PuRFa0WCh4sAGFS5aqWB0QIYpS6nBWwPJrIXpWxugV4,1022
9
9
  vellum_cli/ping.py,sha256=lWyJw6sziXjyTopTYRdFF5hV-sYPVDdX0yVbG5fzcY4,585
10
- vellum_cli/pull.py,sha256=1C3K0QU-NIamiFzYGC6nGPl-DtRo1994envfp40gtG0,9085
10
+ vellum_cli/pull.py,sha256=XrlJqImcqZcr6SRGqJ4x3yyvc_0LHDejBcfeVRpY1mY,9169
11
11
  vellum_cli/push.py,sha256=xjTNbLwOVFNU3kpBrm56Bk5QkSRrJ9z86qceghCzfIA,9655
12
12
  vellum_cli/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  vellum_cli/tests/conftest.py,sha256=AFYZryKA2qnUuCPBxBKmHLFoPiE0WhBFFej9tNwSHdc,1526
14
14
  vellum_cli/tests/test_config.py,sha256=uvKGDc8BoVyT9_H0Z-g8469zVxomn6Oi3Zj-vK7O_wU,2631
15
- vellum_cli/tests/test_init.py,sha256=U0RLvnE65q-kE0BCOiyq2dMBhD8IdPse5hpAEPU_QQU,12502
15
+ vellum_cli/tests/test_init.py,sha256=8UOc_ThfouR4ja5cCl_URuLk7ohr9JXfCnG4yka1OUQ,18754
16
16
  vellum_cli/tests/test_main.py,sha256=qDZG-aQauPwBwM6A2DIu1494n47v3pL28XakTbLGZ-k,272
17
17
  vellum_cli/tests/test_ping.py,sha256=QtbhYKMYn1DFnDyBij2mkQO32j9KOpZ5Pf0yek7k_Ao,1284
18
- vellum_cli/tests/test_pull.py,sha256=9UNIeyD7micVhXb_vs9YvBgE-TSdzbSh-f858G0L9i8,29808
19
- vellum_cli/tests/test_push.py,sha256=zDZfSQCHCdKqSfGVHGRgX9VPm-H7EW5gwMf55dm_PFg,23438
18
+ vellum_cli/tests/test_pull.py,sha256=09BkkBoFvqJXIFRxdCu-_a6CE6FtGzqXkXMPaKlcvwE,30178
19
+ vellum_cli/tests/test_push.py,sha256=zDv_Q1hbXtLwmTJDPRAvwDjbuHC09uNRYOy4FQujUow,23476
20
20
  vellum_ee/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  vellum_ee/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  vellum_ee/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -56,11 +56,11 @@ vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py,sha256=4YUaTeD_OWF-
56
56
  vellum_ee/workflows/display/nodes/vellum/try_node.py,sha256=HBfGz4yt9GlmMW9JxzaCacPnHBDNIeXE8Jhqr9DqLLw,6191
57
57
  vellum_ee/workflows/display/nodes/vellum/utils.py,sha256=F_0BrlSszllK_BhryPbojIleLq2dGXOfQD1rVp3fNFg,4733
58
58
  vellum_ee/workflows/display/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- vellum_ee/workflows/display/tests/test_vellum_workflow_display.py,sha256=1EEvkKQRfOKlnpLxE9-hKSsVLLaelM39LY7007LM5dg,4983
59
+ vellum_ee/workflows/display/tests/test_vellum_workflow_display.py,sha256=yfTwpPgOzxJBrUz4cb-T8QQf8lF3TYm-Of40usUNOnc,7494
60
60
  vellum_ee/workflows/display/tests/workflow_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py,sha256=A1-tIpC5KIKG9JA_rkd1nLS8zUG3Kb4QiVdvb3boFxE,2509
63
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=7Fc-TtPw7hz_pvQ-TWz3G8Vy9h2AztukpyDK0p7REGU,9071
63
+ vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py,sha256=9bIAEXXZQDqsUrDJqmHEeWYiZsYkVTQ4jBY-dPFVXEc,15054
64
64
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py,sha256=1cszL6N6FNGVm61MOa7AEiHnF0QjZWqDQuPOp4yiG94,18277
65
65
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py,sha256=-12ZkZb3f5gyoNASV2yeQtMo5HmNsVEo8nXwL6IC-I8,6261
66
66
  vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py,sha256=6th6kCwzql6lddjkTQx4Jbvvs4ChqtJwctW-B4QuBhI,37352
@@ -89,10 +89,10 @@ vellum_ee/workflows/display/utils/expressions.py,sha256=9FpOslDI-RCR5m4TgAu9KCHh
89
89
  vellum_ee/workflows/display/utils/vellum.py,sha256=UjK_RxnSEmlIu9klGCPWU5RAQBmgZ7cRbRdgxaTbubE,8081
90
90
  vellum_ee/workflows/display/vellum.py,sha256=7mqQaKZPPrLMcXSAQkPIxCy5x8HkKs5PbCu3GRaC2o8,8507
91
91
  vellum_ee/workflows/display/workflows/__init__.py,sha256=kapXsC67VJcgSuiBMa86FdePG5A9kMB5Pi4Uy1O2ob4,207
92
- vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=mqP81wMam8Xl0g0qeBrFiCfpUdKqlwySINK28UU8EzM,16974
92
+ vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=hTX1PQGSpuEiuAjlonyxE9V48UzTy4ReczX-dn8oPeY,18655
93
93
  vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=kp0u8LN_2IwshLrhMImhpZx1hRyAcD5gXY-kDuuaGMQ,1269
94
94
  vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=_yB3-u7_bWdD4lUBWpRdWztJmJL-DXkkZaw9Vy9HH6g,3245
95
- vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=3UHe61Em1Tj68ZAR4B6Ucas_vc1BuHlqwbicN-aJMys,17828
95
+ vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=mbAzCpswOek34ITeTkesbVreCXpulj4NFjIg3RcdVZ8,18243
96
96
  vellum_ee/workflows/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  vellum_ee/workflows/server/virtual_file_loader.py,sha256=X_DdNK7MfyOjKWekk6YQpOSCT6klKcdjT6nVJcBH1sM,1481
98
98
  vellum_ee/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -117,12 +117,12 @@ vellum_ee/workflows/tests/local_workflow/workflow.py,sha256=A4qOzOPNwePYxWbcAgIP
117
117
  vellum_ee/workflows/tests/test_display_meta.py,sha256=pzdqND4KLWs7EUIbpXuqgso7BIRpoUsO3T_bgeENs0Q,2205
118
118
  vellum_ee/workflows/tests/test_server.py,sha256=SvKUrUPmOf3sIInXcFjETekql60npb4cAn1GPbF0bPs,391
119
119
  vellum_ee/workflows/tests/test_virtual_files.py,sha256=TJEcMR0v2S8CkloXNmCHA0QW0K6pYNGaIjraJz7sFvY,2762
120
- vellum/__init__.py,sha256=H0euWMXN6DXaXsgvY0ba4GtmJEn1JwO5f9qJVViITqE,36386
120
+ vellum/__init__.py,sha256=a_aM1_A04XGma4MAIDNeBF9BKzWbiQaVVMRzImHuxjA,36438
121
121
  vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
122
122
  vellum/client/__init__.py,sha256=tKtdM1_GqmGq1gpi9ydWD_T-MM7fPn8QdHh8ww19cNI,117564
123
123
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
124
124
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
125
- vellum/client/core/client_wrapper.py,sha256=IOYqPR7q1d-R9Sq_3i9aSQ7xQhmjVSXHDCAcwE7puAY,1868
125
+ vellum/client/core/client_wrapper.py,sha256=NOBkPB9txdePCb8-MgjYVJwNHQWPG9yrRX2B7sxaT-o,1868
126
126
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
127
127
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
128
128
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -187,7 +187,7 @@ vellum/client/resources/workspace_secrets/__init__.py,sha256=FTtvy8EDg9nNNg9WCat
187
187
  vellum/client/resources/workspace_secrets/client.py,sha256=h7UzXLyTttPq1t-JZGMg1BWxypxJvBGUdqg7KGT7MK4,8027
188
188
  vellum/client/resources/workspaces/__init__.py,sha256=FTtvy8EDg9nNNg9WCatVgKTRYV8-_v1roeGPAKoa_pw,65
189
189
  vellum/client/resources/workspaces/client.py,sha256=RthwzN1o-Jxwg5yyNNodavFyNUSxfLoTv26w3mRR5g8,3595
190
- vellum/client/types/__init__.py,sha256=2WHJ08IyAIERcvKnSDa_3yvK72KNO7SREB5wmxRFxDU,54768
190
+ vellum/client/types/__init__.py,sha256=_1pPNxQxjYSBB4L-j9HM1CHRaOEQNZa6XekVrKrseqg,54850
191
191
  vellum/client/types/ad_hoc_execute_prompt_event.py,sha256=bCjujA2XsOgyF3bRZbcEqV2rOIymRgsLoIRtZpB14xg,607
192
192
  vellum/client/types/ad_hoc_expand_meta.py,sha256=1gv-NCsy_6xBYupLvZH979yf2VMdxAU-l0y0ynMKZaw,1331
193
193
  vellum/client/types/ad_hoc_fulfilled_prompt_execution_meta.py,sha256=Bfvf1d_dkmshxRACVM5vcxbH_7AQY23RmrrnPc0ytYY,939
@@ -267,6 +267,7 @@ vellum/client/types/document_index_indexing_config.py,sha256=xL1pCzUOkw5sSie1OrB
267
267
  vellum/client/types/document_index_indexing_config_request.py,sha256=Wt-ys1o_acHNyLU0c1laG2PVT7rgCfwO54f5nudAxk4,832
268
268
  vellum/client/types/document_index_read.py,sha256=cXL115A4h-TFiGc29tAkXb1pkuK0RzIquyOu1Pv7Jug,1469
269
269
  vellum/client/types/document_processing_state.py,sha256=ISlurj7jQzwHzxPzDZTqeAIgSIIGMBBPgcOSoe04pTU,211
270
+ vellum/client/types/document_prompt_block.py,sha256=sgFxN48PILFuuF2KUIwks6PbJ3XH6sCE_8ydLEE_doU,1019
270
271
  vellum/client/types/document_read.py,sha256=6nwEvVvVe-6y2vtPNYB7KtcFoaydH2ow-WhCmCAvMQ8,1713
271
272
  vellum/client/types/document_status.py,sha256=GD_TSoFmZUBJnPl-chAmaQFzQ2_TYO3PSqi3-9QfEHE,122
272
273
  vellum/client/types/document_vellum_value.py,sha256=a8WQhyntwy80iN9j8L9F5v6Jmq1L4j0ETJo9c9VGabs,768
@@ -453,7 +454,7 @@ vellum/client/types/pdf_search_result_meta_source_request.py,sha256=nUhaD2Kw1paG
453
454
  vellum/client/types/plain_text_prompt_block.py,sha256=cqEN-B4mcvMw_9lBN7FQG8pk9b5LBJ9xpM6PTgkGiqs,930
454
455
  vellum/client/types/price.py,sha256=ewzXDBVLaleuXMVQ-gQ3G1Nl5J2OWOVEMEFfnQIpiTk,610
455
456
  vellum/client/types/processing_failure_reason_enum.py,sha256=R_KIW7TcQejhc-vLhtNf9SdkYADgoZCn4ch4_RRIvsI,195
456
- vellum/client/types/prompt_block.py,sha256=PK3NMPLg0NSmrr7JpMQcbNzCo8DjTP9xvg6ENd2bJZk,747
457
+ vellum/client/types/prompt_block.py,sha256=quAME4X2doCO_DQ-U7v0Py-ZZy1Z5qypVVq2fXuazpw,827
457
458
  vellum/client/types/prompt_block_state.py,sha256=BRAzTYARoSU36IVZGWMeeqhl5fgFMXCyhJ8rCbfB-f0,163
458
459
  vellum/client/types/prompt_deployment_expand_meta_request.py,sha256=agsiAaHB6lDoZPlnfJ2nmhB4Ud4EiJJTX05YmduyCPo,1910
459
460
  vellum/client/types/prompt_deployment_input_request.py,sha256=KrT4-Ew2VvTWXEkYQz2oyHn5EDOgrMW7FzRFaPH3ARg,353
@@ -857,6 +858,7 @@ vellum/types/document_index_indexing_config.py,sha256=q-thOinZy-BBQsKXZcw2jRu3cA
857
858
  vellum/types/document_index_indexing_config_request.py,sha256=m9fL0NlibO4iTqVaJM90VFUQNvV9aG5b57NNh0hvgU0,176
858
859
  vellum/types/document_index_read.py,sha256=7053CeFkTD9X5MRrVpiCRwKHGAQNtzNd6LnVCDePsM0,157
859
860
  vellum/types/document_processing_state.py,sha256=7EKGnlG1AFm62N_xxeWVrbRVfSrNeJ_3rbnZAlle1nQ,163
861
+ vellum/types/document_prompt_block.py,sha256=ioBoNvFp4GpAuQhiu6EnipQb4AG1laY2uHYOdOB8NHg,159
860
862
  vellum/types/document_read.py,sha256=9LR65w4jvzOg-ji8ioucO2MWUuH4RGvIWrKKu03CNQ4,151
861
863
  vellum/types/document_status.py,sha256=RmqdB8mCPuha4ARvKiG6T60PjyoTFUFxCgzuK9HA1HY,153
862
864
  vellum/types/document_vellum_value.py,sha256=S5stAYdvKrIeKu7HY-DT0s4KYvObKL46ohgRYRVy3VA,159
@@ -1385,10 +1387,10 @@ vellum/workflows/nodes/displayable/__init__.py,sha256=6F_4DlSwvHuilWnIalp8iDjjDX
1385
1387
  vellum/workflows/nodes/displayable/api_node/__init__.py,sha256=MoxdQSnidIj1Nf_d-hTxlOxcZXaZnsWFDbE-PkTK24o,56
1386
1388
  vellum/workflows/nodes/displayable/api_node/node.py,sha256=QdpsyGVxo5PcN8nwGZpcpW_YMKHr3_VvmbK1BlrdOFk,2547
1387
1389
  vellum/workflows/nodes/displayable/api_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1388
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py,sha256=yo3zTMRxgpSdWmJ68X610A5rrtCchyfqhcDd2X-GJiU,1249
1390
+ vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py,sha256=Ta-ZkZvllPHpLamiDRdEtVlwBJUFcvBHpyKLY6q06_A,2309
1389
1391
  vellum/workflows/nodes/displayable/bases/__init__.py,sha256=0mWIx3qUrzllV7jqt7wN03vWGMuI1WrrLZeMLT2Cl2c,304
1390
1392
  vellum/workflows/nodes/displayable/bases/api_node/__init__.py,sha256=1jwx4WC358CLA1jgzl_UD-rZmdMm2v9Mps39ndwCD7U,64
1391
- vellum/workflows/nodes/displayable/bases/api_node/node.py,sha256=TeVsAhUPEx_lbiyhGcWarqBKZeJJZAJx8mwym9qhwVs,3994
1393
+ vellum/workflows/nodes/displayable/bases/api_node/node.py,sha256=-LOKjU_rY1UWgD0DS5LJwAClBI8N7zrdmwigE3y5rhc,4000
1392
1394
  vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py,sha256=Org3xTvgp1pA0uUXFfnJr29D3HzCey2lEdYF4zbIUgo,70
1393
1395
  vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py,sha256=nvhoWb8EyRlgtyotYp-wh194n30yQP81UnOH_a8FghY,3140
1394
1396
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py,sha256=Hl35IAoepRpE-j4cALaXVJIYTYOF3qszyVbxTj4kS1s,82
@@ -1500,8 +1502,8 @@ vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnad
1500
1502
  vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1501
1503
  vellum/workflows/workflows/tests/test_base_workflow.py,sha256=NRteiICyJvDM5zrtUfq2fZoXcGQVaWC9xmNlLLVW0cU,7979
1502
1504
  vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
1503
- vellum_ai-0.14.7.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1504
- vellum_ai-0.14.7.dist-info/METADATA,sha256=mTna7Oy2D73zzWO2xRDZLwbd4cgn5ZirVQJ154t1_P4,5407
1505
- vellum_ai-0.14.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1506
- vellum_ai-0.14.7.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1507
- vellum_ai-0.14.7.dist-info/RECORD,,
1505
+ vellum_ai-0.14.8.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1506
+ vellum_ai-0.14.8.dist-info/METADATA,sha256=rjXn1UMzW7AdriljPfHof7rjjp7GseRXLTfRswDoSBc,5407
1507
+ vellum_ai-0.14.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1508
+ vellum_ai-0.14.8.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1509
+ vellum_ai-0.14.8.dist-info/RECORD,,
vellum_cli/__init__.py CHANGED
@@ -361,10 +361,17 @@ def image_push(image: str, tag: Optional[List[str]] = None) -> None:
361
361
 
362
362
  @workflows.command(name="init")
363
363
  @click.argument("template_name", required=False)
364
- def workflows_init(template_name: Optional[str] = None) -> None:
364
+ @click.option(
365
+ "--target-dir",
366
+ "target_directory", # Internal parameter name is target_directory
367
+ type=str,
368
+ help="""Directory to pull the workflow into. If not specified, \
369
+ the workflow will be pulled into the current working directory.""",
370
+ )
371
+ def workflows_init(template_name: Optional[str] = None, target_directory: Optional[str] = None) -> None:
365
372
  """Initialize a new Vellum Workflow using a predefined template"""
366
373
 
367
- init_command(template_name=template_name)
374
+ init_command(template_name=template_name, target_directory=target_directory)
368
375
 
369
376
 
370
377
  if __name__ == "__main__":
vellum_cli/config.py CHANGED
@@ -50,6 +50,7 @@ class WorkflowConfig(UniversalBaseModel):
50
50
  container_image_name: Optional[str] = None
51
51
  container_image_tag: Optional[str] = None
52
52
  workspace: str = DEFAULT_WORKSPACE_CONFIG.name
53
+ target_directory: Optional[str] = None
53
54
 
54
55
  def merge(self, other: "WorkflowConfig") -> "WorkflowConfig":
55
56
  self_deployment_by_id = {
vellum_cli/init.py CHANGED
@@ -18,7 +18,7 @@ ERROR_LOG_FILE_NAME = "error.log"
18
18
  METADATA_FILE_NAME = "metadata.json"
19
19
 
20
20
 
21
- def init_command(template_name: Optional[str] = None):
21
+ def init_command(template_name: Optional[str] = None, target_directory: Optional[str] = None):
22
22
  load_dotenv()
23
23
  logger = load_cli_logger()
24
24
  config = load_vellum_cli_config()
@@ -64,7 +64,11 @@ def init_command(template_name: Optional[str] = None):
64
64
  if not pk:
65
65
  raise ValueError("No workflow sandbox ID found in project to pull from.")
66
66
 
67
- target_dir = os.path.join(os.getcwd(), workflow_config.module)
67
+ # Use target_directory if provided, otherwise use current working directory
68
+ base_dir = os.path.join(os.getcwd(), target_directory) if target_directory else os.getcwd()
69
+ target_dir = os.path.join(base_dir, *workflow_config.module.split("."))
70
+ workflow_config.target_directory = target_dir if target_directory else None
71
+
68
72
  if os.path.exists(target_dir):
69
73
  click.echo(click.style(f"{target_dir} already exists.", fg="red"))
70
74
  return
vellum_cli/pull.py CHANGED
@@ -179,6 +179,7 @@ def pull_command(
179
179
  # Use target_directory if provided, otherwise use current working directory
180
180
  base_dir = os.path.join(os.getcwd(), target_directory) if target_directory else os.getcwd()
181
181
  target_dir = os.path.join(base_dir, *workflow_config.module.split("."))
182
+ workflow_config.target_directory = target_dir if target_directory else None
182
183
 
183
184
  # Delete files in target_dir that aren't in the zip file
184
185
  if os.path.exists(target_dir):
@@ -1,4 +1,3 @@
1
- import pytest
2
1
  import io
3
2
  import json
4
3
  import os
@@ -33,14 +32,7 @@ class MockTemplate:
33
32
  self.label = label
34
33
 
35
34
 
36
- @pytest.mark.parametrize(
37
- "base_command",
38
- [
39
- ["workflows", "init"],
40
- ],
41
- ids=["workflows_init"],
42
- )
43
- def test_init_command(vellum_client, mock_module, base_command):
35
+ def test_init_command(vellum_client, mock_module):
44
36
  # GIVEN a module on the user's filesystem
45
37
  temp_dir = mock_module.temp_dir
46
38
  mock_module.set_pyproject_toml({"workflows": []})
@@ -52,18 +44,11 @@ def test_init_command(vellum_client, mock_module, base_command):
52
44
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
53
45
 
54
46
  # AND the workflow pull API call returns a zip file
55
- vellum_client.workflows.pull.return_value = iter(
56
- [
57
- _zip_file_map(
58
- {
59
- "workflow.py": "print('hello')",
60
- }
61
- )
62
- ]
63
- )
47
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
48
+
64
49
  # WHEN the user runs the `init` command and selects the first template
65
50
  runner = CliRunner()
66
- result = runner.invoke(cli_main, base_command, input="1\n")
51
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
67
52
 
68
53
  # THEN the command returns successfully
69
54
  assert result.exit_code == 0
@@ -94,18 +79,12 @@ def test_init_command(vellum_client, mock_module, base_command):
94
79
  "container_image_name": None,
95
80
  "container_image_tag": None,
96
81
  "workspace": "default",
82
+ "target_directory": None,
97
83
  }
98
84
  ]
99
85
 
100
86
 
101
- @pytest.mark.parametrize(
102
- "base_command",
103
- [
104
- ["workflows", "init"],
105
- ],
106
- ids=["workflows_init"],
107
- )
108
- def test_init_command__invalid_template_id(vellum_client, mock_module, base_command):
87
+ def test_init_command__invalid_template_id(vellum_client, mock_module):
109
88
  # GIVEN a module on the user's filesystem
110
89
  temp_dir = mock_module.temp_dir
111
90
  mock_module.set_pyproject_toml({"workflows": []})
@@ -121,7 +100,7 @@ def test_init_command__invalid_template_id(vellum_client, mock_module, base_comm
121
100
  # Mock click.prompt to raise a KeyboardInterrupt (simulating Ctrl+C)
122
101
  with patch("click.prompt", side_effect=KeyboardInterrupt):
123
102
  runner = CliRunner()
124
- result = runner.invoke(cli_main, base_command)
103
+ result = runner.invoke(cli_main, ["workflows", "init"])
125
104
 
126
105
  # THEN the command is aborted
127
106
  assert result.exit_code != 0
@@ -142,14 +121,7 @@ def test_init_command__invalid_template_id(vellum_client, mock_module, base_comm
142
121
  assert lock_data["workflows"] == []
143
122
 
144
123
 
145
- @pytest.mark.parametrize(
146
- "base_command",
147
- [
148
- ["workflows", "init"],
149
- ],
150
- ids=["workflows_init"],
151
- )
152
- def test_init_command__no_templates(vellum_client, mock_module, base_command):
124
+ def test_init_command__no_templates(vellum_client, mock_module):
153
125
  # GIVEN a module on the user's filesystem
154
126
  temp_dir = mock_module.temp_dir
155
127
  mock_module.set_pyproject_toml({"workflows": []})
@@ -158,7 +130,7 @@ def test_init_command__no_templates(vellum_client, mock_module, base_command):
158
130
 
159
131
  # WHEN the user runs the `init` command
160
132
  runner = CliRunner()
161
- result = runner.invoke(cli_main, base_command)
133
+ result = runner.invoke(cli_main, ["workflows", "init"])
162
134
 
163
135
  # THEN the command gracefully exits
164
136
  assert result.exit_code == 0
@@ -179,14 +151,7 @@ def test_init_command__no_templates(vellum_client, mock_module, base_command):
179
151
  assert lock_data["workflows"] == []
180
152
 
181
153
 
182
- @pytest.mark.parametrize(
183
- "base_command",
184
- [
185
- ["workflows", "init"],
186
- ],
187
- ids=["workflows_init"],
188
- )
189
- def test_init_command_target_directory_exists(vellum_client, mock_module, base_command):
154
+ def test_init_command_target_directory_exists(vellum_client, mock_module):
190
155
  """
191
156
  GIVEN a target directory already exists
192
157
  WHEN the user tries to run the `init` command
@@ -208,19 +173,11 @@ def test_init_command_target_directory_exists(vellum_client, mock_module, base_c
208
173
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
209
174
 
210
175
  # AND the workflow pull API call returns a zip file
211
- vellum_client.workflows.pull.return_value = iter(
212
- [
213
- _zip_file_map(
214
- {
215
- "workflow.py": "print('hello')",
216
- }
217
- )
218
- ]
219
- )
176
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
220
177
 
221
178
  # WHEN the user runs the `init` command and selects the template
222
179
  runner = CliRunner()
223
- result = runner.invoke(cli_main, base_command, input="1\n")
180
+ result = runner.invoke(cli_main, ["workflows", "init"], input="1\n")
224
181
 
225
182
  # THEN the command should detect the existing directory and abort
226
183
  assert result.exit_code == 0
@@ -244,14 +201,7 @@ def test_init_command_target_directory_exists(vellum_client, mock_module, base_c
244
201
  assert lock_data["workflows"] == []
245
202
 
246
203
 
247
- @pytest.mark.parametrize(
248
- "base_command",
249
- [
250
- ["workflows", "init"],
251
- ],
252
- ids=["workflows_init"],
253
- )
254
- def test_init_command_with_template_name(vellum_client, mock_module, base_command):
204
+ def test_init_command_with_template_name(vellum_client, mock_module):
255
205
  # GIVEN a module on the user's filesystem
256
206
  temp_dir = mock_module.temp_dir
257
207
  mock_module.set_pyproject_toml({"workflows": []})
@@ -264,14 +214,12 @@ def test_init_command_with_template_name(vellum_client, mock_module, base_comman
264
214
  vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
265
215
 
266
216
  # AND the workflow pull API call returns a zip file
267
- vellum_client.workflows.pull.return_value = iter(
268
- [_zip_file_map({"workflow.py": "print('hello')", "README.md": "# Another Workflow\nThis is a test template."})]
269
- )
217
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
270
218
 
271
219
  # WHEN the user runs the `init` command with a specific template name
272
220
  template_name = snake_case("Another Workflow")
273
221
  runner = CliRunner()
274
- result = runner.invoke(cli_main, base_command + [template_name])
222
+ result = runner.invoke(cli_main, ["workflows", "init", template_name])
275
223
 
276
224
  # THEN the command returns successfully
277
225
  assert result.exit_code == 0
@@ -305,18 +253,12 @@ def test_init_command_with_template_name(vellum_client, mock_module, base_comman
305
253
  "container_image_name": None,
306
254
  "container_image_tag": None,
307
255
  "workspace": "default",
256
+ "target_directory": None,
308
257
  }
309
258
  ]
310
259
 
311
260
 
312
- @pytest.mark.parametrize(
313
- "base_command",
314
- [
315
- ["workflows", "init"],
316
- ],
317
- ids=["workflows_init"],
318
- )
319
- def test_init_command_with_nonexistent_template_name(vellum_client, mock_module, base_command):
261
+ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module):
320
262
  # GIVEN a module on the user's filesystem
321
263
  temp_dir = mock_module.temp_dir
322
264
  mock_module.set_pyproject_toml({"workflows": []})
@@ -331,7 +273,7 @@ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module,
331
273
  # WHEN the user runs the `init` command with a non-existent template name
332
274
  nonexistent_template = "nonexistent_template"
333
275
  runner = CliRunner()
334
- result = runner.invoke(cli_main, base_command + [nonexistent_template])
276
+ result = runner.invoke(cli_main, ["workflows", "init", nonexistent_template])
335
277
 
336
278
  # THEN the command should indicate the template was not found
337
279
  assert result.exit_code == 0
@@ -353,3 +295,179 @@ def test_init_command_with_nonexistent_template_name(vellum_client, mock_module,
353
295
  with open(vellum_lock_json) as f:
354
296
  lock_data = json.load(f)
355
297
  assert lock_data["workflows"] == []
298
+
299
+
300
+ def test_init__with_target_dir(vellum_client, mock_module):
301
+ # GIVEN a module on the user's filesystem
302
+ temp_dir = mock_module.temp_dir
303
+ mock_module.set_pyproject_toml({"workflows": []})
304
+
305
+ # GIVEN the vellum client returns a list of template workflows
306
+ fake_templates = [
307
+ MockTemplate(id="template-1", label="Example Workflow"),
308
+ ]
309
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
310
+
311
+ # AND the workflow pull API call returns a zip file
312
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
313
+
314
+ # AND a target directory
315
+ target_dir = os.path.join(temp_dir, "dir")
316
+ os.makedirs(target_dir, exist_ok=True)
317
+
318
+ # WHEN the user runs the init command with target-dir
319
+ runner = CliRunner()
320
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", target_dir], input="1\n")
321
+
322
+ # THEN the command returns successfully
323
+ assert result.exit_code == 0
324
+
325
+ # AND the `workflow.py` file should be created in the target directory
326
+ module_path = os.path.join(target_dir, "example_workflow")
327
+ workflow_py = os.path.join(module_path, "workflow.py")
328
+ assert os.path.exists(workflow_py)
329
+ with open(workflow_py) as f:
330
+ assert f.read() == "print('hello')"
331
+
332
+ # AND the files are not in the default module directory
333
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
334
+ assert not os.path.exists(default_module_path)
335
+
336
+ # AND the vellum.lock.json file should be created in the original directory
337
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
338
+ assert os.path.exists(vellum_lock_json)
339
+ with open(vellum_lock_json) as f:
340
+ lock_data = json.load(f)
341
+ assert lock_data["workflows"] == [
342
+ {
343
+ "module": "example_workflow",
344
+ "workflow_sandbox_id": "template-1",
345
+ "ignore": None,
346
+ "deployments": [],
347
+ "container_image_name": None,
348
+ "container_image_tag": None,
349
+ "workspace": "default",
350
+ "target_directory": module_path,
351
+ }
352
+ ]
353
+
354
+
355
+ def test_init__with_nested_target_dir(vellum_client, mock_module):
356
+ # GIVEN a module on the user's filesystem
357
+ temp_dir = mock_module.temp_dir
358
+ mock_module.set_pyproject_toml({"workflows": []})
359
+
360
+ # GIVEN the vellum client returns a list of template workflows
361
+ fake_templates = [
362
+ MockTemplate(id="template-1", label="Example Workflow"),
363
+ ]
364
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
365
+
366
+ # AND the workflow pull API call returns a zip file
367
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
368
+
369
+ # AND a nested target directory that doesn't exist yet
370
+ nested_target_dir = os.path.join(temp_dir, "dir-1", "dir-2")
371
+
372
+ # WHEN the user runs the init command with nested target-dir
373
+ runner = CliRunner()
374
+ result = runner.invoke(cli_main, ["workflows", "init", "--target-dir", nested_target_dir], input="1\n")
375
+
376
+ # THEN the command returns successfully
377
+ assert result.exit_code == 0
378
+
379
+ # AND the nested directory with module subdirectory should be created
380
+ module_path = os.path.join(nested_target_dir, "example_workflow")
381
+ assert os.path.exists(module_path)
382
+
383
+ # AND the workflow.py file is written to the nested target directory
384
+ workflow_py = os.path.join(module_path, "workflow.py")
385
+ assert os.path.exists(workflow_py)
386
+ with open(workflow_py) as f:
387
+ assert f.read() == "print('hello')"
388
+
389
+ # AND the files are not in the default module directory
390
+ default_module_path = os.path.join(temp_dir, "example_workflow", "workflow.py")
391
+ assert not os.path.exists(default_module_path)
392
+
393
+ # AND the vellum.lock.json file is still updated
394
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
395
+ assert os.path.exists(vellum_lock_json)
396
+ with open(vellum_lock_json) as f:
397
+ lock_data = json.load(f)
398
+ assert lock_data["workflows"] == [
399
+ {
400
+ "module": "example_workflow",
401
+ "workflow_sandbox_id": "template-1",
402
+ "ignore": None,
403
+ "deployments": [],
404
+ "container_image_name": None,
405
+ "container_image_tag": None,
406
+ "workspace": "default",
407
+ "target_directory": module_path,
408
+ }
409
+ ]
410
+
411
+
412
+ def test_init__with_template_name_and_target_dir(vellum_client, mock_module):
413
+ # GIVEN a module on the user's filesystem
414
+ temp_dir = mock_module.temp_dir
415
+ mock_module.set_pyproject_toml({"workflows": []})
416
+
417
+ # GIVEN the vellum client returns a list of template workflows
418
+ fake_templates = [
419
+ MockTemplate(id="template-1", label="Example Workflow"),
420
+ MockTemplate(id="template-2", label="Another Workflow"),
421
+ ]
422
+ vellum_client.workflow_sandboxes.list_workflow_sandbox_examples.return_value.results = fake_templates
423
+
424
+ # AND the workflow pull API call returns a zip file
425
+ vellum_client.workflows.pull.return_value = iter([_zip_file_map({"workflow.py": "print('hello')"})])
426
+
427
+ # AND a target directory
428
+ target_dir = os.path.join(temp_dir, "dir")
429
+ os.makedirs(target_dir, exist_ok=True)
430
+
431
+ # WHEN the user runs the init command with a specific template name and target-dir
432
+ template_name = snake_case("Another Workflow")
433
+ runner = CliRunner()
434
+ result = runner.invoke(cli_main, ["workflows", "init", template_name, "--target-dir", target_dir])
435
+
436
+ # THEN the command returns successfully
437
+ assert result.exit_code == 0
438
+
439
+ # AND `vellum_client.workflows.pull` is called with the correct template ID
440
+ vellum_client.workflows.pull.assert_called_once_with(
441
+ "template-2", # ID of "Another Workflow"
442
+ request_options={"additional_query_parameters": {"include_sandbox": True}},
443
+ )
444
+
445
+ # AND the workflow files should be created in the target directory with the correct module subdirectory
446
+ module_path = os.path.join(target_dir, "another_workflow")
447
+ workflow_py = os.path.join(module_path, "workflow.py")
448
+ assert os.path.exists(workflow_py)
449
+ with open(workflow_py) as f:
450
+ assert f.read() == "print('hello')"
451
+
452
+ # AND the files are not in the default module directory
453
+ default_module_path = os.path.join(temp_dir, "another_workflow", "workflow.py")
454
+ assert not os.path.exists(default_module_path)
455
+
456
+ # AND the vellum.lock.json file should be created with the correct data
457
+ vellum_lock_json = os.path.join(temp_dir, "vellum.lock.json")
458
+ assert os.path.exists(vellum_lock_json)
459
+
460
+ with open(vellum_lock_json) as f:
461
+ lock_data = json.load(f)
462
+ assert lock_data["workflows"] == [
463
+ {
464
+ "module": "another_workflow",
465
+ "workflow_sandbox_id": "template-2",
466
+ "ignore": None,
467
+ "deployments": [],
468
+ "container_image_name": None,
469
+ "container_image_tag": None,
470
+ "workspace": "default",
471
+ "target_directory": module_path,
472
+ }
473
+ ]
@@ -73,6 +73,7 @@ def test_pull(vellum_client, mock_module, base_command):
73
73
  "ignore": None,
74
74
  "deployments": [],
75
75
  "workspace": "default",
76
+ "target_directory": None,
76
77
  }
77
78
  ],
78
79
  "workspaces": [],
@@ -167,6 +168,7 @@ def test_pull__with_target_dir(vellum_client, mock_module, base_command):
167
168
  "ignore": None,
168
169
  "deployments": [],
169
170
  "workspace": "default",
171
+ "target_directory": module_path,
170
172
  }
171
173
  ],
172
174
  "workspaces": [],
@@ -233,6 +235,7 @@ def test_pull__with_nested_target_dir(vellum_client, mock_module, base_command):
233
235
  "ignore": None,
234
236
  "deployments": [],
235
237
  "workspace": "default",
238
+ "target_directory": module_path,
236
239
  }
237
240
  ],
238
241
  "workspaces": [],
@@ -289,6 +292,7 @@ def test_pull__sandbox_id_with_no_config(vellum_client):
289
292
  "container_image_tag": None,
290
293
  "container_image_name": None,
291
294
  "workspace": "default",
295
+ "target_directory": None,
292
296
  }
293
297
  ],
294
298
  }
@@ -372,6 +376,7 @@ def test_pull__workflow_deployment_with_no_config(vellum_client):
372
376
  "container_image_tag": None,
373
377
  "container_image_name": None,
374
378
  "workspace": "default",
379
+ "target_directory": None,
375
380
  }
376
381
  ],
377
382
  "workspaces": [],
@@ -619,6 +624,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
619
624
  "container_image_name": None,
620
625
  "container_image_tag": None,
621
626
  "workspace": "default",
627
+ "target_directory": None,
622
628
  },
623
629
  {
624
630
  "module": "super_cool_workflow",
@@ -628,6 +634,7 @@ def test_pull__sandbox_id_with_other_workflow_deployment_in_lock(vellum_client,
628
634
  "container_image_name": "test",
629
635
  "container_image_tag": "1.0",
630
636
  "workspace": "default",
637
+ "target_directory": None,
631
638
  },
632
639
  ]
633
640
 
@@ -771,6 +778,7 @@ def test_pull__module_not_in_config(vellum_client, mock_module):
771
778
  "container_image_name": None,
772
779
  "container_image_tag": None,
773
780
  "workspace": "default",
781
+ "target_directory": None,
774
782
  }
775
783
  ]
776
784
 
@@ -508,6 +508,7 @@ MY_OTHER_VELLUM_API_KEY=aaabbbcccddd
508
508
  "container_image_tag": None,
509
509
  "deployments": [],
510
510
  "ignore": None,
511
+ "target_directory": None,
511
512
  }
512
513
 
513
514
 
@@ -1,4 +1,5 @@
1
1
  from uuid import UUID
2
+ from typing import Dict
2
3
 
3
4
  from vellum.workflows.inputs import BaseInputs
4
5
  from vellum.workflows.nodes import BaseNode
@@ -145,3 +146,85 @@ def test_vellum_workflow_display_serialize_valid_handle_ids_for_base_nodes():
145
146
  assert (
146
147
  node["trigger"]["id"] in edge_target_handle_ids
147
148
  ), f"Trigger {node['trigger']['id']} from node {node['label']} not found in edge target handle ids"
149
+
150
+
151
+ def test_vellum_workflow_display__serialize_with_unused_nodes_and_edges():
152
+ # GIVEN a workflow with active and unused nodes
153
+ class NodeA(BaseNode):
154
+ class Outputs(BaseNode.Outputs):
155
+ result: str
156
+
157
+ class NodeB(BaseNode):
158
+ pass
159
+
160
+ class NodeC(BaseNode):
161
+ pass
162
+
163
+ # AND A workflow that uses them correctly
164
+ class Workflow(BaseWorkflow):
165
+ graph = NodeA
166
+ unused_graphs = {NodeB >> NodeC}
167
+
168
+ class Outputs(BaseWorkflow.Outputs):
169
+ final = NodeA.Outputs.result
170
+
171
+ # WHEN we serialize it
172
+ workflow_display = get_workflow_display(
173
+ base_display_class=VellumWorkflowDisplay,
174
+ workflow_class=Workflow,
175
+ )
176
+
177
+ # WHEN we serialize the workflow
178
+ exec_config = workflow_display.serialize()
179
+
180
+ # THEN the serialized workflow contains the expected nodes and edges
181
+ raw_data = exec_config["workflow_raw_data"]
182
+ assert isinstance(raw_data, dict)
183
+
184
+ nodes = raw_data["nodes"]
185
+ edges = raw_data["edges"]
186
+
187
+ assert isinstance(nodes, list)
188
+ assert isinstance(edges, list)
189
+
190
+ # Find nodes by their definition name
191
+ node_ids: Dict[str, str] = {}
192
+
193
+ for node in nodes:
194
+ assert isinstance(node, dict)
195
+ definition = node.get("definition")
196
+ if definition is None:
197
+ continue
198
+
199
+ assert isinstance(definition, dict)
200
+ name = definition.get("name")
201
+ if not isinstance(name, str):
202
+ continue
203
+
204
+ if name in ["NodeA", "NodeB", "NodeC"]:
205
+ node_id = node.get("id")
206
+ if isinstance(node_id, str):
207
+ node_ids[name] = node_id
208
+
209
+ # Verify all nodes are present
210
+ assert "NodeA" in node_ids, "Active node NodeA not found in serialized output"
211
+ assert "NodeB" in node_ids, "Unused node NodeB not found in serialized output"
212
+ assert "NodeC" in node_ids, "Unused node NodeC not found in serialized output"
213
+
214
+ # Verify the edge between NodeB and NodeC is present
215
+ edge_found = False
216
+ for edge in edges:
217
+ assert isinstance(edge, dict)
218
+ source_id = edge.get("source_node_id")
219
+ target_id = edge.get("target_node_id")
220
+
221
+ if (
222
+ isinstance(source_id, str)
223
+ and isinstance(target_id, str)
224
+ and source_id == node_ids["NodeB"]
225
+ and target_id == node_ids["NodeC"]
226
+ ):
227
+ edge_found = True
228
+ break
229
+
230
+ assert edge_found, "Edge between unused nodes NodeB and NodeC not found in serialized output"
@@ -1,4 +1,3 @@
1
- import pytest
2
1
  from uuid import uuid4
3
2
 
4
3
  from deepdiff import DeepDiff
@@ -217,7 +216,6 @@ def test_serialize_node__try(serialize_node):
217
216
  )
218
217
 
219
218
 
220
- @pytest.mark.skip(reason="Not implemented")
221
219
  def test_serialize_node__stacked():
222
220
  @TryNode.wrap()
223
221
  @RetryNode.wrap(max_attempts=5)
@@ -236,4 +234,121 @@ def test_serialize_node__stacked():
236
234
  exec_config = workflow_display.serialize()
237
235
 
238
236
  # THEN the workflow display is created successfully
239
- assert exec_config is not None
237
+ assert not DeepDiff(
238
+ {
239
+ "workflow_raw_data": {
240
+ "nodes": [
241
+ {
242
+ "id": "c14c1c9b-a7a4-4d2c-84fb-c940cfb09525",
243
+ "type": "ENTRYPOINT",
244
+ "inputs": [],
245
+ "data": {
246
+ "label": "Entrypoint Node",
247
+ "source_handle_id": "51a5eb25-af14-4bee-9ced-d2aa534ea8e9",
248
+ },
249
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
250
+ "base": None,
251
+ "definition": None,
252
+ },
253
+ {
254
+ "id": "074833b0-e142-4bbc-8dec-209a35e178a3",
255
+ "label": "test_serialize_node__stacked.<locals>.InnerStackedGenericNode",
256
+ "type": "GENERIC",
257
+ "display_data": {"position": {"x": 0.0, "y": 0.0}},
258
+ "base": {"name": "BaseNode", "module": ["vellum", "workflows", "nodes", "bases", "base"]},
259
+ "definition": {
260
+ "name": "InnerStackedGenericNode",
261
+ "module": [
262
+ "vellum_ee",
263
+ "workflows",
264
+ "display",
265
+ "tests",
266
+ "workflow_serialization",
267
+ "generic_nodes",
268
+ "test_adornments_serialization",
269
+ ],
270
+ },
271
+ "trigger": {"id": "f206358d-04a5-41c9-beee-0871a074fa48", "merge_behavior": "AWAIT_ATTRIBUTES"},
272
+ "ports": [{"id": "408cd5fb-3a3e-4eb2-9889-61111bd6a129", "name": "default", "type": "DEFAULT"}],
273
+ "adornments": [
274
+ {
275
+ "id": "5be7d260-74f7-4734-b31b-a46a94539586",
276
+ "label": "RetryNode",
277
+ "base": {
278
+ "name": "RetryNode",
279
+ "module": ["vellum", "workflows", "nodes", "core", "retry_node", "node"],
280
+ },
281
+ "attributes": [
282
+ {
283
+ "id": "c91782e3-140f-4938-9c23-d2a7b85dcdd8",
284
+ "name": "retry_on_error_code",
285
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
286
+ },
287
+ {
288
+ "id": "f388e93b-8c68-4f54-8577-bbd0c9091557",
289
+ "name": "max_attempts",
290
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 5}},
291
+ },
292
+ {
293
+ "id": "8a07dc58-3fed-41d4-8ca6-31ee0bb86c61",
294
+ "name": "delay",
295
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
296
+ },
297
+ {
298
+ "id": "73a02e62-4535-4e1f-97b5-1264ca8b1d71",
299
+ "name": "retry_on_condition",
300
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
301
+ },
302
+ ],
303
+ },
304
+ {
305
+ "id": "3344083c-a32c-4a32-920b-0fb5093448fa",
306
+ "label": "TryNode",
307
+ "base": {
308
+ "name": "TryNode",
309
+ "module": ["vellum", "workflows", "nodes", "core", "try_node", "node"],
310
+ },
311
+ "attributes": [
312
+ {
313
+ "id": "ab2fbab0-e2a0-419b-b1ef-ce11ecf11e90",
314
+ "name": "on_error_code",
315
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
316
+ }
317
+ ],
318
+ },
319
+ ],
320
+ "attributes": [],
321
+ "outputs": [],
322
+ },
323
+ ],
324
+ "edges": [
325
+ {
326
+ "id": "e8bd50dd-37a0-49b0-8b7b-f1dd8eb478b9",
327
+ "source_node_id": "c14c1c9b-a7a4-4d2c-84fb-c940cfb09525",
328
+ "source_handle_id": "51a5eb25-af14-4bee-9ced-d2aa534ea8e9",
329
+ "target_node_id": "074833b0-e142-4bbc-8dec-209a35e178a3",
330
+ "target_handle_id": "f206358d-04a5-41c9-beee-0871a074fa48",
331
+ "type": "DEFAULT",
332
+ }
333
+ ],
334
+ "display_data": {"viewport": {"x": 0.0, "y": 0.0, "zoom": 1.0}},
335
+ "definition": {
336
+ "name": "StackedWorkflow",
337
+ "module": [
338
+ "vellum_ee",
339
+ "workflows",
340
+ "display",
341
+ "tests",
342
+ "workflow_serialization",
343
+ "generic_nodes",
344
+ "test_adornments_serialization",
345
+ ],
346
+ },
347
+ },
348
+ "input_variables": [],
349
+ "state_variables": [],
350
+ "output_variables": [],
351
+ },
352
+ exec_config,
353
+ ignore_order=True,
354
+ )
@@ -217,23 +217,30 @@ class BaseWorkflowDisplay(
217
217
  # TODO: We should still serialize nodes that are in the workflow's directory but aren't used in the graph.
218
218
  # https://app.shortcut.com/vellum/story/5394
219
219
  for node in self._workflow.get_nodes():
220
- node_display = self._get_node_display(node)
220
+ extracted_node_displays = self._extract_node_displays(node)
221
221
 
222
- if node not in node_displays:
223
- node_displays[node] = node_display
222
+ for extracted_node, extracted_node_display in extracted_node_displays.items():
223
+ if extracted_node not in node_displays:
224
+ node_displays[extracted_node] = extracted_node_display
224
225
 
225
- if node not in global_node_displays:
226
- global_node_displays[node] = node_display
226
+ if extracted_node not in global_node_displays:
227
+ global_node_displays[extracted_node] = extracted_node_display
227
228
 
228
- # Nodes wrapped in a decorator need to be in our node display dictionary for later retrieval
229
- inner_node = get_wrapped_node(node)
230
- if inner_node:
231
- inner_node_display = self._get_node_display(inner_node)
232
- node_displays[inner_node] = inner_node_display
233
- global_node_displays[inner_node] = inner_node_display
229
+ self._enrich_global_node_output_displays(node, extracted_node_displays[node], global_node_output_displays)
230
+ self._enrich_node_port_displays(node, extracted_node_displays[node], port_displays)
234
231
 
235
- self._enrich_global_node_output_displays(node, node_display, global_node_output_displays)
236
- self._enrich_node_port_displays(node, node_display, port_displays)
232
+ for node in self._workflow.get_unused_nodes():
233
+ extracted_node_displays = self._extract_node_displays(node)
234
+
235
+ for extracted_node, extracted_node_display in extracted_node_displays.items():
236
+ if extracted_node not in node_displays:
237
+ node_displays[extracted_node] = extracted_node_display
238
+
239
+ if extracted_node not in global_node_displays:
240
+ global_node_displays[extracted_node] = extracted_node_display
241
+
242
+ self._enrich_global_node_output_displays(node, extracted_node_displays[node], global_node_output_displays)
243
+ self._enrich_node_port_displays(node, extracted_node_displays[node], port_displays)
237
244
 
238
245
  workflow_input_displays: Dict[WorkflowInputReference, WorkflowInputsDisplayType] = {}
239
246
  # If we're dealing with a nested workflow, then it should have access to the inputs of its parents.
@@ -280,6 +287,15 @@ class BaseWorkflowDisplay(
280
287
  edge, node_displays, port_displays, overrides=edge_display_overrides
281
288
  )
282
289
 
290
+ for edge in self._workflow.get_unused_edges():
291
+ if edge in edge_displays:
292
+ continue
293
+
294
+ edge_display_overrides = self.edge_displays.get((edge.from_port, edge.to_node))
295
+ edge_displays[(edge.from_port, edge.to_node)] = self._generate_edge_display(
296
+ edge, node_displays, port_displays, overrides=edge_display_overrides
297
+ )
298
+
283
299
  workflow_output_displays: Dict[BaseDescriptor, WorkflowOutputDisplay] = {}
284
300
  for workflow_output in self._workflow.Outputs:
285
301
  if workflow_output in workflow_output_displays:
@@ -409,3 +425,20 @@ class BaseWorkflowDisplay(
409
425
  node_displays=temp_node_displays,
410
426
  )
411
427
  return display_meta
428
+
429
+ def _extract_node_displays(self, node: Type[BaseNode]) -> Dict[Type[BaseNode], NodeDisplayType]:
430
+ node_display = self._get_node_display(node)
431
+ additional_node_displays: Dict[Type[BaseNode], NodeDisplayType] = {
432
+ node: node_display,
433
+ }
434
+
435
+ # Nodes wrapped in a decorator need to be in our node display dictionary for later retrieval
436
+ inner_node = get_wrapped_node(node)
437
+ if inner_node:
438
+ inner_node_displays = self._extract_node_displays(inner_node)
439
+
440
+ for node, display in inner_node_displays.items():
441
+ if node not in additional_node_displays:
442
+ additional_node_displays[node] = display
443
+
444
+ return additional_node_displays
@@ -126,6 +126,18 @@ class VellumWorkflowDisplay(
126
126
 
127
127
  nodes.append(serialized_node)
128
128
 
129
+ # Add all unused nodes in the workflow
130
+ for node in self._workflow.get_unused_nodes():
131
+ node_display = self.display_context.node_displays[node]
132
+
133
+ try:
134
+ serialized_node = node_display.serialize(self.display_context)
135
+ except NotImplementedError as e:
136
+ self.add_error(e)
137
+ continue
138
+
139
+ nodes.append(serialized_node)
140
+
129
141
  synthetic_output_edges: JsonArray = []
130
142
  output_variables: JsonArray = []
131
143
  final_output_nodes = [