|
|
@@ -0,0 +1,459 @@
|
|
|
+<template>
|
|
|
+ <el-scrollbar class="w-full box-border p-12px">
|
|
|
+ <div class="knowledge-retrieval-setter">
|
|
|
+ <section class="section-block">
|
|
|
+ <div class="section-title-row">
|
|
|
+ <label class="section-title">{{ texts.queryVariable }}</label>
|
|
|
+ </div>
|
|
|
+ <VarSelect
|
|
|
+ v-model="formData.query_variable.value"
|
|
|
+ class="w-full"
|
|
|
+ :var-type="formData.query_variable.type"
|
|
|
+ :placeholder="texts.queryVariablePlaceholder"
|
|
|
+ @change="handleQueryVariableChange"
|
|
|
+ @clear="handleQueryVariableClear"
|
|
|
+ />
|
|
|
+ <div class="field-hint">{{ texts.queryVariableTip }}</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="section-block">
|
|
|
+ <div class="section-title-row">
|
|
|
+ <label class="section-title">{{ texts.knowledgeBases }}</label>
|
|
|
+ </div>
|
|
|
+ <el-select
|
|
|
+ v-model="formData.knowledge_base_ids"
|
|
|
+ multiple
|
|
|
+ filterable
|
|
|
+ allow-create
|
|
|
+ default-first-option
|
|
|
+ class="id-tag-input"
|
|
|
+ :placeholder="texts.knowledgeBasesPlaceholder"
|
|
|
+ :aria-label="texts.knowledgeBasesPlaceholder"
|
|
|
+ :loading="knowledgeBaseLoading"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in knowledgeBaseOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <div class="field-hint">{{ texts.knowledgeBasesTip }}</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="section-block">
|
|
|
+ <div class="section-title-row">
|
|
|
+ <label class="section-title">{{ texts.knowledgeIds }}</label>
|
|
|
+ </div>
|
|
|
+ <el-select
|
|
|
+ v-model="formData.knowledge_ids"
|
|
|
+ multiple
|
|
|
+ filterable
|
|
|
+ class="id-tag-input"
|
|
|
+ :placeholder="texts.knowledgeIdsPlaceholder"
|
|
|
+ :aria-label="texts.knowledgeIdsPlaceholder"
|
|
|
+ :loading="knowledgeLoading"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in knowledgeOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <div class="field-hint">{{ texts.knowledgeIdsTip }}</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="section-block">
|
|
|
+ <div class="grid-two">
|
|
|
+ <div class="field-block">
|
|
|
+ <div class="section-title-row section-title-row--compact">
|
|
|
+ <label class="section-title">{{ texts.topK }}</label>
|
|
|
+ </div>
|
|
|
+ <el-input-number v-model="formData.top_k" :min="1" :max="100" class="w-full" />
|
|
|
+ </div>
|
|
|
+ <div class="field-block">
|
|
|
+ <div class="section-title-row section-title-row--compact">
|
|
|
+ <label class="section-title">{{ texts.scoreThreshold }}</label>
|
|
|
+ <el-switch v-model="formData.score_threshold_enable" />
|
|
|
+ </div>
|
|
|
+ <el-input-number
|
|
|
+ v-model="formData.score_threshold"
|
|
|
+ :min="0"
|
|
|
+ :max="1"
|
|
|
+ :step="0.05"
|
|
|
+ :disabled="!formData.score_threshold_enable"
|
|
|
+ :precision="2"
|
|
|
+ class="w-full"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="section-block">
|
|
|
+ <el-collapse>
|
|
|
+ <el-collapse-item title="" name="2">
|
|
|
+ <template #title>
|
|
|
+ <div class="flex items-center justify-between beautify">
|
|
|
+ <label class="text-14px font-bold text-gray-700">输出变量</label>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <ul>
|
|
|
+ <li v-for="output in formData.outputs" :key="output.name">
|
|
|
+ <div>
|
|
|
+ <span class="text-#333">{{ output.name }}</span>
|
|
|
+ <span class="text-#999 ml-8px">{{ output.type }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="text-#666">{{ output.describe }}</div>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <NodeRuntimeConfig v-model="formData" />
|
|
|
+ </div>
|
|
|
+ </el-scrollbar>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onMounted, ref, watch } from 'vue'
|
|
|
+import { knowledge } from '@repo/api-service'
|
|
|
+
|
|
|
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
|
|
|
+import VarSelect from '@/nodes/_base/VarSelect.vue'
|
|
|
+import { useI18n } from '@/composables/useI18n'
|
|
|
+import { useSetterModel } from '../_shared/useSetterModel'
|
|
|
+
|
|
|
+import { createDefaultQueryVariable, type KnowledgeRetrievalData } from './index'
|
|
|
+import type { NodeVariableType } from '@/nodes/Interface'
|
|
|
+
|
|
|
+type SelectOption = {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+ raw?: any
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ data: KnowledgeRetrievalData
|
|
|
+}>()
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ (e: 'update', value: KnowledgeRetrievalData): void
|
|
|
+}>()
|
|
|
+
|
|
|
+const { t } = useI18n()
|
|
|
+const formData = useSetterModel<KnowledgeRetrievalData>(props, emit)
|
|
|
+const knowledgeBaseLoading = ref(false)
|
|
|
+const knowledgeLoading = ref(false)
|
|
|
+const knowledgeBaseOptions = ref<SelectOption[]>([])
|
|
|
+const knowledgeOptions = ref<SelectOption[]>([])
|
|
|
+const knowledgePageSize = 20
|
|
|
+
|
|
|
+const texts = computed(() => ({
|
|
|
+ queryVariable: t('pages.knowledgeRetrievalSetter.queryVariable'),
|
|
|
+ queryVariablePlaceholder: t('pages.knowledgeRetrievalSetter.queryVariablePlaceholder'),
|
|
|
+ queryVariableTip: t('pages.knowledgeRetrievalSetter.queryVariableTip'),
|
|
|
+ knowledgeBases: t('pages.knowledgeRetrievalSetter.knowledgeBases'),
|
|
|
+ knowledgeBasesPlaceholder: t('pages.knowledgeRetrievalSetter.knowledgeBasesPlaceholder'),
|
|
|
+ knowledgeBasesTip: t('pages.knowledgeRetrievalSetter.knowledgeBasesTip'),
|
|
|
+ knowledgeIds: t('pages.knowledgeRetrievalSetter.knowledgeIds'),
|
|
|
+ knowledgeIdsPlaceholder: t('pages.knowledgeRetrievalSetter.knowledgeIdsPlaceholder'),
|
|
|
+ knowledgeIdsTip: t('pages.knowledgeRetrievalSetter.knowledgeIdsTip'),
|
|
|
+ topK: t('pages.knowledgeRetrievalSetter.topK'),
|
|
|
+ scoreThreshold: t('pages.knowledgeRetrievalSetter.scoreThreshold'),
|
|
|
+ outputs: t('pages.knowledgeRetrievalSetter.outputs')
|
|
|
+}))
|
|
|
+
|
|
|
+const normalizeOptionLabel = (item: {
|
|
|
+ id?: string
|
|
|
+ knowledge_id?: string
|
|
|
+ name?: string
|
|
|
+ title?: string
|
|
|
+ file_name?: string
|
|
|
+ knowledge_filename?: string
|
|
|
+ knowledge_title?: string
|
|
|
+}) =>
|
|
|
+ item.title ||
|
|
|
+ item.name ||
|
|
|
+ item.file_name ||
|
|
|
+ item.knowledge_filename ||
|
|
|
+ item.knowledge_title ||
|
|
|
+ item.id ||
|
|
|
+ item.knowledge_id ||
|
|
|
+ ''
|
|
|
+
|
|
|
+const normalizeOptionValue = (item: { id?: string; knowledge_id?: string; file_id?: string }) =>
|
|
|
+ item.id || item.knowledge_id || item.file_id || ''
|
|
|
+
|
|
|
+const mergeSelectedOptions = (options: SelectOption[], selectedValues: string[]) => {
|
|
|
+ const optionMap = new Map<string, SelectOption>()
|
|
|
+ options.forEach((item) => {
|
|
|
+ if (item.value) optionMap.set(item.value, item)
|
|
|
+ })
|
|
|
+ selectedValues.forEach((value) => {
|
|
|
+ if (value && !optionMap.has(value)) {
|
|
|
+ optionMap.set(value, {
|
|
|
+ label: value,
|
|
|
+ value
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return Array.from(optionMap.values())
|
|
|
+}
|
|
|
+
|
|
|
+const fetchKnowledgeBaseOptions = async () => {
|
|
|
+ knowledgeBaseLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await knowledge.postKnowledgeBasePageList({
|
|
|
+ keyword: '',
|
|
|
+ type: '',
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: 20
|
|
|
+ })
|
|
|
+ if (!res?.isSuccess) return
|
|
|
+
|
|
|
+ const list = (res.result?.model || []).map((item) => ({
|
|
|
+ label: normalizeOptionLabel(item),
|
|
|
+ value: normalizeOptionValue(item),
|
|
|
+ raw: item
|
|
|
+ }))
|
|
|
+ knowledgeBaseOptions.value = mergeSelectedOptions(
|
|
|
+ list.filter((item) => item.value),
|
|
|
+ formData.value.knowledge_base_ids
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!formData.value.knowledge_base_ids?.length && list[0]?.value) {
|
|
|
+ formData.value.knowledge_base_ids = [list[0].value]
|
|
|
+ }
|
|
|
+
|
|
|
+ return list
|
|
|
+ } finally {
|
|
|
+ knowledgeBaseLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchKnowledgeOptions = async (baseIdsInput?: string[]) => {
|
|
|
+ const baseIds = Array.from(
|
|
|
+ new Set((baseIdsInput || formData.value.knowledge_base_ids || []).filter(Boolean))
|
|
|
+ )
|
|
|
+ if (!baseIds.length) {
|
|
|
+ knowledgeOptions.value = mergeSelectedOptions([], formData.value.knowledge_ids)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ knowledgeLoading.value = true
|
|
|
+ try {
|
|
|
+ const results = await Promise.all(
|
|
|
+ baseIds.map((knowledgeBaseId) =>
|
|
|
+ knowledge.postKnowledgePageList({
|
|
|
+ knowledge_base_id: knowledgeBaseId,
|
|
|
+ title: '',
|
|
|
+ file_type: '',
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: knowledgePageSize
|
|
|
+ })
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ const optionMap = new Map<string, SelectOption>()
|
|
|
+ results.forEach((res) => {
|
|
|
+ if (!res?.isSuccess) return
|
|
|
+ console.log(res.result)
|
|
|
+ ;(res.result?.model || []).forEach((item) => {
|
|
|
+ if (!item.id) return
|
|
|
+ optionMap.set(item.id, {
|
|
|
+ label: normalizeOptionLabel(item),
|
|
|
+ value: item.id,
|
|
|
+ raw: item
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ knowledgeOptions.value = mergeSelectedOptions(
|
|
|
+ Array.from(optionMap.values()),
|
|
|
+ formData.value.knowledge_ids
|
|
|
+ )
|
|
|
+ } finally {
|
|
|
+ knowledgeLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const ensureDefaults = () => {
|
|
|
+ formData.value.title ||= t('nodes.meta.knowledge-retrieval.displayName')
|
|
|
+ formData.value.type ||= 'knowledge-retrieval'
|
|
|
+ formData.value.query_variable ||= createDefaultQueryVariable()
|
|
|
+ formData.value.query_variable.describe ||= t('pages.knowledgeRetrievalSetter.queryDescribe')
|
|
|
+ formData.value.query_variable.name ||= 'query'
|
|
|
+ formData.value.query_variable.type ||= 'string'
|
|
|
+ formData.value.query_variable.value ||= ''
|
|
|
+ formData.value.top_k = typeof formData.value.top_k === 'number' ? formData.value.top_k : 10
|
|
|
+ formData.value.knowledge_base_ids ||= []
|
|
|
+ formData.value.knowledge_ids ||= []
|
|
|
+ formData.value.score_threshold_enable =
|
|
|
+ typeof formData.value.score_threshold_enable === 'boolean'
|
|
|
+ ? formData.value.score_threshold_enable
|
|
|
+ : false
|
|
|
+ formData.value.score_threshold =
|
|
|
+ typeof formData.value.score_threshold === 'number' ? formData.value.score_threshold : 0.8
|
|
|
+ if (!formData.value.outputs?.length) {
|
|
|
+ formData.value.outputs = [
|
|
|
+ {
|
|
|
+ name: 'result',
|
|
|
+ describe: t('nodes.outputs.knowledge-retrieval.result'),
|
|
|
+ type: 'array[object]'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'content',
|
|
|
+ describe: t('nodes.outputs.knowledge-retrieval.content'),
|
|
|
+ type: 'string'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const syncSelectableLists = async () => {
|
|
|
+ const baseList = await fetchKnowledgeBaseOptions()
|
|
|
+ const baseIds = formData.value.knowledge_base_ids?.length
|
|
|
+ ? [...formData.value.knowledge_base_ids]
|
|
|
+ : baseList?.[0]?.value
|
|
|
+ ? [baseList[0].value]
|
|
|
+ : []
|
|
|
+
|
|
|
+ if (!formData.value.knowledge_base_ids?.length && baseIds.length) {
|
|
|
+ formData.value.knowledge_base_ids = [...baseIds]
|
|
|
+ }
|
|
|
+
|
|
|
+ await fetchKnowledgeOptions(baseIds)
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ formData,
|
|
|
+ () => {
|
|
|
+ ensureDefaults()
|
|
|
+ },
|
|
|
+ { deep: true, immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [...formData.value.knowledge_base_ids],
|
|
|
+ (baseIds) => {
|
|
|
+ void fetchKnowledgeOptions(baseIds)
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ void syncSelectableLists()
|
|
|
+})
|
|
|
+
|
|
|
+const handleQueryVariableChange = (val: { value: string; type: NodeVariableType }) => {
|
|
|
+ formData.value.query_variable = {
|
|
|
+ ...formData.value.query_variable,
|
|
|
+ name: 'query',
|
|
|
+ describe: t('pages.knowledgeRetrievalSetter.queryDescribe'),
|
|
|
+ value: val.value,
|
|
|
+ type: val.type || 'string'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleQueryVariableClear = () => {
|
|
|
+ formData.value.query_variable = createDefaultQueryVariable()
|
|
|
+ formData.value.query_variable.describe = t('pages.knowledgeRetrievalSetter.queryDescribe')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.knowledge-retrieval-setter {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-block {
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #eef2f7;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #344054;
|
|
|
+}
|
|
|
+
|
|
|
+.field-hint {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #667085;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-two {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-block {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title-row--compact {
|
|
|
+ min-height: 32px;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.id-tag-input {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.output-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.output-item {
|
|
|
+ padding: 10px 12px;
|
|
|
+ border: 1px solid #eaecf0;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+.output-item__main {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.output-item__name {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #344054;
|
|
|
+}
|
|
|
+
|
|
|
+.output-item__type {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #667085;
|
|
|
+}
|
|
|
+
|
|
|
+.output-item__desc {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #667085;
|
|
|
+}
|
|
|
+</style>
|