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.
Files changed (39) hide show
  1. vellum/client/core/client_wrapper.py +2 -2
  2. vellum/client/types/function_definition.py +5 -0
  3. vellum/client/types/scenario_input_audio_variable_value.py +1 -1
  4. vellum/client/types/scenario_input_document_variable_value.py +1 -1
  5. vellum/client/types/scenario_input_image_variable_value.py +1 -1
  6. vellum/client/types/scenario_input_video_variable_value.py +1 -1
  7. vellum/workflows/emitters/vellum_emitter.py +55 -9
  8. vellum/workflows/events/node.py +1 -1
  9. vellum/workflows/events/tests/test_event.py +1 -1
  10. vellum/workflows/events/workflow.py +1 -1
  11. vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -2
  12. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +21 -15
  13. vellum/workflows/resolvers/resolver.py +18 -2
  14. vellum/workflows/resolvers/tests/test_resolver.py +121 -0
  15. vellum/workflows/runner/runner.py +17 -17
  16. vellum/workflows/state/encoder.py +0 -37
  17. vellum/workflows/state/tests/test_state.py +14 -0
  18. vellum/workflows/types/code_execution_node_wrappers.py +3 -0
  19. vellum/workflows/utils/functions.py +35 -0
  20. vellum/workflows/utils/vellum_variables.py +11 -2
  21. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/METADATA +1 -1
  22. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/RECORD +39 -37
  23. vellum_cli/__init__.py +21 -0
  24. vellum_cli/move.py +56 -0
  25. vellum_cli/tests/test_move.py +154 -0
  26. vellum_ee/workflows/display/base.py +1 -0
  27. vellum_ee/workflows/display/editor/types.py +1 -0
  28. vellum_ee/workflows/display/nodes/base_node_display.py +1 -0
  29. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +18 -2
  30. vellum_ee/workflows/display/tests/test_base_workflow_display.py +52 -2
  31. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +17 -5
  32. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -0
  33. vellum_ee/workflows/display/utils/events.py +1 -0
  34. vellum_ee/workflows/display/utils/expressions.py +44 -0
  35. vellum_ee/workflows/display/utils/tests/test_events.py +11 -1
  36. vellum_ee/workflows/display/workflows/base_workflow_display.py +32 -22
  37. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/LICENSE +0 -0
  38. {vellum_ai-1.3.2.dist-info → vellum_ai-1.3.4.dist-info}/WHEEL +0 -0
  39. {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.2",
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.2",
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
- from typing import Optional
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
- Emit a workflow event to Vellum's infrastructure.
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._send_event(event)
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 emit event {event.name}: {e}")
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 _send_event(self, event: SDKWorkflowEvent) -> None:
114
+ def _send_events(self, events: List[SDKWorkflowEvent]) -> None:
73
115
  """
74
- Send event to Vellum's events endpoint using client.events.create.
116
+ Send events to Vellum's events endpoint using client.events.create.
75
117
 
76
118
  Args:
77
- event: The WorkflowEvent object to send.
119
+ events: List of WorkflowEvent objects to send.
78
120
  """
79
121
  if not self._context:
80
- logger.warning("Cannot send event: No workflow context registered")
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=event, # type: ignore[arg-type]
135
+ request=events, # type: ignore[arg-type]
90
136
  request_options=request_options,
91
137
  )
@@ -141,7 +141,7 @@ class NodeExecutionFulfilledEvent(_BaseNodeEvent, Generic[OutputsType]):
141
141
 
142
142
  class NodeExecutionRejectedBody(_BaseNodeExecutionBody):
143
143
  error: WorkflowError
144
- traceback: Optional[str] = None
144
+ stacktrace: Optional[str] = None
145
145
 
146
146
 
147
147
  class NodeExecutionRejectedEvent(_BaseNodeEvent):
@@ -261,7 +261,7 @@ mock_node_uuid = str(uuid4_from_hash(MockNode.__qualname__))
261
261
  "message": "Workflow failed",
262
262
  "code": "USER_DEFINED_ERROR",
263
263
  },
264
- "traceback": None,
264
+ "stacktrace": None,
265
265
  },
266
266
  "parent": None,
267
267
  "links": None,
@@ -156,7 +156,7 @@ class WorkflowExecutionFulfilledEvent(_BaseWorkflowEvent, Generic[OutputsType]):
156
156
 
157
157
  class WorkflowExecutionRejectedBody(_BaseWorkflowExecutionBody):
158
158
  error: WorkflowError
159
- traceback: Optional[str] = None
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=LazyReference(lambda: State.count.less_than(3)),
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
- LazyReference(
425
- lambda: (
426
- ToolCallingState.current_prompt_output_index.less_than(
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
- # For regular functions, use FunctionNode
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": lambda self, **kwargs: function(**kwargs), # ← Revert back to lambda
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 = state_class(**response.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
- captured_traceback = traceback.format_exc()
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
- traceback=captured_traceback,
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
- captured_traceback = traceback.format_exc()
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
- traceback=captured_traceback,
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, captured_traceback: Optional[str] = None
716
+ self, error: WorkflowError, captured_stacktrace: Optional[str] = None
717
717
  ) -> WorkflowExecutionRejectedEvent:
718
- if captured_traceback is None:
718
+ if captured_stacktrace is None:
719
719
  try:
720
- captured_traceback = traceback.format_exc()
721
- if captured_traceback.strip() == "NoneType: None":
722
- captured_traceback = None
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
- traceback=captured_traceback,
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
- captured_traceback = traceback.format_exc()
777
- self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_traceback))
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
- captured_traceback = traceback.format_exc()
781
- self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error, captured_traceback))
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
- captured_traceback = traceback.format_exc()
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
- captured_traceback,
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.traceback)
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)