Skip to content

Commit 90858d3

Browse files
authored
feat: 【Agent】AI conversation nodes support multimodal capabilities (#5245)
1 parent 07689a8 commit 90858d3

3 files changed

Lines changed: 130 additions & 8 deletions

File tree

apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class ChatNodeSerializer(serializers.Serializer):
4949
label=_("Skill IDs"), )
5050
mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output"))
5151

52+
video_list = serializers.ListField(required=False, label=_("video"))
53+
54+
image_list = serializers.ListField(required=False, label=_("picture"))
55+
5256

5357
class IChatNode(INode):
5458
type = 'ai-chat-node'

apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
@date:2024/6/4 14:30
77
@desc:
88
"""
9+
import base64
910
import json
1011
import re
1112
import time
1213
from functools import reduce
14+
from imghdr import what
1315
from typing import List, Dict
1416

1517
from django.db.models import QuerySet
@@ -25,6 +27,7 @@
2527
from common.utils.rsa_util import rsa_long_decrypt
2628
from common.utils.shared_resource_auth import filter_authorized_ids
2729
from common.utils.tool_code import ToolExecutor
30+
from knowledge.models import File
2831
from models_provider.models import Model
2932
from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id
3033
from tools.models import Tool, ToolType
@@ -196,11 +199,11 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
196199
self.runtime_node_id)
197200
self.context['history_message'] = [{'content': message.content, 'role': message.type} for message in
198201
(history_message if history_message is not None else [])]
199-
question = self.generate_prompt_question(prompt)
202+
question = self.generate_prompt_question(prompt, chat_model)
200203
self.context['question'] = question.content
201204
system = self.workflow_manage.generate_prompt(system)
202205
self.context['system'] = system
203-
message_list = self.generate_message_list(prompt, history_message)
206+
message_list = self.generate_message_list(question, history_message)
204207
self.context['message_list'] = message_list
205208

206209
# 过滤tool_id
@@ -386,11 +389,73 @@ def get_history_message(history_chat_record, dialogue_number, dialogue_type, run
386389
message.content = re.sub(r'<form_rander>.*?<\/form_rander>', '', message.content, flags=re.DOTALL)
387390
return history_message
388391

389-
def generate_prompt_question(self, prompt):
390-
return HumanMessage(self.workflow_manage.generate_prompt(prompt))
392+
def generate_prompt_question(self, prompt, model):
393+
image = self.get_image()
394+
video = self.get_video()
395+
videos = []
396+
images = []
397+
if image:
398+
images = self._process_images(image)
399+
if video:
400+
videos = self._process_videos(video, model)
401+
return HumanMessage(
402+
content=[*videos, *images, {'type': 'text', 'text': self.workflow_manage.generate_prompt(prompt)}])
403+
404+
def get_image(self):
405+
if 'image_list' in self.node_params_serializer.data:
406+
image = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('image_list')[0],
407+
self.node_params_serializer.data.get('image_list')[1:])
408+
return image
409+
return None
410+
411+
def get_video(self):
412+
if 'video_list' in self.node_params_serializer.data:
413+
video = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('video_list')[0],
414+
self.node_params_serializer.data.get('video_list')[1:])
415+
return video
416+
return None
391417

392-
def generate_message_list(self, prompt: str, history_message):
393-
return [*history_message, HumanMessage(self.workflow_manage.generate_prompt(prompt))]
418+
def _process_videos(self, image, video_model):
419+
videos = []
420+
if isinstance(image, str) and image.startswith('http'):
421+
videos.append({'type': 'video_url', 'video_url': {'url': image}})
422+
elif image is not None and len(image) > 0:
423+
for img in image:
424+
if 'file_id' in img:
425+
file_id = img['file_id']
426+
file = QuerySet(File).filter(id=file_id).first()
427+
url = video_model.upload_file_and_get_url(file.get_bytes(), file.file_name)
428+
videos.append(
429+
{'type': 'video_url', 'video_url': {'url': url}})
430+
elif 'url' in img and img['url'].startswith('http'):
431+
videos.append(
432+
{'type': 'video_url', 'video_url': {'url': img['url']}})
433+
return videos
434+
435+
def _process_images(self, image):
436+
"""
437+
处理图像数据,转换为模型可识别的格式
438+
"""
439+
images = []
440+
if isinstance(image, str) and image.startswith('http'):
441+
images.append({'type': 'image_url', 'image_url': {'url': image}})
442+
elif image is not None and len(image) > 0:
443+
for img in image:
444+
if 'file_id' in img:
445+
file_id = img['file_id']
446+
file = QuerySet(File).filter(id=file_id).first()
447+
image_bytes = file.get_bytes()
448+
base64_image = base64.b64encode(image_bytes).decode("utf-8")
449+
image_format = what(None, image_bytes)
450+
images.append(
451+
{'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
452+
elif 'url' in img and img['url'].startswith('http'):
453+
images.append(
454+
{'type': 'image_url', 'image_url': {'url': img["url"]}})
455+
return images
456+
457+
def generate_message_list(self, question, history_message):
458+
return [*history_message, question]
394459

395460
@staticmethod
396461
def reset_message_list(message_list: List[BaseMessage], answer_text):

ui/src/workflow/nodes/ai-chat-node/index.vue

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,60 @@
170170
:step-strictly="true"
171171
/>
172172
</el-form-item>
173-
173+
<el-form-item
174+
:rules="{
175+
type: 'array',
176+
required: true,
177+
message: $t('workflow.nodes.imageUnderstandNode.image.requiredMessage'),
178+
trigger: 'change',
179+
}"
180+
>
181+
<div class="flex align-center">
182+
<div>
183+
<span>{{ $t('workflow.nodes.imageUnderstandNode.image.label') }} </span>
184+
</div>
185+
<el-tooltip effect="dark" placement="right">
186+
<template #content>
187+
<div style="white-space: pre-wrap; font-family: monospace">{{ fileTooltip }}</div>
188+
</template>
189+
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
190+
</el-tooltip>
191+
</div>
192+
<NodeCascader
193+
ref="nodeCascaderRef"
194+
:nodeModel="nodeModel"
195+
class="w-full"
196+
:placeholder="$t('workflow.nodes.imageUnderstandNode.image.requiredMessage')"
197+
v-model="chat_data.image_list"
198+
/>
199+
</el-form-item>
200+
<el-form-item
201+
:rules="{
202+
type: 'array',
203+
required: false,
204+
message: $t('workflow.nodes.videoUnderstandNode.video.requiredMessage'),
205+
trigger: 'change',
206+
}"
207+
>
208+
<div class="flex align-center">
209+
<div>
210+
<span>{{ $t('workflow.nodes.videoUnderstandNode.video.label') }} </span>
211+
</div>
212+
<el-tooltip effect="dark" placement="right">
213+
<template #content>
214+
<div style="white-space: pre-wrap; font-family: monospace">{{ fileTooltip }}</div>
215+
</template>
216+
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
217+
</el-tooltip>
218+
</div>
219+
<NodeCascader
220+
ref="nodeCascaderRef"
221+
:nodeModel="nodeModel"
222+
class="w-full"
223+
:placeholder="$t('workflow.nodes.videoUnderstandNode.video.requiredMessage')"
224+
v-model="chat_data.video_list"
225+
/>
226+
</el-form-item>
174227
<div class="mb-8 mt-12 flex-between">
175228
<span class="mr-4 lighter">
176229
{{ $t('views.tool.skill.title') }}
@@ -555,6 +608,7 @@ import { resetUrl } from '@/utils/common'
555608
import { relatedObject } from '@/utils/array.ts'
556609
import { WorkflowMode } from '@/enums/application'
557610
import ApplicationDialog from '@/views/application/component/ApplicationDialog.vue'
611+
import { fileTooltip } from '@/workflow/common/data.ts'
558612
const workflowMode = (inject('workflowMode') as WorkflowMode) || WorkflowMode.Application
559613
const getResourceDetail = inject('getResourceDetail') as any
560614
const route = useRoute()
@@ -604,7 +658,6 @@ const defaultPrompt = `${t('workflow.nodes.aiChatNode.defaultPrompt')}:
604658
${t('views.problem.title')}:
605659
{{${t('workflow.nodes.startNode.label')}.question}}`
606660
607-
608661
const collapseData = reactive({
609662
MCP: true,
610663
tool: true,

0 commit comments

Comments
 (0)