diff --git a/src/backend/base/langflow/base/agents/crewai/crew.py b/src/backend/base/langflow/base/agents/crewai/crew.py index 8a7819955..f81dac70c 100644 --- a/src/backend/base/langflow/base/agents/crewai/crew.py +++ b/src/backend/base/langflow/base/agents/crewai/crew.py @@ -1,9 +1,11 @@ from collections.abc import Callable -from typing import cast +from typing import Any, cast -from crewai import Agent, Crew, Process, Task +from crewai import LLM, Agent, Crew, Process, Task from crewai.task import TaskOutput +from crewai.tools.base_tool import Tool from langchain_core.agents import AgentAction, AgentFinish +from pydantic import SecretStr from langflow.custom import Component from langflow.inputs.inputs import HandleInput, InputTypes @@ -13,6 +15,82 @@ from langflow.schema.message import Message from langflow.utils.constants import MESSAGE_SENDER_AI +def _find_api_key(model): + """Attempts to find the API key attribute for a LangChain LLM model instance using partial matching. + + Args: + model: LangChain LLM model instance. + + Returns: + The API key if found, otherwise None. + """ + # Define the possible API key attribute patterns + key_patterns = ["key", "token"] + + # Iterate over the model attributes + for attr in dir(model): + attr_lower = attr.lower() + + # Check if the attribute name contains any of the key patterns + if any(pattern in attr_lower for pattern in key_patterns): + value = getattr(model, attr, None) + + # Check if the value is a non-empty string + if isinstance(value, str): + return value + if isinstance(value, SecretStr): + return value.get_secret_value() + + return None + + +def convert_llm(llm: Any, excluded_keys=None) -> LLM: + """Converts a LangChain LLM object to a CrewAI-compatible LLM object. + + Args: + llm: A LangChain LLM object. + excluded_keys: A set of keys to exclude from the conversion. + + Returns: + A CrewAI-compatible LLM object + """ + if not llm: + return None + + # Check if this is already an LLM object + if isinstance(llm, LLM): + return llm + + # Retrieve the API Key from the LLM + if excluded_keys is None: + excluded_keys = {"model", "model_name", "_type", "api_key"} + + # Find the API key in the LLM + api_key = _find_api_key(llm) + + # Convert Langchain LLM to CrewAI-compatible LLM object + return LLM( + model=llm.model_name, + api_key=api_key, + **{k: v for k, v in llm.dict().items() if k not in excluded_keys}, + ) + + +def convert_tools(tools): + """Converts LangChain tools to CrewAI-compatible tools. + + Args: + tools: A LangChain tools list. + + Returns: + A CrewAI-compatible tools list. + """ + if not tools: + return [] + + return [Tool.from_langchain(tool) for tool in tools] + + class BaseCrewComponent(Component): description: str = ( "Represents a group of agents, defining how they should collaborate and the tasks they should perform." @@ -39,12 +117,33 @@ class BaseCrewComponent(Component): Output(display_name="Output", name="output", method="build_output"), ] + # Model properties to exclude when creating a CrewAI LLM object + manager_llm: LLM | None + def task_is_valid(self, task_data: Data, crew_type: Process) -> Task: return "task_type" in task_data and task_data.task_type == crew_type - def get_tasks_and_agents(self) -> tuple[list[Task], list[Agent]]: + def get_tasks_and_agents(self, agents_list=None) -> tuple[list[Task], list[Agent]]: + # Allow passing a custom list of agents + if not agents_list: + agents_list = self.agents or [] + + # Set all the agents llm attribute to the crewai llm + for agent in agents_list: + # Convert Agent LLM and Tools to proper format + agent.llm = convert_llm(agent.llm) + agent.tools = convert_tools(agent.tools) + return self.tasks, self.agents + def get_manager_llm(self) -> LLM | None: + if not self.manager_llm: + return None + + self.manager_llm = convert_llm(self.manager_llm) + + return self.manager_llm + def build_crew(self) -> Crew: msg = "build_crew must be implemented in subclasses" raise NotImplementedError(msg) diff --git a/src/backend/base/langflow/components/crewai/crewai.py b/src/backend/base/langflow/components/crewai/crewai.py index 879977ec7..e942ab236 100644 --- a/src/backend/base/langflow/components/crewai/crewai.py +++ b/src/backend/base/langflow/components/crewai/crewai.py @@ -1,10 +1,23 @@ from crewai import Agent +from langflow.base.agents.crewai.crew import convert_llm, convert_tools from langflow.custom import Component from langflow.io import BoolInput, DictInput, HandleInput, MultilineInput, Output class CrewAIAgentComponent(Component): + """Component for creating a CrewAI agent. + + This component allows you to create a CrewAI agent with the specified role, goal, backstory, tools, + and language model. + + Args: + Component (Component): Base class for all components. + + Returns: + Agent: CrewAI agent. + """ + display_name = "CrewAI Agent" description = "Represents an agent of CrewAI." documentation: str = "https://docs.crewai.com/how-to/LLM-Connections/" @@ -69,17 +82,21 @@ class CrewAIAgentComponent(Component): def build_output(self) -> Agent: kwargs = self.kwargs or {} + + # Define the Agent agent = Agent( role=self.role, goal=self.goal, backstory=self.backstory, - llm=self.llm, + llm=convert_llm(self.llm), verbose=self.verbose, memory=self.memory, - tools=self.tools or [], + tools=convert_tools(self.tools), allow_delegation=self.allow_delegation, allow_code_execution=self.allow_code_execution, **kwargs, ) + self.status = repr(agent) + return agent diff --git a/src/backend/base/langflow/components/crewai/hierarchical_crew.py b/src/backend/base/langflow/components/crewai/hierarchical_crew.py index 829555401..1bc6f3fd2 100644 --- a/src/backend/base/langflow/components/crewai/hierarchical_crew.py +++ b/src/backend/base/langflow/components/crewai/hierarchical_crew.py @@ -1,9 +1,7 @@ -import os - from crewai import Crew, Process from langflow.base.agents.crewai.crew import BaseCrewComponent -from langflow.io import HandleInput, SecretStrInput +from langflow.io import HandleInput class HierarchicalCrewComponent(BaseCrewComponent): @@ -20,20 +18,11 @@ class HierarchicalCrewComponent(BaseCrewComponent): HandleInput(name="tasks", display_name="Tasks", input_types=["HierarchicalTask"], is_list=True), HandleInput(name="manager_llm", display_name="Manager LLM", input_types=["LanguageModel"], required=False), HandleInput(name="manager_agent", display_name="Manager Agent", input_types=["Agent"], required=False), - SecretStrInput( - name="openai_api_key", - display_name="OpenAI API Key", - info="The OpenAI API Key to use for the OpenAI model.", - value="OPENAI_API_KEY", - ), ] def build_crew(self) -> Crew: tasks, agents = self.get_tasks_and_agents() - - # Set the OpenAI API Key - if self.openai_api_key: - os.environ["OPENAI_API_KEY"] = self.openai_api_key + manager_llm = self.get_manager_llm() return Crew( agents=agents, @@ -46,7 +35,7 @@ class HierarchicalCrewComponent(BaseCrewComponent): share_crew=self.share_crew, function_calling_llm=self.function_calling_llm, manager_agent=self.manager_agent, - manager_llm=self.manager_llm, + manager_llm=manager_llm, step_callback=self.get_step_callback(), task_callback=self.get_task_callback(), ) diff --git a/src/backend/base/langflow/components/crewai/sequential_crew.py b/src/backend/base/langflow/components/crewai/sequential_crew.py index df17e7525..99d739ad2 100644 --- a/src/backend/base/langflow/components/crewai/sequential_crew.py +++ b/src/backend/base/langflow/components/crewai/sequential_crew.py @@ -16,11 +16,16 @@ class SequentialCrewComponent(BaseCrewComponent): HandleInput(name="tasks", display_name="Tasks", input_types=["SequentialTask"], is_list=True), ] - def get_tasks_and_agents(self) -> tuple[list[Task], list[Agent]]: - return self.tasks, [task.agent for task in self.tasks] + def get_tasks_and_agents(self, agents_list=None) -> tuple[list[Task], list[Agent]]: + if not agents_list: + agents_list = [task.agent for task in self.tasks] or [] + + # Use the superclass implementation, passing the customized agents_list + return super().get_tasks_and_agents(agents_list=agents_list) def build_crew(self) -> Message: tasks, agents = self.get_tasks_and_agents() + return Crew( agents=agents, tasks=tasks, diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index d676489eb..20ff2d883 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -154,7 +154,7 @@ dependencies = [ "nanoid>=2.0.0", "filelock>=3.15.4", "grandalf>=0.8.0", - "crewai>=0.74.2", + "crewai~=0.80.0", "spider-client>=0.0.27", "diskcache>=5.6.3", "clickhouse-connect==0.7.19", diff --git a/uv.lock b/uv.lock index 1160aacc6..4b16d9f7d 100644 --- a/uv.lock +++ b/uv.lock @@ -1094,7 +1094,7 @@ toml = [ [[package]] name = "crewai" -version = "0.74.2" +version = "0.80.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appdirs" }, @@ -1115,17 +1115,18 @@ dependencies = [ { name = "python-dotenv" }, { name = "pyvis" }, { name = "regex" }, + { name = "tomli" }, { name = "tomli-w" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/7f/998b084d1ebfca9739f7e26e0c97eab6e4f35391689a2836c04497430dd8/crewai-0.74.2.tar.gz", hash = "sha256:de8c70bda1862ee52f52202d0f2afb53cae3e5849ecaee3c6bdcb774bac2358f", size = 5812388 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/38/584389ffb7ca7bc4719438a932d99993b8e24781e81dec33908c8bdb8954/crewai-0.80.0.tar.gz", hash = "sha256:8fc10f8a0344349f5fcc431fcdd03dcb033704d402d67f9b145a6d9d099d8e42", size = 5842314 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ef/5fae2f28d634c146cd06da118b50f3b9c7213ef9649e20f4ba1e7b6d2f06/crewai-0.74.2-py3-none-any.whl", hash = "sha256:4f0acd839ac604da1ad8efea67394166700e02ae643ee829b0f8eb22f2129ef2", size = 187638 }, + { url = "https://files.pythonhosted.org/packages/13/55/8caa2264c59be4c11266be1aae2b57610dcd30cd1c6f0752416589126f3b/crewai-0.80.0-py3-none-any.whl", hash = "sha256:74eb67b6de2688871c831bc617de0a839667c643c8b6b3757b3c1e849bea3ea0", size = 197680 }, ] [[package]] name = "crewai-tools" -version = "0.13.2" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -1143,9 +1144,9 @@ dependencies = [ { name = "requests" }, { name = "selenium" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/02/136f42ed8a7bd706a85663714c615bdcb684e43e95e4719c892aa0ce3d53/crewai_tools-0.13.2.tar.gz", hash = "sha256:c6782f2e868c0e96b25891f1b40fb8c90c01e920bab2fd1388f89ef1d7a4b99b", size = 816250 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/6d/4fa91b481b120f83bb58f365203d8aa8564e8ced1035d79f8aedb7d71e2f/crewai_tools-0.14.0.tar.gz", hash = "sha256:510f3a194bcda4fdae4314bd775521964b5f229ddbe451e5d9e0216cae57f4e3", size = 815892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/30/df215173b6193b2cfb1902a339443be73056eae89579805b853c6f359761/crewai_tools-0.13.2-py3-none-any.whl", hash = "sha256:8c7583c9559fb625f594349c6553a5251ebd7b21918735ad6fbe8bab7ec3db50", size = 463444 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9f4e64e1507062957b0118085332d38b621c1000874baef2d1c4069bfd97/crewai_tools-0.14.0-py3-none-any.whl", hash = "sha256:0a804a828c29869c3af3253f4fc4c3967a3f80f06dab22e9bbe9526608a31564", size = 462980 }, ] [[package]] @@ -3914,7 +3915,7 @@ requires-dist = [ { name = "celery", marker = "extra == 'deploy'", specifier = ">=5.3.1" }, { name = "chardet", specifier = ">=5.2.0" }, { name = "clickhouse-connect", specifier = "==0.7.19" }, - { name = "crewai", specifier = ">=0.74.2" }, + { name = "crewai", specifier = "~=0.80.0" }, { name = "cryptography", specifier = ">=42.0.5,<44.0.0" }, { name = "ctransformers", marker = "extra == 'all'", specifier = ">=0.2" }, { name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2" },