Fix #5931: Propagate Crew.prompt_file to all components during execution

Previously, setting prompt_file on a Crew had no effect on agents, tasks,
tools, or any other components. The custom I18N instance was only used for
the manager agent's role/goal/backstory in hierarchical mode, while all
other code used the hardcoded I18N_DEFAULT singleton.

This fix introduces a contextvars-based mechanism that scopes a custom I18N
instance to the crew's execution:

- Added get_crew_i18n() / set_crew_i18n() / reset_crew_i18n() to i18n.py
  using a ContextVar for thread-safe crew-scoped I18N overrides
- prepare_kickoff() now sets the crew's I18N when prompt_file is provided
- Crew.kickoff() resets the I18N context in its finally block (even on errors)
- Replaced all I18N_DEFAULT references with get_crew_i18n() calls across
  29 source files so every component (Prompts, Task, agent tools, tool usage,
  memory, reasoning, etc.) respects the crew's prompt_file

Added 12 new tests covering:
- Context variable set/get/reset/nesting behavior
- Prompts class using custom I18N
- Task.prompt() using custom I18N
- AgentTools delegation tools using custom I18N
- Full Crew.kickoff() integration (sets context, resets on success/exception)

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2026-05-26 13:32:07 +00:00
parent bad64b1ee6
commit af5c130836
31 changed files with 557 additions and 121 deletions

View File

@@ -102,7 +102,7 @@ from crewai.utilities.converter import Converter, ConverterError
from crewai.utilities.env import get_env_context
from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_for_json
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.prompts import Prompts, StandardPromptResult, SystemPromptResult
from crewai.utilities.pydantic_schema_utils import generate_model_description
@@ -608,7 +608,7 @@ class Agent(BaseAgent):
m.format() for m in matches
)
if memory.strip() != "":
task_prompt += I18N_DEFAULT.slice("memory").format(memory=memory)
task_prompt += get_crew_i18n().slice("memory").format(memory=memory)
crewai_event_bus.emit(
self,
@@ -1024,7 +1024,7 @@ class Agent(BaseAgent):
response_template=self.response_template,
).task_execution()
stop_words = [I18N_DEFAULT.slice("observation")]
stop_words = [get_crew_i18n().slice("observation")]
if self.response_template:
stop_words.append(
self.response_template.split("{{ .Response }}")[1].strip()
@@ -1310,10 +1310,10 @@ class Agent(BaseAgent):
from_agent=self,
),
)
query = I18N_DEFAULT.slice("knowledge_search_query").format(
query = get_crew_i18n().slice("knowledge_search_query").format(
task_prompt=task_prompt
)
rewriter_prompt = I18N_DEFAULT.slice("knowledge_search_query_system_prompt")
rewriter_prompt = get_crew_i18n().slice("knowledge_search_query_system_prompt")
if not isinstance(self.llm, BaseLLM):
self._logger.log(
"warning",
@@ -1488,7 +1488,7 @@ class Agent(BaseAgent):
m.format() for m in matches
)
if memory_block:
formatted_messages += "\n\n" + I18N_DEFAULT.slice("memory").format(
formatted_messages += "\n\n" + get_crew_i18n().slice("memory").format(
memory=memory_block
)
crewai_event_bus.emit(
@@ -1703,7 +1703,7 @@ class Agent(BaseAgent):
try:
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
instructions = get_crew_i18n().slice("formatted_task_instructions").format(
output_format=schema
)

View File

@@ -67,19 +67,19 @@ def build_task_prompt_with_schema(task: Task, task_prompt: str) -> str:
Returns:
The task prompt potentially augmented with schema instructions.
"""
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
if (task.output_json or task.output_pydantic) and not task.response_model:
if task.output_json:
schema_dict = generate_model_description(task.output_json)
schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2)
task_prompt += "\n" + I18N_DEFAULT.slice(
task_prompt += "\n" + get_crew_i18n().slice(
"formatted_task_instructions"
).format(output_format=schema)
elif task.output_pydantic:
schema_dict = generate_model_description(task.output_pydantic)
schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2)
task_prompt += "\n" + I18N_DEFAULT.slice(
task_prompt += "\n" + get_crew_i18n().slice(
"formatted_task_instructions"
).format(output_format=schema)
return task_prompt
@@ -95,10 +95,10 @@ def format_task_with_context(task_prompt: str, context: str | None) -> str:
Returns:
The task prompt formatted with context if provided.
"""
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
if context:
return I18N_DEFAULT.slice("task_with_context").format(
return get_crew_i18n().slice("task_with_context").format(
task=task_prompt, context=context
)
return task_prompt

View File

@@ -33,7 +33,7 @@ from crewai.tools.base_tool import BaseTool
from crewai.types.callback import SerializableCallable
from crewai.utilities import Logger
from crewai.utilities.converter import Converter
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.import_utils import require
@@ -190,7 +190,7 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
task_prompt = task.prompt() if hasattr(task, "prompt") else str(task)
if context:
task_prompt = I18N_DEFAULT.slice("task_with_context").format(
task_prompt = get_crew_i18n().slice("task_with_context").format(
task=task_prompt, context=context
)

View File

@@ -32,7 +32,7 @@ from crewai.events.types.agent_events import (
from crewai.tools import BaseTool
from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.utilities import Logger
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.import_utils import require
@@ -137,7 +137,7 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
try:
task_prompt: str = task.prompt()
if context:
task_prompt = I18N_DEFAULT.slice("task_with_context").format(
task_prompt = get_crew_i18n().slice("task_with_context").format(
task=task_prompt, context=context
)
crewai_event_bus.emit(

View File

@@ -8,7 +8,7 @@ import json
from typing import Any
from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
class OpenAIConverterAdapter(BaseConverterAdapter):
@@ -59,7 +59,7 @@ class OpenAIConverterAdapter(BaseConverterAdapter):
if not self._output_format:
return base_prompt
output_schema: str = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_schema: str = get_crew_i18n().slice("formatted_task_instructions").format(
output_format=json.dumps(self._schema, indent=2)
)

View File

@@ -70,7 +70,7 @@ from crewai.utilities.agent_utils import (
)
from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.file_store import aget_all_files, get_all_files
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.utilities.tool_utils import (
@@ -751,7 +751,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
if tool_finish:
return tool_finish
reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_prompt = get_crew_i18n().slice("post_tool_reasoning")
reasoning_message: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -774,7 +774,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
if tool_finish:
return tool_finish
reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_prompt = get_crew_i18n().slice("post_tool_reasoning")
reasoning_message = {
"role": "user",
"content": reasoning_prompt,
@@ -1430,7 +1430,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
Returns:
Updated action or final answer.
"""
add_image_tool = I18N_DEFAULT.tools("add_image")
add_image_tool = get_crew_i18n().tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()
@@ -1632,5 +1632,5 @@ class CrewAgentExecutor(BaseAgentExecutor):
Formatted message dict.
"""
return format_message_for_llm(
I18N_DEFAULT.slice("feedback_instructions").format(feedback=feedback)
get_crew_i18n().slice("feedback_instructions").format(feedback=feedback)
)

View File

@@ -19,7 +19,7 @@ from crewai.agents.constants import (
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
UNABLE_TO_REPAIR_JSON_RESULTS,
)
from crewai.utilities.i18n import I18N_DEFAULT as _I18N
from crewai.utilities.i18n import get_crew_i18n
@dataclass
@@ -115,13 +115,13 @@ def parse(text: str) -> AgentAction | AgentFinish:
if not ACTION_REGEX.search(text):
raise OutputParserError(
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{get_crew_i18n().slice('final_answer_format')}",
)
if not ACTION_INPUT_ONLY_REGEX.search(text):
raise OutputParserError(
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
)
err_format = _I18N.slice("format_without_tools")
err_format = get_crew_i18n().slice("format_without_tools")
error = f"{err_format}"
raise OutputParserError(
error,

View File

@@ -23,7 +23,7 @@ from crewai.events.types.observation_events import (
StepObservationStartedEvent,
)
from crewai.utilities.agent_utils import extract_task_section
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.planning_types import StepObservation, TodoItem
from crewai.utilities.types import LLMMessage
@@ -231,7 +231,7 @@ class PlannerObserver:
task_desc = extract_task_section(self.kickoff_input)
task_goal = "Complete the task successfully"
system_prompt = I18N_DEFAULT.retrieve("planning", "observation_system_prompt")
system_prompt = get_crew_i18n().retrieve("planning", "observation_system_prompt")
completed_summary = ""
if all_completed:
@@ -256,7 +256,7 @@ class PlannerObserver:
remaining_lines
)
user_prompt = I18N_DEFAULT.retrieve(
user_prompt = get_crew_i18n().retrieve(
"planning", "observation_user_prompt"
).format(
task_description=task_desc,

View File

@@ -39,7 +39,7 @@ from crewai.utilities.agent_utils import (
process_llm_response,
setup_native_tools,
)
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.planning_types import TodoItem
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
from crewai.utilities.string_utils import sanitize_tool_name
@@ -210,14 +210,14 @@ class StepExecutor:
tools_section = ""
if self.tools and not self._use_native_tools:
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
tools_section = I18N_DEFAULT.retrieve(
tools_section = get_crew_i18n().retrieve(
"planning", "step_executor_tools_section"
).format(tool_names=tool_names)
elif self.tools:
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
tools_section = f"\n\nAvailable tools: {tool_names}"
return I18N_DEFAULT.retrieve("planning", "step_executor_system_prompt").format(
return get_crew_i18n().retrieve("planning", "step_executor_system_prompt").format(
role=role,
backstory=backstory,
goal=goal,
@@ -232,7 +232,7 @@ class StepExecutor:
task_section = extract_task_section(context.task_description)
if task_section:
parts.append(
I18N_DEFAULT.retrieve(
get_crew_i18n().retrieve(
"planning", "step_executor_task_context"
).format(
task_context=task_section,
@@ -240,14 +240,14 @@ class StepExecutor:
)
parts.append(
I18N_DEFAULT.retrieve("planning", "step_executor_user_prompt").format(
get_crew_i18n().retrieve("planning", "step_executor_user_prompt").format(
step_description=todo.description,
)
)
if todo.tool_to_use:
parts.append(
I18N_DEFAULT.retrieve(
get_crew_i18n().retrieve(
"planning", "step_executor_suggested_tool"
).format(
tool_to_use=todo.tool_to_use,
@@ -256,16 +256,16 @@ class StepExecutor:
if context.dependency_results:
parts.append(
I18N_DEFAULT.retrieve("planning", "step_executor_context_header")
get_crew_i18n().retrieve("planning", "step_executor_context_header")
)
for step_num, result in sorted(context.dependency_results.items()):
parts.append(
I18N_DEFAULT.retrieve(
get_crew_i18n().retrieve(
"planning", "step_executor_context_entry"
).format(step_number=step_num, result=result)
)
parts.append(I18N_DEFAULT.retrieve("planning", "step_executor_complete_step"))
parts.append(get_crew_i18n().retrieve("planning", "step_executor_complete_step"))
return "\n".join(parts)

View File

@@ -213,6 +213,7 @@ class Crew(FlowTrackable, BaseModel):
default_factory=TaskOutputStorageHandler
)
_kickoff_event_id: str | None = PrivateAttr(default=None)
_i18n_token: Any = PrivateAttr(default=None)
name: str | None = Field(default="crew")
cache: bool = Field(default=True)
@@ -1044,6 +1045,11 @@ class Crew(FlowTrackable, BaseModel):
if self._memory is not None and hasattr(self._memory, "drain_writes"):
self._memory.drain_writes()
clear_files(self.id)
if self._i18n_token is not None:
from crewai.utilities.i18n import reset_crew_i18n
reset_crew_i18n(self._i18n_token)
self._i18n_token = None
detach(token)
def _post_kickoff(self, result: CrewOutput) -> CrewOutput:

View File

@@ -354,6 +354,11 @@ def prepare_kickoff(
crew._set_tasks_callbacks()
crew._set_allow_crewai_trigger_context_for_first_task()
if crew.prompt_file:
from crewai.utilities.i18n import get_i18n, set_crew_i18n
crew._i18n_token = set_crew_i18n(get_i18n(crew.prompt_file))
agents_to_setup: list[BaseAgent] = list(crew.agents)
seen_agent_ids: set[int] = {id(agent) for agent in agents_to_setup}
for task in crew.tasks:

View File

@@ -93,7 +93,7 @@ from crewai.utilities.agent_utils import (
track_delegation_if_needed,
)
from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.planning_types import (
PlanStep,
StepObservation,
@@ -1448,7 +1448,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
action.result = str(e)
self._append_message_to_state(action.text)
reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_prompt = get_crew_i18n().slice("post_tool_reasoning")
reasoning_message: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -1469,7 +1469,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self.state.is_finished = True
return "tool_result_is_final"
reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_prompt = get_crew_i18n().slice("post_tool_reasoning")
reasoning_message_post: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -2220,10 +2220,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
# Build synthesis prompt
role = self.agent.role if self.agent else "Assistant"
system_prompt = I18N_DEFAULT.retrieve(
system_prompt = get_crew_i18n().retrieve(
"planning", "synthesis_system_prompt"
).format(role=role)
user_prompt = I18N_DEFAULT.retrieve("planning", "synthesis_user_prompt").format(
user_prompt = get_crew_i18n().retrieve("planning", "synthesis_user_prompt").format(
task_description=task_description,
combined_steps=combined_steps,
)
@@ -2470,7 +2470,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self.task.description if self.task else getattr(self, "_kickoff_input", "")
)
enhancement = I18N_DEFAULT.retrieve(
enhancement = get_crew_i18n().retrieve(
"planning", "replan_enhancement_prompt"
).format(previous_context=previous_context)
@@ -2776,7 +2776,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
Returns:
Updated action or final answer.
"""
add_image_tool = I18N_DEFAULT.tools("add_image")
add_image_tool = get_crew_i18n().tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()

View File

@@ -3511,7 +3511,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM as BaseLLMClass
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
llm_instance: BaseLLMClass
if isinstance(llm, str):
@@ -3531,7 +3531,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
description=f"The outcome that best matches the feedback. Must be one of: {', '.join(outcomes)}"
)
prompt_template = I18N_DEFAULT.slice("human_feedback_collapse")
prompt_template = get_crew_i18n().slice("human_feedback_collapse")
prompt = prompt_template.format(
feedback=feedback,

View File

@@ -363,9 +363,9 @@ def human_feedback(
def _get_hitl_prompt(key: str) -> str:
"""Read a HITL prompt from the i18n translations."""
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
return I18N_DEFAULT.slice(key)
return get_crew_i18n().slice(key)
def _resolve_llm_instance() -> Any:
"""Resolve the ``llm`` parameter to a BaseLLM instance.

View File

@@ -93,7 +93,7 @@ from crewai.utilities.converter import (
)
from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_for_json
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.pydantic_schema_utils import (
generate_model_description,
@@ -577,7 +577,7 @@ class LiteAgent(FlowTrackable, BaseModel):
f"- {m.record.content}" for m in matches
)
if memory_block:
formatted = I18N_DEFAULT.slice("memory").format(memory=memory_block)
formatted = get_crew_i18n().slice("memory").format(memory=memory_block)
if self._messages and self._messages[0].get("role") == "system":
existing_content = self._messages[0].get("content", "")
if not isinstance(existing_content, str):
@@ -650,7 +650,7 @@ class LiteAgent(FlowTrackable, BaseModel):
try:
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
instructions = get_crew_i18n().slice("formatted_task_instructions").format(
output_format=schema
)
@@ -799,7 +799,7 @@ class LiteAgent(FlowTrackable, BaseModel):
base_prompt = ""
if self._parsed_tools:
# Use the prompt template for agents with tools
base_prompt = I18N_DEFAULT.slice(
base_prompt = get_crew_i18n().slice(
"lite_agent_system_prompt_with_tools"
).format(
role=self.role,
@@ -810,7 +810,7 @@ class LiteAgent(FlowTrackable, BaseModel):
)
else:
# Use the prompt template for agents without tools
base_prompt = I18N_DEFAULT.slice(
base_prompt = get_crew_i18n().slice(
"lite_agent_system_prompt_without_tools"
).format(
role=self.role,
@@ -822,7 +822,7 @@ class LiteAgent(FlowTrackable, BaseModel):
if active_response_format:
model_description = generate_model_description(active_response_format)
schema_json = json.dumps(model_description, indent=2)
base_prompt += I18N_DEFAULT.slice("lite_agent_response_format").format(
base_prompt += get_crew_i18n().slice("lite_agent_response_format").format(
response_format=schema_json
)

View File

@@ -9,7 +9,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from crewai.memory.types import MemoryRecord, ScopeInfo
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
_logger = logging.getLogger(__name__)
@@ -149,7 +149,7 @@ def _get_prompt(key: str) -> str:
Returns:
The prompt string.
"""
return I18N_DEFAULT.memory(key)
return get_crew_i18n().memory(key)
def extract_memories_from_content(content: str, llm: Any) -> list[str]:

View File

@@ -91,7 +91,7 @@ from crewai.utilities.guardrail_types import (
GuardrailType,
GuardrailsType,
)
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.string_utils import interpolate_only
@@ -970,7 +970,7 @@ class Task(BaseModel):
tasks_slices = [description]
output = I18N_DEFAULT.slice("expected_output").format(
output = get_crew_i18n().slice("expected_output").format(
expected_output=self.expected_output
)
tasks_slices = [description, output]
@@ -1042,7 +1042,7 @@ Follow these guidelines:
raise ValueError(f"Error interpolating output_file path: {e!s}") from e
if inputs.get("crew_chat_messages"):
conversation_instruction = I18N_DEFAULT.slice(
conversation_instruction = get_crew_i18n().slice(
"conversation_history_instruction"
)
@@ -1318,7 +1318,7 @@ Follow these guidelines:
self.retry_count += 1
current_retry_count = self.retry_count
context = I18N_DEFAULT.errors("validation_error").format(
context = get_crew_i18n().errors("validation_error").format(
guardrail_result_error=guardrail_result.error,
task_output=task_output.raw,
)
@@ -1429,7 +1429,7 @@ Follow these guidelines:
self.retry_count += 1
current_retry_count = self.retry_count
context = I18N_DEFAULT.errors("validation_error").format(
context = get_crew_i18n().errors("validation_error").format(
guardrail_result_error=guardrail_result.error,
task_output=task_output.raw,
)

View File

@@ -3,7 +3,7 @@ from typing import Any
from pydantic import BaseModel, Field
from crewai.tools.base_tool import BaseTool
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
class AddImageToolSchema(BaseModel):
@@ -16,9 +16,9 @@ class AddImageToolSchema(BaseModel):
class AddImageTool(BaseTool):
"""Tool for adding images to the content"""
name: str = Field(default_factory=lambda: I18N_DEFAULT.tools("add_image")["name"]) # type: ignore[index]
name: str = Field(default_factory=lambda: get_crew_i18n().tools("add_image")["name"]) # type: ignore[index]
description: str = Field(
default_factory=lambda: I18N_DEFAULT.tools("add_image")["description"] # type: ignore[index]
default_factory=lambda: get_crew_i18n().tools("add_image")["description"] # type: ignore[index]
)
args_schema: type[BaseModel] = AddImageToolSchema
@@ -28,7 +28,7 @@ class AddImageTool(BaseTool):
action: str | None = None,
**kwargs: Any,
) -> dict[str, Any]:
action = action or I18N_DEFAULT.tools("add_image")["default_action"] # type: ignore
action = action or get_crew_i18n().tools("add_image")["default_action"] # type: ignore
content = [
{"type": "text", "text": action},
{

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool
from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
if TYPE_CHECKING:
@@ -25,12 +25,12 @@ class AgentTools:
delegate_tool = DelegateWorkTool(
agents=self.agents,
description=I18N_DEFAULT.tools("delegate_work").format(coworkers=coworkers), # type: ignore
description=get_crew_i18n().tools("delegate_work").format(coworkers=coworkers), # type: ignore
)
ask_tool = AskQuestionTool(
agents=self.agents,
description=I18N_DEFAULT.tools("ask_question").format(coworkers=coworkers), # type: ignore
description=get_crew_i18n().tools("ask_question").format(coworkers=coworkers), # type: ignore
)
return [delegate_tool, ask_tool]

View File

@@ -6,7 +6,7 @@ from pydantic import Field
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
logger = logging.getLogger(__name__)
@@ -90,7 +90,7 @@ class BaseAgentTool(BaseTool):
)
except (AttributeError, ValueError) as e:
# Handle specific exceptions that might occur during role name processing
return I18N_DEFAULT.errors("agent_tool_unexisting_coworker").format(
return get_crew_i18n().errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[
f"- {self.sanitize_agent_name(agent.role)}"
@@ -102,7 +102,7 @@ class BaseAgentTool(BaseTool):
if not agent:
# No matching agent found after sanitization
return I18N_DEFAULT.errors("agent_tool_unexisting_coworker").format(
return get_crew_i18n().errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[
f"- {self.sanitize_agent_name(agent.role)}"
@@ -117,7 +117,7 @@ class BaseAgentTool(BaseTool):
task_with_assigned_agent = Task(
description=task,
agent=selected_agent,
expected_output=I18N_DEFAULT.slice("manager_request"),
expected_output=get_crew_i18n().slice("manager_request"),
)
logger.debug(
f"Created task for agent '{self.sanitize_agent_name(selected_agent.role)}': {task}"
@@ -125,6 +125,6 @@ class BaseAgentTool(BaseTool):
return selected_agent.execute_task(task_with_assigned_agent, context)
except Exception as e:
# Handle task creation or execution errors
return I18N_DEFAULT.errors("agent_tool_execution_error").format(
return get_crew_i18n().errors("agent_tool_execution_error").format(
agent_role=self.sanitize_agent_name(selected_agent.role), error=str(e)
)

View File

@@ -7,7 +7,7 @@ from typing import Any
from pydantic import BaseModel, Field
from crewai.tools.base_tool import BaseTool
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
class RecallMemorySchema(BaseModel):
@@ -117,14 +117,14 @@ def create_memory_tools(memory: Any) -> list[BaseTool]:
tools: list[BaseTool] = [
RecallMemoryTool(
memory=memory,
description=I18N_DEFAULT.tools("recall_memory"),
description=get_crew_i18n().tools("recall_memory"),
),
]
if not memory.read_only:
tools.append(
RememberTool(
memory=memory,
description=I18N_DEFAULT.tools("save_to_memory"),
description=get_crew_i18n().tools("save_to_memory"),
)
)
return tools

View File

@@ -29,7 +29,7 @@ from crewai.utilities.agent_utils import (
render_text_description_and_args,
)
from crewai.utilities.converter import Converter
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.string_utils import sanitize_tool_name
@@ -145,7 +145,7 @@ class ToolUsage:
if (
isinstance(tool, CrewStructuredTool)
and sanitize_tool_name(tool.name)
== sanitize_tool_name(I18N_DEFAULT.tools("add_image")["name"]) # type: ignore
== sanitize_tool_name(get_crew_i18n().tools("add_image")["name"]) # type: ignore
):
try:
return self._use(tool_string=tool_string, tool=tool, calling=calling)
@@ -193,7 +193,7 @@ class ToolUsage:
if (
isinstance(tool, CrewStructuredTool)
and sanitize_tool_name(tool.name)
== sanitize_tool_name(I18N_DEFAULT.tools("add_image")["name"]) # type: ignore
== sanitize_tool_name(get_crew_i18n().tools("add_image")["name"]) # type: ignore
):
try:
return await self._ause(
@@ -229,7 +229,7 @@ class ToolUsage:
"""
if self._check_tool_repeated_usage(calling=calling):
try:
result = I18N_DEFAULT.errors("task_repeated_usage").format(
result = get_crew_i18n().errors("task_repeated_usage").format(
tool_names=self.tools_names
)
self._telemetry.tool_repeated_usage(
@@ -414,7 +414,7 @@ class ToolUsage:
self._run_attempts += 1
if self._run_attempts > self._max_parsing_attempts:
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
error_message = I18N_DEFAULT.errors(
error_message = get_crew_i18n().errors(
"tool_usage_exception"
).format(
error=e,
@@ -422,7 +422,7 @@ class ToolUsage:
tool_inputs=tool.description,
)
result = ToolUsageError(
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
f"\n{error_message}.\nMoving on then. {get_crew_i18n().slice('format').format(tool_names=self.tools_names)}"
).message
if self.task:
self.task.increment_tools_errors()
@@ -460,7 +460,7 @@ class ToolUsage:
# Repeated usage check happens before event emission - safe to return early
if self._check_tool_repeated_usage(calling=calling):
try:
result = I18N_DEFAULT.errors("task_repeated_usage").format(
result = get_crew_i18n().errors("task_repeated_usage").format(
tool_names=self.tools_names
)
self._telemetry.tool_repeated_usage(
@@ -647,7 +647,7 @@ class ToolUsage:
self._run_attempts += 1
if self._run_attempts > self._max_parsing_attempts:
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
error_message = I18N_DEFAULT.errors(
error_message = get_crew_i18n().errors(
"tool_usage_exception"
).format(
error=e,
@@ -655,7 +655,7 @@ class ToolUsage:
tool_inputs=tool.description,
)
result = ToolUsageError(
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
f"\n{error_message}.\nMoving on then. {get_crew_i18n().slice('format').format(tool_names=self.tools_names)}"
).message
if self.task:
self.task.increment_tools_errors()
@@ -698,7 +698,7 @@ class ToolUsage:
def _remember_format(self, result: str) -> str:
result = str(result)
result += "\n\n" + I18N_DEFAULT.slice("tools").format(
result += "\n\n" + get_crew_i18n().slice("tools").format(
tools=self.tools_description, tool_names=self.tools_names
)
return result
@@ -824,12 +824,12 @@ class ToolUsage:
except Exception:
if raise_error:
raise
return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
return ToolUsageError(f"{get_crew_i18n().errors('tool_arguments_error')}")
if not isinstance(arguments, dict):
if raise_error:
raise
return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
return ToolUsageError(f"{get_crew_i18n().errors('tool_arguments_error')}")
return ToolCalling(
tool_name=sanitize_tool_name(tool.name),
@@ -855,7 +855,7 @@ class ToolUsage:
if self.agent and self.agent.verbose:
PRINTER.print(content=f"\n\n{e}\n", color="red")
return ToolUsageError(
f"{I18N_DEFAULT.errors('tool_usage_error').format(error=e)}\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
f"{get_crew_i18n().errors('tool_usage_error').format(error=e)}\nMoving on then. {get_crew_i18n().slice('format').format(tool_names=self.tools_names)}"
)
return self._tool_calling(tool_string)

View File

@@ -33,7 +33,7 @@ from crewai.utilities.errors import AgentRepositoryError
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.token_counter_callback import TokenCalcHandler
@@ -313,10 +313,10 @@ def handle_max_iterations_exceeded(
if formatted_answer and hasattr(formatted_answer, "text"):
assistant_message = (
formatted_answer.text + f"\n{I18N_DEFAULT.errors('force_final_answer')}"
formatted_answer.text + f"\n{get_crew_i18n().errors('force_final_answer')}"
)
else:
assistant_message = I18N_DEFAULT.errors("force_final_answer")
assistant_message = get_crew_i18n().errors("force_final_answer")
messages.append(format_message_for_llm(assistant_message, role="assistant"))
@@ -902,10 +902,10 @@ async def _asummarize_chunks(
conversation_text = _format_messages_for_summary(chunk)
summarization_messages = [
format_message_for_llm(
I18N_DEFAULT.slice("summarizer_system_message"), role="system"
get_crew_i18n().slice("summarizer_system_message"), role="system"
),
format_message_for_llm(
I18N_DEFAULT.slice("summarize_instruction").format(
get_crew_i18n().slice("summarize_instruction").format(
conversation=conversation_text
),
),
@@ -972,10 +972,10 @@ def summarize_messages(
conversation_text = _format_messages_for_summary(chunk)
summarization_messages = [
format_message_for_llm(
I18N_DEFAULT.slice("summarizer_system_message"), role="system"
get_crew_i18n().slice("summarizer_system_message"), role="system"
),
format_message_for_llm(
I18N_DEFAULT.slice("summarize_instruction").format(
get_crew_i18n().slice("summarize_instruction").format(
conversation=conversation_text
),
),
@@ -1005,7 +1005,7 @@ def summarize_messages(
messages.extend(system_messages)
summary_message = format_message_for_llm(
I18N_DEFAULT.slice("summary").format(merged_summary=merged_summary)
get_crew_i18n().slice("summary").format(merged_summary=merged_summary)
)
if preserved_files:
summary_message["files"] = preserved_files

View File

@@ -10,7 +10,7 @@ from pydantic import BaseModel, ValidationError
from typing_extensions import Unpack
from crewai.agents.agent_builder.utilities.base_output_converter import OutputConverter
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.internal_instructor import InternalInstructor
from crewai.utilities.pydantic_schema_utils import generate_model_description
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
from crewai.llms.base_llm import BaseLLM
_JSON_PATTERN: Final[re.Pattern[str]] = re.compile(r"({.*})", re.DOTALL)
_I18N = I18N_DEFAULT
class ConverterError(Exception):
@@ -548,14 +548,14 @@ def get_conversion_instructions(
):
schema_dict = generate_model_description(model)
schema = json.dumps(schema_dict, indent=2)
formatted_task_instructions = _I18N.slice("formatted_task_instructions").format(
formatted_task_instructions = get_crew_i18n().slice("formatted_task_instructions").format(
output_format=schema
)
instructions += formatted_task_instructions
else:
model_description = generate_model_description(model)
schema_json = json.dumps(model_description, indent=2)
formatted_task_instructions = _I18N.slice("formatted_task_instructions").format(
formatted_task_instructions = get_crew_i18n().slice("formatted_task_instructions").format(
output_format=schema_json
)
instructions += formatted_task_instructions

View File

@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.task_events import TaskEvaluationEvent
from crewai.utilities.converter import Converter
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.training_converter import TrainingConverter
@@ -98,7 +98,7 @@ class TaskEvaluator:
if not self.llm.supports_function_calling(): # type: ignore[union-attr]
schema_dict = generate_model_description(TaskEvaluation)
output_schema: str = I18N_DEFAULT.slice(
output_schema: str = get_crew_i18n().slice(
"formatted_task_instructions"
).format(output_format=json.dumps(schema_dict, indent=2))
instructions = f"{instructions}\n\n{output_schema}"
@@ -172,7 +172,7 @@ class TaskEvaluator:
if not self.llm.supports_function_calling(): # type: ignore[union-attr]
schema_dict = generate_model_description(TrainingTaskEvaluation)
output_schema: str = I18N_DEFAULT.slice(
output_schema: str = get_crew_i18n().slice(
"formatted_task_instructions"
).format(output_format=json.dumps(schema_dict, indent=2))
instructions = f"{instructions}\n\n{output_schema}"

View File

@@ -1,5 +1,6 @@
"""Internationalization support for CrewAI prompts and messages."""
import contextvars
from functools import lru_cache
import json
import os
@@ -145,3 +146,33 @@ def get_i18n(prompt_file: str | None = None) -> I18N:
I18N_DEFAULT: I18N = get_i18n()
_crew_i18n_var: contextvars.ContextVar[I18N | None] = contextvars.ContextVar(
"_crew_i18n", default=None
)
def get_crew_i18n() -> I18N:
"""Return the crew-scoped I18N override if set, otherwise ``I18N_DEFAULT``.
When a :class:`Crew` with a ``prompt_file`` is kicked off, the custom
I18N instance is pushed into a :class:`contextvars.ContextVar` so that
all code running during that crew execution automatically picks up the
custom prompts.
"""
return _crew_i18n_var.get() or I18N_DEFAULT
def set_crew_i18n(i18n: I18N | None) -> contextvars.Token[I18N | None]:
"""Set (or clear) the crew-scoped I18N override.
Returns a token that can be passed to :func:`reset_crew_i18n` for
proper cleanup.
"""
return _crew_i18n_var.set(i18n)
def reset_crew_i18n(token: contextvars.Token[I18N | None]) -> None:
"""Reset the crew-scoped I18N override to its previous value."""
_crew_i18n_var.reset(token)

View File

@@ -6,7 +6,7 @@ from typing import Any, Literal
from pydantic import BaseModel, Field
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
class StandardPromptResult(BaseModel):
@@ -158,13 +158,13 @@ class Prompts(BaseModel):
if not system_template or not prompt_template:
# If any of the required templates are missing, fall back to the default format
prompt_parts: list[str] = [
I18N_DEFAULT.slice(component) for component in components
get_crew_i18n().slice(component) for component in components
]
prompt = "".join(prompt_parts)
else:
# All templates are provided, use them
template_parts: list[str] = [
I18N_DEFAULT.slice(component)
get_crew_i18n().slice(component)
for component in components
if component != "task"
]
@@ -172,7 +172,7 @@ class Prompts(BaseModel):
"{{ .System }}", "".join(template_parts)
)
prompt = prompt_template.replace(
"{{ .Prompt }}", "".join(I18N_DEFAULT.slice("task"))
"{{ .Prompt }}", "".join(get_crew_i18n().slice("task"))
)
# Handle missing response_template
if response_template:

View File

@@ -15,7 +15,7 @@ from crewai.events.types.reasoning_events import (
AgentReasoningStartedEvent,
)
from crewai.llm import LLM
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.planning_types import PlanStep
from crewai.utilities.string_utils import sanitize_tool_name
@@ -482,17 +482,17 @@ class AgentReasoning:
"""Get the system prompt for planning.
Returns:
The system prompt, either custom or from I18N_DEFAULT.
The system prompt, either custom or from get_crew_i18n().
"""
if self.config.system_prompt is not None:
return self.config.system_prompt
# Try new "planning" section first, fall back to "reasoning" for compatibility
try:
return I18N_DEFAULT.retrieve("planning", "system_prompt")
return get_crew_i18n().retrieve("planning", "system_prompt")
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
return I18N_DEFAULT.retrieve("reasoning", "initial_plan").format(
return get_crew_i18n().retrieve("reasoning", "initial_plan").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -528,7 +528,7 @@ class AgentReasoning:
# Try new "planning" section first
try:
return I18N_DEFAULT.retrieve("planning", "create_plan_prompt").format(
return get_crew_i18n().retrieve("planning", "create_plan_prompt").format(
description=self.description,
expected_output=self.expected_output,
tools=available_tools,
@@ -536,7 +536,7 @@ class AgentReasoning:
)
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
return I18N_DEFAULT.retrieve("reasoning", "create_plan_prompt").format(
return get_crew_i18n().retrieve("reasoning", "create_plan_prompt").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -585,12 +585,12 @@ class AgentReasoning:
# Try new "planning" section first
try:
return I18N_DEFAULT.retrieve("planning", "refine_plan_prompt").format(
return get_crew_i18n().retrieve("planning", "refine_plan_prompt").format(
current_plan=current_plan,
)
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
return I18N_DEFAULT.retrieve("reasoning", "refine_plan_prompt").format(
return get_crew_i18n().retrieve("reasoning", "refine_plan_prompt").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -643,7 +643,7 @@ def _call_llm_with_reasoning_prompt(
Returns:
The LLM response.
"""
system_prompt = I18N_DEFAULT.retrieve("reasoning", plan_type).format(
system_prompt = get_crew_i18n().retrieve("reasoning", plan_type).format(
role=reasoning_agent.role,
goal=reasoning_agent.goal,
backstory=backstory,

View File

@@ -13,7 +13,7 @@ from crewai.security.fingerprint import Fingerprint
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.tool_types import ToolResult
from crewai.tools.tool_usage import ToolUsage, ToolUsageError
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.i18n import get_crew_i18n
from crewai.utilities.logger import Logger
from crewai.utilities.string_utils import sanitize_tool_name
@@ -140,7 +140,7 @@ async def aexecute_tool_and_check_finality(
return ToolResult(modified_result, tool.result_as_answer)
tool_result = I18N_DEFAULT.errors("wrong_tool_name").format(
tool_result = get_crew_i18n().errors("wrong_tool_name").format(
tool=sanitized_tool_name,
tools=", ".join(tool_name_to_tool_map.keys()),
)
@@ -259,7 +259,7 @@ def execute_tool_and_check_finality(
return ToolResult(modified_result, tool.result_as_answer)
tool_result = I18N_DEFAULT.errors("wrong_tool_name").format(
tool_result = get_crew_i18n().errors("wrong_tool_name").format(
tool=sanitized_tool_name,
tools=", ".join(tool_name_to_tool_map.keys()),
)

View File

@@ -0,0 +1,101 @@
{
"hierarchical_manager_agent": {
"role": "CUSTOM_role",
"goal": "CUSTOM_goal",
"backstory": "CUSTOM_backstory"
},
"slices": {
"observation": "CUSTOM_observation",
"task": "CUSTOM_task",
"memory": "CUSTOM_memory",
"role_playing": "CUSTOM_role_playing",
"tools": "CUSTOM_tools",
"no_tools": "CUSTOM_no_tools",
"task_no_tools": "CUSTOM_task_no_tools",
"native_tools": "CUSTOM_native_tools",
"native_task": "CUSTOM_native_task",
"post_tool_reasoning": "CUSTOM_post_tool_reasoning",
"format": "CUSTOM_format",
"final_answer_format": "CUSTOM_final_answer_format",
"format_without_tools": "CUSTOM_format_without_tools",
"task_with_context": "CUSTOM_task_with_context",
"expected_output": "CUSTOM_expected_output",
"human_feedback": "CUSTOM_human_feedback",
"getting_input": "CUSTOM_getting_input",
"summarizer_system_message": "CUSTOM_summarizer_system_message",
"summarize_instruction": "CUSTOM_summarize_instruction",
"summary": "CUSTOM_summary",
"manager_request": "CUSTOM_manager_request",
"formatted_task_instructions": "CUSTOM_formatted_task_instructions",
"conversation_history_instruction": "CUSTOM_conversation_history_instruction",
"feedback_instructions": "CUSTOM_feedback_instructions",
"lite_agent_system_prompt_with_tools": "CUSTOM_lite_agent_system_prompt_with_tools",
"lite_agent_system_prompt_without_tools": "CUSTOM_lite_agent_system_prompt_without_tools",
"lite_agent_response_format": "CUSTOM_lite_agent_response_format",
"knowledge_search_query": "CUSTOM_knowledge_search_query",
"knowledge_search_query_system_prompt": "CUSTOM_knowledge_search_query_system_prompt",
"human_feedback_collapse": "CUSTOM_human_feedback_collapse",
"hitl_pre_review_system": "CUSTOM_hitl_pre_review_system",
"hitl_pre_review_user": "CUSTOM_hitl_pre_review_user",
"hitl_distill_system": "CUSTOM_hitl_distill_system",
"hitl_distill_user": "CUSTOM_hitl_distill_user"
},
"errors": {
"force_final_answer_error": "CUSTOM_force_final_answer_error",
"force_final_answer": "CUSTOM_force_final_answer",
"agent_tool_unexisting_coworker": "CUSTOM_agent_tool_unexisting_coworker",
"task_repeated_usage": "CUSTOM_task_repeated_usage",
"tool_usage_error": "CUSTOM_tool_usage_error",
"tool_arguments_error": "CUSTOM_tool_arguments_error",
"wrong_tool_name": "CUSTOM_wrong_tool_name",
"tool_usage_exception": "CUSTOM_tool_usage_exception",
"agent_tool_execution_error": "CUSTOM_agent_tool_execution_error",
"validation_error": "CUSTOM_validation_error"
},
"tools": {
"delegate_work": "CUSTOM_delegate_work",
"ask_question": "CUSTOM_ask_question",
"add_image": {
"name": "CUSTOM_name",
"description": "CUSTOM_description",
"default_action": "CUSTOM_default_action"
},
"recall_memory": "CUSTOM_recall_memory",
"save_to_memory": "CUSTOM_save_to_memory"
},
"memory": {
"query_system": "CUSTOM_query_system",
"extract_memories_system": "CUSTOM_extract_memories_system",
"extract_memories_user": "CUSTOM_extract_memories_user",
"query_user": "CUSTOM_query_user",
"save_system": "CUSTOM_save_system",
"save_user": "CUSTOM_save_user",
"consolidation_system": "CUSTOM_consolidation_system",
"consolidation_user": "CUSTOM_consolidation_user"
},
"reasoning": {
"initial_plan": "CUSTOM_initial_plan",
"refine_plan": "CUSTOM_refine_plan",
"create_plan_prompt": "CUSTOM_create_plan_prompt",
"refine_plan_prompt": "CUSTOM_refine_plan_prompt"
},
"planning": {
"system_prompt": "CUSTOM_system_prompt",
"create_plan_prompt": "CUSTOM_create_plan_prompt",
"refine_plan_prompt": "CUSTOM_refine_plan_prompt",
"observation_system_prompt": "CUSTOM_observation_system_prompt",
"observation_user_prompt": "CUSTOM_observation_user_prompt",
"step_executor_system_prompt": "CUSTOM_step_executor_system_prompt",
"step_executor_tools_section": "CUSTOM_step_executor_tools_section",
"step_executor_user_prompt": "CUSTOM_step_executor_user_prompt",
"step_executor_suggested_tool": "CUSTOM_step_executor_suggested_tool",
"step_executor_context_header": "CUSTOM_step_executor_context_header",
"step_executor_context_entry": "CUSTOM_step_executor_context_entry",
"step_executor_complete_step": "CUSTOM_step_executor_complete_step",
"todo_system_prompt": "CUSTOM_todo_system_prompt",
"synthesis_system_prompt": "CUSTOM_synthesis_system_prompt",
"synthesis_user_prompt": "CUSTOM_synthesis_user_prompt",
"replan_enhancement_prompt": "CUSTOM_replan_enhancement_prompt",
"step_executor_task_context": "CUSTOM_step_executor_task_context"
}
}

View File

@@ -0,0 +1,293 @@
"""Tests for Crew.prompt_file propagation to all components.
Verifies fix for https://github.com/crewAIInc/crewAI/issues/5931
"""
import os
from unittest.mock import MagicMock, patch
import pytest
from crewai.utilities.i18n import (
I18N,
I18N_DEFAULT,
get_crew_i18n,
get_i18n,
reset_crew_i18n,
set_crew_i18n,
)
CUSTOM_PROMPTS = os.path.join(os.path.dirname(__file__), "custom_prompts.json")
class TestCrewI18nContextVar:
"""Test the context variable mechanism for crew-scoped I18N."""
def test_get_crew_i18n_returns_default_when_no_override(self):
assert get_crew_i18n() is I18N_DEFAULT
def test_set_and_get_crew_i18n(self):
custom = get_i18n(CUSTOM_PROMPTS)
token = set_crew_i18n(custom)
try:
assert get_crew_i18n() is custom
assert get_crew_i18n().slice("role_playing") == "CUSTOM_role_playing"
finally:
reset_crew_i18n(token)
def test_reset_crew_i18n_restores_default(self):
custom = get_i18n(CUSTOM_PROMPTS)
token = set_crew_i18n(custom)
reset_crew_i18n(token)
assert get_crew_i18n() is I18N_DEFAULT
def test_nested_set_and_reset(self):
custom1 = I18N(prompt_file=CUSTOM_PROMPTS)
custom2 = I18N(prompt_file=CUSTOM_PROMPTS)
token1 = set_crew_i18n(custom1)
assert get_crew_i18n() is custom1
token2 = set_crew_i18n(custom2)
assert get_crew_i18n() is custom2
reset_crew_i18n(token2)
assert get_crew_i18n() is custom1
reset_crew_i18n(token1)
assert get_crew_i18n() is I18N_DEFAULT
class TestPromptsUsesCrewI18n:
"""Test that the Prompts class picks up the crew-scoped I18N."""
def test_prompts_build_prompt_uses_crew_i18n(self):
from crewai.utilities.prompts import Prompts
agent = MagicMock()
agent.role = "TestRole"
agent.goal = "TestGoal"
agent.backstory = "TestBackstory"
agent.skills = []
custom = get_i18n(CUSTOM_PROMPTS)
token = set_crew_i18n(custom)
try:
prompts = Prompts(agent=agent, has_tools=False)
result = prompts.task_execution()
assert "CUSTOM_role_playing" in result.prompt
assert "CUSTOM_no_tools" in result.prompt
finally:
reset_crew_i18n(token)
def test_prompts_uses_default_when_no_override(self):
from crewai.utilities.prompts import Prompts
agent = MagicMock()
agent.role = "TestRole"
agent.goal = "TestGoal"
agent.backstory = "TestBackstory"
agent.skills = []
prompts = Prompts(agent=agent, has_tools=False)
result = prompts.task_execution()
# Should use default en.json prompts, not custom ones
assert "CUSTOM_" not in result.prompt
assert "TestRole" in result.prompt
class TestTaskPromptUsesCrewI18n:
"""Test that Task.prompt() picks up the crew-scoped I18N."""
def test_task_prompt_uses_crew_i18n(self):
from crewai.task import Task
task = Task(
description="Test task",
expected_output="Test output",
)
custom = get_i18n(CUSTOM_PROMPTS)
token = set_crew_i18n(custom)
try:
prompt = task.prompt()
assert "CUSTOM_expected_output" in prompt
finally:
reset_crew_i18n(token)
def test_task_prompt_uses_default_when_no_override(self):
from crewai.task import Task
task = Task(
description="Test task",
expected_output="Test output",
)
prompt = task.prompt()
assert "CUSTOM_" not in prompt
assert "Test output" in prompt
class TestAgentToolsUseCrewI18n:
"""Test that delegation tools pick up the crew-scoped I18N."""
def test_agent_tools_use_crew_i18n(self):
from crewai.agent.core import Agent
from crewai.tools.agent_tools.agent_tools import AgentTools
agent = Agent(
role="TestAgent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o",
)
custom = get_i18n(CUSTOM_PROMPTS)
token = set_crew_i18n(custom)
try:
tools = AgentTools(agents=[agent]).tools()
descriptions = [t.description for t in tools]
assert any("CUSTOM_delegate_work" in d for d in descriptions)
assert any("CUSTOM_ask_question" in d for d in descriptions)
finally:
reset_crew_i18n(token)
class TestCrewKickoffSetsI18nContext:
"""Test that Crew.kickoff() properly sets and resets the I18N context."""
@patch("crewai.crew.Crew._run_sequential_process")
@patch("crewai.crews.utils.setup_agents")
@patch("crewai.crew.Crew.calculate_usage_metrics")
def test_kickoff_sets_i18n_for_custom_prompt_file(
self, mock_metrics, mock_setup, mock_seq
):
from crewai.agent.core import Agent
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
from crewai.task import Task
mock_metrics.return_value = MagicMock()
from crewai.types.usage_metrics import UsageMetrics
captured_i18n = []
def capture_i18n(*args, **kwargs):
captured_i18n.append(get_crew_i18n())
return CrewOutput(
raw="done",
tasks_output=[],
json_dict=None,
pydantic=None,
token_usage=UsageMetrics(),
)
mock_seq.side_effect = capture_i18n
agent = Agent(
role="Researcher",
goal="Research stuff",
backstory="Expert researcher",
llm="gpt-4o",
)
task = Task(
description="Do research",
expected_output="A report",
agent=agent,
)
crew = Crew(
agents=[agent],
tasks=[task],
prompt_file=CUSTOM_PROMPTS,
)
crew.kickoff()
assert len(captured_i18n) == 1
assert captured_i18n[0].slice("role_playing") == "CUSTOM_role_playing"
# After kickoff, context should be reset
assert get_crew_i18n() is I18N_DEFAULT
@patch("crewai.crew.Crew._run_sequential_process")
@patch("crewai.crews.utils.setup_agents")
@patch("crewai.crew.Crew.calculate_usage_metrics")
def test_kickoff_resets_i18n_on_exception(
self, mock_metrics, mock_setup, mock_seq
):
from crewai.agent.core import Agent
from crewai.crew import Crew
from crewai.task import Task
mock_seq.side_effect = RuntimeError("boom")
agent = Agent(
role="Researcher",
goal="Research stuff",
backstory="Expert researcher",
llm="gpt-4o",
)
task = Task(
description="Do research",
expected_output="A report",
agent=agent,
)
crew = Crew(
agents=[agent],
tasks=[task],
prompt_file=CUSTOM_PROMPTS,
)
with pytest.raises(RuntimeError, match="boom"):
crew.kickoff()
# Context must be reset even on exception
assert get_crew_i18n() is I18N_DEFAULT
@patch("crewai.crew.Crew._run_sequential_process")
@patch("crewai.crews.utils.setup_agents")
@patch("crewai.crew.Crew.calculate_usage_metrics")
def test_kickoff_without_prompt_file_uses_default(
self, mock_metrics, mock_setup, mock_seq
):
from crewai.agent.core import Agent
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
from crewai.task import Task
from crewai.types.usage_metrics import UsageMetrics
mock_metrics.return_value = MagicMock()
captured_i18n = []
def capture_i18n(*args, **kwargs):
captured_i18n.append(get_crew_i18n())
return CrewOutput(
raw="done",
tasks_output=[],
json_dict=None,
pydantic=None,
token_usage=UsageMetrics(),
)
mock_seq.side_effect = capture_i18n
agent = Agent(
role="Researcher",
goal="Research stuff",
backstory="Expert researcher",
llm="gpt-4o",
)
task = Task(
description="Do research",
expected_output="A report",
agent=agent,
)
crew = Crew(
agents=[agent],
tasks=[task],
)
crew.kickoff()
assert len(captured_i18n) == 1
assert captured_i18n[0] is I18N_DEFAULT