feat: cache i18n prompts for efficient use

This commit is contained in:
Greyson LaLonde
2025-11-03 07:39:05 -05:00
committed by GitHub
parent 40932af3fa
commit 60332e0b19
13 changed files with 227 additions and 201 deletions

View File

@@ -7,7 +7,7 @@ output conversion for OpenAI agents, supporting JSON and Pydantic model formats.
from typing import Any from typing import Any
from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import get_i18n
class OpenAIConverterAdapter(BaseConverterAdapter): class OpenAIConverterAdapter(BaseConverterAdapter):
@@ -59,7 +59,7 @@ class OpenAIConverterAdapter(BaseConverterAdapter):
return base_prompt return base_prompt
output_schema: str = ( output_schema: str = (
I18N() get_i18n()
.slice("formatted_task_instructions") .slice("formatted_task_instructions")
.format(output_format=self._schema) .format(output_format=self._schema)
) )

View File

@@ -29,7 +29,7 @@ from crewai.rag.embeddings.types import EmbedderConfig
from crewai.security.security_config import SecurityConfig from crewai.security.security_config import SecurityConfig
from crewai.tools.base_tool import BaseTool, Tool from crewai.tools.base_tool import BaseTool, Tool
from crewai.utilities.config import process_config from crewai.utilities.config import process_config
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.logger import Logger from crewai.utilities.logger import Logger
from crewai.utilities.rpm_controller import RPMController from crewai.utilities.rpm_controller import RPMController
from crewai.utilities.string_utils import interpolate_only from crewai.utilities.string_utils import interpolate_only
@@ -107,7 +107,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
Set private attributes. Set private attributes.
""" """
__hash__ = object.__hash__ # type: ignore __hash__ = object.__hash__
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=False)) _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=False))
_rpm_controller: RPMController | None = PrivateAttr(default=None) _rpm_controller: RPMController | None = PrivateAttr(default=None)
_request_within_rpm_limit: Any = PrivateAttr(default=None) _request_within_rpm_limit: Any = PrivateAttr(default=None)
@@ -150,7 +150,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
) )
crew: Any = Field(default=None, description="Crew to which the agent belongs.") crew: Any = Field(default=None, description="Crew to which the agent belongs.")
i18n: I18N = Field( i18n: I18N = Field(
default_factory=I18N, description="Internationalization settings." default_factory=get_i18n, description="Internationalization settings."
) )
cache_handler: CacheHandler | None = Field( cache_handler: CacheHandler | None = Field(
default=None, description="An instance of the CacheHandler class." default=None, description="An instance of the CacheHandler class."
@@ -180,8 +180,8 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
default_factory=SecurityConfig, default_factory=SecurityConfig,
description="Security configuration for the agent, including fingerprinting.", description="Security configuration for the agent, including fingerprinting.",
) )
callbacks: list[Callable] = Field( callbacks: list[Callable[[Any], Any]] = Field(
default=[], description="Callbacks to be used for the agent" default_factory=list, description="Callbacks to be used for the agent"
) )
adapted_agent: bool = Field( adapted_agent: bool = Field(
default=False, description="Whether the agent is adapted" default=False, description="Whether the agent is adapted"
@@ -201,7 +201,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod
def process_model_config(cls, values): def process_model_config(cls, values: Any) -> dict[str, Any]:
return process_config(values, cls) return process_config(values, cls)
@field_validator("tools") @field_validator("tools")
@@ -269,7 +269,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
return list(set(validated_mcps)) return list(set(validated_mcps))
@model_validator(mode="after") @model_validator(mode="after")
def validate_and_set_attributes(self): def validate_and_set_attributes(self) -> Self:
# Validate required fields # Validate required fields
for field in ["role", "goal", "backstory"]: for field in ["role", "goal", "backstory"]:
if getattr(self, field) is None: if getattr(self, field) is None:
@@ -301,7 +301,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
) )
@model_validator(mode="after") @model_validator(mode="after")
def set_private_attrs(self): def set_private_attrs(self) -> Self:
"""Set private attributes.""" """Set private attributes."""
self._logger = Logger(verbose=self.verbose) self._logger = Logger(verbose=self.verbose)
if self.max_rpm and not self._rpm_controller: if self.max_rpm and not self._rpm_controller:
@@ -313,7 +313,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
return self return self
@property @property
def key(self): def key(self) -> str:
source = [ source = [
self._original_role or self.role, self._original_role or self.role,
self._original_goal or self.goal, self._original_goal or self.goal,
@@ -331,7 +331,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
pass pass
@abstractmethod @abstractmethod
def create_agent_executor(self, tools=None) -> None: def create_agent_executor(self, tools: list[BaseTool] | None = None) -> None:
pass pass
@abstractmethod @abstractmethod
@@ -443,5 +443,5 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
self._rpm_controller = rpm_controller self._rpm_controller = rpm_controller
self.create_agent_executor() self.create_agent_executor()
def set_knowledge(self, crew_embedder: EmbedderConfig | None = None): def set_knowledge(self, crew_embedder: EmbedderConfig | None = None) -> None:
pass pass

View File

@@ -37,7 +37,7 @@ from crewai.utilities.agent_utils import (
process_llm_response, process_llm_response,
) )
from crewai.utilities.constants import TRAINING_DATA_FILE from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.printer import Printer from crewai.utilities.printer import Printer
from crewai.utilities.tool_utils import execute_tool_and_check_finality from crewai.utilities.tool_utils import execute_tool_and_check_finality
from crewai.utilities.training_handler import CrewTrainingHandler from crewai.utilities.training_handler import CrewTrainingHandler
@@ -65,7 +65,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
def __init__( def __init__(
self, self,
llm: BaseLLM | Any | None, llm: BaseLLM,
task: Task, task: Task,
crew: Crew, crew: Crew,
agent: Agent, agent: Agent,
@@ -106,7 +106,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
callbacks: Optional callbacks list. callbacks: Optional callbacks list.
response_model: Optional Pydantic model for structured outputs. response_model: Optional Pydantic model for structured outputs.
""" """
self._i18n: I18N = I18N() self._i18n: I18N = get_i18n()
self.llm = llm self.llm = llm
self.task = task self.task = task
self.agent = agent self.agent = agent

View File

@@ -18,10 +18,10 @@ from crewai.agents.constants import (
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE, MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
UNABLE_TO_REPAIR_JSON_RESULTS, UNABLE_TO_REPAIR_JSON_RESULTS,
) )
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import get_i18n
_I18N = I18N() _I18N = get_i18n()
@dataclass @dataclass

View File

@@ -27,6 +27,7 @@ from pydantic import (
model_validator, model_validator,
) )
from pydantic_core import PydanticCustomError from pydantic_core import PydanticCustomError
from typing_extensions import Self
from crewai.agent import Agent from crewai.agent import Agent
from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.base_agent import BaseAgent
@@ -70,7 +71,7 @@ from crewai.task import Task
from crewai.tasks.conditional_task import ConditionalTask from crewai.tasks.conditional_task import ConditionalTask
from crewai.tasks.task_output import TaskOutput from crewai.tasks.task_output import TaskOutput
from crewai.tools.agent_tools.agent_tools import AgentTools from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.tools.base_tool import BaseTool, Tool from crewai.tools.base_tool import BaseTool
from crewai.types.usage_metrics import UsageMetrics from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities.constants import NOT_SPECIFIED, TRAINING_DATA_FILE from crewai.utilities.constants import NOT_SPECIFIED, TRAINING_DATA_FILE
from crewai.utilities.crew.models import CrewContext from crewai.utilities.crew.models import CrewContext
@@ -81,7 +82,7 @@ from crewai.utilities.formatter import (
aggregate_raw_outputs_from_task_outputs, aggregate_raw_outputs_from_task_outputs,
aggregate_raw_outputs_from_tasks, aggregate_raw_outputs_from_tasks,
) )
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import get_i18n
from crewai.utilities.llm_utils import create_llm from crewai.utilities.llm_utils import create_llm
from crewai.utilities.logger import Logger from crewai.utilities.logger import Logger
from crewai.utilities.planning_handler import CrewPlanner from crewai.utilities.planning_handler import CrewPlanner
@@ -195,7 +196,7 @@ class Crew(FlowTrackable, BaseModel):
function_calling_llm: str | InstanceOf[LLM] | Any | None = Field( function_calling_llm: str | InstanceOf[LLM] | Any | None = Field(
description="Language model that will run the agent.", default=None description="Language model that will run the agent.", default=None
) )
config: Json | dict[str, Any] | None = Field(default=None) config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True) id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
share_crew: bool | None = Field(default=False) share_crew: bool | None = Field(default=False)
step_callback: Any | None = Field( step_callback: Any | None = Field(
@@ -294,7 +295,9 @@ class Crew(FlowTrackable, BaseModel):
@field_validator("config", mode="before") @field_validator("config", mode="before")
@classmethod @classmethod
def check_config_type(cls, v: Json | dict[str, Any]) -> Json | dict[str, Any]: def check_config_type(
cls, v: Json[dict[str, Any]] | dict[str, Any]
) -> dict[str, Any]:
"""Validates that the config is a valid type. """Validates that the config is a valid type.
Args: Args:
v: The config to be validated. v: The config to be validated.
@@ -310,7 +313,7 @@ class Crew(FlowTrackable, BaseModel):
"""set private attributes.""" """set private attributes."""
self._cache_handler = CacheHandler() self._cache_handler = CacheHandler()
event_listener = EventListener() event_listener = EventListener() # type: ignore[no-untyped-call]
if ( if (
is_tracing_enabled() is_tracing_enabled()
@@ -330,13 +333,13 @@ class Crew(FlowTrackable, BaseModel):
return self return self
def _initialize_default_memories(self): def _initialize_default_memories(self) -> None:
self._long_term_memory = self._long_term_memory or LongTermMemory() self._long_term_memory = self._long_term_memory or LongTermMemory() # type: ignore[no-untyped-call]
self._short_term_memory = self._short_term_memory or ShortTermMemory( self._short_term_memory = self._short_term_memory or ShortTermMemory( # type: ignore[no-untyped-call]
crew=self, crew=self,
embedder_config=self.embedder, embedder_config=self.embedder,
) )
self._entity_memory = self.entity_memory or EntityMemory( self._entity_memory = self.entity_memory or EntityMemory( # type: ignore[no-untyped-call]
crew=self, embedder_config=self.embedder crew=self, embedder_config=self.embedder
) )
@@ -380,7 +383,7 @@ class Crew(FlowTrackable, BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def check_manager_llm(self): def check_manager_llm(self) -> Self:
"""Validates that the language model is set when using hierarchical process.""" """Validates that the language model is set when using hierarchical process."""
if self.process == Process.hierarchical: if self.process == Process.hierarchical:
if not self.manager_llm and not self.manager_agent: if not self.manager_llm and not self.manager_agent:
@@ -405,7 +408,7 @@ class Crew(FlowTrackable, BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def check_config(self): def check_config(self) -> Self:
"""Validates that the crew is properly configured with agents and tasks.""" """Validates that the crew is properly configured with agents and tasks."""
if not self.config and not self.tasks and not self.agents: if not self.config and not self.tasks and not self.agents:
raise PydanticCustomError( raise PydanticCustomError(
@@ -426,23 +429,20 @@ class Crew(FlowTrackable, BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def validate_tasks(self): def validate_tasks(self) -> Self:
if self.process == Process.sequential: if self.process == Process.sequential:
for task in self.tasks: for task in self.tasks:
if task.agent is None: if task.agent is None:
raise PydanticCustomError( raise PydanticCustomError(
"missing_agent_in_task", "missing_agent_in_task",
( "Sequential process error: Agent is missing in the task with the following description: {description}",
f"Sequential process error: Agent is missing in the task " {"description": task.description},
f"with the following description: {task.description}"
), # type: ignore # Dynamic string in error message
{},
) )
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def validate_end_with_at_most_one_async_task(self): def validate_end_with_at_most_one_async_task(self) -> Self:
"""Validates that the crew ends with at most one asynchronous task.""" """Validates that the crew ends with at most one asynchronous task."""
final_async_task_count = 0 final_async_task_count = 0
@@ -505,7 +505,9 @@ class Crew(FlowTrackable, BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def validate_async_task_cannot_include_sequential_async_tasks_in_context(self): def validate_async_task_cannot_include_sequential_async_tasks_in_context(
self,
) -> Self:
""" """
Validates that if a task is set to be executed asynchronously, Validates that if a task is set to be executed asynchronously,
it cannot include other asynchronous tasks in its context unless it cannot include other asynchronous tasks in its context unless
@@ -527,7 +529,7 @@ class Crew(FlowTrackable, BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def validate_context_no_future_tasks(self): def validate_context_no_future_tasks(self) -> Self:
"""Validates that a task's context does not include future tasks.""" """Validates that a task's context does not include future tasks."""
task_indices = {id(task): i for i, task in enumerate(self.tasks)} task_indices = {id(task): i for i, task in enumerate(self.tasks)}
@@ -561,7 +563,7 @@ class Crew(FlowTrackable, BaseModel):
""" """
return self.security_config.fingerprint return self.security_config.fingerprint
def _setup_from_config(self): def _setup_from_config(self) -> None:
"""Initializes agents and tasks from the provided config.""" """Initializes agents and tasks from the provided config."""
if self.config is None: if self.config is None:
raise ValueError("Config should not be None.") raise ValueError("Config should not be None.")
@@ -628,12 +630,12 @@ class Crew(FlowTrackable, BaseModel):
for agent in train_crew.agents: for agent in train_crew.agents:
if training_data.get(str(agent.id)): if training_data.get(str(agent.id)):
result = TaskEvaluator(agent).evaluate_training_data( result = TaskEvaluator(agent).evaluate_training_data( # type: ignore[arg-type]
training_data=training_data, agent_id=str(agent.id) training_data=training_data, agent_id=str(agent.id)
) )
CrewTrainingHandler(filename).save_trained_data( CrewTrainingHandler(filename).save_trained_data(
agent_id=str(agent.role), agent_id=str(agent.role),
trained_data=result.model_dump(), # type: ignore[arg-type] trained_data=result.model_dump(),
) )
crewai_event_bus.emit( crewai_event_bus.emit(
@@ -684,12 +686,8 @@ class Crew(FlowTrackable, BaseModel):
self._set_tasks_callbacks() self._set_tasks_callbacks()
self._set_allow_crewai_trigger_context_for_first_task() self._set_allow_crewai_trigger_context_for_first_task()
i18n = I18N(prompt_file=self.prompt_file)
for agent in self.agents: for agent in self.agents:
agent.i18n = i18n agent.crew = self
# type: ignore[attr-defined] # Argument 1 to "_interpolate_inputs" of "Crew" has incompatible type "dict[str, Any] | None"; expected "dict[str, Any]"
agent.crew = self # type: ignore[attr-defined]
agent.set_knowledge(crew_embedder=self.embedder) agent.set_knowledge(crew_embedder=self.embedder)
# TODO: Create an AgentFunctionCalling protocol for future refactoring # TODO: Create an AgentFunctionCalling protocol for future refactoring
if not agent.function_calling_llm: # type: ignore # "BaseAgent" has no attribute "function_calling_llm" if not agent.function_calling_llm: # type: ignore # "BaseAgent" has no attribute "function_calling_llm"
@@ -753,10 +751,12 @@ class Crew(FlowTrackable, BaseModel):
inputs = inputs or {} inputs = inputs or {}
return await asyncio.to_thread(self.kickoff, inputs) return await asyncio.to_thread(self.kickoff, inputs)
async def kickoff_for_each_async(self, inputs: list[dict]) -> list[CrewOutput]: async def kickoff_for_each_async(
self, inputs: list[dict[str, Any]]
) -> list[CrewOutput]:
crew_copies = [self.copy() for _ in inputs] crew_copies = [self.copy() for _ in inputs]
async def run_crew(crew, input_data): async def run_crew(crew: Self, input_data: Any) -> CrewOutput:
return await crew.kickoff_async(inputs=input_data) return await crew.kickoff_async(inputs=input_data)
tasks = [ tasks = [
@@ -775,7 +775,7 @@ class Crew(FlowTrackable, BaseModel):
self._task_output_handler.reset() self._task_output_handler.reset()
return results return results
def _handle_crew_planning(self): def _handle_crew_planning(self) -> None:
"""Handles the Crew planning.""" """Handles the Crew planning."""
self._logger.log("info", "Planning the crew execution") self._logger.log("info", "Planning the crew execution")
result = CrewPlanner( result = CrewPlanner(
@@ -793,7 +793,7 @@ class Crew(FlowTrackable, BaseModel):
output: TaskOutput, output: TaskOutput,
task_index: int, task_index: int,
was_replayed: bool = False, was_replayed: bool = False,
): ) -> None:
if self._inputs: if self._inputs:
inputs = self._inputs inputs = self._inputs
else: else:
@@ -825,19 +825,21 @@ class Crew(FlowTrackable, BaseModel):
self._create_manager_agent() self._create_manager_agent()
return self._execute_tasks(self.tasks) return self._execute_tasks(self.tasks)
def _create_manager_agent(self): def _create_manager_agent(self) -> None:
i18n = I18N(prompt_file=self.prompt_file)
if self.manager_agent is not None: if self.manager_agent is not None:
self.manager_agent.allow_delegation = True self.manager_agent.allow_delegation = True
manager = self.manager_agent manager = self.manager_agent
if manager.tools is not None and len(manager.tools) > 0: if manager.tools is not None and len(manager.tools) > 0:
self._logger.log( self._logger.log(
"warning", "Manager agent should not have tools", color="orange" "warning",
"Manager agent should not have tools",
color="bold_yellow",
) )
manager.tools = [] manager.tools = []
raise Exception("Manager agent should not have tools") raise Exception("Manager agent should not have tools")
else: else:
self.manager_llm = create_llm(self.manager_llm) self.manager_llm = create_llm(self.manager_llm)
i18n = get_i18n(prompt_file=self.prompt_file)
manager = Agent( manager = Agent(
role=i18n.retrieve("hierarchical_manager_agent", "role"), role=i18n.retrieve("hierarchical_manager_agent", "role"),
goal=i18n.retrieve("hierarchical_manager_agent", "goal"), goal=i18n.retrieve("hierarchical_manager_agent", "goal"),
@@ -895,7 +897,7 @@ class Crew(FlowTrackable, BaseModel):
tools_for_task = self._prepare_tools( tools_for_task = self._prepare_tools(
agent_to_use, agent_to_use,
task, task,
cast(list[Tool] | list[BaseTool], tools_for_task), tools_for_task,
) )
self._log_task_start(task, agent_to_use.role) self._log_task_start(task, agent_to_use.role)
@@ -915,7 +917,7 @@ class Crew(FlowTrackable, BaseModel):
future = task.execute_async( future = task.execute_async(
agent=agent_to_use, agent=agent_to_use,
context=context, context=context,
tools=cast(list[BaseTool], tools_for_task), tools=tools_for_task,
) )
futures.append((task, future, task_index)) futures.append((task, future, task_index))
else: else:
@@ -927,7 +929,7 @@ class Crew(FlowTrackable, BaseModel):
task_output = task.execute_sync( task_output = task.execute_sync(
agent=agent_to_use, agent=agent_to_use,
context=context, context=context,
tools=cast(list[BaseTool], tools_for_task), tools=tools_for_task,
) )
task_outputs.append(task_output) task_outputs.append(task_output)
self._process_task_result(task, task_output) self._process_task_result(task, task_output)
@@ -965,7 +967,7 @@ class Crew(FlowTrackable, BaseModel):
return None return None
def _prepare_tools( def _prepare_tools(
self, agent: BaseAgent, task: Task, tools: list[Tool] | list[BaseTool] self, agent: BaseAgent, task: Task, tools: list[BaseTool]
) -> list[BaseTool]: ) -> list[BaseTool]:
# Add delegation tools if agent allows delegation # Add delegation tools if agent allows delegation
if hasattr(agent, "allow_delegation") and getattr( if hasattr(agent, "allow_delegation") and getattr(
@@ -1002,21 +1004,21 @@ class Crew(FlowTrackable, BaseModel):
tools = self._add_mcp_tools(task, tools) tools = self._add_mcp_tools(task, tools)
# Return a list[BaseTool] compatible with Task.execute_sync and execute_async # Return a list[BaseTool] compatible with Task.execute_sync and execute_async
return cast(list[BaseTool], tools) return tools
def _get_agent_to_use(self, task: Task) -> BaseAgent | None: def _get_agent_to_use(self, task: Task) -> BaseAgent | None:
if self.process == Process.hierarchical: if self.process == Process.hierarchical:
return self.manager_agent return self.manager_agent
return task.agent return task.agent
@staticmethod
def _merge_tools( def _merge_tools(
self, existing_tools: list[BaseTool],
existing_tools: list[Tool] | list[BaseTool], new_tools: list[BaseTool],
new_tools: list[Tool] | list[BaseTool],
) -> list[BaseTool]: ) -> list[BaseTool]:
"""Merge new tools into existing tools list, avoiding duplicates.""" """Merge new tools into existing tools list, avoiding duplicates."""
if not new_tools: if not new_tools:
return cast(list[BaseTool], existing_tools) return existing_tools
# Create mapping of tool names to new tools # Create mapping of tool names to new tools
new_tool_map = {tool.name: tool for tool in new_tools} new_tool_map = {tool.name: tool for tool in new_tools}
@@ -1027,63 +1029,62 @@ class Crew(FlowTrackable, BaseModel):
# Add all new tools # Add all new tools
tools.extend(new_tools) tools.extend(new_tools)
return cast(list[BaseTool], tools) return tools
def _inject_delegation_tools( def _inject_delegation_tools(
self, self,
tools: list[Tool] | list[BaseTool], tools: list[BaseTool],
task_agent: BaseAgent, task_agent: BaseAgent,
agents: list[BaseAgent], agents: list[BaseAgent],
) -> list[BaseTool]: ) -> list[BaseTool]:
if hasattr(task_agent, "get_delegation_tools"): if hasattr(task_agent, "get_delegation_tools"):
delegation_tools = task_agent.get_delegation_tools(agents) delegation_tools = task_agent.get_delegation_tools(agents)
# Cast delegation_tools to the expected type for _merge_tools # Cast delegation_tools to the expected type for _merge_tools
return self._merge_tools(tools, cast(list[BaseTool], delegation_tools)) return self._merge_tools(tools, delegation_tools)
return cast(list[BaseTool], tools) return tools
def _inject_platform_tools( def _inject_platform_tools(
self, self,
tools: list[Tool] | list[BaseTool], tools: list[BaseTool],
task_agent: BaseAgent, task_agent: BaseAgent,
) -> list[BaseTool]: ) -> list[BaseTool]:
apps = getattr(task_agent, "apps", None) or [] apps = getattr(task_agent, "apps", None) or []
if hasattr(task_agent, "get_platform_tools") and apps: if hasattr(task_agent, "get_platform_tools") and apps:
platform_tools = task_agent.get_platform_tools(apps=apps) platform_tools = task_agent.get_platform_tools(apps=apps)
return self._merge_tools(tools, cast(list[BaseTool], platform_tools)) return self._merge_tools(tools, platform_tools)
return cast(list[BaseTool], tools) return tools
def _inject_mcp_tools( def _inject_mcp_tools(
self, self,
tools: list[Tool] | list[BaseTool], tools: list[BaseTool],
task_agent: BaseAgent, task_agent: BaseAgent,
) -> list[BaseTool]: ) -> list[BaseTool]:
mcps = getattr(task_agent, "mcps", None) or [] mcps = getattr(task_agent, "mcps", None) or []
if hasattr(task_agent, "get_mcp_tools") and mcps: if hasattr(task_agent, "get_mcp_tools") and mcps:
mcp_tools = task_agent.get_mcp_tools(mcps=mcps) mcp_tools = task_agent.get_mcp_tools(mcps=mcps)
return self._merge_tools(tools, cast(list[BaseTool], mcp_tools)) return self._merge_tools(tools, mcp_tools)
return cast(list[BaseTool], tools) return tools
def _add_multimodal_tools( def _add_multimodal_tools(
self, agent: BaseAgent, tools: list[Tool] | list[BaseTool] self, agent: BaseAgent, tools: list[BaseTool]
) -> list[BaseTool]: ) -> list[BaseTool]:
if hasattr(agent, "get_multimodal_tools"): if hasattr(agent, "get_multimodal_tools"):
multimodal_tools = agent.get_multimodal_tools() multimodal_tools = agent.get_multimodal_tools()
# Cast multimodal_tools to the expected type for _merge_tools
return self._merge_tools(tools, cast(list[BaseTool], multimodal_tools)) return self._merge_tools(tools, cast(list[BaseTool], multimodal_tools))
return cast(list[BaseTool], tools) return tools
def _add_code_execution_tools( def _add_code_execution_tools(
self, agent: BaseAgent, tools: list[Tool] | list[BaseTool] self, agent: BaseAgent, tools: list[BaseTool]
) -> list[BaseTool]: ) -> list[BaseTool]:
if hasattr(agent, "get_code_execution_tools"): if hasattr(agent, "get_code_execution_tools"):
code_tools = agent.get_code_execution_tools() code_tools = agent.get_code_execution_tools()
# Cast code_tools to the expected type for _merge_tools # Cast code_tools to the expected type for _merge_tools
return self._merge_tools(tools, cast(list[BaseTool], code_tools)) return self._merge_tools(tools, cast(list[BaseTool], code_tools))
return cast(list[BaseTool], tools) return tools
def _add_delegation_tools( def _add_delegation_tools(
self, task: Task, tools: list[Tool] | list[BaseTool] self, task: Task, tools: list[BaseTool]
) -> list[BaseTool]: ) -> list[BaseTool]:
agents_for_delegation = [agent for agent in self.agents if agent != task.agent] agents_for_delegation = [agent for agent in self.agents if agent != task.agent]
if len(self.agents) > 1 and len(agents_for_delegation) > 0 and task.agent: if len(self.agents) > 1 and len(agents_for_delegation) > 0 and task.agent:
@@ -1092,25 +1093,21 @@ class Crew(FlowTrackable, BaseModel):
tools = self._inject_delegation_tools( tools = self._inject_delegation_tools(
tools, task.agent, agents_for_delegation tools, task.agent, agents_for_delegation
) )
return cast(list[BaseTool], tools) return tools
def _add_platform_tools( def _add_platform_tools(self, task: Task, tools: list[BaseTool]) -> list[BaseTool]:
self, task: Task, tools: list[Tool] | list[BaseTool]
) -> list[BaseTool]:
if task.agent: if task.agent:
tools = self._inject_platform_tools(tools, task.agent) tools = self._inject_platform_tools(tools, task.agent)
return cast(list[BaseTool], tools or []) return tools or []
def _add_mcp_tools( def _add_mcp_tools(self, task: Task, tools: list[BaseTool]) -> list[BaseTool]:
self, task: Task, tools: list[Tool] | list[BaseTool]
) -> list[BaseTool]:
if task.agent: if task.agent:
tools = self._inject_mcp_tools(tools, task.agent) tools = self._inject_mcp_tools(tools, task.agent)
return cast(list[BaseTool], tools or []) return tools or []
def _log_task_start(self, task: Task, role: str = "None"): def _log_task_start(self, task: Task, role: str = "None") -> None:
if self.output_log_file: if self.output_log_file:
self._file_handler.log( self._file_handler.log(
task_name=task.name, # type: ignore[arg-type] task_name=task.name, # type: ignore[arg-type]
@@ -1120,7 +1117,7 @@ class Crew(FlowTrackable, BaseModel):
) )
def _update_manager_tools( def _update_manager_tools(
self, task: Task, tools: list[Tool] | list[BaseTool] self, task: Task, tools: list[BaseTool]
) -> list[BaseTool]: ) -> list[BaseTool]:
if self.manager_agent: if self.manager_agent:
if task.agent: if task.agent:
@@ -1129,7 +1126,7 @@ class Crew(FlowTrackable, BaseModel):
tools = self._inject_delegation_tools( tools = self._inject_delegation_tools(
tools, self.manager_agent, self.agents tools, self.manager_agent, self.agents
) )
return cast(list[BaseTool], tools) return tools
def _get_context(self, task: Task, task_outputs: list[TaskOutput]) -> str: def _get_context(self, task: Task, task_outputs: list[TaskOutput]) -> str:
if not task.context: if not task.context:
@@ -1280,7 +1277,7 @@ class Crew(FlowTrackable, BaseModel):
return required_inputs return required_inputs
def copy(self): def copy(self) -> Crew: # type: ignore[override]
""" """
Creates a deep copy of the Crew instance. Creates a deep copy of the Crew instance.
@@ -1311,7 +1308,7 @@ class Crew(FlowTrackable, BaseModel):
manager_agent = self.manager_agent.copy() if self.manager_agent else None manager_agent = self.manager_agent.copy() if self.manager_agent else None
manager_llm = shallow_copy(self.manager_llm) if self.manager_llm else None manager_llm = shallow_copy(self.manager_llm) if self.manager_llm else None
task_mapping = {} task_mapping: dict[str, Any] = {}
cloned_tasks = [] cloned_tasks = []
existing_knowledge_sources = shallow_copy(self.knowledge_sources) existing_knowledge_sources = shallow_copy(self.knowledge_sources)
@@ -1373,7 +1370,6 @@ class Crew(FlowTrackable, BaseModel):
) )
for task in self.tasks for task in self.tasks
] ]
# type: ignore # "interpolate_inputs" of "Agent" does not return a value (it only ever returns None)
for agent in self.agents: for agent in self.agents:
agent.interpolate_inputs(inputs) agent.interpolate_inputs(inputs)
@@ -1463,7 +1459,7 @@ class Crew(FlowTrackable, BaseModel):
) )
raise raise
def __repr__(self): def __repr__(self) -> str:
return ( return (
f"Crew(id={self.id}, process={self.process}, " f"Crew(id={self.id}, process={self.process}, "
f"number_of_agents={len(self.agents)}, " f"number_of_agents={len(self.agents)}, "
@@ -1520,7 +1516,9 @@ class Crew(FlowTrackable, BaseModel):
if (system := config.get("system")) is not None: if (system := config.get("system")) is not None:
name = config.get("name") name = config.get("name")
try: try:
reset_fn: Callable = cast(Callable, config.get("reset")) reset_fn: Callable[[Any], Any] = cast(
Callable[[Any], Any], config.get("reset")
)
reset_fn(system) reset_fn(system)
self._logger.log( self._logger.log(
"info", "info",
@@ -1551,7 +1549,9 @@ class Crew(FlowTrackable, BaseModel):
raise RuntimeError(f"{name} memory system is not initialized") raise RuntimeError(f"{name} memory system is not initialized")
try: try:
reset_fn: Callable = cast(Callable, config.get("reset")) reset_fn: Callable[[Any], Any] = cast(
Callable[[Any], Any], config.get("reset")
)
reset_fn(system) reset_fn(system)
self._logger.log( self._logger.log(
"info", "info",
@@ -1564,7 +1564,7 @@ class Crew(FlowTrackable, BaseModel):
f"Failed to reset {name} memory: {e!s}" f"Failed to reset {name} memory: {e!s}"
) from e ) from e
def _get_memory_systems(self): def _get_memory_systems(self) -> dict[str, Any]:
"""Get all available memory systems with their configuration. """Get all available memory systems with their configuration.
Returns: Returns:
@@ -1572,10 +1572,10 @@ class Crew(FlowTrackable, BaseModel):
display names. display names.
""" """
def default_reset(memory): def default_reset(memory: Any) -> Any:
return memory.reset() return memory.reset()
def knowledge_reset(memory): def knowledge_reset(memory: Any) -> Any:
return self.reset_knowledge(memory) return self.reset_knowledge(memory)
# Get knowledge for agents # Get knowledge for agents
@@ -1635,7 +1635,7 @@ class Crew(FlowTrackable, BaseModel):
for ks in knowledges: for ks in knowledges:
ks.reset() ks.reset()
def _set_allow_crewai_trigger_context_for_first_task(self): def _set_allow_crewai_trigger_context_for_first_task(self) -> None:
crewai_trigger_payload = self._inputs and self._inputs.get( crewai_trigger_payload = self._inputs and self._inputs.get(
"crewai_trigger_payload" "crewai_trigger_payload"
) )

View File

@@ -61,7 +61,7 @@ from crewai.utilities.agent_utils import (
from crewai.utilities.converter import generate_model_description from crewai.utilities.converter import generate_model_description
from crewai.utilities.guardrail import process_guardrail from crewai.utilities.guardrail import process_guardrail
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.llm_utils import create_llm from crewai.utilities.llm_utils import create_llm
from crewai.utilities.printer import Printer from crewai.utilities.printer import Printer
from crewai.utilities.token_counter_callback import TokenCalcHandler from crewai.utilities.token_counter_callback import TokenCalcHandler
@@ -90,8 +90,6 @@ class LiteAgent(FlowTrackable, BaseModel):
""" """
model_config = {"arbitrary_types_allowed": True} model_config = {"arbitrary_types_allowed": True}
# Core Agent Properties
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True) id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
role: str = Field(description="Role of the agent") role: str = Field(description="Role of the agent")
goal: str = Field(description="Goal of the agent") goal: str = Field(description="Goal of the agent")
@@ -102,8 +100,6 @@ class LiteAgent(FlowTrackable, BaseModel):
tools: list[BaseTool] = Field( tools: list[BaseTool] = Field(
default_factory=list, description="Tools at agent's disposal" default_factory=list, description="Tools at agent's disposal"
) )
# Execution Control Properties
max_iterations: int = Field( max_iterations: int = Field(
default=15, description="Maximum number of iterations for tool usage" default=15, description="Maximum number of iterations for tool usage"
) )
@@ -120,24 +116,17 @@ class LiteAgent(FlowTrackable, BaseModel):
) )
request_within_rpm_limit: Callable[[], bool] | None = Field( request_within_rpm_limit: Callable[[], bool] | None = Field(
default=None, default=None,
description="Callback to check if the request is within the RPM limit", description="Callback to check if the request is within the RPM8 limit",
) )
i18n: I18N = Field( i18n: I18N = Field(
default_factory=I18N, description="Internationalization settings." default_factory=get_i18n, description="Internationalization settings."
) )
# Output and Formatting Properties
response_format: type[BaseModel] | None = Field( response_format: type[BaseModel] | None = Field(
default=None, description="Pydantic model for structured output" default=None, description="Pydantic model for structured output"
) )
verbose: bool = Field( verbose: bool = Field(
default=False, description="Whether to print execution details" default=False, description="Whether to print execution details"
) )
callbacks: list[Callable] = Field(
default_factory=list, description="Callbacks to be used for the agent"
)
# Guardrail Properties
guardrail: GuardrailType | None = Field( guardrail: GuardrailType | None = Field(
default=None, default=None,
description="Function or string description of a guardrail to validate agent output", description="Function or string description of a guardrail to validate agent output",
@@ -145,17 +134,12 @@ class LiteAgent(FlowTrackable, BaseModel):
guardrail_max_retries: int = Field( guardrail_max_retries: int = Field(
default=3, description="Maximum number of retries when guardrail fails" default=3, description="Maximum number of retries when guardrail fails"
) )
# State and Results
tools_results: list[dict[str, Any]] = Field( tools_results: list[dict[str, Any]] = Field(
default_factory=list, description="Results of the tools used by the agent." default_factory=list, description="Results of the tools used by the agent."
) )
# Reference of Agent
original_agent: BaseAgent | None = Field( original_agent: BaseAgent | None = Field(
default=None, description="Reference to the agent that created this LiteAgent" default=None, description="Reference to the agent that created this LiteAgent"
) )
# Private Attributes
_parsed_tools: list[CrewStructuredTool] = PrivateAttr(default_factory=list) _parsed_tools: list[CrewStructuredTool] = PrivateAttr(default_factory=list)
_token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess) _token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess)
_cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler) _cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler)
@@ -165,6 +149,7 @@ class LiteAgent(FlowTrackable, BaseModel):
_printer: Printer = PrivateAttr(default_factory=Printer) _printer: Printer = PrivateAttr(default_factory=Printer)
_guardrail: GuardrailCallable | None = PrivateAttr(default=None) _guardrail: GuardrailCallable | None = PrivateAttr(default=None)
_guardrail_retry_count: int = PrivateAttr(default=0) _guardrail_retry_count: int = PrivateAttr(default=0)
_callbacks: list[TokenCalcHandler] = PrivateAttr(default_factory=list)
@model_validator(mode="after") @model_validator(mode="after")
def setup_llm(self) -> Self: def setup_llm(self) -> Self:
@@ -174,15 +159,13 @@ class LiteAgent(FlowTrackable, BaseModel):
raise ValueError( raise ValueError(
f"Expected LLM instance of type BaseLLM, got {type(self.llm).__name__}" f"Expected LLM instance of type BaseLLM, got {type(self.llm).__name__}"
) )
# Initialize callbacks
token_callback = TokenCalcHandler(token_cost_process=self._token_process) token_callback = TokenCalcHandler(token_cost_process=self._token_process)
self._callbacks = [token_callback] self._callbacks = [token_callback]
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def parse_tools(self): def parse_tools(self) -> Self:
"""Parse the tools and convert them to CrewStructuredTool instances.""" """Parse the tools and convert them to CrewStructuredTool instances."""
self._parsed_tools = parse_tools(self.tools) self._parsed_tools = parse_tools(self.tools)
@@ -201,7 +184,7 @@ class LiteAgent(FlowTrackable, BaseModel):
) )
self._guardrail = cast( self._guardrail = cast(
GuardrailCallable, GuardrailCallable,
LLMGuardrail(description=self.guardrail, llm=self.llm), cast(object, LLMGuardrail(description=self.guardrail, llm=self.llm)),
) )
return self return self
@@ -209,8 +192,8 @@ class LiteAgent(FlowTrackable, BaseModel):
@field_validator("guardrail", mode="before") @field_validator("guardrail", mode="before")
@classmethod @classmethod
def validate_guardrail_function( def validate_guardrail_function(
cls, v: Callable | str | None cls, v: GuardrailCallable | str | None
) -> Callable | str | None: ) -> GuardrailCallable | str | None:
"""Validate that the guardrail function has the correct signature. """Validate that the guardrail function has the correct signature.
If v is a callable, validate that it has the correct signature. If v is a callable, validate that it has the correct signature.
@@ -559,7 +542,7 @@ class LiteAgent(FlowTrackable, BaseModel):
self._show_logs(formatted_answer) self._show_logs(formatted_answer)
return formatted_answer return formatted_answer
def _show_logs(self, formatted_answer: AgentAction | AgentFinish): def _show_logs(self, formatted_answer: AgentAction | AgentFinish) -> None:
"""Show logs for the agent's execution.""" """Show logs for the agent's execution."""
crewai_event_bus.emit( crewai_event_bus.emit(
self, self,
@@ -574,4 +557,4 @@ class LiteAgent(FlowTrackable, BaseModel):
self, text: str, role: Literal["user", "assistant", "system"] = "assistant" self, text: str, role: Literal["user", "assistant", "system"] = "assistant"
) -> None: ) -> None:
"""Append a message to the message list with the given role.""" """Append a message to the message list with the given role."""
self._messages.append(cast(LLMMessage, format_message_for_llm(text, role=role))) self._messages.append(format_message_for_llm(text, role=role))

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from concurrent.futures import Future from concurrent.futures import Future
from copy import copy as shallow_copy from copy import copy as shallow_copy
import datetime import datetime
@@ -29,6 +28,7 @@ from pydantic import (
model_validator, model_validator,
) )
from pydantic_core import PydanticCustomError from pydantic_core import PydanticCustomError
from typing_extensions import Self
from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.events.event_bus import crewai_event_bus from crewai.events.event_bus import crewai_event_bus
@@ -52,7 +52,7 @@ from crewai.utilities.guardrail_types import (
GuardrailType, GuardrailType,
GuardrailsType, GuardrailsType,
) )
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.printer import Printer from crewai.utilities.printer import Printer
from crewai.utilities.string_utils import interpolate_only from crewai.utilities.string_utils import interpolate_only
@@ -90,7 +90,7 @@ class Task(BaseModel):
used_tools: int = 0 used_tools: int = 0
tools_errors: int = 0 tools_errors: int = 0
delegations: int = 0 delegations: int = 0
i18n: I18N = Field(default_factory=I18N) i18n: I18N = Field(default_factory=get_i18n)
name: str | None = Field(default=None) name: str | None = Field(default=None)
prompt_context: str | None = None prompt_context: str | None = None
description: str = Field(description="Description of the actual task.") description: str = Field(description="Description of the actual task.")
@@ -207,8 +207,8 @@ class Task(BaseModel):
@field_validator("guardrail") @field_validator("guardrail")
@classmethod @classmethod
def validate_guardrail_function( def validate_guardrail_function(
cls, v: str | Callable | None cls, v: str | GuardrailCallable | None
) -> str | Callable | None: ) -> str | GuardrailCallable | None:
""" """
If v is a callable, validate that the guardrail function has the correct signature and behavior. If v is a callable, validate that the guardrail function has the correct signature and behavior.
If v is a string, return it as is. If v is a string, return it as is.
@@ -265,11 +265,11 @@ class Task(BaseModel):
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod
def process_model_config(cls, values): def process_model_config(cls, values: dict[str, Any]) -> dict[str, Any]:
return process_config(values, cls) return process_config(values, cls)
@model_validator(mode="after") @model_validator(mode="after")
def validate_required_fields(self): def validate_required_fields(self) -> Self:
required_fields = ["description", "expected_output"] required_fields = ["description", "expected_output"]
for field in required_fields: for field in required_fields:
if getattr(self, field) is None: if getattr(self, field) is None:
@@ -418,14 +418,14 @@ class Task(BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def check_tools(self): def check_tools(self) -> Self:
"""Check if the tools are set.""" """Check if the tools are set."""
if not self.tools and self.agent and self.agent.tools: if not self.tools and self.agent and self.agent.tools:
self.tools.extend(self.agent.tools) self.tools = self.agent.tools
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def check_output(self): def check_output(self) -> Self:
"""Check if an output type is set.""" """Check if an output type is set."""
output_types = [self.output_json, self.output_pydantic] output_types = [self.output_json, self.output_pydantic]
if len([type for type in output_types if type]) > 1: if len([type for type in output_types if type]) > 1:
@@ -437,7 +437,7 @@ class Task(BaseModel):
return self return self
@model_validator(mode="after") @model_validator(mode="after")
def handle_max_retries_deprecation(self): def handle_max_retries_deprecation(self) -> Self:
if self.max_retries is not None: if self.max_retries is not None:
warnings.warn( warnings.warn(
"The 'max_retries' parameter is deprecated and will be removed in CrewAI v1.0.0. " "The 'max_retries' parameter is deprecated and will be removed in CrewAI v1.0.0. "
@@ -518,7 +518,7 @@ class Task(BaseModel):
tools = tools or self.tools or [] tools = tools or self.tools or []
self.processed_by_agents.add(agent.role) self.processed_by_agents.add(agent.role)
crewai_event_bus.emit(self, TaskStartedEvent(context=context, task=self)) crewai_event_bus.emit(self, TaskStartedEvent(context=context, task=self)) # type: ignore[no-untyped-call]
result = agent.execute_task( result = agent.execute_task(
task=self, task=self,
context=context, context=context,
@@ -576,12 +576,13 @@ class Task(BaseModel):
) )
self._save_file(content) self._save_file(content)
crewai_event_bus.emit( crewai_event_bus.emit(
self, TaskCompletedEvent(output=task_output, task=self) self,
TaskCompletedEvent(output=task_output, task=self), # type: ignore[no-untyped-call]
) )
return task_output return task_output
except Exception as e: except Exception as e:
self.end_time = datetime.datetime.now() self.end_time = datetime.datetime.now()
crewai_event_bus.emit(self, TaskFailedEvent(error=str(e), task=self)) crewai_event_bus.emit(self, TaskFailedEvent(error=str(e), task=self)) # type: ignore[no-untyped-call]
raise e # Re-raise the exception after emitting the event raise e # Re-raise the exception after emitting the event
def prompt(self) -> str: def prompt(self) -> str:
@@ -786,7 +787,7 @@ Follow these guidelines:
return OutputFormat.PYDANTIC return OutputFormat.PYDANTIC
return OutputFormat.RAW return OutputFormat.RAW
def _save_file(self, result: dict | str | Any) -> None: def _save_file(self, result: dict[str, Any] | str | Any) -> None:
"""Save task output to a file. """Save task output to a file.
Note: Note:
@@ -838,7 +839,7 @@ Follow these guidelines:
) from e ) from e
return return
def __repr__(self): def __repr__(self) -> str:
return f"Task(description={self.description}, expected_output={self.expected_output})" return f"Task(description={self.description}, expected_output={self.expected_output})"
@property @property

View File

@@ -1,6 +1,14 @@
from crewai.agents.agent_builder.base_agent import BaseAgent from __future__ import annotations
from typing import TYPE_CHECKING
from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool
from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool
from crewai.utilities.i18n import get_i18n
if TYPE_CHECKING:
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.tools.base_tool import BaseTool from crewai.tools.base_tool import BaseTool
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N
@@ -10,7 +18,7 @@ class AgentTools:
def __init__(self, agents: list[BaseAgent], i18n: I18N | None = None) -> None: def __init__(self, agents: list[BaseAgent], i18n: I18N | None = None) -> None:
self.agents = agents self.agents = agents
self.i18n = i18n if i18n is not None else I18N() self.i18n = i18n if i18n is not None else get_i18n()
def tools(self) -> list[BaseTool]: def tools(self) -> list[BaseTool]:
"""Get all available agent tools""" """Get all available agent tools"""

View File

@@ -1,12 +1,12 @@
import logging import logging
from typing import Any
from pydantic import Field from pydantic import Field
from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.task import Task from crewai.task import Task
from crewai.tools.base_tool import BaseTool from crewai.tools.base_tool import BaseTool
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,7 +17,7 @@ class BaseAgentTool(BaseTool):
agents: list[BaseAgent] = Field(description="List of available agents") agents: list[BaseAgent] = Field(description="List of available agents")
i18n: I18N = Field( i18n: I18N = Field(
default_factory=I18N, description="Internationalization settings" default_factory=get_i18n, description="Internationalization settings"
) )
def sanitize_agent_name(self, name: str) -> str: def sanitize_agent_name(self, name: str) -> str:
@@ -40,7 +40,7 @@ class BaseAgentTool(BaseTool):
return normalized.replace('"', "").casefold() return normalized.replace('"', "").casefold()
@staticmethod @staticmethod
def _get_coworker(coworker: str | None, **kwargs) -> str | None: def _get_coworker(coworker: str | None, **kwargs: Any) -> str | None:
coworker = coworker or kwargs.get("co_worker") or kwargs.get("coworker") coworker = coworker or kwargs.get("co_worker") or kwargs.get("coworker")
if coworker: if coworker:
is_list = coworker.startswith("[") and coworker.endswith("]") is_list = coworker.startswith("[") and coworker.endswith("]")
@@ -83,7 +83,7 @@ class BaseAgentTool(BaseTool):
available_agents = [agent.role for agent in self.agents] available_agents = [agent.role for agent in self.agents]
logger.debug(f"Available agents: {available_agents}") logger.debug(f"Available agents: {available_agents}")
agent = [ # type: ignore # Incompatible types in assignment (expression has type "list[BaseAgent]", variable has type "str | None") agent = [
available_agent available_agent
for available_agent in self.agents for available_agent in self.agents
if self.sanitize_agent_name(available_agent.role) == sanitized_name if self.sanitize_agent_name(available_agent.role) == sanitized_name

View File

@@ -7,10 +7,10 @@ import json
from json import JSONDecodeError from json import JSONDecodeError
from textwrap import dedent from textwrap import dedent
import time import time
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Literal
import json5 import json5
from json_repair import repair_json # type: ignore[import-untyped,import-error] from json_repair import repair_json # type: ignore[import-untyped]
from crewai.events.event_bus import crewai_event_bus from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.tool_usage_events import ( from crewai.events.types.tool_usage_events import (
@@ -28,7 +28,7 @@ from crewai.utilities.agent_utils import (
render_text_description_and_args, render_text_description_and_args,
) )
from crewai.utilities.converter import Converter from crewai.utilities.converter import Converter
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.printer import Printer from crewai.utilities.printer import Printer
@@ -39,7 +39,18 @@ if TYPE_CHECKING:
from crewai.llm import LLM from crewai.llm import LLM
from crewai.task import Task from crewai.task import Task
OPENAI_BIGGER_MODELS = [
OPENAI_BIGGER_MODELS: list[
Literal[
"gpt-4",
"gpt-4o",
"o1-preview",
"o1-mini",
"o1",
"o3",
"o3-mini",
]
] = [
"gpt-4", "gpt-4",
"gpt-4o", "gpt-4o",
"o1-preview", "o1-preview",
@@ -81,7 +92,7 @@ class ToolUsage:
action: Any = None, action: Any = None,
fingerprint_context: dict[str, str] | None = None, fingerprint_context: dict[str, str] | None = None,
) -> None: ) -> None:
self._i18n: I18N = agent.i18n if agent else I18N() self._i18n: I18N = agent.i18n if agent else get_i18n()
self._printer: Printer = Printer() self._printer: Printer = Printer()
self._telemetry: Telemetry = Telemetry() self._telemetry: Telemetry = Telemetry()
self._run_attempts: int = 1 self._run_attempts: int = 1
@@ -100,12 +111,14 @@ class ToolUsage:
# Set the maximum parsing attempts for bigger models # Set the maximum parsing attempts for bigger models
if ( if (
self.function_calling_llm self.function_calling_llm
and self.function_calling_llm in OPENAI_BIGGER_MODELS and self.function_calling_llm.model in OPENAI_BIGGER_MODELS
): ):
self._max_parsing_attempts = 2 self._max_parsing_attempts = 2
self._remember_format_after_usages = 4 self._remember_format_after_usages = 4
def parse_tool_calling(self, tool_string: str): def parse_tool_calling(
self, tool_string: str
) -> ToolCalling | InstructorToolCalling | ToolUsageError:
"""Parse the tool string and return the tool calling.""" """Parse the tool string and return the tool calling."""
return self._tool_calling(tool_string) return self._tool_calling(tool_string)
@@ -153,7 +166,7 @@ class ToolUsage:
tool: CrewStructuredTool, tool: CrewStructuredTool,
calling: ToolCalling | InstructorToolCalling, calling: ToolCalling | InstructorToolCalling,
) -> str: ) -> str:
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None) if self._check_tool_repeated_usage(calling=calling):
try: try:
result = self._i18n.errors("task_repeated_usage").format( result = self._i18n.errors("task_repeated_usage").format(
tool_names=self.tools_names tool_names=self.tools_names
@@ -163,7 +176,7 @@ class ToolUsage:
tool_name=tool.name, tool_name=tool.name,
attempts=self._run_attempts, attempts=self._run_attempts,
) )
return self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None) return self._format_result(result=result)
except Exception: except Exception:
if self.task: if self.task:
@@ -241,7 +254,7 @@ class ToolUsage:
try: try:
acceptable_args = tool.args_schema.model_json_schema()[ acceptable_args = tool.args_schema.model_json_schema()[
"properties" "properties"
].keys() # type: ignore ].keys()
arguments = { arguments = {
k: v k: v
for k, v in calling.arguments.items() for k, v in calling.arguments.items()
@@ -276,19 +289,19 @@ class ToolUsage:
self._printer.print( self._printer.print(
content=f"\n\n{error_message}\n", color="red" content=f"\n\n{error_message}\n", color="red"
) )
return error # type: ignore # No return value expected return error
if self.task: if self.task:
self.task.increment_tools_errors() self.task.increment_tools_errors()
return self.use(calling=calling, tool_string=tool_string) # type: ignore # No return value expected return self.use(calling=calling, tool_string=tool_string)
if self.tools_handler: if self.tools_handler:
should_cache = True should_cache = True
if ( if (
hasattr(available_tool, "cache_function") hasattr(available_tool, "cache_function")
and available_tool.cache_function # type: ignore # Item "None" of "Any | None" has no attribute "cache_function" and available_tool.cache_function
): ):
should_cache = available_tool.cache_function( # type: ignore # Item "None" of "Any | None" has no attribute "cache_function" should_cache = available_tool.cache_function(
calling.arguments, result calling.arguments, result
) )
@@ -300,7 +313,7 @@ class ToolUsage:
tool_name=tool.name, tool_name=tool.name,
attempts=self._run_attempts, attempts=self._run_attempts,
) )
result = self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None) result = self._format_result(result=result)
data = { data = {
"result": result, "result": result,
"tool_name": tool.name, "tool_name": tool.name,
@@ -508,7 +521,7 @@ class ToolUsage:
self.task.increment_tools_errors() self.task.increment_tools_errors()
if self.agent and self.agent.verbose: if self.agent and self.agent.verbose:
self._printer.print(content=f"\n\n{e}\n", color="red") self._printer.print(content=f"\n\n{e}\n", color="red")
return ToolUsageError( # type: ignore # Incompatible return value type (got "ToolUsageError", expected "ToolCalling | InstructorToolCalling") return ToolUsageError(
f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}" f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
) )
return self._tool_calling(tool_string) return self._tool_calling(tool_string)
@@ -567,7 +580,7 @@ class ToolUsage:
# If all parsing attempts fail, raise an error # If all parsing attempts fail, raise an error
raise Exception(error_message) raise Exception(error_message)
def _emit_validate_input_error(self, final_error: str): def _emit_validate_input_error(self, final_error: str) -> None:
tool_selection_data = { tool_selection_data = {
"agent_key": getattr(self.agent, "key", None) if self.agent else None, "agent_key": getattr(self.agent, "key", None) if self.agent else None,
"agent_role": getattr(self.agent, "role", None) if self.agent else None, "agent_role": getattr(self.agent, "role", None) if self.agent else None,
@@ -636,7 +649,7 @@ class ToolUsage:
def _prepare_event_data( def _prepare_event_data(
self, tool: Any, tool_calling: ToolCalling | InstructorToolCalling self, tool: Any, tool_calling: ToolCalling | InstructorToolCalling
) -> dict: ) -> dict[str, Any]:
event_data = { event_data = {
"run_attempts": self._run_attempts, "run_attempts": self._run_attempts,
"delegations": self.task.delegations if self.task else 0, "delegations": self.task.delegations if self.task else 0,
@@ -660,7 +673,7 @@ class ToolUsage:
return event_data return event_data
def _add_fingerprint_metadata(self, arguments: dict) -> dict: def _add_fingerprint_metadata(self, arguments: dict[str, Any]) -> dict[str, Any]:
"""Add fingerprint metadata to tool arguments if available. """Add fingerprint metadata to tool arguments if available.
Args: Args:

View File

@@ -1,5 +1,6 @@
"""Internationalization support for CrewAI prompts and messages.""" """Internationalization support for CrewAI prompts and messages."""
from functools import lru_cache
import json import json
import os import os
from typing import Literal from typing import Literal
@@ -108,3 +109,19 @@ class I18N(BaseModel):
return self._prompts[kind][key] return self._prompts[kind][key]
except Exception as e: except Exception as e:
raise Exception(f"Prompt for '{kind}':'{key}' not found.") from e raise Exception(f"Prompt for '{kind}':'{key}' not found.") from e
@lru_cache(maxsize=None)
def get_i18n(prompt_file: str | None = None) -> I18N:
"""Get a cached I18N instance.
This function caches I18N instances to avoid redundant file I/O and JSON parsing.
Each unique prompt_file path gets its own cached instance.
Args:
prompt_file: Optional custom prompt file path. Defaults to None (uses built-in prompts).
Returns:
Cached I18N instance.
"""
return I18N(prompt_file=prompt_file)

View File

@@ -1,29 +1,38 @@
"""Prompt generation and management utilities for CrewAI agents."""
from __future__ import annotations from __future__ import annotations
from typing import Any, TypedDict from typing import Annotated, Any, Literal, TypedDict
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from crewai.utilities.i18n import I18N from crewai.utilities.i18n import I18N, get_i18n
class StandardPromptResult(TypedDict): class StandardPromptResult(TypedDict):
"""Result with only prompt field for standard mode.""" """Result with only prompt field for standard mode."""
prompt: str prompt: Annotated[str, "The generated prompt string"]
class SystemPromptResult(StandardPromptResult): class SystemPromptResult(StandardPromptResult):
"""Result with system, user, and prompt fields for system prompt mode.""" """Result with system, user, and prompt fields for system prompt mode."""
system: str system: Annotated[str, "The system prompt component"]
user: str user: Annotated[str, "The user prompt component"]
COMPONENTS = Literal["role_playing", "tools", "no_tools", "task"]
class Prompts(BaseModel): class Prompts(BaseModel):
"""Manages and generates prompts for a generic agent.""" """Manages and generates prompts for a generic agent.
i18n: I18N = Field(default_factory=I18N) Notes:
- Need to refactor so that prompt is not tightly coupled to agent.
"""
i18n: I18N = Field(default_factory=get_i18n)
has_tools: bool = Field( has_tools: bool = Field(
default=False, description="Indicates if the agent has access to tools" default=False, description="Indicates if the agent has access to tools"
) )
@@ -36,7 +45,7 @@ class Prompts(BaseModel):
response_template: str | None = Field( response_template: str | None = Field(
default=None, description="Custom response prompt template" default=None, description="Custom response prompt template"
) )
use_system_prompt: bool | None = Field( use_system_prompt: bool = Field(
default=False, default=False,
description="Whether to use the system prompt when no custom templates are provided", description="Whether to use the system prompt when no custom templates are provided",
) )
@@ -48,7 +57,7 @@ class Prompts(BaseModel):
Returns: Returns:
A dictionary containing the constructed prompt(s). A dictionary containing the constructed prompt(s).
""" """
slices: list[str] = ["role_playing"] slices: list[COMPONENTS] = ["role_playing"]
if self.has_tools: if self.has_tools:
slices.append("tools") slices.append("tools")
else: else:
@@ -77,7 +86,7 @@ class Prompts(BaseModel):
def _build_prompt( def _build_prompt(
self, self,
components: list[str], components: list[COMPONENTS],
system_template: str | None = None, system_template: str | None = None,
prompt_template: str | None = None, prompt_template: str | None = None,
response_template: str | None = None, response_template: str | None = None,

View File

@@ -13,7 +13,6 @@ from crewai.events.types.reasoning_events import (
) )
from crewai.llm import LLM from crewai.llm import LLM
from crewai.task import Task from crewai.task import Task
from crewai.utilities.i18n import I18N
class ReasoningPlan(BaseModel): class ReasoningPlan(BaseModel):
@@ -62,7 +61,6 @@ class AgentReasoning:
agent: The agent performing the reasoning. agent: The agent performing the reasoning.
llm: The language model used for reasoning. llm: The language model used for reasoning.
logger: Logger for logging events and errors. logger: Logger for logging events and errors.
i18n: Internationalization utility for retrieving prompts.
""" """
def __init__(self, task: Task, agent: Agent) -> None: def __init__(self, task: Task, agent: Agent) -> None:
@@ -76,7 +74,6 @@ class AgentReasoning:
self.agent = agent self.agent = agent
self.llm = cast(LLM, agent.llm) self.llm = cast(LLM, agent.llm)
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.i18n = I18N()
def handle_agent_reasoning(self) -> AgentReasoningOutput: def handle_agent_reasoning(self) -> AgentReasoningOutput:
"""Public method for the reasoning process that creates and refines a plan for the task until the agent is ready to execute it. """Public method for the reasoning process that creates and refines a plan for the task until the agent is ready to execute it.
@@ -163,8 +160,7 @@ class AgentReasoning:
llm=self.llm, llm=self.llm,
prompt=reasoning_prompt, prompt=reasoning_prompt,
task=self.task, task=self.task,
agent=self.agent, reasoning_agent=self.agent,
i18n=self.i18n,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
plan_type="initial_plan", plan_type="initial_plan",
) )
@@ -208,8 +204,7 @@ class AgentReasoning:
llm=self.llm, llm=self.llm,
prompt=refine_prompt, prompt=refine_prompt,
task=self.task, task=self.task,
agent=self.agent, reasoning_agent=self.agent,
i18n=self.i18n,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
plan_type="refine_plan", plan_type="refine_plan",
) )
@@ -238,14 +233,14 @@ class AgentReasoning:
self.logger.debug(f"Using function calling for {prompt_type} reasoning") self.logger.debug(f"Using function calling for {prompt_type} reasoning")
try: try:
system_prompt = self.i18n.retrieve("reasoning", prompt_type).format( system_prompt = self.agent.i18n.retrieve("reasoning", prompt_type).format(
role=self.agent.role, role=self.agent.role,
goal=self.agent.goal, goal=self.agent.goal,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
) )
# Prepare a simple callable that just returns the tool arguments as JSON # Prepare a simple callable that just returns the tool arguments as JSON
def _create_reasoning_plan(plan: str, ready: bool = True): def _create_reasoning_plan(plan: str, ready: bool = True) -> str:
"""Return the reasoning plan result in JSON string form.""" """Return the reasoning plan result in JSON string form."""
return json.dumps({"plan": plan, "ready": ready}) return json.dumps({"plan": plan, "ready": ready})
@@ -281,7 +276,9 @@ class AgentReasoning:
) )
try: try:
system_prompt = self.i18n.retrieve("reasoning", prompt_type).format( system_prompt = self.agent.i18n.retrieve(
"reasoning", prompt_type
).format(
role=self.agent.role, role=self.agent.role,
goal=self.agent.goal, goal=self.agent.goal,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
@@ -326,7 +323,7 @@ class AgentReasoning:
""" """
available_tools = self.__format_available_tools() available_tools = self.__format_available_tools()
return self.i18n.retrieve("reasoning", "create_plan_prompt").format( return self.agent.i18n.retrieve("reasoning", "create_plan_prompt").format(
role=self.agent.role, role=self.agent.role,
goal=self.agent.goal, goal=self.agent.goal,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
@@ -357,7 +354,7 @@ class AgentReasoning:
Returns: Returns:
str: The refine prompt. str: The refine prompt.
""" """
return self.i18n.retrieve("reasoning", "refine_plan_prompt").format( return self.agent.i18n.retrieve("reasoning", "refine_plan_prompt").format(
role=self.agent.role, role=self.agent.role,
goal=self.agent.goal, goal=self.agent.goal,
backstory=self.__get_agent_backstory(), backstory=self.__get_agent_backstory(),
@@ -405,8 +402,7 @@ def _call_llm_with_reasoning_prompt(
llm: LLM, llm: LLM,
prompt: str, prompt: str,
task: Task, task: Task,
agent: Agent, reasoning_agent: Agent,
i18n: I18N,
backstory: str, backstory: str,
plan_type: Literal["initial_plan", "refine_plan"], plan_type: Literal["initial_plan", "refine_plan"],
) -> str: ) -> str:
@@ -416,17 +412,16 @@ def _call_llm_with_reasoning_prompt(
llm: The language model to use. llm: The language model to use.
prompt: The prompt to send to the LLM. prompt: The prompt to send to the LLM.
task: The task for which the agent is reasoning. task: The task for which the agent is reasoning.
agent: The agent performing the reasoning. reasoning_agent: The agent performing the reasoning.
i18n: Internationalization utility for retrieving prompts.
backstory: The agent's backstory. backstory: The agent's backstory.
plan_type: The type of plan being created ("initial_plan" or "refine_plan"). plan_type: The type of plan being created ("initial_plan" or "refine_plan").
Returns: Returns:
The LLM response. The LLM response.
""" """
system_prompt = i18n.retrieve("reasoning", plan_type).format( system_prompt = reasoning_agent.i18n.retrieve("reasoning", plan_type).format(
role=agent.role, role=reasoning_agent.role,
goal=agent.goal, goal=reasoning_agent.goal,
backstory=backstory, backstory=backstory,
) )
@@ -436,6 +431,6 @@ def _call_llm_with_reasoning_prompt(
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
from_task=task, from_task=task,
from_agent=agent, from_agent=reasoning_agent,
) )
return str(response) return str(response)