feat: add MCP server tools integration and UI components
This commit is contained in:
parent
f9c4e96f97
commit
2d6ac806ff
@ -25,13 +25,14 @@ from .speech_to_text_step_node import BaseSpeechToTextNode
|
|||||||
from .start_node import *
|
from .start_node import *
|
||||||
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
|
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
|
||||||
from .variable_assign_node import BaseVariableAssignNode
|
from .variable_assign_node import BaseVariableAssignNode
|
||||||
|
from .mcp_node import BaseMcpNode
|
||||||
|
|
||||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
|
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
|
||||||
BaseConditionNode, BaseReplyNode,
|
BaseConditionNode, BaseReplyNode,
|
||||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||||
BaseDocumentExtractNode,
|
BaseDocumentExtractNode,
|
||||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||||
BaseImageGenerateNode, BaseVariableAssignNode]
|
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
|
||||||
|
|
||||||
|
|
||||||
def get_node(node_type):
|
def get_node(node_type):
|
||||||
|
|||||||
3
apps/application/flow/step_node/mcp_node/__init__.py
Normal file
3
apps/application/flow/step_node/mcp_node/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
from .impl import *
|
||||||
35
apps/application/flow/step_node/mcp_node/i_mcp_node.py
Normal file
35
apps/application/flow/step_node/mcp_node/i_mcp_node.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from application.flow.i_step_node import INode, NodeResult
|
||||||
|
from common.util.field_message import ErrMessage
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class McpNodeSerializer(serializers.Serializer):
|
||||||
|
mcp_servers = serializers.JSONField(required=True,
|
||||||
|
error_messages=ErrMessage.char(_("Mcp servers")))
|
||||||
|
|
||||||
|
mcp_server = serializers.CharField(required=True,
|
||||||
|
error_messages=ErrMessage.char(_("Mcp server")))
|
||||||
|
|
||||||
|
mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
|
||||||
|
|
||||||
|
tool_params = serializers.DictField(required=True,
|
||||||
|
error_messages=ErrMessage.char(_("Tool parameters")))
|
||||||
|
|
||||||
|
|
||||||
|
class IMcpNode(INode):
|
||||||
|
type = 'mcp-node'
|
||||||
|
|
||||||
|
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||||
|
return McpNodeSerializer
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||||
|
|
||||||
|
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||||
|
pass
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
from .base_mcp_node import BaseMcpNode
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||||
|
|
||||||
|
from application.flow.i_step_node import NodeResult
|
||||||
|
from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMcpNode(IMcpNode):
|
||||||
|
def save_context(self, details, workflow_manage):
|
||||||
|
self.context['result'] = details.get('result')
|
||||||
|
self.context['tool_params'] = details.get('tool_params')
|
||||||
|
self.context['mcp_tool'] = details.get('mcp_tool')
|
||||||
|
self.answer_text = details.get('result')
|
||||||
|
|
||||||
|
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
|
||||||
|
servers = json.loads(mcp_servers)
|
||||||
|
params = self.handle_variables(tool_params)
|
||||||
|
|
||||||
|
async def call_tool(s, session, t, a):
|
||||||
|
async with MultiServerMCPClient(s) as client:
|
||||||
|
s = await client.sessions[session].call_tool(t, a)
|
||||||
|
return s
|
||||||
|
|
||||||
|
res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
|
||||||
|
return NodeResult({'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})
|
||||||
|
|
||||||
|
def handle_variables(self, tool_params):
|
||||||
|
# 处理参数中的变量
|
||||||
|
for k, v in tool_params.items():
|
||||||
|
if type(v) == str:
|
||||||
|
tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k])
|
||||||
|
if type(v) == dict:
|
||||||
|
self.handle_variables(v)
|
||||||
|
return tool_params
|
||||||
|
|
||||||
|
def get_reference_content(self, fields: List[str]):
|
||||||
|
return str(self.workflow_manage.get_reference_field(
|
||||||
|
fields[0],
|
||||||
|
fields[1:]))
|
||||||
|
|
||||||
|
def get_details(self, index: int, **kwargs):
|
||||||
|
return {
|
||||||
|
'name': self.node.properties.get('stepName'),
|
||||||
|
"index": index,
|
||||||
|
'run_time': self.context.get('run_time'),
|
||||||
|
'status': self.status,
|
||||||
|
'err_message': self.err_message,
|
||||||
|
'type': self.node.type,
|
||||||
|
'mcp_tool': self.context.get('mcp_tool'),
|
||||||
|
'tool_params': self.context.get('tool_params'),
|
||||||
|
'result': self.context.get('result'),
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@
|
|||||||
@date:2023/11/7 10:02
|
@date:2023/11/7 10:02
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@ -23,6 +24,8 @@ from django.db.models import QuerySet
|
|||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||||
|
from mcp.client.sse import sse_client
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
from rest_framework.utils.formatting import lazy_format
|
from rest_framework.utils.formatting import lazy_format
|
||||||
|
|
||||||
@ -1305,3 +1308,28 @@ class ApplicationSerializer(serializers.Serializer):
|
|||||||
application_api_key.save()
|
application_api_key.save()
|
||||||
# 写入缓存
|
# 写入缓存
|
||||||
get_application_api_key(application_api_key.secret_key, False)
|
get_application_api_key(application_api_key.secret_key, False)
|
||||||
|
|
||||||
|
class McpServers(serializers.Serializer):
|
||||||
|
mcp_servers = serializers.JSONField(required=True)
|
||||||
|
|
||||||
|
def get_mcp_servers(self, with_valid=True):
|
||||||
|
if with_valid:
|
||||||
|
self.is_valid(raise_exception=True)
|
||||||
|
servers = json.loads(self.data.get('mcp_servers'))
|
||||||
|
|
||||||
|
async def get_mcp_tools(servers):
|
||||||
|
async with MultiServerMCPClient(servers) as client:
|
||||||
|
return client.get_tools()
|
||||||
|
|
||||||
|
tools = []
|
||||||
|
for server in servers:
|
||||||
|
tools += [
|
||||||
|
{
|
||||||
|
'server': server,
|
||||||
|
'name': tool.name,
|
||||||
|
'description': tool.description,
|
||||||
|
'args_schema': tool.args_schema,
|
||||||
|
}
|
||||||
|
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
|
||||||
|
return tools
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ urlpatterns = [
|
|||||||
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
|
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
|
||||||
path('application/embed', views.Application.Embed.as_view()),
|
path('application/embed', views.Application.Embed.as_view()),
|
||||||
path('application/authentication', views.Application.Authentication.as_view()),
|
path('application/authentication', views.Application.Authentication.as_view()),
|
||||||
|
path('application/mcp_servers', views.Application.McpServers.as_view()),
|
||||||
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
|
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
|
||||||
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
|
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
|
||||||
path('application/<str:application_id>/export', views.Application.Export.as_view()),
|
path('application/<str:application_id>/export', views.Application.Export.as_view()),
|
||||||
|
|||||||
@ -700,3 +700,13 @@ class Application(APIView):
|
|||||||
data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data)
|
data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data)
|
||||||
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
|
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
|
||||||
'Content-Disposition': 'attachment; filename="abc.mp3"'})
|
'Content-Disposition': 'attachment; filename="abc.mp3"'})
|
||||||
|
|
||||||
|
class McpServers(APIView):
|
||||||
|
authentication_classes = [TokenAuth]
|
||||||
|
|
||||||
|
@action(methods=['GET'], detail=False)
|
||||||
|
@has_permissions(PermissionConstants.APPLICATION_READ, compare=CompareConstants.AND)
|
||||||
|
@log(menu='Application', operate="Get the MCP server tools")
|
||||||
|
def get(self, request: Request):
|
||||||
|
return result.success(ApplicationSerializer.McpServers(
|
||||||
|
data={'mcp_servers': request.query_params.get('mcp_servers')}).get_mcp_servers())
|
||||||
|
|||||||
@ -350,6 +350,13 @@ const getFunctionLib: (
|
|||||||
return get(`${prefix}/${application_id}/function_lib/${function_lib_id}`, undefined, loading)
|
return get(`${prefix}/${application_id}/function_lib/${function_lib_id}`, undefined, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMcpTools: (
|
||||||
|
data: any,
|
||||||
|
loading?: Ref<boolean>
|
||||||
|
) => Promise<Result<any>> = (data, loading) => {
|
||||||
|
return get(`${prefix}/mcp_servers`, data, loading)
|
||||||
|
}
|
||||||
|
|
||||||
const getApplicationById: (
|
const getApplicationById: (
|
||||||
application_id: String,
|
application_id: String,
|
||||||
app_id: String,
|
app_id: String,
|
||||||
@ -576,5 +583,6 @@ export default {
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
exportApplication,
|
exportApplication,
|
||||||
importApplication,
|
importApplication,
|
||||||
getApplicationById
|
getApplicationById,
|
||||||
|
getMcpTools
|
||||||
}
|
}
|
||||||
|
|||||||
@ -639,6 +639,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- MCP 节点 -->
|
||||||
|
<template v-if="item.type === WorkflowType.McpNode">
|
||||||
|
<div class="card-never border-r-4">
|
||||||
|
<h5 class="p-8-12">
|
||||||
|
{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}
|
||||||
|
</h5>
|
||||||
|
<div class="p-8-12 border-t-dashed lighter">
|
||||||
|
<div class="mb-8">
|
||||||
|
<span class="color-secondary"> {{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}: </span> {{ item.mcp_tool }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-never border-r-4">
|
||||||
|
<h5 class="p-8-12">
|
||||||
|
{{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}
|
||||||
|
</h5>
|
||||||
|
<div class="p-8-12 border-t-dashed lighter">
|
||||||
|
<div v-for="(value, name) in item.tool_params" :key="name" class="mb-8">
|
||||||
|
<span class="color-secondary">{{ name }}:</span> {{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-never border-r-4">
|
||||||
|
<h5 class="p-8-12">
|
||||||
|
{{ $t('common.param.outputParam') }}
|
||||||
|
</h5>
|
||||||
|
<div class="p-8-12 border-t-dashed lighter">
|
||||||
|
<div v-for="(f, i) in item.result" :key="i" class="mb-8">
|
||||||
|
<span class="color-secondary">result:</span> {{ f }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="card-never border-r-4">
|
<div class="card-never border-r-4">
|
||||||
|
|||||||
@ -16,5 +16,6 @@ export enum WorkflowType {
|
|||||||
FormNode = 'form-node',
|
FormNode = 'form-node',
|
||||||
TextToSpeechNode = 'text-to-speech-node',
|
TextToSpeechNode = 'text-to-speech-node',
|
||||||
SpeechToTextNode = 'speech-to-text-node',
|
SpeechToTextNode = 'speech-to-text-node',
|
||||||
ImageGenerateNode = 'image-generate-node'
|
ImageGenerateNode = 'image-generate-node',
|
||||||
|
McpNode = 'mcp-node',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -220,6 +220,14 @@ export default {
|
|||||||
text: 'Update the value of the global variable',
|
text: 'Update the value of the global variable',
|
||||||
assign: 'Set Value'
|
assign: 'Set Value'
|
||||||
},
|
},
|
||||||
|
mcpNode: {
|
||||||
|
label: 'MCP Server',
|
||||||
|
text: 'Call MCP Tools',
|
||||||
|
getToolsSuccess: 'Get Tools Successfully',
|
||||||
|
getTool: 'Get Tools',
|
||||||
|
tool: 'Tool',
|
||||||
|
toolParam: 'Tool Params'
|
||||||
|
},
|
||||||
imageGenerateNode: {
|
imageGenerateNode: {
|
||||||
label: 'Image Generation',
|
label: 'Image Generation',
|
||||||
text: 'Generate images based on provided text content',
|
text: 'Generate images based on provided text content',
|
||||||
|
|||||||
@ -220,6 +220,14 @@ export default {
|
|||||||
text: '更新全局变量的值',
|
text: '更新全局变量的值',
|
||||||
assign: '赋值'
|
assign: '赋值'
|
||||||
},
|
},
|
||||||
|
mcpNode: {
|
||||||
|
label: 'MCP 节点',
|
||||||
|
text: '调用 MCP 工具',
|
||||||
|
getToolsSuccess: '获取工具成功',
|
||||||
|
getTool: '获取工具',
|
||||||
|
tool: '工具',
|
||||||
|
toolParam: '工具参数'
|
||||||
|
},
|
||||||
imageGenerateNode: {
|
imageGenerateNode: {
|
||||||
label: '图片生成',
|
label: '图片生成',
|
||||||
text: '根据提供的文本内容生成图片',
|
text: '根据提供的文本内容生成图片',
|
||||||
|
|||||||
@ -220,6 +220,14 @@ export default {
|
|||||||
text: '更新全域變數的值',
|
text: '更新全域變數的值',
|
||||||
assign: '賦值'
|
assign: '賦值'
|
||||||
},
|
},
|
||||||
|
mcpNode: {
|
||||||
|
label: 'MCP 節點',
|
||||||
|
text: '呼叫 MCP 工具',
|
||||||
|
getToolsSuccess: '獲取工具成功',
|
||||||
|
getTool: '獲取工具',
|
||||||
|
tool: '工具',
|
||||||
|
toolParam: '工具變數'
|
||||||
|
},
|
||||||
imageGenerateNode: {
|
imageGenerateNode: {
|
||||||
label: '圖片生成',
|
label: '圖片生成',
|
||||||
text: '根據提供的文本內容生成圖片',
|
text: '根據提供的文本內容生成圖片',
|
||||||
|
|||||||
@ -263,6 +263,24 @@ export const variableAssignNode = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mcpNode = {
|
||||||
|
type: WorkflowType.McpNode,
|
||||||
|
text: t('views.applicationWorkflow.nodes.mcpNode.text'),
|
||||||
|
label: t('views.applicationWorkflow.nodes.mcpNode.label'),
|
||||||
|
height: 252,
|
||||||
|
properties: {
|
||||||
|
stepName: t('views.applicationWorkflow.nodes.mcpNode.label'),
|
||||||
|
config: {
|
||||||
|
fields:[
|
||||||
|
{
|
||||||
|
label: t('common.result'),
|
||||||
|
value: 'result'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const imageGenerateNode = {
|
export const imageGenerateNode = {
|
||||||
type: WorkflowType.ImageGenerateNode,
|
type: WorkflowType.ImageGenerateNode,
|
||||||
text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'),
|
text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'),
|
||||||
@ -332,7 +350,8 @@ export const menuNodes = [
|
|||||||
documentExtractNode,
|
documentExtractNode,
|
||||||
speechToTextNode,
|
speechToTextNode,
|
||||||
textToSpeechNode,
|
textToSpeechNode,
|
||||||
variableAssignNode
|
variableAssignNode,
|
||||||
|
mcpNode
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -426,7 +445,8 @@ export const nodeDict: any = {
|
|||||||
[WorkflowType.TextToSpeechNode]: textToSpeechNode,
|
[WorkflowType.TextToSpeechNode]: textToSpeechNode,
|
||||||
[WorkflowType.SpeechToTextNode]: speechToTextNode,
|
[WorkflowType.SpeechToTextNode]: speechToTextNode,
|
||||||
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
||||||
[WorkflowType.VariableAssignNode]: variableAssignNode
|
[WorkflowType.VariableAssignNode]: variableAssignNode,
|
||||||
|
[WorkflowType.McpNode]: mcpNode,
|
||||||
}
|
}
|
||||||
export function isWorkFlow(type: string | undefined) {
|
export function isWorkFlow(type: string | undefined) {
|
||||||
return type === 'WORK_FLOW'
|
return type === 'WORK_FLOW'
|
||||||
|
|||||||
6
ui/src/workflow/icons/mcp-node-icon.vue
Normal file
6
ui/src/workflow/icons/mcp-node-icon.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<AppAvatar shape="square" class="avatar-blue">
|
||||||
|
<img src="@/assets/icon_assigner.svg" style="width: 65%" alt="" />
|
||||||
|
</AppAvatar>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts"></script>
|
||||||
14
ui/src/workflow/nodes/mcp-node/index.ts
Normal file
14
ui/src/workflow/nodes/mcp-node/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import McpNodeVue from './index.vue'
|
||||||
|
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||||
|
|
||||||
|
class McpNode extends AppNode {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props, McpNodeVue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'mcp-node',
|
||||||
|
model: AppNodeModel,
|
||||||
|
view: McpNode
|
||||||
|
}
|
||||||
239
ui/src/workflow/nodes/mcp-node/index.vue
Normal file
239
ui/src/workflow/nodes/mcp-node/index.vue
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<NodeContainer :nodeModel="nodeModel">
|
||||||
|
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
|
||||||
|
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter">
|
||||||
|
<el-form
|
||||||
|
@submit.prevent
|
||||||
|
:model="form_data"
|
||||||
|
label-position="top"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
ref="replyNodeFormRef"
|
||||||
|
hide-required-asterisk
|
||||||
|
>
|
||||||
|
<el-form-item label="MCP Server Config">
|
||||||
|
<MdEditorMagnify
|
||||||
|
@wheel="wheel"
|
||||||
|
title="MCP Server Config"
|
||||||
|
v-model="form_data.mcp_servers"
|
||||||
|
style="height: 150px"
|
||||||
|
@submitDialog="submitDialog"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template v-slot:label>
|
||||||
|
<div class="flex-between">
|
||||||
|
<span>{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</span>
|
||||||
|
<el-button type="primary" link @click="getTools()">
|
||||||
|
{{ $t('views.applicationWorkflow.nodes.mcpNode.getTool') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-select
|
||||||
|
v-model="form_data.mcp_tool"
|
||||||
|
@change="changeTool"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in form_data.mcp_tools"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
>
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="dark"
|
||||||
|
:content="item.description"
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<h5 class="title-decoration-1 mb-8">
|
||||||
|
{{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}</h5>
|
||||||
|
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter" v-if="form_data.tool_params[form_data.params_nested]">
|
||||||
|
<DynamicsForm
|
||||||
|
v-if="form_data.mcp_tool"
|
||||||
|
v-model="form_data.tool_params[form_data.params_nested]"
|
||||||
|
:model="form_data.tool_params[form_data.params_nested]"
|
||||||
|
label-position="top"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
:render_data="form_data.tool_form_field"
|
||||||
|
ref="dynamicsFormRef"
|
||||||
|
>
|
||||||
|
</DynamicsForm>
|
||||||
|
</div>
|
||||||
|
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter" v-else>
|
||||||
|
<DynamicsForm
|
||||||
|
v-if="form_data.mcp_tool"
|
||||||
|
v-model="form_data.tool_params"
|
||||||
|
:model="form_data.tool_params"
|
||||||
|
label-position="top"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
:render_data="form_data.tool_form_field"
|
||||||
|
ref="dynamicsFormRef"
|
||||||
|
>
|
||||||
|
</DynamicsForm>
|
||||||
|
</div>
|
||||||
|
</NodeContainer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cloneDeep, set } from 'lodash'
|
||||||
|
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { isLastNode } from '@/workflow/common/data'
|
||||||
|
import applicationApi from '@/api/application'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||||
|
import { MsgSuccess } from '@/utils/message'
|
||||||
|
|
||||||
|
const props = defineProps<{ nodeModel: any }>()
|
||||||
|
|
||||||
|
const dynamicsFormRef = ref()
|
||||||
|
|
||||||
|
|
||||||
|
const wheel = (e: any) => {
|
||||||
|
if (e.ctrlKey === true) {
|
||||||
|
e.preventDefault()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
e.stopPropagation()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const form = {
|
||||||
|
mcp_tool: '',
|
||||||
|
mcp_tools: [],
|
||||||
|
mcp_servers: '',
|
||||||
|
mcp_server: '',
|
||||||
|
tool_params: {},
|
||||||
|
tool_form_field: [],
|
||||||
|
params_nested: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function submitDialog(val: string) {
|
||||||
|
set(props.nodeModel.properties.node_data, 'mcp_servers', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTools() {
|
||||||
|
applicationApi.getMcpTools({ mcp_servers: form_data.value.mcp_servers }).then((res: any) => {
|
||||||
|
form_data.value.mcp_tools = res.data
|
||||||
|
MsgSuccess(t('views.applicationWorkflow.nodes.mcpNode.getToolsSuccess'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTool() {
|
||||||
|
form_data.value.mcp_server = form_data.value.mcp_tools.filter((item: any) => item.name === form_data.value.mcp_tool)[0].server
|
||||||
|
// console.log(form_data.value.mcp_server)
|
||||||
|
|
||||||
|
const args_schema = form_data.value.mcp_tools.filter((item: any) => item.name === form_data.value.mcp_tool)[0].args_schema
|
||||||
|
form_data.value.tool_form_field = []
|
||||||
|
for (const item in args_schema.properties) {
|
||||||
|
let params = args_schema.properties[item].properties
|
||||||
|
if (params) {
|
||||||
|
form_data.value.params_nested = item
|
||||||
|
for (const item2 in params) {
|
||||||
|
form_data.value.tool_form_field.push({
|
||||||
|
field: item2,
|
||||||
|
label: {
|
||||||
|
input_type: 'TooltipLabel',
|
||||||
|
label: item2,
|
||||||
|
attrs: { tooltip: params[item2].description },
|
||||||
|
props_info: {}
|
||||||
|
},
|
||||||
|
input_type: 'TextInput',
|
||||||
|
required: args_schema.properties[item].required?.indexOf(item2) !== -1,
|
||||||
|
default_value: '',
|
||||||
|
show_default_value: false,
|
||||||
|
props_info: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: args_schema.properties[item].required?.indexOf(item2) !== -1,
|
||||||
|
message: t('dynamicsForm.tip.requiredMessage'),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form_data.value.params_nested = ''
|
||||||
|
form_data.value.tool_form_field.push({
|
||||||
|
field: item,
|
||||||
|
label: {
|
||||||
|
input_type: 'TooltipLabel',
|
||||||
|
label: item,
|
||||||
|
attrs: { tooltip: args_schema.properties[item].description },
|
||||||
|
props_info: {}
|
||||||
|
},
|
||||||
|
input_type: 'TextInput',
|
||||||
|
required: args_schema.required?.indexOf(item) !== -1,
|
||||||
|
default_value: '',
|
||||||
|
show_default_value: false,
|
||||||
|
props_info: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: args_schema.required?.indexOf(item) !== -1,
|
||||||
|
message: t('dynamicsForm.tip.requiredMessage'),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (form_data.value.params_nested) {
|
||||||
|
form_data.value.tool_params = { [form_data.value.params_nested]: {} }
|
||||||
|
dynamicsFormRef.value?.render(form_data.value.tool_form_field, form_data.value.tool_params[form_data.value.params_nested])
|
||||||
|
} else {
|
||||||
|
form_data.value.tool_params = {}
|
||||||
|
dynamicsFormRef.value?.render(form_data.value.tool_form_field, form_data.value.tool_params)
|
||||||
|
}
|
||||||
|
console.log(form_data.value.tool_params)
|
||||||
|
}
|
||||||
|
|
||||||
|
const form_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 replyNodeFormRef = ref()
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
let ps = [
|
||||||
|
replyNodeFormRef.value?.validate(),
|
||||||
|
dynamicsFormRef.value?.validate()
|
||||||
|
]
|
||||||
|
return Promise.all(ps).catch((err: any) => {
|
||||||
|
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
||||||
|
if (isLastNode(props.nodeModel)) {
|
||||||
|
set(props.nodeModel.properties.node_data, 'is_result', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(props.nodeModel, 'validate', validate)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
Loading…
Reference in New Issue
Block a user