From 18acd304a7b08e52571a02de8d7ea390410e093b Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Fri, 10 Jan 2025 09:15:37 -0300 Subject: [PATCH] feature: add command feature column to toolset table and other minor table improvements (#5343) * refactor: Update toolset configuration display name and description This commit updates the display name and description of the toolset configuration in the custom component. The display name is changed to "Edit tools" and the description is modified to "Modify tool names and descriptions to help agents understand when to use each tool." This improves the clarity and usability of the toolset configuration. * refactor: Update table modal header description handling * refactor: Add EditMode.POPOVER option to table schema * refactor: Add EditMode.POPOVER option to table schema * refactor: Add table icon to table schema and components * add style for not editable cells * Add "commands" field to tool table schema * refactor: Add "no_blank" and "valid_csv" field parsers - Added "no_blank" field parser to remove leading and trailing whitespace from strings and throw an error if the string is blank. - Added "valid_csv" field parser to replace whitespace with commas in strings. * refactor: Add commands to tool description in ComponentToolkit * refactor: Improve parsing of commands in validCommands function * Update src/frontend/src/utils/stringManipulation.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * [autofix.ci] apply automated fixes * add support for hints on table header * update descriptions on tool table * [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> --- .../langflow/base/tools/component_tool.py | 8 ++ .../base/langflow/base/tools/constants.py | 22 ++++-- .../custom/custom_component/component.py | 12 ++- .../base/langflow/inputs/input_mixin.py | 1 + src/backend/base/langflow/schema/table.py | 5 ++ src/frontend/src/App.css | 11 +++ .../components/TableNodeComponent/index.tsx | 27 ++++++- .../core/parameterRenderComponent/index.tsx | 1 + .../core/parameterRenderComponent/types.ts | 1 + src/frontend/src/modals/tableModal/index.tsx | 19 ++++- src/frontend/src/types/api/index.ts | 6 +- src/frontend/src/types/utils/functions.ts | 2 +- src/frontend/src/utils/stringManipulation.ts | 79 ++++++++++++++----- src/frontend/src/utils/utils.ts | 42 ++++++---- 14 files changed, 185 insertions(+), 51 deletions(-) diff --git a/src/backend/base/langflow/base/tools/component_tool.py b/src/backend/base/langflow/base/tools/component_tool.py index 30e1d83d4..baba33773 100644 --- a/src/backend/base/langflow/base/tools/component_tool.py +++ b/src/backend/base/langflow/base/tools/component_tool.py @@ -159,6 +159,10 @@ def _format_tool_name(name: str): return re.sub(r"[^a-zA-Z0-9_-]", "-", name) +def _add_commands_to_tool_description(tool_description: str, commands: str): + return f"very_time you see one of those commands {commands} run the tool. tool description is {tool_description}" + + class ComponentToolkit: def __init__(self, component: Component, metadata: pd.DataFrame | None = None): self.component = component @@ -276,6 +280,10 @@ class ComponentToolkit: 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") + ) else: msg = f"Expected a StructuredTool or BaseTool, got {type(tool)}" raise TypeError(msg) diff --git a/src/backend/base/langflow/base/tools/constants.py b/src/backend/base/langflow/base/tools/constants.py index 64bd4c3cf..cb1c15050 100644 --- a/src/backend/base/langflow/base/tools/constants.py +++ b/src/backend/base/langflow/base/tools/constants.py @@ -20,21 +20,31 @@ TOOL_TABLE_SCHEMA = [ "description": "Describe the purpose of the tool.", "sortable": False, "filterable": False, - "edit_mode": EditMode.INLINE, + "edit_mode": EditMode.POPOVER, }, { "name": "tags", "display_name": "Tool Identifiers", "type": "str", - "description": ( - "These are the default identifiers for the tools and cannot be changed. " - "Tool Name and Tool Description are the only editable fields." - ), + "description": ("The default identifiers for the tools and cannot be changed."), "disable_edit": True, "sortable": False, "filterable": False, "edit_mode": EditMode.INLINE, }, + { + "name": "commands", + "display_name": "Commands", + "type": "str", + "description": ( + "Add commands to the tool. These commands will be used to run the tool. Start all commands with a `/`. " + "You can add multiple commands separated by a comma.\n" + "Example: `/command1`, `/command2`, `/command3`" + ), + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, + }, ] -TOOLS_METADATA_INFO = "Use the table to configure the tools." +TOOLS_METADATA_INFO = "Modify tool names and descriptions to help agents understand when to use each tool." diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index aa51a223c..e3465a172 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -1209,13 +1209,13 @@ class Component(CustomComponent): return TableInput( name=TOOLS_METADATA_INPUT_NAME, - info=TOOLS_METADATA_INFO, - display_name="Toolset configuration", + display_name="Edit tools", real_time_refresh=True, table_schema=TOOL_TABLE_SCHEMA, value=tool_data, + table_icon="Hammer", trigger_icon="Hammer", - trigger_text="Open toolset", + trigger_text="", table_options=TableOptions( block_add=True, block_delete=True, @@ -1225,6 +1225,10 @@ class Component(CustomComponent): block_hide=True, block_select=True, hide_options=True, - field_parsers={"name": FieldParserType.SNAKE_CASE}, + field_parsers={ + "name": [FieldParserType.SNAKE_CASE, FieldParserType.NO_BLANK], + "commands": FieldParserType.COMMANDS, + }, + description=TOOLS_METADATA_INFO, ), ) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 3a2c85fa2..7656ae23a 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -186,6 +186,7 @@ class TableMixin(BaseModel): table_schema: TableSchema | list[Column] | None = None trigger_text: str = Field(default="Open table") trigger_icon: str = Field(default="Table") + table_icon: str = Field(default="Table") table_options: TableOptions | None = None @field_validator("table_schema") diff --git a/src/backend/base/langflow/schema/table.py b/src/backend/base/langflow/schema/table.py index efbce244e..4ec914163 100644 --- a/src/backend/base/langflow/schema/table.py +++ b/src/backend/base/langflow/schema/table.py @@ -15,6 +15,7 @@ class FormatterType(str, Enum): class EditMode(str, Enum): MODAL = "modal" + POPOVER = "popover" INLINE = "inline" @@ -83,6 +84,9 @@ class FieldParserType(str, Enum): KEBAB_CASE = "kebab_case" LOWERCASE = "lowercase" UPPERCASE = "uppercase" + NO_BLANK = "no_blank" + VALID_CSV = ("valid_csv",) + COMMANDS = "commands" class TableOptions(BaseModel): @@ -96,3 +100,4 @@ class TableOptions(BaseModel): hide_options: bool = Field(default=False) field_validators: dict[str, list[FieldValidatorType] | FieldValidatorType] | None = Field(default=None) field_parsers: dict[str, list[FieldParserType] | FieldParserType] | None = Field(default=None) + description: str | None = Field(default=None) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 37a3b6848..91d5f3a14 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -209,3 +209,14 @@ code { resize: none !important; height: 100% !important; } + +.cell-disable-edit { + cursor: not-allowed; +} + +.cell-disable-edit.ag-cell-focus { + outline: none !important; + border: 0px !important; + box-shadow: none !important; + border-radius: 0px !important; +} 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 516a2dadf..5215e2282 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx @@ -1,3 +1,4 @@ +import ShadTooltip from "@/components/common/shadTooltipComponent"; import TableModal from "@/modals/tableModal"; import { FormatColumns, generateBackendColumnsFromValue } from "@/utils/utils"; import { DataTypeDefinition, SelectionChangedEvent } from "ag-grid-community"; @@ -20,6 +21,7 @@ export default function TableNodeComponent({ table_options, trigger_icon = "Table", trigger_text = "Open Table", + table_icon, }: InputProps): JSX.Element { const dataTypeDefinitions: { [cellDataType: string]: DataTypeDefinition; @@ -67,7 +69,26 @@ export default function TableNodeComponent({ const componentColumns = columns ? columns : generateBackendColumnsFromValue(value ?? [], table_options); - const AgColumns = FormatColumns(componentColumns); + let AgColumns = FormatColumns(componentColumns); + // add info to each column + AgColumns = AgColumns.map((col) => { + if (col.context?.info) { + return { + ...col, + headerComponent: () => ( +
+
{col.headerName}
+ +
+ +
+
+
+ ), + }; + } + return col; + }); function setAllRows() { if (agGrid.current && !agGrid.current.api.isDestroyed()) { const rows: any = []; @@ -107,7 +128,7 @@ export default function TableNodeComponent({ .map((column) => { const isCustomEdit = column.formatter && - ((column.formatter === "text" && column.edit_mode !== "inline") || + ((column.formatter === "text" && column.edit_mode === "modal") || column.formatter === "json"); return { field: column.name, @@ -128,6 +149,8 @@ export default function TableNodeComponent({ >
); case "slider": diff --git a/src/frontend/src/components/core/parameterRenderComponent/types.ts b/src/frontend/src/components/core/parameterRenderComponent/types.ts index e8d631f70..ba8b2d119 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/types.ts +++ b/src/frontend/src/components/core/parameterRenderComponent/types.ts @@ -31,6 +31,7 @@ export type TableComponentType = { table_options?: TableOptionsTypeAPI; trigger_text?: string; trigger_icon?: string; + table_icon?: string; }; export type FloatComponentType = { diff --git a/src/frontend/src/modals/tableModal/index.tsx b/src/frontend/src/modals/tableModal/index.tsx index d09ad26b8..92984243d 100644 --- a/src/frontend/src/modals/tableModal/index.tsx +++ b/src/frontend/src/modals/tableModal/index.tsx @@ -16,11 +16,19 @@ interface TableModalProps extends TableComponentProps { children: React.ReactNode; tableOptions?: TableOptionsTypeAPI; hideColumns?: boolean | string[]; + tableIcon?: string; } const TableModal = forwardRef( ( - { tableTitle, description, children, disabled, ...props }: TableModalProps, + { + tableTitle, + description, + children, + disabled, + tableIcon, + ...props + }: TableModalProps, ref: ForwardedRef, ) => { return ( @@ -37,9 +45,14 @@ const TableModal = forwardRef( disable={disabled} > {children} - + {tableTitle} - + ; field_parsers?: Array; + description?: string; }; diff --git a/src/frontend/src/types/utils/functions.ts b/src/frontend/src/types/utils/functions.ts index fafb21bba..c88b4421f 100644 --- a/src/frontend/src/types/utils/functions.ts +++ b/src/frontend/src/types/utils/functions.ts @@ -27,5 +27,5 @@ export interface ColumnField { description?: string; disable_edit?: boolean; default?: any; // Add this line - edit_mode?: "modal" | "inline"; + edit_mode?: "modal" | "inline" | "popover"; } diff --git a/src/frontend/src/utils/stringManipulation.ts b/src/frontend/src/utils/stringManipulation.ts index cdce60044..246a48712 100644 --- a/src/frontend/src/utils/stringManipulation.ts +++ b/src/frontend/src/utils/stringManipulation.ts @@ -33,6 +33,34 @@ function toUpperCase(str: string): string { return str.toUpperCase(); } +function noBlank(str: string): string { + const trim = str.trim(); + if (trim === "") { + throw new Error("String is blank"); + } + return trim; +} + +function validCsv(str: string): string { + return str.trim().replace(/\s+/g, ","); +} + +function validCommands(str: string): string { + return str + .trim() + .split(/[\s,]+/) + .flatMap((cmd) => { + cmd = cmd.trim(); + cmd = cmd.replace(/\\/g, "/"); + return cmd + .split("/") + .filter((part) => part.length > 0) + .map((part) => `/${part}`); + }) + .filter((cmd) => cmd.length > 1) + .join(", "); +} + export function parseString( str: string, parsers: FieldParserType[] | FieldParserType, @@ -48,25 +76,38 @@ export function parseString( } for (const parser of parsersArray) { - switch (parser) { - case "snake_case": - result = toSnakeCase(result); - break; - case "camel_case": - result = toCamelCase(result); - break; - case "pascal_case": - result = toPascalCase(result); - break; - case "kebab_case": - result = toKebabCase(result); - break; - case "lowercase": - result = toLowerCase(result); - break; - case "uppercase": - result = toUpperCase(result); - break; + try { + switch (parser) { + case "snake_case": + result = toSnakeCase(result); + break; + case "camel_case": + result = toCamelCase(result); + break; + case "pascal_case": + result = toPascalCase(result); + break; + case "kebab_case": + result = toKebabCase(result); + break; + case "lowercase": + result = toLowerCase(result); + break; + case "uppercase": + result = toUpperCase(result); + break; + case "no_blank": + result = noBlank(result); + break; + case "valid_csv": + result = validCsv(result); + break; + case "commands": + result = validCommands(result); + break; + } + } catch (error) { + throw new Error(`Error in parser ${parser}`); } } diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 8feb62294..502808215 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,4 +1,5 @@ import TableAutoCellRender from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender"; +import useAlertStore from "@/stores/alertStore"; import { ColumnField, FormatterType } from "@/types/utils/functions"; import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community"; import clsx, { ClassValue } from "clsx"; @@ -526,17 +527,26 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { field: col.name, sortable: col.sortable, filter: col.filterable, - editable: !col.disable_edit, + context: col.description ? { info: col.description } : {}, + cellClass: col.disable_edit ? "cell-disable-edit" : "", valueParser: (params: ValueParserParams) => { - const { context, newValue, colDef } = params; + const { context, newValue, colDef, oldValue } = params; if ( context.field_parsers && context.field_parsers[colDef.field ?? ""] ) { - return parseString( - newValue, - context.field_parsers[colDef.field ?? ""], - ); + try { + return parseString( + newValue, + context.field_parsers[colDef.field ?? ""], + ); + } catch (error: any) { + useAlertStore.getState().setErrorData({ + title: "Error parsing string", + list: [String(error.message ?? error)], + }); + return oldValue; + } } return newValue; }, @@ -551,15 +561,17 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { formatter: col.formatter, }; if (col.formatter !== FormatterType.text || col.edit_mode !== "inline") { - newCol.cellRenderer = TableAutoCellRender; - } else { - newCol.wrapText = true; - newCol.autoHeight = true; - newCol.cellEditor = "agLargeTextCellEditor"; - newCol.cellEditorPopup = true; - newCol.cellEditorParams = { - maxLength: 100000000, - }; + if (col.edit_mode === "popover") { + newCol.wrapText = true; + newCol.autoHeight = true; + newCol.cellEditor = "agLargeTextCellEditor"; + newCol.cellEditorPopup = true; + newCol.cellEditorParams = { + maxLength: 100000000, + }; + } else { + newCol.cellRenderer = TableAutoCellRender; + } } } return newCol;