vellum-ai 1.0.5__py3-none-any.whl → 1.0.7__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 (38) hide show
  1. vellum/__init__.py +0 -8
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/client/types/__init__.py +0 -8
  4. vellum/client/types/organization_read.py +1 -2
  5. vellum/workflows/events/context.py +111 -0
  6. vellum/workflows/integrations/__init__.py +0 -0
  7. vellum/workflows/integrations/composio_service.py +138 -0
  8. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +8 -2
  9. vellum/workflows/nodes/displayable/bases/api_node/node.py +36 -9
  10. vellum/workflows/nodes/displayable/bases/api_node/tests/__init__.py +0 -0
  11. vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +124 -0
  12. vellum/workflows/nodes/displayable/tool_calling_node/node.py +2 -2
  13. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py +63 -58
  14. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +147 -2
  15. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +61 -41
  16. vellum/workflows/types/definition.py +4 -2
  17. vellum/workflows/utils/functions.py +29 -2
  18. vellum/workflows/utils/tests/test_functions.py +115 -1
  19. {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/METADATA +1 -3
  20. {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/RECORD +29 -33
  21. vellum_cli/push.py +11 -2
  22. vellum_cli/tests/test_push.py +57 -1
  23. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -0
  24. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +16 -0
  25. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +3 -0
  26. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +8 -2
  27. vellum/client/types/name_enum.py +0 -7
  28. vellum/client/types/organization_limit_config.py +0 -25
  29. vellum/client/types/quota.py +0 -22
  30. vellum/client/types/vembda_service_tier_enum.py +0 -5
  31. vellum/types/name_enum.py +0 -3
  32. vellum/types/organization_limit_config.py +0 -3
  33. vellum/types/quota.py +0 -3
  34. vellum/types/vembda_service_tier_enum.py +0 -3
  35. vellum/workflows/nodes/displayable/tool_calling_node/composio_service.py +0 -83
  36. {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/LICENSE +0 -0
  37. {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/WHEEL +0 -0
  38. {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,124 @@
1
+ import pytest
2
+
3
+ from vellum.client.types.execute_api_response import ExecuteApiResponse
4
+ from vellum.workflows.constants import APIRequestMethod
5
+ from vellum.workflows.errors.types import WorkflowErrorCode
6
+ from vellum.workflows.exceptions import NodeException
7
+ from vellum.workflows.nodes.displayable.bases.api_node.node import BaseAPINode
8
+ from vellum.workflows.types.core import VellumSecret
9
+
10
+
11
+ @pytest.mark.parametrize("method_value", ["GET", "get", APIRequestMethod.GET])
12
+ def test_api_node_with_string_method(method_value, vellum_client):
13
+ class TestAPINode(BaseAPINode):
14
+ method = method_value
15
+ url = "https://example.com"
16
+ headers = {"Authorization": VellumSecret(name="API_KEY")}
17
+
18
+ mock_response = ExecuteApiResponse(
19
+ json_={"status": "success"},
20
+ headers={"content-type": "application/json"},
21
+ status_code=200,
22
+ text='{"status": "success"}',
23
+ )
24
+ vellum_client.execute_api.return_value = mock_response
25
+
26
+ node = TestAPINode()
27
+ result = node.run()
28
+
29
+ assert result.status_code == 200
30
+
31
+ vellum_client.execute_api.assert_called_once()
32
+ call_args = vellum_client.execute_api.call_args
33
+ assert call_args[1]["method"] == "GET"
34
+
35
+
36
+ def test_api_node_with_invalid_method():
37
+ class TestAPINode(BaseAPINode):
38
+ method = "INVALID_METHOD"
39
+ url = "https://example.com"
40
+
41
+ node = TestAPINode()
42
+
43
+ with pytest.raises(NodeException) as exc_info:
44
+ node.run()
45
+
46
+ assert exc_info.value.code == WorkflowErrorCode.INVALID_INPUTS
47
+ assert "Invalid HTTP method 'INVALID_METHOD'" == str(exc_info.value)
48
+
49
+
50
+ def test_api_node_adds_user_agent_header_when_none_provided(requests_mock):
51
+ """
52
+ Tests that the API node adds User-Agent header when no headers are provided.
53
+ """
54
+
55
+ class TestAPINode(BaseAPINode):
56
+ method = APIRequestMethod.GET
57
+ url = "https://example.com/test"
58
+
59
+ response_mock = requests_mock.get(
60
+ "https://example.com/test",
61
+ json={"result": "success"},
62
+ status_code=200,
63
+ )
64
+
65
+ node = TestAPINode()
66
+ result = node.run()
67
+
68
+ assert response_mock.last_request
69
+ assert "vellum-ai" in response_mock.last_request.headers.get("User-Agent", "")
70
+
71
+ assert result.status_code == 200
72
+
73
+
74
+ def test_api_node_adds_user_agent_header_when_headers_provided_without_user_agent(requests_mock):
75
+ """
76
+ Tests that the API node adds User-Agent header when headers are provided but don't include User-Agent.
77
+ """
78
+
79
+ class TestAPINode(BaseAPINode):
80
+ method = APIRequestMethod.POST
81
+ url = "https://example.com/test"
82
+ headers = {"Content-Type": "application/json", "Custom-Header": "value"}
83
+ json = {"test": "data"}
84
+
85
+ response_mock = requests_mock.post(
86
+ "https://example.com/test",
87
+ json={"result": "success"},
88
+ status_code=200,
89
+ )
90
+
91
+ node = TestAPINode()
92
+ result = node.run()
93
+
94
+ assert response_mock.last_request
95
+ assert "vellum-ai" in response_mock.last_request.headers.get("User-Agent", "")
96
+ assert response_mock.last_request.headers.get("Content-Type") == "application/json"
97
+ assert response_mock.last_request.headers.get("Custom-Header") == "value"
98
+
99
+ assert result.status_code == 200
100
+
101
+
102
+ def test_api_node_preserves_custom_user_agent_header(requests_mock):
103
+ """
104
+ Tests that the API node preserves a custom User-Agent header if provided.
105
+ """
106
+
107
+ class TestAPINode(BaseAPINode):
108
+ method = APIRequestMethod.GET
109
+ url = "https://example.com/test"
110
+ headers = {"User-Agent": "Custom-Agent/1.0"}
111
+
112
+ response_mock = requests_mock.get(
113
+ "https://example.com/test",
114
+ json={"result": "success"},
115
+ status_code=200,
116
+ )
117
+
118
+ node = TestAPINode()
119
+ result = node.run()
120
+
121
+ assert response_mock.last_request
122
+ assert response_mock.last_request.headers.get("User-Agent") == "Custom-Agent/1.0"
123
+
124
+ assert result.status_code == 200
@@ -1,4 +1,4 @@
1
- from typing import ClassVar, Iterator, List, Optional, Set
1
+ from typing import Any, ClassVar, Dict, Iterator, List, Optional, Set, Union
2
2
 
3
3
  from vellum import ChatMessage, PromptBlock
4
4
  from vellum.client.types.prompt_parameters import PromptParameters
@@ -36,7 +36,7 @@ class ToolCallingNode(BaseNode):
36
36
  """
37
37
 
38
38
  ml_model: ClassVar[str] = "gpt-4o-mini"
39
- blocks: ClassVar[List[PromptBlock]] = []
39
+ blocks: ClassVar[List[Union[PromptBlock, Dict[str, Any]]]] = []
40
40
  functions: ClassVar[List[Tool]] = []
41
41
  prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
42
42
  parameters: PromptParameters = DEFAULT_PROMPT_PARAMETERS
@@ -1,72 +1,65 @@
1
1
  import pytest
2
2
  from unittest.mock import Mock, patch
3
3
 
4
- from vellum.workflows.nodes.displayable.tool_calling_node.composio_service import ComposioService, ConnectionInfo
4
+ from vellum.workflows.integrations.composio_service import ComposioService, ConnectionInfo
5
5
 
6
6
 
7
7
  @pytest.fixture
8
- def mock_composio_client():
9
- """Mock the Composio client completely"""
10
- with patch("vellum.workflows.nodes.displayable.tool_calling_node.composio_service.ComposioClient") as mock_composio:
11
- yield mock_composio.return_value
8
+ def mock_requests():
9
+ """Mock requests module"""
10
+ with patch("vellum.workflows.integrations.composio_service.requests") as mock_requests:
11
+ yield mock_requests
12
12
 
13
13
 
14
14
  @pytest.fixture
15
15
  def mock_connected_accounts_response():
16
- """Mock response for connected accounts"""
17
- mock_item1 = Mock()
18
- mock_item1.id = "conn-123"
19
- mock_item1.toolkit.slug = "github"
20
- mock_item1.status = "ACTIVE"
21
- mock_item1.created_at = "2023-01-01T00:00:00Z"
22
- mock_item1.updated_at = "2023-01-15T10:30:00Z"
23
-
24
- mock_item2 = Mock()
25
- mock_item2.id = "conn-456"
26
- mock_item2.toolkit.slug = "slack"
27
- mock_item2.status = "ACTIVE"
28
- mock_item2.created_at = "2023-01-01T00:00:00Z"
29
- mock_item2.updated_at = "2023-01-10T08:00:00Z"
30
-
31
- mock_response = Mock()
32
- mock_response.items = [mock_item1, mock_item2]
33
-
34
- return mock_response
16
+ """Mock response for connected accounts API"""
17
+ return {
18
+ "items": [
19
+ {
20
+ "id": "conn-123",
21
+ "toolkit": {"slug": "github"},
22
+ "status": "ACTIVE",
23
+ "created_at": "2023-01-01T00:00:00Z",
24
+ "updated_at": "2023-01-15T10:30:00Z",
25
+ },
26
+ {
27
+ "id": "conn-456",
28
+ "toolkit": {"slug": "slack"},
29
+ "status": "ACTIVE",
30
+ "created_at": "2023-01-01T00:00:00Z",
31
+ "updated_at": "2023-01-10T08:00:00Z",
32
+ },
33
+ ]
34
+ }
35
35
 
36
36
 
37
37
  @pytest.fixture
38
- def mock_composio_core_client():
39
- """Mock the composio-core Composio client"""
40
- with patch("vellum.workflows.nodes.displayable.tool_calling_node.composio_service.Composio") as mock_composio:
41
- yield mock_composio.return_value
38
+ def mock_tool_execution_response():
39
+ """Mock response for tool execution API"""
40
+ return {
41
+ "data": {"items": [], "total": 0},
42
+ "successful": True,
43
+ "error": None,
44
+ }
42
45
 
43
46
 
44
47
  @pytest.fixture
45
- def mock_action():
46
- """Mock the Action class and specific actions"""
47
- with patch("vellum.workflows.nodes.displayable.tool_calling_node.composio_service.Action") as mock_action_class:
48
- # Mock a specific action
49
- mock_hackernews_action = Mock()
50
- mock_action_class.HACKERNEWS_GET_USER = mock_hackernews_action
51
- mock_action_class.GITHUB_GET_USER = Mock()
52
- yield mock_action_class
53
-
54
-
55
- @pytest.fixture
56
- def composio_service(mock_composio_client, mock_composio_core_client):
57
- """Create ComposioService with mocked clients"""
48
+ def composio_service():
49
+ """Create ComposioService with test API key"""
58
50
  return ComposioService(api_key="test-key")
59
51
 
60
52
 
61
53
  class TestComposioAccountService:
62
54
  """Test suite for ComposioAccountService"""
63
55
 
64
- def test_get_user_connections_success(
65
- self, composio_service, mock_composio_client, mock_connected_accounts_response
66
- ):
56
+ def test_get_user_connections_success(self, composio_service, mock_requests, mock_connected_accounts_response):
67
57
  """Test successful retrieval of user connections"""
68
- # GIVEN the Composio client returns a valid response with two connections
69
- mock_composio_client.connected_accounts.list.return_value = mock_connected_accounts_response
58
+ # GIVEN the requests mock returns a valid response with two connections
59
+ mock_response = Mock()
60
+ mock_response.json.return_value = mock_connected_accounts_response
61
+ mock_response.raise_for_status.return_value = None
62
+ mock_requests.get.return_value = mock_response
70
63
 
71
64
  # WHEN we request user connections
72
65
  result = composio_service.get_user_connections()
@@ -86,14 +79,21 @@ class TestComposioAccountService:
86
79
  assert result[1].created_at == "2023-01-01T00:00:00Z"
87
80
  assert result[1].updated_at == "2023-01-10T08:00:00Z"
88
81
 
89
- mock_composio_client.connected_accounts.list.assert_called_once()
82
+ # Verify the correct API endpoint was called
83
+ mock_requests.get.assert_called_once_with(
84
+ "https://backend.composio.dev/api/v3/connected_accounts",
85
+ headers={"x-api-key": "test-key", "Content-Type": "application/json"},
86
+ params={},
87
+ timeout=30,
88
+ )
90
89
 
91
- def test_get_user_connections_empty_response(self, composio_service, mock_composio_client):
90
+ def test_get_user_connections_empty_response(self, composio_service, mock_requests):
92
91
  """Test handling of empty connections response"""
93
- # GIVEN the Composio client returns an empty response
92
+ # GIVEN the requests mock returns an empty response
94
93
  mock_response = Mock()
95
- mock_response.items = []
96
- mock_composio_client.connected_accounts.list.return_value = mock_response
94
+ mock_response.json.return_value = {"items": []}
95
+ mock_response.raise_for_status.return_value = None
96
+ mock_requests.get.return_value = mock_response
97
97
 
98
98
  # WHEN we request user connections
99
99
  result = composio_service.get_user_connections()
@@ -105,18 +105,23 @@ class TestComposioAccountService:
105
105
  class TestComposioCoreService:
106
106
  """Test suite for ComposioCoreService"""
107
107
 
108
- def test_execute_tool_success(self, composio_service, mock_composio_core_client, mock_action):
108
+ def test_execute_tool_success(self, composio_service, mock_requests, mock_tool_execution_response):
109
109
  """Test executing a tool with complex argument structure"""
110
110
  # GIVEN complex arguments and a mock response
111
111
  complex_args = {"filters": {"status": "active"}, "limit": 10, "sort": "created_at"}
112
- expected_result = {"items": [], "total": 0}
113
- mock_composio_core_client.actions.execute.return_value = expected_result
112
+ mock_response = Mock()
113
+ mock_response.json.return_value = mock_tool_execution_response
114
+ mock_response.raise_for_status.return_value = None
115
+ mock_requests.post.return_value = mock_response
114
116
 
115
117
  # WHEN we execute a tool with complex arguments
116
118
  result = composio_service.execute_tool("HACKERNEWS_GET_USER", complex_args)
117
119
 
118
- # THEN the arguments are passed through correctly
119
- mock_composio_core_client.actions.execute.assert_called_once_with(
120
- mock_action.HACKERNEWS_GET_USER, params=complex_args
120
+ # THEN the arguments are passed through correctly and we get the expected result
121
+ mock_requests.post.assert_called_once_with(
122
+ "https://backend.composio.dev/api/v3/tools/execute/HACKERNEWS_GET_USER",
123
+ headers={"x-api-key": "test-key", "Content-Type": "application/json"},
124
+ json={"arguments": complex_args},
125
+ timeout=30,
121
126
  )
122
- assert result == expected_result
127
+ assert result == {"items": [], "total": 0}
@@ -1,9 +1,18 @@
1
1
  import pytest
2
-
2
+ from uuid import uuid4
3
+
4
+ from vellum.client.types.chat_message_prompt_block import ChatMessagePromptBlock
5
+ from vellum.client.types.fulfilled_execute_prompt_event import FulfilledExecutePromptEvent
6
+ from vellum.client.types.initiated_execute_prompt_event import InitiatedExecutePromptEvent
7
+ from vellum.client.types.plain_text_prompt_block import PlainTextPromptBlock
8
+ from vellum.client.types.rich_text_prompt_block import RichTextPromptBlock
9
+ from vellum.client.types.string_vellum_value import StringVellumValue
10
+ from vellum.client.types.variable_prompt_block import VariablePromptBlock
11
+ from vellum.prompts.constants import DEFAULT_PROMPT_PARAMETERS
3
12
  from vellum.workflows import BaseWorkflow
4
13
  from vellum.workflows.inputs.base import BaseInputs
5
14
  from vellum.workflows.nodes.bases import BaseNode
6
- from vellum.workflows.nodes.displayable.tool_calling_node.utils import get_function_name
15
+ from vellum.workflows.nodes.displayable.tool_calling_node.utils import create_tool_router_node, get_function_name
7
16
  from vellum.workflows.outputs.base import BaseOutputs
8
17
  from vellum.workflows.state.base import BaseState
9
18
  from vellum.workflows.types.definition import ComposioToolDefinition, DeploymentDefinition
@@ -76,3 +85,139 @@ def test_get_function_name_composio_tool_definition_various_toolkits(
76
85
  result = get_function_name(composio_tool)
77
86
 
78
87
  assert result == expected_result
88
+
89
+
90
+ def test_create_tool_router_node_max_prompt_iterations(vellum_adhoc_prompt_client):
91
+ # GIVEN a tool router node with max_prompt_iterations set to None
92
+ tool_router_node = create_tool_router_node(
93
+ ml_model="gpt-4o-mini",
94
+ blocks=[],
95
+ functions=[],
96
+ prompt_inputs=None,
97
+ parameters=DEFAULT_PROMPT_PARAMETERS,
98
+ max_prompt_iterations=None,
99
+ )
100
+
101
+ def generate_prompt_events(*args, **kwargs):
102
+ execution_id = str(uuid4())
103
+ events = [
104
+ InitiatedExecutePromptEvent(execution_id=execution_id),
105
+ FulfilledExecutePromptEvent(
106
+ execution_id=execution_id,
107
+ outputs=[StringVellumValue(value="test output")],
108
+ ),
109
+ ]
110
+ yield from events
111
+
112
+ vellum_adhoc_prompt_client.adhoc_execute_prompt_stream.side_effect = generate_prompt_events
113
+
114
+ # WHEN we run the tool router node
115
+ node_instance = tool_router_node()
116
+ outputs = list(node_instance.run())
117
+ assert outputs[0].name == "results"
118
+ assert outputs[0].value == [StringVellumValue(type="STRING", value="test output")]
119
+ assert outputs[1].name == "text"
120
+ assert outputs[1].value == "test output"
121
+
122
+
123
+ def test_create_tool_router_node_chat_history_block_dict(vellum_adhoc_prompt_client):
124
+ # GIVEN a list of blocks with a chat history block
125
+ blocks = [
126
+ {
127
+ "block_type": "CHAT_MESSAGE",
128
+ "chat_role": "SYSTEM",
129
+ "blocks": [
130
+ {
131
+ "block_type": "RICH_TEXT",
132
+ "blocks": [{"block_type": "PLAIN_TEXT", "cache_config": None, "text": "first message"}],
133
+ }
134
+ ],
135
+ },
136
+ {
137
+ "block_type": "CHAT_MESSAGE",
138
+ "chat_role": "USER",
139
+ "blocks": [
140
+ {
141
+ "block_type": "RICH_TEXT",
142
+ "blocks": [
143
+ {"block_type": "PLAIN_TEXT", "text": "second message"},
144
+ {"block_type": "PLAIN_TEXT", "text": "third message"},
145
+ ],
146
+ }
147
+ ],
148
+ },
149
+ ]
150
+
151
+ tool_router_node = create_tool_router_node(
152
+ ml_model="gpt-4o-mini",
153
+ blocks=blocks, # type: ignore
154
+ functions=[],
155
+ prompt_inputs=None,
156
+ parameters=DEFAULT_PROMPT_PARAMETERS,
157
+ )
158
+
159
+ def generate_prompt_events(*args, **kwargs):
160
+ execution_id = str(uuid4())
161
+ events = [
162
+ InitiatedExecutePromptEvent(execution_id=execution_id),
163
+ FulfilledExecutePromptEvent(
164
+ execution_id=execution_id,
165
+ outputs=[StringVellumValue(value="test output")],
166
+ ),
167
+ ]
168
+ yield from events
169
+
170
+ vellum_adhoc_prompt_client.adhoc_execute_prompt_stream.side_effect = generate_prompt_events
171
+
172
+ # WHEN we run the tool router node
173
+ node_instance = tool_router_node()
174
+ list(node_instance.run())
175
+
176
+ # THEN the API was called with compiled blocks
177
+ blocks = vellum_adhoc_prompt_client.adhoc_execute_prompt_stream.call_args[1]["blocks"]
178
+ assert blocks == [
179
+ ChatMessagePromptBlock(
180
+ block_type="CHAT_MESSAGE",
181
+ state=None,
182
+ cache_config=None,
183
+ chat_role="SYSTEM",
184
+ chat_source=None,
185
+ chat_message_unterminated=None,
186
+ blocks=[
187
+ RichTextPromptBlock(
188
+ block_type="RICH_TEXT",
189
+ state=None,
190
+ cache_config=None,
191
+ blocks=[
192
+ PlainTextPromptBlock(
193
+ block_type="PLAIN_TEXT", state=None, cache_config=None, text="first message"
194
+ )
195
+ ],
196
+ )
197
+ ],
198
+ ),
199
+ ChatMessagePromptBlock(
200
+ block_type="CHAT_MESSAGE",
201
+ state=None,
202
+ cache_config=None,
203
+ chat_role="USER",
204
+ chat_source=None,
205
+ chat_message_unterminated=None,
206
+ blocks=[
207
+ RichTextPromptBlock(
208
+ block_type="RICH_TEXT",
209
+ state=None,
210
+ cache_config=None,
211
+ blocks=[
212
+ PlainTextPromptBlock(
213
+ block_type="PLAIN_TEXT", state=None, cache_config=None, text="second message"
214
+ ),
215
+ PlainTextPromptBlock(
216
+ block_type="PLAIN_TEXT", state=None, cache_config=None, text="third message"
217
+ ),
218
+ ],
219
+ )
220
+ ],
221
+ ),
222
+ VariablePromptBlock(block_type="VARIABLE", state=None, cache_config=None, input_variable="chat_history"),
223
+ ]
@@ -1,12 +1,13 @@
1
1
  import json
2
- import os
3
- from typing import Any, Callable, Iterator, List, Optional, Type, cast
2
+ import logging
3
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Type, Union, cast
4
4
 
5
5
  from pydash import snake_case
6
6
 
7
7
  from vellum import ChatMessage, PromptBlock
8
8
  from vellum.client.types.function_call_chat_message_content import FunctionCallChatMessageContent
9
9
  from vellum.client.types.function_call_chat_message_content_value import FunctionCallChatMessageContentValue
10
+ from vellum.client.types.function_definition import FunctionDefinition
10
11
  from vellum.client.types.prompt_output import PromptOutput
11
12
  from vellum.client.types.prompt_parameters import PromptParameters
12
13
  from vellum.client.types.string_chat_message_content import StringChatMessageContent
@@ -15,11 +16,11 @@ from vellum.workflows.errors.types import WorkflowErrorCode
15
16
  from vellum.workflows.exceptions import NodeException
16
17
  from vellum.workflows.expressions.concat import ConcatExpression
17
18
  from vellum.workflows.inputs import BaseInputs
19
+ from vellum.workflows.integrations.composio_service import ComposioService
18
20
  from vellum.workflows.nodes.bases import BaseNode
19
21
  from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubworkflowNode
20
22
  from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
21
23
  from vellum.workflows.nodes.displayable.subworkflow_deployment_node.node import SubworkflowDeploymentNode
22
- from vellum.workflows.nodes.displayable.tool_calling_node.composio_service import ComposioService
23
24
  from vellum.workflows.nodes.displayable.tool_calling_node.state import ToolCallingState
24
25
  from vellum.workflows.outputs.base import BaseOutput
25
26
  from vellum.workflows.ports.port import Port
@@ -33,6 +34,9 @@ from vellum.workflows.types.generics import is_workflow_class
33
34
  CHAT_HISTORY_VARIABLE = "chat_history"
34
35
 
35
36
 
37
+ logger = logging.getLogger(__name__)
38
+
39
+
36
40
  class FunctionCallNodeMixin:
37
41
  """Mixin providing common functionality for nodes that handle function calls."""
38
42
 
@@ -63,7 +67,7 @@ class ToolRouterNode(InlinePromptNode[ToolCallingState]):
63
67
  merge_behavior = MergeBehavior.AWAIT_ATTRIBUTES
64
68
 
65
69
  def run(self) -> Iterator[BaseOutput]:
66
- if self.state.prompt_iterations >= self.max_prompt_iterations:
70
+ if self.max_prompt_iterations is not None and self.state.prompt_iterations >= self.max_prompt_iterations:
67
71
  max_iterations_message = f"Maximum number of prompt iterations `{self.max_prompt_iterations}` reached."
68
72
  raise NodeException(message=max_iterations_message, code=WorkflowErrorCode.NODE_EXECUTION)
69
73
 
@@ -176,29 +180,9 @@ class ComposioNode(BaseNode[ToolCallingState], FunctionCallNodeMixin):
176
180
  # Extract arguments from function call
177
181
  arguments = self._extract_function_arguments()
178
182
 
179
- # HACK: Use first Composio API key found in environment variables
180
- composio_api_key = None
181
- common_env_var_names = ["COMPOSIO_API_KEY", "COMPOSIO_KEY"]
182
-
183
- for env_var_name in common_env_var_names:
184
- value = os.environ.get(env_var_name)
185
- if value:
186
- composio_api_key = value
187
- break
188
-
189
- if not composio_api_key:
190
- raise NodeException(
191
- message=(
192
- "No Composio API key found in environment variables. "
193
- "Please ensure one of these environment variables is set: "
194
- )
195
- + ", ".join(common_env_var_names),
196
- code=WorkflowErrorCode.NODE_EXECUTION,
197
- )
198
-
199
183
  try:
200
184
  # Execute using ComposioService
201
- composio_service = ComposioService(api_key=composio_api_key)
185
+ composio_service = ComposioService()
202
186
  result = composio_service.execute_tool(tool_name=self.composio_tool.action, arguments=arguments)
203
187
  except Exception as e:
204
188
  raise NodeException(
@@ -212,26 +196,46 @@ class ComposioNode(BaseNode[ToolCallingState], FunctionCallNodeMixin):
212
196
  yield from []
213
197
 
214
198
 
215
- def create_composio_wrapper_function(tool_def: ComposioToolDefinition):
216
- """Create a real Python function that wraps the Composio tool for prompt layer compatibility."""
199
+ def _hydrate_composio_tool_definition(tool_def: ComposioToolDefinition) -> ComposioToolDefinition:
200
+ """Hydrate a ComposioToolDefinition with detailed information from the Composio API.
217
201
 
218
- def wrapper_function(**kwargs):
219
- # This should never be called due to routing, but satisfies introspection
220
- raise RuntimeError(
221
- f"ComposioToolDefinition wrapper for '{tool_def.action}' should not be called directly. "
222
- f"Execution should go through ComposioNode. This suggests a routing issue."
202
+ Args:
203
+ tool_def: The basic ComposioToolDefinition to enhance
204
+
205
+ Returns:
206
+ ComposioToolDefinition with detailed parameters and description
207
+ """
208
+ try:
209
+ composio_service = ComposioService()
210
+ tool_details = composio_service.get_tool_by_slug(tool_def.action)
211
+
212
+ # Extract toolkit information from API response
213
+ toolkit_info = tool_details.get("toolkit", {})
214
+ toolkit_slug = (
215
+ toolkit_info.get("slug", tool_def.toolkit) if isinstance(toolkit_info, dict) else tool_def.toolkit
223
216
  )
224
217
 
225
- # Set proper function attributes for prompt layer introspection
226
- wrapper_function.__name__ = tool_def.name
227
- wrapper_function.__doc__ = tool_def.description
218
+ # Create a version of the tool definition with proper field extraction
219
+ return ComposioToolDefinition(
220
+ type=tool_def.type,
221
+ toolkit=toolkit_slug.upper() if toolkit_slug else tool_def.toolkit,
222
+ action=tool_details.get("slug", tool_def.action),
223
+ description=tool_details.get("description", tool_def.description),
224
+ display_name=tool_details.get("name", tool_def.display_name),
225
+ parameters=tool_details.get("input_parameters", tool_def.parameters),
226
+ version=tool_details.get("version", tool_def.version),
227
+ tags=tool_details.get("tags", tool_def.tags),
228
+ )
228
229
 
229
- return wrapper_function
230
+ except Exception as e:
231
+ # If hydration fails (including no API key), log and return original
232
+ logger.warning(f"Failed to enhance Composio tool '{tool_def.action}': {e}")
233
+ return tool_def
230
234
 
231
235
 
232
236
  def create_tool_router_node(
233
237
  ml_model: str,
234
- blocks: List[PromptBlock],
238
+ blocks: List[Union[PromptBlock, Dict[str, Any]]],
235
239
  functions: List[Tool],
236
240
  prompt_inputs: Optional[EntityInputsInterface],
237
241
  parameters: PromptParameters,
@@ -240,12 +244,19 @@ def create_tool_router_node(
240
244
  if functions and len(functions) > 0:
241
245
  # Create dynamic ports and convert functions in a single loop
242
246
  Ports = type("Ports", (), {})
243
- prompt_functions = []
247
+ prompt_functions: List[Union[Tool, FunctionDefinition]] = []
244
248
 
245
249
  for function in functions:
246
- # Convert ComposioToolDefinition to wrapper function for prompt layer
247
250
  if isinstance(function, ComposioToolDefinition):
248
- prompt_functions.append(create_composio_wrapper_function(function))
251
+ # Get Composio tool details and hydrate the function definition
252
+ enhanced_function = _hydrate_composio_tool_definition(function)
253
+ prompt_functions.append(
254
+ FunctionDefinition(
255
+ name=enhanced_function.name,
256
+ description=enhanced_function.description,
257
+ parameters=enhanced_function.parameters,
258
+ )
259
+ )
249
260
  else:
250
261
  prompt_functions.append(function)
251
262
 
@@ -276,7 +287,16 @@ def create_tool_router_node(
276
287
 
277
288
  # Add a chat history block to blocks only if one doesn't already exist
278
289
  has_chat_history_block = any(
279
- block.block_type == "VARIABLE" and block.input_variable == CHAT_HISTORY_VARIABLE for block in blocks
290
+ (
291
+ (block["block_type"] if isinstance(block, dict) else block.block_type) == "VARIABLE"
292
+ and (
293
+ block["input_variable"]
294
+ if isinstance(block, dict)
295
+ else block.input_variable if isinstance(block, VariablePromptBlock) else None
296
+ )
297
+ == CHAT_HISTORY_VARIABLE
298
+ )
299
+ for block in blocks
280
300
  )
281
301
 
282
302
  if not has_chat_history_block:
@@ -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, Literal, Optional, Union
5
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
6
6
 
7
7
  from pydantic import BeforeValidator
8
8
 
@@ -109,8 +109,10 @@ class ComposioToolDefinition(UniversalBaseModel):
109
109
  action: str # Specific action like "GITHUB_CREATE_AN_ISSUE"
110
110
  description: str
111
111
 
112
- # Optional cached metadata
113
112
  display_name: Optional[str] = None
113
+ parameters: Optional[Dict[str, Any]] = None
114
+ version: Optional[str] = None
115
+ tags: Optional[List[str]] = None
114
116
 
115
117
  @property
116
118
  def name(self) -> str: