From 40c09167cd6de1c853a5dd03c88b4fdcb279dfe1 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 16 Jun 2026 23:24:27 +0200 Subject: [PATCH] refac --- backend/open_webui/models/chats.py | 15 ++++- backend/open_webui/models/feedbacks.py | 45 +++++++++++++++ backend/open_webui/routers/analytics.py | 55 +++++++++---------- backend/open_webui/utils/models.py | 6 +- .../admin/Evaluations/Leaderboard.svelte | 28 ++++++++-- 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index a8d3ce1e80..13eebf46b1 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -957,6 +957,20 @@ class ChatTable: all_chats = result.scalars().all() return [ChatModel.model_validate(chat) for chat in all_chats] + async def get_chat_metas_by_chat_ids( + self, + chat_ids: list[str], + include_archived: bool = False, + db: AsyncSession | None = None, + ) -> list[dict]: + async with get_async_db_context(db) as session: + stmt = select(Chat.meta).filter(Chat.id.in_(chat_ids)) + if not include_archived: + stmt = stmt.filter_by(archived=False) + + result = await session.execute(stmt) + return [meta for meta in result.scalars().all() if isinstance(meta, dict)] + # retrieve conversation async def get_chat_by_id( self, @@ -1389,7 +1403,6 @@ class ChatTable: for chat in all_chats ] - async def get_chats_by_folder_ids_and_user_id( self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None ) -> list[ChatModel]: diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index d288980501..7484445750 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -133,6 +133,12 @@ class ModelHistoryEntry(BaseModel): lost: int +class ModelHistoryCounts(BaseModel): + date: str + won: int = 0 + lost: int = 0 + + class ModelHistoryResponse(BaseModel): model_id: str history: list[ModelHistoryEntry] @@ -375,6 +381,45 @@ class FeedbackTable: return result + async def get_model_feedback_counts_by_day( + self, + model_id: str, + start_date: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> list[ModelHistoryCounts]: + """Get aggregated feedback counts per day for a model, preserving all matching days.""" + from collections import defaultdict + from datetime import datetime + + async with get_async_db_context(db) as db: + stmt = select(Feedback.created_at, Feedback.data).filter(Feedback.data['model_id'].as_string() == model_id) + if start_date is not None: + stmt = stmt.filter(Feedback.created_at >= start_date) + + result = await db.execute(stmt.order_by(Feedback.created_at.asc())) + rows = result.all() + + daily_counts = defaultdict(lambda: {'won': 0, 'lost': 0}) + + for created_at, data in rows: + if not data: + continue + + rating_str = str(data.get('rating', '')) + if rating_str not in ('1', '-1'): + continue + + date_str = datetime.fromtimestamp(created_at).strftime('%Y-%m-%d') + if rating_str == '1': + daily_counts[date_str]['won'] += 1 + else: + daily_counts[date_str]['lost'] += 1 + + return [ + ModelHistoryCounts(date=date_str, won=counts['won'], lost=counts['lost']) + for date_str, counts in sorted(daily_counts.items()) + ] + async def get_feedbacks_by_type(self, type: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: async with get_async_db_context(db) as db: result = await db.execute(select(Feedback).filter_by(type=type).order_by(Feedback.updated_at.desc())) diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index fcef30342c..b525d26466 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -367,6 +367,12 @@ async def get_model_overview( ): """Get model overview with feedback history and chat tags.""" + # Calculate start date for history + now = datetime.now() + start_dt = None + if days > 0: + start_dt = now - timedelta(days=days) + # Get chat IDs that used this model chat_ids = await ChatMessages.get_chat_ids_by_model_id( model_id=model_id, @@ -377,31 +383,18 @@ async def get_model_overview( db=db, ) - # Get feedback history per day - history_counts: dict[str, dict] = defaultdict(lambda: {'won': 0, 'lost': 0}) - - # Calculate start date for history - now = datetime.now() - start_dt = None - if days > 0: - start_dt = now - timedelta(days=days) - - for chat_id in chat_ids: - feedbacks = await Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db) - for fb in feedbacks: - if fb.data and 'rating' in fb.data: - rating = fb.data['rating'] - fb_date = datetime.fromtimestamp(fb.created_at) - - # Filter by date range - if start_dt and fb_date < start_dt: - continue - - date_str = fb_date.strftime('%Y-%m-%d') - if rating == 1: - history_counts[date_str]['won'] += 1 - elif rating == -1: - history_counts[date_str]['lost'] += 1 + history_rows = await Feedbacks.get_model_feedback_counts_by_day( + model_id=model_id, + start_date=int(start_dt.timestamp()) if start_dt else None, + db=db, + ) + history_counts = { + entry.date: { + 'won': entry.won, + 'lost': entry.lost, + } + for entry in history_rows + } # Fill in missing days history = [] @@ -430,10 +423,14 @@ async def get_model_overview( # Get chat tags tag_counts: dict[str, int] = defaultdict(int) - for chat_id in chat_ids: - chat = await Chats.get_chat_by_id(chat_id, db=db) - if chat and chat.meta: - for tag in chat.meta.get('tags', []): + if chat_ids: + chat_metas = await Chats.get_chat_metas_by_chat_ids( + chat_ids, + include_archived=True, + db=db, + ) + for meta in chat_metas: + for tag in meta.get('tags', []): tag_counts[tag] += 1 # Sort by count and take top 10 diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 60c5836637..a014267f38 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -368,7 +368,11 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) models_dict = {model['id']: model for model in models} if isinstance(request.app.state.MODELS, RedisDict): - request.app.state.MODELS.set(models_dict) + try: + request.app.state.MODELS.set(models_dict) + except Exception as e: + log.warning(f'Failed to update Redis model cache, using in-process cache: {e}') + request.app.state.MODELS = models_dict else: request.app.state.MODELS = models_dict diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index a5deba0335..7adefe5af7 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -46,8 +46,9 @@ try { const result = await getLeaderboard(localStorage.token, searchQuery); const statsMap = new Map((result?.entries ?? []).map((e) => [e.model_id, e])); + const modelMap = new Map(($models ?? []).map((m) => [m.id, m])); - rankedModels = $models + const activeModels = $models .filter((m) => m?.owned_by !== 'arena' && !m?.info?.meta?.hidden) .map((model) => { const s = statsMap.get(model.id); @@ -61,12 +62,27 @@ }, top_tags: s?.top_tags ?? [] }; - }) - .sort((a, b) => { - if (a.rating === '-') return 1; - if (b.rating === '-') return -1; - return b.rating - a.rating; }); + + const evaluatedModels = (result?.entries ?? []) + .filter((entry) => !modelMap.has(entry.model_id)) + .map((entry) => ({ + id: entry.model_id, + name: entry.model_id, + rating: entry.rating, + stats: { + count: entry.won + entry.lost, + won: entry.won.toString(), + lost: entry.lost.toString() + }, + top_tags: entry.top_tags ?? [] + })); + + rankedModels = [...activeModels, ...evaluatedModels].sort((a, b) => { + if (a.rating === '-') return 1; + if (b.rating === '-') return -1; + return b.rating - a.rating; + }); } catch (err) { console.error('Leaderboard load failed:', err); }