From 87ab2178bfe0e65262c1a9ce61a81fb300fbbda0 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 <80892890+shaohuzhang1@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:35:20 +0800 Subject: [PATCH] feat: application chat log (#3230) --- apps/application/api/application_chat.py | 135 ++++++++ .../api/application_chat_record.py | 180 +++++++++++ .../serializers/application_chat.py | 225 +++++++++++++ .../serializers/application_chat_record.py | 306 ++++++++++++++++++ .../sql/export_application_chat.sql | 40 +++ .../application/sql/list_application_chat.sql | 19 ++ apps/application/urls.py | 23 ++ apps/application/views/__init__.py | 4 +- apps/application/views/application_chat.py | 80 +++++ .../views/application_chat_record.py | 150 +++++++++ apps/common/constants/permission_constants.py | 32 +- pyproject.toml | 1 + 12 files changed, 1193 insertions(+), 2 deletions(-) create mode 100644 apps/application/api/application_chat.py create mode 100644 apps/application/api/application_chat_record.py create mode 100644 apps/application/serializers/application_chat.py create mode 100644 apps/application/serializers/application_chat_record.py create mode 100644 apps/application/sql/export_application_chat.sql create mode 100644 apps/application/sql/list_application_chat.sql create mode 100644 apps/application/views/application_chat.py create mode 100644 apps/application/views/application_chat_record.py diff --git a/apps/application/api/application_chat.py b/apps/application/api/application_chat.py new file mode 100644 index 000000000..fe009e245 --- /dev/null +++ b/apps/application/api/application_chat.py @@ -0,0 +1,135 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat.py + @date:2025/6/10 13:54 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from application.serializers.application_chat import ApplicationChatQuerySerializers, \ + ApplicationChatResponseSerializers, ApplicationChatRecordExportRequest +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer, ResultPageSerializer + + +class ApplicationChatListResponseSerializers(ResultSerializer): + def get_data(self): + return ApplicationChatResponseSerializers(many=True) + + +class ApplicationChatPageResponseSerializers(ResultPageSerializer): + def get_data(self): + return ApplicationChatResponseSerializers(many=True) + + +class ApplicationChatQueryAPI(APIMixin): + @staticmethod + def get_request(): + return ApplicationChatQuerySerializers + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="application_id", + description="application ID", + type=OpenApiTypes.STR, + location='path', + required=True, + ), OpenApiParameter( + name="start_time", + description="start Time", + type=OpenApiTypes.STR, + required=True, + ), + OpenApiParameter( + name="end_time", + description="end Time", + type=OpenApiTypes.STR, + required=True, + ), + OpenApiParameter( + name="abstract", + description="summary", + type=OpenApiTypes.STR, + required=False, + ), + OpenApiParameter( + name="min_star", + description=_("Minimum number of likes"), + type=OpenApiTypes.INT, + required=False, + ), + OpenApiParameter( + name="min_trample", + description=_("Minimum number of clicks"), + type=OpenApiTypes.INT, + required=False, + ), + OpenApiParameter( + name="comparer", + description=_("Comparator"), + type=OpenApiTypes.STR, + required=False, + ), + ] + + @staticmethod + def get_response(): + return ApplicationChatListResponseSerializers + + +class ApplicationChatQueryPageAPI(APIMixin): + @staticmethod + def get_request(): + return ApplicationChatQueryAPI.get_request() + + @staticmethod + def get_parameters(): + return [ + *ApplicationChatQueryAPI.get_parameters(), + OpenApiParameter( + name="current_page", + description=_("Current page"), + type=OpenApiTypes.INT, + location='path', + required=True, + ), + OpenApiParameter( + name="page_size", + description=_("Page size"), + type=OpenApiTypes.INT, + location='path', + required=True, + ), + + ] + + @staticmethod + def get_response(): + return ApplicationChatPageResponseSerializers + + +class ApplicationChatExportAPI(APIMixin): + @staticmethod + def get_request(): + return ApplicationChatRecordExportRequest + + @staticmethod + def get_parameters(): + return ApplicationChatQueryAPI.get_parameters() + + @staticmethod + def get_response(): + return None diff --git a/apps/application/api/application_chat_record.py b/apps/application/api/application_chat_record.py new file mode 100644 index 000000000..668a5a6f7 --- /dev/null +++ b/apps/application/api/application_chat_record.py @@ -0,0 +1,180 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat_record.py + @date:2025/6/10 15:19 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from application.serializers.application_chat_record import ApplicationChatRecordAddKnowledgeSerializer, \ + ApplicationChatRecordImproveInstanceSerializer +from common.mixins.api_mixin import APIMixin + + +class ApplicationChatRecordQueryAPI(APIMixin): + @staticmethod + def get_response(): + pass + + @staticmethod + def get_request(): + pass + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="application_id", + description="Application ID", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="chat_id", + description=_("Chat ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="order_asc", + description=_("Is it in order"), + type=OpenApiTypes.BOOL, + required=True, + ) + ] + + +class ApplicationChatRecordPageQueryAPI(APIMixin): + @staticmethod + def get_response(): + pass + + @staticmethod + def get_request(): + pass + + @staticmethod + def get_parameters(): + return [*ApplicationChatRecordQueryAPI.get_parameters(), + OpenApiParameter( + name="current_page", + description=_("Current page"), + type=OpenApiTypes.INT, + location='path', + required=True, + ), + OpenApiParameter( + name="page_size", + description=_("Page size"), + type=OpenApiTypes.INT, + location='path', + required=True, + )] + + +class ApplicationChatRecordImproveParagraphAPI(APIMixin): + @staticmethod + def get_response(): + pass + + @staticmethod + def get_request(): + return ApplicationChatRecordImproveInstanceSerializer + + @staticmethod + def get_parameters(): + return [OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="application_id", + description="Application ID", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="chat_id", + description=_("Chat ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="chat_record_id", + description=_("Chat Record ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="knowledge_id", + description=_("Knowledge ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="document_id", + description=_("Document ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + ) + ] + + class Operate(APIMixin): + @staticmethod + def get_parameters(): + return [*ApplicationChatRecordImproveParagraphAPI.get_parameters(), OpenApiParameter( + name="paragraph_id", + description=_("Paragraph ID"), + type=OpenApiTypes.STR, + location='path', + required=True, + )] + + +class ApplicationChatRecordAddKnowledgeAPI(APIMixin): + @staticmethod + def get_request(): + return ApplicationChatRecordAddKnowledgeSerializer + + @staticmethod + def get_response(): + return None + + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="application_id", + description="Application ID", + type=OpenApiTypes.STR, + location='path', + required=True, + )] diff --git a/apps/application/serializers/application_chat.py b/apps/application/serializers/application_chat.py new file mode 100644 index 000000000..1260dbd82 --- /dev/null +++ b/apps/application/serializers/application_chat.py @@ -0,0 +1,225 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat.py + @date:2025/6/10 11:06 + @desc: +""" +import datetime +import os +import re +from io import BytesIO +from typing import Dict + +import openpyxl +import pytz +from django.core import validators +from django.db import models +from django.db.models import QuerySet, Q +from django.http import StreamingHttpResponse +from django.utils.translation import gettext_lazy as _, gettext +from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE +from rest_framework import serializers + +from application.models import Chat +from common.db.search import get_dynamics_model, native_search, native_page_search +from common.utils.common import get_file_content +from maxkb.conf import PROJECT_DIR +from maxkb.settings import TIME_ZONE + + +class ApplicationChatResponseSerializers(serializers.Serializer): + id = serializers.UUIDField(required=True, label=_("chat id")) + abstract = serializers.CharField(required=True, label=_("summary")) + chat_user_id = serializers.UUIDField(required=True, label=_("Chat User ID")) + chat_user_type = serializers.CharField(required=True, label=_("Chat User Type")) + is_deleted = serializers.BooleanField(required=True, label=_("Is delete")) + application_id = serializers.UUIDField(required=True, label=_("Application ID")) + chat_record_count = serializers.IntegerField(required=True, label=_("Number of conversations")) + star_num = serializers.IntegerField(required=True, label=_("Number of Likes")) + trample_num = serializers.IntegerField(required=True, label=_("Number of thumbs-downs")) + mark_sum = serializers.IntegerField(required=True, label=_("Number of tags")) + + +class ApplicationChatRecordExportRequest(serializers.Serializer): + select_ids = serializers.ListField(required=True, label=_("Chat ID List"), + child=serializers.UUIDField(required=True, label=_("Chat ID"))) + + +class ApplicationChatQuerySerializers(serializers.Serializer): + abstract = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("summary")) + start_time = serializers.DateField(format='%Y-%m-%d', label=_("Start time")) + end_time = serializers.DateField(format='%Y-%m-%d', label=_("End time")) + application_id = serializers.UUIDField(required=True, label=_("Application ID")) + min_star = serializers.IntegerField(required=False, min_value=0, + label=_("Minimum number of likes")) + min_trample = serializers.IntegerField(required=False, min_value=0, + label=_("Minimum number of clicks")) + comparer = serializers.CharField(required=False, label=_("Comparator"), validators=[ + validators.RegexValidator(regex=re.compile("^and|or$"), + message=_("Only supports and|or"), code=500) + ]) + + def get_end_time(self): + return datetime.datetime.combine( + datetime.datetime.strptime(self.data.get('end_time'), '%Y-%m-%d'), + datetime.datetime.max.time()) + + def get_start_time(self): + return self.data.get('start_time') + + def get_query_set(self, select_ids=None): + end_time = self.get_end_time() + start_time = self.get_start_time() + query_set = QuerySet(model=get_dynamics_model( + {'application_chat.application_id': models.CharField(), + 'application_chat.abstract': models.CharField(), + "star_num": models.IntegerField(), + 'trample_num': models.IntegerField(), + 'comparer': models.CharField(), + 'application_chat.update_time': models.DateTimeField(), + 'application_chat.id': models.UUIDField(), })) + + base_query_dict = {'application_chat.application_id': self.data.get("application_id"), + 'application_chat.update_time__gte': start_time, + 'application_chat.update_time__lte': end_time, + } + if 'abstract' in self.data and self.data.get('abstract') is not None: + base_query_dict['application_chat.abstract__icontains'] = self.data.get('abstract') + + if select_ids is not None and len(select_ids) > 0: + base_query_dict['application_chat.id__in'] = select_ids + base_condition = Q(**base_query_dict) + min_star_query = None + min_trample_query = None + if 'min_star' in self.data and self.data.get('min_star') is not None: + min_star_query = Q(star_num__gte=self.data.get('min_star')) + if 'min_trample' in self.data and self.data.get('min_trample') is not None: + min_trample_query = Q(trample_num__gte=self.data.get('min_trample')) + if min_star_query is not None and min_trample_query is not None: + if self.data.get( + 'comparer') is not None and self.data.get('comparer') == 'or': + condition = base_condition & (min_star_query | min_trample_query) + else: + condition = base_condition & (min_star_query & min_trample_query) + elif min_star_query is not None: + condition = base_condition & min_star_query + elif min_trample_query is not None: + condition = base_condition & min_trample_query + else: + condition = base_condition + inner_queryset = QuerySet(Chat).filter(application_id=self.data.get("application_id")) + if 'abstract' in self.data and self.data.get('abstract') is not None: + inner_queryset = inner_queryset.filter(abstract__icontains=self.data.get('abstract')) + + return { + 'inner_queryset': inner_queryset, + 'default_queryset': query_set.filter(condition).order_by("-application_chat.update_time") + } + + def list(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return native_search(self.get_query_set(), select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'list_application_chat.sql')), + with_table_name=False) + + @staticmethod + def paragraph_list_to_string(paragraph_list): + return "\n**********\n".join( + [f"{paragraph.get('title')}:\n{paragraph.get('content')}" for paragraph in + paragraph_list] if paragraph_list is not None else '') + + @staticmethod + def to_row(row: Dict): + details = row.get('details') + padding_problem_text = ' '.join(node.get("answer", "") for key, node in details.items() if + node.get("type") == 'question-node') + search_dataset_node_list = [(key, node) for key, node in details.items() if + node.get("type") == 'search-dataset-node' or node.get( + "step_type") == 'search_step'] + reference_paragraph_len = '\n'.join([str(len(node.get('paragraph_list', + []))) if key == 'search_step' else node.get( + 'name') + ':' + str( + len(node.get('paragraph_list', [])) if node.get('paragraph_list', []) is not None else '0') for + key, node in search_dataset_node_list]) + reference_paragraph = '\n----------\n'.join( + [ApplicationChatQuerySerializers.paragraph_list_to_string(node.get('paragraph_list', + [])) if key == 'search_step' else node.get( + 'name') + ':\n' + ApplicationChatQuerySerializers.paragraph_list_to_string(node.get('paragraph_list', + [])) for + key, node in search_dataset_node_list]) + improve_paragraph_list = row.get('improve_paragraph_list') + vote_status_map = {'-1': '未投票', '0': '赞同', '1': '反对'} + return [str(row.get('chat_id')), row.get('abstract'), row.get('problem_text'), padding_problem_text, + row.get('answer_text'), vote_status_map.get(row.get('vote_status')), reference_paragraph_len, + reference_paragraph, + "\n".join([ + f"{improve_paragraph_list[index].get('title')}\n{improve_paragraph_list[index].get('content')}" + for index in range(len(improve_paragraph_list))]), + row.get('message_tokens') + row.get('answer_tokens'), row.get('run_time'), + str(row.get('create_time').astimezone(pytz.timezone(TIME_ZONE)).strftime('%Y-%m-%d %H:%M:%S') + )] + + def export(self, data, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ApplicationChatRecordExportRequest(data=data).is_valid(raise_exception=True) + data_list = native_search(self.get_query_set(data.get('select_ids')), + select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', + 'export_application_chat.sql')), + with_table_name=False) + + batch_size = 500 + + def stream_response(): + workbook = openpyxl.Workbook() + worksheet = workbook.active + worksheet.title = 'Sheet1' + + headers = [gettext('Conversation ID'), gettext('summary'), gettext('User Questions'), + gettext('Problem after optimization'), + gettext('answer'), gettext('User feedback'), + gettext('Reference segment number'), + gettext('Section title + content'), + gettext('Annotation'), gettext('Consuming tokens'), + gettext('Time consumed (s)'), + gettext('Question Time')] + for col_idx, header in enumerate(headers, 1): + cell = worksheet.cell(row=1, column=col_idx) + cell.value = header + + for i in range(0, len(data_list), batch_size): + batch_data = data_list[i:i + batch_size] + + for row_idx, row in enumerate(batch_data, start=i + 2): + for col_idx, value in enumerate(self.to_row(row), 1): + cell = worksheet.cell(row=row_idx, column=col_idx) + if isinstance(value, str): + value = re.sub(ILLEGAL_CHARACTERS_RE, '', value) + if isinstance(value, datetime.datetime): + eastern = pytz.timezone(TIME_ZONE) + c = datetime.timezone(eastern._utcoffset) + value = value.astimezone(c) + cell.value = value + + output = BytesIO() + workbook.save(output) + output.seek(0) + yield output.getvalue() + output.close() + workbook.close() + + response = StreamingHttpResponse(stream_response(), + content_type='application/vnd.open.xmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = 'attachment; filename="data.xlsx"' + return response + + def page(self, current_page: int, page_size: int, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'list_application_chat.sql')), + with_table_name=False) diff --git a/apps/application/serializers/application_chat_record.py b/apps/application/serializers/application_chat_record.py new file mode 100644 index 000000000..32c68a2f2 --- /dev/null +++ b/apps/application/serializers/application_chat_record.py @@ -0,0 +1,306 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat_record.py + @date:2025/6/10 15:10 + @desc: +""" +import uuid +from functools import reduce +from typing import Dict + +from django.db import transaction +from django.db.models import QuerySet +from rest_framework import serializers +from rest_framework.utils.formatting import lazy_format + +from application.models import ChatRecord +from common.db.search import page_search +from common.exception.app_exception import AppApiException +from common.utils.common import post +from knowledge.models import Paragraph, Document, Problem, ProblemParagraphMapping + +from django.utils.translation import gettext_lazy as _, gettext + +from knowledge.serializers.common import get_embedding_model_id_by_knowledge_id, update_document_char_length +from knowledge.serializers.paragraph import ParagraphSerializers +from knowledge.task.embedding import embedding_by_paragraph, embedding_by_paragraph_list + + +class ChatRecordSerializerModel(serializers.ModelSerializer): + class Meta: + model = ChatRecord + fields = ['id', 'chat_id', 'vote_status', 'problem_text', 'answer_text', + 'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index', + 'answer_text_list', + 'create_time', 'update_time'] + + +class ApplicationChatRecordQuerySerializers(serializers.Serializer): + application_id = serializers.UUIDField(required=True, label=_("Application ID")) + chat_id = serializers.UUIDField(required=True, label=_("Chat ID")) + order_asc = serializers.BooleanField(required=False, allow_null=True, label=_("Is it in order")) + + def list(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')) + order_by = 'create_time' if self.data.get('order_asc') is None or self.data.get( + 'order_asc') else '-create_time' + return [ChatRecordSerializerModel(chat_record).data for chat_record in + QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by)] + + @staticmethod + def reset_chat_record(chat_record): + knowledge_list = [] + paragraph_list = [] + + if 'search_step' in chat_record.details and chat_record.details.get('search_step').get( + 'paragraph_list') is not None: + paragraph_list = chat_record.details.get('search_step').get( + 'paragraph_list') + knowledge_list = [{'id': dataset_id, 'name': name} for dataset_id, name in reduce(lambda x, y: {**x, **y}, + [{row.get( + 'knowledge_id'): row.get( + "knowledge_name")} for + row in + paragraph_list], + {}).items()] + if len(chat_record.improve_paragraph_id_list) > 0: + paragraph_model_list = QuerySet(Paragraph).filter(id__in=chat_record.improve_paragraph_id_list) + if len(paragraph_model_list) < len(chat_record.improve_paragraph_id_list): + paragraph_model_id_list = [str(p.id) for p in paragraph_model_list] + chat_record.improve_paragraph_id_list = list( + filter(lambda p_id: paragraph_model_id_list.__contains__(p_id), + chat_record.improve_paragraph_id_list)) + chat_record.save() + + return { + **ChatRecordSerializerModel(chat_record).data, + 'padding_problem_text': chat_record.details.get('problem_padding').get( + 'padding_problem_text') if 'problem_padding' in chat_record.details else None, + 'knowledge_list': knowledge_list, + 'paragraph_list': paragraph_list, + 'execution_details': [chat_record.details[key] for key in chat_record.details] + } + + def page(self, current_page: int, page_size: int, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + order_by = '-create_time' if self.data.get('order_asc') is None or self.data.get( + 'order_asc') else 'create_time' + page = page_search(current_page, page_size, + QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by), + post_records_handler=lambda chat_record: self.reset_chat_record(chat_record)) + return page + + +class ParagraphModel(serializers.ModelSerializer): + class Meta: + model = Paragraph + fields = "__all__" + + +class ChatRecordImproveSerializer(serializers.Serializer): + chat_id = serializers.UUIDField(required=True, label=_("Conversation ID")) + + chat_record_id = serializers.UUIDField(required=True, + label=_("Conversation record id")) + + def get(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + chat_record_id = self.data.get('chat_record_id') + chat_id = self.data.get('chat_id') + chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first() + if chat_record is None: + raise AppApiException(500, gettext('Conversation record does not exist')) + if chat_record.improve_paragraph_id_list is None or len(chat_record.improve_paragraph_id_list) == 0: + return [] + + paragraph_model_list = QuerySet(Paragraph).filter(id__in=chat_record.improve_paragraph_id_list) + if len(paragraph_model_list) < len(chat_record.improve_paragraph_id_list): + paragraph_model_id_list = [str(p.id) for p in paragraph_model_list] + chat_record.improve_paragraph_id_list = list( + filter(lambda p_id: paragraph_model_id_list.__contains__(p_id), + chat_record.improve_paragraph_id_list)) + chat_record.save() + return [ParagraphModel(p).data for p in paragraph_model_list] + + +class ApplicationChatRecordImproveInstanceSerializer(serializers.Serializer): + title = serializers.CharField(required=False, max_length=256, allow_null=True, allow_blank=True, + label=_("Section title")) + content = serializers.CharField(required=True, label=_("Paragraph content")) + + problem_text = serializers.CharField(required=False, max_length=256, allow_null=True, allow_blank=True, + label=_("question")) + + +class ApplicationChatRecordAddKnowledgeSerializer(serializers.Serializer): + knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id")) + document_id = serializers.UUIDField(required=True, label=_("Document id")) + chat_ids = serializers.ListSerializer(child=serializers.UUIDField(), required=True, + label=_("Conversation ID")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if not Document.objects.filter(id=self.data['document_id'], knowledge_id=self.data['knowledge_id']).exists(): + raise AppApiException(500, gettext("The document id is incorrect")) + + @staticmethod + def post_embedding_paragraph(paragraph_ids, knowledge_id): + model_id = get_embedding_model_id_by_knowledge_id(knowledge_id) + embedding_by_paragraph_list(paragraph_ids, model_id) + + @post(post_function=post_embedding_paragraph) + @transaction.atomic + def post_improve(self, instance: Dict): + ApplicationChatRecordAddKnowledgeSerializer(data=instance).is_valid(raise_exception=True) + + chat_ids = instance['chat_ids'] + document_id = instance['document_id'] + knowledge_id = instance['knowledge_id'] + + # 获取所有聊天记录 + chat_record_list = list(ChatRecord.objects.filter(chat_id__in=chat_ids)) + if len(chat_record_list) < len(chat_ids): + raise AppApiException(500, gettext("Conversation records that do not exist")) + + # 批量创建段落和问题映射 + paragraphs = [] + paragraph_ids = [] + problem_paragraph_mappings = [] + for chat_record in chat_record_list: + paragraph = Paragraph( + id=uuid.uuid1(), + document_id=document_id, + content=chat_record.answer_text, + knowledge_id=knowledge_id, + title=chat_record.problem_text + ) + problem, _ = Problem.objects.get_or_create(content=chat_record.problem_text, knowledge_id=knowledge_id) + problem_paragraph_mapping = ProblemParagraphMapping( + id=uuid.uuid1(), + knowledge_id=knowledge_id, + document_id=document_id, + problem_id=problem.id, + paragraph_id=paragraph.id + ) + paragraphs.append(paragraph) + paragraph_ids.append(paragraph.id) + problem_paragraph_mappings.append(problem_paragraph_mapping) + chat_record.improve_paragraph_id_list.append(paragraph.id) + + # 批量保存段落和问题映射 + Paragraph.objects.bulk_create(paragraphs) + ProblemParagraphMapping.objects.bulk_create(problem_paragraph_mappings) + + # 批量保存聊天记录 + ChatRecord.objects.bulk_update(chat_record_list, ['improve_paragraph_id_list']) + update_document_char_length(document_id) + + return paragraph_ids, knowledge_id + + +class ApplicationChatRecordImproveSerializer(serializers.Serializer): + chat_id = serializers.UUIDField(required=True, label=_("Conversation ID")) + + chat_record_id = serializers.UUIDField(required=True, + label=_("Conversation record id")) + + knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id")) + + document_id = serializers.UUIDField(required=True, label=_("Document id")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if not QuerySet(Document).filter(id=self.data.get('document_id'), + knowledge_id=self.data.get('knowledge_id')).exists(): + raise AppApiException(500, gettext("The document id is incorrect")) + + @staticmethod + def post_embedding_paragraph(chat_record, paragraph_id, knowledge_id): + model_id = get_embedding_model_id_by_knowledge_id(knowledge_id) + # 发送向量化事件 + embedding_by_paragraph(paragraph_id, model_id) + return chat_record + + @post(post_function=post_embedding_paragraph) + @transaction.atomic + def improve(self, instance: Dict, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ApplicationChatRecordImproveInstanceSerializer(data=instance).is_valid(raise_exception=True) + chat_record_id = self.data.get('chat_record_id') + chat_id = self.data.get('chat_id') + chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first() + if chat_record is None: + raise AppApiException(500, gettext('Conversation record does not exist')) + + document_id = self.data.get("document_id") + knowledge_id = self.data.get("knowledge_id") + paragraph = Paragraph(id=uuid.uuid1(), + document_id=document_id, + content=instance.get("content"), + knowledge_id=knowledge_id, + title=instance.get("title") if 'title' in instance else '') + problem_text = instance.get('problem_text') if instance.get( + 'problem_text') is not None else chat_record.problem_text + problem, _ = QuerySet(Problem).get_or_create(content=problem_text, knowledge_id=knowledge_id) + problem_paragraph_mapping = ProblemParagraphMapping(id=uuid.uuid1(), knowledge_id=knowledge_id, + document_id=document_id, + problem_id=problem.id, + paragraph_id=paragraph.id) + # 插入段落 + paragraph.save() + # 插入关联问题 + problem_paragraph_mapping.save() + chat_record.improve_paragraph_id_list.append(paragraph.id) + update_document_char_length(document_id) + # 添加标注 + chat_record.save() + return ChatRecordSerializerModel(chat_record).data, paragraph.id, knowledge_id + + class Operate(serializers.Serializer): + chat_id = serializers.UUIDField(required=True, label=_("Conversation ID")) + + chat_record_id = serializers.UUIDField(required=True, + label=_("Conversation record id")) + + knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id")) + + document_id = serializers.UUIDField(required=True, label=_("Document id")) + + paragraph_id = serializers.UUIDField(required=True, label=_("Paragraph id")) + + workspace_id = serializers.CharField(required=True, label=_("Workspace ID")) + + def delete(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + chat_record_id = self.data.get('chat_record_id') + chat_id = self.data.get('chat_id') + knowledge_id = self.data.get('knowledge_id') + document_id = self.data.get('document_id') + paragraph_id = self.data.get('paragraph_id') + chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first() + if chat_record is None: + raise AppApiException(500, gettext('Conversation record does not exist')) + if not chat_record.improve_paragraph_id_list.__contains__(uuid.UUID(paragraph_id)): + message = lazy_format( + gettext( + 'The paragraph id is wrong. The current conversation record does not exist. [{paragraph_id}] paragraph id'), + paragraph_id=paragraph_id) + raise AppApiException(500, message.__str__()) + chat_record.improve_paragraph_id_list = [row for row in chat_record.improve_paragraph_id_list if + str(row) != paragraph_id] + chat_record.save() + o = ParagraphSerializers.Operate( + data={"workspace_id": workspace_id, "knowledge_id": knowledge_id, 'document_id': document_id, + "paragraph_id": paragraph_id}) + o.is_valid(raise_exception=True) + o.delete() + return True diff --git a/apps/application/sql/export_application_chat.sql b/apps/application/sql/export_application_chat.sql new file mode 100644 index 000000000..cc83a2396 --- /dev/null +++ b/apps/application/sql/export_application_chat.sql @@ -0,0 +1,40 @@ +SELECT + application_chat."id" as chat_id, + application_chat.abstract as abstract, + application_chat_record_temp.problem_text as problem_text, + application_chat_record_temp.answer_text as answer_text, + application_chat_record_temp.message_tokens as message_tokens, + application_chat_record_temp.answer_tokens as answer_tokens, + application_chat_record_temp.run_time as run_time, + application_chat_record_temp.details::JSON as details, + application_chat_record_temp."index" as "index", + application_chat_record_temp.improve_paragraph_list as improve_paragraph_list, + application_chat_record_temp.vote_status as vote_status, + application_chat_record_temp.create_time as create_time +FROM + application_chat application_chat + LEFT JOIN ( + SELECT COUNT + ( "id" ) AS chat_record_count, + SUM ( CASE WHEN "vote_status" = '0' THEN 1 ELSE 0 END ) AS star_num, + SUM ( CASE WHEN "vote_status" = '1' THEN 1 ELSE 0 END ) AS trample_num, + SUM ( CASE WHEN array_length( application_chat_record.improve_paragraph_id_list, 1 ) IS NULL THEN 0 ELSE array_length( application_chat_record.improve_paragraph_id_list, 1 ) END ) AS mark_sum, + chat_id + FROM + application_chat_record + WHERE chat_id IN ( + SELECT id FROM application_chat ${inner_queryset}) + GROUP BY + application_chat_record.chat_id + ) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id + LEFT JOIN ( + SELECT + *, + CASE + WHEN array_length( application_chat_record.improve_paragraph_id_list, 1 ) IS NULL THEN + '{}' ELSE ( SELECT ARRAY_AGG ( row_to_json ( paragraph ) ) FROM paragraph WHERE "id" = ANY ( application_chat_record.improve_paragraph_id_list ) ) + END as improve_paragraph_list + FROM + application_chat_record application_chat_record + ) application_chat_record_temp ON application_chat_record_temp.chat_id = application_chat."id" + ${default_queryset} \ No newline at end of file diff --git a/apps/application/sql/list_application_chat.sql b/apps/application/sql/list_application_chat.sql new file mode 100644 index 000000000..f5b6d3e57 --- /dev/null +++ b/apps/application/sql/list_application_chat.sql @@ -0,0 +1,19 @@ +SELECT + * +FROM + application_chat application_chat + LEFT JOIN ( + SELECT COUNT + ( "id" ) AS chat_record_count, + SUM ( CASE WHEN "vote_status" = '0' THEN 1 ELSE 0 END ) AS star_num, + SUM ( CASE WHEN "vote_status" = '1' THEN 1 ELSE 0 END ) AS trample_num, + SUM ( CASE WHEN array_length( application_chat_record.improve_paragraph_id_list, 1 ) IS NULL THEN 0 ELSE array_length( application_chat_record.improve_paragraph_id_list, 1 ) END ) AS mark_sum, + chat_id + FROM + application_chat_record + WHERE chat_id IN ( + SELECT id FROM application_chat ${inner_queryset}) + GROUP BY + application_chat_record.chat_id + ) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id +${default_queryset} \ No newline at end of file diff --git a/apps/application/urls.py b/apps/application/urls.py index 11351fb14..c9a7fa679 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -23,6 +23,29 @@ urlpatterns = [ views.ApplicationVersionView.as_view()), path('workspace//application//access_token', views.AccessToken.as_view()), + path('workspace//application//add_knowledge', + views.ApplicationChatRecordAddKnowledge.as_view()), + path('workspace//application//chat', + views.ApplicationChat.as_view()), + path('workspace//application//chat/export', + views.ApplicationChat.Export.as_view()), + path('workspace//application//chat//', + views.ApplicationChat.Page.as_view()), + path( + 'workspace//application//chat//chat_record', + views.ApplicationChatRecord.as_view()), + path( + 'workspace//application//chat//chat_record//', + views.ApplicationChatRecord.Page.as_view()), + path( + 'workspace//application//chat//chat_record//improve', + views.ApplicationChatRecordImprove.as_view()), + path( + 'workspace//application//chat//chat_record//knowledge//document//improve', + views.ApplicationChatRecordImproveParagraph.as_view()), + path( + 'workspace//application//chat//chat_record//knowledge//document//paragraph//improve', + views.ApplicationChatRecordImproveParagraph.Operate.as_view()), path( 'workspace//application//work_flow_version//', views.ApplicationVersionView.Page.as_view()), diff --git a/apps/application/views/__init__.py b/apps/application/views/__init__.py index 23edf317e..81c9f1581 100644 --- a/apps/application/views/__init__.py +++ b/apps/application/views/__init__.py @@ -10,4 +10,6 @@ from .application_api_key import * from .application import * from .application_version import * from .application_access_token import * -from .application_stats import * \ No newline at end of file +from .application_stats import * +from .application_chat import * +from .application_chat_record import * diff --git a/apps/application/views/application_chat.py b/apps/application/views/application_chat.py new file mode 100644 index 000000000..29b205c85 --- /dev/null +++ b/apps/application/views/application_chat.py @@ -0,0 +1,80 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat.py + @date:2025/6/10 11:00 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from application.api.application_chat import ApplicationChatQueryAPI, ApplicationChatQueryPageAPI, \ + ApplicationChatExportAPI +from application.serializers.application_chat import ApplicationChatQuerySerializers +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants +from common.result import result +from common.utils.common import query_params_to_single_dict + + +class ApplicationChat(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the conversation list"), + summary=_("Get the conversation list"), + operation_id=_("Get the conversation list"), # type: ignore + request=ApplicationChatQueryAPI.get_request(), + parameters=ApplicationChatQueryAPI.get_parameters(), + responses=ApplicationChatQueryAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def get(self, request: Request, workspace_id: str, application_id: str): + return result.success(ApplicationChatQuerySerializers( + data={**query_params_to_single_dict(request.query_params), 'application_id': application_id, + }).list()) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the conversation list by page"), + summary=_("Get the conversation list by page"), + operation_id=_("Get the conversation list by page"), # type: ignore + request=ApplicationChatQueryPageAPI.get_request(), + parameters=ApplicationChatQueryPageAPI.get_parameters(), + responses=ApplicationChatQueryPageAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int): + return result.success(ApplicationChatQuerySerializers( + data={**query_params_to_single_dict(request.query_params), 'application_id': application_id, + }).page(current_page=current_page, + page_size=page_size)) + + class Export(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + description=_("Export conversation"), + summary=_("Export conversation"), + operation_id=_("Export conversation"), # type: ignore + request=ApplicationChatExportAPI.get_request(), + parameters=ApplicationChatExportAPI.get_parameters(), + responses=ApplicationChatExportAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_EXPORT.get_workspace_application_permission()) + def post(self, request: Request, workspace_id: str, application_id: str): + return ApplicationChatQuerySerializers( + data={**query_params_to_single_dict(request.query_params), 'application_id': application_id, + }).export(request.data) diff --git a/apps/application/views/application_chat_record.py b/apps/application/views/application_chat_record.py new file mode 100644 index 000000000..ba874ee5c --- /dev/null +++ b/apps/application/views/application_chat_record.py @@ -0,0 +1,150 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_chat_record.py + @date:2025/6/10 15:08 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from application.api.application_chat_record import ApplicationChatRecordQueryAPI, \ + ApplicationChatRecordImproveParagraphAPI, ApplicationChatRecordAddKnowledgeAPI +from application.serializers.application_chat_record import ApplicationChatRecordQuerySerializers, \ + ApplicationChatRecordImproveSerializer, ChatRecordImproveSerializer, ApplicationChatRecordAddKnowledgeSerializer +from common import result +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants +from common.utils.common import query_params_to_single_dict + + +class ApplicationChatRecord(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the conversation list"), + summary=_("Get the conversation list"), + operation_id=_("Get the conversation list"), # type: ignore + request=ApplicationChatRecordQueryAPI.get_request(), + parameters=ApplicationChatRecordQueryAPI.get_parameters(), + responses=ApplicationChatRecordQueryAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str): + return result.success(ApplicationChatRecordQuerySerializers( + data={**query_params_to_single_dict(request.query_params), 'application_id': application_id, + 'chat_id': chat_id + }).list()) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the conversation record list by page"), + summary=_("Get the conversation record list by page"), + operation_id=_("Get the conversation record list by page"), # type: ignore + request=ApplicationChatRecordQueryAPI.get_request(), + parameters=ApplicationChatRecordQueryAPI.get_parameters(), + responses=ApplicationChatRecordQueryAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, current_page: int, + page_size: int): + return result.success(ApplicationChatRecordQuerySerializers( + data={**query_params_to_single_dict(request.query_params), 'application_id': application_id, + 'chat_id': chat_id}).page( + current_page=current_page, + page_size=page_size)) + + +class ApplicationChatRecordAddKnowledge(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + description=_("Add to Knowledge Base"), + summary=_("Add to Knowledge Base"), + operation_id=_("Add to Knowledge Base"), # type: ignore + request=ApplicationChatRecordAddKnowledgeAPI.get_request(), + parameters=ApplicationChatRecordAddKnowledgeAPI.get_parameters(), + responses=ApplicationChatRecordAddKnowledgeAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def post(self, request: Request, workspace_id: str, application_id: str): + return result.success(ApplicationChatRecordAddKnowledgeSerializer().post_improve(request.data)) + + +class ApplicationChatRecordImprove(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the list of marked paragraphs"), + summary=_("Get the list of marked paragraphs"), + operation_id=_("Get the list of marked paragraphs"), # type: ignore + request=ApplicationChatRecordQueryAPI.get_request(), + parameters=ApplicationChatRecordQueryAPI.get_parameters(), + responses=ApplicationChatRecordQueryAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG.get_workspace_application_permission()) + def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str): + return result.success(ChatRecordImproveSerializer( + data={'chat_id': chat_id, 'chat_record_id': chat_record_id}).get()) + + +class ApplicationChatRecordImproveParagraph(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Annotation"), + summary=_("Annotation"), + operation_id=_("Annotation"), # type: ignore + request=ApplicationChatRecordImproveParagraphAPI.get_request(), + parameters=ApplicationChatRecordImproveParagraphAPI.get_parameters(), + responses=ApplicationChatRecordImproveParagraphAPI.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission()) + def put(self, request: Request, + workspace_id: str, + application_id: str, + chat_id: str, + chat_record_id: str, + knowledge_id: str, + document_id: str): + return result.success(ApplicationChatRecordImproveSerializer( + data={'chat_id': chat_id, 'chat_record_id': chat_record_id, + 'knowledge_id': knowledge_id, 'document_id': document_id}).improve(request.data)) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['DELETE'], + description=_("Delete a Annotation"), + summary=_("Delete a Annotation"), + operation_id=_("Delete a Annotation"), # type: ignore + request=ApplicationChatRecordImproveParagraphAPI.Operate.get_request(), + parameters=ApplicationChatRecordImproveParagraphAPI.Operate.get_parameters(), + responses=ApplicationChatRecordImproveParagraphAPI.Operate.get_response(), + tags=[_("Application/Conversation Log")] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission()) + def delete(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str, + knowledge_id: str, + document_id: str, paragraph_id: str): + return result.success(ApplicationChatRecordImproveSerializer.Operate( + data={'chat_id': chat_id, 'chat_record_id': chat_record_id, 'workspace_id': workspace_id, + 'knowledge_id': knowledge_id, 'document_id': document_id, + 'paragraph_id': paragraph_id}).delete()) diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index 4abfb1b06..2abb6dc34 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -61,6 +61,7 @@ class Group(Enum): OTHER = "OTHER" OVERVIEW = "OVERVIEW" APPLICATION_ACCESS = "APPLICATION_ACCESS" + APPLICATION_CHAT_LOG = "APPLICATION_CHAT_LOG" class SystemGroup(Enum): @@ -124,6 +125,8 @@ class Operate(Enum): MIGRATE = "READ+MIGRATE" # 迁移 RELATE = "READ+RELATE" # 关联 USER_GROUP = "READ+USER_GROUP" # 用户组 + ANNOTATION = "READ+ANNOTATION" # 标注 + CLEAR_POLICY = "READ+CLEAR_POLICY" class RoleGroup(Enum): @@ -237,6 +240,8 @@ Permission_Label = { Operate.VECTOR.value: _("Vector"), Operate.MIGRATE.value: _("Migrate"), Operate.RELATE.value: _("Relate"), + Operate.ANNOTATION.value: _("Annotation"), + Operate.CLEAR_POLICY.value: _("Clear Policy"), Group.LOGIN_AUTH.value: _("Login Auth"), Group.DISPLAY_SETTINGS.value: _("Display Settings"), Group.SYSTEM_API_KEY.value: _("System API Key"), @@ -257,7 +262,8 @@ Permission_Label = { Group.SYSTEM_RES_KNOWLEDGE_PROBLEM.value: _("Problem"), Group.WORKSPACE_USER_GROUP.value: _("User Group"), Group.WORKSPACE_CHAT_USER.value: _("Chat User"), - Group.WORKSPACE_WORKSPACE: _("Workspace"), + Group.WORKSPACE_WORKSPACE.value: _("Workspace"), + Group.APPLICATION_CHAT_LOG.value: _("Dialogue log") } @@ -649,6 +655,30 @@ class PermissionConstants(Enum): resource_permission_group_list=[ResourcePermissionGroup.VIEW], label=_('Public settings') ) + APPLICATION_CHAT_LOG = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.READ, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + resource_permission_group_list=[ResourcePermissionGroup.VIEW], + label=_('Dialogue log')) + + APPLICATION_CHAT_LOG_ANNOTATION = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.ANNOTATION, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + resource_permission_group_list=[ResourcePermissionGroup.VIEW], + label=_('Dialogue log')) + + APPLICATION_CHAT_LOG_EXPORT = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.EXPORT, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + resource_permission_group_list=[ResourcePermissionGroup.VIEW], + label=_('Dialogue log')) + + APPLICATION_CHAT_LOG_CLEAR_POLICY = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.CLEAR_POLICY, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + resource_permission_group_list=[ResourcePermissionGroup.VIEW], + label=_('Dialogue log')) + APPLICATION_ACCESS_READ = Permission(group=Group.APPLICATION_ACCESS, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER], parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], diff --git a/pyproject.toml b/pyproject.toml index 1207cc947..224c7d8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ pymupdf = "1.26.0" pypdf = "5.5.0" gunicorn = "23.0.0" python-daemon = "3.1.2" +pytz = "^2025.2" [build-system] requires = ["poetry-core"]