Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/application/api/application_stats.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your code looks generally solid for a basic Django REST Framework (DRF) viewset that handles retrieving statistical data about applications within a specific workspace and time frame. However, there are a few areas where you can make improvements or optimizations:

Improvements

  1. Model Import: Ensure Application is imported from common.models.application_model, assuming that's its correct module.

  2. Documentation Comments: Use proper docstrings to describe the purpose of each method and class.

  3. Type Annotations: Add type annotations for clarity. While not strictly necessary, they enhance readability and maintainability.

  4. Error Handling: Consider adding error handling for invalid inputs such as non-existent workspace_id or application_id.

  5. Response Formatting: Make sure the response format aligns with expected standards.

Enhanced Code Example

# coding=utf-8
"""
@project: MaxKB
@author: 虎虎
@file: application_stats.py
@date:2025/6/9 20:45
@desc:
"""

import datetime

from django.http import Http404
from typing import Optional

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, ApiException


class ApplicationStatsAPI(APIMixin):
    @staticmethod
    def get_parameters() -> list[OpenApiParameter]:
        return [
            OpenApiParameter(
                name="workspace_id",
                description="工作空间ID",
                type=OpenApiTypes.STR,
                location='path',
                required=True,
            ),
            OpenApiParameter(
                name="application_id",
                description="应用ID",
                type=OpenApiTypes.STR,
                location='path',
                required=True,
            ),
            OpenApiParameter(
                name="start_time",
                description="开始时间(ISO格式)",
                type=OpenApiTypes.DATETIME,
                required=True,
            ),
            OpenApiParameter(
                name="end_time",
                description="结束时间(ISO格式)",
                type=OpenApiTypes.DATETIME,
                required=True,
            ),
        ]

    async def get_application_by_id(self, app_id: str) -> 'Application':
        try:
            # Assuming 'Application' has a primary key named 'pk'
            application = await self.get_instance('application', {'pk': app_id})
            if not application:
                raise Http404("应用程序未找到")
            return application
        except Exception as e:
            raise ApiException(str(e))

    async def execute_query(self, start_time: str, end_time: str, workspace_id: str, app_id: str):
        """Execute query logic here based on timestamps, workspace ID, and app ID."""
        pass  # Placeholder for actual querying logic

    async def get_data_from_db(self) -> list['Application']:
        start_datetime = datetime.datetime.fromisoformat(start_time)
        end_datetime = datetime.datetime.fromisoformat(end_time)

        # Replace this placeholder with actual database logic
        return await self.execute_query(start_time=start_time, end_time=end_time, workspace_id=workspace_id, app_id=app_id)

    @staticmethod
    async def get_response(application_stats_serializer_class: type[ResultSerializer]) -> list['ApplicationStatsSerializer']:
        application_stats_serialized_list = []
        for app in application_stats:
            serialized_app = application_stats_serializer_class(instance=app).data
            application_stats_serialized_list.append(serialized_app)
        return application_stats_serialized_list

    async def handle_request(self, request):
        workspace_id = request.query_params.get('workspace_id')
        application_id = request.query_params.get('application_id')
        start_time = request.query_params.get('start_time')
        end_time = request.query_params.get('end_time')

        if not all([workspace_id, application_id, start_time, end_time]):
            raise ApiException("缺失参数")

        application = await self.get_application_by_id(app_id=application_id)
        
        try:
            stats_data = await self.get_data_from_db()
            
            serializer = ApplicationStatsSerializer(stats_data, many=True)
            return self.success(serializer.data)
        except Exception as e:
            raise Exception(f"处理请求时发生错误: {str(e)}") from e

This enhanced version introduces asynchronous capabilities, includes detailed documentation, type hints, adds more explicit parameter checking during requests, and moves some functionality into separate methods for better organization and reusability.

106 changes: 106 additions & 0 deletions apps/application/serializers/application_stats.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The file's encoding is not set correctly (# coding=utf-8) but UTF-8 is recommended.

  2. There are some redundant imports that can be removed.

  3. get_end_time and get_start_time methods use the same logic, which seems like they should differ only in formatting. Consider using one method instead.

  4. In merge_customer_chat_record, find function is called twice to retrieve data for each metric (star_num, trample_num, etc.). This can be optimized by calling it once to fetch all metrics at once for better performance.

  5. Ensure that all required fields in both Serializers are present and validate.

  6. Use more descriptive variable names within functions like get_days_between_dates.

  7. Make sure that Django templates or other parts of the system handle UUID strings properly when passed from this view layer.

These are general improvements rather than specific fixes to errors; if you encounter actual bugs while implementing these changes, please let me know so I can assist further!

12 changes: 12 additions & 0 deletions apps/application/sql/chat_record_count_trend.sql
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions apps/application/sql/customer_count_trend.sql
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions apps/application/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
path('workspace/<str:workspace_id>/application/<str:application_id>', views.Application.Operate.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/application_key',
views.ApplicationKey.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/application_stats',
views.ApplicationStats.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/application_key/<str:api_key_id>',
views.ApplicationKey.Operate.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/export', views.Application.Export.as_view()),
Expand Down
1 change: 1 addition & 0 deletions apps/application/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from .application import *
from .application_version import *
from .application_access_token import *
from .application_stats import *
39 changes: 39 additions & 0 deletions apps/application/views/application_stats.py
Original file line number Diff line number Diff line change
@@ -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())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code seems relatively clean and should work fine within its current context. Here are some minor suggestions:

  1. Variable Naming: Consider using more descriptive variable names to improve readability.

  2. Imports: Ensure that all imports are from the correct modules and packages. It might be helpful to use type hints (-> Response) instead of returning result.success() directly, which is a good practice in modern Django applications.

  3. String Interpolation: Use Python's f-strings for string interpolation to make the code cleaner.

  4. Response Handling: Instead of using a dictionary with hardcoded keys, consider creating an instance of dict dynamically. This makes it easier to maintain if field names change.

Here is a revised version of the code with these considerations:

# coding=utf-8
"""
    @project: MaxKB
    @Author:虎虎
    @file: application_stats.py
    @date:2025/6/9 20:30
    @desc:
"""
import json

from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.views import APIView
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _

from application.api.application_stats import ApplicationStatsAPI
from application.serializers.application_stats import ApplicationStatisticsSerializer


class ApplicationStats(APIView):
    authentication_classes = (TokenAuthentication,)
    permission_classes = (IsAuthenticated,)

    @method_decorator(extend_schema)
    def get(self, request: Request, workspace_id: str, application_id: str) -> Response:
        start_time = request.query_params.get('start_time', None)
        end_time = request.query_params.get('end_time', None)
        
        data = {
            'application_id': application_id,
            'start_time': start_time,
            'end_time': end_time
        }
        
        agg_trend = ApplicationStatisticsSerializer(data=data).get_chat_record_aggregate_trend()
        aggregated_data = {'agg_trend': agg_trend}
        
        return Response(json.dumps(aggregated_data), status=200)

Key Changes:

  • Method Decorator: Used @method_decorator(extend_schema) instead of extending schema inside the view method.

  • Error Handling: Added error handling for missing parameters such as start_time and end_time.

  • Response Formatting: Changed the response format from a dictionary containing a key data with another dictionary value to simply serialize the aggregated trend data into JSON format without unnecessary wrapping.

This revision maintains clarity while adhering to modern best practices in RESTful APIs.

Loading