Feat: Set the outputs type of list operation. #10427 (#11366)

### What problem does this PR solve?

Feat: Set the outputs type of list operation. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-19 13:59:43 +08:00
committed by GitHub
parent 0884e9a4d9
commit 971197d595
8 changed files with 148 additions and 42 deletions

View File

@ -6,6 +6,7 @@ interface NumberInputProps {
value?: number; value?: number;
onChange?: (value: number) => void; onChange?: (value: number) => void;
height?: number | string; height?: number | string;
min?: number;
} }
const NumberInput: React.FC<NumberInputProps> = ({ const NumberInput: React.FC<NumberInputProps> = ({
@ -13,6 +14,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
value: initialValue, value: initialValue,
onChange, onChange,
height, height,
min = 0,
}) => { }) => {
const [value, setValue] = useState<number>(() => { const [value, setValue] = useState<number>(() => {
return initialValue ?? 0; return initialValue ?? 0;
@ -76,6 +78,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
onChange={handleChange} onChange={handleChange}
className="w-full flex-1 text-center bg-transparent focus:outline-none" className="w-full flex-1 text-center bg-transparent focus:outline-none"
style={style} style={style}
min={min}
/> />
<button <button
type="button" type="button"

View File

@ -1062,7 +1062,7 @@ Example: https://fsn1.your-objectstorage.com`,
apiKeyPlaceholder: apiKeyPlaceholder:
'YOUR_API_KEY (obtained from https://serpapi.com/manage-api-key)', 'YOUR_API_KEY (obtained from https://serpapi.com/manage-api-key)',
flowStart: 'Start', flowStart: 'Start',
flowNum: 'Num', flowNum: 'N',
test: 'Test', test: 'Test',
extractDepth: 'Extract Depth', extractDepth: 'Extract Depth',
format: 'Format', format: 'Format',

View File

@ -8,6 +8,7 @@ import {
AgentStructuredOutputField, AgentStructuredOutputField,
CodeTemplateStrMap, CodeTemplateStrMap,
ComparisonOperator, ComparisonOperator,
JsonSchemaDataType,
Operator, Operator,
ProgrammingLanguage, ProgrammingLanguage,
SwitchOperatorOptions, SwitchOperatorOptions,
@ -610,15 +611,15 @@ export const initialListOperationsValues = {
query: '', query: '',
operations: ListOperations.TopN, operations: ListOperations.TopN,
outputs: { outputs: {
result: { // result: {
type: 'Array<?>', // type: 'Array<?>',
}, // },
first: { // first: {
type: '?', // type: '?',
}, // },
last: { // last: {
type: '?', // type: '?',
}, // },
}, },
}; };
@ -874,3 +875,22 @@ export enum ExportFileType {
Markdown = 'md', Markdown = 'md',
DOCX = 'docx', DOCX = 'docx',
} }
export enum TypesWithArray {
String = 'string',
Number = 'number',
Boolean = 'boolean',
Object = 'object',
ArrayString = 'array<string>',
ArrayNumber = 'array<number>',
ArrayBoolean = 'array<boolean>',
ArrayObject = 'array<object>',
}
export const ArrayFields = [
JsonSchemaDataType.Array,
TypesWithArray.ArrayBoolean,
TypesWithArray.ArrayNumber,
TypesWithArray.ArrayString,
TypesWithArray.ArrayObject,
];

View File

@ -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 { JsonSchemaDataType } from '../../constant'; import { ArrayFields } 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"
types={[JsonSchemaDataType.Array]} types={ArrayFields as any[]}
></QueryVariable> ></QueryVariable>
</FormContainer> </FormContainer>
<DynamicOutput node={node}></DynamicOutput> <DynamicOutput node={node}></DynamicOutput>

View File

@ -13,20 +13,22 @@ import { Separator } from '@/components/ui/separator';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options'; import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react'; import { memo, useCallback, useEffect, useMemo } from 'react';
import { useForm, useWatch } 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 { import {
ArrayFields,
DataOperationsOperatorOptions, DataOperationsOperatorOptions,
JsonSchemaDataType,
ListOperations, ListOperations,
SortMethod, SortMethod,
initialListOperationsValues, initialListOperationsValues,
} from '../../constant'; } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values'; import { useFormValues } from '../../hooks/use-form-values';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
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 { getArrayElementType } from '../../utils';
import { buildOutputList } from '../../utils/build-output-list'; import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper'; import { FormWrapper } from '../components/form-wrapper';
import { Output, OutputSchema } from '../components/output'; import { Output, OutputSchema } from '../components/output';
@ -36,7 +38,7 @@ import { QueryVariable } from '../components/query-variable';
export const RetrievalPartialSchema = { export const RetrievalPartialSchema = {
query: z.string(), query: z.string(),
operations: z.string(), operations: z.string(),
n: z.number().int().min(0).optional(), n: z.number().int().min(1).optional(),
sort_method: z.string().optional(), sort_method: z.string().optional(),
filter: z filter: z
.object({ .object({
@ -47,26 +49,68 @@ export const RetrievalPartialSchema = {
...OutputSchema, ...OutputSchema,
}; };
const NumFields = [
ListOperations.TopN,
ListOperations.Head,
ListOperations.Tail,
];
function showField(operations: string) {
const showNum = NumFields.includes(operations as ListOperations);
const showSortMethod = [ListOperations.Sort].includes(
operations as ListOperations,
);
const showFilter = [ListOperations.Filter].includes(
operations as ListOperations,
);
return {
showNum,
showSortMethod,
showFilter,
};
}
export const FormSchema = z.object(RetrievalPartialSchema); export const FormSchema = z.object(RetrievalPartialSchema);
export type ListOperationsFormSchemaType = z.infer<typeof FormSchema>; export type ListOperationsFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialListOperationsValues.outputs);
function ListOperationsForm({ node }: INextOperatorForm) { function ListOperationsForm({ node }: INextOperatorForm) {
const { t } = useTranslation(); const { t } = useTranslation();
const { getType } = useGetVariableLabelOrTypeByValue();
const defaultValues = useFormValues(initialListOperationsValues, node); const defaultValues = useFormValues(initialListOperationsValues, node);
const form = useForm<ListOperationsFormSchemaType>({ const form = useForm<ListOperationsFormSchemaType>({
defaultValues: defaultValues, defaultValues: defaultValues,
mode: 'onChange', mode: 'onChange',
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
shouldUnregister: true, // shouldUnregister: true,
}); });
const operations = useWatch({ control: form.control, name: 'operations' }); const operations = useWatch({ control: form.control, name: 'operations' });
const query = useWatch({ control: form.control, name: 'query' });
const subType = getArrayElementType(getType(query));
const currentOutputs = useMemo(() => {
return {
result: {
type: `Array<${subType}>`,
},
first: {
type: subType,
},
last: {
type: subType,
},
};
}, [subType]);
const outputList = buildOutputList(currentOutputs);
const ListOperationsOptions = buildOptions( const ListOperationsOptions = buildOptions(
ListOperations, ListOperations,
t, t,
@ -79,9 +123,39 @@ function ListOperationsForm({ node }: INextOperatorForm) {
`flow.SortMethodOptions`, `flow.SortMethodOptions`,
true, true,
); );
const operatorOptions = useBuildSwitchOperatorOptions( const operatorOptions = useBuildSwitchOperatorOptions(
DataOperationsOperatorOptions, DataOperationsOperatorOptions,
); );
const { showFilter, showNum, showSortMethod } = showField(operations);
const handleOperationsChange = useCallback(
(operations: string) => {
const { showFilter, showNum, showSortMethod } = showField(operations);
if (showNum) {
form.setValue('n', 1, { shouldDirty: true });
}
if (showSortMethod) {
form.setValue('sort_method', SortMethodOptions.at(0)?.value, {
shouldDirty: true,
});
}
if (showFilter) {
form.setValue('filter.operator', operatorOptions.at(0)?.value, {
shouldDirty: true,
});
}
},
[SortMethodOptions, form, operatorOptions],
);
useEffect(() => {
form.setValue('outputs', currentOutputs, { shouldDirty: true });
}, [currentOutputs, form]);
useWatchFormChange(node?.id, form, true); useWatchFormChange(node?.id, form, true);
return ( return (
@ -90,37 +164,46 @@ function ListOperationsForm({ node }: INextOperatorForm) {
<QueryVariable <QueryVariable
name="query" name="query"
className="flex-1" className="flex-1"
types={[JsonSchemaDataType.Array]} types={ArrayFields as any[]}
></QueryVariable> ></QueryVariable>
<Separator /> <Separator />
<RAGFlowFormItem name="operations" label={t('flow.operations')}> <RAGFlowFormItem name="operations" label={t('flow.operations')}>
<SelectWithSearch options={ListOperationsOptions} /> {(field) => (
<SelectWithSearch
options={ListOperationsOptions}
value={field.value}
onChange={(val) => {
handleOperationsChange(val);
field.onChange(val);
}}
/>
)}
</RAGFlowFormItem> </RAGFlowFormItem>
{[ {showNum && (
ListOperations.TopN,
ListOperations.Head,
ListOperations.Tail,
].includes(operations as ListOperations) && (
<FormField <FormField
control={form.control} control={form.control}
name="n" name="n"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('flowNum')}</FormLabel> <FormLabel>{t('flow.flowNum')}</FormLabel>
<FormControl> <FormControl>
<NumberInput {...field} className="w-full"></NumberInput> <NumberInput
{...field}
className="w-full"
min={1}
></NumberInput>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)} )}
{[ListOperations.Sort].includes(operations as ListOperations) && ( {showSortMethod && (
<RAGFlowFormItem name="sort_method" label={t('flow.sortMethod')}> <RAGFlowFormItem name="sort_method" label={t('flow.sortMethod')}>
<SelectWithSearch options={SortMethodOptions} /> <SelectWithSearch options={SortMethodOptions} />
</RAGFlowFormItem> </RAGFlowFormItem>
)} )}
{[ListOperations.Filter].includes(operations as ListOperations) && ( {showFilter && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RAGFlowFormItem name="filter.operator" className="flex-1"> <RAGFlowFormItem name="filter.operator" className="flex-1">
<SelectWithSearch options={operatorOptions}></SelectWithSearch> <SelectWithSearch options={operatorOptions}></SelectWithSearch>

View File

@ -19,6 +19,7 @@ import {
VariableAssignerLogicalOperator, VariableAssignerLogicalOperator,
} from '../../constant'; } from '../../constant';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query'; import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import { getArrayElementType } from '../../utils';
import { DynamicFormHeader } from '../components/dynamic-fom-header'; import { DynamicFormHeader } from '../components/dynamic-fom-header';
import { QueryVariable } from '../components/query-variable'; import { QueryVariable } from '../components/query-variable';
import { useBuildLogicalOptions } from './use-build-logical-options'; import { useBuildLogicalOptions } from './use-build-logical-options';
@ -152,9 +153,13 @@ export function DynamicVariables({
} else if ( } else if (
logicalOperator === VariableAssignerLogicalArrayOperator.Append logicalOperator === VariableAssignerLogicalArrayOperator.Append
) { ) {
const subType = type.match(/<([^>]+)>/).at(1); const subType = getArrayElementType(type);
return ( return (
<QueryVariable types={[subType]} hideLabel pureQuery></QueryVariable> <QueryVariable
types={[subType as JsonSchemaDataType]}
hideLabel
pureQuery
></QueryVariable>
); );
} }
}, },

View File

@ -1,6 +1,8 @@
import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form'; import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
import { buildSelectOptions } from '@/utils/component-util'; import { buildSelectOptions } from '@/utils/component-util';
import { t } from 'i18next'; import { t } from 'i18next';
import { TypesWithArray } from '../constant';
export { TypesWithArray } from '../constant';
// const TypesWithoutArray = Object.values(JsonSchemaDataType).filter( // const TypesWithoutArray = Object.values(JsonSchemaDataType).filter(
// (item) => item !== JsonSchemaDataType.Array, // (item) => item !== JsonSchemaDataType.Array,
// ); // );
@ -9,17 +11,6 @@ import { t } from 'i18next';
// ...TypesWithoutArray.map((item) => `array<${item}>`), // ...TypesWithoutArray.map((item) => `array<${item}>`),
// ]; // ];
export enum TypesWithArray {
String = 'string',
Number = 'number',
Boolean = 'boolean',
Object = 'object',
ArrayString = 'array<string>',
ArrayNumber = 'array<number>',
ArrayBoolean = 'array<boolean>',
ArrayObject = 'array<object>',
}
export const GlobalFormFields = [ export const GlobalFormFields = [
{ {
label: t('flow.name'), label: t('flow.name'),

View File

@ -732,3 +732,7 @@ export function buildBeginQueryWithObject(
return nextInputs; return nextInputs;
} }
export function getArrayElementType(type: string) {
return typeof type === 'string' ? type.match(/<([^>]+)>/)?.at(1) ?? '' : '';
}