feat: Add Graph Settings component for managing graph extraction configurations

This commit is contained in:
begoniezhao
2025-11-26 09:23:33 +00:00
parent b4ae609103
commit c419049435
9 changed files with 1018 additions and 435 deletions
+53 -36
View File
@@ -271,6 +271,56 @@ export default {
saveFailed: 'Failed to save configuration: {message}'
}
},
graphSettings: {
title: 'Knowledge Graph Configuration',
description: 'Configure entity-relationship extraction to automatically build knowledge graphs from text',
enableLabel: 'Enable Entity-Relationship Extraction',
enableDescription: 'Automatically extract entities and relationships from text when enabled',
tagsLabel: 'Relationship Types',
tagsDescription: 'Define relationship type tags to extract, separated by commas',
tagsPlaceholder: 'Enter relationship types, e.g., works_at, colleague, friend',
generateRandomTags: 'Generate Random Tags',
sampleTextLabel: 'Sample Text',
sampleTextDescription: 'Sample text for testing entity-relationship extraction',
sampleTextPlaceholder: 'Enter text containing entities and relationships...',
generateRandomText: 'Generate Random Text',
entityListLabel: 'Entity List',
entityListDescription: 'Entities and their attributes extracted from text',
nodeNamePlaceholder: 'Enter entity name',
attributePlaceholder: 'Enter attribute value',
addAttribute: 'Add Attribute',
manageEntitiesLabel: 'Manage Entities',
manageEntitiesDescription: 'Add or remove entity nodes',
addEntity: 'Add Entity',
relationListLabel: 'Relationship List',
relationListDescription: 'Define relationship connections between entities',
selectEntity: 'Select Entity',
selectRelationType: 'Select Relationship Type',
manageRelationsLabel: 'Manage Relationships',
manageRelationsDescription: 'Add or remove relationships between entities',
addRelation: 'Add Relationship',
extractActionsLabel: 'Extraction Actions',
extractActionsDescription: 'Perform entity-relationship extraction or manage sample data',
startExtraction: 'Start Extraction',
extracting: 'Extracting...',
defaultExample: 'Default Example',
clearExample: 'Clear Example',
completeModelConfig: 'Please complete model configuration first',
tagsGenerated: 'Tags generated successfully',
tagsGenerateFailed: 'Failed to generate tags',
textGenerated: 'Text generated successfully',
textGenerateFailed: 'Failed to generate text',
pleaseInputText: 'Please enter sample text first',
extractSuccess: 'Entity-relationship extraction successful',
extractFailed: 'Entity-relationship extraction failed',
exampleLoaded: 'Example loaded',
exampleCleared: 'Example cleared',
saveSuccess: 'Graph configuration saved',
saveFailed: 'Failed to save configuration: {message}',
errors: {
unknown: 'Unknown error',
},
},
initialization: {
title: 'Initialization',
welcome: 'Welcome to WeKnora',
@@ -770,7 +820,8 @@ export default {
models: 'Model Configuration',
chunking: 'Chunking Settings',
advanced: 'Advanced Settings',
faq: 'FAQ Settings'
faq: 'FAQ Settings',
graph: 'Knowledge Graph'
},
basic: {
title: 'Basic Information',
@@ -936,7 +987,7 @@ export default {
},
advanced: {
title: 'Advanced Settings',
description: 'Configure multimodal and knowledge graph features',
description: 'Configure multimodal features',
multimodal: {
label: 'Multimodal Feature',
description: 'Enable understanding of multimodal content such as images and videos',
@@ -981,40 +1032,6 @@ export default {
pathPrefixDescription: 'Optional prefix for stored file paths',
pathPrefixPlaceholder: 'Enter path prefix'
}
},
graph: {
label: 'Knowledge Graph Extraction',
description: 'Automatically extract entities and relationships to build a knowledge graph',
configTitle: 'Graph Configuration',
promptLabel: 'Prompt Text',
promptDescription: 'Prompt used to guide the model when extracting entities and relationships',
promptPlaceholder: 'Enter prompt text',
tagsLabel: 'Tags',
tagsDescription: 'Predefined entity tags (separate multiple tags with commas)',
tagsPlaceholder: 'Enter a tag and press Enter',
nodesLabel: 'Node Configuration',
nodesDescription: 'Define entity node types and attributes to extract',
addNode: 'Add Node',
nodeNameLabel: 'Node Name',
nodeNamePlaceholder: 'e.g., Person, Organization',
nodeChunksLabel: 'Associated Chunks',
nodeChunksPlaceholder: 'Enter chunk ID and press Enter',
nodeAttributesLabel: 'Node Attributes',
nodeAttributesPlaceholder: 'Enter attribute name and press Enter',
relationsLabel: 'Relation Configuration',
relationsDescription: 'Define relationship types between nodes',
addRelation: 'Add Relation',
relationNode1Label: 'Source Node',
relationNode1Placeholder: 'e.g., Person',
relationNode2Label: 'Target Node',
relationNode2Placeholder: 'e.g., Organization',
relationTypeLabel: 'Relation Type',
relationTypePlaceholder: 'e.g., WORKS_FOR, BELONGS_TO',
deleteNode: 'Delete Node',
deleteRelation: 'Delete Relation',
disabledWarning:
'Graph database is not enabled. Please follow the "Enable Knowledge Graph" guide before using this feature.',
howToEnable: 'View guide'
}
}
},
+53 -15
View File
@@ -270,6 +270,56 @@ export default {
saveFailed: 'Не удалось сохранить настройки: {message}'
}
},
graphSettings: {
title: 'Настройки графа знаний',
description: 'Настройте извлечение сущностей и отношений для автоматического построения графа знаний из текста',
enableLabel: 'Включить извлечение сущностей и отношений',
enableDescription: 'Автоматически извлекать сущности и отношения из текста при включении',
tagsLabel: 'Типы отношений',
tagsDescription: 'Определите теги типов отношений для извлечения, разделённые запятыми',
tagsPlaceholder: 'Введите типы отношений, например: работает_в, коллега, друг',
generateRandomTags: 'Сгенерировать случайные теги',
sampleTextLabel: 'Образец текста',
sampleTextDescription: 'Образец текста для тестирования извлечения сущностей и отношений',
sampleTextPlaceholder: 'Введите текст, содержащий сущности и отношения...',
generateRandomText: 'Сгенерировать случайный текст',
entityListLabel: 'Список сущностей',
entityListDescription: 'Сущности и их атрибуты, извлечённые из текста',
nodeNamePlaceholder: 'Введите имя сущности',
attributePlaceholder: 'Введите значение атрибута',
addAttribute: 'Добавить атрибут',
manageEntitiesLabel: 'Управление сущностями',
manageEntitiesDescription: 'Добавить или удалить узлы сущностей',
addEntity: 'Добавить сущность',
relationListLabel: 'Список отношений',
relationListDescription: 'Определите связи отношений между сущностями',
selectEntity: 'Выберите сущность',
selectRelationType: 'Выберите тип отношения',
manageRelationsLabel: 'Управление отношениями',
manageRelationsDescription: 'Добавить или удалить отношения между сущностями',
addRelation: 'Добавить отношение',
extractActionsLabel: 'Действия извлечения',
extractActionsDescription: 'Выполнить извлечение сущностей и отношений или управлять образцами данных',
startExtraction: 'Начать извлечение',
extracting: 'Извлечение...',
defaultExample: 'Пример по умолчанию',
clearExample: 'Очистить пример',
completeModelConfig: 'Пожалуйста, сначала завершите настройку модели',
tagsGenerated: 'Теги успешно сгенерированы',
tagsGenerateFailed: 'Не удалось сгенерировать теги',
textGenerated: 'Текст успешно сгенерирован',
textGenerateFailed: 'Не удалось сгенерировать текст',
pleaseInputText: 'Пожалуйста, сначала введите образец текста',
extractSuccess: 'Извлечение сущностей и отношений выполнено успешно',
extractFailed: 'Не удалось извлечь сущности и отношения',
exampleLoaded: 'Пример загружен',
exampleCleared: 'Пример очищен',
saveSuccess: 'Настройки графа сохранены',
saveFailed: 'Не удалось сохранить настройки: {message}',
errors: {
unknown: 'Неизвестная ошибка',
},
},
initialization: {
title: 'Инициализация',
welcome: 'Добро пожаловать в WeKnora',
@@ -858,7 +908,8 @@ export default {
models: 'Конфигурация моделей',
chunking: 'Настройки разбиения',
advanced: 'Дополнительные настройки',
faq: 'FAQ настройки'
faq: 'FAQ настройки',
graph: 'Граф знаний'
},
basic: {
title: 'Основная информация',
@@ -1021,7 +1072,7 @@ export default {
},
advanced: {
title: 'Расширенные настройки',
description: 'Настройте мультимодальные возможности и извлечение знаний',
description: 'Настройте мультимодальные возможности',
multimodal: {
label: 'Мультимодальная функция',
description: 'Включите понимание мультимедийного контента, такого как изображения и видео',
@@ -1066,19 +1117,6 @@ export default {
pathPrefixDescription: 'Необязательный префикс для путей хранения файлов',
pathPrefixPlaceholder: 'Введите префикс пути'
}
},
graph: {
label: 'Извлечение знаний',
description: 'Автоматически извлекайте сущности и связи для построения графа знаний',
configTitle: 'Настройки графа',
promptLabel: 'Текст подсказки',
promptDescription: 'Текст подсказки, который помогает модели извлекать сущности и связи',
promptPlaceholder: 'Введите текст подсказки',
tagsLabel: 'Теги',
tagsDescription: 'Предопределённые теги сущностей (несколько тегов разделяйте запятыми)',
tagsPlaceholder: 'Введите тег и нажмите Enter',
disabledWarning: 'Графовая база данных не включена. Пожалуйста, сначала включите графовую базу данных, чтобы использовать эту функцию.',
howToEnable: 'Как включить'
}
}
},
+52 -35
View File
@@ -373,6 +373,56 @@ export default {
saveFailed: "保存配置失败: {message}",
},
},
graphSettings: {
title: "知识图谱配置",
description: "配置实体关系提取功能,自动从文本中提取实体和关系构建知识图谱",
enableLabel: "启用实体关系提取",
enableDescription: "开启后将自动从文本中提取实体和关系",
tagsLabel: "关系类型",
tagsDescription: "定义要提取的关系类型标签,多个标签用逗号分隔",
tagsPlaceholder: "输入关系类型,如:工作于、同事、朋友等",
generateRandomTags: "生成随机标签",
sampleTextLabel: "示例文本",
sampleTextDescription: "用于测试实体关系提取的示例文本",
sampleTextPlaceholder: "输入一段包含实体和关系的文本...",
generateRandomText: "生成随机文本",
entityListLabel: "实体列表",
entityListDescription: "从文本中提取的实体及其属性",
nodeNamePlaceholder: "输入实体名称",
attributePlaceholder: "输入属性值",
addAttribute: "添加属性",
manageEntitiesLabel: "管理实体",
manageEntitiesDescription: "添加或删除实体节点",
addEntity: "添加实体",
relationListLabel: "关系列表",
relationListDescription: "定义实体之间的关系连接",
selectEntity: "选择实体",
selectRelationType: "选择关系类型",
manageRelationsLabel: "管理关系",
manageRelationsDescription: "添加或删除实体间的关系",
addRelation: "添加关系",
extractActionsLabel: "提取操作",
extractActionsDescription: "执行实体关系提取或管理示例数据",
startExtraction: "开始提取",
extracting: "提取中...",
defaultExample: "默认示例",
clearExample: "清除示例",
completeModelConfig: "请先完成模型配置",
tagsGenerated: "标签生成成功",
tagsGenerateFailed: "标签生成失败",
textGenerated: "文本生成成功",
textGenerateFailed: "文本生成失败",
pleaseInputText: "请先输入示例文本",
extractSuccess: "实体关系提取成功",
extractFailed: "实体关系提取失败",
exampleLoaded: "示例已加载",
exampleCleared: "示例已清除",
saveSuccess: "图谱配置已保存",
saveFailed: "保存配置失败: {message}",
errors: {
unknown: "未知错误",
},
},
initialization: {
title: "初始化",
welcome: "欢迎使用WeKnora",
@@ -1094,6 +1144,7 @@ export default {
chunking: "分块设置",
advanced: "高级设置",
faq: "FAQ 设置",
graph: "知识图谱",
},
basic: {
title: "基本信息",
@@ -1260,7 +1311,7 @@ export default {
},
advanced: {
title: "高级设置",
description: "配置多模态、知识图谱等高级功能",
description: "配置多模态等高级功能",
multimodal: {
label: "多模态功能",
description: "启用图片、视频等多模态内容的理解能力",
@@ -1307,40 +1358,6 @@ export default {
pathPrefixPlaceholder: "请输入路径前缀",
},
},
graph: {
label: "知识图谱提取",
description: "从文档中自动提取实体和关系,构建知识图谱",
configTitle: "图谱配置",
promptLabel: "提示文本",
promptDescription: "用于引导模型提取实体和关系的提示文本",
promptPlaceholder: "请输入提示文本",
tagsLabel: "标签",
tagsDescription: "预定义的实体标签(多个标签用逗号分隔)",
tagsPlaceholder: "输入标签后按回车",
nodesLabel: "节点配置",
nodesDescription: "定义需要提取的实体节点类型及其属性",
addNode: "添加节点",
nodeNameLabel: "节点名称",
nodeNamePlaceholder: "如:Person、Organization",
nodeChunksLabel: "关联文档块",
nodeChunksPlaceholder: "输入 chunk ID 后按回车",
nodeAttributesLabel: "节点属性",
nodeAttributesPlaceholder: "输入属性名后按回车",
relationsLabel: "关系配置",
relationsDescription: "定义节点之间的关系类型",
addRelation: "添加关系",
relationNode1Label: "起始节点",
relationNode1Placeholder: "如:Person",
relationNode2Label: "目标节点",
relationNode2Placeholder: "如:Organization",
relationTypeLabel: "关系类型",
relationTypePlaceholder: "如:WORKS_FOR、BELONGS_TO",
deleteNode: "删除节点",
deleteRelation: "删除关系",
disabledWarning:
"图数据库未启用,无法使用知识图谱功能。请先根据《开启知识图谱功能指南》完成配置。",
howToEnable: "查看启用说明",
},
},
},
input: {
@@ -128,16 +128,24 @@
/>
</div>
<!-- 图谱设置 -->
<div v-if="!isFAQ" v-show="currentSection === 'graph'" class="section">
<GraphSettings
v-if="formData"
:graph-extract="formData.extractConfig"
:all-models="allModels"
@update:graphExtract="handleNodeExtractUpdate"
/>
</div>
<!-- 高级设置 -->
<div v-if="!isFAQ" v-show="currentSection === 'advanced'" class="section">
<KBAdvancedSettings
ref="advancedSettingsRef"
v-if="formData"
:multimodal="formData.multimodalConfig"
:node-extract="formData.nodeExtractConfig"
:all-models="allModels"
@update:multimodal="handleMultimodalUpdate"
@update:nodeExtract="handleNodeExtractUpdate"
/>
</div>
</div>
@@ -169,6 +177,7 @@ import { useUIStore } from '@/stores/ui'
import KBModelConfig from './settings/KBModelConfig.vue'
import KBChunkingSettings from './settings/KBChunkingSettings.vue'
import KBAdvancedSettings from './settings/KBAdvancedSettings.vue'
import GraphSettings from './settings/GraphSettings.vue'
import { useI18n } from 'vue-i18n'
const uiStore = useUIStore()
@@ -204,6 +213,7 @@ const navItems = computed(() => {
} else {
items.push(
{ key: 'chunking', icon: 'file-copy', label: t('knowledgeEditor.sidebar.chunking') },
{ key: 'graph', icon: 'chart-bubble', label: t('knowledgeEditor.sidebar.graph') },
{ key: 'advanced', icon: 'setting', label: t('knowledgeEditor.sidebar.advanced') }
)
}
@@ -272,13 +282,12 @@ const initFormData = (type: 'document' | 'faq' = 'document') => {
pathPrefix: ''
}
},
nodeExtractConfig: {
extractConfig: {
enabled: false,
text: '',
tags: [] as string[],
nodes: [] as Array<{
name: string
chunks: string[]
attributes: string[]
}>,
relations: [] as Array<{
@@ -286,7 +295,7 @@ const initFormData = (type: 'document' | 'faq' = 'document') => {
node2: string
type: string
}>
}
},
}
}
@@ -358,13 +367,16 @@ const loadKBData = async () => {
pathPrefix: kb.cos_config?.path_prefix || ''
}
},
nodeExtractConfig: {
extractConfig: {
enabled: kb.extract_config?.enabled || false,
text: kb.extract_config?.text || '',
tags: kb.extract_config?.tags || [],
nodes: kb.extract_config?.nodes || [],
nodes: (kb.extract_config?.nodes || []).map((node: any) => ({
name: node.name,
attributes: node.attributes || []
})),
relations: kb.extract_config?.relations || []
}
},
}
} catch (error) {
console.error('Failed to load knowledge base data:', error)
@@ -396,7 +408,7 @@ const handleMultimodalUpdate = (config: any) => {
const handleNodeExtractUpdate = (config: any) => {
if (formData.value) {
formData.value.nodeExtractConfig = { ...config }
formData.value.extractConfig = { ...config }
}
}
@@ -492,13 +504,13 @@ const buildSubmitData = () => {
}
// 添加知识图谱配置
if (formData.value.nodeExtractConfig.enabled) {
if (formData.value.extractConfig.enabled) {
data.extract_config = {
enabled: true,
text: formData.value.nodeExtractConfig.text,
tags: formData.value.nodeExtractConfig.tags,
nodes: formData.value.nodeExtractConfig.nodes,
relations: formData.value.nodeExtractConfig.relations
text: formData.value.extractConfig.text,
tags: formData.value.extractConfig.tags,
nodes: formData.value.extractConfig.nodes,
relations: formData.value.extractConfig.relations
}
}
@@ -584,8 +596,8 @@ const handleSubmit = async () => {
enabled: data.extract_config?.enabled || false,
text: data.extract_config?.text || '',
tags: data.extract_config?.tags || [],
nodes: [],
relations: []
nodes: data.extract_config?.nodes || [],
relations: data.extract_config?.relations || []
}
}
@@ -0,0 +1,746 @@
<template>
<div class="graph-settings">
<div class="section-header">
<h2>{{ t('graphSettings.title') }}</h2>
<p class="section-description">{{ t('graphSettings.description') }}</p>
</div>
<div class="settings-group">
<!-- 启用实体关系提取 -->
<div class="setting-row">
<div class="setting-info">
<label>{{ t('graphSettings.enableLabel') }}</label>
<p class="desc">{{ t('graphSettings.enableDescription') }}</p>
</div>
<div class="setting-control">
<t-switch
v-model="localGraphExtract.enabled"
@change="handleEnabledChange"
/>
</div>
</div>
<!-- 关系类型配置 -->
<div v-if="localGraphExtract.enabled" class="setting-row vertical">
<div class="setting-info">
<label>{{ t('graphSettings.tagsLabel') }}</label>
<p class="desc">{{ t('graphSettings.tagsDescription') }}</p>
</div>
<div class="setting-control full-width">
<div class="tags-control-group">
<t-button
theme="default"
size="medium"
:disabled="!modelStatus.llm.available"
:loading="tagFabring"
@click="handleFabriTag"
class="gen-tags-btn"
>
{{ t('graphSettings.generateRandomTags') }}
</t-button>
<t-select
v-model="localGraphExtract.tags"
multiple
:placeholder="t('graphSettings.tagsPlaceholder')"
clearable
creatable
filterable
@change="handleTagsChange"
style="flex: 1; min-width: 400px;"
/>
</div>
<div v-if="!modelStatus.llm.available" class="control-tip">
<t-icon name="info-circle" class="tip-icon" />
<span>{{ t('graphSettings.completeModelConfig') }}</span>
</div>
</div>
</div>
<!-- 示例文本 -->
<div v-if="localGraphExtract.enabled" class="setting-row vertical">
<div class="setting-info">
<label>{{ t('graphSettings.sampleTextLabel') }}</label>
<p class="desc">{{ t('graphSettings.sampleTextDescription') }}</p>
</div>
<div class="setting-control full-width">
<div class="text-control-group">
<t-button
theme="default"
size="medium"
:disabled="!modelStatus.llm.available"
:loading="textFabring"
@click="handleFabriText"
class="gen-text-btn"
>
{{ t('graphSettings.generateRandomText') }}
</t-button>
<t-textarea
v-model="localGraphExtract.text"
:placeholder="t('graphSettings.sampleTextPlaceholder')"
:autosize="{ minRows: 6, maxRows: 12 }"
show-word-limit
maxlength="5000"
@change="handleTextChange"
style="width: 100%;"
/>
</div>
<div v-if="!modelStatus.llm.available" class="control-tip">
<t-icon name="info-circle" class="tip-icon" />
<span>{{ t('graphSettings.completeModelConfig') }}</span>
</div>
</div>
</div>
<!-- 实体列表 -->
<div v-if="localGraphExtract.enabled && localGraphExtract.nodes.length > 0" class="setting-row vertical">
<div class="setting-info">
<label>{{ t('graphSettings.entityListLabel') }}</label>
<p class="desc">{{ t('graphSettings.entityListDescription') }}</p>
</div>
<div class="setting-control full-width">
<div class="node-list">
<div v-for="(node, nodeIndex) in localGraphExtract.nodes" :key="nodeIndex" class="node-item">
<div class="node-header">
<t-icon name="user" class="node-icon" />
<t-input
v-model="node.name"
:placeholder="t('graphSettings.nodeNamePlaceholder')"
@change="handleNodesChange"
class="node-name-input"
/>
<t-button
theme="default"
size="small"
@click="removeNode(nodeIndex)"
>
<t-icon name="delete" />
</t-button>
</div>
<div class="node-attributes">
<div v-for="(attribute, attrIndex) in node.attributes" :key="attrIndex" class="attribute-item">
<t-input
v-model="node.attributes[attrIndex]"
:placeholder="t('graphSettings.attributePlaceholder')"
@change="handleNodesChange"
class="attribute-input"
/>
<t-button
theme="default"
size="small"
@click="removeAttribute(nodeIndex, attrIndex)"
>
<t-icon name="close" />
</t-button>
</div>
<t-button
theme="default"
size="small"
@click="addAttribute(nodeIndex)"
class="add-attr-btn"
>
{{ t('graphSettings.addAttribute') }}
</t-button>
</div>
</div>
</div>
</div>
</div>
<!-- 添加实体按钮 -->
<div v-if="localGraphExtract.enabled" class="setting-row">
<div class="setting-info">
<label>{{ t('graphSettings.manageEntitiesLabel') }}</label>
<p class="desc">{{ t('graphSettings.manageEntitiesDescription') }}</p>
</div>
<div class="setting-control">
<t-button
theme="primary"
@click="addNode"
>
{{ t('graphSettings.addEntity') }}
</t-button>
</div>
</div>
<!-- 关系列表 -->
<div v-if="localGraphExtract.enabled && localGraphExtract.relations.length > 0" class="setting-row vertical">
<div class="setting-info">
<label>{{ t('graphSettings.relationListLabel') }}</label>
<p class="desc">{{ t('graphSettings.relationListDescription') }}</p>
</div>
<div class="setting-control full-width">
<div class="relation-list">
<div v-for="(relation, index) in localGraphExtract.relations" :key="index" class="relation-item">
<t-select
v-model="relation.node1"
:placeholder="t('graphSettings.selectEntity')"
@change="handleRelationsChange"
class="relation-select"
>
<t-option
v-for="node in localGraphExtract.nodes"
:key="node.name"
:value="node.name"
:label="node.name"
/>
</t-select>
<t-icon name="arrow-right" class="relation-arrow" />
<t-select
v-model="relation.type"
:placeholder="t('graphSettings.selectRelationType')"
clearable
creatable
filterable
@change="handleRelationsChange"
class="relation-select"
>
<t-option
v-for="tag in localGraphExtract.tags"
:key="tag"
:value="tag"
:label="tag"
/>
</t-select>
<t-icon name="arrow-right" class="relation-arrow" />
<t-select
v-model="relation.node2"
:placeholder="t('graphSettings.selectEntity')"
@change="handleRelationsChange"
class="relation-select"
>
<t-option
v-for="node in localGraphExtract.nodes"
:key="node.name"
:value="node.name"
:label="node.name"
/>
</t-select>
<t-button
theme="default"
size="small"
@click="removeRelation(index)"
>
<t-icon name="delete" />
</t-button>
</div>
</div>
</div>
</div>
<!-- 添加关系按钮 -->
<div v-if="localGraphExtract.enabled" class="setting-row">
<div class="setting-info">
<label>{{ t('graphSettings.manageRelationsLabel') }}</label>
<p class="desc">{{ t('graphSettings.manageRelationsDescription') }}</p>
</div>
<div class="setting-control">
<t-button
theme="primary"
@click="addRelation"
>
{{ t('graphSettings.addRelation') }}
</t-button>
</div>
</div>
<!-- 提取操作按钮 -->
<div v-if="localGraphExtract.enabled" class="setting-row">
<div class="setting-info">
<label>{{ t('graphSettings.extractActionsLabel') }}</label>
<p class="desc">{{ t('graphSettings.extractActionsDescription') }}</p>
</div>
<div class="setting-control">
<div class="action-buttons">
<t-button
theme="primary"
:disabled="!modelStatus.llm.available || !localGraphExtract.text"
:loading="extracting"
@click="handleExtract"
>
{{ extracting ? t('graphSettings.extracting') : t('graphSettings.startExtraction') }}
</t-button>
<t-button
theme="default"
@click="defaultExtractExample"
>
{{ t('graphSettings.defaultExample') }}
</t-button>
<t-button
theme="default"
@click="clearExtractExample"
>
{{ t('graphSettings.clearExample') }}
</t-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n'
import { extractTextRelations, fabriText, fabriTag, type Node, type Relation, type LLMConfig } from '@/api/initialization'
import { listModels } from '@/api/model'
const { t } = useI18n()
interface GraphExtractConfig {
enabled: boolean
text: string
tags: string[]
nodes: Node[]
relations: Relation[]
}
interface Props {
graphExtract: GraphExtractConfig
allModels?: any[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:graphExtract': [value: GraphExtractConfig]
}>()
// 本地状态
const localGraphExtract = ref<GraphExtractConfig>({
...props.graphExtract,
nodes: props.graphExtract.nodes || [],
relations: props.graphExtract.relations || []
})
// 加载状态
const tagFabring = ref(false)
const textFabring = ref(false)
const extracting = ref(false)
// 模型状态
const modelStatus = ref({
llm: {
available: false,
config: null as LLMConfig | null
}
})
// Watch for prop changes
watch(() => props.graphExtract, (newVal) => {
localGraphExtract.value = {
...newVal,
nodes: newVal.nodes || [],
relations: newVal.relations || []
}
}, { deep: true })
// 处理配置变更
const handleConfigChange = () => {
emit('update:graphExtract', localGraphExtract.value)
}
// 处理启用/禁用切换
const handleEnabledChange = () => {
// 当关闭提取功能时,清空所有数据
if (!localGraphExtract.value.enabled) {
localGraphExtract.value.text = ''
localGraphExtract.value.tags = []
localGraphExtract.value.nodes = []
localGraphExtract.value.relations = []
}
handleConfigChange()
}
const handleTagsChange = () => {
handleConfigChange()
}
const handleTextChange = () => {
handleConfigChange()
}
const handleNodesChange = () => {
handleConfigChange()
}
const handleRelationsChange = () => {
handleConfigChange()
}
// 节点操作
const addNode = () => {
if (!localGraphExtract.value.nodes) {
localGraphExtract.value.nodes = []
}
localGraphExtract.value.nodes.push({
name: '',
attributes: []
})
handleNodesChange()
}
const removeNode = (index: number) => {
localGraphExtract.value.nodes.splice(index, 1)
handleNodesChange()
}
const addAttribute = (nodeIndex: number) => {
localGraphExtract.value.nodes[nodeIndex].attributes.push('')
handleNodesChange()
}
const removeAttribute = (nodeIndex: number, attrIndex: number) => {
localGraphExtract.value.nodes[nodeIndex].attributes.splice(attrIndex, 1)
handleNodesChange()
}
// 关系操作
const addRelation = () => {
if (!localGraphExtract.value.relations) {
localGraphExtract.value.relations = []
}
localGraphExtract.value.relations.push({
node1: '',
node2: '',
type: ''
})
handleRelationsChange()
}
const removeRelation = (index: number) => {
localGraphExtract.value.relations.splice(index, 1)
handleRelationsChange()
}
// 生成随机标签
const handleFabriTag = async () => {
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
return
}
tagFabring.value = true
try {
const response = await fabriTag({
llmConfig: modelStatus.value.llm.config
})
localGraphExtract.value.tags = response.tags || []
handleTagsChange()
MessagePlugin.success(t('graphSettings.tagsGenerated'))
} catch (error: any) {
console.error('Failed to generate tags:', error)
MessagePlugin.error(t('graphSettings.tagsGenerateFailed'))
} finally {
tagFabring.value = false
}
}
// 生成随机文本
const handleFabriText = async () => {
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
return
}
textFabring.value = true
try {
const response = await fabriText({
tags: localGraphExtract.value.tags,
llmConfig: modelStatus.value.llm.config
})
localGraphExtract.value.text = response.text || ''
handleTextChange()
MessagePlugin.success(t('graphSettings.textGenerated'))
} catch (error: any) {
console.error('Failed to generate text:', error)
MessagePlugin.error(t('graphSettings.textGenerateFailed'))
} finally {
textFabring.value = false
}
}
// 提取实体关系
const handleExtract = async () => {
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
return
}
if (!localGraphExtract.value.text) {
MessagePlugin.warning(t('graphSettings.pleaseInputText'))
return
}
extracting.value = true
try {
const response = await extractTextRelations({
text: localGraphExtract.value.text,
tags: localGraphExtract.value.tags,
llmConfig: modelStatus.value.llm.config
})
localGraphExtract.value.nodes = response.nodes || []
localGraphExtract.value.relations = response.relations || []
handleNodesChange()
MessagePlugin.success(t('graphSettings.extractSuccess'))
} catch (error: any) {
console.error('Failed to extract relations:', error)
MessagePlugin.error(t('graphSettings.extractFailed'))
} finally {
extracting.value = false
}
}
// 默认示例
const defaultExtractExample = () => {
localGraphExtract.value.text = `《红楼梦》,又名《石头记》,是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著,后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景,以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间(1743年前后),是中国文学史上现实主义的高峰,对后世影响深远。`
localGraphExtract.value.tags = ['作者', '别名']
localGraphExtract.value.nodes = [
{name: '红楼梦', attributes: ['中国古典四大名著之一', '又名《石头记》', '被誉为中国封建社会的百科全书']},
{name: '石头记', attributes: ['《红楼梦》的别名']},
{name: '曹雪芹', attributes: ['清代作家', '《红楼梦》前 80 回的作者']},
{name: '高鹗', attributes: ['一般认为是《红楼梦》后 40 回的续写者']}
]
localGraphExtract.value.relations = [
{node1: '红楼梦', node2: '石头记', type: '别名'},
{node1: '红楼梦', node2: '曹雪芹', type: '作者'},
{node1: '红楼梦', node2: '高鹗', type: '作者'}
]
handleNodesChange()
MessagePlugin.success(t('graphSettings.exampleLoaded'))
}
// 清除示例
const clearExtractExample = () => {
localGraphExtract.value.text = ''
localGraphExtract.value.tags = []
localGraphExtract.value.nodes = []
localGraphExtract.value.relations = []
handleNodesChange()
MessagePlugin.success(t('graphSettings.exampleCleared'))
}
// 加载模型状态
const loadModelStatus = async () => {
try {
const models = await listModels()
// 查找可用的KnowledgeQA模型(对话模型)
const llmModels = models.filter((m: any) => m.type === 'KnowledgeQA')
if (llmModels.length > 0) {
const llmModel = llmModels[0]
modelStatus.value.llm.available = true
modelStatus.value.llm.config = {
source: llmModel.source,
modelName: llmModel.name,
baseUrl: llmModel.parameters?.base_url || '',
apiKey: llmModel.parameters?.api_key || ''
}
}
} catch (error: any) {
console.error('Failed to load model status:', error)
}
}
// 初始化
onMounted(async () => {
await loadModelStatus()
})
</script>
<style lang="less" scoped>
.graph-settings {
width: 100%;
}
.section-header {
margin-bottom: 32px;
h2 {
font-size: 20px;
font-weight: 600;
color: #333333;
margin: 0 0 8px 0;
}
.section-description {
font-size: 14px;
color: #666666;
margin: 0;
line-height: 1.5;
}
}
.settings-group {
display: flex;
flex-direction: column;
gap: 0;
}
.setting-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px 0;
border-bottom: 1px solid #e5e7eb;
&:last-child {
border-bottom: none;
}
&.vertical {
flex-direction: column;
gap: 12px;
.setting-control {
width: 100%;
max-width: 100%;
}
}
}
.setting-info {
flex: 1;
max-width: 65%;
padding-right: 24px;
label {
font-size: 15px;
font-weight: 500;
color: #333333;
display: block;
margin-bottom: 4px;
}
.desc {
font-size: 13px;
color: #666666;
margin: 0;
line-height: 1.5;
}
}
.setting-control {
flex-shrink: 0;
min-width: 280px;
display: flex;
justify-content: flex-end;
align-items: center;
&.full-width {
width: 100%;
max-width: 100%;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
.tags-control-group,
.text-control-group {
display: flex;
gap: 12px;
width: 100%;
align-items: flex-start;
}
.text-control-group {
flex-direction: column;
}
.control-tip {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666666;
.tip-icon {
color: #0052d9;
}
}
.node-list {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
.node-item {
background: #f8fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
}
.node-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.node-icon {
font-size: 20px;
color: #0052d9;
}
.node-name-input {
flex: 1;
}
}
.node-attributes {
display: flex;
flex-direction: column;
gap: 8px;
padding-left: 32px;
}
.attribute-item {
display: flex;
gap: 8px;
align-items: center;
.attribute-input {
flex: 1;
}
}
.add-attr-btn {
align-self: flex-start;
}
.relation-list {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.relation-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
.relation-select {
flex: 1;
min-width: 150px;
}
.relation-arrow {
color: #666666;
font-size: 16px;
}
}
.action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
</style>
@@ -210,198 +210,6 @@
</div>
</div>
<!-- Knowledge graph extraction -->
<div class="setting-row">
<div class="setting-info">
<label>{{ $t('knowledgeEditor.advanced.graph.label') }}</label>
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.description') }}</p>
<!-- Warning message when graph database is not enabled -->
<t-alert
v-if="!isGraphDatabaseEnabled"
theme="warning"
style="margin-top: 8px;"
>
<template #message>
<div>{{ $t('knowledgeEditor.advanced.graph.disabledWarning') }}</div>
<t-link class="graph-guide-link" theme="primary" @click="handleOpenGraphGuide">
{{ $t('knowledgeEditor.advanced.graph.howToEnable') }}
</t-link>
</template>
</t-alert>
</div>
<div class="setting-control">
<t-switch
v-model="localNodeExtract.enabled"
@change="handleNodeExtractToggle"
:disabled="!isGraphDatabaseEnabled"
size="large"
/>
</div>
</div>
<!-- Knowledge graph configuration -->
<div v-if="localNodeExtract.enabled && isGraphDatabaseEnabled" class="subsection">
<div class="subsection-header">
<h4>{{ $t('knowledgeEditor.advanced.graph.configTitle') }}</h4>
</div>
<div class="setting-row">
<div class="setting-info">
<label>{{ $t('knowledgeEditor.advanced.graph.promptLabel') }}</label>
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.promptDescription') }}</p>
</div>
<div class="setting-control">
<t-textarea
v-model="localNodeExtract.text"
:placeholder="$t('knowledgeEditor.advanced.graph.promptPlaceholder')"
:autosize="{ minRows: 3, maxRows: 6 }"
@change="handleConfigChange"
style="width: 280px;"
/>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<label>{{ $t('knowledgeEditor.advanced.graph.tagsLabel') }}</label>
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.tagsDescription') }}</p>
</div>
<div class="setting-control">
<t-tag-input
v-model="localNodeExtract.tags"
:placeholder="$t('knowledgeEditor.advanced.graph.tagsPlaceholder')"
@change="handleConfigChange"
style="width: 280px;"
/>
</div>
</div>
<!-- Nodes Configuration -->
<div class="subsection-header">
<h4>{{ $t('knowledgeEditor.advanced.graph.nodesLabel') }}</h4>
<p class="subsection-desc">{{ $t('knowledgeEditor.advanced.graph.nodesDescription') }}</p>
</div>
<div v-for="(node, index) in localNodeExtract.nodes" :key="index" class="config-item">
<div class="config-item-header">
<span class="config-item-title">节点 #{{ index + 1 }}</span>
<t-button
theme="danger"
variant="text"
size="small"
@click="removeNode(index)"
>
{{ $t('knowledgeEditor.advanced.graph.deleteNode') }}
</t-button>
</div>
<div class="config-item-body">
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.nodeNameLabel') }}</label>
<t-input
v-model="node.name"
:placeholder="$t('knowledgeEditor.advanced.graph.nodeNamePlaceholder')"
@change="handleConfigChange"
/>
</div>
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.nodeChunksLabel') }}</label>
<t-tag-input
v-model="node.chunks"
:placeholder="$t('knowledgeEditor.advanced.graph.nodeChunksPlaceholder')"
@change="handleConfigChange"
/>
</div>
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.nodeAttributesLabel') }}</label>
<t-tag-input
v-model="node.attributes"
:placeholder="$t('knowledgeEditor.advanced.graph.nodeAttributesPlaceholder')"
@change="handleConfigChange"
/>
</div>
</div>
</div>
<t-button
theme="primary"
variant="outline"
@click="addNode"
style="margin-top: 12px;"
>
<template #icon>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 2a1 1 0 011 1v4h4a1 1 0 110 2H9v4a1 1 0 11-2 0V9H3a1 1 0 110-2h4V3a1 1 0 011-1z"/>
</svg>
</template>
{{ $t('knowledgeEditor.advanced.graph.addNode') }}
</t-button>
<!-- Relations Configuration -->
<div class="subsection-header" style="margin-top: 24px;">
<h4>{{ $t('knowledgeEditor.advanced.graph.relationsLabel') }}</h4>
<p class="subsection-desc">{{ $t('knowledgeEditor.advanced.graph.relationsDescription') }}</p>
</div>
<div v-for="(relation, index) in localNodeExtract.relations" :key="index" class="config-item">
<div class="config-item-header">
<span class="config-item-title">关系 #{{ index + 1 }}</span>
<t-button
theme="danger"
variant="text"
size="small"
@click="removeRelation(index)"
>
{{ $t('knowledgeEditor.advanced.graph.deleteRelation') }}
</t-button>
</div>
<div class="config-item-body">
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.relationNode1Label') }}</label>
<t-input
v-model="relation.node1"
:placeholder="$t('knowledgeEditor.advanced.graph.relationNode1Placeholder')"
@change="handleConfigChange"
/>
</div>
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.relationNode2Label') }}</label>
<t-input
v-model="relation.node2"
:placeholder="$t('knowledgeEditor.advanced.graph.relationNode2Placeholder')"
@change="handleConfigChange"
/>
</div>
<div class="config-field">
<label>{{ $t('knowledgeEditor.advanced.graph.relationTypeLabel') }}</label>
<t-input
v-model="relation.type"
:placeholder="$t('knowledgeEditor.advanced.graph.relationTypePlaceholder')"
@change="handleConfigChange"
/>
</div>
</div>
</div>
<t-button
theme="primary"
variant="outline"
@click="addRelation"
style="margin-top: 12px;"
>
<template #icon>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 2a1 1 0 011 1v4h4a1 1 0 110 2H9v4a1 1 0 11-2 0V9H3a1 1 0 110-2h4V3a1 1 0 011-1z"/>
</svg>
</template>
{{ $t('knowledgeEditor.advanced.graph.addRelation') }}
</t-button>
</div>
</div>
</div>
</template>
@@ -433,25 +241,8 @@ interface MultimodalConfig {
}
}
interface NodeExtractConfig {
enabled: boolean
text: string
tags: string[]
nodes: Array<{
name: string
chunks: string[]
attributes: string[]
}>
relations: Array<{
node1: string
node2: string
type: string
}>
}
interface Props {
multimodal: MultimodalConfig
nodeExtract: NodeExtractConfig
allModels?: any[]
}
@@ -459,18 +250,11 @@ const props = defineProps<Props>()
const emit = defineEmits<{
'update:multimodal': [value: MultimodalConfig]
'update:nodeExtract': [value: NodeExtractConfig]
}>()
const localMultimodal = ref<MultimodalConfig>({ ...props.multimodal })
const localNodeExtract = ref<NodeExtractConfig>({
...props.nodeExtract,
nodes: props.nodeExtract.nodes || [],
relations: props.nodeExtract.relations || []
})
const vllmSelectorRef = ref()
const isGraphDatabaseEnabled = ref(false)
const isMinioEnabled = ref(false)
// Check system status on mount
@@ -478,28 +262,6 @@ onMounted(async () => {
try {
const systemInfo = await getSystemInfo()
// Check graph database status
if (systemInfo.data?.graph_database_engine) {
// Check if graph database is enabled
// Enabled if it's "Neo4j" or any other non-empty value that's not a disabled indicator
const engine = systemInfo.data.graph_database_engine.trim()
const disabledIndicators = ['未启用', '未配置', 'Unknown', 'Неизвестно', '']
isGraphDatabaseEnabled.value = !disabledIndicators.includes(engine) && engine.length > 0
// If graph database is disabled, also disable node extract
if (!isGraphDatabaseEnabled.value && localNodeExtract.value.enabled) {
localNodeExtract.value.enabled = false
emit('update:nodeExtract', localNodeExtract.value)
}
} else {
// No graph database engine info, assume disabled
isGraphDatabaseEnabled.value = false
if (localNodeExtract.value.enabled) {
localNodeExtract.value.enabled = false
emit('update:nodeExtract', localNodeExtract.value)
}
}
// Check MinIO status
isMinioEnabled.value = systemInfo.data?.minio_enabled === true
@@ -511,12 +273,7 @@ onMounted(async () => {
} catch (error) {
console.error('Failed to fetch system info:', error)
// Default to disabled if we can't fetch the info
isGraphDatabaseEnabled.value = false
isMinioEnabled.value = false
if (localNodeExtract.value.enabled) {
localNodeExtract.value.enabled = false
emit('update:nodeExtract', localNodeExtract.value)
}
// If MinIO status unknown and storage type is minio, switch to cos
if (localMultimodal.value.storageType === 'minio') {
localMultimodal.value.storageType = 'cos'
@@ -530,14 +287,6 @@ watch(() => props.multimodal, (newVal) => {
localMultimodal.value = { ...newVal }
}, { deep: true })
watch(() => props.nodeExtract, (newVal) => {
localNodeExtract.value = {
...newVal,
nodes: newVal.nodes || [],
relations: newVal.relations || []
}
}, { deep: true })
// Handle multimodal toggle
const handleMultimodalToggle = () => {
// Reset related configuration when multimodal is disabled
@@ -580,67 +329,9 @@ const handleAddModel = (subSection: string) => {
uiStore.openSettings('models', subSection)
}
const graphGuideUrl =
import.meta.env.VITE_KG_GUIDE_URL ||
'https://github.com/Tencent/WeKnora/blob/main/docs/%E5%BC%80%E5%90%AF%E7%9F%A5%E8%AF%86%E5%9B%BE%E8%B0%B1%E5%8A%9F%E8%83%BD.md'
// Handle knowledge graph toggle
const handleNodeExtractToggle = () => {
// Prevent enabling if graph database is not enabled
if (!isGraphDatabaseEnabled.value) {
localNodeExtract.value.enabled = false
return
}
emit('update:nodeExtract', localNodeExtract.value)
}
// Open guide documentation to show how to enable graph database
const handleOpenGraphGuide = () => {
window.open(graphGuideUrl, '_blank', 'noopener')
}
// Handle configuration change
const handleConfigChange = () => {
emit('update:multimodal', localMultimodal.value)
emit('update:nodeExtract', localNodeExtract.value)
}
// Add a new node
const addNode = () => {
if (!localNodeExtract.value.nodes) {
localNodeExtract.value.nodes = []
}
localNodeExtract.value.nodes.push({
name: '',
chunks: [],
attributes: []
})
handleConfigChange()
}
// Remove a node
const removeNode = (index: number) => {
localNodeExtract.value.nodes.splice(index, 1)
handleConfigChange()
}
// Add a new relation
const addRelation = () => {
if (!localNodeExtract.value.relations) {
localNodeExtract.value.relations = []
}
localNodeExtract.value.relations.push({
node1: '',
node2: '',
type: ''
})
handleConfigChange()
}
// Remove a relation
const removeRelation = (index: number) => {
localNodeExtract.value.relations.splice(index, 1)
handleConfigChange()
}
// The allModels prop keeps model options in sync
@@ -716,11 +407,6 @@ const removeRelation = (index: number) => {
align-items: center;
}
.graph-guide-link {
display: inline-block;
margin-top: 8px;
}
.subsection {
padding: 16px 20px;
margin: 12px 0 0 0;
+9 -15
View File
@@ -319,13 +319,19 @@ func (h *InitializationHandler) UpdateKBConfig(c *gin.Context) {
}
kb.ExtractConfig = &types.ExtractConfig{
Enabled: req.NodeExtract.Enabled,
Text: req.NodeExtract.Text,
Tags: req.NodeExtract.Tags,
Nodes: nodes,
Relations: relations,
}
} else {
kb.ExtractConfig = nil
kb.ExtractConfig = &types.ExtractConfig{Enabled: false}
}
if err := validateExtractConfig(kb.ExtractConfig); err != nil {
logger.Error(ctx, "Invalid extract configuration", err)
c.Error(err)
return
}
// 保存更新后的知识库
@@ -1811,13 +1817,7 @@ func (h *InitializationHandler) ExtractTextRelations(c *gin.Context) {
result, err := h.extractRelationsFromText(ctx, req.Text, req.Tags, req.LLMConfig)
if err != nil {
logger.Error(ctx, "文本关系提取失败", err)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"success": false,
"message": err.Error(),
},
})
c.Error(errors.NewInternalServerError("文本关系提取失败: " + err.Error()))
return
}
@@ -1885,13 +1885,7 @@ func (h *InitializationHandler) FabriText(c *gin.Context) {
result, err := h.fabriText(ctx, req.Tags, req.LLMConfig)
if err != nil {
logger.Error(ctx, "生成示例文本失败", err)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"success": false,
"message": err.Error(),
},
})
c.Error(errors.NewInternalServerError("生成示例文本失败: " + err.Error()))
return
}
+73
View File
@@ -3,6 +3,7 @@ package handler
import (
"context"
"net/http"
"strconv"
"github.com/Tencent/WeKnora/internal/errors"
"github.com/Tencent/WeKnora/internal/logger"
@@ -78,6 +79,11 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c *gin.Context) {
c.Error(errors.NewBadRequestError("Invalid request parameters").WithDetails(err.Error()))
return
}
if err := validateExtractConfig(req.ExtractConfig); err != nil {
logger.Error(ctx, "Invalid extract configuration", err)
c.Error(err)
return
}
logger.Infof(ctx, "Creating knowledge base, name: %s", req.Name)
// Create knowledge base using the service
@@ -267,3 +273,70 @@ func (h *KnowledgeBaseHandler) CopyKnowledgeBase(c *gin.Context) {
"message": "Knowledge base copy successfully",
})
}
// validateExtractConfig validates the graph configuration parameters
func validateExtractConfig(config *types.ExtractConfig) error {
logger.Errorf(context.Background(), "Validating extract configuration: %+v", config)
if config == nil {
return nil
}
if !config.Enabled {
*config = types.ExtractConfig{Enabled: false}
return nil
}
// Validate text field
if config.Text == "" {
return errors.NewBadRequestError("text cannot be empty")
}
// Validate tags field
if len(config.Tags) == 0 {
return errors.NewBadRequestError("tags cannot be empty")
}
for i, tag := range config.Tags {
if tag == "" {
return errors.NewBadRequestError("tag cannot be empty at index " + strconv.Itoa(i))
}
}
// Validate nodes
if len(config.Nodes) == 0 {
return errors.NewBadRequestError("nodes cannot be empty")
}
nodeNames := make(map[string]bool)
for i, node := range config.Nodes {
if node.Name == "" {
return errors.NewBadRequestError("node name cannot be empty at index " + strconv.Itoa(i))
}
// Check for duplicate node names
if nodeNames[node.Name] {
return errors.NewBadRequestError("duplicate node name: " + node.Name)
}
nodeNames[node.Name] = true
}
if len(config.Relations) == 0 {
return errors.NewBadRequestError("relations cannot be empty")
}
// Validate relations
for i, relation := range config.Relations {
if relation.Node1 == "" {
return errors.NewBadRequestError("relation node1 cannot be empty at index " + strconv.Itoa(i))
}
if relation.Node2 == "" {
return errors.NewBadRequestError("relation node2 cannot be empty at index " + strconv.Itoa(i))
}
if relation.Type == "" {
return errors.NewBadRequestError("relation type cannot be empty at index " + strconv.Itoa(i))
}
// Check if referenced nodes exist
if !nodeNames[relation.Node1] {
return errors.NewBadRequestError("relation references non-existent node1: " + relation.Node1)
}
if !nodeNames[relation.Node2] {
return errors.NewBadRequestError("relation references non-existent node2: " + relation.Node2)
}
}
return nil
}
+4 -4
View File
@@ -197,10 +197,10 @@ func (c *VLMConfig) Scan(value interface{}) error {
type ExtractConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
Text string `yaml:"text" json:"text"`
Tags []string `yaml:"tags" json:"tags"`
Nodes []*GraphNode `yaml:"nodes" json:"nodes"`
Relations []*GraphRelation `yaml:"relations" json:"relations"`
Text string `yaml:"text" json:"text,omitempty"`
Tags []string `yaml:"tags" json:"tags,omitempty"`
Nodes []*GraphNode `yaml:"nodes" json:"nodes,omitempty"`
Relations []*GraphRelation `yaml:"relations" json:"relations,omitempty"`
}
// Value implements the driver.Valuer interface, used to convert ExtractConfig to database value