diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py
index 215e36aa2..33f7f6179 100644
--- a/backend/open_webui/models/feedbacks.py
+++ b/backend/open_webui/models/feedbacks.py
@@ -4,7 +4,7 @@ import uuid
from typing import Optional
from open_webui.internal.db import Base, get_db
-from open_webui.models.chats import Chats
+from open_webui.models.users import User
from open_webui.env import SRC_LOG_LEVELS
from pydantic import BaseModel, ConfigDict
@@ -92,6 +92,28 @@ class FeedbackForm(BaseModel):
model_config = ConfigDict(extra="allow")
+class UserResponse(BaseModel):
+ id: str
+ name: str
+ email: str
+ role: str = "pending"
+
+ last_active_at: int # timestamp in epoch
+ updated_at: int # timestamp in epoch
+ created_at: int # timestamp in epoch
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class FeedbackUserResponse(FeedbackResponse):
+ user: Optional[UserResponse] = None
+
+
+class FeedbackListResponse(BaseModel):
+ items: list[FeedbackUserResponse]
+ total: int
+
+
class FeedbackTable:
def insert_new_feedback(
self, user_id: str, form_data: FeedbackForm
@@ -143,6 +165,70 @@ class FeedbackTable:
except Exception:
return None
+ def get_feedback_items(
+ self, filter: dict = {}, skip: int = 0, limit: int = 30
+ ) -> FeedbackListResponse:
+ with get_db() as db:
+ query = db.query(Feedback, User).join(User, Feedback.user_id == User.id)
+
+ if filter:
+ order_by = filter.get("order_by")
+ direction = filter.get("direction")
+
+ if order_by == "username":
+ if direction == "asc":
+ query = query.order_by(User.name.asc())
+ else:
+ query = query.order_by(User.name.desc())
+ elif order_by == "model_id":
+ # it's stored in feedback.data['model_id']
+ if direction == "asc":
+ query = query.order_by(
+ Feedback.data["model_id"].as_string().asc()
+ )
+ else:
+ query = query.order_by(
+ Feedback.data["model_id"].as_string().desc()
+ )
+ elif order_by == "rating":
+ # it's stored in feedback.data['rating']
+ if direction == "asc":
+ query = query.order_by(
+ Feedback.data["rating"].as_string().asc()
+ )
+ else:
+ query = query.order_by(
+ Feedback.data["rating"].as_string().desc()
+ )
+ elif order_by == "updated_at":
+ if direction == "asc":
+ query = query.order_by(Feedback.updated_at.asc())
+ else:
+ query = query.order_by(Feedback.updated_at.desc())
+
+ else:
+ query = query.order_by(Feedback.created_at.desc())
+
+ # Count BEFORE pagination
+ total = query.count()
+
+ if skip:
+ query = query.offset(skip)
+ if limit:
+ query = query.limit(limit)
+
+ items = query.all()
+
+ feedbacks = []
+ for feedback, user in items:
+ feedback_model = FeedbackModel.model_validate(feedback)
+ user_model = UserResponse.model_validate(user)
+ feedbacks.append(
+ FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)
+ )
+
+ return FeedbackListResponse(items=feedbacks, total=total)
+
def get_all_feedbacks(self) -> list[FeedbackModel]:
with get_db() as db:
return [
diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py
index c76a1f691..3e5e14801 100644
--- a/backend/open_webui/routers/evaluations.py
+++ b/backend/open_webui/routers/evaluations.py
@@ -7,6 +7,8 @@ from open_webui.models.feedbacks import (
FeedbackModel,
FeedbackResponse,
FeedbackForm,
+ FeedbackUserResponse,
+ FeedbackListResponse,
Feedbacks,
)
@@ -56,35 +58,10 @@ async def update_config(
}
-class UserResponse(BaseModel):
- id: str
- name: str
- email: str
- role: str = "pending"
-
- last_active_at: int # timestamp in epoch
- updated_at: int # timestamp in epoch
- created_at: int # timestamp in epoch
-
-
-class FeedbackUserResponse(FeedbackResponse):
- user: Optional[UserResponse] = None
-
-
-@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
+@router.get("/feedbacks/all", response_model=list[FeedbackResponse])
async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks()
-
- feedback_list = []
- for feedback in feedbacks:
- user = Users.get_user_by_id(feedback.user_id)
- feedback_list.append(
- FeedbackUserResponse(
- **feedback.model_dump(),
- user=UserResponse(**user.model_dump()) if user else None,
- )
- )
- return feedback_list
+ return feedbacks
@router.delete("/feedbacks/all")
@@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)):
return success
+PAGE_ITEM_COUNT = 30
+
+
+@router.get("/feedbacks/list", response_model=FeedbackListResponse)
+async def get_feedbacks(
+ order_by: Optional[str] = None,
+ direction: Optional[str] = None,
+ page: Optional[int] = 1,
+ user=Depends(get_admin_user),
+):
+ limit = PAGE_ITEM_COUNT
+
+ page = max(1, page)
+ skip = (page - 1) * limit
+
+ filter = {}
+ if order_by:
+ filter["order_by"] = order_by
+ if direction:
+ filter["direction"] = direction
+
+ result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit)
+ return result
+
+
@router.post("/feedback", response_model=FeedbackModel)
async def create_feedback(
request: Request,
diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts
index 96a689fcb..1f48c7bfb 100644
--- a/src/lib/apis/evaluations/index.ts
+++ b/src/lib/apis/evaluations/index.ts
@@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => {
return res;
};
+export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => {
+ let error = null;
+
+ const searchParams = new URLSearchParams();
+ if (orderBy) searchParams.append('order_by', orderBy);
+ if (direction) searchParams.append('direction', direction);
+ if (page) searchParams.append('page', page.toString());
+
+ const res = await fetch(
+ `${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${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();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.error(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
export const exportAllFeedbacks = async (token: string = '') => {
let error = null;
diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte
index d29dee746..e2849bd98 100644
--- a/src/lib/components/admin/Evaluations.svelte
+++ b/src/lib/components/admin/Evaluations.svelte
@@ -33,7 +33,9 @@
let feedbacks = [];
onMount(async () => {
+ // TODO: feedbacks elo rating calculation should be done in the backend; remove below line later
feedbacks = await getAllFeedbacks(localStorage.token);
+
loaded = true;
const containerElement = document.getElementById('users-tabs-container');
@@ -117,7 +119,7 @@
{#if selectedTab === 'leaderboard'}