vellum-workflow-server 0.14.67__py3-none-any.whl → 0.14.68.post1__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.

Potentially problematic release.


This version of vellum-workflow-server might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 0.14.67
3
+ Version: 0.14.68.post1
4
4
  Summary:
5
5
  License: AGPL
6
6
  Requires-Python: >=3.9.0,<4
@@ -28,7 +28,7 @@ Requires-Dist: pebble (==5.0.7)
28
28
  Requires-Dist: pyjwt (==2.10.0)
29
29
  Requires-Dist: python-dotenv (==1.0.1)
30
30
  Requires-Dist: sentry-sdk[flask] (==2.20.0)
31
- Requires-Dist: vellum-ai (==0.14.67)
31
+ Requires-Dist: vellum-ai (==0.14.68)
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  # Vellum Workflow Runner Server
@@ -3,9 +3,9 @@ workflow_server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
3
3
  workflow_server/api/auth_middleware.py,sha256=IlZaCiwZ5nwQqk5sYQorvOFj7lt0p1ZSSEqUxfiFaW0,2458
4
4
  workflow_server/api/healthz_view.py,sha256=itiRvBDBXncrw8Kbbc73UZLwqMAhgHOR3uSre_dAfgY,404
5
5
  workflow_server/api/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- workflow_server/api/tests/test_workflow_view.py,sha256=dSAeIAeZZGtPA2bN1njuoTTYenqi7ZY-poOpEy1LcZ8,402
6
+ workflow_server/api/tests/test_workflow_view.py,sha256=wlVFBmKcoI-RdzfGPioeW46k6zaXyUeIerPc6m4aQls,7150
7
7
  workflow_server/api/tests/test_workflow_view_stream_workflow_route.py,sha256=wWW4sukIUmHnL525MLSAB2E5P6CeASnhsCoKQXEgPcI,21297
8
- workflow_server/api/workflow_view.py,sha256=6i8O2p6WYAy0DUv-gqyoY-6gIGWGLhmTmfyryJPkr08,12387
8
+ workflow_server/api/workflow_view.py,sha256=duiMnAZ7PRpoPz63s9z37pxUxGR-9yAi3qxG9APXCao,14244
9
9
  workflow_server/code_exec_runner.py,sha256=tfijklTVkX4y45jeFTfrY2hVhdwo0VrLFc3SMeIiVYs,3096
10
10
  workflow_server/config.py,sha256=Jk1kmncI7g2LTIujssIpncD_eQoIowL_5oARQ6XpkJ0,1281
11
11
  workflow_server/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -23,7 +23,7 @@ workflow_server/utils/sentry.py,sha256=Pr3xKvHdk0XFSpXgy-55bWI4J3bbf_36gjDyLOs7o
23
23
  workflow_server/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  workflow_server/utils/tests/test_utils.py,sha256=MKGY4F_jnRdwqyQ8Krhlk7MpMEPql5MDATcwBaxpIEA,4786
25
25
  workflow_server/utils/utils.py,sha256=dpFeUQW7cmiH2-aLsO65jAa4RubfvptWNkW1ZkvDlmE,3710
26
- vellum_workflow_server-0.14.67.dist-info/METADATA,sha256=LMdZ-Nk9tl4FWMIBeBvyEJY7ZHW5h0-QRl6f70mebHE,2237
27
- vellum_workflow_server-0.14.67.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
28
- vellum_workflow_server-0.14.67.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
29
- vellum_workflow_server-0.14.67.dist-info/RECORD,,
26
+ vellum_workflow_server-0.14.68.post1.dist-info/METADATA,sha256=8nKE924gHjUlK3hYNOR_AXBVsVOH_QG9Sip7B3WFR_0,2243
27
+ vellum_workflow_server-0.14.68.post1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
28
+ vellum_workflow_server-0.14.68.post1.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
29
+ vellum_workflow_server-0.14.68.post1.dist-info/RECORD,,
@@ -1,4 +1,7 @@
1
+ import logging
1
2
  import re
3
+ from unittest.mock import patch
4
+ from uuid import UUID
2
5
 
3
6
  from workflow_server.server import create_app
4
7
 
@@ -11,3 +14,215 @@ def test_version_route():
11
14
  assert response.status_code == 200
12
15
  assert re.match(r"[0-9]*\.[0-9]*\.[0-9]*", response.json["sdk_version"])
13
16
  assert response.json["server_version"] == "local"
17
+
18
+
19
+ def test_version_route__with_single_node_file(tmp_path):
20
+ # GIVEN a temporary custom_nodes directory with a test node
21
+ custom_nodes_dir = tmp_path / "vellum_custom_nodes"
22
+ custom_nodes_dir.mkdir()
23
+
24
+ node_file = custom_nodes_dir / "test_node.py"
25
+ node_file.write_text(
26
+ """
27
+ from vellum.workflows.nodes import BaseNode
28
+
29
+ class TestNode(BaseNode):
30
+ \"""A test node for processing data.
31
+
32
+ This is a detailed description of what the node does.
33
+ \"""
34
+ label = "Test Node"
35
+ """
36
+ )
37
+
38
+ flask_app = create_app()
39
+
40
+ # WHEN we make a request to the version route
41
+ with patch("os.getcwd", return_value=str(tmp_path)), flask_app.test_client() as test_client:
42
+ response = test_client.get("/workflow/version")
43
+
44
+ # THEN we should get a successful response
45
+ assert response.status_code == 200
46
+
47
+ # AND we should find exactly one node
48
+ nodes = response.json["nodes"]
49
+ assert len(nodes) == 1
50
+
51
+ # AND the node should have the correct metadata
52
+ node = nodes[0]
53
+ assert UUID(node["id"])
54
+ assert node["module"] == "vellum_custom_nodes"
55
+ assert node["name"] == "TestNode"
56
+ assert node["label"] == "Test Node"
57
+ assert "A test node for processing data." in node["description"]
58
+ assert "This is a detailed description" in node["description"]
59
+
60
+
61
+ def test_version_route__with_nodes_in_multiple_files(tmp_path):
62
+ # GIVEN a temporary custom_nodes directory
63
+ custom_nodes_dir = tmp_path / "vellum_custom_nodes"
64
+ custom_nodes_dir.mkdir()
65
+
66
+ # AND a first node file
67
+ first_node_file = custom_nodes_dir / "first_node.py"
68
+ first_node_file.write_text(
69
+ """
70
+ from vellum.workflows.nodes import BaseNode
71
+
72
+ class SomeNode(BaseNode):
73
+ \"""This is Some Node.\"""
74
+ label = "Some node"
75
+ """
76
+ )
77
+
78
+ # AND a second node file
79
+ second_node_file = custom_nodes_dir / "second_node.py"
80
+ second_node_file.write_text(
81
+ """
82
+ from vellum.workflows.nodes import BaseNode
83
+
84
+ class SomeOtherNode(BaseNode):
85
+ \"""This is Some Other Node.\"""
86
+ label = "Some other node"
87
+ """
88
+ )
89
+
90
+ flask_app = create_app()
91
+
92
+ # WHEN we make a request to the version route
93
+ with patch("os.getcwd", return_value=str(tmp_path)), flask_app.test_client() as test_client:
94
+ response = test_client.get("/workflow/version")
95
+
96
+ # THEN we should get a successful response
97
+ assert response.status_code == 200
98
+
99
+ # AND we should find both nodes
100
+ nodes = response.json["nodes"]
101
+ assert len(nodes) == 2
102
+
103
+ # AND the first node should have correct metadata
104
+ some_node = nodes[0]
105
+ assert some_node["label"] == "Some node"
106
+ assert some_node["description"] == "This is Some Node."
107
+ assert UUID(some_node["id"])
108
+ assert some_node["module"] == "vellum_custom_nodes"
109
+
110
+ # AND the second node should have correct metadata
111
+ some_other_node = nodes[1]
112
+ assert some_other_node["label"] == "Some other node"
113
+ assert some_other_node["description"] == "This is Some Other Node."
114
+ assert UUID(some_other_node["id"])
115
+ assert some_other_node["module"] == "vellum_custom_nodes"
116
+
117
+
118
+ def test_version_route__no_custom_nodes_dir(tmp_path):
119
+ # GIVEN a Flask application and an empty temp directory
120
+ flask_app = create_app()
121
+
122
+ # WHEN we make a request to the version route
123
+ with patch("os.getcwd", return_value=str(tmp_path)), flask_app.test_client() as test_client:
124
+ response = test_client.get("/workflow/version")
125
+
126
+ # THEN we should get a successful response
127
+ assert response.status_code == 200
128
+
129
+ # AND the nodes list should be empty
130
+ assert response.json["nodes"] == []
131
+
132
+
133
+ def test_version_route__with_multiple_nodes_in_file(tmp_path):
134
+ # Create a temporary custom_nodes directory with multiple nodes in one file
135
+ custom_nodes_dir = tmp_path / "vellum_custom_nodes"
136
+ custom_nodes_dir.mkdir()
137
+
138
+ # Create a test node file with multiple nodes
139
+ node_file = custom_nodes_dir / "multiple_nodes.py"
140
+ node_file.write_text(
141
+ """
142
+ from vellum.workflows.nodes import BaseNode
143
+
144
+ class ProcessingNode(BaseNode):
145
+ \"""Processes input data.\"""
146
+ label = "Processing Node"
147
+
148
+ class TransformationNode(BaseNode):
149
+ \"""Transforms data format.\"""
150
+ label = "Transformation Node"
151
+
152
+ # This class should not be discovered
153
+ class HelperClass:
154
+ pass
155
+ """
156
+ )
157
+
158
+ flask_app = create_app()
159
+
160
+ # Mock the current working directory to point to our temp directory
161
+ with patch("os.getcwd", return_value=str(tmp_path)), flask_app.test_client() as test_client:
162
+ response = test_client.get("/workflow/version")
163
+
164
+ assert response.status_code == 200
165
+ nodes = response.json["nodes"]
166
+ assert len(nodes) == 2
167
+
168
+ # Nodes should be discovered regardless of their order in the file
169
+ node_names = {node["name"] for node in nodes}
170
+ assert node_names == {"ProcessingNode", "TransformationNode"}
171
+
172
+
173
+ def test_version_route__with_invalid_node_file(tmp_path, caplog):
174
+ caplog.set_level(logging.WARNING)
175
+
176
+ # GIVEN a temporary custom_nodes directory
177
+ custom_nodes_dir = tmp_path / "vellum_custom_nodes"
178
+ custom_nodes_dir.mkdir()
179
+
180
+ # AND a valid node file
181
+ valid_node_file = custom_nodes_dir / "valid_node.py"
182
+ valid_node_file.write_text(
183
+ """
184
+ from vellum.workflows.nodes import BaseNode
185
+
186
+ class SomeNode(BaseNode):
187
+ \"\"\"This is Some Node.\"\"\"
188
+ label = "Some node"
189
+ """
190
+ )
191
+
192
+ # AND an invalid node file with syntax error of missing colon in the class
193
+ invalid_node_file = custom_nodes_dir / "invalid_node.py"
194
+ invalid_node_file.write_text(
195
+ """
196
+ from vellum.workflows.nodes import BaseNode
197
+
198
+ class BrokenNode(BaseNode)
199
+ \"\"\"This node has a syntax error.\"\"\"
200
+ label = "Broken Node"
201
+ """
202
+ )
203
+
204
+ flask_app = create_app()
205
+
206
+ # WHEN we make a request to the version route
207
+ with patch("os.getcwd", return_value=str(tmp_path)), flask_app.test_client() as test_client:
208
+ response = test_client.get("/workflow/version")
209
+
210
+ # THEN we should get a successful response
211
+ assert response.status_code == 200
212
+
213
+ # AND we should find only the valid node
214
+ nodes = response.json["nodes"]
215
+ assert len(nodes) == 1
216
+
217
+ # AND the valid node should have correct metadata
218
+ valid_node = nodes[0]
219
+ assert valid_node["label"] == "Some node"
220
+ assert valid_node["description"] == "This is Some Node."
221
+ assert UUID(valid_node["id"])
222
+ assert valid_node["module"] == "vellum_custom_nodes"
223
+
224
+ # AND the error should be logged with full traceback
225
+ assert len(caplog.records) > 0
226
+ error_message = caplog.records[0].message
227
+ assert "Failed to load node from module invalid_node" in error_message
228
+ assert "invalid_node.py, line 4" in error_message
@@ -1,8 +1,13 @@
1
1
  from datetime import datetime
2
+ import importlib
3
+ import inspect
2
4
  import json
3
5
  import logging
4
6
  from multiprocessing import Queue, set_start_method
7
+ import os
8
+ import pkgutil
5
9
  from queue import Empty
10
+ import sys
6
11
  import time
7
12
  import traceback
8
13
  from uuid import uuid4
@@ -11,6 +16,7 @@ from typing import Generator, Iterator, Union
11
16
  from flask import Blueprint, Response, current_app as app, request, stream_with_context
12
17
  from pydantic import ValidationError
13
18
 
19
+ from vellum.workflows.nodes import BaseNode
14
20
  from workflow_server.config import MEMORY_LIMIT_MB
15
21
  from workflow_server.core.events import (
16
22
  STREAM_FINISHED_EVENT,
@@ -35,6 +41,8 @@ set_start_method("fork", force=True)
35
41
 
36
42
  logger = logging.getLogger(__name__)
37
43
 
44
+ CUSTOM_NODES_DIRECTORY = "vellum_custom_nodes"
45
+
38
46
 
39
47
  @bp.route("/stream", methods=["POST"])
40
48
  def stream_workflow_route() -> Response:
@@ -322,6 +330,40 @@ def stream_node_route() -> Response:
322
330
  def get_version_route() -> tuple[dict, int]:
323
331
  resp = get_version()
324
332
 
333
+ try:
334
+ # Discover nodes in the container
335
+ nodes = []
336
+
337
+ # Look for custom_nodes directory in the container
338
+ custom_nodes_path = os.path.join(os.getcwd(), CUSTOM_NODES_DIRECTORY)
339
+ if os.path.exists(custom_nodes_path):
340
+ # Add the custom_nodes directory to Python path so we can import from it
341
+ sys.path.append(os.path.dirname(custom_nodes_path))
342
+
343
+ # Import all Python files in the custom_nodes directory
344
+ for _, name, _ in pkgutil.iter_modules([custom_nodes_path]):
345
+ try:
346
+ module = importlib.import_module(f"{CUSTOM_NODES_DIRECTORY}.{name}")
347
+ for _, obj in inspect.getmembers(module):
348
+ # Look for classes that inherit from BaseNode
349
+ if inspect.isclass(obj) and obj != BaseNode and issubclass(obj, BaseNode):
350
+ nodes.append(
351
+ {
352
+ "id": str(uuid4()),
353
+ "module": CUSTOM_NODES_DIRECTORY,
354
+ "name": obj.__name__,
355
+ "label": obj().label if hasattr(obj, "label") else obj.__name__, # type: ignore
356
+ "description": inspect.getdoc(obj) or "",
357
+ }
358
+ )
359
+ except Exception as e:
360
+ logger.warning(f"Failed to load node from module {name}: {str(e)}", exc_info=True)
361
+
362
+ resp["nodes"] = nodes
363
+ except Exception as e:
364
+ logger.exception(f"Failed to discover nodes: {str(e)}")
365
+ resp["nodes"] = []
366
+
325
367
  return resp, 200
326
368
 
327
369