From c370e993045be7500378112c726664d2adcc37ec Mon Sep 17 00:00:00 2001 From: shaohuzhang1 <80892890+shaohuzhang1@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:16:47 +0800 Subject: [PATCH] feat: Support openai dialogue (#3551) --- apps/chat/api/chat_authentication_api.py | 7 ++ apps/chat/serializers/chat.py | 63 ++++++++++++- apps/chat/urls.py | 2 + apps/chat/views/chat.py | 24 ++++- .../auth/handle/impl/application_key.py | 43 +++++++++ apps/maxkb/settings/auth.py | 5 +- ui/src/views/application-overview/index.vue | 93 +++++++++---------- 7 files changed, 180 insertions(+), 57 deletions(-) create mode 100644 apps/common/auth/handle/impl/application_key.py diff --git a/apps/chat/api/chat_authentication_api.py b/apps/chat/api/chat_authentication_api.py index 89fbd1450..3e65b739c 100644 --- a/apps/chat/api/chat_authentication_api.py +++ b/apps/chat/api/chat_authentication_api.py @@ -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(): diff --git a/apps/chat/serializers/chat.py b/apps/chat/serializers/chat.py index fde79abf7..4398ebbd3 100644 --- a/apps/chat/serializers/chat.py +++ b/apps/chat/serializers/chat.py @@ -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")) diff --git a/apps/chat/urls.py b/apps/chat/urls.py index bb7f87e2b..b31b3fcc7 100644 --- a/apps/chat/urls.py +++ b/apps/chat/urls.py @@ -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('/chat/completions', views.OpenAIView.as_view(), + name='application/chat_completions'), path('vote/chat//chat_record/', views.VoteView.as_view(), name='vote'), path('historical_conversation', views.HistoricalConversationView.as_view(), name='historical_conversation'), path('historical_conversation//record/',views.ChatRecordView.as_view(),name='conversation_details'), diff --git a/apps/chat/views/chat.py b/apps/chat/views/chat.py index 8d2ca52f1..89a70569a 100644 --- a/apps/chat/views/chat.py +++ b/apps/chat/views/chat.py @@ -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( diff --git a/apps/common/auth/handle/impl/application_key.py b/apps/common/auth/handle/impl/application_key.py new file mode 100644 index 000000000..4667684a1 --- /dev/null +++ b/apps/common/auth/handle/impl/application_key.py @@ -0,0 +1,43 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_key.py + @date:2025/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-") diff --git a/apps/maxkb/settings/auth.py b/apps/maxkb/settings/auth.py index 739536b1f..7c6bbd97a 100644 --- a/apps/maxkb/settings/auth.py +++ b/apps/maxkb/settings/auth.py @@ -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 ] diff --git a/ui/src/views/application-overview/index.vue b/ui/src/views/application-overview/index.vue index fe1210cff..8da2409a9 100644 --- a/ui/src/views/application-overview/index.vue +++ b/ui/src/views/application-overview/index.vue @@ -9,11 +9,9 @@
-
+
- +
@@ -23,9 +21,8 @@
- {{ - $t('views.applicationOverview.appInfo.publicAccessLink') - }} + {{ $t('views.applicationOverview.appInfo.publicAccessLink') }} - + @@ -86,7 +83,7 @@ - + {{ $t('views.applicationOverview.appInfo.accessControl') }} @@ -96,7 +93,7 @@ v-if="permissionPrecise.overview_display(id)" > - + {{ $t('views.applicationOverview.appInfo.displaySetting') }} @@ -105,14 +102,12 @@
{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }} + >{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
- API {{ $t('common.fileUpload.document') }}: - + API {{ $t('common.fileUpload.document') }}: {{ - baseUrl + id - }} + baseUrl + id + }} @@ -143,11 +138,10 @@ v-if="permissionPrecise.overview_api_key(id)" > - + {{ $t('views.applicationOverview.appInfo.apiKey') }} - +
@@ -183,7 +177,7 @@ />
- +
@@ -194,17 +188,17 @@ :data="detail" :api-input-params="mapToUrlParams(apiInputParams)" /> - + - + - +