vellum-workflow-server 1.8.6.post2__tar.gz → 1.9.2__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.
Files changed (35) hide show
  1. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/PKG-INFO +2 -2
  2. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/pyproject.toml +2 -2
  3. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/auth_middleware.py +2 -2
  4. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/tests/test_workflow_view.py +51 -0
  5. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/workflow_view.py +201 -91
  6. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/config.py +2 -0
  7. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/executor.py +17 -37
  8. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/workflow_executor_context.py +4 -0
  9. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/start.py +6 -0
  10. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/README.md +0 -0
  11. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/__init__.py +0 -0
  12. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/__init__.py +0 -0
  13. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/healthz_view.py +0 -0
  14. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/status_view.py +0 -0
  15. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/tests/__init__.py +0 -0
  16. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/tests/test_input_display_mapping.py +0 -0
  17. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +0 -0
  18. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/code_exec_runner.py +0 -0
  19. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/__init__.py +0 -0
  20. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/cancel_workflow.py +0 -0
  21. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/events.py +0 -0
  22. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/core/utils.py +0 -0
  23. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/logging_config.py +0 -0
  24. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/server.py +0 -0
  25. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/__init__.py +0 -0
  26. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/exit_handler.py +0 -0
  27. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/log_proxy.py +0 -0
  28. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/oom_killer.py +0 -0
  29. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/sentry.py +0 -0
  30. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/system_utils.py +0 -0
  31. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/tests/__init__.py +0 -0
  32. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/tests/test_sentry_integration.py +0 -0
  33. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/tests/test_system_utils.py +0 -0
  34. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/tests/test_utils.py +0 -0
  35. {vellum_workflow_server-1.8.6.post2 → vellum_workflow_server-1.9.2}/src/workflow_server/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 1.8.6.post2
3
+ Version: 1.9.2
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.8.6)
32
+ Requires-Dist: vellum-ai (==1.9.2)
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.8.6.post2"
6
+ version = "1.9.2"
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.8.6"
48
+ vellum-ai = "1.9.2"
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"])
@@ -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.
@@ -8,6 +8,7 @@ import os
8
8
  import pkgutil
9
9
  from queue import Empty
10
10
  import sys
11
+ import threading
11
12
  import time
12
13
  import traceback
13
14
  from uuid import uuid4
@@ -71,19 +72,195 @@ WORKFLOW_INITIATION_TIMEOUT_SECONDS = 60
71
72
  @bp.route("/stream", methods=["POST"])
72
73
  def stream_workflow_route() -> Response:
73
74
  data = request.get_json()
75
+ try:
76
+ context = WorkflowExecutorContext.model_validate(data)
77
+ except ValidationError as e:
78
+ error_message = e.errors()[0]["msg"]
79
+ error_location = e.errors()[0]["loc"]
80
+
81
+ return Response(
82
+ json.dumps({"detail": f"Invalid context: {error_message} at {error_location}"}),
83
+ status=400,
84
+ content_type="application/json",
85
+ )
86
+
87
+ headers = _get_headers(context)
88
+
89
+ # We can exceed the concurrency count currently with long running workflows due to a knative issue. So here
90
+ # if we detect a memory problem just exit us early
91
+ if not wait_for_available_process():
92
+ return Response(
93
+ json.dumps(
94
+ {
95
+ "detail": f"Workflow server concurrent request rate exceeded. "
96
+ f"Process count: {get_active_process_count()}"
97
+ }
98
+ ),
99
+ status=429,
100
+ content_type="application/json",
101
+ headers=headers,
102
+ )
103
+
104
+ start_workflow_state = _start_workflow(context)
105
+ if isinstance(start_workflow_state, Response):
106
+ return start_workflow_state
107
+
108
+ workflow_events, vembda_initiated_event, process, span_id, headers = start_workflow_state
74
109
 
110
+ def generator() -> Generator[str, None, None]:
111
+ try:
112
+ yield "\n"
113
+ yield vembda_initiated_event.model_dump_json()
114
+ yield "\n"
115
+ for row in workflow_events:
116
+ yield "\n"
117
+ if isinstance(row, dict):
118
+ dump = json.dumps(row)
119
+ yield dump
120
+ else:
121
+ yield row
122
+ yield "\n"
123
+ # Sometimes the connections get hung after they finish with the vembda fulfilled event
124
+ # if it happens during a knative scale down event. So we emit an END string so that
125
+ # we don't have to do string compares on all the events for performance.
126
+ yield "\n"
127
+ yield "END"
128
+ yield "\n"
129
+
130
+ logger.info(
131
+ f"Workflow stream completed, execution ID: {span_id}, process count: {get_active_process_count()}"
132
+ )
133
+ except GeneratorExit:
134
+ # These can happen either from Vembda disconnects (possibily from predict disconnects) or
135
+ # from knative activator gateway timeouts which are caused by idleTimeout or responseStartSeconds
136
+ # being exceeded.
137
+ app.logger.error(
138
+ "Client disconnected in the middle of the Workflow Stream",
139
+ extra={
140
+ "sentry_tags": {
141
+ "server_version": vembda_initiated_event.body.server_version,
142
+ "sdk_version": vembda_initiated_event.body.sdk_version,
143
+ }
144
+ },
145
+ )
146
+ return
147
+ except Exception as e:
148
+ logger.exception("Error during workflow response stream generator", extra={"error": e})
149
+ yield "\n"
150
+ yield "END"
151
+ yield "\n"
152
+ return
153
+ finally:
154
+ if ENABLE_PROCESS_WRAPPER:
155
+ try:
156
+ if process and process.is_alive():
157
+ process.kill()
158
+ if process:
159
+ increment_process_count(-1)
160
+ remove_active_span_id(span_id)
161
+ except Exception as e:
162
+ logger.error("Failed to kill process", e)
163
+ else:
164
+ increment_process_count(-1)
165
+ remove_active_span_id(span_id)
166
+
167
+ resp = Response(
168
+ stream_with_context(generator()),
169
+ status=200,
170
+ content_type="application/x-ndjson",
171
+ headers=headers,
172
+ )
173
+ return resp
174
+
175
+
176
+ @bp.route("/async-exec", methods=["POST"])
177
+ def async_exec_workflow() -> Response:
178
+ data = request.get_json()
75
179
  try:
76
180
  context = WorkflowExecutorContext.model_validate(data)
77
181
  except ValidationError as e:
78
182
  error_message = e.errors()[0]["msg"]
79
183
  error_location = e.errors()[0]["loc"]
80
184
 
185
+ # TODO need to convert this to a vembda event so that trigger'd execs can me notified
186
+ # can either do it here in the workflow server or
81
187
  return Response(
82
188
  json.dumps({"detail": f"Invalid context: {error_message} at {error_location}"}),
83
189
  status=400,
84
190
  content_type="application/json",
85
191
  )
86
192
 
193
+ # Reject back to the queue handler if were low on memory here, though maybe we should update the is_available
194
+ # route to look at memory too. Don't send this response as an event. Though we might want some logic to catch
195
+ # if they have a workflow server that can never start a workflow because the base image uses so much memory.
196
+ if not wait_for_available_process():
197
+ return Response(
198
+ json.dumps({"detail": f"Server resources low." f"Process count: {get_active_process_count()}"}),
199
+ status=429,
200
+ content_type="application/json",
201
+ )
202
+
203
+ def run_workflow_background() -> None:
204
+ process: Optional[Process] = None
205
+ span_id: Optional[str] = None
206
+
207
+ try:
208
+ start_workflow_result = _start_workflow(context)
209
+ if isinstance(start_workflow_result, Response):
210
+ # TODO same here, should return this response as en event or it will get yeeted to the nether
211
+ # return start_workflow_result
212
+ return
213
+
214
+ workflow_events, vembda_initiated_event, process, span_id, headers = start_workflow_result
215
+
216
+ for _ in workflow_events:
217
+ # This is way inefficient in process mode since were just having the main proc stream the events
218
+ # to nowhere wasting memory I/O and cpu.
219
+ continue
220
+ logger.info(
221
+ f"Workflow async exec completed, execution ID: {span_id}, process count: {get_active_process_count()}"
222
+ )
223
+ except Exception as e:
224
+ logger.exception("Error during workflow async background worker", e)
225
+ finally:
226
+ if ENABLE_PROCESS_WRAPPER:
227
+ try:
228
+ if process and process.is_alive():
229
+ process.kill()
230
+ if process:
231
+ increment_process_count(-1)
232
+ if span_id:
233
+ remove_active_span_id(span_id)
234
+ except Exception as e:
235
+ logger.error("Failed to kill process", e)
236
+ else:
237
+ increment_process_count(-1)
238
+ if span_id:
239
+ remove_active_span_id(span_id)
240
+
241
+ thread = threading.Thread(target=run_workflow_background)
242
+ thread.start()
243
+
244
+ return Response(
245
+ json.dumps({"success": True}),
246
+ status=200,
247
+ content_type="application/json",
248
+ )
249
+
250
+
251
+ def _start_workflow(
252
+ context: WorkflowExecutorContext,
253
+ ) -> Union[
254
+ Response,
255
+ tuple[
256
+ Iterator[Union[str, dict]],
257
+ VembdaExecutionInitiatedEvent,
258
+ Optional[Process],
259
+ str,
260
+ dict[str, str],
261
+ ],
262
+ ]:
263
+ headers = _get_headers(context)
87
264
  logger.info(
88
265
  f"Starting Workflow Server Request, trace ID: {context.trace_id}, "
89
266
  f"process count: {get_active_process_count()}, process wrapper: {ENABLE_PROCESS_WRAPPER}"
@@ -100,29 +277,7 @@ def stream_workflow_route() -> Response:
100
277
  parent=None,
101
278
  )
102
279
 
103
- process_output_queue: Queue[Union[str, dict]] = Queue()
104
-
105
- headers = {
106
- "X-Vellum-SDK-Version": vembda_initiated_event.body.sdk_version,
107
- "X-Vellum-Server-Version": vembda_initiated_event.body.server_version,
108
- "X-Vellum-Events-Emitted": str(is_events_emitting_enabled(context)),
109
- }
110
-
111
- # We can exceed the concurrency count currently with long running workflows due to a knative issue. So here
112
- # if we detect a memory problem just exit us early
113
- if not wait_for_available_process():
114
- return Response(
115
- json.dumps(
116
- {
117
- "detail": f"Workflow server concurrent request rate exceeded. "
118
- f"Process count: {get_active_process_count()}"
119
- }
120
- ),
121
- status=429,
122
- content_type="application/json",
123
- headers=headers,
124
- )
125
-
280
+ output_queue: Queue[Union[str, dict]] = Queue()
126
281
  cancel_signal = MultiprocessingEvent()
127
282
  timeout_signal = MultiprocessingEvent()
128
283
 
@@ -131,7 +286,7 @@ def stream_workflow_route() -> Response:
131
286
  try:
132
287
  process = stream_workflow_process_timeout(
133
288
  executor_context=context,
134
- queue=process_output_queue,
289
+ queue=output_queue,
135
290
  cancel_signal=cancel_signal,
136
291
  timeout_signal=timeout_signal,
137
292
  )
@@ -139,10 +294,10 @@ def stream_workflow_route() -> Response:
139
294
  except Exception as e:
140
295
  logger.exception(e)
141
296
 
142
- process_output_queue.put(create_vembda_rejected_event(context, traceback.format_exc()))
297
+ output_queue.put(create_vembda_rejected_event(context, traceback.format_exc()))
143
298
 
144
299
  try:
145
- first_item = process_output_queue.get(timeout=WORKFLOW_INITIATION_TIMEOUT_SECONDS)
300
+ first_item = output_queue.get(timeout=WORKFLOW_INITIATION_TIMEOUT_SECONDS)
146
301
  except Empty:
147
302
  logger.error("Request timed out trying to initiate the Workflow")
148
303
 
@@ -291,72 +446,9 @@ def stream_workflow_route() -> Response:
291
446
  break
292
447
  yield event
293
448
 
294
- workflow_events = process_events(process_output_queue)
449
+ workflow_events = process_events(output_queue)
295
450
 
296
- def generator() -> Generator[str, None, None]:
297
- try:
298
- yield "\n"
299
- yield vembda_initiated_event.model_dump_json()
300
- yield "\n"
301
- for row in workflow_events:
302
- yield "\n"
303
- if isinstance(row, dict):
304
- dump = json.dumps(row)
305
- yield dump
306
- else:
307
- yield row
308
- yield "\n"
309
- # Sometimes the connections get hung after they finish with the vembda fulfilled event
310
- # if it happens during a knative scale down event. So we emit an END string so that
311
- # we don't have to do string compares on all the events for performance.
312
- yield "\n"
313
- yield "END"
314
- yield "\n"
315
-
316
- logger.info(
317
- f"Workflow stream completed, execution ID: {span_id}, process count: {get_active_process_count()}"
318
- )
319
- except GeneratorExit:
320
- # These can happen either from Vembda disconnects (possibily from predict disconnects) or
321
- # from knative activator gateway timeouts which are caused by idleTimeout or responseStartSeconds
322
- # being exceeded.
323
- app.logger.error(
324
- "Client disconnected in the middle of the Workflow Stream",
325
- extra={
326
- "sentry_tags": {
327
- "server_version": vembda_initiated_event.body.server_version,
328
- "sdk_version": vembda_initiated_event.body.sdk_version,
329
- }
330
- },
331
- )
332
- return
333
- except Exception as e:
334
- logger.exception("Error during workflow response stream generator", extra={"error": e})
335
- yield "\n"
336
- yield "END"
337
- yield "\n"
338
- return
339
- finally:
340
- if ENABLE_PROCESS_WRAPPER:
341
- try:
342
- if process and process.is_alive():
343
- process.kill()
344
- if process:
345
- increment_process_count(-1)
346
- remove_active_span_id(span_id)
347
- except Exception as e:
348
- logger.error("Failed to kill process", e)
349
- else:
350
- increment_process_count(-1)
351
- remove_active_span_id(span_id)
352
-
353
- resp = Response(
354
- stream_with_context(generator()),
355
- status=200,
356
- content_type="application/x-ndjson",
357
- headers=headers,
358
- )
359
- return resp
451
+ return workflow_events, vembda_initiated_event, process, span_id, headers
360
452
 
361
453
 
362
454
  @bp.route("/stream-node", methods=["POST"])
@@ -435,6 +527,7 @@ def serialize_route() -> Response:
435
527
 
436
528
  files = data.get("files", {})
437
529
  workspace_api_key = data.get("workspace_api_key")
530
+ is_new_server = data.get("is_new_server", False)
438
531
 
439
532
  if not files:
440
533
  return Response(
@@ -448,6 +541,11 @@ def serialize_route() -> Response:
448
541
  # Generate a unique namespace for this serialization request
449
542
  namespace = get_random_namespace()
450
543
  virtual_finder = VirtualFileFinder(files, namespace)
544
+
545
+ headers = {
546
+ "X-Vellum-Is-New-Server": str(is_new_server).lower(),
547
+ }
548
+
451
549
  try:
452
550
  sys.meta_path.append(virtual_finder)
453
551
  result = BaseWorkflowDisplay.serialize_module(namespace, client=client, dry_run=True)
@@ -456,6 +554,7 @@ def serialize_route() -> Response:
456
554
  json.dumps(result.model_dump()),
457
555
  status=200,
458
556
  content_type="application/json",
557
+ headers=headers,
459
558
  )
460
559
 
461
560
  except WorkflowInitializationException as e:
@@ -465,6 +564,7 @@ def serialize_route() -> Response:
465
564
  json.dumps({"detail": error_message}),
466
565
  status=400,
467
566
  content_type="application/json",
567
+ headers=headers,
468
568
  )
469
569
 
470
570
  except Exception as e:
@@ -473,6 +573,7 @@ def serialize_route() -> Response:
473
573
  json.dumps({"detail": f"Serialization failed: {str(e)}"}),
474
574
  status=500,
475
575
  content_type="application/json",
576
+ headers=headers,
476
577
  )
477
578
 
478
579
  finally:
@@ -555,3 +656,12 @@ def startup_error_generator(
555
656
  },
556
657
  )
557
658
  return
659
+
660
+
661
+ def _get_headers(context: WorkflowExecutorContext) -> dict[str, Union[str, Any]]:
662
+ headers = {
663
+ "X-Vellum-SDK-Version": get_version()["sdk_version"],
664
+ "X-Vellum-Server-Version": get_version()["server_version"],
665
+ "X-Vellum-Events-Emitted": str(is_events_emitting_enabled(context)),
666
+ }
667
+ return headers
@@ -42,6 +42,8 @@ LOCAL_WORKFLOW_MODULE = os.getenv("LOCAL_WORKFLOW_MODULE")
42
42
  # The deployment name to match against when using local mode so you can still run your normal workflow
43
43
  LOCAL_DEPLOYMENT = os.getenv("LOCAL_DEPLOYMENT")
44
44
 
45
+ IS_ASYNC_MODE = os.getenv("IS_ASYNC_MODE", "false").lower() == "true"
46
+
45
47
 
46
48
  def is_development() -> bool:
47
49
  return os.getenv("FLASK_ENV", "local") == "local"
@@ -1,5 +1,4 @@
1
1
  from datetime import datetime
2
- import importlib
3
2
  from io import StringIO
4
3
  import json
5
4
  import logging
@@ -32,6 +31,7 @@ from vellum.workflows.resolvers.base import BaseWorkflowResolver
32
31
  from vellum.workflows.resolvers.resolver import VellumResolver
33
32
  from vellum.workflows.state.context import WorkflowContext
34
33
  from vellum.workflows.state.store import EmptyStore
34
+ from vellum.workflows.triggers import BaseTrigger
35
35
  from vellum.workflows.types import CancelSignal
36
36
  from vellum.workflows.workflows.event_filters import workflow_sandbox_event_filter
37
37
  from workflow_server.config import LOCAL_DEPLOYMENT, LOCAL_WORKFLOW_MODULE
@@ -150,7 +150,21 @@ def stream_workflow(
150
150
  cancel_watcher_kill_switch = ThreadingEvent()
151
151
  try:
152
152
  workflow, namespace = _create_workflow(executor_context)
153
- workflow_inputs = _get_workflow_inputs(executor_context, workflow.__class__)
153
+
154
+ trigger_id = executor_context.trigger_id
155
+
156
+ inputs_or_trigger = workflow.deserialize_trigger(trigger_id=trigger_id, inputs=executor_context.inputs)
157
+
158
+ # Determine whether we have inputs or a trigger
159
+ if isinstance(inputs_or_trigger, BaseInputs):
160
+ workflow_inputs = inputs_or_trigger
161
+ trigger = None
162
+ elif isinstance(inputs_or_trigger, BaseTrigger):
163
+ workflow_inputs = None
164
+ trigger = inputs_or_trigger
165
+ else:
166
+ workflow_inputs = None
167
+ trigger = None
154
168
 
155
169
  workflow_state = (
156
170
  workflow.deserialize_state(
@@ -176,6 +190,7 @@ def stream_workflow(
176
190
  entrypoint_nodes=[executor_context.node_id] if executor_context.node_id else None,
177
191
  previous_execution_id=executor_context.previous_execution_id,
178
192
  timeout=executor_context.timeout,
193
+ trigger=trigger,
179
194
  )
180
195
  except WorkflowInitializationException as e:
181
196
  cancel_watcher_kill_switch.set()
@@ -473,38 +488,3 @@ def _dump_event(event: BaseEvent, executor_context: BaseExecutorContext) -> dict
473
488
  dump["body"]["node_definition"]["module"] = module_base + dump["body"]["node_definition"]["module"][1:]
474
489
 
475
490
  return dump
476
-
477
-
478
- def _get_workflow_inputs(
479
- executor_context: BaseExecutorContext, workflow_class: Type[BaseWorkflow]
480
- ) -> Optional[BaseInputs]:
481
- if not executor_context.inputs:
482
- return None
483
-
484
- if not executor_context.files.get("inputs.py"):
485
- return None
486
-
487
- namespace = _get_file_namespace(executor_context)
488
- inputs_module_path = f"{namespace}.inputs"
489
- try:
490
- inputs_module = importlib.import_module(inputs_module_path)
491
- except Exception as e:
492
- raise WorkflowInitializationException(
493
- message=f"Failed to initialize workflow inputs: {e}",
494
- workflow_definition=workflow_class,
495
- ) from e
496
-
497
- if not hasattr(inputs_module, "Inputs"):
498
- raise WorkflowInitializationException(
499
- message=f"Inputs module {inputs_module_path} does not have a required Inputs class",
500
- workflow_definition=workflow_class,
501
- )
502
-
503
- if not issubclass(inputs_module.Inputs, BaseInputs):
504
- raise WorkflowInitializationException(
505
- message=f"""The class {inputs_module_path}.Inputs was expected to be a subclass of BaseInputs, \
506
- but found {inputs_module.Inputs.__class__.__name__}""",
507
- workflow_definition=workflow_class,
508
- )
509
-
510
- return inputs_module.Inputs(**executor_context.inputs)
@@ -36,6 +36,10 @@ class BaseExecutorContext(UniversalBaseModel):
36
36
  previous_execution_id: Optional[UUID] = None
37
37
  feature_flags: Optional[dict[str, bool]] = None
38
38
  is_new_server: bool = False
39
+ trigger_id: Optional[UUID] = None
40
+ # The actual 'execution id' of the workflow that we pass into the workflow
41
+ # when running in async mode.
42
+ workflow_span_id: Optional[UUID] = None
39
43
 
40
44
  @field_validator("inputs", mode="before")
41
45
  @classmethod
@@ -33,6 +33,7 @@ class CustomGunicornLogger(glogging.Logger):
33
33
  logger = logging.getLogger("gunicorn.access")
34
34
  logger.addFilter(HealthCheckFilter())
35
35
  logger.addFilter(SignalFilter())
36
+ logger.addFilter(StatusIsAvailableFilter())
36
37
 
37
38
 
38
39
  class HealthCheckFilter(logging.Filter):
@@ -45,6 +46,11 @@ class SignalFilter(logging.Filter):
45
46
  return "SIGTERM" not in record.getMessage()
46
47
 
47
48
 
49
+ class StatusIsAvailableFilter(logging.Filter):
50
+ def filter(self, record: Any) -> bool:
51
+ return "/status/is_available" not in record.getMessage()
52
+
53
+
48
54
  def start() -> None:
49
55
  if not is_development():
50
56
  start_oom_killer_worker()