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:
Vinicius Brasil
2026-08-28 19:36:29 -03:00
committed by GitHub
parent e9d4c57b2a
commit da4daadba0
3 changed files with 119 additions and 5 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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)