feat: knowledge workflow
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run

This commit is contained in:
shaohuzhang1 2025-11-19 16:32:11 +08:00
parent 8baa8e81cf
commit e758f015f3
12 changed files with 250 additions and 15 deletions

View File

@ -21,6 +21,7 @@ from application.flow.common import Answer, NodeChunk
from application.models import ApplicationChatUserStats from application.models import ApplicationChatUserStats
from application.models import ChatRecord, ChatUserType from application.models import ChatRecord, ChatUserType
from common.field.common import InstanceField from common.field.common import InstanceField
from knowledge.models.knowledge_action import KnowledgeAction, State
chat_cache = cache chat_cache = cache
@ -97,11 +98,13 @@ class WorkFlowPostHandler:
class KnowledgeWorkflowPostHandler(WorkFlowPostHandler): class KnowledgeWorkflowPostHandler(WorkFlowPostHandler):
def __init__(self, chat_info): def __init__(self, chat_info, knowledge_action_id):
super().__init__(chat_info) super().__init__(chat_info)
self.knowledge_action_id = knowledge_action_id
def handler(self, workflow): def handler(self, workflow):
pass QuerySet(KnowledgeAction).filter(id=self.knowledge_action_id).update(
state=State.SUCCESS)
class NodeResult: class NodeResult:
@ -161,7 +164,8 @@ class FlowParamsSerializer(serializers.Serializer):
class KnowledgeFlowParamsSerializer(serializers.Serializer): class KnowledgeFlowParamsSerializer(serializers.Serializer):
knowledge_id = serializers.CharField(required=True, label="知识库id") knowledge_id = serializers.UUIDField(required=True, label="知识库id")
knowledge_action_id = serializers.UUIDField(required=True, label="知识库任务执行器id")
data_source = serializers.DictField(required=True, label="数据源") data_source = serializers.DictField(required=True, label="数据源")
knowledge_base = serializers.DictField(required=False, label="知识库设置") knowledge_base = serializers.DictField(required=False, label="知识库设置")
@ -241,7 +245,8 @@ class INode:
self.status = 500 self.status = 500
self.answer_text = str(e) self.answer_text = str(e)
self.err_message = str(e) self.err_message = str(e)
self.context['run_time'] = time.time() - self.context['start_time'] current_time = time.time()
self.context['run_time'] = current_time - (self.context.get('start_time') or current_time)
def write_error_context(answer, status=200): def write_error_context(answer, status=200):
pass pass

View File

@ -6,12 +6,20 @@
@date2025/11/13 19:02 @date2025/11/13 19:02
@desc: @desc:
""" """
import traceback
from concurrent.futures import ThreadPoolExecutor
from django.db.models import QuerySet
from django.utils.translation import get_language
from application.flow.common import Workflow from application.flow.common import Workflow
from application.flow.i_step_node import WorkFlowPostHandler, KnowledgeFlowParamsSerializer from application.flow.i_step_node import WorkFlowPostHandler, KnowledgeFlowParamsSerializer
from application.flow.workflow_manage import WorkflowManage from application.flow.workflow_manage import WorkflowManage
from common.handle.base_to_response import BaseToResponse from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.system_to_response import SystemToResponse from common.handle.impl.response.system_to_response import SystemToResponse
from knowledge.models.knowledge_action import KnowledgeAction, State
executor = ThreadPoolExecutor(max_workers=200)
class KnowledgeWorkflowManage(WorkflowManage): class KnowledgeWorkflowManage(WorkflowManage):
@ -33,3 +41,46 @@ class KnowledgeWorkflowManage(WorkflowManage):
start_node_list = [node for node in self.flow.nodes if start_node_list = [node for node in self.flow.nodes if
self.params.get('data_source', {}).get('node_id') == node.id] self.params.get('data_source', {}).get('node_id') == node.id]
return start_node_list[0] return start_node_list[0]
def run(self):
executor.submit(self._run)
def _run(self):
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
state=State.STARTED)
language = get_language()
self.run_chain_async(self.start_node, None, language)
while self.is_run():
pass
self.work_flow_post_handler.handler(self)
def run_chain(self, current_node, node_result_future=None):
if node_result_future is None:
node_result_future = self.run_node_future(current_node)
try:
result = self.hand_node_result(current_node, node_result_future)
return result
except Exception as e:
traceback.print_exc()
return None
def hand_node_result(self, current_node, node_result_future):
try:
current_result = node_result_future.result()
result = current_result.write_context(current_node, self)
if result is not None:
# 阻塞获取结果
list(result)
return current_result
except Exception as e:
traceback.print_exc()
self.status = 500
current_node.get_write_error_context(e)
self.answer += str(e)
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
details=self.get_runtime_details(),
state=State.FAILURE)
finally:
current_node.node_chunk.end()
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
details=self.get_runtime_details())

View File

@ -37,3 +37,15 @@ class BaseDataSourceLocalNode(IDataSourceLocalNode):
def execute(self, file_type_list, file_size_limit, file_count_limit, **kwargs) -> NodeResult: def execute(self, file_type_list, file_size_limit, file_count_limit, **kwargs) -> NodeResult:
return NodeResult({'file_list': self.workflow_manage.params.get('data_source', {}).get('file_list')}, return NodeResult({'file_list': self.workflow_manage.params.get('data_source', {}).get('file_list')},
self.workflow_manage.params.get('knowledge_base') or {}) self.workflow_manage.params.get('knowledge_base') or {})
def get_details(self, index: int, **kwargs):
return {
'name': self.node.properties.get('stepName'),
"index": index,
'run_time': self.context.get('run_time'),
'type': self.node.type,
'file_list': self.context.get('file_list'),
'knowledge_base': self.workflow_params.get('knowledge_base'),
'status': self.status,
'err_message': self.err_message
}

View File

@ -0,0 +1,32 @@
# Generated by Django 5.2.8 on 2025-11-19 06:06
import common.encoder.encoder
import django.db.models.deletion
import uuid_utils.compat
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('knowledge', '0004_alter_document_type_alter_knowledge_type_and_more'),
]
operations = [
migrations.CreateModel(
name='KnowledgeAction',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('state', models.CharField(choices=[('PENDING', 'Pending'), ('STARTED', 'Started'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKE', 'Revoke'), ('REVOKED', 'Revoked')], default='STARTED', max_length=20, verbose_name='状态')),
('details', models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='执行详情')),
('run_time', models.FloatField(default=0, verbose_name='运行时长')),
('meta', models.JSONField(default=dict, verbose_name='元数据')),
('knowledge', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='knowledge.knowledge', verbose_name='知识库')),
],
options={
'db_table': 'knowledge_action',
},
),
]

View File

@ -0,0 +1,49 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file knowledge_action.py
@date2025/11/18 17:59
@desc:
"""
import uuid_utils.compat as uuid
from django.db import models
from common.encoder.encoder import SystemEncoder
from common.mixins.app_model_mixin import AppModelMixin
from knowledge.models import Knowledge
class State(models.TextChoices):
# 等待
PENDING = 'PENDING'
# 执行中
STARTED = 'STARTED'
# 成功
SUCCESS = 'SUCCESS'
# 失败
FAILURE = 'FAILURE'
# 取消任务
REVOKE = 'REVOKE'
# 取消成功
REVOKED = 'REVOKED'
class KnowledgeAction(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
knowledge = models.ForeignKey(Knowledge, on_delete=models.DO_NOTHING, verbose_name="知识库", db_constraint=False)
state = models.CharField(verbose_name='状态', max_length=20,
choices=State.choices,
default=State.STARTED)
details = models.JSONField(verbose_name="执行详情", default=dict, encoder=SystemEncoder)
run_time = models.FloatField(verbose_name="运行时长", default=0)
meta = models.JSONField(verbose_name="元数据", default=dict)
class Meta:
db_table = "knowledge_action"

View File

@ -14,6 +14,7 @@ from application.flow.knowledge_workflow_manage import KnowledgeWorkflowManage
from application.flow.step_node import get_node from application.flow.step_node import get_node
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from knowledge.models import KnowledgeScope, Knowledge, KnowledgeType, KnowledgeWorkflow from knowledge.models import KnowledgeScope, Knowledge, KnowledgeType, KnowledgeWorkflow
from knowledge.models.knowledge_action import KnowledgeAction, State
from knowledge.serializers.knowledge import KnowledgeModelSerializer from knowledge.serializers.knowledge import KnowledgeModelSerializer
from system_manage.models import AuthTargetType from system_manage.models import AuthTargetType
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
@ -34,13 +35,30 @@ class KnowledgeWorkflowActionSerializer(serializers.Serializer):
if with_valid: if with_valid:
self.is_valid(raise_exception=True) self.is_valid(raise_exception=True)
knowledge_workflow = QuerySet(KnowledgeWorkflow).filter(knowledge_id=self.data.get("knowledge_id")).first() knowledge_workflow = QuerySet(KnowledgeWorkflow).filter(knowledge_id=self.data.get("knowledge_id")).first()
knowledge_action_id = uuid.uuid7()
KnowledgeAction(id=knowledge_action_id, knowledge_id=self.data.get("knowledge_id"), state=State.STARTED).save()
work_flow_manage = KnowledgeWorkflowManage( work_flow_manage = KnowledgeWorkflowManage(
Workflow.new_instance(knowledge_workflow.work_flow, WorkflowMode.KNOWLEDGE), Workflow.new_instance(knowledge_workflow.work_flow, WorkflowMode.KNOWLEDGE),
{'knowledge_id': self.data.get("knowledge_id"), 'stream': True, {'knowledge_id': self.data.get("knowledge_id"), 'knowledge_action_id': knowledge_action_id, 'stream': True,
**instance}, **instance},
KnowledgeWorkflowPostHandler(None)) KnowledgeWorkflowPostHandler(None, knowledge_action_id))
r = work_flow_manage.run() work_flow_manage.run()
return r return {'id': knowledge_action_id, 'knowledge_id': self.data.get("knowledge_id"), 'state': State.STARTED,
'details': {}}
class Operate(serializers.Serializer):
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id'))
id = serializers.UUIDField(required=True, label=_('knowledge action id'))
def one(self, is_valid=True):
if is_valid:
self.is_valid(raise_exception=True)
knowledge_action_id = self.data.get("id")
knowledge_action = QuerySet(KnowledgeAction).filter(id=knowledge_action_id).first()
return {'id': knowledge_action_id, 'knowledge_id': knowledge_action.knowledge_id,
'state': knowledge_action.state,
'details': knowledge_action.details}
class KnowledgeWorkflowSerializer(serializers.Serializer): class KnowledgeWorkflowSerializer(serializers.Serializer):

View File

@ -70,6 +70,7 @@ urlpatterns = [
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<int:current_page>/<int:page_size>', views.DocumentView.Page.as_view()), path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<int:current_page>/<int:page_size>', views.DocumentView.Page.as_view()),
path('workspace/<str:workspace_id>/knowledge/<int:current_page>/<int:page_size>', views.KnowledgeView.Page.as_view()), path('workspace/<str:workspace_id>/knowledge/<int:current_page>/<int:page_size>', views.KnowledgeView.Page.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/form_list/<str:type>/<str:id>', views.KnowledgeWorkflowFormView.as_view()), path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/form_list/<str:type>/<str:id>', views.KnowledgeWorkflowFormView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/action', views.KnowledgeWorkflowActionView.as_view()) path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/action', views.KnowledgeWorkflowActionView.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/action/<str:knowledge_action_id>', views.KnowledgeWorkflowActionView.Operate.as_view())
] ]

View File

@ -27,8 +27,16 @@ class KnowledgeWorkflowActionView(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]
def post(self, request: Request, workspace_id: str, knowledge_id: str): def post(self, request: Request, workspace_id: str, knowledge_id: str):
return KnowledgeWorkflowActionSerializer( return result.success(KnowledgeWorkflowActionSerializer(
data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id}).action(request.data, True) data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id}).action(request.data, True))
class Operate(APIView):
authentication_classes = [TokenAuth]
def get(self, request, workspace_id: str, knowledge_id: str, knowledge_action_id: str):
return result.success(KnowledgeWorkflowActionSerializer.Operate(
data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'id': knowledge_action_id})
.one())
class KnowledgeWorkflowView(APIView): class KnowledgeWorkflowView(APIView):

View File

@ -337,7 +337,13 @@ const workflowAction: (
) => Promise<Result<any>> = (knowledge_id: string, instance, loading) => { ) => Promise<Result<any>> = (knowledge_id: string, instance, loading) => {
return post(`${prefix.value}/${knowledge_id}/action`, instance, {}, loading) return post(`${prefix.value}/${knowledge_id}/action`, instance, {}, loading)
} }
const getWorkflowAction: (
knowledge_id: string,
knowledge_action_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (knowledge_id: string, knowledge_action_id, loading) => {
return get(`${prefix.value}/${knowledge_id}/action/${knowledge_action_id}`, {}, loading)
}
export default { export default {
getKnowledgeList, getKnowledgeList,
getKnowledgeListPage, getKnowledgeListPage,
@ -364,4 +370,5 @@ export default {
createWorkflowKnowledge, createWorkflowKnowledge,
getKnowledgeWorkflowFormList, getKnowledgeWorkflowFormList,
workflowAction, workflowAction,
getWorkflowAction,
} }

View File

@ -0,0 +1,38 @@
<template>
<div>
<ExecutionDetailContent :detail="detail" app-type="WORK_FLOW"></ExecutionDetailContent>
</div>
</template>
<script setup lang="ts">
import { onUnmounted, ref, computed } from 'vue'
import knowledgeApi from '@/api/knowledge/knowledge'
const props = defineProps<{ id: string; knowledge_id: string }>()
import ExecutionDetailContent from '@/components/ai-chat/component/knowledge-source-component/ExecutionDetailContent.vue'
const detail = computed(() => {
if (knowledge_action.value) {
return Object.values(knowledge_action.value.details)
}
return []
})
const state = computed(() => {
if (knowledge_action.value) {
return knowledge_action.value.state
}
return 'PADDING'
})
const knowledge_action = ref<any>()
const getKnowledgeWorkflowAction = () => {
knowledgeApi.getWorkflowAction(props.knowledge_id, props.id).then((ok) => {
knowledge_action.value = ok.data
if (['SUCCESS', 'FAILURE', 'REVOKED'].includes(state.value)) {
clearInterval(polling)
}
})
}
const polling = setInterval(getKnowledgeWorkflowAction, 2000)
onUnmounted(() => {
clearInterval(polling)
})
</script>
<style lang="scss"></style>

View File

@ -1,7 +1,13 @@
<template> <template>
<div style="height: 100%; width: 100%"> <div style="height: 100%; width: 100%">
<div style="height: calc(100% - 57px); overflow-y: auto; width: 100%"> <div style="height: calc(100% - 57px); overflow-y: auto; width: 100%">
<component ref="ActionRef" :is="ak[active]" :workflow="workflow"></component> <component
ref="ActionRef"
:is="ak[active]"
:workflow="workflow"
:knowledge_id="knowledge_id"
:id="action_id"
></component>
</div> </div>
<div class="el-drawer__footer"> <div class="el-drawer__footer">
<el-button>Cancel</el-button> <el-button>Cancel</el-button>
@ -23,6 +29,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, provide, type Ref } from 'vue' import { computed, ref, provide, type Ref } from 'vue'
import DataSource from '@/views/knowledge-workflow/component/action/DataSource.vue' import DataSource from '@/views/knowledge-workflow/component/action/DataSource.vue'
import Result from '@/views/knowledge-workflow/component/action/Result.vue'
import applicationApi from '@/api/application/application' import applicationApi from '@/api/application/application'
import KnowledgeBase from '@/views/knowledge-workflow/component/action/KnowledgeBase.vue' import KnowledgeBase from '@/views/knowledge-workflow/component/action/KnowledgeBase.vue'
import { WorkflowType } from '@/enums/application' import { WorkflowType } from '@/enums/application'
@ -33,10 +40,12 @@ provide('upload', (file: any, loading?: Ref<boolean>) => {
const ak = { const ak = {
data_source: DataSource, data_source: DataSource,
knowledge_base: KnowledgeBase, knowledge_base: KnowledgeBase,
result: Result,
} }
const action_id = ref<string>()
const ActionRef = ref() const ActionRef = ref()
const form_data = ref<any>({}) const form_data = ref<any>({})
const active = ref<'data_source' | 'knowledge_base'>('data_source') const active = ref<'data_source' | 'knowledge_base' | 'result'>('data_source')
const props = defineProps<{ const props = defineProps<{
workflow: any workflow: any
knowledge_id: string knowledge_id: string
@ -62,7 +71,10 @@ const up = () => {
const upload = () => { const upload = () => {
ActionRef.value.validate().then(() => { ActionRef.value.validate().then(() => {
form_data.value[active.value] = ActionRef.value.get_data() form_data.value[active.value] = ActionRef.value.get_data()
KnowledgeApi.workflowAction(props.knowledge_id, form_data.value) KnowledgeApi.workflowAction(props.knowledge_id, form_data.value).then((ok) => {
action_id.value = ok.data.id
active.value = 'result'
})
}) })
} }
</script> </script>

View File

@ -159,6 +159,7 @@ function refreshFieldList(data: any, index: any) {
value: item.field, value: item.field,
})) }))
set(props.nodeModel.properties, 'user_input_field_list', cloneDeep(inputFieldList.value))
set(props.nodeModel.properties.config, 'fields', fields) set(props.nodeModel.properties.config, 'fields', fields)
onDragHandle() onDragHandle()
} }
@ -210,6 +211,7 @@ function onDragHandle() {
} }
onMounted(() => { onMounted(() => {
inputFieldList.value = []
if (props.nodeModel.properties.user_input_field_list) { if (props.nodeModel.properties.user_input_field_list) {
inputFieldList.value = cloneDeep(props.nodeModel.properties.user_input_field_list) inputFieldList.value = cloneDeep(props.nodeModel.properties.user_input_field_list)
} }