From f039d2a502e45566304cf022de73f3bcfdca0e85 Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 31 Aug 2026 17:35:48 -0300 Subject: [PATCH] Use a clear contract for platform tools The platform tool builder keyed schemas by action name. Actions with the same name from different apps could overwrite each other. Resolve selectors into immutable tool metadata and use one private client to fetch and execute actions. Keep each app identity when CrewAI builds the tool collection. --- .../tools/crewai_platform_tools/__init__.py | 4 - .../tools/crewai_platform_tools/_client.py | 153 +++++++ .../application_selector.py | 54 --- .../crewai_platform_action_tool.py | 84 ++-- .../crewai_platform_tool_builder.py | 118 ------ .../crewai_platform_tools.py | 25 +- .../tools/crewai_platform_tools/misc.py | 6 - .../crewai_platform_tools/test_client.py | 247 ++++++++++++ .../test_crewai_platform_action_tool.py | 182 ++++----- .../test_crewai_platform_tool_builder.py | 375 ------------------ .../test_crewai_platform_tools.py | 187 +++++++-- lib/crewai/tests/agents/test_lite_agent.py | 47 ++- ...est_agent_kickoff_with_platform_tools.yaml | 344 ---------------- lib/crewai/tests/tools/test_tool_failure.py | 59 ++- 14 files changed, 752 insertions(+), 1133 deletions(-) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/_client.py delete mode 100644 lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/application_selector.py delete mode 100644 lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py create mode 100644 lib/crewai-tools/tests/tools/crewai_platform_tools/test_client.py delete mode 100644 lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py delete mode 100644 lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py index 588414e19..4eb9d7d43 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py @@ -7,9 +7,6 @@ through the CrewAI platform API. from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( CrewAIPlatformActionTool, ) -from crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder import ( - CrewaiPlatformToolBuilder, -) from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import ( CrewaiPlatformTools, ) @@ -17,6 +14,5 @@ from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import ( __all__ = [ "CrewAIPlatformActionTool", - "CrewaiPlatformToolBuilder", "CrewaiPlatformTools", ] diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/_client.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/_client.py new file mode 100644 index 000000000..7c32b3763 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/_client.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +from dataclasses import dataclass +import logging +import os +from typing import Any +from uuid import UUID + +from crewai.plus_api import PlusAPI +import requests + +from crewai_tools.tools.crewai_platform_tools.misc import ( + get_platform_integration_token, +) + + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class _PlatformToolSelector: + app: str + action: str | None + connection_id: UUID | None + + @classmethod + def from_string(cls, value: str) -> _PlatformToolSelector: + if not value: + raise ValueError(f"Invalid application selector {value!r}: cannot be empty") + if "@" in value and "/" in value and value.index("@") < value.index("/"): + raise ValueError( + f"Invalid application selector {value!r}: " + "connection ID must be the last segment" + ) + + app_and_action, connection_separator, connection_id = value.partition("@") + app, action_separator, action = app_and_action.partition("/") + + if not app: + raise ValueError( + f"Invalid application selector {value!r}: application cannot be empty" + ) + if action_separator and not action: + raise ValueError( + f"Invalid application selector {value!r}: action cannot be empty" + ) + if connection_separator and not connection_id: + raise ValueError( + f"Invalid application selector {value!r}: connection ID cannot be empty" + ) + + parsed_connection_id = None + if connection_id: + try: + parsed_connection_id = UUID(connection_id) + except ValueError as error: + raise ValueError( + f"Invalid application selector {value!r}: " + "connection ID must be a valid UUID" + ) from error + + return cls( + app=app, + action=action if action_separator else None, + connection_id=parsed_connection_id, + ) + + +@dataclass(frozen=True) +class _PlatformToolInfo: + app: str + action: str + connection_id: UUID | None + description: str + parameters: dict[str, Any] + + +def _should_verify_ssl() -> bool: + return os.environ.get("CREWAI_FACTORY", "false").lower() != "true" + + +class _PlatformToolsClient: + def _headers(self) -> dict[str, str]: + token = get_platform_integration_token() + headers = {"Authorization": f"Bearer {token}"} + if deployment_instance_uuid := os.getenv("CREWAI_DEPLOYMENT_INSTANCE_UUID"): + headers["X-Crewai-Deployment-Instance-Id"] = deployment_instance_uuid + return headers + + def get_tools( + self, selectors: list[_PlatformToolSelector] + ) -> list[_PlatformToolInfo]: + headers = self._headers() + resolved_tools: list[_PlatformToolInfo] = [] + + for selector in selectors: + resolved_tools.extend(self._get_tools_for_selector(selector, headers)) + + return resolved_tools + + def _get_tools_for_selector( + self, selector: _PlatformToolSelector, headers: dict[str, str] + ) -> list[_PlatformToolInfo]: + try: + url = f"{PlusAPI().base_url}/clipper/v1/applications/{selector.app}/tools" + if selector.action is not None: + url = f"{url}/{selector.action}" + connection_id = ( + str(selector.connection_id) if selector.connection_id else None + ) + response = requests.get( + url, + headers=headers, + timeout=30, + params={"connection_id": connection_id} if connection_id else None, + verify=_should_verify_ssl(), + ) + response.raise_for_status() + data = response.json()["data"] + tools = data if selector.action is None else [data] + return [ + _PlatformToolInfo( + app=selector.app, + action=tool["slug"], + connection_id=selector.connection_id, + description=tool["description"], + parameters=tool["input_schema"], + ) + for tool in tools + ] + except Exception as error: + logger.error(f"Failed to fetch platform tools for {selector}: {error}") + return [] + + def execute_action( + self, tool_info: _PlatformToolInfo, arguments: dict[str, Any] + ) -> requests.Response: + payload: dict[str, Any] = {"arguments": arguments} + if tool_info.connection_id: + payload["connection_id"] = str(tool_info.connection_id) + return requests.post( + url=( + f"{PlusAPI().base_url}/clipper/v1/applications/{tool_info.app}/" + f"tools/{tool_info.action}/execute" + ), + headers={ + **self._headers(), + "Content-Type": "application/json", + }, + json=payload, + timeout=60, + verify=_should_verify_ssl(), + ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/application_selector.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/application_selector.py deleted file mode 100644 index ac2b9e347..000000000 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/application_selector.py +++ /dev/null @@ -1,54 +0,0 @@ -from uuid import UUID - - -class ApplicationSelector: - """Parse an application selector. - - Selectors use the ``application[/action][@connection_uuid]`` syntax. - - Raises: - ValueError: If the selector does not follow the supported syntax. - """ - - name: str - action: str | None - connection_id: UUID | None - - def __init__(self, value: str) -> None: - if not value: - raise ValueError(f"Invalid application selector {value!r}: cannot be empty") - if "@" in value and "/" in value and value.index("@") < value.index("/"): - raise ValueError( - f"Invalid application selector {value!r}: " - "connection ID must be the last segment" - ) - - app, connection_separator, connection_id = value.partition("@") - name, action_separator, action = app.partition("/") - - if not name: - raise ValueError( - f"Invalid application selector {value!r}: application cannot be empty" - ) - if action_separator and not action: - raise ValueError( - f"Invalid application selector {value!r}: action cannot be empty" - ) - if connection_separator and not connection_id: - raise ValueError( - f"Invalid application selector {value!r}: connection ID cannot be empty" - ) - - parsed_connection_id = None - if connection_id: - try: - parsed_connection_id = UUID(connection_id) - except ValueError as error: - raise ValueError( - f"Invalid application selector {value!r}: " - "connection ID must be a valid UUID" - ) from error - - self.name = name - self.action = action if action_separator else None - self.connection_id = parsed_connection_id diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py index 6c6a6c05c..be444cd3e 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py @@ -1,57 +1,63 @@ """Crewai Enterprise Tools.""" import json -import os from typing import Any from crewai.tools import BaseTool from crewai.tools.tool_failure import ToolFailure from crewai.utilities.pydantic_schema_utils import create_model_from_schema -from pydantic import Field, create_model -import requests +from crewai.utilities.string_utils import sanitize_tool_name +from pydantic import Field, PrivateAttr, create_model -from crewai_tools.tools.crewai_platform_tools.misc import ( - get_platform_api_base_url, - get_platform_integration_token, +from crewai_tools.tools.crewai_platform_tools._client import ( + _PlatformToolInfo, + _PlatformToolsClient, ) class CrewAIPlatformActionTool(BaseTool): + _client: _PlatformToolsClient = PrivateAttr() + _tool_info: _PlatformToolInfo = PrivateAttr() app: str = Field(description="The integration slug for this action") - action_name: str = Field(default="", description="The name of the action") - action_schema: dict[str, Any] = Field( - default_factory=dict, description="The schema of the action" - ) def __init__( self, - description: str, - app: str, - action_name: str, - action_schema: dict[str, Any], - ): - parameters = action_schema.get("function", {}).get("parameters", {}) + tool_info: _PlatformToolInfo, + client: _PlatformToolsClient, + ) -> None: + action = tool_info.action + parameters = tool_info.parameters if parameters and parameters.get("properties"): try: if "title" not in parameters: - parameters = {**parameters, "title": f"{action_name}Schema"} + parameters = { + **parameters, + "title": f"{action}Schema", + } if "type" not in parameters: parameters = {**parameters, "type": "object"} args_schema = create_model_from_schema(parameters) except Exception: - args_schema = create_model(f"{action_name}Schema") + args_schema = create_model(f"{action}Schema") else: - args_schema = create_model(f"{action_name}Schema") + args_schema = create_model(f"{action}Schema") + + name_parts = [ + tool_info.app, + action, + str(tool_info.connection_id) if tool_info.connection_id else None, + ] + name = sanitize_tool_name("_".join(part for part in name_parts if part)) super().__init__( - name=action_name.lower().replace(" ", "_"), - description=description, + name=name, + description=tool_info.description, args_schema=args_schema, - app=app, + app=tool_info.app, ) - self.action_name = action_name - self.action_schema = action_schema + self._tool_info = tool_info + self._client = client def _run(self, **kwargs: Any) -> Any: try: @@ -59,26 +65,10 @@ class CrewAIPlatformActionTool(BaseTool): key: value for key, value in kwargs.items() if value is not None } - api_url = ( - f"{get_platform_api_base_url()}/actions/{self.action_name}/execute" + response = self._client.execute_action( + tool_info=self._tool_info, + arguments=cleaned_kwargs, ) - token = get_platform_integration_token() - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - payload = { - "integration": cleaned_kwargs if cleaned_kwargs else {"_noop": True} - } - - response = requests.post( - url=api_url, - headers=headers, - json=payload, - timeout=60, - verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true", - ) - data = response.json() if not response.ok: if isinstance(data, dict): @@ -89,20 +79,18 @@ class CrewAIPlatformActionTool(BaseTool): error_message = str(error_info) else: error_message = str(data) - # A non-2xx here means the upstream app rejected the action - # (e.g. Slack's channel_not_found) -- report it, not prose. return ToolFailure( message=f"API request failed: {error_message}", code=str(response.status_code), retryable=response.status_code >= 500, - details={"action": self.action_name}, + details={"action": self._tool_info.action}, ) return json.dumps(data, indent=2) except Exception as e: return ToolFailure( - message=f"Error executing action {self.action_name}: {e!s}", + message=f"Error executing action {self._tool_info.action}: {e!s}", code=e.__class__.__name__, - details={"action": self.action_name}, + details={"action": self._tool_info.action}, ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py deleted file mode 100644 index 23b6b9293..000000000 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py +++ /dev/null @@ -1,118 +0,0 @@ -"""CrewAI platform tool builder for fetching and creating action tools.""" - -import logging -import os -from types import TracebackType -from typing import Any - -from crewai.tools import BaseTool -import requests - -from crewai_tools.tools.crewai_platform_tools.application_selector import ( - ApplicationSelector, -) -from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( - CrewAIPlatformActionTool, -) -from crewai_tools.tools.crewai_platform_tools.misc import ( - get_platform_api_base_url, - get_platform_integration_token, -) - - -logger = logging.getLogger(__name__) - - -class CrewaiPlatformToolBuilder: - """Builds platform tools from remote action schemas.""" - - def __init__( - self, - apps: list[str], - ) -> None: - self._apps = [ApplicationSelector(app) for app in apps] - self._actions_schema: dict[str, dict[str, Any]] = {} - self._tools: list[BaseTool] | None = None - - def tools(self) -> list[BaseTool]: - """Fetch actions and return built tools.""" - if self._tools is None: - self._fetch_actions() - self._create_tools() - return self._tools if self._tools is not None else [] - - def _fetch_actions(self) -> None: - """Fetch action schemas from the platform API.""" - actions_url = f"{get_platform_api_base_url()}/actions" - headers = {"Authorization": f"Bearer {get_platform_integration_token()}"} - apps = [ - f"{app.name}/{app.action}" if app.action is not None else app.name - for app in self._apps - ] - - try: - response = requests.get( - actions_url, - headers=headers, - timeout=30, - params={"apps": ",".join(apps)}, - verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true", - ) - response.raise_for_status() - except Exception as e: - logger.error(f"Failed to fetch platform tools for apps {apps}: {e}") - return - - raw_data = response.json() - - self._actions_schema = {} - action_categories = raw_data.get("actions", {}) - - for app, action_list in action_categories.items(): - if isinstance(action_list, list): - for action in action_list: - if not isinstance(action, dict): - continue - if action_name := action.get("name"): - action_schema = { - "function": { - "name": action_name, - "description": action.get( - "description", f"Execute {action_name}" - ), - "parameters": action.get("parameters", {}), - "app": app, - } - } - self._actions_schema[action_name] = action_schema - - def _create_tools(self) -> None: - """Create tool instances from fetched action schemas.""" - tools: list[BaseTool] = [] - - for action_name, action_schema in self._actions_schema.items(): - function_details = action_schema.get("function", {}) - description = function_details.get("description", f"Execute {action_name}") - - tool = CrewAIPlatformActionTool( - description=description, - app=function_details["app"], - action_name=action_name, - action_schema=action_schema, - ) - - tools.append(tool) - - self._tools = tools - - def __enter__(self) -> list[BaseTool]: - """Enter context manager and return tools.""" - return self.tools() - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Exit context manager.""" diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py index 83016ddb8..190b99f0f 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py @@ -1,14 +1,13 @@ -import logging - from crewai.tools import BaseTool from crewai_tools.adapters.tool_collection import ToolCollection -from crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder import ( - CrewaiPlatformToolBuilder, +from crewai_tools.tools.crewai_platform_tools._client import ( + _PlatformToolSelector, + _PlatformToolsClient, +) +from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( + CrewAIPlatformActionTool, ) - - -logger = logging.getLogger(__name__) def CrewaiPlatformTools( # noqa: N802 @@ -22,6 +21,14 @@ def CrewaiPlatformTools( # noqa: N802 Returns: A list of BaseTool instances for platform actions """ - builder = CrewaiPlatformToolBuilder(apps=apps) + client = _PlatformToolsClient() + selectors = [_PlatformToolSelector.from_string(app) for app in apps] - return builder.tools() # type: ignore + tool_infos = client.get_tools(selectors) + + return ToolCollection( + [ + CrewAIPlatformActionTool(tool_info=tool_info, client=client) + for tool_info in tool_infos + ] + ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/misc.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/misc.py index 06cf7147d..60b87595c 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/misc.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/misc.py @@ -1,12 +1,6 @@ import os -def get_platform_api_base_url() -> str: - """Get the platform API base URL from environment or use default.""" - base_url = os.getenv("CREWAI_PLUS_URL", "https://app.crewai.com") - return f"{base_url}/crewai_plus/api/v1/integrations" - - def get_platform_integration_token() -> str: """Get the platform API base URL from environment or use default.""" token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN") or "" diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_client.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_client.py new file mode 100644 index 000000000..6ba0ed01f --- /dev/null +++ b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_client.py @@ -0,0 +1,247 @@ +from unittest.mock import Mock, patch + +import pytest +import requests + +from crewai_tools.tools.crewai_platform_tools._client import ( + _PlatformToolInfo, + _PlatformToolSelector, + _PlatformToolsClient, +) + + +@patch.dict( + "os.environ", + { + "CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", + "CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment_uuid", + }, + clear=True, +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") +def test_client_resolves_tool_info(mock_get): + github_response = Mock() + github_response.raise_for_status.return_value = None + github_response.json.return_value = { + "data": [ + { + "slug": "create_issue", + "description": "Create a GitHub issue", + "input_schema": { + "type": "object", + "properties": {"title": {"type": "string"}}, + }, + } + ] + } + slack_response = Mock() + slack_response.raise_for_status.return_value = None + slack_response.json.return_value = { + "data": { + "slug": "send_message", + "description": "Send a Slack message", + "input_schema": {}, + } + } + mock_get.side_effect = [github_response, slack_response] + selectors = [ + _PlatformToolSelector.from_string( + "github@550e8400-e29b-41d4-a716-446655440000" + ), + _PlatformToolSelector.from_string( + "slack/send_message@67e55044-10b1-426f-9247-bb680e5fe0c8" + ), + ] + + resolved = _PlatformToolsClient().get_tools(selectors) + + assert [call.kwargs for call in mock_get.call_args_list] == [ + { + "headers": { + "Authorization": "Bearer test_token", + "X-Crewai-Deployment-Instance-Id": "deployment_uuid", + }, + "timeout": 30, + "params": { + "connection_id": "550e8400-e29b-41d4-a716-446655440000" + }, + "verify": True, + }, + { + "headers": { + "Authorization": "Bearer test_token", + "X-Crewai-Deployment-Instance-Id": "deployment_uuid", + }, + "timeout": 30, + "params": { + "connection_id": "67e55044-10b1-426f-9247-bb680e5fe0c8" + }, + "verify": True, + }, + ] + assert [call.args[0] for call in mock_get.call_args_list] == [ + "https://app.crewai.com/clipper/v1/applications/github/tools", + "https://app.crewai.com/clipper/v1/applications/slack/tools/send_message", + ] + assert resolved == [ + _PlatformToolInfo( + app="github", + action="create_issue", + connection_id=selectors[0].connection_id, + description="Create a GitHub issue", + parameters={ + "type": "object", + "properties": {"title": {"type": "string"}}, + }, + ), + _PlatformToolInfo( + app="slack", + action="send_message", + connection_id=selectors[1].connection_id, + description="Send a Slack message", + parameters={}, + ), + ] + + +@patch.dict( + "os.environ", + {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, + clear=True, +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") +def test_client_accepts_empty_action_list(mock_get): + response = Mock() + response.raise_for_status.return_value = None + response.json.return_value = {"data": []} + mock_get.return_value = response + + resolved = _PlatformToolsClient().get_tools( + [_PlatformToolSelector.from_string("github")] + ) + + assert resolved == [] + + +@patch.dict( + "os.environ", + {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, + clear=True, +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") +def test_client_keeps_tools_when_one_selector_is_unavailable(mock_get): + github_response = Mock() + github_response.raise_for_status.return_value = None + github_response.json.return_value = { + "data": [ + { + "slug": "create_issue", + "description": "Create a GitHub issue", + "input_schema": {}, + } + ] + } + unavailable_response = Mock() + unavailable_response.raise_for_status.side_effect = requests.HTTPError( + "Application unavailable" + ) + slack_response = Mock() + slack_response.raise_for_status.return_value = None + slack_response.json.return_value = { + "data": [ + { + "slug": "send_message", + "description": "Send a Slack message", + "input_schema": {}, + } + ] + } + mock_get.side_effect = [github_response, unavailable_response, slack_response] + + resolved = _PlatformToolsClient().get_tools( + [ + _PlatformToolSelector.from_string("github"), + _PlatformToolSelector.from_string("unavailable"), + _PlatformToolSelector.from_string("slack"), + ] + ) + + assert resolved == [ + _PlatformToolInfo( + app="github", + action="create_issue", + connection_id=None, + description="Create a GitHub issue", + parameters={}, + ), + _PlatformToolInfo( + app="slack", + action="send_message", + connection_id=None, + description="Send a Slack message", + parameters={}, + ), + ] + + +@pytest.mark.parametrize( + ("factory_value", "expected"), + [ + (None, True), + ("false", True), + ("FALSE", True), + ("true", False), + ("TRUE", False), + ], +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") +def test_client_ssl_verification(mock_get, monkeypatch, factory_value, expected): + monkeypatch.setenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "test_token") + if factory_value is None: + monkeypatch.delenv("CREWAI_FACTORY", raising=False) + else: + monkeypatch.setenv("CREWAI_FACTORY", factory_value) + response = Mock() + response.raise_for_status.return_value = None + response.json.return_value = {"data": []} + mock_get.return_value = response + + _PlatformToolsClient().get_tools( + [_PlatformToolSelector.from_string("github")] + ) + + assert mock_get.call_args.kwargs["verify"] is expected + + +def test_client_requires_token(monkeypatch): + monkeypatch.delenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", raising=False) + + with pytest.raises(ValueError, match="No platform integration token found"): + _PlatformToolsClient().get_tools( + [_PlatformToolSelector.from_string("github")] + ) + + +@pytest.mark.parametrize( + ("selector", "message"), + [ + ("", "cannot be empty"), + ( + "@550e8400-e29b-41d4-a716-446655440000", + "application cannot be empty", + ), + ("github/", "action cannot be empty"), + ("github@", "connection ID cannot be empty"), + ("github@not-a-uuid", "connection ID must be a valid UUID"), + ( + "github@550e8400-e29b-41d4-a716-446655440000/issues", + "connection ID must be the last segment", + ), + ], +) +def test_rejects_invalid_selector(selector, message): + with pytest.raises(ValueError) as error: + _PlatformToolSelector.from_string(selector) + + assert repr(selector) in str(error.value) + assert message in str(error.value) diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py index 7737baf80..7e03d4e45 100644 --- a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py +++ b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py @@ -1,114 +1,100 @@ -from unittest.mock import patch, Mock -import os +from unittest.mock import Mock, patch -from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( - CrewAIPlatformActionTool, +import pytest + +from crewai_tools.tools.crewai_platform_tools._client import ( + _PlatformToolInfo, + _PlatformToolsClient, ) -class TestCrewAIPlatformActionToolVerify: - """Test suite for SSL verification behavior based on CREWAI_FACTORY environment variable""" +TOOL_INFO = _PlatformToolInfo( + app="test_app", + action="test_action", + connection_id=None, + description="Test action tool", + parameters={}, +) - def setup_method(self): - self.action_schema = { - "function": { - "name": "test_action", - "parameters": { - "properties": { - "test_param": { - "type": "string", - "description": "Test parameter" - } - }, - "required": [] - } - } - } - def create_test_tool(self): - return CrewAIPlatformActionTool( - description="Test action tool", - app="test_app", - action_name="test_action", - action_schema=self.action_schema - ) +@patch.dict( + "os.environ", + { + "CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", + "CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment_uuid", + }, + clear=True, +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") +def test_client_executes_action(mock_post): + response = Mock(ok=True, status_code=200) + response.json.return_value = {"result": "success"} + mock_post.return_value = response - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True) - @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") - def test_run_with_ssl_verification_default(self, mock_post): - """Test that _run uses SSL verification by default when CREWAI_FACTORY is not set""" - mock_response = Mock() - mock_response.ok = True - mock_response.json.return_value = {"result": "success"} - mock_post.return_value = mock_response + result = _PlatformToolsClient().execute_action( + TOOL_INFO, + {"test_param": "test_value"}, + ) - tool = self.create_test_tool() - tool._run(test_param="test_value") + mock_post.assert_called_once_with( + url=( + "https://app.crewai.com/clipper/v1/applications/" + "test_app/tools/test_action/execute" + ), + headers={ + "Authorization": "Bearer test_token", + "X-Crewai-Deployment-Instance-Id": "deployment_uuid", + "Content-Type": "application/json", + }, + json={"arguments": {"test_param": "test_value"}}, + timeout=60, + verify=True, + ) + assert result is response - mock_post.assert_called_once() - call_args = mock_post.call_args - assert call_args.kwargs["verify"] is True - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True) - @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") - def test_run_with_ssl_verification_factory_false(self, mock_post): - """Test that _run uses SSL verification when CREWAI_FACTORY is 'false'""" - mock_response = Mock() - mock_response.ok = True - mock_response.json.return_value = {"result": "success"} - mock_post.return_value = mock_response +@patch.dict( + "os.environ", + {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, + clear=True, +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") +def test_client_returns_action_error_response(mock_post): + response = Mock(ok=False, status_code=500) + response.json.return_value = {"error": "Action failed"} + mock_post.return_value = response - tool = self.create_test_tool() - tool._run(test_param="test_value") + result = _PlatformToolsClient().execute_action(TOOL_INFO, {}) - mock_post.assert_called_once() - call_args = mock_post.call_args - assert call_args.kwargs["verify"] is True + assert result is response - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True) - @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") - def test_run_with_ssl_verification_factory_false_uppercase(self, mock_post): - """Test that _run uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)""" - mock_response = Mock() - mock_response.ok = True - mock_response.json.return_value = {"result": "success"} - mock_post.return_value = mock_response - tool = self.create_test_tool() - tool._run(test_param="test_value") +@pytest.mark.parametrize( + ("factory_value", "expected"), + [ + (None, True), + ("false", True), + ("FALSE", True), + ("true", False), + ("TRUE", False), + ], +) +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") +def test_client_ssl_verification( + mock_post, + monkeypatch, + factory_value, + expected, +): + monkeypatch.setenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "test_token") + if factory_value is None: + monkeypatch.delenv("CREWAI_FACTORY", raising=False) + else: + monkeypatch.setenv("CREWAI_FACTORY", factory_value) + response = Mock(ok=True, status_code=200) + response.json.return_value = {"result": "success"} + mock_post.return_value = response - mock_post.assert_called_once() - call_args = mock_post.call_args - assert call_args.kwargs["verify"] is True + _PlatformToolsClient().execute_action(TOOL_INFO, {}) - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True) - @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") - def test_run_without_ssl_verification_factory_true(self, mock_post): - """Test that _run disables SSL verification when CREWAI_FACTORY is 'true'""" - mock_response = Mock() - mock_response.ok = True - mock_response.json.return_value = {"result": "success"} - mock_post.return_value = mock_response - - tool = self.create_test_tool() - tool._run(test_param="test_value") - - mock_post.assert_called_once() - call_args = mock_post.call_args - assert call_args.kwargs["verify"] is False - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True) - @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") - def test_run_without_ssl_verification_factory_true_uppercase(self, mock_post): - """Test that _run disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)""" - mock_response = Mock() - mock_response.ok = True - mock_response.json.return_value = {"result": "success"} - mock_post.return_value = mock_response - - tool = self.create_test_tool() - tool._run(test_param="test_value") - - mock_post.assert_called_once() - call_args = mock_post.call_args - assert call_args.kwargs["verify"] is False + assert mock_post.call_args.kwargs["verify"] is expected diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py deleted file mode 100644 index e66022792..000000000 --- a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py +++ /dev/null @@ -1,375 +0,0 @@ -import unittest -from unittest.mock import Mock, patch - -from crewai_tools.tools.crewai_platform_tools import ( - CrewAIPlatformActionTool, - CrewaiPlatformToolBuilder, -) -import pytest - - -class TestCrewaiPlatformToolBuilder(unittest.TestCase): - @pytest.fixture - def platform_tool_builder(self): - """Create a CrewaiPlatformToolBuilder instance for testing""" - return CrewaiPlatformToolBuilder(apps=["github", "slack"]) - - @pytest.fixture - def mock_api_response(self): - return { - "actions": { - "github": [ - { - "name": "create_issue", - "description": "Create a GitHub issue", - "parameters": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Issue title", - }, - "body": {"type": "string", "description": "Issue body"}, - }, - "required": ["title"], - }, - } - ], - "slack": [ - { - "name": "send_message", - "description": "Send a Slack message", - "parameters": { - "type": "object", - "properties": { - "channel": { - "type": "string", - "description": "Channel name", - }, - "text": { - "type": "string", - "description": "Message text", - }, - }, - "required": ["channel", "text"], - }, - } - ], - } - } - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_success(self, mock_get): - mock_api_response = { - "actions": { - "github": [ - { - "name": "create_issue", - "description": "Create a GitHub issue", - "parameters": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Issue title", - } - }, - "required": ["title"], - }, - } - ] - } - } - - builder = CrewaiPlatformToolBuilder( - apps=["github", "slack/send_message", "custom/path/to/action"] - ) - - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = mock_api_response - mock_get.return_value = mock_response - - builder._fetch_actions() - - mock_get.assert_called_once() - args, kwargs = mock_get.call_args - - assert "/actions" in args[0] - assert kwargs["headers"]["Authorization"] == "Bearer test_token" - assert kwargs["params"]["apps"] == ( - "github,slack/send_message,custom/path/to/action" - ) - - assert "create_issue" in builder._actions_schema - assert ( - builder._actions_schema["create_issue"]["function"]["name"] - == "create_issue" - ) - - def test_fetch_actions_no_token(self): - builder = CrewaiPlatformToolBuilder(apps=["github"]) - - with patch.dict("os.environ", {}, clear=True): - with self.assertRaises(ValueError) as context: - builder._fetch_actions() - assert "No platform integration token found" in str(context.exception) - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_create_tools(self, mock_get): - mock_api_response = { - "actions": { - "github": [ - { - "name": "create_issue", - "description": "Create a GitHub issue", - "parameters": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Issue title", - } - }, - "required": ["title"], - }, - } - ], - "slack": [ - { - "name": "send_message", - "description": "Send a Slack message", - "parameters": { - "type": "object", - "properties": { - "channel": { - "type": "string", - "description": "Channel name", - } - }, - "required": ["channel"], - }, - } - ], - } - } - - builder = CrewaiPlatformToolBuilder(apps=["github", "slack"]) - - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = mock_api_response - mock_get.return_value = mock_response - - tools = builder.tools() - - assert len(tools) == 2 - assert all(isinstance(tool, CrewAIPlatformActionTool) for tool in tools) - - tool_names = [tool.action_name for tool in tools] - assert "create_issue" in tool_names - assert "send_message" in tool_names - assert {tool.action_name: tool.app for tool in tools} == { - "create_issue": "github", - "send_message": "slack", - } - - github_tool = next((t for t in tools if t.action_name == "create_issue"), None) - slack_tool = next((t for t in tools if t.action_name == "send_message"), None) - - assert github_tool is not None - assert slack_tool is not None - assert "Create a GitHub issue" in github_tool.description - assert "Send a Slack message" in slack_tool.description - - def test_tools_caching(self): - builder = CrewaiPlatformToolBuilder(apps=["github"]) - - cached_tools = [] - - def mock_create_tools(): - builder._tools = cached_tools - - with ( - patch.object(builder, "_fetch_actions") as mock_fetch, - patch.object( - builder, "_create_tools", side_effect=mock_create_tools - ) as mock_create, - ): - tools1 = builder.tools() - assert mock_fetch.call_count == 1 - assert mock_create.call_count == 1 - - tools2 = builder.tools() - assert mock_fetch.call_count == 1 - assert mock_create.call_count == 1 - - assert tools1 is tools2 - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) - def test_empty_apps_list(self): - builder = CrewaiPlatformToolBuilder(apps=[]) - - with patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) as mock_get: - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - tools = builder.tools() - - assert isinstance(tools, list) - assert len(tools) == 0 - - _, kwargs = mock_get.call_args - assert kwargs["params"]["apps"] == "" - -class TestCrewaiPlatformToolBuilderVerify(unittest.TestCase): - """Test suite for SSL verification behavior in CrewaiPlatformToolBuilder""" - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_with_ssl_verification_default(self, mock_get): - """Test that _fetch_actions uses SSL verification by default when CREWAI_FACTORY is not set""" - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder(apps=["github"]) - builder._fetch_actions() - - mock_get.assert_called_once() - call_args = mock_get.call_args - assert call_args.kwargs["verify"] is True - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_with_ssl_verification_factory_false(self, mock_get): - """Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'false'""" - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder(apps=["github"]) - builder._fetch_actions() - - mock_get.assert_called_once() - call_args = mock_get.call_args - assert call_args.kwargs["verify"] is True - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_with_ssl_verification_factory_false_uppercase(self, mock_get): - """Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)""" - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder(apps=["github"]) - builder._fetch_actions() - - mock_get.assert_called_once() - call_args = mock_get.call_args - assert call_args.kwargs["verify"] is True - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_without_ssl_verification_factory_true(self, mock_get): - """Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'true'""" - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder(apps=["github"]) - builder._fetch_actions() - - mock_get.assert_called_once() - call_args = mock_get.call_args - assert call_args.kwargs["verify"] is False - - @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True) - @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" - ) - def test_fetch_actions_without_ssl_verification_factory_true_uppercase(self, mock_get): - """Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)""" - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder(apps=["github"]) - builder._fetch_actions() - - mock_get.assert_called_once() - call_args = mock_get.call_args - assert call_args.kwargs["verify"] is False - - -@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) -@patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" -) -def test_connection_ids_are_parsed_but_not_sent(mock_get): - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} - mock_get.return_value = mock_response - - builder = CrewaiPlatformToolBuilder( - apps=[ - "github@550E8400-E29B-41D4-A716-446655440000", - "slack/send_message@67e55044-10b1-426f-9247-bb680e5fe0c8", - ] - ) - - builder.tools() - - assert mock_get.call_args.kwargs["params"]["apps"] == ( - "github,slack/send_message" - ) - - -@pytest.mark.parametrize( - ("selector", "message"), - [ - ("", "cannot be empty"), - ( - "@550e8400-e29b-41d4-a716-446655440000", - "application cannot be empty", - ), - ("github/", "action cannot be empty"), - ("github@", "connection ID cannot be empty"), - ("github@not-a-uuid", "connection ID must be a valid UUID"), - ( - "github@550e8400-e29b-41d4-a716-446655440000/issues", - "connection ID must be the last segment", - ), - ], -) -def test_rejects_invalid_app_selector(selector, message): - with pytest.raises(ValueError) as error: - CrewaiPlatformToolBuilder(apps=[selector]) - - assert repr(selector) in str(error.value) - assert message in str(error.value) diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py index b69b073ed..f17a098e3 100644 --- a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py +++ b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py @@ -1,18 +1,20 @@ import unittest from unittest.mock import Mock, patch +import requests + from crewai_tools.tools.crewai_platform_tools import CrewaiPlatformTools class TestCrewaiPlatformTools(unittest.TestCase): @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" + "crewai_tools.tools.crewai_platform_tools._client.requests.get" ) def test_crewai_platform_tools_basic(self, mock_get): mock_response = Mock() mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {"github": []}} + mock_response.json.return_value = {"data": []} mock_get.return_value = mock_response tools = CrewaiPlatformTools(apps=["github"]) @@ -21,18 +23,17 @@ class TestCrewaiPlatformTools(unittest.TestCase): @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" + "crewai_tools.tools.crewai_platform_tools._client.requests.get" ) def test_crewai_platform_tools_multiple_apps(self, mock_get): - mock_response = Mock() - mock_response.raise_for_status.return_value = None - mock_response.json.return_value = { - "actions": { - "github": [ + github_response = Mock() + github_response.raise_for_status.return_value = None + github_response.json.return_value = { + "data": [ { - "name": "create_issue", + "slug": "create_issue", "description": "Create a GitHub issue", - "parameters": { + "input_schema": { "type": "object", "properties": { "title": { @@ -44,12 +45,16 @@ class TestCrewaiPlatformTools(unittest.TestCase): "required": ["title"], }, } - ], - "slack": [ + ] + } + slack_response = Mock() + slack_response.raise_for_status.return_value = None + slack_response.json.return_value = { + "data": [ { - "name": "send_message", + "slug": "send_message", "description": "Send a Slack message", - "parameters": { + "input_schema": { "type": "object", "properties": { "channel": { @@ -64,31 +69,151 @@ class TestCrewaiPlatformTools(unittest.TestCase): "required": ["channel", "text"], }, } - ], - } + ] } - mock_get.return_value = mock_response + mock_get.side_effect = [github_response, slack_response] tools = CrewaiPlatformTools(apps=["github", "slack"]) assert tools is not None assert isinstance(tools, list) assert len(tools) == 2 - mock_get.assert_called_once() - args, kwargs = mock_get.call_args - assert ( - "apps=github,slack" in args[0] - or kwargs.get("params", {}).get("apps") == "github,slack" + assert [call.args[0].split("/clipper", 1)[1] for call in mock_get.call_args_list] == [ + "/v1/applications/github/tools", + "/v1/applications/slack/tools", + ] + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") + def test_keeps_same_named_actions_from_different_apps(self, mock_get): + github_response = Mock() + github_response.raise_for_status.return_value = None + github_response.json.return_value = { + "data": [{"slug": "search", "description": "Search", "input_schema": {}}] + } + slack_response = Mock() + slack_response.raise_for_status.return_value = None + slack_response.json.return_value = { + "data": [{"slug": "search", "description": "Search", "input_schema": {}}] + } + mock_get.side_effect = [github_response, slack_response] + + tools = CrewaiPlatformTools(apps=["github", "slack"]) + + assert tools["github_search"].app == "github" + assert tools["slack_search"].app == "slack" + + @patch.dict( + "os.environ", + { + "CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", + "CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment_uuid", + }, + clear=True, + ) + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") + def test_executes_same_action_for_each_app(self, mock_get, mock_post): + github_response = Mock() + github_response.raise_for_status.return_value = None + github_response.json.return_value = { + "data": { + "slug": "search", + "description": "Search", + "input_schema": { + "properties": {"query": {"type": "string"}} + }, + } + } + slack_response = Mock() + slack_response.raise_for_status.return_value = None + slack_response.json.return_value = github_response.json.return_value + mock_get.side_effect = [github_response, slack_response] + response = Mock(ok=True, status_code=200) + response.json.return_value = {"data": {"output": []}} + mock_post.return_value = response + + tools = CrewaiPlatformTools(apps=["github/search", "slack/search"]) + tools["github_search"].run(query="release") + tools["slack_search"].run(query="release") + + assert [call.kwargs["url"] for call in mock_post.call_args_list] == [ + "https://app.crewai.com/clipper/v1/applications/github/tools/search/execute", + "https://app.crewai.com/clipper/v1/applications/slack/tools/search/execute", + ] + + @patch.dict( + "os.environ", + { + "CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", + "CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment_uuid", + }, + clear=True, + ) + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") + def test_executes_same_action_for_each_connection(self, mock_get, mock_post): + response = Mock() + response.raise_for_status.return_value = None + response.json.return_value = { + "data": { + "slug": "search", + "description": "Search", + "input_schema": { + "properties": {"query": {"type": "string"}} + }, + } + } + mock_get.return_value = response + execution_response = Mock(ok=True, status_code=200) + execution_response.json.return_value = {"data": {"output": []}} + mock_post.return_value = execution_response + connection_ids = [ + "550e8400-e29b-41d4-a716-446655440000", + "67e55044-10b1-426f-9247-bb680e5fe0c8", + ] + + tools = CrewaiPlatformTools( + apps=[f"github/search@{connection_id}" for connection_id in connection_ids] ) + for tool in tools: + tool.run(query="release") + + assert len(tools) == 2 + assert len({tool.name for tool in tools}) == 2 + assert all(tools[tool.name] is tool for tool in tools) + assert [call.kwargs["json"] for call in mock_post.call_args_list] == [ + { + "arguments": {"query": "release"}, + "connection_id": connection_id, + } + for connection_id in connection_ids + ] + + @patch.dict( + "os.environ", + {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, + clear=True, + ) + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") + def test_invalid_discovery_response_returns_empty_collection(self, mock_get): + response = Mock() + response.raise_for_status.return_value = None + response.json.side_effect = ValueError("Invalid JSON") + mock_get.return_value = response + + tools = CrewaiPlatformTools(apps=["github"]) + + assert tools == [] @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) def test_crewai_platform_tools_empty_apps(self): with patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" + "crewai_tools.tools.crewai_platform_tools._client.requests.get" ) as mock_get: mock_response = Mock() mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"actions": {}} + mock_response.json.return_value = {"data": []} mock_get.return_value = mock_response tools = CrewaiPlatformTools(apps=[]) @@ -98,16 +223,26 @@ class TestCrewaiPlatformTools(unittest.TestCase): @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) @patch( - "crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get" + "crewai_tools.tools.crewai_platform_tools._client.requests.get" ) def test_crewai_platform_tools_api_error_handling(self, mock_get): - mock_get.side_effect = Exception("API Error") + mock_get.side_effect = requests.RequestException("API Error") tools = CrewaiPlatformTools(apps=["github"]) assert tools is not None assert isinstance(tools, list) assert len(tools) == 0 + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") + def test_crewai_platform_tools_handles_unexpected_fetch_error(self, mock_get): + mock_get.side_effect = RuntimeError("API Error") + + tools = CrewaiPlatformTools(apps=["github"]) + + assert isinstance(tools, list) + assert len(tools) == 0 + def test_crewai_platform_tools_no_token(self): with patch.dict("os.environ", {}, clear=True): with self.assertRaises(ValueError) as context: diff --git a/lib/crewai/tests/agents/test_lite_agent.py b/lib/crewai/tests/agents/test_lite_agent.py index db3686a6a..60c9d2b5c 100644 --- a/lib/crewai/tests/agents/test_lite_agent.py +++ b/lib/crewai/tests/agents/test_lite_agent.py @@ -612,30 +612,27 @@ def test_lite_agent_with_invalid_llm(): @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) -@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") -@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") -@pytest.mark.vcr() +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.post") +@patch("crewai_tools.tools.crewai_platform_tools._client.requests.get") def test_agent_kickoff_with_platform_tools(mock_get, mock_post): """Test that Agent.kickoff() properly integrates platform tools with LiteAgent""" mock_response = Mock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = { - "actions": { - "github": [ - { - "name": "create_issue", - "description": "Create a GitHub issue", - "parameters": { - "type": "object", - "properties": { - "title": {"type": "string", "description": "Issue title"}, - "body": {"type": "string", "description": "Issue body"}, - }, - "required": ["title"], + "data": [ + { + "slug": "create_issue", + "description": "Create a GitHub issue", + "input_schema": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Issue title"}, + "body": {"type": "string", "description": "Issue body"}, }, - } - ] - } + "required": ["title"], + }, + } + ] } mock_get.return_value = mock_response @@ -647,11 +644,22 @@ def test_agent_kickoff_with_platform_tools(mock_get, mock_post): } mock_post.return_value = mock_post_response + mock_llm = Mock(spec=LLM) + mock_llm.call.side_effect = [ + "Thought: Create the issue\n" + "Action: github_create_issue\n" + 'Action Input: {"title": "Test GitHub Issue", "body": "Test body"}', + "Thought: The issue was created\nFinal Answer: Issue created", + ] + mock_llm.supports_function_calling.return_value = False + mock_llm.stop = [] + mock_llm.get_token_usage_summary.return_value = UsageMetrics() + agent = Agent( role="Test Agent", goal="Test goal", backstory="Test backstory", - llm=LLM(model="gpt-3.5-turbo"), + llm=mock_llm, apps=["github"], verbose=True ) @@ -660,6 +668,7 @@ def test_agent_kickoff_with_platform_tools(mock_get, mock_post): assert isinstance(result, LiteAgentOutput) assert result.raw is not None + mock_post.assert_called_once() @patch.dict("os.environ", {"EXA_API_KEY": "test_exa_key"}) diff --git a/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml b/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml deleted file mode 100644 index 7a64787cc..000000000 --- a/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml +++ /dev/null @@ -1,344 +0,0 @@ -interactions: -- request: - body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour - personal goal is: Test goal"},{"role":"user","content":"\nCurrent Task: Create - a GitHub issue"}],"model":"gpt-3.5-turbo","tool_choice":"auto","tools":[{"type":"function","function":{"name":"create_issue","description":"Create - a GitHub issue","strict":true,"parameters":{"additionalProperties":false,"properties":{"title":{"description":"Issue - title","title":"Title","type":"string"},"body":{"default":null,"description":"Issue - body","title":"Body","type":"string"}},"required":["title","body"],"type":"object"}}}]}' - headers: - User-Agent: - - X-USER-AGENT-XXX - accept: - - application/json - accept-encoding: - - ACCEPT-ENCODING-XXX - authorization: - - AUTHORIZATION-XXX - connection: - - keep-alive - content-length: - - '596' - content-type: - - application/json - host: - - api.openai.com - x-stainless-arch: - - X-STAINLESS-ARCH-XXX - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - X-STAINLESS-OS-XXX - x-stainless-package-version: - - 1.83.0 - x-stainless-read-timeout: - - X-STAINLESS-READ-TIMEOUT-XXX - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.13.3 - method: POST - uri: https://api.openai.com/v1/chat/completions - response: - body: - string: "{\n \"id\": \"chatcmpl-D8D6N1Fkz1at9vvgiZp4cYgerJbuU\",\n \"object\": - \"chat.completion\",\n \"created\": 1770849359,\n \"model\": \"gpt-3.5-turbo-0125\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n - \ \"id\": \"call_lLWMJ8icAP8pCVCANTDjIl9s\",\n \"type\": - \"function\",\n \"function\": {\n \"name\": \"create_issue\",\n - \ \"arguments\": \"{\\\"title\\\":\\\"Test GitHub Issue\\\",\\\"body\\\":\\\"This - is a test GitHub issue created for testing purposes.\\\"}\"\n }\n - \ }\n ],\n \"refusal\": null,\n \"annotations\": - []\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n - \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 93,\n \"completion_tokens\": - 30,\n \"total_tokens\": 123,\n \"prompt_tokens_details\": {\n \"cached_tokens\": - 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": - {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": - 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": null\n}\n" - headers: - CF-RAY: - - CF-RAY-XXX - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 11 Feb 2026 22:36:00 GMT - Server: - - cloudflare - Strict-Transport-Security: - - STS-XXX - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - X-CONTENT-TYPE-XXX - access-control-expose-headers: - - ACCESS-CONTROL-XXX - alt-svc: - - h3=":443"; ma=86400 - cf-cache-status: - - DYNAMIC - openai-organization: - - OPENAI-ORG-XXX - openai-processing-ms: - - '2999' - openai-project: - - OPENAI-PROJECT-XXX - openai-version: - - '2020-10-01' - set-cookie: - - SET-COOKIE-XXX - x-openai-proxy-wasm: - - v0.1 - x-ratelimit-limit-requests: - - X-RATELIMIT-LIMIT-REQUESTS-XXX - x-ratelimit-limit-tokens: - - X-RATELIMIT-LIMIT-TOKENS-XXX - x-ratelimit-remaining-requests: - - X-RATELIMIT-REMAINING-REQUESTS-XXX - x-ratelimit-remaining-tokens: - - X-RATELIMIT-REMAINING-TOKENS-XXX - x-ratelimit-reset-requests: - - X-RATELIMIT-RESET-REQUESTS-XXX - x-ratelimit-reset-tokens: - - X-RATELIMIT-RESET-TOKENS-XXX - x-request-id: - - X-REQUEST-ID-XXX - status: - code: 200 - message: OK -- request: - body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour - personal goal is: Test goal"},{"role":"user","content":"\nCurrent Task: Create - a GitHub issue"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_lLWMJ8icAP8pCVCANTDjIl9s","type":"function","function":{"name":"create_issue","arguments":"{\"title\":\"Test - GitHub Issue\",\"body\":\"This is a test GitHub issue created for testing purposes.\"}"}}]},{"role":"tool","tool_call_id":"call_lLWMJ8icAP8pCVCANTDjIl9s","name":"create_issue","content":"{\n \"success\": - true,\n \"issue_url\": \"https://github.com/test/repo/issues/1\"\n}"}],"model":"gpt-3.5-turbo","tool_choice":"auto","tools":[{"type":"function","function":{"name":"create_issue","description":"Create - a GitHub issue","strict":true,"parameters":{"additionalProperties":false,"properties":{"title":{"description":"Issue - title","title":"Title","type":"string"},"body":{"default":null,"description":"Issue - body","title":"Body","type":"string"}},"required":["title","body"],"type":"object"}}}]}' - headers: - User-Agent: - - X-USER-AGENT-XXX - accept: - - application/json - accept-encoding: - - ACCEPT-ENCODING-XXX - authorization: - - AUTHORIZATION-XXX - connection: - - keep-alive - content-length: - - '1042' - content-type: - - application/json - cookie: - - COOKIE-XXX - host: - - api.openai.com - x-stainless-arch: - - X-STAINLESS-ARCH-XXX - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - X-STAINLESS-OS-XXX - x-stainless-package-version: - - 1.83.0 - x-stainless-read-timeout: - - X-STAINLESS-READ-TIMEOUT-XXX - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.13.3 - method: POST - uri: https://api.openai.com/v1/chat/completions - response: - body: - string: "{\n \"id\": \"chatcmpl-D8D6Q2wrEW1ncIy69lWGrk2NTH9ln\",\n \"object\": - \"chat.completion\",\n \"created\": 1770849362,\n \"model\": \"gpt-3.5-turbo-0125\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"I have successfully created a test - GitHub issue. You can view it at [this link](https://github.com/test/repo/issues/1).\",\n - \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": - null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": - 156,\n \"completion_tokens\": 30,\n \"total_tokens\": 186,\n \"prompt_tokens_details\": - {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": - {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": - 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": null\n}\n" - headers: - CF-RAY: - - CF-RAY-XXX - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 11 Feb 2026 22:36:03 GMT - Server: - - cloudflare - Strict-Transport-Security: - - STS-XXX - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - X-CONTENT-TYPE-XXX - access-control-expose-headers: - - ACCESS-CONTROL-XXX - alt-svc: - - h3=":443"; ma=86400 - cf-cache-status: - - DYNAMIC - openai-organization: - - OPENAI-ORG-XXX - openai-processing-ms: - - '2740' - openai-project: - - OPENAI-PROJECT-XXX - openai-version: - - '2020-10-01' - set-cookie: - - SET-COOKIE-XXX - x-openai-proxy-wasm: - - v0.1 - x-ratelimit-limit-requests: - - X-RATELIMIT-LIMIT-REQUESTS-XXX - x-ratelimit-limit-tokens: - - X-RATELIMIT-LIMIT-TOKENS-XXX - x-ratelimit-remaining-requests: - - X-RATELIMIT-REMAINING-REQUESTS-XXX - x-ratelimit-remaining-tokens: - - X-RATELIMIT-REMAINING-TOKENS-XXX - x-ratelimit-reset-requests: - - X-RATELIMIT-RESET-REQUESTS-XXX - x-ratelimit-reset-tokens: - - X-RATELIMIT-RESET-TOKENS-XXX - x-request-id: - - X-REQUEST-ID-XXX - status: - code: 200 - message: OK -- request: - body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour - personal goal is: Test goal"},{"role":"user","content":"\nCurrent Task: Create - a GitHub issue"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_lLWMJ8icAP8pCVCANTDjIl9s","type":"function","function":{"name":"create_issue","arguments":"{\"title\":\"Test - GitHub Issue\",\"body\":\"This is a test GitHub issue created for testing purposes.\"}"}}]},{"role":"tool","tool_call_id":"call_lLWMJ8icAP8pCVCANTDjIl9s","name":"create_issue","content":"{\n \"success\": - true,\n \"issue_url\": \"https://github.com/test/repo/issues/1\"\n}"},{"role":"assistant","content":"I - have successfully created a test GitHub issue. You can view it at [this link](https://github.com/test/repo/issues/1)."}],"model":"gpt-3.5-turbo","tool_choice":"auto","tools":[{"type":"function","function":{"name":"create_issue","description":"Create - a GitHub issue","strict":true,"parameters":{"additionalProperties":false,"properties":{"title":{"description":"Issue - title","title":"Title","type":"string"},"body":{"default":null,"description":"Issue - body","title":"Body","type":"string"}},"required":["title","body"],"type":"object"}}}]}' - headers: - User-Agent: - - X-USER-AGENT-XXX - accept: - - application/json - accept-encoding: - - ACCEPT-ENCODING-XXX - authorization: - - AUTHORIZATION-XXX - connection: - - keep-alive - content-length: - - '1195' - content-type: - - application/json - cookie: - - COOKIE-XXX - host: - - api.openai.com - x-stainless-arch: - - X-STAINLESS-ARCH-XXX - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - X-STAINLESS-OS-XXX - x-stainless-package-version: - - 1.83.0 - x-stainless-read-timeout: - - X-STAINLESS-READ-TIMEOUT-XXX - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.13.3 - method: POST - uri: https://api.openai.com/v1/chat/completions - response: - body: - string: "{\n \"id\": \"chatcmpl-D8D6R4KzXuzOPsV3Sa6gWnZyrLjm0\",\n \"object\": - \"chat.completion\",\n \"created\": 1770849363,\n \"model\": \"gpt-3.5-turbo-0125\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"Test goal\",\n \"refusal\": - null,\n \"annotations\": []\n },\n \"logprobs\": null,\n - \ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": - 189,\n \"completion_tokens\": 3,\n \"total_tokens\": 192,\n \"prompt_tokens_details\": - {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": - {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": - 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": null\n}\n" - headers: - CF-RAY: - - CF-RAY-XXX - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 11 Feb 2026 22:36:03 GMT - Server: - - cloudflare - Strict-Transport-Security: - - STS-XXX - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - X-CONTENT-TYPE-XXX - access-control-expose-headers: - - ACCESS-CONTROL-XXX - alt-svc: - - h3=":443"; ma=86400 - cf-cache-status: - - DYNAMIC - openai-organization: - - OPENAI-ORG-XXX - openai-processing-ms: - - '515' - openai-project: - - OPENAI-PROJECT-XXX - openai-version: - - '2020-10-01' - set-cookie: - - SET-COOKIE-XXX - x-openai-proxy-wasm: - - v0.1 - x-ratelimit-limit-requests: - - X-RATELIMIT-LIMIT-REQUESTS-XXX - x-ratelimit-limit-tokens: - - X-RATELIMIT-LIMIT-TOKENS-XXX - x-ratelimit-remaining-requests: - - X-RATELIMIT-REMAINING-REQUESTS-XXX - x-ratelimit-remaining-tokens: - - X-RATELIMIT-REMAINING-TOKENS-XXX - x-ratelimit-reset-requests: - - X-RATELIMIT-RESET-REQUESTS-XXX - x-ratelimit-reset-tokens: - - X-RATELIMIT-RESET-TOKENS-XXX - x-request-id: - - X-REQUEST-ID-XXX - status: - code: 200 - message: OK -version: 1 diff --git a/lib/crewai/tests/tools/test_tool_failure.py b/lib/crewai/tests/tools/test_tool_failure.py index 3e9e5f82e..17bfdb94c 100644 --- a/lib/crewai/tests/tools/test_tool_failure.py +++ b/lib/crewai/tests/tools/test_tool_failure.py @@ -1693,56 +1693,51 @@ class TestPlatformActionTool: """CrewAI AMP agentic-app actions -- the Slack case from the bug report.""" @staticmethod - def _tool() -> Any: + def _tool(client: Any) -> Any: import crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool as mod - - return mod.CrewAIPlatformActionTool( - description="Send a Slack message", - app="slack", - action_name="slackbot_send_message", - action_schema={ - "function": { - "name": "slackbot_send_message", - "parameters": { - "properties": {"channel": {"type": "string"}}, - "required": [], - }, - } - }, + from crewai_tools.tools.crewai_platform_tools._client import ( + _PlatformToolInfo, ) - def test_non_ok_response_becomes_a_tool_failure(self, monkeypatch) -> None: # noqa: ANN001 + return mod.CrewAIPlatformActionTool( + tool_info=_PlatformToolInfo( + app="slack", + action="slackbot_send_message", + connection_id=None, + description="Send a Slack message", + parameters={ + "properties": {"channel": {"type": "string"}}, + "required": [], + }, + ), + client=client, + ) + + def test_non_ok_response_becomes_a_tool_failure(self) -> None: from unittest.mock import Mock - import crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool as mod - - response = Mock() - response.ok = False - response.status_code = 500 + response = Mock(ok=False, status_code=500) response.json.return_value = { "error": "Failed to execute action: Slack API error: channel_not_found" } - monkeypatch.setattr(mod.requests, "post", Mock(return_value=response)) - monkeypatch.setenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "t") + client = Mock() + client.execute_action.return_value = response - result = self._tool()._run(channel="#joao-message") + result = self._tool(client)._run(channel="#joao-message") assert isinstance(result, ToolFailure) assert "channel_not_found" in result.message assert result.retryable is True - def test_ok_response_still_returns_json(self, monkeypatch) -> None: # noqa: ANN001 + def test_ok_response_still_returns_json(self) -> None: from unittest.mock import Mock - import crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool as mod - - response = Mock() - response.ok = True + response = Mock(ok=True) response.json.return_value = {"ts": "1234.5678"} - monkeypatch.setattr(mod.requests, "post", Mock(return_value=response)) - monkeypatch.setenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "t") + client = Mock() + client.execute_action.return_value = response - result = self._tool()._run(channel="#general") + result = self._tool(client)._run(channel="#general") assert not isinstance(result, ToolFailure) assert "1234.5678" in result