Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to integrate external agents into the framework #246

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
9 changes: 5 additions & 4 deletions src/crewai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def check_agent_executor(self) -> "Agent":
def execute_task(
self,
task: Any,
context: Optional[str] = None,
context: Optional[List[str]] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this breaking anything in terms of retro-compatibility? Not an issue, per se, but I wonder if there's any way around if that's the case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought that the natural kind of context is a list of past messages in a conversation, and it's more natural to pass that through as a list instead of squashing it into a str

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it. It makes sense! My concern is the breaking change. What if we moved it to an str | List[str] | None type?

tools: Optional[List[Any]] = None,
) -> str:
"""Execute a task with the agent.
Expand All @@ -147,6 +147,7 @@ def execute_task(
task_prompt = task.prompt()

if context:
context = "\n".join(context)
task_prompt = self.i18n.slice("task_with_context").format(
task=task_prompt, context=context
)
Expand Down Expand Up @@ -215,9 +216,9 @@ def create_agent_executor(self) -> None:
}

if self._rpm_controller:
executor_args[
"request_within_rpm_limit"
] = self._rpm_controller.check_or_wait
executor_args["request_within_rpm_limit"] = (
self._rpm_controller.check_or_wait
)

if self.memory:
summary_memory = ConversationSummaryMemory(
Expand Down
52 changes: 52 additions & 0 deletions src/crewai/agents/agent_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from abc import ABC, abstractmethod
from typing import Optional, List, Any, Dict

from pydantic import BaseModel, PrivateAttr, Field

from crewai.utilities import I18N


class AgentWrapperParent(ABC, BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really loved the generic agent interface. In a perfect world, this would be the only way the engine would refer to agents, right!? Should we call it AgentInterface, like the file name? #namingishard

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make it clear, the dream here is that even Agent respects AgentInterface, then anything that complies to the interface would be able to be an agent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, that was exactly the idea :)

_i18n: I18N = PrivateAttr(default=I18N())
data: Dict[str, Any] = Field(
default_factory=dict,
description="Data storage for children, as pydantic doesn't play well with inheritance.",
)
role: str = Field(description="Role of the agent", default="")
allow_delegation: bool = Field(
description="Allow delegation of tasks to other agents?", default=False
)

@property
def i18n(self) -> I18N:
if hasattr(self, "_agent") and hasattr(self._agent, "i18n"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we prevent this kind of indirection by using some sort of interface? We are moving fast to become a strongly-typed library and this makes it very hard to make sure things are working properly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an offician AgentInterface existed, this could be part of it :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanna draft something here?

return self._agent.i18n
else:
return self._i18n

@i18n.setter
def i18n(self, value: I18N) -> None:
if hasattr(self, "_agent") and hasattr(self._agent, "i18n"):
self._agent.i18n = value
else:
self._i18n = value

@abstractmethod
def execute_task(
self,
task: str,
context: Optional[str] = None,
tools: Optional[List[Any]] = None,
) -> str:
pass

@property
@abstractmethod
def tools(self) -> List[Any]:
pass

def set_cache_handler(self, cache_handler: Any) -> None:
pass

def set_rpm_controller(self, rpm_controller: Any) -> None:
pass
52 changes: 52 additions & 0 deletions src/crewai/agents/langchain_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Any, Optional, List, Callable
import warnings

from langchain_core.messages import AIMessage
from crewai.agents.agent_interface import AgentWrapperParent


class LangchainCrewAgent(AgentWrapperParent):

def __init__(
self,
agent: Any,
role: str,
allow_delegation: bool = False,
tools: List[Any] | None = None,
**data: Any,
):
super().__init__(role=role, allow_delegation=allow_delegation, **data)
self.data.update(data)
self.data["agent"] = agent
# store tools by name to eliminate duplicates
self.data["tools"] = {}
self.tools = tools or []

def execute_task(
self,
task: str,
context: Optional[List[str]] = None,
tools: Optional[List[Any]] = None,
) -> str:
used_tools = self.tools + (tools or [])

if context:
context = [AIMessage(content=ctx) for ctx in context]
else:
context = []
# https://github.com/langchain-ai/langchain/discussions/17403
return self.data["agent"].invoke(
{"input": task, "chat_history": context, "tools": used_tools}
)["output"]

@property
def tools(self) -> List[Any]:
return list(self.data["tools"].values())

@tools.setter
def tools(self, tools: List[Any]) -> None:
for tool in tools:
if tool.name not in self.data["tools"]:
self.data["tools"][tool.name] = tool
else:
warnings.warn(f"Tool {tool.name} already exists in the agent.")
4 changes: 3 additions & 1 deletion src/crewai/crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
)
from pydantic_core import PydanticCustomError

from crewai.agents.agent_interface import AgentWrapperParent

from crewai.agent import Agent
from crewai.agents.cache import CacheHandler
from crewai.process import Process
Expand Down Expand Up @@ -51,7 +53,7 @@ class Crew(BaseModel):
_cache_handler: InstanceOf[CacheHandler] = PrivateAttr(default=CacheHandler())
model_config = ConfigDict(arbitrary_types_allowed=True)
tasks: List[Task] = Field(default_factory=list)
agents: List[Agent] = Field(default_factory=list)
agents: List[Agent | AgentWrapperParent] = Field(default_factory=list)
process: Process = Field(default=Process.sequential)
verbose: Union[int, bool] = Field(default=0)
full_output: Optional[bool] = Field(
Expand Down
4 changes: 2 additions & 2 deletions src/crewai/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from crewai.agent import Agent
from crewai.tasks.task_output import TaskOutput
from crewai.utilities import I18N
from crewai.agents.agent_interface import AgentWrapperParent


class Task(BaseModel):
Expand All @@ -24,7 +25,7 @@ class Config:
callback: Optional[Any] = Field(
description="Callback to be executed after the task is completed.", default=None
)
agent: Optional[Agent] = Field(
agent: Optional[Agent | AgentWrapperParent] = Field(
description="Agent responsible for execution the task.", default=None
)
expected_output: Optional[str] = Field(
Expand Down Expand Up @@ -91,7 +92,6 @@ def execute(
if task.async_execution:
task.thread.join()
context.append(task.output.result)
context = "\n".join(context)

tools = tools or self.tools

Expand Down
6 changes: 5 additions & 1 deletion src/crewai/tools/agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
from crewai.task import Task
from crewai.utilities import I18N

from crewai.agents.agent_interface import AgentWrapperParent


class AgentTools(BaseModel):
"""Default tools around agent delegation"""

agents: List[Agent] = Field(description="List of agents in this crew.")
agents: List[Agent | AgentWrapperParent] = Field(
description="List of agents in this crew."
)
i18n: I18N = Field(default=I18N(), description="Internationalization settings.")

def tools(self):
Expand Down