mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Support openai dialogue (#3551)
This commit is contained in:
parent
691d7ceaa5
commit
c370e99304
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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-")
|
||||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
: []
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue