Compare commits

...

4 Commits

Author SHA1 Message Date
Devin AI
448c3e8b67 Address PR feedback: Improve I18N implementation with centralized instantiation and better validation
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-07 10:37:45 +00:00
Devin AI
8c9b8fff84 Fix import sorting with ruff
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-07 10:32:38 +00:00
Devin AI
afa71f9f5e Fix import sorting in crew_language_test.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-07 10:31:28 +00:00
Devin AI
ebd0803c0c Fix issue #2528: Restore language option in crew configuration
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-07 10:29:21 +00:00
4 changed files with 133 additions and 10 deletions

View File

@@ -191,6 +191,14 @@ class Crew(BaseModel):
default=None,
description="Path to the prompt json file to be used for the crew.",
)
language: Optional[str] = Field(
default="en",
description="Language used for the crew, defaults to English.",
)
language_file: Optional[str] = Field(
default=None,
description="Path to the language file to be used for the crew.",
)
output_log_file: Optional[Union[bool, str]] = Field(
default=None,
description="Path to the log file to be saved",
@@ -609,7 +617,7 @@ class Crew(BaseModel):
self._interpolate_inputs(inputs)
self._set_tasks_callbacks()
i18n = I18N(prompt_file=self.prompt_file)
i18n = self._create_i18n()
for agent in self.agents:
agent.i18n = i18n
@@ -750,8 +758,17 @@ class Crew(BaseModel):
self._create_manager_agent()
return self._execute_tasks(self.tasks)
def _create_i18n(self) -> I18N:
"""
Create an I18N instance with the crew's configuration.
Returns:
I18N: An internationalization instance configured with the crew's settings.
"""
return I18N(prompt_file=self.prompt_file, language=self.language)
def _create_manager_agent(self):
i18n = I18N(prompt_file=self.prompt_file)
i18n = self._create_i18n()
if self.manager_agent is not None:
self.manager_agent.allow_delegation = True
manager = self.manager_agent

View File

@@ -1,11 +1,15 @@
import json
import os
from typing import Dict, Optional, Union
from functools import lru_cache
from pathlib import Path
from typing import Dict, Literal, Optional, Union
from pydantic import BaseModel, Field, PrivateAttr, model_validator
"""Internationalization support for CrewAI prompts and messages."""
SUPPORTED_LANGUAGES = Literal["en", "fr", "es", "pt"]
class I18N(BaseModel):
"""Handles loading and retrieving internationalized prompts."""
_prompts: Dict[str, Dict[str, str]] = PrivateAttr()
@@ -13,22 +17,57 @@ class I18N(BaseModel):
default=None,
description="Path to the prompt_file file to load",
)
language: Optional[str] = Field(
default="en",
description="Language to use for translations. Defaults to English.",
)
@model_validator(mode="before")
@classmethod
def validate_language(cls, data):
"""
Validate the language parameter.
If the language is not supported, it will fall back to English.
"""
if isinstance(data, dict) and "language" in data:
lang = data["language"]
if lang and lang not in ["en", "fr", "es", "pt"]:
print(f"Warning: Language '{lang}' not supported. Falling back to English.")
data["language"] = "en"
return data
@model_validator(mode="after")
def load_prompts(self) -> "I18N":
"""Load prompts from a JSON file."""
"""
Load prompts from a JSON file.
If prompt_file is provided, loads from that file.
Otherwise, attempts to load from the language-specific translation file.
Falls back to English if the specified language file doesn't exist.
Raises:
Exception: If the prompt file is not found or contains invalid JSON.
Returns:
I18N: The instance with loaded prompts.
"""
try:
if self.prompt_file:
with open(self.prompt_file, "r", encoding="utf-8") as f:
self._prompts = json.load(f)
else:
dir_path = os.path.dirname(os.path.realpath(__file__))
prompts_path = os.path.join(dir_path, "../translations/en.json")
with open(prompts_path, "r", encoding="utf-8") as f:
base_path = Path(__file__).parent / "../translations"
lang = self.language or "en"
lang_file = base_path / f"{lang}.json"
if not lang_file.exists():
lang_file = base_path / "en.json"
with open(lang_file.resolve(), "r", encoding="utf-8") as f:
self._prompts = json.load(f)
except FileNotFoundError:
raise Exception(f"Prompt file '{self.prompt_file}' not found.")
raise Exception(f"Prompt file '{self.prompt_file or lang_file}' not found.")
except json.JSONDecodeError:
raise Exception("Error decoding JSON from the prompts file.")
@@ -38,15 +77,31 @@ class I18N(BaseModel):
return self
def slice(self, slice: str) -> str:
"""Get a slice prompt by key."""
return self.retrieve("slices", slice)
def errors(self, error: str) -> str:
"""Get an error message by key."""
return self.retrieve("errors", error)
def tools(self, tool: str) -> Union[str, Dict[str, str]]:
"""Get a tool prompt by key."""
return self.retrieve("tools", tool)
def retrieve(self, kind, key) -> str:
def retrieve(self, kind: str, key: str) -> str:
"""
Retrieve a prompt by section and key.
Args:
kind: The section in the prompts file (e.g., "slices", "errors")
key: The specific key within the section
Returns:
The prompt text
Raises:
Exception: If the prompt is not found
"""
try:
return self._prompts[kind][key]
except Exception as _:

View File

@@ -0,0 +1,41 @@
from unittest.mock import patch
import pytest
from crewai import Agent, Crew, Process, Task
from crewai.utilities.i18n import I18N
def test_crew_with_language():
i18n = I18N(language="en")
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
verbose=True
)
task = Task(
description="Test Task",
expected_output="Test Output",
agent=agent
)
with patch('crewai.crew.I18N') as mock_i18n:
mock_i18n.return_value = i18n
crew = Crew(
agents=[agent],
tasks=[task],
process=Process.sequential,
verbose=True,
language="fr" # Use French as an example
)
with patch.object(crew, '_run_sequential_process'):
with patch.object(crew, '_set_tasks_callbacks'):
with patch('crewai.agent.Agent.create_agent_executor'):
crew.kickoff()
mock_i18n.assert_called_with(prompt_file=None, language="fr")

View File

@@ -42,3 +42,13 @@ def test_prompt_file():
i18n.load_prompts()
assert isinstance(i18n.retrieve("slices", "role_playing"), str)
assert i18n.retrieve("slices", "role_playing") == "Lorem ipsum dolor sit amet"
def test_language_parameter():
i18n = I18N(language="en")
i18n.load_prompts()
assert isinstance(i18n.slice("role_playing"), str)
i18n = I18N(language="nonexistent")
i18n.load_prompts()
assert isinstance(i18n.slice("role_playing"), str)