vellum-ai 1.2.0__py3-none-any.whl → 1.2.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.
Files changed (94) hide show
  1. vellum/__init__.py +18 -1
  2. vellum/client/__init__.py +3 -0
  3. vellum/client/core/client_wrapper.py +2 -2
  4. vellum/client/errors/__init__.py +10 -1
  5. vellum/client/errors/too_many_requests_error.py +11 -0
  6. vellum/client/errors/unauthorized_error.py +11 -0
  7. vellum/client/reference.md +94 -0
  8. vellum/client/resources/__init__.py +2 -0
  9. vellum/client/resources/events/__init__.py +4 -0
  10. vellum/client/resources/events/client.py +165 -0
  11. vellum/client/resources/events/raw_client.py +207 -0
  12. vellum/client/types/__init__.py +6 -0
  13. vellum/client/types/error_detail_response.py +22 -0
  14. vellum/client/types/event_create_response.py +26 -0
  15. vellum/client/types/execution_thinking_vellum_value.py +1 -1
  16. vellum/client/types/thinking_vellum_value.py +1 -1
  17. vellum/client/types/thinking_vellum_value_request.py +1 -1
  18. vellum/client/types/workflow_event.py +33 -0
  19. vellum/errors/too_many_requests_error.py +3 -0
  20. vellum/errors/unauthorized_error.py +3 -0
  21. vellum/prompts/blocks/compilation.py +13 -11
  22. vellum/resources/events/__init__.py +3 -0
  23. vellum/resources/events/client.py +3 -0
  24. vellum/resources/events/raw_client.py +3 -0
  25. vellum/types/error_detail_response.py +3 -0
  26. vellum/types/event_create_response.py +3 -0
  27. vellum/types/workflow_event.py +3 -0
  28. vellum/workflows/emitters/vellum_emitter.py +16 -69
  29. vellum/workflows/events/tests/test_event.py +1 -0
  30. vellum/workflows/events/workflow.py +3 -0
  31. vellum/workflows/nodes/bases/base.py +0 -1
  32. vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py +35 -0
  33. vellum/workflows/nodes/displayable/bases/api_node/node.py +4 -0
  34. vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +26 -0
  35. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +6 -1
  36. vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +22 -0
  37. vellum/workflows/nodes/displayable/bases/utils.py +4 -2
  38. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +88 -2
  39. vellum/workflows/nodes/displayable/tool_calling_node/node.py +1 -0
  40. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +85 -1
  41. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +12 -0
  42. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +5 -2
  43. vellum/workflows/ports/port.py +1 -11
  44. vellum/workflows/sandbox.py +6 -3
  45. vellum/workflows/state/context.py +14 -0
  46. vellum/workflows/state/encoder.py +19 -1
  47. vellum/workflows/types/definition.py +4 -4
  48. vellum/workflows/utils/hmac.py +44 -0
  49. vellum/workflows/utils/vellum_variables.py +5 -3
  50. vellum/workflows/workflows/base.py +1 -0
  51. {vellum_ai-1.2.0.dist-info → vellum_ai-1.2.2.dist-info}/METADATA +1 -1
  52. {vellum_ai-1.2.0.dist-info → vellum_ai-1.2.2.dist-info}/RECORD +94 -76
  53. vellum_ee/workflows/display/nodes/base_node_display.py +19 -10
  54. vellum_ee/workflows/display/nodes/vellum/api_node.py +1 -4
  55. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -4
  56. vellum_ee/workflows/display/nodes/vellum/conditional_node.py +1 -4
  57. vellum_ee/workflows/display/nodes/vellum/error_node.py +6 -4
  58. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +6 -4
  59. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +1 -4
  60. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +34 -15
  61. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +1 -4
  62. vellum_ee/workflows/display/nodes/vellum/map_node.py +1 -4
  63. vellum_ee/workflows/display/nodes/vellum/merge_node.py +1 -4
  64. vellum_ee/workflows/display/nodes/vellum/note_node.py +2 -4
  65. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +1 -4
  66. vellum_ee/workflows/display/nodes/vellum/search_node.py +1 -4
  67. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +1 -4
  68. vellum_ee/workflows/display/nodes/vellum/templating_node.py +1 -4
  69. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +1 -0
  70. vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +239 -1
  71. vellum_ee/workflows/display/tests/test_base_workflow_display.py +53 -1
  72. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +4 -0
  73. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +12 -0
  74. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +16 -0
  75. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +5 -0
  76. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +4 -0
  77. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +4 -0
  78. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +4 -0
  79. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +4 -0
  80. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +12 -0
  81. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +4 -0
  82. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +4 -0
  83. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +4 -0
  84. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +5 -0
  85. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +1 -0
  86. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +5 -0
  87. vellum_ee/workflows/display/utils/expressions.py +4 -0
  88. vellum_ee/workflows/display/utils/registry.py +46 -0
  89. vellum_ee/workflows/display/workflows/base_workflow_display.py +1 -1
  90. vellum_ee/workflows/tests/test_registry.py +169 -0
  91. vellum_ee/workflows/tests/test_server.py +72 -0
  92. {vellum_ai-1.2.0.dist-info → vellum_ai-1.2.2.dist-info}/LICENSE +0 -0
  93. {vellum_ai-1.2.0.dist-info → vellum_ai-1.2.2.dist-info}/WHEEL +0 -0
  94. {vellum_ai-1.2.0.dist-info → vellum_ai-1.2.2.dist-info}/entry_points.txt +0 -0
@@ -114,6 +114,11 @@ def test_serialize_workflow():
114
114
  "name": "FailNode",
115
115
  "module": ["tests", "workflows", "basic_error_node", "workflow"],
116
116
  },
117
+ "trigger": {
118
+ "id": "70c19f1c-309c-4a5d-ba65-664c0bb2fedf",
119
+ "merge_behavior": "AWAIT_ATTRIBUTES",
120
+ },
121
+ "ports": [],
117
122
  },
118
123
  error_node,
119
124
  ignore_order=True,
@@ -117,6 +117,10 @@ def test_serialize_workflow():
117
117
  "module": ["tests", "workflows", "basic_guardrail_node", "workflow"],
118
118
  "name": "ExampleGuardrailNode",
119
119
  },
120
+ "trigger": {
121
+ "id": "ce5b85b1-eded-46dd-b4b7-020afcdc67ab",
122
+ "merge_behavior": "AWAIT_ANY",
123
+ },
120
124
  "ports": [{"id": "0ed87407-697e-4ae9-ab9b-6c5cc2e57cf7", "name": "default", "type": "DEFAULT"}],
121
125
  }
122
126
 
@@ -324,6 +324,10 @@ def test_serialize_workflow():
324
324
  "name": "ExampleInlineSubworkflowNode",
325
325
  "module": ["tests", "workflows", "basic_inline_subworkflow", "workflow"],
326
326
  },
327
+ "trigger": {
328
+ "id": "859a75a6-1bd2-4350-9509-4af66245e8e4",
329
+ "merge_behavior": "AWAIT_ATTRIBUTES",
330
+ },
327
331
  "ports": [{"id": "cfd831bc-ee7f-44d0-8d76-0ba0cd0277dc", "name": "default", "type": "DEFAULT"}],
328
332
  },
329
333
  subworkflow_node,
@@ -286,6 +286,10 @@ def test_serialize_workflow():
286
286
  "name": "MapFruitsNode",
287
287
  "module": ["tests", "workflows", "basic_map_node", "workflow"],
288
288
  },
289
+ "trigger": {
290
+ "id": "b5e8182e-20c5-482b-b4c5-4dde48c01472",
291
+ "merge_behavior": "AWAIT_ATTRIBUTES",
292
+ },
289
293
  "ports": [{"id": "a2171a61-0657-43ad-b6d9-cf93ce3270d0", "name": "default", "type": "DEFAULT"}],
290
294
  },
291
295
  map_node,
@@ -83,6 +83,10 @@ def test_serialize_workflow__await_all():
83
83
  "module": ["tests", "workflows", "basic_merge_node", "await_all_workflow"],
84
84
  "name": "AwaitAllMergeNode",
85
85
  },
86
+ "trigger": {
87
+ "id": "0efd256f-f5f6-45fe-9adb-651780f5e63d",
88
+ "merge_behavior": "AWAIT_ALL",
89
+ },
86
90
  "ports": [{"id": "3bbc469f-0fb0-4b3d-a28b-746fefec2818", "name": "default", "type": "DEFAULT"}],
87
91
  },
88
92
  merge_node,
@@ -150,6 +150,10 @@ def test_serialize_workflow(vellum_client):
150
150
  "name": "ExamplePromptDeploymentNode",
151
151
  "module": ["tests", "workflows", "basic_text_prompt_deployment", "workflow"],
152
152
  },
153
+ "trigger": {
154
+ "id": "b7605c48-0937-4ecc-914e-0d1058130e65",
155
+ "merge_behavior": "AWAIT_ANY",
156
+ },
153
157
  "ports": [{"id": "2f26c7e0-283d-4f04-b639-adebb56bc679", "name": "default", "type": "DEFAULT"}],
154
158
  "outputs": [
155
159
  {"id": "180355a8-e67c-4ce6-9ac3-e5dbb75a6629", "name": "json", "type": "JSON", "value": None},
@@ -384,6 +388,10 @@ def test_serialize_workflow_with_prompt_and_templating(vellum_client):
384
388
  "workflow_with_prompt_deployment_json_reference",
385
389
  ],
386
390
  },
391
+ "trigger": {
392
+ "id": "b7605c48-0937-4ecc-914e-0d1058130e65",
393
+ "merge_behavior": "AWAIT_ANY",
394
+ },
387
395
  "ports": [{"id": "2f26c7e0-283d-4f04-b639-adebb56bc679", "name": "default", "type": "DEFAULT"}],
388
396
  "outputs": [
389
397
  {"id": "180355a8-e67c-4ce6-9ac3-e5dbb75a6629", "name": "json", "type": "JSON", "value": None},
@@ -479,6 +487,10 @@ def test_serialize_workflow_with_prompt_and_templating(vellum_client):
479
487
  "workflow_with_prompt_deployment_json_reference",
480
488
  ],
481
489
  },
490
+ "trigger": {
491
+ "id": "58427684-3848-498a-8299-c6b0fc70265d",
492
+ "merge_behavior": "AWAIT_ATTRIBUTES",
493
+ },
482
494
  "ports": [{"id": "39317827-df43-4f5a-bfbc-20bffc839748", "name": "default", "type": "DEFAULT"}],
483
495
  }
484
496
 
@@ -238,6 +238,10 @@ def test_serialize_workflow():
238
238
  "name": "SimpleSearchNode",
239
239
  "module": ["tests", "workflows", "basic_search_node", "workflow"],
240
240
  },
241
+ "trigger": {
242
+ "id": "6d50305f-588b-469f-a042-b0767d3f99b1",
243
+ "merge_behavior": "AWAIT_ANY",
244
+ },
241
245
  "ports": [{"id": "00ae06b3-f8d9-4ae6-9fbf-e4ff4d520e9b", "name": "default", "type": "DEFAULT"}],
242
246
  }
243
247
 
@@ -153,6 +153,10 @@ def test_serialize_workflow(vellum_client):
153
153
  "module": ["tests", "workflows", "basic_subworkflow_deployment", "workflow"],
154
154
  "name": "ExampleSubworkflowDeploymentNode",
155
155
  },
156
+ "trigger": {
157
+ "id": "e4d80502-9281-42c8-91e3-10817bcd7d9e",
158
+ "merge_behavior": "AWAIT_ANY",
159
+ },
156
160
  "ports": [{"id": "ab0db8a9-7b53-4d88-8667-273b31303273", "name": "default", "type": "DEFAULT"}],
157
161
  }
158
162
 
@@ -112,6 +112,10 @@ def test_serialize_workflow():
112
112
  "name": "ExampleTemplatingNode",
113
113
  "module": ["tests", "workflows", "basic_templating_node", "workflow_with_json_input"],
114
114
  },
115
+ "trigger": {
116
+ "id": "58427684-3848-498a-8299-c6b0fc70265d",
117
+ "merge_behavior": "AWAIT_ATTRIBUTES",
118
+ },
115
119
  "ports": [{"id": "39317827-df43-4f5a-bfbc-20bffc839748", "name": "default", "type": "DEFAULT"}],
116
120
  },
117
121
  templating_node,
@@ -103,4 +103,9 @@ def test_serialize_workflow():
103
103
  "value": {"type": "WORKFLOW_INPUT", "input_variable_id": "e39a7b63-de15-490a-ae9b-8112c767aea0"},
104
104
  }
105
105
  ],
106
+ "trigger": {
107
+ "id": "0173d3c6-11d1-44b7-b070-ca9ff5119046",
108
+ "merge_behavior": "AWAIT_ANY",
109
+ },
110
+ "ports": [],
106
111
  }
@@ -54,6 +54,7 @@ def test_serialize_workflow():
54
54
  "action": "GITHUB_CREATE_AN_ISSUE",
55
55
  "description": "Create a new issue in a GitHub repository",
56
56
  "user_id": None,
57
+ "name": "github_create_an_issue",
57
58
  }
58
59
 
59
60
  # AND the rest of the node structure should be correct
@@ -81,6 +81,11 @@ def test_serialize_workflow__missing_final_output_node():
81
81
  },
82
82
  }
83
83
  ],
84
+ "trigger": {
85
+ "id": "a0c2eb7a-398e-4f28-b63d-f3bae9b563ee",
86
+ "merge_behavior": "AWAIT_ANY",
87
+ },
88
+ "ports": [],
84
89
  },
85
90
  {
86
91
  "id": "bb88768d-472e-4997-b7ea-de09163d1b4c",
@@ -11,6 +11,7 @@ from vellum.workflows.expressions.and_ import AndExpression
11
11
  from vellum.workflows.expressions.begins_with import BeginsWithExpression
12
12
  from vellum.workflows.expressions.between import BetweenExpression
13
13
  from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
14
+ from vellum.workflows.expressions.concat import ConcatExpression
14
15
  from vellum.workflows.expressions.contains import ContainsExpression
15
16
  from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
16
17
  from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
@@ -105,6 +106,8 @@ def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperato
105
106
  return "+"
106
107
  elif isinstance(descriptor, MinusExpression):
107
108
  return "-"
109
+ elif isinstance(descriptor, ConcatExpression):
110
+ return "concat"
108
111
  else:
109
112
  raise ValueError(f"Unsupported descriptor type: {descriptor}")
110
113
 
@@ -171,6 +174,7 @@ def _serialize_condition(display_context: "WorkflowDisplayContext", condition: B
171
174
  AndExpression,
172
175
  BeginsWithExpression,
173
176
  CoalesceExpression,
177
+ ConcatExpression,
174
178
  ContainsExpression,
175
179
  DoesNotBeginWithExpression,
176
180
  DoesNotContainExpression,
@@ -1,10 +1,14 @@
1
+ from uuid import UUID
1
2
  from typing import TYPE_CHECKING, Dict, Optional, Type
2
3
 
4
+ from vellum.workflows.events.types import BaseEvent
3
5
  from vellum.workflows.nodes import BaseNode
4
6
  from vellum.workflows.workflows.base import BaseWorkflow
5
7
 
6
8
  if TYPE_CHECKING:
9
+ from vellum.workflows.events.types import ParentContext
7
10
  from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
11
+ from vellum_ee.workflows.display.types import WorkflowDisplayContext
8
12
  from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
9
13
 
10
14
 
@@ -14,6 +18,9 @@ _workflow_display_registry: Dict[Type[BaseWorkflow], Type["BaseWorkflowDisplay"]
14
18
  # Used to store the mapping between node types and their display classes
15
19
  _node_display_registry: Dict[Type[BaseNode], Type["BaseNodeDisplay"]] = {}
16
20
 
21
+ # Registry to store active workflow display contexts by span ID for nested workflow inheritance
22
+ _active_workflow_display_contexts: Dict[UUID, "WorkflowDisplayContext"] = {}
23
+
17
24
 
18
25
  def get_from_workflow_display_registry(workflow_class: Type[BaseWorkflow]) -> Optional[Type["BaseWorkflowDisplay"]]:
19
26
  return _workflow_display_registry.get(workflow_class)
@@ -35,3 +42,42 @@ def get_from_node_display_registry(node_class: Type[BaseNode]) -> Optional[Type[
35
42
 
36
43
  def register_node_display_class(node_class: Type[BaseNode], node_display_class: Type["BaseNodeDisplay"]) -> None:
37
44
  _node_display_registry[node_class] = node_display_class
45
+
46
+
47
+ def register_workflow_display_context(span_id: UUID, display_context: "WorkflowDisplayContext") -> None:
48
+ """Register a workflow display context by span ID for nested workflow inheritance."""
49
+ _active_workflow_display_contexts[span_id] = display_context
50
+
51
+
52
+ def _get_parent_display_context_for_span(span_id: UUID) -> Optional["WorkflowDisplayContext"]:
53
+ """Get the parent display context for a given span ID."""
54
+ return _active_workflow_display_contexts.get(span_id)
55
+
56
+
57
+ def get_parent_display_context_from_event(event: BaseEvent) -> Optional["WorkflowDisplayContext"]:
58
+ """Extract parent display context from an event by traversing the parent chain.
59
+
60
+ This function traverses up the parent chain starting from the event's parent,
61
+ looking for workflow parents and attempting to get their display context.
62
+
63
+ Args:
64
+ event: The event to extract parent display context from
65
+
66
+ Returns:
67
+ The parent workflow display context if found, None otherwise
68
+ """
69
+ if not event.parent:
70
+ return None
71
+
72
+ current_parent: Optional["ParentContext"] = event.parent
73
+ while current_parent:
74
+ if current_parent.type == "WORKFLOW":
75
+ # Found a parent workflow, try to get its display context
76
+ parent_span_id = current_parent.span_id
77
+ parent_display_context = _get_parent_display_context_for_span(parent_span_id)
78
+ if parent_display_context:
79
+ return parent_display_context
80
+ # Move up the parent chain
81
+ current_parent = current_parent.parent
82
+
83
+ return None
@@ -528,7 +528,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
528
528
  workflow_input_displays: WorkflowInputsDisplays = {}
529
529
  # If we're dealing with a nested workflow, then it should have access to the inputs of its parents.
530
530
  global_workflow_input_displays = (
531
- copy(self._parent_display_context.workflow_input_displays) if self._parent_display_context else {}
531
+ copy(self._parent_display_context.global_workflow_input_displays) if self._parent_display_context else {}
532
532
  )
533
533
  for workflow_input in self._workflow.get_inputs_class():
534
534
  workflow_input_display_overrides = self.inputs_display.get(workflow_input)
@@ -0,0 +1,169 @@
1
+ from datetime import datetime, timezone
2
+ from uuid import uuid4
3
+
4
+ from vellum.workflows.events.types import NodeParentContext, WorkflowParentContext
5
+ from vellum.workflows.events.workflow import WorkflowExecutionInitiatedBody, WorkflowExecutionInitiatedEvent
6
+ from vellum.workflows.inputs.base import BaseInputs
7
+ from vellum.workflows.nodes import BaseNode
8
+ from vellum.workflows.state.base import BaseState
9
+ from vellum.workflows.workflows.base import BaseWorkflow
10
+ from vellum_ee.workflows.display.utils.registry import (
11
+ get_parent_display_context_from_event,
12
+ register_workflow_display_context,
13
+ )
14
+
15
+
16
+ class MockInputs(BaseInputs):
17
+ pass
18
+
19
+
20
+ class MockState(BaseState):
21
+ pass
22
+
23
+
24
+ class MockNode(BaseNode):
25
+ pass
26
+
27
+
28
+ class MockWorkflow(BaseWorkflow[MockInputs, MockState]):
29
+ pass
30
+
31
+
32
+ class MockWorkflowDisplayContext:
33
+ pass
34
+
35
+
36
+ def test_get_parent_display_context_from_event__no_parent():
37
+ """Test event with no parent returns None"""
38
+ # GIVEN a workflow execution initiated event with no parent
39
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
40
+ id=uuid4(),
41
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
42
+ trace_id=uuid4(),
43
+ span_id=uuid4(),
44
+ body=WorkflowExecutionInitiatedBody(
45
+ workflow_definition=MockWorkflow,
46
+ inputs=MockInputs(),
47
+ ),
48
+ parent=None, # No parent
49
+ )
50
+
51
+ # WHEN getting parent display context
52
+ result = get_parent_display_context_from_event(event)
53
+
54
+ # THEN it should return None
55
+ assert result is None
56
+
57
+
58
+ def test_get_parent_display_context_from_event__non_workflow_parent():
59
+ """Test event with non-workflow parent continues traversal"""
60
+ # GIVEN an event with a non-workflow parent (NodeParentContext)
61
+ non_workflow_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=None)
62
+
63
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
64
+ id=uuid4(),
65
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
66
+ trace_id=uuid4(),
67
+ span_id=uuid4(),
68
+ body=WorkflowExecutionInitiatedBody(
69
+ workflow_definition=MockWorkflow,
70
+ inputs=MockInputs(),
71
+ ),
72
+ parent=non_workflow_parent,
73
+ )
74
+
75
+ # WHEN getting parent display context
76
+ result = get_parent_display_context_from_event(event)
77
+
78
+ # THEN it should return None (no workflow parent found)
79
+ assert result is None
80
+
81
+
82
+ def test_get_parent_display_context_from_event__nested_workflow_parents():
83
+ """Test event with nested workflow parents traverses correctly"""
84
+ # GIVEN a chain of nested contexts:
85
+ # Event -> WorkflowParent -> NodeParent -> MiddleWorkflowParent -> NodeParent
86
+
87
+ # Top level workflow parent
88
+ top_workflow_span_id = uuid4()
89
+ top_context = MockWorkflowDisplayContext()
90
+ register_workflow_display_context(top_workflow_span_id, top_context) # type: ignore[arg-type]
91
+
92
+ top_workflow_parent = WorkflowParentContext(
93
+ workflow_definition=MockWorkflow, span_id=top_workflow_span_id, parent=None
94
+ )
95
+
96
+ top_node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=top_workflow_parent)
97
+
98
+ # AND middle workflow parent (no display context)
99
+ middle_workflow_span_id = uuid4()
100
+ middle_workflow_parent = WorkflowParentContext(
101
+ workflow_definition=MockWorkflow, span_id=middle_workflow_span_id, parent=top_node_parent
102
+ )
103
+
104
+ # AND node parent between middle workflow and event
105
+ node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=middle_workflow_parent)
106
+
107
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
108
+ id=uuid4(),
109
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
110
+ trace_id=uuid4(),
111
+ span_id=uuid4(),
112
+ body=WorkflowExecutionInitiatedBody(
113
+ workflow_definition=MockWorkflow,
114
+ inputs=MockInputs(),
115
+ ),
116
+ parent=node_parent,
117
+ )
118
+
119
+ # WHEN getting parent display context
120
+ result = get_parent_display_context_from_event(event)
121
+
122
+ # THEN it should find the top-level workflow context
123
+ assert result == top_context
124
+
125
+
126
+ def test_get_parent_display_context_from_event__middle_workflow_has_context():
127
+ """Test event returns middle workflow context when it's the first one with registered context"""
128
+ # GIVEN a chain of nested contexts:
129
+ # Event -> WorkflowParent -> NodeParent -> MiddleWorkflowParent -> NodeParent
130
+
131
+ top_workflow_span_id = uuid4()
132
+ top_context = MockWorkflowDisplayContext()
133
+ register_workflow_display_context(top_workflow_span_id, top_context) # type: ignore[arg-type]
134
+
135
+ top_workflow_parent = WorkflowParentContext(
136
+ workflow_definition=MockWorkflow, span_id=top_workflow_span_id, parent=None
137
+ )
138
+
139
+ # AND node parent between top workflow and middle workflow
140
+ top_node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=top_workflow_parent)
141
+
142
+ # AND middle workflow parent
143
+ middle_workflow_span_id = uuid4()
144
+ middle_context = MockWorkflowDisplayContext()
145
+ register_workflow_display_context(middle_workflow_span_id, middle_context) # type: ignore[arg-type]
146
+
147
+ middle_workflow_parent = WorkflowParentContext(
148
+ workflow_definition=MockWorkflow, span_id=middle_workflow_span_id, parent=top_node_parent
149
+ )
150
+
151
+ node_parent = NodeParentContext(node_definition=MockNode, span_id=uuid4(), parent=middle_workflow_parent)
152
+
153
+ event: WorkflowExecutionInitiatedEvent = WorkflowExecutionInitiatedEvent(
154
+ id=uuid4(),
155
+ timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
156
+ trace_id=uuid4(),
157
+ span_id=uuid4(),
158
+ body=WorkflowExecutionInitiatedBody(
159
+ workflow_definition=MockWorkflow,
160
+ inputs=MockInputs(),
161
+ ),
162
+ parent=node_parent,
163
+ )
164
+
165
+ # WHEN getting parent display context
166
+ result = get_parent_display_context_from_event(event)
167
+
168
+ # THEN it should find the MIDDLE workflow context
169
+ assert result == middle_context
@@ -8,6 +8,7 @@ from vellum.client.types.number_vellum_value import NumberVellumValue
8
8
  from vellum.workflows import BaseWorkflow
9
9
  from vellum.workflows.nodes import BaseNode
10
10
  from vellum.workflows.state.context import WorkflowContext
11
+ from vellum_ee.workflows.display.workflows.base_workflow_display import BaseWorkflowDisplay
11
12
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
12
13
 
13
14
 
@@ -494,3 +495,74 @@ class MapNodeWorkflow(BaseWorkflow):
494
495
 
495
496
  # AND we get the map node results as a list
496
497
  assert event.body.outputs == {"results": [1.0, 1.0, 1.0]}
498
+
499
+
500
+ def test_serialize_module__tool_calling_node_with_single_tool():
501
+ """Test that serialize_module works with a tool calling node that has a single tool."""
502
+
503
+ # GIVEN a simple tool function
504
+ tool_function_code = '''def get_weather(location: str) -> str:
505
+ """Get the current weather for a location."""
506
+ return f"The weather in {location} is sunny."
507
+ '''
508
+
509
+ # AND a workflow module with a tool calling node using that single tool
510
+ files = {
511
+ "__init__.py": "",
512
+ "workflow.py": """
513
+ from vellum.workflows import BaseWorkflow
514
+ from vellum.workflows.nodes.displayable.tool_calling_node import ToolCallingNode
515
+ from vellum.workflows.state.base import BaseState
516
+ from vellum.workflows.workflows.base import BaseInputs
517
+ from vellum.client.types.chat_message_prompt_block import ChatMessagePromptBlock
518
+ from vellum.client.types.plain_text_prompt_block import PlainTextPromptBlock
519
+ from vellum.client.types.rich_text_prompt_block import RichTextPromptBlock
520
+ from vellum.client.types.variable_prompt_block import VariablePromptBlock
521
+
522
+ from .get_weather import get_weather
523
+
524
+
525
+ class Inputs(BaseInputs):
526
+ location: str
527
+
528
+
529
+ class WeatherNode(ToolCallingNode):
530
+ ml_model = "gpt-4o-mini"
531
+ blocks = [
532
+ ChatMessagePromptBlock(
533
+ chat_role="USER",
534
+ blocks=[
535
+ RichTextPromptBlock(
536
+ blocks=[
537
+ VariablePromptBlock(
538
+ input_variable="location",
539
+ ),
540
+ ],
541
+ ),
542
+ ],
543
+ ),
544
+ ]
545
+ functions = [get_weather]
546
+ prompt_inputs = {
547
+ "location": Inputs.location,
548
+ }
549
+
550
+
551
+ class Workflow(BaseWorkflow[Inputs, BaseState]):
552
+ graph = WeatherNode
553
+
554
+ class Outputs(BaseWorkflow.Outputs):
555
+ result = WeatherNode.Outputs.text
556
+ """,
557
+ "get_weather.py": tool_function_code,
558
+ }
559
+
560
+ namespace = str(uuid4())
561
+
562
+ # AND the virtual file loader is registered
563
+ sys.meta_path.append(VirtualFileFinder(files, namespace))
564
+
565
+ result = BaseWorkflowDisplay.serialize_module(namespace)
566
+
567
+ # THEN the serialization should complete successfully
568
+ assert result is not None