From 9496e8f7b5ff64a93167f928a9f49bf752b08880 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 9 Jan 2026 20:19:51 +0400 Subject: [PATCH] feat: model evaluation activity chart --- backend/open_webui/models/feedbacks.py | 78 +++++++ backend/open_webui/routers/evaluations.py | 18 ++ src/lib/apis/evaluations/index.ts | 38 ++++ .../admin/Evaluations/LeaderboardModal.svelte | 83 +++++++- .../Evaluations/ModelActivityChart.svelte | 195 ++++++++++++++++++ 5 files changed, 407 insertions(+), 5 deletions(-) create mode 100644 src/lib/components/admin/Evaluations/ModelActivityChart.svelte diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 1e807ac8d..66037e486 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -127,6 +127,17 @@ class FeedbackListResponse(BaseModel): total: int +class ModelHistoryEntry(BaseModel): + date: str + won: int + lost: int + + +class ModelHistoryResponse(BaseModel): + model_id: str + history: list[ModelHistoryEntry] + + class FeedbackTable: def insert_new_feedback( self, user_id: str, form_data: FeedbackForm, db: Optional[Session] = None @@ -288,6 +299,73 @@ class FeedbackTable: for row in db.query(Feedback.id, Feedback.data).all() ] + def get_model_evaluation_history( + self, model_id: str, days: int = 30, db: Optional[Session] = None + ) -> list[ModelHistoryEntry]: + """ + Get daily wins/losses for a specific model over the past N days. + If days=0, returns all time data starting from first feedback. + Returns: [{"date": "2026-01-08", "won": 5, "lost": 2}, ...] + """ + from datetime import datetime, timedelta + from collections import defaultdict + + with get_db_context(db) as db: + if days == 0: + # All time - no cutoff + rows = db.query(Feedback.created_at, Feedback.data).all() + else: + cutoff = int(time.time()) - (days * 86400) + rows = ( + db.query(Feedback.created_at, Feedback.data) + .filter(Feedback.created_at >= cutoff) + .all() + ) + + daily_counts = defaultdict(lambda: {"won": 0, "lost": 0}) + first_date = None + + for created_at, data in rows: + if not data: + continue + if data.get("model_id") != model_id: + 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 + + # Track first date for this model + if first_date is None or date_str < first_date: + first_date = date_str + + # Generate date range + result = [] + today = datetime.now().date() + + if days == 0 and first_date: + # All time: start from first feedback date + start_date = datetime.strptime(first_date, "%Y-%m-%d").date() + num_days = (today - start_date).days + 1 + else: + # Fixed range + num_days = days + start_date = today - timedelta(days=days - 1) + + for i in range(num_days): + d = start_date + timedelta(days=i) + date_str = d.strftime("%Y-%m-%d") + counts = daily_counts.get(date_str, {"won": 0, "lost": 0}) + result.append(ModelHistoryEntry(date=date_str, won=counts["won"], lost=counts["lost"])) + + return result + def get_feedbacks_by_type( self, type: str, db: Optional[Session] = None ) -> list[FeedbackModel]: diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index 86fe02224..3beea54f3 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -13,6 +13,8 @@ from open_webui.models.feedbacks import ( FeedbackUserResponse, FeedbackListResponse, LeaderboardFeedbackData, + ModelHistoryEntry, + ModelHistoryResponse, Feedbacks, ) @@ -254,6 +256,22 @@ async def get_leaderboard( return LeaderboardResponse(entries=entries) + + +@router.get("/leaderboard/{model_id}/history", response_model=ModelHistoryResponse) +async def get_model_history( + model_id: str, + days: int = 30, + user=Depends(get_admin_user), + db: Session = Depends(get_session), +): + """Get daily win/loss history for a specific model.""" + history = Feedbacks.get_model_evaluation_history( + model_id=model_id, days=days, db=db + ) + return ModelHistoryResponse(model_id=model_id, history=history) + + ############################ # GetConfig ############################ diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index 315315fb0..318e766e5 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -127,6 +127,44 @@ export const getLeaderboard = async (token: string = '', query: string = '') => return res; }; +export const getModelHistory = async ( + token: string = '', + modelId: string, + days: number = 30 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('days', days.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/leaderboard/${encodeURIComponent(modelId)}/history?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => { let error = null; diff --git a/src/lib/components/admin/Evaluations/LeaderboardModal.svelte b/src/lib/components/admin/Evaluations/LeaderboardModal.svelte index 0b82f9a6f..2a19fd6af 100644 --- a/src/lib/components/admin/Evaluations/LeaderboardModal.svelte +++ b/src/lib/components/admin/Evaluations/LeaderboardModal.svelte @@ -1,22 +1,63 @@ - + {#if model}
@@ -27,9 +68,40 @@
-
+ +
+
+
+ {$i18n.t('Activity')} +
+
+ {#each TIME_RANGES as range} + + {/each} +
+
+ +
+ +
+
+ {$i18n.t('Tags')} +
{#if topTags.length} -
+
{#each topTags as tagInfo} {tagInfo.tag} {tagInfo.count} @@ -37,9 +109,10 @@ {/each}
{:else} - - + - {/if}
+