Skip to content

Commit

Permalink
Merge branch 'main' into feat-context
Browse files Browse the repository at this point in the history
  • Loading branch information
ogabrielluiz authored Nov 8, 2024
2 parents f3bcbc1 + 19301b1 commit cb61de5
Show file tree
Hide file tree
Showing 73 changed files with 1,767 additions and 620 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
with:
timeout_minutes: 12
max_attempts: 2
command: make unit_tests async=false args="--splits ${{ matrix.splitCount }} --group ${{ matrix.group }}"
command: make unit_tests async=false args="-x --splits ${{ matrix.splitCount }} --group ${{ matrix.group }}"
- name: Minimize uv cache
run: uv cache prune --ci
integration-tests:
Expand Down
61 changes: 57 additions & 4 deletions src/backend/base/langflow/base/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from langflow.base.agents.events import ExceptionWithMessageError, process_agent_events
from langflow.base.agents.utils import data_to_messages
from langflow.custom import Component
from langflow.inputs.inputs import InputTypes
from langflow.custom.custom_component.component import _get_component_toolkit
from langflow.field_typing import Tool
from langflow.inputs.inputs import InputTypes, MultilineInput
from langflow.io import BoolInput, HandleInput, IntInput, MessageTextInput
from langflow.memory import delete_message
from langflow.schema import Data
Expand All @@ -24,10 +26,19 @@
from langchain_core.messages import BaseMessage


DEFAULT_TOOLS_DESCRIPTION = "A helpful assistant with access to the following tools:"
DEFAULT_AGENT_NAME = "Agent ({tools_names})"


class LCAgentComponent(Component):
trace_type = "agent"
_base_inputs: list[InputTypes] = [
MessageTextInput(name="input_value", display_name="Input"),
MessageTextInput(
name="input_value",
display_name="Input",
info="The input provided by the user for the agent to process.",
tool_mode=True,
),
BoolInput(
name="handle_parsing_errors",
display_name="Handle Parse Errors",
Expand All @@ -46,6 +57,16 @@ class LCAgentComponent(Component):
value=15,
advanced=True,
),
MultilineInput(
name="agent_description",
display_name="Agent Description",
info=(
"The description of the agent. This is only used when in Tool Mode. "
f"Defaults to '{DEFAULT_TOOLS_DESCRIPTION}' and tools are added dynamically."
),
advanced=True,
value=DEFAULT_TOOLS_DESCRIPTION,
),
]

outputs = [
Expand Down Expand Up @@ -104,6 +125,9 @@ async def run_agent(
if isinstance(agent, AgentExecutor):
runnable = agent
else:
if not self.tools:
msg = "Tools are required to run the agent."
raise ValueError(msg)
runnable = AgentExecutor.from_agent_and_tools(
agent=agent,
tools=self.tools,
Expand All @@ -117,7 +141,7 @@ async def run_agent(

agent_message = Message(
sender=MESSAGE_SENDER_AI,
sender_name="Agent",
sender_name=self.display_name or "Agent",
properties={"icon": "Bot", "state": "partial"},
content_blocks=[ContentBlock(title="Agent Steps", contents=[])],
session_id=self.graph.session_id,
Expand Down Expand Up @@ -151,7 +175,11 @@ def create_agent_runnable(self) -> Runnable:
class LCToolsAgentComponent(LCAgentComponent):
_base_inputs = [
HandleInput(
name="tools", display_name="Tools", input_types=["Tool", "BaseTool", "StructuredTool"], is_list=True
name="tools",
display_name="Tools",
input_types=["Tool", "BaseTool", "StructuredTool"],
is_list=True,
required=True,
),
*LCAgentComponent._base_inputs,
]
Expand All @@ -167,3 +195,28 @@ def build_agent(self) -> AgentExecutor:
@abstractmethod
def create_agent_runnable(self) -> Runnable:
"""Create the agent."""

def get_tool_name(self) -> str:
return self.display_name or "Agent"

def get_tool_description(self) -> str:
return self.agent_description or 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 to_toolkit(self) -> list[Tool]:
component_toolkit = _get_component_toolkit()
tools_names = self._build_tools_names()
agent_description = self.get_tool_description()
# Check if tools_description is the default value
if agent_description == DEFAULT_TOOLS_DESCRIPTION:
description = f"{agent_description}{tools_names}"
else:
description = agent_description
return component_toolkit(component=self).get_tools(
tool_name=self.get_tool_name(), tool_description=description, callbacks=self.get_langchain_callbacks()
)
2 changes: 1 addition & 1 deletion src/backend/base/langflow/base/agents/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,6 @@ async def process_agent_events(
agent_message, start_time = chain_handler(event, agent_message, send_message_method, start_time)
start_time = start_time or perf_counter()
agent_message.properties.state = "complete"
return Message(**agent_message.model_dump())
except Exception as e:
raise ExceptionWithMessageError(e, agent_message) from e
return Message(**agent_message.model_dump())
165 changes: 147 additions & 18 deletions src/backend/base/langflow/base/tools/component_tool.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
from __future__ import annotations

import asyncio
import re
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

from langchain_core.tools import ToolException
from langchain_core.tools.structured import StructuredTool
from loguru import logger
from pydantic import BaseModel

from langflow.base.tools.constants import TOOL_OUTPUT_NAME
from langflow.io.schema import create_input_schema
from langflow.schema.data import Data
from langflow.schema.message import Message

if TYPE_CHECKING:
from collections.abc import Callable

from langchain_core.callbacks import Callbacks
from langchain_core.tools import BaseTool

from langflow.custom.custom_component.component import Component
from langflow.events.event_manager import EventManager
from langflow.inputs.inputs import InputTypes
from langflow.io import Output
from langflow.schema.content_block import ContentBlock


def _get_input_type(_input: InputTypes):
Expand Down Expand Up @@ -47,10 +53,57 @@ def build_description(component: Component, output: Output) -> str:
return f"{output.method}({args}) - {component.description}"


def send_message_noop(
message: Message,
text: str | None = None, # noqa: ARG001
background_color: str | None = None, # noqa: ARG001
text_color: str | None = None, # noqa: ARG001
icon: str | None = None, # noqa: ARG001
content_blocks: list[ContentBlock] | None = None, # noqa: ARG001
format_type: Literal["default", "error", "warning", "info"] = "default", # noqa: ARG001
id_: str | None = None, # noqa: ARG001
*,
allow_markdown: bool = True, # noqa: ARG001
) -> Message:
"""No-op implementation of send_message."""
return message


def patch_components_send_message(component: Component):
old_send_message = component.send_message
component.send_message = send_message_noop # type: ignore[method-assign, assignment]
return old_send_message


def _patch_send_message_decorator(component, func):
"""Decorator to patch the send_message method of a component.
This is useful when we want to use a component as a tool, but we don't want to
send any messages to the UI. With this only the Component calling the tool
will send messages to the UI.
"""

async def async_wrapper(*args, **kwargs):
original_send_message = component.send_message
component.send_message = send_message_noop
try:
return await func(*args, **kwargs)
finally:
component.send_message = original_send_message

def sync_wrapper(*args, **kwargs):
original_send_message = component.send_message
component.send_message = send_message_noop
try:
return func(*args, **kwargs)
finally:
component.send_message = original_send_message

return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper


def _build_output_function(component: Component, output_method: Callable, event_manager: EventManager | None = None):
def output_function(*args, **kwargs):
# set the component with the arguments
# set functionality was updatedto handle list of components and other values separately
try:
if event_manager:
event_manager.on_build_start(data={"id": component._id})
Expand All @@ -60,10 +113,40 @@ def output_function(*args, **kwargs):
event_manager.on_build_end(data={"id": component._id})
except Exception as e:
raise ToolException(e) from e
else:
return result

return output_function
if isinstance(result, Message):
return result.get_text()
if isinstance(result, Data):
return result.data
if isinstance(result, BaseModel):
return result.model_dump()
return result

return _patch_send_message_decorator(component, output_function)


def _build_output_async_function(
component: Component, output_method: Callable, event_manager: EventManager | None = None
):
async def output_function(*args, **kwargs):
try:
if event_manager:
event_manager.on_build_start(data={"id": component._id})
component.set(*args, **kwargs)
result = await output_method()
if event_manager:
event_manager.on_build_end(data={"id": component._id})
except Exception as e:
raise ToolException(e) from e
if isinstance(result, Message):
return result.get_text()
if isinstance(result, Data):
return result.data
if isinstance(result, BaseModel):
return result.model_dump()
return result

return _patch_send_message_decorator(component, output_function)


def _format_tool_name(name: str):
Expand All @@ -77,7 +160,9 @@ class ComponentToolkit:
def __init__(self, component: Component):
self.component = component

def get_tools(self) -> list[BaseTool]:
def get_tools(
self, tool_name: str | None = None, tool_description: str | None = None, callbacks: Callbacks | None = None
) -> list[BaseTool]:
tools = []
for output in self.component.outputs:
if output.name == TOOL_OUTPUT_NAME:
Expand All @@ -89,23 +174,67 @@ def get_tools(self) -> list[BaseTool]:

output_method: Callable = getattr(self.component, output.method)
args_schema = None
tool_mode_inputs = [_input for _input in self.component.inputs if getattr(_input, "tool_mode", False)]
if output.required_inputs:
inputs = [self.component._inputs[input_name] for input_name in output.required_inputs]
inputs = [
self.component._inputs[input_name]
for input_name in output.required_inputs
if getattr(self.component, input_name) is None
]
# If any of the required inputs are not in tool mode, this means
# that when the tool is called it will raise an error.
# so we should raise an error here.
if not all(getattr(_input, "tool_mode", False) for _input in inputs):
non_tool_mode_inputs = [
input_.name
for input_ in inputs
if not getattr(input_, "tool_mode", False) and input_.name is not None
]
non_tool_mode_inputs_str = ", ".join(non_tool_mode_inputs)
msg = (
f"Output '{output.name}' requires inputs that are not in tool mode. "
f"The following inputs are not in tool mode: {non_tool_mode_inputs_str}. "
"Please ensure all required inputs are set to tool mode."
)
raise ValueError(msg)
args_schema = create_input_schema(inputs)
elif tool_mode_inputs:
args_schema = create_input_schema(tool_mode_inputs)
else:
args_schema = create_input_schema(self.component.inputs)
name = f"{self.component.name}.{output.method}"
formatted_name = _format_tool_name(name)
tools.append(
StructuredTool(
name=formatted_name,
description=build_description(component=self.component, output=output),
func=_build_output_function(
component=self.component,
output_method=output_method,
event_manager=self.component._event_manager,
),
args_schema=args_schema,
event_manager = self.component._event_manager
if asyncio.iscoroutinefunction(output_method):
tools.append(
StructuredTool(
name=formatted_name,
description=build_description(self.component, output),
coroutine=_build_output_async_function(self.component, output_method, event_manager),
args_schema=args_schema,
handle_tool_error=True,
callbacks=callbacks,
)
)
else:
tools.append(
StructuredTool(
name=formatted_name,
description=build_description(self.component, output),
func=_build_output_function(self.component, output_method, event_manager),
args_schema=args_schema,
handle_tool_error=True,
callbacks=callbacks,
)
)
if len(tools) == 1 and (tool_name or tool_description):
tool = tools[0]
tool.name = tool_name or tool.name
tool.description = tool_description or tool.description
elif tool_name or tool_description:
msg = (
"When passing a tool name or description, there must be only one tool, "
f"but {len(tools)} tools were found."
)
raise ValueError(msg)
return tools
1 change: 1 addition & 0 deletions src/backend/base/langflow/base/tools/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
TOOL_OUTPUT_NAME = "component_as_tool"
TOOL_OUTPUT_DISPLAY_NAME = "Toolset"
Loading

0 comments on commit cb61de5

Please sign in to comment.