diff --git a/web/src/components/ragflow-form.tsx b/web/src/components/ragflow-form.tsx index 28d1138b9..4b1b06943 100644 --- a/web/src/components/ragflow-form.tsx +++ b/web/src/components/ragflow-form.tsx @@ -17,6 +17,7 @@ type RAGFlowFormItemProps = { horizontal?: boolean; required?: boolean; labelClassName?: string; + className?: string; }; export function RAGFlowFormItem({ @@ -27,6 +28,7 @@ export function RAGFlowFormItem({ horizontal = false, required = false, labelClassName, + className, }: RAGFlowFormItemProps) { const form = useFormContext(); return ( @@ -35,9 +37,12 @@ export function RAGFlowFormItem({ name={name} render={({ field }) => ( {label && ( ', + }, + }, }; export const CategorizeAnchorPointPositions = [ diff --git a/web/src/pages/agent/form/components/query-variable-list.tsx b/web/src/pages/agent/form/components/query-variable-list.tsx new file mode 100644 index 000000000..a01f5c1e8 --- /dev/null +++ b/web/src/pages/agent/form/components/query-variable-list.tsx @@ -0,0 +1,46 @@ +import { BlockButton, Button } from '@/components/ui/button'; +import { X } from 'lucide-react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { JsonSchemaDataType } from '../../constant'; +import { QueryVariable } from './query-variable'; + +type QueryVariableListProps = { + types?: JsonSchemaDataType[]; +}; +export function QueryVariableList({ types }: QueryVariableListProps) { + const { t } = useTranslation(); + const form = useFormContext(); + const name = 'inputs'; + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
+ {fields.map((field, index) => { + const nameField = `${name}.${index}.input`; + + return ( +
+ + +
+ ); + })} + + append({ input: '' })}> + {t('common.add')} + +
+ ); +} diff --git a/web/src/pages/agent/form/components/query-variable.tsx b/web/src/pages/agent/form/components/query-variable.tsx index fec842171..d315283c0 100644 --- a/web/src/pages/agent/form/components/query-variable.tsx +++ b/web/src/pages/agent/form/components/query-variable.tsx @@ -5,24 +5,28 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { toLower } from 'lodash'; +import { isEmpty, toLower } from 'lodash'; import { ReactNode, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { VariableType } from '../../constant'; +import { JsonSchemaDataType } from '../../constant'; import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu'; type QueryVariableProps = { name?: string; - type?: VariableType; + types?: JsonSchemaDataType[]; label?: ReactNode; + hideLabel?: boolean; + className?: string; }; export function QueryVariable({ name = 'query', - type, + types = [], label, + hideLabel = false, + className, }: QueryVariableProps) { const { t } = useTranslation(); const form = useFormContext(); @@ -30,23 +34,25 @@ export function QueryVariable({ const nextOptions = useBuildQueryVariableOptions(); const finalOptions = useMemo(() => { - return type + return !isEmpty(types) ? nextOptions.map((x) => { return { ...x, - options: x.options.filter((y) => toLower(y.type).includes(type)), + options: x.options.filter((y) => + types?.some((x) => toLower(y.type).includes(x)), + ), }; }) : nextOptions; - }, [nextOptions, type]); + }, [nextOptions, types]); return ( ( - - {label || ( + + {hideLabel || label || ( {t('flow.query')} @@ -56,7 +62,7 @@ export function QueryVariable({ options={finalOptions} {...field} // allowClear - type={type} + types={types} > diff --git a/web/src/pages/agent/form/components/select-with-secondary-menu.tsx b/web/src/pages/agent/form/components/select-with-secondary-menu.tsx index 237d29e35..24c7a6f45 100644 --- a/web/src/pages/agent/form/components/select-with-secondary-menu.tsx +++ b/web/src/pages/agent/form/components/select-with-secondary-menu.tsx @@ -23,7 +23,7 @@ import { ChevronDownIcon, XIcon } from 'lucide-react'; import * as React from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { VariableType } from '../../constant'; +import { JsonSchemaDataType } from '../../constant'; import { useFindAgentStructuredOutputLabel, useShowSecondaryMenu, @@ -52,7 +52,7 @@ interface GroupedSelectWithSecondaryMenuProps { value?: string; onChange?: (value: string) => void; placeholder?: string; - type?: VariableType; + types?: JsonSchemaDataType[]; } export function GroupedSelectWithSecondaryMenu({ @@ -60,7 +60,7 @@ export function GroupedSelectWithSecondaryMenu({ value, onChange, placeholder, - type, + types, }: GroupedSelectWithSecondaryMenuProps) { const { t } = useTranslation(); const [open, setOpen] = React.useState(false); @@ -157,7 +157,7 @@ export function GroupedSelectWithSecondaryMenu({ key={option.value} data={option} click={handleSecondaryMenuClick} - type={type} + types={types} > ); } diff --git a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx index ee18fc8aa..15e91de8b 100644 --- a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx +++ b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx @@ -8,7 +8,7 @@ import { get, isEmpty, isPlainObject } from 'lodash'; import { ChevronRight } from 'lucide-react'; import { PropsWithChildren, ReactNode, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { JsonSchemaDataType, VariableType } from '../../constant'; +import { JsonSchemaDataType } from '../../constant'; import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output'; import { hasJsonSchemaChild, @@ -20,13 +20,13 @@ type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode }; type StructuredOutputSecondaryMenuProps = { data: DataItem; click(option: { label: ReactNode; value: string }): void; - type?: VariableType | JsonSchemaDataType; + types?: JsonSchemaDataType[]; } & PropsWithChildren; export function StructuredOutputSecondaryMenu({ data, click, - type, + types = [], }: StructuredOutputSecondaryMenuProps) { const { t } = useTranslation(); const filterStructuredOutput = useGetStructuredOutputByValue(); @@ -35,18 +35,21 @@ export function StructuredOutputSecondaryMenu({ const handleSubMenuClick = useCallback( (option: { label: ReactNode; value: string }, dataType?: string) => () => { // The query variable of the iteration operator can only select array type data. - if ((type && type === dataType) || !type) { + if ( + (!isEmpty(types) && types?.some((x) => x === dataType)) || + isEmpty(types) + ) { click(option); } }, - [click, type], + [click, types], ); const handleMenuClick = useCallback(() => { - if (isEmpty(type) || type === JsonSchemaDataType.Object) { + if (isEmpty(types) || types?.some((x) => x === JsonSchemaDataType.Object)) { click(data); } - }, [click, data, type]); + }, [click, data, types]); const renderAgentStructuredOutput = useCallback( (values: any, option: { label: ReactNode; value: string }) => { @@ -62,10 +65,10 @@ export function StructuredOutputSecondaryMenu({ const dataType = get(value, 'type'); if ( - !type || - (type && - (dataType === type || - hasSpecificTypeChild(value ?? {}, type))) + isEmpty(types) || + (!isEmpty(types) && + (types?.some((x) => x === dataType) || + hasSpecificTypeChild(value ?? {}, types))) ) { return (
  • @@ -90,10 +93,13 @@ export function StructuredOutputSecondaryMenu({ return
    ; }, - [handleSubMenuClick, type], + [handleSubMenuClick, types], ); - if (!hasJsonSchemaChild(structuredOutput)) { + if ( + !hasJsonSchemaChild(structuredOutput) || + (!isEmpty(types) && !hasSpecificTypeChild(structuredOutput, types)) + ) { return null; } diff --git a/web/src/pages/agent/form/data-operations-form/filter-values.tsx b/web/src/pages/agent/form/data-operations-form/filter-values.tsx new file mode 100644 index 000000000..cea0751be --- /dev/null +++ b/web/src/pages/agent/form/data-operations-form/filter-values.tsx @@ -0,0 +1,73 @@ +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { BlockButton, Button } from '@/components/ui/button'; +import { FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type SelectKeysProps = { + name: string; + label: ReactNode; + tooltip?: string; + keyField?: string; + valueField?: string; + operatorField?: string; +}; +export function FilterValues({ + name, + label, + tooltip, + keyField = 'key', + valueField = 'value', + operatorField = 'operator', +}: SelectKeysProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
    + {label} +
    + {fields.map((field, index) => { + const keyFieldAlias = `${name}.${index}.${keyField}`; + const valueFieldAlias = `${name}.${index}.${valueField}`; + const operatorFieldAlias = `${name}.${index}.${operatorField}`; + + return ( +
    + + + + + + + + + + + + + + +
    + ); + })} +
    + + append({ [keyField]: '', [valueField]: '' })}> + {t('common.add')} + +
    + ); +} diff --git a/web/src/pages/agent/form/data-operations-form/index.tsx b/web/src/pages/agent/form/data-operations-form/index.tsx index 5991995f4..838f75f96 100644 --- a/web/src/pages/agent/form/data-operations-form/index.tsx +++ b/web/src/pages/agent/form/data-operations-form/index.tsx @@ -1,44 +1,132 @@ import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; -import { Form } from '@/components/ui/form'; +import { Form, FormLabel } from '@/components/ui/form'; +import { buildOptions } from '@/utils/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { initialDataOperationsValues } from '../../constant'; +import { + JsonSchemaDataType, + Operations, + initialDataOperationsValues, +} from '../../constant'; import { useFormValues } from '../../hooks/use-form-values'; import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { INextOperatorForm } from '../../interface'; +import { buildOutputList } from '../../utils/build-output-list'; import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; +import { QueryVariableList } from '../components/query-variable-list'; +import { FilterValues } from './filter-values'; +import { SelectKeys } from './select-keys'; +import { Updates } from './updates'; export const RetrievalPartialSchema = { - select_operation: z.string(), + inputs: z.array(z.object({ input: z.string().optional() })), + operations: z.string(), + select_keys: z.array(z.object({ name: z.string().optional() })).optional(), + remove_keys: z.array(z.object({ name: z.string().optional() })).optional(), + updates: z + .array( + z.object({ key: z.string().optional(), value: z.string().optional() }), + ) + .optional(), + rename_keys: z + .array( + z.object({ + old_key: z.string().optional(), + new_key: z.string().optional(), + }), + ) + .optional(), + filter_values: z + .array( + z.object({ + key: z.string().optional(), + value: z.string().optional(), + operator: z.string().optional(), + }), + ) + .optional(), }; export const FormSchema = z.object(RetrievalPartialSchema); +export type DataOperationsFormSchemaType = z.infer; + +const outputList = buildOutputList(initialDataOperationsValues.outputs); + function DataOperationsForm({ node }: INextOperatorForm) { const { t } = useTranslation(); const defaultValues = useFormValues(initialDataOperationsValues, node); - const form = useForm({ + const form = useForm({ defaultValues: defaultValues, resolver: zodResolver(FormSchema), + shouldUnregister: true, }); + const operations = useWatch({ control: form.control, name: 'operations' }); + + const OperationsOptions = buildOptions( + Operations, + t, + `flow.operationsOptions`, + true, + ); + useWatchFormChange(node?.id, form); return (
    - - +
    + {t('flow.query')} + +
    + + - - + {operations === Operations.SelectKeys && ( + + )} + {operations === Operations.RemoveKeys && ( + + )} + {operations === Operations.AppendOrUpdate && ( + + )} + {operations === Operations.RenameKeys && ( + + )} + {operations === Operations.FilterValues && ( + + )} +
    ); diff --git a/web/src/pages/agent/form/data-operations-form/select-keys.tsx b/web/src/pages/agent/form/data-operations-form/select-keys.tsx new file mode 100644 index 000000000..918ca977e --- /dev/null +++ b/web/src/pages/agent/form/data-operations-form/select-keys.tsx @@ -0,0 +1,49 @@ +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { BlockButton, Button } from '@/components/ui/button'; +import { FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type SelectKeysProps = { + name: string; + label: ReactNode; + tooltip?: string; +}; +export function SelectKeys({ name, label, tooltip }: SelectKeysProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
    + {label} +
    + {fields.map((field, index) => { + const nameField = `${name}.${index}.name`; + + return ( +
    + + + + +
    + ); + })} +
    + + append({ name: '' })}> + {t('common.add')} + +
    + ); +} diff --git a/web/src/pages/agent/form/data-operations-form/updates.tsx b/web/src/pages/agent/form/data-operations-form/updates.tsx new file mode 100644 index 000000000..953c89d36 --- /dev/null +++ b/web/src/pages/agent/form/data-operations-form/updates.tsx @@ -0,0 +1,61 @@ +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { BlockButton, Button } from '@/components/ui/button'; +import { FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type SelectKeysProps = { + name: string; + label: ReactNode; + tooltip?: string; + keyField: string; + valueField: string; +}; +export function Updates({ + name, + label, + tooltip, + keyField, + valueField, +}: SelectKeysProps) { + const { t } = useTranslation(); + const form = useFormContext(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
    + {label} +
    + {fields.map((field, index) => { + const keyFieldAlias = `${name}.${index}.${keyField}`; + const valueFieldAlias = `${name}.${index}.${valueField}`; + + return ( +
    + + + + + + + +
    + ); + })} +
    + + append({ [keyField]: '', [valueField]: '' })}> + {t('common.add')} + +
    + ); +} diff --git a/web/src/pages/agent/form/iteration-form/index.tsx b/web/src/pages/agent/form/iteration-form/index.tsx index c70b764fb..6f51195b1 100644 --- a/web/src/pages/agent/form/iteration-form/index.tsx +++ b/web/src/pages/agent/form/iteration-form/index.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { memo, useMemo } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; -import { VariableType } from '../../constant'; +import { JsonSchemaDataType } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; @@ -44,7 +44,7 @@ function IterationForm({ node }: INextOperatorForm) { diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index 9757b436b..89fd06ede 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -29,6 +29,7 @@ import { NodeHandleId, Operator, } from './constant'; +import { DataOperationsFormSchemaType } from './form/data-operations-form'; import { ExtractorFormSchemaType } from './form/extractor-form'; import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form'; import { ParserFormSchemaType } from './form/parser-form'; @@ -267,6 +268,15 @@ function transformExtractorParams(params: ExtractorFormSchemaType) { return { ...params, prompts: [{ content: params.prompts, role: 'user' }] }; } +function transformDataOperationsParams(params: DataOperationsFormSchemaType) { + return { + ...params, + select_keys: params?.select_keys?.map((x) => x.name), + remove_keys: params?.remove_keys?.map((x) => x.name), + inputs: params.inputs.map((x) => x.input), + }; +} + // construct a dsl based on the node information of the graph export const buildDslComponentsByGraph = ( nodes: RAGFlowNodeType[], @@ -313,6 +323,9 @@ export const buildDslComponentsByGraph = ( case Operator.Extractor: params = transformExtractorParams(params); break; + case Operator.DataOperations: + params = transformDataOperationsParams(params); + break; default: break; diff --git a/web/src/pages/agent/utils/filter-agent-structured-output.ts b/web/src/pages/agent/utils/filter-agent-structured-output.ts index 190d15333..b83193036 100644 --- a/web/src/pages/agent/utils/filter-agent-structured-output.ts +++ b/web/src/pages/agent/utils/filter-agent-structured-output.ts @@ -4,14 +4,14 @@ import { JsonSchemaDataType } from '../constant'; export function hasSpecificTypeChild( data: Record | Array, - type: string, + types: string[] = [], ) { if (Array.isArray(data)) { for (const value of data) { - if (isPlainObject(value) && value.type === type) { + if (isPlainObject(value) && types.some((x) => x === value.type)) { return true; } - if (hasSpecificTypeChild(value, type)) { + if (hasSpecificTypeChild(value, types)) { return true; } } @@ -19,11 +19,11 @@ export function hasSpecificTypeChild( if (isPlainObject(data)) { for (const value of Object.values(data)) { - if (isPlainObject(value) && value.type === type) { + if (isPlainObject(value) && types.some((x) => x === value.type)) { return true; } - if (hasSpecificTypeChild(value, type)) { + if (hasSpecificTypeChild(value, types)) { return true; } } @@ -33,7 +33,7 @@ export function hasSpecificTypeChild( } export function hasArrayChild(data: Record | Array) { - return hasSpecificTypeChild(data, JsonSchemaDataType.Array); + return hasSpecificTypeChild(data, [JsonSchemaDataType.Array]); } export function hasJsonSchemaChild(data: JSONSchema) { diff --git a/web/src/utils/form.ts b/web/src/utils/form.ts index ecb56c424..cb091efcc 100644 --- a/web/src/utils/form.ts +++ b/web/src/utils/form.ts @@ -1,5 +1,6 @@ import { variableEnabledFieldMap } from '@/constants/chat'; import { TFunction } from 'i18next'; +import { camelCase } from 'lodash'; import omit from 'lodash/omit'; // chat model setting and generate operator @@ -32,11 +33,12 @@ export function buildOptions( data: Record, t?: TFunction<['translation', ...string[]], undefined>, prefix?: string, + camel: boolean = false, ) { if (t) { return Object.values(data).map((val) => ({ label: t( - `${prefix ? prefix + '.' : ''}${typeof val === 'string' ? val.toLowerCase() : val}`, + `${prefix ? prefix + '.' : ''}${typeof val === 'string' ? (camel ? camelCase(val) : val.toLowerCase()) : val}`, ), value: val, }));