mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 10:03:37 +00:00
Parse platform application selectors (#7148)
Stores application, action, and connection values in an internal selector. Validates the selector with clearer error messages. Keeps existing application syntax and legacy API requests unchanged.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
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
|
||||
@@ -8,6 +8,9 @@ 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,
|
||||
)
|
||||
@@ -27,7 +30,7 @@ class CrewaiPlatformToolBuilder:
|
||||
self,
|
||||
apps: list[str],
|
||||
) -> None:
|
||||
self._apps = apps
|
||||
self._apps = [ApplicationSelector(app) for app in apps]
|
||||
self._actions_schema: dict[str, dict[str, Any]] = {}
|
||||
self._tools: list[BaseTool] | None = None
|
||||
|
||||
@@ -42,18 +45,22 @@ class CrewaiPlatformToolBuilder:
|
||||
"""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(self._apps)},
|
||||
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 {self._apps}: {e}")
|
||||
logger.error(f"Failed to fetch platform tools for apps {apps}: {e}")
|
||||
return
|
||||
|
||||
raw_data = response.json()
|
||||
|
||||
@@ -84,7 +84,9 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github", "slack/send_message"])
|
||||
builder = CrewaiPlatformToolBuilder(
|
||||
apps=["github", "slack/send_message", "custom/path/to/action"]
|
||||
)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
@@ -98,7 +100,9 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
|
||||
|
||||
assert "/actions" in args[0]
|
||||
assert kwargs["headers"]["Authorization"] == "Bearer test_token"
|
||||
assert kwargs["params"]["apps"] == "github,slack/send_message"
|
||||
assert kwargs["params"]["apps"] == (
|
||||
"github,slack/send_message,custom/path/to/action"
|
||||
)
|
||||
|
||||
assert "create_issue" in builder._actions_schema
|
||||
assert (
|
||||
@@ -320,3 +324,52 @@ class TestCrewaiPlatformToolBuilderVerify(unittest.TestCase):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user