feat: AI chat node mcp server config
This commit is contained in:
parent
2d6ac806ff
commit
563516f835
@ -33,6 +33,9 @@ class ChatNodeSerializer(serializers.Serializer):
|
|||||||
error_messages=ErrMessage.dict('Model settings'))
|
error_messages=ErrMessage.dict('Model settings'))
|
||||||
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||||
error_messages=ErrMessage.char(_("Context Type")))
|
error_messages=ErrMessage.char(_("Context Type")))
|
||||||
|
mcp_enable = serializers.BooleanField(required=False,
|
||||||
|
error_messages=ErrMessage.boolean(_("Whether to enable MCP")))
|
||||||
|
mcp_servers = serializers.JSONField(required=False, error_messages=ErrMessage.list(_("MCP Server")))
|
||||||
|
|
||||||
|
|
||||||
class IChatNode(INode):
|
class IChatNode(INode):
|
||||||
@ -49,5 +52,7 @@ class IChatNode(INode):
|
|||||||
model_params_setting=None,
|
model_params_setting=None,
|
||||||
dialogue_type=None,
|
dialogue_type=None,
|
||||||
model_setting=None,
|
model_setting=None,
|
||||||
|
mcp_enable=False,
|
||||||
|
mcp_servers=None,
|
||||||
**kwargs) -> NodeResult:
|
**kwargs) -> NodeResult:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -6,14 +6,19 @@
|
|||||||
@date:2024/6/4 14:30
|
@date:2024/6/4 14:30
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from types import AsyncGeneratorType
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from langchain.schema import HumanMessage, SystemMessage
|
from langchain.schema import HumanMessage, SystemMessage
|
||||||
from langchain_core.messages import BaseMessage, AIMessage
|
from langchain_core.messages import BaseMessage, AIMessage, AIMessageChunk, ToolMessage
|
||||||
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||||
|
from langgraph.prebuilt import create_react_agent
|
||||||
|
|
||||||
from application.flow.i_step_node import NodeResult, INode
|
from application.flow.i_step_node import NodeResult, INode
|
||||||
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
|
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
|
||||||
@ -56,6 +61,7 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||||||
reasoning = Reasoning(model_setting.get('reasoning_content_start', '<think>'),
|
reasoning = Reasoning(model_setting.get('reasoning_content_start', '<think>'),
|
||||||
model_setting.get('reasoning_content_end', '</think>'))
|
model_setting.get('reasoning_content_end', '</think>'))
|
||||||
response_reasoning_content = False
|
response_reasoning_content = False
|
||||||
|
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
reasoning_chunk = reasoning.get_reasoning_content(chunk)
|
reasoning_chunk = reasoning.get_reasoning_content(chunk)
|
||||||
content_chunk = reasoning_chunk.get('content')
|
content_chunk = reasoning_chunk.get('content')
|
||||||
@ -84,6 +90,47 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
|
|||||||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def _yield_mcp_response(chat_model, message_list, mcp_servers):
|
||||||
|
async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
|
||||||
|
agent = create_react_agent(chat_model, client.get_tools())
|
||||||
|
response = agent.astream({"messages": message_list}, stream_mode='messages')
|
||||||
|
async for chunk in response:
|
||||||
|
# if isinstance(chunk[0], ToolMessage):
|
||||||
|
# print(chunk[0])
|
||||||
|
if isinstance(chunk[0], AIMessageChunk):
|
||||||
|
yield chunk[0]
|
||||||
|
|
||||||
|
def mcp_response_generator(chat_model, message_list, mcp_servers):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
try:
|
||||||
|
async_gen = _yield_mcp_response(chat_model, message_list, mcp_servers)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = loop.run_until_complete(anext_async(async_gen))
|
||||||
|
yield chunk
|
||||||
|
except StopAsyncIteration:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f'exception: {e}')
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
async def anext_async(agen):
|
||||||
|
return await agen.__anext__()
|
||||||
|
|
||||||
|
async def _get_mcp_response(chat_model, message_list, mcp_servers):
|
||||||
|
async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
|
||||||
|
agent = create_react_agent(chat_model, client.get_tools())
|
||||||
|
response = agent.astream({"messages": message_list}, stream_mode='messages')
|
||||||
|
result = []
|
||||||
|
async for chunk in response:
|
||||||
|
# if isinstance(chunk[0], ToolMessage):
|
||||||
|
# print(chunk[0].content)
|
||||||
|
if isinstance(chunk[0], AIMessageChunk):
|
||||||
|
result.append(chunk[0])
|
||||||
|
return result
|
||||||
|
|
||||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||||
"""
|
"""
|
||||||
写入上下文数据
|
写入上下文数据
|
||||||
@ -142,6 +189,8 @@ class BaseChatNode(IChatNode):
|
|||||||
model_params_setting=None,
|
model_params_setting=None,
|
||||||
dialogue_type=None,
|
dialogue_type=None,
|
||||||
model_setting=None,
|
model_setting=None,
|
||||||
|
mcp_enable=False,
|
||||||
|
mcp_servers=None,
|
||||||
**kwargs) -> NodeResult:
|
**kwargs) -> NodeResult:
|
||||||
if dialogue_type is None:
|
if dialogue_type is None:
|
||||||
dialogue_type = 'WORKFLOW'
|
dialogue_type = 'WORKFLOW'
|
||||||
@ -163,6 +212,14 @@ class BaseChatNode(IChatNode):
|
|||||||
self.context['system'] = system
|
self.context['system'] = system
|
||||||
message_list = self.generate_message_list(system, prompt, history_message)
|
message_list = self.generate_message_list(system, prompt, history_message)
|
||||||
self.context['message_list'] = message_list
|
self.context['message_list'] = message_list
|
||||||
|
|
||||||
|
if mcp_enable and mcp_servers is not None:
|
||||||
|
r = mcp_response_generator(chat_model, message_list, mcp_servers)
|
||||||
|
return NodeResult(
|
||||||
|
{'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||||
|
'history_message': history_message, 'question': question.content}, {},
|
||||||
|
_write_context=write_context_stream)
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
r = chat_model.stream(message_list)
|
r = chat_model.stream(message_list)
|
||||||
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
|
||||||
|
|||||||
98
ui/src/views/application/component/McpServersDialog.vue
Normal file
98
ui/src/views/application/component/McpServersDialog.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
align-center
|
||||||
|
:title="$t('common.setting')"
|
||||||
|
class="param-dialog"
|
||||||
|
v-model="dialogVisible"
|
||||||
|
style="width: 550px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
>
|
||||||
|
<el-form label-position="top" ref="paramFormRef" :model="form"
|
||||||
|
require-asterisk-position="right">
|
||||||
|
<el-form-item label="MCP" prop="mcp_enable">
|
||||||
|
<el-switch v-model="form.mcp_enable" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="MCP Server Config" prop="mcp_servers"
|
||||||
|
:rules="[{ required: true, message: $t('common.required') }]">
|
||||||
|
<el-input
|
||||||
|
v-model="form.mcp_servers"
|
||||||
|
:rows="6"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer p-16">
|
||||||
|
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="submit()" :loading="loading">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
const paramFormRef = ref()
|
||||||
|
|
||||||
|
const form = ref<any>({
|
||||||
|
mcp_servers: '',
|
||||||
|
mcp_enable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref<boolean>(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
watch(dialogVisible, (bool) => {
|
||||||
|
if (!bool) {
|
||||||
|
form.value = {
|
||||||
|
mcp_servers: '',
|
||||||
|
mcp_enable: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = (data: any) => {
|
||||||
|
form.value = { ...form.value, ...data }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
paramFormRef.value.validate().then((valid: any) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('refresh', form.value)
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.param-dialog {
|
||||||
|
padding: 8px 8px 24px 8px;
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-max-height {
|
||||||
|
height: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider {
|
||||||
|
.el-input-number.is-without-controls .el-input__wrapper {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -116,6 +116,22 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex-between">
|
||||||
|
<div>{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="openMcpServersDialog"
|
||||||
|
@refreshForm="refreshParam"
|
||||||
|
>
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item @click.prevent>
|
<el-form-item @click.prevent>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex align-center">
|
<div class="flex align-center">
|
||||||
@ -163,6 +179,7 @@
|
|||||||
ref="ReasoningParamSettingDialogRef"
|
ref="ReasoningParamSettingDialogRef"
|
||||||
@refresh="submitReasoningDialog"
|
@refresh="submitReasoningDialog"
|
||||||
/>
|
/>
|
||||||
|
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
|
||||||
</NodeContainer>
|
</NodeContainer>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -177,6 +194,7 @@ import { isLastNode } from '@/workflow/common/data'
|
|||||||
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
|
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
import ReasoningParamSettingDialog from '@/views/application/component/ReasoningParamSettingDialog.vue'
|
import ReasoningParamSettingDialog from '@/views/application/component/ReasoningParamSettingDialog.vue'
|
||||||
|
import McpServersDialog from '@/views/application/component/McpServersDialog.vue'
|
||||||
const { model } = useStore()
|
const { model } = useStore()
|
||||||
|
|
||||||
const wheel = (e: any) => {
|
const wheel = (e: any) => {
|
||||||
@ -300,6 +318,20 @@ function submitReasoningDialog(val: any) {
|
|||||||
set(props.nodeModel.properties.node_data, 'model_setting', model_setting)
|
set(props.nodeModel.properties.node_data, 'model_setting', model_setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mcpServersDialogRef = ref()
|
||||||
|
function openMcpServersDialog() {
|
||||||
|
const config = {
|
||||||
|
mcp_servers: chat_data.value.mcp_servers,
|
||||||
|
mcp_enable: chat_data.value.mcp_enable
|
||||||
|
}
|
||||||
|
mcpServersDialogRef.value.open(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitMcpServersDialog(config: any) {
|
||||||
|
set(props.nodeModel.properties.node_data, 'mcp_servers', config.mcp_servers)
|
||||||
|
set(props.nodeModel.properties.node_data, 'mcp_enable', config.mcp_enable)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getModel()
|
getModel()
|
||||||
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user