Skip to content

Commit

Permalink
fix: CrewAI-based flows with no extra openai (#4683)
Browse files Browse the repository at this point in the history
* fix: CrewAI-based flows with no extra openai

* [autofix.ci] apply automated fixes

* Clean up the location of the crewai model processing

* [autofix.ci] apply automated fixes

* Properly subclass the tasks and agents method

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
erichare and autofix-ci[bot] authored Nov 18, 2024
1 parent 3188517 commit 2fa2580
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 29 deletions.
105 changes: 102 additions & 3 deletions src/backend/base/langflow/base/agents/crewai/crew.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +15,82 @@
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."
Expand All @@ -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)
Expand Down
21 changes: 19 additions & 2 deletions src/backend/base/langflow/components/crewai/crewai.py
Original file line number Diff line number Diff line change
@@ -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/"
Expand Down Expand Up @@ -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
17 changes: 3 additions & 14 deletions src/backend/base/langflow/components/crewai/hierarchical_crew.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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,
Expand All @@ -46,7 +35,7 @@ def build_crew(self) -> Crew:
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(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/backend/base/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 8 additions & 7 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2fa2580

Please sign in to comment.