mirror of
https://github.com/langgenius/dify.git
synced 2025-12-26 09:32:17 +00:00
feat: knowledge base node
This commit is contained in:
parent
12c060b795
commit
a478d95950
@ -4,7 +4,7 @@ import {
|
||||
} from '@/app/components/workflow/context'
|
||||
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import dataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
|
||||
import knowledgeBaseNodeDefault from '@/app/components/workflow/nodes/knowledge-base/default'
|
||||
import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
@ -13,11 +13,11 @@ import { createRagPipelineSliceSlice } from './store'
|
||||
import RagPipelineMain from './components/rag-pipeline-main'
|
||||
|
||||
const RagPipeline = () => {
|
||||
const { newNode: DataSourceNode } = generateNewNode({
|
||||
const { newNode: knowledgeBaseNode } = generateNewNode({
|
||||
data: {
|
||||
type: dataSourceNodeDefault.metaData.type,
|
||||
title: 'data-source',
|
||||
...dataSourceNodeDefault.defaultValue,
|
||||
type: knowledgeBaseNodeDefault.metaData.type,
|
||||
title: 'knowledge-base',
|
||||
...knowledgeBaseNodeDefault.defaultValue,
|
||||
},
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET,
|
||||
@ -30,11 +30,11 @@ const RagPipeline = () => {
|
||||
>
|
||||
<WorkflowWithDefaultContext
|
||||
edges={[]}
|
||||
nodes={[DataSourceNode]}
|
||||
nodes={[knowledgeBaseNode]}
|
||||
>
|
||||
<RagPipelineMain
|
||||
edges={[]}
|
||||
nodes={[DataSourceNode]}
|
||||
nodes={[knowledgeBaseNode]}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
</WorkflowContextProvider>
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
GeneralChunk,
|
||||
ParentChildChunk,
|
||||
QuestionAndAnswer,
|
||||
} from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import { ChunkStructureEnum } from '../../types'
|
||||
import type { Option } from './type'
|
||||
|
||||
export const useChunkStructure = () => {
|
||||
const [chunk, setChunk] = useState('general')
|
||||
const GeneralOption: Option = {
|
||||
key: 'general',
|
||||
id: ChunkStructureEnum.general,
|
||||
icon: <GeneralChunk className='h-[18px] w-[18px] text-util-colors-indigo-indigo-600' />,
|
||||
title: 'General',
|
||||
description: 'General text chunking mode, the chunks retrieved and recalled are the same.',
|
||||
@ -17,7 +16,7 @@ export const useChunkStructure = () => {
|
||||
showEffectColor: true,
|
||||
}
|
||||
const ParentChildOption: Option = {
|
||||
key: 'parent-child',
|
||||
id: ChunkStructureEnum.parent_child,
|
||||
icon: <ParentChildChunk className='h-[18px] w-[18px] text-util-colors-blue-light-blue-light-500' />,
|
||||
title: 'Parent-Child',
|
||||
description: 'Parent-child text chunking mode, the chunks retrieved and recalled are different.',
|
||||
@ -25,16 +24,16 @@ export const useChunkStructure = () => {
|
||||
showEffectColor: true,
|
||||
}
|
||||
const QuestionAnswerOption: Option = {
|
||||
key: 'question-answer',
|
||||
id: ChunkStructureEnum.question_answer,
|
||||
icon: <QuestionAndAnswer className='h-[18px] w-[18px] text-text-tertiary' />,
|
||||
title: 'Question-Answer',
|
||||
description: 'Question-answer text chunking mode, the chunks retrieved and recalled are different.',
|
||||
}
|
||||
|
||||
const optionMap: Record<string, Option> = {
|
||||
'general': GeneralOption,
|
||||
'parent-child': ParentChildOption,
|
||||
'question-answer': QuestionAnswerOption,
|
||||
const optionMap: Record<ChunkStructureEnum, Option> = {
|
||||
[ChunkStructureEnum.general]: GeneralOption,
|
||||
[ChunkStructureEnum.parent_child]: ParentChildOption,
|
||||
[ChunkStructureEnum.question_answer]: QuestionAnswerOption,
|
||||
}
|
||||
|
||||
const options = [
|
||||
@ -46,7 +45,5 @@ export const useChunkStructure = () => {
|
||||
return {
|
||||
options,
|
||||
optionMap,
|
||||
chunk,
|
||||
setChunk,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import { memo } from 'react'
|
||||
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import type { ChunkStructureEnum } from '../../types'
|
||||
import OptionCard from '../option-card'
|
||||
import Selector from './selector'
|
||||
import { useChunkStructure } from './hooks'
|
||||
|
||||
const ChunkStructure = () => {
|
||||
type ChunkStructureProps = {
|
||||
chunkStructure: ChunkStructureEnum
|
||||
onChunkStructureChange: (value: ChunkStructureEnum) => void
|
||||
}
|
||||
const ChunkStructure = ({
|
||||
chunkStructure,
|
||||
onChunkStructureChange,
|
||||
}: ChunkStructureProps) => {
|
||||
const {
|
||||
chunk,
|
||||
setChunk,
|
||||
options,
|
||||
optionMap,
|
||||
} = useChunkStructure()
|
||||
@ -19,15 +26,15 @@ const ChunkStructure = () => {
|
||||
operation: (
|
||||
<Selector
|
||||
options={options}
|
||||
value={chunk}
|
||||
onChange={setChunk}
|
||||
value={chunkStructure}
|
||||
onChange={onChunkStructureChange}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<OptionCard {...optionMap[chunk]} />
|
||||
<OptionCard {...optionMap[chunkStructure]} />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChunkStructure
|
||||
export default memo(ChunkStructure)
|
||||
|
||||
@ -5,13 +5,14 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { ChunkStructureEnum } from '../../types'
|
||||
import OptionCard from '../option-card'
|
||||
import type { Option } from './type'
|
||||
|
||||
type SelectorProps = {
|
||||
options: Option[]
|
||||
value: string
|
||||
onChange: (key: string) => void
|
||||
value: ChunkStructureEnum
|
||||
onChange: (key: ChunkStructureEnum) => void
|
||||
}
|
||||
const Selector = ({
|
||||
options,
|
||||
@ -47,15 +48,16 @@ const Selector = ({
|
||||
{
|
||||
options.map(option => (
|
||||
<OptionCard
|
||||
key={option.key}
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
icon={option.icon}
|
||||
title={option.title}
|
||||
description={option.description}
|
||||
onClick={() => {
|
||||
onChange(option.key)
|
||||
onChange(option.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
showHighlightBorder={value === option.key}
|
||||
showHighlightBorder={value === option.id}
|
||||
></OptionCard>
|
||||
))
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ChunkStructureEnum } from '../../types'
|
||||
|
||||
export type Option = {
|
||||
key: string
|
||||
id: ChunkStructureEnum
|
||||
icon: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import {
|
||||
@ -11,10 +14,33 @@ import Input from '@/app/components/base/input'
|
||||
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import OptionCard from './option-card'
|
||||
import cn from '@/utils/classnames'
|
||||
import { IndexMethodEnum } from '../types'
|
||||
|
||||
const IndexMethod = () => {
|
||||
type IndexMethodProps = {
|
||||
indexMethod: IndexMethodEnum
|
||||
onIndexMethodChange: (value: IndexMethodEnum) => void
|
||||
keywordNumber: number
|
||||
onKeywordNumberChange: (value: number) => void
|
||||
}
|
||||
const IndexMethod = ({
|
||||
indexMethod,
|
||||
onIndexMethodChange,
|
||||
keywordNumber,
|
||||
onKeywordNumberChange,
|
||||
}: IndexMethodProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [method, setMethod] = useState('high_quality')
|
||||
const isHighQuality = indexMethod === IndexMethodEnum.QUALIFIED
|
||||
const isEconomy = indexMethod === IndexMethodEnum.ECONOMICAL
|
||||
|
||||
const handleIndexMethodChange = useCallback((newIndexMethod: IndexMethodEnum) => {
|
||||
onIndexMethodChange(newIndexMethod)
|
||||
}, [onIndexMethodChange])
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = Number(e.target.value)
|
||||
if (!Number.isNaN(value))
|
||||
onKeywordNumberChange(value)
|
||||
}, [onKeywordNumberChange])
|
||||
|
||||
return (
|
||||
<Field
|
||||
@ -23,37 +49,39 @@ const IndexMethod = () => {
|
||||
}}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
<OptionCard
|
||||
<OptionCard<IndexMethodEnum>
|
||||
id={IndexMethodEnum.QUALIFIED}
|
||||
icon={
|
||||
<HighQuality
|
||||
className={cn(
|
||||
'h-[15px] w-[15px] text-text-tertiary',
|
||||
method === 'high_quality' && 'text-util-colors-orange-orange-500',
|
||||
isHighQuality && 'text-util-colors-orange-orange-500',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
title={t('datasetCreation.stepTwo.qualified')}
|
||||
description={t('datasetSettings.form.indexMethodHighQualityTip')}
|
||||
showHighlightBorder={method === 'high_quality'}
|
||||
onClick={() => setMethod('high_quality')}
|
||||
showHighlightBorder={isHighQuality}
|
||||
onClick={handleIndexMethodChange}
|
||||
isRecommended
|
||||
></OptionCard>
|
||||
<OptionCard
|
||||
id={IndexMethodEnum.ECONOMICAL}
|
||||
icon={
|
||||
<Economic
|
||||
className={cn(
|
||||
'h-[15px] w-[15px] text-text-tertiary',
|
||||
method === 'economy' && 'text-util-colors-indigo-indigo-500',
|
||||
isEconomy && 'text-util-colors-indigo-indigo-500',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
title={t('datasetSettings.form.indexMethodEconomy')}
|
||||
description={t('datasetSettings.form.indexMethodEconomyTip')}
|
||||
showChildren={method === 'economy'}
|
||||
showHighlightBorder={method === 'economy'}
|
||||
onClick={() => setMethod('economy')}
|
||||
showChildren={isEconomy}
|
||||
showHighlightBorder={isEconomy}
|
||||
onClick={handleIndexMethodChange}
|
||||
effectColor='blue'
|
||||
showEffectColor={method === 'economy'}
|
||||
showEffectColor={isEconomy}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex grow items-center'>
|
||||
@ -68,15 +96,15 @@ const IndexMethod = () => {
|
||||
</div>
|
||||
<Slider
|
||||
className='mr-3 w-24 shrink-0'
|
||||
value={0}
|
||||
onChange={() => {
|
||||
console.log('change')
|
||||
}}
|
||||
value={keywordNumber}
|
||||
onChange={onKeywordNumberChange}
|
||||
/>
|
||||
<Input
|
||||
className='shrink-0'
|
||||
wrapperClassName='shrink-0 w-[72px]'
|
||||
type='number'
|
||||
value={keywordNumber}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</OptionCard>
|
||||
@ -85,4 +113,4 @@ const IndexMethod = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexMethod
|
||||
export default memo(IndexMethod)
|
||||
|
||||
@ -16,7 +16,9 @@ const HEADER_EFFECT_MAP: Record<string, ReactNode> = {
|
||||
'orange': <OptionCardEffectOrange />,
|
||||
'purple': <OptionCardEffectPurple />,
|
||||
}
|
||||
type OptionCardProps = {
|
||||
type OptionCardProps<T> = {
|
||||
id: T
|
||||
className?: string
|
||||
showHighlightBorder?: boolean
|
||||
showRadio?: boolean
|
||||
radioIsActive?: boolean
|
||||
@ -28,9 +30,11 @@ type OptionCardProps = {
|
||||
showChildren?: boolean
|
||||
effectColor?: string
|
||||
showEffectColor?: boolean
|
||||
onClick?: () => void
|
||||
onClick?: (id: T) => void
|
||||
}
|
||||
const OptionCard = ({
|
||||
const OptionCard = memo(({
|
||||
id,
|
||||
className,
|
||||
showHighlightBorder,
|
||||
showRadio,
|
||||
radioIsActive,
|
||||
@ -43,17 +47,18 @@ const OptionCard = ({
|
||||
effectColor,
|
||||
showEffectColor,
|
||||
onClick,
|
||||
}: OptionCardProps) => {
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg',
|
||||
showHighlightBorder && 'border-[2px] border-components-option-card-option-selected-border',
|
||||
)}
|
||||
onClick={onClick}
|
||||
onClick={() => onClick?.(id)}
|
||||
>
|
||||
<div className={cn(
|
||||
'relative flex rounded-t-xl p-2',
|
||||
className,
|
||||
)}>
|
||||
{
|
||||
effectColor && showEffectColor && (
|
||||
@ -110,6 +115,6 @@ const OptionCard = ({
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}) as <T>(props: OptionCardProps<T>) => JSX.Element
|
||||
|
||||
export default memo(OptionCard)
|
||||
export default OptionCard
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { FullTextSearch } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import OptionCard from '../option-card'
|
||||
|
||||
const FullTextSearchCard = () => {
|
||||
return (
|
||||
<OptionCard
|
||||
icon={<FullTextSearch className='h-[15px] w-[15px] text-text-tertiary' />}
|
||||
title='Full-Text Search'
|
||||
description="Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model."
|
||||
effectColor='purple'
|
||||
>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div>Vector Search Settings</div>
|
||||
<div>Additional Settings</div>
|
||||
</div>
|
||||
</OptionCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default FullTextSearchCard
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
FullTextSearch,
|
||||
HybridSearch,
|
||||
VectorSearch,
|
||||
} from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import {
|
||||
HybridSearchModeEnum,
|
||||
RetrievalSearchMethodEnum,
|
||||
} from '../../types'
|
||||
import type {
|
||||
HybridSearchModeOption,
|
||||
Option,
|
||||
} from './type'
|
||||
|
||||
export const useRetrievalSetting = () => {
|
||||
const VectorSearchOption: Option = {
|
||||
id: RetrievalSearchMethodEnum.semantic,
|
||||
icon: VectorSearch as any,
|
||||
title: 'Vector Search',
|
||||
description: 'Generate query embeddings and search for the text chunk most similar to its vector representation.',
|
||||
effectColor: 'purple',
|
||||
}
|
||||
const FullTextSearchOption: Option = {
|
||||
id: RetrievalSearchMethodEnum.fullText,
|
||||
icon: FullTextSearch as any,
|
||||
title: 'Full-Text Search',
|
||||
description: 'Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user\'s query. Users can choose to set weights or configure to a Rerank model.',
|
||||
effectColor: 'purple',
|
||||
}
|
||||
const HybridSearchOption: Option = {
|
||||
id: RetrievalSearchMethodEnum.hybrid,
|
||||
icon: HybridSearch as any,
|
||||
title: 'Hybrid Search',
|
||||
description: 'Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user\'s query. Users can choose to set weights or configure to a Rerank model.',
|
||||
effectColor: 'purple',
|
||||
}
|
||||
|
||||
const options = [
|
||||
VectorSearchOption,
|
||||
FullTextSearchOption,
|
||||
HybridSearchOption,
|
||||
]
|
||||
|
||||
const WeightedScoreModeOption: HybridSearchModeOption = {
|
||||
id: HybridSearchModeEnum.WeightedScore,
|
||||
title: 'Weighted Score',
|
||||
description: 'By adjusting the weights assigned, this rerank strategy determines whether to prioritize semantic or keyword matching.',
|
||||
}
|
||||
|
||||
const RerankModelModeOption: HybridSearchModeOption = {
|
||||
id: HybridSearchModeEnum.RerankingModel,
|
||||
title: 'Rerank Model',
|
||||
description: 'Rerank model will reorder the candidate document list based on the semantic match with user query, improving the results of semantic ranking.',
|
||||
}
|
||||
|
||||
const hybridSearchModeOptions = [
|
||||
WeightedScoreModeOption,
|
||||
RerankModelModeOption,
|
||||
]
|
||||
|
||||
return {
|
||||
options,
|
||||
hybridSearchModeOptions,
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { HybridSearch } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import OptionCard from '../option-card'
|
||||
|
||||
const HybridSearchCard = () => {
|
||||
return (
|
||||
<OptionCard
|
||||
icon={<HybridSearch className='h-[15px] w-[15px] text-text-tertiary' />}
|
||||
title='Hybrid Search'
|
||||
description="Execute full-text search and vector searches simultaneously, re-rank to select the best match for the user's query. Users can choose to set weights or configure to a Rerank model."
|
||||
effectColor='purple'
|
||||
isRecommended
|
||||
>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div>Vector Search Settings</div>
|
||||
<div>Additional Settings</div>
|
||||
</div>
|
||||
</OptionCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default HybridSearchCard
|
||||
@ -1,9 +1,139 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import VectorSearchCard from './vector-search'
|
||||
import FullTextSearchCard from './full-text-search'
|
||||
import HybridSearchCard from './hybrid-search'
|
||||
import cn from '@/utils/classnames'
|
||||
import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
|
||||
import { DEFAULT_WEIGHTED_SCORE } from '@/models/datasets'
|
||||
import {
|
||||
HybridSearchModeEnum,
|
||||
RetrievalSearchMethodEnum,
|
||||
} from '../../types'
|
||||
import type {
|
||||
RerankingModel,
|
||||
WeightedScore,
|
||||
} from '../../types'
|
||||
import OptionCard from '../option-card'
|
||||
import { useRetrievalSetting } from './hooks'
|
||||
import type { Option } from './type'
|
||||
import TopKAndScoreThreshold from './top-k-and-score-threshold'
|
||||
import RerankingModelSelector from './reranking-model-selector'
|
||||
|
||||
type RetrievalSettingProps = {
|
||||
searchMethod: RetrievalSearchMethodEnum
|
||||
onRetrievalSearchMethodChange: (value: RetrievalSearchMethodEnum) => void
|
||||
hybridSearchMode: HybridSearchModeEnum
|
||||
onHybridSearchModeChange: (value: HybridSearchModeEnum) => void
|
||||
rerankingModel?: RerankingModel
|
||||
onRerankingModelChange: (model: RerankingModel) => void
|
||||
weightedScore?: WeightedScore
|
||||
onWeightedScoreChange: (value: { value: number[] }) => void
|
||||
}
|
||||
const RetrievalSetting = ({
|
||||
searchMethod,
|
||||
onRetrievalSearchMethodChange,
|
||||
hybridSearchMode,
|
||||
onHybridSearchModeChange,
|
||||
weightedScore,
|
||||
onWeightedScoreChange,
|
||||
rerankingModel,
|
||||
onRerankingModelChange,
|
||||
}: RetrievalSettingProps) => {
|
||||
const {
|
||||
options,
|
||||
hybridSearchModeOptions,
|
||||
} = useRetrievalSetting()
|
||||
|
||||
const renderOptionCard = useCallback((option: Option) => {
|
||||
const Icon = option.icon
|
||||
const isActive = searchMethod === option.id
|
||||
const isHybridSearch = searchMethod === RetrievalSearchMethodEnum.hybrid
|
||||
const isHybridSearchWeightedScoreMode = hybridSearchMode === HybridSearchModeEnum.WeightedScore
|
||||
const weightedScoreValue = (() => {
|
||||
const sematicWeightedScore = weightedScore?.vector_setting.vector_weight ?? DEFAULT_WEIGHTED_SCORE.other.semantic
|
||||
const keywordWeightedScore = weightedScore?.keyword_setting.keyword_weight ?? DEFAULT_WEIGHTED_SCORE.other.keyword
|
||||
const mergedValue = [sematicWeightedScore, keywordWeightedScore]
|
||||
|
||||
return {
|
||||
value: mergedValue,
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<OptionCard
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
icon={
|
||||
<Icon
|
||||
className={cn(
|
||||
'h-[15px] w-[15px] text-text-tertiary',
|
||||
isActive && 'text-util-colors-purple-purple-600',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
title={option.title}
|
||||
description={option.description}
|
||||
effectColor={option.effectColor}
|
||||
isRecommended={option.id === RetrievalSearchMethodEnum.hybrid}
|
||||
onClick={onRetrievalSearchMethodChange}
|
||||
showChildren={isActive}
|
||||
showHighlightBorder={isActive}
|
||||
showEffectColor={isActive}
|
||||
>
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
isHybridSearch && (
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
hybridSearchModeOptions.map(hybridOption => (
|
||||
<OptionCard
|
||||
key={hybridOption.id}
|
||||
id={hybridOption.id}
|
||||
className='p-3'
|
||||
title={hybridOption.title}
|
||||
description={hybridOption.description}
|
||||
showRadio
|
||||
radioIsActive={hybridOption.id === hybridSearchMode}
|
||||
onClick={onHybridSearchModeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isHybridSearch && isHybridSearchWeightedScoreMode && (
|
||||
<WeightedScoreComponent
|
||||
value={weightedScoreValue}
|
||||
onChange={onWeightedScoreChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!(isHybridSearch && hybridSearchMode === HybridSearchModeEnum.WeightedScore) && (
|
||||
<RerankingModelSelector
|
||||
rerankingModel={rerankingModel}
|
||||
onRerankingModelChange={onRerankingModelChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<TopKAndScoreThreshold />
|
||||
</div>
|
||||
</OptionCard>
|
||||
)
|
||||
}, [
|
||||
searchMethod,
|
||||
onRetrievalSearchMethodChange,
|
||||
hybridSearchModeOptions,
|
||||
hybridSearchMode,
|
||||
onHybridSearchModeChange,
|
||||
rerankingModel,
|
||||
onRerankingModelChange,
|
||||
weightedScore,
|
||||
onWeightedScoreChange,
|
||||
])
|
||||
|
||||
const RetrievalSetting = () => {
|
||||
return (
|
||||
<Field
|
||||
fieldTitleProps={{
|
||||
@ -24,12 +154,10 @@ const RetrievalSetting = () => {
|
||||
}}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
<VectorSearchCard />
|
||||
<FullTextSearchCard />
|
||||
<HybridSearchCard />
|
||||
{options.map(renderOptionCard)}
|
||||
</div>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetrievalSetting
|
||||
export default memo(RetrievalSetting)
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RerankingModel } from '../../types'
|
||||
|
||||
type RerankingModelSelectorProps = {
|
||||
rerankingModel?: RerankingModel
|
||||
onRerankingModelChange?: (model: RerankingModel) => void
|
||||
}
|
||||
const RerankingModelSelector = ({
|
||||
rerankingModel,
|
||||
onRerankingModelChange,
|
||||
}: RerankingModelSelectorProps) => {
|
||||
const {
|
||||
modelList: rerankModelList,
|
||||
} = useModelListAndDefaultModel(ModelTypeEnum.rerank)
|
||||
const rerankModel = useMemo(() => {
|
||||
if (!rerankingModel)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
provider_name: rerankingModel.reranking_provider_name,
|
||||
model_name: rerankingModel.reranking_model_name,
|
||||
}
|
||||
}, [rerankingModel])
|
||||
|
||||
const handleRerankingModelChange = (model: DefaultModel) => {
|
||||
onRerankingModelChange?.({
|
||||
reranking_provider_name: model.provider,
|
||||
reranking_model_name: model.model,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
defaultModel={rerankModel && { provider: rerankModel.provider_name, model: rerankModel.model_name }}
|
||||
modelList={rerankModelList}
|
||||
onSelect={handleRerankingModelChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RerankingModelSelector)
|
||||
@ -0,0 +1,42 @@
|
||||
import { memo } from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const TopKAndScoreThreshold = () => {
|
||||
return (
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<div className='system-xs-medium mb-0.5 flex h-6 items-center text-text-secondary'>
|
||||
Top k
|
||||
<Tooltip
|
||||
triggerClassName='ml-0.5 shrink-0 w-3.5 h-3.5'
|
||||
popupContent='top k'
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type='number'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-0.5 flex h-6 items-center'>
|
||||
<Switch
|
||||
className='mr-2'
|
||||
/>
|
||||
<div className='system-sm-medium grow truncate text-text-secondary'>
|
||||
Score Threshold
|
||||
</div>
|
||||
<Tooltip
|
||||
triggerClassName='shrink-0 ml-0.5 w-3.5 h-3.5'
|
||||
popupContent='Score Threshold'
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type='number'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(TopKAndScoreThreshold)
|
||||
@ -0,0 +1,20 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type {
|
||||
HybridSearchModeEnum,
|
||||
RetrievalSearchMethodEnum,
|
||||
} from '../../types'
|
||||
|
||||
export type Option = {
|
||||
id: RetrievalSearchMethodEnum
|
||||
icon: ComponentType<any>
|
||||
title: any
|
||||
description: string
|
||||
effectColor?: string
|
||||
showEffectColor?: boolean,
|
||||
}
|
||||
|
||||
export type HybridSearchModeOption = {
|
||||
id: HybridSearchModeEnum
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { VectorSearch } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import OptionCard from '../option-card'
|
||||
|
||||
const VectorSearchCard = () => {
|
||||
return (
|
||||
<OptionCard
|
||||
icon={<VectorSearch className='h-[15px] w-[15px] text-text-tertiary' />}
|
||||
title='Vector Search'
|
||||
description='Generate query embeddings and search for the text chunk most similar to its vector representation.'
|
||||
effectColor='purple'
|
||||
>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div>Vector Search Settings</div>
|
||||
<div>Additional Settings</div>
|
||||
</div>
|
||||
</OptionCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default VectorSearchCard
|
||||
@ -1,5 +1,11 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { KnowledgeBaseNodeType } from './types'
|
||||
import {
|
||||
ChunkStructureEnum,
|
||||
HybridSearchModeEnum,
|
||||
IndexMethodEnum,
|
||||
RetrievalSearchMethodEnum,
|
||||
} from './types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
@ -9,7 +15,19 @@ const metaData = genNodeMetaData({
|
||||
})
|
||||
const nodeDefault: NodeDefault<KnowledgeBaseNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {},
|
||||
defaultValue: {
|
||||
index_chunk_variable_selector: [],
|
||||
chunk_structure: ChunkStructureEnum.general,
|
||||
indexing_technique: IndexMethodEnum.QUALIFIED,
|
||||
keyword_number: 10,
|
||||
retrieval_model: {
|
||||
search_method: RetrievalSearchMethodEnum.hybrid,
|
||||
top_k: 2,
|
||||
score_threshold_enabled: false,
|
||||
score_threshold: 0.5,
|
||||
hybridSearchMode: HybridSearchModeEnum.WeightedScore,
|
||||
},
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
|
||||
@ -1,19 +1,107 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import type { KnowledgeBaseNodeType } from '../types'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||
import type {
|
||||
ChunkStructureEnum,
|
||||
HybridSearchModeEnum,
|
||||
IndexMethodEnum,
|
||||
KnowledgeBaseNodeType,
|
||||
RerankingModel,
|
||||
RetrievalSearchMethodEnum,
|
||||
} from '../types'
|
||||
|
||||
export const useConfig = (id: string, payload: KnowledgeBaseNodeType) => {
|
||||
const {
|
||||
inputs,
|
||||
setInputs,
|
||||
} = useNodeCrud(id, payload)
|
||||
const ref = useRef(inputs)
|
||||
export const useConfig = (id: string) => {
|
||||
const store = useStoreApi()
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const handleInputsChange = useCallback((newInputs: KnowledgeBaseNodeType) => {
|
||||
setInputs(newInputs)
|
||||
ref.current = newInputs
|
||||
}, [setInputs, ref])
|
||||
const getNodeData = useCallback(() => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.find(node => node.id === id)
|
||||
}, [store, id])
|
||||
|
||||
const handleNodeDataUpdate = useCallback((data: Partial<KnowledgeBaseNodeType>) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data,
|
||||
})
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft])
|
||||
|
||||
const handleChunkStructureChange = useCallback((chunkStructure: ChunkStructureEnum) => {
|
||||
handleNodeDataUpdate({ chunk_structure: chunkStructure })
|
||||
}, [handleNodeDataUpdate])
|
||||
|
||||
const handleIndexMethodChange = useCallback((indexMethod: IndexMethodEnum) => {
|
||||
handleNodeDataUpdate({ indexing_technique: indexMethod })
|
||||
}, [handleNodeDataUpdate])
|
||||
|
||||
const handleKeywordNumberChange = useCallback((keywordNumber: number) => {
|
||||
handleNodeDataUpdate({ keyword_number: keywordNumber })
|
||||
}, [handleNodeDataUpdate])
|
||||
|
||||
const handleRetrievalSearchMethodChange = useCallback((searchMethod: RetrievalSearchMethodEnum) => {
|
||||
const nodeData = getNodeData()
|
||||
handleNodeDataUpdate({
|
||||
retrieval_model: {
|
||||
...nodeData?.data.retrieval_model,
|
||||
search_method: searchMethod,
|
||||
},
|
||||
})
|
||||
}, [getNodeData, handleNodeDataUpdate])
|
||||
|
||||
const handleHybridSearchModeChange = useCallback((hybridSearchMode: HybridSearchModeEnum) => {
|
||||
const nodeData = getNodeData()
|
||||
handleNodeDataUpdate({
|
||||
retrieval_model: {
|
||||
...nodeData?.data.retrieval_model,
|
||||
hybridSearchMode,
|
||||
},
|
||||
})
|
||||
}, [getNodeData, handleNodeDataUpdate])
|
||||
|
||||
const handleWeighedScoreChange = useCallback((weightedScore: { value: number[] }) => {
|
||||
const nodeData = getNodeData()
|
||||
handleNodeDataUpdate({
|
||||
retrieval_model: {
|
||||
...nodeData?.data.retrieval_model,
|
||||
weights: {
|
||||
weight_type: 'weighted_score',
|
||||
vector_setting: {
|
||||
vector_weight: weightedScore.value[0],
|
||||
embedding_provider_name: '',
|
||||
embedding_model_name: '',
|
||||
},
|
||||
keyword_setting: {
|
||||
keyword_weight: weightedScore.value[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [getNodeData, handleNodeDataUpdate])
|
||||
|
||||
const handleRerankingModelChange = useCallback((rerankingModel: RerankingModel) => {
|
||||
const nodeData = getNodeData()
|
||||
handleNodeDataUpdate({
|
||||
retrieval_model: {
|
||||
...nodeData?.data.retrieval_model,
|
||||
reranking_model: {
|
||||
reranking_provider_name: rerankingModel.reranking_provider_name,
|
||||
reranking_model_name: rerankingModel.reranking_model_name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [getNodeData, handleNodeDataUpdate])
|
||||
|
||||
return {
|
||||
handleChunkStructureChange,
|
||||
handleIndexMethodChange,
|
||||
handleKeywordNumberChange,
|
||||
handleRetrievalSearchMethodChange,
|
||||
handleHybridSearchModeChange,
|
||||
handleWeighedScoreChange,
|
||||
handleRerankingModelChange,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import type { KnowledgeBaseNodeType } from './types'
|
||||
import {
|
||||
IndexMethodEnum,
|
||||
} from './types'
|
||||
import InputVariable from './components/input-variable'
|
||||
import ChunkStructure from './components/chunk-structure'
|
||||
import IndexMethod from './components/index-method'
|
||||
import RetrievalSetting from './components/retrieval-setting'
|
||||
import EmbeddingModel from './components/embedding-model'
|
||||
import { useConfig } from './hooks/use-config'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import {
|
||||
Group,
|
||||
@ -13,7 +19,20 @@ import {
|
||||
} from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import Split from '../_base/components/split'
|
||||
|
||||
const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = () => {
|
||||
const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const {
|
||||
handleChunkStructureChange,
|
||||
handleIndexMethodChange,
|
||||
handleKeywordNumberChange,
|
||||
handleRetrievalSearchMethodChange,
|
||||
handleHybridSearchModeChange,
|
||||
handleWeighedScoreChange,
|
||||
handleRerankingModelChange,
|
||||
} = useConfig(id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GroupWithBox boxProps={{ withBorderBottom: true }}>
|
||||
@ -23,16 +42,37 @@ const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = () => {
|
||||
className='py-3'
|
||||
withBorderBottom
|
||||
>
|
||||
<ChunkStructure />
|
||||
<ChunkStructure
|
||||
chunkStructure={data.chunk_structure}
|
||||
onChunkStructureChange={handleChunkStructureChange}
|
||||
/>
|
||||
</Group>
|
||||
<GroupWithBox>
|
||||
<div className='space-y-3'>
|
||||
<IndexMethod />
|
||||
<EmbeddingModel />
|
||||
<IndexMethod
|
||||
indexMethod={data.indexing_technique}
|
||||
onIndexMethodChange={handleIndexMethodChange}
|
||||
keywordNumber={data.keyword_number}
|
||||
onKeywordNumberChange={handleKeywordNumberChange}
|
||||
/>
|
||||
{
|
||||
data.indexing_technique === IndexMethodEnum.QUALIFIED && (
|
||||
<EmbeddingModel />
|
||||
)
|
||||
}
|
||||
<div className='pt-1'>
|
||||
<Split className='h-[1px]' />
|
||||
</div>
|
||||
<RetrievalSetting />
|
||||
<RetrievalSetting
|
||||
searchMethod={data.retrieval_model.search_method}
|
||||
onRetrievalSearchMethodChange={handleRetrievalSearchMethodChange}
|
||||
hybridSearchMode={data.retrieval_model.hybridSearchMode}
|
||||
onHybridSearchModeChange={handleHybridSearchModeChange}
|
||||
weightedScore={data.retrieval_model.weights}
|
||||
onWeightedScoreChange={handleWeighedScoreChange}
|
||||
rerankingModel={data.retrieval_model.reranking_model}
|
||||
onRerankingModelChange={handleRerankingModelChange}
|
||||
/>
|
||||
</div>
|
||||
</GroupWithBox>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,52 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import type { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import type { WeightedScoreEnum } from '@/models/datasets'
|
||||
import type { RerankingModeEnum } from '@/models/datasets'
|
||||
export { WeightedScoreEnum } from '@/models/datasets'
|
||||
export { IndexingType as IndexMethodEnum } from '@/app/components/datasets/create/step-two'
|
||||
export { RETRIEVE_METHOD as RetrievalSearchMethodEnum } from '@/types/app'
|
||||
export { RerankingModeEnum as HybridSearchModeEnum } from '@/models/datasets'
|
||||
|
||||
export type KnowledgeBaseNodeType = CommonNodeType
|
||||
export enum ChunkStructureEnum {
|
||||
general = 'general',
|
||||
parent_child = 'parent-child',
|
||||
question_answer = 'question-answer',
|
||||
}
|
||||
|
||||
export type RerankingModel = {
|
||||
reranking_provider_name: string
|
||||
reranking_model_name: string
|
||||
}
|
||||
|
||||
export type WeightedScore = {
|
||||
weight_type: WeightedScoreEnum
|
||||
vector_setting: {
|
||||
vector_weight: number
|
||||
embedding_provider_name: string
|
||||
embedding_model_name: string
|
||||
}
|
||||
keyword_setting: {
|
||||
keyword_weight: number
|
||||
}
|
||||
}
|
||||
|
||||
export type RetrievalSetting = {
|
||||
search_method: RETRIEVE_METHOD
|
||||
reranking_enable?: boolean
|
||||
reranking_model?: RerankingModel
|
||||
weights?: WeightedScore
|
||||
top_k: number
|
||||
score_threshold_enabled: boolean
|
||||
score_threshold: number
|
||||
hybridSearchMode: RerankingModeEnum
|
||||
}
|
||||
export type KnowledgeBaseNodeType = CommonNodeType & {
|
||||
index_chunk_variable_selector: string[]
|
||||
chunk_structure: ChunkStructureEnum
|
||||
indexing_technique: IndexingType
|
||||
embedding_model?: string
|
||||
embedding_model_provider?: string
|
||||
keyword_number: number
|
||||
retrieval_model: RetrievalSetting
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export enum BlockEnum {
|
||||
LoopStart = 'loop-start',
|
||||
LoopEnd = 'loop-end',
|
||||
DataSource = 'data-source',
|
||||
KnowledgeBase = 'knowledge-base',
|
||||
KnowledgeBase = 'knowledge-index',
|
||||
}
|
||||
|
||||
export enum ControlMode {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user