vellum-workflow-server 1.11.21__py3-none-any.whl → 1.12.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 1.11.21
3
+ Version: 1.12.6
4
4
  Summary:
5
5
  License: AGPL
6
6
  Requires-Python: >=3.9.0,<4
@@ -30,7 +30,7 @@ Requires-Dist: pyjwt (==2.10.0)
30
30
  Requires-Dist: python-dotenv (==1.2.1)
31
31
  Requires-Dist: retrying (==1.3.4)
32
32
  Requires-Dist: sentry-sdk[flask] (==2.20.0)
33
- Requires-Dist: vellum-ai (==1.11.21)
33
+ Requires-Dist: vellum-ai (==1.12.6)
34
34
  Description-Content-Type: text/markdown
35
35
 
36
36
  # Vellum Workflow Runner Server
@@ -7,16 +7,16 @@ workflow_server/api/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
7
7
  workflow_server/api/tests/test_input_display_mapping.py,sha256=drBZqMudFyB5wgiUOcMgRXz7E7ge-Qgxbstw4E4f0zE,2211
8
8
  workflow_server/api/tests/test_workflow_view.py,sha256=I2sd11ptKDqbylzB9rKqkMXeZoh8ttad3zIhNus86vk,32491
9
9
  workflow_server/api/tests/test_workflow_view_async_exec.py,sha256=eP_H2xI9SRfJdoJ6HPeynQecnxR50I_8aDCooF-YzIw,11952
10
- workflow_server/api/tests/test_workflow_view_stream_workflow_route.py,sha256=QilhiYv3_iaF-xvbmqUqqQqqVIZC4KgBe1u8Ku1q52s,42511
11
- workflow_server/api/workflow_view.py,sha256=1VSHGY0VXPnJQTz4XIX_sBiYFlTdByHH7RSAoOHBdtk,26650
10
+ workflow_server/api/tests/test_workflow_view_stream_workflow_route.py,sha256=PLHU7rZUVZqToSEuo6uJI4PTLdmaR1qBYiv9k_86A4w,48140
11
+ workflow_server/api/workflow_view.py,sha256=qPIN6iicMQVngy-Jr7dtOT3wqdFW2Bl3N_hbPdyTCAs,28177
12
12
  workflow_server/code_exec_runner.py,sha256=vJlCQ8FkcG8RfCZ34Ea2Xt6J7dNkU5EqA-KxRkbVOeo,2219
13
13
  workflow_server/config.py,sha256=I4hfTsjIbHxoSKylPCjKnrysPV0jO5nfRKwpKvEcfAE,2193
14
14
  workflow_server/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  workflow_server/core/cancel_workflow.py,sha256=uMPZg_rQ6iKZBLuxgwla1NYwhkcbO0gLh8QYBfU_2_0,2371
16
16
  workflow_server/core/events.py,sha256=24MA66DVQuaLJJcZrS8IL1Zq4Ohi9CoouKZ5VgoH3Cs,1402
17
- workflow_server/core/executor.py,sha256=7mJDuP8uedCAygNorp2Vscy6w7xHHqNZ_jxJXkJZzeY,17112
17
+ workflow_server/core/executor.py,sha256=-FT4x05ijlnERY4BnghFAoF8sxx2yjQBrkrVf_anXHQ,17172
18
18
  workflow_server/core/utils.py,sha256=mecVPqQkthrC4mpop3r8J3IWnBmKbDgqfCrSagyzVEg,2021
19
- workflow_server/core/workflow_executor_context.py,sha256=8faOdpU4cBeIbmOvg9VzD3eS5i_PKcH7tyNGzx_rehg,3899
19
+ workflow_server/core/workflow_executor_context.py,sha256=SXO5aVgO9rdsp7LSYJZkNIky-GvYwY3lJNBdgWK5KjE,3940
20
20
  workflow_server/logging_config.py,sha256=Hvx1t8uhqMMinl-5qcef7ufUvzs6x14VRnCb7YZxEAg,1206
21
21
  workflow_server/server.py,sha256=pBl0OQmrLE-PbTDwTgsVmxgz_Ai3TVhFRaMnr6PX6Yk,1849
22
22
  workflow_server/start.py,sha256=dvV8EKUH_oaTbOzNmUolF7RpkPWW8IkFwlgqOV9BhZQ,2842
@@ -29,9 +29,9 @@ workflow_server/utils/system_utils.py,sha256=3jNv113zRkKJ0928i2Vm6TqFHrDulteQu1k
29
29
  workflow_server/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  workflow_server/utils/tests/test_sentry_integration.py,sha256=14PfuW8AaQNNtqLmBs16EPe5T3f_iTI7YJMCRtiboZk,4502
31
31
  workflow_server/utils/tests/test_system_utils.py,sha256=_4GwXvVvU5BrATxUEWwQIPg0bzQXMWBtiBmjP8MTxJM,4314
32
- workflow_server/utils/tests/test_utils.py,sha256=0Nq6du8o-iBtTrip9_wgHES53JSiJbVdSXaBnPobw3s,6930
33
- workflow_server/utils/utils.py,sha256=dwUBP_0ngq6q-y2IFrjEWbGvao2roDR-VAekN6z57kE,5078
34
- vellum_workflow_server-1.11.21.dist-info/METADATA,sha256=WEJ5yhlrLvZjT0HHImwf0MlRjS6FwAUBTlghLEdbeWY,2277
35
- vellum_workflow_server-1.11.21.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- vellum_workflow_server-1.11.21.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
37
- vellum_workflow_server-1.11.21.dist-info/RECORD,,
32
+ workflow_server/utils/tests/test_utils.py,sha256=8gbgZyzVdJteDbTdIbpiwJh6q4J2kQBcpkd9yjXnGEc,6882
33
+ workflow_server/utils/utils.py,sha256=QeSDrM-AnniomRM4oqYWxhFkSqEKmGfzx_qpL-bfflU,5690
34
+ vellum_workflow_server-1.12.6.dist-info/METADATA,sha256=7BruaYAyKPAHRF6HwGIzvXsKUC2XgZ3uJTdYILn5eOU,2275
35
+ vellum_workflow_server-1.12.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ vellum_workflow_server-1.12.6.dist-info/entry_points.txt,sha256=uB_0yPkr7YV6RhEXzvFReUM8P4OQBlVXD6TN6eb9-oc,277
37
+ vellum_workflow_server-1.12.6.dist-info/RECORD,,
@@ -1313,3 +1313,161 @@ class OOMWorkflow(BaseWorkflow):
1313
1313
  assert (
1314
1314
  vembda_fulfilled_event["body"].get("timed_out") is not True
1315
1315
  ), "timed_out flag should not be set when OOM occurs"
1316
+
1317
+
1318
+ @mock.patch("workflow_server.api.workflow_view.ENABLE_PROCESS_WRAPPER", False)
1319
+ def test_stream_workflow_route__client_disconnect_emits_rejected_event():
1320
+ """
1321
+ Tests that when a client disconnects mid-stream (GeneratorExit), we emit a workflow execution
1322
+ rejected event to the events.create API.
1323
+ """
1324
+ # GIVEN a valid request body for a workflow that yields multiple events
1325
+ span_id = uuid4()
1326
+ trace_id = uuid4()
1327
+ request_body = {
1328
+ "timeout": 360,
1329
+ "execution_id": str(span_id),
1330
+ "execution_context": {
1331
+ "trace_id": str(trace_id),
1332
+ },
1333
+ "inputs": [],
1334
+ "environment_api_key": "test",
1335
+ "module": "workflow",
1336
+ "files": {
1337
+ "__init__.py": "",
1338
+ "workflow.py": """\
1339
+ from vellum.workflows import BaseWorkflow
1340
+
1341
+ class Workflow(BaseWorkflow):
1342
+ class Outputs(BaseWorkflow.Outputs):
1343
+ foo = "hello"
1344
+ """,
1345
+ },
1346
+ }
1347
+
1348
+ # AND a mock to capture events.create calls
1349
+ events_create_calls = []
1350
+
1351
+ def mock_events_create(request):
1352
+ events_create_calls.append(request)
1353
+
1354
+ # WHEN we call the stream route and simulate a client disconnect
1355
+ flask_app = create_app()
1356
+ with flask_app.test_client() as test_client:
1357
+ with mock.patch("workflow_server.core.workflow_executor_context.create_vellum_client") as mock_create_client:
1358
+ mock_client = mock.MagicMock()
1359
+ mock_client.events.create = mock_events_create
1360
+ mock_create_client.return_value = mock_client
1361
+
1362
+ response = test_client.post("/workflow/stream", json=request_body)
1363
+
1364
+ # Get the response iterator and consume a few chunks to start the stream
1365
+ response_iter = response.response
1366
+ next(response_iter)
1367
+
1368
+ # Close the response to trigger GeneratorExit
1369
+ response_iter.close()
1370
+
1371
+ # THEN the events.create API should have been called with rejected event
1372
+ assert len(events_create_calls) > 0, "events.create should have been called on client disconnect"
1373
+
1374
+ # AND the call should include a workflow.execution.rejected event (sent as SDK event model)
1375
+ last_call = events_create_calls[-1]
1376
+ assert isinstance(last_call, list), "events.create should be called with a list"
1377
+ assert len(last_call) == 1, "Should have exactly one rejected event"
1378
+
1379
+ rejected_event = last_call[0]
1380
+ assert rejected_event.name == "workflow.execution.rejected", "Should be a rejected event"
1381
+
1382
+ # AND the rejected event should have the correct error message
1383
+ assert "client disconnected" in rejected_event.body.error.message.lower()
1384
+
1385
+ # AND the rejected event should have a workflow_definition
1386
+ # TODO: In the future, we should capture the real workflow_definition from the initiated event.
1387
+ # For now, we use BaseWorkflow as a placeholder.
1388
+ assert rejected_event.body.workflow_definition is not None, "Should have a workflow_definition"
1389
+
1390
+
1391
+ def test_stream_workflow_route__array_input_string_methods(both_stream_types):
1392
+ """
1393
+ Tests that array inputs of strings can have string methods called on them.
1394
+
1395
+ This is a regression test for APO-2423 where array inputs of strings were being
1396
+ deserialized as VellumValue objects instead of plain strings, causing string
1397
+ methods like .upper() to fail.
1398
+ """
1399
+
1400
+ # GIVEN a workflow that takes an array of strings and calls .upper() on each item
1401
+ span_id = uuid4()
1402
+ request_body = {
1403
+ "timeout": 360,
1404
+ "execution_id": str(span_id),
1405
+ "inputs": [
1406
+ {
1407
+ "name": "items",
1408
+ "type": "ARRAY",
1409
+ "value": [
1410
+ {"type": "STRING", "value": "hello"},
1411
+ {"type": "STRING", "value": "world"},
1412
+ ],
1413
+ },
1414
+ ],
1415
+ "environment_api_key": "test",
1416
+ "module": "workflow",
1417
+ "files": {
1418
+ "__init__.py": "",
1419
+ "workflow.py": """\
1420
+ from typing import List
1421
+
1422
+ from vellum.workflows import BaseWorkflow
1423
+ from vellum.workflows.inputs import BaseInputs
1424
+ from vellum.workflows.nodes.bases.base import BaseNode
1425
+ from vellum.workflows.state import BaseState
1426
+
1427
+
1428
+ class Inputs(BaseInputs):
1429
+ items: List[str]
1430
+
1431
+
1432
+ class UppercaseNode(BaseNode):
1433
+ items = Inputs.items
1434
+
1435
+ class Outputs(BaseNode.Outputs):
1436
+ result: List[str]
1437
+
1438
+ def run(self) -> Outputs:
1439
+ # This should work if items is a list of strings
1440
+ # but will fail if items is a list of VellumValue objects
1441
+ uppercased = [item.upper() for item in self.items]
1442
+ return self.Outputs(result=uppercased)
1443
+
1444
+
1445
+ class Workflow(BaseWorkflow[Inputs, BaseState]):
1446
+ graph = UppercaseNode
1447
+
1448
+ class Outputs(BaseWorkflow.Outputs):
1449
+ result = UppercaseNode.Outputs.result
1450
+ """,
1451
+ },
1452
+ }
1453
+
1454
+ # WHEN we call the stream route
1455
+ status_code, events = both_stream_types(request_body)
1456
+
1457
+ # THEN we get a 200 response
1458
+ assert status_code == 200, events
1459
+
1460
+ # AND we should get the expected events without errors
1461
+ event_names = [e["name"] for e in events]
1462
+ assert "vembda.execution.initiated" in event_names
1463
+ assert "workflow.execution.initiated" in event_names
1464
+ assert "workflow.execution.fulfilled" in event_names
1465
+
1466
+ # AND the workflow should NOT be rejected
1467
+ assert "workflow.execution.rejected" not in event_names, (
1468
+ f"Workflow was rejected when it should have succeeded. " f"Events: {events}"
1469
+ )
1470
+
1471
+ # AND the output should be the uppercased strings
1472
+ fulfilled_event = next(e for e in events if e["name"] == "workflow.execution.fulfilled")
1473
+ assert fulfilled_event["body"]["outputs"]["result"] == ["HELLO", "WORLD"]
@@ -22,6 +22,7 @@ from vellum_ee.workflows.display.types import WorkflowDisplayContext
22
22
  from vellum_ee.workflows.display.workflows import BaseWorkflowDisplay
23
23
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
24
24
 
25
+ from vellum.workflows import BaseWorkflow
25
26
  from vellum.workflows.errors import WorkflowError, WorkflowErrorCode
26
27
  from vellum.workflows.events.workflow import (
27
28
  WorkflowExecutionInitiatedBody,
@@ -142,7 +143,7 @@ def stream_workflow_route() -> Response:
142
143
  # These can happen either from Vembda disconnects (possibily from predict disconnects) or
143
144
  # from knative activator gateway timeouts which are caused by idleTimeout or responseStartSeconds
144
145
  # being exceeded.
145
- app.logger.error(
146
+ app.logger.warning(
146
147
  "Client disconnected in the middle of the Workflow Stream",
147
148
  extra={
148
149
  "sentry_tags": {
@@ -151,6 +152,11 @@ def stream_workflow_route() -> Response:
151
152
  }
152
153
  },
153
154
  )
155
+ _emit_client_disconnect_events(
156
+ context,
157
+ span_id,
158
+ "Client disconnected in the middle of the Workflow Stream",
159
+ )
154
160
  return
155
161
  except Exception as e:
156
162
  logger.exception("Error during workflow response stream generator", extra={"error": e})
@@ -218,6 +224,38 @@ def _emit_async_error_events(
218
224
  logger.exception(f"Failed to emit async error events: {e}")
219
225
 
220
226
 
227
+ def _emit_client_disconnect_events(
228
+ context: WorkflowExecutorContext,
229
+ workflow_span_id: str,
230
+ error_message: str,
231
+ ) -> None:
232
+ """
233
+ Emit workflow execution rejected event when a client disconnects mid-stream.
234
+
235
+ Since the workflow has already started streaming (the initiated event was already emitted),
236
+ we only need to emit the rejected event to properly close out the execution.
237
+ """
238
+ try:
239
+ # TODO: In the future, we should capture the real workflow_definition from the initiated event
240
+ # For now, we use BaseWorkflow as a placeholder
241
+ rejected_event = WorkflowExecutionRejectedEvent(
242
+ trace_id=context.trace_id,
243
+ span_id=workflow_span_id,
244
+ body=WorkflowExecutionRejectedBody(
245
+ workflow_definition=BaseWorkflow,
246
+ error=WorkflowError(
247
+ message=error_message,
248
+ code=WorkflowErrorCode.WORKFLOW_CANCELLED,
249
+ ),
250
+ ),
251
+ parent=context.execution_context.parent_context if context.execution_context else None,
252
+ )
253
+
254
+ context.vellum_client.events.create(request=[rejected_event]) # type: ignore[list-item]
255
+ except Exception as e:
256
+ logger.exception(f"Failed to emit client disconnect events: {e}")
257
+
258
+
221
259
  @bp.route("/async-exec", methods=["POST"])
222
260
  def async_exec_workflow() -> Response:
223
261
  data = request.get_json()
@@ -194,6 +194,7 @@ def stream_workflow(
194
194
  timeout=executor_context.timeout,
195
195
  trigger=trigger,
196
196
  execution_id=executor_context.workflow_span_id,
197
+ event_max_size=executor_context.event_max_size,
197
198
  )
198
199
  except WorkflowInitializationException as e:
199
200
  cancel_watcher_kill_switch.set()
@@ -41,6 +41,7 @@ class BaseExecutorContext(UniversalBaseModel):
41
41
  # when running in async mode.
42
42
  workflow_span_id: Optional[UUID] = None
43
43
  vembda_service_initiated_timestamp: Optional[int] = None
44
+ event_max_size: Optional[int] = None
44
45
 
45
46
  @field_validator("inputs", mode="before")
46
47
  @classmethod
@@ -5,7 +5,6 @@ from vellum import (
5
5
  FunctionCall,
6
6
  SearchResult,
7
7
  SearchResultDocument,
8
- StringVellumValue,
9
8
  VellumAudio,
10
9
  VellumDocument,
11
10
  VellumError,
@@ -104,7 +103,7 @@ from workflow_server.utils.utils import (
104
103
  ),
105
104
  (
106
105
  {"type": "ARRAY", "name": "array", "value": [{"type": "STRING", "value": "<example-string-value>"}]},
107
- {"array": [StringVellumValue(value="<example-string-value>")]},
106
+ {"array": ["<example-string-value>"]},
108
107
  ),
109
108
  (
110
109
  {"type": "NUMBER", "name": "123", "value": 123},
@@ -6,6 +6,7 @@ from typing import Any, List
6
6
 
7
7
  from vellum import (
8
8
  ArrayInput,
9
+ ArrayVellumValue,
9
10
  ChatHistoryInput,
10
11
  ErrorInput,
11
12
  FunctionCallInput,
@@ -13,11 +14,26 @@ from vellum import (
13
14
  VellumAudio,
14
15
  VellumDocument,
15
16
  VellumImage,
17
+ VellumValue,
16
18
  VellumVideo,
17
19
  )
18
20
  from workflow_server.config import CONTAINER_IMAGE, is_development
19
21
 
20
22
 
23
+ def unwrap_vellum_value(item: VellumValue) -> Any:
24
+ """Recursively unwrap VellumValue objects to their primitive values.
25
+
26
+ This is needed because ArrayInput.value returns List[VellumValue] objects,
27
+ but workflows expect primitive Python values (str, int, etc.).
28
+ """
29
+ if isinstance(item, ArrayVellumValue):
30
+ if item.value is None:
31
+ return None
32
+ return [unwrap_vellum_value(nested_item) for nested_item in item.value]
33
+ else:
34
+ return item.value
35
+
36
+
21
37
  def convert_json_inputs_to_vellum(inputs: List[dict]) -> dict:
22
38
  vellum_inputs: dict[str, Any] = {}
23
39
 
@@ -41,7 +57,8 @@ def convert_json_inputs_to_vellum(inputs: List[dict]) -> dict:
41
57
  elif type == "ERROR":
42
58
  vellum_inputs[name] = ErrorInput.model_validate(input).value
43
59
  elif type == "ARRAY":
44
- vellum_inputs[name] = ArrayInput.model_validate(input).value
60
+ array_value = ArrayInput.model_validate(input).value
61
+ vellum_inputs[name] = [unwrap_vellum_value(item) for item in array_value]
45
62
  # Once we export *Input classes for these two cases, we can add the union to the WorkflowExecutorContext
46
63
  # model and simplify this method to just a {to_python_safe_snake_case(input.name): input.value} mapping
47
64
  elif type == "IMAGE":