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.
Files changed (33) hide show
  1. vellum/__init__.py +0 -6
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/client/types/__init__.py +0 -6
  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/bases/api_node/node.py +27 -9
  9. vellum/workflows/nodes/displayable/bases/api_node/tests/__init__.py +0 -0
  10. vellum/workflows/nodes/displayable/bases/api_node/tests/test_node.py +47 -0
  11. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_composio_service.py +63 -58
  12. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +21 -1
  13. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +124 -59
  14. vellum/workflows/types/definition.py +4 -2
  15. vellum/workflows/utils/functions.py +13 -1
  16. vellum/workflows/utils/tests/test_functions.py +32 -1
  17. {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/METADATA +1 -3
  18. {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/RECORD +26 -27
  19. vellum_cli/push.py +11 -2
  20. vellum_cli/tests/test_push.py +57 -1
  21. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +2 -0
  22. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +16 -0
  23. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +89 -0
  24. vellum/client/types/organization_limit_config.py +0 -25
  25. vellum/client/types/quota.py +0 -21
  26. vellum/client/types/vembda_service_tier_enum.py +0 -5
  27. vellum/types/organization_limit_config.py +0 -3
  28. vellum/types/quota.py +0 -3
  29. vellum/types/vembda_service_tier_enum.py +0 -3
  30. vellum/workflows/nodes/displayable/tool_calling_node/composio_service.py +0 -83
  31. {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/LICENSE +0 -0
  32. {vellum_ai-1.0.4.dist-info → vellum_ai-1.0.6.dist-info}/WHEEL +0 -0
  33. {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.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,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
- from typing import Any, Callable, Iterator, List, Optional, Type, cast
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
- if self.function_call_output and len(self.function_call_output) > 0:
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.chat_history.append(
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(InlineSubworkflowNode[ToolCallingState, BaseInputs, BaseState]):
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
- if self.function_call_output and len(self.function_call_output) > 0:
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.chat_history.append(
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
- if self.function_call_output and len(self.function_call_output) > 0:
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.chat_history.append(
175
- ChatMessage(
176
- role="FUNCTION",
177
- content=StringChatMessageContent(value=json.dumps(result, cls=DefaultStateEncoder)),
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
- # If we have functions, create dynamic ports for each function
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
- (FunctionNode,),
365
+ (ComposioNode,),
301
366
  {
302
- "function_definition": composio_not_implemented,
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.4
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)