diff --git a/backend/open_webui/apps/webui/models/feedbacks.py b/backend/open_webui/apps/webui/models/feedbacks.py index 76410f417..7255b48fd 100644 --- a/backend/open_webui/apps/webui/models/feedbacks.py +++ b/backend/open_webui/apps/webui/models/feedbacks.py @@ -23,9 +23,11 @@ class Feedback(Base): __tablename__ = "feedback" id = Column(Text, primary_key=True) user_id = Column(Text) + version = Column(BigInteger, default=0) type = Column(Text) data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) + snapshot = Column(JSON, nullable=True) created_at = Column(BigInteger) updated_at = Column(BigInteger) @@ -33,9 +35,11 @@ class Feedback(Base): class FeedbackModel(BaseModel): id: str user_id: str + version: int type: str data: Optional[dict] = None meta: Optional[dict] = None + snapshot: Optional[dict] = None created_at: int updated_at: int @@ -47,30 +51,44 @@ class FeedbackModel(BaseModel): #################### +class FeedbackResponse(BaseModel): + id: str + user_id: str + version: int + type: str + data: Optional[dict] = None + meta: Optional[dict] = None + created_at: int + updated_at: int + + class RatingData(BaseModel): - rating: str - comment: str - model_config = ConfigDict(extra="allow") - - -class VoteData(BaseModel): - rating: str - model_id: str - model_ids: list[str] + rating: Optional[str | int] = None + model_id: Optional[str] = None + sibling_model_ids: Optional[list[str]] = None + reason: Optional[str] = None + comment: Optional[str] = None model_config = ConfigDict(extra="allow") class MetaData(BaseModel): - chat: Optional[dict] = None + arena: Optional[bool] = None + chat_id: Optional[str] = None message_id: Optional[str] = None tags: Optional[list[str]] = None model_config = ConfigDict(extra="allow") +class SnapshotData(BaseModel): + chat: Optional[dict] = None + model_config = ConfigDict(extra="allow") + + class FeedbackForm(BaseModel): type: str - data: Optional[RatingData | VoteData] = None + data: Optional[RatingData] = None meta: Optional[dict] = None + snapshot: Optional[SnapshotData] = None model_config = ConfigDict(extra="allow") @@ -84,10 +102,10 @@ class FeedbackTable: **{ "id": id, "user_id": user_id, - "type": form_data.type, - "data": form_data.data, - "meta": form_data.meta, + "version": 0, + **form_data.model_dump(), "created_at": int(time.time()), + "updated_at": int(time.time()), } ) try: @@ -113,6 +131,25 @@ class FeedbackTable: except Exception: return None + def get_feedback_by_id_and_user_id( + self, id: str, user_id: str + ) -> Optional[FeedbackModel]: + try: + with get_db() as db: + feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + if not feedback: + return None + return FeedbackModel.model_validate(feedback) + except Exception: + return None + + def get_all_feedbacks(self) -> list[FeedbackModel]: + with get_db() as db: + return [ + FeedbackModel.model_validate(feedback) + for feedback in db.query(Feedback).all() + ] + def get_feedbacks_by_type(self, type: str) -> list[FeedbackModel]: with get_db() as db: return [ @@ -136,9 +173,31 @@ class FeedbackTable: return None if form_data.data: - feedback.data = form_data.data + feedback.data = form_data.data.model_dump() if form_data.meta: feedback.meta = form_data.meta + if form_data.snapshot: + feedback.snapshot = form_data.snapshot.model_dump() + + feedback.updated_at = int(time.time()) + + db.commit() + return FeedbackModel.model_validate(feedback) + + def update_feedback_by_id_and_user_id( + self, id: str, user_id: str, form_data: FeedbackForm + ) -> Optional[FeedbackModel]: + with get_db() as db: + feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + if not feedback: + return None + + if form_data.data: + feedback.data = form_data.data.model_dump() + if form_data.meta: + feedback.meta = form_data.meta + if form_data.snapshot: + feedback.snapshot = form_data.snapshot.model_dump() feedback.updated_at = int(time.time()) @@ -154,5 +213,34 @@ class FeedbackTable: db.commit() return True + def delete_feedback_by_id_and_user_id(self, id: str, user_id: str) -> bool: + with get_db() as db: + feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + if not feedback: + return False + db.delete(feedback) + db.commit() + return True + + def delete_feedbacks_by_user_id(self, user_id: str) -> bool: + with get_db() as db: + feedbacks = db.query(Feedback).filter_by(user_id=user_id).all() + if not feedbacks: + return False + for feedback in feedbacks: + db.delete(feedback) + db.commit() + return True + + def delete_all_feedbacks(self) -> bool: + with get_db() as db: + feedbacks = db.query(Feedback).all() + if not feedbacks: + return False + for feedback in feedbacks: + db.delete(feedback) + db.commit() + return True + Feedbacks = FeedbackTable() diff --git a/backend/open_webui/apps/webui/routers/evaluations.py b/backend/open_webui/apps/webui/routers/evaluations.py index b40953e49..f0e9236dc 100644 --- a/backend/open_webui/apps/webui/routers/evaluations.py +++ b/backend/open_webui/apps/webui/routers/evaluations.py @@ -3,6 +3,12 @@ from fastapi import APIRouter, Depends, HTTPException, status, Request from pydantic import BaseModel +from open_webui.apps.webui.models.feedbacks import ( + FeedbackModel, + FeedbackForm, + Feedbacks, +) + from open_webui.constants import ERROR_MESSAGES from open_webui.utils.utils import get_admin_user, get_verified_user @@ -47,3 +53,86 @@ async def update_config( "ENABLE_EVALUATION_ARENA_MODELS": config.ENABLE_EVALUATION_ARENA_MODELS, "EVALUATION_ARENA_MODELS": config.EVALUATION_ARENA_MODELS, } + + +@router.get("/feedbacks", response_model=list[FeedbackModel]) +async def get_feedbacks(user=Depends(get_verified_user)): + feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id) + return feedbacks + + +@router.delete("/feedbacks", response_model=bool) +async def delete_feedbacks(user=Depends(get_verified_user)): + success = Feedbacks.delete_feedbacks_by_user_id(user.id) + return success + + +@router.delete("/feedbacks/all") +async def delete_all_feedbacks(user=Depends(get_admin_user)): + success = Feedbacks.delete_all_feedbacks() + return success + + +@router.get("/feedbacks/all", response_model=list[FeedbackModel]) +async def get_all_feedbacks(user=Depends(get_admin_user)): + feedbacks = Feedbacks.get_all_feedbacks() + return feedbacks + + +@router.post("/feedback", response_model=FeedbackModel) +async def create_feedback( + request: Request, + form_data: FeedbackForm, + user=Depends(get_verified_user), +): + feedback = Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data) + if not feedback: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + return feedback + + +@router.get("/feedback/{id}", response_model=FeedbackModel) +async def get_feedback_by_id(id: str, user=Depends(get_verified_user)): + feedback = Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id) + + if not feedback: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + return feedback + + +@router.post("/feedback/{id}", response_model=FeedbackModel) +async def update_feedback_by_id( + id: str, form_data: FeedbackForm, user=Depends(get_verified_user) +): + feedback = Feedbacks.update_feedback_by_id_and_user_id( + id=id, user_id=user.id, form_data=form_data + ) + + if not feedback: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + return feedback + + +@router.delete("/feedback/{id}") +async def delete_feedback_by_id(id: str, user=Depends(get_verified_user)): + if user.role == "admin": + success = Feedbacks.delete_feedback_by_id(id=id) + else: + success = Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id) + + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + return success diff --git a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py index 8119c9396..9116aa388 100644 --- a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py +++ b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py @@ -26,11 +26,17 @@ def upgrade(): sa.Column( "user_id", sa.Text(), nullable=True ), # ID of the user providing the feedback (TEXT type) + sa.Column( + "version", sa.BigInteger(), default=0 + ), # Version of feedback (BIGINT type) sa.Column("type", sa.Text(), nullable=True), # Type of feedback (TEXT type) sa.Column("data", sa.JSON(), nullable=True), # Feedback data (JSON type) sa.Column( "meta", sa.JSON(), nullable=True ), # Metadata for feedback (JSON type) + sa.Column( + "snapshot", sa.JSON(), nullable=True + ), # snapshot data for feedback (JSON type) sa.Column( "created_at", sa.BigInteger(), nullable=False ), # Feedback creation timestamp (BIGINT representing epoch) diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index 21130bd17..854b3abb8 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -61,3 +61,155 @@ export const updateConfig = async (token: string, config: object) => { return res; }; + +export const getAllFeedbacks = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all`, { + 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.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createNewFeedback = async (token: string, feedback: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...feedback + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFeedbackById = async (token: string, feedbackId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + 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.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFeedbackById = async (token: string, feedbackId: string, feedback: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...feedback + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFeedbackById = async (token: string, feedbackId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + method: 'DELETE', + 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.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte index 7fe3c5756..c9b678ffa 100644 --- a/src/lib/components/admin/Evaluations.svelte +++ b/src/lib/components/admin/Evaluations.svelte @@ -2,19 +2,24 @@ import { onMount, getContext } from 'svelte'; import { models } from '$lib/stores'; + import GarbageBin from '../icons/GarbageBin.svelte'; + import FeedbackMenu from './Evaluations/FeedbackMenu.svelte'; + import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; + import { getAllFeedbacks } from '$lib/apis/evaluations'; const i18n = getContext('i18n'); let rankedModels = []; + let feedbacks = []; + let loaded = false; - - onMount(() => { - loaded = true; - + onMount(async () => { + feedbacks = await getAllFeedbacks(localStorage.token); rankedModels = $models .filter((m) => m?.owned_by !== 'arena' && (m?.info?.meta?.hidden ?? false) !== true) .map((model) => { return { ...model, + ranking: '-', rating: '-', stats: { won: '-', @@ -34,11 +39,13 @@ // If both ratings are '-', sort alphabetically (by 'name') return a.name.localeCompare(b.name); }); + + loaded = true; }); {#if loaded} -