diff --git a/backend/open_webui/migrations/versions/790ecce592df_add_chat_id_to_feedback.py b/backend/open_webui/migrations/versions/790ecce592df_add_chat_id_to_feedback.py new file mode 100644 index 000000000..ece52d0ab --- /dev/null +++ b/backend/open_webui/migrations/versions/790ecce592df_add_chat_id_to_feedback.py @@ -0,0 +1,30 @@ +"""add_chat_id_to_feedback + +Revision ID: 790ecce592df +Revises: 3781e22d8b01 +Create Date: 2025-01-08 15:13:16.063379 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +revision: str = '790ecce592df' +down_revision: Union[str, None] = '3781e22d8b01' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + + # Add chat_id column to feedback + op.add_column( + "feedback", + sa.Column("chat_id", sa.Text(), nullable=True), + ) + + +def downgrade(): + op.drop_column("feedback", "chat_id") \ No newline at end of file diff --git a/backend/open_webui/migrations/versions/96dcb0b3212f_migrate_chat_id_in_feedback.py b/backend/open_webui/migrations/versions/96dcb0b3212f_migrate_chat_id_in_feedback.py new file mode 100644 index 000000000..9187885de --- /dev/null +++ b/backend/open_webui/migrations/versions/96dcb0b3212f_migrate_chat_id_in_feedback.py @@ -0,0 +1,50 @@ +"""migrate_chat_id_in_feedback + +Revision ID: 96dcb0b3212f +Revises: 790ecce592df +Create Date: 2025-01-13 08:08:22.582335 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +revision: str = '96dcb0b3212f' +down_revision: Union[str, None] = '790ecce592df' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # Migrate chat_id data from meta to new chat_id column + conn = op.get_bind() + feedback = sa.table( + 'feedback', + sa.column('id', sa.String), + sa.column('chat_id', sa.Text), + sa.column('meta', sa.JSON) + ) + + result = conn.execute( + sa.select(feedback.c.id, feedback.c.meta) + .where(sa.and_( + feedback.c.chat_id.is_(None), + feedback.c.meta.isnot(None) + )) + ) + + for row in result: + if row.meta and 'chat_id' in row.meta: + chat_id = row.meta['chat_id'] + conn.execute( + feedback.update() + .where(feedback.c.id == row.id) + .values(chat_id=chat_id) + ) + + +def downgrade() -> None: + pass diff --git a/backend/open_webui/migrations/versions/e56c262570d7_make_chat_id_in_feedback_required.py b/backend/open_webui/migrations/versions/e56c262570d7_make_chat_id_in_feedback_required.py new file mode 100644 index 000000000..821d58626 --- /dev/null +++ b/backend/open_webui/migrations/versions/e56c262570d7_make_chat_id_in_feedback_required.py @@ -0,0 +1,42 @@ +"""make_chat_id_in_feedback_required + +Revision ID: e56c262570d7 +Revises: 96dcb0b3212f +Create Date: 2025-01-13 08:18:51.348376 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +revision: str = 'e56c262570d7' +down_revision: Union[str, None] = '96dcb0b3212f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # Check if there are feedbacks with NULL chat_ids, if not, make chat_id required + conn = op.get_bind() + feedback = sa.table('feedback', sa.column('chat_id', sa.Text)) + result = conn.execute(sa.select(feedback).where(feedback.c.chat_id.is_(None))) + + if result.first() is not None: + raise Exception( + "There are feedbacks with NULL chat_ids. Previous migration failed." + ) + + with op.batch_alter_table('feedback') as batch_op: + batch_op.alter_column('chat_id', + existing_type=sa.Text(), + nullable=False) + + +def downgrade() -> None: + with op.batch_alter_table('feedback') as batch_op: + batch_op.alter_column('chat_id', + existing_type=sa.Text(), + nullable=True) diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 7ff5c4540..5da3872ec 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -30,6 +30,7 @@ class Feedback(Base): snapshot = Column(JSON, nullable=True) created_at = Column(BigInteger) updated_at = Column(BigInteger) + chat_id = Column(Text) class FeedbackModel(BaseModel): @@ -42,7 +43,7 @@ class FeedbackModel(BaseModel): snapshot: Optional[dict] = None created_at: int updated_at: int - + chat_id: str model_config = ConfigDict(from_attributes=True) @@ -58,6 +59,7 @@ class FeedbackResponse(BaseModel): type: str data: Optional[dict] = None meta: Optional[dict] = None + snapshot: Optional[dict] = None created_at: int updated_at: int @@ -98,10 +100,12 @@ class FeedbackTable: ) -> Optional[FeedbackModel]: with get_db() as db: id = str(uuid.uuid4()) + chat_id = form_data.meta.get('chat_id') if form_data.meta else None feedback = FeedbackModel( **{ "id": id, "user_id": user_id, + "chat_id": chat_id, "version": 0, **form_data.model_dump(), "created_at": int(time.time()), @@ -220,6 +224,16 @@ class FeedbackTable: db.delete(feedback) db.commit() return True + + def get_feedbacks_by_chat_id(self, chat_id: str) -> list[FeedbackModel]: + with get_db() as db: + return [ + FeedbackModel.model_validate(feedback) + for feedback in db.query(Feedback) + .filter_by(chat_id=chat_id) + .order_by(Feedback.updated_at.desc()) + .all() + ] def delete_feedback_by_id_and_user_id(self, id: str, user_id: str) -> bool: with get_db() as db: diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index f0c4a6b06..535e1300b 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -157,3 +157,13 @@ async def delete_feedback_by_id(id: str, user=Depends(get_verified_user)): ) return success + +@router.get("/feedbacks/chat/{id}", response_model=list[FeedbackUserResponse]) +async def get_feedbacks_by_chat_id(id: str, user=Depends(get_admin_user)): + feedbacks = Feedbacks.get_feedbacks_by_chat_id(chat_id=id) + return [ + FeedbackUserResponse( + **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id) + ) + for feedback in feedbacks + ] \ No newline at end of file diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index f6f35f7c1..007d9914e 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -186,6 +186,37 @@ export const getFeedbackById = async (token: string, feedbackId: string) => { return res; }; +export const getFeedbacksByChatId = async (token: string = '', chatId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/chat/${chatId}`, { + 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; diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index e43081302..43235653f 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -20,7 +20,7 @@ import FeedbackMenu from './FeedbackMenu.svelte'; import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte'; - export let feedbacks = []; + export let feedbacks: Feedback[] = []; let page = 1; $: paginatedFeedbacks = feedbacks.slice((page - 1) * 10, page * 10); @@ -35,10 +35,19 @@ comment: string; tags: string[]; }; + meta: { + chat_id: string; + message_id?: string; + }; user: { name: string; profile_image_url: string; }; + snapshot: { + chat: { + title: string; + } + }; updated_at: number; }; @@ -152,6 +161,10 @@ {$i18n.t('Models')} +