vellum-ai 0.14.72__py3-none-any.whl → 0.14.73__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 (66) hide show
  1. vellum/__init__.py +8 -0
  2. vellum/client/core/client_wrapper.py +1 -1
  3. vellum/client/core/serialization.py +1 -0
  4. vellum/client/types/__init__.py +8 -0
  5. vellum/client/types/build_status_enum.py +5 -0
  6. vellum/client/types/container_image_build_config.py +20 -0
  7. vellum/client/types/container_image_read.py +4 -0
  8. vellum/client/types/execute_api_response.py +2 -2
  9. vellum/client/types/folder_entity.py +2 -0
  10. vellum/client/types/folder_entity_dataset.py +26 -0
  11. vellum/client/types/folder_entity_dataset_data.py +25 -0
  12. vellum/types/build_status_enum.py +3 -0
  13. vellum/types/container_image_build_config.py +3 -0
  14. vellum/types/folder_entity_dataset.py +3 -0
  15. vellum/types/folder_entity_dataset_data.py +3 -0
  16. vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -1
  17. vellum/workflows/nodes/displayable/api_node/node.py +2 -0
  18. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +43 -0
  19. vellum/workflows/nodes/displayable/bases/api_node/node.py +6 -0
  20. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +30 -4
  21. vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +43 -3
  22. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +68 -58
  23. vellum/workflows/nodes/experimental/tool_calling_node/node.py +10 -10
  24. vellum/workflows/nodes/experimental/tool_calling_node/tests/__init__.py +0 -0
  25. vellum/workflows/nodes/experimental/tool_calling_node/tests/test_utils.py +49 -0
  26. vellum/workflows/nodes/experimental/tool_calling_node/utils.py +67 -6
  27. vellum/workflows/ports/utils.py +26 -6
  28. vellum/workflows/runner/runner.py +35 -3
  29. vellum/workflows/types/core.py +12 -0
  30. vellum/workflows/types/definition.py +6 -0
  31. vellum/workflows/utils/functions.py +9 -9
  32. vellum/workflows/utils/pydantic_schema.py +38 -0
  33. vellum/workflows/utils/tests/test_functions.py +11 -11
  34. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/METADATA +1 -1
  35. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/RECORD +66 -54
  36. vellum_cli/push.py +6 -8
  37. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +8 -1
  38. vellum_ee/workflows/display/tests/test_base_workflow_display.py +1 -1
  39. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +1 -1
  40. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +1 -1
  41. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +5 -5
  42. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +12 -12
  43. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +10 -10
  44. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +3 -3
  45. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +3 -3
  46. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +20 -9
  47. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +3 -3
  48. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +3 -3
  49. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +8 -8
  50. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +6 -6
  51. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -3
  52. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +8 -8
  53. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +3 -3
  54. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +4 -4
  55. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +3 -3
  56. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -2
  57. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +5 -5
  58. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
  59. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +1 -1
  60. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +2 -2
  61. vellum_ee/workflows/display/utils/auto_layout.py +1 -1
  62. vellum_ee/workflows/display/workflows/base_workflow_display.py +179 -4
  63. vellum_ee/workflows/tests/test_serialize_module.py +47 -0
  64. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/LICENSE +0 -0
  65. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/WHEEL +0 -0
  66. {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/entry_points.txt +0 -0
@@ -21,6 +21,7 @@ from vellum.workflows.errors import WorkflowErrorCode
21
21
  from vellum.workflows.errors.types import workflow_event_error_to_workflow_error
22
22
  from vellum.workflows.events.types import default_serializer
23
23
  from vellum.workflows.exceptions import NodeException
24
+ from vellum.workflows.inputs.base import BaseInputs
24
25
  from vellum.workflows.nodes.bases.base import BaseNode
25
26
  from vellum.workflows.outputs.base import BaseOutput
26
27
  from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior
@@ -43,7 +44,7 @@ class SubworkflowDeploymentNode(BaseNode[StateType], Generic[StateType]):
43
44
 
44
45
  # Either the Workflow Deployment's UUID or its name.
45
46
  deployment: ClassVar[Union[UUID, str]]
46
- subworkflow_inputs: ClassVar[EntityInputsInterface] = {}
47
+ subworkflow_inputs: ClassVar[Union[EntityInputsInterface, BaseInputs]] = {}
47
48
 
48
49
  release_tag: str = LATEST_RELEASE_TAG
49
50
  external_id: Optional[str] = OMIT
@@ -62,68 +63,77 @@ class SubworkflowDeploymentNode(BaseNode[StateType], Generic[StateType]):
62
63
 
63
64
  compiled_inputs: List[WorkflowRequestInputRequest] = []
64
65
 
65
- for input_name, input_value in self.subworkflow_inputs.items():
66
- # Exclude inputs that resolved to be null. This ensure that we don't pass input values
67
- # to optional subworkflow inputs whose values were unresolved.
68
- if input_value is None:
69
- continue
70
- if isinstance(input_value, str):
71
- compiled_inputs.append(
72
- WorkflowRequestStringInputRequest(
73
- name=input_name,
74
- value=input_value,
75
- )
66
+ if isinstance(self.subworkflow_inputs, BaseInputs):
67
+ for input_descriptor, input_value in self.subworkflow_inputs:
68
+ self._add_compiled_input(compiled_inputs, input_descriptor.name, input_value)
69
+ else:
70
+ for input_name, input_value in self.subworkflow_inputs.items():
71
+ self._add_compiled_input(compiled_inputs, input_name, input_value)
72
+
73
+ return compiled_inputs
74
+
75
+ def _add_compiled_input(
76
+ self, compiled_inputs: List[WorkflowRequestInputRequest], input_name: str, input_value: Any
77
+ ) -> None:
78
+ # Exclude inputs that resolved to be null. This ensure that we don't pass input values
79
+ # to optional subworkflow inputs whose values were unresolved.
80
+ if input_value is None:
81
+ return
82
+ if isinstance(input_value, str):
83
+ compiled_inputs.append(
84
+ WorkflowRequestStringInputRequest(
85
+ name=input_name,
86
+ value=input_value,
76
87
  )
77
- elif (
78
- isinstance(input_value, list)
79
- and len(input_value) > 0
80
- and all(isinstance(message, (ChatMessage, ChatMessageRequest)) for message in input_value)
81
- ):
82
- chat_history = [
83
- (
84
- message
85
- if isinstance(message, ChatMessageRequest)
86
- else ChatMessageRequest.model_validate(message.model_dump())
87
- )
88
- for message in input_value
89
- if isinstance(message, (ChatMessage, ChatMessageRequest))
90
- ]
91
- compiled_inputs.append(
92
- WorkflowRequestChatHistoryInputRequest(
93
- name=input_name,
94
- value=chat_history,
95
- )
88
+ )
89
+ elif (
90
+ isinstance(input_value, list)
91
+ and len(input_value) > 0
92
+ and all(isinstance(message, (ChatMessage, ChatMessageRequest)) for message in input_value)
93
+ ):
94
+ chat_history = [
95
+ (
96
+ message
97
+ if isinstance(message, ChatMessageRequest)
98
+ else ChatMessageRequest.model_validate(message.model_dump())
96
99
  )
97
- elif isinstance(input_value, dict):
98
- compiled_inputs.append(
99
- WorkflowRequestJsonInputRequest(
100
- name=input_name,
101
- value=cast(Dict[str, Any], input_value),
102
- )
100
+ for message in input_value
101
+ if isinstance(message, (ChatMessage, ChatMessageRequest))
102
+ ]
103
+ compiled_inputs.append(
104
+ WorkflowRequestChatHistoryInputRequest(
105
+ name=input_name,
106
+ value=chat_history,
103
107
  )
104
- elif isinstance(input_value, (int, float)):
105
- compiled_inputs.append(
106
- WorkflowRequestNumberInputRequest(
107
- name=input_name,
108
- value=input_value,
109
- )
108
+ )
109
+ elif isinstance(input_value, dict):
110
+ compiled_inputs.append(
111
+ WorkflowRequestJsonInputRequest(
112
+ name=input_name,
113
+ value=cast(Dict[str, Any], input_value),
110
114
  )
111
- else:
112
- try:
113
- input_value = default_serializer(input_value)
114
- except json.JSONDecodeError as e:
115
- raise NodeException(
116
- message=f"Failed to serialize input '{input_name}' of type '{input_value.__class__}': {e}",
117
- code=WorkflowErrorCode.INVALID_INPUTS,
118
- )
119
- compiled_inputs.append(
120
- WorkflowRequestJsonInputRequest(
121
- name=input_name,
122
- value=input_value,
123
- )
115
+ )
116
+ elif isinstance(input_value, (int, float)):
117
+ compiled_inputs.append(
118
+ WorkflowRequestNumberInputRequest(
119
+ name=input_name,
120
+ value=input_value,
124
121
  )
125
-
126
- return compiled_inputs
122
+ )
123
+ else:
124
+ try:
125
+ input_value = default_serializer(input_value)
126
+ except json.JSONDecodeError as e:
127
+ raise NodeException(
128
+ message=f"Failed to serialize input '{input_name}' of type '{input_value.__class__}': {e}",
129
+ code=WorkflowErrorCode.INVALID_INPUTS,
130
+ )
131
+ compiled_inputs.append(
132
+ WorkflowRequestJsonInputRequest(
133
+ name=input_name,
134
+ value=input_value,
135
+ )
136
+ )
127
137
 
128
138
  def run(self) -> Iterator[BaseOutput]:
129
139
  execution_context = get_execution_context()
@@ -1,8 +1,6 @@
1
- from collections.abc import Callable, Sequence
1
+ from collections.abc import Sequence
2
2
  from typing import Any, ClassVar, Dict, List, Optional, cast
3
3
 
4
- from pydash import snake_case
5
-
6
4
  from vellum import ChatMessage, PromptBlock
7
5
  from vellum.client.types.code_execution_package import CodeExecutionPackage
8
6
  from vellum.client.types.code_execution_runtime import CodeExecutionRuntime
@@ -12,11 +10,15 @@ from vellum.workflows.exceptions import NodeException
12
10
  from vellum.workflows.graph.graph import Graph
13
11
  from vellum.workflows.inputs.base import BaseInputs
14
12
  from vellum.workflows.nodes.bases import BaseNode
15
- from vellum.workflows.nodes.experimental.tool_calling_node.utils import create_function_node, create_tool_router_node
13
+ from vellum.workflows.nodes.experimental.tool_calling_node.utils import (
14
+ create_function_node,
15
+ create_tool_router_node,
16
+ get_function_name,
17
+ )
16
18
  from vellum.workflows.outputs.base import BaseOutputs
17
19
  from vellum.workflows.state.base import BaseState
18
20
  from vellum.workflows.state.context import WorkflowContext
19
- from vellum.workflows.types.core import EntityInputsInterface
21
+ from vellum.workflows.types.core import EntityInputsInterface, Tool
20
22
  from vellum.workflows.workflows.base import BaseWorkflow
21
23
 
22
24
 
@@ -27,15 +29,14 @@ class ToolCallingNode(BaseNode):
27
29
  Attributes:
28
30
  ml_model: str - The model to use for tool calling (e.g., "gpt-4o-mini")
29
31
  blocks: List[PromptBlock] - The prompt blocks to use (same format as InlinePromptNode)
30
- functions: List[FunctionDefinition] - The functions that can be called
31
- function_callables: List[Callable] - The callables that can be called
32
+ functions: List[Tool] - The functions that can be called
32
33
  prompt_inputs: Optional[EntityInputsInterface] - Mapping of input variable names to values
33
34
  function_configs: Optional[Dict[str, Dict[str, Any]]] - Mapping of function names to their configuration
34
35
  """
35
36
 
36
37
  ml_model: ClassVar[str] = "gpt-4o-mini"
37
38
  blocks: ClassVar[List[PromptBlock]] = []
38
- functions: ClassVar[List[Callable[..., Any]]] = []
39
+ functions: ClassVar[List[Tool]] = []
39
40
  prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
40
41
  function_configs: ClassVar[Optional[Dict[str, Dict[str, Any]]]] = None
41
42
 
@@ -106,8 +107,7 @@ class ToolCallingNode(BaseNode):
106
107
 
107
108
  self._function_nodes = {}
108
109
  for function in self.functions:
109
- function_name = snake_case(function.__name__)
110
-
110
+ function_name = get_function_name(function)
111
111
  # Get configuration for this function
112
112
  config = {}
113
113
  if callable(function) and self.function_configs and function.__name__ in self.function_configs:
@@ -0,0 +1,49 @@
1
+ from vellum.workflows import BaseWorkflow
2
+ from vellum.workflows.inputs.base import BaseInputs
3
+ from vellum.workflows.nodes.bases import BaseNode
4
+ from vellum.workflows.nodes.experimental.tool_calling_node.utils import get_function_name
5
+ from vellum.workflows.outputs.base import BaseOutputs
6
+ from vellum.workflows.state.base import BaseState
7
+ from vellum.workflows.types.definition import DeploymentDefinition
8
+
9
+
10
+ def test_get_function_name_callable():
11
+ """Test callable"""
12
+
13
+ def my_function() -> str:
14
+ return "test"
15
+
16
+ function = my_function
17
+
18
+ result = get_function_name(function)
19
+
20
+ assert result == "my_function"
21
+
22
+
23
+ def test_get_function_name_workflow_class():
24
+ """Test workflow class."""
25
+
26
+ class MyWorkflow(BaseWorkflow[BaseInputs, BaseState]):
27
+ class MyNode(BaseNode):
28
+ class Outputs(BaseOutputs):
29
+ result: str
30
+
31
+ def run(self) -> Outputs:
32
+ return self.Outputs(result="test")
33
+
34
+ graph = MyNode
35
+
36
+ workflow_class = MyWorkflow
37
+
38
+ result = get_function_name(workflow_class)
39
+
40
+ assert result == "my_workflow"
41
+
42
+
43
+ def test_get_function_name_subworkflow_deployment():
44
+ """Test subworkflow deployment."""
45
+ deployment_config = DeploymentDefinition(deployment="my-test-deployment", release_tag="v1.0.0")
46
+
47
+ result = get_function_name(deployment_config)
48
+
49
+ assert result == "my-test-deployment"
@@ -1,4 +1,4 @@
1
- from collections.abc import Callable, Sequence
1
+ from collections.abc import Sequence
2
2
  import inspect
3
3
  import json
4
4
  import types
@@ -18,13 +18,15 @@ from vellum.workflows.exceptions import NodeException
18
18
  from vellum.workflows.nodes.bases import BaseNode
19
19
  from vellum.workflows.nodes.displayable.code_execution_node.node import CodeExecutionNode
20
20
  from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
21
+ from vellum.workflows.nodes.displayable.subworkflow_deployment_node.node import SubworkflowDeploymentNode
21
22
  from vellum.workflows.outputs.base import BaseOutput
22
23
  from vellum.workflows.ports.port import Port
23
24
  from vellum.workflows.references.lazy import LazyReference
24
25
  from vellum.workflows.state.base import BaseState
25
26
  from vellum.workflows.state.context import WorkflowContext
26
27
  from vellum.workflows.state.encoder import DefaultStateEncoder
27
- from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior
28
+ from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior, Tool
29
+ from vellum.workflows.types.definition import DeploymentDefinition
28
30
  from vellum.workflows.types.generics import is_workflow_class
29
31
 
30
32
 
@@ -68,14 +70,14 @@ class ToolRouterNode(InlinePromptNode):
68
70
  def create_tool_router_node(
69
71
  ml_model: str,
70
72
  blocks: List[PromptBlock],
71
- functions: List[Callable[..., Any]],
73
+ functions: List[Tool],
72
74
  prompt_inputs: Optional[EntityInputsInterface],
73
75
  ) -> Type[ToolRouterNode]:
74
76
  if functions and len(functions) > 0:
75
77
  # If we have functions, create dynamic ports for each function
76
78
  Ports = type("Ports", (), {})
77
79
  for function in functions:
78
- function_name = snake_case(function.__name__)
80
+ function_name = get_function_name(function)
79
81
 
80
82
  # Avoid using lambda to capture function_name
81
83
  # lambda will capture the function_name by reference,
@@ -127,7 +129,7 @@ def create_tool_router_node(
127
129
 
128
130
 
129
131
  def create_function_node(
130
- function: Callable[..., Any],
132
+ function: Tool,
131
133
  tool_router_node: Type[ToolRouterNode],
132
134
  packages: Optional[Sequence[CodeExecutionPackage]] = None,
133
135
  runtime: CodeExecutionRuntime = "PYTHON_3_11_6",
@@ -143,7 +145,59 @@ def create_function_node(
143
145
  packages: Optional list of packages to install for code execution (only used for regular functions)
144
146
  runtime: The runtime to use for code execution (default: "PYTHON_3_11_6")
145
147
  """
146
- if is_workflow_class(function):
148
+ if isinstance(function, DeploymentDefinition):
149
+ deployment = function.deployment
150
+ release_tag = function.release_tag
151
+
152
+ def execute_deployment_workflow_function(self) -> BaseNode.Outputs:
153
+ function_call_output = self.state.meta.node_outputs.get(tool_router_node.Outputs.results)
154
+ if function_call_output and len(function_call_output) > 0:
155
+ function_call = function_call_output[0]
156
+ arguments = function_call.value.arguments
157
+ else:
158
+ arguments = {}
159
+
160
+ subworkflow_node = type(
161
+ f"DynamicSubworkflowNode_{deployment}",
162
+ (SubworkflowDeploymentNode,),
163
+ {
164
+ "deployment": deployment,
165
+ "release_tag": release_tag,
166
+ "subworkflow_inputs": arguments,
167
+ "__module__": __name__,
168
+ },
169
+ )
170
+
171
+ node_instance = subworkflow_node(
172
+ context=WorkflowContext.create_from(self._context),
173
+ state=self.state,
174
+ )
175
+
176
+ outputs = {}
177
+ for output in node_instance.run():
178
+ outputs[output.name] = output.value
179
+
180
+ self.state.chat_history.append(
181
+ ChatMessage(
182
+ role="FUNCTION",
183
+ content=StringChatMessageContent(value=json.dumps(outputs, cls=DefaultStateEncoder)),
184
+ )
185
+ )
186
+
187
+ return self.Outputs()
188
+
189
+ node = type(
190
+ f"DeploymentWorkflowNode_{deployment}",
191
+ (FunctionNode,),
192
+ {
193
+ "run": execute_deployment_workflow_function,
194
+ "__module__": __name__,
195
+ },
196
+ )
197
+
198
+ return node
199
+
200
+ elif is_workflow_class(function):
147
201
  # Create a class-level wrapper that calls the original function
148
202
  def execute_inline_workflow_function(self) -> BaseNode.Outputs:
149
203
  outputs = self.state.meta.node_outputs.get(tool_router_node.Outputs.text)
@@ -246,3 +300,10 @@ def main(arguments):
246
300
  )
247
301
 
248
302
  return node
303
+
304
+
305
+ def get_function_name(function: Tool) -> str:
306
+ if isinstance(function, DeploymentDefinition):
307
+ return function.deployment
308
+ else:
309
+ return snake_case(function.__name__)
@@ -1,6 +1,8 @@
1
1
  from collections import Counter
2
2
  from typing import List
3
3
 
4
+ from vellum.workflows.errors.types import WorkflowErrorCode
5
+ from vellum.workflows.exceptions import NodeException
4
6
  from vellum.workflows.ports.port import Port
5
7
  from vellum.workflows.types.core import ConditionType
6
8
 
@@ -31,14 +33,20 @@ def get_port_groups(ports: List[Port]) -> List[List[ConditionType]]:
31
33
  else:
32
34
  # If we see an ELIF or ELSE without a preceding IF, that's an error
33
35
  if not current_port_group:
34
- raise ValueError(f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else")
36
+ raise NodeException(
37
+ message=f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else",
38
+ code=WorkflowErrorCode.INVALID_INPUTS,
39
+ )
35
40
  current_port_group.append(port_type)
36
41
 
37
42
  if current_port_group and current_port_group[0] == ConditionType.IF:
38
43
  port_groups.append(current_port_group)
39
44
  elif current_port_group:
40
45
  # If the last group doesn't start with IF, that's an error
41
- raise ValueError(f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else")
46
+ raise NodeException(
47
+ message=f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else",
48
+ code=WorkflowErrorCode.INVALID_INPUTS,
49
+ )
42
50
 
43
51
  return port_groups
44
52
 
@@ -51,7 +59,10 @@ def validate_ports(ports: List[Port]) -> bool:
51
59
  # Check that each port group is in the correct order
52
60
  sorted_group = sorted(group, key=lambda port_type: PORT_TYPE_PRIORITIES[port_type])
53
61
  if sorted_group != group:
54
- raise ValueError(f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else")
62
+ raise NodeException(
63
+ message=f"Class {ports_class} must have ports in the following order: on_if, on_elif, on_else",
64
+ code=WorkflowErrorCode.INVALID_INPUTS,
65
+ )
55
66
 
56
67
  # Count the types in this port group
57
68
  counter = Counter(group)
@@ -61,13 +72,22 @@ def validate_ports(ports: List[Port]) -> bool:
61
72
 
62
73
  # Apply the rules to each port group
63
74
  if number_of_if_ports != 1:
64
- raise ValueError(f"Class {ports_class} must have exactly one on_if condition")
75
+ raise NodeException(
76
+ message=f"Class {ports_class} must have exactly one on_if condition",
77
+ code=WorkflowErrorCode.INVALID_INPUTS,
78
+ )
65
79
 
66
80
  if number_of_elif_ports > 0 and number_of_if_ports != 1:
67
- raise ValueError(f"Class {ports_class} containing on_elif ports must have exactly one on_if condition")
81
+ raise NodeException(
82
+ message=f"Class {ports_class} containing on_elif ports must have exactly one on_if condition",
83
+ code=WorkflowErrorCode.INVALID_INPUTS,
84
+ )
68
85
 
69
86
  if number_of_else_ports > 1:
70
- raise ValueError(f"Class {ports_class} must have at most one on_else condition")
87
+ raise NodeException(
88
+ message=f"Class {ports_class} must have at most one on_else condition",
89
+ code=WorkflowErrorCode.INVALID_INPUTS,
90
+ )
71
91
 
72
92
  enforce_single_invoked_conditional_port = len(port_groups) <= 1
73
93
  return enforce_single_invoked_conditional_port
@@ -3,7 +3,9 @@ from copy import deepcopy
3
3
  from dataclasses import dataclass
4
4
  import logging
5
5
  from queue import Empty, Queue
6
+ import sys
6
7
  from threading import Event as ThreadingEvent, Thread
8
+ import traceback
7
9
  from uuid import UUID, uuid4
8
10
  from typing import (
9
11
  TYPE_CHECKING,
@@ -346,6 +348,7 @@ class WorkflowRunner(Generic[StateType]):
346
348
  )
347
349
  except NodeException as e:
348
350
  logger.info(e)
351
+
349
352
  self._workflow_event_inner_queue.put(
350
353
  NodeExecutionRejectedEvent(
351
354
  trace_id=execution.trace_id,
@@ -370,8 +373,15 @@ class WorkflowRunner(Generic[StateType]):
370
373
  parent=execution.parent_context,
371
374
  )
372
375
  )
376
+
373
377
  except Exception as e:
374
- logger.exception(f"An unexpected error occurred while running node {node.__class__.__name__}")
378
+ error_message = self._parse_error_message(e)
379
+ if error_message is None:
380
+ logger.exception(f"An unexpected error occurred while running node {node.__class__.__name__}")
381
+ error_code = WorkflowErrorCode.INTERNAL_ERROR
382
+ error_message = "Internal error"
383
+ else:
384
+ error_code = WorkflowErrorCode.NODE_EXECUTION
375
385
 
376
386
  self._workflow_event_inner_queue.put(
377
387
  NodeExecutionRejectedEvent(
@@ -380,8 +390,8 @@ class WorkflowRunner(Generic[StateType]):
380
390
  body=NodeExecutionRejectedBody(
381
391
  node_definition=node.__class__,
382
392
  error=WorkflowError(
383
- message=str(e),
384
- code=WorkflowErrorCode.INTERNAL_ERROR,
393
+ message=error_message,
394
+ code=error_code,
385
395
  ),
386
396
  ),
387
397
  parent=execution.parent_context,
@@ -390,6 +400,28 @@ class WorkflowRunner(Generic[StateType]):
390
400
 
391
401
  logger.debug(f"Finished running node: {node.__class__.__name__}")
392
402
 
403
+ def _parse_error_message(self, exception: Exception) -> Optional[str]:
404
+ try:
405
+ _, _, tb = sys.exc_info()
406
+ if tb:
407
+ tb_list = traceback.extract_tb(tb)
408
+ if tb_list:
409
+ last_frame = tb_list[-1]
410
+ exception_module = next(
411
+ (
412
+ mod.__name__
413
+ for mod in sys.modules.values()
414
+ if hasattr(mod, "__file__") and mod.__file__ == last_frame.filename
415
+ ),
416
+ None,
417
+ )
418
+ if exception_module and not exception_module.startswith("vellum."):
419
+ return str(exception)
420
+ except Exception:
421
+ pass
422
+
423
+ return None
424
+
393
425
  def _context_run_work_item(
394
426
  self,
395
427
  node: BaseNode[StateType],
@@ -1,8 +1,11 @@
1
1
  from enum import Enum
2
2
  from typing import ( # type: ignore[attr-defined]
3
+ TYPE_CHECKING,
3
4
  Any,
5
+ Callable,
4
6
  Dict,
5
7
  List,
8
+ Type,
6
9
  Union,
7
10
  _GenericAlias,
8
11
  _SpecialGenericAlias,
@@ -10,6 +13,11 @@ from typing import ( # type: ignore[attr-defined]
10
13
  )
11
14
 
12
15
  from vellum.client.core.pydantic_utilities import UniversalBaseModel
16
+ from vellum.workflows.types.definition import DeploymentDefinition
17
+
18
+ if TYPE_CHECKING:
19
+ from vellum.workflows.workflows.base import BaseWorkflow
20
+
13
21
 
14
22
  JsonArray = List["Json"]
15
23
  JsonObject = Dict[str, "Json"]
@@ -39,3 +47,7 @@ class ConditionType(Enum):
39
47
  IF = "IF"
40
48
  ELIF = "ELIF"
41
49
  ELSE = "ELSE"
50
+
51
+
52
+ # Type alias for functions that can be called in tool calling nodes
53
+ Tool = Union[Callable[..., Any], DeploymentDefinition, Type["BaseWorkflow"]]
@@ -6,6 +6,7 @@ from typing import Annotated, Any, Dict, Optional, Union
6
6
 
7
7
  from pydantic import BeforeValidator
8
8
 
9
+ from vellum.client.core.pydantic_utilities import UniversalBaseModel
9
10
  from vellum.client.types.code_resource_definition import CodeResourceDefinition as ClientCodeResourceDefinition
10
11
 
11
12
 
@@ -69,3 +70,8 @@ VellumCodeResourceDefinition = Annotated[
69
70
  CodeResourceDefinition,
70
71
  BeforeValidator(lambda d: (d if type(d) is dict else serialize_type_encoder_with_id(d))),
71
72
  ]
73
+
74
+
75
+ class DeploymentDefinition(UniversalBaseModel):
76
+ deployment: str
77
+ release_tag: Optional[str] = "LATEST"
@@ -26,27 +26,27 @@ type_map = {
26
26
  }
27
27
 
28
28
 
29
- def _compile_annotation(annotation: Optional[Any], defs: dict[str, Any]) -> dict:
29
+ def compile_annotation(annotation: Optional[Any], defs: dict[str, Any]) -> dict:
30
30
  if annotation is None:
31
31
  return {"type": "null"}
32
32
 
33
33
  if get_origin(annotation) is Union:
34
- return {"anyOf": [_compile_annotation(a, defs) for a in get_args(annotation)]}
34
+ return {"anyOf": [compile_annotation(a, defs) for a in get_args(annotation)]}
35
35
 
36
36
  if get_origin(annotation) is dict:
37
37
  _, value_type = get_args(annotation)
38
- return {"type": "object", "additionalProperties": _compile_annotation(value_type, defs)}
38
+ return {"type": "object", "additionalProperties": compile_annotation(value_type, defs)}
39
39
 
40
40
  if get_origin(annotation) is list:
41
41
  item_type = get_args(annotation)[0]
42
- return {"type": "array", "items": _compile_annotation(item_type, defs)}
42
+ return {"type": "array", "items": compile_annotation(item_type, defs)}
43
43
 
44
44
  if dataclasses.is_dataclass(annotation):
45
45
  if annotation.__name__ not in defs:
46
46
  properties = {}
47
47
  required = []
48
48
  for field in dataclasses.fields(annotation):
49
- properties[field.name] = _compile_annotation(field.type, defs)
49
+ properties[field.name] = compile_annotation(field.type, defs)
50
50
  if field.default is dataclasses.MISSING:
51
51
  required.append(field.name)
52
52
  else:
@@ -61,7 +61,7 @@ def _compile_annotation(annotation: Optional[Any], defs: dict[str, Any]) -> dict
61
61
  for field_name, field in annotation.model_fields.items():
62
62
  # Mypy is incorrect here, the `annotation` attribute is defined on `FieldInfo`
63
63
  field_annotation = field.annotation # type: ignore[attr-defined]
64
- properties[field_name] = _compile_annotation(field_annotation, defs)
64
+ properties[field_name] = compile_annotation(field_annotation, defs)
65
65
  if field.default is PydanticUndefined:
66
66
  required.append(field_name)
67
67
  else:
@@ -115,7 +115,7 @@ def compile_function_definition(function: Callable) -> FunctionDefinition:
115
115
  required = []
116
116
  defs: dict[str, Any] = {}
117
117
  for param in signature.parameters.values():
118
- properties[param.name] = _compile_annotation(param.annotation, defs)
118
+ properties[param.name] = compile_annotation(param.annotation, defs)
119
119
  if param.default is inspect.Parameter.empty:
120
120
  required.append(param.name)
121
121
  else:
@@ -132,7 +132,7 @@ def compile_function_definition(function: Callable) -> FunctionDefinition:
132
132
  )
133
133
 
134
134
 
135
- def compile_workflow_function_definition(workflow_class: Type["BaseWorkflow"]) -> FunctionDefinition:
135
+ def compile_inline_workflow_function_definition(workflow_class: Type["BaseWorkflow"]) -> FunctionDefinition:
136
136
  """
137
137
  Converts a base workflow class into our Vellum-native FunctionDefinition type.
138
138
  """
@@ -148,7 +148,7 @@ def compile_workflow_function_definition(workflow_class: Type["BaseWorkflow"]) -
148
148
  if name.startswith("__"):
149
149
  continue
150
150
 
151
- properties[name] = _compile_annotation(field_type, defs)
151
+ properties[name] = compile_annotation(field_type, defs)
152
152
 
153
153
  # Check if the field has a default value
154
154
  if name not in vars_inputs_class: