vellum-ai 1.0.9__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 (34) 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/emitters/__init__.py +2 -0
  5. vellum/workflows/emitters/base.py +17 -0
  6. vellum/workflows/emitters/vellum_emitter.py +138 -0
  7. vellum/workflows/expressions/accessor.py +23 -15
  8. vellum/workflows/expressions/add.py +41 -0
  9. vellum/workflows/expressions/length.py +35 -0
  10. vellum/workflows/expressions/minus.py +41 -0
  11. vellum/workflows/expressions/tests/test_add.py +72 -0
  12. vellum/workflows/expressions/tests/test_length.py +38 -0
  13. vellum/workflows/expressions/tests/test_minus.py +72 -0
  14. vellum/workflows/integrations/composio_service.py +10 -2
  15. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +1 -1
  16. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +2 -2
  17. vellum/workflows/nodes/displayable/tool_calling_node/node.py +24 -20
  18. vellum/workflows/nodes/displayable/tool_calling_node/state.py +2 -0
  19. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py +92 -0
  20. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +25 -10
  21. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +7 -5
  22. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +141 -86
  23. vellum/workflows/types/core.py +3 -5
  24. vellum/workflows/types/definition.py +2 -6
  25. vellum/workflows/types/tests/test_definition.py +5 -2
  26. {vellum_ai-1.0.9.dist-info → vellum_ai-1.0.11.dist-info}/METADATA +1 -1
  27. {vellum_ai-1.0.9.dist-info → vellum_ai-1.0.11.dist-info}/RECORD +34 -27
  28. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +1 -4
  29. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +0 -5
  30. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +0 -5
  31. vellum_ee/workflows/display/utils/expressions.py +12 -0
  32. {vellum_ai-1.0.9.dist-info → vellum_ai-1.0.11.dist-info}/LICENSE +0 -0
  33. {vellum_ai-1.0.9.dist-info → vellum_ai-1.0.11.dist-info}/WHEEL +0 -0
  34. {vellum_ai-1.0.9.dist-info → vellum_ai-1.0.11.dist-info}/entry_points.txt +0 -0
@@ -135,16 +135,24 @@ class ComposioService:
135
135
  else:
136
136
  raise NodeException(f"Failed to retrieve tool details for '{tool_slug}': {error_message}")
137
137
 
138
- def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
138
+ def execute_tool(self, tool_name: str, arguments: Dict[str, Any], user_id: Optional[str] = None) -> Any:
139
139
  """Execute a tool using direct API request
140
140
 
141
141
  Args:
142
142
  tool_name: The name of the tool to execute (e.g., "HACKERNEWS_GET_USER")
143
143
  arguments: Dictionary of arguments to pass to the tool
144
+ user_id: Optional user ID to identify which user's Composio connection to use
144
145
 
145
146
  Returns:
146
147
  The result of the tool execution
147
148
  """
148
149
  endpoint = f"/tools/execute/{tool_name}"
149
- response = self._make_request(endpoint, method="POST", json_data={"arguments": arguments})
150
+ json_data: Dict[str, Any] = {"arguments": arguments}
151
+ if user_id is not None:
152
+ json_data["user_id"] = user_id
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
+
150
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
 
@@ -1,4 +1,4 @@
1
- from typing import Any, ClassVar, Dict, Iterator, List, Optional, Set, Union
1
+ from typing import Any, ClassVar, Dict, Generic, Iterator, List, Optional, Set, Union
2
2
 
3
3
  from vellum import ChatMessage, PromptBlock
4
4
  from vellum.client.types.prompt_parameters import PromptParameters
@@ -12,19 +12,23 @@ 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,
17
+ create_mcp_tool_node,
16
18
  create_tool_router_node,
17
19
  get_function_name,
20
+ get_mcp_tool_name,
18
21
  hydrate_mcp_tool_definitions,
19
22
  )
20
23
  from vellum.workflows.outputs.base import BaseOutput, BaseOutputs
21
24
  from vellum.workflows.state.context import WorkflowContext
22
- from vellum.workflows.types.core import EntityInputsInterface, Tool, ToolSource
25
+ from vellum.workflows.types.core import EntityInputsInterface, Tool
23
26
  from vellum.workflows.types.definition import MCPServer
27
+ from vellum.workflows.types.generics import StateType
24
28
  from vellum.workflows.workflows.event_filters import all_workflow_event_filter
25
29
 
26
30
 
27
- class ToolCallingNode(BaseNode):
31
+ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
28
32
  """
29
33
  A Node that dynamically invokes the provided functions to the underlying Prompt
30
34
 
@@ -32,7 +36,6 @@ class ToolCallingNode(BaseNode):
32
36
  ml_model: str - The model to use for tool calling (e.g., "gpt-4o-mini")
33
37
  blocks: List[PromptBlock] - The prompt blocks to use (same format as InlinePromptNode)
34
38
  functions: List[Tool] - The functions that can be called
35
- tool_sources: List[ToolSource] - The tool sources that can be called
36
39
  prompt_inputs: Optional[EntityInputsInterface] - Mapping of input variable names to values
37
40
  parameters: PromptParameters - The parameters for the Prompt
38
41
  max_prompt_iterations: Optional[int] - Maximum number of prompt iterations before stopping
@@ -41,7 +44,6 @@ class ToolCallingNode(BaseNode):
41
44
  ml_model: ClassVar[str] = "gpt-4o-mini"
42
45
  blocks: ClassVar[List[Union[PromptBlock, Dict[str, Any]]]] = []
43
46
  functions: ClassVar[List[Tool]] = []
44
- tool_sources: ClassVar[List[ToolSource]] = []
45
47
  prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
46
48
  parameters: PromptParameters = DEFAULT_PROMPT_PARAMETERS
47
49
  max_prompt_iterations: ClassVar[Optional[int]] = 5
@@ -138,7 +140,6 @@ class ToolCallingNode(BaseNode):
138
140
  ml_model=self.ml_model,
139
141
  blocks=self.blocks,
140
142
  functions=self.functions,
141
- tool_sources=self.tool_sources,
142
143
  prompt_inputs=self.prompt_inputs,
143
144
  parameters=self.parameters,
144
145
  max_prompt_iterations=self.max_prompt_iterations,
@@ -146,23 +147,22 @@ class ToolCallingNode(BaseNode):
146
147
 
147
148
  self._function_nodes = {}
148
149
  for function in self.functions:
149
- function_name = get_function_name(function)
150
-
151
- self._function_nodes[function_name] = create_function_node(
152
- function=function,
153
- tool_router_node=self.tool_router_node,
154
- )
155
-
156
- for tool_source in self.tool_sources:
157
- if isinstance(tool_source, MCPServer):
158
- tool_definitions = hydrate_mcp_tool_definitions(tool_source)
150
+ if isinstance(function, MCPServer):
151
+ tool_definitions = hydrate_mcp_tool_definitions(function)
159
152
  for tool_definition in tool_definitions:
160
- function_name = get_function_name(tool_definition)
153
+ function_name = get_mcp_tool_name(tool_definition)
161
154
 
162
- self._function_nodes[function_name] = create_function_node(
163
- function=tool_definition,
155
+ self._function_nodes[function_name] = create_mcp_tool_node(
156
+ tool_def=tool_definition,
164
157
  tool_router_node=self.tool_router_node,
165
158
  )
159
+ else:
160
+ function_name = get_function_name(function)
161
+
162
+ self._function_nodes[function_name] = create_function_node(
163
+ function=function,
164
+ tool_router_node=self.tool_router_node,
165
+ )
166
166
 
167
167
  graph_set = set()
168
168
 
@@ -172,7 +172,11 @@ class ToolCallingNode(BaseNode):
172
172
  edge_graph = router_port >> FunctionNodeClass >> self.tool_router_node
173
173
  graph_set.add(edge_graph)
174
174
 
175
- 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
+ }
176
180
  graph_set.add(default_port)
177
181
 
178
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"""
@@ -125,3 +140,80 @@ class TestComposioCoreService:
125
140
  timeout=30,
126
141
  )
127
142
  assert result == {"items": [], "total": 0}
143
+
144
+ def test_execute_tool_with_user_id(self, composio_service, mock_requests, mock_tool_execution_response):
145
+ """Test executing a tool with user_id parameter"""
146
+ # GIVEN a user_id and tool arguments
147
+ user_id = "test_user_123"
148
+ tool_args = {"param1": "value1"}
149
+ mock_response = Mock()
150
+ mock_response.json.return_value = mock_tool_execution_response
151
+ mock_response.raise_for_status.return_value = None
152
+ mock_requests.post.return_value = mock_response
153
+
154
+ # WHEN we execute a tool with user_id
155
+ result = composio_service.execute_tool("TEST_TOOL", tool_args, user_id=user_id)
156
+
157
+ # THEN the user_id should be included in the request payload
158
+ mock_requests.post.assert_called_once_with(
159
+ "https://backend.composio.dev/api/v3/tools/execute/TEST_TOOL",
160
+ headers={"x-api-key": "test-key", "Content-Type": "application/json"},
161
+ json={"arguments": tool_args, "user_id": user_id},
162
+ timeout=30,
163
+ )
164
+ assert result == {"items": [], "total": 0}
165
+
166
+ def test_execute_tool_without_user_id(self, composio_service, mock_requests, mock_tool_execution_response):
167
+ """Test executing a tool without user_id parameter maintains backward compatibility"""
168
+ # GIVEN tool arguments without user_id
169
+ tool_args = {"param1": "value1"}
170
+ mock_response = Mock()
171
+ mock_response.json.return_value = mock_tool_execution_response
172
+ mock_response.raise_for_status.return_value = None
173
+ mock_requests.post.return_value = mock_response
174
+
175
+ # WHEN we execute a tool without user_id
176
+ result = composio_service.execute_tool("TEST_TOOL", tool_args)
177
+
178
+ # THEN the user_id should NOT be included in the request payload
179
+ mock_requests.post.assert_called_once_with(
180
+ "https://backend.composio.dev/api/v3/tools/execute/TEST_TOOL",
181
+ headers={"x-api-key": "test-key", "Content-Type": "application/json"},
182
+ json={"arguments": tool_args},
183
+ timeout=30,
184
+ )
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
@@ -39,13 +40,12 @@ def test_port_condition_match_function_name():
39
40
  ml_model="test-model",
40
41
  blocks=[],
41
42
  functions=[first_function, second_function],
42
- tool_sources=[],
43
43
  prompt_inputs=None,
44
44
  parameters=DEFAULT_PROMPT_PARAMETERS,
45
45
  )
46
46
 
47
47
  # AND a state with a function call to the first function
48
- state = BaseState(
48
+ state = ToolCallingState(
49
49
  meta=StateMeta(
50
50
  node_outputs={
51
51
  router_node.Outputs.results: [
@@ -98,7 +98,6 @@ def test_tool_calling_node_inline_workflow_context():
98
98
  ml_model="test-model",
99
99
  blocks=[],
100
100
  functions=[MyWorkflow],
101
- tool_sources=[],
102
101
  prompt_inputs=None,
103
102
  parameters=DEFAULT_PROMPT_PARAMETERS,
104
103
  )
@@ -118,12 +117,6 @@ def test_tool_calling_node_inline_workflow_context():
118
117
  )
119
118
  function_node._context = parent_context
120
119
 
121
- # Create a state with chat_history for the function node
122
- class TestState(BaseState):
123
- chat_history: List[ChatMessage] = []
124
-
125
- function_node.state = TestState(meta=StateMeta(node_outputs={tool_router_node.Outputs.text: '{"arguments": {}}'}))
126
-
127
120
  # WHEN the function node runs
128
121
  outputs = list(function_node.run())
129
122
 
@@ -213,3 +206,25 @@ def test_tool_calling_node_with_user_provided_chat_history_block(vellum_adhoc_pr
213
206
  ]
214
207
  assert len(chat_history_inputs) == 1
215
208
  assert chat_history_inputs[0].value == [ChatMessage(role="USER", text="Hello from user")]
209
+
210
+
211
+ def test_tool_calling_node_with_generic_type_parameter():
212
+ # GIVEN a custom state class
213
+ class State(BaseState):
214
+ pass
215
+
216
+ # AND a ToolCallingNode that uses the generic type parameter
217
+ class TestToolCallingNode(ToolCallingNode[State]):
218
+ ml_model = "gpt-4o-mini"
219
+ blocks = []
220
+ functions = [first_function]
221
+ max_prompt_iterations = 1
222
+
223
+ # WHEN we create an instance of the node
224
+ state = State()
225
+ node = TestToolCallingNode(state=state)
226
+
227
+ # THEN the node should be created successfully
228
+ assert node is not None
229
+ assert isinstance(node, TestToolCallingNode)
230
+ assert node.state == state
@@ -12,7 +12,11 @@ from vellum.prompts.constants import DEFAULT_PROMPT_PARAMETERS
12
12
  from vellum.workflows import BaseWorkflow
13
13
  from vellum.workflows.inputs.base import BaseInputs
14
14
  from vellum.workflows.nodes.bases import BaseNode
15
- from vellum.workflows.nodes.displayable.tool_calling_node.utils import create_tool_router_node, get_function_name
15
+ from vellum.workflows.nodes.displayable.tool_calling_node.utils import (
16
+ create_tool_router_node,
17
+ get_function_name,
18
+ get_mcp_tool_name,
19
+ )
16
20
  from vellum.workflows.outputs.base import BaseOutputs
17
21
  from vellum.workflows.state.base import BaseState
18
22
  from vellum.workflows.types.definition import ComposioToolDefinition, DeploymentDefinition, MCPServer, MCPToolDefinition
@@ -77,7 +81,7 @@ def test_get_function_name_mcp_tool_definition():
77
81
  parameters={"repository_name": "string", "description": "string"},
78
82
  )
79
83
 
80
- result = get_function_name(mcp_tool)
84
+ result = get_mcp_tool_name(mcp_tool)
81
85
 
82
86
  assert result == "github__create_repository"
83
87
 
@@ -93,7 +97,7 @@ def test_get_function_name_composio_tool_definition_various_toolkits(
93
97
  toolkit: str, action: str, description: str, expected_result: str
94
98
  ):
95
99
  """Test ComposioToolDefinition function name generation with various toolkits."""
96
- composio_tool = ComposioToolDefinition(toolkit=toolkit, action=action, description=description)
100
+ composio_tool = ComposioToolDefinition(toolkit=toolkit, action=action, description=description, user_id=None)
97
101
 
98
102
  result = get_function_name(composio_tool)
99
103
 
@@ -106,7 +110,6 @@ def test_create_tool_router_node_max_prompt_iterations(vellum_adhoc_prompt_clien
106
110
  ml_model="gpt-4o-mini",
107
111
  blocks=[],
108
112
  functions=[],
109
- tool_sources=[],
110
113
  prompt_inputs=None,
111
114
  parameters=DEFAULT_PROMPT_PARAMETERS,
112
115
  max_prompt_iterations=None,
@@ -166,7 +169,6 @@ def test_create_tool_router_node_chat_history_block_dict(vellum_adhoc_prompt_cli
166
169
  ml_model="gpt-4o-mini",
167
170
  blocks=blocks, # type: ignore
168
171
  functions=[],
169
- tool_sources=[],
170
172
  prompt_inputs=None,
171
173
  parameters=DEFAULT_PROMPT_PARAMETERS,
172
174
  )