Feat: Filter the agent form's large model list by type #3221 (#9049)

### What problem does this PR solve?

Feat: Filter the agent form's large model list by type #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-07-25 19:25:19 +08:00
committed by GitHub
parent c63d12b936
commit ad77f504f9
9 changed files with 331 additions and 170 deletions

View File

@ -1,3 +1,9 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { import {
FormControl, FormControl,
FormField, FormField,
@ -5,27 +11,88 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { useFormContext } from 'react-hook-form'; import { LlmModelType } from '@/constants/knowledge';
import { Funnel } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { NextLLMSelect } from './llm-select/next'; import { NextLLMSelect } from './llm-select/next';
import { Button } from './ui/button';
const ModelTypes = [
{
title: 'All Models',
value: 'all',
},
{
title: 'Text-only Models',
value: LlmModelType.Chat,
},
{
title: 'Multimodal Models',
value: LlmModelType.Image2text,
},
];
export const LargeModelFilterFormSchema = {
llm_filter: z.string().optional(),
};
export function LargeModelFormField() { export function LargeModelFormField() {
const form = useFormContext(); const form = useFormContext();
const { t } = useTranslation(); const { t } = useTranslation();
const filter = useWatch({ control: form.control, name: 'llm_filter' });
return ( return (
<FormField <>
control={form.control} <FormField
name="llm_id" control={form.control}
render={({ field }) => ( name="llm_id"
<FormItem> render={({ field }) => (
<FormLabel tooltip={t('chat.modelTip')}>{t('chat.model')}</FormLabel> <FormItem>
<FormControl> <FormLabel tooltip={t('chat.modelTip')}>
<NextLLMSelect {...field} /> {t('chat.model')}
</FormControl> </FormLabel>
<FormMessage /> <section className="flex gap-2.5">
</FormItem> <FormField
)} control={form.control}
/> name="llm_filter"
render={({ field }) => (
<FormItem>
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant={'ghost'}>
<Funnel />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{ModelTypes.map((x) => (
<DropdownMenuItem
key={x.value}
onClick={() => {
field.onChange(x.value);
}}
>
{x.title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
</FormItem>
)}
/>
<FormControl>
<NextLLMSelect {...field} filter={filter} />
</FormControl>
</section>
<FormMessage />
</FormItem>
)}
/>
</>
); );
} }

View File

@ -12,17 +12,19 @@ interface IProps {
onInitialValue?: (value: string, option: any) => void; onInitialValue?: (value: string, option: any) => void;
onChange?: (value: string) => void; onChange?: (value: string) => void;
disabled?: boolean; disabled?: boolean;
filter?: string;
} }
const NextInnerLLMSelect = forwardRef< const NextInnerLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps IProps
>(({ value, disabled }, ref) => { >(({ value, disabled, filter }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelOptions = useComposeLlmOptionsByModelTypes([ const modelTypes =
LlmModelType.Chat, filter === 'all' || filter === undefined
LlmModelType.Image2text, ? [LlmModelType.Chat, LlmModelType.Image2text]
]); : [filter as LlmModelType];
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
return ( return (
<Select disabled={disabled} value={value}> <Select disabled={disabled} value={value}>
@ -45,7 +47,7 @@ const NextInnerLLMSelect = forwardRef<
</SelectTrigger> </SelectTrigger>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent side={'left'}> <PopoverContent side={'left'}>
<LlmSettingFieldItems></LlmSettingFieldItems> <LlmSettingFieldItems options={modelOptions}></LlmSettingFieldItems>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</Select> </Select>

View File

@ -25,6 +25,7 @@ import { useHandleFreedomChange } from './use-watch-change';
interface LlmSettingFieldItemsProps { interface LlmSettingFieldItemsProps {
prefix?: string; prefix?: string;
options?: any[];
} }
export const LlmSettingSchema = { export const LlmSettingSchema = {
@ -40,9 +41,13 @@ export const LlmSettingSchema = {
maxTokensEnabled: z.boolean(), maxTokensEnabled: z.boolean(),
}; };
export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { export function LlmSettingFieldItems({
prefix,
options,
}: LlmSettingFieldItemsProps) {
const form = useFormContext(); const form = useFormContext();
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const modelOptions = useComposeLlmOptionsByModelTypes([ const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat, LlmModelType.Chat,
LlmModelType.Image2text, LlmModelType.Image2text,
@ -72,30 +77,9 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
<FormLabel>{t('model')}</FormLabel> <FormLabel>{t('model')}</FormLabel>
<FormControl> <FormControl>
<SelectWithSearch <SelectWithSearch
options={modelOptions} options={options || modelOptions}
{...field} {...field}
></SelectWithSearch> ></SelectWithSearch>
{/* <Select onValueChange={field.onChange} {...field}>
<SelectTrigger value={field.value}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{modelOptions.map((x) => (
<SelectGroup key={x.value}>
<SelectLabel>{x.label}</SelectLabel>
{x.options.map((y) => (
<SelectItem
value={y.value}
key={y.value}
disabled={y.disabled}
>
{y.label}
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select> */}
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -1,8 +1,9 @@
'use client'; 'use client';
import { CheckIcon, ChevronDownIcon } from 'lucide-react'; import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react';
import { import {
Fragment, Fragment,
MouseEventHandler,
ReactNode, ReactNode,
forwardRef, forwardRef,
useCallback, useCallback,
@ -28,6 +29,7 @@ import {
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { RAGFlowSelectOptionType } from '../ui/select'; import { RAGFlowSelectOptionType } from '../ui/select';
import { Separator } from '../ui/separator';
export type SelectWithSearchFlagOptionType = { export type SelectWithSearchFlagOptionType = {
label: ReactNode; label: ReactNode;
@ -41,122 +43,159 @@ export type SelectWithSearchFlagProps = {
value?: string; value?: string;
onChange?(value: string): void; onChange?(value: string): void;
triggerClassName?: string; triggerClassName?: string;
allowClear?: boolean;
}; };
export const SelectWithSearch = forwardRef< export const SelectWithSearch = forwardRef<
React.ElementRef<typeof Button>, React.ElementRef<typeof Button>,
SelectWithSearchFlagProps SelectWithSearchFlagProps
>(({ value: val = '', onChange, options = [], triggerClassName }, ref) => { >(
const id = useId(); (
const [open, setOpen] = useState<boolean>(false); {
const [value, setValue] = useState<string>(''); value: val = '',
onChange,
const handleSelect = useCallback( options = [],
(val: string) => { triggerClassName,
setValue(val); allowClear = false,
setOpen(false);
onChange?.(val);
}, },
[onChange], ref,
); ) => {
const id = useId();
const [open, setOpen] = useState<boolean>(false);
const [value, setValue] = useState<string>('');
useEffect(() => { const handleSelect = useCallback(
setValue(val); (val: string) => {
}, [val]); setValue(val);
const selectLabel = useMemo(() => { setOpen(false);
const optionTemp = options[0]; onChange?.(val);
if (optionTemp?.options) { },
return options [onChange],
.map((group) => group?.options?.find((item) => item.value === value)) );
.filter(Boolean)[0]?.label;
} else { const handleClear: MouseEventHandler<SVGElement> = useCallback(
return options.find((opt) => opt.value === value)?.label || ''; (e) => {
} e.stopPropagation();
}, [options, value]); setValue('');
return ( onChange?.('');
<Popover open={open} onOpenChange={setOpen}> },
<PopoverTrigger asChild> [onChange],
<Button );
id={id}
variant="outline" useEffect(() => {
role="combobox" setValue(val);
aria-expanded={open} }, [val]);
ref={ref} const selectLabel = useMemo(() => {
className={cn( const optionTemp = options[0];
'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]', if (optionTemp?.options) {
triggerClassName, return options
)} .map((group) => group?.options?.find((item) => item.value === value))
> .filter(Boolean)[0]?.label;
{value ? ( } else {
<span className="flex min-w-0 options-center gap-2"> return options.find((opt) => opt.value === value)?.label || '';
<span className="text-lg leading-none truncate"> }
{selectLabel} }, [options, value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
role="combobox"
aria-expanded={open}
ref={ref}
className={cn(
'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
triggerClassName,
)}
>
{value ? (
<span className="flex min-w-0 options-center gap-2">
<span className="text-lg leading-none truncate">
{selectLabel}
</span>
</span> </span>
</span> ) : (
) : ( <span className="text-muted-foreground">Select value</span>
<span className="text-muted-foreground">Select value</span> )}
)} <div className="flex items-center justify-between">
<ChevronDownIcon {value && allowClear && (
size={16} <>
className="text-muted-foreground/80 shrink-0" <XIcon
aria-hidden="true" className="h-4 mx-2 cursor-pointer text-muted-foreground"
/> onClick={handleClear}
</Button> />
</PopoverTrigger> <Separator
<PopoverContent orientation="vertical"
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0" className="flex min-h-6 h-full"
align="start" />
> </>
<Command> )}
<CommandInput placeholder="Search ..." /> <ChevronDownIcon
<CommandList> size={16}
<CommandEmpty>No data found.</CommandEmpty> className="text-muted-foreground/80 shrink-0 ml-2"
{options.map((group, idx) => { aria-hidden="true"
if (group.options) { />
return ( </div>
<Fragment key={idx}> </Button>
<CommandGroup heading={group.label}> </PopoverTrigger>
{group.options.map((option) => ( <PopoverContent
<CommandItem className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
key={option.value} align="start"
value={option.value} >
disabled={option.disabled} <Command>
onSelect={handleSelect} <CommandInput placeholder="Search ..." />
> <CommandList>
<span className="text-lg leading-none"> <CommandEmpty>No data found.</CommandEmpty>
{option.label} {options.map((group, idx) => {
</span> if (group.options) {
return (
<Fragment key={idx}>
<CommandGroup heading={group.label}>
{group.options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
disabled={option.disabled}
onSelect={handleSelect}
>
<span className="text-lg leading-none">
{option.label}
</span>
{value === option.value && ( {value === option.value && (
<CheckIcon size={16} className="ml-auto" /> <CheckIcon size={16} className="ml-auto" />
)} )}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</Fragment> </Fragment>
); );
} else { } else {
return ( return (
<CommandItem <CommandItem
key={group.value} key={group.value}
value={group.value} value={group.value}
disabled={group.disabled} disabled={group.disabled}
onSelect={handleSelect} onSelect={handleSelect}
> >
<span className="text-lg leading-none">{group.label}</span> <span className="text-lg leading-none">
{group.label}
</span>
{value === group.value && ( {value === group.value && (
<CheckIcon size={16} className="ml-auto" /> <CheckIcon size={16} className="ml-auto" />
)} )}
</CommandItem> </CommandItem>
); );
} }
})} })}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
}); },
);
SelectWithSearch.displayName = 'SelectWithSearch'; SelectWithSearch.displayName = 'SelectWithSearch';

View File

@ -0,0 +1,43 @@
import { LlmModelType } from '@/constants/knowledge';
import userService from '@/services/user-service';
import { useQuery } from '@tanstack/react-query';
import {
IThirdOAIModelCollection as IThirdAiModelCollection,
IThirdOAIModel,
} from '@/interfaces/database/llm';
import { buildLlmUuid } from '@/utils/llm-util';
export const useFetchLlmList = (modelType?: LlmModelType) => {
const { data } = useQuery<IThirdAiModelCollection>({
queryKey: ['llmList'],
initialData: {},
queryFn: async () => {
const { data } = await userService.llm_list({ model_type: modelType });
return data?.data ?? {};
},
});
return data;
};
type IThirdOAIModelWithUuid = IThirdOAIModel & { uuid: string };
export function useSelectFlatLlmList(modelType?: LlmModelType) {
const llmList = useFetchLlmList(modelType);
return Object.values(llmList).reduce<IThirdOAIModelWithUuid[]>((pre, cur) => {
pre.push(...cur.map((x) => ({ ...x, uuid: buildLlmUuid(x) })));
return pre;
}, []);
}
export function useFindLlmByUuid(modelType?: LlmModelType) {
const flatList = useSelectFlatLlmList(modelType);
return (uuid: string) => {
return flatList.find((x) => x.uuid === uuid);
};
}

View File

@ -1,6 +1,9 @@
import { Collapse } from '@/components/collapse'; import { Collapse } from '@/components/collapse';
import { FormContainer } from '@/components/form-container'; import { FormContainer } from '@/components/form-container';
import { LargeModelFormField } from '@/components/large-model-form-field'; import {
LargeModelFilterFormSchema,
LargeModelFormField,
} from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { import {
@ -12,10 +15,12 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input, NumberInput } from '@/components/ui/input'; import { Input, NumberInput } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select'; import { RAGFlowSelect } from '@/components/ui/select';
import { LlmModelType } from '@/constants/knowledge';
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react'; import { memo, useMemo } 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 { import {
@ -65,6 +70,7 @@ const FormSchema = z.object({
exception_method: z.string().nullable(), exception_method: z.string().nullable(),
exception_comment: z.string().optional(), exception_comment: z.string().optional(),
exception_goto: z.string().optional(), exception_goto: z.string().optional(),
...LargeModelFilterFormSchema,
}); });
function AgentForm({ node }: INextOperatorForm) { function AgentForm({ node }: INextOperatorForm) {
@ -88,6 +94,10 @@ function AgentForm({ node }: INextOperatorForm) {
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
}); });
const llmId = useWatch({ control: form.control, name: 'llm_id' });
const findLlmByUuid = useFindLlmByUuid();
useWatchFormChange(node?.id, form); useWatchFormChange(node?.id, form);
return ( return (
@ -101,6 +111,16 @@ function AgentForm({ node }: INextOperatorForm) {
<FormContainer> <FormContainer>
{isSubAgent && <DescriptionField></DescriptionField>} {isSubAgent && <DescriptionField></DescriptionField>}
<LargeModelFormField></LargeModelFormField> <LargeModelFormField></LargeModelFormField>
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
<QueryVariable
name="visual_files_var"
label="Visual Input File"
type={VariableType.File}
></QueryVariable>
)}
</FormContainer>
<FormContainer>
<FormField <FormField
control={form.control} control={form.control}
name={`sys_prompt`} name={`sys_prompt`}
@ -117,7 +137,6 @@ function AgentForm({ node }: INextOperatorForm) {
</FormItem> </FormItem>
)} )}
/> />
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
</FormContainer> </FormContainer>
{isSubAgent || ( {isSubAgent || (
<FormContainer> <FormContainer>
@ -148,11 +167,7 @@ function AgentForm({ node }: INextOperatorForm) {
</FormContainer> </FormContainer>
<Collapse title={<div>Advanced Settings</div>}> <Collapse title={<div>Advanced Settings</div>}>
<FormContainer> <FormContainer>
<QueryVariable <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
name="visual_files_var"
label="Visual Input File"
type={VariableType.File}
></QueryVariable>
<FormField <FormField
control={form.control} control={form.control}
name={`max_retries`} name={`max_retries`}

View File

@ -55,6 +55,7 @@ export function QueryVariable({
<SelectWithSearch <SelectWithSearch
options={finalOptions} options={finalOptions}
{...field} {...field}
allowClear
></SelectWithSearch> ></SelectWithSearch>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@ -161,17 +161,21 @@ export default function Agent() {
<Upload /> <Upload />
{t('flow.export')} {t('flow.export')}
</AgentDropdownMenuItem> </AgentDropdownMenuItem>
<DropdownMenuSeparator /> {location.hostname !== 'demo.ragflow.io' && (
<AgentDropdownMenuItem <>
onClick={showEmbedModal} <DropdownMenuSeparator />
disabled={ <AgentDropdownMenuItem
!isBeginNodeDataQuerySafe || onClick={showEmbedModal}
userInfo.nickname !== agentDetail.nickname disabled={
} !isBeginNodeDataQuerySafe ||
> userInfo.nickname !== agentDetail.nickname
<ScreenShare /> }
{t('common.embedIntoSite')} >
</AgentDropdownMenuItem> <ScreenShare />
{t('common.embedIntoSite')}
</AgentDropdownMenuItem>
</>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -1,3 +1,5 @@
import { IThirdOAIModel } from '@/interfaces/database/llm';
export const getLLMIconName = (fid: string, llm_name: string) => { export const getLLMIconName = (fid: string, llm_name: string) => {
if (fid === 'FastEmbed') { if (fid === 'FastEmbed') {
return llm_name.split('/').at(0) ?? ''; return llm_name.split('/').at(0) ?? '';
@ -16,3 +18,7 @@ export const getLlmNameAndFIdByLlmId = (llmId?: string) => {
export function getRealModelName(llmName: string) { export function getRealModelName(llmName: string) {
return llmName.split('__').at(0) ?? ''; return llmName.split('__').at(0) ?? '';
} }
export function buildLlmUuid(llm: IThirdOAIModel) {
return `${llm.llm_name}@${llm.fid}`;
}