1208 lines
41 KiB
Python
1208 lines
41 KiB
Python
"""Test provider functionality.
|
|
|
|
This test module verifies provider functionality including:
|
|
- Provider initialization
|
|
- execute_tool function setup and usage
|
|
- Provider helper methods
|
|
- Both agentic and non-agentic provider behavior
|
|
"""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from composio.client.types import Tool, tool_list_response
|
|
from composio.core.models.base import allow_tracking
|
|
from composio.core.models.tools import Tools
|
|
from composio.core.provider import AgenticProvider, NonAgenticProvider
|
|
from tests.conftest import mock_http_client
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def disable_telemetry():
|
|
"""Disable telemetry for all tests to prevent thread issues."""
|
|
token = allow_tracking.set(False)
|
|
yield
|
|
allow_tracking.reset(token)
|
|
|
|
|
|
def create_mock_tool(
|
|
slug: str, toolkit_slug: str, version: str = "12012025_00"
|
|
) -> Tool:
|
|
"""Create a mock tool for testing."""
|
|
return Tool(
|
|
name=f"Test {slug}",
|
|
slug=slug,
|
|
description="Test tool for provider testing",
|
|
input_parameters={"type": "object", "properties": {}},
|
|
output_parameters={},
|
|
available_versions=[version],
|
|
version=version,
|
|
scopes=[],
|
|
toolkit=tool_list_response.ItemToolkit(
|
|
name=toolkit_slug.title(), slug=toolkit_slug, logo=""
|
|
),
|
|
deprecated=tool_list_response.ItemDeprecated(
|
|
available_versions=[version],
|
|
displayName=f"Test {slug}",
|
|
version=version,
|
|
toolkit=tool_list_response.ItemDeprecatedToolkit(logo=""),
|
|
is_deprecated=False,
|
|
),
|
|
is_deprecated=False,
|
|
no_auth=False,
|
|
tags=[],
|
|
)
|
|
|
|
|
|
class TestProviderInitialization:
|
|
"""Test cases for provider initialization."""
|
|
|
|
def test_non_agentic_provider_initialization(self):
|
|
"""Test that non-agentic providers initialize correctly."""
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
assert provider is not None
|
|
assert provider.name == "openai"
|
|
assert isinstance(provider, NonAgenticProvider)
|
|
|
|
def test_agentic_provider_initialization(self):
|
|
"""Test that agentic providers initialize correctly."""
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {"slug": tool.slug, "execute": execute_tool}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
assert provider is not None
|
|
assert provider.name == "test_agentic"
|
|
assert isinstance(provider, AgenticProvider)
|
|
|
|
def test_provider_has_name_attribute(self):
|
|
"""Test that all providers have a name attribute."""
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
assert hasattr(provider, "name")
|
|
assert isinstance(provider.name, str)
|
|
assert len(provider.name) > 0
|
|
|
|
|
|
class TestProviderExecuteToolSetup:
|
|
"""Test cases for execute_tool setup during initialization."""
|
|
|
|
def test_execute_tool_set_during_tools_initialization_non_agentic(self):
|
|
"""Test that execute_tool is set during Tools initialization for non-agentic providers."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
# Create Tools instance
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# After initialization, provider should have execute_tool
|
|
assert hasattr(provider, "execute_tool")
|
|
assert provider.execute_tool is not None
|
|
assert callable(provider.execute_tool)
|
|
|
|
def test_execute_tool_set_during_tools_initialization_agentic(self):
|
|
"""Test that execute_tool is set during Tools initialization for agentic providers."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {"slug": tool.slug, "execute": execute_tool}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
# Create Tools instance
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# After initialization, provider should have execute_tool
|
|
assert hasattr(provider, "execute_tool")
|
|
assert provider.execute_tool is not None
|
|
assert callable(provider.execute_tool)
|
|
|
|
def test_execute_tool_available_immediately_after_initialization(self):
|
|
"""Test that execute_tool is available immediately after initialization, before get() is called."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
# Create Tools instance (but don't call get())
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# execute_tool should be available immediately
|
|
assert hasattr(provider, "execute_tool")
|
|
assert callable(provider.execute_tool)
|
|
|
|
def test_execute_tool_signature_matches_protocol(self):
|
|
"""Test that execute_tool has the correct signature matching ExecuteToolFn protocol."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# Verify it's callable
|
|
assert callable(provider.execute_tool)
|
|
|
|
# Try calling it with the protocol signature
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
# Should accept slug, arguments, and keyword-only modifiers/user_id
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
modifiers=None,
|
|
user_id="test-user",
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
|
|
class TestProviderExecuteToolFunctionality:
|
|
"""Test cases for execute_tool functionality."""
|
|
|
|
def test_execute_tool_executes_composio_tool(self):
|
|
"""Test that provider.execute_tool executes Composio tools correctly."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True, "repo": "composio/composio"},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
assert result["data"]["repo"] == "composio/composio"
|
|
mock_client.tools.execute.assert_called_once()
|
|
|
|
def test_execute_tool_uses_dangerously_skip_version_check(self):
|
|
"""Test that execute_tool automatically sets dangerously_skip_version_check=True."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
# Create Tools without explicit toolkit versions (defaults to 'latest')
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "latest")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
# Should NOT raise ToolVersionRequiredError even with 'latest'
|
|
# because dangerously_skip_version_check=True is set automatically
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
def test_execute_tool_passes_user_id(self):
|
|
"""Test that execute_tool correctly passes user_id parameter."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
user_id="user-123",
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
call_args = mock_client.tools.execute.call_args
|
|
assert call_args.kwargs["user_id"] == "user-123"
|
|
|
|
def test_execute_tool_passes_modifiers(self):
|
|
"""Test that execute_tool correctly passes modifiers."""
|
|
from composio.core.models._modifiers import before_execute
|
|
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
def modify_arguments(tool, toolkit, params):
|
|
params["arguments"]["modified"] = True
|
|
return params
|
|
|
|
modifier = before_execute(modify_arguments)
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
modifiers=[modifier],
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
call_args = mock_client.tools.execute.call_args
|
|
assert call_args.kwargs["arguments"]["modified"] is True
|
|
|
|
def test_execute_tool_with_toolkit_versions(self):
|
|
"""Test that execute_tool uses configured toolkit versions."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00", "slack": "12012025_01"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
call_args = mock_client.tools.execute.call_args
|
|
assert call_args.kwargs["version"] == "12012025_00"
|
|
|
|
|
|
class TestNonAgenticProviderHelperMethods:
|
|
"""Test cases for non-agentic provider helper methods."""
|
|
|
|
def test_openai_provider_execute_tool_call(self):
|
|
"""Test that OpenAIProvider.execute_tool_call uses provider.execute_tool."""
|
|
from openai.types.chat.chat_completion_message_tool_call import (
|
|
ChatCompletionMessageToolCall,
|
|
Function,
|
|
)
|
|
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
tool_call = ChatCompletionMessageToolCall(
|
|
id="call_123",
|
|
function=Function(
|
|
name="GITHUB_STAR_REPO",
|
|
arguments='{"repo": "composio/composio"}',
|
|
),
|
|
type="function",
|
|
)
|
|
|
|
result = provider.execute_tool_call(
|
|
user_id="test-user",
|
|
tool_call=tool_call,
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
|
|
def test_openai_provider_handle_tool_calls(self):
|
|
"""Test that OpenAIProvider.handle_tool_calls processes multiple tool calls."""
|
|
from openai.types.chat import ChatCompletion
|
|
from openai.types.chat.chat_completion import Choice
|
|
from openai.types.chat.chat_completion_message import ChatCompletionMessage
|
|
from openai.types.chat.chat_completion_message_tool_call import (
|
|
ChatCompletionMessageToolCall,
|
|
Function,
|
|
)
|
|
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
tool_call = ChatCompletionMessageToolCall(
|
|
id="call_123",
|
|
function=Function(
|
|
name="GITHUB_STAR_REPO",
|
|
arguments='{"repo": "composio/composio"}',
|
|
),
|
|
type="function",
|
|
)
|
|
|
|
message = ChatCompletionMessage(
|
|
role="assistant",
|
|
content=None,
|
|
tool_calls=[tool_call],
|
|
)
|
|
|
|
choice = Choice(
|
|
finish_reason="tool_calls",
|
|
index=0,
|
|
message=message,
|
|
)
|
|
|
|
completion = ChatCompletion(
|
|
id="chatcmpl-123",
|
|
choices=[choice],
|
|
created=1234567890,
|
|
model="gpt-4",
|
|
object="chat.completion",
|
|
)
|
|
|
|
results = provider.handle_tool_calls(
|
|
user_id="test-user",
|
|
response=completion,
|
|
)
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["successful"] is True
|
|
assert results[0]["data"]["starred"] is True
|
|
|
|
def test_openai_provider_handle_tool_calls_only_first_choice(self):
|
|
"""Only the first choice runs; n > 1 alternatives would orphan tool_call_ids."""
|
|
from openai.types.chat import ChatCompletion
|
|
from openai.types.chat.chat_completion import Choice
|
|
from openai.types.chat.chat_completion_message import ChatCompletionMessage
|
|
from openai.types.chat.chat_completion_message_tool_call import (
|
|
ChatCompletionMessageToolCall,
|
|
Function,
|
|
)
|
|
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
def make_choice(index: int, call_id: str) -> Choice:
|
|
return Choice(
|
|
finish_reason="tool_calls",
|
|
index=index,
|
|
message=ChatCompletionMessage(
|
|
role="assistant",
|
|
content=None,
|
|
tool_calls=[
|
|
ChatCompletionMessageToolCall(
|
|
id=call_id,
|
|
function=Function(
|
|
name="GITHUB_STAR_REPO",
|
|
arguments='{"repo": "composio/composio"}',
|
|
),
|
|
type="function",
|
|
)
|
|
],
|
|
),
|
|
)
|
|
|
|
completion = ChatCompletion(
|
|
id="chatcmpl-123",
|
|
choices=[make_choice(0, "call_first"), make_choice(1, "call_second")],
|
|
created=1234567890,
|
|
model="gpt-4",
|
|
object="chat.completion",
|
|
)
|
|
|
|
results = provider.handle_tool_calls(
|
|
user_id="test-user",
|
|
response=completion,
|
|
)
|
|
|
|
assert len(results) == 1
|
|
assert mock_client.tools.execute.call_count == 1
|
|
|
|
def test_openai_provider_wrap_tools(self):
|
|
"""Test that OpenAIProvider.wrap_tools creates proper tool definitions."""
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
tools_list = [
|
|
create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00"),
|
|
create_mock_tool("SLACK_SEND_MESSAGE", "slack", "12012025_01"),
|
|
]
|
|
|
|
wrapped = provider.wrap_tools(tools_list)
|
|
|
|
assert len(wrapped) == 2
|
|
assert all(tool["type"] == "function" for tool in wrapped)
|
|
assert wrapped[0]["function"]["name"] == "GITHUB_STAR_REPO"
|
|
assert wrapped[1]["function"]["name"] == "SLACK_SEND_MESSAGE"
|
|
|
|
|
|
class TestAgenticProviderFunctionality:
|
|
"""Test cases for agentic providers."""
|
|
|
|
def test_agentic_provider_has_execute_tool_after_initialization(self):
|
|
"""Test that agentic providers have execute_tool after Tools initialization."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {"slug": tool.slug, "execute": execute_tool}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
assert hasattr(provider, "execute_tool")
|
|
assert callable(provider.execute_tool)
|
|
|
|
def test_agentic_provider_execute_tool_works(self):
|
|
"""Test that agentic provider's execute_tool executes tools correctly."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {"slug": tool.slug, "execute": execute_tool}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
|
|
def test_agentic_provider_wrapped_tools_receive_execute_function(self):
|
|
"""Test that wrapped tools from agentic providers receive the execute function."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {
|
|
"name": tool.slug,
|
|
"description": tool.description,
|
|
"executor": execute_tool,
|
|
}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
tools = Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_list_response = Mock()
|
|
mock_list_response.items = [github_tool]
|
|
mock_client.tools.list.return_value = mock_list_response
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
wrapped_tools = tools.get(
|
|
user_id="test-user",
|
|
slug="GITHUB_STAR_REPO",
|
|
)
|
|
|
|
assert len(wrapped_tools) == 1
|
|
wrapped_tool = wrapped_tools[0]
|
|
assert "executor" in wrapped_tool
|
|
assert callable(wrapped_tool["executor"])
|
|
|
|
# Call the executor
|
|
result = wrapped_tool["executor"](
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
|
|
def test_agentic_provider_wrap_tools_receives_execute_function(self):
|
|
"""Test that wrap_tools method receives the execute_tool function."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {"slug": tool.slug, "has_executor": callable(execute_tool)}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
# Verify execute_tool is callable
|
|
assert callable(execute_tool)
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
tools = Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
|
|
mock_list_response = Mock()
|
|
mock_list_response.items = [github_tool]
|
|
mock_client.tools.list.return_value = mock_list_response
|
|
|
|
wrapped = tools.get(user_id="test-user", slug="GITHUB_STAR_REPO")
|
|
|
|
assert len(wrapped) == 1
|
|
assert wrapped[0]["has_executor"] is True
|
|
|
|
|
|
class TestLangchainReservedKeywords:
|
|
"""Regression tests for PLEN-1671: ValueError when tool schema has Python reserved keywords as parameter names.
|
|
|
|
Tools from some toolkits (e.g. Intercom) have parameters named 'from',
|
|
which is a Python reserved keyword. The LangchainProvider must substitute
|
|
these names before building function signatures and reinstate them when
|
|
executing the tool.
|
|
"""
|
|
|
|
def _make_tool_with_params(self, properties: dict, required: list | None = None):
|
|
"""Helper: create a mock Tool whose input_parameters use the given properties."""
|
|
return Tool(
|
|
name="Test Tool",
|
|
slug="TEST_TOOL",
|
|
description="A tool for testing reserved keyword handling",
|
|
input_parameters={
|
|
"type": "object",
|
|
"title": "TestToolRequest",
|
|
"properties": properties,
|
|
"required": required or [],
|
|
},
|
|
output_parameters={},
|
|
available_versions=["12012025_00"],
|
|
version="12012025_00",
|
|
scopes=[],
|
|
toolkit=tool_list_response.ItemToolkit(name="Test", slug="test", logo=""),
|
|
deprecated=tool_list_response.ItemDeprecated(
|
|
available_versions=["12012025_00"],
|
|
displayName="Test Tool",
|
|
version="12012025_00",
|
|
toolkit=tool_list_response.ItemDeprecatedToolkit(logo=""),
|
|
is_deprecated=False,
|
|
),
|
|
is_deprecated=False,
|
|
no_auth=False,
|
|
tags=[],
|
|
)
|
|
|
|
def test_wrap_tool_with_from_parameter(self):
|
|
"""PLEN-1671: 'from' parameter must not raise ValueError."""
|
|
from composio_langchain import LangchainProvider
|
|
|
|
provider = LangchainProvider()
|
|
tool = self._make_tool_with_params(
|
|
properties={
|
|
"from": {
|
|
"type": "string",
|
|
"description": "Starting point for listing",
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Max results",
|
|
"default": 20,
|
|
},
|
|
},
|
|
)
|
|
|
|
captured = {}
|
|
|
|
def mock_execute(slug, arguments):
|
|
captured.update(arguments)
|
|
return {"data": {}, "error": None, "successful": True}
|
|
|
|
# Must not raise ValueError: 'from' is not a valid parameter name
|
|
wrapped = provider.wrap_tool(tool, mock_execute)
|
|
|
|
assert wrapped.name == "TEST_TOOL"
|
|
|
|
# Invoke the tool and verify 'from' is reinstated in the arguments
|
|
wrapped.run({"from_rs": "2024-01-01", "limit": 10})
|
|
assert captured == {"from": "2024-01-01", "limit": 10}
|
|
|
|
def test_wrap_tool_with_multiple_reserved_keywords(self):
|
|
"""All Python reserved keywords used as parameter names must be handled."""
|
|
from composio_langchain import LangchainProvider
|
|
|
|
provider = LangchainProvider()
|
|
tool = self._make_tool_with_params(
|
|
properties={
|
|
"from": {
|
|
"type": "string",
|
|
"description": "Start date",
|
|
},
|
|
"for": {
|
|
"type": "string",
|
|
"description": "Recipient",
|
|
},
|
|
"import": {
|
|
"type": "string",
|
|
"description": "Import source",
|
|
},
|
|
"class": {
|
|
"type": "string",
|
|
"description": "CSS class",
|
|
},
|
|
"async": {
|
|
"type": "boolean",
|
|
"description": "Async flag",
|
|
"default": False,
|
|
},
|
|
"normal_param": {
|
|
"type": "string",
|
|
"description": "A normal parameter",
|
|
},
|
|
},
|
|
)
|
|
|
|
captured = {}
|
|
|
|
def mock_execute(slug, arguments):
|
|
captured.update(arguments)
|
|
return {"data": {}, "error": None, "successful": True}
|
|
|
|
wrapped = provider.wrap_tool(tool, mock_execute)
|
|
assert wrapped.name == "TEST_TOOL"
|
|
|
|
# Invoke with renamed parameters and verify originals are reinstated
|
|
wrapped.run(
|
|
{
|
|
"from_rs": "2024-01-01",
|
|
"for_rs": "user@example.com",
|
|
"import_rs": "csv",
|
|
"class_rs": "primary",
|
|
"async_rs": True,
|
|
"normal_param": "hello",
|
|
}
|
|
)
|
|
assert captured == {
|
|
"from": "2024-01-01",
|
|
"for": "user@example.com",
|
|
"import": "csv",
|
|
"class": "primary",
|
|
"async": True,
|
|
"normal_param": "hello",
|
|
}
|
|
|
|
def test_wrap_tool_with_required_reserved_keyword_param(self):
|
|
"""Reserved keywords that are also required params must work."""
|
|
from composio_langchain import LangchainProvider
|
|
|
|
provider = LangchainProvider()
|
|
tool = self._make_tool_with_params(
|
|
properties={
|
|
"from": {
|
|
"type": "string",
|
|
"description": "Required start date",
|
|
},
|
|
},
|
|
required=["from"],
|
|
)
|
|
|
|
captured = {}
|
|
|
|
def mock_execute(slug, arguments):
|
|
captured.update(arguments)
|
|
return {"data": {}, "error": None, "successful": True}
|
|
|
|
wrapped = provider.wrap_tool(tool, mock_execute)
|
|
wrapped.run({"from_rs": "2024-01-01"})
|
|
assert captured == {"from": "2024-01-01"}
|
|
|
|
|
|
class TestProviderEdgeCases:
|
|
"""Test edge cases and error handling for providers."""
|
|
|
|
def test_execute_tool_with_none_parameters(self):
|
|
"""Test that execute_tool works when optional parameters are None."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
modifiers=None,
|
|
user_id=None,
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
def test_execute_tool_with_multiple_toolkit_versions(self):
|
|
"""Test execute_tool with multiple configured toolkit versions."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
toolkit_versions = {
|
|
"github": "12012025_00",
|
|
"slack": "12012025_01",
|
|
"notion": "12012025_02",
|
|
}
|
|
|
|
Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions=toolkit_versions,
|
|
)
|
|
|
|
# Test each toolkit uses its configured version
|
|
for toolkit, version in toolkit_versions.items():
|
|
tool = create_mock_tool(f"{toolkit.upper()}_TEST", toolkit, version)
|
|
mock_client.tools.retrieve.return_value = tool
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"success": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
result = provider.execute_tool(
|
|
slug=f"{toolkit.upper()}_TEST",
|
|
arguments={"test": "data"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
|
|
call_args = mock_client.tools.execute.call_args
|
|
assert call_args.kwargs["version"] == version
|
|
|
|
|
|
class TestProviderIntegration:
|
|
"""Integration tests for provider functionality."""
|
|
|
|
def test_provider_workflow_non_agentic(self):
|
|
"""Test complete workflow with non-agentic provider."""
|
|
mock_client = mock_http_client()
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
# Initialize Tools
|
|
tools = Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# Verify provider has execute_tool
|
|
assert hasattr(provider, "execute_tool")
|
|
|
|
# Mock tool and response
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_list_response = Mock()
|
|
mock_list_response.items = [github_tool]
|
|
mock_client.tools.list.return_value = mock_list_response
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
# Get wrapped tools
|
|
wrapped_tools = tools.get(user_id="test-user", slug="GITHUB_STAR_REPO")
|
|
|
|
# Verify wrapped tools are in OpenAI format
|
|
assert len(wrapped_tools) == 1
|
|
wrapped_tool = wrapped_tools[0]
|
|
assert wrapped_tool["type"] == "function"
|
|
assert wrapped_tool["function"]["name"] == "GITHUB_STAR_REPO"
|
|
|
|
# Execute tool directly via provider
|
|
result = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
user_id="test-user",
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
|
|
def test_provider_workflow_agentic(self):
|
|
"""Test complete workflow with agentic provider."""
|
|
mock_client = mock_http_client()
|
|
|
|
class TestAgenticProvider(AgenticProvider, name="test_agentic"):
|
|
def wrap_tool(self, tool, execute_tool):
|
|
return {
|
|
"name": tool.slug,
|
|
"description": tool.description,
|
|
"parameters": tool.input_parameters,
|
|
"executor": execute_tool,
|
|
}
|
|
|
|
def wrap_tools(self, tools, execute_tool):
|
|
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
|
|
provider = TestAgenticProvider()
|
|
|
|
# Initialize Tools
|
|
tools = Tools(
|
|
client=mock_client,
|
|
provider=provider,
|
|
toolkit_versions={"github": "12012025_00"},
|
|
)
|
|
|
|
# Verify provider has execute_tool
|
|
assert hasattr(provider, "execute_tool")
|
|
|
|
# Mock tool and response
|
|
github_tool = create_mock_tool("GITHUB_STAR_REPO", "github", "12012025_00")
|
|
mock_client.tools.retrieve.return_value = github_tool
|
|
|
|
mock_list_response = Mock()
|
|
mock_list_response.items = [github_tool]
|
|
mock_client.tools.list.return_value = mock_list_response
|
|
|
|
mock_execute_response = Mock()
|
|
mock_execute_response.model_dump.return_value = {
|
|
"data": {"starred": True},
|
|
"error": None,
|
|
"successful": True,
|
|
}
|
|
mock_client.tools.execute.return_value = mock_execute_response
|
|
|
|
# Get wrapped tools
|
|
wrapped_tools = tools.get(user_id="test-user", slug="GITHUB_STAR_REPO")
|
|
|
|
# Verify wrapped tools have executor
|
|
assert len(wrapped_tools) == 1
|
|
wrapped_tool = wrapped_tools[0]
|
|
assert "executor" in wrapped_tool
|
|
assert callable(wrapped_tool["executor"])
|
|
|
|
# Execute via wrapped tool executor
|
|
result = wrapped_tool["executor"](
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
)
|
|
|
|
assert result["successful"] is True
|
|
assert result["data"]["starred"] is True
|
|
|
|
# Also test direct execution via provider
|
|
result2 = provider.execute_tool(
|
|
slug="GITHUB_STAR_REPO",
|
|
arguments={"repo": "composio/composio"},
|
|
user_id="test-user",
|
|
)
|
|
|
|
assert result2["successful"] is True
|
|
assert result2["data"]["starred"] is True
|
|
|
|
|
|
class TestAgenticSkipDefaultsParity:
|
|
"""Regression: agentic providers must honor skip_defaults in __signature__.
|
|
|
|
The langchain and langgraph providers strip schema defaults from
|
|
args_schema when schema_config={"skip_defaults": True}, but kept the
|
|
defaults in the function __signature__, so the signature and args_schema
|
|
disagreed about which optional params were required. They now pass the
|
|
same skip_default to both, matching the sibling llamaindex provider.
|
|
|
|
autogen only builds __signature__ (no args_schema), so it never had the
|
|
mismatch, but its signature likewise ignored skip_defaults; it is aligned
|
|
too and checked on the signature alone.
|
|
|
|
Provider packages are optional (the unit-test job installs langchain and
|
|
autogen), so each case skips via importorskip when its provider is absent.
|
|
"""
|
|
|
|
def _make_tool_with_default(self):
|
|
return Tool(
|
|
name="Test Tool",
|
|
slug="TEST_TOOL",
|
|
description="A tool with a defaulted optional param",
|
|
input_parameters={
|
|
"type": "object",
|
|
"title": "TestToolRequest",
|
|
"properties": {
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Max results",
|
|
"default": 5,
|
|
},
|
|
},
|
|
"required": [],
|
|
},
|
|
output_parameters={},
|
|
available_versions=["12012025_00"],
|
|
version="12012025_00",
|
|
scopes=[],
|
|
toolkit=tool_list_response.ItemToolkit(name="Test", slug="test", logo=""),
|
|
deprecated=tool_list_response.ItemDeprecated(
|
|
available_versions=["12012025_00"],
|
|
displayName="Test Tool",
|
|
version="12012025_00",
|
|
toolkit=tool_list_response.ItemDeprecatedToolkit(logo=""),
|
|
is_deprecated=False,
|
|
),
|
|
is_deprecated=False,
|
|
no_auth=False,
|
|
tags=[],
|
|
)
|
|
|
|
def _provider(self, name, schema_config=None):
|
|
module = pytest.importorskip(f"composio_{name}")
|
|
provider_cls = getattr(module, f"{name.capitalize()}Provider")
|
|
return (
|
|
provider_cls(schema_config=schema_config)
|
|
if schema_config
|
|
else provider_cls()
|
|
)
|
|
|
|
@pytest.mark.parametrize("name", ["langchain", "langgraph"])
|
|
def test_skip_defaults_signature_matches_args_schema(self, name):
|
|
from inspect import Parameter
|
|
|
|
provider = self._provider(name, {"skip_defaults": True})
|
|
wrapped = provider.wrap_tool(self._make_tool_with_default(), Mock())
|
|
|
|
limit = wrapped.func.__signature__.parameters["limit"]
|
|
assert limit.default is Parameter.empty
|
|
assert wrapped.args_schema.model_fields["limit"].is_required()
|
|
|
|
@pytest.mark.parametrize("name", ["langchain", "langgraph"])
|
|
def test_default_preserved_without_skip_defaults(self, name):
|
|
provider = self._provider(name)
|
|
wrapped = provider.wrap_tool(self._make_tool_with_default(), Mock())
|
|
|
|
limit = wrapped.func.__signature__.parameters["limit"]
|
|
assert limit.default == 5
|
|
assert not wrapped.args_schema.model_fields["limit"].is_required()
|
|
|
|
def test_autogen_signature_honors_skip_defaults(self):
|
|
from inspect import Parameter
|
|
|
|
provider = self._provider("autogen", {"skip_defaults": True})
|
|
wrapped = provider.wrap_tool(self._make_tool_with_default(), Mock())
|
|
|
|
# autogen exposes the wrapped function as `_func` and has no args_schema.
|
|
limit = wrapped._func.__signature__.parameters["limit"]
|
|
assert limit.default is Parameter.empty
|
|
|
|
def test_autogen_signature_preserves_default(self):
|
|
provider = self._provider("autogen")
|
|
wrapped = provider.wrap_tool(self._make_tool_with_default(), Mock())
|
|
|
|
limit = wrapped._func.__signature__.parameters["limit"]
|
|
assert limit.default == 5
|