mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-26 12:50:20 +00:00
Compare commits
1 Commits
feat/crewa
...
clear-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f039d2a502 |
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user