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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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 { 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,

View File

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

View File

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

View File

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

View File

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