Skip to content

Commit 13e5ec7

Browse files
authored
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
1 parent e070c14 commit 13e5ec7

File tree

7 files changed

+315
-104
lines changed

7 files changed

+315
-104
lines changed

src/crewai/agent.py

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import shutil
22
import subprocess
33
import time
4+
from collections.abc import Callable, Sequence
45
from typing import (
56
Any,
6-
Callable,
7-
Dict,
8-
List,
97
Literal,
10-
Optional,
11-
Sequence,
12-
Tuple,
13-
Type,
14-
Union,
8+
cast,
159
)
1610

1711
from pydantic import Field, InstanceOf, PrivateAttr, model_validator
1812

1913
from crewai.agents import CacheHandler
20-
from crewai.agents.agent_builder.base_agent import BaseAgent
14+
from crewai.agents.agent_builder.base_agent import BaseAgent, PlatformAppOrAction
2115
from crewai.agents.crew_agent_executor import CrewAgentExecutor
16+
from crewai.events.event_bus import crewai_event_bus
17+
from crewai.events.types.agent_events import (
18+
AgentExecutionCompletedEvent,
19+
AgentExecutionErrorEvent,
20+
AgentExecutionStartedEvent,
21+
)
22+
from crewai.events.types.knowledge_events import (
23+
KnowledgeQueryCompletedEvent,
24+
KnowledgeQueryFailedEvent,
25+
KnowledgeQueryStartedEvent,
26+
KnowledgeRetrievalCompletedEvent,
27+
KnowledgeRetrievalStartedEvent,
28+
KnowledgeSearchQueryFailedEvent,
29+
)
30+
from crewai.events.types.memory_events import (
31+
MemoryRetrievalCompletedEvent,
32+
MemoryRetrievalStartedEvent,
33+
)
2234
from crewai.knowledge.knowledge import Knowledge
2335
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
2436
from crewai.knowledge.utils.knowledge_utils import extract_knowledge_context
@@ -38,24 +50,6 @@
3850
)
3951
from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE
4052
from crewai.utilities.converter import generate_model_description
41-
from crewai.events.types.agent_events import (
42-
AgentExecutionCompletedEvent,
43-
AgentExecutionErrorEvent,
44-
AgentExecutionStartedEvent,
45-
)
46-
from crewai.events.event_bus import crewai_event_bus
47-
from crewai.events.types.memory_events import (
48-
MemoryRetrievalStartedEvent,
49-
MemoryRetrievalCompletedEvent,
50-
)
51-
from crewai.events.types.knowledge_events import (
52-
KnowledgeQueryCompletedEvent,
53-
KnowledgeQueryFailedEvent,
54-
KnowledgeQueryStartedEvent,
55-
KnowledgeRetrievalCompletedEvent,
56-
KnowledgeRetrievalStartedEvent,
57-
KnowledgeSearchQueryFailedEvent,
58-
)
5953
from crewai.utilities.llm_utils import create_llm
6054
from crewai.utilities.token_counter_callback import TokenCalcHandler
6155
from crewai.utilities.training_handler import CrewTrainingHandler
@@ -84,39 +78,40 @@ class Agent(BaseAgent):
8478
step_callback: Callback to be executed after each step of the agent execution.
8579
knowledge_sources: Knowledge sources for the agent.
8680
embedder: Embedder configuration for the agent.
81+
apps: List of applications that the agent can access through CrewAI Platform.
8782
"""
8883

8984
_times_executed: int = PrivateAttr(default=0)
90-
max_execution_time: Optional[int] = Field(
85+
max_execution_time: int | None = Field(
9186
default=None,
9287
description="Maximum execution time for an agent to execute a task",
9388
)
9489
agent_ops_agent_name: str = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")
9590
agent_ops_agent_id: str = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")
96-
step_callback: Optional[Any] = Field(
91+
step_callback: Any | None = Field(
9792
default=None,
9893
description="Callback to be executed after each step of the agent execution.",
9994
)
100-
use_system_prompt: Optional[bool] = Field(
95+
use_system_prompt: bool | None = Field(
10196
default=True,
10297
description="Use system prompt for the agent.",
10398
)
104-
llm: Union[str, InstanceOf[BaseLLM], Any] = Field(
99+
llm: str | InstanceOf[BaseLLM] | Any = Field(
105100
description="Language model that will run the agent.", default=None
106101
)
107-
function_calling_llm: Optional[Union[str, InstanceOf[BaseLLM], Any]] = Field(
102+
function_calling_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
108103
description="Language model that will run the agent.", default=None
109104
)
110-
system_template: Optional[str] = Field(
105+
system_template: str | None = Field(
111106
default=None, description="System format for the agent."
112107
)
113-
prompt_template: Optional[str] = Field(
108+
prompt_template: str | None = Field(
114109
default=None, description="Prompt format for the agent."
115110
)
116-
response_template: Optional[str] = Field(
111+
response_template: str | None = Field(
117112
default=None, description="Response format for the agent."
118113
)
119-
allow_code_execution: Optional[bool] = Field(
114+
allow_code_execution: bool | None = Field(
120115
default=False, description="Enable code execution for the agent."
121116
)
122117
respect_context_window: bool = Field(
@@ -147,31 +142,31 @@ class Agent(BaseAgent):
147142
default=False,
148143
description="Whether the agent should reflect and create a plan before executing a task.",
149144
)
150-
max_reasoning_attempts: Optional[int] = Field(
145+
max_reasoning_attempts: int | None = Field(
151146
default=None,
152147
description="Maximum number of reasoning attempts before executing the task. If None, will try until ready.",
153148
)
154-
embedder: Optional[Dict[str, Any]] = Field(
149+
embedder: dict[str, Any] | None = Field(
155150
default=None,
156151
description="Embedder configuration for the agent.",
157152
)
158-
agent_knowledge_context: Optional[str] = Field(
153+
agent_knowledge_context: str | None = Field(
159154
default=None,
160155
description="Knowledge context for the agent.",
161156
)
162-
crew_knowledge_context: Optional[str] = Field(
157+
crew_knowledge_context: str | None = Field(
163158
default=None,
164159
description="Knowledge context for the crew.",
165160
)
166-
knowledge_search_query: Optional[str] = Field(
161+
knowledge_search_query: str | None = Field(
167162
default=None,
168163
description="Knowledge search query for the agent dynamically generated by the agent.",
169164
)
170-
from_repository: Optional[str] = Field(
165+
from_repository: str | None = Field(
171166
default=None,
172167
description="The Agent's role to be used from your repository.",
173168
)
174-
guardrail: Optional[Union[Callable[[Any], Tuple[bool, Any]], str]] = Field(
169+
guardrail: Callable[[Any], tuple[bool, Any]] | str | None = Field(
175170
default=None,
176171
description="Function or string description of a guardrail to validate agent output",
177172
)
@@ -180,6 +175,7 @@ class Agent(BaseAgent):
180175
)
181176

182177
@model_validator(mode="before")
178+
@classmethod
183179
def validate_from_repository(cls, v):
184180
if v is not None and (from_repository := v.get("from_repository")):
185181
return load_agent_from_repository(from_repository) | v
@@ -208,7 +204,7 @@ def _setup_agent_executor(self):
208204
self.cache_handler = CacheHandler()
209205
self.set_cache_handler(self.cache_handler)
210206

211-
def set_knowledge(self, crew_embedder: Optional[Dict[str, Any]] = None):
207+
def set_knowledge(self, crew_embedder: dict[str, Any] | None = None):
212208
try:
213209
if self.embedder is None and crew_embedder:
214210
self.embedder = crew_embedder
@@ -224,7 +220,7 @@ def set_knowledge(self, crew_embedder: Optional[Dict[str, Any]] = None):
224220
)
225221
self.knowledge.add_sources()
226222
except (TypeError, ValueError) as e:
227-
raise ValueError(f"Invalid Knowledge Configuration: {str(e)}")
223+
raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from e
228224

229225
def _is_any_available_memory(self) -> bool:
230226
"""Check if any memory is available."""
@@ -244,8 +240,8 @@ def _is_any_available_memory(self) -> bool:
244240
def execute_task(
245241
self,
246242
task: Task,
247-
context: Optional[str] = None,
248-
tools: Optional[List[BaseTool]] = None,
243+
context: str | None = None,
244+
tools: list[BaseTool] | None = None,
249245
) -> str:
250246
"""Execute a task with the agent.
251247
@@ -277,13 +273,9 @@ def execute_task(
277273
# Add the reasoning plan to the task description
278274
task.description += f"\n\nReasoning Plan:\n{reasoning_output.plan.plan}"
279275
except Exception as e:
280-
if hasattr(self, "_logger"):
281-
self._logger.log(
282-
"error", f"Error during reasoning process: {str(e)}"
283-
)
284-
else:
285-
print(f"Error during reasoning process: {str(e)}")
286-
276+
self._logger.log(
277+
"error", f"Error during reasoning process: {e!s}"
278+
)
287279
self._inject_date_to_task(task)
288280

289281
if self.tools_handler:
@@ -335,7 +327,7 @@ def execute_task(
335327
agent=self,
336328
task=task,
337329
)
338-
memory = contextual_memory.build_context_for_task(task, context)
330+
memory = contextual_memory.build_context_for_task(task, context or "")
339331
if memory.strip() != "":
340332
task_prompt += self.i18n.slice("memory").format(memory=memory)
341333

@@ -525,14 +517,14 @@ def _execute_with_timeout(self, task_prompt: str, task: Task, timeout: int) -> s
525517

526518
try:
527519
return future.result(timeout=timeout)
528-
except concurrent.futures.TimeoutError:
520+
except concurrent.futures.TimeoutError as e:
529521
future.cancel()
530522
raise TimeoutError(
531523
f"Task '{task.description}' execution timed out after {timeout} seconds. Consider increasing max_execution_time or optimizing the task."
532-
)
524+
) from e
533525
except Exception as e:
534526
future.cancel()
535-
raise RuntimeError(f"Task execution failed: {str(e)}")
527+
raise RuntimeError(f"Task execution failed: {e!s}") from e
536528

537529
def _execute_without_timeout(self, task_prompt: str, task: Task) -> str:
538530
"""Execute a task without a timeout.
@@ -554,14 +546,14 @@ def _execute_without_timeout(self, task_prompt: str, task: Task) -> str:
554546
)["output"]
555547

556548
def create_agent_executor(
557-
self, tools: Optional[List[BaseTool]] = None, task=None
549+
self, tools: list[BaseTool] | None = None, task=None
558550
) -> None:
559551
"""Create an agent executor for the agent.
560552
561553
Returns:
562554
An instance of the CrewAgentExecutor class.
563555
"""
564-
raw_tools: List[BaseTool] = tools or self.tools or []
556+
raw_tools: list[BaseTool] = tools or self.tools or []
565557
parsed_tools = parse_tools(raw_tools)
566558

567559
prompt = Prompts(
@@ -587,7 +579,7 @@ def create_agent_executor(
587579
agent=self,
588580
crew=self.crew,
589581
tools=parsed_tools,
590-
prompt=prompt,
582+
prompt=cast(dict[str, str], prompt),
591583
original_tools=raw_tools,
592584
stop_words=stop_words,
593585
max_iter=self.max_iter,
@@ -603,10 +595,18 @@ def create_agent_executor(
603595
callbacks=[TokenCalcHandler(self._token_process)],
604596
)
605597

606-
def get_delegation_tools(self, agents: List[BaseAgent]):
598+
def get_delegation_tools(self, agents: list[BaseAgent]):
607599
agent_tools = AgentTools(agents=agents)
608-
tools = agent_tools.tools()
609-
return tools
600+
return agent_tools.tools()
601+
602+
def get_platform_tools(self, apps: list[PlatformAppOrAction]) -> list[BaseTool]:
603+
try:
604+
from crewai_tools import CrewaiPlatformTools # type: ignore[import-untyped]
605+
606+
return CrewaiPlatformTools(apps=apps)
607+
except Exception as e:
608+
self._logger.log("error", f"Error getting platform tools: {e!s}")
609+
return []
610610

611611
def get_multimodal_tools(self) -> Sequence[BaseTool]:
612612
from crewai.tools.agent_tools.add_image_tool import AddImageTool
@@ -654,7 +654,7 @@ def _use_trained_data(self, task_prompt: str) -> str:
654654
)
655655
return task_prompt
656656

657-
def _render_text_description(self, tools: List[Any]) -> str:
657+
def _render_text_description(self, tools: list[Any]) -> str:
658658
"""Render the tool name and description in plain text.
659659
660660
Output will be in the format of:
@@ -664,14 +664,13 @@ def _render_text_description(self, tools: List[Any]) -> str:
664664
search: This tool is used for search
665665
calculator: This tool is used for math
666666
"""
667-
description = "\n".join(
667+
return "\n".join(
668668
[
669669
f"Tool name: {tool.name}\nTool description:\n{tool.description}"
670670
for tool in tools
671671
]
672672
)
673673

674-
return description
675674

676675
def _inject_date_to_task(self, task):
677676
"""Inject the current date into the task description if inject_date is enabled."""
@@ -700,28 +699,33 @@ def _inject_date_to_task(self, task):
700699
task.description += f"\n\nCurrent Date: {current_date}"
701700
except Exception as e:
702701
if hasattr(self, "_logger"):
703-
self._logger.log("warning", f"Failed to inject date: {str(e)}")
702+
self._logger.log("warning", f"Failed to inject date: {e!s}")
704703
else:
705-
print(f"Warning: Failed to inject date: {str(e)}")
704+
print(f"Warning: Failed to inject date: {e!s}")
706705

707706
def _validate_docker_installation(self) -> None:
708707
"""Check if Docker is installed and running."""
709-
if not shutil.which("docker"):
708+
docker_path = shutil.which("docker")
709+
if not docker_path:
710710
raise RuntimeError(
711711
f"Docker is not installed. Please install Docker to use code execution with agent: {self.role}"
712712
)
713713

714714
try:
715-
subprocess.run(
716-
["docker", "info"],
715+
subprocess.run( # noqa: S603
716+
[docker_path, "info"],
717717
check=True,
718718
stdout=subprocess.PIPE,
719719
stderr=subprocess.PIPE,
720720
)
721-
except subprocess.CalledProcessError:
721+
except subprocess.CalledProcessError as e:
722722
raise RuntimeError(
723723
f"Docker is not running. Please start Docker to use code execution with agent: {self.role}"
724-
)
724+
) from e
725+
except subprocess.TimeoutExpired as e:
726+
raise RuntimeError(
727+
f"Docker command timed out. Please check your Docker installation for agent: {self.role}"
728+
) from e
725729

726730
def __repr__(self):
727731
return f"Agent(role={self.role}, goal={self.goal}, backstory={self.backstory})"
@@ -796,8 +800,8 @@ def _get_knowledge_search_query(self, task_prompt: str) -> str | None:
796800

797801
def kickoff(
798802
self,
799-
messages: Union[str, List[Dict[str, str]]],
800-
response_format: Optional[Type[Any]] = None,
803+
messages: str | list[dict[str, str]],
804+
response_format: type[Any] | None = None,
801805
) -> LiteAgentOutput:
802806
"""
803807
Execute the agent with the given messages using a LiteAgent instance.
@@ -836,8 +840,8 @@ def kickoff(
836840

837841
async def kickoff_async(
838842
self,
839-
messages: Union[str, List[Dict[str, str]]],
840-
response_format: Optional[Type[Any]] = None,
843+
messages: str | list[dict[str, str]],
844+
response_format: type[Any] | None = None,
841845
) -> LiteAgentOutput:
842846
"""
843847
Execute the agent asynchronously with the given messages using a LiteAgent instance.

0 commit comments

Comments
 (0)