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

View File

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

View File

@ -8,6 +8,7 @@ import {
AgentStructuredOutputField,
CodeTemplateStrMap,
ComparisonOperator,
JsonSchemaDataType,
Operator,
ProgrammingLanguage,
SwitchOperatorOptions,
@ -610,15 +611,15 @@ export const initialListOperationsValues = {
query: '',
operations: ListOperations.TopN,
outputs: {
result: {
type: 'Array<?>',
},
first: {
type: '?',
},
last: {
type: '?',
},
// result: {
// type: 'Array<?>',
// },
// first: {
// type: '?',
// },
// last: {
// type: '?',
// },
},
};
@ -874,3 +875,22 @@ export enum ExportFileType {
Markdown = 'md',
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 { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { JsonSchemaDataType } from '../../constant';
import { ArrayFields } 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) {
<FormContainer>
<QueryVariable
name="items_ref"
types={[JsonSchemaDataType.Array]}
types={ArrayFields as any[]}
></QueryVariable>
</FormContainer>
<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 { buildOptions } from '@/utils/form';
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 { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
ArrayFields,
DataOperationsOperatorOptions,
JsonSchemaDataType,
ListOperations,
SortMethod,
initialListOperationsValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { getArrayElementType } from '../../utils';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output, OutputSchema } from '../components/output';
@ -36,7 +38,7 @@ import { QueryVariable } from '../components/query-variable';
export const RetrievalPartialSchema = {
query: z.string(),
operations: z.string(),
n: z.number().int().min(0).optional(),
n: z.number().int().min(1).optional(),
sort_method: z.string().optional(),
filter: z
.object({
@ -47,26 +49,68 @@ export const RetrievalPartialSchema = {
...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 type ListOperationsFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialListOperationsValues.outputs);
function ListOperationsForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const { getType } = useGetVariableLabelOrTypeByValue();
const defaultValues = useFormValues(initialListOperationsValues, node);
const form = useForm<ListOperationsFormSchemaType>({
defaultValues: defaultValues,
mode: 'onChange',
resolver: zodResolver(FormSchema),
shouldUnregister: true,
// shouldUnregister: true,
});
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(
ListOperations,
t,
@ -79,9 +123,39 @@ function ListOperationsForm({ node }: INextOperatorForm) {
`flow.SortMethodOptions`,
true,
);
const operatorOptions = useBuildSwitchOperatorOptions(
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);
return (
@ -90,37 +164,46 @@ function ListOperationsForm({ node }: INextOperatorForm) {
<QueryVariable
name="query"
className="flex-1"
types={[JsonSchemaDataType.Array]}
types={ArrayFields as any[]}
></QueryVariable>
<Separator />
<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>
{[
ListOperations.TopN,
ListOperations.Head,
ListOperations.Tail,
].includes(operations as ListOperations) && (
{showNum && (
<FormField
control={form.control}
name="n"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flowNum')}</FormLabel>
<FormLabel>{t('flow.flowNum')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
<NumberInput
{...field}
className="w-full"
min={1}
></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{[ListOperations.Sort].includes(operations as ListOperations) && (
{showSortMethod && (
<RAGFlowFormItem name="sort_method" label={t('flow.sortMethod')}>
<SelectWithSearch options={SortMethodOptions} />
</RAGFlowFormItem>
)}
{[ListOperations.Filter].includes(operations as ListOperations) && (
{showFilter && (
<div className="flex items-center gap-2">
<RAGFlowFormItem name="filter.operator" className="flex-1">
<SelectWithSearch options={operatorOptions}></SelectWithSearch>

View File

@ -19,6 +19,7 @@ import {
VariableAssignerLogicalOperator,
} from '../../constant';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import { getArrayElementType } from '../../utils';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
import { QueryVariable } from '../components/query-variable';
import { useBuildLogicalOptions } from './use-build-logical-options';
@ -152,9 +153,13 @@ export function DynamicVariables({
} else if (
logicalOperator === VariableAssignerLogicalArrayOperator.Append
) {
const subType = type.match(/<([^>]+)>/).at(1);
const subType = getArrayElementType(type);
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 { buildSelectOptions } from '@/utils/component-util';
import { t } from 'i18next';
import { TypesWithArray } from '../constant';
export { TypesWithArray } from '../constant';
// const TypesWithoutArray = Object.values(JsonSchemaDataType).filter(
// (item) => item !== JsonSchemaDataType.Array,
// );
@ -9,17 +11,6 @@ import { t } from 'i18next';
// ...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 = [
{
label: t('flow.name'),

View File

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