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

feat: migrate agents and toolkits to Component syntax #2579

Merged
merged 5 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 82 additions & 69 deletions src/backend/base/langflow/base/agents/agent.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,91 @@
from typing import List, Optional, Union, cast
from abc import abstractmethod
from typing import List

from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
from langchain_core.messages import BaseMessage
from langchain.agents.agent import RunnableAgent

from langchain.agents import AgentExecutor
from langchain_core.runnables import Runnable

from langflow.base.agents.utils import data_to_messages, get_agents_list
from langflow.custom import CustomComponent
from langflow.field_typing import Text, Tool
from langflow.schema import Data
from langflow.custom import Component
from langflow.inputs import BoolInput, IntInput, HandleInput
from langflow.inputs.inputs import InputTypes
from langflow.template import Output


class LCAgentComponent(Component):
trace_type = "agent"
_base_inputs: List[InputTypes] = [
BoolInput(
name="handle_parsing_errors",
display_name="Handle Parse Errors",
value=True,
advanced=True,
),
BoolInput(
name="verbose",
display_name="Verbose",
value=True,
advanced=True,
),
IntInput(
name="max_iterations",
display_name="Max Iterations",
value=15,
advanced=True,
),
]

outputs = [
Output(display_name="Agent", name="agent", method="build_agent"),
]

class LCAgentComponent(CustomComponent):
def get_agents_list(self):
return get_agents_list()
def _validate_outputs(self):
required_output_methods = ["build_agent"]
output_names = [output.name for output in self.outputs]
for method_name in required_output_methods:
if method_name not in output_names:
raise ValueError(f"Output with name '{method_name}' must be defined.")
elif not hasattr(self, method_name):
raise ValueError(f"Method '{method_name}' must be defined.")

def build_config(self):
return {
"lc": {
"display_name": "LangChain",
"info": "The LangChain to interact with.",
},
"handle_parsing_errors": {
"display_name": "Handle Parsing Errors",
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
"advanced": True,
},
"output_key": {
"display_name": "Output Key",
"info": "The key to use to get the output from the agent.",
"advanced": True,
},
"memory": {
"display_name": "Memory",
"info": "Memory to use for the agent.",
},
"tools": {
"display_name": "Tools",
"info": "Tools the agent can use.",
},
"input_value": {
"display_name": "Input",
"info": "Input text to pass to the agent.",
},
def get_agent_kwargs(self, flatten: bool = False) -> dict:
base = {
"handle_parsing_errors": self.handle_parsing_errors,
"verbose": self.verbose,
"allow_dangerous_code": True,
}
agent_kwargs = {
"handle_parsing_errors": self.handle_parsing_errors,
"max_iterations": self.max_iterations,
}
if flatten:
return {
**base,
**agent_kwargs,
}
return {**base, "agent_executor_kwargs": agent_kwargs}


class LCToolsAgentComponent(LCAgentComponent):
_base_inputs = LCAgentComponent._base_inputs + [
HandleInput(
name="tools",
display_name="Tools",
input_types=["Tool"],
is_list=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
]

async def run_agent(
self,
agent: Union[Runnable, BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
inputs: str,
tools: List[Tool],
message_history: Optional[List[Data]] = None,
handle_parsing_errors: bool = True,
output_key: str = "output",
) -> Text:
if isinstance(agent, AgentExecutor):
runnable = agent
else:
runnable = AgentExecutor.from_agent_and_tools(
agent=agent, # type: ignore
tools=tools,
verbose=True,
handle_parsing_errors=handle_parsing_errors,
)
input_dict: dict[str, str | list[BaseMessage]] = {"input": inputs}
if message_history:
input_dict["chat_history"] = data_to_messages(message_history)
result = await runnable.ainvoke(input_dict)
self.status = result
if output_key in result:
return cast(str, result.get(output_key))
elif "output" not in result:
if output_key != "output":
raise ValueError(f"Output key not found in result. Tried '{output_key}' and 'output'.")
else:
raise ValueError("Output key not found in result. Tried 'output'.")
def build_agent(self) -> AgentExecutor:
agent = self.creat_agent_runnable()
return AgentExecutor.from_agent_and_tools(
agent=RunnableAgent(runnable=agent, input_keys_arg=["input"], return_keys_arg=["output"]),
tools=self.tools,
**self.get_agent_kwargs(flatten=True),
)

return cast(str, result.get("output"))
@abstractmethod
def creat_agent_runnable(self) -> Runnable:
"""Create the agent."""
pass
12 changes: 10 additions & 2 deletions src/backend/base/langflow/base/vectorstores/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, cast

from langchain_core.documents import Document
from loguru import logger
Expand All @@ -23,11 +23,16 @@ class LCVectorStoreComponent(Component):
name="search_results",
method="search_documents",
),
Output(
display_name="Vector Store",
name="vector_store",
method="cast_vector_store",
),
]

def _validate_outputs(self):
# At least these three outputs must be defined
required_output_methods = ["build_base_retriever", "search_documents"]
required_output_methods = ["build_base_retriever", "search_documents", "build_vector_store"]
output_names = [output.name for output in self.outputs]
for method_name in required_output_methods:
if method_name not in output_names:
Expand Down Expand Up @@ -67,6 +72,9 @@ def search_with_vector_store(
self.status = data
return data

def cast_vector_store(self) -> VectorStore:
return cast(VectorStore, self.build_vector_store())

def build_vector_store(self) -> VectorStore:
"""
Builds the Vector Store object.c
Expand Down
42 changes: 17 additions & 25 deletions src/backend/base/langflow/components/agents/CSVAgent.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
from langchain_experimental.agents.agent_toolkits.csv.base import create_csv_agent

from langflow.custom import CustomComponent
from langflow.field_typing import AgentExecutor, LanguageModel
from langflow.base.agents.agent import LCAgentComponent
from langflow.field_typing import AgentExecutor
from langflow.inputs import HandleInput, FileInput, DropdownInput


class CSVAgentComponent(CustomComponent):
class CSVAgentComponent(LCAgentComponent):
display_name = "CSVAgent"
description = "Construct a CSV agent from a CSV and tools."
documentation = "https://python.langchain.com/docs/modules/agents/toolkits/csv"
name = "CSVAgent"

def build_config(self):
return {
"llm": {"display_name": "LLM", "type": LanguageModel},
"path": {"display_name": "Path", "field_type": "file", "suffixes": [".csv"], "file_types": [".csv"]},
"handle_parsing_errors": {"display_name": "Handle Parse Errors", "advanced": True},
"agent_type": {
"display_name": "Agent Type",
"options": ["zero-shot-react-description", "openai-functions", "openai-tools"],
"advanced": True,
},
}
inputs = LCAgentComponent._base_inputs + [
FileInput(name="path", display_name="File Path", file_types=["csv"], required=True),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
DropdownInput(
name="agent_type",
display_name="Agent Type",
advanced=True,
options=["zero-shot-react-description", "openai-functions", "openai-tools"],
value="openai-tools",
),
]

def build(
self, llm: LanguageModel, path: str, handle_parsing_errors: bool = True, agent_type: str = "openai-tools"
) -> AgentExecutor:
# Instantiate and return the CSV agent class with the provided llm and path
return create_csv_agent(
llm=llm,
path=path,
agent_type=agent_type,
verbose=True,
agent_executor_kwargs=dict(handle_parsing_errors=handle_parsing_errors),
)
def build_agent(self) -> AgentExecutor:
return create_csv_agent(llm=self.llm, path=self.path, agent_type=self.agent_type, **self.get_agent_kwargs())
34 changes: 20 additions & 14 deletions src/backend/base/langflow/components/agents/JsonAgent.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
from pathlib import Path

import yaml
from langchain.agents import AgentExecutor
from langchain_community.agent_toolkits import create_json_agent
from langchain_community.agent_toolkits.json.toolkit import JsonToolkit
from langchain_community.tools.json.tool import JsonSpec

from langflow.custom import CustomComponent
from langflow.field_typing import LanguageModel
from langflow.base.agents.agent import LCAgentComponent
from langflow.inputs import HandleInput, FileInput


class JsonAgentComponent(CustomComponent):
class JsonAgentComponent(LCAgentComponent):
display_name = "JsonAgent"
description = "Construct a json agent from an LLM and tools."
name = "JsonAgent"

def build_config(self):
return {
"llm": {"display_name": "LLM"},
"toolkit": {"display_name": "Toolkit"},
}
inputs = LCAgentComponent._base_inputs + [
FileInput(name="path", display_name="File Path", file_types=["json", "yaml", "yml"], required=True),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
]

def build_agent(self) -> AgentExecutor:
if self.path.endswith("yaml") or self.path.endswith("yml"):
yaml_dict = yaml.load(open(self.path, "r"), Loader=yaml.FullLoader)
spec = JsonSpec(dict_=yaml_dict)
else:
spec = JsonSpec.from_file(Path(self.path))
toolkit = JsonToolkit(spec=spec)

def build(
self,
llm: LanguageModel,
toolkit: JsonToolkit,
) -> AgentExecutor:
return create_json_agent(llm=llm, toolkit=toolkit)
return create_json_agent(llm=self.llm, toolkit=toolkit, **self.get_agent_kwargs())
36 changes: 36 additions & 0 deletions src/backend/base/langflow/components/agents/OpenAIToolsAgent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from langchain.agents import create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate, HumanMessagePromptTemplate

from langflow.base.agents.agent import LCToolsAgentComponent
from langflow.inputs import MultilineInput


class OpenAIToolsAgentComponent(LCToolsAgentComponent):
display_name: str = "OpenAI Tools Agent"
description: str = "Agent that uses tools via openai-tools."
icon = "LangChain"
beta = True
name = "OpenAIToolsAgent"

inputs = LCToolsAgentComponent._base_inputs + [
MultilineInput(
name="system_prompt",
display_name="System Prompt",
info="System prompt for the agent.",
value="You are a helpful assistant",
),
MultilineInput(
name="user_prompt", display_name="Prompt", info="This prompt must contain 'input' key.", value="{input}"
),
]

def creat_agent_runnable(self):
if "input" not in self.user_prompt:
raise ValueError("Prompt must contain 'input' key.")
messages = [
("system", self.system_prompt),
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=["input"], template=self.user_prompt)),
("placeholder", "{agent_scratchpad}"),
]
prompt = ChatPromptTemplate.from_messages(messages)
return create_openai_tools_agent(self.llm, self.tools, prompt)
42 changes: 42 additions & 0 deletions src/backend/base/langflow/components/agents/OpenAPIAgent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pathlib import Path

import yaml
from langchain.agents import AgentExecutor
from langchain_community.agent_toolkits import create_openapi_agent
from langchain_community.tools.json.tool import JsonSpec
from langchain_community.agent_toolkits.openapi.toolkit import OpenAPIToolkit

from langflow.base.agents.agent import LCAgentComponent
from langflow.inputs import BoolInput, HandleInput, FileInput
from langchain_community.utilities.requests import TextRequestsWrapper


class OpenAPIAgentComponent(LCAgentComponent):
display_name = "OpenAPI Agent"
description = "Agent to interact with OpenAPI API."
name = "OpenAPIAgent"

inputs = LCAgentComponent._base_inputs + [
FileInput(name="path", display_name="File Path", file_types=["json", "yaml", "yml"], required=True),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
BoolInput(name="allow_dangerous_requests", display_name="Allow Dangerous Requests", value=False, required=True),
]

def build_agent(self) -> AgentExecutor:
if self.path.endswith("yaml") or self.path.endswith("yml"):
yaml_dict = yaml.load(open(self.path, "r"), Loader=yaml.FullLoader)
spec = JsonSpec(dict_=yaml_dict)
else:
spec = JsonSpec.from_file(Path(self.path))
requests_wrapper = TextRequestsWrapper()
toolkit = OpenAPIToolkit.from_llm(
llm=self.llm,
json_spec=spec,
requests_wrapper=requests_wrapper,
allow_dangerous_requests=self.allow_dangerous_requests,
)

agent_args = self.get_agent_kwargs()
agent_args["max_iterations"] = agent_args["agent_executor_kwargs"]["max_iterations"]
del agent_args["agent_executor_kwargs"]["max_iterations"]
return create_openapi_agent(llm=self.llm, toolkit=toolkit, **agent_args)
Loading
Loading