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
336 changes: 276 additions & 60 deletions apps/application/serializers/application_stats.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/application/sql/get_token_usage.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SELECT SUM(application_chat_record.message_tokens + application_chat_record.answer_tokens) as "token_usage",
COUNT(application_chat_record."id") as "chat_record_count",
MAX(COALESCE(application_chat.asker ->>'username', '游客')) as "username"
FROM application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id
Expand Down
3 changes: 2 additions & 1 deletion apps/application/sql/get_token_usage_ee.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SELECT SUM(application_chat_record.message_tokens + application_chat_record.answer_tokens) as "token_usage",
COUNT(application_chat_record."id") as "chat_record_count",
MAX(COALESCE(chat_user.username, application_chat.asker ->>'username', '游客')) as "username"
FROM application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id
Expand All @@ -7,4 +8,4 @@ FROM application_chat_record application_chat_record
GROUP BY
application_chat.chat_user_id
ORDER BY
"token_usage" DESC
"token_usage" DESC
1 change: 1 addition & 0 deletions apps/application/sql/top_questions.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SELECT COUNT(application_chat_record."id") AS chat_record_count,
SUM(application_chat_record.message_tokens + application_chat_record.answer_tokens) AS token_usage,
MAX(COALESCE(application_chat.asker ->>'username', '游客')) as "username"
FROM application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id
Expand Down
1 change: 1 addition & 0 deletions apps/application/sql/top_questions_ee.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SELECT COUNT(application_chat_record."id") AS chat_record_count,
SUM(application_chat_record.message_tokens + application_chat_record.answer_tokens) AS token_usage,
MAX(COALESCE(chat_user.username, application_chat.asker ->>'username', '游客')) as "username"
FROM application_chat_record application_chat_record
LEFT JOIN application_chat application_chat ON application_chat."id" = application_chat_record.chat_id
Expand Down
186 changes: 186 additions & 0 deletions apps/application/tests/test_application_ranking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# coding=utf-8
"""应用概览用户排行(分页 / 汇总 / 导出)测试。"""

import io

from django.test import TestCase
from django.utils import timezone
from openpyxl import load_workbook

from application.models import Application, ApplicationFolder, Chat, ChatRecord
from application.serializers.application_stats import ApplicationStatisticsSerializer
from users.models import User


def make_user(username):
return User.objects.create(
username=username,
nick_name=username,
password="x",
role="USER",
source="LOCAL",
)


class ApplicationRankingSerializerTests(TestCase):
def setUp(self):
today = timezone.localdate()
self.today_str = today.strftime("%Y-%m-%d")
self.user = make_user("owner")
folder = ApplicationFolder.objects.create(
id="default",
name="默认分组",
workspace_id="default",
user=self.user,
)
self.application = Application.objects.create(
name="测试应用",
workspace_id="default",
folder=folder,
user=self.user,
model_setting={"prompt": "hello"},
model_params_setting={},
knowledge_setting={},
icon="./favicon.ico",
work_flow={},
)
self.serializer_data = {
"application_id": str(self.application.id),
"workspace_id": "default",
"start_time": self.today_str,
"end_time": self.today_str,
}

@staticmethod
def make_chat(application, chat_user_id, username, create_time=None):
chat = Chat.objects.create(
application=application,
abstract="",
chat_user_id=chat_user_id,
chat_user_type="ANONYMOUS_USER",
asker={"username": username},
)
if create_time is not None:
Chat.objects.filter(id=chat.id).update(create_time=create_time)
return chat

@staticmethod
def make_record(chat, message_tokens, answer_tokens, create_time=None):
record = ChatRecord.objects.create(
chat=chat,
problem_text="问题",
answer_text="答案",
message_tokens=message_tokens,
answer_tokens=answer_tokens,
index=1,
)
if create_time is not None:
ChatRecord.objects.filter(id=record.id).update(create_time=create_time)
return record

def test_user_tokens_ranking_order_and_pagination(self):
# u1: 30 tokens, u2: 50 tokens, u3: 40 tokens
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 10, 20)
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 30, 20)
c3 = self.make_chat(self.application, "u3", "carol")
self.make_record(c3, 20, 20)

page = ApplicationStatisticsSerializer(data=self.serializer_data).get_ranking_statistics("userTokens", 1, 2)
self.assertEqual(page.total, 3)
self.assertEqual(page.size, 2)
self.assertEqual(page.current, 1)
self.assertEqual(len(page.records), 2)
# 按 total_tokens 降序
self.assertEqual(page.records[0]["chat_user_id"], "u2")
self.assertEqual(page.records[1]["chat_user_id"], "u3")
self.assertEqual(page.records[0]["asker"]["username"], "bob")
self.assertEqual(page.records[0]["total_tokens"], 50)
self.assertEqual(page.records[0]["chat_record_count"], 1)

page2 = ApplicationStatisticsSerializer(data=self.serializer_data).get_ranking_statistics("userTokens", 2, 2)
self.assertEqual(len(page2.records), 1)
self.assertEqual(page2.records[0]["chat_user_id"], "u1")

def test_questions_ranking_order(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 1, 1)
self.make_record(c1, 1, 1)
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 1, 1)

page = ApplicationStatisticsSerializer(data=self.serializer_data).get_ranking_statistics("questions", 1, 10)
self.assertEqual(page.total, 2)
# 按对话次数降序
self.assertEqual(page.records[0]["chat_user_id"], "u1")
self.assertEqual(page.records[0]["chat_record_count"], 2)

def test_ranking_name_search(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 10, 0)
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 5, 0)

data = {**self.serializer_data, "name": "ali"}
page = ApplicationStatisticsSerializer(data=data).get_ranking_statistics("userTokens", 1, 10)
self.assertEqual(page.total, 1)
self.assertEqual(page.records[0]["chat_user_id"], "u1")

def test_aggregations(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 10, 20)
self.make_record(c1, 5, 5)
serializer = ApplicationStatisticsSerializer(data=self.serializer_data)
self.assertEqual(serializer.get_tokens_aggregation(), 40)
self.assertEqual(serializer.get_chat_record_aggregation(), 2)

def test_export(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 10, 20)
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 5, 5)
response = ApplicationStatisticsSerializer(data=self.serializer_data).export_ranking_statistics("userTokens")
self.assertEqual(response.status_code, 200)
self.assertIn("spreadsheet", response["Content-Type"])
# 导出内容为 xlsx(zip 魔数)
raw = b"".join(response.streaming_content) if response.streaming else response.content
self.assertEqual(raw[:2], b"PK")

def _workbook_rows(self, kind):
serializer = ApplicationStatisticsSerializer(data=self.serializer_data)
response = serializer.export_ranking_statistics(kind)
raw = b"".join(response.streaming_content) if response.streaming else response.content
workbook = load_workbook(io.BytesIO(raw))
return workbook.active

def test_export_user_tokens_cells(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 30, 0) # 30 tokens
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 5, 5) # 10 tokens
ws = self._workbook_rows("userTokens")
# 表头 + 2 行
self.assertEqual(ws.max_row, 3)
self.assertEqual(ws.cell(2, 2).value, "bob") # 40 tokens -> 第一
self.assertEqual(ws.cell(2, 3).value, 40)
self.assertEqual(ws.cell(3, 2).value, "alice")
self.assertEqual(ws.cell(3, 3).value, 30)
# 占比 = 该用户 tokens / 总 tokens
self.assertAlmostEqual(ws.cell(2, 4).value, 40 / 70)
self.assertAlmostEqual(ws.cell(3, 4).value, 30 / 70)

def test_export_questions_cells(self):
c1 = self.make_chat(self.application, "u1", "alice")
self.make_record(c1, 10, 0)
self.make_record(c1, 10, 0) # 2 次对话
c2 = self.make_chat(self.application, "u2", "bob")
self.make_record(c2, 5, 0) # 1 次对话
ws = self._workbook_rows("questions")
self.assertEqual(ws.max_row, 3)
# 按对话次数降序:alice(2) 在前
self.assertEqual(ws.cell(2, 2).value, "alice")
self.assertEqual(ws.cell(2, 5).value, 2)
# 占比 = 对话次数 / 总对话次数
self.assertAlmostEqual(ws.cell(2, 4).value, 2 / 3)
self.assertAlmostEqual(ws.cell(3, 4).value, 1 / 3)
119 changes: 119 additions & 0 deletions apps/application/tests/test_chat_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# coding=utf-8

from django.test import SimpleTestCase

from application.serializers.application_chat import ApplicationChatQuerySerializers


class ChatExportToRowTests(SimpleTestCase):
def setUp(self):
def make_row(details):
return {
"chat_id": "chat-1",
"abstract": "abstract",
"problem_text": "",
"details": details,
"answer_text": "",
"vote_status": None,
"vote_reason": None,
"vote_other_content": None,
"improve_paragraph_list": [],
"asker": {"username": "u"},
"message_tokens": 1,
"answer_tokens": 2,
"ip_address": "1.1.1.1",
"source": None,
"run_time": 1.0,
"create_time": None,
}

self.make_row = make_row
self.question = {"type": "question-node", "answer": "optimized"}
self.search = {
"type": "search-knowledge-node",
"name": "kb",
"paragraph_list": [{"title": "t", "content": "c"}],
}

def test_dict_form(self):
row = self.make_row({"q": self.question, "s": self.search})
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[3], "optimized")
self.assertEqual(result[8], "kb:1")
self.assertIn("kb:\nt:\nc", result[9])

def test_list_form(self):
row = self.make_row([self.question, self.search])
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[3], "optimized")
self.assertEqual(result[8], "kb:1")
self.assertIn("kb:\nt:\nc", result[9])

def test_loop_nested_form(self):
loop = {"type": "loop-node", "loop_node_data": [{"q": self.question, "s": self.search}]}
row = self.make_row([loop])
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[3], "optimized")
self.assertEqual(result[8], "kb:1")
self.assertIn("kb:\nt:\nc", result[9])

def test_v3_raw_list_form(self):
start = {"type": "start-node", "name": "开始", "question": "你好"}
ai = {"type": "ai-chat-node", "name": "AI 对话", "node_id": "n1", "answer": "你好!有什么我可以帮你的吗?"}
row = self.make_row([start, ai])
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[2], "你好")
self.assertEqual(result[4], "你好!有什么我可以帮你的吗?")

def test_v3_execution_details_form(self):
start = {"type": "start-node", "name": "开始", "question": "你好"}
ai = {"type": "ai-chat-node", "name": "AI 对话", "node_id": "n1", "answer": "hi"}
details = [{"execution_details": [start, ai], "paragraph_list": [], "padding_problem_text": None}]
row = self.make_row(details)
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[2], "你好")
self.assertEqual(result[4], "hi")

def test_v3_answer_includes_reasoning(self):
ai = {
"type": "ai-chat-node",
"name": "AI 对话",
"node_id": "n1",
"reasoning_content": "思考过程",
"answer": "答案",
}
row = self.make_row([ai])
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[4], "思考过程\n\n答案")

def test_v3_answer_from_node_messages(self):
ai = {
"type": "ai-chat-node",
"name": "AI 对话",
"node_id": "n1",
"messages": [
{"type": "REASONING", "content": "思考过程"},
{"type": "TEXT", "content": "你好"},
],
}
row = self.make_row([ai])
result = ApplicationChatQuerySerializers.to_row(row)
self.assertEqual(result[4], "思考过程\n\n你好")

def test_v3_answer_from_record_messages(self):
details = [
{
"messages": [
{"type": "REASONING", "content": "思考过程"},
{"type": "TEXT", "content": "你好"},
],
"execution_details": [],
}
]
result = ApplicationChatQuerySerializers.to_row(self.make_row(details))
self.assertEqual(result[4], "思考过程\n\n你好")

def test_empty_details(self):
for details in ({}, None, []):
result = ApplicationChatQuerySerializers.to_row(self.make_row(details))
self.assertEqual((result[3], result[8], result[9]), ("", "", ""))
6 changes: 6 additions & 0 deletions apps/application/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
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_token_usage', views.ApplicationStats.TokenUsageStatistics.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/top_questions', views.ApplicationStats.TopQuestionsStatistics.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/application_token_usage/<int:current_page>/<int:page_size>', views.ApplicationStats.TokenUsageRanking.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/application_token_usage/export', views.ApplicationStats.TokenUsageExport.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/top_questions/<int:current_page>/<int:page_size>', views.ApplicationStats.TopQuestionsRanking.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/top_questions/export', views.ApplicationStats.TopQuestionsExport.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/tokens/aggregation', views.ApplicationStats.TokensAggregation.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/chat_record/aggregation', views.ApplicationStats.ChatRecordAggregation.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>/application_key/<int:current_page>/<int:page_size>', views.ApplicationKey.Page.as_view()),
path('workspace/<str:workspace_id>/application/<str:application_id>/export', views.ApplicationAPI.Export.as_view()),
Expand Down
Loading
Loading