vellum-ai 1.3.2__py3-none-any.whl → 1.3.4__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/client/core/client_wrapper.py +2 -2
- vellum/client/types/function_definition.py +5 -0
- vellum/client/types/scenario_input_audio_variable_value.py +1 -1
- vellum/client/types/scenario_input_document_variable_value.py +1 -1
- vellum/client/types/scenario_input_image_variable_value.py +1 -1
- vellum/client/types/scenario_input_video_variable_value.py +1 -1
- vellum/workflows/emitters/vellum_emitter.py +55 -9
- vellum/workflows/events/node.py +1 -1
- vellum/workflows/events/tests/test_event.py +1 -1
- vellum/workflows/events/workflow.py +1 -1
- vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -2
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +21 -15
- vellum/workflows/resolvers/resolver.py +18 -2
- vellum/workflows/resolvers/tests/test_resolver.py +121 -0
- vellum/workflows/runner/runner.py +17 -17
- vellum/workflows/state/encoder.py +0 -37
- vellum/workflows/state/tests/test_state.py +14 -0
- vellum/workflows/types/code_execution_node_wrappers.py +3 -0
- vellum/workflows/utils/functions.py +35 -0
- vellum/workflows/utils/vellum_variables.py +11 -2
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/METADATA +1 -1
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/RECORD +39 -37
- vellum_cli/__init__.py +21 -0
- vellum_cli/move.py +56 -0
- vellum_cli/tests/test_move.py +154 -0
- vellum_ee/workflows/display/base.py +1 -0
- vellum_ee/workflows/display/editor/types.py +1 -0
- vellum_ee/workflows/display/nodes/base_node_display.py +1 -0
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +18 -2
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +52 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +17 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -0
- vellum_ee/workflows/display/utils/events.py +1 -0
- vellum_ee/workflows/display/utils/expressions.py +44 -0
- vellum_ee/workflows/display/utils/tests/test_events.py +11 -1
- vellum_ee/workflows/display/workflows/base_workflow_display.py +32 -22
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/LICENSE +0 -0
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/WHEEL +0 -0
- {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/entry_points.txt +0 -0
@@ -27,10 +27,10 @@ class BaseClientWrapper:
|
|
27
27
|
|
28
28
|
def get_headers(self) -> typing.Dict[str, str]:
|
29
29
|
headers: typing.Dict[str, str] = {
|
30
|
-
"User-Agent": "vellum-ai/1.3.
|
30
|
+
"User-Agent": "vellum-ai/1.3.4",
|
31
31
|
"X-Fern-Language": "Python",
|
32
32
|
"X-Fern-SDK-Name": "vellum-ai",
|
33
|
-
"X-Fern-SDK-Version": "1.3.
|
33
|
+
"X-Fern-SDK-Version": "1.3.4",
|
34
34
|
**(self.get_custom_headers() or {}),
|
35
35
|
}
|
36
36
|
if self._api_version is not None:
|
@@ -30,6 +30,11 @@ class FunctionDefinition(UniversalBaseModel):
|
|
30
30
|
An OpenAPI specification of parameters that are supported by this function.
|
31
31
|
"""
|
32
32
|
|
33
|
+
inputs: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None)
|
34
|
+
"""
|
35
|
+
Optional user defined input mappings for this function.
|
36
|
+
"""
|
37
|
+
|
33
38
|
forced: typing.Optional[bool] = pydantic.Field(default=None)
|
34
39
|
"""
|
35
40
|
Set this option to true to force the model to return a function call of this function.
|
@@ -9,7 +9,7 @@ from .vellum_audio import VellumAudio
|
|
9
9
|
|
10
10
|
class ScenarioInputAudioVariableValue(UniversalBaseModel):
|
11
11
|
type: typing.Literal["AUDIO"] = "AUDIO"
|
12
|
-
value: VellumAudio
|
12
|
+
value: typing.Optional[VellumAudio] = None
|
13
13
|
input_variable_id: str
|
14
14
|
|
15
15
|
if IS_PYDANTIC_V2:
|
@@ -9,7 +9,7 @@ from .vellum_document import VellumDocument
|
|
9
9
|
|
10
10
|
class ScenarioInputDocumentVariableValue(UniversalBaseModel):
|
11
11
|
type: typing.Literal["DOCUMENT"] = "DOCUMENT"
|
12
|
-
value: VellumDocument
|
12
|
+
value: typing.Optional[VellumDocument] = None
|
13
13
|
input_variable_id: str
|
14
14
|
|
15
15
|
if IS_PYDANTIC_V2:
|
@@ -9,7 +9,7 @@ from .vellum_image import VellumImage
|
|
9
9
|
|
10
10
|
class ScenarioInputImageVariableValue(UniversalBaseModel):
|
11
11
|
type: typing.Literal["IMAGE"] = "IMAGE"
|
12
|
-
value: VellumImage
|
12
|
+
value: typing.Optional[VellumImage] = None
|
13
13
|
input_variable_id: str
|
14
14
|
|
15
15
|
if IS_PYDANTIC_V2:
|
@@ -9,7 +9,7 @@ from .vellum_video import VellumVideo
|
|
9
9
|
|
10
10
|
class ScenarioInputVideoVariableValue(UniversalBaseModel):
|
11
11
|
type: typing.Literal["VIDEO"] = "VIDEO"
|
12
|
-
value: VellumVideo
|
12
|
+
value: typing.Optional[VellumVideo] = None
|
13
13
|
input_variable_id: str
|
14
14
|
|
15
15
|
if IS_PYDANTIC_V2:
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import logging
|
2
|
-
|
2
|
+
import threading
|
3
|
+
from typing import List, Optional
|
3
4
|
|
4
5
|
from vellum.core.request_options import RequestOptions
|
5
6
|
from vellum.workflows.emitters.base import BaseWorkflowEmitter
|
@@ -29,6 +30,7 @@ class VellumEmitter(BaseWorkflowEmitter):
|
|
29
30
|
*,
|
30
31
|
timeout: Optional[float] = 30.0,
|
31
32
|
max_retries: int = 3,
|
33
|
+
debounce_timeout: float = 0.1,
|
32
34
|
):
|
33
35
|
"""
|
34
36
|
Initialize the VellumEmitter.
|
@@ -36,14 +38,19 @@ class VellumEmitter(BaseWorkflowEmitter):
|
|
36
38
|
Args:
|
37
39
|
timeout: Request timeout in seconds.
|
38
40
|
max_retries: Maximum number of retry attempts for failed requests.
|
41
|
+
debounce_timeout: Time in seconds to wait before sending batched events.
|
39
42
|
"""
|
40
43
|
super().__init__()
|
41
44
|
self._timeout = timeout
|
42
45
|
self._max_retries = max_retries
|
46
|
+
self._debounce_timeout = debounce_timeout
|
47
|
+
self._event_queue: List[SDKWorkflowEvent] = []
|
48
|
+
self._queue_lock = threading.Lock()
|
49
|
+
self._debounce_timer: Optional[threading.Timer] = None
|
43
50
|
|
44
51
|
def emit_event(self, event: SDKWorkflowEvent) -> None:
|
45
52
|
"""
|
46
|
-
|
53
|
+
Queue a workflow event for batched emission to Vellum's infrastructure.
|
47
54
|
|
48
55
|
Args:
|
49
56
|
event: The workflow event to emit.
|
@@ -55,10 +62,45 @@ class VellumEmitter(BaseWorkflowEmitter):
|
|
55
62
|
return
|
56
63
|
|
57
64
|
try:
|
58
|
-
self.
|
65
|
+
with self._queue_lock:
|
66
|
+
self._event_queue.append(event)
|
59
67
|
|
68
|
+
if self._debounce_timer:
|
69
|
+
self._debounce_timer.cancel()
|
70
|
+
|
71
|
+
self._debounce_timer = threading.Timer(self._debounce_timeout, self._flush_events)
|
72
|
+
self._debounce_timer.start()
|
73
|
+
|
74
|
+
except Exception as e:
|
75
|
+
logger.exception(f"Failed to queue event {event.name}: {e}")
|
76
|
+
|
77
|
+
def _flush_events(self) -> None:
|
78
|
+
"""
|
79
|
+
Send all queued events as a batch to Vellum's infrastructure.
|
80
|
+
"""
|
81
|
+
with self._queue_lock:
|
82
|
+
if not self._event_queue:
|
83
|
+
return
|
84
|
+
|
85
|
+
events_to_send = self._event_queue.copy()
|
86
|
+
self._event_queue.clear()
|
87
|
+
self._debounce_timer = None
|
88
|
+
|
89
|
+
try:
|
90
|
+
self._send_events(events_to_send)
|
60
91
|
except Exception as e:
|
61
|
-
logger.exception(f"Failed to
|
92
|
+
logger.exception(f"Failed to send batched events: {e}")
|
93
|
+
|
94
|
+
def __del__(self) -> None:
|
95
|
+
"""
|
96
|
+
Cleanup: flush any pending events and cancel timer.
|
97
|
+
"""
|
98
|
+
try:
|
99
|
+
if self._debounce_timer:
|
100
|
+
self._debounce_timer.cancel()
|
101
|
+
self._flush_events()
|
102
|
+
except Exception:
|
103
|
+
pass
|
62
104
|
|
63
105
|
def snapshot_state(self, state: BaseState) -> None:
|
64
106
|
"""
|
@@ -69,23 +111,27 @@ class VellumEmitter(BaseWorkflowEmitter):
|
|
69
111
|
"""
|
70
112
|
pass
|
71
113
|
|
72
|
-
def
|
114
|
+
def _send_events(self, events: List[SDKWorkflowEvent]) -> None:
|
73
115
|
"""
|
74
|
-
Send
|
116
|
+
Send events to Vellum's events endpoint using client.events.create.
|
75
117
|
|
76
118
|
Args:
|
77
|
-
|
119
|
+
events: List of WorkflowEvent objects to send.
|
78
120
|
"""
|
79
121
|
if not self._context:
|
80
|
-
logger.warning("Cannot send
|
122
|
+
logger.warning("Cannot send events: No workflow context registered")
|
123
|
+
return
|
124
|
+
|
125
|
+
if not events:
|
81
126
|
return
|
82
127
|
|
83
128
|
client = self._context.vellum_client
|
84
129
|
request_options = RequestOptions(timeout_in_seconds=self._timeout, max_retries=self._max_retries)
|
130
|
+
|
85
131
|
client.events.create(
|
86
132
|
# The API accepts a ClientWorkflowEvent but our SDK emits an SDKWorkflowEvent. These shapes are
|
87
133
|
# meant to be identical, just with different helper methods. We may consolidate the two in the future.
|
88
134
|
# But for now, the type ignore allows us to avoid an additional Model -> json -> Model conversion.
|
89
|
-
request=
|
135
|
+
request=events, # type: ignore[arg-type]
|
90
136
|
request_options=request_options,
|
91
137
|
)
|
vellum/workflows/events/node.py
CHANGED
@@ -141,7 +141,7 @@ class NodeExecutionFulfilledEvent(_BaseNodeEvent, Generic[OutputsType]):
|
|
141
141
|
|
142
142
|
class NodeExecutionRejectedBody(_BaseNodeExecutionBody):
|
143
143
|
error: WorkflowError
|
144
|
-
|
144
|
+
stacktrace: Optional[str] = None
|
145
145
|
|
146
146
|
|
147
147
|
class NodeExecutionRejectedEvent(_BaseNodeEvent):
|
@@ -156,7 +156,7 @@ class WorkflowExecutionFulfilledEvent(_BaseWorkflowEvent, Generic[OutputsType]):
|
|
156
156
|
|
157
157
|
class WorkflowExecutionRejectedBody(_BaseWorkflowExecutionBody):
|
158
158
|
error: WorkflowError
|
159
|
-
|
159
|
+
stacktrace: Optional[str] = None
|
160
160
|
|
161
161
|
|
162
162
|
class WorkflowExecutionRejectedEvent(_BaseWorkflowEvent):
|
@@ -6,7 +6,6 @@ from vellum.workflows.inputs.base import BaseInputs
|
|
6
6
|
from vellum.workflows.nodes.bases import BaseNode
|
7
7
|
from vellum.workflows.nodes.core.retry_node.node import RetryNode
|
8
8
|
from vellum.workflows.outputs import BaseOutputs
|
9
|
-
from vellum.workflows.references.lazy import LazyReference
|
10
9
|
from vellum.workflows.state.base import BaseState, StateMeta
|
11
10
|
|
12
11
|
|
@@ -102,7 +101,7 @@ def test_retry_node__condition_arg_successfully_retries():
|
|
102
101
|
# AND a retry node that retries on a condition
|
103
102
|
@RetryNode.wrap(
|
104
103
|
max_attempts=5,
|
105
|
-
retry_on_condition=
|
104
|
+
retry_on_condition=State.count.less_than(3),
|
106
105
|
)
|
107
106
|
class TestNode(BaseNode[State]):
|
108
107
|
attempt_number = RetryNode.SubworkflowInputs.attempt_number
|
@@ -27,7 +27,6 @@ from vellum.workflows.nodes.displayable.subworkflow_deployment_node.node import
|
|
27
27
|
from vellum.workflows.nodes.displayable.tool_calling_node.state import ToolCallingState
|
28
28
|
from vellum.workflows.outputs.base import BaseOutput
|
29
29
|
from vellum.workflows.ports.port import Port
|
30
|
-
from vellum.workflows.references.lazy import LazyReference
|
31
30
|
from vellum.workflows.state import BaseState
|
32
31
|
from vellum.workflows.state.encoder import DefaultStateEncoder
|
33
32
|
from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior, Tool, ToolBase
|
@@ -421,19 +420,13 @@ def create_router_node(
|
|
421
420
|
# and if the function_name is changed, the port_condition will also change.
|
422
421
|
def create_port_condition(fn_name):
|
423
422
|
return Port.on_if(
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
tool_prompt_node.Outputs.results.length()
|
428
|
-
)
|
429
|
-
& tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
|
430
|
-
"FUNCTION_CALL"
|
431
|
-
)
|
432
|
-
& tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"][
|
433
|
-
"name"
|
434
|
-
].equals(fn_name)
|
435
|
-
)
|
423
|
+
ToolCallingState.current_prompt_output_index.less_than(tool_prompt_node.Outputs.results.length())
|
424
|
+
& tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
|
425
|
+
"FUNCTION_CALL"
|
436
426
|
)
|
427
|
+
& tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"][
|
428
|
+
"name"
|
429
|
+
].equals(fn_name)
|
437
430
|
)
|
438
431
|
|
439
432
|
for function in functions:
|
@@ -527,12 +520,25 @@ def create_function_node(
|
|
527
520
|
},
|
528
521
|
)
|
529
522
|
else:
|
530
|
-
|
523
|
+
|
524
|
+
def create_function_wrapper(func):
|
525
|
+
def wrapper(self, **kwargs):
|
526
|
+
merged_kwargs = kwargs.copy()
|
527
|
+
inputs = getattr(func, "__vellum_inputs__", {})
|
528
|
+
if inputs:
|
529
|
+
for param_name, param_ref in inputs.items():
|
530
|
+
resolved_value = param_ref.resolve(self.state)
|
531
|
+
merged_kwargs[param_name] = resolved_value
|
532
|
+
|
533
|
+
return func(**merged_kwargs)
|
534
|
+
|
535
|
+
return wrapper
|
536
|
+
|
531
537
|
node = type(
|
532
538
|
f"FunctionNode_{function.__name__}",
|
533
539
|
(FunctionNode,),
|
534
540
|
{
|
535
|
-
"function_definition":
|
541
|
+
"function_definition": create_function_wrapper(function),
|
536
542
|
"function_call_output": tool_prompt_node.Outputs.results,
|
537
543
|
"__module__": __name__,
|
538
544
|
},
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import logging
|
2
2
|
from uuid import UUID
|
3
|
-
from typing import Iterator, List, Optional, Tuple, Union
|
3
|
+
from typing import Iterator, List, Optional, Tuple, Type, Union
|
4
4
|
|
5
5
|
from vellum.client.types.vellum_span import VellumSpan
|
6
6
|
from vellum.client.types.workflow_execution_initiated_event import WorkflowExecutionInitiatedEvent
|
7
7
|
from vellum.workflows.events.workflow import WorkflowEvent
|
8
|
+
from vellum.workflows.nodes.utils import cast_to_output_type
|
8
9
|
from vellum.workflows.resolvers.base import BaseWorkflowResolver
|
9
10
|
from vellum.workflows.resolvers.types import LoadStateResult
|
10
11
|
from vellum.workflows.state.base import BaseState
|
@@ -51,6 +52,21 @@ class VellumResolver(BaseWorkflowResolver):
|
|
51
52
|
|
52
53
|
return previous_trace_id, root_trace_id, previous_span_id, root_span_id
|
53
54
|
|
55
|
+
def _deserialize_state(self, state_data: dict, state_class: Type[BaseState]) -> BaseState:
|
56
|
+
"""Deserialize state data with proper type conversion for complex types like List[ChatMessage]."""
|
57
|
+
converted_data = {}
|
58
|
+
|
59
|
+
annotations = getattr(state_class, "__annotations__", {})
|
60
|
+
|
61
|
+
for field_name, field_value in state_data.items():
|
62
|
+
if field_name in annotations:
|
63
|
+
field_type = annotations[field_name]
|
64
|
+
converted_data[field_name] = cast_to_output_type(field_value, field_type)
|
65
|
+
else:
|
66
|
+
converted_data[field_name] = field_value
|
67
|
+
|
68
|
+
return state_class(**converted_data)
|
69
|
+
|
54
70
|
def load_state(self, previous_execution_id: Optional[Union[UUID, str]] = None) -> Optional[LoadStateResult]:
|
55
71
|
if isinstance(previous_execution_id, UUID):
|
56
72
|
previous_execution_id = str(previous_execution_id)
|
@@ -83,7 +99,7 @@ class VellumResolver(BaseWorkflowResolver):
|
|
83
99
|
|
84
100
|
if self._workflow_class:
|
85
101
|
state_class = self._workflow_class.get_state_class()
|
86
|
-
state =
|
102
|
+
state = self._deserialize_state(response.state, state_class)
|
87
103
|
else:
|
88
104
|
logger.warning("No workflow class registered, falling back to BaseState")
|
89
105
|
state = BaseState(**response.state)
|
@@ -1,7 +1,9 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from unittest.mock import Mock
|
3
3
|
from uuid import uuid4
|
4
|
+
from typing import List
|
4
5
|
|
6
|
+
from vellum import ChatMessage
|
5
7
|
from vellum.client.types.span_link import SpanLink
|
6
8
|
from vellum.client.types.vellum_code_resource_definition import VellumCodeResourceDefinition
|
7
9
|
from vellum.client.types.workflow_execution_detail import WorkflowExecutionDetail
|
@@ -129,3 +131,122 @@ def test_load_state_with_context_success():
|
|
129
131
|
mock_client.workflow_executions.retrieve_workflow_execution_detail.assert_called_once_with(
|
130
132
|
execution_id=str(execution_id)
|
131
133
|
)
|
134
|
+
|
135
|
+
|
136
|
+
def test_load_state_with_chat_message_list():
|
137
|
+
"""Test load_state successfully loads state with chat_history containing ChatMessage list."""
|
138
|
+
resolver = VellumResolver()
|
139
|
+
execution_id = uuid4()
|
140
|
+
root_execution_id = uuid4()
|
141
|
+
|
142
|
+
class TestStateWithChatHistory(BaseState):
|
143
|
+
test_key: str = "test_value"
|
144
|
+
chat_history: List[ChatMessage] = []
|
145
|
+
|
146
|
+
class TestWorkflow(BaseWorkflow[BaseInputs, TestStateWithChatHistory]):
|
147
|
+
pass
|
148
|
+
|
149
|
+
# GIVEN a state dictionary with chat_history containing ChatMessage objects
|
150
|
+
prev_id = str(uuid4())
|
151
|
+
prev_span_id = str(uuid4())
|
152
|
+
state_dict = {
|
153
|
+
"test_key": "test_value",
|
154
|
+
"chat_history": [
|
155
|
+
{"role": "USER", "text": "Hello, how are you?"},
|
156
|
+
{"role": "ASSISTANT", "text": "I'm doing well, thank you!"},
|
157
|
+
{"role": "USER", "text": "What can you help me with?"},
|
158
|
+
],
|
159
|
+
"meta": {
|
160
|
+
"workflow_definition": "MockWorkflow",
|
161
|
+
"id": prev_id,
|
162
|
+
"span_id": prev_span_id,
|
163
|
+
"updated_ts": datetime.now().isoformat(),
|
164
|
+
"workflow_inputs": BaseInputs(),
|
165
|
+
"external_inputs": {},
|
166
|
+
"node_outputs": {},
|
167
|
+
"node_execution_cache": NodeExecutionCache(),
|
168
|
+
"parent": None,
|
169
|
+
},
|
170
|
+
}
|
171
|
+
|
172
|
+
mock_workflow_definition = VellumCodeResourceDefinition(
|
173
|
+
name="TestWorkflow", module=["test", "module"], id=str(uuid4())
|
174
|
+
)
|
175
|
+
|
176
|
+
mock_body = WorkflowExecutionInitiatedBody(workflow_definition=mock_workflow_definition, inputs={})
|
177
|
+
|
178
|
+
previous_trace_id = str(uuid4())
|
179
|
+
root_trace_id = str(uuid4())
|
180
|
+
|
181
|
+
previous_invocation = WorkflowExecutionInitiatedEvent(
|
182
|
+
id=str(uuid4()),
|
183
|
+
timestamp=datetime.now(),
|
184
|
+
trace_id=previous_trace_id,
|
185
|
+
span_id=str(execution_id),
|
186
|
+
body=mock_body,
|
187
|
+
links=[
|
188
|
+
SpanLink(
|
189
|
+
trace_id=previous_trace_id,
|
190
|
+
type="PREVIOUS_SPAN",
|
191
|
+
span_context=WorkflowParentContext(workflow_definition=mock_workflow_definition, span_id=str(uuid4())),
|
192
|
+
),
|
193
|
+
SpanLink(
|
194
|
+
trace_id=root_trace_id,
|
195
|
+
type="ROOT_SPAN",
|
196
|
+
span_context=WorkflowParentContext(
|
197
|
+
workflow_definition=mock_workflow_definition, span_id=str(root_execution_id)
|
198
|
+
),
|
199
|
+
),
|
200
|
+
],
|
201
|
+
)
|
202
|
+
|
203
|
+
root_invocation = WorkflowExecutionInitiatedEvent(
|
204
|
+
id=str(uuid4()),
|
205
|
+
timestamp=datetime.now(),
|
206
|
+
trace_id=root_trace_id,
|
207
|
+
span_id=str(root_execution_id),
|
208
|
+
body=mock_body,
|
209
|
+
links=None,
|
210
|
+
)
|
211
|
+
|
212
|
+
mock_span = WorkflowExecutionSpan(
|
213
|
+
span_id=str(execution_id),
|
214
|
+
start_ts=datetime.now(),
|
215
|
+
end_ts=datetime.now(),
|
216
|
+
attributes=WorkflowExecutionSpanAttributes(label="Test Workflow", workflow_id=str(uuid4())),
|
217
|
+
events=[previous_invocation, root_invocation],
|
218
|
+
)
|
219
|
+
|
220
|
+
mock_response = WorkflowExecutionDetail(
|
221
|
+
span_id="test-span-id", start=datetime.now(), inputs=[], outputs=[], spans=[mock_span], state=state_dict
|
222
|
+
)
|
223
|
+
|
224
|
+
mock_client = Mock()
|
225
|
+
mock_client.workflow_executions.retrieve_workflow_execution_detail.return_value = mock_response
|
226
|
+
|
227
|
+
# AND context with the test workflow class is set up
|
228
|
+
context = WorkflowContext(vellum_client=mock_client)
|
229
|
+
TestWorkflow(context=context, resolvers=[resolver])
|
230
|
+
|
231
|
+
# WHEN load_state is called
|
232
|
+
result = resolver.load_state(previous_execution_id=execution_id)
|
233
|
+
|
234
|
+
# THEN should return LoadStateResult with state containing chat_history
|
235
|
+
assert isinstance(result, LoadStateResult)
|
236
|
+
assert result.state is not None
|
237
|
+
assert isinstance(result.state, TestStateWithChatHistory)
|
238
|
+
assert result.state.test_key == "test_value"
|
239
|
+
|
240
|
+
# AND the chat_history should be properly deserialized as ChatMessage objects
|
241
|
+
assert len(result.state.chat_history) == 3
|
242
|
+
assert all(isinstance(msg, ChatMessage) for msg in result.state.chat_history)
|
243
|
+
assert result.state.chat_history[0].role == "USER"
|
244
|
+
assert result.state.chat_history[0].text == "Hello, how are you?"
|
245
|
+
assert result.state.chat_history[1].role == "ASSISTANT"
|
246
|
+
assert result.state.chat_history[1].text == "I'm doing well, thank you!"
|
247
|
+
assert result.state.chat_history[2].role == "USER"
|
248
|
+
assert result.state.chat_history[2].text == "What can you help me with?"
|
249
|
+
|
250
|
+
mock_client.workflow_executions.retrieve_workflow_execution_detail.assert_called_once_with(
|
251
|
+
execution_id=str(execution_id)
|
252
|
+
)
|
@@ -404,7 +404,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
404
404
|
)
|
405
405
|
except NodeException as e:
|
406
406
|
logger.info(e)
|
407
|
-
|
407
|
+
captured_stacktrace = traceback.format_exc()
|
408
408
|
|
409
409
|
self._workflow_event_inner_queue.put(
|
410
410
|
NodeExecutionRejectedEvent(
|
@@ -413,14 +413,14 @@ class WorkflowRunner(Generic[StateType]):
|
|
413
413
|
body=NodeExecutionRejectedBody(
|
414
414
|
node_definition=node.__class__,
|
415
415
|
error=e.error,
|
416
|
-
|
416
|
+
stacktrace=captured_stacktrace,
|
417
417
|
),
|
418
418
|
parent=execution.parent_context,
|
419
419
|
)
|
420
420
|
)
|
421
421
|
except WorkflowInitializationException as e:
|
422
422
|
logger.info(e)
|
423
|
-
|
423
|
+
captured_stacktrace = traceback.format_exc()
|
424
424
|
self._workflow_event_inner_queue.put(
|
425
425
|
NodeExecutionRejectedEvent(
|
426
426
|
trace_id=execution.trace_id,
|
@@ -428,7 +428,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
428
428
|
body=NodeExecutionRejectedBody(
|
429
429
|
node_definition=node.__class__,
|
430
430
|
error=e.error,
|
431
|
-
|
431
|
+
stacktrace=captured_stacktrace,
|
432
432
|
),
|
433
433
|
parent=execution.parent_context,
|
434
434
|
)
|
@@ -713,13 +713,13 @@ class WorkflowRunner(Generic[StateType]):
|
|
713
713
|
)
|
714
714
|
|
715
715
|
def _reject_workflow_event(
|
716
|
-
self, error: WorkflowError,
|
716
|
+
self, error: WorkflowError, captured_stacktrace: Optional[str] = None
|
717
717
|
) -> WorkflowExecutionRejectedEvent:
|
718
|
-
if
|
718
|
+
if captured_stacktrace is None:
|
719
719
|
try:
|
720
|
-
|
721
|
-
if
|
722
|
-
|
720
|
+
captured_stacktrace = traceback.format_exc()
|
721
|
+
if captured_stacktrace.strip() == "NoneType: None":
|
722
|
+
captured_stacktrace = None
|
723
723
|
except Exception:
|
724
724
|
pass
|
725
725
|
|
@@ -729,7 +729,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
729
729
|
body=WorkflowExecutionRejectedBody(
|
730
730
|
workflow_definition=self.workflow.__class__,
|
731
731
|
error=error,
|
732
|
-
|
732
|
+
stacktrace=captured_stacktrace,
|
733
733
|
),
|
734
734
|
parent=self._execution_context.parent_context,
|
735
735
|
)
|
@@ -773,21 +773,21 @@ class WorkflowRunner(Generic[StateType]):
|
|
773
773
|
else:
|
774
774
|
self._concurrency_queue.put((self._initial_state, node_cls, None))
|
775
775
|
except NodeException as e:
|
776
|
-
|
777
|
-
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error,
|
776
|
+
captured_stacktrace = traceback.format_exc()
|
777
|
+
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_stacktrace))
|
778
778
|
return
|
779
779
|
except WorkflowInitializationException as e:
|
780
|
-
|
781
|
-
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error,
|
780
|
+
captured_stacktrace = traceback.format_exc()
|
781
|
+
self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_stacktrace))
|
782
782
|
return
|
783
783
|
except Exception:
|
784
784
|
err_message = f"An unexpected error occurred while initializing node {node_cls.__name__}"
|
785
785
|
logger.exception(err_message)
|
786
|
-
|
786
|
+
captured_stacktrace = traceback.format_exc()
|
787
787
|
self._workflow_event_outer_queue.put(
|
788
788
|
self._reject_workflow_event(
|
789
789
|
WorkflowError(code=WorkflowErrorCode.INTERNAL_ERROR, message=err_message),
|
790
|
-
|
790
|
+
captured_stacktrace,
|
791
791
|
)
|
792
792
|
)
|
793
793
|
return
|
@@ -838,7 +838,7 @@ class WorkflowRunner(Generic[StateType]):
|
|
838
838
|
|
839
839
|
if rejection_event:
|
840
840
|
self._workflow_event_outer_queue.put(
|
841
|
-
self._reject_workflow_event(rejection_event.error, rejection_event.body.
|
841
|
+
self._reject_workflow_event(rejection_event.error, rejection_event.body.stacktrace)
|
842
842
|
)
|
843
843
|
return
|
844
844
|
|
@@ -1,11 +1,8 @@
|
|
1
1
|
from dataclasses import asdict, is_dataclass
|
2
2
|
from datetime import datetime
|
3
3
|
import enum
|
4
|
-
import inspect
|
5
|
-
from io import StringIO
|
6
4
|
from json import JSONEncoder
|
7
5
|
from queue import Queue
|
8
|
-
import sys
|
9
6
|
from uuid import UUID
|
10
7
|
from typing import Any, Callable, Dict, Type
|
11
8
|
|
@@ -17,23 +14,6 @@ from vellum.workflows.inputs.base import BaseInputs
|
|
17
14
|
from vellum.workflows.outputs.base import BaseOutput, BaseOutputs
|
18
15
|
from vellum.workflows.ports.port import Port
|
19
16
|
from vellum.workflows.state.base import BaseState, NodeExecutionCache
|
20
|
-
from vellum.workflows.utils.functions import compile_function_definition
|
21
|
-
|
22
|
-
|
23
|
-
def virtual_open(file_path: str, mode: str = "r"):
|
24
|
-
"""
|
25
|
-
Open a file, checking VirtualFileFinder instances first before falling back to regular open().
|
26
|
-
"""
|
27
|
-
for finder in sys.meta_path:
|
28
|
-
if hasattr(finder, "loader") and hasattr(finder.loader, "_get_code"):
|
29
|
-
namespace = finder.loader.namespace
|
30
|
-
if file_path.startswith(namespace + "/"):
|
31
|
-
relative_path = file_path[len(namespace) + 1 :]
|
32
|
-
content = finder.loader._get_code(relative_path)
|
33
|
-
if content is not None:
|
34
|
-
return StringIO(content)
|
35
|
-
|
36
|
-
return open(file_path, mode)
|
37
17
|
|
38
18
|
|
39
19
|
class DefaultStateEncoder(JSONEncoder):
|
@@ -80,23 +60,6 @@ class DefaultStateEncoder(JSONEncoder):
|
|
80
60
|
if isinstance(obj, type):
|
81
61
|
return str(obj)
|
82
62
|
|
83
|
-
if callable(obj):
|
84
|
-
function_definition = compile_function_definition(obj)
|
85
|
-
source_path = inspect.getsourcefile(obj)
|
86
|
-
if source_path is not None:
|
87
|
-
with virtual_open(source_path) as f:
|
88
|
-
source_code = f.read()
|
89
|
-
else:
|
90
|
-
source_code = f"# Error: Source code not available for {obj.__name__}"
|
91
|
-
|
92
|
-
return {
|
93
|
-
"type": "CODE_EXECUTION",
|
94
|
-
"name": function_definition.name,
|
95
|
-
"description": function_definition.description,
|
96
|
-
"definition": function_definition,
|
97
|
-
"src": source_code,
|
98
|
-
}
|
99
|
-
|
100
63
|
if obj.__class__ in self.encoders:
|
101
64
|
return self.encoders[obj.__class__](obj)
|
102
65
|
|
@@ -4,11 +4,13 @@ import json
|
|
4
4
|
from queue import Queue
|
5
5
|
from typing import Dict, List, cast
|
6
6
|
|
7
|
+
from vellum.workflows.constants import undefined
|
7
8
|
from vellum.workflows.nodes.bases import BaseNode
|
8
9
|
from vellum.workflows.outputs.base import BaseOutputs
|
9
10
|
from vellum.workflows.state.base import BaseState
|
10
11
|
from vellum.workflows.state.delta import SetStateDelta, StateDelta
|
11
12
|
from vellum.workflows.state.encoder import DefaultStateEncoder
|
13
|
+
from vellum.workflows.types.code_execution_node_wrappers import DictWrapper
|
12
14
|
|
13
15
|
|
14
16
|
@pytest.fixture()
|
@@ -229,3 +231,15 @@ def test_state_snapshot__deepcopy_fails__logs_error(mock_deepcopy, mock_logger):
|
|
229
231
|
|
230
232
|
# AND alert sentry once
|
231
233
|
assert mock_logger.exception.call_count == 1
|
234
|
+
|
235
|
+
|
236
|
+
def test_state_deepcopy_handles_undefined_values():
|
237
|
+
# GIVEN a state with undefined values in node outputs
|
238
|
+
state = MockState(foo="bar")
|
239
|
+
state.meta.node_outputs[MockNode.Outputs.baz] = DictWrapper({"foo": undefined})
|
240
|
+
|
241
|
+
# WHEN we deepcopy the state
|
242
|
+
deepcopied_state = deepcopy(state)
|
243
|
+
|
244
|
+
# THEN the undefined values are preserved
|
245
|
+
assert deepcopied_state.meta.node_outputs[MockNode.Outputs.baz] == {"foo": undefined}
|
@@ -72,6 +72,9 @@ class DictWrapper(dict):
|
|
72
72
|
# several values as VellumValue objects, we use the "value" key to return itself
|
73
73
|
return self
|
74
74
|
|
75
|
+
if attr.startswith("__") and attr.endswith("__"):
|
76
|
+
return super().__getattribute__(attr)
|
77
|
+
|
75
78
|
return undefined
|
76
79
|
|
77
80
|
item = super().__getitem__(attr)
|