From dffc2d51cddd33fc855a0403d78f37acf986849b Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 16 Dec 2024 14:50:00 -0300 Subject: [PATCH 1/5] feature: Improve Table customization to enhance ux on tool mode (#5216) * refactor: Add field validation options to TableOptions * refactor: Add field validation options and trigger text/icon to TableMixin * refactor: Add field validation options and trigger text/icon to TableMixin * refactor: Add field validation options and trigger text/icon to TableMixin * update table trigger for toolmode usage * Refactor table trigger and field validation options - Updated the table trigger for toolmode usage - Added field validation options and trigger text/icon to TableMixin - Modified TableOptions to block certain actions and hide options * Refactor TableOptionsTypeAPI field names for blocking actions * Refactor TableOptions default values for blocking actions * Refactor TableOptions default values for blocking actions * Refactor TableOptions component to include tableOptions prop * Refactor table selection and pagination options * Refactor TOOL_TABLE_SCHEMA to disable sorting and filtering for the "name" and "description" fields * Refactor TableOptions to allow blocking hiding of fields * Refactor TableModal and TableNodeComponent to include support for block hiding columns * Refactor Column model to include support for different edit modes * Refactor TableOptions to include support for field parsers * Refactor TableOptions to include support for field parsers and blocking hiding of fields * Refactor TableOptions to include support for inline editing of fields * Refactor App.css to style large text inputs and text areas in AgGrid * update types * Update table modal to prevent closing the the modal while editing cell * Refactor string manipulation utilities to support parsing and transforming strings based on specified field parsers * add inline input support * Refactor TextModal component to remove close button in the footer * add field parser in context * format code * format code * Add disable_edit field to Column class * Refactor TableNodeComponent to exclude columns with disable_edit field from being editable * [autofix.ci] apply automated fixes * Fix casing in selector text for "Open table" in tableInputComponent tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida --- .../base/langflow/base/tools/constants.py | 8 ++ .../custom/custom_component/component.py | 17 ++++- .../base/langflow/inputs/input_mixin.py | 5 +- src/backend/base/langflow/schema/table.py | 48 ++++++++++++ src/frontend/src/App.css | 9 +++ .../components/TableNodeComponent/index.tsx | 44 +++++++---- .../components/TableOptions/index.tsx | 5 +- .../components/tableComponent/index.tsx | 20 ++++- .../core/parameterRenderComponent/index.tsx | 3 + .../core/parameterRenderComponent/types.ts | 5 +- src/frontend/src/modals/tableModal/index.tsx | 26 +++++-- src/frontend/src/modals/textModal/index.tsx | 3 - src/frontend/src/types/api/index.ts | 37 ++++++++++ src/frontend/src/types/utils/functions.ts | 4 + src/frontend/src/utils/stringManipulation.ts | 74 +++++++++++++++++++ src/frontend/src/utils/utils.ts | 45 +++++++++-- .../core/unit/tableInputComponent.spec.ts | 8 +- 17 files changed, 320 insertions(+), 41 deletions(-) create mode 100644 src/frontend/src/utils/stringManipulation.ts diff --git a/src/backend/base/langflow/base/tools/constants.py b/src/backend/base/langflow/base/tools/constants.py index ea3189701847..507445c5ea68 100644 --- a/src/backend/base/langflow/base/tools/constants.py +++ b/src/backend/base/langflow/base/tools/constants.py @@ -1,3 +1,5 @@ +from langflow.schema.table import EditMode + TOOL_OUTPUT_NAME = "component_as_tool" TOOL_OUTPUT_DISPLAY_NAME = "Toolset" TOOLS_METADATA_INPUT_NAME = "tools_metadata" @@ -7,11 +9,17 @@ "display_name": "Name", "type": "str", "description": "Specify the name of the output field.", + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, }, { "name": "description", "display_name": "Description", "type": "str", "description": "Describe the purpose of the output field.", + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, }, ] diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 1a9757b0a066..948a9cb48ce6 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -30,6 +30,7 @@ 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 @@ -1183,8 +1184,22 @@ def _build_tools_metadata_input(self): return TableInput( name=TOOLS_METADATA_INPUT_NAME, - display_name="Tools Metadata", + info="Use the table to configure the tools.", + display_name="Toolset configuration", real_time_refresh=True, table_schema=TOOL_TABLE_SCHEMA, value=tool_data, + trigger_icon="Hammer", + trigger_text="Open toolset", + 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}, + ), ) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index b5a2098f8680..3a2c85fa2fb6 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -12,7 +12,7 @@ from langflow.field_typing.range_spec import RangeSpec from langflow.inputs.validators import CoalesceBool -from langflow.schema.table import Column, TableSchema +from langflow.schema.table import Column, TableOptions, TableSchema class FieldTypes(str, Enum): @@ -184,6 +184,9 @@ class SliderMixin(BaseModel): class TableMixin(BaseModel): table_schema: TableSchema | list[Column] | None = None + trigger_text: str = Field(default="Open table") + trigger_icon: str = Field(default="Table") + table_options: TableOptions | None = None @field_validator("table_schema") @classmethod diff --git a/src/backend/base/langflow/schema/table.py b/src/backend/base/langflow/schema/table.py index 0850369e431e..efbce244e0bf 100644 --- a/src/backend/base/langflow/schema/table.py +++ b/src/backend/base/langflow/schema/table.py @@ -13,6 +13,11 @@ class FormatterType(str, Enum): boolean = "boolean" +class EditMode(str, Enum): + MODAL = "modal" + INLINE = "inline" + + class Column(BaseModel): model_config = ConfigDict(populate_by_name=True) name: str @@ -22,6 +27,8 @@ class Column(BaseModel): formatter: FormatterType | str | None = Field(default=None, alias="type") description: str | None = None default: str | None = None + disable_edit: bool = Field(default=False) + edit_mode: EditMode | None = Field(default=EditMode.MODAL) @model_validator(mode="after") def set_display_name(self): @@ -48,3 +55,44 @@ def validate_formatter(cls, value): class TableSchema(BaseModel): columns: list[Column] + + +class FieldValidatorType(str, Enum): + """Enum for field validation types.""" + + NO_SPACES = "no_spaces" # Prevents spaces in input + LOWERCASE = "lowercase" # Forces lowercase + UPPERCASE = "uppercase" # Forces uppercase + EMAIL = "email" # Validates email format + URL = "url" # Validates URL format + ALPHANUMERIC = "alphanumeric" # Only letters and numbers + NUMERIC = "numeric" # Only numbers + ALPHA = "alpha" # Only letters + PHONE = "phone" # Phone number format + SLUG = "slug" # URL slug format (lowercase, hyphens) + USERNAME = "username" # Alphanumeric with underscores + PASSWORD = "password" # Minimum security requirements # noqa: S105 + + +class FieldParserType(str, Enum): + """Enum for field parser types.""" + + SNAKE_CASE = "snake_case" + CAMEL_CASE = "camel_case" + PASCAL_CASE = "pascal_case" + KEBAB_CASE = "kebab_case" + LOWERCASE = "lowercase" + UPPERCASE = "uppercase" + + +class TableOptions(BaseModel): + block_add: bool = Field(default=False) + block_delete: bool = Field(default=False) + block_edit: bool = Field(default=False) + block_sort: bool = Field(default=False) + block_filter: bool = Field(default=False) + block_hide: bool | list[str] = Field(default=False) + block_select: bool = Field(default=False) + 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) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index f099a3aba0fa..d45d358ba25d 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -197,3 +197,12 @@ code { appearance: none; margin: 0; } + +.ag-large-text-input.ag-text-area.ag-input-field { + background-color: hsl(var(--background)) !important; +} + +.ag-large-text-input.ag-text-area.ag-input-field textarea { + resize: none !important; + height: 100% !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 cd90d60f08e8..516a2dadfe05 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/TableNodeComponent/index.tsx @@ -17,6 +17,9 @@ export default function TableNodeComponent({ columns, handleOnNewValue, disabled = false, + table_options, + trigger_icon = "Table", + trigger_text = "Open Table", }: InputProps): JSX.Element { const dataTypeDefinitions: { [cellDataType: string]: DataTypeDefinition; @@ -63,7 +66,7 @@ export default function TableNodeComponent({ const agGrid = useRef(null); const componentColumns = columns ? columns - : generateBackendColumnsFromValue(value ?? []); + : generateBackendColumnsFromValue(value ?? [], table_options); const AgColumns = FormatColumns(componentColumns); function setAllRows() { if (agGrid.current && !agGrid.current.api.isDestroyed()) { @@ -100,16 +103,22 @@ export default function TableNodeComponent({ function updateComponent() { setAllRows(); } - const editable = componentColumns.map((column) => { - const isCustomEdit = - column.formatter && - (column.formatter === "text" || column.formatter === "json"); - return { - field: column.name, - onUpdate: updateComponent, - editableCell: isCustomEdit ? false : true, - }; - }); + const editable = componentColumns + .map((column) => { + const isCustomEdit = + column.formatter && + ((column.formatter === "text" && column.edit_mode !== "inline") || + column.formatter === "json"); + return { + field: column.name, + onUpdate: updateComponent, + editableCell: isCustomEdit ? false : true, + }; + }) + .filter( + (col) => + columns?.find((c) => c.name === col.field)?.disable_edit !== true, + ); return (
{ setSelectedNodes(event.api.getSelectedNodes()); }} - rowSelection="multiple" + rowSelection={table_options?.block_select ? undefined : "multiple"} suppressRowClickSelection={true} editable={editable} - pagination={true} + pagination={!table_options?.hide_options} addRow={addRow} onDelete={deleteRow} onDuplicate={duplicateRow} @@ -138,6 +148,7 @@ export default function TableNodeComponent({ className="h-full w-full" columnDefs={AgColumns} rowData={value} + context={{ field_parsers: table_options?.field_parsers }} >
diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx index b9a7e5c57c6d..8297c9fa279a 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/components/TableOptions/index.tsx @@ -1,6 +1,7 @@ import IconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Button } from "@/components/ui/button"; +import { TableOptionsTypeAPI } from "@/types/api"; import { cn } from "@/utils/utils"; export default function TableOptions({ @@ -10,6 +11,7 @@ export default function TableOptions({ hasSelection, stateChange, addRow, + tableOptions, }: { resetGrid: () => void; duplicateRow?: () => void; @@ -17,11 +19,12 @@ export default function TableOptions({ addRow?: () => void; hasSelection: boolean; stateChange: boolean; + tableOptions?: TableOptionsTypeAPI; }): JSX.Element { return (
- {addRow && ( + {addRow && !tableOptions?.block_add && (
)} -
diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 514eadc6fff2..6b59ef8756c0 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -290,3 +290,40 @@ export type useMutationFunctionType< "mutationFn" | "mutationKey" >, ) => UseMutationResult; + +export type FieldValidatorType = + | "no_spaces" + | "lowercase" + | "uppercase" + | "email" + | "url" + | "alphanumeric" + | "numeric" + | "alpha" + | "phone" + | "slug" + | "username" + | "password"; + +export type FieldParserType = + | "snake_case" + | "camel_case" + | "pascal_case" + | "kebab_case" + | "lowercase" + | "uppercase"; + +export type TableOptionsTypeAPI = { + block_add?: boolean; + block_delete?: boolean; + block_edit?: boolean; + block_sort?: boolean; + block_filter?: boolean; + block_hide?: boolean | string[]; + block_select?: boolean; + hide_options?: boolean; + field_validators?: Array< + FieldValidatorType | { [key: string]: FieldValidatorType } + >; + field_parsers?: Array; +}; diff --git a/src/frontend/src/types/utils/functions.ts b/src/frontend/src/types/utils/functions.ts index f4ea85e9e7bb..fafb21bba8a7 100644 --- a/src/frontend/src/types/utils/functions.ts +++ b/src/frontend/src/types/utils/functions.ts @@ -1,3 +1,5 @@ +import { FieldParserType, FieldValidatorType } from "../api"; + export type getCodesObjProps = { runCurlCode: string; webhookCurlCode: string; @@ -23,5 +25,7 @@ export interface ColumnField { filterable: boolean; formatter?: FormatterType; description?: string; + disable_edit?: boolean; default?: any; // Add this line + edit_mode?: "modal" | "inline"; } diff --git a/src/frontend/src/utils/stringManipulation.ts b/src/frontend/src/utils/stringManipulation.ts new file mode 100644 index 000000000000..cdce6004467f --- /dev/null +++ b/src/frontend/src/utils/stringManipulation.ts @@ -0,0 +1,74 @@ +import { FieldParserType } from "../types/api"; + +function toSnakeCase(str: string): string { + return str.trim().replace(/\s+/g, "_"); +} + +function toCamelCase(str: string): string { + return str + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")) + .replace(/^[A-Z]/, (c) => c.toLowerCase()); +} + +function toPascalCase(str: string): string { + return str + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")) + .replace(/^[a-z]/, (c) => c.toUpperCase()); +} + +function toKebabCase(str: string): string { + return str + .replace(/([A-Z])/g, " $1") + .trim() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[_]+/g, "-"); +} + +function toLowerCase(str: string): string { + return str.toLowerCase(); +} + +function toUpperCase(str: string): string { + return str.toUpperCase(); +} + +export function parseString( + str: string, + parsers: FieldParserType[] | FieldParserType, +): string { + let result = str; + + let parsersArray: FieldParserType[] = []; + + if (typeof parsers === "string") { + parsersArray = [parsers]; + } else { + parsersArray = parsers; + } + + 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; + } + } + + return result; +} diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 851f0ddb6c2a..79963c095f7d 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -1,6 +1,6 @@ import TableAutoCellRender from "@/components/core/parameterRenderComponent/components/tableComponent/components/tableAutoCellRender"; import { ColumnField, FormatterType } from "@/types/utils/functions"; -import { ColDef, ColGroupDef } from "ag-grid-community"; +import { ColDef, ColGroupDef, ValueParserParams } from "ag-grid-community"; import clsx, { ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { @@ -9,7 +9,12 @@ import { MODAL_CLASSES, SHORTCUT_KEYS, } from "../constants/constants"; -import { APIDataType, InputFieldType, VertexDataTypeAPI } from "../types/api"; +import { + APIDataType, + InputFieldType, + TableOptionsTypeAPI, + VertexDataTypeAPI, +} from "../types/api"; import { groupedObjType, nodeGroupedObjType, @@ -18,6 +23,7 @@ import { import { NodeDataType, NodeType } from "../types/flow"; import { FlowState } from "../types/tabs"; import { isErrorLog } from "../types/utils/typeCheckingUtils"; +import { parseString } from "./stringManipulation"; export function classNames(...classes: Array): string { return classes.filter(Boolean).join(" "); @@ -515,6 +521,20 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { field: col.name, sortable: col.sortable, filter: col.filterable, + editable: !col.disable_edit, + valueParser: (params: ValueParserParams) => { + const { context, newValue, colDef } = params; + if ( + context.field_parsers && + context.field_parsers[colDef.field ?? ""] + ) { + return parseString( + newValue, + context.field_parsers[colDef.field ?? ""], + ); + } + return newValue; + }, }; if (!col.formatter) { col.formatter = FormatterType.text; @@ -525,7 +545,17 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { newCol.cellRendererParams = { formatter: col.formatter, }; - newCol.cellRenderer = TableAutoCellRender; + 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, + }; + } } return newCol; }); @@ -533,14 +563,17 @@ export function FormatColumns(columns: ColumnField[]): ColDef[] { return colDefs; } -export function generateBackendColumnsFromValue(rows: Object[]): ColumnField[] { +export function generateBackendColumnsFromValue( + rows: Object[], + tableOptions?: TableOptionsTypeAPI, +): ColumnField[] { const columns = extractColumnsFromRows(rows, "union"); return columns.map((column) => { const newColumn: ColumnField = { name: column.field ?? "", display_name: column.headerName ?? "", - sortable: true, - filterable: true, + sortable: !tableOptions?.block_sort, + filterable: !tableOptions?.block_filter, default: null, // Initialize default to null or appropriate value }; diff --git a/src/frontend/tests/core/unit/tableInputComponent.spec.ts b/src/frontend/tests/core/unit/tableInputComponent.spec.ts index f460928b3a08..847cd63fc0b8 100644 --- a/src/frontend/tests/core/unit/tableInputComponent.spec.ts +++ b/src/frontend/tests/core/unit/tableInputComponent.spec.ts @@ -83,11 +83,11 @@ class CustomComponent(Component): await page.getByText("Check & Save").last().click(); - await page.waitForSelector('text="Open Table"', { + await page.waitForSelector('text="Open table"', { timeout: 3000, }); - await page.getByText("Open Table").click(); + await page.getByText("Open table").click(); await page.waitForSelector(".ag-cell-value", { timeout: 3000, @@ -166,11 +166,11 @@ class CustomComponent(Component): await page.getByText("Close").last().click(); - await page.waitForSelector("text=Open Table", { + await page.waitForSelector("text=Open table", { timeout: 3000, }); - await page.getByText("Open Table").click(); + await page.getByText("Open table").click(); await page.waitForSelector(".ag-cell-value", { timeout: 3000, From ef6226d25dfb01f4e1ebe80a2fe242fc47297cda Mon Sep 17 00:00:00 2001 From: VICTOR CORREA GOMES <112295415+Vigtu@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:13:25 -0300 Subject: [PATCH 2/5] feat: Enhance ConditionalRouterComponent with Regex Matching and UI Improvements (#5217) * fix: revert response return type to Message for compatibility * Change the max_iterations and case_sensitive advanced options * feat: add regex matching and real-time refresh functionality * Refactor evaluate_condition method to use elif statements for clarity - Changed if statements to elif in the evaluate_condition method to improve readability and indicate mutually exclusive conditions. - Removed redundant advanced=True line from BoolInput for case sensitivity. * [autofix.ci] apply automated fixes * Format code using make format - Applied code formatting to ensure consistency and adherence to style guidelines. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Hare --- .../components/logic/conditional_router.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/backend/base/langflow/components/logic/conditional_router.py b/src/backend/base/langflow/components/logic/conditional_router.py index f8a166c2b071..80c2739a6b52 100644 --- a/src/backend/base/langflow/components/logic/conditional_router.py +++ b/src/backend/base/langflow/components/logic/conditional_router.py @@ -1,3 +1,5 @@ +import re + from langflow.custom import Component from langflow.io import BoolInput, DropdownInput, IntInput, MessageInput, MessageTextInput, Output from langflow.schema.message import Message @@ -27,16 +29,16 @@ def __init__(self, *args, **kwargs): DropdownInput( name="operator", display_name="Operator", - options=["equals", "not equals", "contains", "starts with", "ends with"], + options=["equals", "not equals", "contains", "starts with", "ends with", "matches regex"], info="The operator to apply for comparing the texts.", value="equals", + real_time_refresh=True, ), BoolInput( name="case_sensitive", display_name="Case Sensitive", info="If true, the comparison will be case sensitive.", value=False, - advanced=True, ), MessageInput( name="message", @@ -49,6 +51,7 @@ def __init__(self, *args, **kwargs): display_name="Max Iterations", info="The maximum number of iterations for the conditional router.", value=10, + advanced=True, ), DropdownInput( name="default_route", @@ -69,7 +72,7 @@ def _pre_run_setup(self): self.__iteration_updated = False def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, case_sensitive: bool) -> bool: - if not case_sensitive: + if not case_sensitive and operator != "matches regex": input_text = input_text.lower() match_text = match_text.lower() @@ -83,6 +86,11 @@ def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, return input_text.startswith(match_text) if operator == "ends with": return input_text.endswith(match_text) + if operator == "matches regex": + try: + return bool(re.match(match_text, input_text)) + except re.error: + return False # Return False if the regex is invalid return False def iterate_and_stop_once(self, route_to_stop: str): @@ -90,11 +98,10 @@ def iterate_and_stop_once(self, route_to_stop: str): self.update_ctx({f"{self._id}_iteration": self.ctx.get(f"{self._id}_iteration", 0) + 1}) self.__iteration_updated = True if self.ctx.get(f"{self._id}_iteration", 0) >= self.max_iterations and route_to_stop == self.default_route: - # We need to stop the other route route_to_stop = "true_result" if route_to_stop == "false_result" else "false_result" self.stop(route_to_stop) - def true_response(self) -> Message | str: + def true_response(self) -> Message: result = self.evaluate_condition( self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive ) @@ -103,9 +110,9 @@ def true_response(self) -> Message | str: self.iterate_and_stop_once("false_result") return self.message self.iterate_and_stop_once("true_result") - return "" + return Message(content="") - def false_response(self) -> Message | str: + def false_response(self) -> Message: result = self.evaluate_condition( self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive ) @@ -114,4 +121,18 @@ def false_response(self) -> Message | str: self.iterate_and_stop_once("true_result") return self.message self.iterate_and_stop_once("false_result") - return "" + return Message(content="") + + def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict: + if field_name == "operator": + if field_value == "matches regex": + if "case_sensitive" in build_config: + del build_config["case_sensitive"] + # Ensure case_sensitive is present for all other operators + elif "case_sensitive" not in build_config: + case_sensitive_input = next( + (input_field for input_field in self.inputs if input_field.name == "case_sensitive"), None + ) + if case_sensitive_input: + build_config["case_sensitive"] = case_sensitive_input.to_dict() + return build_config From 4fc7b187c409a4506153ac38e9036570544d1810 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Mon, 16 Dec 2024 13:33:35 -0500 Subject: [PATCH 3/5] fix: fixes tool metadata order and update issue, resolves agent tool metadata update failure (#5248) This pull request includes several changes to the langflow project, focusing on deprecating the agent_description feature and enhancing the tools' metadata handling. The most important changes include marking the agent_description as deprecated, updating the tools' metadata management, and adding tags to tools. --- .../base/langflow/base/agents/agent.py | 17 +++++----- .../langflow/base/tools/component_tool.py | 32 ++++++++++++++++--- .../base/langflow/base/tools/constants.py | 23 ++++++++++--- .../custom/custom_component/component.py | 5 +-- .../Instagram Copywriter.json | 4 +-- .../starter_projects/Market Research.json | 4 +-- .../starter_projects/Research Agent.json | 4 +-- .../starter_projects/SaaS Pricing.json | 4 +-- .../Sequential Tasks Agents .json | 12 +++---- .../starter_projects/Simple Agent.json | 4 +-- .../Travel Planning Agents.json | 12 +++---- 11 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/backend/base/langflow/base/agents/agent.py b/src/backend/base/langflow/base/agents/agent.py index e154b1968a7d..0bceeadede0e 100644 --- a/src/backend/base/langflow/base/agents/agent.py +++ b/src/backend/base/langflow/base/agents/agent.py @@ -57,10 +57,11 @@ class LCAgentComponent(Component): ), MultilineInput( name="agent_description", - display_name="Agent Description", + display_name="Agent Description [Deprecated]", info=( "The description of the agent. This is only used when in Tool Mode. " - f"Defaults to '{DEFAULT_TOOLS_DESCRIPTION}' and tools are added dynamically." + f"Defaults to '{DEFAULT_TOOLS_DESCRIPTION}' and tools are added dynamically. " + "This feature is deprecated and will be removed in future versions." ), advanced=True, value=DEFAULT_TOOLS_DESCRIPTION, @@ -236,11 +237,11 @@ def to_toolkit(self) -> list[Tool]: component_toolkit = _get_component_toolkit() tools_names = self._build_tools_names() agent_description = self.get_tool_description() - # Check if tools_description is the default value - if agent_description == DEFAULT_TOOLS_DESCRIPTION: - description = f"{agent_description}{tools_names}" - else: - description = agent_description - return component_toolkit(component=self).get_tools( + # TODO: Agent Description Depreciated Feature to be removed + description = f"{agent_description}{tools_names}" + tools = component_toolkit(component=self).get_tools( tool_name=self.get_tool_name(), tool_description=description, callbacks=self.get_langchain_callbacks() ) + if hasattr(self, "tools_metadata"): + tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools) + return tools diff --git a/src/backend/base/langflow/base/tools/component_tool.py b/src/backend/base/langflow/base/tools/component_tool.py index e3f82d8d62c9..30e1d83d45cf 100644 --- a/src/backend/base/langflow/base/tools/component_tool.py +++ b/src/backend/base/langflow/base/tools/component_tool.py @@ -218,6 +218,7 @@ def get_tools( args_schema=args_schema, handle_tool_error=True, callbacks=callbacks, + tags=[formatted_name], ) ) else: @@ -229,6 +230,7 @@ def get_tools( args_schema=args_schema, handle_tool_error=True, callbacks=callbacks, + tags=[formatted_name], ) ) if len(tools) == 1 and (tool_name or tool_description): @@ -243,17 +245,37 @@ def get_tools( raise ValueError(msg) return tools + def get_tools_metadata_dictionary(self) -> dict: + if isinstance(self.metadata, pd.DataFrame): + try: + return { + record["tags"][0]: record + for record in self.metadata.to_dict(orient="records") + if record.get("tags") + } + except (KeyError, IndexError) as e: + msg = "Error processing metadata records: " + str(e) + raise ValueError(msg) from e + return {} + def update_tools_metadata( self, tools: list[BaseTool | StructuredTool], ) -> list[BaseTool]: # 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.metadata.to_dict(orient="records") - for tool, metadata in zip(tools, metadata_dict, strict=False): - if isinstance(tool, StructuredTool | BaseTool): - tool.name = metadata.get("name", tool.name) - tool.description = metadata.get("description", tool.description) + metadata_dict = self.get_tools_metadata_dictionary() + for tool in tools: + if isinstance(tool, StructuredTool | BaseTool) and tool.tags: + try: + tag = tool.tags[0] + except IndexError: + msg = "Tool tags cannot be empty." + 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) 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 507445c5ea68..64bd4c3cf6d7 100644 --- a/src/backend/base/langflow/base/tools/constants.py +++ b/src/backend/base/langflow/base/tools/constants.py @@ -6,20 +6,35 @@ TOOL_TABLE_SCHEMA = [ { "name": "name", - "display_name": "Name", + "display_name": "Tool Name", "type": "str", - "description": "Specify the name of the output field.", + "description": "Specify the name of the tool.", "sortable": False, "filterable": False, "edit_mode": EditMode.INLINE, }, { "name": "description", - "display_name": "Description", + "display_name": "Tool Description", "type": "str", - "description": "Describe the purpose of the output field.", + "description": "Describe the purpose of the tool.", + "sortable": False, + "filterable": False, + "edit_mode": EditMode.INLINE, + }, + { + "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." + ), + "disable_edit": True, "sortable": False, "filterable": False, "edit_mode": EditMode.INLINE, }, ] + +TOOLS_METADATA_INFO = "Use the table to configure the tools." diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 948a9cb48ce6..a25a4ef854a8 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -18,6 +18,7 @@ TOOL_OUTPUT_DISPLAY_NAME, TOOL_OUTPUT_NAME, TOOL_TABLE_SCHEMA, + TOOLS_METADATA_INFO, TOOLS_METADATA_INPUT_NAME, ) from langflow.custom.tree_visitor import RequiredInputsVisitor @@ -1174,7 +1175,7 @@ def _build_tools_metadata_input(self): tool_data = ( self.tools_metadata if hasattr(self, TOOLS_METADATA_INPUT_NAME) - else [{"name": tool.name, "description": tool.description} for tool in tools] + else [{"name": tool.name, "description": tool.description, "tags": tool.tags} for tool in tools] ) try: from langflow.io import TableInput @@ -1184,7 +1185,7 @@ def _build_tools_metadata_input(self): return TableInput( name=TOOLS_METADATA_INPUT_NAME, - info="Use the table to configure the tools.", + info=TOOLS_METADATA_INFO, display_name="Toolset configuration", real_time_refresh=True, table_schema=TOOL_TABLE_SCHEMA, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json index 868c4b652c61..d21a514669d5 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json @@ -1656,9 +1656,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json index 17785e589060..5092ba26ef68 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json @@ -1900,9 +1900,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json index e6a53e43c129..a7f5903da9e0 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json @@ -2147,9 +2147,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json index ef620be6f194..9f3af6a970a2 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json @@ -721,9 +721,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents .json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents .json index 916cc46ec6d2..ee5075964b0e 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents .json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents .json @@ -670,9 +670,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], @@ -1248,9 +1248,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], @@ -3141,9 +3141,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json index 1f45fb50416d..ac27610bc461 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json @@ -204,9 +204,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json index 2068cc5e39e1..0e2ff530c3ce 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json @@ -1299,9 +1299,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], @@ -1877,9 +1877,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], @@ -2455,9 +2455,9 @@ "agent_description": { "_input_type": "MultilineInput", "advanced": true, - "display_name": "Agent Description", + "display_name": "Agent Description [Deprecated]", "dynamic": false, - "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically.", + "info": "The description of the agent. This is only used when in Tool Mode. Defaults to 'A helpful assistant with access to the following tools:' and tools are added dynamically. This feature is deprecated and will be removed in future versions.", "input_types": [ "Message" ], From 5b6bfcafe28fc32b941b4fa7921ff65bfa199a43 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Mon, 16 Dec 2024 13:56:37 -0500 Subject: [PATCH 4/5] fix: Update stored message to give out only the latest message (#4954) * Update store_message.py update stored message to give out only the latest message * Update store_message.py * Update store_message.py * Update store_message.py update with error handling --- .../components/helpers/store_message.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/backend/base/langflow/components/helpers/store_message.py b/src/backend/base/langflow/components/helpers/store_message.py index 20ef31c0b934..10ffab6df057 100644 --- a/src/backend/base/langflow/components/helpers/store_message.py +++ b/src/backend/base/langflow/components/helpers/store_message.py @@ -46,7 +46,7 @@ class StoreMessageComponent(Component): ] outputs = [ - Output(display_name="Stored Messages", name="stored_messages", method="store_message"), + Output(display_name="Stored Message", name="stored_message", method="store_message"), ] async def store_message(self) -> Message: @@ -61,14 +61,19 @@ async def store_message(self) -> Message: self.memory.session_id = message.session_id lc_message = message.to_lc_message() await self.memory.aadd_messages([lc_message]) - stored = await self.memory.aget_messages() - stored = [Message.from_lc_message(m) for m in stored] + stored_message = await self.memory.aget_messages() + stored_message = [Message.from_lc_message(m) for m in stored_message] if message.sender: - stored = [m for m in stored if m.sender == message.sender] + stored_message = [m for m in stored_message if m.sender == message.sender] else: await astore_message(message, flow_id=self.graph.flow_id) - stored = await aget_messages( + stored_messages = await aget_messages( session_id=message.session_id, sender_name=message.sender_name, sender=message.sender ) - self.status = stored - return stored + if not stored_messages: + msg = "No messages were stored. Please ensure that the session ID and sender are properly set." + raise ValueError(msg) + stored_message = stored_messages[0] + + self.status = stored_message + return stored_message From 4736aaed8cd57508580c27f0093af5e91b43a03e Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 16 Dec 2024 17:05:54 -0300 Subject: [PATCH 5/5] fix: update message update logic and add comment (#5295) * refactor: Simplify message update logic in aupdate_messages function * add comment --- src/backend/base/langflow/memory.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/base/langflow/memory.py b/src/backend/base/langflow/memory.py index 3efdc59c42e5..ebf8fac3d119 100644 --- a/src/backend/base/langflow/memory.py +++ b/src/backend/base/langflow/memory.py @@ -135,10 +135,10 @@ async def aupdate_messages(messages: Message | list[Message]) -> list[Message]: for message in messages: msg = await session.get(MessageTable, message.id) if msg: - if hasattr(message, "data"): - msg = msg.sqlmodel_update(message.data) - else: - msg = msg.sqlmodel_update(message.model_dump(exclude_unset=True, exclude_none=True)) + msg = msg.sqlmodel_update(message.model_dump(exclude_unset=True, exclude_none=True)) + # Convert flow_id to UUID if it's a string preventing error when saving to database + if msg.flow_id and isinstance(msg.flow_id, str): + msg.flow_id = UUID(msg.flow_id) session.add(msg) await session.commit() await session.refresh(msg)