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')} + + {$i18n.t('Chat')} + + {$i18n.t('Result')} @@ -211,6 +224,15 @@ + + + +
+ {feedback.snapshot?.chat?.title} +
+
+ +
{#if feedback.data.rating.toString() === '1'} diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 7f18f3a35..4b49e1a99 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -12,6 +12,8 @@ import Loader from '../common/Loader.svelte'; import Spinner from '../common/Spinner.svelte'; + import Feedback from '$lib/components/admin/Evaluations/Feedbacks.svelte'; + import ChatPlaceholder from './ChatPlaceholder.svelte'; const i18n = getContext('i18n'); @@ -333,6 +335,8 @@ }, 100); } }; + + export let feedbacks: Feedback[] = [];
@@ -410,6 +414,7 @@ {addMessages} {triggerScroll} {readOnly} + feedback={feedbacks.find(f => f.meta.message_id === message.id)} /> {/each}
diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte index 8c14fda0f..0bedbc11d 100644 --- a/src/lib/components/chat/Messages/Message.svelte +++ b/src/lib/components/chat/Messages/Message.svelte @@ -11,6 +11,9 @@ import MultiResponseMessages from './MultiResponseMessages.svelte'; import ResponseMessage from './ResponseMessage.svelte'; import UserMessage from './UserMessage.svelte'; + import Badge from '$lib/components/common/Badge.svelte'; + + import Feedback from '$lib/components/admin/Evaluations/Feedbacks.svelte'; export let chatId; export let idx = 0; @@ -38,6 +41,8 @@ export let addMessages; export let triggerScroll; export let readOnly = false; + + export let feedback: Feedback | undefined = undefined;
{#if history.messages[messageId]} - {#if history.messages[messageId].role === 'user'} - message.parentId === null) - .map((message) => message.id) ?? [])} - {showPreviousMessage} - {showNextMessage} - {editMessage} - {deleteMessage} - {readOnly} - /> - {:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} - - {:else} - +
+ {#if history.messages[messageId].role === 'user'} + message.parentId === null) + .map((message) => message.id) ?? [])} + {showPreviousMessage} + {showNextMessage} + {editMessage} + {deleteMessage} + {readOnly} + /> + {:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} + + {:else} + + {/if} +
+ + {#if feedback} +
+
+ {#if feedback.data?.rating === 1} + + {:else if feedback.data?.rating === 0} + + {:else if feedback.data?.rating === -1} + + {/if} +
+
+
+ {$i18n.t('Comment')}: {feedback.data?.comment} +
+
+ {$i18n.t('Reason')}: + {#if feedback.data?.reason === 'accurate_information'} + {$i18n.t('Accurate information')} + {:else if feedback.data?.reason === 'followed_instructions_perfectly'} + {$i18n.t('Followed instructions perfectly')} + {:else if feedback.data?.reason === 'showcased_creativity'} + {$i18n.t('Showcased creativity')} + {:else if feedback.data?.reason === 'positive_attitude'} + {$i18n.t('Positive attitude')} + {:else if feedback.data?.reason === 'attention_to_detail'} + {$i18n.t('Attention to detail')} + {:else if feedback.data?.reason === 'thorough_explanation'} + {$i18n.t('Thorough explanation')} + {:else if feedback.data?.reason === 'dont_like_the_style'} + {$i18n.t("Don't like the style")} + {:else if feedback.data?.reason === 'too_verbose'} + {$i18n.t('Too verbose')} + {:else if feedback.data?.reason === 'not_helpful'} + {$i18n.t('Not helpful')} + {:else if feedback.data?.reason === 'not_factually_correct'} + {$i18n.t('Not factually correct')} + {:else if feedback.data?.reason === 'didnt_fully_follow_instructions'} + {$i18n.t("Didn't fully follow instructions")} + {:else if feedback.data?.reason === 'refused_when_it_shouldnt_have'} + {$i18n.t("Refused when it shouldn't have")} + {:else if feedback.data?.reason === 'being_lazy'} + {$i18n.t('Being lazy')} + {:else if feedback.data?.reason === 'other'} + {$i18n.t('Other')} + {:else} + {feedback.data?.reason} + {/if} +
+
+ {$i18n.t('Rating')}: {feedback.data?.details?.rating}/10 +
+
+
{/if} {/if}
diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 7a5aaf5cf..6bac742a3 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -5,7 +5,7 @@ import dayjs from 'dayjs'; - import { settings, chatId, WEBUI_NAME, models } from '$lib/stores'; + import { settings, chatId, WEBUI_NAME, models, user as _user } from '$lib/stores'; import { convertMessagesToHistory, createMessagesList } from '$lib/utils'; import { getChatByShareId, cloneSharedChatById } from '$lib/apis/chats'; @@ -16,6 +16,7 @@ import { getUserById } from '$lib/apis/users'; import { getModels } from '$lib/apis'; import { toast } from 'svelte-sonner'; + import { getFeedbacksByChatId } from '$lib/apis/evaluations'; const i18n = getContext('i18n'); @@ -24,6 +25,9 @@ let autoScroll = true; let processing = ''; let messagesContainerElement: HTMLDivElement; + + let showFeedback = false; + let feedbacks: Feedback[] = []; // let chatId = $page.params.id; let showModelSelector = false; @@ -41,6 +45,10 @@ currentId: null }; + const getChatFeedbacks = async () => { + feedbacks = await getFeedbacksByChatId(localStorage.token, $chatId); + }; + $: messages = createMessagesList(history, history.currentId); $: if ($page.params.id) { @@ -48,6 +56,11 @@ if (await loadSharedChat()) { await tick(); loaded = true; + const urlParams = new URLSearchParams(window.location.search); + showFeedback = urlParams.get('showFeedback') === 'true'; + if(showFeedback && $_user?.role === 'admin') { + await getChatFeedbacks(); + } } else { await goto('/'); } @@ -153,6 +166,7 @@ readOnly={true} {selectedModels} {processing} + {feedbacks} bind:history bind:messages bind:autoScroll