diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 0ab3220749..2b2bd3cedd 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1,7 +1,7 @@ import json import logging from typing import Optional - +import asyncio from open_webui.utils.misc import get_message_list from open_webui.socket.main import get_event_emitter @@ -209,8 +209,161 @@ class ChatStatsExportList(BaseModel): page: int +def calculate_chat_stats(user_id, skip=0, limit=10, filter=None): + if filter is None: + filter = {} + + result = Chats.get_chats_by_user_id( + user_id, + skip=skip, + limit=limit, + filter=filter, + ) + + chat_stats_export_list = [] + + for chat in result.items: + try: + messages_map = chat.chat.get("history", {}).get("messages", {}) + message_id = chat.chat.get("history", {}).get("currentId") + + history_models = {} + history_message_count = len(messages_map) + history_user_messages = [] + history_assistant_messages = [] + + export_messages = {} + for key, message in messages_map.items(): + try: + content = message.get("content", "") + if isinstance(content, str): + content_length = len(content) + else: + content_length = ( + 0 # Handle cases where content might be None or not string + ) + + # Extract rating safely + rating = message.get("annotation", {}).get("rating") + tags = message.get("annotation", {}).get("tags") + + message_stat = MessageStats( + id=message.get("id"), + role=message.get("role"), + model=message.get("model"), + timestamp=message.get("timestamp"), + content_length=content_length, + token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") + rating=rating, + tags=tags, + ) + + export_messages[key] = message_stat + + # --- Aggregation Logic (copied/adapted from usage stats) --- + role = message.get("role", "") + if role == "user": + history_user_messages.append(message) + elif role == "assistant": + history_assistant_messages.append(message) + model = message.get("model") + if model: + if model not in history_models: + history_models[model] = 0 + history_models[model] += 1 + except Exception as e: + log.debug(f"Error processing message {key}: {e}") + continue + + # Calculate Averages + average_user_message_content_length = ( + sum( + len(m.get("content", "")) + for m in history_user_messages + if isinstance(m.get("content"), str) + ) + / len(history_user_messages) + if history_user_messages + else 0 + ) + + average_assistant_message_content_length = ( + sum( + len(m.get("content", "")) + for m in history_assistant_messages + if isinstance(m.get("content"), str) + ) + / len(history_assistant_messages) + if history_assistant_messages + else 0 + ) + + # Response Times + response_times = [] + for message in history_assistant_messages: + user_message_id = message.get("parentId", None) + if user_message_id and user_message_id in messages_map: + user_message = messages_map[user_message_id] + # Ensure timestamps exist + t1 = message.get("timestamp") + t0 = user_message.get("timestamp") + if t1 and t0: + response_times.append(t1 - t0) + + average_response_time = ( + sum(response_times) / len(response_times) if response_times else 0 + ) + + # Current Message List Logic (Main path) + message_list = get_message_list(messages_map, message_id) + message_count = len(message_list) + models = {} + for message in reversed(message_list): + if message.get("role") == "assistant": + model = message.get("model") + if model: + if model not in models: + models[model] = 0 + models[model] += 1 + + # Construct Aggregate Stats + stats = AggregateChatStats( + average_response_time=average_response_time, + average_user_message_content_length=average_user_message_content_length, + average_assistant_message_content_length=average_assistant_message_content_length, + models=models, + message_count=message_count, + history_models=history_models, + history_message_count=history_message_count, + history_user_message_count=len(history_user_messages), + history_assistant_message_count=len(history_assistant_messages), + ) + + # Construct Chat Body + chat_body = ChatBody( + history=ChatHistoryStats(messages=export_messages, currentId=message_id) + ) + + chat_stat = ChatStatsExport( + id=chat.id, + user_id=chat.user_id, + created_at=chat.created_at, + updated_at=chat.updated_at, + tags=chat.meta.get("tags", []), + stats=stats, + chat=chat_body, + ) + + chat_stats_export_list.append(chat_stat) + except Exception as e: + log.debug(f"Error exporting stats for chat {chat.id}: {e}") + continue + + return chat_stats_export_list, result.total + + @router.get("/stats/export", response_model=ChatStatsExportList) -def export_chat_stats( +async def export_chat_stats( request: Request, chat_id: Optional[str] = None, start_time: Optional[int] = None, @@ -244,155 +397,11 @@ def export_chat_stats( if end_time: filter["end_time"] = end_time - result = Chats.get_chats_by_user_id( - user.id, - skip=skip, - limit=limit, - filter=filter, + chat_stats_export_list, total = await asyncio.to_thread( + calculate_chat_stats, user.id, skip, limit, filter ) - chat_stats_export_list = [] - - for chat in result.items: - try: - messages_map = chat.chat.get("history", {}).get("messages", {}) - message_id = chat.chat.get("history", {}).get("currentId") - - history_models = {} - history_message_count = len(messages_map) - history_user_messages = [] - history_assistant_messages = [] - - export_messages = {} - for key, message in messages_map.items(): - try: - content = message.get("content", "") - if isinstance(content, str): - content_length = len(content) - else: - content_length = 0 # Handle cases where content might be None or not string - - # Extract rating safely - rating = message.get("annotation", {}).get("rating") - tags = message.get("annotation", {}).get("tags") - - message_stat = MessageStats( - id=message.get("id"), - role=message.get("role"), - model=message.get("model"), - timestamp=message.get("timestamp"), - content_length=content_length, - token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") - rating=rating, - tags=tags, - ) - - export_messages[key] = message_stat - - # --- Aggregation Logic (copied/adapted from usage stats) --- - role = message.get("role", "") - if role == "user": - history_user_messages.append(message) - elif role == "assistant": - history_assistant_messages.append(message) - model = message.get("model") - if model: - if model not in history_models: - history_models[model] = 0 - history_models[model] += 1 - except Exception as e: - log.debug(f"Error processing message {key}: {e}") - continue - - # Calculate Averages - average_user_message_content_length = ( - sum( - len(m.get("content", "")) - for m in history_user_messages - if isinstance(m.get("content"), str) - ) - / len(history_user_messages) - if history_user_messages - else 0 - ) - - average_assistant_message_content_length = ( - sum( - len(m.get("content", "")) - for m in history_assistant_messages - if isinstance(m.get("content"), str) - ) - / len(history_assistant_messages) - if history_assistant_messages - else 0 - ) - - # Response Times - response_times = [] - for message in history_assistant_messages: - user_message_id = message.get("parentId", None) - if user_message_id and user_message_id in messages_map: - user_message = messages_map[user_message_id] - # Ensure timestamps exist - t1 = message.get("timestamp") - t0 = user_message.get("timestamp") - if t1 and t0: - response_times.append(t1 - t0) - - average_response_time = ( - sum(response_times) / len(response_times) if response_times else 0 - ) - - # Current Message List Logic (Main path) - message_list = get_message_list(messages_map, message_id) - message_count = len(message_list) - models = {} - for message in reversed(message_list): - if message.get("role") == "assistant": - model = message.get("model") - if model: - if model not in models: - models[model] = 0 - models[model] += 1 - - # Construct Aggregate Stats - stats = AggregateChatStats( - average_response_time=average_response_time, - average_user_message_content_length=average_user_message_content_length, - average_assistant_message_content_length=average_assistant_message_content_length, - models=models, - message_count=message_count, - history_models=history_models, - history_message_count=history_message_count, - history_user_message_count=len(history_user_messages), - history_assistant_message_count=len(history_assistant_messages), - ) - - # Construct Chat Body - chat_body = ChatBody( - history=ChatHistoryStats( - messages=export_messages, currentId=message_id - ) - ) - - chat_stat = ChatStatsExport( - id=chat.id, - user_id=chat.user_id, - created_at=chat.created_at, - updated_at=chat.updated_at, - tags=chat.meta.get("tags", []), - stats=stats, - chat=chat_body, - ) - - chat_stats_export_list.append(chat_stat) - except Exception as e: - log.debug(f"Error exporting stats for chat {chat.id}: {e}") - continue - - return ChatStatsExportList( - items=chat_stats_export_list, total=result.total, page=page - ) + return ChatStatsExportList(items=chat_stats_export_list, total=total, page=page) except Exception as e: log.debug(f"Error exporting chat stats: {e}")