vellum-ai 1.0.4__py3-none-any.whl → 1.0.6__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 -6
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/types/__init__.py +0 -6
- 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/bases/api_node/node.py +27 -9
- vellum/workflows/nodes/displayable/bases/api_node/tests/__init__.py +0 -0
- vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +47 -0
- 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 +21 -1
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +124 -59
- vellum/workflows/types/definition.py +4 -2
- vellum/workflows/utils/functions.py +13 -1
- vellum/workflows/utils/tests/test_functions.py +32 -1
- {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/METADATA +1 -3
- {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/RECORD +26 -27
- 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 +89 -0
- vellum/client/types/organization_limit_config.py +0 -25
- vellum/client/types/quota.py +0 -21
- vellum/client/types/vembda_service_tier_enum.py +0 -5
- 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.4.dist-info → vellum_ai-1.0.6.dist-info}/LICENSE +0 -0
- {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/WHEEL +0 -0
- {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/entry_points.txt +0 -0
@@ -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,10 +1,12 @@
|
|
1
|
+
import pytest
|
2
|
+
|
1
3
|
from vellum.workflows import BaseWorkflow
|
2
4
|
from vellum.workflows.inputs.base import BaseInputs
|
3
5
|
from vellum.workflows.nodes.bases import BaseNode
|
4
6
|
from vellum.workflows.nodes.displayable.tool_calling_node.utils import get_function_name
|
5
7
|
from vellum.workflows.outputs.base import BaseOutputs
|
6
8
|
from vellum.workflows.state.base import BaseState
|
7
|
-
from vellum.workflows.types.definition import DeploymentDefinition
|
9
|
+
from vellum.workflows.types.definition import ComposioToolDefinition, DeploymentDefinition
|
8
10
|
|
9
11
|
|
10
12
|
def test_get_function_name_callable():
|
@@ -56,3 +58,21 @@ def test_get_function_name_subworkflow_deployment_uuid():
|
|
56
58
|
result = get_function_name(deployment_config)
|
57
59
|
|
58
60
|
assert result == "57f09bebb46340e0bf9ec972e664352f"
|
61
|
+
|
62
|
+
|
63
|
+
@pytest.mark.parametrize(
|
64
|
+
"toolkit,action,description,expected_result",
|
65
|
+
[
|
66
|
+
("SLACK", "SLACK_SEND_MESSAGE", "Send message to Slack", "slack_send_message"),
|
67
|
+
("GMAIL", "GMAIL_CREATE_EMAIL_DRAFT", "Create Gmail draft", "gmail_create_email_draft"),
|
68
|
+
],
|
69
|
+
)
|
70
|
+
def test_get_function_name_composio_tool_definition_various_toolkits(
|
71
|
+
toolkit: str, action: str, description: str, expected_result: str
|
72
|
+
):
|
73
|
+
"""Test ComposioToolDefinition function name generation with various toolkits."""
|
74
|
+
composio_tool = ComposioToolDefinition(toolkit=toolkit, action=action, description=description)
|
75
|
+
|
76
|
+
result = get_function_name(composio_tool)
|
77
|
+
|
78
|
+
assert result == expected_result
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import json
|
2
|
-
|
2
|
+
import logging
|
3
|
+
from typing import Any, Callable, Iterator, List, Optional, Type, Union, cast
|
3
4
|
|
4
5
|
from pydash import snake_case
|
5
6
|
|
6
7
|
from vellum import ChatMessage, PromptBlock
|
7
8
|
from vellum.client.types.function_call_chat_message_content import FunctionCallChatMessageContent
|
8
9
|
from vellum.client.types.function_call_chat_message_content_value import FunctionCallChatMessageContentValue
|
10
|
+
from vellum.client.types.function_definition import FunctionDefinition
|
9
11
|
from vellum.client.types.prompt_output import PromptOutput
|
10
12
|
from vellum.client.types.prompt_parameters import PromptParameters
|
11
13
|
from vellum.client.types.string_chat_message_content import StringChatMessageContent
|
@@ -14,6 +16,7 @@ from vellum.workflows.errors.types import WorkflowErrorCode
|
|
14
16
|
from vellum.workflows.exceptions import NodeException
|
15
17
|
from vellum.workflows.expressions.concat import ConcatExpression
|
16
18
|
from vellum.workflows.inputs import BaseInputs
|
19
|
+
from vellum.workflows.integrations.composio_service import ComposioService
|
17
20
|
from vellum.workflows.nodes.bases import BaseNode
|
18
21
|
from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubworkflowNode
|
19
22
|
from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
|
@@ -31,6 +34,32 @@ from vellum.workflows.types.generics import is_workflow_class
|
|
31
34
|
CHAT_HISTORY_VARIABLE = "chat_history"
|
32
35
|
|
33
36
|
|
37
|
+
logger = logging.getLogger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class FunctionCallNodeMixin:
|
41
|
+
"""Mixin providing common functionality for nodes that handle function calls."""
|
42
|
+
|
43
|
+
function_call_output: List[PromptOutput]
|
44
|
+
|
45
|
+
def _extract_function_arguments(self) -> dict:
|
46
|
+
"""Extract arguments from function call output."""
|
47
|
+
if self.function_call_output and len(self.function_call_output) > 0:
|
48
|
+
function_call = self.function_call_output[0]
|
49
|
+
if function_call.type == "FUNCTION_CALL" and function_call.value is not None:
|
50
|
+
return function_call.value.arguments or {}
|
51
|
+
return {}
|
52
|
+
|
53
|
+
def _add_function_result_to_chat_history(self, result: Any, state: ToolCallingState) -> None:
|
54
|
+
"""Add function execution result to chat history."""
|
55
|
+
state.chat_history.append(
|
56
|
+
ChatMessage(
|
57
|
+
role="FUNCTION",
|
58
|
+
content=StringChatMessageContent(value=json.dumps(result, cls=DefaultStateEncoder)),
|
59
|
+
)
|
60
|
+
)
|
61
|
+
|
62
|
+
|
34
63
|
class ToolRouterNode(InlinePromptNode[ToolCallingState]):
|
35
64
|
max_prompt_iterations: Optional[int] = 5
|
36
65
|
|
@@ -69,20 +98,11 @@ class ToolRouterNode(InlinePromptNode[ToolCallingState]):
|
|
69
98
|
yield output
|
70
99
|
|
71
100
|
|
72
|
-
class DynamicSubworkflowDeploymentNode(SubworkflowDeploymentNode[ToolCallingState]):
|
101
|
+
class DynamicSubworkflowDeploymentNode(SubworkflowDeploymentNode[ToolCallingState], FunctionCallNodeMixin):
|
73
102
|
"""Node that executes a deployment definition with function call output."""
|
74
103
|
|
75
|
-
function_call_output: List[PromptOutput]
|
76
|
-
|
77
104
|
def run(self) -> Iterator[BaseOutput]:
|
78
|
-
|
79
|
-
function_call = self.function_call_output[0]
|
80
|
-
if function_call.type == "FUNCTION_CALL" and function_call.value is not None:
|
81
|
-
arguments = function_call.value.arguments
|
82
|
-
else:
|
83
|
-
arguments = {}
|
84
|
-
else:
|
85
|
-
arguments = {}
|
105
|
+
arguments = self._extract_function_arguments()
|
86
106
|
|
87
107
|
# Mypy doesn't like instance assignments of class attributes. It's safe in our case tho bc it's what
|
88
108
|
# we do in the `__init__` method. Long term, instead of the function_call_output attribute above, we
|
@@ -103,28 +123,16 @@ class DynamicSubworkflowDeploymentNode(SubworkflowDeploymentNode[ToolCallingStat
|
|
103
123
|
yield output
|
104
124
|
|
105
125
|
# Add the result to the chat history
|
106
|
-
self.state
|
107
|
-
ChatMessage(
|
108
|
-
role="FUNCTION",
|
109
|
-
content=StringChatMessageContent(value=json.dumps(outputs, cls=DefaultStateEncoder)),
|
110
|
-
)
|
111
|
-
)
|
126
|
+
self._add_function_result_to_chat_history(outputs, self.state)
|
112
127
|
|
113
128
|
|
114
|
-
class DynamicInlineSubworkflowNode(
|
129
|
+
class DynamicInlineSubworkflowNode(
|
130
|
+
InlineSubworkflowNode[ToolCallingState, BaseInputs, BaseState], FunctionCallNodeMixin
|
131
|
+
):
|
115
132
|
"""Node that executes an inline subworkflow with function call output."""
|
116
133
|
|
117
|
-
function_call_output: List[PromptOutput]
|
118
|
-
|
119
134
|
def run(self) -> Iterator[BaseOutput]:
|
120
|
-
|
121
|
-
function_call = self.function_call_output[0]
|
122
|
-
if function_call.type == "FUNCTION_CALL" and function_call.value is not None:
|
123
|
-
arguments = function_call.value.arguments
|
124
|
-
else:
|
125
|
-
arguments = {}
|
126
|
-
else:
|
127
|
-
arguments = {}
|
135
|
+
arguments = self._extract_function_arguments()
|
128
136
|
|
129
137
|
self.subworkflow_inputs = arguments # type: ignore[misc]
|
130
138
|
|
@@ -137,29 +145,16 @@ class DynamicInlineSubworkflowNode(InlineSubworkflowNode[ToolCallingState, BaseI
|
|
137
145
|
yield output
|
138
146
|
|
139
147
|
# Add the result to the chat history
|
140
|
-
self.state
|
141
|
-
ChatMessage(
|
142
|
-
role="FUNCTION",
|
143
|
-
content=StringChatMessageContent(value=json.dumps(outputs, cls=DefaultStateEncoder)),
|
144
|
-
)
|
145
|
-
)
|
148
|
+
self._add_function_result_to_chat_history(outputs, self.state)
|
146
149
|
|
147
150
|
|
148
|
-
class FunctionNode(BaseNode[ToolCallingState]):
|
151
|
+
class FunctionNode(BaseNode[ToolCallingState], FunctionCallNodeMixin):
|
149
152
|
"""Node that executes a regular Python function with function call output."""
|
150
153
|
|
151
|
-
function_call_output: List[PromptOutput]
|
152
154
|
function_definition: Callable[..., Any]
|
153
155
|
|
154
156
|
def run(self) -> Iterator[BaseOutput]:
|
155
|
-
|
156
|
-
function_call = self.function_call_output[0]
|
157
|
-
if function_call.type == "FUNCTION_CALL" and function_call.value is not None:
|
158
|
-
arguments = function_call.value.arguments
|
159
|
-
else:
|
160
|
-
arguments = {}
|
161
|
-
else:
|
162
|
-
arguments = {}
|
157
|
+
arguments = self._extract_function_arguments()
|
163
158
|
|
164
159
|
try:
|
165
160
|
result = self.function_definition(**arguments)
|
@@ -171,16 +166,73 @@ class FunctionNode(BaseNode[ToolCallingState]):
|
|
171
166
|
)
|
172
167
|
|
173
168
|
# Add the result to the chat history
|
174
|
-
self.state
|
175
|
-
|
176
|
-
|
177
|
-
|
169
|
+
self._add_function_result_to_chat_history(result, self.state)
|
170
|
+
|
171
|
+
yield from []
|
172
|
+
|
173
|
+
|
174
|
+
class ComposioNode(BaseNode[ToolCallingState], FunctionCallNodeMixin):
|
175
|
+
"""Node that executes a Composio tool with function call output."""
|
176
|
+
|
177
|
+
composio_tool: ComposioToolDefinition
|
178
|
+
|
179
|
+
def run(self) -> Iterator[BaseOutput]:
|
180
|
+
# Extract arguments from function call
|
181
|
+
arguments = self._extract_function_arguments()
|
182
|
+
|
183
|
+
try:
|
184
|
+
# Execute using ComposioService
|
185
|
+
composio_service = ComposioService()
|
186
|
+
result = composio_service.execute_tool(tool_name=self.composio_tool.action, arguments=arguments)
|
187
|
+
except Exception as e:
|
188
|
+
raise NodeException(
|
189
|
+
message=f"Error executing Composio tool '{self.composio_tool.action}': {str(e)}",
|
190
|
+
code=WorkflowErrorCode.NODE_EXECUTION,
|
178
191
|
)
|
179
|
-
|
192
|
+
|
193
|
+
# Add result to chat history
|
194
|
+
self._add_function_result_to_chat_history(result, self.state)
|
180
195
|
|
181
196
|
yield from []
|
182
197
|
|
183
198
|
|
199
|
+
def _hydrate_composio_tool_definition(tool_def: ComposioToolDefinition) -> ComposioToolDefinition:
|
200
|
+
"""Hydrate a ComposioToolDefinition with detailed information from the Composio API.
|
201
|
+
|
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
|
216
|
+
)
|
217
|
+
|
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
|
+
)
|
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
|
234
|
+
|
235
|
+
|
184
236
|
def create_tool_router_node(
|
185
237
|
ml_model: str,
|
186
238
|
blocks: List[PromptBlock],
|
@@ -190,9 +242,25 @@ def create_tool_router_node(
|
|
190
242
|
max_prompt_iterations: Optional[int] = None,
|
191
243
|
) -> Type[ToolRouterNode]:
|
192
244
|
if functions and len(functions) > 0:
|
193
|
-
#
|
245
|
+
# Create dynamic ports and convert functions in a single loop
|
194
246
|
Ports = type("Ports", (), {})
|
247
|
+
prompt_functions: List[Union[Tool, FunctionDefinition]] = []
|
248
|
+
|
195
249
|
for function in functions:
|
250
|
+
if isinstance(function, ComposioToolDefinition):
|
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
|
+
)
|
260
|
+
else:
|
261
|
+
prompt_functions.append(function)
|
262
|
+
|
263
|
+
# Create port for this function (using original function for get_function_name)
|
196
264
|
function_name = get_function_name(function)
|
197
265
|
|
198
266
|
# Avoid using lambda to capture function_name
|
@@ -215,6 +283,7 @@ def create_tool_router_node(
|
|
215
283
|
else:
|
216
284
|
# If no functions exist, create a simple Ports class with just a default port
|
217
285
|
Ports = type("Ports", (), {"default": Port(default=True)})
|
286
|
+
prompt_functions = []
|
218
287
|
|
219
288
|
# Add a chat history block to blocks only if one doesn't already exist
|
220
289
|
has_chat_history_block = any(
|
@@ -247,7 +316,7 @@ def create_tool_router_node(
|
|
247
316
|
{
|
248
317
|
"ml_model": ml_model,
|
249
318
|
"blocks": blocks,
|
250
|
-
"functions": functions
|
319
|
+
"functions": prompt_functions, # Use converted functions for prompt layer
|
251
320
|
"prompt_inputs": node_prompt_inputs,
|
252
321
|
"parameters": parameters,
|
253
322
|
"max_prompt_iterations": max_prompt_iterations,
|
@@ -291,20 +360,16 @@ def create_function_node(
|
|
291
360
|
return node
|
292
361
|
|
293
362
|
elif isinstance(function, ComposioToolDefinition):
|
294
|
-
# ComposioToolDefinition execution not yet implemented
|
295
|
-
def composio_not_implemented(**kwargs):
|
296
|
-
raise NotImplementedError("ComposioToolDefinition execution not yet implemented")
|
297
|
-
|
298
363
|
node = type(
|
299
364
|
f"ComposioNode_{function.name}",
|
300
|
-
(
|
365
|
+
(ComposioNode,),
|
301
366
|
{
|
302
|
-
"
|
367
|
+
"composio_tool": function,
|
303
368
|
"function_call_output": tool_router_node.Outputs.results,
|
304
369
|
"__module__": __name__,
|
305
370
|
},
|
306
371
|
)
|
307
|
-
|
372
|
+
return node
|
308
373
|
elif is_workflow_class(function):
|
309
374
|
node = type(
|
310
375
|
f"DynamicInlineSubworkflowNode_{function.__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, 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:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import dataclasses
|
2
2
|
import inspect
|
3
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union, get_args, get_origin
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Optional, Type, Union, get_args, get_origin
|
4
4
|
|
5
5
|
from pydantic import BaseModel
|
6
6
|
from pydantic_core import PydanticUndefined
|
@@ -33,6 +33,18 @@ def compile_annotation(annotation: Optional[Any], defs: dict[str, Any]) -> dict:
|
|
33
33
|
if get_origin(annotation) is Union:
|
34
34
|
return {"anyOf": [compile_annotation(a, defs) for a in get_args(annotation)]}
|
35
35
|
|
36
|
+
if get_origin(annotation) is Literal:
|
37
|
+
values = list(get_args(annotation))
|
38
|
+
types = {type(value) for value in values}
|
39
|
+
if len(types) == 1:
|
40
|
+
value_type = types.pop()
|
41
|
+
if value_type in type_map:
|
42
|
+
return {"type": type_map[value_type], "enum": values}
|
43
|
+
else:
|
44
|
+
return {"enum": values}
|
45
|
+
else:
|
46
|
+
return {"enum": values}
|
47
|
+
|
36
48
|
if get_origin(annotation) is dict:
|
37
49
|
_, value_type = get_args(annotation)
|
38
50
|
return {"type": "object", "additionalProperties": compile_annotation(value_type, defs)}
|
@@ -1,6 +1,8 @@
|
|
1
|
+
import pytest
|
1
2
|
from dataclasses import dataclass
|
3
|
+
from enum import Enum
|
2
4
|
from unittest.mock import Mock
|
3
|
-
from typing import Dict, List, Optional, Union
|
5
|
+
from typing import Dict, List, Literal, Optional, Union
|
4
6
|
|
5
7
|
from pydantic import BaseModel
|
6
8
|
|
@@ -581,3 +583,32 @@ def test_compile_workflow_deployment_function_definition__defaults():
|
|
581
583
|
"required": ["no_default"],
|
582
584
|
},
|
583
585
|
)
|
586
|
+
|
587
|
+
|
588
|
+
@pytest.mark.parametrize(
|
589
|
+
"annotation,expected_schema",
|
590
|
+
[
|
591
|
+
(Literal["a", "b"], {"type": "string", "enum": ["a", "b"]}),
|
592
|
+
(Literal["a", 1], {"enum": ["a", 1]}),
|
593
|
+
],
|
594
|
+
)
|
595
|
+
def test_compile_function_definition__literal(annotation, expected_schema):
|
596
|
+
def my_function(a: annotation): # type: ignore
|
597
|
+
pass
|
598
|
+
|
599
|
+
compiled_function = compile_function_definition(my_function)
|
600
|
+
assert isinstance(compiled_function.parameters, dict)
|
601
|
+
assert compiled_function.parameters["properties"]["a"] == expected_schema
|
602
|
+
|
603
|
+
|
604
|
+
def test_compile_function_definition__literal_type_not_in_map():
|
605
|
+
class MyEnum(Enum):
|
606
|
+
FOO = "foo"
|
607
|
+
BAR = "bar"
|
608
|
+
|
609
|
+
def my_function(a: Literal[MyEnum.FOO, MyEnum.BAR]):
|
610
|
+
pass
|
611
|
+
|
612
|
+
compiled_function = compile_function_definition(my_function)
|
613
|
+
assert isinstance(compiled_function.parameters, dict)
|
614
|
+
assert compiled_function.parameters["properties"]["a"] == {"enum": [MyEnum.FOO, MyEnum.BAR]}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: vellum-ai
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.6
|
4
4
|
Summary:
|
5
5
|
License: MIT
|
6
6
|
Requires-Python: >=3.9,<4.0
|
@@ -22,8 +22,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Typing :: Typed
|
23
23
|
Requires-Dist: Jinja2 (>=3.1.0,<4.0.0)
|
24
24
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
25
|
-
Requires-Dist: composio-client (>=1.5.0,<2.0.0)
|
26
|
-
Requires-Dist: composio-core (>=0.7.20,<1.0.0)
|
27
25
|
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
28
26
|
Requires-Dist: httpx (>=0.21.2)
|
29
27
|
Requires-Dist: openai (>=1.0.0,<2.0.0)
|