mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Add a form with data operations operators #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -17,6 +17,7 @@ type RAGFlowFormItemProps = {
|
|||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RAGFlowFormItem({
|
export function RAGFlowFormItem({
|
||||||
@ -27,6 +28,7 @@ export function RAGFlowFormItem({
|
|||||||
horizontal = false,
|
horizontal = false,
|
||||||
required = false,
|
required = false,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
|
className,
|
||||||
}: RAGFlowFormItemProps) {
|
}: RAGFlowFormItemProps) {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
return (
|
return (
|
||||||
@ -35,9 +37,12 @@ export function RAGFlowFormItem({
|
|||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem
|
<FormItem
|
||||||
className={cn({
|
className={cn(
|
||||||
|
{
|
||||||
'flex items-center': horizontal,
|
'flex items-center': horizontal,
|
||||||
})}
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<FormLabel
|
<FormLabel
|
||||||
|
|||||||
@ -1728,6 +1728,16 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
structuredOutput: 'Structured output',
|
structuredOutput: 'Structured output',
|
||||||
},
|
},
|
||||||
|
operations: 'Operations',
|
||||||
|
operationsOptions: {
|
||||||
|
selectKeys: 'Select keys',
|
||||||
|
literalEval: 'Literal eval',
|
||||||
|
combine: 'Combine',
|
||||||
|
filterValues: 'Filter values',
|
||||||
|
appendOrUpdate: 'Append or update',
|
||||||
|
removeKeys: 'Remove keys',
|
||||||
|
renameKeys: 'Rename keys',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
llmTools: {
|
llmTools: {
|
||||||
bad_calculator: {
|
bad_calculator: {
|
||||||
|
|||||||
@ -1609,6 +1609,16 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
|||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
structuredOutput: '结构化输出',
|
structuredOutput: '结构化输出',
|
||||||
},
|
},
|
||||||
|
operations: '操作',
|
||||||
|
operationsOptions: {
|
||||||
|
selectKeys: '选择键',
|
||||||
|
literalEval: '字面值求值',
|
||||||
|
combine: '合并',
|
||||||
|
filterValues: '筛选值',
|
||||||
|
appendOrUpdate: '追加或更新',
|
||||||
|
removeKeys: '删除键',
|
||||||
|
renameKeys: '重命名键',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
profile: 'All rights reserved @ React',
|
profile: 'All rights reserved @ React',
|
||||||
|
|||||||
@ -715,8 +715,23 @@ export const initialPlaceholderValues = {
|
|||||||
// It's just a visual placeholder
|
// It's just a visual placeholder
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum Operations {
|
||||||
|
SelectKeys = 'select keys',
|
||||||
|
LiteralEval = 'literal eval',
|
||||||
|
Combine = 'combine',
|
||||||
|
FilterValues = 'filter values',
|
||||||
|
AppendOrUpdate = 'append or update',
|
||||||
|
RemoveKeys = 'remove keys',
|
||||||
|
RenameKeys = 'rename keys',
|
||||||
|
}
|
||||||
|
|
||||||
export const initialDataOperationsValues = {
|
export const initialDataOperationsValues = {
|
||||||
outputs: {},
|
operations: Operations.SelectKeys,
|
||||||
|
outputs: {
|
||||||
|
result: {
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CategorizeAnchorPointPositions = [
|
export const CategorizeAnchorPointPositions = [
|
||||||
|
|||||||
46
web/src/pages/agent/form/components/query-variable-list.tsx
Normal file
46
web/src/pages/agent/form/components/query-variable-list.tsx
Normal file
@ -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 (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const nameField = `${name}.${index}.input`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<QueryVariable
|
||||||
|
name={nameField}
|
||||||
|
hideLabel
|
||||||
|
className="flex-1"
|
||||||
|
types={types}
|
||||||
|
></QueryVariable>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X className="text-text-sub-title-invert " />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<BlockButton onClick={() => append({ input: '' })}>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,24 +5,28 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { toLower } from 'lodash';
|
import { isEmpty, toLower } from 'lodash';
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { VariableType } from '../../constant';
|
import { JsonSchemaDataType } from '../../constant';
|
||||||
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
|
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
|
||||||
|
|
||||||
type QueryVariableProps = {
|
type QueryVariableProps = {
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: VariableType;
|
types?: JsonSchemaDataType[];
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
hideLabel?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QueryVariable({
|
export function QueryVariable({
|
||||||
name = 'query',
|
name = 'query',
|
||||||
type,
|
types = [],
|
||||||
label,
|
label,
|
||||||
|
hideLabel = false,
|
||||||
|
className,
|
||||||
}: QueryVariableProps) {
|
}: QueryVariableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
@ -30,23 +34,25 @@ export function QueryVariable({
|
|||||||
const nextOptions = useBuildQueryVariableOptions();
|
const nextOptions = useBuildQueryVariableOptions();
|
||||||
|
|
||||||
const finalOptions = useMemo(() => {
|
const finalOptions = useMemo(() => {
|
||||||
return type
|
return !isEmpty(types)
|
||||||
? nextOptions.map((x) => {
|
? nextOptions.map((x) => {
|
||||||
return {
|
return {
|
||||||
...x,
|
...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;
|
||||||
}, [nextOptions, type]);
|
}, [nextOptions, types]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className={className}>
|
||||||
{label || (
|
{hideLabel || label || (
|
||||||
<FormLabel tooltip={t('flow.queryTip')}>
|
<FormLabel tooltip={t('flow.queryTip')}>
|
||||||
{t('flow.query')}
|
{t('flow.query')}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@ -56,7 +62,7 @@ export function QueryVariable({
|
|||||||
options={finalOptions}
|
options={finalOptions}
|
||||||
{...field}
|
{...field}
|
||||||
// allowClear
|
// allowClear
|
||||||
type={type}
|
types={types}
|
||||||
></GroupedSelectWithSecondaryMenu>
|
></GroupedSelectWithSecondaryMenu>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { ChevronDownIcon, XIcon } from 'lucide-react';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { VariableType } from '../../constant';
|
import { JsonSchemaDataType } from '../../constant';
|
||||||
import {
|
import {
|
||||||
useFindAgentStructuredOutputLabel,
|
useFindAgentStructuredOutputLabel,
|
||||||
useShowSecondaryMenu,
|
useShowSecondaryMenu,
|
||||||
@ -52,7 +52,7 @@ interface GroupedSelectWithSecondaryMenuProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
type?: VariableType;
|
types?: JsonSchemaDataType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupedSelectWithSecondaryMenu({
|
export function GroupedSelectWithSecondaryMenu({
|
||||||
@ -60,7 +60,7 @@ export function GroupedSelectWithSecondaryMenu({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
type,
|
types,
|
||||||
}: GroupedSelectWithSecondaryMenuProps) {
|
}: GroupedSelectWithSecondaryMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
@ -157,7 +157,7 @@ export function GroupedSelectWithSecondaryMenu({
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
data={option}
|
data={option}
|
||||||
click={handleSecondaryMenuClick}
|
click={handleSecondaryMenuClick}
|
||||||
type={type}
|
types={types}
|
||||||
></StructuredOutputSecondaryMenu>
|
></StructuredOutputSecondaryMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { get, isEmpty, isPlainObject } from 'lodash';
|
|||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { PropsWithChildren, ReactNode, useCallback } from 'react';
|
import { PropsWithChildren, ReactNode, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { JsonSchemaDataType, VariableType } from '../../constant';
|
import { JsonSchemaDataType } from '../../constant';
|
||||||
import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output';
|
import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output';
|
||||||
import {
|
import {
|
||||||
hasJsonSchemaChild,
|
hasJsonSchemaChild,
|
||||||
@ -20,13 +20,13 @@ type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode };
|
|||||||
type StructuredOutputSecondaryMenuProps = {
|
type StructuredOutputSecondaryMenuProps = {
|
||||||
data: DataItem;
|
data: DataItem;
|
||||||
click(option: { label: ReactNode; value: string }): void;
|
click(option: { label: ReactNode; value: string }): void;
|
||||||
type?: VariableType | JsonSchemaDataType;
|
types?: JsonSchemaDataType[];
|
||||||
} & PropsWithChildren;
|
} & PropsWithChildren;
|
||||||
|
|
||||||
export function StructuredOutputSecondaryMenu({
|
export function StructuredOutputSecondaryMenu({
|
||||||
data,
|
data,
|
||||||
click,
|
click,
|
||||||
type,
|
types = [],
|
||||||
}: StructuredOutputSecondaryMenuProps) {
|
}: StructuredOutputSecondaryMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filterStructuredOutput = useGetStructuredOutputByValue();
|
const filterStructuredOutput = useGetStructuredOutputByValue();
|
||||||
@ -35,18 +35,21 @@ export function StructuredOutputSecondaryMenu({
|
|||||||
const handleSubMenuClick = useCallback(
|
const handleSubMenuClick = useCallback(
|
||||||
(option: { label: ReactNode; value: string }, dataType?: string) => () => {
|
(option: { label: ReactNode; value: string }, dataType?: string) => () => {
|
||||||
// The query variable of the iteration operator can only select array type data.
|
// 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(option);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[click, type],
|
[click, types],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMenuClick = useCallback(() => {
|
const handleMenuClick = useCallback(() => {
|
||||||
if (isEmpty(type) || type === JsonSchemaDataType.Object) {
|
if (isEmpty(types) || types?.some((x) => x === JsonSchemaDataType.Object)) {
|
||||||
click(data);
|
click(data);
|
||||||
}
|
}
|
||||||
}, [click, data, type]);
|
}, [click, data, types]);
|
||||||
|
|
||||||
const renderAgentStructuredOutput = useCallback(
|
const renderAgentStructuredOutput = useCallback(
|
||||||
(values: any, option: { label: ReactNode; value: string }) => {
|
(values: any, option: { label: ReactNode; value: string }) => {
|
||||||
@ -62,10 +65,10 @@ export function StructuredOutputSecondaryMenu({
|
|||||||
const dataType = get(value, 'type');
|
const dataType = get(value, 'type');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!type ||
|
isEmpty(types) ||
|
||||||
(type &&
|
(!isEmpty(types) &&
|
||||||
(dataType === type ||
|
(types?.some((x) => x === dataType) ||
|
||||||
hasSpecificTypeChild(value ?? {}, type)))
|
hasSpecificTypeChild(value ?? {}, types)))
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<li key={key} className="pl-1">
|
<li key={key} className="pl-1">
|
||||||
@ -90,10 +93,13 @@ export function StructuredOutputSecondaryMenu({
|
|||||||
|
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
},
|
},
|
||||||
[handleSubMenuClick, type],
|
[handleSubMenuClick, types],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasJsonSchemaChild(structuredOutput)) {
|
if (
|
||||||
|
!hasJsonSchemaChild(structuredOutput) ||
|
||||||
|
(!isEmpty(types) && !hasSpecificTypeChild(structuredOutput, types))
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<FormLabel tooltip={tooltip}>{label}</FormLabel>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const keyFieldAlias = `${name}.${index}.${keyField}`;
|
||||||
|
const valueFieldAlias = `${name}.${index}.${valueField}`;
|
||||||
|
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<RAGFlowFormItem name={keyFieldAlias} className="flex-1">
|
||||||
|
<Input></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Separator orientation="vertical" className="h-2.5" />
|
||||||
|
|
||||||
|
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
|
||||||
|
<SelectWithSearch {...field} options={[]}></SelectWithSearch>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Separator orientation="vertical" className="h-2.5" />
|
||||||
|
|
||||||
|
<RAGFlowFormItem name={valueFieldAlias} className="flex-1">
|
||||||
|
<Input></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BlockButton onClick={() => append({ [keyField]: '', [valueField]: '' })}>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,44 +1,132 @@
|
|||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
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 { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { initialDataOperationsValues } from '../../constant';
|
import {
|
||||||
|
JsonSchemaDataType,
|
||||||
|
Operations,
|
||||||
|
initialDataOperationsValues,
|
||||||
|
} from '../../constant';
|
||||||
import { useFormValues } from '../../hooks/use-form-values';
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
import { INextOperatorForm } from '../../interface';
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
import { FormWrapper } from '../components/form-wrapper';
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
import { Output } from '../components/output';
|
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 = {
|
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 const FormSchema = z.object(RetrievalPartialSchema);
|
||||||
|
|
||||||
|
export type DataOperationsFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialDataOperationsValues.outputs);
|
||||||
|
|
||||||
function DataOperationsForm({ node }: INextOperatorForm) {
|
function DataOperationsForm({ node }: INextOperatorForm) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const defaultValues = useFormValues(initialDataOperationsValues, node);
|
const defaultValues = useFormValues(initialDataOperationsValues, node);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm<DataOperationsFormSchemaType>({
|
||||||
defaultValues: defaultValues,
|
defaultValues: defaultValues,
|
||||||
resolver: zodResolver(FormSchema),
|
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);
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<RAGFlowFormItem name="query" label={t('flow.query')}>
|
<div className="space-y-2">
|
||||||
<SelectWithSearch options={[]} allowClear />
|
<FormLabel tooltip={t('flow.queryTip')}>{t('flow.query')}</FormLabel>
|
||||||
|
<QueryVariableList
|
||||||
|
types={[JsonSchemaDataType.Array, JsonSchemaDataType.Object]}
|
||||||
|
></QueryVariableList>
|
||||||
|
</div>
|
||||||
|
<RAGFlowFormItem name="operations" label={t('flow.operations')}>
|
||||||
|
<SelectWithSearch options={OperationsOptions} allowClear />
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
|
{operations === Operations.SelectKeys && (
|
||||||
<Output list={[]}></Output>
|
<SelectKeys
|
||||||
|
name="select_keys"
|
||||||
|
label={t('flow.operationsOptions.selectKeys')}
|
||||||
|
></SelectKeys>
|
||||||
|
)}
|
||||||
|
{operations === Operations.RemoveKeys && (
|
||||||
|
<SelectKeys
|
||||||
|
name="remove_keys"
|
||||||
|
label={t('flow.operationsOptions.removeKeys')}
|
||||||
|
></SelectKeys>
|
||||||
|
)}
|
||||||
|
{operations === Operations.AppendOrUpdate && (
|
||||||
|
<Updates
|
||||||
|
name="updates"
|
||||||
|
label={t('flow.operationsOptions.updates')}
|
||||||
|
keyField="key"
|
||||||
|
valueField="value"
|
||||||
|
></Updates>
|
||||||
|
)}
|
||||||
|
{operations === Operations.RenameKeys && (
|
||||||
|
<Updates
|
||||||
|
name="rename_keys"
|
||||||
|
label={t('flow.operationsOptions.renameKeys')}
|
||||||
|
keyField="old_key"
|
||||||
|
valueField="new_key"
|
||||||
|
></Updates>
|
||||||
|
)}
|
||||||
|
{operations === Operations.FilterValues && (
|
||||||
|
<FilterValues
|
||||||
|
name="filter_values"
|
||||||
|
label={t('flow.operationsOptions.filterValues')}
|
||||||
|
></FilterValues>
|
||||||
|
)}
|
||||||
|
<Output list={outputList}></Output>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<FormLabel tooltip={tooltip}>{label}</FormLabel>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const nameField = `${name}.${index}.name`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<RAGFlowFormItem name={nameField} className="flex-1">
|
||||||
|
<Input></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BlockButton onClick={() => append({ name: '' })}>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
web/src/pages/agent/form/data-operations-form/updates.tsx
Normal file
61
web/src/pages/agent/form/data-operations-form/updates.tsx
Normal file
@ -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 (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<FormLabel tooltip={tooltip}>{label}</FormLabel>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const keyFieldAlias = `${name}.${index}.${keyField}`;
|
||||||
|
const valueFieldAlias = `${name}.${index}.${valueField}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<RAGFlowFormItem name={keyFieldAlias} className="flex-1">
|
||||||
|
<Input></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<RAGFlowFormItem name={valueFieldAlias} className="flex-1">
|
||||||
|
<Input></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BlockButton onClick={() => append({ [keyField]: '', [valueField]: '' })}>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { VariableType } from '../../constant';
|
import { JsonSchemaDataType } from '../../constant';
|
||||||
import { INextOperatorForm } from '../../interface';
|
import { INextOperatorForm } from '../../interface';
|
||||||
import { FormWrapper } from '../components/form-wrapper';
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
import { Output } from '../components/output';
|
import { Output } from '../components/output';
|
||||||
@ -44,7 +44,7 @@ function IterationForm({ node }: INextOperatorForm) {
|
|||||||
<FormContainer>
|
<FormContainer>
|
||||||
<QueryVariable
|
<QueryVariable
|
||||||
name="items_ref"
|
name="items_ref"
|
||||||
type={VariableType.Array}
|
types={[JsonSchemaDataType.Array]}
|
||||||
></QueryVariable>
|
></QueryVariable>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
<DynamicOutput node={node}></DynamicOutput>
|
<DynamicOutput node={node}></DynamicOutput>
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
NodeHandleId,
|
NodeHandleId,
|
||||||
Operator,
|
Operator,
|
||||||
} from './constant';
|
} from './constant';
|
||||||
|
import { DataOperationsFormSchemaType } from './form/data-operations-form';
|
||||||
import { ExtractorFormSchemaType } from './form/extractor-form';
|
import { ExtractorFormSchemaType } from './form/extractor-form';
|
||||||
import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form';
|
import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form';
|
||||||
import { ParserFormSchemaType } from './form/parser-form';
|
import { ParserFormSchemaType } from './form/parser-form';
|
||||||
@ -267,6 +268,15 @@ function transformExtractorParams(params: ExtractorFormSchemaType) {
|
|||||||
return { ...params, prompts: [{ content: params.prompts, role: 'user' }] };
|
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
|
// construct a dsl based on the node information of the graph
|
||||||
export const buildDslComponentsByGraph = (
|
export const buildDslComponentsByGraph = (
|
||||||
nodes: RAGFlowNodeType[],
|
nodes: RAGFlowNodeType[],
|
||||||
@ -313,6 +323,9 @@ export const buildDslComponentsByGraph = (
|
|||||||
case Operator.Extractor:
|
case Operator.Extractor:
|
||||||
params = transformExtractorParams(params);
|
params = transformExtractorParams(params);
|
||||||
break;
|
break;
|
||||||
|
case Operator.DataOperations:
|
||||||
|
params = transformDataOperationsParams(params);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import { JsonSchemaDataType } from '../constant';
|
|||||||
|
|
||||||
export function hasSpecificTypeChild(
|
export function hasSpecificTypeChild(
|
||||||
data: Record<string, any> | Array<any>,
|
data: Record<string, any> | Array<any>,
|
||||||
type: string,
|
types: string[] = [],
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
for (const value of data) {
|
for (const value of data) {
|
||||||
if (isPlainObject(value) && value.type === type) {
|
if (isPlainObject(value) && types.some((x) => x === value.type)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hasSpecificTypeChild(value, type)) {
|
if (hasSpecificTypeChild(value, types)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,11 +19,11 @@ export function hasSpecificTypeChild(
|
|||||||
|
|
||||||
if (isPlainObject(data)) {
|
if (isPlainObject(data)) {
|
||||||
for (const value of Object.values(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasSpecificTypeChild(value, type)) {
|
if (hasSpecificTypeChild(value, types)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ export function hasSpecificTypeChild(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasArrayChild(data: Record<string, any> | Array<any>) {
|
export function hasArrayChild(data: Record<string, any> | Array<any>) {
|
||||||
return hasSpecificTypeChild(data, JsonSchemaDataType.Array);
|
return hasSpecificTypeChild(data, [JsonSchemaDataType.Array]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasJsonSchemaChild(data: JSONSchema) {
|
export function hasJsonSchemaChild(data: JSONSchema) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { variableEnabledFieldMap } from '@/constants/chat';
|
import { variableEnabledFieldMap } from '@/constants/chat';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
|
import { camelCase } from 'lodash';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
// chat model setting and generate operator
|
// chat model setting and generate operator
|
||||||
@ -32,11 +33,12 @@ export function buildOptions(
|
|||||||
data: Record<string, any>,
|
data: Record<string, any>,
|
||||||
t?: TFunction<['translation', ...string[]], undefined>,
|
t?: TFunction<['translation', ...string[]], undefined>,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
|
camel: boolean = false,
|
||||||
) {
|
) {
|
||||||
if (t) {
|
if (t) {
|
||||||
return Object.values(data).map((val) => ({
|
return Object.values(data).map((val) => ({
|
||||||
label: t(
|
label: t(
|
||||||
`${prefix ? prefix + '.' : ''}${typeof val === 'string' ? val.toLowerCase() : val}`,
|
`${prefix ? prefix + '.' : ''}${typeof val === 'string' ? (camel ? camelCase(val) : val.toLowerCase()) : val}`,
|
||||||
),
|
),
|
||||||
value: val,
|
value: val,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user