Skip to content

Commit 64443ee

Browse files
authored
feat: Application import and export (#1836)
1 parent 390014f commit 64443ee

File tree

8 files changed

+260
-24
lines changed

8 files changed

+260
-24
lines changed

apps/application/serializers/application_serializers.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import hashlib
1111
import json
1212
import os
13+
import pickle
1314
import re
1415
import uuid
1516
from functools import reduce
@@ -19,10 +20,10 @@
1920
from django.core import cache, validators
2021
from django.core import signing
2122
from django.db import transaction, models
22-
from django.db.models import QuerySet, Q
23+
from django.db.models import QuerySet
2324
from django.http import HttpResponse
2425
from django.template import Template, Context
25-
from rest_framework import serializers
26+
from rest_framework import serializers, status
2627

2728
from application.flow.workflow_manage import Flow
2829
from application.models import Application, ApplicationDatasetMapping, ApplicationTypeChoices, WorkFlowVersion
@@ -34,15 +35,17 @@
3435
from common.db.search import get_dynamics_model, native_search, native_page_search
3536
from common.db.sql_execute import select_list
3637
from common.exception.app_exception import AppApiException, NotFound404, AppUnauthorizedFailed
37-
from common.field.common import UploadedImageField
38+
from common.field.common import UploadedImageField, UploadedFileField
3839
from common.models.db_model_manage import DBModelManage
40+
from common.response import result
3941
from common.util.common import valid_license, password_encrypt
4042
from common.util.field_message import ErrMessage
4143
from common.util.file_util import get_file_content
4244
from dataset.models import DataSet, Document, Image
4345
from dataset.serializers.common_serializers import list_paragraph, get_embedding_model_by_dataset_id_list
4446
from embedding.models import SearchMode
45-
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer
47+
from function_lib.models.function import FunctionLib, PermissionType
48+
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer, FunctionLibModelSerializer
4649
from setting.models import AuthOperate
4750
from setting.models.model_management import Model
4851
from setting.models_provider import get_model_credential
@@ -54,6 +57,13 @@
5457
chat_cache = cache.caches['chat_cache']
5558

5659

60+
class MKInstance:
61+
def __init__(self, application: dict, function_lib_list: List[dict], version: str):
62+
self.application = application
63+
self.function_lib_list = function_lib_list
64+
self.version = version
65+
66+
5767
class ModelDatasetAssociation(serializers.Serializer):
5868
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
5969
model_id = serializers.CharField(required=False, allow_null=True, allow_blank=True,
@@ -662,6 +672,72 @@ def edit(self, with_valid=True):
662672
get_application_access_token(application_access_token.access_token, False)
663673
return {**ApplicationSerializer.Query.reset_application(ApplicationSerializerModel(application).data)}
664674

675+
class Import(serializers.Serializer):
676+
file = UploadedFileField(required=True, error_messages=ErrMessage.image("文件"))
677+
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
678+
679+
@valid_license(model=Application, count=5,
680+
message='社区版最多支持 5 个应用,如需拥有更多应用,请联系我们(https://fit2cloud.com/)。')
681+
@transaction.atomic
682+
def import_(self, with_valid=True):
683+
if with_valid:
684+
self.is_valid()
685+
user_id = self.data.get('user_id')
686+
mk_instance_bytes = self.data.get('file').read()
687+
mk_instance = pickle.loads(mk_instance_bytes)
688+
application = mk_instance.application
689+
function_lib_list = mk_instance.function_lib_list
690+
if len(function_lib_list) > 0:
691+
function_lib_id_list = [function_lib.get('id') for function_lib in function_lib_list]
692+
exits_function_lib_id_list = [str(function_lib.id) for function_lib in
693+
QuerySet(FunctionLib).filter(id__in=function_lib_id_list)]
694+
# 获取到需要插入的函数
695+
function_lib_list = [function_lib for function_lib in function_lib_list if
696+
not exits_function_lib_id_list.__contains__(function_lib.get('id'))]
697+
application_model = self.to_application(application, user_id)
698+
function_lib_model_list = [self.to_function_lib(f, user_id) for f in function_lib_list]
699+
application_model.save()
700+
QuerySet(FunctionLib).bulk_create(function_lib_model_list) if len(function_lib_model_list) > 0 else None
701+
return True
702+
703+
@staticmethod
704+
def to_application(application, user_id):
705+
work_flow = application.get('work_flow')
706+
for node in work_flow.get('nodes', []):
707+
if node.get('type') == 'search-dataset-node':
708+
node.get('properties', {}).get('node_data', {})['dataset_id_list'] = []
709+
return Application(id=uuid.uuid1(), user_id=user_id, name=application.get('name'),
710+
desc=application.get('desc'),
711+
prologue=application.get('prologue'), dialogue_number=application.get('dialogue_number'),
712+
dataset_setting=application.get('dataset_setting'),
713+
model_params_setting=application.get('model_params_setting'),
714+
tts_model_params_setting=application.get('tts_model_params_setting'),
715+
problem_optimization=application.get('problem_optimization'),
716+
icon=application.get('icon'),
717+
work_flow=work_flow,
718+
type=application.get('type'),
719+
problem_optimization_prompt=application.get('problem_optimization_prompt'),
720+
tts_model_enable=application.get('tts_model_enable'),
721+
stt_model_enable=application.get('stt_model_enable'),
722+
tts_type=application.get('tts_type'),
723+
clean_time=application.get('clean_time'),
724+
file_upload_enable=application.get('file_upload_enable'),
725+
file_upload_setting=application.get('file_upload_setting'),
726+
)
727+
728+
@staticmethod
729+
def to_function_lib(function_lib, user_id):
730+
"""
731+
732+
@param user_id: 用户id
733+
@param function_lib: 函数库
734+
@return:
735+
"""
736+
return FunctionLib(id=function_lib.get('id'), user_id=user_id, name=function_lib.get('name'),
737+
code=function_lib.get('code'), input_field_list=function_lib.get('input_field_list'),
738+
is_active=function_lib.get('is_active'),
739+
permission_type=PermissionType.PRIVATE)
740+
665741
class Operate(serializers.Serializer):
666742
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id"))
667743
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
@@ -708,6 +784,31 @@ def delete(self, with_valid=True):
708784
QuerySet(Application).filter(id=self.data.get('application_id')).delete()
709785
return True
710786

787+
def export(self, with_valid=True):
788+
try:
789+
if with_valid:
790+
self.is_valid()
791+
application_id = self.data.get('application_id')
792+
application = QuerySet(Application).filter(id=application_id).first()
793+
function_lib_id_list = [node.get('properties', {}).get('node_data', {}).get('function_lib_id') for node
794+
in
795+
application.work_flow.get('nodes', []) if
796+
node.get('type') == 'function-lib-node']
797+
function_lib_list = []
798+
if len(function_lib_id_list) > 0:
799+
function_lib_list = QuerySet(FunctionLib).filter(id__in=function_lib_id_list)
800+
application_dict = ApplicationSerializerModel(application).data
801+
802+
mk_instance = MKInstance(application_dict,
803+
[FunctionLibModelSerializer(function_lib).data for function_lib in
804+
function_lib_list], 'v1')
805+
application_pickle = pickle.dumps(mk_instance)
806+
response = HttpResponse(content_type='text/plain', content=application_pickle)
807+
response['Content-Disposition'] = f'attachment; filename="{application.name}.mk"'
808+
return response
809+
except Exception as e:
810+
return result.error(str(e), response_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
811+
711812
@transaction.atomic
712813
def publish(self, instance, with_valid=True):
713814
if with_valid:

apps/application/swagger_api/application_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,27 @@ def get_request_params_api():
336336
description='应用描述')
337337
]
338338

339+
class Export(ApiMixin):
340+
@staticmethod
341+
def get_request_params_api():
342+
return [openapi.Parameter(name='application_id',
343+
in_=openapi.IN_PATH,
344+
type=openapi.TYPE_STRING,
345+
required=True,
346+
description='应用id'),
347+
348+
]
349+
350+
class Import(ApiMixin):
351+
@staticmethod
352+
def get_request_params_api():
353+
return [openapi.Parameter(name='file',
354+
in_=openapi.IN_FORM,
355+
type=openapi.TYPE_FILE,
356+
required=True,
357+
description='上传图片文件')
358+
]
359+
339360
class Operate(ApiMixin):
340361
@staticmethod
341362
def get_request_params_api():

apps/application/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
app_name = "application"
66
urlpatterns = [
77
path('application', views.Application.as_view(), name="application"),
8+
path('application/import', views.Application.Import.as_view()),
89
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
910
path('application/embed', views.Application.Embed.as_view()),
1011
path('application/authentication', views.Application.Authentication.as_view()),
1112
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
1213
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
14+
path('application/<str:application_id>/export', views.Application.Export.as_view()),
1315
path('application/<str:application_id>/statistics/customer_count',
1416
views.ApplicationStatistics.CustomerCount.as_view()),
1517
path('application/<str:application_id>/statistics/customer_count_trend',

apps/application/views/application_views.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from common.swagger_api.common_api import CommonApi
2828
from common.util.common import query_params_to_single_dict
2929
from dataset.serializers.dataset_serializers import DataSetSerializers
30-
from setting.swagger_api.provide_api import ProvideApi
3130

3231
chat_cache = cache.caches['chat_cache']
3332

@@ -158,6 +157,34 @@ def put(self, request: Request, application_id: str):
158157
data={'application_id': application_id, 'user_id': request.user.id,
159158
'image': request.FILES.get('file')}).edit(request.data))
160159

160+
class Import(APIView):
161+
authentication_classes = [TokenAuth]
162+
parser_classes = [MultiPartParser]
163+
164+
@action(methods="GET", detail=False)
165+
@swagger_auto_schema(operation_summary="导入应用", operation_id="导入应用",
166+
manual_parameters=ApplicationApi.Import.get_request_params_api(),
167+
tags=["应用"]
168+
)
169+
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
170+
def post(self, request: Request):
171+
return result.success(ApplicationSerializer.Import(
172+
data={'user_id': request.user.id, 'file': request.FILES.get('file')}).import_())
173+
174+
class Export(APIView):
175+
authentication_classes = [TokenAuth]
176+
177+
@action(methods="GET", detail=False)
178+
@swagger_auto_schema(operation_summary="导出应用", operation_id="导出应用",
179+
manual_parameters=ApplicationApi.Export.get_request_params_api(),
180+
tags=["应用"]
181+
)
182+
@has_permissions(lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
183+
dynamic_tag=keywords.get('application_id')))
184+
def get(self, request: Request, application_id: str):
185+
return ApplicationSerializer.Operate(
186+
data={'application_id': application_id, 'user_id': request.user.id}).export()
187+
161188
class Embed(APIView):
162189
@action(methods=["GET"], detail=False)
163190
@swagger_auto_schema(operation_summary="获取嵌入js",
@@ -362,7 +389,8 @@ class AccessToken(APIView):
362389
compare=CompareConstants.AND))
363390
def put(self, request: Request, application_id: str):
364391
return result.success(
365-
ApplicationSerializer.AccessTokenSerializer(data={'application_id': application_id}).edit(request.data))
392+
ApplicationSerializer.AccessTokenSerializer(data={'application_id': application_id}).edit(
393+
request.data))
366394

367395
@action(methods=['GET'], detail=False)
368396
@swagger_auto_schema(operation_summary="获取应用 AccessToken信息",
@@ -382,9 +410,10 @@ def get(self, request: Request, application_id: str):
382410
class Authentication(APIView):
383411
@action(methods=['OPTIONS'], detail=False)
384412
def options(self, request, *args, **kwargs):
385-
return HttpResponse(headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
386-
"Access-Control-Allow-Methods": "POST",
387-
"Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
413+
return HttpResponse(
414+
headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
415+
"Access-Control-Allow-Methods": "POST",
416+
"Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
388417

389418
@action(methods=['POST'], detail=False)
390419
@swagger_auto_schema(operation_summary="应用认证",
@@ -404,6 +433,7 @@ def post(self, request: Request):
404433
)
405434

406435
@action(methods=['POST'], detail=False)
436+
407437
@swagger_auto_schema(operation_summary="创建应用",
408438
operation_id="创建应用",
409439
request_body=ApplicationApi.Create.get_request_body_api(),
@@ -444,7 +474,8 @@ def get(self, request: Request, application_id: str):
444474
"query_text": request.query_params.get("query_text"),
445475
"top_number": request.query_params.get("top_number"),
446476
'similarity': request.query_params.get('similarity'),
447-
'search_mode': request.query_params.get('search_mode')}).hit_test(
477+
'search_mode': request.query_params.get(
478+
'search_mode')}).hit_test(
448479
))
449480

450481
class Publish(APIView):
@@ -502,7 +533,8 @@ def delete(self, request: Request, application_id: str):
502533
compare=CompareConstants.AND))
503534
def put(self, request: Request, application_id: str):
504535
return result.success(
505-
ApplicationSerializer.Operate(data={'application_id': application_id, 'user_id': request.user.id}).edit(
536+
ApplicationSerializer.Operate(
537+
data={'application_id': application_id, 'user_id': request.user.id}).edit(
506538
request.data))
507539

508540
@action(methods=['GET'], detail=False)
@@ -528,11 +560,14 @@ class ListApplicationDataSet(APIView):
528560
@swagger_auto_schema(operation_summary="获取当前应用可使用的知识库",
529561
operation_id="获取当前应用可使用的知识库",
530562
manual_parameters=ApplicationApi.Operate.get_request_params_api(),
531-
responses=result.get_api_array_response(DataSetSerializers.Query.get_response_body_api()),
563+
responses=result.get_api_array_response(
564+
DataSetSerializers.Query.get_response_body_api()),
532565
tags=['应用'])
533566
@has_permissions(ViewPermission([RoleConstants.ADMIN, RoleConstants.USER],
534-
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
535-
dynamic_tag=keywords.get('application_id'))],
567+
[lambda r, keywords: Permission(group=Group.APPLICATION,
568+
operate=Operate.USE,
569+
dynamic_tag=keywords.get(
570+
'application_id'))],
536571
compare=CompareConstants.AND))
537572
def get(self, request: Request, application_id: str):
538573
return result.success(ApplicationSerializer.Operate(

apps/common/response/result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ def success(data, **kwargs):
157157
return Result(data=data, **kwargs)
158158

159159

160-
def error(message):
160+
def error(message, **kwargs):
161161
"""
162162
获取一个失败的响应对象
163163
:param message: 错误提示
164164
:return: 接口响应对象
165165
"""
166-
return Result(code=500, message=message)
166+
return Result(code=500, message=message, **kwargs)

ui/src/api/application.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Result } from '@/request/Result'
2-
import { get, post, postStream, del, put, request, download } from '@/request/index'
2+
import { get, post, postStream, del, put, request, download, exportFile } from '@/request/index'
33
import type { pageRequest } from '@/api/type/common'
44
import type { ApplicationFormType } from '@/api/type/application'
55
import { type Ref } from 'vue'
@@ -300,7 +300,6 @@ const getApplicationTTIModel: (
300300
return get(`${prefix}/${application_id}/model`, { model_type: 'TTI' }, loading)
301301
}
302302

303-
304303
/**
305304
* 发布应用
306305
* @param 参数
@@ -377,7 +376,6 @@ const uploadFile: (
377376
return post(`${prefix}/${application_id}/chat/${chat_id}/upload_file`, data, undefined, loading)
378377
}
379378

380-
381379
/**
382380
* 语音转文本
383381
*/
@@ -503,6 +501,28 @@ const getUserList: (type: string, loading?: Ref<boolean>) => Promise<Result<any>
503501
return get(`/user/list/${type}`, undefined, loading)
504502
}
505503

504+
const exportApplication = (
505+
application_id: string,
506+
application_name: string,
507+
loading?: Ref<boolean>
508+
) => {
509+
return exportFile(
510+
application_name + '.mk',
511+
`/application/${application_id}/export`,
512+
undefined,
513+
loading
514+
)
515+
}
516+
517+
/**
518+
* 导入应用
519+
*/
520+
const importApplication: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
521+
data,
522+
loading
523+
) => {
524+
return post(`${prefix}/import`, data, undefined, loading)
525+
}
506526
export default {
507527
getAllAppilcation,
508528
getApplication,
@@ -544,5 +564,7 @@ export default {
544564
playDemoText,
545565
getUserList,
546566
getApplicationList,
547-
uploadFile
567+
uploadFile,
568+
exportApplication,
569+
importApplication
548570
}

0 commit comments

Comments
 (0)