vellum-ai 1.0.10__py3-none-any.whl → 1.0.11__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 (27) hide show
  1. vellum/client/core/client_wrapper.py +2 -2
  2. vellum/workflows/descriptors/base.py +31 -1
  3. vellum/workflows/descriptors/utils.py +19 -1
  4. vellum/workflows/expressions/accessor.py +23 -15
  5. vellum/workflows/expressions/add.py +41 -0
  6. vellum/workflows/expressions/length.py +35 -0
  7. vellum/workflows/expressions/minus.py +41 -0
  8. vellum/workflows/expressions/tests/test_add.py +72 -0
  9. vellum/workflows/expressions/tests/test_length.py +38 -0
  10. vellum/workflows/expressions/tests/test_minus.py +72 -0
  11. vellum/workflows/integrations/composio_service.py +4 -0
  12. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +1 -1
  13. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +2 -2
  14. vellum/workflows/nodes/displayable/tool_calling_node/node.py +6 -1
  15. vellum/workflows/nodes/displayable/tool_calling_node/state.py +2 -0
  16. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py +49 -0
  17. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +3 -8
  18. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +92 -50
  19. vellum/workflows/types/definition.py +1 -6
  20. vellum/workflows/types/tests/test_definition.py +1 -1
  21. {vellum_ai-1.0.10.dist-info → vellum_ai-1.0.11.dist-info}/METADATA +1 -1
  22. {vellum_ai-1.0.10.dist-info → vellum_ai-1.0.11.dist-info}/RECORD +27 -21
  23. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +0 -4
  24. vellum_ee/workflows/display/utils/expressions.py +12 -0
  25. {vellum_ai-1.0.10.dist-info → vellum_ai-1.0.11.dist-info}/LICENSE +0 -0
  26. {vellum_ai-1.0.10.dist-info → vellum_ai-1.0.11.dist-info}/WHEEL +0 -0
  27. {vellum_ai-1.0.10.dist-info → vellum_ai-1.0.11.dist-info}/entry_points.txt +0 -0
@@ -25,10 +25,10 @@ class BaseClientWrapper:
25
25
 
26
26
  def get_headers(self) -> typing.Dict[str, str]:
27
27
  headers: typing.Dict[str, str] = {
28
- "User-Agent": "vellum-ai/1.0.10",
28
+ "User-Agent": "vellum-ai/1.0.11",
29
29
  "X-Fern-Language": "Python",
30
30
  "X-Fern-SDK-Name": "vellum-ai",
31
- "X-Fern-SDK-Version": "1.0.10",
31
+ "X-Fern-SDK-Version": "1.0.11",
32
32
  }
33
33
  if self._api_version is not None:
34
34
  headers["X-API-Version"] = self._api_version
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any, Generic, Optional, Tuple, Type, TypeVar,
2
2
 
3
3
  if TYPE_CHECKING:
4
4
  from vellum.workflows.expressions.accessor import AccessorExpression
5
+ from vellum.workflows.expressions.add import AddExpression
5
6
  from vellum.workflows.expressions.and_ import AndExpression
6
7
  from vellum.workflows.expressions.begins_with import BeginsWithExpression
7
8
  from vellum.workflows.expressions.between import BetweenExpression
@@ -26,8 +27,10 @@ if TYPE_CHECKING:
26
27
  from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
27
28
  from vellum.workflows.expressions.is_null import IsNullExpression
28
29
  from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
30
+ from vellum.workflows.expressions.length import LengthExpression
29
31
  from vellum.workflows.expressions.less_than import LessThanExpression
30
32
  from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
33
+ from vellum.workflows.expressions.minus import MinusExpression
31
34
  from vellum.workflows.expressions.not_between import NotBetweenExpression
32
35
  from vellum.workflows.expressions.not_in import NotInExpression
33
36
  from vellum.workflows.expressions.or_ import OrExpression
@@ -127,7 +130,7 @@ class BaseDescriptor(Generic[_T]):
127
130
 
128
131
  return CoalesceExpression(lhs=self, rhs=other)
129
132
 
130
- def __getitem__(self, field: Union[str, int]) -> "AccessorExpression":
133
+ def __getitem__(self, field: Union[str, int, "BaseDescriptor[str]", "BaseDescriptor[int]"]) -> "AccessorExpression":
131
134
  from vellum.workflows.expressions.accessor import AccessorExpression
132
135
 
133
136
  return AccessorExpression(base=self, field=field)
@@ -376,3 +379,30 @@ class BaseDescriptor(Generic[_T]):
376
379
  from vellum.workflows.expressions.concat import ConcatExpression
377
380
 
378
381
  return ConcatExpression(lhs=self, rhs=other)
382
+
383
+ def length(self) -> "LengthExpression[_T]":
384
+ from vellum.workflows.expressions.length import LengthExpression
385
+
386
+ return LengthExpression(expression=self)
387
+
388
+ @overload
389
+ def add(self, other: "BaseDescriptor[_O]") -> "AddExpression[_T, _O]": ...
390
+
391
+ @overload
392
+ def add(self, other: _O) -> "AddExpression[_T, _O]": ...
393
+
394
+ def add(self, other: "Union[BaseDescriptor[_O], _O]") -> "AddExpression[_T, _O]":
395
+ from vellum.workflows.expressions.add import AddExpression
396
+
397
+ return AddExpression(lhs=self, rhs=other)
398
+
399
+ @overload
400
+ def minus(self, other: "BaseDescriptor[_O]") -> "MinusExpression[_T, _O]": ...
401
+
402
+ @overload
403
+ def minus(self, other: _O) -> "MinusExpression[_T, _O]": ...
404
+
405
+ def minus(self, other: "Union[BaseDescriptor[_O], _O]") -> "MinusExpression[_T, _O]":
406
+ from vellum.workflows.expressions.minus import MinusExpression
407
+
408
+ return MinusExpression(lhs=self, rhs=other)
@@ -1,7 +1,8 @@
1
1
  from collections.abc import Mapping
2
2
  import dataclasses
3
3
  import inspect
4
- from typing import Any, Dict, Optional, Sequence, Set, TypeVar, Union, cast, overload
4
+ from typing import Any, Dict, Optional, Sequence, Set, Type, TypeVar, Union, cast, overload
5
+ from typing_extensions import TypeGuard
5
6
 
6
7
  from pydantic import BaseModel
7
8
 
@@ -115,3 +116,20 @@ def is_unresolved(value: Any) -> bool:
115
116
  return any(is_unresolved(item) for item in value)
116
117
 
117
118
  return False
119
+
120
+
121
+ _ResolvedType = TypeVar("_ResolvedType")
122
+
123
+
124
+ def is_resolved_instance(value: Any, type_: Type[_ResolvedType]) -> TypeGuard[_ResolvedType]:
125
+ """
126
+ Checks if a value is an instance of a type or a descriptor that resolves to that type.
127
+ """
128
+
129
+ if isinstance(value, type_):
130
+ return True
131
+
132
+ if isinstance(value, BaseDescriptor) and value.types:
133
+ return issubclass(value.types[0], type_)
134
+
135
+ return False
@@ -7,10 +7,11 @@ from pydantic_core import core_schema
7
7
 
8
8
  from vellum.workflows.descriptors.base import BaseDescriptor
9
9
  from vellum.workflows.descriptors.exceptions import InvalidExpressionException
10
- from vellum.workflows.descriptors.utils import resolve_value
10
+ from vellum.workflows.descriptors.utils import is_resolved_instance, resolve_value
11
11
  from vellum.workflows.state.base import BaseState
12
12
 
13
13
  LHS = TypeVar("LHS")
14
+ AccessorField = Union[str, int, BaseDescriptor[str], BaseDescriptor[int]]
14
15
 
15
16
 
16
17
  class AccessorExpression(BaseDescriptor[Any]):
@@ -18,7 +19,7 @@ class AccessorExpression(BaseDescriptor[Any]):
18
19
  self,
19
20
  *,
20
21
  base: BaseDescriptor[LHS],
21
- field: Union[str, int],
22
+ field: AccessorField,
22
23
  ) -> None:
23
24
  super().__init__(
24
25
  name=f"{base.name}.{field}",
@@ -28,7 +29,7 @@ class AccessorExpression(BaseDescriptor[Any]):
28
29
  self._base = base
29
30
  self._field = field
30
31
 
31
- def _infer_accessor_types(self, base: BaseDescriptor[LHS], field: Union[str, int]) -> tuple[Type, ...]:
32
+ def _infer_accessor_types(self, base: BaseDescriptor[LHS], field: AccessorField) -> tuple[Type, ...]:
32
33
  """
33
34
  Infer the types for this accessor expression based on the base descriptor's types
34
35
  and the field being accessed.
@@ -42,7 +43,7 @@ class AccessorExpression(BaseDescriptor[Any]):
42
43
  origin = get_origin(base_type)
43
44
  args = get_args(base_type)
44
45
 
45
- if isinstance(field, int) and origin in (list, tuple) and args:
46
+ if is_resolved_instance(field, int) and origin in (list, tuple) and args:
46
47
  if origin is list:
47
48
  inferred_types.append(args[0])
48
49
  elif origin is tuple and len(args) == 2 and args[1] is ...:
@@ -52,44 +53,51 @@ class AccessorExpression(BaseDescriptor[Any]):
52
53
  inferred_types.append(args[field])
53
54
  else:
54
55
  inferred_types.append(args[field])
55
- elif isinstance(field, str) and origin in (dict,) and len(args) >= 2:
56
+ elif is_resolved_instance(field, str) and origin in (dict,) and len(args) >= 2:
56
57
  inferred_types.append(args[1]) # Value type from Dict[K, V]
57
58
 
58
59
  return tuple(set(inferred_types)) if inferred_types else ()
59
60
 
60
61
  def resolve(self, state: "BaseState") -> Any:
61
62
  base = resolve_value(self._base, state)
63
+ accessor_field = resolve_value(self._field, state)
62
64
 
63
65
  if dataclasses.is_dataclass(base):
64
- if isinstance(self._field, int):
66
+ if isinstance(accessor_field, int):
65
67
  raise InvalidExpressionException("Cannot access field by index on a dataclass")
66
68
 
67
69
  try:
68
- return getattr(base, self._field)
70
+ return getattr(base, accessor_field)
69
71
  except AttributeError:
70
- raise InvalidExpressionException(f"Field '{self._field}' not found on dataclass {type(base).__name__}")
72
+ raise InvalidExpressionException(
73
+ f"Field '{accessor_field}' not found on dataclass {type(base).__name__}"
74
+ )
71
75
 
72
76
  if isinstance(base, BaseModel):
73
- if isinstance(self._field, int):
77
+ if isinstance(accessor_field, int):
74
78
  raise InvalidExpressionException("Cannot access field by index on a BaseModel")
75
79
 
76
80
  try:
77
- return getattr(base, self._field)
81
+ return getattr(base, accessor_field)
78
82
  except AttributeError:
79
- raise InvalidExpressionException(f"Field '{self._field}' not found on BaseModel {type(base).__name__}")
83
+ raise InvalidExpressionException(
84
+ f"Field '{accessor_field}' not found on BaseModel {type(base).__name__}"
85
+ )
80
86
 
81
87
  if isinstance(base, Mapping):
82
88
  try:
83
- return base[self._field]
89
+ return base[accessor_field]
84
90
  except KeyError:
85
- raise InvalidExpressionException(f"Key '{self._field}' not found in mapping")
91
+ raise InvalidExpressionException(f"Key '{accessor_field}' not found in mapping")
86
92
 
87
93
  if isinstance(base, Sequence):
88
94
  try:
89
- index = int(self._field)
95
+ index = int(accessor_field)
90
96
  return base[index]
91
97
  except (IndexError, ValueError):
92
- if isinstance(self._field, int) or (isinstance(self._field, str) and self._field.lstrip("-").isdigit()):
98
+ if isinstance(accessor_field, int) or (
99
+ isinstance(accessor_field, str) and accessor_field.lstrip("-").isdigit()
100
+ ):
93
101
  raise InvalidExpressionException(
94
102
  f"Index {self._field} is out of bounds for sequence of length {len(base)}"
95
103
  )
@@ -0,0 +1,41 @@
1
+ from typing import Any, Generic, Protocol, TypeVar, Union, runtime_checkable
2
+ from typing_extensions import TypeGuard
3
+
4
+ from vellum.workflows.descriptors.base import BaseDescriptor
5
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
6
+ from vellum.workflows.descriptors.utils import resolve_value
7
+ from vellum.workflows.state.base import BaseState
8
+
9
+
10
+ @runtime_checkable
11
+ class SupportsAdd(Protocol):
12
+ def __add__(self, other: Any) -> Any: ...
13
+
14
+
15
+ def has_add(obj: Any) -> TypeGuard[SupportsAdd]:
16
+ return hasattr(obj, "__add__")
17
+
18
+
19
+ LHS = TypeVar("LHS")
20
+ RHS = TypeVar("RHS")
21
+
22
+
23
+ class AddExpression(BaseDescriptor[Any], Generic[LHS, RHS]):
24
+ def __init__(
25
+ self,
26
+ *,
27
+ lhs: Union[BaseDescriptor[LHS], LHS],
28
+ rhs: Union[BaseDescriptor[RHS], RHS],
29
+ ) -> None:
30
+ super().__init__(name=f"{lhs} + {rhs}", types=(object,))
31
+ self._lhs = lhs
32
+ self._rhs = rhs
33
+
34
+ def resolve(self, state: "BaseState") -> Any:
35
+ lhs = resolve_value(self._lhs, state)
36
+ rhs = resolve_value(self._rhs, state)
37
+
38
+ if not has_add(lhs):
39
+ raise InvalidExpressionException(f"'{lhs.__class__.__name__}' must support the '+' operator")
40
+
41
+ return lhs + rhs
@@ -0,0 +1,35 @@
1
+ from typing import Generic, TypeVar, Union
2
+
3
+ from vellum.workflows.constants import undefined
4
+ from vellum.workflows.descriptors.base import BaseDescriptor
5
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
6
+ from vellum.workflows.descriptors.utils import resolve_value
7
+ from vellum.workflows.state.base import BaseState
8
+
9
+ _T = TypeVar("_T")
10
+
11
+
12
+ class LengthExpression(BaseDescriptor[int], Generic[_T]):
13
+ def __init__(
14
+ self,
15
+ *,
16
+ expression: Union[BaseDescriptor[_T], _T],
17
+ ) -> None:
18
+ super().__init__(name=f"length({expression})", types=(int,))
19
+ self._expression = expression
20
+
21
+ def resolve(self, state: "BaseState") -> int:
22
+ expression = resolve_value(self._expression, state)
23
+
24
+ if expression is undefined:
25
+ raise InvalidExpressionException("Cannot get length of undefined value")
26
+
27
+ if not hasattr(expression, "__len__"):
28
+ raise InvalidExpressionException(
29
+ f"Expected an object that supports `len()`, got `{expression.__class__.__name__}`"
30
+ )
31
+
32
+ try:
33
+ return len(expression)
34
+ except TypeError as e:
35
+ raise InvalidExpressionException(f"Cannot get length of `{expression.__class__.__name__}`: {str(e)}")
@@ -0,0 +1,41 @@
1
+ from typing import Any, Generic, Protocol, TypeVar, Union, runtime_checkable
2
+ from typing_extensions import TypeGuard
3
+
4
+ from vellum.workflows.descriptors.base import BaseDescriptor
5
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
6
+ from vellum.workflows.descriptors.utils import resolve_value
7
+ from vellum.workflows.state.base import BaseState
8
+
9
+
10
+ @runtime_checkable
11
+ class SupportsMinus(Protocol):
12
+ def __sub__(self, other: Any) -> Any: ...
13
+
14
+
15
+ def has_sub(obj: Any) -> TypeGuard[SupportsMinus]:
16
+ return hasattr(obj, "__sub__")
17
+
18
+
19
+ LHS = TypeVar("LHS")
20
+ RHS = TypeVar("RHS")
21
+
22
+
23
+ class MinusExpression(BaseDescriptor[Any], Generic[LHS, RHS]):
24
+ def __init__(
25
+ self,
26
+ *,
27
+ lhs: Union[BaseDescriptor[LHS], LHS],
28
+ rhs: Union[BaseDescriptor[RHS], RHS],
29
+ ) -> None:
30
+ super().__init__(name=f"{lhs} - {rhs}", types=(object,))
31
+ self._lhs = lhs
32
+ self._rhs = rhs
33
+
34
+ def resolve(self, state: "BaseState") -> Any:
35
+ lhs = resolve_value(self._lhs, state)
36
+ rhs = resolve_value(self._rhs, state)
37
+
38
+ if not has_sub(lhs):
39
+ raise InvalidExpressionException(f"'{lhs.__class__.__name__}' must support the '-' operator")
40
+
41
+ return lhs - rhs
@@ -0,0 +1,72 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
4
+ from vellum.workflows.expressions.add import AddExpression
5
+ from vellum.workflows.state.base import BaseState
6
+
7
+
8
+ class TestState(BaseState):
9
+ number_value: int = 5
10
+ string_value: str = "hello"
11
+
12
+
13
+ def test_add_expression_numbers():
14
+ """
15
+ Tests that AddExpression correctly adds two numbers.
16
+ """
17
+
18
+ state = TestState()
19
+
20
+ expression = TestState.number_value.add(10)
21
+
22
+ result = expression.resolve(state)
23
+ assert result == 15
24
+
25
+
26
+ def test_add_expression_strings():
27
+ """
28
+ Tests that AddExpression correctly concatenates two strings.
29
+ """
30
+
31
+ state = TestState()
32
+
33
+ expression = TestState.string_value.add(" world")
34
+
35
+ result = expression.resolve(state)
36
+ assert result == "hello world"
37
+
38
+
39
+ def test_add_expression_unsupported_type():
40
+ """
41
+ Tests that AddExpression raises an exception for unsupported types.
42
+ """
43
+
44
+ class NoAddSupport:
45
+ pass
46
+
47
+ no_add_obj = NoAddSupport()
48
+ expression = AddExpression(lhs=no_add_obj, rhs=5)
49
+ state = TestState()
50
+
51
+ with pytest.raises(InvalidExpressionException, match="'NoAddSupport' must support the '\\+' operator"):
52
+ expression.resolve(state)
53
+
54
+
55
+ def test_add_expression_name():
56
+ """
57
+ Tests that AddExpression has the correct name.
58
+ """
59
+
60
+ expression = AddExpression(lhs=5, rhs=3)
61
+
62
+ assert expression.name == "5 + 3"
63
+
64
+
65
+ def test_add_expression_types():
66
+ """
67
+ Tests that AddExpression has the correct types.
68
+ """
69
+
70
+ expression = AddExpression(lhs=5, rhs=3)
71
+
72
+ assert expression.types == (object,)
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.constants import undefined
4
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
5
+ from vellum.workflows.expressions.length import LengthExpression
6
+ from vellum.workflows.state.base import BaseState
7
+
8
+
9
+ class TestState(BaseState):
10
+ string_value: str = "hello world"
11
+
12
+
13
+ def test_length_expression_string():
14
+ """
15
+ Tests that LengthExpression correctly returns the length of a string.
16
+ """
17
+
18
+ state = TestState()
19
+
20
+ expression = TestState.string_value.length()
21
+ result = expression.resolve(state)
22
+
23
+ assert result == 11
24
+
25
+
26
+ def test_length_expression_undefined():
27
+ """
28
+ Tests that LengthExpression raises an exception for undefined values.
29
+ """
30
+
31
+ expression = LengthExpression(expression=undefined)
32
+ state = TestState()
33
+
34
+ # THEN we should get an InvalidExpressionException
35
+ with pytest.raises(InvalidExpressionException) as exc_info:
36
+ expression.resolve(state)
37
+
38
+ assert "Cannot get length of undefined value" in str(exc_info.value)
@@ -0,0 +1,72 @@
1
+ import pytest
2
+
3
+ from vellum.workflows.descriptors.exceptions import InvalidExpressionException
4
+ from vellum.workflows.expressions.minus import MinusExpression
5
+ from vellum.workflows.state.base import BaseState
6
+
7
+
8
+ class TestState(BaseState):
9
+ number_value: int = 10
10
+ float_value: float = 15.5
11
+
12
+
13
+ def test_minus_expression_numbers():
14
+ """
15
+ Tests that MinusExpression correctly subtracts two numbers.
16
+ """
17
+
18
+ state = TestState()
19
+
20
+ expression = TestState.number_value.minus(3)
21
+
22
+ result = expression.resolve(state)
23
+ assert result == 7
24
+
25
+
26
+ def test_minus_expression_floats():
27
+ """
28
+ Tests that MinusExpression correctly subtracts two floats.
29
+ """
30
+
31
+ state = TestState()
32
+
33
+ expression = TestState.float_value.minus(5.5)
34
+
35
+ result = expression.resolve(state)
36
+ assert result == 10.0
37
+
38
+
39
+ def test_minus_expression_unsupported_type():
40
+ """
41
+ Tests that MinusExpression raises an exception for unsupported types.
42
+ """
43
+
44
+ class NoSubSupport:
45
+ pass
46
+
47
+ no_sub_obj = NoSubSupport()
48
+ expression = MinusExpression(lhs=no_sub_obj, rhs=5)
49
+ state = TestState()
50
+
51
+ with pytest.raises(InvalidExpressionException, match="'NoSubSupport' must support the '-' operator"):
52
+ expression.resolve(state)
53
+
54
+
55
+ def test_minus_expression_name():
56
+ """
57
+ Tests that MinusExpression has the correct name.
58
+ """
59
+
60
+ expression = MinusExpression(lhs=10, rhs=3)
61
+
62
+ assert expression.name == "10 - 3"
63
+
64
+
65
+ def test_minus_expression_types():
66
+ """
67
+ Tests that MinusExpression has the correct types.
68
+ """
69
+
70
+ expression = MinusExpression(lhs=10, rhs=3)
71
+
72
+ assert expression.types == (object,)
@@ -151,4 +151,8 @@ class ComposioService:
151
151
  if user_id is not None:
152
152
  json_data["user_id"] = user_id
153
153
  response = self._make_request(endpoint, method="POST", json_data=json_data)
154
+
155
+ if not response.get("successful", True):
156
+ return response.get("error", "Tool execution failed")
157
+
154
158
  return response.get("data", response)
@@ -14,7 +14,7 @@ from vellum.workflows.types.core import EntityInputsInterface, MergeBehavior
14
14
  from vellum.workflows.types.generics import StateType
15
15
 
16
16
 
17
- class BasePromptNode(BaseNode, Generic[StateType]):
17
+ class BasePromptNode(BaseNode[StateType], Generic[StateType]):
18
18
  # Inputs that are passed to the Prompt
19
19
  prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
20
20
 
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Dict, Iterator, Type, Union
2
+ from typing import Any, Dict, Generic, Iterator, Type, Union
3
3
 
4
4
  from vellum.workflows.constants import undefined
5
5
  from vellum.workflows.errors import WorkflowErrorCode
@@ -10,7 +10,7 @@ from vellum.workflows.types import MergeBehavior
10
10
  from vellum.workflows.types.generics import StateType
11
11
 
12
12
 
13
- class InlinePromptNode(BaseInlinePromptNode[StateType]):
13
+ class InlinePromptNode(BaseInlinePromptNode[StateType], Generic[StateType]):
14
14
  """
15
15
  Used to execute a Prompt defined inline.
16
16
 
@@ -12,6 +12,7 @@ from vellum.workflows.inputs.base import BaseInputs
12
12
  from vellum.workflows.nodes.bases import BaseNode
13
13
  from vellum.workflows.nodes.displayable.tool_calling_node.state import ToolCallingState
14
14
  from vellum.workflows.nodes.displayable.tool_calling_node.utils import (
15
+ create_else_node,
15
16
  create_function_node,
16
17
  create_mcp_tool_node,
17
18
  create_tool_router_node,
@@ -171,7 +172,11 @@ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
171
172
  edge_graph = router_port >> FunctionNodeClass >> self.tool_router_node
172
173
  graph_set.add(edge_graph)
173
174
 
174
- default_port = getattr(self.tool_router_node.Ports, "default")
175
+ else_node = create_else_node(self.tool_router_node)
176
+ default_port = self.tool_router_node.Ports.default >> {
177
+ else_node.Ports.loop >> self.tool_router_node,
178
+ else_node.Ports.end,
179
+ }
175
180
  graph_set.add(default_port)
176
181
 
177
182
  self._graph = Graph.from_set(graph_set)
@@ -7,3 +7,5 @@ from vellum.workflows.state.base import BaseState
7
7
  class ToolCallingState(BaseState):
8
8
  chat_history: List[ChatMessage] = []
9
9
  prompt_iterations: int = 0
10
+ current_prompt_output_index: int = 0
11
+ current_function_calls_processed: int = 0
@@ -44,6 +44,21 @@ def mock_tool_execution_response():
44
44
  }
45
45
 
46
46
 
47
+ @pytest.fixture
48
+ def mock_tool_execution_error_response():
49
+ """Mock response for failed tool execution API"""
50
+ return {
51
+ "data": {},
52
+ "successful": False,
53
+ "error": (
54
+ 'Request failed error: `{"message":"Not Found",'
55
+ '"documentation_url":"https://docs.github.com/rest/pulls/pulls#get-a-pull-request",'
56
+ '"status":"404"}`'
57
+ ),
58
+ "log_id": "log_raE_fIWNcDPo",
59
+ }
60
+
61
+
47
62
  @pytest.fixture
48
63
  def composio_service():
49
64
  """Create ComposioService with test API key"""
@@ -168,3 +183,37 @@ class TestComposioCoreService:
168
183
  timeout=30,
169
184
  )
170
185
  assert result == {"items": [], "total": 0}
186
+
187
+ def test_execute_tool_failure_surfaces_error(
188
+ self, composio_service, mock_requests, mock_tool_execution_error_response
189
+ ):
190
+ """Test that tool execution failures surface detailed error information"""
191
+ # GIVEN a mock response indicating tool execution failure
192
+ mock_response = Mock()
193
+ mock_response.json.return_value = mock_tool_execution_error_response
194
+ mock_response.raise_for_status.return_value = None
195
+ mock_requests.post.return_value = mock_response
196
+
197
+ # WHEN we execute a tool that fails
198
+ result = composio_service.execute_tool("GITHUB_GET_PR", {"repo": "test", "pr_number": 999})
199
+
200
+ # THEN the result should contain the detailed error message from the API
201
+ assert "Request failed error" in result
202
+ assert "Not Found" in result
203
+
204
+ def test_execute_tool_failure_with_generic_error_message(self, composio_service, mock_requests):
205
+ """Test that tool execution failures with missing error field use generic message"""
206
+ # GIVEN a mock response indicating tool execution failure without error field
207
+ mock_response = Mock()
208
+ mock_response.json.return_value = {
209
+ "data": {},
210
+ "successful": False,
211
+ }
212
+ mock_response.raise_for_status.return_value = None
213
+ mock_requests.post.return_value = mock_response
214
+
215
+ # WHEN we execute a tool that fails
216
+ result = composio_service.execute_tool("TEST_TOOL", {"param": "value"})
217
+
218
+ # THEN the result should contain the generic error message
219
+ assert result == "Tool execution failed"
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from uuid import uuid4
3
- from typing import Any, Iterator, List
3
+ from typing import Any, Iterator
4
4
 
5
5
  from vellum import ChatMessage
6
6
  from vellum.client.types.fulfilled_execute_prompt_event import FulfilledExecutePromptEvent
@@ -15,6 +15,7 @@ from vellum.workflows import BaseWorkflow
15
15
  from vellum.workflows.inputs.base import BaseInputs
16
16
  from vellum.workflows.nodes.bases import BaseNode
17
17
  from vellum.workflows.nodes.displayable.tool_calling_node.node import ToolCallingNode
18
+ from vellum.workflows.nodes.displayable.tool_calling_node.state import ToolCallingState
18
19
  from vellum.workflows.nodes.displayable.tool_calling_node.utils import create_function_node, create_tool_router_node
19
20
  from vellum.workflows.outputs.base import BaseOutputs
20
21
  from vellum.workflows.state.base import BaseState, StateMeta
@@ -44,7 +45,7 @@ def test_port_condition_match_function_name():
44
45
  )
45
46
 
46
47
  # AND a state with a function call to the first function
47
- state = BaseState(
48
+ state = ToolCallingState(
48
49
  meta=StateMeta(
49
50
  node_outputs={
50
51
  router_node.Outputs.results: [
@@ -116,12 +117,6 @@ def test_tool_calling_node_inline_workflow_context():
116
117
  )
117
118
  function_node._context = parent_context
118
119
 
119
- # Create a state with chat_history for the function node
120
- class TestState(BaseState):
121
- chat_history: List[ChatMessage] = []
122
-
123
- function_node.state = TestState(meta=StateMeta(node_outputs={tool_router_node.Outputs.text: '{"arguments": {}}'}))
124
-
125
120
  # WHEN the function node runs
126
121
  outputs = list(function_node.run())
127
122
 
@@ -5,6 +5,8 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Type, Union, c
5
5
  from pydash import snake_case
6
6
 
7
7
  from vellum import ChatMessage, PromptBlock
8
+ from vellum.client.types.array_chat_message_content import ArrayChatMessageContent
9
+ from vellum.client.types.array_chat_message_content_item import ArrayChatMessageContentItem
8
10
  from vellum.client.types.function_call_chat_message_content import FunctionCallChatMessageContent
9
11
  from vellum.client.types.function_call_chat_message_content_value import FunctionCallChatMessageContentValue
10
12
  from vellum.client.types.function_definition import FunctionDefinition
@@ -59,6 +61,9 @@ class FunctionCallNodeMixin:
59
61
  content=StringChatMessageContent(value=json.dumps(result, cls=DefaultStateEncoder)),
60
62
  )
61
63
  )
64
+ with state.__quiet__():
65
+ state.current_function_calls_processed += 1
66
+ state.current_prompt_output_index += 1
62
67
 
63
68
 
64
69
  class ToolRouterNode(InlinePromptNode[ToolCallingState]):
@@ -73,29 +78,37 @@ class ToolRouterNode(InlinePromptNode[ToolCallingState]):
73
78
  raise NodeException(message=max_iterations_message, code=WorkflowErrorCode.NODE_EXECUTION)
74
79
 
75
80
  generator = super().run()
81
+ with self.state.__quiet__():
82
+ self.state.current_prompt_output_index = 0
83
+ self.state.current_function_calls_processed = 0
84
+ self.state.prompt_iterations += 1
76
85
  for output in generator:
77
- if output.name == "results" and output.value:
78
- values = cast(List[Any], output.value)
79
- if values and len(values) > 0:
80
- if values[0].type == "STRING":
81
- self.state.chat_history.append(ChatMessage(role="ASSISTANT", text=values[0].value))
82
- elif values[0].type == "FUNCTION_CALL":
83
- self.state.prompt_iterations += 1
84
-
85
- function_call = values[0].value
86
- if function_call is not None:
87
- self.state.chat_history.append(
88
- ChatMessage(
89
- role="ASSISTANT",
90
- content=FunctionCallChatMessageContent(
91
- value=FunctionCallChatMessageContentValue(
92
- name=function_call.name,
93
- arguments=function_call.arguments,
94
- id=function_call.id,
95
- ),
96
- ),
97
- )
86
+ if output.name == InlinePromptNode.Outputs.results.name and output.value:
87
+ prompt_outputs = cast(List[PromptOutput], output.value)
88
+ chat_contents: List[ArrayChatMessageContentItem] = []
89
+ for prompt_output in prompt_outputs:
90
+ if prompt_output.type == "STRING":
91
+ chat_contents.append(StringChatMessageContent(value=prompt_output.value))
92
+ elif prompt_output.type == "FUNCTION_CALL" and prompt_output.value:
93
+ raw_function_call = prompt_output.value.model_dump()
94
+ if "state" in raw_function_call:
95
+ del raw_function_call["state"]
96
+ chat_contents.append(
97
+ FunctionCallChatMessageContent(
98
+ value=FunctionCallChatMessageContentValue.model_validate(raw_function_call)
98
99
  )
100
+ )
101
+
102
+ if len(chat_contents) == 1:
103
+ if chat_contents[0].type == "STRING":
104
+ self.state.chat_history.append(ChatMessage(role="ASSISTANT", text=chat_contents[0].value))
105
+ else:
106
+ self.state.chat_history.append(ChatMessage(role="ASSISTANT", content=chat_contents[0]))
107
+ else:
108
+ self.state.chat_history.append(
109
+ ChatMessage(role="ASSISTANT", content=ArrayChatMessageContent(value=chat_contents))
110
+ )
111
+
99
112
  yield output
100
113
 
101
114
 
@@ -225,42 +238,51 @@ class MCPNode(BaseNode[ToolCallingState], FunctionCallNodeMixin):
225
238
  yield from []
226
239
 
227
240
 
228
- def _hydrate_composio_tool_definition(tool_def: ComposioToolDefinition) -> ComposioToolDefinition:
241
+ class ElseNode(BaseNode[ToolCallingState]):
242
+ """Node that executes when no function conditions match."""
243
+
244
+ class Ports(BaseNode.Ports):
245
+ # Redefined in the create_else_node function, but defined here to resolve mypy errors
246
+ loop = Port.on_if(
247
+ ToolCallingState.current_prompt_output_index.less_than(1)
248
+ | ToolCallingState.current_function_calls_processed.greater_than(0)
249
+ )
250
+ end = Port.on_else()
251
+
252
+ def run(self) -> BaseNode.Outputs:
253
+ with self.state.__quiet__():
254
+ self.state.current_prompt_output_index += 1
255
+ return self.Outputs()
256
+
257
+
258
+ def _hydrate_composio_tool_definition(tool_def: ComposioToolDefinition) -> FunctionDefinition:
229
259
  """Hydrate a ComposioToolDefinition with detailed information from the Composio API.
230
260
 
231
261
  Args:
232
262
  tool_def: The basic ComposioToolDefinition to enhance
233
263
 
234
264
  Returns:
235
- ComposioToolDefinition with detailed parameters and description
265
+ FunctionDefinition with detailed parameters and description
236
266
  """
237
267
  try:
238
268
  composio_service = ComposioService()
239
269
  tool_details = composio_service.get_tool_by_slug(tool_def.action)
240
270
 
241
- # Extract toolkit information from API response
242
- toolkit_info = tool_details.get("toolkit", {})
243
- toolkit_slug = (
244
- toolkit_info.get("slug", tool_def.toolkit) if isinstance(toolkit_info, dict) else tool_def.toolkit
245
- )
246
-
247
- # Create a version of the tool definition with proper field extraction
248
- return ComposioToolDefinition(
249
- type=tool_def.type,
250
- toolkit=toolkit_slug.upper() if toolkit_slug else tool_def.toolkit,
251
- action=tool_details.get("slug", tool_def.action),
271
+ # Create a FunctionDefinition directly with proper field extraction
272
+ return FunctionDefinition(
273
+ name=tool_def.name,
252
274
  description=tool_details.get("description", tool_def.description),
253
- display_name=tool_details.get("name", tool_def.display_name),
254
- parameters=tool_details.get("input_parameters", tool_def.parameters),
255
- version=tool_details.get("version", tool_def.version),
256
- tags=tool_details.get("tags", tool_def.tags),
257
- user_id=tool_def.user_id,
275
+ parameters=tool_details.get("input_parameters", {}),
258
276
  )
259
277
 
260
278
  except Exception as e:
261
- # If hydration fails (including no API key), log and return original
279
+ # If hydration fails (including no API key), log and return basic function definition
262
280
  logger.warning(f"Failed to enhance Composio tool '{tool_def.action}': {e}")
263
- return tool_def
281
+ return FunctionDefinition(
282
+ name=tool_def.name,
283
+ description=tool_def.description,
284
+ parameters={},
285
+ )
264
286
 
265
287
 
266
288
  def hydrate_mcp_tool_definitions(server_def: MCPServer) -> List[MCPToolDefinition]:
@@ -303,8 +325,13 @@ def create_tool_router_node(
303
325
  return Port.on_if(
304
326
  LazyReference(
305
327
  lambda: (
306
- node.Outputs.results[0]["type"].equals("FUNCTION_CALL")
307
- & node.Outputs.results[0]["value"]["name"].equals(fn_name)
328
+ ToolCallingState.current_prompt_output_index.less_than(node.Outputs.results.length())
329
+ & node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
330
+ "FUNCTION_CALL"
331
+ )
332
+ & node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"]["name"].equals(
333
+ fn_name
334
+ )
308
335
  )
309
336
  )
310
337
  )
@@ -313,13 +340,7 @@ def create_tool_router_node(
313
340
  if isinstance(function, ComposioToolDefinition):
314
341
  # Get Composio tool details and hydrate the function definition
315
342
  enhanced_function = _hydrate_composio_tool_definition(function)
316
- prompt_functions.append(
317
- FunctionDefinition(
318
- name=enhanced_function.name,
319
- description=enhanced_function.description,
320
- parameters=enhanced_function.parameters,
321
- )
322
- )
343
+ prompt_functions.append(enhanced_function)
323
344
  # Create port for this function (using original function for get_function_name)
324
345
  function_name = get_function_name(function)
325
346
  port = create_port_condition(function_name)
@@ -485,6 +506,27 @@ def create_mcp_tool_node(
485
506
  return node
486
507
 
487
508
 
509
+ def create_else_node(
510
+ tool_router_node: Type[ToolRouterNode],
511
+ ) -> Type[ElseNode]:
512
+ class Ports(ElseNode.Ports):
513
+ loop = Port.on_if(
514
+ ToolCallingState.current_prompt_output_index.less_than(tool_router_node.Outputs.results.length())
515
+ | ToolCallingState.current_function_calls_processed.greater_than(0)
516
+ )
517
+ end = Port.on_else()
518
+
519
+ node = type(
520
+ f"{tool_router_node.__name__}_ElseNode",
521
+ (ElseNode,),
522
+ {
523
+ "Ports": Ports,
524
+ "__module__": __name__,
525
+ },
526
+ )
527
+ return node
528
+
529
+
488
530
  def get_function_name(function: ToolBase) -> str:
489
531
  if isinstance(function, DeploymentDefinition):
490
532
  name = str(function.deployment_id or function.deployment_name)
@@ -2,7 +2,7 @@ import importlib
2
2
  import inspect
3
3
  from types import FrameType
4
4
  from uuid import UUID
5
- from typing import Annotated, Any, Dict, List, Literal, Optional, Union
5
+ from typing import Annotated, Any, Dict, Literal, Optional, Union
6
6
 
7
7
  from pydantic import BeforeValidator
8
8
 
@@ -111,11 +111,6 @@ class ComposioToolDefinition(UniversalBaseModel):
111
111
  toolkit: str # "GITHUB", "SLACK", etc.
112
112
  action: str # Specific action like "GITHUB_CREATE_AN_ISSUE"
113
113
  description: str
114
-
115
- display_name: Optional[str] = None
116
- parameters: Optional[Dict[str, Any]] = None
117
- version: Optional[str] = None
118
- tags: Optional[List[str]] = None
119
114
  user_id: Optional[str] = None
120
115
 
121
116
  @property
@@ -48,7 +48,7 @@ def test_composio_tool_definition_creation():
48
48
  assert composio_tool.toolkit == "GITHUB"
49
49
  assert composio_tool.action == "GITHUB_CREATE_AN_ISSUE"
50
50
  assert composio_tool.description == "Create a new issue in a GitHub repository"
51
- assert composio_tool.display_name is None
51
+ assert composio_tool.user_id is None
52
52
  assert composio_tool.name == "github_create_an_issue"
53
53
 
54
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.0.10
3
+ Version: 1.0.11
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0
@@ -94,7 +94,7 @@ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_
94
94
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py,sha256=XWrhHg_acLsRHwjstBAii9Pmes9oXFtAUWSAVF1oSBc,11225
95
95
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py,sha256=V8b6gKghLlO7PJI8xeNdnfn8aII0W_IFQvSQBQM62UQ,7721
96
96
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py,sha256=hDWtKXmGI1CKhTwTNqpu_d5RkE5n7SolMLtgd87KqTI,3856
97
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py,sha256=sWKSqw1B4iAEamIzbRJBfjtMCy_D54gxEu3aPSDrS_o,3819
97
+ vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py,sha256=AUfULtIangNYkvLH3jd2Ar8X5ulW4tGmezeCfMmXFUU,3697
98
98
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py,sha256=4t1lkN2nsZF6lFqP6QnskUQWJlhasF8C2_f6atzk8ZY,26298
99
99
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py,sha256=B0rDsCvO24qPp0gkmj8SdTDY5CxZYkvKwknsKBuAPyA,10017
100
100
  vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py,sha256=mova0sPD3evHiHIN1O0VynxlCp-uOcEIKve5Pd_oCDg,4069
@@ -105,7 +105,7 @@ vellum_ee/workflows/display/types.py,sha256=i4T7ElU5b5h-nA1i3scmEhO1BqmNDc4eJDHa
105
105
  vellum_ee/workflows/display/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
106
  vellum_ee/workflows/display/utils/auto_layout.py,sha256=f4GiLn_LazweupfqTpubcdtdfE_vrOcmZudSsnYIY9E,3906
107
107
  vellum_ee/workflows/display/utils/exceptions.py,sha256=LSwwxCYNxFkf5XMUcFkaZKpQ13OSrI7y_bpEUwbKVk0,169
108
- vellum_ee/workflows/display/utils/expressions.py,sha256=H5_yWpX4QQNr83iIIocOHddTpktsHbVDp1ylayvjjTE,15840
108
+ vellum_ee/workflows/display/utils/expressions.py,sha256=YZjd6wrOkmmnTe5LstKaVaBqmR8Q8XS81QzSFwkLtN0,16324
109
109
  vellum_ee/workflows/display/utils/registry.py,sha256=fWIm5Jj-10gNFjgn34iBu4RWv3Vd15ijtSN0V97bpW8,1513
110
110
  vellum_ee/workflows/display/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
111
  vellum_ee/workflows/display/utils/tests/test_auto_layout.py,sha256=vfXI769418s9vda5Gb5NFBH747WMOwSgHRXeLCTLVm8,2356
@@ -145,7 +145,7 @@ vellum/client/README.md,sha256=Dle5iytCXxP1pNeNd7uZyhFo0rl7tp7vU7s8gmi10OQ,4863
145
145
  vellum/client/__init__.py,sha256=KmkyOgReuTsjmXF3WC_dPQ9QqJgYrB3Sr8_LcSUIQyI,125258
146
146
  vellum/client/core/__init__.py,sha256=SQ85PF84B9MuKnBwHNHWemSGuy-g_515gFYNFhvEE0I,1438
147
147
  vellum/client/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
148
- vellum/client/core/client_wrapper.py,sha256=PYKT4GSWVfnUCdamESx8hYo2KF9SVuaYQAHwwHxyYSI,2385
148
+ vellum/client/core/client_wrapper.py,sha256=GLUu-tM9O4VyvaRt1Uz-BgKMqUFNR3H4hdD14V7NGqM,2385
149
149
  vellum/client/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
150
150
  vellum/client/core/file.py,sha256=d4NNbX8XvXP32z8KpK2Xovv33nFfruIrpz0QWxlgpZk,2663
151
151
  vellum/client/core/http_client.py,sha256=cKs2w0ybDBk1wHQf-fTALm_MmvaMe3cZKcYJxqmCxkE,19539
@@ -1530,10 +1530,10 @@ vellum/workflows/__init__.py,sha256=CssPsbNvN6rDhoLuqpEv7MMKGa51vE6dvAh6U31Pcio,
1530
1530
  vellum/workflows/constants.py,sha256=2yg4_uo5gpqViy3ZLSwfC8qTybleYCtOnhA4Rj6bacM,1310
1531
1531
  vellum/workflows/context.py,sha256=jvMuyeRluay8BQa7GX1TqUlmoHLCycAVYKkp87sfXSo,1644
1532
1532
  vellum/workflows/descriptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1533
- vellum/workflows/descriptors/base.py,sha256=nZeCuuoJj3eJJjhrqOyfBB4FLUkN6fuO8PDdx6YDkzc,15373
1533
+ vellum/workflows/descriptors/base.py,sha256=fRnRkECyDjfz2QEDCY9Q5mAerlJ6jR0R4nE-MP2VP_k,16558
1534
1534
  vellum/workflows/descriptors/exceptions.py,sha256=gUy4UD9JFUKSeQnQpeuDSLiRqWjWiIsxLahB7p_q3JY,54
1535
1535
  vellum/workflows/descriptors/tests/test_utils.py,sha256=HJ5DoRz0sJvViGxyZ_FtytZjxN2J8xTkGtaVwCy6Q90,6928
1536
- vellum/workflows/descriptors/utils.py,sha256=1siECBf6AI54gwwUwkF6mP9rYsRryUGaOYBbMpQaceM,3848
1536
+ vellum/workflows/descriptors/utils.py,sha256=7QvS_IOZWIoKvhNwpYBOTP3NasLSIBKTnhyASry2HCM,4320
1537
1537
  vellum/workflows/edges/__init__.py,sha256=wSkmAnz9xyi4vZwtDbKxwlplt2skD7n3NsxkvR_pUus,50
1538
1538
  vellum/workflows/edges/edge.py,sha256=N0SnY3gKVuxImPAdCbPMPlHJIXbkQ3fwq_LbJRvVMFc,677
1539
1539
  vellum/workflows/emitters/__init__.py,sha256=d9QFOI3eVg6rzpSFLvrjkDYXWikf1tcp3ruTRa2Boyc,143
@@ -1553,7 +1553,8 @@ vellum/workflows/events/types.py,sha256=jc6vEcydzYETfK3u2Cx1BfUDmYh3Jxa86sdsrrQ5
1553
1553
  vellum/workflows/events/workflow.py,sha256=cR2aG6A-JqvLMKFqonDhKjSg496qldEtuks4BXXMTrY,7768
1554
1554
  vellum/workflows/exceptions.py,sha256=NiBiR3ggfmPxBVqD-H1SqmjI-7mIn0EStSN1BqApvCM,1213
1555
1555
  vellum/workflows/expressions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1556
- vellum/workflows/expressions/accessor.py,sha256=4OvF9JQKHSyYLTz7gGprcw3Da5-zA8D0Co0j1eIeer0,4141
1556
+ vellum/workflows/expressions/accessor.py,sha256=3lu1-_-dBfZdJvtX-q66jbmRAZtqIfdsh_3_JNuzg1E,4462
1557
+ vellum/workflows/expressions/add.py,sha256=Rr1O83nksL5Z0kam4eaQOokvDrEwlUg7LqWnXzGUW40,1226
1557
1558
  vellum/workflows/expressions/and_.py,sha256=I7lNqrUM3-m_5hmjjiMhaHhJtKcLj39kEFVWPDOqwfo,916
1558
1559
  vellum/workflows/expressions/begins_with.py,sha256=FnWsQXbENm0ZwkfEP7dR8Qx4_MMrzj6C1yqAV2KaNHw,1123
1559
1560
  vellum/workflows/expressions/between.py,sha256=dVeddT6YA91eOAlE1Utg7C7gnCiYE7WP-dg17yXUeAY,1492
@@ -1578,16 +1579,21 @@ vellum/workflows/expressions/is_not_null.py,sha256=EoHXFgZScKP_BM2a5Z7YFQN6l7RME
1578
1579
  vellum/workflows/expressions/is_not_undefined.py,sha256=9s-RUQBqM17-_nIRvwsHuarLdHVtrxVuwnqBNJEtmh0,735
1579
1580
  vellum/workflows/expressions/is_null.py,sha256=C75ALGlG_sTGcxI46tm9HtgPVfJ7DwTIyKzX8qtEiDU,660
1580
1581
  vellum/workflows/expressions/is_undefined.py,sha256=uUBK3rxYbwoeRq36AGFc7d61hXzTp8UacQAi-1JbaW0,724
1582
+ vellum/workflows/expressions/length.py,sha256=Gq4eBXnLLmuyhKHwERcvBd4qLHLkw_JebpZ778gw08M,1274
1581
1583
  vellum/workflows/expressions/less_than.py,sha256=chY9puJ6jDB2EinjfyGNrSplJ1NJC-BB-GGSSB33bqI,1237
1582
1584
  vellum/workflows/expressions/less_than_or_equal_to.py,sha256=JtTDBa8yFKy3fGaCOA1tb_5s1JkY8FFnH6kpoeXGnT4,1267
1585
+ vellum/workflows/expressions/minus.py,sha256=qTIuOF3knMwwGiA9BNGvMP6vVw3z0g5wr4LziaYMRIE,1232
1583
1586
  vellum/workflows/expressions/not_between.py,sha256=ZtRJeJDSSlOvajL8YoBoh5o_khjIn9xSSeQCnXYbHFE,1506
1584
1587
  vellum/workflows/expressions/not_in.py,sha256=pFvwkFPsn3WJw61ssFgM2U1dqWEeglfz4FVT4xwm5Mc,1144
1585
1588
  vellum/workflows/expressions/or_.py,sha256=s-8YdMSSCDS2yijR38kguwok3iqmDMMgDYKV93b4O4s,914
1586
1589
  vellum/workflows/expressions/parse_json.py,sha256=xsk6j3HF7bU1yF6fwt5P9Ugcyd5D9ZXrdng11FRilUI,1088
1587
1590
  vellum/workflows/expressions/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1588
1591
  vellum/workflows/expressions/tests/test_accessor.py,sha256=g2z0mJjuWwVKeXS0yGoFW-aRmT5n_LrhfuBorSmj9kI,7585
1592
+ vellum/workflows/expressions/tests/test_add.py,sha256=_MjlRvIGAVM5wve2ru5jc_5Ae4x_ywvh4vN0S2yQ-8M,1615
1589
1593
  vellum/workflows/expressions/tests/test_concat.py,sha256=fDHXlmFvCtqPkdZQD9Qs22i6sJq_MJjbUXCnTlSMvA0,1666
1590
1594
  vellum/workflows/expressions/tests/test_expressions.py,sha256=3b6k8xs-CItBBw95NygFLUNoNPKxI4VA1GyWbkMtqyI,11623
1595
+ vellum/workflows/expressions/tests/test_length.py,sha256=pQA1tYSwqxE6euclboY024NXEOs7yaVgwTKkMPYUT08,1035
1596
+ vellum/workflows/expressions/tests/test_minus.py,sha256=pq7dvdRGNhSSn95LGNzRErsYUsFk5SpOKHDcSR5QToc,1632
1591
1597
  vellum/workflows/expressions/tests/test_parse_json.py,sha256=zpB_qE5_EwWQL7ULQUJm0o1PRSfWZdAqZNW6Ah13oJE,1059
1592
1598
  vellum/workflows/graph/__init__.py,sha256=3sHlay5d_-uD7j3QJXiGl0WHFZZ_QScRvgyDhN2GhHY,74
1593
1599
  vellum/workflows/graph/graph.py,sha256=GGR8cGpSuNWPIiTWNWsj6l70upb5nIxAyFcn7VdaJIs,5506
@@ -1598,7 +1604,7 @@ vellum/workflows/inputs/base.py,sha256=w3owT5B3rLBmIj-v-jL2l-HD4yd3hXK9RmHVd557B
1598
1604
  vellum/workflows/inputs/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1599
1605
  vellum/workflows/inputs/tests/test_inputs.py,sha256=lioA8917mFLYq7Ml69UNkqUjcWbbxkxnpIEJ4FBaYBk,2206
1600
1606
  vellum/workflows/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1601
- vellum/workflows/integrations/composio_service.py,sha256=v1rVQXTh1rnupguj8oIM20V7bSKaJiAoJ5yjz2NeKA8,5906
1607
+ vellum/workflows/integrations/composio_service.py,sha256=rSliaZtNiBcDSvDxz9k5i1KkyUIrbxyegu0yU9cDByU,6023
1602
1608
  vellum/workflows/integrations/mcp_service.py,sha256=SaOLg76JBAiBDAMUn04mxVWmf2Btobd1kDjc8B1atng,8712
1603
1609
  vellum/workflows/logging.py,sha256=_a217XogktV4Ncz6xKFz7WfYmZAzkfVRVuC0rWob8ls,437
1604
1610
  vellum/workflows/nodes/__init__.py,sha256=aVdQVv7Y3Ro3JlqXGpxwaU2zrI06plDHD2aumH5WUIs,1157
@@ -1641,7 +1647,7 @@ vellum/workflows/nodes/displayable/bases/api_node/node.py,sha256=iUtdPsbJs1jwo3V
1641
1647
  vellum/workflows/nodes/displayable/bases/api_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1642
1648
  vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py,sha256=Pf51DIyhtUxx-pCu0zJYDB4Z5_IW5mRwkJIoPT53_9I,3894
1643
1649
  vellum/workflows/nodes/displayable/bases/base_prompt_node/__init__.py,sha256=Org3xTvgp1pA0uUXFfnJr29D3HzCey2lEdYF4zbIUgo,70
1644
- vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py,sha256=EJsGaz8Umss6-JWGGYbJp93ZHx3IlZQWAySlHAdNYtY,4466
1650
+ vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py,sha256=ea20icDM1HB942wkH-XtXNSNCBDcjeOiN3vowkHL4fs,4477
1645
1651
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/__init__.py,sha256=Hl35IAoepRpE-j4cALaXVJIYTYOF3qszyVbxTj4kS1s,82
1646
1652
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py,sha256=hy_fy4zImhpnHuPczKtOLhv_uOmvxvJsQA9Zl9DTmSw,12851
1647
1653
  vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1672,7 +1678,7 @@ vellum/workflows/nodes/displayable/guardrail_node/test_node.py,sha256=SAGv6hSFcB
1672
1678
  vellum/workflows/nodes/displayable/guardrail_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1673
1679
  vellum/workflows/nodes/displayable/guardrail_node/tests/test_node.py,sha256=X2pd6TI8miYxIa7rgvs1pHTEreyWcf77EyR0_Jsa700,2055
1674
1680
  vellum/workflows/nodes/displayable/inline_prompt_node/__init__.py,sha256=gSUOoEZLlrx35-tQhSAd3An8WDwBqyiQh-sIebLU9wU,74
1675
- vellum/workflows/nodes/displayable/inline_prompt_node/node.py,sha256=wgZ9bt9IFe5cqWSghfMlD1NgmFhRnuDLRzXzMhJomV0,2912
1681
+ vellum/workflows/nodes/displayable/inline_prompt_node/node.py,sha256=3jY-2UJarVJSz2eupt7c9Mp-Mk56U2meXU0d7c0KVNk,2941
1676
1682
  vellum/workflows/nodes/displayable/inline_prompt_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1677
1683
  vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py,sha256=bBHs90mV5SZ3rJPAL0wx4WWyawUA406LgMPOdvpZC_A,10923
1678
1684
  vellum/workflows/nodes/displayable/merge_node/__init__.py,sha256=J8IC08dSH7P76wKlNuxe1sn7toNGtSQdFirUbtPDEs0,60
@@ -1697,13 +1703,13 @@ vellum/workflows/nodes/displayable/tests/test_search_node_error_handling.py,sha2
1697
1703
  vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py,sha256=VepO5z1277c1y5N6LLIC31nnWD1aak2m5oPFplfJHHs,6935
1698
1704
  vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py,sha256=dc3EEn1sOICpr3GdS8eyeFtExaGwWWcw9eHSdkRhQJU,2584
1699
1705
  vellum/workflows/nodes/displayable/tool_calling_node/__init__.py,sha256=3n0-ysmFKsr40CVxPthc0rfJgqVJeZuUEsCmYudLVRg,117
1700
- vellum/workflows/nodes/displayable/tool_calling_node/node.py,sha256=ftPf7hmPvk_rJeIoxnJGkTLex-kDW1CRuvVDUwUdMxg,7283
1701
- vellum/workflows/nodes/displayable/tool_calling_node/state.py,sha256=oQg_GAtc349nPB5BL_oeDYYD7q1qSDPAqjj8iA8OoAw,215
1706
+ vellum/workflows/nodes/displayable/tool_calling_node/node.py,sha256=RFoMIr4CVy50fjuXAFphwfW1oXeZo7s-JY4Bqvlphn0,7460
1707
+ vellum/workflows/nodes/displayable/tool_calling_node/state.py,sha256=CcBVb_YtwfSSka4ze678k6-qwmzMSfjfVP8_Y95feSo,302
1702
1708
  vellum/workflows/nodes/displayable/tool_calling_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1703
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py,sha256=y7KAqbiJHoya6N5EWv1qgz0htM_Yzz7zjAHVp78IMFo,6919
1704
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py,sha256=TPafJhCAV6oLg5kTttQw0hL56ct3a2Xatvnld6dK8CY,8628
1709
+ vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py,sha256=in1fbEz5x1tx3uKv9YXdvOncsHucNL8Ro6Go7lBuuOQ,8962
1710
+ vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py,sha256=1QwfUOS26pesN6Ckqfm-RVlOSyjuF945vKC20AXedKw,8458
1705
1711
  vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py,sha256=om4FztVQ33jFZK_lbusi6khOM7zgzNCHlUcEb5-r6pU,8361
1706
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py,sha256=fvy0O3YpibWUpw4aLKnk8PdwlRCJC7Z2acjryOTiuxY,19728
1712
+ vellum/workflows/nodes/displayable/tool_calling_node/utils.py,sha256=7B_8FY5xEjra3A3k6kcABnrgobqieCKNCEA6ijKKaW8,21284
1707
1713
  vellum/workflows/nodes/experimental/README.md,sha256=eF6DfIL8t-HbF9-mcofOMymKrraiBHDLKTlnBa51ZiE,284
1708
1714
  vellum/workflows/nodes/experimental/__init__.py,sha256=jCQgvZEknXKfuNhGSOou4XPfrPqZ1_XBj5F0n0fgiWM,106
1709
1715
  vellum/workflows/nodes/experimental/openai_chat_completion_node/__init__.py,sha256=lsyD9laR9p7kx5-BXGH2gUTM242UhKy8SMV0SR6S2iE,90
@@ -1751,11 +1757,11 @@ vellum/workflows/tests/test_undefined.py,sha256=zMCVliCXVNLrlC6hEGyOWDnQADJ2g83y
1751
1757
  vellum/workflows/types/__init__.py,sha256=KxUTMBGzuRCfiMqzzsykOeVvrrkaZmTTo1a7SLu8gRM,68
1752
1758
  vellum/workflows/types/code_execution_node_wrappers.py,sha256=3MNIoFZKzVzNS5qFLVuDwMV17QJw72zo7NRf52yMq5A,3074
1753
1759
  vellum/workflows/types/core.py,sha256=TggDVs2lVya33xvu374EDhMC1b7RRlAAs0zWLaF46BA,1385
1754
- vellum/workflows/types/definition.py,sha256=fzWfsfbXLS4sZvjOQMSDoiuSaFo4Ii2kC8AOiPade9o,4602
1760
+ vellum/workflows/types/definition.py,sha256=ee55MvZs4scFgI7y3ykIcP_5kSTqb4ojNrtGmnvISTw,4437
1755
1761
  vellum/workflows/types/generics.py,sha256=8jptbEx1fnJV0Lhj0MpCJOT6yNiEWeTOYOwrEAb5CRU,1576
1756
1762
  vellum/workflows/types/stack.py,sha256=h7NE0vXR7l9DevFBIzIAk1Zh59K-kECQtDTKOUunwMY,1314
1757
1763
  vellum/workflows/types/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1758
- vellum/workflows/types/tests/test_definition.py,sha256=4Qqlf7GpoG9MrLuMCkcRzEZMgwrr7du4DROcB1xfv0E,5050
1764
+ vellum/workflows/types/tests/test_definition.py,sha256=AM2uaADSWb6QUAP_CwjeSuNayBjoZ_yQWjYefw0mbGI,5045
1759
1765
  vellum/workflows/types/tests/test_utils.py,sha256=UnZog59tR577mVwqZRqqWn2fScoOU1H6up0EzS8zYhw,2536
1760
1766
  vellum/workflows/types/utils.py,sha256=mTctHITBybpt4855x32oCKALBEcMNLn-9cCmfEKgJHQ,6498
1761
1767
  vellum/workflows/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1776,8 +1782,8 @@ vellum/workflows/workflows/event_filters.py,sha256=GSxIgwrX26a1Smfd-6yss2abGCnad
1776
1782
  vellum/workflows/workflows/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1777
1783
  vellum/workflows/workflows/tests/test_base_workflow.py,sha256=ptMntHzVyy8ZuzNgeTuk7hREgKQ5UBdgq8VJFSGaW4Y,20832
1778
1784
  vellum/workflows/workflows/tests/test_context.py,sha256=VJBUcyWVtMa_lE5KxdhgMu0WYNYnUQUDvTF7qm89hJ0,2333
1779
- vellum_ai-1.0.10.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1780
- vellum_ai-1.0.10.dist-info/METADATA,sha256=j-b34bQSbPMhl5YG7dLuwC4_EZYCtRrdhoTxjXYJ3-U,5555
1781
- vellum_ai-1.0.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1782
- vellum_ai-1.0.10.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1783
- vellum_ai-1.0.10.dist-info/RECORD,,
1785
+ vellum_ai-1.0.11.dist-info/LICENSE,sha256=hOypcdt481qGNISA784bnAGWAE6tyIf9gc2E78mYC3E,1574
1786
+ vellum_ai-1.0.11.dist-info/METADATA,sha256=8M0wZ1WdaD1OxRcbUt8a0g6566qeU-QnXRHQ6heBKiU,5555
1787
+ vellum_ai-1.0.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1788
+ vellum_ai-1.0.11.dist-info/entry_points.txt,sha256=HCH4yc_V3J_nDv3qJzZ_nYS8llCHZViCDP1ejgCc5Ak,42
1789
+ vellum_ai-1.0.11.dist-info/RECORD,,
@@ -53,10 +53,6 @@ def test_serialize_workflow():
53
53
  "toolkit": "GITHUB",
54
54
  "action": "GITHUB_CREATE_AN_ISSUE",
55
55
  "description": "Create a new issue in a GitHub repository",
56
- "display_name": "Create GitHub Issue",
57
- "parameters": None,
58
- "version": None,
59
- "tags": None,
60
56
  "user_id": None,
61
57
  }
62
58
 
@@ -6,6 +6,7 @@ from pydantic import BaseModel
6
6
  from vellum.client.types.logical_operator import LogicalOperator
7
7
  from vellum.workflows.descriptors.base import BaseDescriptor
8
8
  from vellum.workflows.expressions.accessor import AccessorExpression
9
+ from vellum.workflows.expressions.add import AddExpression
9
10
  from vellum.workflows.expressions.and_ import AndExpression
10
11
  from vellum.workflows.expressions.begins_with import BeginsWithExpression
11
12
  from vellum.workflows.expressions.between import BetweenExpression
@@ -26,8 +27,10 @@ from vellum.workflows.expressions.is_not_null import IsNotNullExpression
26
27
  from vellum.workflows.expressions.is_not_undefined import IsNotUndefinedExpression
27
28
  from vellum.workflows.expressions.is_null import IsNullExpression
28
29
  from vellum.workflows.expressions.is_undefined import IsUndefinedExpression
30
+ from vellum.workflows.expressions.length import LengthExpression
29
31
  from vellum.workflows.expressions.less_than import LessThanExpression
30
32
  from vellum.workflows.expressions.less_than_or_equal_to import LessThanOrEqualToExpression
33
+ from vellum.workflows.expressions.minus import MinusExpression
31
34
  from vellum.workflows.expressions.not_between import NotBetweenExpression
32
35
  from vellum.workflows.expressions.not_in import NotInExpression
33
36
  from vellum.workflows.expressions.or_ import OrExpression
@@ -96,6 +99,12 @@ def convert_descriptor_to_operator(descriptor: BaseDescriptor) -> LogicalOperato
96
99
  return "coalesce"
97
100
  elif isinstance(descriptor, ParseJsonExpression):
98
101
  return "parseJson"
102
+ elif isinstance(descriptor, LengthExpression):
103
+ return "length"
104
+ elif isinstance(descriptor, AddExpression):
105
+ return "+"
106
+ elif isinstance(descriptor, MinusExpression):
107
+ return "-"
99
108
  else:
100
109
  raise ValueError(f"Unsupported descriptor type: {descriptor}")
101
110
 
@@ -133,6 +142,7 @@ def _serialize_condition(display_context: "WorkflowDisplayContext", condition: B
133
142
  IsNotNilExpression,
134
143
  IsUndefinedExpression,
135
144
  IsNotUndefinedExpression,
145
+ LengthExpression,
136
146
  ParseJsonExpression,
137
147
  ),
138
148
  ):
@@ -157,6 +167,7 @@ def _serialize_condition(display_context: "WorkflowDisplayContext", condition: B
157
167
  elif isinstance(
158
168
  condition,
159
169
  (
170
+ AddExpression,
160
171
  AndExpression,
161
172
  BeginsWithExpression,
162
173
  CoalesceExpression,
@@ -172,6 +183,7 @@ def _serialize_condition(display_context: "WorkflowDisplayContext", condition: B
172
183
  InExpression,
173
184
  LessThanExpression,
174
185
  LessThanOrEqualToExpression,
186
+ MinusExpression,
175
187
  NotInExpression,
176
188
  OrExpression,
177
189
  ),