Merge branch 'pr@main@application_flow' of github.com:1Panel-dev/MaxKB into pr@main@application_flow

This commit is contained in:
shaohuzhang1 2024-06-24 18:42:16 +08:00
commit 96cfd07576
18 changed files with 167 additions and 46 deletions

View File

@ -0,0 +1,73 @@
<template>
<el-dialog title="执行详情" v-model="dialogVisible" destroy-on-close align-center @click.stop>
<el-scrollbar>
<div class="execution-details">
<template v-for="(item, index) in arraySort(detail, 'index')" :key="index">
<el-card class="mb-8" shadow="never" style="--el-card-padding: 12px 16px">
<div class="flex-between cursor" @click="current = index">
<div class="flex align-center">
<el-icon class="mr-8 arrow-icon" :class="current === index ? 'rotate-90' : ''"
><CaretRight
/></el-icon>
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8" :size="24" />
<h4>{{ item.name }}</h4>
</div>
<div class="flex align-center">
<span
class="mr-16 color-secondary"
v-if="item.type === WorkflowType.Question || item.type === WorkflowType.AiChat"
>{{ item?.message_tokens + item?.answer_tokens }} tokens</span
>
<span class="mr-16 color-secondary">{{ item?.run_time?.toFixed(2) }} s</span>
<el-icon class="success" :size="16"><CircleCheck /></el-icon>
</div>
</div>
<el-collapse-transition>
<div class="card-never border-r-4 mt-8" v-if="current === index">
<h5 class="p-8-12">参数输入</h5>
<div class="p-8-12 border-t-dashed lighter">如何快速开始</div>
</div>
</el-collapse-transition>
</el-card>
</template>
</div>
</el-scrollbar>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
import { cloneDeep } from 'lodash'
import { arraySort } from '@/utils/utils'
import { iconComponent } from '@/workflow/icons/utils'
import { WorkflowType } from '@/enums/workflow'
import { MdPreview } from 'md-editor-v3'
const dialogVisible = ref(false)
const detail = ref<any[]>([])
const current = ref<number | string>('')
watch(dialogVisible, (bool) => {
if (!bool) {
detail.value = []
}
})
const open = (data: any) => {
detail.value = cloneDeep(data)
dialogVisible.value = true
}
onBeforeUnmount(() => {
dialogVisible.value = false
})
defineExpose({ open })
</script>
<style lang="scss">
.execution-details {
max-height: calc(100vh - 260px);
.arrow-icon {
transition: 0.2s;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="flex align-center mt-16">
<div class="flex align-center mt-16" v-if="!isWorkFlow(props.type)">
<span class="mr-4 color-secondary">知识来源</span>
<el-divider direction="vertical" />
<el-button type="primary" class="mr-8" link @click="openParagraph(data)">
@ -7,7 +7,7 @@
引用分段 {{ data.paragraph_list?.length || 0 }}</el-button
>
</div>
<div class="mt-8 mb-12">
<div class="mt-8" v-if="!isWorkFlow(props.type)">
<el-space wrap>
<el-button
v-for="(dataset, index) in data.dataset_list"
@ -20,28 +20,51 @@
</el-space>
</div>
<div class="border-t color-secondary" style="padding-top: 12px">
<span class="mr-8"> 消耗 tokens: {{ data?.message_tokens + data?.answer_tokens }} </span>
<span> 耗时: {{ data?.run_time?.toFixed(2) }} s</span>
<div class="border-t color-secondary flex-between mt-12" style="padding-top: 12px">
<div>
<span class="mr-8"> 消耗 tokens: {{ data?.message_tokens + data?.answer_tokens }} </span>
<span> 耗时: {{ data?.run_time?.toFixed(2) }} s</span>
</div>
<el-button
v-if="isWorkFlow(props.type)"
type="primary"
link
@click="openExecutionDetail(data.execution_details)"
>
<el-icon class="mr-4"><Document /></el-icon>
执行详情</el-button
>
</div>
<!-- 知识库引用 dialog -->
<ParagraphSourceDialog ref="ParagraphSourceDialogRef" />
<!-- 执行详情 dialog -->
<ExecutionDetailDialog ref="ExecutionDetialDialogRef" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ParagraphSourceDialog from './ParagraphSourceDialog.vue'
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
import { isWorkFlow } from '@/utils/application'
const props = defineProps({
data: {
type: Object,
default: () => {}
},
type: {
type: String,
default: ''
}
})
const ParagraphSourceDialogRef = ref()
const ExecutionDetialDialogRef = ref()
function openParagraph(row: any, id?: string) {
ParagraphSourceDialogRef.value.open(row, id)
}
function openExecutionDetail(row: any) {
ExecutionDetialDialogRef.value.open(row)
}
</script>
<style lang="scss" scoped>
.source_dataset-button {

View File

@ -75,7 +75,7 @@
<MdRenderer :source="item.answer_text"></MdRenderer>
<!-- 知识来源 -->
<div v-if="showSource(item)">
<KnowledgeSource :data="item" />
<KnowledgeSource :data="item" :type="props.data.type" />
</div>
</el-card>
<div class="flex-between mt-8" v-if="log">
@ -153,6 +153,7 @@ import { ChatManagement, type chatType } from '@/api/type/application'
import { randomId } from '@/utils/utils'
import useStore from '@/stores'
import MdRenderer from '@/components/markdown-renderer/MdRenderer.vue'
import { isWorkFlow } from '@/utils/application'
import { MdPreview } from 'md-editor-v3'
import { debounce } from 'lodash'
defineOptions({ name: 'AiChat' })
@ -335,7 +336,7 @@ function getChartOpenId(chat?: any) {
}
})
} else {
if (obj.type === 'WORK_FLOW') {
if (isWorkFlow(obj.type)) {
const submitObj = {
work_flow: obj.work_flow
}

9
ui/src/enums/workflow.ts Normal file
View File

@ -0,0 +1,9 @@
export enum WorkflowType {
Base = 'base-node',
Start = 'start-node',
AiChat = 'ai-chat-node',
SearchDataset = 'search-dataset-node',
Question = 'question-node',
Condition = 'condition-node',
Reply = 'reply-node'
}

View File

@ -120,7 +120,7 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'
import { isAppIcon } from '@/utils/application'
import { isAppIcon, isWorkFlow } from '@/utils/application'
import useStore from '@/stores'
const { common, dataset, application } = useStore()
const route = useRoute()
@ -162,7 +162,7 @@ function changeMenu(id: string) {
} else if (isApplication.value) {
const type = list.value?.filter((v) => v.id === id)?.[0]?.type
if (
type === 'WORK_FLOW' &&
isWorkFlow(type) &&
(lastMatched.name === 'AppSetting' || lastMatched.name === 'AppHitTest')
) {
router.push({ path: `/application/${id}/${type}/overview` })

View File

@ -17,7 +17,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'
import { isWorkFlow } from '@/utils/application'
const props = defineProps<{
menu: RouteRecordRaw
activeMenu: any
@ -30,7 +30,7 @@ const {
} = route as any
function showMenu() {
if (type === 'WORK_FLOW') {
if (isWorkFlow(type)) {
return props.menu.name !== 'AppHitTest'
} else {
return true
@ -38,7 +38,7 @@ function showMenu() {
}
function clickHandle(item: any) {
if (type === 'WORK_FLOW' && item.name === 'AppSetting') {
if (isWorkFlow(type) && item.name === 'AppSetting') {
router.push({ path: `/application/${id}/workflow` })
}
}

View File

@ -288,6 +288,10 @@ h5 {
border-radius: 4px;
}
.border-t-dashed {
border-top: 1px dashed var(--el-border-color);
}
.cursor {
cursor: pointer;
}

View File

@ -4,3 +4,7 @@ export const defaultIcon = '/ui/favicon.ico'
export function isAppIcon(url: string | undefined) {
return url === defaultIcon ? '' : url
}
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'
}

View File

@ -62,7 +62,7 @@ export function relatedObject(list: any, val: any, attr: string) {
}
// 排序
export function arraySort(list: Array<string>, property: any, desc?: boolean) {
export function arraySort(list: Array<any>, property: any, desc?: boolean) {
return list.sort((a: any, b: any) => {
return desc ? b[property] - a[property] : a[property] - b[property]
})

View File

@ -29,7 +29,7 @@
<h5 class="title">基础组件</h5>
<template v-for="(item, index) in menuNodes" :key="index">
<div class="workflow-dropdown-item cursor flex p-8-12" @mousedown="onmousedown(item)">
<component :is="iconComponent(item.icon)" class="mr-8 mt-4" :size="32" />
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
<div class="pre-line">
<div class="lighter">{{ item.label }}</div>
<el-text type="info" size="small">{{ item.text }}</el-text>
@ -87,6 +87,7 @@ import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
import { WorkFlowInstanse } from '@/workflow/common/validate'
const { application } = useStore()
const route = useRoute()
@ -134,6 +135,7 @@ const clickShowDebug = () => {
const workflow = new WorkFlowInstanse(graphData)
try {
workflow.is_valid()
getDetail()
showDebug.value = true
} catch (e: any) {
MsgError(e.toString())
@ -242,7 +244,7 @@ onBeforeUnmount(() => {
}
.workflow-debug-container {
z-index: 10000;
z-index: 2000;
position: relative;
border-radius: 8px;
border: 1px solid #ffffff;

View File

@ -49,7 +49,7 @@
<el-card
shadow="never"
class="mb-16"
:class="applicationForm.type === 'WORK_FLOW' ? 'active' : ''"
:class="isWorkFlow(applicationForm.type) ? 'active' : ''"
>
<el-radio value="WORK_FLOW" size="large">
<p class="mb-4">高级编排</p>
@ -80,6 +80,7 @@ import type { ApplicationFormType } from '@/api/type/application'
import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application'
import { MsgSuccess } from '@/utils/message'
import { isWorkFlow } from '@/utils/application'
import { t } from '@/locales'
const router = useRouter()
@ -174,7 +175,7 @@ const submitHandle = async (formEl: FormInstance | undefined) => {
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
console.log(res)
MsgSuccess(t('views.application.applicationForm.buttons.createSuccess'))
if (applicationForm.value.type === 'WORK_FLOW') {
if (isWorkFlow(applicationForm.value.type)) {
router.push({ path: `/application/${res.data.id}/workflow` })
} else {
router.push({ path: `/application/${res.data.id}/${res.data.type}/setting` })

View File

@ -115,6 +115,7 @@ import CreateApplicationDialog from './component/CreateApplicationDialog.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { isAppIcon } from '@/utils/application'
import { useRouter } from 'vue-router'
import { isWorkFlow } from '@/utils/application'
import useStore from '@/stores'
import { t } from '@/locales'
const { application } = useStore()
@ -133,10 +134,6 @@ const paginationConfig = reactive({
const searchValue = ref('')
function isWorkFlow(type: string) {
return type === 'WORK_FLOW'
}
function settingApplication(row: any) {
if (isWorkFlow(row.type)) {
router.push({ path: `/application/${row.id}/workflow` })

View File

@ -55,6 +55,7 @@
import { ref } from 'vue'
import { iconComponent } from '../icons/utils'
import { copyClick } from '@/utils/clipboard'
import { WorkflowType } from '@/enums/workflow'
const height = ref<{
stepContainerHeight: number
inputContainerHeight: number
@ -79,7 +80,7 @@ const props = defineProps<{
}>()
function showOperate(type: string) {
return type !== 'base-node' && type !== 'start-node'
return type !== WorkflowType.Base && type !== WorkflowType.Start
}
</script>
<style lang="scss" scoped>

View File

@ -8,6 +8,7 @@ import { h as lh } from '@logicflow/core'
import { createApp, h } from 'vue'
import directives from '@/directives'
import i18n from '@/locales'
import { WorkflowType } from '@/enums/workflow'
class AppNode extends HtmlResize.view {
isMounted
@ -171,8 +172,8 @@ class AppNodeModel extends HtmlResize.model {
const { id, x, y, width } = this
const anchors: any = []
if (this.type !== 'base-node') {
if (this.type !== 'start-node') {
if (this.type !== WorkflowType.Base) {
if (this.type !== WorkflowType.Start) {
anchors.push({
x: x - width / 2 + 10,
y: y,

View File

@ -1,11 +1,13 @@
import { WorkflowType } from '@/enums/workflow'
/**
*
* type nodes
*/
export const baseNodes = [
{
id: 'base-node',
type: 'base-node',
id: WorkflowType.Base,
type: WorkflowType.Base,
x: 200,
y: 270,
properties: {
@ -20,8 +22,8 @@ export const baseNodes = [
}
},
{
id: 'start-node',
type: 'start-node',
id: WorkflowType.Start,
type: WorkflowType.Start,
x: 180,
y: 720,
properties: {
@ -32,7 +34,7 @@ export const baseNodes = [
label: '用户问题',
value: 'question',
globeLabel: '{{开始.question}}',
globeValue: "{{content['start-node'].question}}"
globeValue: `{{content['${WorkflowType.Start}'].question}}`
}
]
}
@ -41,10 +43,9 @@ export const baseNodes = [
export const menuNodes = [
{
type: 'ai-chat-node',
type: WorkflowType.AiChat,
text: '与 AI 大模型进行对话',
label: 'AI 对话',
icon: 'ai-chat-node-icon',
properties: {
stepName: 'AI 对话',
fields: [
@ -56,10 +57,9 @@ export const menuNodes = [
}
},
{
type: 'search-dataset-node',
type: WorkflowType.SearchDataset,
text: '关联知识库,查找与问题相关的分段',
label: '知识库检索',
icon: 'search-dataset-node-icon',
properties: {
stepName: '知识库检索',
fields: [
@ -77,10 +77,9 @@ export const menuNodes = [
}
},
{
type: 'question-node',
type: WorkflowType.Question,
text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段',
label: '问题优化',
icon: 'question-node-icon',
properties: {
stepName: '问题优化',
fields: [
@ -92,20 +91,18 @@ export const menuNodes = [
}
},
{
type: 'condition-node',
type: WorkflowType.Condition,
text: '根据不同条件执行不同的节点',
label: '判断器',
icon: 'condition-node-icon',
properties: {
width: 600,
stepName: '判断器'
}
},
{
type: 'reply-node',
type: WorkflowType.Reply,
text: '指定回复内容,引用变量会转换为字符串进行输出',
label: '指定回复',
icon: 'reply-node-icon',
properties: {
stepName: '指定回复'
}
@ -125,3 +122,7 @@ export const compareList = [
{ value: 'len_lt', label: '长度小于' },
{ value: 'lt', label: '小于' }
]
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'
}

View File

@ -1,4 +1,6 @@
const end_nodes = ['ai-chat-node', 'reply-node']
import { WorkflowType } from '@/enums/workflow'
const end_nodes = [WorkflowType.AiChat, WorkflowType.Reply]
export class WorkFlowInstanse {
nodes
edges
@ -10,7 +12,7 @@ export class WorkFlowInstanse {
*
*/
private is_valid_start_node() {
const start_node_list = this.nodes.filter((item) => item.id === 'start-node')
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
if (start_node_list.length == 0) {
throw '开始节点必填'
} else if (start_node_list.length > 1) {
@ -21,7 +23,7 @@ export class WorkFlowInstanse {
*
*/
private is_valid_base_node() {
const start_node_list = this.nodes.filter((item) => item.id === 'base-node')
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
if (start_node_list.length == 0) {
throw '基本信息节点必填'
} else if (start_node_list.length > 1) {
@ -43,7 +45,7 @@ export class WorkFlowInstanse {
* @returns
*/
get_start_node() {
const start_node_list = this.nodes.filter((item) => item.id === 'start-node')
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
return start_node_list[0]
}
/**
@ -51,7 +53,7 @@ export class WorkFlowInstanse {
* @returns
*/
get_base_node() {
const base_node_list = this.nodes.filter((item) => item.id === 'base-node')
const base_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
return base_node_list[0]
}
@ -86,7 +88,7 @@ export class WorkFlowInstanse {
}
private is_valid_nodes() {
for (const node of this.nodes) {
if (node.type !== 'base-node' && node.type !== 'start-node') {
if (node.type !== WorkflowType.Base && node.type !== WorkflowType.Start) {
console.log(node.properties.stepName)
if (!this.edges.some((edge) => edge.targetNodeId === node.id)) {
throw `未在流程中的节点:${node.properties.stepName}`
@ -99,7 +101,7 @@ export class WorkFlowInstanse {
* @param node
*/
private is_valid_node(node: any) {
if (node.type === 'condition-node') {
if (node.type === WorkflowType.Condition) {
const branch_list = node.properties.node_data.branch
for (const branch of branch_list) {
const source_anchor_id = `${node.id}_${branch.id}_right`

View File

@ -8,6 +8,7 @@
@mousedown.stop
@keydown.stop
@click.stop
@wheel.stop
:model="chat_data"
label-position="top"
require-asterisk-position="right"

View File

@ -8,6 +8,7 @@
@mousedown.stop
@keydown.stop
@click.stop
@wheel.stop
:model="form_data"
label-position="top"
require-asterisk-position="right"