feat: new tool mode dialog and UI (#7647)

* Add tools input to the backend

* Add tools type

* Add tools component to parameter render

* Added tools to langflow supported types

* Add tools modal

* Instantiate tools component in parameter render

* Removed div when there is not any visible actions

* Added margin

* Add ComboBoxItem and ListItem components for enhanced selection UI

* Update ComboBoxItem to display item description and enhance ToolsModal layout

* Refactor ToolsModal header styling for improved layout and icon padding

* Enhance ComboBoxItem layout and styling; update ToolsModal size and class for better responsiveness

* Changed display name

* Adds truncate for badges

* Adds custom styling for table used for Tools modal

* Added Tools modal with AgGrid

* Changed button

* made name and description editable

* Parse values for saving

* Add focused row without triggering checkbox click

* [autofix.ci] apply automated fixes

* Added types for tools modal

* added toolsTable with sidebar on toolsModal

* Added changes to work with MCP

* update component.py to add display name and display description in tool mode

* removed editing directly

* Fixed editing

* removed to upper case

* Make editing apply filters

* Adds design changes for MCP

* Adds new design and null check

* 📝 (frontend): add data-testid attribute to elements for testing purposes
🔧 (frontend): update data-testid attribute values for consistency and clarity in testing
✅ (frontend): update tests to use correct selectors and improve test coverage for editing tools functionality

* ✅ (edit-tools.spec.ts): update test to use a more reliable method for checking visibility of an element

* ✅ (edit-tools.spec.ts): add "@components" tag to the test to categorize it under components for better organization and filtering in test suites.

* Updated design with new design

* update padding

* send args to tools data

* Implemented showing arguments passed to LLM in tool mode

* add componentg name to description

* update package lock

* fixed tests

* fixed backend test

* fixed backend test

* fixed formatting

* Fixed frontend tests

* updated font sizes for badges on actions and styling on sortable list

* Update tool mode design

* added tooltips for info

* tool name update

* Update component_tool.py

* styling utils

* default values change

* fixed tools test

* fix format issues

---------

Co-authored-by: deon-sanchez <deon.sanchez@datastax.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2025-04-28 20:24:48 -03:00 • committed by GitHub
commit b1217f96fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1045 additions and 188 deletions

View file

@ -273,12 +273,14 @@ class ComposioBaseComponent(Component):
def configure_tools(self, toolset: ComposioToolSet) -> list[Tool]:
tools = toolset.get_tools(actions=self._actions_data.keys())
logger.info(f"Tools: {tools}")
configured_tools = []
for tool in tools:
# Set the sanitized name
tool.name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
display_name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
# Set the tags
tool.tags = [tool.name]
tool.metadata = {"display_name": display_name, "display_description": tool.description}
configured_tools.append(tool)
return configured_tools

View file

@ -42,19 +42,19 @@ def _get_input_type(input_: InputTypes):
def build_description(component: Component, output: Output) -> str:
if not output.required_inputs:
logger.warning(f"Output {output.name} does not have required inputs defined")
name = component.name or component.__class__.__name__
if output.required_inputs:
args = ", ".join(
sorted(
[
f"{input_name}: {_get_input_type(component._inputs[input_name])}"
f"{name}. {input_name}: {_get_input_type(component._inputs[input_name])}"
for input_name in output.required_inputs
]
)
)
else:
args = ""
return f"{output.method}({args}) - {component.description}"
return f"{name}. {output.method}({args}) - {component.description}"
async def send_message_noop(
@ -239,7 +239,7 @@ class ComponentToolkit:
else:
args_schema = create_input_schema(self.component.inputs)
name = f"{self.component.name or self.component.__class__.__name__ or ''}.{output.method}".strip(".")
name = f"{output.method}".strip(".")
formatted_name = _format_tool_name(name)
event_manager = self.component._event_manager
if asyncio.iscoroutinefunction(output_method):
@ -252,6 +252,10 @@ class ComponentToolkit:
handle_tool_error=True,
callbacks=callbacks,
tags=[formatted_name],
metadata={
"display_name": formatted_name,
"display_description": build_description(self.component, output),
},
)
)
else:
@ -264,6 +268,10 @@ class ComponentToolkit:
handle_tool_error=True,
callbacks=callbacks,
tags=[formatted_name],
metadata={
"display_name": formatted_name,
"display_description": build_description(self.component, output),
},
)
)
if len(tools) == 1 and (tool_name or tool_description):

View file

@ -394,6 +394,6 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
def set_default_tools(self):
self._default_tools = {
self.sanitize_action_name("GMAIL_SEND_EMAIL").replace(" ", "-"),
self.sanitize_action_name("GMAIL_FETCH_EMAILS").replace(" ", "-"),
"GMAIL_SEND_EMAIL",
"GMAIL_FETCH_EMAILS",
}

View file

@ -581,6 +581,6 @@ class ComposioSlackAPIComponent(ComposioBaseComponent):
def set_default_tools(self):
self._default_tools = {
self.sanitize_action_name("SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL").replace(" ", "-"),
self.sanitize_action_name("SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY").replace(" ", "-"),
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY",
}

View file

@ -18,7 +18,6 @@ from pydantic import BaseModel, ValidationError
from langflow.base.tools.constants import (
TOOL_OUTPUT_DISPLAY_NAME,
TOOL_OUTPUT_NAME,
TOOL_TABLE_SCHEMA,
TOOLS_METADATA_INFO,
TOOLS_METADATA_INPUT_NAME,
)
@ -33,7 +32,6 @@ from langflow.schema.artifact import get_artifact_type, post_process_raw
from langflow.schema.data import Data
from langflow.schema.message import ErrorMessage, Message
from langflow.schema.properties import Source
from langflow.schema.table import FieldParserType, TableOptions
from langflow.services.tracing.schema import Log
from langflow.template.field.base import UNDEFINED, Input, Output
from langflow.template.frontend_node.custom_components import ComponentFrontendNode
@ -1174,10 +1172,14 @@ class Component(CustomComponent):
"description": tool.description,
"tags": tool.tags if hasattr(tool, "tags") and tool.tags else [tool.name],
"status": True, # Initialize all tools with status True
"display_name": tool.metadata.get("display_name", tool.name),
"display_description": tool.metadata.get("display_description", tool.description),
"args": tool.args,
# "args_schema": tool.args_schema,
}
for tool in tools
]
# print(tool_data)
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
old_tags = self._extract_tools_tags(self.tools_metadata)
new_tags = self._extract_tools_tags(tool_data)
@ -1203,35 +1205,16 @@ class Component(CustomComponent):
self.tools_metadata = tool_data
try:
from langflow.io import TableInput
from langflow.io import ToolsInput
except ImportError as e:
msg = "Failed to import TableInput from langflow.io"
msg = "Failed to import ToolsInput from langflow.io"
raise ImportError(msg) from e
return TableInput(
return ToolsInput(
name=TOOLS_METADATA_INPUT_NAME,
display_name="Edit tools",
real_time_refresh=True,
table_schema=TOOL_TABLE_SCHEMA,
display_name="Actions",
info=TOOLS_METADATA_INFO,
value=tool_data,
table_icon="Hammer",
trigger_icon="Hammer",
trigger_text="",
table_options=TableOptions(
block_add=True,
block_delete=True,
block_edit=True, # Allow editing for status toggle
block_sort=True,
block_filter=True,
block_hide=True,
block_select=True,
hide_options=True,
field_parsers={
"name": [FieldParserType.SNAKE_CASE, FieldParserType.NO_BLANK],
"commands": FieldParserType.COMMANDS,
},
description=TOOLS_METADATA_INFO,
),
)
def get_project_name(self):

View file

@ -242,7 +242,7 @@ class ParameterHandler:
params[field_name] = val
case str():
params[field_name] = bool(val)
case "table":
case "table" | "tools":
if isinstance(val, list) and all(isinstance(item, dict) for item in val):
params[field_name] = pd.DataFrame(val)
else:

View file

@ -28,6 +28,7 @@ from .inputs import (
StrInput,
TabInput,
TableInput,
ToolsInput,
)
__all__ = [
@ -62,4 +63,5 @@ __all__ = [
"StrInput",
"TabInput",
"TableInput",
"ToolsInput",
]

View file

@ -36,6 +36,7 @@ class FieldTypes(str, Enum):
SLIDER = "slider"
TAB = "tab"
QUERY = "query"
TOOLS = "tools"
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]

View file

@ -87,6 +87,21 @@ class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin):
field_type: SerializableFieldTypes = FieldTypes.OTHER
class ToolsInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolModeMixin):
"""Represents an Input that contains a list of tools to activate, deactivate, or edit.
Attributes:
field_type (SerializableFieldTypes): The field type of the input.
value (list[dict]): The value of the input.
"""
field_type: SerializableFieldTypes = FieldTypes.TOOLS
value: list[dict] = Field(default_factory=list)
is_list: bool = True
real_time_refresh: bool = True
class DataInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin):
"""Represents an Input that has a Handle that receives a Data object.
@ -644,6 +659,7 @@ InputTypes: TypeAlias = (
| MultilineInput
| MultilineSecretInput
| NestedDictInput
| ToolsInput
| PromptInput
| CodeInput
| SecretStrInput

View file

@ -25,6 +25,7 @@ from langflow.inputs import (
StrInput,
TabInput,
TableInput,
ToolsInput,
)
from langflow.template import Output
@ -57,4 +58,5 @@ __all__ = [
"StrInput",
"TabInput",
"TableInput",
"ToolsInput",
]

View file

@ -69,6 +69,7 @@ DIRECT_TYPES = [
"auth",
"connect",
"query",
"tools",
]

View file

@ -15,7 +15,7 @@ def test_component_tool():
calculator_component = CalculatorToolComponent()
component_toolkit = ComponentToolkit(component=calculator_component)
component_tool = component_toolkit.get_tools()[0]
assert component_tool.name == "CalculatorTool-run_model"
assert component_tool.name == "run_model"
assert issubclass(component_tool.args_schema, BaseModel)
# TODO: fix this
# assert component_tool.args_schema.model_json_schema()["properties"] == {

View file

@ -122,7 +122,7 @@ def test_component_inputs_toolkit():
component = AllInputsComponent()
component_toolkit = ComponentToolkit(component=component)
component_tool = component_toolkit.get_tools()[0]
assert component_tool.name == "AllInputsComponent-build_output"
assert component_tool.name == "build_output"
assert issubclass(component_tool.args_schema, BaseModel)
properties = component_tool.args_schema.model_json_schema()["properties"]