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.
- vellum/__init__.py +8 -0
- vellum/client/core/client_wrapper.py +1 -1
- vellum/client/core/serialization.py +1 -0
- vellum/client/types/__init__.py +8 -0
- vellum/client/types/build_status_enum.py +5 -0
- vellum/client/types/container_image_build_config.py +20 -0
- vellum/client/types/container_image_read.py +4 -0
- vellum/client/types/execute_api_response.py +2 -2
- vellum/client/types/folder_entity.py +2 -0
- vellum/client/types/folder_entity_dataset.py +26 -0
- vellum/client/types/folder_entity_dataset_data.py +25 -0
- vellum/types/build_status_enum.py +3 -0
- vellum/types/container_image_build_config.py +3 -0
- vellum/types/folder_entity_dataset.py +3 -0
- vellum/types/folder_entity_dataset_data.py +3 -0
- vellum/workflows/nodes/core/retry_node/tests/test_node.py +1 -1
- vellum/workflows/nodes/displayable/api_node/node.py +2 -0
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +43 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +6 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +30 -4
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +43 -3
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +68 -58
- vellum/workflows/nodes/experimental/tool_calling_node/node.py +10 -10
- vellum/workflows/nodes/experimental/tool_calling_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/experimental/tool_calling_node/tests/test_utils.py +49 -0
- vellum/workflows/nodes/experimental/tool_calling_node/utils.py +67 -6
- vellum/workflows/ports/utils.py +26 -6
- vellum/workflows/runner/runner.py +35 -3
- vellum/workflows/types/core.py +12 -0
- vellum/workflows/types/definition.py +6 -0
- vellum/workflows/utils/functions.py +9 -9
- vellum/workflows/utils/pydantic_schema.py +38 -0
- vellum/workflows/utils/tests/test_functions.py +11 -11
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/METADATA +1 -1
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/RECORD +66 -54
- vellum_cli/push.py +6 -8
- vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +8 -1
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +5 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +12 -12
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +10 -10
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +20 -9
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +8 -8
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +6 -6
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +8 -8
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +4 -4
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +3 -3
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +5 -5
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +2 -2
- vellum_ee/workflows/display/utils/auto_layout.py +1 -1
- vellum_ee/workflows/display/workflows/base_workflow_display.py +179 -4
- vellum_ee/workflows/tests/test_serialize_module.py +47 -0
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/LICENSE +0 -0
- {vellum_ai-0.14.72.dist-info → vellum_ai-0.14.73.dist-info}/WHEEL +0 -0
- {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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
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
|
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
|
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[
|
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[
|
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 =
|
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:
|
File without changes
|
@@ -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
|
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[
|
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 =
|
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:
|
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
|
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__)
|
vellum/workflows/ports/utils.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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=
|
384
|
-
code=
|
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],
|
vellum/workflows/types/core.py
CHANGED
@@ -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
|
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": [
|
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":
|
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":
|
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] =
|
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] =
|
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] =
|
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
|
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] =
|
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:
|