feat: add apps & actions attributes to Agent (#3504)

* feat: add app attributes to Agent

* feat: add actions attribute to Agent

* chore: resolve linter issues

* refactor: merge the apps and actions parameters into a single one

* fix: remove unnecessary print

* feat: logging error when CrewaiPlatformTools fails

* chore: export CrewaiPlatformTools directly from crewai_tools

* style: resolver linter issues

* test: fix broken tests

* style: solve linter issues

* fix: fix broken test
This commit is contained in:
Lucas Gomide
2025-09-25 17:46:51 -03:00
committed by GitHub
parent e070c1400c
commit 13e5ec711d
7 changed files with 315 additions and 104 deletions

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional
from typing import Any
import pytest
from pydantic import BaseModel
@@ -12,7 +12,7 @@ from crewai.utilities.token_counter_callback import TokenProcess
# Concrete implementation for testing
class ConcreteAgentAdapter(BaseAgentAdapter):
def configure_tools(
self, tools: Optional[List[BaseTool]] = None, **kwargs: Any
self, tools: list[BaseTool] | None = None, **kwargs: Any
) -> None:
# Simple implementation for testing
self.tools = tools or []
@@ -20,19 +20,19 @@ class ConcreteAgentAdapter(BaseAgentAdapter):
def execute_task(
self,
task: Any,
context: Optional[str] = None,
tools: Optional[List[Any]] = None,
context: str | None = None,
tools: list[Any] | None = None,
) -> str:
# Dummy implementation needed due to BaseAgent inheritance
return "Task executed"
def create_agent_executor(self, tools: Optional[List[BaseTool]] = None) -> Any:
def create_agent_executor(self, tools: list[BaseTool] | None = None) -> Any:
# Dummy implementation
return None
def get_delegation_tools(
self, tools: List[BaseTool], tool_map: Optional[Dict[str, BaseTool]]
) -> List[BaseTool]:
self, tools: list[BaseTool], tool_map: dict[str, BaseTool] | None
) -> list[BaseTool]:
# Dummy implementation
return []
@@ -40,10 +40,14 @@ class ConcreteAgentAdapter(BaseAgentAdapter):
# Dummy implementation
pass
def get_output_converter(self, tools: Optional[List[BaseTool]] = None) -> Any:
def get_output_converter(self, tools: list[BaseTool] | None = None) -> Any:
# Dummy implementation
return None
def get_platform_tools(self, apps: Any) -> list[BaseTool]:
# Dummy implementation
return []
def test_base_agent_adapter_initialization():
"""Test initialization of the concrete agent adapter."""
@@ -95,7 +99,6 @@ def test_configure_structured_output_method_exists():
adapter.configure_structured_output(structured_output)
# Add assertions here if configure_structured_output modifies state
# For now, just ensuring it runs without error is sufficient
pass
def test_base_agent_adapter_inherits_base_agent():

View File

@@ -1,5 +1,5 @@
import hashlib
from typing import Any, List, Optional
from typing import Any
from pydantic import BaseModel
@@ -11,14 +11,16 @@ class MockAgent(BaseAgent):
def execute_task(
self,
task: Any,
context: Optional[str] = None,
tools: Optional[List[BaseTool]] = None,
context: str | None = None,
tools: list[BaseTool] | None = None,
) -> str:
return ""
def create_agent_executor(self, tools=None) -> None: ...
def get_delegation_tools(self, agents: List["BaseAgent"]): ...
def get_delegation_tools(self, agents: list["BaseAgent"]): ...
def get_platform_tools(self, apps: list[Any]): ...
def get_output_converter(
self, llm: Any, text: str, model: type[BaseModel] | None, instructions: str
@@ -31,5 +33,5 @@ def test_key():
goal="test goal",
backstory="test backstory",
)
hash = hashlib.md5("test role|test goal|test backstory".encode()).hexdigest()
hash = hashlib.md5("test role|test goal|test backstory".encode(), usedforsecurity=False).hexdigest()
assert agent.key == hash

View File

@@ -1,5 +1,6 @@
"""Test Agent creation and execution basic functionality."""
# ruff: noqa: S106
import os
from unittest import mock
from unittest.mock import MagicMock, patch
@@ -2368,7 +2369,7 @@ def test_agent_from_repository(mock_get_agent, mock_get_auth_token):
tool_action = EnterpriseActionTool(
name="test_name",
description="test_description",
enterprise_action_token="test_token", # noqa: S106
enterprise_action_token="test_token",
action_name="test_action_name",
action_schema={"test": "test"},
)
@@ -2522,3 +2523,132 @@ def test_agent_from_repository_without_org_set(
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
style="yellow",
)
def test_agent_apps_consolidated_functionality():
agent = Agent(
role="Platform Agent",
goal="Use platform tools",
backstory="Platform specialist",
apps=["gmail/create_task", "slack/update_status", "hubspot"]
)
expected = {"gmail/create_task", "slack/update_status", "hubspot"}
assert set(agent.apps) == expected
agent_apps_only = Agent(
role="App Agent",
goal="Use apps",
backstory="App specialist",
apps=["gmail", "slack"]
)
assert set(agent_apps_only.apps) == {"gmail", "slack"}
agent_default = Agent(
role="Regular Agent",
goal="Regular tasks",
backstory="Regular agent"
)
assert agent_default.apps is None
def test_agent_apps_validation():
agent = Agent(
role="Custom Agent",
goal="Test validation",
backstory="Test agent",
apps=["custom_app", "another_app/action"]
)
assert set(agent.apps) == {"custom_app", "another_app/action"}
with pytest.raises(ValueError, match=r"Invalid app format.*Apps can only have one '/' for app/action format"):
Agent(
role="Invalid Agent",
goal="Test validation",
backstory="Test agent",
apps=["app/action/invalid"]
)
@patch.object(Agent, 'get_platform_tools')
def test_app_actions_propagated_to_platform_tools(mock_get_platform_tools):
from crewai.tools import tool
@tool
def action_tool() -> str:
"""Mock action platform tool."""
return "action tool result"
mock_get_platform_tools.return_value = [action_tool]
agent = Agent(
role="Action Agent",
goal="Execute actions",
backstory="Action specialist",
apps=["gmail/send_email", "slack/update_status"]
)
task = Task(
description="Test task",
expected_output="Test output",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task])
tools = crew._prepare_tools(agent, task, [])
mock_get_platform_tools.assert_called_once()
call_args = mock_get_platform_tools.call_args[1]
assert set(call_args["apps"]) == {"gmail/send_email", "slack/update_status"}
assert len(tools) >= 1
@patch.object(Agent, 'get_platform_tools')
def test_mixed_apps_and_actions_propagated(mock_get_platform_tools):
from crewai.tools import tool
@tool
def combined_tool() -> str:
"""Mock combined platform tool."""
return "combined tool result"
mock_get_platform_tools.return_value = [combined_tool]
agent = Agent(
role="Combined Agent",
goal="Use apps and actions",
backstory="Platform specialist",
apps=["gmail", "slack", "gmail/create_task", "slack/update_status"]
)
task = Task(
description="Test task",
expected_output="Test output",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task])
tools = crew._prepare_tools(agent, task, [])
mock_get_platform_tools.assert_called_once()
call_args = mock_get_platform_tools.call_args[1]
expected_apps = {"gmail", "slack", "gmail/create_task", "slack/update_status"}
assert set(call_args["apps"]) == expected_apps
assert len(tools) >= 1
def test_agent_without_apps_no_platform_tools():
"""Test that agents without apps don't trigger platform tools integration."""
agent = Agent(
role="Regular Agent",
goal="Regular tasks",
backstory="Regular agent"
)
task = Task(
description="Test task",
expected_output="Test output",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task])
tools = crew._prepare_tools(agent, task, [])
assert tools == []