mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-27 20:42:52 +00:00
feat: 工作编排
This commit is contained in:
parent
694ddb0809
commit
e80f58dd7b
|
|
@ -533,24 +533,18 @@ export const iconMap: any = {
|
|||
'svg',
|
||||
{
|
||||
style: { height: '100%', width: '100%' },
|
||||
viewBox: '0 0 16 16',
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M7.99984 3.66667C8.46007 3.66667 8.83317 4.03977 8.83317 4.5C8.83317 4.96023 8.46007 5.33333 7.99984 5.33333C7.5396 5.33333 7.1665 4.96023 7.1665 4.5C7.1665 4.03977 7.5396 3.66667 7.99984 3.66667Z',
|
||||
d: 'M512 234.666667A53.333333 53.333333 0 1 1 512 341.333333a53.333333 53.333333 0 0 1 0-106.666666zM522.666667 384h-64a21.333333 21.333333 0 0 0-21.333334 21.333333v42.666667a21.333333 21.333333 0 0 0 21.333334 21.333333h21.333333v213.333334H426.666667a21.333333 21.333333 0 0 0-21.333334 21.333333v42.666667a21.333333 21.333333 0 0 0 21.333334 21.333333h192a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333h-53.333334v-256a42.666667 42.666667 0 0 0-42.666666-42.666667z',
|
||||
fill: 'currentColor'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M8.1665 6H7.33317C7.05703 6 6.83317 6.22386 6.83317 6.5V6.83333C6.83317 7.10948 7.05703 7.33333 7.33317 7.33333H7.49984V10.6667H6.83317C6.55703 10.6667 6.33317 10.8905 6.33317 11.1667V11.5C6.33317 11.7761 6.55703 12 6.83317 12H9.49984C9.77598 12 9.99984 11.7761 9.99984 11.5V11.1667C9.99984 10.8905 9.77598 10.6667 9.49984 10.6667H8.83317V6.66667C8.83317 6.29848 8.53469 6 8.1665 6Z',
|
||||
d: 'M512 981.333333C252.8 981.333333 42.666667 771.2 42.666667 512S252.8 42.666667 512 42.666667s469.333333 210.133333 469.333333 469.333333-210.133333 469.333333-469.333333 469.333333z m0-85.333333a384 384 0 1 0 0-768 384 384 0 0 0 0 768z',
|
||||
fill: 'currentColor'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M7.99984 15.3332C3.94984 15.3332 0.666504 12.0498 0.666504 7.99984C0.666504 3.94984 3.94984 0.666504 7.99984 0.666504C12.0498 0.666504 15.3332 3.94984 15.3332 7.99984C15.3332 12.0498 12.0498 15.3332 7.99984 15.3332ZM7.99984 13.9998C11.3135 13.9998 13.9998 11.3135 13.9998 7.99984C13.9998 4.68617 11.3135 1.99984 7.99984 1.99984C4.68617 1.99984 1.99984 4.68617 1.99984 7.99984C1.99984 11.3135 4.68617 13.9998 7.99984 13.9998Z',
|
||||
fill: 'currentColor',
|
||||
fillRule: 'evenodd',
|
||||
clipRule: 'evenodd'
|
||||
})
|
||||
]
|
||||
)
|
||||
|
|
@ -935,5 +929,30 @@ export const iconMap: any = {
|
|||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
'app-fitview': {
|
||||
iconReader: () => {
|
||||
return h('i', [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
style: { height: '100%', width: '100%' },
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M128 85.333333h192a21.333333 21.333333 0 0 1 21.333333 21.333334v42.666666a21.333333 21.333333 0 0 1-21.333333 21.333334H170.666667v149.333333a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333333V128a42.666667 42.666667 0 0 1 42.666667-42.666667z m768 853.333334h-192a21.333333 21.333333 0 0 1-21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 1 21.333333-21.333334H853.333333v-149.333333a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666a21.333333 21.333333 0 0 1 21.333334 21.333333V896a42.666667 42.666667 0 0 1-42.666667 42.666667zM85.333333 896v-192a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666a21.333333 21.333333 0 0 1 21.333334 21.333333V853.333333h149.333333a21.333333 21.333333 0 0 1 21.333333 21.333334v42.666666a21.333333 21.333333 0 0 1-21.333333 21.333334H128a42.666667 42.666667 0 0 1-42.666667-42.666667zM938.666667 128v192a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333333V170.666667h-149.333333a21.333333 21.333333 0 0 1-21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 1 21.333333-21.333334H896a42.666667 42.666667 0 0 1 42.666667 42.666667z',
|
||||
fill: 'currentColor'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M512 512m-170.666667 0a170.666667 170.666667 0 1 0 341.333334 0 170.666667 170.666667 0 1 0-341.333334 0Z',
|
||||
fill: 'currentColor'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ h5 {
|
|||
.w-500 {
|
||||
width: 500px;
|
||||
}
|
||||
.max-w-200 {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: calc(var(--app-base-px) - 4px);
|
||||
|
|
@ -567,6 +570,13 @@ h5 {
|
|||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
.model-icon {
|
||||
width: 20px;
|
||||
}
|
||||
.check-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 段落card
|
||||
|
|
|
|||
|
|
@ -539,13 +539,7 @@ onMounted(() => {
|
|||
height: calc(var(--app-main-height) - 150px);
|
||||
}
|
||||
}
|
||||
.model-icon {
|
||||
width: 20px;
|
||||
}
|
||||
.check-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.prologue-md-editor {
|
||||
height: 150px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<el-breadcrumb separator=">">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
title="选择供应商"
|
||||
append-to-body
|
||||
>
|
||||
<el-row :gutter="12" v-loading="loading">
|
||||
<el-col :span="12" class="mb-16" v-for="(data, index) in list_provider" :key="index">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-button-group>
|
||||
<el-button size="small" @click="zoomIn">放大</el-button>
|
||||
<el-button size="small" @click="zoomOut">缩小</el-button>
|
||||
<el-button size="small" @click="fitView">适应</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
<el-card shadow="always" style="--el-card-padding: 8px 12px; --el-card-border-radius: 8px">
|
||||
<el-button link @click="zoomOut">
|
||||
<el-icon :size="16" title="缩小"><ZoomOut /></el-icon>
|
||||
</el-button>
|
||||
<el-button link @click="zoomIn">
|
||||
<el-icon :size="16" title="放大"><ZoomIn /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-divider direction="vertical" />
|
||||
<el-button link @click="zoomIn">
|
||||
<AppIcon iconName="app-fitview" title="适应"></AppIcon>
|
||||
</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -22,7 +28,7 @@ function zoomOut() {
|
|||
function fitView() {
|
||||
props.lf?.resetZoom()
|
||||
props.lf?.resetTranslate()
|
||||
// props.lf?.fitView()
|
||||
props.lf?.fitView()
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export const baseNodes = [
|
|||
node_data: {
|
||||
name: '',
|
||||
desc: '',
|
||||
prologue: ''
|
||||
prologue:
|
||||
'您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。\n- MaxKB 主要功能有什么?\n- MaxKB 支持哪些大语言模型?\n- MaxKB 支持哪些文档类型?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -94,6 +95,32 @@ export const menuNodes = [
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'question-node',
|
||||
text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段',
|
||||
label: '问题优化',
|
||||
icon: 'question-node-icon',
|
||||
properties: {
|
||||
height: '',
|
||||
stepName: '问题优化',
|
||||
input: [
|
||||
{
|
||||
key: ''
|
||||
}
|
||||
],
|
||||
output: [
|
||||
{
|
||||
key: ''
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
label: '用户问题',
|
||||
value: 'question'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'condition-node',
|
||||
text: '根据不同条件执行不同的节点',
|
||||
|
|
@ -113,5 +140,25 @@ export const menuNodes = [
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'reply-node',
|
||||
text: '指定回复内容,引用变量会转换为字符串进行输出',
|
||||
label: '指定回复',
|
||||
icon: 'reply-node-icon',
|
||||
properties: {
|
||||
height: '',
|
||||
stepName: '指定回复',
|
||||
input: [
|
||||
{
|
||||
key: ''
|
||||
}
|
||||
],
|
||||
output: [
|
||||
{
|
||||
key: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34C724">
|
||||
<img src="@/assets/icon_setting.svg" style="width: 75%" alt="" />
|
||||
<img src="@/assets/icon_setting.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #FF8800">
|
||||
<img src="@/assets/icon_reply.svg" style="width: 75%" alt="" />
|
||||
<img src="@/assets/icon_reply.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
|
|||
|
|
@ -101,8 +101,7 @@ const graphData = {
|
|||
height: '',
|
||||
stepName: 'AI 对话',
|
||||
input: [{ key: '输入' }],
|
||||
output: [{ key: '输出' }],
|
||||
node_data: { model: '', name: '' }
|
||||
output: [{ key: '输出' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -114,8 +113,7 @@ const graphData = {
|
|||
height: '',
|
||||
stepName: 'AI 对话',
|
||||
input: [{ key: '输入' }],
|
||||
output: [{ key: '输出' }],
|
||||
node_data: { model: '', name: '' }
|
||||
output: [{ key: '输出' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -127,8 +125,7 @@ const graphData = {
|
|||
height: '',
|
||||
stepName: 'AI 对话',
|
||||
input: [{ key: '输入' }],
|
||||
output: [{ key: '输出' }],
|
||||
node_data: { model: '', name: '' }
|
||||
output: [{ key: '输出' }]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -12,62 +12,208 @@
|
|||
ref="aiChatNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
label="模型"
|
||||
prop="model"
|
||||
label="AI 模型"
|
||||
prop="model_id"
|
||||
:rules="{
|
||||
message: '模型不能为空',
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
required: true,
|
||||
message: 'AI 模型',
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<el-select v-model="chat_data.model" placeholder="请选择模型">
|
||||
<el-option label="Zone one" value="shanghai" />
|
||||
<el-option label="Zone two" value="beijing" />
|
||||
<el-select
|
||||
v-model="chat_data.model_id"
|
||||
placeholder="请选择 AI 模型"
|
||||
class="w-full"
|
||||
popper-class="select-model"
|
||||
:clearable="true"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="(value, label) in modelOptions"
|
||||
:key="value"
|
||||
:label="relatedObject(providerOptions, label, 'provider')?.name"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
class="flex-between"
|
||||
>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
|
||||
class="model-icon mr-8"
|
||||
></span>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<el-icon class="check-icon" v-if="item.id === chat_data.model_id"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</el-option>
|
||||
<!-- 不可用 -->
|
||||
<el-option
|
||||
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
class="flex-between"
|
||||
disabled
|
||||
>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
|
||||
class="model-icon mr-8"
|
||||
></span>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="danger">(不可用)</span>
|
||||
</div>
|
||||
<el-icon class="check-icon" v-if="item.id === chat_data.model_id"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<template #footer>
|
||||
<div class="w-full text-left cursor" @click="openCreateModel()">
|
||||
<el-button type="primary" link>
|
||||
<el-icon class="mr-4"><Plus /></el-icon>
|
||||
添加模型
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="提示词"
|
||||
:rules="{
|
||||
message: '提示词不能为空',
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
prop="name"
|
||||
>
|
||||
<el-input v-model="chat_data.name" />
|
||||
<el-form-item label="角色设定">
|
||||
<el-input v-model="chat_data.role" placeholder="角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词" prop="model_setting.prompt">
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="flex-between mr-4">
|
||||
<span>提示词<span class="danger">*</span></span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content
|
||||
>通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头。可以使用变量:{data}
|
||||
是携带知识库中已知信息;{question} 是用户提出的问题。</template
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="chat_data.model_setting.prompt"
|
||||
:rows="6"
|
||||
type="textarea"
|
||||
maxlength="2048"
|
||||
:placeholder="defaultPrompt"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="历史聊天记录">
|
||||
<el-input-number
|
||||
v-model="chat_data.record"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<h5 class="title-decoration-1 mb-8 mt-8">参数输出</h5>
|
||||
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter">AI 回答内容 {content}</div>
|
||||
<!-- 添加模版 -->
|
||||
<CreateModelDialog
|
||||
ref="createModelRef"
|
||||
@submit="getModel"
|
||||
@change="openCreateModel($event)"
|
||||
></CreateModelDialog>
|
||||
<SelectProviderDialog ref="selectProviderRef" @change="openCreateModel($event)" />
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { set } from 'lodash'
|
||||
import { set, groupBy } from 'lodash'
|
||||
import { app } from '@/main'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import CreateModelDialog from '@/views/template/component/CreateModelDialog.vue'
|
||||
import SelectProviderDialog from '@/views/template/component/SelectProviderDialog.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import useStore from '@/stores'
|
||||
import { relatedObject } from '@/utils/utils'
|
||||
import type { Provider } from '@/api/type/model'
|
||||
const { model } = useStore()
|
||||
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
// @ts-ignore
|
||||
const defaultPrompt =
|
||||
'已知信息:\n{data}\n回答要求:\n- 请使用简洁且专业的语言来回答用户的问题。\n- 如果你不知道答案,请回答“没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作”。\n- 避免提及你是从已知信息中获得的知识。\n- 请保证答案与已知信息中描述的一致。\n- 请使用 Markdown 语法优化答案的格式。\n- 已知信息中的图片、链接地址和脚本语言请直接返回。\n- 请使用与问题相同的语言来回答。\n问题:\n{question}'
|
||||
const form = {
|
||||
model_id: '',
|
||||
role: '',
|
||||
model_setting: {
|
||||
prompt: defaultPrompt
|
||||
},
|
||||
record: 1
|
||||
}
|
||||
|
||||
const chat_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
props.nodeModel.properties.node_data = { model: '', name: '' }
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
props.nodeModel.properties.node_data = value
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const aiChatNodeFormRef = ref<FormInstance>()
|
||||
const createModelRef = ref<InstanceType<typeof CreateModelDialog>>()
|
||||
const selectProviderRef = ref<InstanceType<typeof SelectProviderDialog>>()
|
||||
|
||||
const modelOptions = ref<any>(null)
|
||||
const providerOptions = ref<Array<Provider>>([])
|
||||
|
||||
const validate = () => {
|
||||
aiChatNodeFormRef.value?.validate()
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
if (id) {
|
||||
applicationApi.getApplicationModel(id).then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
} else {
|
||||
model.asyncGetModel().then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getProvider() {
|
||||
model.asyncGetProvider().then((res: any) => {
|
||||
providerOptions.value = res?.data
|
||||
})
|
||||
}
|
||||
|
||||
const openCreateModel = (provider?: Provider) => {
|
||||
if (provider && provider.provider) {
|
||||
createModelRef.value?.open(provider)
|
||||
} else {
|
||||
selectProviderRef.value?.open()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getProvider()
|
||||
getModel()
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -55,17 +55,18 @@ import { ref, computed, onMounted } from 'vue'
|
|||
import { MdEditor } from 'md-editor-v3'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const form = {
|
||||
name: '',
|
||||
desc: '',
|
||||
prologue:
|
||||
'您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。\n- MaxKB 主要功能有什么?\n- MaxKB 支持哪些大语言模型?\n- MaxKB 支持哪些文档类型?'
|
||||
}
|
||||
const chat_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
props.nodeModel.properties.node_data = {
|
||||
name: '',
|
||||
desc: '',
|
||||
prologue:
|
||||
'您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。\n- MaxKB 主要功能有什么?\n- MaxKB 支持哪些大语言模型?\n- MaxKB 支持哪些文档类型?'
|
||||
}
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import ChatNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node.ts'
|
||||
class ChatNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, ChatNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'ai-chat-node',
|
||||
model: AppNodeModel,
|
||||
view: ChatNode
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">节点设置</h5>
|
||||
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
class="mb-24"
|
||||
label-width="auto"
|
||||
ref="aiChatNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
label="AI 模型"
|
||||
prop="model_id"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: 'AI 模型',
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<el-select
|
||||
v-model="chat_data.model_id"
|
||||
placeholder="请选择 AI 模型"
|
||||
class="w-full"
|
||||
popper-class="select-model"
|
||||
:clearable="true"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="(value, label) in modelOptions"
|
||||
:key="value"
|
||||
:label="relatedObject(providerOptions, label, 'provider')?.name"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
class="flex-between"
|
||||
>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
|
||||
class="model-icon mr-8"
|
||||
></span>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<el-icon class="check-icon" v-if="item.id === chat_data.model_id"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</el-option>
|
||||
<!-- 不可用 -->
|
||||
<el-option
|
||||
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
class="flex-between"
|
||||
disabled
|
||||
>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
|
||||
class="model-icon mr-8"
|
||||
></span>
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="danger">(不可用)</span>
|
||||
</div>
|
||||
<el-icon class="check-icon" v-if="item.id === chat_data.model_id"
|
||||
><Check
|
||||
/></el-icon>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
<template #footer>
|
||||
<div class="w-full text-left cursor" @click="openCreateModel()">
|
||||
<el-button type="primary" link>
|
||||
<el-icon class="mr-4"><Plus /></el-icon>
|
||||
添加模型
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色设定">
|
||||
<el-input v-model="chat_data.role" placeholder="角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词" prop="model_setting.prompt">
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="flex-between mr-4">
|
||||
<span>提示词<span class="danger">*</span></span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content
|
||||
>通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头。可以使用变量:{data}
|
||||
是携带知识库中已知信息;{question} 是用户提出的问题。</template
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="chat_data.model_setting.prompt"
|
||||
:rows="6"
|
||||
type="textarea"
|
||||
maxlength="2048"
|
||||
:placeholder="defaultPrompt"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="历史聊天记录">
|
||||
<el-input-number
|
||||
v-model="chat_data.record"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<h5 class="title-decoration-1 mb-8 mt-8">参数输出</h5>
|
||||
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter">用户问题 {question}</div>
|
||||
<!-- 添加模版 -->
|
||||
<CreateModelDialog
|
||||
ref="createModelRef"
|
||||
@submit="getModel"
|
||||
@change="openCreateModel($event)"
|
||||
></CreateModelDialog>
|
||||
<SelectProviderDialog ref="selectProviderRef" @change="openCreateModel($event)" />
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { set, groupBy } from 'lodash'
|
||||
import { app } from '@/main'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import CreateModelDialog from '@/views/template/component/CreateModelDialog.vue'
|
||||
import SelectProviderDialog from '@/views/template/component/SelectProviderDialog.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import useStore from '@/stores'
|
||||
import { relatedObject } from '@/utils/utils'
|
||||
import type { Provider } from '@/api/type/model'
|
||||
const { model } = useStore()
|
||||
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
// @ts-ignore
|
||||
const defaultPrompt =
|
||||
'已知信息:\n{data}\n回答要求:\n- 请使用简洁且专业的语言来回答用户的问题。\n- 如果你不知道答案,请回答“没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作”。\n- 避免提及你是从已知信息中获得的知识。\n- 请保证答案与已知信息中描述的一致。\n- 请使用 Markdown 语法优化答案的格式。\n- 已知信息中的图片、链接地址和脚本语言请直接返回。\n- 请使用与问题相同的语言来回答。\n问题:\n{question}'
|
||||
const form = {
|
||||
model_id: '',
|
||||
role: '',
|
||||
model_setting: {
|
||||
prompt: defaultPrompt
|
||||
},
|
||||
record: 1
|
||||
}
|
||||
|
||||
const chat_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)
|
||||
}
|
||||
})
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const aiChatNodeFormRef = ref<FormInstance>()
|
||||
const createModelRef = ref<InstanceType<typeof CreateModelDialog>>()
|
||||
const selectProviderRef = ref<InstanceType<typeof SelectProviderDialog>>()
|
||||
|
||||
const modelOptions = ref<any>(null)
|
||||
const providerOptions = ref<Array<Provider>>([])
|
||||
|
||||
const validate = () => {
|
||||
aiChatNodeFormRef.value?.validate()
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
if (id) {
|
||||
applicationApi.getApplicationModel(id).then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
} else {
|
||||
model.asyncGetModel().then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getProvider() {
|
||||
model.asyncGetProvider().then((res: any) => {
|
||||
providerOptions.value = res?.data
|
||||
})
|
||||
}
|
||||
|
||||
const openCreateModel = (provider?: Provider) => {
|
||||
if (provider && provider.provider) {
|
||||
createModelRef.value?.open(provider)
|
||||
} else {
|
||||
selectProviderRef.value?.open()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getProvider()
|
||||
getModel()
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -134,7 +134,7 @@ const form_data = computed({
|
|||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form.value)
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue