feat: knowledge workflow

This commit is contained in:
shaohuzhang1 2025-11-11 15:54:02 +08:00
parent 107fc44049
commit b886d8d458
18 changed files with 417 additions and 14 deletions

View File

@ -0,0 +1,8 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/11/11 10:06
@desc:
"""

View File

@ -0,0 +1,38 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file i_data_source_local_node.py
@date2025/11/11 10:06
@desc:
"""
from abc import abstractmethod
from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult
class DataSourceLocalNodeParamsSerializer(serializers.Serializer):
file_format = serializers.ListField(child=serializers.CharField)
max_file_number = serializers.IntegerField(required=True, label=_("Number of uploaded files"))
file_max_size = serializers.IntegerField(required=True, label=_("Upload file size"))
class IDataSourceLocalNode(INode):
type = 'data-source-local-node'
@abstractmethod
def get_form_class(self):
pass
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return DataSourceLocalNodeParamsSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self, file_format, max_file_number, file_max_size, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,8 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/11/11 10:08
@desc:
"""

View File

@ -0,0 +1,27 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file base_data_source_local_node.py
@date2025/11/11 10:30
@desc:
"""
from application.flow.i_step_node import NodeResult
from application.flow.step_node.data_source_local_node.i_data_source_local_node import IDataSourceLocalNode
from common import forms
from common.forms import BaseForm
class BaseDataSourceLocalNodeForm(BaseForm):
api_key = forms.PasswordInputField('API Key', required=True)
class BaseDataSourceLocalNode(IDataSourceLocalNode):
def save_context(self, details, workflow_manage):
pass
def get_form_class(self):
return BaseDataSourceLocalNodeForm()
def execute(self, file_format, max_file_number, file_max_size, **kwargs) -> NodeResult:
pass

View File

@ -8,11 +8,13 @@ from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.flow.step_node import get_node
from common.exception.app_exception import AppApiException
from knowledge.models import KnowledgeScope, Knowledge, KnowledgeType, KnowledgeWorkflow
from knowledge.serializers.knowledge import KnowledgeModelSerializer
from system_manage.models import AuthTargetType
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
from tools.models import Tool
class KnowledgeWorkflowModelSerializer(serializers.ModelSerializer):
@ -22,6 +24,20 @@ class KnowledgeWorkflowModelSerializer(serializers.ModelSerializer):
class KnowledgeWorkflowSerializer(serializers.Serializer):
class Form(serializers.Serializer):
type = serializers.CharField(required=True, label=_('type'))
id = serializers.CharField(required=True, label=_('type'))
def get_form_list(self):
self.is_valid(raise_exception=True)
if self.data.get('type') == 'local':
node = get_node(self.data.get('id'))
return node.get_form_class()().to_form_list()
elif self.data.get('type') == 'tool':
tool = QuerySet(Tool).filter(id=self.data.get("id")).first()
# todo 调用工具数据源的函数获取表单列表
return None
class Create(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_('user id'))
workspace_id = serializers.CharField(required=True, label=_('workspace id'))

View File

@ -12,10 +12,16 @@ from common.log.log import log
from common.result import result
from knowledge.api.knowledge_workflow import KnowledgeWorkflowApi
from knowledge.serializers.common import get_knowledge_operation_object
from knowledge.serializers.knowledge import KnowledgeSerializer
from knowledge.serializers.knowledge_workflow import KnowledgeWorkflowSerializer
class KnowledgeWorkflowFormView(APIView):
authentication_classes = [TokenAuth]
def get(self):
return result.success(KnowledgeWorkflowSerializer.Form().get_form_list())
class KnowledgeWorkflowView(APIView):
authentication_classes = [TokenAuth]

View File

@ -38,6 +38,10 @@ export enum WorkflowType {
VariableAggregationNode = 'variable-aggregation-node',
VideoUnderstandNode = 'video-understand-node',
ParameterExtractionNode = 'parameter-extraction-node',
DataSourceLocalNode = 'data-source-local-node',
}
export enum WorkflowKind {
DataSource = 'data-source',
}
export enum WorkflowMode {
// 应用工作流

View File

@ -1,5 +1,9 @@
<template>
<div v-show="show" class="workflow-dropdown-menu border border-r-6 white-bg" :style="{ width: activeName === 'base' ? '400px':'640px' }">
<div
v-show="show"
class="workflow-dropdown-menu border border-r-6 white-bg"
:style="{ width: activeName === 'base' ? '400px' : '640px' }"
>
<el-tabs v-model="activeName" class="workflow-dropdown-tabs" @tab-change="handleClick">
<div
v-show="activeName === 'base'"

View File

@ -0,0 +1,11 @@
<template>
<div></div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
workflow: any
}>()
const source_node_list = computed(() => {})
</script>
<style lang="scss" scoped></style>

View File

@ -301,7 +301,7 @@ const publish = () => {
?.validate()
.then(() => {
const workflow = getGraphData()
const workflowInstance = new WorkFlowInstance(workflow)
const workflowInstance = new WorkFlowInstance(workflow, WorkflowMode.Knowledge)
try {
workflowInstance.is_valid()
} catch (e: any) {
@ -384,7 +384,7 @@ const clickShowDebug = () => {
?.validate()
.then(() => {
const graphData = getGraphData()
const workflow = new WorkFlowInstance(graphData)
const workflow = new WorkFlowInstance(graphData, WorkflowMode.Knowledge)
try {
workflow.is_valid()
detail.value = {
@ -396,6 +396,7 @@ const clickShowDebug = () => {
showDebug.value = true
} catch (e: any) {
console.log(e)
MsgError(e.toString())
}
})

View File

@ -1,3 +1,4 @@
import { WorkflowKind } from './../../enums/application'
import Components from '@/components'
import ElementPlus from 'element-plus'
import * as ElementPlusIcons from '@element-plus/icons-vue'
@ -414,9 +415,11 @@ class AppNodeModel extends HtmlResize.model {
const { id, x, y, width } = this
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = []
if (![WorkflowType.Base as string, WorkflowType.KnowledgeBase as string].includes(this.type)) {
if (![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type)) {
if (
![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type) &&
this.properties.kind != WorkflowKind.DataSource
) {
anchors.push({
x: x - width / 2 + 10,
y: showNode ? y : y - 15,

View File

@ -1,3 +1,4 @@
import { WorkflowKind } from './../../enums/application'
import { WorkflowType, WorkflowMode } from '@/enums/application'
import { t } from '@/locales'
@ -79,6 +80,25 @@ export const knowledgeBaseNode = {
user_input_field_list: [],
},
}
export const dataSourceLocalNode = {
id: WorkflowType.DataSourceLocalNode,
type: WorkflowType.DataSourceLocalNode,
x: 360,
y: 2761.3875,
text: t('views.applicationWorkflow.nodes.dataSourceLocalNode.text', '本地文件'),
label: t('views.applicationWorkflow.nodes.dataSourceLocalNode.label', '本地文件'),
properties: {
kind: WorkflowKind.DataSource,
height: 728.375,
stepName: t('views.applicationWorkflow.nodes.dataSourceLocalNode.label', '本地文件'),
input_field_list: [],
node_data: {},
config: {},
showNode: true,
user_input_config: {},
user_input_field_list: [],
},
}
/**
*
* type nodes
@ -641,6 +661,10 @@ export const loopBreakNode = {
}
export const knowledgeMenuNodes = [
{
label: t('views.applicationWorkflow.nodes.classify.dataSource', '数据源'),
list: [dataSourceLocalNode],
},
{
label: t('views.applicationWorkflow.nodes.classify.aiCapability'),
list: [
@ -868,7 +892,6 @@ export const compareList = [
{ value: 'start_with', label: 'startWith' },
{ value: 'end_with', label: 'endWith' },
]
export const nodeDict: any = {
[WorkflowType.AiChat]: aiChatNode,
[WorkflowType.SearchKnowledge]: searchKnowledgeNode,
@ -903,6 +926,7 @@ export const nodeDict: any = {
[WorkflowType.ParameterExtractionNode]: parameterExtractionNode,
[WorkflowType.VariableAggregationNode]: variableAggregationNode,
[WorkflowType.KnowledgeBase]: knowledgeBaseNode,
[WorkflowType.DataSourceLocalNode]: dataSourceLocalNode,
}
export function isWorkFlow(type: string | undefined) {

View File

@ -1,3 +1,4 @@
import { WorkflowKind } from './../../enums/application'
import { WorkflowType, WorkflowMode } from '@/enums/application'
import { t } from '@/locales'
@ -43,7 +44,9 @@ const loop_end_nodes: Array<string> = [
]
const end_nodes_dict = {
[WorkflowMode.Application]: end_nodes,
[WorkflowMode.Knowledge]: end_nodes,
[WorkflowMode.ApplicationLoop]: loop_end_nodes,
[WorkflowMode.KnowledgeLoop]: loop_end_nodes,
}
export class WorkFlowInstance {
@ -63,8 +66,10 @@ export class WorkFlowInstance {
*
*/
private is_valid_start_node() {
const start_node_list = this.nodes.filter((item) =>
[WorkflowType.Start, WorkflowType.LoopStartNode].includes(item.id),
const start_node_list = this.nodes.filter(
(item) =>
[WorkflowType.Start, WorkflowType.LoopStartNode].includes(item.id) ||
item.properties.kind == WorkflowKind.DataSource,
)
if (start_node_list.length == 0) {
throw t('views.applicationWorkflow.validate.startNodeRequired')
@ -77,6 +82,10 @@ export class WorkFlowInstance {
*
*/
private is_valid_base_node() {
console.log(this.workflowModel)
if (this.workflowModel == WorkflowMode.Knowledge) {
return
}
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
if (start_node_list.length == 0) {
throw t('views.applicationWorkflow.validate.baseNodeRequired')
@ -106,8 +115,10 @@ export class WorkFlowInstance {
* @returns
*/
get_start_node() {
const start_node_list = this.nodes.filter((item) =>
[WorkflowType.Start, WorkflowType.LoopStartNode].includes(item.id),
const start_node_list = this.nodes.filter(
(item) =>
[WorkflowType.Start, WorkflowType.LoopStartNode].includes(item.id) ||
item.properties.kind == WorkflowKind.DataSource,
)
return start_node_list[0]
}
@ -143,9 +154,26 @@ export class WorkFlowInstance {
private is_valid_work_flow() {
this.workFlowNodes = []
this._is_valid_work_flow()
if (this.workflowModel == WorkflowMode.Knowledge) {
const start_node_list = this.nodes.filter(
(item) =>
[WorkflowType.Start, WorkflowType.LoopStartNode].includes(item.id) ||
item.properties.kind == WorkflowKind.DataSource,
)
start_node_list.forEach((startNode) => {
this._is_valid_work_flow(startNode)
})
} else {
this._is_valid_work_flow()
}
const notInWorkFlowNodes = this.nodes
.filter((node: any) => node.id !== WorkflowType.Start && node.id !== WorkflowType.Base)
.filter(
(node: any) =>
node.id !== WorkflowType.Start &&
node.id !== WorkflowType.Base &&
node.id !== WorkflowType.KnowledgeBase,
)
.filter((node) => !this.workFlowNodes.includes(node))
if (notInWorkFlowNodes.length > 0) {
throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${notInWorkFlowNodes.map((node) => node.properties.stepName).join('')}`
@ -175,7 +203,9 @@ export class WorkFlowInstance {
if (
node.type !== WorkflowType.Base &&
node.type !== WorkflowType.Start &&
node.type !== WorkflowType.LoopStartNode
node.type !== WorkflowType.LoopStartNode &&
node.type !== WorkflowType.KnowledgeBase &&
node.properties.kind !== WorkflowKind.DataSource
) {
if (!this.edges.some((edge) => edge.targetNodeId === node.id)) {
throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${node.properties.stepName}`

View File

@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" style="background: #14c0ff">
<img src="@/assets/workflow/icon_condition.svg" style="width: 75%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,12 @@
import DataSourceWebNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class DataSourceWebNode extends AppNode {
constructor(props: any) {
super(props, DataSourceWebNodeVue)
}
}
export default {
type: 'data-source-local-node',
model: AppNodeModel,
view: DataSourceWebNode,
}

View File

@ -0,0 +1,132 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
>
<el-form-item
:label="
$t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.fileFormat.label',
'支持的文件格式',
)
"
:rules="{
type: 'array',
required: true,
message: $t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.fileFormat.message',
'请选择文件格式',
),
trigger: 'change',
}"
>
<el-select
v-model="form_data.file_format"
:placeholder="
$t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.fileFormat.placeholder',
'请选择文件格式',
)
"
style="width: 240px"
clearable
multiple
>
<template #label="{ label, value }">
<span>{{ label }} </span>
</template>
<el-option
v-for="item in file_format_list"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
:label="
$t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.maxFileNumber.label',
'每次上传最大文件数',
)
"
:rules="{
type: 'array',
required: true,
message: $t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.maxFileNumber.placeholder',
'请输入最大文件数',
),
trigger: 'change',
}"
>
<el-slider v-model="form_data.max_file_number" show-input />
</el-form-item>
<el-form-item
:label="
$t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.maxFileNumber.label',
'上传的每个文档最大(MB)',
)
"
:rules="{
type: 'array',
required: true,
message: $t(
'views.applicationWorkflow.nodes.dataSourceLocalNode.maxFileNumber.placeholder',
'上传的每个文档最大(MB) 必填',
),
trigger: 'change',
}"
>
<el-slider v-model="form_data.file_max_size" show-input />
</el-form-item>
</el-form>
</el-card>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed } from 'vue'
import { set } from 'lodash'
const props = defineProps<{ nodeModel: any }>()
const file_format_list = [
{ label: 'TXT', value: '.txt' },
{ label: 'DOCX', value: '.docx' },
{ label: 'PDF', value: '.pdf' },
{ label: 'HTML', value: '.html' },
{ label: 'XLS', value: '.xls' },
{ label: 'XLSX', value: '.xlsx' },
{ label: 'ZIP', value: '.zip' },
{ label: 'CSV', value: '.csv' },
]
const form = {
file_format: [],
max_file_number: 50,
file_max_size: 100,
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
},
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,12 @@
import DataSourceWebNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class DataSourceWebNode extends AppNode {
constructor(props: any) {
super(props, DataSourceWebNodeVue)
}
}
export default {
type: 'data-source-web-node',
model: AppNodeModel,
view: DataSourceWebNode,
}

View File

@ -0,0 +1,61 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
>
<el-form-item
:label="$t('views.problem.relateParagraph.selectDocument')"
:rules="{
type: 'array',
required: true,
message: $t('views.chatLog.documentPlaceholder'),
trigger: 'change',
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.chatLog.documentPlaceholder')"
v-model="form_data.document_list"
/>
</el-form-item>
</el-form>
</el-card>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed } from 'vue'
import { set } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
const props = defineProps<{ nodeModel: any }>()
const form = {
document_list: ['start-node', 'document'],
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
},
})
</script>
<style lang="scss" scoped></style>