vellum-workflow-server 1.7.4.post1__tar.gz → 1.9.6.post2__tar.gz
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_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/PKG-INFO +2 -2
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/pyproject.toml +2 -2
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/auth_middleware.py +2 -2
- vellum_workflow_server-1.9.6.post2/src/workflow_server/api/status_view.py +19 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/tests/test_workflow_view.py +53 -2
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +65 -9
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/workflow_view.py +214 -86
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/config.py +8 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/cancel_workflow.py +1 -1
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/executor.py +89 -93
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/utils.py +1 -5
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/workflow_executor_context.py +8 -41
- vellum_workflow_server-1.9.6.post2/src/workflow_server/logging_config.py +39 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/server.py +22 -5
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/start.py +13 -1
- vellum_workflow_server-1.9.6.post2/src/workflow_server/utils/exit_handler.py +56 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/sentry.py +21 -0
- vellum_workflow_server-1.9.6.post2/src/workflow_server/utils/tests/test_sentry_integration.py +143 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/utils.py +2 -1
- vellum_workflow_server-1.7.4.post1/src/workflow_server/utils/exit_handler.py +0 -27
- vellum_workflow_server-1.7.4.post1/src/workflow_server/utils/tests/test_sentry_integration.py +0 -69
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/README.md +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/healthz_view.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/tests/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/api/tests/test_input_display_mapping.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/code_exec_runner.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/core/events.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/log_proxy.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/oom_killer.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/system_utils.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/tests/__init__.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/tests/test_system_utils.py +0 -0
- {vellum_workflow_server-1.7.4.post1 → vellum_workflow_server-1.9.6.post2}/src/workflow_server/utils/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vellum-workflow-server
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.6.post2
|
|
4
4
|
Summary:
|
|
5
5
|
License: AGPL
|
|
6
6
|
Requires-Python: >=3.9.0,<4
|
|
@@ -29,7 +29,7 @@ Requires-Dist: pyjwt (==2.10.0)
|
|
|
29
29
|
Requires-Dist: python-dotenv (==1.0.1)
|
|
30
30
|
Requires-Dist: retrying (==1.3.4)
|
|
31
31
|
Requires-Dist: sentry-sdk[flask] (==2.20.0)
|
|
32
|
-
Requires-Dist: vellum-ai (==1.
|
|
32
|
+
Requires-Dist: vellum-ai (==1.9.6)
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
# Vellum Workflow Runner Server
|
|
@@ -3,7 +3,7 @@ name = "vellum-workflow-server"
|
|
|
3
3
|
|
|
4
4
|
[tool.poetry]
|
|
5
5
|
name = "vellum-workflow-server"
|
|
6
|
-
version = "1.
|
|
6
|
+
version = "1.9.6.post2"
|
|
7
7
|
description = ""
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
authors = []
|
|
@@ -45,7 +45,7 @@ flask = "2.3.3"
|
|
|
45
45
|
orderly-set = "5.2.2"
|
|
46
46
|
pebble = "5.0.7"
|
|
47
47
|
gunicorn = "23.0.0"
|
|
48
|
-
vellum-ai = "1.
|
|
48
|
+
vellum-ai = "1.9.6"
|
|
49
49
|
python-dotenv = "1.0.1"
|
|
50
50
|
retrying = "1.3.4"
|
|
51
51
|
sentry-sdk = {extras = ["flask"], version = "2.20.0"}
|
|
@@ -5,7 +5,7 @@ from flask import Flask, Request, Response
|
|
|
5
5
|
import jwt
|
|
6
6
|
from jwt import ExpiredSignatureError
|
|
7
7
|
|
|
8
|
-
from workflow_server.config import IS_VPC, NAMESPACE, VEMBDA_PUBLIC_KEY, is_development
|
|
8
|
+
from workflow_server.config import IS_ASYNC_MODE, IS_VPC, NAMESPACE, VEMBDA_PUBLIC_KEY, is_development
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class AuthMiddleware:
|
|
@@ -15,7 +15,7 @@ class AuthMiddleware:
|
|
|
15
15
|
def __call__(self, environ: Dict[str, Any], start_response: Any) -> Any:
|
|
16
16
|
try:
|
|
17
17
|
request = Request(environ)
|
|
18
|
-
if not request.path.startswith("/healthz") and not is_development() and not IS_VPC:
|
|
18
|
+
if not request.path.startswith("/healthz") and not is_development() and not IS_VPC and not IS_ASYNC_MODE:
|
|
19
19
|
token = request.headers.get("X-Vembda-Signature")
|
|
20
20
|
if token:
|
|
21
21
|
decoded = jwt.decode(token, VEMBDA_PUBLIC_KEY, algorithms=["RS256"])
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, Response, jsonify
|
|
4
|
+
|
|
5
|
+
from workflow_server.config import CONCURRENCY
|
|
6
|
+
from workflow_server.utils.system_utils import get_active_process_count
|
|
7
|
+
|
|
8
|
+
bp = Blueprint("status", __name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@bp.route("/is_available", methods=["GET"])
|
|
12
|
+
def is_available() -> Tuple[Response, int]:
|
|
13
|
+
resp = jsonify(
|
|
14
|
+
available=get_active_process_count() < CONCURRENCY,
|
|
15
|
+
process_count=get_active_process_count(),
|
|
16
|
+
max_concurrency=CONCURRENCY,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return resp, 200
|
|
@@ -373,12 +373,12 @@ class MyAdditionNode(BaseNode):
|
|
|
373
373
|
{
|
|
374
374
|
"id": "aed3bcbb-d243-4a77-bb5e-409e9a28e868",
|
|
375
375
|
"name": "arg1",
|
|
376
|
-
"value":
|
|
376
|
+
"value": None,
|
|
377
377
|
},
|
|
378
378
|
{
|
|
379
379
|
"id": "9225d225-a41b-4642-8964-f28f58dcf4bf",
|
|
380
380
|
"name": "arg2",
|
|
381
|
-
"value":
|
|
381
|
+
"value": None,
|
|
382
382
|
},
|
|
383
383
|
],
|
|
384
384
|
"base": {"module": ["vellum", "workflows", "nodes", "bases", "base"], "name": "BaseNode"},
|
|
@@ -537,6 +537,57 @@ def test_serialize_route__with_invalid_workspace_api_key():
|
|
|
537
537
|
assert "exec_config" in response.json
|
|
538
538
|
|
|
539
539
|
|
|
540
|
+
def test_serialize_route__with_is_new_server_header():
|
|
541
|
+
"""
|
|
542
|
+
Tests that the serialize route returns the is_new_server header.
|
|
543
|
+
"""
|
|
544
|
+
# GIVEN a Flask application
|
|
545
|
+
flask_app = create_app()
|
|
546
|
+
|
|
547
|
+
workflow_files = {
|
|
548
|
+
"__init__.py": "",
|
|
549
|
+
"workflow.py": (
|
|
550
|
+
"from vellum.workflows import BaseWorkflow\n\n"
|
|
551
|
+
"class Workflow(BaseWorkflow):\n"
|
|
552
|
+
" class Outputs(BaseWorkflow.Outputs):\n"
|
|
553
|
+
" foo = 'hello'\n"
|
|
554
|
+
),
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# WHEN we make a request with is_new_server=True
|
|
558
|
+
with flask_app.test_client() as test_client:
|
|
559
|
+
response = test_client.post("/workflow/serialize", json={"files": workflow_files, "is_new_server": True})
|
|
560
|
+
|
|
561
|
+
# THEN we should get a successful response
|
|
562
|
+
assert response.status_code == 200
|
|
563
|
+
|
|
564
|
+
# AND the response should contain the is_new_server header set to true
|
|
565
|
+
assert "X-Vellum-Is-New-Server" in response.headers
|
|
566
|
+
assert response.headers["X-Vellum-Is-New-Server"] == "true"
|
|
567
|
+
|
|
568
|
+
# WHEN we make a request with is_new_server=False
|
|
569
|
+
with flask_app.test_client() as test_client:
|
|
570
|
+
response = test_client.post("/workflow/serialize", json={"files": workflow_files, "is_new_server": False})
|
|
571
|
+
|
|
572
|
+
# THEN we should get a successful response
|
|
573
|
+
assert response.status_code == 200
|
|
574
|
+
|
|
575
|
+
# AND the response should contain the is_new_server header set to false
|
|
576
|
+
assert "X-Vellum-Is-New-Server" in response.headers
|
|
577
|
+
assert response.headers["X-Vellum-Is-New-Server"] == "false"
|
|
578
|
+
|
|
579
|
+
# WHEN we make a request without is_new_server
|
|
580
|
+
with flask_app.test_client() as test_client:
|
|
581
|
+
response = test_client.post("/workflow/serialize", json={"files": workflow_files})
|
|
582
|
+
|
|
583
|
+
# THEN we should get a successful response
|
|
584
|
+
assert response.status_code == 200
|
|
585
|
+
|
|
586
|
+
# AND the response should contain the is_new_server header set to false (default)
|
|
587
|
+
assert "X-Vellum-Is-New-Server" in response.headers
|
|
588
|
+
assert response.headers["X-Vellum-Is-New-Server"] == "false"
|
|
589
|
+
|
|
590
|
+
|
|
540
591
|
def test_stream_node_route__with_node_id():
|
|
541
592
|
"""
|
|
542
593
|
Tests that the stream-node endpoint works with node_id.
|
|
@@ -5,6 +5,7 @@ import io
|
|
|
5
5
|
import json
|
|
6
6
|
from queue import Empty
|
|
7
7
|
import re
|
|
8
|
+
import time
|
|
8
9
|
from unittest import mock
|
|
9
10
|
from uuid import uuid4
|
|
10
11
|
|
|
@@ -117,7 +118,7 @@ def test_stream_workflow_route__happy_path(both_stream_types):
|
|
|
117
118
|
"execution_id": str(span_id),
|
|
118
119
|
"inputs": [],
|
|
119
120
|
"environment_api_key": "test",
|
|
120
|
-
"module": "
|
|
121
|
+
"module": "test",
|
|
121
122
|
"timeout": 360,
|
|
122
123
|
"files": {
|
|
123
124
|
"__init__.py": "",
|
|
@@ -131,8 +132,11 @@ class Workflow(BaseWorkflow):
|
|
|
131
132
|
},
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
with mock.patch("builtins.open", mock.mock_open(read_data="104857600")):
|
|
136
|
+
# WHEN we call the stream route
|
|
137
|
+
ts_ns = time.time_ns()
|
|
138
|
+
request_body["vembda_service_initiated_timestamp"] = ts_ns
|
|
139
|
+
status_code, events = both_stream_types(request_body)
|
|
136
140
|
|
|
137
141
|
# THEN we get a 200 response
|
|
138
142
|
assert status_code == 200, events
|
|
@@ -154,6 +158,7 @@ class Workflow(BaseWorkflow):
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
assert events[1]["name"] == "workflow.execution.initiated", events[1]
|
|
161
|
+
assert events[1]["body"]["workflow_definition"]["module"] == ["test", "workflow"]
|
|
157
162
|
assert "display_context" in events[1]["body"], events[1]["body"]
|
|
158
163
|
display_context = events[1]["body"]["display_context"]
|
|
159
164
|
assert "node_displays" in display_context
|
|
@@ -163,7 +168,36 @@ class Workflow(BaseWorkflow):
|
|
|
163
168
|
assert isinstance(display_context["workflow_inputs"], dict)
|
|
164
169
|
assert isinstance(display_context["workflow_outputs"], dict)
|
|
165
170
|
assert "foo" in display_context["workflow_outputs"]
|
|
171
|
+
|
|
172
|
+
# AND the initiated event should have server_metadata with version info and memory usage
|
|
173
|
+
assert "server_metadata" in events[1]["body"], events[1]["body"]
|
|
174
|
+
server_metadata = events[1]["body"]["server_metadata"]
|
|
175
|
+
assert server_metadata is not None, "server_metadata should not be None"
|
|
176
|
+
assert "server_version" in server_metadata
|
|
177
|
+
assert "sdk_version" in server_metadata
|
|
178
|
+
assert "memory_usage_mb" in server_metadata
|
|
179
|
+
assert isinstance(server_metadata["memory_usage_mb"], (int, float))
|
|
180
|
+
assert "is_new_server" in server_metadata
|
|
181
|
+
assert server_metadata["is_new_server"] is False
|
|
182
|
+
|
|
183
|
+
# AND the initiated event should have initiated_latency within a reasonable range
|
|
184
|
+
assert "initiated_latency" in server_metadata, "initiated_latency should be present in server_metadata"
|
|
185
|
+
initiated_latency = server_metadata["initiated_latency"]
|
|
186
|
+
assert isinstance(initiated_latency, int), "initiated_latency should be an integer (nanoseconds)"
|
|
187
|
+
# Latency should be positive and less than 60 seconds (60_000_000_000 nanoseconds) for CI
|
|
188
|
+
assert (
|
|
189
|
+
0 < initiated_latency < 60_000_000_000
|
|
190
|
+
), f"initiated_latency should be between 0 and 60 seconds, got {initiated_latency} ns"
|
|
191
|
+
|
|
166
192
|
assert events[2]["name"] == "workflow.execution.fulfilled", events[2]
|
|
193
|
+
assert events[2]["body"]["workflow_definition"]["module"] == ["test", "workflow"]
|
|
194
|
+
|
|
195
|
+
# AND the fulfilled event should have server_metadata with memory usage
|
|
196
|
+
assert "server_metadata" in events[2]["body"], events[2]["body"]
|
|
197
|
+
fulfilled_metadata = events[2]["body"]["server_metadata"]
|
|
198
|
+
assert fulfilled_metadata is not None, "fulfilled server_metadata should not be None"
|
|
199
|
+
assert "memory_usage_mb" in fulfilled_metadata
|
|
200
|
+
assert isinstance(fulfilled_metadata["memory_usage_mb"], (int, float))
|
|
167
201
|
|
|
168
202
|
assert events[3] == {
|
|
169
203
|
"id": mock.ANY,
|
|
@@ -364,9 +398,15 @@ class State(BaseState):
|
|
|
364
398
|
def test_stream_workflow_route__bad_indent_in_inputs_file(both_stream_types):
|
|
365
399
|
# GIVEN a valid request body
|
|
366
400
|
span_id = uuid4()
|
|
401
|
+
trace_id = uuid4()
|
|
402
|
+
parent_span_id = uuid4()
|
|
367
403
|
request_body = {
|
|
368
404
|
"timeout": 360,
|
|
369
405
|
"execution_id": str(span_id),
|
|
406
|
+
"execution_context": {
|
|
407
|
+
"trace_id": str(trace_id),
|
|
408
|
+
"parent_context": {"span_id": str(parent_span_id)},
|
|
409
|
+
},
|
|
370
410
|
"inputs": [
|
|
371
411
|
{"name": "foo", "type": "STRING", "value": "hello"},
|
|
372
412
|
],
|
|
@@ -403,7 +443,7 @@ from vellum.workflows.inputs import BaseInputs
|
|
|
403
443
|
|
|
404
444
|
assert events[0] == {
|
|
405
445
|
"id": mock.ANY,
|
|
406
|
-
"trace_id":
|
|
446
|
+
"trace_id": str(trace_id),
|
|
407
447
|
"span_id": str(span_id),
|
|
408
448
|
"timestamp": mock.ANY,
|
|
409
449
|
"api_version": "2024-10-25",
|
|
@@ -417,12 +457,22 @@ from vellum.workflows.inputs import BaseInputs
|
|
|
417
457
|
}
|
|
418
458
|
|
|
419
459
|
assert events[1]["name"] == "workflow.execution.initiated"
|
|
460
|
+
assert events[1]["trace_id"] == str(trace_id), "workflow initiated event should use request trace_id"
|
|
461
|
+
assert events[1]["parent"] is not None, "workflow initiated event should have parent context"
|
|
462
|
+
assert events[1]["parent"]["span_id"] == str(
|
|
463
|
+
parent_span_id
|
|
464
|
+
), "workflow initiated event parent should match request parent_context"
|
|
420
465
|
|
|
421
466
|
assert events[2]["name"] == "workflow.execution.rejected"
|
|
467
|
+
assert events[2]["trace_id"] == str(trace_id), "workflow rejected event should use request trace_id"
|
|
422
468
|
assert events[2]["span_id"] == events[1]["span_id"]
|
|
469
|
+
assert events[2]["parent"] is not None, "workflow rejected event should have parent context"
|
|
470
|
+
assert events[2]["parent"]["span_id"] == str(
|
|
471
|
+
parent_span_id
|
|
472
|
+
), "workflow rejected event parent should match request parent_context"
|
|
423
473
|
assert (
|
|
424
|
-
"
|
|
425
|
-
in events[2]["body"]["error"]["message"]
|
|
474
|
+
"Syntax Error raised while loading Workflow: "
|
|
475
|
+
"unexpected indent (inputs.py, line 3)" in events[2]["body"]["error"]["message"]
|
|
426
476
|
)
|
|
427
477
|
|
|
428
478
|
assert events[3] == {
|
|
@@ -645,10 +695,16 @@ class TimeoutWorkflow(BaseWorkflow):
|
|
|
645
695
|
assert "workflow.execution.initiated" in event_names
|
|
646
696
|
assert "node.execution.initiated" in event_names
|
|
647
697
|
|
|
648
|
-
|
|
649
|
-
|
|
698
|
+
assert "node.execution.rejected" in event_names, "Should emit node.execution.rejected on timeout"
|
|
699
|
+
node_execution_rejected = next(e for e in events if e["name"] == "node.execution.rejected")
|
|
700
|
+
assert "vellum/workflows/runner/runner.py" in node_execution_rejected["body"]["stacktrace"]
|
|
650
701
|
|
|
651
702
|
assert "workflow.execution.rejected" in event_names, "Should emit workflow.execution.rejected on timeout"
|
|
703
|
+
workflow_execution_rejected = next(e for e in events if e["name"] == "workflow.execution.rejected")
|
|
704
|
+
assert workflow_execution_rejected["body"]["error"]["code"] == "WORKFLOW_TIMEOUT"
|
|
705
|
+
# TODO: Uncomment once version 1.8.1 is released
|
|
706
|
+
# assert "stacktrace" in workflow_execution_rejected["body"]
|
|
707
|
+
# assert "vellum/workflows/runner/runner.py" in workflow_execution_rejected["body"]["stacktrace"]
|
|
652
708
|
|
|
653
709
|
assert "vembda.execution.fulfilled" in event_names
|
|
654
710
|
vembda_fulfilled = next(e for e in events if e["name"] == "vembda.execution.fulfilled")
|
|
@@ -1175,7 +1231,7 @@ class InvalidWorkflow(BaseWorkflow):
|
|
|
1175
1231
|
# AND the error message should contain information about the invalid graph structure
|
|
1176
1232
|
error_message = events[2]["body"]["error"]["message"]
|
|
1177
1233
|
expected_message = (
|
|
1178
|
-
"
|
|
1234
|
+
"Invalid graph structure detected. "
|
|
1179
1235
|
"Nested sets or unsupported graph types are not allowed. "
|
|
1180
1236
|
"Please contact Vellum support for assistance with Workflow configuration."
|
|
1181
1237
|
)
|