diff --git a/src/backend/base/langflow/base/composio/composio_base.py b/src/backend/base/langflow/base/composio/composio_base.py
index 3ca7da3ad..cb847bea1 100644
--- a/src/backend/base/langflow/base/composio/composio_base.py
+++ b/src/backend/base/langflow/base/composio/composio_base.py
@@ -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
diff --git a/src/backend/base/langflow/base/tools/component_tool.py b/src/backend/base/langflow/base/tools/component_tool.py
index b8dab8f96..29be7a309 100644
--- a/src/backend/base/langflow/base/tools/component_tool.py
+++ b/src/backend/base/langflow/base/tools/component_tool.py
@@ -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):
diff --git a/src/backend/base/langflow/components/composio/gmail_composio.py b/src/backend/base/langflow/components/composio/gmail_composio.py
index a6f0c9950..01bb8296d 100644
--- a/src/backend/base/langflow/components/composio/gmail_composio.py
+++ b/src/backend/base/langflow/components/composio/gmail_composio.py
@@ -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",
}
diff --git a/src/backend/base/langflow/components/composio/slack_composio.py b/src/backend/base/langflow/components/composio/slack_composio.py
index fc97b2bd1..80d45ebf3 100644
--- a/src/backend/base/langflow/components/composio/slack_composio.py
+++ b/src/backend/base/langflow/components/composio/slack_composio.py
@@ -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",
}
diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py
index cf8220738..faa274a1b 100644
--- a/src/backend/base/langflow/custom/custom_component/component.py
+++ b/src/backend/base/langflow/custom/custom_component/component.py
@@ -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):
diff --git a/src/backend/base/langflow/graph/vertex/param_handler.py b/src/backend/base/langflow/graph/vertex/param_handler.py
index cdaabb3ad..7ed0c1de7 100644
--- a/src/backend/base/langflow/graph/vertex/param_handler.py
+++ b/src/backend/base/langflow/graph/vertex/param_handler.py
@@ -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:
diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py
index 92cd8841f..d6d1f8339 100644
--- a/src/backend/base/langflow/inputs/__init__.py
+++ b/src/backend/base/langflow/inputs/__init__.py
@@ -28,6 +28,7 @@ from .inputs import (
StrInput,
TabInput,
TableInput,
+ ToolsInput,
)
__all__ = [
@@ -62,4 +63,5 @@ __all__ = [
"StrInput",
"TabInput",
"TableInput",
+ "ToolsInput",
]
diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py
index 2e05590f3..8806336bd 100644
--- a/src/backend/base/langflow/inputs/input_mixin.py
+++ b/src/backend/base/langflow/inputs/input_mixin.py
@@ -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)]
diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py
index e9f3f5cd1..c10171906 100644
--- a/src/backend/base/langflow/inputs/inputs.py
+++ b/src/backend/base/langflow/inputs/inputs.py
@@ -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
diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py
index 86339e7de..f32bc3820 100644
--- a/src/backend/base/langflow/io/__init__.py
+++ b/src/backend/base/langflow/io/__init__.py
@@ -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",
]
diff --git a/src/backend/base/langflow/utils/constants.py b/src/backend/base/langflow/utils/constants.py
index 81027b78a..c194b2a67 100644
--- a/src/backend/base/langflow/utils/constants.py
+++ b/src/backend/base/langflow/utils/constants.py
@@ -69,6 +69,7 @@ DIRECT_TYPES = [
"auth",
"connect",
"query",
+ "tools",
]
diff --git a/src/backend/tests/unit/base/tools/test_component_toolkit.py b/src/backend/tests/unit/base/tools/test_component_toolkit.py
index bf8df7a18..61552a998 100644
--- a/src/backend/tests/unit/base/tools/test_component_toolkit.py
+++ b/src/backend/tests/unit/base/tools/test_component_toolkit.py
@@ -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"] == {
diff --git a/src/backend/tests/unit/base/tools/test_toolmodemixin.py b/src/backend/tests/unit/base/tools/test_toolmodemixin.py
index 27518360c..b837a1508 100644
--- a/src/backend/tests/unit/base/tools/test_toolmodemixin.py
+++ b/src/backend/tests/unit/base/tools/test_toolmodemixin.py
@@ -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"]
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 27f1c1eb7..bb1b03b1e 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "langflow",
- "version": "1.3.3",
+ "version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "langflow",
- "version": "1.3.3",
+ "version": "1.3.4",
"dependencies": {
"@chakra-ui/number-input": "^2.1.2",
"@headlessui/react": "^2.0.4",
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx
new file mode 100644
index 000000000..eb9eac75e
--- /dev/null
+++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx
@@ -0,0 +1,64 @@
+import { Button } from "@/components/ui/button";
+import { useState } from "react";
+
+interface ComboBoxItemProps {
+ item: any;
+}
+
+const ComboBoxItem = ({ item }: ComboBoxItemProps) => {
+ const [isChecked, setIsChecked] = useState(false);
+
+ return (
+
+
+
+ setIsChecked(!isChecked)}
+ className="peer h-5 w-5 cursor-pointer appearance-none rounded border border-border border-muted-foreground shadow transition-all hover:shadow-md"
+ id={`check-${item?.name}`}
+ />
+
+
+
+
+
+
+
+
+ {item?.name}
+
+
+
+ {item?.description}
+
+
+
+
+ );
+};
+export default ComboBoxItem;
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx
new file mode 100644
index 000000000..720910866
--- /dev/null
+++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx
@@ -0,0 +1,118 @@
+import ForwardedIconComponent from "@/components/common/genericIconComponent";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/utils/utils";
+import { useEffect, useRef, useState } from "react";
+
+const ListItem = ({
+ item,
+ isSelected,
+ onClick,
+ className,
+ onMouseEnter,
+ onMouseLeave,
+ isFocused,
+ isKeyboardNavActive,
+ dataTestId,
+}: {
+ item: any;
+ isSelected: boolean;
+ onClick: () => void;
+ className?: string;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+ isFocused: boolean;
+ isKeyboardNavActive: boolean;
+ dataTestId: string;
+}) => {
+ const [isHovered, setIsHovered] = useState(false);
+ const itemRef = useRef(null);
+
+ // Clear hover state when keyboard navigation is active
+ useEffect(() => {
+ if (isKeyboardNavActive) {
+ setIsHovered(false);
+ }
+ }, [isKeyboardNavActive]);
+
+ // Scroll into view when focused by keyboard
+ useEffect(() => {
+ if (isFocused && itemRef.current) {
+ itemRef.current.scrollIntoView({ block: "nearest" });
+ }
+ }, [isFocused]);
+
+ return (
+ {
+ if (!isKeyboardNavActive) {
+ setIsHovered(true);
+ onMouseEnter();
+ }
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ onMouseLeave();
+ }}
+ // Disable pointer events during keyboard navigation
+ style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
+ >
+
+ {item.icon && (
+
+
+
+ )}
+
+
+ {item.name}
+
+ {"metaData" in item && item.metaData && (
+
+ {item.metaData}
+
+ )}
+
+
+ {isHovered || isFocused ? (
+
+ ) : (
+ // Always show the check icon when selected, regardless of hover/focus state
+ isSelected && (
+
+ )
+ )}
+
+
+ );
+};
+
+export default ListItem;
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx
index 2bef4121b..1f7af104e 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx
@@ -1,12 +1,11 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
-import ShadTooltip from "@/components/common/shadTooltipComponent";
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
import { InputProps } from "@/components/core/parameterRenderComponent/types";
-import { Button } from "@/components/ui/button";
import { DialogHeader } from "@/components/ui/dialog";
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
-import { cn, testIdCase } from "@/utils/utils";
+import { testIdCase } from "@/utils/utils";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import ListItem from "./ListItem";
// Update interface with better types
interface ListSelectionComponentProps {
@@ -20,118 +19,6 @@ interface ListSelectionComponentProps {
limit?: number;
}
-const ListItem = ({
- item,
- isSelected,
- onClick,
- className,
- onMouseEnter,
- onMouseLeave,
- isFocused,
- isKeyboardNavActive,
- dataTestId,
-}: {
- item: any;
- isSelected: boolean;
- onClick: () => void;
- className?: string;
- onMouseEnter: () => void;
- onMouseLeave: () => void;
- isFocused: boolean;
- isKeyboardNavActive: boolean;
- dataTestId: string;
-}) => {
- const [isHovered, setIsHovered] = useState(false);
- const itemRef = useRef(null);
-
- // Clear hover state when keyboard navigation is active
- useEffect(() => {
- if (isKeyboardNavActive) {
- setIsHovered(false);
- }
- }, [isKeyboardNavActive]);
-
- // Scroll into view when focused by keyboard
- useEffect(() => {
- if (isFocused && itemRef.current) {
- itemRef.current.scrollIntoView({ block: "nearest" });
- }
- }, [isFocused]);
-
- return (
- {
- if (!isKeyboardNavActive) {
- setIsHovered(true);
- onMouseEnter();
- }
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- onMouseLeave();
- }}
- // Disable pointer events during keyboard navigation
- style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
- >
-
- {item.icon && (
-
-
-
- )}
-
-
- {item.name}
-
- {"metaData" in item && item.metaData && (
-
- {item.metaData}
-
- )}
-
-
- {isHovered || isFocused ? (
-
- ) : (
- // Always show the check icon when selected, regardless of hover/focus state
- isSelected && (
-
- )
- )}
-
-
- );
-};
-
const ListSelectionComponent = ({
open,
onClose,
@@ -286,7 +173,7 @@ const ListSelectionComponent = ({
name={nodeClass?.icon || "unknown"}
className="h-[18px] w-[18px] text-muted-foreground"
/>
-
+
{nodeClass?.display_name}
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
index c0ab05610..b7e10f289 100644
--- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx
@@ -78,7 +78,11 @@ const RenderInputParameters = ({
(templateField: string, idx) => {
const template = data.node?.template[templateField];
- if (!template?.show || template?.advanced) {
+ if (
+ !template?.show ||
+ template?.advanced ||
+ (template?.tool_mode && isToolMode)
+ ) {
return null;
}
diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx
new file mode 100644
index 000000000..3a6740d51
--- /dev/null
+++ b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx
@@ -0,0 +1,142 @@
+import { ICON_STROKE_WIDTH } from "@/constants/constants";
+import ToolsModal from "@/modals/toolsModal";
+import { cn, testIdCase } from "@/utils/utils";
+import { useState } from "react";
+import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
+import { Badge } from "../../../../ui/badge";
+import { Button } from "../../../../ui/button";
+import { Skeleton } from "../../../../ui/skeleton";
+import { InputProps, ToolsComponentType } from "../../types";
+
+export default function ToolsComponent({
+ description,
+ value,
+ editNode = false,
+ id = "",
+ handleOnNewValue,
+ isAction = false,
+ button_description,
+ title,
+ icon,
+ disabled = false,
+ template,
+}: InputProps): JSX.Element {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const actions = value
+ ?.filter((action) => action.status === true)
+ .map((action) => {
+ return {
+ name: action.name,
+ tags: action.tags,
+ description: action.description,
+ };
+ });
+
+ const visibleActionsQt = isAction ? 20 : 4;
+
+ const visibleActions = actions?.slice(0, visibleActionsQt) || [];
+ const remainingCount = actions
+ ? Math.max(0, actions.length - visibleActionsQt)
+ : 0;
+
+ return (
+
+ {value && (
+
+ )}
+
+ {(visibleActions.length > 0 || isAction) && (
+
setIsModalOpen(true)}
+ >
+
+ {button_description}
+
+ )}
+ {!value ? (
+
+ {[...Array(4)].map((_, index) => (
+
+ ))}
+
+ ) : visibleActions.length > 0 ? (
+
+ {visibleActions.map((action, index) => (
+
+
+ {action.name.toUpperCase()}
+
+
+ ))}
+ {remainingCount > 0 && (
+
+ +{remainingCount} more
+
+ )}
+
+ ) : (
+ visibleActions.length === 0 &&
+ isAction && (
+
+
+ No actions added to this server
+
+ setIsModalOpen(true)}>
+ Add actions
+
+
+ )
+ )}
+
+ {visibleActions.length === 0 && !isAction && value && (
+
setIsModalOpen(true)}
+ >
+ Select actions
+
+ )}
+
+
+ );
+}
diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx
index 8d11c292e..c763c7db0 100644
--- a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx
+++ b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx
@@ -32,27 +32,27 @@ const SortableListItem = memo(
}) => (
{limit !== 1 && (
)}
{limit !== 1 && (
-
+
{index + 1}
)}
@@ -61,22 +61,16 @@ const SortableListItem = memo(
-
+
),
diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx
index 45eda0702..b489961d1 100644
--- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx
+++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx
@@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
import { TEXT_FIELD_TYPES } from "@/constants/constants";
import { APIClassType, InputFieldType } from "@/types/api";
import { useMemo } from "react";
+import ToolsComponent from "./components/ToolsComponent";
import ConnectionComponent from "./components/connectionComponent";
import DictComponent from "./components/dictComponent";
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
@@ -203,6 +204,16 @@ export function ParameterRenderComponent({
table_icon={templateData?.table_icon}
/>
);
+ case "tools":
+ return (
+
+ );
case "slider":
return (
void;
+ open: boolean;
+ handleOnNewValue: handleOnNewValueType;
+ isAction: boolean;
+}) {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [selectedRows, setSelectedRows] = useState(null);
+ const agGrid = useRef(null);
+
+ const [focusedRow, setFocusedRow] = useState(null);
+ const [sidebarName, setSidebarName] = useState("");
+ const [sidebarDescription, setSidebarDescription] = useState("");
+
+ const { setOpen: setSidebarOpen } = useSidebar();
+
+ const getRowId = useMemo(() => {
+ return (params: any) => params.data.display_name ?? params.data.name;
+ }, []);
+
+ useEffect(() => {
+ const initialData = cloneDeep(rows);
+ setData(initialData);
+ const filter = initialData.filter((row) => row.status === true);
+ setSelectedRows(filter);
+ }, [rows, open]);
+
+ useEffect(() => {
+ const initialData = cloneDeep(rows);
+ const filter = initialData.filter((row) => row.status === true);
+ if (agGrid.current) {
+ agGrid.current?.api?.forEachNode((node) => {
+ if (
+ filter.some(
+ (row) =>
+ (row.display_name ?? row.name) ===
+ (node.data.display_name ?? node.data.name),
+ )
+ ) {
+ node.setSelected(true);
+ } else {
+ node.setSelected(false);
+ }
+ });
+ }
+ }, [agGrid.current]);
+
+ useEffect(() => {
+ if (!open && selectedRows) {
+ handleOnNewValue({
+ value: data.map((row) => {
+ const processedValue = (
+ row.name !== ""
+ ? parseString(row.name, ["snake_case", "no_blank", "lowercase"])
+ : parseString(row.display_name, [
+ "snake_case",
+ "no_blank",
+ "lowercase",
+ ])
+ ).slice(0, 46);
+
+ const processedDescription =
+ row.description !== "" ? row.description : row.display_description;
+
+ return selectedRows?.some(
+ (selected) =>
+ (selected.display_name ?? selected.name) ===
+ (row.display_name ?? row.name),
+ )
+ ? {
+ ...row,
+ status: true,
+ name: processedValue,
+ description: processedDescription,
+ }
+ : {
+ ...row,
+ status: false,
+ name: processedValue,
+ description: processedDescription,
+ };
+ }),
+ });
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (focusedRow) {
+ setSidebarName(focusedRow.name);
+ setSidebarDescription(focusedRow.description);
+ } else {
+ setSidebarName("");
+ setSidebarDescription("");
+ }
+ }, [focusedRow]);
+
+ const columnDefs: ColDef[] = [
+ {
+ field: isAction ? "display_name" : "name",
+ headerName: isAction ? "Flow Name" : "Name",
+ flex: 1,
+ valueGetter: (params) =>
+ !isAction
+ ? parseString(
+ params.data.display_name !== ""
+ ? params.data.display_name
+ : params.data.name,
+ ["space_case"],
+ )
+ : params.data.display_name,
+ },
+ {
+ field: "description",
+ headerName: "Description",
+ flex: 2,
+ cellClass: "text-muted-foreground",
+ },
+ {
+ field: isAction ? "name" : "tags",
+ headerName: isAction ? "Action" : "Slug",
+ flex: 1,
+ resizable: false,
+ valueGetter: (params) =>
+ isAction
+ ? params.data.name !== ""
+ ? parseString(params.data.name, [
+ "snake_case",
+ "no_blank",
+ "uppercase",
+ ])
+ : parseString(params.data.display_name, [
+ "snake_case",
+ "no_blank",
+ "uppercase",
+ ])
+ : parseString(params.data.tags.join(", "), [
+ "snake_case",
+ "uppercase",
+ ]),
+ cellClass: "text-muted-foreground",
+ },
+ {
+ field: "tags",
+ headerName: "Tags",
+ flex: 1,
+ hide: true,
+ },
+ ];
+ const handleSelectionChanged = (event) => {
+ if (open) {
+ const selectedData = event.api.getSelectedRows();
+ setSelectedRows(selectedData);
+ }
+ };
+
+ const handleSidebarInputChange = (
+ field: "name" | "description",
+ value: string,
+ ) => {
+ if (!focusedRow) return;
+
+ const originalName = focusedRow.display_name;
+
+ setFocusedRow((prev) => (prev ? { ...prev, [field]: value } : null));
+
+ if (agGrid.current) {
+ const updatedRow = { ...focusedRow, [field]: value };
+
+ agGrid.current.api.applyTransaction({
+ update: [updatedRow],
+ });
+
+ const updatedData = data.map((row) =>
+ (row.display_name ?? row.name) === originalName ? updatedRow : row,
+ );
+ setData(updatedData);
+ }
+ };
+
+ const actionArgs = useMemo(() => {
+ return Object.entries(focusedRow?.args ?? {}).map(
+ ([key, value]: [string, any]) => ({
+ display_name: value.title,
+ name: key,
+ description: value.description ?? null,
+ }),
+ );
+ }, [focusedRow]);
+
+ return (
+ <>
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
{
+ setFocusedRow(event.data);
+ setSidebarOpen(true);
+ }}
+ getRowId={getRowId}
+ />
+
+
+
+
+ {focusedRow &&
+ (isAction ? (
+
+
+
+ Name
+
+
+ {
+ setSidebarName(e.target.value);
+ handleSidebarInputChange("name", e.target.value);
+ }}
+ maxLength={46}
+ placeholder="Edit name..."
+ data-testid="input_update_name"
+ />
+
+ {isAction
+ ? "Used as the function name when this flow is exposed to clients."
+ : "Used as the function name when this tool is exposed to the agent."}
+
+
+
+
+ Description
+
+
+
+
+ ) : (
+
+
+ {parseString(focusedRow?.display_name || focusedRow?.name, [
+ "space_case",
+ ])}
+
+
+ {focusedRow?.display_description ?? focusedRow?.description}
+
+
+ ))}
+
+ {!isAction && }
+
+ {focusedRow && (
+
+
+
+
+ {actionArgs.length > 0 && (
+
+
+ Parameters
+
+
+ Manage inputs for this action
+
+
+ )}
+ {actionArgs.map((field) => (
+
+
+ {field.display_name}
+ {field.description && (
+
+
+
+
+
+ )}
+
+ {}}
+ />
+
+ ))}
+
+
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/frontend/src/modals/toolsModal/index.tsx b/src/frontend/src/modals/toolsModal/index.tsx
new file mode 100644
index 000000000..6b6be7387
--- /dev/null
+++ b/src/frontend/src/modals/toolsModal/index.tsx
@@ -0,0 +1,89 @@
+import ForwardedIconComponent from "@/components/common/genericIconComponent";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
+import { APITemplateType } from "@/types/api";
+import { AgGridReact } from "ag-grid-react";
+import { cloneDeep } from "lodash";
+import { ForwardedRef, forwardRef, useState } from "react";
+import BaseModal from "../baseModal";
+import ToolsTable from "./components/toolsTable";
+
+interface ToolsModalProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ description: string;
+ rows: {
+ name: string;
+ tags: string[];
+ description: string;
+ status: boolean;
+ }[];
+ handleOnNewValue: handleOnNewValueType;
+ title: string;
+ icon?: string;
+ isAction?: boolean;
+ template?: APITemplateType;
+}
+
+const ToolsModal = forwardRef(
+ (
+ {
+ description,
+ rows,
+ handleOnNewValue,
+ template,
+ title,
+ icon,
+ open,
+ isAction = false,
+ setOpen,
+ ...props
+ }: ToolsModalProps,
+ ref: ForwardedRef,
+ ) => {
+ const handleSetOpen = (newOpen: boolean) => {
+ if (setOpen) {
+ setOpen(newOpen);
+ }
+ };
+
+ const [data, setData] = useState(cloneDeep(rows));
+
+ return (
+ {
+ handleSetOpen(newOpen);
+ }}
+ >
+
+
+ {icon && (
+
+ )}
+
{title}
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+export default ToolsModal;
diff --git a/src/frontend/src/style/ag-theme-shadcn.css b/src/frontend/src/style/ag-theme-shadcn.css
index 1e83eb1b0..a538f6cb4 100644
--- a/src/frontend/src/style/ag-theme-shadcn.css
+++ b/src/frontend/src/style/ag-theme-shadcn.css
@@ -14,6 +14,7 @@
--ag-panel-background-color: hsl(var(--accent)) !important;
--ag-row-hover-color: hsl(var(--accent)) !important;
--ag-header-height: 2.5rem !important;
+ --ag-checkbox-checked-color: hsl(var(--primary)) !important;
}
.ag-theme-shadcn .ag-paging-panel {
@@ -33,7 +34,6 @@
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
align-items: self-start;
position: relative;
- top: 2px;
}
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
@@ -99,3 +99,46 @@
box-shadow: none !important;
outline: none !important;
}
+
+.ag-tool-mode .ag-root-wrapper {
+ border: none !important;
+}
+.ag-tool-mode .ag-row {
+ border-bottom: none !important;
+}
+
+.ag-tool-mode .ag-header {
+ margin-bottom: 0rem !important;
+ border-bottom: 0 !important;
+}
+
+.ag-tool-mode .ag-header-cell {
+ padding: 0rem 1rem !important;
+ margin: 1px;
+}
+
+.ag-tool-mode .ag-ltr .ag-header-select-all {
+ margin-right: var(--ag-cell-widget-spacing);
+}
+
+.ag-tool-mode .ag-paging-panel {
+ border-top: none !important;
+}
+
+.ag-tool-mode .ag-cell-focus:not(.ag-cell-inline-editing) {
+ border: 1px solid transparent !important;
+ box-shadow: none !important;
+ outline: none !important;
+}
+
+.ag-tool-mode .ag-cell {
+ padding: 0rem 1rem !important;
+}
+
+.ag-tool-mode .ag-row-focus {
+ background-color: hsl(var(--accent)) !important;
+}
+
+.ag-tool-mode .ag-row-selected:not(.ag-row-focus)::before {
+ background-color: hsl(var(--background)) !important;
+}
diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts
index ebbe5115e..a884f710e 100644
--- a/src/frontend/src/types/api/index.ts
+++ b/src/frontend/src/types/api/index.ts
@@ -324,6 +324,7 @@ export type FieldParserType =
| "uppercase"
| "no_blank"
| "valid_csv"
+ | "space_case"
| "commands";
export type TableOptionsTypeAPI = {
diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts
index fd8ad754a..e0a07f789 100644
--- a/src/frontend/src/types/components/index.ts
+++ b/src/frontend/src/types/components/index.ts
@@ -1,3 +1,4 @@
+import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
import { ReactFlowJsonObject } from "@xyflow/react";
import { ReactElement, ReactNode } from "react";
import { InputOutput } from "../../constants/enums";
@@ -676,6 +677,21 @@ export type textModalPropsType = {
setOpen?: (open: boolean) => void;
onCloseModal?: () => void;
};
+
+export interface ToolsModalProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ description: string;
+ rows: {
+ name: string;
+ tags: string[];
+ description: string;
+ status: boolean;
+ }[];
+ handleOnNewValue: handleOnNewValueType;
+ title: string;
+ icon: string;
+}
export type queryModalPropsType = {
setValue: (value: string) => void;
value: string;
diff --git a/src/frontend/src/utils/stringManipulation.ts b/src/frontend/src/utils/stringManipulation.ts
index fb6608cd5..0dde0aed1 100644
--- a/src/frontend/src/utils/stringManipulation.ts
+++ b/src/frontend/src/utils/stringManipulation.ts
@@ -3,7 +3,7 @@ import React from "react";
import { FieldParserType } from "../types/api";
function toSnakeCase(str: string): string {
- return str.trim().replace(/\s+/g, "_");
+ return str.trim().replace(/[-\s]+/g, "_");
}
function toCamelCase(str: string): string {
@@ -35,6 +35,15 @@ function toUpperCase(str: string): string {
return str?.toUpperCase();
}
+function toSpaceCase(str: string): string {
+ return str
+ .trim()
+ .replace(/[_\s-]+/g, " ")
+ .split(" ")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ");
+}
+
function noBlank(str: string): string {
const trim = str.trim();
if (trim === "") {
@@ -101,6 +110,9 @@ export function parseString(
case "no_blank":
result = noBlank(result);
break;
+ case "space_case":
+ result = toSpaceCase(result);
+ break;
case "valid_csv":
result = validCsv(result);
break;
diff --git a/src/frontend/tests/extended/features/edit-tools.spec.ts b/src/frontend/tests/extended/features/edit-tools.spec.ts
index a9354be3c..c00dd24fd 100644
--- a/src/frontend/tests/extended/features/edit-tools.spec.ts
+++ b/src/frontend/tests/extended/features/edit-tools.spec.ts
@@ -35,26 +35,46 @@ test(
await page.getByTestId("tool-mode-button").click();
- await page.locator('[data-testid="icon-Hammer"]').nth(1).waitFor({
+ await page.locator('[data-testid="icon-Hammer"]').nth(0).waitFor({
timeout: 3000,
state: "visible",
});
- await page.getByTestId("icon-Hammer").nth(1).click();
+ await page.waitForSelector("text=actions", { timeout: 30000 });
- await page.waitForSelector("text=edit tools", { timeout: 30000 });
+ await page.getByTestId("button_open_actions").click();
+
+ await page.waitForSelector("text=API Request", { timeout: 30000 });
const rowsCount = await page.getByRole("gridcell").count();
expect(rowsCount).toBeGreaterThan(3);
- expect(await page.getByRole("switch").nth(0).isChecked()).toBe(true);
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(0).isChecked(),
+ ).toBe(true);
- await page.getByRole("switch").nth(0).click();
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
+ ).toBe(true);
- expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
+ ).toBe(true);
- await page.getByText("Save").last().click();
+ await page.locator('input[data-ref="eInput"]').nth(0).click();
+
+ await page.waitForTimeout(500);
+
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
+ ).toBe(false);
+
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
+ ).toBe(false);
+
+ await page.getByText("Close").last().click();
await page.waitForSelector(
'[data-testid="generic-node-title-arrangement"]',
@@ -63,14 +83,54 @@ test(
},
);
- await page.waitForTimeout(500);
+ await page.waitForSelector('[data-testid="div-tools_tools_metadata"]', {
+ timeout: 3000,
+ });
- await page.getByTestId("icon-Hammer").nth(1).click();
+ expect(
+ await page
+ .locator('[data-testid="div-tools_tools_metadata"]')
+ .isVisible(),
+ ).toBe(true);
- await page.waitForSelector("text=edit tools", { timeout: 30000 });
+ await page.getByTestId("div-tools_tools_metadata").click();
await page.waitForTimeout(500);
- expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
+ ).toBe(false);
+
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
+ ).toBe(false);
+
+ await page.locator('input[data-ref="eInput"]').nth(3).click();
+
+ await page.waitForTimeout(500);
+
+ expect(
+ await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
+ ).toBe(true);
+
+ await page.getByRole("gridcell").nth(0).click();
+
+ expect(
+ await page.locator('[data-testid="sidebar_header_name"]').isVisible(),
+ ).toBe(true);
+
+ expect(
+ await page
+ .locator('[data-testid="sidebar_header_description"]')
+ .isVisible(),
+ ).toBe(true);
+
+ await page.waitForTimeout(500);
+
+ await page.getByText("Close").last().click();
+
+ expect(
+ await page.locator('[data-testid="tool_make_requests"]').isVisible(),
+ ).toBe(true);
},
);