Skip to content

Commit ac8c4e2

Browse files
committed
feat: question node
1 parent e33a1fb commit ac8c4e2

6 files changed

Lines changed: 231 additions & 2 deletions

File tree

‎apps/application/workflow/nodes/question_node/question_node.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ def execute(self):
117117
message_list = [SystemMessage(system), *history_message, question]
118118
else:
119119
message_list = [*history_message, question]
120-
self.write_context("message_list", message_list)
120+
self.write_context(
121+
"message_list",
122+
[{"content": m.content, "role": m.type} for m in message_list],
123+
)
121124

122125
response = chat_model.stream(message_list)
123126
answer = ""

‎ui/src/workflow-canvas/component/NodeMenu.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const workflowComponentGroups = [
2929
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: 'AI 对话', value: WorkflowNodeType.AiChat },
3030
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '意图识别', value: WorkflowNodeType.IntentNode },
3131
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '文本转语音', value: WorkflowNodeType.TextToSpeechNode },
32+
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '问题优化', value: WorkflowNodeType.Question},
3233
],
3334
},
3435
{
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<el-avatar class="avatar-gradient" shape="square">
3+
<img src="@/assets/workflow/icon_ai_chat.svg" style="width: 75%" alt="" />
4+
</el-avatar>
5+
</template>
6+
<script setup lang="ts"></script>

‎ui/src/workflow-canvas/nodes/intent-node/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface IntentNodeData {
2222
}
2323

2424
/** 节点顶部到第一个分类行锚点的纵向偏移(近似)。 */
25-
const BRANCH_ANCHOR_TOP_OFFSET = 422
25+
const BRANCH_ANCHOR_TOP_OFFSET = 400
2626
/** 相邻分类行锚点的纵向间距(近似行高)。 */
2727
const BRANCH_ANCHOR_GAP = 40
2828

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import QuestionNodeVue from './index.vue'
2+
import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node'
3+
import { WorkflowNodeType } from '@/workflow-canvas/types.ts'
4+
5+
class QuestionNodeView extends WorkflowNodeView {
6+
constructor(props: ConstructorParameters<typeof WorkflowNodeView>[0]) {
7+
super(props, QuestionNodeVue)
8+
}
9+
}
10+
11+
export default {
12+
type: WorkflowNodeType.Question,
13+
model: WorkflowNodeModel,
14+
view: QuestionNodeView,
15+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<script setup lang="ts">
2+
import { computed, inject, onMounted, ref, useTemplateRef } from 'vue'
3+
import { QuestionFilled } from '@element-plus/icons-vue'
4+
import { set } from 'lodash'
5+
import type { FormInstance } from 'element-plus'
6+
import ModelSelect from '@/components/business/model-select/index.vue'
7+
import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue'
8+
import NodeContainer from '@/workflow-canvas/core/NodeContainer.vue'
9+
import { useWorkflowStore } from '@/workflow-canvas/store'
10+
import type { BaseNodeModel } from '@logicflow/core'
11+
import type { ModelItem, ModelProviderItem } from '@/api/types'
12+
13+
defineOptions({ name: 'WorkflowQuestionNode' })
14+
const getModel = inject('getModel') as () => BaseNodeModel
15+
const apiType = (inject('apiType') as string) || 'workspace'
16+
const model = getModel()
17+
18+
interface QuestionNodeForm {
19+
model_id: string
20+
model_id_reference: string[]
21+
model_id_type: 'custom' | 'reference'
22+
system: string
23+
prompt: string
24+
dialogue_type: 'NODE' | 'WORKFLOW'
25+
dialogue_number: number
26+
model_params_setting: Record<string, unknown>
27+
}
28+
29+
const formRef = useTemplateRef<FormInstance>('formRef')
30+
const modelCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('modelCascaderRef')
31+
32+
const store = useWorkflowStore(apiType)
33+
const modelList = ref<Array<ModelItem>>([])
34+
const providerOptions = ref<Array<ModelProviderItem>>([])
35+
36+
const formData = computed<QuestionNodeForm>({
37+
get: () => {
38+
if (!model.properties.node_data) {
39+
set(model.properties, 'node_data', {
40+
model_id: '',
41+
model_id_type: 'custom',
42+
model_id_reference: [],
43+
system:
44+
'# 角色\n你是一位问题优化大师,擅长根据上下文精准揣测用户意图,并对用户提出的问题进行优化。\n\n' +
45+
'## 技能\n### 技能 1: 优化问题\n2. 接收用户输入的问题。\n3. 依据上下文仔细分析问题含义。\n4. 输出优化后的问题。\n\n' +
46+
'## 限制:\n - 仅返回优化后的问题,不进行额外解释或说明。\n - 确保优化后的问题准确反映原始问题意图,不得改变原意。',
47+
prompt: '{{开始.question}}',
48+
dialogue_type: 'WORKFLOW',
49+
dialogue_number: 1,
50+
model_params_setting: {},
51+
})
52+
}
53+
const data = model.properties.node_data as QuestionNodeForm
54+
if (data.model_id_type === undefined) set(data, 'model_id_type', 'custom')
55+
if (!Array.isArray(data.model_id_reference)) set(data, 'model_id_reference', [])
56+
if (data.system === undefined) set(data, 'system', '')
57+
if (data.prompt === undefined) set(data, 'prompt', '{{开始.question}}')
58+
if (data.dialogue_type === undefined) set(data, 'dialogue_type', 'WORKFLOW')
59+
if (data.dialogue_number === undefined || data.dialogue_number === null) {
60+
set(data, 'dialogue_number', 1)
61+
}
62+
if (!data.model_params_setting) set(data, 'model_params_setting', {})
63+
return data
64+
},
65+
set: (value) => (model.properties.node_data = value),
66+
})
67+
68+
function validate() {
69+
return Promise.all([
70+
formData.value.model_id_type === 'reference'
71+
? modelCascaderRef.value?.validate()
72+
: Promise.resolve(),
73+
formRef.value?.validate(),
74+
]).catch((error) => Promise.reject({ node: model, errMessage: error }))
75+
}
76+
77+
onMounted(() => {
78+
set(model, 'validate', validate)
79+
store.getModelList({ model_type: 'LLM' }).then((data) => {
80+
modelList.value = data
81+
})
82+
store.getProviderList().then((data) => {
83+
providerOptions.value = data
84+
})
85+
})
86+
</script>
87+
88+
<template>
89+
<NodeContainer :node-model="model">
90+
<h6 class="mb-3">节点设置</h6>
91+
<el-form
92+
ref="formRef"
93+
:model="formData"
94+
label-position="top"
95+
require-asterisk-position="right"
96+
@submit.prevent
97+
>
98+
<el-form-item
99+
:prop="formData.model_id_type === 'reference' ? 'model_id_reference' : 'model_id'"
100+
:rules="{ required: true, message: '请选择或填写 AI 模型', trigger: 'change' }"
101+
>
102+
<template #label>
103+
<div class="flex-between gap-3 w-full">
104+
<span>AI 模型</span>
105+
<el-select
106+
v-model="formData.model_id_type"
107+
:teleported="false"
108+
class="w-30!"
109+
size="small"
110+
@change="formData.model_id_reference = []"
111+
>
112+
<el-option label="引用变量" value="reference" />
113+
<el-option label="自定义" value="custom" />
114+
</el-select>
115+
</div>
116+
</template>
117+
<NodeCascader
118+
v-if="formData.model_id_type === 'reference'"
119+
ref="modelCascaderRef"
120+
v-model="formData.model_id_reference"
121+
:node-model="model"
122+
class="w-full"
123+
placeholder="请选择变量"
124+
/>
125+
<ModelSelect
126+
v-else
127+
placeholder="请输入 AI 模型 ID"
128+
:options="modelList"
129+
:provider-options="providerOptions"
130+
v-model="formData.model_id"
131+
></ModelSelect>
132+
</el-form-item>
133+
134+
<el-form-item>
135+
<template #label>
136+
<div class="flex items-center gap-1">
137+
<span>系统提示词</span>
138+
<el-tooltip effect="dark" placement="right" content="设定模型扮演的角色或遵循的指令">
139+
<MkIcon :icon="QuestionFilled" class="cursor-help text-N600" />
140+
</el-tooltip>
141+
</div>
142+
</template>
143+
<el-input
144+
v-model="formData.system"
145+
:rows="4"
146+
:placeholder="`系统提示词,可以引用变量,如 {{开始.question}}`"
147+
type="textarea"
148+
/>
149+
</el-form-item>
150+
151+
<el-form-item
152+
label="用户提示词"
153+
prop="prompt"
154+
:rules="{ required: true, message: '请输入用户提示词', trigger: 'blur' }"
155+
>
156+
<template #label>
157+
<div class="flex items-center gap-1">
158+
<span>用户提示词</span>
159+
<el-tooltip effect="dark" placement="right" content="用户向模型提出的问题或输入的指令">
160+
<MkIcon :icon="QuestionFilled" class="cursor-help text-N600" />
161+
</el-tooltip>
162+
</div>
163+
</template>
164+
<el-input
165+
v-model="formData.prompt"
166+
:rows="5"
167+
:placeholder="`用户提示词,可以引用变量,如 {{开始.question}}`"
168+
type="textarea"
169+
/>
170+
</el-form-item>
171+
172+
<el-form-item label="历史聊天记录">
173+
<template #label>
174+
<div class="flex-between gap-3 w-full">
175+
<span class="whitespace-nowrap">历史聊天记录</span>
176+
<el-select
177+
v-model="formData.dialogue_type"
178+
:teleported="false"
179+
class="w-20"
180+
size="small"
181+
>
182+
<el-option label="节点" value="NODE" />
183+
<el-option label="工作流" value="WORKFLOW" />
184+
</el-select>
185+
</div>
186+
</template>
187+
<el-input-number
188+
v-model="formData.dialogue_number"
189+
:min="0"
190+
:value-on-clear="0"
191+
controls-position="right"
192+
class="w-full!"
193+
:step="1"
194+
:step-strictly="true"
195+
/>
196+
</el-form-item>
197+
</el-form>
198+
</NodeContainer>
199+
</template>
200+
<style lang="scss" scoped>
201+
:deep(.el-form-item) {
202+
margin-bottom: 16px;
203+
}
204+
</style>

0 commit comments

Comments
 (0)