### What problem does this PR solve? Feat: New Agent startup parameters add knowledge base parameter #9194 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.1
| import { DocumentParserType } from '@/constants/knowledge'; | import { DocumentParserType } from '@/constants/knowledge'; | ||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | ||||
| import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; | |||||
| import { UserOutlined } from '@ant-design/icons'; | import { UserOutlined } from '@ant-design/icons'; | ||||
| import { Avatar as AntAvatar, Form, Select, Space } from 'antd'; | import { Avatar as AntAvatar, Form, Select, Space } from 'antd'; | ||||
| import { toLower } from 'lodash'; | |||||
| import { useMemo } from 'react'; | |||||
| import { useFormContext } from 'react-hook-form'; | import { useFormContext } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { RAGFlowAvatar } from './ragflow-avatar'; | import { RAGFlowAvatar } from './ragflow-avatar'; | ||||
| import { FormControl, FormField, FormItem, FormLabel } from './ui/form'; | import { FormControl, FormField, FormItem, FormLabel } from './ui/form'; | ||||
| import { MultiSelect } from './ui/multi-select'; | import { MultiSelect } from './ui/multi-select'; | ||||
| export default KnowledgeBaseItem; | export default KnowledgeBaseItem; | ||||
| export function KnowledgeBaseFormField() { | |||||
| export function KnowledgeBaseFormField({ | |||||
| showVariable = false, | |||||
| }: { | |||||
| showVariable?: boolean; | |||||
| }) { | |||||
| const form = useFormContext(); | const form = useFormContext(); | ||||
| const { t } = useTranslate('chat'); | |||||
| const { t } = useTranslation(); | |||||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | const { list: knowledgeList } = useFetchKnowledgeList(true); | ||||
| (x) => x.parser_id !== DocumentParserType.Tag, | (x) => x.parser_id !== DocumentParserType.Tag, | ||||
| ); | ); | ||||
| const nextOptions = useBuildQueryVariableOptions(); | |||||
| const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | ||||
| label: x.name, | label: x.name, | ||||
| value: x.id, | value: x.id, | ||||
| ), | ), | ||||
| })); | })); | ||||
| const options = useMemo(() => { | |||||
| if (showVariable) { | |||||
| return [ | |||||
| { | |||||
| label: t('knowledgeDetails.dataset'), | |||||
| options: knowledgeOptions, | |||||
| }, | |||||
| ...nextOptions.map((x) => { | |||||
| return { | |||||
| ...x, | |||||
| options: x.options | |||||
| .filter((y) => toLower(y.type).includes('string')) | |||||
| .map((x) => ({ | |||||
| ...x, | |||||
| icon: () => ( | |||||
| <RAGFlowAvatar | |||||
| className="size-4 mr-2" | |||||
| avatar={x.label} | |||||
| name={x.label} | |||||
| /> | |||||
| ), | |||||
| })), | |||||
| }; | |||||
| }), | |||||
| ]; | |||||
| } | |||||
| return knowledgeOptions; | |||||
| }, [knowledgeOptions, nextOptions, showVariable, t]); | |||||
| return ( | return ( | ||||
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name="kb_ids" | name="kb_ids" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>{t('knowledgeBases')}</FormLabel> | |||||
| <FormLabel>{t('chat.knowledgeBases')}</FormLabel> | |||||
| <FormControl> | <FormControl> | ||||
| <MultiSelect | <MultiSelect | ||||
| options={knowledgeOptions} | |||||
| options={options} | |||||
| onValueChange={field.onChange} | onValueChange={field.onChange} | ||||
| placeholder={t('knowledgeBasesMessage')} | |||||
| placeholder={t('chat.knowledgeBasesMessage')} | |||||
| variant="inverted" | variant="inverted" | ||||
| maxCount={100} | maxCount={100} | ||||
| defaultValue={field.value} | defaultValue={field.value} |
| // https://github.com/sersavan/shadcn-multi-select-component | |||||
| // src/components/multi-select.tsx | // src/components/multi-select.tsx | ||||
| import { cva, type VariantProps } from 'class-variance-authority'; | import { cva, type VariantProps } from 'class-variance-authority'; | ||||
| import { Separator } from '@/components/ui/separator'; | import { Separator } from '@/components/ui/separator'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| export type MultiSelectOptionType = { | |||||
| label: React.ReactNode; | |||||
| value: string; | |||||
| disabled?: boolean; | |||||
| icon?: React.ComponentType<{ className?: string }>; | |||||
| }; | |||||
| export type MultiSelectGroupOptionType = { | |||||
| label: React.ReactNode; | |||||
| options: MultiSelectOptionType[]; | |||||
| }; | |||||
| function MultiCommandItem({ | |||||
| option, | |||||
| isSelected, | |||||
| toggleOption, | |||||
| }: { | |||||
| option: MultiSelectOptionType; | |||||
| isSelected: boolean; | |||||
| toggleOption(value: string): void; | |||||
| }) { | |||||
| return ( | |||||
| <CommandItem | |||||
| key={option.value} | |||||
| onSelect={() => toggleOption(option.value)} | |||||
| className="cursor-pointer" | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||||
| isSelected | |||||
| ? 'bg-primary text-primary-foreground' | |||||
| : 'opacity-50 [&_svg]:invisible', | |||||
| )} | |||||
| > | |||||
| <CheckIcon className="h-4 w-4" /> | |||||
| </div> | |||||
| {option.icon && ( | |||||
| <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||||
| )} | |||||
| <span>{option.label}</span> | |||||
| </CommandItem> | |||||
| ); | |||||
| } | |||||
| /** | /** | ||||
| * Variants for the multi-select component to handle different styles. | * Variants for the multi-select component to handle different styles. | ||||
| * Uses class-variance-authority (cva) to define different styles based on "variant" prop. | * Uses class-variance-authority (cva) to define different styles based on "variant" prop. | ||||
| * An array of option objects to be displayed in the multi-select component. | * An array of option objects to be displayed in the multi-select component. | ||||
| * Each option object has a label, value, and an optional icon. | * Each option object has a label, value, and an optional icon. | ||||
| */ | */ | ||||
| options: { | |||||
| /** The text to display for the option. */ | |||||
| label: string; | |||||
| /** The unique value associated with the option. */ | |||||
| value: string; | |||||
| /** Optional icon component to display alongside the option. */ | |||||
| icon?: React.ComponentType<{ className?: string }>; | |||||
| }[]; | |||||
| options: (MultiSelectGroupOptionType | MultiSelectOptionType)[]; | |||||
| /** | /** | ||||
| * Callback function triggered when the selected values change. | * Callback function triggered when the selected values change. | ||||
| const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); | const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); | ||||
| const [isAnimating, setIsAnimating] = React.useState(false); | const [isAnimating, setIsAnimating] = React.useState(false); | ||||
| const flatOptions = React.useMemo(() => { | |||||
| return options.flatMap((option) => | |||||
| 'options' in option ? option.options : [option], | |||||
| ); | |||||
| }, [options]); | |||||
| const handleInputKeyDown = ( | const handleInputKeyDown = ( | ||||
| event: React.KeyboardEvent<HTMLInputElement>, | event: React.KeyboardEvent<HTMLInputElement>, | ||||
| ) => { | ) => { | ||||
| }; | }; | ||||
| const toggleAll = () => { | const toggleAll = () => { | ||||
| if (selectedValues.length === options.length) { | |||||
| if (selectedValues.length === flatOptions.length) { | |||||
| handleClear(); | handleClear(); | ||||
| } else { | } else { | ||||
| const allValues = options.map((option) => option.value); | |||||
| const allValues = flatOptions.map((option) => option.value); | |||||
| setSelectedValues(allValues); | setSelectedValues(allValues); | ||||
| onValueChange(allValues); | onValueChange(allValues); | ||||
| } | } | ||||
| <div className="flex justify-between items-center w-full"> | <div className="flex justify-between items-center w-full"> | ||||
| <div className="flex flex-wrap items-center"> | <div className="flex flex-wrap items-center"> | ||||
| {selectedValues?.slice(0, maxCount)?.map((value) => { | {selectedValues?.slice(0, maxCount)?.map((value) => { | ||||
| const option = options.find((o) => o.value === value); | |||||
| const option = flatOptions.find((o) => o.value === value); | |||||
| const IconComponent = option?.icon; | const IconComponent = option?.icon; | ||||
| return ( | return ( | ||||
| <Badge | <Badge | ||||
| <div | <div | ||||
| className={cn( | className={cn( | ||||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | ||||
| selectedValues.length === options.length | |||||
| selectedValues.length === flatOptions.length | |||||
| ? 'bg-primary text-primary-foreground' | ? 'bg-primary text-primary-foreground' | ||||
| : 'opacity-50 [&_svg]:invisible', | : 'opacity-50 [&_svg]:invisible', | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <span>(Select All)</span> | <span>(Select All)</span> | ||||
| </CommandItem> | </CommandItem> | ||||
| {options.map((option) => { | |||||
| const isSelected = selectedValues.includes(option.value); | |||||
| return ( | |||||
| <CommandItem | |||||
| key={option.value} | |||||
| onSelect={() => toggleOption(option.value)} | |||||
| className="cursor-pointer" | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||||
| isSelected | |||||
| ? 'bg-primary text-primary-foreground' | |||||
| : 'opacity-50 [&_svg]:invisible', | |||||
| )} | |||||
| > | |||||
| <CheckIcon className="h-4 w-4" /> | |||||
| </div> | |||||
| {option.icon && ( | |||||
| <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||||
| )} | |||||
| <span>{option.label}</span> | |||||
| </CommandItem> | |||||
| ); | |||||
| })} | |||||
| {!options.some((x) => 'options' in x) && | |||||
| (options as unknown as MultiSelectOptionType[]).map( | |||||
| (option) => { | |||||
| const isSelected = selectedValues.includes(option.value); | |||||
| return ( | |||||
| <MultiCommandItem | |||||
| option={option} | |||||
| key={option.value} | |||||
| isSelected={isSelected} | |||||
| toggleOption={toggleOption} | |||||
| ></MultiCommandItem> | |||||
| ); | |||||
| }, | |||||
| )} | |||||
| </CommandGroup> | </CommandGroup> | ||||
| {options.every((x) => 'options' in x) && | |||||
| options.map((x, idx) => ( | |||||
| <CommandGroup heading={x.label} key={idx}> | |||||
| {x.options.map((option) => { | |||||
| const isSelected = selectedValues.includes(option.value); | |||||
| return ( | |||||
| <MultiCommandItem | |||||
| option={option} | |||||
| key={option.value} | |||||
| isSelected={isSelected} | |||||
| toggleOption={toggleOption} | |||||
| ></MultiCommandItem> | |||||
| ); | |||||
| })} | |||||
| </CommandGroup> | |||||
| ))} | |||||
| <CommandSeparator /> | <CommandSeparator /> | ||||
| <CommandGroup> | <CommandGroup> | ||||
| <div className="flex items-center justify-between"> | <div className="flex items-center justify-between"> |
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | ||||
| import { IRetrievalNode } from '@/interfaces/database/flow'; | import { IRetrievalNode } from '@/interfaces/database/flow'; | ||||
| import { NodeProps, Position } from '@xyflow/react'; | import { NodeProps, Position } from '@xyflow/react'; | ||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo, useMemo } from 'react'; | import { memo, useMemo } from 'react'; | ||||
| import { NodeHandleId } from '../../constant'; | import { NodeHandleId } from '../../constant'; | ||||
| import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query'; | |||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| selected, | selected, | ||||
| }: NodeProps<IRetrievalNode>) { | }: NodeProps<IRetrievalNode>) { | ||||
| const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | ||||
| console.log('🚀 ~ InnerRetrievalNode ~ knowledgeBaseIds:', knowledgeBaseIds); | |||||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | const { list: knowledgeList } = useFetchKnowledgeList(true); | ||||
| const knowledgeBases = useMemo(() => { | const knowledgeBases = useMemo(() => { | ||||
| return knowledgeBaseIds.map((x) => { | return knowledgeBaseIds.map((x) => { | ||||
| }); | }); | ||||
| }, [knowledgeList, knowledgeBaseIds]); | }, [knowledgeList, knowledgeBaseIds]); | ||||
| const getLabel = useGetVariableLabelByValue(id); | |||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper selected={selected}> | <NodeWrapper selected={selected}> | ||||
| [styles.nodeHeader]: knowledgeBaseIds.length > 0, | [styles.nodeHeader]: knowledgeBaseIds.length > 0, | ||||
| })} | })} | ||||
| ></NodeHeader> | ></NodeHeader> | ||||
| <Flex vertical gap={8}> | |||||
| {knowledgeBases.map((knowledge) => { | |||||
| <section className="flex flex-col gap-2"> | |||||
| {knowledgeBaseIds.map((id) => { | |||||
| const item = knowledgeList.find((y) => id === y.id); | |||||
| const label = getLabel(id); | |||||
| return ( | return ( | ||||
| <div className={styles.nodeText} key={knowledge.id}> | |||||
| <Flex align={'center'} gap={6}> | |||||
| <div className={styles.nodeText} key={id}> | |||||
| <div className="flex items-center gap-1.5"> | |||||
| <RAGFlowAvatar | <RAGFlowAvatar | ||||
| className="size-6 rounded-lg" | className="size-6 rounded-lg" | ||||
| avatar={knowledge.avatar} | |||||
| name={knowledge.name || 'CN'} | |||||
| avatar={id} | |||||
| name={item?.name || (label as string) || 'CN'} | |||||
| isPerson={true} | isPerson={true} | ||||
| /> | /> | ||||
| <Flex className={styles.knowledgeNodeName} flex={1}> | |||||
| {knowledge.name} | |||||
| </Flex> | |||||
| </Flex> | |||||
| <div className={'truncate flex-1'}>{label || item?.name}</div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| })} | })} | ||||
| </Flex> | |||||
| </section> | |||||
| </NodeWrapper> | </NodeWrapper> | ||||
| </ToolBar> | </ToolBar> | ||||
| ); | ); |
| <FormWrapper> | <FormWrapper> | ||||
| <FormContainer> | <FormContainer> | ||||
| <QueryVariable></QueryVariable> | <QueryVariable></QueryVariable> | ||||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | |||||
| </FormContainer> | </FormContainer> | ||||
| <Collapse title={<div>Advanced Settings</div>}> | <Collapse title={<div>Advanced Settings</div>}> | ||||
| <FormContainer> | <FormContainer> |
| > | > | ||||
| <FormContainer> | <FormContainer> | ||||
| <DescriptionField></DescriptionField> | <DescriptionField></DescriptionField> | ||||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | |||||
| </FormContainer> | </FormContainer> | ||||
| <Collapse title={<div>Advanced Settings</div>}> | <Collapse title={<div>Advanced Settings</div>}> | ||||
| <FormContainer> | <FormContainer> |
| ); | ); | ||||
| })} | })} | ||||
| </div> | </div> | ||||
| <div ref={ref} /> | |||||
| <div ref={ref.scrollRef} /> | |||||
| </div> | </div> | ||||
| <div className="flex w-full justify-center mb-8"> | <div className="flex w-full justify-center mb-8"> | ||||
| <div className="w-5/6"> | <div className="w-5/6"> |