diff --git a/src/backend/base/langflow/base/tools/constants.py b/src/backend/base/langflow/base/tools/constants.py index cfc1bcf32..539ca94be 100644 --- a/src/backend/base/langflow/base/tools/constants.py +++ b/src/backend/base/langflow/base/tools/constants.py @@ -38,3 +38,5 @@ TOOL_TABLE_SCHEMA = [ ] TOOLS_METADATA_INFO = "Modify tool names and descriptions to help agents understand when to use each tool." + +TOOL_UPDATE_CONSTANTS = ["tool_mode", "tool_actions", TOOLS_METADATA_INPUT_NAME, "flow_name_selected"] diff --git a/src/backend/base/langflow/base/tools/run_flow.py b/src/backend/base/langflow/base/tools/run_flow.py index d13aa4520..1bd29b36a 100644 --- a/src/backend/base/langflow/base/tools/run_flow.py +++ b/src/backend/base/langflow/base/tools/run_flow.py @@ -2,8 +2,8 @@ from abc import abstractmethod from typing import TYPE_CHECKING from loguru import logger +from typing_extensions import override -from langflow.base.tools.constants import TOOLS_METADATA_INPUT_NAME from langflow.custom import Component from langflow.custom.custom_component.component import _get_component_toolkit from langflow.field_typing import Tool @@ -36,7 +36,6 @@ class RunFlowBaseComponent(Component): info="The name of the flow to run.", options=[], real_time_refresh=True, - refresh_button=True, value=None, ), MessageInput( @@ -201,12 +200,13 @@ class RunFlowBaseComponent(Component): field.input_types = [] return fields - async def to_toolkit(self) -> list[Tool]: + @override + async def _get_tools(self) -> list[Tool]: component_toolkit: type[ComponentToolkit] = _get_component_toolkit() flow_description, tool_mode_inputs = await self.get_required_data(self.flow_name_selected) # # convert list of dicts to list of dotdicts tool_mode_inputs = [dotdict(field) for field in tool_mode_inputs] - tools = component_toolkit(component=self).get_tools( + return component_toolkit(component=self).get_tools( tool_name=f"{self.flow_name_selected}_tool", tool_description=( f"Tool designed to execute the flow '{self.flow_name_selected}'. Flow details: {flow_description}." @@ -214,6 +214,3 @@ class RunFlowBaseComponent(Component): callbacks=self.get_langchain_callbacks(), flow_mode_inputs=tool_mode_inputs, ) - if hasattr(self, TOOLS_METADATA_INPUT_NAME): - tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools) - return tools diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 44e7a3ef2..7b9286e88 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -49,6 +49,7 @@ if TYPE_CHECKING: from langflow.graph.edge.schema import EdgeData from langflow.graph.vertex.base import Vertex from langflow.inputs.inputs import InputTypes + from langflow.schema.dataframe import DataFrame from langflow.schema.log import LoggableType @@ -1050,12 +1051,94 @@ class Component(CustomComponent): return Input(**kwargs) async def to_toolkit(self) -> list[Tool]: - component_toolkit: type[ComponentToolkit] = _get_component_toolkit() - tools = component_toolkit(component=self).get_tools(callbacks=self.get_langchain_callbacks()) + """Convert component to a list of tools. + + This is a template method that defines the skeleton of the toolkit creation + algorithm. Subclasses can override _get_tools() to provide custom tool + implementations while maintaining the metadata update functionality. + + Returns: + list[Tool]: A list of tools with updated metadata. Each tool contains: + - name: The name of the tool + - description: A description of what the tool does + - tags: List of tags associated with the tool + """ + # Get tools from subclass implementation + tools = await self._get_tools() + if hasattr(self, TOOLS_METADATA_INPUT_NAME): - tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools) + return self._update_tools_with_metadata(tools, self.tools_metadata) return tools + async def _get_tools(self) -> list[Tool]: + """Get the list of tools for this component. + + This method can be overridden by subclasses to provide custom tool implementations. + The default implementation uses ComponentToolkit. + + Returns: + list[Tool]: List of tools provided by this component + """ + component_toolkit: type[ComponentToolkit] = _get_component_toolkit() + return component_toolkit(component=self).get_tools(callbacks=self.get_langchain_callbacks()) + + def _extract_tools_tags(self, tools_metadata: list[dict]) -> list[str]: + """Extract the first tag from each tool's metadata.""" + return [tool["tags"][0] for tool in tools_metadata if tool["tags"]] + + def _update_tools_with_metadata(self, tools: list[Tool], metadata: DataFrame | None) -> list[Tool]: + """Update tools with provided metadata.""" + component_toolkit: type[ComponentToolkit] = _get_component_toolkit() + return component_toolkit(component=self, metadata=metadata).update_tools_metadata(tools=tools) + + def check_for_tool_tag_change(self, old_tags: list[str], new_tags: list[str]) -> bool: + return old_tags != new_tags + + async def _build_tools_metadata_input(self): + tools = await self.to_toolkit() + # Always use the latest tool data + tool_data = [{"name": tool.name, "description": tool.description, "tags": tool.tags} 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): + self.tools_metadata = tool_data + else: + tool_data = self.tools_metadata + else: + self.tools_metadata = tool_data + try: + from langflow.io import TableInput + except ImportError as e: + msg = "Failed to import TableInput from langflow.io" + raise ImportError(msg) from e + + return TableInput( + name=TOOLS_METADATA_INPUT_NAME, + display_name="Edit tools", + real_time_refresh=True, + table_schema=TOOL_TABLE_SCHEMA, + value=tool_data, + table_icon="Hammer", + trigger_icon="Hammer", + trigger_text="", + table_options=TableOptions( + block_add=True, + block_delete=True, + block_edit=True, + 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): if hasattr(self, "_tracing_service") and self._tracing_service: return self._tracing_service.project_name @@ -1267,45 +1350,6 @@ class Component(CustomComponent): def _build_tool_output(self) -> Output: return Output(name=TOOL_OUTPUT_NAME, display_name=TOOL_OUTPUT_DISPLAY_NAME, method="to_toolkit", types=["Tool"]) - async def _build_tools_metadata_input(self): - tools = await self.to_toolkit() - tool_data = ( - self.tools_metadata - if hasattr(self, TOOLS_METADATA_INPUT_NAME) - else [{"name": tool.name, "description": tool.description, "tags": tool.tags} for tool in tools] - ) - try: - from langflow.io import TableInput - except ImportError as e: - msg = "Failed to import TableInput from langflow.io" - raise ImportError(msg) from e - - return TableInput( - name=TOOLS_METADATA_INPUT_NAME, - display_name="Edit tools", - real_time_refresh=True, - table_schema=TOOL_TABLE_SCHEMA, - value=tool_data, - table_icon="Hammer", - trigger_icon="Hammer", - trigger_text="", - table_options=TableOptions( - block_add=True, - block_delete=True, - block_edit=True, - 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_input_display_name(self, input_name: str) -> str: """Get the display name of an input. diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx index ec5925706..cf584cd31 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx @@ -4,7 +4,7 @@ import { FormatColumns, generateBackendColumnsFromValue } from "@/utils/utils"; import { DataTypeDefinition, SelectionChangedEvent } from "ag-grid-community"; import { AgGridReact } from "ag-grid-react"; import { cloneDeep } from "lodash"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { ForwardedIconComponent } from "../../../../common/genericIconComponent"; import { Button } from "../../../../ui/button"; import { InputProps, TableComponentType } from "../../types"; @@ -68,6 +68,11 @@ export default function TableNodeComponent({ const [tempValue, setTempValue] = useState(cloneDeep(value)); const [isModalOpen, setIsModalOpen] = useState(false); const agGrid = useRef(null); + // Add useEffect to sync with incoming value changes + useEffect(() => { + setTempValue(cloneDeep(value)); + }, [value]); + const componentColumns = columns ? columns : generateBackendColumnsFromValue(tempValue ?? [], table_options);