Skip to content

Commit 68a1e87

Browse files
committed
enh: analytics model modal
1 parent e8a36f0 commit 68a1e87

8 files changed

Lines changed: 867 additions & 119 deletions

File tree

backend/open_webui/models/chat_messages.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class ChatMessageModel(BaseModel):
111111
embeds: Optional[list] = None
112112
done: bool = True
113113
status_history: Optional[list] = None
114-
error: Optional[dict] = None
114+
error: Optional[dict | str] = None
115115
usage: Optional[dict] = None
116116
created_at: int
117117
updated_at: int
@@ -269,6 +269,36 @@ def get_messages_by_model_id(
269269
)
270270
return [ChatMessageModel.model_validate(message) for message in messages]
271271

272+
def get_chat_ids_by_model_id(
273+
self,
274+
model_id: str,
275+
start_date: Optional[int] = None,
276+
end_date: Optional[int] = None,
277+
skip: int = 0,
278+
limit: int = 50,
279+
db: Optional[Session] = None,
280+
) -> list[str]:
281+
"""Get distinct chat_ids that used a specific model."""
282+
from sqlalchemy import distinct
283+
284+
with get_db_context(db) as db:
285+
query = db.query(distinct(ChatMessage.chat_id)).filter(
286+
ChatMessage.model_id == model_id
287+
)
288+
if start_date:
289+
query = query.filter(ChatMessage.created_at >= start_date)
290+
if end_date:
291+
query = query.filter(ChatMessage.created_at <= end_date)
292+
293+
# Order by most recent message in each chat
294+
chat_ids = (
295+
query.order_by(ChatMessage.created_at.desc())
296+
.offset(skip)
297+
.limit(limit)
298+
.all()
299+
)
300+
return [chat_id for (chat_id,) in chat_ids]
301+
272302
def delete_messages_by_chat_id(
273303
self, chat_id: str, db: Optional[Session] = None
274304
) -> bool:
@@ -289,7 +319,11 @@ def get_message_count_by_model(
289319

290320
query = db.query(
291321
ChatMessage.model_id, func.count(ChatMessage.id).label("count")
292-
).filter(ChatMessage.role == "assistant", ChatMessage.model_id.isnot(None))
322+
).filter(
323+
ChatMessage.role == "assistant",
324+
ChatMessage.model_id.isnot(None),
325+
~ChatMessage.user_id.like("shared-%"),
326+
)
293327

294328
if start_date:
295329
query = query.filter(ChatMessage.created_at >= start_date)
@@ -338,6 +372,7 @@ def get_token_usage_by_model(
338372
ChatMessage.role == "assistant",
339373
ChatMessage.model_id.isnot(None),
340374
ChatMessage.usage.isnot(None),
375+
~ChatMessage.user_id.like("shared-%"),
341376
)
342377

343378
if start_date:
@@ -396,6 +431,7 @@ def get_token_usage_by_user(
396431
ChatMessage.role == "assistant",
397432
ChatMessage.user_id.isnot(None),
398433
ChatMessage.usage.isnot(None),
434+
~ChatMessage.user_id.like("shared-%"),
399435
)
400436

401437
if start_date:
@@ -426,7 +462,7 @@ def get_message_count_by_user(
426462

427463
query = db.query(
428464
ChatMessage.user_id, func.count(ChatMessage.id).label("count")
429-
)
465+
).filter(~ChatMessage.user_id.like("shared-%"))
430466

431467
if start_date:
432468
query = query.filter(ChatMessage.created_at >= start_date)
@@ -447,7 +483,7 @@ def get_message_count_by_chat(
447483

448484
query = db.query(
449485
ChatMessage.chat_id, func.count(ChatMessage.id).label("count")
450-
)
486+
).filter(~ChatMessage.user_id.like("shared-%"))
451487

452488
if start_date:
453489
query = query.filter(ChatMessage.created_at >= start_date)
@@ -469,7 +505,8 @@ def get_daily_message_counts_by_model(
469505

470506
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
471507
ChatMessage.role == "assistant",
472-
ChatMessage.model_id.isnot(None)
508+
ChatMessage.model_id.isnot(None),
509+
~ChatMessage.user_id.like("shared-%"),
473510
)
474511

475512
if start_date:
@@ -511,7 +548,8 @@ def get_hourly_message_counts_by_model(
511548

512549
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
513550
ChatMessage.role == "assistant",
514-
ChatMessage.model_id.isnot(None)
551+
ChatMessage.model_id.isnot(None),
552+
~ChatMessage.user_id.like("shared-%"),
515553
)
516554

517555
if start_date:

backend/open_webui/models/feedbacks.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,23 @@ def get_feedback_by_id_and_user_id(
191191
except Exception:
192192
return None
193193

194+
def get_feedbacks_by_chat_id(
195+
self, chat_id: str, db: Optional[Session] = None
196+
) -> list[FeedbackModel]:
197+
"""Get all feedbacks for a specific chat."""
198+
try:
199+
with get_db_context(db) as db:
200+
# meta.chat_id stores the chat reference
201+
feedbacks = (
202+
db.query(Feedback)
203+
.filter(Feedback.meta["chat_id"].as_string() == chat_id)
204+
.order_by(Feedback.created_at.desc())
205+
.all()
206+
)
207+
return [FeedbackModel.model_validate(fb) for fb in feedbacks]
208+
except Exception:
209+
return []
210+
194211
def get_feedback_items(
195212
self,
196213
filter: dict = {},

backend/open_webui/routers/analytics.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
from typing import Optional
2+
from datetime import datetime, timedelta
3+
from collections import defaultdict
24
import logging
35
from fastapi import APIRouter, Depends, Query
46
from pydantic import BaseModel
57

68
from open_webui.models.chat_messages import ChatMessages, ChatMessageModel
9+
from open_webui.models.chats import Chats
10+
from open_webui.models.users import Users
11+
from open_webui.models.feedbacks import Feedbacks
712
from open_webui.utils.auth import get_admin_user
813
from open_webui.internal.db import get_session
914
from sqlalchemy.orm import Session
@@ -245,3 +250,198 @@ async def get_token_usage(
245250
total_output_tokens=total_output,
246251
total_tokens=total_input + total_output,
247252
)
253+
254+
255+
####################
256+
# Model Chats Browser
257+
####################
258+
259+
260+
class ModelChatEntry(BaseModel):
261+
chat_id: str
262+
user_id: Optional[str] = None
263+
user_name: Optional[str] = None
264+
first_message: Optional[str] = None
265+
updated_at: int
266+
267+
268+
class ModelChatsResponse(BaseModel):
269+
chats: list[ModelChatEntry]
270+
total: int
271+
272+
273+
@router.get("/models/{model_id}/chats", response_model=ModelChatsResponse)
274+
async def get_model_chats(
275+
model_id: str,
276+
start_date: Optional[int] = Query(None),
277+
end_date: Optional[int] = Query(None),
278+
skip: int = Query(0),
279+
limit: int = Query(50, le=100),
280+
user=Depends(get_admin_user),
281+
db: Session = Depends(get_session),
282+
):
283+
"""Get chats that used a specific model, with preview and feedback info."""
284+
285+
# Get chat IDs that used this model
286+
chat_ids = ChatMessages.get_chat_ids_by_model_id(
287+
model_id=model_id,
288+
start_date=start_date,
289+
end_date=end_date,
290+
skip=skip,
291+
limit=limit,
292+
db=db,
293+
)
294+
295+
if not chat_ids:
296+
return ModelChatsResponse(chats=[], total=0)
297+
298+
# Get chat details from messages only
299+
chats_data = []
300+
for chat_id in chat_ids:
301+
messages = ChatMessages.get_messages_by_chat_id(chat_id, db=db)
302+
if not messages:
303+
continue
304+
305+
# Get user_id from first user message
306+
first_user_msg = next((m for m in messages if m.role == "user"), None)
307+
user_id = first_user_msg.user_id if first_user_msg else None
308+
309+
# Extract first message content as preview
310+
first_message = None
311+
if first_user_msg and first_user_msg.content:
312+
content = first_user_msg.content
313+
if isinstance(content, str):
314+
first_message = content[:200]
315+
elif isinstance(content, list):
316+
text_parts = [
317+
b.get("text", "") for b in content if isinstance(b, dict)
318+
]
319+
first_message = " ".join(text_parts)[:200]
320+
321+
# Get user info
322+
user_name = None
323+
if user_id:
324+
user_info = Users.get_user_by_id(user_id, db=db)
325+
user_name = user_info.name if user_info else None
326+
327+
# Timestamps from messages
328+
updated_at = max(m.created_at for m in messages) if messages else 0
329+
330+
331+
chats_data.append(
332+
ModelChatEntry(
333+
chat_id=chat_id,
334+
user_id=user_id,
335+
user_name=user_name,
336+
first_message=first_message,
337+
updated_at=updated_at,
338+
)
339+
)
340+
341+
return ModelChatsResponse(chats=chats_data, total=len(chats_data))
342+
343+
344+
####################
345+
# Model Overview
346+
####################
347+
348+
349+
class HistoryEntry(BaseModel):
350+
date: str
351+
won: int = 0
352+
lost: int = 0
353+
354+
355+
class TagEntry(BaseModel):
356+
tag: str
357+
count: int
358+
359+
360+
class ModelOverviewResponse(BaseModel):
361+
history: list[HistoryEntry]
362+
tags: list[TagEntry]
363+
364+
365+
@router.get("/models/{model_id}/overview", response_model=ModelOverviewResponse)
366+
async def get_model_overview(
367+
model_id: str,
368+
days: int = Query(30, description="Number of days of history (0 for all)"),
369+
user=Depends(get_admin_user),
370+
db: Session = Depends(get_session),
371+
):
372+
"""Get model overview with feedback history and chat tags."""
373+
374+
# Get chat IDs that used this model
375+
chat_ids = ChatMessages.get_chat_ids_by_model_id(
376+
model_id=model_id,
377+
start_date=None,
378+
end_date=None,
379+
skip=0,
380+
limit=10000, # Get all chats
381+
db=db,
382+
)
383+
384+
# Get feedback history per day
385+
history_counts: dict[str, dict] = defaultdict(lambda: {"won": 0, "lost": 0})
386+
387+
# Calculate start date for history
388+
now = datetime.now()
389+
start_dt = None
390+
if days > 0:
391+
start_dt = now - timedelta(days=days)
392+
393+
for chat_id in chat_ids:
394+
feedbacks = Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db)
395+
for fb in feedbacks:
396+
if fb.data and "rating" in fb.data:
397+
rating = fb.data["rating"]
398+
fb_date = datetime.fromtimestamp(fb.created_at)
399+
400+
# Filter by date range
401+
if start_dt and fb_date < start_dt:
402+
continue
403+
404+
date_str = fb_date.strftime("%Y-%m-%d")
405+
if rating == 1:
406+
history_counts[date_str]["won"] += 1
407+
elif rating == -1:
408+
history_counts[date_str]["lost"] += 1
409+
410+
# Fill in missing days
411+
history = []
412+
if history_counts or days > 0:
413+
end_dt = now
414+
if days > 0:
415+
current = start_dt
416+
elif history_counts:
417+
# Find earliest date
418+
min_date = min(history_counts.keys())
419+
current = datetime.strptime(min_date, "%Y-%m-%d")
420+
else:
421+
current = now
422+
423+
while current <= end_dt:
424+
date_str = current.strftime("%Y-%m-%d")
425+
counts = history_counts.get(date_str, {"won": 0, "lost": 0})
426+
history.append(HistoryEntry(
427+
date=date_str,
428+
won=counts["won"],
429+
lost=counts["lost"],
430+
))
431+
current += timedelta(days=1)
432+
433+
# Get chat tags
434+
tag_counts: dict[str, int] = defaultdict(int)
435+
for chat_id in chat_ids:
436+
chat = Chats.get_chat_by_id(chat_id, db=db)
437+
if chat and chat.meta:
438+
for tag in chat.meta.get("tags", []):
439+
tag_counts[tag] += 1
440+
441+
# Sort by count and take top 10
442+
tags = [
443+
TagEntry(tag=tag, count=count)
444+
for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]
445+
]
446+
447+
return ModelOverviewResponse(history=history, tags=tags)

0 commit comments

Comments
 (0)