vellum-ai 1.3.0__py3-none-any.whl → 1.3.2__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.
- vellum/__init__.py +6 -0
- vellum/client/README.md +5 -5
- vellum/client/__init__.py +20 -0
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/raw_client.py +20 -0
- vellum/client/reference.md +61 -27
- vellum/client/resources/ad_hoc/client.py +29 -29
- vellum/client/resources/ad_hoc/raw_client.py +13 -13
- vellum/client/resources/events/client.py +69 -33
- vellum/client/resources/events/raw_client.py +13 -9
- vellum/client/types/__init__.py +6 -0
- vellum/client/types/create_workflow_event_request.py +7 -0
- vellum/client/types/deprecated_prompt_request_input.py +8 -0
- vellum/client/types/event_create_response.py +5 -0
- vellum/client/types/logical_operator.py +1 -0
- vellum/client/types/processing_failure_reason_enum.py +3 -1
- vellum/client/types/slim_document.py +1 -0
- vellum/client/types/workflow_input.py +31 -0
- vellum/types/create_workflow_event_request.py +3 -0
- vellum/types/deprecated_prompt_request_input.py +3 -0
- vellum/types/workflow_input.py +3 -0
- vellum/workflows/constants.py +3 -0
- vellum/workflows/events/node.py +1 -0
- vellum/workflows/events/tests/test_event.py +1 -0
- vellum/workflows/events/workflow.py +10 -2
- vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +16 -0
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_node.py +3 -13
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +11 -0
- vellum/workflows/nodes/displayable/web_search_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/web_search_node/tests/test_node.py +319 -0
- vellum/workflows/nodes/tests/test_utils.py +23 -0
- vellum/workflows/nodes/utils.py +14 -0
- vellum/workflows/runner/runner.py +33 -12
- vellum/workflows/types/code_execution_node_wrappers.py +2 -1
- {vellum_ai-1.3.0.dist-info → vellum_ai-1.3.2.dist-info}/METADATA +1 -1
- {vellum_ai-1.3.0.dist-info → vellum_ai-1.3.2.dist-info}/RECORD +41 -32
- vellum_ee/workflows/display/tests/workflow_serialization/test_web_search_node_serialization.py +81 -0
- vellum_ee/workflows/display/utils/events.py +3 -0
- {vellum_ai-1.3.0.dist-info → vellum_ai-1.3.2.dist-info}/LICENSE +0 -0
- {vellum_ai-1.3.0.dist-info → vellum_ai-1.3.2.dist-info}/WHEEL +0 -0
- {vellum_ai-1.3.0.dist-info → vellum_ai-1.3.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,319 @@
|
|
1
|
+
import pytest
|
2
|
+
from unittest.mock import MagicMock
|
3
|
+
|
4
|
+
import requests
|
5
|
+
|
6
|
+
from vellum.workflows.errors.types import WorkflowErrorCode
|
7
|
+
from vellum.workflows.exceptions import NodeException
|
8
|
+
from vellum.workflows.inputs import BaseInputs
|
9
|
+
from vellum.workflows.state import BaseState
|
10
|
+
from vellum.workflows.state.base import StateMeta
|
11
|
+
|
12
|
+
from ..node import WebSearchNode
|
13
|
+
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def base_node_setup(vellum_client):
|
17
|
+
"""Basic node setup with required inputs."""
|
18
|
+
|
19
|
+
class Inputs(BaseInputs):
|
20
|
+
query: str
|
21
|
+
api_key: str
|
22
|
+
num_results: int
|
23
|
+
|
24
|
+
class State(BaseState):
|
25
|
+
pass
|
26
|
+
|
27
|
+
class TestableWebSearchNode(WebSearchNode):
|
28
|
+
query = Inputs.query
|
29
|
+
api_key = Inputs.api_key
|
30
|
+
num_results = Inputs.num_results
|
31
|
+
|
32
|
+
state = State(meta=StateMeta(workflow_inputs=Inputs(query="test query", api_key="test_api_key", num_results=3)))
|
33
|
+
context = MagicMock()
|
34
|
+
context.vellum_client = vellum_client
|
35
|
+
node = TestableWebSearchNode(state=state, context=context)
|
36
|
+
return node
|
37
|
+
|
38
|
+
|
39
|
+
def test_successful_search_with_results(base_node_setup, requests_mock):
|
40
|
+
"""Test successful SerpAPI search with typical organic results."""
|
41
|
+
# GIVEN a mock SerpAPI response with organic results
|
42
|
+
mock_response = {
|
43
|
+
"organic_results": [
|
44
|
+
{
|
45
|
+
"title": "First Result",
|
46
|
+
"snippet": "This is the first search result snippet",
|
47
|
+
"link": "https://example1.com",
|
48
|
+
"position": 1,
|
49
|
+
},
|
50
|
+
{
|
51
|
+
"title": "Second Result",
|
52
|
+
"snippet": "This is the second search result snippet",
|
53
|
+
"link": "https://example2.com",
|
54
|
+
"position": 2,
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"title": "Third Result",
|
58
|
+
"snippet": "This is the third search result snippet",
|
59
|
+
"link": "https://example3.com",
|
60
|
+
"position": 3,
|
61
|
+
},
|
62
|
+
]
|
63
|
+
}
|
64
|
+
|
65
|
+
requests_mock.get("https://serpapi.com/search", json=mock_response)
|
66
|
+
|
67
|
+
# WHEN we run the node
|
68
|
+
outputs = base_node_setup.run()
|
69
|
+
|
70
|
+
# THEN the text output should be properly formatted
|
71
|
+
expected_text = (
|
72
|
+
"First Result: This is the first search result snippet\n\n"
|
73
|
+
"Second Result: This is the second search result snippet\n\n"
|
74
|
+
"Third Result: This is the third search result snippet"
|
75
|
+
)
|
76
|
+
assert outputs.text == expected_text
|
77
|
+
|
78
|
+
# AND URLs should be extracted correctly
|
79
|
+
assert outputs.urls == ["https://example1.com", "https://example2.com", "https://example3.com"]
|
80
|
+
|
81
|
+
# AND raw results should be preserved
|
82
|
+
assert outputs.results == mock_response["organic_results"]
|
83
|
+
|
84
|
+
# AND the request should have the correct parameters
|
85
|
+
assert requests_mock.last_request.qs == {
|
86
|
+
"q": ["test query"],
|
87
|
+
"api_key": ["test_api_key"],
|
88
|
+
"num": ["3"],
|
89
|
+
"engine": ["google"],
|
90
|
+
}
|
91
|
+
|
92
|
+
|
93
|
+
def test_search_with_location_parameter(base_node_setup, requests_mock):
|
94
|
+
"""Test that location parameter is properly passed to SerpAPI."""
|
95
|
+
# GIVEN a location parameter is set
|
96
|
+
base_node_setup.location = "New York, NY"
|
97
|
+
|
98
|
+
requests_mock.get("https://serpapi.com/search", json={"organic_results": []})
|
99
|
+
|
100
|
+
# WHEN we run the node
|
101
|
+
base_node_setup.run()
|
102
|
+
|
103
|
+
# THEN the location parameter should be included (URL encoding may lowercase)
|
104
|
+
assert "location" in requests_mock.last_request.qs
|
105
|
+
assert requests_mock.last_request.qs["location"][0].lower() == "new york, ny"
|
106
|
+
|
107
|
+
|
108
|
+
def test_authentication_error_401(base_node_setup, requests_mock):
|
109
|
+
"""Test 401 authentication error raises NodeException with INVALID_INPUTS."""
|
110
|
+
# GIVEN SerpAPI returns a 401 authentication error
|
111
|
+
requests_mock.get("https://serpapi.com/search", status_code=401)
|
112
|
+
|
113
|
+
# WHEN we run the node
|
114
|
+
with pytest.raises(NodeException) as exc_info:
|
115
|
+
base_node_setup.run()
|
116
|
+
|
117
|
+
# THEN it should raise the appropriate error
|
118
|
+
assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
|
119
|
+
assert "Invalid API key" in str(exc_info.value)
|
120
|
+
|
121
|
+
|
122
|
+
def test_rate_limit_error_429(base_node_setup, requests_mock):
|
123
|
+
"""Test 429 rate limit error raises NodeException with PROVIDER_ERROR."""
|
124
|
+
# GIVEN SerpAPI returns a 429 rate limit error
|
125
|
+
requests_mock.get("https://serpapi.com/search", status_code=429)
|
126
|
+
|
127
|
+
# WHEN we run the node
|
128
|
+
with pytest.raises(NodeException) as exc_info:
|
129
|
+
base_node_setup.run()
|
130
|
+
|
131
|
+
# THEN it should raise the appropriate error
|
132
|
+
assert exc_info.value.code == WorkflowErrorCode.PROVIDER_ERROR
|
133
|
+
assert "Rate limit exceeded" in str(exc_info.value)
|
134
|
+
|
135
|
+
|
136
|
+
def test_server_error_500(base_node_setup, requests_mock):
|
137
|
+
"""Test 500+ server errors raise NodeException with PROVIDER_ERROR."""
|
138
|
+
# GIVEN SerpAPI returns a 500 server error
|
139
|
+
requests_mock.get("https://serpapi.com/search", status_code=500)
|
140
|
+
|
141
|
+
# WHEN we run the node
|
142
|
+
with pytest.raises(NodeException) as exc_info:
|
143
|
+
base_node_setup.run()
|
144
|
+
|
145
|
+
# THEN it should raise the appropriate error
|
146
|
+
assert exc_info.value.code == WorkflowErrorCode.PROVIDER_ERROR
|
147
|
+
assert "SerpAPI error: HTTP 500" in str(exc_info.value)
|
148
|
+
|
149
|
+
|
150
|
+
def test_invalid_json_response(base_node_setup, requests_mock):
|
151
|
+
"""Test non-JSON response raises appropriate NodeException."""
|
152
|
+
# GIVEN SerpAPI returns non-JSON content
|
153
|
+
requests_mock.get("https://serpapi.com/search", text="Not JSON")
|
154
|
+
|
155
|
+
# WHEN we run the node
|
156
|
+
with pytest.raises(NodeException) as exc_info:
|
157
|
+
base_node_setup.run()
|
158
|
+
|
159
|
+
# THEN it should raise the appropriate error
|
160
|
+
assert exc_info.value.code == WorkflowErrorCode.PROVIDER_ERROR
|
161
|
+
assert "Invalid JSON response" in str(exc_info.value)
|
162
|
+
|
163
|
+
|
164
|
+
def test_serpapi_error_in_response(base_node_setup, requests_mock):
|
165
|
+
"""Test SerpAPI error field in response raises NodeException."""
|
166
|
+
# GIVEN SerpAPI returns an error in the response body
|
167
|
+
requests_mock.get("https://serpapi.com/search", json={"error": "Invalid search parameters"})
|
168
|
+
|
169
|
+
# WHEN we run the node
|
170
|
+
with pytest.raises(NodeException) as exc_info:
|
171
|
+
base_node_setup.run()
|
172
|
+
|
173
|
+
# THEN it should raise the appropriate error
|
174
|
+
assert exc_info.value.code == WorkflowErrorCode.PROVIDER_ERROR
|
175
|
+
assert "Invalid search parameters" in str(exc_info.value)
|
176
|
+
|
177
|
+
|
178
|
+
def test_empty_query_validation(vellum_client):
|
179
|
+
"""Test empty query raises validation error."""
|
180
|
+
|
181
|
+
# GIVEN a node with an empty query
|
182
|
+
class TestNode(WebSearchNode):
|
183
|
+
query = ""
|
184
|
+
api_key = "test_key"
|
185
|
+
num_results = 10
|
186
|
+
|
187
|
+
context = MagicMock()
|
188
|
+
context.vellum_client = vellum_client
|
189
|
+
node = TestNode(state=BaseState(meta=StateMeta(workflow_inputs=BaseInputs())), context=context)
|
190
|
+
|
191
|
+
# WHEN we run the node
|
192
|
+
with pytest.raises(NodeException) as exc_info:
|
193
|
+
node.run()
|
194
|
+
|
195
|
+
# THEN it should raise a validation error
|
196
|
+
assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
|
197
|
+
assert "Query is required" in str(exc_info.value)
|
198
|
+
|
199
|
+
|
200
|
+
def test_missing_api_key_validation(vellum_client):
|
201
|
+
"""Test missing API key raises validation error."""
|
202
|
+
|
203
|
+
# GIVEN a node with no API key
|
204
|
+
class TestNode(WebSearchNode):
|
205
|
+
query = "test query"
|
206
|
+
api_key = None
|
207
|
+
num_results = 10
|
208
|
+
|
209
|
+
context = MagicMock()
|
210
|
+
context.vellum_client = vellum_client
|
211
|
+
node = TestNode(state=BaseState(meta=StateMeta(workflow_inputs=BaseInputs())), context=context)
|
212
|
+
|
213
|
+
# WHEN we run the node
|
214
|
+
with pytest.raises(NodeException) as exc_info:
|
215
|
+
node.run()
|
216
|
+
|
217
|
+
# THEN it should raise a validation error
|
218
|
+
assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
|
219
|
+
assert "API key is required" in str(exc_info.value)
|
220
|
+
|
221
|
+
|
222
|
+
def test_invalid_num_results_validation(vellum_client):
|
223
|
+
"""Test invalid num_results raises validation error."""
|
224
|
+
|
225
|
+
# GIVEN a node with invalid num_results
|
226
|
+
class TestNode(WebSearchNode):
|
227
|
+
query = "test query"
|
228
|
+
api_key = "test_key"
|
229
|
+
num_results = -1
|
230
|
+
|
231
|
+
context = MagicMock()
|
232
|
+
context.vellum_client = vellum_client
|
233
|
+
node = TestNode(state=BaseState(meta=StateMeta(workflow_inputs=BaseInputs())), context=context)
|
234
|
+
|
235
|
+
# WHEN we run the node
|
236
|
+
with pytest.raises(NodeException) as exc_info:
|
237
|
+
node.run()
|
238
|
+
|
239
|
+
# THEN it should raise a validation error
|
240
|
+
assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
|
241
|
+
assert "num_results must be a positive integer" in str(exc_info.value)
|
242
|
+
|
243
|
+
|
244
|
+
def test_empty_organic_results(base_node_setup, requests_mock):
|
245
|
+
"""Test handling of empty search results."""
|
246
|
+
# GIVEN SerpAPI returns no organic results
|
247
|
+
requests_mock.get("https://serpapi.com/search", json={"organic_results": []})
|
248
|
+
|
249
|
+
# WHEN we run the node
|
250
|
+
outputs = base_node_setup.run()
|
251
|
+
|
252
|
+
# THEN all outputs should be empty
|
253
|
+
assert outputs.text == ""
|
254
|
+
assert outputs.urls == []
|
255
|
+
assert outputs.results == []
|
256
|
+
|
257
|
+
|
258
|
+
def test_missing_fields_in_results(base_node_setup, requests_mock):
|
259
|
+
"""Test handling of missing title, snippet, or link fields."""
|
260
|
+
# GIVEN SerpAPI returns results with missing fields
|
261
|
+
mock_response = {
|
262
|
+
"organic_results": [
|
263
|
+
{
|
264
|
+
"title": "Only Title",
|
265
|
+
# Missing snippet and link
|
266
|
+
},
|
267
|
+
{
|
268
|
+
"snippet": "Only snippet, no title or link"
|
269
|
+
# Missing title and link
|
270
|
+
},
|
271
|
+
{
|
272
|
+
"title": "Title with link",
|
273
|
+
"link": "https://example.com",
|
274
|
+
# Missing snippet
|
275
|
+
},
|
276
|
+
{
|
277
|
+
# All fields missing - should be skipped
|
278
|
+
"position": 4
|
279
|
+
},
|
280
|
+
]
|
281
|
+
}
|
282
|
+
|
283
|
+
requests_mock.get("https://serpapi.com/search", json=mock_response)
|
284
|
+
|
285
|
+
# WHEN we run the node
|
286
|
+
outputs = base_node_setup.run()
|
287
|
+
|
288
|
+
# THEN text should handle missing fields gracefully
|
289
|
+
expected_text = "Only Title\n\n" "Only snippet, no title or link\n\n" "Title with link"
|
290
|
+
assert outputs.text == expected_text
|
291
|
+
|
292
|
+
# AND URLs should only include valid links
|
293
|
+
assert outputs.urls == ["https://example.com"]
|
294
|
+
|
295
|
+
|
296
|
+
def test_request_timeout_handling(base_node_setup, requests_mock):
|
297
|
+
"""Test network timeout raises appropriate error."""
|
298
|
+
# GIVEN a network timeout occurs
|
299
|
+
requests_mock.get("https://serpapi.com/search", exc=requests.exceptions.Timeout("Connection timed out"))
|
300
|
+
|
301
|
+
# WHEN we run the node
|
302
|
+
with pytest.raises(NodeException) as exc_info:
|
303
|
+
base_node_setup.run()
|
304
|
+
|
305
|
+
# THEN it should raise a provider error
|
306
|
+
assert exc_info.value.code == WorkflowErrorCode.PROVIDER_ERROR
|
307
|
+
assert "HTTP request failed" in str(exc_info.value)
|
308
|
+
|
309
|
+
|
310
|
+
def test_user_agent_header_included(base_node_setup, requests_mock):
|
311
|
+
"""Test that User-Agent header from vellum_client is included."""
|
312
|
+
# GIVEN a successful request
|
313
|
+
requests_mock.get("https://serpapi.com/search", json={"organic_results": []})
|
314
|
+
|
315
|
+
# WHEN we run the node
|
316
|
+
base_node_setup.run()
|
317
|
+
|
318
|
+
# THEN the User-Agent header should be included
|
319
|
+
assert requests_mock.last_request.headers["User-Agent"] == "vellum-python-sdk/1.0.0"
|
@@ -150,3 +150,26 @@ def test_cast_to_output_type_none_value(output_type, expected_result):
|
|
150
150
|
"""Test that cast_to_output_type returns appropriate default values when None is provided."""
|
151
151
|
result = cast_to_output_type(None, output_type)
|
152
152
|
assert result == expected_result
|
153
|
+
|
154
|
+
|
155
|
+
@pytest.mark.parametrize(
|
156
|
+
"input_value,expected_result",
|
157
|
+
[
|
158
|
+
('{"name": "Alice", "age": 30}', {"name": "Alice", "age": 30}),
|
159
|
+
("[1, 2, 3]", [1, 2, 3]),
|
160
|
+
("invalid json", "invalid json"),
|
161
|
+
([1, 2, 3], [1, 2, 3]),
|
162
|
+
({"already": "dict"}, {"already": "dict"}),
|
163
|
+
],
|
164
|
+
ids=[
|
165
|
+
"valid_json_object",
|
166
|
+
"valid_json_array",
|
167
|
+
"invalid_json_string",
|
168
|
+
"non_string_list",
|
169
|
+
"non_string_dict",
|
170
|
+
],
|
171
|
+
)
|
172
|
+
def test_cast_to_output_type_any_json_parsing(input_value, expected_result):
|
173
|
+
"""Test that cast_to_output_type attempts JSON parsing for Any type and falls back gracefully."""
|
174
|
+
result = cast_to_output_type(input_value, Any)
|
175
|
+
assert result == expected_result
|
vellum/workflows/nodes/utils.py
CHANGED
@@ -8,6 +8,7 @@ from typing import Any, Callable, Dict, ForwardRef, List, Optional, Type, TypeVa
|
|
8
8
|
from pydantic import BaseModel, create_model
|
9
9
|
|
10
10
|
from vellum.client.types.function_call import FunctionCall
|
11
|
+
from vellum.workflows.constants import undefined
|
11
12
|
from vellum.workflows.errors.types import WorkflowErrorCode
|
12
13
|
from vellum.workflows.exceptions import NodeException
|
13
14
|
from vellum.workflows.inputs.base import BaseInputs
|
@@ -253,6 +254,19 @@ def cast_to_output_type(result: Any, output_type: Any) -> Any:
|
|
253
254
|
if result is None:
|
254
255
|
return _get_default_value(output_type)
|
255
256
|
|
257
|
+
if result is undefined:
|
258
|
+
return _get_default_value(output_type)
|
259
|
+
|
260
|
+
# Attempt JSON parse if type is Any
|
261
|
+
if output_type is Any:
|
262
|
+
if isinstance(result, str):
|
263
|
+
try:
|
264
|
+
return json.loads(result)
|
265
|
+
except (json.JSONDecodeError, TypeError):
|
266
|
+
# If JSON parsing fails, fall back to original result
|
267
|
+
pass
|
268
|
+
return result
|
269
|
+
|
256
270
|
clean_output_type = _clean_output_type(output_type)
|
257
271
|
DynamicModel = create_model("Output", output_type=(clean_output_type, ...))
|
258
272
|
|
@@ -404,6 +404,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
404
404
|
)
|
405
405
|
except NodeException as e:
|
406
406
|
logger.info(e)
|
407
|
+
captured_traceback = traceback.format_exc()
|
407
408
|
|
408
409
|
self._workflow_event_inner_queue.put(
|
409
410
|
NodeExecutionRejectedEvent(
|
@@ -412,12 +413,14 @@ class WorkflowRunner(Generic[StateType]):
|
|
412
413
|
body=NodeExecutionRejectedBody(
|
413
414
|
node_definition=node.__class__,
|
414
415
|
error=e.error,
|
416
|
+
traceback=captured_traceback,
|
415
417
|
),
|
416
418
|
parent=execution.parent_context,
|
417
419
|
)
|
418
420
|
)
|
419
421
|
except WorkflowInitializationException as e:
|
420
422
|
logger.info(e)
|
423
|
+
captured_traceback = traceback.format_exc()
|
421
424
|
self._workflow_event_inner_queue.put(
|
422
425
|
NodeExecutionRejectedEvent(
|
423
426
|
trace_id=execution.trace_id,
|
@@ -425,6 +428,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
425
428
|
body=NodeExecutionRejectedBody(
|
426
429
|
node_definition=node.__class__,
|
427
430
|
error=e.error,
|
431
|
+
traceback=captured_traceback,
|
428
432
|
),
|
429
433
|
parent=execution.parent_context,
|
430
434
|
)
|
@@ -574,7 +578,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
574
578
|
)
|
575
579
|
worker_thread.start()
|
576
580
|
|
577
|
-
def _handle_work_item_event(self, event: WorkflowEvent) -> Optional[
|
581
|
+
def _handle_work_item_event(self, event: WorkflowEvent) -> Optional[NodeExecutionRejectedEvent]:
|
578
582
|
active_node = self._active_nodes_by_execution_id.get(event.span_id)
|
579
583
|
if not active_node:
|
580
584
|
return None
|
@@ -582,7 +586,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
582
586
|
node = active_node.node
|
583
587
|
if event.name == "node.execution.rejected":
|
584
588
|
self._active_nodes_by_execution_id.pop(event.span_id)
|
585
|
-
return event
|
589
|
+
return event
|
586
590
|
|
587
591
|
if event.name == "node.execution.streaming":
|
588
592
|
for workflow_output_descriptor in self.workflow.Outputs:
|
@@ -708,13 +712,24 @@ class WorkflowRunner(Generic[StateType]):
|
|
708
712
|
parent=self._execution_context.parent_context,
|
709
713
|
)
|
710
714
|
|
711
|
-
def _reject_workflow_event(
|
715
|
+
def _reject_workflow_event(
|
716
|
+
self, error: WorkflowError, captured_traceback: Optional[str] = None
|
717
|
+
) -> WorkflowExecutionRejectedEvent:
|
718
|
+
if captured_traceback is None:
|
719
|
+
try:
|
720
|
+
captured_traceback = traceback.format_exc()
|
721
|
+
if captured_traceback.strip() == "NoneType: None":
|
722
|
+
captured_traceback = None
|
723
|
+
except Exception:
|
724
|
+
pass
|
725
|
+
|
712
726
|
return WorkflowExecutionRejectedEvent(
|
713
727
|
trace_id=self._execution_context.trace_id,
|
714
728
|
span_id=self._initial_state.meta.span_id,
|
715
729
|
body=WorkflowExecutionRejectedBody(
|
716
730
|
workflow_definition=self.workflow.__class__,
|
717
731
|
error=error,
|
732
|
+
traceback=captured_traceback,
|
718
733
|
),
|
719
734
|
parent=self._execution_context.parent_context,
|
720
735
|
)
|
@@ -758,22 +773,26 @@ class WorkflowRunner(Generic[StateType]):
|
|
758
773
|
else:
|
759
774
|
self._concurrency_queue.put((self._initial_state, node_cls, None))
|
760
775
|
except NodeException as e:
|
761
|
-
|
776
|
+
captured_traceback = traceback.format_exc()
|
777
|
+
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_traceback))
|
762
778
|
return
|
763
779
|
except WorkflowInitializationException as e:
|
764
|
-
|
780
|
+
captured_traceback = traceback.format_exc()
|
781
|
+
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_traceback))
|
765
782
|
return
|
766
783
|
except Exception:
|
767
784
|
err_message = f"An unexpected error occurred while initializing node {node_cls.__name__}"
|
768
785
|
logger.exception(err_message)
|
786
|
+
captured_traceback = traceback.format_exc()
|
769
787
|
self._workflow_event_outer_queue.put(
|
770
788
|
self._reject_workflow_event(
|
771
789
|
WorkflowError(code=WorkflowErrorCode.INTERNAL_ERROR, message=err_message),
|
790
|
+
captured_traceback,
|
772
791
|
)
|
773
792
|
)
|
774
793
|
return
|
775
794
|
|
776
|
-
|
795
|
+
rejection_event: Optional[NodeExecutionRejectedEvent] = None
|
777
796
|
|
778
797
|
while True:
|
779
798
|
if not self._active_nodes_by_execution_id:
|
@@ -784,9 +803,9 @@ class WorkflowRunner(Generic[StateType]):
|
|
784
803
|
self._workflow_event_outer_queue.put(event)
|
785
804
|
|
786
805
|
with execution_context(parent_context=current_parent, trace_id=self._execution_context.trace_id):
|
787
|
-
|
806
|
+
rejection_event = self._handle_work_item_event(event)
|
788
807
|
|
789
|
-
if
|
808
|
+
if rejection_event:
|
790
809
|
break
|
791
810
|
|
792
811
|
# Handle any remaining events
|
@@ -795,9 +814,9 @@ class WorkflowRunner(Generic[StateType]):
|
|
795
814
|
self._workflow_event_outer_queue.put(event)
|
796
815
|
|
797
816
|
with execution_context(parent_context=current_parent, trace_id=self._execution_context.trace_id):
|
798
|
-
|
817
|
+
rejection_event = self._handle_work_item_event(event)
|
799
818
|
|
800
|
-
if
|
819
|
+
if rejection_event:
|
801
820
|
break
|
802
821
|
except Empty:
|
803
822
|
pass
|
@@ -817,8 +836,10 @@ class WorkflowRunner(Generic[StateType]):
|
|
817
836
|
)
|
818
837
|
return
|
819
838
|
|
820
|
-
if
|
821
|
-
self._workflow_event_outer_queue.put(
|
839
|
+
if rejection_event:
|
840
|
+
self._workflow_event_outer_queue.put(
|
841
|
+
self._reject_workflow_event(rejection_event.error, rejection_event.body.traceback)
|
842
|
+
)
|
822
843
|
return
|
823
844
|
|
824
845
|
fulfilled_outputs = self.workflow.Outputs()
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from vellum.client.types.function_call import FunctionCall
|
2
|
+
from vellum.workflows.constants import undefined
|
2
3
|
|
3
4
|
|
4
5
|
class StringValueWrapper(str):
|
@@ -71,7 +72,7 @@ class DictWrapper(dict):
|
|
71
72
|
# several values as VellumValue objects, we use the "value" key to return itself
|
72
73
|
return self
|
73
74
|
|
74
|
-
|
75
|
+
return undefined
|
75
76
|
|
76
77
|
item = super().__getitem__(attr)
|
77
78
|
if not isinstance(item, DictWrapper) and not isinstance(item, ListWrapper):
|