feat: add tool filter (#6951)

* updates to tool mode

* fix the update real time table refresh issues.

* Update src/backend/base/langflow/custom/custom_component/component.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Update component.py

* Update component.py

* format errors

* [autofix.ci] apply automated fixes

* Update index.tsx

* [autofix.ci] apply automated fixes

* lint error fix

* [autofix.ci] apply automated fixes

* fix format issue

* [autofix.ci] apply automated fixes

* fix typing.override does not exist in your Python version

* filter added

* Update index.tsx

* [autofix.ci] apply automated fixes

* update variables

* modify tool filter

* added dynamic property to set the tools in a better effective way

* filter with property

* lint fix

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Edwin Jose 2025-03-11 12:19:10 -04:00 committed by GitHub
commit 5a13f32da5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 100 additions and 13 deletions

View file

@ -306,6 +306,7 @@ class ComponentToolkit:
# update the tool_name and description according to the name and secriotion mentioned in the list
if isinstance(self.metadata, pd.DataFrame):
metadata_dict = self.get_tools_metadata_dictionary()
filtered_tools = []
for tool in tools:
if isinstance(tool, StructuredTool | BaseTool) and tool.tags:
try:
@ -315,13 +316,17 @@ class ComponentToolkit:
raise ValueError(msg) from None
if tag in metadata_dict:
tool_metadata = metadata_dict[tag]
tool.name = tool_metadata.get("name", tool.name)
tool.description = tool_metadata.get("description", tool.description)
if tool_metadata.get("commands"):
tool.description = _add_commands_to_tool_description(
tool.description, tool_metadata.get("commands")
)
# Only include tools with status=True
if tool_metadata.get("status", False):
tool.name = tool_metadata.get("name", tool.name)
tool.description = tool_metadata.get("description", tool.description)
if tool_metadata.get("commands"):
tool.description = _add_commands_to_tool_description(
tool.description, tool_metadata.get("commands")
)
filtered_tools.append(tool)
else:
msg = f"Expected a StructuredTool or BaseTool, got {type(tool)}"
raise TypeError(msg)
return filtered_tools
return tools

View file

@ -35,6 +35,13 @@ TOOL_TABLE_SCHEMA = [
"edit_mode": EditMode.INLINE,
"hidden": True,
},
{
"name": "status",
"display_name": "Enable",
"type": "boolean",
"description": "Indicates whether the tool is currently active. Set to True to activate this tool.",
"default": True,
},
]
TOOLS_METADATA_INFO = "Modify tool names and descriptions to help agents understand when to use each tool."

View file

@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, get_type_hints
from uuid import UUID
import nanoid
import pandas as pd
import yaml
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, ValidationError
@ -162,6 +163,20 @@ class Component(CustomComponent):
self.set_class_code()
self._set_output_required_inputs()
@property
def enabled_tools(self) -> list[str] | None:
"""Dynamically determine which tools should be enabled.
This property can be overridden by subclasses to provide custom tool filtering.
By default, it returns None, which means all tools are enabled.
Returns:
list[str] | None: List of tool names or tags to enable, or None to enable all tools.
"""
# Default implementation returns None (all tools enabled)
# Subclasses can override this to provide custom filtering
return None
def _there_is_overlap_in_inputs_and_outputs(self) -> set[str]:
"""Check the `.name` of inputs and outputs to see if there is overlap.
@ -1067,8 +1082,11 @@ class Component(CustomComponent):
tools = await self._get_tools()
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
return self._update_tools_with_metadata(tools, self.tools_metadata)
return tools
tools = self._filter_tools_by_status(tools=tools, metadata=self.tools_metadata)
return self._update_tools_with_metadata(tools=tools, metadata=self.tools_metadata)
# If no metadata exists yet, filter based on enabled_tools
return self._filter_tools_by_status(tools=tools, metadata=None)
async def _get_tools(self) -> list[Tool]:
"""Get the list of tools for this component.
@ -1094,19 +1112,77 @@ class Component(CustomComponent):
def check_for_tool_tag_change(self, old_tags: list[str], new_tags: list[str]) -> bool:
return old_tags != new_tags
def _filter_tools_by_status(self, tools: list[Tool], metadata: pd.DataFrame | None) -> list[Tool]:
"""Filter tools based on their status in metadata.
Args:
tools (list[Tool]): List of tools to filter.
metadata (list[dict] | None): Tools metadata containing status information.
Returns:
list[Tool]: Filtered list of tools.
"""
# Convert metadata to a list of dicts if it's a DataFrame
metadata_dict = None
if isinstance(metadata, pd.DataFrame):
metadata_dict = metadata.to_dict(orient="records")
# If metadata is None or empty, use enabled_tools
if not metadata_dict:
enabled = self.enabled_tools
return (
tools
if enabled is None
else [
tool for tool in tools if any(enabled_name in [tool.name, *tool.tags] for enabled_name in enabled)
]
)
# Ensure metadata is a list of dicts
if not isinstance(metadata_dict, list):
return tools
# Create a mapping of tool names to their status
tool_status = {item["name"]: item.get("status", True) for item in metadata_dict}
return [tool for tool in tools if tool_status.get(tool.name, True)]
async def _build_tools_metadata_input(self):
tools = await self.to_toolkit()
tools = await self._get_tools()
# Always use the latest tool data
tool_data = [{"name": tool.name, "description": tool.description, "tags": tool.tags} for tool in tools]
tool_data = [
{
"name": tool.name,
"description": tool.description,
"tags": tool.tags,
"status": True, # Initialize all tools with status True
}
for tool in tools
]
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
old_tags = self._extract_tools_tags(self.tools_metadata)
new_tags = self._extract_tools_tags(tool_data)
if self.check_for_tool_tag_change(old_tags, new_tags):
# If enabled tools are set, update status based on them
enabled = self.enabled_tools
if enabled is not None:
for item in tool_data:
item["status"] = any(enabled_name in [item["name"], *item["tags"]] for enabled_name in enabled)
self.tools_metadata = tool_data
else:
# Preserve existing status values
existing_status = {item["name"]: item.get("status", True) for item in self.tools_metadata}
for item in tool_data:
item["status"] = existing_status.get(item["name"], True)
tool_data = self.tools_metadata
else:
# If enabled tools are set, update status based on them
enabled = self.enabled_tools
if enabled is not None:
for item in tool_data:
item["status"] = any(enabled_name in [item["name"], *item["tags"]] for enabled_name in enabled)
self.tools_metadata = tool_data
try:
from langflow.io import TableInput
except ImportError as e:
@ -1125,7 +1201,7 @@ class Component(CustomComponent):
table_options=TableOptions(
block_add=True,
block_delete=True,
block_edit=True,
block_edit=True, # Allow editing for status toggle
block_sort=True,
block_filter=True,
block_hide=True,

View file

@ -146,8 +146,7 @@ export default function TableNodeComponent({
const isCustomEdit =
column.formatter &&
((column.formatter === "text" && column.edit_mode === "modal") ||
column.formatter === "json" ||
column.formatter === "boolean");
column.formatter === "json");
return {
field: column.name,
onUpdate: updateComponent,