Skip to content

Commit

Permalink
Adding support for translations (crewAIInc#120)
Browse files Browse the repository at this point in the history
Add translations support
  • Loading branch information
joaomdmoura authored Jan 12, 2024
1 parent ea7759b commit 8e7772c
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 101 deletions.
33 changes: 20 additions & 13 deletions src/crewai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CrewAgentOutputParser,
ToolsHandler,
)
from crewai.i18n import I18N
from crewai.prompts import Prompts


Expand Down Expand Up @@ -54,13 +55,6 @@ class Agent(BaseModel):
role: str = Field(description="Role of the agent")
goal: str = Field(description="Objective of the agent")
backstory: str = Field(description="Backstory of the agent")
llm: Optional[Any] = Field(
default_factory=lambda: ChatOpenAI(
temperature=0.7,
model_name="gpt-4",
),
description="Language model that will run the agent.",
)
memory: bool = Field(
default=True, description="Whether the agent should have memory or not"
)
Expand All @@ -85,6 +79,16 @@ class Agent(BaseModel):
cache_handler: Optional[InstanceOf[CacheHandler]] = Field(
default=CacheHandler(), description="An instance of the CacheHandler class."
)
i18n: Optional[I18N] = Field(
default=I18N(), description="Internationalization settings."
)
llm: Optional[Any] = Field(
default_factory=lambda: ChatOpenAI(
temperature=0.7,
model_name="gpt-4",
),
description="Language model that will run the agent.",
)

@field_validator("id", mode="before")
@classmethod
Expand Down Expand Up @@ -114,8 +118,8 @@ def execute_task(
Output of the agent
"""
if context:
task = "\n".join(
[task, "\nThis is the context you are working with:", context]
task = self.i18n.slice("task_with_context").format(
task=task, context=context
)

tools = tools or self.tools
Expand Down Expand Up @@ -148,6 +152,7 @@ def __create_agent_executor(self) -> CrewAgentExecutor:
"agent_scratchpad": lambda x: format_log_to_str(x["intermediate_steps"]),
}
executor_args = {
"i18n": self.i18n,
"tools": self.tools,
"verbose": self.verbose,
"handle_parsing_errors": True,
Expand All @@ -160,23 +165,25 @@ def __create_agent_executor(self) -> CrewAgentExecutor:
)
executor_args["memory"] = summary_memory
agent_args["chat_history"] = lambda x: x["chat_history"]
prompt = Prompts().task_execution_with_memory()
prompt = Prompts(i18n=self.i18n).task_execution_with_memory()
else:
prompt = Prompts().task_execution()
prompt = Prompts(i18n=self.i18n).task_execution()

execution_prompt = prompt.partial(
goal=self.goal,
role=self.role,
backstory=self.backstory,
)

bind = self.llm.bind(stop=["\nObservation"])
bind = self.llm.bind(stop=[self.i18n.slice("observation")])
inner_agent = (
agent_args
| execution_prompt
| bind
| CrewAgentOutputParser(
tools_handler=self.tools_handler, cache=self.cache_handler
tools_handler=self.tools_handler,
cache=self.cache_handler,
i18n=self.i18n,
)
)
self.agent_executor = CrewAgentExecutor(agent=inner_agent, **executor_args)
Expand Down
12 changes: 9 additions & 3 deletions src/crewai/agents/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
from langchain_core.exceptions import OutputParserException

from crewai.i18n import I18N


class TaskRepeatedUsageException(OutputParserException):
"""Exception raised when a task is used twice in a roll."""

i18n: I18N = I18N()
error: str = "TaskRepeatedUsageException"
message: str = "I just used the {tool} tool with input {tool_input}. So I already know the result of that and don't need to use it now.\n"
message: str

def __init__(self, tool: str, tool_input: str, text: str):
def __init__(self, i18n: I18N, tool: str, tool_input: str, text: str):
self.i18n = i18n
self.text = text
self.tool = tool
self.tool_input = tool_input
self.message = self.message.format(tool=tool, tool_input=tool_input)
self.message = self.i18n.errors("task_repeated_usage").format(
tool=tool, tool_input=tool_input
)

super().__init__(
error=self.error,
Expand Down
15 changes: 5 additions & 10 deletions src/crewai/agents/executor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import time
from textwrap import dedent
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union

from langchain.agents import AgentExecutor
Expand All @@ -12,11 +11,13 @@
from langchain_core.tools import BaseTool
from langchain_core.utils.input import get_color_mapping

from ..tools.cache_tools import CacheTools
from .cache.cache_hit import CacheHit
from crewai.agents.cache.cache_hit import CacheHit
from crewai.i18n import I18N
from crewai.tools.cache_tools import CacheTools


class CrewAgentExecutor(AgentExecutor):
i18n: I18N = I18N()
iterations: int = 0
max_iterations: Optional[int] = 15
force_answer_max_iterations: Optional[int] = None
Expand All @@ -31,13 +32,7 @@ def _should_force_answer(self) -> bool:

def _force_answer(self, output: AgentAction):
return AgentStep(
action=output,
observation=dedent(
"""\
I've used too many tools for this task.
I'm going to give you my absolute BEST Final answer now and
not use any more tools."""
),
action=output, observation=self.i18n.errors("used_too_many_tools")
)

def _call(
Expand Down
15 changes: 10 additions & 5 deletions src/crewai/agents/output_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from langchain.agents.output_parsers import ReActSingleInputOutputParser
from langchain_core.agents import AgentAction, AgentFinish

from .cache import CacheHandler, CacheHit
from .exceptions import TaskRepeatedUsageException
from .tools_handler import ToolsHandler
from crewai.agents.cache import CacheHandler, CacheHit
from crewai.agents.exceptions import TaskRepeatedUsageException
from crewai.agents.tools_handler import ToolsHandler
from crewai.i18n import I18N

FINAL_ANSWER_ACTION = "Final Answer:"
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE = (
Expand Down Expand Up @@ -46,6 +47,7 @@ class Config:

tools_handler: ToolsHandler
cache: CacheHandler
i18n: I18N

def parse(self, text: str) -> Union[AgentAction, AgentFinish, CacheHit]:
FINAL_ANSWER_ACTION in text
Expand All @@ -65,10 +67,13 @@ def parse(self, text: str) -> Union[AgentAction, AgentFinish, CacheHit]:
}
if usage == last_tool_usage:
raise TaskRepeatedUsageException(
tool=action, tool_input=tool_input, text=text
text=text,
tool=action,
tool_input=tool_input,
i18n=self.i18n,
)

if result := self.cache.read(action, tool_input):
if self.cache.read(action, tool_input):
action = AgentAction(action, tool_input, text)
return CacheHit(action=action, cache=self.cache)

Expand Down
6 changes: 6 additions & 0 deletions src/crewai/crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from crewai.agent import Agent
from crewai.agents.cache import CacheHandler
from crewai.i18n import I18N
from crewai.process import Process
from crewai.task import Task
from crewai.tools.agent_tools import AgentTools
Expand Down Expand Up @@ -44,6 +45,10 @@ class Crew(BaseModel):
config: Optional[Union[Json, Dict[str, Any]]] = Field(default=None)
cache_handler: Optional[InstanceOf[CacheHandler]] = Field(default=CacheHandler())
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
language: str = Field(
default="en",
description="Language used for the crew, defaults to English.",
)

@field_validator("id", mode="before")
@classmethod
Expand Down Expand Up @@ -99,6 +104,7 @@ def kickoff(self) -> str:
"""Starts the crew to work on its assigned tasks."""
for agent in self.agents:
agent.cache_handler = self.cache_handler
agent.i18n = I18N(language=self.language)

if self.process == Process.sequential:
return self._sequential_loop()
Expand Down
45 changes: 45 additions & 0 deletions src/crewai/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import json
import os
from typing import Dict, Optional

from pydantic import BaseModel, Field, PrivateAttr, ValidationError, model_validator


class I18N(BaseModel):
_translations: Optional[Dict[str, str]] = PrivateAttr()
language: Optional[str] = Field(
default="en",
description="Language used to load translations",
)

@model_validator(mode="after")
def load_translation(self) -> "I18N":
"""Load translations from a JSON file based on the specified language."""
try:
dir_path = os.path.dirname(os.path.realpath(__file__))
prompts_path = os.path.join(dir_path, f"translations/{self.language}.json")

with open(prompts_path, "r") as f:
self._translations = json.load(f)
except FileNotFoundError:
raise ValidationError(
f"Trasnlation file for language '{self.language}' not found."
)
except json.JSONDecodeError:
raise ValidationError(f"Error decoding JSON from the prompts file.")
return self

def slice(self, slice: str) -> str:
return self.retrieve("slices", slice)

def errors(self, error: str) -> str:
return self.retrieve("errors", error)

def tools(self, error: str) -> str:
return self.retrieve("tools", error)

def retrieve(self, kind, key):
try:
return self._translations[kind].get(key)
except:
raise ValidationError(f"Translation for '{kind}':'{key}' not found.")
37 changes: 6 additions & 31 deletions src/crewai/prompts.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
import json
import os
from typing import ClassVar, Dict, Optional
from typing import ClassVar

from langchain.prompts import PromptTemplate
from pydantic import BaseModel, Field, PrivateAttr, ValidationError, model_validator
from pydantic import BaseModel, Field

from .i18n import I18N


class Prompts(BaseModel):
"""Manages and generates prompts for a generic agent with support for different languages."""

_prompts: Optional[Dict[str, str]] = PrivateAttr()
language: Optional[str] = Field(
default="en",
description="Language of the prompts.",
)

@model_validator(mode="after")
def load_prompts(self) -> "Prompts":
"""Load prompts from a JSON file based on the specified language."""
try:
dir_path = os.path.dirname(os.path.realpath(__file__))
prompts_path = os.path.join(dir_path, f"prompts/{self.language}.json")

with open(prompts_path, "r") as f:
self._prompts = json.load(f)["slices"]
except FileNotFoundError:
raise ValidationError(
f"Prompt file for language '{self.language}' not found."
)
except json.JSONDecodeError:
raise ValidationError(f"Error decoding JSON from the prompts file.")
return self
i18n: I18N = Field(default=I18N())

SCRATCHPAD_SLICE: ClassVar[str] = "\n{agent_scratchpad}"

Expand All @@ -48,10 +27,6 @@ def task_execution(self) -> str:

def _build_prompt(self, components: [str]) -> str:
"""Constructs a prompt string from specified components."""
prompt_parts = [
self._prompts[component]
for component in components
if component in self._prompts
]
prompt_parts = [self.i18n.slice(component) for component in components]
prompt_parts.append(self.SCRATCHPAD_SLICE)
return PromptTemplate.from_template("".join(prompt_parts))
8 changes: 0 additions & 8 deletions src/crewai/prompts/en.json

This file was deleted.

Loading

0 comments on commit 8e7772c

Please sign in to comment.