feat: knowledge base node

This commit is contained in:
zxhlyh 2025-05-06 17:24:30 +08:00
parent 12c060b795
commit a478d95950
20 changed files with 623 additions and 146 deletions

View File

@ -4,7 +4,7 @@ import {
} from '@/app/components/workflow/context' } from '@/app/components/workflow/context'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import { generateNewNode } from '@/app/components/workflow/utils' 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 { import {
NODE_WIDTH_X_OFFSET, NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION, START_INITIAL_POSITION,
@ -13,11 +13,11 @@ import { createRagPipelineSliceSlice } from './store'
import RagPipelineMain from './components/rag-pipeline-main' import RagPipelineMain from './components/rag-pipeline-main'
const RagPipeline = () => { const RagPipeline = () => {
const { newNode: DataSourceNode } = generateNewNode({ const { newNode: knowledgeBaseNode } = generateNewNode({
data: { data: {
type: dataSourceNodeDefault.metaData.type, type: knowledgeBaseNodeDefault.metaData.type,
title: 'data-source', title: 'knowledge-base',
...dataSourceNodeDefault.defaultValue, ...knowledgeBaseNodeDefault.defaultValue,
}, },
position: { position: {
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET, x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET,
@ -30,11 +30,11 @@ const RagPipeline = () => {
> >
<WorkflowWithDefaultContext <WorkflowWithDefaultContext
edges={[]} edges={[]}
nodes={[DataSourceNode]} nodes={[knowledgeBaseNode]}
> >
<RagPipelineMain <RagPipelineMain
edges={[]} edges={[]}
nodes={[DataSourceNode]} nodes={[knowledgeBaseNode]}
/> />
</WorkflowWithDefaultContext> </WorkflowWithDefaultContext>
</WorkflowContextProvider> </WorkflowContextProvider>

View File

@ -1,15 +1,14 @@
import { useState } from 'react'
import { import {
GeneralChunk, GeneralChunk,
ParentChildChunk, ParentChildChunk,
QuestionAndAnswer, QuestionAndAnswer,
} from '@/app/components/base/icons/src/vender/knowledge' } from '@/app/components/base/icons/src/vender/knowledge'
import { ChunkStructureEnum } from '../../types'
import type { Option } from './type' import type { Option } from './type'
export const useChunkStructure = () => { export const useChunkStructure = () => {
const [chunk, setChunk] = useState('general')
const GeneralOption: Option = { const GeneralOption: Option = {
key: 'general', id: ChunkStructureEnum.general,
icon: <GeneralChunk className='h-[18px] w-[18px] text-util-colors-indigo-indigo-600' />, icon: <GeneralChunk className='h-[18px] w-[18px] text-util-colors-indigo-indigo-600' />,
title: 'General', title: 'General',
description: 'General text chunking mode, the chunks retrieved and recalled are the same.', description: 'General text chunking mode, the chunks retrieved and recalled are the same.',
@ -17,7 +16,7 @@ export const useChunkStructure = () => {
showEffectColor: true, showEffectColor: true,
} }
const ParentChildOption: Option = { 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' />, icon: <ParentChildChunk className='h-[18px] w-[18px] text-util-colors-blue-light-blue-light-500' />,
title: 'Parent-Child', title: 'Parent-Child',
description: 'Parent-child text chunking mode, the chunks retrieved and recalled are different.', description: 'Parent-child text chunking mode, the chunks retrieved and recalled are different.',
@ -25,16 +24,16 @@ export const useChunkStructure = () => {
showEffectColor: true, showEffectColor: true,
} }
const QuestionAnswerOption: Option = { const QuestionAnswerOption: Option = {
key: 'question-answer', id: ChunkStructureEnum.question_answer,
icon: <QuestionAndAnswer className='h-[18px] w-[18px] text-text-tertiary' />, icon: <QuestionAndAnswer className='h-[18px] w-[18px] text-text-tertiary' />,
title: 'Question-Answer', title: 'Question-Answer',
description: 'Question-answer text chunking mode, the chunks retrieved and recalled are different.', description: 'Question-answer text chunking mode, the chunks retrieved and recalled are different.',
} }
const optionMap: Record<string, Option> = { const optionMap: Record<ChunkStructureEnum, Option> = {
'general': GeneralOption, [ChunkStructureEnum.general]: GeneralOption,
'parent-child': ParentChildOption, [ChunkStructureEnum.parent_child]: ParentChildOption,
'question-answer': QuestionAnswerOption, [ChunkStructureEnum.question_answer]: QuestionAnswerOption,
} }
const options = [ const options = [
@ -46,7 +45,5 @@ export const useChunkStructure = () => {
return { return {
options, options,
optionMap, optionMap,
chunk,
setChunk,
} }
} }

View File

@ -1,12 +1,19 @@
import { memo } from 'react'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout' import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import type { ChunkStructureEnum } from '../../types'
import OptionCard from '../option-card' import OptionCard from '../option-card'
import Selector from './selector' import Selector from './selector'
import { useChunkStructure } from './hooks' import { useChunkStructure } from './hooks'
const ChunkStructure = () => { type ChunkStructureProps = {
chunkStructure: ChunkStructureEnum
onChunkStructureChange: (value: ChunkStructureEnum) => void
}
const ChunkStructure = ({
chunkStructure,
onChunkStructureChange,
}: ChunkStructureProps) => {
const { const {
chunk,
setChunk,
options, options,
optionMap, optionMap,
} = useChunkStructure() } = useChunkStructure()
@ -19,15 +26,15 @@ const ChunkStructure = () => {
operation: ( operation: (
<Selector <Selector
options={options} options={options}
value={chunk} value={chunkStructure}
onChange={setChunk} onChange={onChunkStructureChange}
/> />
), ),
}} }}
> >
<OptionCard {...optionMap[chunk]} /> <OptionCard {...optionMap[chunkStructure]} />
</Field> </Field>
) )
} }
export default ChunkStructure export default memo(ChunkStructure)

View File

@ -5,13 +5,14 @@ import {
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { ChunkStructureEnum } from '../../types'
import OptionCard from '../option-card' import OptionCard from '../option-card'
import type { Option } from './type' import type { Option } from './type'
type SelectorProps = { type SelectorProps = {
options: Option[] options: Option[]
value: string value: ChunkStructureEnum
onChange: (key: string) => void onChange: (key: ChunkStructureEnum) => void
} }
const Selector = ({ const Selector = ({
options, options,
@ -47,15 +48,16 @@ const Selector = ({
{ {
options.map(option => ( options.map(option => (
<OptionCard <OptionCard
key={option.key} key={option.id}
id={option.id}
icon={option.icon} icon={option.icon}
title={option.title} title={option.title}
description={option.description} description={option.description}
onClick={() => { onClick={() => {
onChange(option.key) onChange(option.id)
setOpen(false) setOpen(false)
}} }}
showHighlightBorder={value === option.key} showHighlightBorder={value === option.id}
></OptionCard> ></OptionCard>
)) ))
} }

View File

@ -1,7 +1,8 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import type { ChunkStructureEnum } from '../../types'
export type Option = { export type Option = {
key: string id: ChunkStructureEnum
icon: ReactNode icon: ReactNode
title: string title: string
description: string description: string

View File

@ -1,4 +1,7 @@
import { useState } from 'react' import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiQuestionLine } from '@remixicon/react' import { RiQuestionLine } from '@remixicon/react'
import { import {
@ -11,10 +14,33 @@ import Input from '@/app/components/base/input'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout' import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import OptionCard from './option-card' import OptionCard from './option-card'
import cn from '@/utils/classnames' 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 { 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 ( return (
<Field <Field
@ -23,37 +49,39 @@ const IndexMethod = () => {
}} }}
> >
<div className='space-y-1'> <div className='space-y-1'>
<OptionCard <OptionCard<IndexMethodEnum>
id={IndexMethodEnum.QUALIFIED}
icon={ icon={
<HighQuality <HighQuality
className={cn( className={cn(
'h-[15px] w-[15px] text-text-tertiary', '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')} title={t('datasetCreation.stepTwo.qualified')}
description={t('datasetSettings.form.indexMethodHighQualityTip')} description={t('datasetSettings.form.indexMethodHighQualityTip')}
showHighlightBorder={method === 'high_quality'} showHighlightBorder={isHighQuality}
onClick={() => setMethod('high_quality')} onClick={handleIndexMethodChange}
isRecommended isRecommended
></OptionCard> ></OptionCard>
<OptionCard <OptionCard
id={IndexMethodEnum.ECONOMICAL}
icon={ icon={
<Economic <Economic
className={cn( className={cn(
'h-[15px] w-[15px] text-text-tertiary', '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')} title={t('datasetSettings.form.indexMethodEconomy')}
description={t('datasetSettings.form.indexMethodEconomyTip')} description={t('datasetSettings.form.indexMethodEconomyTip')}
showChildren={method === 'economy'} showChildren={isEconomy}
showHighlightBorder={method === 'economy'} showHighlightBorder={isEconomy}
onClick={() => setMethod('economy')} onClick={handleIndexMethodChange}
effectColor='blue' effectColor='blue'
showEffectColor={method === 'economy'} showEffectColor={isEconomy}
> >
<div className='flex items-center'> <div className='flex items-center'>
<div className='flex grow items-center'> <div className='flex grow items-center'>
@ -68,15 +96,15 @@ const IndexMethod = () => {
</div> </div>
<Slider <Slider
className='mr-3 w-24 shrink-0' className='mr-3 w-24 shrink-0'
value={0} value={keywordNumber}
onChange={() => { onChange={onKeywordNumberChange}
console.log('change')
}}
/> />
<Input <Input
className='shrink-0' className='shrink-0'
wrapperClassName='shrink-0 w-[72px]' wrapperClassName='shrink-0 w-[72px]'
type='number' type='number'
value={keywordNumber}
onChange={handleInputChange}
/> />
</div> </div>
</OptionCard> </OptionCard>
@ -85,4 +113,4 @@ const IndexMethod = () => {
) )
} }
export default IndexMethod export default memo(IndexMethod)

View File

@ -16,7 +16,9 @@ const HEADER_EFFECT_MAP: Record<string, ReactNode> = {
'orange': <OptionCardEffectOrange />, 'orange': <OptionCardEffectOrange />,
'purple': <OptionCardEffectPurple />, 'purple': <OptionCardEffectPurple />,
} }
type OptionCardProps = { type OptionCardProps<T> = {
id: T
className?: string
showHighlightBorder?: boolean showHighlightBorder?: boolean
showRadio?: boolean showRadio?: boolean
radioIsActive?: boolean radioIsActive?: boolean
@ -28,9 +30,11 @@ type OptionCardProps = {
showChildren?: boolean showChildren?: boolean
effectColor?: string effectColor?: string
showEffectColor?: boolean showEffectColor?: boolean
onClick?: () => void onClick?: (id: T) => void
} }
const OptionCard = ({ const OptionCard = memo(({
id,
className,
showHighlightBorder, showHighlightBorder,
showRadio, showRadio,
radioIsActive, radioIsActive,
@ -43,17 +47,18 @@ const OptionCard = ({
effectColor, effectColor,
showEffectColor, showEffectColor,
onClick, onClick,
}: OptionCardProps) => { }) => {
return ( return (
<div <div
className={cn( className={cn(
'cursor-pointer rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg', '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', showHighlightBorder && 'border-[2px] border-components-option-card-option-selected-border',
)} )}
onClick={onClick} onClick={() => onClick?.(id)}
> >
<div className={cn( <div className={cn(
'relative flex rounded-t-xl p-2', 'relative flex rounded-t-xl p-2',
className,
)}> )}>
{ {
effectColor && showEffectColor && ( effectColor && showEffectColor && (
@ -110,6 +115,6 @@ const OptionCard = ({
} }
</div> </div>
) )
} }) as <T>(props: OptionCardProps<T>) => JSX.Element
export default memo(OptionCard) export default OptionCard

View File

@ -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

View File

@ -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,
}
}

View File

@ -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

View File

@ -1,9 +1,139 @@
import {
memo,
useCallback,
} from 'react'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout' import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import VectorSearchCard from './vector-search' import cn from '@/utils/classnames'
import FullTextSearchCard from './full-text-search' import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
import HybridSearchCard from './hybrid-search' 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 ( return (
<Field <Field
fieldTitleProps={{ fieldTitleProps={{
@ -24,12 +154,10 @@ const RetrievalSetting = () => {
}} }}
> >
<div className='space-y-1'> <div className='space-y-1'>
<VectorSearchCard /> {options.map(renderOptionCard)}
<FullTextSearchCard />
<HybridSearchCard />
</div> </div>
</Field> </Field>
) )
} }
export default RetrievalSetting export default memo(RetrievalSetting)

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -1,5 +1,11 @@
import type { NodeDefault } from '../../types' import type { NodeDefault } from '../../types'
import type { KnowledgeBaseNodeType } from './types' import type { KnowledgeBaseNodeType } from './types'
import {
ChunkStructureEnum,
HybridSearchModeEnum,
IndexMethodEnum,
RetrievalSearchMethodEnum,
} from './types'
import { genNodeMetaData } from '@/app/components/workflow/utils' import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
@ -9,7 +15,19 @@ const metaData = genNodeMetaData({
}) })
const nodeDefault: NodeDefault<KnowledgeBaseNodeType> = { const nodeDefault: NodeDefault<KnowledgeBaseNodeType> = {
metaData, 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() { checkValid() {
return { return {
isValid: true, isValid: true,

View File

@ -1,19 +1,107 @@
import { import {
useCallback, useCallback,
useRef,
} from 'react' } from 'react'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useStoreApi } from 'reactflow'
import type { KnowledgeBaseNodeType } from '../types' import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type {
ChunkStructureEnum,
HybridSearchModeEnum,
IndexMethodEnum,
KnowledgeBaseNodeType,
RerankingModel,
RetrievalSearchMethodEnum,
} from '../types'
export const useConfig = (id: string, payload: KnowledgeBaseNodeType) => { export const useConfig = (id: string) => {
const { const store = useStoreApi()
inputs, const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
setInputs,
} = useNodeCrud(id, payload)
const ref = useRef(inputs)
const handleInputsChange = useCallback((newInputs: KnowledgeBaseNodeType) => { const getNodeData = useCallback(() => {
setInputs(newInputs) const { getNodes } = store.getState()
ref.current = newInputs const nodes = getNodes()
}, [setInputs, ref])
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,
}
} }

View File

@ -1,11 +1,17 @@
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react' import {
memo,
} from 'react'
import type { KnowledgeBaseNodeType } from './types' import type { KnowledgeBaseNodeType } from './types'
import {
IndexMethodEnum,
} from './types'
import InputVariable from './components/input-variable' import InputVariable from './components/input-variable'
import ChunkStructure from './components/chunk-structure' import ChunkStructure from './components/chunk-structure'
import IndexMethod from './components/index-method' import IndexMethod from './components/index-method'
import RetrievalSetting from './components/retrieval-setting' import RetrievalSetting from './components/retrieval-setting'
import EmbeddingModel from './components/embedding-model' import EmbeddingModel from './components/embedding-model'
import { useConfig } from './hooks/use-config'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import { import {
Group, Group,
@ -13,7 +19,20 @@ import {
} from '@/app/components/workflow/nodes/_base/components/layout' } from '@/app/components/workflow/nodes/_base/components/layout'
import Split from '../_base/components/split' 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 ( return (
<div> <div>
<GroupWithBox boxProps={{ withBorderBottom: true }}> <GroupWithBox boxProps={{ withBorderBottom: true }}>
@ -23,16 +42,37 @@ const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = () => {
className='py-3' className='py-3'
withBorderBottom withBorderBottom
> >
<ChunkStructure /> <ChunkStructure
chunkStructure={data.chunk_structure}
onChunkStructureChange={handleChunkStructureChange}
/>
</Group> </Group>
<GroupWithBox> <GroupWithBox>
<div className='space-y-3'> <div className='space-y-3'>
<IndexMethod /> <IndexMethod
<EmbeddingModel /> indexMethod={data.indexing_technique}
onIndexMethodChange={handleIndexMethodChange}
keywordNumber={data.keyword_number}
onKeywordNumberChange={handleKeywordNumberChange}
/>
{
data.indexing_technique === IndexMethodEnum.QUALIFIED && (
<EmbeddingModel />
)
}
<div className='pt-1'> <div className='pt-1'>
<Split className='h-[1px]' /> <Split className='h-[1px]' />
</div> </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> </div>
</GroupWithBox> </GroupWithBox>
</div> </div>

View File

@ -1,3 +1,52 @@
import type { CommonNodeType } from '@/app/components/workflow/types' 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
}

View File

@ -42,7 +42,7 @@ export enum BlockEnum {
LoopStart = 'loop-start', LoopStart = 'loop-start',
LoopEnd = 'loop-end', LoopEnd = 'loop-end',
DataSource = 'data-source', DataSource = 'data-source',
KnowledgeBase = 'knowledge-base', KnowledgeBase = 'knowledge-index',
} }
export enum ControlMode { export enum ControlMode {