feat: external knowledge base

This commit is contained in:
Yi 2024-09-27 00:33:56 +08:00
parent ff0260e564
commit 1c7cb3fbc0
22 changed files with 470 additions and 149 deletions

View File

@ -1,6 +1,6 @@
'use client'
import type { FC, SVGProps } from 'react'
import React, { useEffect } from 'react'
import React, { useEffect, useMemo } from 'react'
import { usePathname } from 'next/navigation'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
@ -203,12 +203,23 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
datasetId,
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
const navigation = [
{ name: t('common.datasetMenus.documents'), href: `/datasets/${datasetId}/documents`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
]
const navigation = useMemo(() => {
const baseNavigation = [
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
]
if (datasetRes?.provider !== 'external') {
baseNavigation.unshift({
name: t('common.datasetMenus.documents'),
href: `/datasets/${datasetId}/documents`,
icon: DocumentTextIcon,
selectedIcon: DocumentTextSolidIcon,
})
}
return baseNavigation
}, [datasetRes?.provider, datasetId, t])
useEffect(() => {
if (datasetRes)
@ -233,6 +244,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
icon_background={datasetRes?.icon_background || '#F5F5F5'}
desc={datasetRes?.description || '--'}
isExternal={datasetRes?.provider === 'external'}
navigation={navigation}
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} /> : undefined}
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}

View File

@ -33,6 +33,7 @@ const DatasetCard = ({
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { push } = useRouter()
const EXTERNAL_PROVIDER = 'external' as const
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const [tags, setTags] = useState<Tag[]>(dataset.tags)
@ -40,6 +41,7 @@ const DatasetCard = ({
const [showRenameModal, setShowRenameModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [confirmMessage, setConfirmMessage] = useState<string>('')
const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
@ -113,10 +115,12 @@ const DatasetCard = ({
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
push(`/datasets/${dataset.id}/documents`)
isExternalProvider(dataset.provider)
? push(`/datasets/${dataset.id}/hitTesting`)
: push(`/datasets/${dataset.id}/documents`)
}}
>
{dataset.provider === 'external' && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
{isExternalProvider(dataset.provider) && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className={cn(
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',

View File

@ -6,6 +6,7 @@ export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
icon?: string
icon_background?: string | null
isExternal?: boolean
name: string
type: string | React.ReactNode
hoverTip?: string
@ -52,7 +53,7 @@ const ICON_MAP = {
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
}
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
return (
<div className="flex items-start p-1">
{icon && icon_background && iconType === 'app' && (
@ -83,6 +84,7 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
}
</div>
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? 'External' : ''}</div>
</div>}
</div>
)

View File

@ -15,6 +15,7 @@ export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset' | 'notion'
title: string
desc: string
isExternal?: boolean
icon: string
icon_background: string
navigation: Array<{
@ -26,7 +27,7 @@ export type IAppDetailNavProps = {
extraInfo?: (modeState: string) => React.ReactNode
}
const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
const { appSidebarExpand, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
appSidebarExpand: state.appSidebarExpand,
setAppSiderbarExpand: state.setAppSiderbarExpand,
@ -70,6 +71,7 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
icon_background={icon_background}
name={title}
type={desc}
isExternal={isExternal}
/>
)}
</div>

View File

@ -36,6 +36,12 @@ export type UsageScene = 'doc' | 'hitTesting'
type ISegmentCardProps = {
loading: boolean
detail?: SegmentDetailModel & { document: { name: string } }
contentExternal?: string
refSource?: {
title: string
uri: string
}
isExternal?: boolean
score?: number
onClick?: () => void
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
@ -48,6 +54,8 @@ type ISegmentCardProps = {
const SegmentCard: FC<ISegmentCardProps> = ({
detail = {},
contentExternal,
refSource,
score,
onClick,
onChangeSwitch,
@ -88,6 +96,9 @@ const SegmentCard: FC<ISegmentCardProps> = ({
)
}
if (contentExternal)
return contentExternal
return content
}
@ -201,8 +212,8 @@ const SegmentCard: FC<ISegmentCardProps> = ({
<Divider />
<div className="relative flex items-center w-full">
<DocumentTitle
name={detail?.document?.name || ''}
extension={(detail?.document?.name || '').split('.').pop() || 'txt'}
name={detail?.document?.name || refSource?.title || ''}
extension={(detail?.document?.name || refSource?.title || '').split('.').pop() || 'txt'}
wrapperCls='w-full'
iconCls="!h-4 !w-4 !bg-contain"
textCls="text-xs text-gray-700 !font-normal overflow-hidden whitespace-nowrap text-ellipsis"

View File

@ -1,20 +1,30 @@
'use client'
import React from 'react'
import React, { useState } from 'react'
import { useToastContext } from '@/app/components/base/toast'
import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create'
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
import { createExternalKnowledgeBase } from '@/service/datasets'
const ExternalKnowledgeBaseConnector = () => {
const { notify } = useToastContext()
const [loading, setLoading] = useState(false)
const handleConnect = async (formValue: CreateKnowledgeBaseReq) => {
try {
setLoading(true)
const result = await createExternalKnowledgeBase({ body: formValue })
if (result && result.id)
notify({ type: 'success', message: 'External Knowledge Base Connected Successfully' })
else
throw new Error('Failed to create external knowledge base')
}
catch (error) {
console.error('Error creating external knowledge base:', error)
}
setLoading(false)
}
return <ExternalKnowledgeBaseCreate onConnect={handleConnect} />
return <ExternalKnowledgeBaseCreate onConnect={handleConnect} loading={loading} />
}
export default ExternalKnowledgeBaseConnector

View File

@ -16,7 +16,7 @@ const KnowledgeBaseInfo: React.FC<KnowledgeBaseInfoProps> = ({ name, description
onChange({ name: e.target.value })
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange({ description: e.target.value })
}
@ -38,11 +38,11 @@ const KnowledgeBaseInfo: React.FC<KnowledgeBaseInfoProps> = ({ name, description
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeDescription')}</label>
</div>
<div className='flex flex-col gap-1 self-stretch'>
<Input
<textarea
value={description}
onChange={handleDescriptionChange}
onChange={ e => handleDescriptionChange(e)}
placeholder={t('dataset.externalKnowledgeDescriptionPlaceholder') ?? ''}
className='flex h-20 p-2 self-stretch items-start'
className='flex h-20 p-2 self-stretch items-start rounded-lg bg-components-input-bg-normal text-components-input-text-placeholder system-sm-regular'
/>
<div className='flex py-0.5 gap-1 self-stretch'>
<div className='flex p-0.5 items-center gap-2'>

View File

@ -3,22 +3,32 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import TopKItem from '@/app/components/base/param-item/top-k-item'
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
import cn from '@/utils/classnames'
type RetrievalSettingsProps = {
topK: number
scoreThreshold: number
isInHitTesting?: boolean
isInRetrievalSetting?: boolean
onChange: (data: { top_k?: number; score_threshold?: number }) => void
}
const RetrievalSettings: FC<RetrievalSettingsProps> = ({ topK, scoreThreshold, onChange }) => {
const RetrievalSettings: FC<RetrievalSettingsProps> = ({ topK, scoreThreshold, onChange, isInHitTesting = false, isInRetrievalSetting = false }) => {
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false)
const { t } = useTranslation()
return (
<div className='flex flex-col gap-2 self-stretch'>
<div className='flex h-7 pt-1 flex-col gap-2 self-stretch'>
<div className={cn('flex flex-col gap-2 self-stretch', isInRetrievalSetting && 'w-full max-w-[480px]')}>
{!isInHitTesting && !isInRetrievalSetting && <div className='flex h-7 pt-1 flex-col gap-2 self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.retrievalSettings')}</label>
</div>
<div className='flex gap-4 self-stretch'>
</div>}
<div className={cn(
'flex gap-4 self-stretch',
{
'flex-col': isInHitTesting,
'flex-row': isInRetrievalSetting,
'flex-col sm:flex-row': !isInHitTesting && !isInRetrievalSetting,
},
)}>
<div className='flex flex-col gap-1 flex-grow'>
<TopKItem
className='grow'

View File

@ -4,7 +4,7 @@ export type CreateKnowledgeBaseReq = {
external_knowledge_api_id: string
provider: 'external'
external_knowledge_id: string
external_retrieval_modal: {
external_retrieval_model: {
top_k: number
score_threshold: number
}

View File

@ -14,9 +14,10 @@ import Button from '@/app/components/base/button'
type ExternalKnowledgeBaseCreateProps = {
onConnect: (formValue: CreateKnowledgeBaseReq) => void
loading: boolean
}
const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect }) => {
const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect, loading }) => {
const { t } = useTranslation()
const router = useRouter()
const [formData, setFormData] = useState<CreateKnowledgeBaseReq>({
@ -24,7 +25,7 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
description: '',
external_knowledge_api_id: '',
external_knowledge_id: '',
external_retrieval_modal: {
external_retrieval_model: {
top_k: 2,
score_threshold: 0.5,
},
@ -43,8 +44,8 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
const isFormValid = formData.name !== ''
&& formData.external_knowledge_api_id !== ''
&& formData.external_knowledge_id !== ''
&& formData.external_retrieval_modal.top_k !== undefined
&& formData.external_retrieval_modal.score_threshold !== undefined
&& formData.external_retrieval_model.top_k !== undefined
&& formData.external_retrieval_model.score_threshold !== undefined
return (
<div className='flex flex-col flex-grow self-stretch rounded-t-2xl border-t border-effects-highlight bg-components-panel-bg'>
@ -79,12 +80,12 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
})}
/>
<RetrievalSettings
topK={formData.external_retrieval_modal.top_k}
scoreThreshold={formData.external_retrieval_modal.score_threshold}
topK={formData.external_retrieval_model.top_k}
scoreThreshold={formData.external_retrieval_model.score_threshold}
onChange={data => handleFormChange({
...formData,
external_retrieval_modal: {
...formData.external_retrieval_modal,
external_retrieval_model: {
...formData.external_retrieval_model,
...data,
},
})}
@ -98,7 +99,10 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
onClick={() => {
onConnect(formData)
navBackHandle()
}} disabled={!isFormValid}>
}}
disabled={!isFormValid}
loading={loading}
>
<div className='text-components-button-primary-text system-sm-medium'>{t('dataset.externalKnowledgeForm.connect')}</div>
<RiArrowRightLine className='w-4 h-4 text-components-button-primary-text' />
</Button>

View File

@ -30,36 +30,40 @@ const HitDetail: FC<IHitDetailProps> = ({ segInfo }) => {
}
return (
<div className='overflow-x-auto'>
<div className="bg-gray-25 p-6">
<div className="flex items-center">
<SegmentIndexTag
positionId={segInfo?.position || ''}
className="w-fit mr-6"
/>
<div className={cn(s.commonIcon, s.typeSquareIcon)} />
<span className={cn('mr-6', s.numberInfo)}>
{segInfo?.word_count} {t('datasetDocuments.segment.characters')}
</span>
<div className={cn(s.commonIcon, s.targetIcon)} />
<span className={s.numberInfo}>
{segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')}
</span>
</div>
<Divider />
segInfo?.id === 'external'
? <div className='bg-gray-25 p-10'>
<div className={s.segModalContent}>{renderContent()}</div>
<div className={s.keywordTitle}>
{t('datasetDocuments.segment.keywords')}
</div>
<div className={s.keywordWrapper}>
{!segInfo?.keywords?.length
? '-'
: segInfo?.keywords?.map((word, index) => {
return <div key={index} className={s.keyword}>{word}</div>
})}
</div>
: <div className='overflow-x-auto'>
<div className="bg-gray-25 p-6">
<div className="flex items-center">
<SegmentIndexTag
positionId={segInfo?.position || ''}
className="w-fit mr-6"
/>
<div className={cn(s.commonIcon, s.typeSquareIcon)} />
<span className={cn('mr-6', s.numberInfo)}>
{segInfo?.word_count} {t('datasetDocuments.segment.characters')}
</span>
<div className={cn(s.commonIcon, s.targetIcon)} />
<span className={s.numberInfo}>
{segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')}
</span>
</div>
<Divider />
<div className={s.segModalContent}>{renderContent()}</div>
<div className={s.keywordTitle}>
{t('datasetDocuments.segment.keywords')}
</div>
<div className={s.keywordWrapper}>
{!segInfo?.keywords?.length
? '-'
: segInfo?.keywords?.map((word, index) => {
return <div key={index} className={s.keyword}>{word}</div>
})}
</div>
</div>
</div>
</div>
)
}

View File

@ -13,7 +13,7 @@ import s from './style.module.css'
import HitDetail from './hit-detail'
import ModifyRetrievalModal from './modify-retrieval-modal'
import cn from '@/utils/classnames'
import type { HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Drawer from '@/app/components/base/drawer'
@ -49,8 +49,10 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
const isMobile = media === MediaType.mobile
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>() // 初始化记录为空数组
const [externalHitResult, setExternalHitResult] = useState<ExternalKnowledgeBaseHitTestingResponse | undefined>()
const [submitLoading, setSubmitLoading] = useState(false)
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false })
const [externalCurrParagraph, setExternalCurrParagraph] = useState<{ paraInfo?: ExternalKnowledgeBaseHitTestingType; showModal: boolean }>({ showModal: false })
const [text, setText] = useState('')
const [currPage, setCurrPage] = React.useState<number>(0)
@ -66,12 +68,50 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
setCurrParagraph({ paraInfo: detail, showModal: true })
}
const onClickExternalCard = (detail: ExternalKnowledgeBaseHitTestingType) => {
setExternalCurrParagraph({ paraInfo: detail, showModal: true })
}
const { dataset: currentDataset } = useContext(DatasetDetailContext)
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
const renderHitResults = (results: any[], onClickCard: (record: any) => void) => (
<>
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
<div className='overflow-auto flex-1'>
<div className={s.cardWrapper}>
{results.map((record, idx) => (
<SegmentCard
key={idx}
loading={false}
refSource= {{
title: record.title,
uri: record.metadata ? record.metadata['x-amz-bedrock-kb-source-uri'] : '',
}}
detail={record.segment}
contentExternal={record.content}
score={record.score}
scene='hitTesting'
className='h-[216px] mb-4'
onClick={() => onClickCard(record)}
/>
))}
</div>
</div>
</>
)
const renderEmptyState = () => (
<div className='h-full flex flex-col justify-center items-center'>
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
<div className='text-gray-300 text-[13px] mt-3'>
{t('datasetHitTesting.hit.emptyTip')}
</div>
</div>
)
useEffect(() => {
setShowRightPanel(!isMobile)
}, [isMobile, setShowRightPanel])
@ -86,12 +126,14 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
<Textarea
datasetId={datasetId}
setHitResult={setHitResult}
setExternalHitResult={setExternalHitResult}
onSubmit={showRightPanel}
onUpdateList={recordsMutate}
loading={submitLoading}
setLoading={setSubmitLoading}
setText={setText}
text={text}
isExternal={currentDataset?.provider === 'external'}
onClickRetrievalMethod={() => setIsShowModifyRetrievalModal(true)}
retrievalConfig={retrievalConfig}
isEconomy={currentDataset?.indexing_technique === 'economy'}
@ -159,47 +201,42 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
className='h-[216px]'
/>
</div>
: !hitResult?.records.length
? (
<div className='h-full flex flex-col justify-center items-center'>
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
<div className='text-gray-300 text-[13px] mt-3'>
{t('datasetHitTesting.hit.emptyTip')}
</div>
</div>
)
: (
<>
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
<div className='overflow-auto flex-1'>
<div className={s.cardWrapper}>
{hitResult?.records.map((record, idx) => {
return <SegmentCard
key={idx}
loading={false}
detail={record.segment as any}
score={record.score}
scene='hitTesting'
className='h-[216px] mb-4'
onClick={() => onClickCard(record as any)}
/>
})}
</div>
</div>
</>
)
: (
(() => {
if (!hitResult?.records.length && !externalHitResult?.records.length)
return renderEmptyState()
if (hitResult?.records.length)
return renderHitResults(hitResult.records, onClickCard)
return renderHitResults(externalHitResult?.records || [], onClickExternalCard)
})()
)
}
</div>
</FloatRightContainer>
<Modal
className='w-[520px] p-0'
className='w-[520px] px-8 py-6'
closable
onClose={() => setCurrParagraph({ showModal: false })}
isShow={currParagraph.showModal}
onClose={() => {
setCurrParagraph({ showModal: false })
setExternalCurrParagraph({ showModal: false })
}}
isShow={currParagraph.showModal || externalCurrParagraph.showModal}
>
{currParagraph.showModal && <HitDetail
segInfo={currParagraph.paraInfo?.segment}
/>}
{currParagraph.showModal && (
<HitDetail
segInfo={currParagraph.paraInfo?.segment}
/>
)}
{externalCurrParagraph.showModal && (
<HitDetail
segInfo={{
id: 'external',
content: externalCurrParagraph.paraInfo?.content,
}}
/>
)}
</Modal>
<Drawer isOpen={isShowModifyRetrievalModal} onClose={() => setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
<ModifyRetrievalModal

View File

@ -0,0 +1,65 @@
import { useState } from 'react'
import {
RiCloseLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import RetrievalSettings from '../external-knowledge-base/create/RetrievalSettings'
import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
type ModifyExternalRetrievalModalProps = {
onClose: () => void
onSave: (data: { top_k: number; score_threshold: number }) => void
initialTopK: number
initialScoreThreshold: number
}
const ModifyExternalRetrievalModal: React.FC<ModifyExternalRetrievalModalProps> = ({
onClose,
onSave,
initialTopK,
initialScoreThreshold,
}) => {
const { t } = useTranslation()
const [topK, setTopK] = useState(initialTopK)
const [scoreThreshold, setScoreThreshold] = useState(initialScoreThreshold)
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number }) => {
if (data.top_k !== undefined)
setTopK(data.top_k)
if (data.score_threshold !== undefined)
setScoreThreshold(data.score_threshold)
}
const handleSave = () => {
onSave({ top_k: topK, score_threshold: scoreThreshold })
onClose()
}
return (
<div className='absolute z-10 top-[36px] right-[14px] flex w-[320px] flex-col items-start rounded-2xl border-[0.5px]
border-components-panel-border bg-components-panel-bg shadows-shadow-2xl'
>
<div className='flex p-4 pb-2 items-center justify-between self-stretch'>
<div className='text-text-primary system-xl-semibold flex-grow'>{t('datasetHitTesting.settingTitle')}</div>
<ActionButton className='ml-auto' onClick={onClose}>
<RiCloseLine className='w-4 h-4 flex-shrink-0' />
</ActionButton>
</div>
<div className='flex p-4 pt-2 flex-col justify-center items-start gap-4 self-stretch'>
<RetrievalSettings
topK={topK}
scoreThreshold={scoreThreshold}
onChange={handleSettingsChange}
isInHitTesting={true}
/>
</div>
<div className='flex p-4 pt-2 justify-end items-end gap-1 w-full'>
<Button className='flex-shrink-0 min-w-[72px]' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='flex-shrink-0 min-w-[72px]' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
)
}
export default ModifyExternalRetrievalModal

View File

@ -1,12 +1,17 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import Button from '../../base/button'
import Tag from '../../base/tag'
import { getIcon } from '../common/retrieval-method-info'
import s from './style.module.css'
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import type { HitTestingResponse } from '@/models/datasets'
import { hitTesting } from '@/service/datasets'
import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets'
import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
import { asyncRunSafe } from '@/utils'
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
@ -14,10 +19,12 @@ type TextAreaWithButtonIProps = {
datasetId: string
onUpdateList: () => void
setHitResult: (res: HitTestingResponse) => void
setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
loading: boolean
setLoading: (v: boolean) => void
text: string
setText: (v: string) => void
isExternal?: boolean
onClickRetrievalMethod: () => void
retrievalConfig: RetrievalConfig
isEconomy: boolean
@ -28,16 +35,28 @@ const TextAreaWithButton = ({
datasetId,
onUpdateList,
setHitResult,
setExternalHitResult,
setLoading,
loading,
text,
setText,
isExternal = false,
onClickRetrievalMethod,
retrievalConfig,
isEconomy,
onSubmit: _onSubmit,
}: TextAreaWithButtonIProps) => {
const { t } = useTranslation()
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
top_k: 2,
score_threshold: 0.5,
})
const handleSaveExternalRetrievalSettings = (data: { top_k: number; score_threshold: number }) => {
setExternalRetrievalSettings(data)
setIsSettingsOpen(false)
}
function handleTextChange(event: any) {
setText(event.target.value)
@ -63,28 +82,70 @@ const TextAreaWithButton = ({
_onSubmit && _onSubmit()
}
const externalRetrievalTestingOnSubmit = async () => {
setLoading(true)
const [e, res] = await asyncRunSafe<ExternalKnowledgeBaseHitTestingResponse>(
externalKnowledgeBaseHitTesting({
datasetId,
query: text,
external_retrieval_model: {
top_k: externalRetrievalSettings.top_k,
score_threshold: externalRetrievalSettings.score_threshold,
},
}) as Promise<ExternalKnowledgeBaseHitTestingResponse>,
)
if (!e) {
setExternalHitResult(res)
onUpdateList?.()
}
setLoading(false)
_onSubmit && _onSubmit()
}
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.invertedIndex : retrievalConfig.search_method
const Icon = getIcon(retrievalMethod)
return (
<>
<div className={s.wrapper}>
<div className='pt-2 rounded-tl-xl rounded-tr-xl bg-[#EEF4FF]'>
<div className='relative pt-2 rounded-tl-xl rounded-tr-xl bg-[#EEF4FF]'>
<div className="px-4 pb-2 flex justify-between h-8 items-center">
<span className="text-gray-800 font-semibold text-sm">
{t('datasetHitTesting.input.title')}
</span>
<Tooltip
popupContent={t('dataset.retrieval.changeRetrievalMethod')}
>
<div
onClick={onClickRetrievalMethod}
className='flex px-2 h-7 items-center space-x-1 bg-white hover:bg-[#ECE9FE] rounded-md shadow-sm cursor-pointer text-[#6927DA]'
{isExternal
? <Button
variant='secondary'
size='small'
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
>
<Icon className='w-3.5 h-3.5'></Icon>
<div className='text-xs font-medium'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
</div>
</Tooltip>
<RiEqualizer2Line className='text-components-button-secondary-text w-3.5 h-3.5' />
<div className='flex px-[3px] justify-center items-center gap-1'>
<span className='text-components-button-secondary-text system-xs-medium'>{t('datasetHitTesting.settingTitle')}</span>
</div>
</Button>
: <Tooltip
popupContent={t('dataset.retrieval.changeRetrievalMethod')}
>
<div
onClick={onClickRetrievalMethod}
className='flex px-2 h-7 items-center space-x-1 bg-white hover:bg-[#ECE9FE] rounded-md shadow-sm cursor-pointer text-[#6927DA]'
>
<Icon className='w-3.5 h-3.5'></Icon>
<div className='text-xs font-medium'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
</div>
</Tooltip>
}
</div>
{
isSettingsOpen && (
<ModifyExternalRetrievalModal
onClose={() => setIsSettingsOpen(false)}
onSave={handleSaveExternalRetrievalSettings}
initialTopK={externalRetrievalSettings.top_k}
initialScoreThreshold={externalRetrievalSettings.score_threshold}
/>
)
}
<div className='h-2 rounded-tl-xl rounded-tr-xl bg-white'></div>
</div>
<div className='px-4 pb-11'>
@ -122,7 +183,7 @@ const TextAreaWithButton = ({
<div>
<Button
onClick={onSubmit}
onClick={isExternal ? externalRetrievalTestingOnSubmit : onSubmit}
variant="primary"
loading={loading}
disabled={(!text?.length || text?.length > 200)}
@ -132,7 +193,6 @@ const TextAreaWithButton = ({
</div>
</div>
</div>
</div>
</>
)

View File

@ -8,11 +8,14 @@ import { useSWRConfig } from 'swr'
import { unstable_serialize } from 'swr/infinite'
import PermissionSelector from '../permission-selector'
import IndexMethodRadio from '../index-method-radio'
import RetrievalSettings from '../../external-knowledge-base/create/RetrievalSettings'
import cn from '@/utils/classnames'
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { updateDatasetSetting } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
@ -55,6 +58,8 @@ const Form = () => {
const [name, setName] = useState(currentDataset?.name ?? '')
const [description, setDescription] = useState(currentDataset?.description ?? '')
const [permission, setPermission] = useState(currentDataset?.permission)
const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset?.partial_member_list || [])
const [memberList, setMemberList] = useState<Member[]>([])
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
@ -85,6 +90,13 @@ const Form = () => {
setMemberList(accounts)
}
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number }) => {
if (data.top_k !== undefined)
setTopK(data.top_k)
if (data.score_threshold !== undefined)
setScoreThreshold(data.score_threshold)
}
useMount(() => {
getMembers()
})
@ -126,10 +138,16 @@ const Form = () => {
description,
permission,
indexing_technique: indexMethod,
external_retrieval_model: {
top_k: topK,
score_threshold: scoreThreshold,
},
retrieval_model: {
...postRetrievalConfig,
score_threshold: postRetrievalConfig.score_threshold_enabled ? postRetrievalConfig.score_threshold : 0,
},
external_knowledge_id: currentDataset!.external_knowledge_info.external_knowledge_id,
external_knowledge_api_id: currentDataset!.external_knowledge_info.external_knowledge_api_id,
embedding_model: embeddingModel.model,
embedding_model_provider: embeddingModel.provider,
},
@ -161,7 +179,7 @@ const Form = () => {
<div className='w-full sm:w-[800px] p-4 sm:px-16 sm:py-6'>
<div className={rowClass}>
<div className={labelClass}>
<div>{t('datasetSettings.form.name')}</div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
</div>
<div className='w-full max-w-[480px]'>
<input
@ -174,7 +192,7 @@ const Form = () => {
</div>
<div className={rowClass}>
<div className={labelClass}>
<div>{t('datasetSettings.form.desc')}</div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
</div>
<div className='w-full max-w-[480px]'>
<textarea
@ -192,7 +210,7 @@ const Form = () => {
</div>
<div className={rowClass}>
<div className={labelClass}>
<div>{t('datasetSettings.form.permissions')}</div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.permissions')}</div>
</div>
<div className='w-full sm:w-[480px]'>
<PermissionSelector
@ -210,7 +228,7 @@ const Form = () => {
<div className='w-full h-0 border-b-[0.5px] border-b-gray-200 my-2' />
<div className={rowClass}>
<div className={labelClass}>
<div>{t('datasetSettings.form.indexMethod')}</div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.indexMethod')}</div>
</div>
<div className='w-full sm:w-[480px]'>
<IndexMethodRadio
@ -225,7 +243,7 @@ const Form = () => {
{indexMethod === 'high_quality' && (
<div className={rowClass}>
<div className={labelClass}>
<div>{t('datasetSettings.form.embeddingModel')}</div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.embeddingModel')}</div>
</div>
<div className='w-[480px]'>
<ModelSelector
@ -240,32 +258,75 @@ const Form = () => {
</div>
)}
{/* Retrieval Method Config */}
<div className={rowClass}>
<div className={labelClass}>
<div>
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='leading-[18px] text-xs font-normal text-gray-500'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
{t('datasetSettings.form.retrievalSetting.description')}
{currentDataset?.provider === 'external'
? <>
<div className={rowClass}><Divider/></div>
<div className={rowClass}>
<div className={labelClass}>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
</div>
<RetrievalSettings
topK={topK}
scoreThreshold={scoreThreshold}
onChange={handleSettingsChange}
isInRetrievalSetting={true}
/>
</div>
<div className={rowClass}><Divider/></div>
<div className={rowClass}>
<div className={labelClass}>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeAPI')}</div>
</div>
<div className='w-full max-w-[480px]'>
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
<ApiConnectionMod className='w-4 h-4 text-text-secondary' />
<div className='overflow-hidden text-text-secondary text-ellipsis system-sm-medium'>
{currentDataset?.external_knowledge_info.external_knowledge_api_name}
</div>
<div className='text-text-tertiary system-xs-regular'>·</div>
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}</div>
</div>
</div>
</div>
<div className={rowClass}>
<div className={labelClass}>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeID')}</div>
</div>
<div className='w-full max-w-[480px]'>
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_id}</div>
</div>
</div>
</div>
<div className={rowClass}><Divider/></div>
</>
: <div className={rowClass}>
<div className={labelClass}>
<div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='leading-[18px] text-xs font-normal text-gray-500'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
</div>
</div>
<div className='w-[480px]'>
{indexMethod === 'high_quality'
? (
<RetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)
: (
<EconomicalRetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)}
</div>
</div>
<div className='w-[480px]'>
{indexMethod === 'high_quality'
? (
<RetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)
: (
<EconomicalRetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)}
</div>
</div>
}
<div className={rowClass}>
<div className={labelClass} />
<div className='w-[480px]'>

View File

@ -51,7 +51,7 @@ const DatasetNav = () => {
navs={datasetItems.map(dataset => ({
id: dataset.id,
name: dataset.name,
link: `/datasets/${dataset.id}/documents`,
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
icon: dataset.icon,
icon_background: dataset.icon_background,
})) as NavItem[]}

View File

@ -1,6 +1,7 @@
const translation = {
title: 'Retrieval Testing',
desc: 'Test the hitting effect of the Knowledge based on the given query text.',
settingTitle: 'Retrieval Setting',
desc: 'Test the hitting effect of the Knowledge based on the given query text',
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
recents: 'Recents',
table: {

View File

@ -1,13 +1,13 @@
const translation = {
title: 'Knowledge settings',
desc: 'Here you can modify the properties and working methods of the Knowledge.',
desc: 'Here you can modify the properties and retrieval settings of this Knowledge.',
form: {
name: 'Knowledge Name',
namePlaceholder: 'Please enter the Knowledge name',
nameError: 'Name cannot be empty',
desc: 'Knowledge description',
desc: 'Knowledge Description',
descInfo: 'Please write a clear textual description to outline the content of the Knowledge. This description will be used as a basis for matching when selecting from multiple Knowledge for inference.',
descPlaceholder: 'Describe what is in this Knowledge. A detailed description allows AI to access the content of the Knowledge in a timely manner. If empty, Dify will use the default hit strategy.',
descPlaceholder: 'Describe what\'s in this Knowledge (optional)',
descWrite: 'Learn how to write a good Knowledge description.',
permissions: 'Permissions',
permissionsOnlyMe: 'Only me',
@ -23,11 +23,14 @@ const translation = {
embeddingModelTip: 'Change the embedded model, please go to ',
embeddingModelTipLink: 'Settings',
retrievalSetting: {
title: 'Retrieval setting',
title: 'Retrieval Setting',
learnMore: 'Learn more',
description: ' about retrieval method.',
longDescription: ' about retrieval method, you can change this at any time in the Knowledge settings.',
},
externalKnowledgeAPI: 'External Knowledge API',
externalKnowledgeID: 'External Knowledge ID',
retrievalSettings: 'Retrieval Settings',
save: 'Save',
},
}

View File

@ -1,6 +1,7 @@
const translation = {
title: '召回测试',
desc: '基于给定的查询文本测试知识库的召回效果。',
settingTitle: '召回设置',
desc: '基于给定的查询文本测试知识库的召回效果',
dateTimeFormat: 'YYYY-MM-DD HH:mm',
recents: '最近查询',
table: {

View File

@ -1,13 +1,13 @@
const translation = {
title: '知识库设置',
desc: '在这里您可以修改知识库的工作方式以及其它设置。',
desc: '在这里,您可以修改此知识库的属性和检索设置',
form: {
name: '知识库名称',
namePlaceholder: '请输入知识库名称',
nameError: '名称不能为空',
desc: '知识库描述',
descInfo: '请写出清楚的文字描述来概述知识库的内容。当从多个知识库中进行选择匹配时,该描述将用作匹配的基础。',
descPlaceholder: '描述这个知识库中的内容。详细的描述可以让 AI 及时访问知识库的内容。如果为空Dify 将使用默认的命中策略。',
descPlaceholder: '请描述这个知识库包含的内容(可选)',
descWrite: '了解如何编写更好的知识库描述。',
permissions: '可见权限',
permissionsOnlyMe: '只有我',
@ -28,6 +28,8 @@ const translation = {
description: '关于检索方法。',
longDescription: '关于检索方法,您可以随时在知识库设置中更改此设置。',
},
externalKnowledgeAPI: '外部知识 API',
externalKnowledgeID: '外部知识库 ID',
save: '保存',
},
}

View File

@ -33,6 +33,16 @@ export type DataSet = {
retrieval_model: RetrievalConfig
tags: Tag[]
partial_member_list?: any[]
external_knowledge_info: {
external_knowledge_id: string
external_knowledge_api_id: string
external_knowledge_api_name: string
external_knowledge_api_endpoint: string
}
external_retrieval_model: {
top_k: number
score_threshold: number
}
}
export type ExternalAPIItem = {
@ -434,6 +444,16 @@ export type HitTesting = {
tsne_position: TsnePosition
}
export type ExternalKnowledgeBaseHitTesting = {
content: string
title: string
score: number
metadata: {
'x-amz-bedrock-kb-source-uri': string
'x-amz-bedrock-kb-data-source-id': string
}
}
export type Segment = {
id: string
document: Document
@ -474,6 +494,13 @@ export type HitTestingResponse = {
records: Array<HitTesting>
}
export type ExternalKnowledgeBaseHitTestingResponse = {
query: {
content: string
}
records: Array<ExternalKnowledgeBaseHitTesting>
}
export type RelatedApp = {
id: string
name: string

View File

@ -12,6 +12,7 @@ import type {
ExternalAPIItem,
ExternalAPIListResponse,
ExternalAPIUsage,
ExternalKnowledgeBaseHitTestingResponse,
ExternalKnowledgeItem,
FileIndexingEstimateResponse,
HitTestingRecordsResponse,
@ -244,6 +245,10 @@ export const hitTesting: Fetcher<HitTestingResponse, { datasetId: string; queryT
return post<HitTestingResponse>(`/datasets/${datasetId}/hit-testing`, { body: { query: queryText, retrieval_model } })
}
export const externalKnowledgeBaseHitTesting: Fetcher<ExternalKnowledgeBaseHitTestingResponse, { datasetId: string; query: string; external_retrieval_model: { top_k: number; score_threshold: number } }> = ({ datasetId, query, external_retrieval_model }) => {
return post<ExternalKnowledgeBaseHitTestingResponse>(`/datasets/${datasetId}/external-hit-testing`, { body: { query, external_retrieval_model } })
}
export const fetchTestingRecords: Fetcher<HitTestingRecordsResponse, { datasetId: string; params: { page: number; limit: number } }> = ({ datasetId, params }) => {
return get<HitTestingRecordsResponse>(`/datasets/${datasetId}/queries`, { params })
}