feat: Support openai dialogue (#3551)

This commit is contained in:
shaohuzhang1 2025-07-10 19:16:47 +08:00 committed by GitHub
parent 691d7ceaa5
commit c370e99304
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 180 additions and 57 deletions

View File

@ -11,10 +11,17 @@ from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from chat.serializers.chat import OpenAIInstanceSerializer
from chat.serializers.chat_authentication import AnonymousAuthenticationSerializer
from common.mixins.api_mixin import APIMixin
class OpenAIAPI(APIMixin):
@staticmethod
def get_request():
return OpenAIInstanceSerializer
class ChatAuthenticationAPI(APIMixin):
@staticmethod
def get_request():

View File

@ -8,7 +8,7 @@
"""
from gettext import gettext
from typing import List
from typing import List, Dict
import uuid_utils.compat as uuid
from django.db.models import QuerySet
@ -31,6 +31,7 @@ from application.serializers.application import ApplicationOperateSerializer
from application.serializers.common import ChatInfo
from common.exception.app_exception import AppApiException, AppChatNumOutOfBoundsFailed, ChatException
from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.openai_to_response import OpenaiToResponse
from common.handle.impl.response.system_to_response import SystemToResponse
from common.utils.common import flat_map
from knowledge.models import Document, Paragraph
@ -111,6 +112,66 @@ class DebugChatSerializers(serializers.Serializer):
}).chat(instance, base_to_response)
class OpenAIMessage(serializers.Serializer):
content = serializers.CharField(required=True, label=_('content'))
role = serializers.CharField(required=True, label=_('Role'))
class OpenAIInstanceSerializer(serializers.Serializer):
messages = serializers.ListField(child=OpenAIMessage())
chat_id = serializers.UUIDField(required=False, label=_("Conversation ID"))
re_chat = serializers.BooleanField(required=False, label=_("Regenerate"))
stream = serializers.BooleanField(required=False, label=_("Streaming Output"))
class OpenAIChatSerializer(serializers.Serializer):
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
chat_user_id = serializers.CharField(required=True, label=_("Client id"))
chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
@staticmethod
def get_message(instance):
return instance.get('messages')[-1].get('content')
@staticmethod
def generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type):
if chat_id is None:
chat_id = str(uuid.uuid1())
chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, [], [],
application_id)
chat_info.set_cache()
return chat_id
def chat(self, instance: Dict, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
OpenAIInstanceSerializer(data=instance).is_valid(raise_exception=True)
chat_id = instance.get('chat_id')
message = self.get_message(instance)
re_chat = instance.get('re_chat', False)
stream = instance.get('stream', False)
application_id = self.data.get('application_id')
chat_user_id = self.data.get('chat_user_id')
chat_user_type = self.data.get('chat_user_type')
chat_id = self.generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type)
return ChatSerializers(
data={
'chat_id': chat_id,
'chat_user_id': chat_user_id,
'chat_user_type': chat_user_type,
'application_id': application_id
}
).chat({'message': message,
're_chat': re_chat,
'stream': stream,
'form_data': instance.get('form_data', {}),
'image_list': instance.get('image_list', []),
'document_list': instance.get('document_list', []),
'audio_list': instance.get('audio_list', []),
'other_list': instance.get('other_list', [])},
base_to_response=OpenaiToResponse())
class ChatSerializers(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
chat_user_id = serializers.CharField(required=True, label=_("Client id"))

View File

@ -14,6 +14,8 @@ urlpatterns = [
path('text_to_speech', views.TextToSpeech.as_view()),
path('speech_to_text', views.SpeechToText.as_view()),
path('captcha', views.CaptchaView.as_view(), name='captcha'),
path('<str:application_id>/chat/completions', views.OpenAIView.as_view(),
name='application/chat_completions'),
path('vote/chat/<str:chat_id>/chat_record/<str:chat_record_id>', views.VoteView.as_view(), name='vote'),
path('historical_conversation', views.HistoricalConversationView.as_view(), name='historical_conversation'),
path('historical_conversation/<str:chat_id>/record/<str:chat_record_id>',views.ChatRecordView.as_view(),name='conversation_details'),

View File

@ -14,14 +14,13 @@ from rest_framework.request import Request
from rest_framework.views import APIView
from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
from application.serializers.application import ApplicationOperateSerializer
from chat.api.chat_api import ChatAPI
from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI
from chat.serializers.chat import OpenChatSerializers, ChatSerializers, SpeechToTextSerializers, TextToSpeechSerializers
from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI, OpenAIAPI
from chat.serializers.chat import OpenChatSerializers, ChatSerializers, SpeechToTextSerializers, \
TextToSpeechSerializers, OpenAIChatSerializer
from chat.serializers.chat_authentication import AnonymousAuthenticationSerializer, ApplicationProfileSerializer, \
AuthProfileSerializer
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
from common.constants.permission_constants import ChatAuth
from common.exception.app_exception import AppAuthenticationFailed
from common.result import result
@ -31,6 +30,23 @@ from users.api import CaptchaAPI
from users.serializers.login import CaptchaSerializer
class OpenAIView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
methods=['POST'],
description=_('OpenAI Interface Dialogue'),
summary=_('OpenAI Interface Dialogue'),
operation_id=_('OpenAI Interface Dialogue'), # type: ignore
request=OpenAIAPI.get_request(),
responses=None,
tags=[_('Chat')] # type: ignore
)
def post(self, request: Request, application_id: str):
return OpenAIChatSerializer(data={'application_id': application_id, 'chat_user_id': request.auth.chat_user_id,
'chat_user_type': request.auth.chat_user_type}).chat(request.data)
class AnonymousAuthentication(APIView):
def options(self, request, *args, **kwargs):
return HttpResponse(

View File

@ -0,0 +1,43 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_key.py
@date2025/7/10 03:02
@desc: 应用api key认证
"""
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from application.models import ApplicationApiKey, ChatUserType
from common.auth.handle.auth_base_handle import AuthBaseHandle
from common.constants.permission_constants import Permission, Group, Operate, RoleConstants, ChatAuth
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import AppAuthenticationFailed
class ApplicationKey(AuthBaseHandle):
def handle(self, request, token: str, get_token_details):
application_api_key = QuerySet(ApplicationApiKey).filter(secret_key=token).first()
if application_api_key is None:
raise AppAuthenticationFailed(500, _('Secret key is invalid'))
if not application_api_key.is_active:
raise AppAuthenticationFailed(500, _('Secret key is invalid'))
application_setting_model = DatabaseModelManage.get_model("application_setting")
if application_setting_model is not None:
application_setting = QuerySet(application_setting_model).filter(
application_id=application_api_key.application_id).first()
if application_setting.authentication:
if application_setting.authentication != 'password':
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
return None, ChatAuth(
current_role_list=[RoleConstants.CHAT_ANONYMOUS_USER],
permission_list=[
Permission(group=Group.APPLICATION,
operate=Operate.READ)],
application_id=application_api_key.application_id,
chat_user_id=str(application_api_key.id),
chat_user_type=ChatUserType.ANONYMOUS_USER.value)
def support(self, request, token: str, get_token_details):
return str(token).startswith("application-")

View File

@ -8,8 +8,9 @@
"""
USER_TOKEN_AUTH = 'common.auth.handle.impl.user_token.UserToken'
CHAT_ANONYMOUS_USER_AURH = 'common.auth.handle.impl.chat_anonymous_user_token.ChatAnonymousUserToken'
APPLICATION_KEY_AUTH = 'common.auth.handle.impl.application_key.ApplicationKey'
AUTH_HANDLES = [
USER_TOKEN_AUTH,
CHAT_ANONYMOUS_USER_AURH
CHAT_ANONYMOUS_USER_AURH,
APPLICATION_KEY_AUTH
]

View File

@ -9,11 +9,9 @@
</h4>
<el-card shadow="never" class="overview-card" v-loading="loading">
<div class="title flex align-center">
<div
class="edit-avatar mr-12"
>
<div class="edit-avatar mr-12">
<el-avatar shape="square" :size="32" style="background: none">
<img :src="resetUrl(detail?.icon, resetUrl('./favicon.ico'))" alt=""/>
<img :src="resetUrl(detail?.icon, resetUrl('./favicon.ico'))" alt="" />
</el-avatar>
</div>
@ -23,9 +21,8 @@
<el-row :gutter="12">
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">{{
$t('views.applicationOverview.appInfo.publicAccessLink')
}}
<el-text type="info"
>{{ $t('views.applicationOverview.appInfo.publicAccessLink') }}
</el-text>
<el-switch
v-model="accessToken.is_active"
@ -55,7 +52,7 @@
style="margin-left: 1px"
>
<el-icon>
<RefreshRight/>
<RefreshRight />
</el-icon>
</el-button>
</el-tooltip>
@ -86,7 +83,7 @@
<!-- 访问限制 -->
<el-button @click="openLimitDialog" v-if="permissionPrecise.overview_access(id)">
<el-icon class="mr-4">
<Lock/>
<Lock />
</el-icon>
{{ $t('views.applicationOverview.appInfo.accessControl') }}
</el-button>
@ -96,7 +93,7 @@
v-if="permissionPrecise.overview_display(id)"
>
<el-icon class="mr-4">
<Setting/>
<Setting />
</el-icon>
{{ $t('views.applicationOverview.appInfo.displaySetting') }}
</el-button>
@ -105,14 +102,12 @@
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info"
>{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
>{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
</el-text>
</div>
<div class="mt-4 mb-16 url-height">
<div>
<el-text>API {{ $t('common.fileUpload.document') }}
</el-text
>
<el-text>API {{ $t('common.fileUpload.document') }} </el-text>
<el-button
type="primary"
link
@ -128,8 +123,8 @@
</span>
<span class="vertical-middle lighter break-all ellipsis-1">{{
baseUrl + id
}}</span>
baseUrl + id
}}</span>
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button type="primary" text @click="copyClick(baseUrl + id)">
<AppIcon iconName="app-copy"></AppIcon>
@ -143,11 +138,10 @@
v-if="permissionPrecise.overview_api_key(id)"
>
<el-icon class="mr-4">
<Key/>
<Key />
</el-icon>
{{ $t('views.applicationOverview.appInfo.apiKey') }}
</el-button
>
</el-button>
</div>
</el-col>
</el-row>
@ -183,7 +177,7 @@
/>
</div>
<div v-loading="statisticsLoading">
<StatisticsCharts :data="statisticsData"/>
<StatisticsCharts :data="statisticsData" />
</div>
</el-card>
</div>
@ -194,17 +188,17 @@
:data="detail"
:api-input-params="mapToUrlParams(apiInputParams)"
/>
<APIKeyDialog ref="APIKeyDialogRef"/>
<APIKeyDialog ref="APIKeyDialogRef" />
<!-- 社区版访问限制 -->
<component :is="currentLimitDialog" ref="LimitDialogRef" @refresh="refresh"/>
<component :is="currentLimitDialog" ref="LimitDialogRef" @refresh="refresh" />
<!-- 显示设置 -->
<component :is="currentDisplaySettingDialog" ref="DisplaySettingDialogRef" @refresh="refresh"/>
<component :is="currentDisplaySettingDialog" ref="DisplaySettingDialogRef" @refresh="refresh" />
</div>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, shallowRef, nextTick} from 'vue'
import {useRoute} from 'vue-router'
import { ref, computed, onMounted, shallowRef, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import EmbedDialog from './component/EmbedDialog.vue'
import APIKeyDialog from './component/APIKeyDialog.vue'
import LimitDialog from './component/LimitDialog.vue'
@ -213,15 +207,15 @@ import DisplaySettingDialog from './component/DisplaySettingDialog.vue'
import XPackDisplaySettingDialog from './xpack-component/XPackDisplaySettingDialog.vue'
import StatisticsCharts from './component/StatisticsCharts.vue'
import applicationApi from '@/api/application/application'
import {nowDate, beforeDay} from '@/utils/time'
import {MsgSuccess, MsgConfirm} from '@/utils/message'
import {copyClick} from '@/utils/clipboard'
import {isAppIcon, resetUrl} from '@/utils/common'
import {mapToUrlParams} from '@/utils/application'
import { nowDate, beforeDay } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { copyClick } from '@/utils/clipboard'
import { isAppIcon, resetUrl } from '@/utils/common'
import { mapToUrlParams } from '@/utils/application'
import useStore from '@/stores'
import {t} from '@/locales'
import {EditionConst} from '@/utils/permission/data'
import {hasPermission} from '@/utils/permission/index'
import { t } from '@/locales'
import { EditionConst } from '@/utils/permission/data'
import { hasPermission } from '@/utils/permission/index'
import permissionMap from '@/permission'
const route = useRoute()
@ -233,14 +227,14 @@ const permissionPrecise = computed(() => {
return permissionMap['application'][apiType.value]
})
const {user, application} = useStore()
const { user, application } = useStore()
const {
params: {id},
params: { id },
} = route as any
const apiUrl = window.location.origin + '/doc/chat/'
const baseUrl = window.location.origin + '/api/application/'
const baseUrl = window.location.origin + `${window.MaxKB.chatPrefix}/api/`
const APIKeyDialogRef = ref()
const EmbedDialogRef = ref()
@ -379,8 +373,7 @@ function refreshAccessToken() {
const str = t('views.applicationOverview.appInfo.refreshToken.refreshSuccess')
updateAccessToken(obj, str)
})
.catch(() => {
})
.catch(() => {})
}
function changeState(bool: boolean) {
@ -426,20 +419,20 @@ function getDetail() {
.map((v: any) => {
apiInputParams.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => {
return {
name: v.variable,
value: v.default_value,
}
})
return {
name: v.variable,
value: v.default_value,
}
})
: v.properties.input_field_list
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
return {
name: v.variable,
value: v.default_value,
}
})
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
return {
name: v.variable,
value: v.default_value,
}
})
: []
})
})