vellum-ai 0.14.21__py3-none-any.whl → 0.14.22__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.14.21",
21
+ "X-Fern-SDK-Version": "0.14.22",
22
22
  }
23
23
  headers["X_API_KEY"] = self.api_key
24
24
  return headers
@@ -10,7 +10,7 @@ import pydash
10
10
  import pytz
11
11
  import yaml
12
12
 
13
- from vellum.utils.templating.custom_filters import is_valid_json_string, replace
13
+ from vellum.utils.templating.custom_filters import is_valid_json_string, replace, safe_tojson
14
14
 
15
15
  DEFAULT_JINJA_GLOBALS: Dict[str, Any] = {
16
16
  "datetime": datetime,
@@ -24,9 +24,14 @@ DEFAULT_JINJA_GLOBALS: Dict[str, Any] = {
24
24
  "yaml": yaml,
25
25
  }
26
26
 
27
- FilterFunc = Union[Callable[[Union[str, bytes]], bool], Callable[[Any, Any, Any], str]]
27
+ FilterFunc = Union[
28
+ Callable[[Union[str, bytes]], bool], # is_valid_json_string
29
+ Callable[[Any, Any, Any], str], # replace
30
+ Callable[[Any], str], # safe_tojson
31
+ ]
28
32
 
29
33
  DEFAULT_JINJA_CUSTOM_FILTERS: Dict[str, FilterFunc] = {
30
34
  "is_valid_json_string": is_valid_json_string,
31
35
  "replace": replace,
36
+ "tojson": safe_tojson,
32
37
  }
@@ -1,6 +1,9 @@
1
1
  import json
2
2
  from typing import Any, Union
3
3
 
4
+ from jinja2 import Undefined
5
+ from jinja2.utils import htmlsafe_json_dumps
6
+
4
7
  from vellum.workflows.state.encoder import DefaultStateEncoder
5
8
 
6
9
 
@@ -31,3 +34,9 @@ def replace(s: Any, old: Any, new: Any) -> str:
31
34
  old_str = encode_to_str(old)
32
35
  new_str = encode_to_str(new)
33
36
  return s_str.replace(old_str, new_str)
37
+
38
+
39
+ def safe_tojson(value: Any) -> str:
40
+ if isinstance(value, Undefined):
41
+ return ""
42
+ return htmlsafe_json_dumps(value, cls=DefaultStateEncoder)
@@ -77,7 +77,8 @@ code to UI and vice versa.
77
77
  ```bash
78
78
  python my_workflow.py
79
79
 
80
- Note: To use most out-of-box Nodes, and to push/pull to/from the Velłum UI, you'll need a Vellum account and API key. [Talk to us](https://www.vellum.ai/landing-pages/request-demo) or visit our [pricing page](https://www.vellum.ai/pricing) for more information.
80
+ Note: To use most out-of-box Nodes, and to push/pull to/from the Vellum UI, you'll need a Vellum account and API key.
81
+ You can [sign up for free here](https://app.vellum.ai/signup?f=wsdk&utm_source=github&utm_medium=repo_quickstart&utm_campaign=sdk).
81
82
 
82
83
 
83
84
  ## Documentation
@@ -86,5 +87,5 @@ Complete documentation for the Vellum Workflows SDK can be found at https://docs
86
87
 
87
88
  ## Stability
88
89
 
89
- This SDK is currently in <Availability type="beta" /> and is subject to change. If you'd like to pariticpate in
90
+ This SDK is currently in <Availability type="beta" /> and is subject to change. If you'd like to participate in
90
91
  our beta program, please [contact us](https://docs.vellum.ai/home/getting-started/support).
@@ -299,3 +299,21 @@ def test_templating_node__empty_string_to_list():
299
299
 
300
300
  # THEN the output should be an empty list, not raise an exception
301
301
  assert outputs.result == []
302
+
303
+
304
+ def test_api_error_templating_node():
305
+ class UndefinedTemplatingNode(TemplatingNode[BaseState, str]):
306
+ template = """{{ foo | tojson }}"""
307
+ inputs = {
308
+ "bar": "bar",
309
+ # foo is not define
310
+ }
311
+
312
+ # GIVEN a templating node with an undefined value
313
+ node = UndefinedTemplatingNode()
314
+
315
+ # WHEN the node is run
316
+ outputs = node.run()
317
+
318
+ # THEN the output should be empty string
319
+ assert outputs.result == ""
@@ -20,7 +20,7 @@ def test_run_workflow__secrets(vellum_client):
20
20
  method = APIRequestMethod.POST
21
21
  authorization_type = AuthorizationType.BEARER_TOKEN
22
22
  url = "https://api.vellum.ai"
23
- body = {
23
+ json = {
24
24
  "key": "value",
25
25
  }
26
26
  headers = {
@@ -32,6 +32,7 @@ def test_run_workflow__secrets(vellum_client):
32
32
  terminal = node.run()
33
33
 
34
34
  assert vellum_client.execute_api.call_count == 1
35
+ assert vellum_client.execute_api.call_args.kwargs["body"] == {"key": "value"}
35
36
  bearer_token = vellum_client.execute_api.call_args.kwargs["bearer_token"]
36
37
  assert bearer_token == ClientVellumSecret(name="secret")
37
38
  assert terminal.headers == {"X-Response-Header": "bar"}
@@ -45,7 +46,7 @@ def test_api_node_raises_error_when_api_call_fails(vellum_client):
45
46
  method = APIRequestMethod.GET
46
47
  authorization_type = AuthorizationType.BEARER_TOKEN
47
48
  url = "https://api.vellum.ai"
48
- body = {
49
+ json = {
49
50
  "key": "value",
50
51
  }
51
52
  headers = {
@@ -64,6 +65,7 @@ def test_api_node_raises_error_when_api_call_fails(vellum_client):
64
65
 
65
66
  # AND the API call should have been made
66
67
  assert vellum_client.execute_api.call_count == 1
68
+ assert vellum_client.execute_api.call_args.kwargs["body"] == {"key": "value"}
67
69
 
68
70
 
69
71
  def test_api_node_defaults_to_get_method(vellum_client):
@@ -57,7 +57,7 @@ class BaseAPINode(BaseNode, Generic[StateType]):
57
57
  if isinstance(headers[header], VellumSecret):
58
58
  vellum_instance = True
59
59
  if vellum_instance or bearer_token:
60
- return self._vellum_execute_api(bearer_token, data, headers, method, url)
60
+ return self._vellum_execute_api(bearer_token, json, headers, method, url)
61
61
  else:
62
62
  return self._local_execute_api(data, headers, json, method, url)
63
63
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.21
3
+ Version: 0.14.22
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -90,8 +90,7 @@ Description-Content-Type: text/markdown
90
90
 
91
91
  ## Get Started
92
92
 
93
- Most functionality within the SDK requires a Vellum account and API key. To sign up, [talk to us](https://www.vellum.ai/landing-pages/request-demo)
94
- or visit our [pricing page](https://www.vellum.ai/pricing).
93
+ Most functionality within the Vellum SDK requires a Vellum account and API key. You can sign up for free [here](https://app.vellum.ai/signup?f=wsdk&utm_source=github&utm_medium=repo_readme&utm_campaign=sdk) or visit our [pricing page](https://www.vellum.ai/pricing) for paid options.
95
94
 
96
95
  Even without a Vellum account, you can use the Workflows SDK to define the control flow of your AI systems. [Learn
97
96
  more below](#workflows-sdk).
@@ -33,7 +33,7 @@ vellum_ee/workflows/display/nodes/types.py,sha256=St1BB6no528OyELGiyRabWao0GGw6m
33
33
  vellum_ee/workflows/display/nodes/utils.py,sha256=sloya5TpXsnot1HURc9L51INwflRqUzHxRVnCS9Cd-4,973
34
34
  vellum_ee/workflows/display/nodes/vellum/__init__.py,sha256=nUIgH2s0-7IbQRNrBhLPyRNe8YIrx3Yo9HeeW-aXXFk,1668
35
35
  vellum_ee/workflows/display/nodes/vellum/api_node.py,sha256=lumtypr0JEZfiA32hcs_olTmnT0wIUzPBy0pnZVfyU4,8532
36
- vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py,sha256=oJAQrAm5iFQq0_fX94sMbS3RQEK1M1VsoUck4vsPs9A,5820
36
+ vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py,sha256=Bzqplrnx-bDIRD1JgU7036m8pSWSO45zEReNv8RJTu4,6379
37
37
  vellum_ee/workflows/display/nodes/vellum/code_execution_node.py,sha256=IYx0nll0t-tsPcjfgpfHMZE4FJgMFIsOiaQanGLYF0Q,4410
38
38
  vellum_ee/workflows/display/nodes/vellum/conditional_node.py,sha256=ybLIa4uclqVIy3VAQvI1ivg2tnK5Ug_1R5a69DFqL7E,11104
39
39
  vellum_ee/workflows/display/nodes/vellum/error_node.py,sha256=I1Jkp2htRINJATtv1e-zs9BrReFX842djpiVgBPHDYg,2186
@@ -93,10 +93,10 @@ vellum_ee/workflows/display/utils/expressions.py,sha256=9FpOslDI-RCR5m4TgAu9KCHh
93
93
  vellum_ee/workflows/display/utils/vellum.py,sha256=EVPQUSsZ3OIeLTEbV6LHPor37t9fnj9kJxDqP4PmTLQ,8234
94
94
  vellum_ee/workflows/display/vellum.py,sha256=bevbLCd2KqJBKqJ3lQayeRfjY7x1Djf57F9iJ-6KBJw,5162
95
95
  vellum_ee/workflows/display/workflows/__init__.py,sha256=kapXsC67VJcgSuiBMa86FdePG5A9kMB5Pi4Uy1O2ob4,207
96
- vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=2PyHM8bpgRqNtImCtN9fi5VqYvkOi5saVM9OXlwGmGQ,20400
96
+ vellum_ee/workflows/display/workflows/base_workflow_display.py,sha256=cvpDj5gDzoO1Sdt8BCo1acvZsNb30bpX-xGftl3u0xE,19940
97
97
  vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py,sha256=kp0u8LN_2IwshLrhMImhpZx1hRyAcD5gXY-kDuuaGMQ,1269
98
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=Ny9VWjCxc1_w8Z5xiQEJmNmEwtjlDlxgFCrmCWSmFmA,8511
99
- vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=wLXrYY0ccR37IG2MWwV49ELmwquVwDlDe3W4KXwOitM,16691
98
+ vellum_ee/workflows/display/workflows/tests/test_workflow_display.py,sha256=TwxfmIpCL1xrJOjA54d52q5Ko0CFUT2bW60DVD0wrlY,10095
99
+ vellum_ee/workflows/display/workflows/vellum_workflow_display.py,sha256=AS-vMrM93KEjb02-ye0Il29l3bX74o0Q8P2Nvf9NIZ0,16683
100
100
  vellum_ee/workflows/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
101
  vellum_ee/workflows/server/virtual_file_loader.py,sha256=ET-Q83W5Cgqzqz3qtFNwtS2nJEIcm3VtvR5kffsT3VY,2262
102
102
  vellum_ee/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -126,7 +126,7 @@ vellum/client/README.md,sha256=JkCJjmMZl4jrPj46pkmL9dpK4gSzQQmP5I7z4aME4LY,4749
126
126
  vellum/client/__init__.py,sha256=tKtdM1_GqmGq1gpi9ydWD_T-MM7fPn8QdHh8ww19cNI,117564
127
127
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
128
128
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
129
- vellum/client/core/client_wrapper.py,sha256=r9FX6fq7T8lmJtpc4VW9IORBb0_3s14dOpTkKYA_l68,1869
129
+ vellum/client/core/client_wrapper.py,sha256=DpiUwVloITIZTAtfFwbXdgMJxWZ-qVWlr-Qx6Kc1pZc,1869
130
130
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
131
131
  vellum/client/core/file.py,sha256=X9IbmkZmB2bB_DpmZAO3crWdXagOakAyn6UCOCImCPg,2322
132
132
  vellum/client/core/http_client.py,sha256=R0pQpCppnEtxccGvXl4uJ76s7ro_65Fo_erlNNLp_AI,19228
@@ -1292,8 +1292,8 @@ vellum/types/workspace_read.py,sha256=9CvgvK8Li8vL6qC5KX7f3-nEHslJ4lw2w07bvXcrjA
1292
1292
  vellum/types/workspace_secret_read.py,sha256=Z6QNXHxVHRdrLXSI31KxngePRwJTVoJYMXVbtPQwrxs,159
1293
1293
  vellum/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1294
1294
  vellum/utils/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1295
- vellum/utils/templating/constants.py,sha256=8OHMO6WFAEimbIaiHc5gy6s91D7_KvW-vTlEMWwvl_M,711
1296
- vellum/utils/templating/custom_filters.py,sha256=XVHriwazejRZmxB_eg4xHgCxl7AiQQ2sx-hRLMmylfU,885
1295
+ vellum/utils/templating/constants.py,sha256=mvAcJloHe1D9-LzM_jpzVZEJVYy326OCUMtSqD_vmo0,838
1296
+ vellum/utils/templating/custom_filters.py,sha256=qdbDSBPVIz-jKbetD4OnZ4In6O-SdeV0oZsixYoiWOY,1116
1297
1297
  vellum/utils/templating/exceptions.py,sha256=cDp140PP4OnInW4qAvg3KqiSiF70C71UyEAKRBR1Abo,46
1298
1298
  vellum/utils/templating/render.py,sha256=P2t9qU4w_WdXAVLM5Nj3bc1-XlIKOkwK-czQ80pHBag,2172
1299
1299
  vellum/utils/templating/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1301,7 +1301,7 @@ vellum/utils/templating/tests/test_custom_filters.py,sha256=mkJwc7t1gE13SKgPxhF-
1301
1301
  vellum/utils/typing.py,sha256=wx_daFqD69cYkuJTVnvNrpjhqC3uuhbnyJ9_bIwC9OU,327
1302
1302
  vellum/utils/uuid.py,sha256=Ch6wWRgwICxLxJCTl5iE3EdRlZj2zADR-zUMUtjcMWM,214
1303
1303
  vellum/version.py,sha256=jq-1PlAYxN9AXuaZqbYk9ak27SgE2lw9Ia5gx1b1gVI,76
1304
- vellum/workflows/README.md,sha256=MLNm-ihc0ao6I8gwwOhXQQBf0jOf-EsA9C519ALYI1o,3610
1304
+ vellum/workflows/README.md,sha256=hZdTKBIcsTKPofK68oPkBhyt0nnRh0csqC12k4FMHHA,3597
1305
1305
  vellum/workflows/__init__.py,sha256=CssPsbNvN6rDhoLuqpEv7MMKGa51vE6dvAh6U31Pcio,71
1306
1306
  vellum/workflows/constants.py,sha256=2yg4_uo5gpqViy3ZLSwfC8qTybleYCtOnhA4Rj6bacM,1310
1307
1307
  vellum/workflows/context.py,sha256=DwSf8lO9NHABiqOoD3exgrjUoRuNsKtutaL5TgRbD-A,1441
@@ -1390,7 +1390,7 @@ vellum/workflows/nodes/core/retry_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TI
1390
1390
  vellum/workflows/nodes/core/retry_node/tests/test_node.py,sha256=fNgDufkIsrTC-6ftvogqSpWhqqBj9iNESdfK19B1Yx0,5159
1391
1391
  vellum/workflows/nodes/core/templating_node/__init__.py,sha256=GmyuYo81_A1_Bz6id69ozVFS6FKiuDsZTiA3I6MaL2U,70
1392
1392
  vellum/workflows/nodes/core/templating_node/node.py,sha256=iqBmr2i-f-BqhisNQJiDfewjol0ur7-XpupLStyMJsg,3731
1393
- vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py,sha256=MHofz-BwAgt7EXkab8VIyacYznDEIJ7Er7MJUaxNQQo,9614
1393
+ vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py,sha256=nXkgGDBg4wP36NwykdMEVWwx_xjv8oGT2rYkwuCB_VU,10075
1394
1394
  vellum/workflows/nodes/core/try_node/__init__.py,sha256=JVD4DrldTIqFQQFrubs9KtWCCc0YCAc7Fzol5ZWIWeM,56
1395
1395
  vellum/workflows/nodes/core/try_node/node.py,sha256=RbxL0NRXS0IxRP0MJAnLABolF6dkwVniiqsagzy-lwk,4445
1396
1396
  vellum/workflows/nodes/core/try_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1399,10 +1399,10 @@ vellum/workflows/nodes/displayable/__init__.py,sha256=6F_4DlSwvHuilWnIalp8iDjjDX
1399
1399
  vellum/workflows/nodes/displayable/api_node/__init__.py,sha256=MoxdQSnidIj1Nf_d-hTxlOxcZXaZnsWFDbE-PkTK24o,56
1400
1400
  vellum/workflows/nodes/displayable/api_node/node.py,sha256=cp0nAukcOpM6TcNhbz12h08TMJxp_LM-MLDl1dAzYsk,2534
1401
1401
  vellum/workflows/nodes/displayable/api_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1402
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py,sha256=fiskhfcI4c6CxFlbSWb1JKsuq-98zAeUWOExc848ALw,3130
1402
+ vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py,sha256=J21H1dQT0BJ0oAalaA-9mgKv-NRcCJaTImhnKXp-cX4,3294
1403
1403
  vellum/workflows/nodes/displayable/bases/__init__.py,sha256=0mWIx3qUrzllV7jqt7wN03vWGMuI1WrrLZeMLT2Cl2c,304
1404
1404
  vellum/workflows/nodes/displayable/bases/api_node/__init__.py,sha256=1jwx4WC358CLA1jgzl_UD-rZmdMm2v9Mps39ndwCD7U,64
1405
- vellum/workflows/nodes/displayable/bases/api_node/node.py,sha256=I4zdxSXJ7-pjVcNNrSAEN_hAnhQGUBM6u5RczCActkw,4343
1405
+ vellum/workflows/nodes/displayable/bases/api_node/node.py,sha256=70pLGU0UzWvSbKwNkx3YlUYrDSkl7MmhVHoI8bzN79c,4343
1406
1406
  vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py,sha256=Org3xTvgp1pA0uUXFfnJr29D3HzCey2lEdYF4zbIUgo,70
1407
1407
  vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py,sha256=HGNoGLJ9lbqflGdYFDIiuHFyi0iJ-agJu4kkJ7D3dGs,3212
1408
1408
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py,sha256=Hl35IAoepRpE-j4cALaXVJIYTYOF3qszyVbxTj4kS1s,82
@@ -1522,8 +1522,8 @@ vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnad
1522
1522
  vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1523
1523
  vellum/workflows/workflows/tests/test_base_workflow.py,sha256=NRteiICyJvDM5zrtUfq2fZoXcGQVaWC9xmNlLLVW0cU,7979
1524
1524
  vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
1525
- vellum_ai-0.14.21.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1526
- vellum_ai-0.14.21.dist-info/METADATA,sha256=UqD3qeDGaeZTzlZPvYTpGbLEGaN7RYFGaTsxUDoboCo,5408
1527
- vellum_ai-0.14.21.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1528
- vellum_ai-0.14.21.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1529
- vellum_ai-0.14.21.dist-info/RECORD,,
1525
+ vellum_ai-0.14.22.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1526
+ vellum_ai-0.14.22.dist-info/METADATA,sha256=n1LtcR20giCKtaq1b6qilNyA4MKvG4EON_hG8EC8A9U,5484
1527
+ vellum_ai-0.14.22.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1528
+ vellum_ai-0.14.22.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1529
+ vellum_ai-0.14.22.dist-info/RECORD,,
@@ -3,6 +3,7 @@ import types
3
3
  from uuid import UUID
4
4
  from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, cast
5
5
 
6
+ from vellum.workflows.nodes.bases.base import BaseNode
6
7
  from vellum.workflows.nodes.bases.base_adornment_node import BaseAdornmentNode
7
8
  from vellum.workflows.nodes.utils import get_wrapped_node
8
9
  from vellum.workflows.types.core import JsonArray, JsonObject
@@ -17,7 +18,62 @@ from vellum_ee.workflows.display.types import WorkflowDisplayContext
17
18
  _BaseAdornmentNodeType = TypeVar("_BaseAdornmentNodeType", bound=BaseAdornmentNode)
18
19
 
19
20
 
21
+ def _recursively_replace_wrapped_node(node_class: Type[BaseNode], wrapped_node_display_class: Type["BaseNodeDisplay"]):
22
+ if not issubclass(node_class, BaseAdornmentNode):
23
+ return
24
+
25
+ # We must edit the node display class to use __wrapped_node__ everywhere it
26
+ # references the adorned node class, which is three places:
27
+ wrapped_node_class = node_class.__wrapped_node__
28
+ if not wrapped_node_class:
29
+ raise ValueError(f"Node {node_class.__name__} must be used as an adornment with the `wrap` method.")
30
+
31
+ # 1. The node display class' parameterized type
32
+ original_base_node_display = get_original_base(wrapped_node_display_class)
33
+ original_base_node_display.__args__ = (wrapped_node_class,)
34
+ wrapped_node_display_class._node_display_registry[wrapped_node_class] = wrapped_node_display_class
35
+ wrapped_node_display_class.__annotate_node__()
36
+
37
+ # 2. The node display class' output displays
38
+ for old_output in node_class.Outputs:
39
+ new_output = getattr(wrapped_node_class.Outputs, old_output.name, None)
40
+ if new_output is None:
41
+ # If the adornment is adding a new output, such as TryNode adding an "error" output,
42
+ # we skip it, since it should not be included in the adorned node's output displays
43
+ wrapped_node_display_class.output_display.pop(old_output, None)
44
+ continue
45
+
46
+ if old_output not in wrapped_node_display_class.output_display:
47
+ # If the adorned node doesn't have an output display defined for this output, we define one
48
+ wrapped_node_display_class.output_display[new_output] = NodeOutputDisplay(
49
+ id=wrapped_node_class.__output_ids__[old_output.name],
50
+ name=old_output.name,
51
+ )
52
+ else:
53
+ wrapped_node_display_class.output_display[new_output] = wrapped_node_display_class.output_display.pop(
54
+ old_output
55
+ )
56
+
57
+ # 3. The node display class' port displays
58
+ old_ports = list(wrapped_node_display_class.port_displays.keys())
59
+ for old_port in old_ports:
60
+ new_port = getattr(wrapped_node_class.Ports, old_port.name)
61
+ wrapped_node_display_class.port_displays[new_port] = wrapped_node_display_class.port_displays.pop(old_port)
62
+
63
+ # Now we keep drilling down recursively
64
+ if (
65
+ issubclass(wrapped_node_display_class, BaseAdornmentNodeDisplay)
66
+ and wrapped_node_display_class.__wrapped_node_display__
67
+ ):
68
+ _recursively_replace_wrapped_node(
69
+ wrapped_node_class,
70
+ wrapped_node_display_class.__wrapped_node_display__,
71
+ )
72
+
73
+
20
74
  class BaseAdornmentNodeDisplay(BaseNodeVellumDisplay[_BaseAdornmentNodeType], Generic[_BaseAdornmentNodeType]):
75
+ __wrapped_node_display__: Optional[Type[BaseNodeDisplay]] = None
76
+
21
77
  def serialize(
22
78
  self,
23
79
  display_context: "WorkflowDisplayContext",
@@ -52,10 +108,6 @@ class BaseAdornmentNodeDisplay(BaseNodeVellumDisplay[_BaseAdornmentNodeType], Ge
52
108
  if not issubclass(node_class, BaseAdornmentNode):
53
109
  raise ValueError(f"Node {node_class.__name__} must be wrapped with a {BaseAdornmentNode.__name__}")
54
110
 
55
- wrapped_node_class = node_class.__wrapped_node__
56
- if not wrapped_node_class:
57
- raise ValueError(f"Node {node_class.__name__} must be used as an adornment with the `wrap` method.")
58
-
59
111
  # `mypy` is wrong here, `cls` is indexable bc it's Generic
60
112
  BaseAdornmentDisplay = cls[node_class] # type: ignore[index]
61
113
 
@@ -64,6 +116,7 @@ class BaseAdornmentNodeDisplay(BaseNodeVellumDisplay[_BaseAdornmentNodeType], Ge
64
116
  ns[key] = kwarg
65
117
 
66
118
  ns["node_id"] = node_id or uuid4_from_hash(node_class.__qualname__)
119
+ ns["__wrapped_node_display__"] = wrapped_node_display_class
67
120
 
68
121
  AdornmentDisplay = types.new_class(
69
122
  re.sub(r"^Base", "", cls.__name__), bases=(BaseAdornmentDisplay,), exec_body=exec_body
@@ -71,43 +124,11 @@ class BaseAdornmentNodeDisplay(BaseNodeVellumDisplay[_BaseAdornmentNodeType], Ge
71
124
 
72
125
  setattr(wrapped_node_display_class, "__adorned_by__", AdornmentDisplay)
73
126
 
74
- # We must edit the node display class to use __wrapped_node__ everywhere it
75
- # references the adorned node class, which is three places:
76
-
77
- # 1. The node display class' parameterized type
78
- original_base_node_display = get_original_base(wrapped_node_display_class)
79
- original_base_node_display.__args__ = (wrapped_node_class,)
80
- wrapped_node_display_class._node_display_registry[wrapped_node_class] = wrapped_node_display_class
81
- wrapped_node_display_class.__annotate_node__()
82
-
83
- # 2. The node display class' output displays
84
- for old_output in node_class.Outputs:
85
- new_output = getattr(wrapped_node_class.Outputs, old_output.name, None)
86
- if new_output is None:
87
- # If the adornment is adding a new output, such as TryNode adding an "error" output,
88
- # we skip it, since it should not be included in the adorned node's output displays
89
- wrapped_node_display_class.output_display.pop(old_output, None)
90
- continue
91
-
92
- if old_output not in wrapped_node_display_class.output_display:
93
- # If the adorned node doesn't have an output display defined for this output, we define one
94
- wrapped_node_display_class.output_display[new_output] = NodeOutputDisplay(
95
- id=wrapped_node_class.__output_ids__[old_output.name],
96
- name=old_output.name,
97
- )
98
- else:
99
- wrapped_node_display_class.output_display[new_output] = (
100
- wrapped_node_display_class.output_display.pop(old_output)
101
- )
102
-
103
- # 3. The node display class' port displays
104
- old_ports = list(wrapped_node_display_class.port_displays.keys())
105
- for old_port in old_ports:
106
- new_port = getattr(wrapped_node_class.Ports, old_port.name)
107
- wrapped_node_display_class.port_displays[new_port] = wrapped_node_display_class.port_displays.pop(
108
- old_port
109
- )
110
-
111
- return wrapped_node_display_class
127
+ _recursively_replace_wrapped_node(
128
+ node_class,
129
+ wrapped_node_display_class,
130
+ )
131
+
132
+ return AdornmentDisplay
112
133
 
113
134
  return decorator
@@ -150,8 +150,6 @@ class BaseWorkflowDisplay(
150
150
  if node_output in node_output_displays:
151
151
  continue
152
152
 
153
- # TODO: Make sure this output ID matches the workflow output ID of the subworkflow node's workflow
154
- # https://app.shortcut.com/vellum/story/5660/fix-output-id-in-subworkflow-nodes
155
153
  node_output_displays[node_output] = node_display.get_node_output_display(node_output)
156
154
 
157
155
  def _enrich_node_port_displays(
@@ -272,8 +270,8 @@ class BaseWorkflowDisplay(
272
270
  continue
273
271
 
274
272
  edge_display_overrides = self.edge_displays.get((edge.from_port, edge.to_node))
275
- edge_displays[(edge.from_port, edge.to_node)] = self._generate_edge_display(
276
- edge, node_displays, overrides=edge_display_overrides
273
+ edge_displays[(edge.from_port, edge.to_node)] = edge_display_overrides or self._generate_edge_display(
274
+ edge, node_displays
277
275
  )
278
276
 
279
277
  for edge in self._workflow.get_unused_edges():
@@ -281,8 +279,8 @@ class BaseWorkflowDisplay(
281
279
  continue
282
280
 
283
281
  edge_display_overrides = self.edge_displays.get((edge.from_port, edge.to_node))
284
- edge_displays[(edge.from_port, edge.to_node)] = self._generate_edge_display(
285
- edge, node_displays, overrides=edge_display_overrides
282
+ edge_displays[(edge.from_port, edge.to_node)] = edge_display_overrides or self._generate_edge_display(
283
+ edge, node_displays
286
284
  )
287
285
 
288
286
  workflow_output_displays: Dict[BaseDescriptor, WorkflowOutputDisplay] = {}
@@ -443,30 +441,20 @@ class BaseWorkflowDisplay(
443
441
 
444
442
  return additional_node_displays
445
443
 
446
- def _generate_edge_display(
447
- self,
448
- edge: Edge,
449
- node_displays: Dict[Type[BaseNode], BaseNodeDisplay],
450
- overrides: Optional[EdgeDisplay] = None,
451
- ) -> EdgeDisplay:
444
+ def _generate_edge_display(self, edge: Edge, node_displays: Dict[Type[BaseNode], BaseNodeDisplay]) -> EdgeDisplay:
452
445
  source_node = get_unadorned_node(edge.from_port.node_class)
453
446
  target_node = get_unadorned_node(edge.to_node)
454
447
 
455
448
  source_node_id = node_displays[source_node].node_id
456
449
  target_node_id = node_displays[target_node].node_id
457
450
 
458
- return self._generate_edge_display_from_source(source_node_id, target_node_id, overrides)
451
+ return self._generate_edge_display_from_source(source_node_id, target_node_id)
459
452
 
460
453
  def _generate_edge_display_from_source(
461
454
  self,
462
455
  source_node_id: UUID,
463
456
  target_node_id: UUID,
464
- overrides: Optional[EdgeDisplay] = None,
465
457
  ) -> EdgeDisplay:
466
- edge_id: UUID
467
- if overrides:
468
- edge_id = overrides.id
469
- else:
470
- edge_id = uuid4_from_hash(f"{self.workflow_id}|id|{source_node_id}|{target_node_id}")
471
-
472
- return EdgeDisplay(id=edge_id)
458
+ return EdgeDisplay(
459
+ id=uuid4_from_hash(f"{self.workflow_id}|id|{source_node_id}|{target_node_id}"),
460
+ )
@@ -235,3 +235,43 @@ def test_get_event_display_context__templating_node_input_display():
235
235
  node_event_display = display_context.node_displays[MyNode.__id__]
236
236
 
237
237
  assert node_event_display.input_display.keys() == {"inputs.foo"}
238
+
239
+
240
+ def test_get_event_display_context__node_display_for_mutiple_adornments():
241
+ # GIVEN a simple workflow with multiple adornments
242
+ @TryNode.wrap()
243
+ @RetryNode.wrap()
244
+ class MyNode(BaseNode):
245
+ class Outputs(BaseNode.Outputs):
246
+ foo: str
247
+
248
+ class MyWorkflow(BaseWorkflow):
249
+ graph = MyNode
250
+
251
+ # AND a display class for the node
252
+ node_id = uuid4()
253
+ inner_node_id = uuid4()
254
+ innermost_node_id = uuid4()
255
+
256
+ @BaseTryNodeDisplay.wrap(node_id=node_id)
257
+ @BaseRetryNodeDisplay.wrap(node_id=inner_node_id)
258
+ class MyNodeDisplay(BaseNodeDisplay[MyNode]):
259
+ node_id = innermost_node_id
260
+
261
+ # WHEN we gather the event display context
262
+ display_context = VellumWorkflowDisplay(MyWorkflow).get_event_display_context()
263
+
264
+ # THEN the subworkflow display should be included
265
+ assert node_id in display_context.node_displays
266
+ node_event_display = display_context.node_displays[node_id]
267
+ assert node_event_display.subworkflow_display
268
+
269
+ # AND the inner node should be included
270
+ assert inner_node_id in node_event_display.subworkflow_display.node_displays
271
+ inner_node_event_display = node_event_display.subworkflow_display.node_displays[inner_node_id]
272
+ assert inner_node_event_display.subworkflow_display
273
+
274
+ # AND the innermost node should be included
275
+ assert innermost_node_id in inner_node_event_display.subworkflow_display.node_displays
276
+ innermost_node_event_display = inner_node_event_display.subworkflow_display.node_displays[innermost_node_id]
277
+ assert not innermost_node_event_display.subworkflow_display
@@ -363,8 +363,8 @@ class VellumWorkflowDisplay(
363
363
  target_node_display = node_displays[entrypoint_target]
364
364
  target_node_id = target_node_display.node_id
365
365
 
366
- edge_display = self._generate_edge_display_from_source(
367
- entrypoint_node_id, target_node_id, overrides=edge_display_overrides
366
+ edge_display = edge_display_overrides or self._generate_edge_display_from_source(
367
+ entrypoint_node_id, target_node_id
368
368
  )
369
369
 
370
370
  return EntrypointVellumDisplay(id=entrypoint_id, edge_display=edge_display)