diff --git a/apps/application/api/application_stats.py b/apps/application/api/application_stats.py new file mode 100644 index 000000000..0b584cfd0 --- /dev/null +++ b/apps/application/api/application_stats.py @@ -0,0 +1,55 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_stats.py + @date:2025/6/9 20:45 + @desc: +""" +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from application.serializers.application_stats import ApplicationStatsSerializer +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer + + +class ApplicationStatsResult(ResultSerializer): + def get_data(self): + return ApplicationStatsSerializer(many=True) + + +class ApplicationStatsAPI(APIMixin): + @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, + ), + ] + + @staticmethod + def get_response(): + return ApplicationStatsResult diff --git a/apps/application/serializers/application_stats.py b/apps/application/serializers/application_stats.py new file mode 100644 index 000000000..8ebfe1332 --- /dev/null +++ b/apps/application/serializers/application_stats.py @@ -0,0 +1,106 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_stats.py + @date:2025/6/9 20:34 + @desc: +""" +import datetime +import os +from typing import Dict, List + +from django.db import models +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.models import ApplicationChatUserStats +from common.db.search import native_search, get_dynamics_model +from common.utils.common import get_file_content +from maxkb.conf import PROJECT_DIR + + +class ApplicationStatsSerializer(serializers.Serializer): + chat_record_count = serializers.IntegerField(required=True, label=_("Number of conversations")) + customer_added_count = serializers.IntegerField(required=True, label=_("Number of new users")) + customer_num = serializers.IntegerField(required=True, label=_("Total number of users")) + day = serializers.CharField(required=True, label=_("date")) + star_num = serializers.IntegerField(required=True, label=_("Number of Likes")) + tokens_num = serializers.IntegerField(required=True, label=_("Tokens consumption")) + trample_num = serializers.IntegerField(required=True, label=_("Number of thumbs-downs")) + + +class ApplicationStatisticsSerializer(serializers.Serializer): + application_id = serializers.UUIDField(required=True, label=_("Application ID")) + start_time = serializers.DateField(format='%Y-%m-%d', label=_("Start time")) + end_time = serializers.DateField(format='%Y-%m-%d', label=_("End time")) + + 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_customer_count_trend(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + start_time = self.get_start_time() + end_time = self.get_end_time() + return native_search( + {'default_sql': QuerySet(ApplicationChatUserStats).filter( + application_id=self.data.get('application_id'), + create_time__gte=start_time, + create_time__lte=end_time)}, + select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'customer_count_trend.sql'))) + + def get_chat_record_aggregate_trend(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + start_time = self.get_start_time() + end_time = self.get_end_time() + chat_record_aggregate_trend = native_search( + {'default_sql': QuerySet(model=get_dynamics_model( + {'application_chat.application_id': models.UUIDField(), + 'application_chat_record.create_time': models.DateTimeField()})).filter( + **{'application_chat.application_id': self.data.get('application_id'), + 'application_chat_record.create_time__gte': start_time, + 'application_chat_record.create_time__lte': end_time} + )}, + select_string=get_file_content( + os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'chat_record_count_trend.sql'))) + customer_count_trend = self.get_customer_count_trend(with_valid=False) + return self.merge_customer_chat_record(chat_record_aggregate_trend, customer_count_trend) + + def merge_customer_chat_record(self, chat_record_aggregate_trend: List[Dict], customer_count_trend: List[Dict]): + + return [{**self.find(chat_record_aggregate_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day, + {'star_num': 0, 'trample_num': 0, 'tokens_num': 0, 'chat_record_count': 0, + 'customer_num': 0, + 'day': day}), + **self.find(customer_count_trend, lambda c: c.get('day').strftime('%Y-%m-%d') == day, + {'customer_added_count': 0})} + for + day in + self.get_days_between_dates(self.data.get('start_time'), self.data.get('end_time'))] + + @staticmethod + def find(source_list, condition, default): + value_list = [row for row in source_list if condition(row)] + if len(value_list) > 0: + return value_list[0] + return default + + @staticmethod + def get_days_between_dates(start_date, end_date): + start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d') + end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') + days = [] + current_date = start_date + while current_date <= end_date: + days.append(current_date.strftime('%Y-%m-%d')) + current_date += datetime.timedelta(days=1) + return days diff --git a/apps/application/sql/chat_record_count_trend.sql b/apps/application/sql/chat_record_count_trend.sql new file mode 100644 index 000000000..44fc43c54 --- /dev/null +++ b/apps/application/sql/chat_record_count_trend.sql @@ -0,0 +1,12 @@ +SELECT SUM + ( CASE WHEN application_chat_record.vote_status = '0' THEN 1 ELSE 0 END ) AS "star_num", + SUM ( CASE WHEN application_chat_record.vote_status = '1' THEN 1 ELSE 0 END ) AS "trample_num", + SUM ( application_chat_record.message_tokens + application_chat_record.answer_tokens ) as "tokens_num", + "count"(application_chat_record."id") as chat_record_count, + "count"(DISTINCT application_chat.chat_user_id) customer_num, + application_chat_record.create_time :: DATE as "day" +FROM + application_chat_record application_chat_record + LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id +${default_sql} +GROUP BY "day" \ No newline at end of file diff --git a/apps/application/sql/customer_count_trend.sql b/apps/application/sql/customer_count_trend.sql new file mode 100644 index 000000000..a20865bad --- /dev/null +++ b/apps/application/sql/customer_count_trend.sql @@ -0,0 +1,7 @@ +SELECT + COUNT ( "application_chat_user_stats"."id" ) AS "customer_added_count", + create_time :: DATE as "day" +FROM + "application_chat_user_stats" +${default_sql} +GROUP BY "day" \ No newline at end of file diff --git a/apps/application/urls.py b/apps/application/urls.py index 4dc769896..11351fb14 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path('workspace//application/', views.Application.Operate.as_view()), path('workspace//application//application_key', views.ApplicationKey.as_view()), + path('workspace//application//application_stats', + views.ApplicationStats.as_view()), path('workspace//application//application_key/', views.ApplicationKey.Operate.as_view()), path('workspace//application//export', views.Application.Export.as_view()), diff --git a/apps/application/views/__init__.py b/apps/application/views/__init__.py index 1733aeba6..23edf317e 100644 --- a/apps/application/views/__init__.py +++ b/apps/application/views/__init__.py @@ -10,3 +10,4 @@ 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 diff --git a/apps/application/views/application_stats.py b/apps/application/views/application_stats.py new file mode 100644 index 000000000..ba6c33eac --- /dev/null +++ b/apps/application/views/application_stats.py @@ -0,0 +1,39 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_stats.py + @date:2025/6/9 20:30 + @desc: +""" +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from application.api.application_stats import ApplicationStatsAPI +from application.serializers.application_stats import ApplicationStatisticsSerializer +from common import result +from common.auth import TokenAuth +from django.utils.translation import gettext_lazy as _ + + +class ApplicationStats(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_('Dialogue-related statistical trends'), + summary=_('Dialogue-related statistical trends'), + operation_id=_('Dialogue-related statistical trends'), # type: ignore + parameters=ApplicationStatsAPI.get_parameters(), + responses=ApplicationStatsAPI.get_response(), + tags=[_('Application')] # type: ignore + ) + def get(self, request: Request, workspace_id: str, application_id: str): + return result.success( + ApplicationStatisticsSerializer(data={'application_id': application_id, + 'start_time': request.query_params.get( + 'start_time'), + 'end_time': request.query_params.get( + 'end_time') + }).get_chat_record_aggregate_trend())