Skip to content

refactor: Optimize agent component for efficiency and readability #5593

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
111 changes: 46 additions & 65 deletions src/backend/base/langflow/base/agents/agent.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import re
from abc import abstractmethod
from typing import TYPE_CHECKING, cast
Expand Down Expand Up @@ -28,7 +29,7 @@


DEFAULT_TOOLS_DESCRIPTION = "A helpful assistant with access to the following tools:"
DEFAULT_AGENT_NAME = "Agent ({tools_names})"
TOOL_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")


class LCAgentComponent(Component):
Expand Down Expand Up @@ -81,82 +82,62 @@
"""Run the agent and return the response."""
agent = self.build_agent()
message = await self.run_agent(agent=agent)

self.status = message
return message

def _validate_outputs(self) -> None:
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:
msg = f"Output with name '{method_name}' must be defined."
raise ValueError(msg)
if not hasattr(self, method_name):
msg = f"Method '{method_name}' must be defined."
raise ValueError(msg)

def get_agent_kwargs(self, *, flatten: bool = False) -> dict:
base = {
def get_agent_kwargs(self) -> dict:
"""Get agent configuration arguments."""
return {
"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,
"allow_dangerous_code": True,
}
if flatten:
return {
**base,
**agent_kwargs,
}
return {**base, "agent_executor_kwargs": agent_kwargs}

def get_chat_history_data(self) -> list[Data] | None:
# might be overridden in subclasses
"""Retrieve chat history data if available."""
return None

async def run_agent(
self,
agent: Runnable | BaseSingleActionAgent | BaseMultiActionAgent | AgentExecutor,
) -> Message:
if isinstance(agent, AgentExecutor):
runnable = agent
else:
if not hasattr(self, "tools") or not self.tools:
msg = "Tools are required to run the agent."
raise ValueError(msg)
handle_parsing_errors = hasattr(self, "handle_parsing_errors") and self.handle_parsing_errors
verbose = hasattr(self, "verbose") and self.verbose
max_iterations = hasattr(self, "max_iterations") and self.max_iterations
runnable = AgentExecutor.from_agent_and_tools(
"""Run the agent and process its events."""
if not hasattr(self, "tools") or not self.tools:
error_message = "Tools are required to run the agent."
raise ValueError(error_message)

runnable = (
agent
if isinstance(agent, AgentExecutor)
else AgentExecutor.from_agent_and_tools(
agent=agent,
tools=self.tools,
handle_parsing_errors=handle_parsing_errors,
verbose=verbose,
max_iterations=max_iterations,
**self.get_agent_kwargs(),
)
)

input_dict: dict[str, str | list[BaseMessage]] = {"input": self.input_value}
if hasattr(self, "system_prompt"):
input_dict["system_prompt"] = self.system_prompt
if hasattr(self, "chat_history") and self.chat_history:
input_dict["chat_history"] = data_to_messages(self.chat_history)

if hasattr(self, "graph"):
session_id = self.graph.session_id
elif hasattr(self, "_session_id"):
session_id = self._session_id
else:
session_id = None

if hasattr(self, "chat_history") and self.chat_history:
messages = data_to_messages(self.chat_history)
filtered_messages = [msg for msg in messages if msg.get("content", "").strip()]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error details:
AttributeError
Details: 'HumanMessage' object has no attribute 'get'

if not filtered_messages:
error_message = "No valid messages to process in chat history."
raise ValueError(error_message)
input_dict["chat_history"] = filtered_messages
Comment on lines +126 to +130
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no problem in having 0 messages.


session_id = getattr(self, "graph", getattr(self, "_session_id", None))
agent_message = Message(
sender=MESSAGE_SENDER_AI,
sender_name=self.display_name or "Agent",
properties={"icon": "Bot", "state": "partial"},
content_blocks=[ContentBlock(title="Agent Steps", contents=[])],
session_id=session_id,
)

try:
result = await process_agent_events(
runnable.astream_events(
Expand All @@ -172,7 +153,8 @@
await delete_message(id_=msg_id)
await self._send_message_event(e.agent_message, category="remove_message")
raise
except Exception:
except Exception as error:
logging.exception("An error occurred: %s", error)

Check failure on line 157 in src/backend/base/langflow/base/agents/agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (TRY401)

src/backend/base/langflow/base/agents/agent.py:157:56: TRY401 Redundant exception object included in `logging.exception` call
raise

self.status = result
Expand All @@ -184,15 +166,14 @@

def validate_tool_names(self) -> None:
"""Validate tool names to ensure they match the required pattern."""
pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
if hasattr(self, "tools") and self.tools:
for tool in self.tools:
if not pattern.match(tool.name):
msg = (
f"Invalid tool name '{tool.name}': must only contain letters, numbers, underscores, dashes,"
" and cannot contain spaces."
if not TOOL_NAME_PATTERN.match(tool.name):
error_message = (
f"Invalid tool name '{tool.name}': must only contain letters, numbers, underscores, dashes, "
"and cannot contain spaces."
)
raise ValueError(msg)
raise ValueError(error_message)


class LCToolsAgentComponent(LCAgentComponent):
Expand All @@ -209,36 +190,36 @@
]

def build_agent(self) -> AgentExecutor:
"""Build the agent executor with tools."""
self.validate_tool_names()
agent = self.create_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),
**self.get_agent_kwargs(),
)

@abstractmethod
def create_agent_runnable(self) -> Runnable:
"""Create the agent."""

def get_tool_name(self) -> str:
"""Get the name of the tool."""
return self.display_name or "Agent"

def get_tool_description(self) -> str:
return self.agent_description or DEFAULT_TOOLS_DESCRIPTION
"""Get the description of the tool."""
return DEFAULT_TOOLS_DESCRIPTION

def _build_tools_names(self):
tools_names = ""
if self.tools:
tools_names = ", ".join([tool.name for tool in self.tools])
return tools_names
def _build_tools_names(self) -> str:
"""Build a string of tool names."""
return ", ".join(tool.name for tool in self.tools) if self.tools else ""

def to_toolkit(self) -> list[Tool]:
"""Convert the component to a toolkit."""
component_toolkit = _get_component_toolkit()
tools_names = self._build_tools_names()
agent_description = self.get_tool_description()
# TODO: Agent Description Depreciated Feature to be removed
description = f"{agent_description}{tools_names}"
description = f"{DEFAULT_TOOLS_DESCRIPTION}{tools_names}"
tools = component_toolkit(component=self).get_tools(
tool_name=self.get_tool_name(), tool_description=description, callbacks=self.get_langchain_callbacks()
)
Expand Down
Loading