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.
- vellum/__init__.py +0 -8
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/types/__init__.py +0 -8
- vellum/client/types/organization_read.py +1 -2
- vellum/workflows/events/context.py +111 -0
- vellum/workflows/integrations/__init__.py +0 -0
- vellum/workflows/integrations/composio_service.py +138 -0
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +8 -2
- vellum/workflows/nodes/displayable/bases/api_node/node.py +36 -9
- vellum/workflows/nodes/displayable/bases/api_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +124 -0
- vellum/workflows/nodes/displayable/tool_calling_node/node.py +2 -2
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py +63 -58
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +147 -2
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +61 -41
- vellum/workflows/types/definition.py +4 -2
- vellum/workflows/utils/functions.py +29 -2
- vellum/workflows/utils/tests/test_functions.py +115 -1
- {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/METADATA +1 -3
- {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/RECORD +29 -33
- vellum_cli/push.py +11 -2
- vellum_cli/tests/test_push.py +57 -1
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -0
- vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +16 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +3 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +8 -2
- vellum/client/types/name_enum.py +0 -7
- vellum/client/types/organization_limit_config.py +0 -25
- vellum/client/types/quota.py +0 -22
- vellum/client/types/vembda_service_tier_enum.py +0 -5
- vellum/types/name_enum.py +0 -3
- vellum/types/organization_limit_config.py +0 -3
- vellum/types/quota.py +0 -3
- vellum/types/vembda_service_tier_enum.py +0 -3
- vellum/workflows/nodes/displayable/tool_calling_node/composio_service.py +0 -83
- {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/LICENSE +0 -0
- {vellum_ai-1.0.5.dist-info → vellum_ai-1.0.7.dist-info}/WHEEL +0 -0
- {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.
|
4
|
+
from vellum.workflows.integrations.composio_service import ComposioService, ConnectionInfo
|
5
5
|
|
6
6
|
|
7
7
|
@pytest.fixture
|
8
|
-
def
|
9
|
-
"""Mock
|
10
|
-
with patch("vellum.workflows.
|
11
|
-
yield
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
39
|
-
"""Mock
|
40
|
-
|
41
|
-
|
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
|
46
|
-
"""
|
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
|
69
|
-
|
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
|
-
|
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,
|
90
|
+
def test_get_user_connections_empty_response(self, composio_service, mock_requests):
|
92
91
|
"""Test handling of empty connections response"""
|
93
|
-
# GIVEN the
|
92
|
+
# GIVEN the requests mock returns an empty response
|
94
93
|
mock_response = Mock()
|
95
|
-
mock_response.
|
96
|
-
|
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,
|
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
|
-
|
113
|
-
|
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
|
-
|
120
|
-
|
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 ==
|
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
|
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(
|
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
|
216
|
-
"""
|
199
|
+
def _hydrate_composio_tool_definition(tool_def: ComposioToolDefinition) -> ComposioToolDefinition:
|
200
|
+
"""Hydrate a ComposioToolDefinition with detailed information from the Composio API.
|
217
201
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|