Browse Source

feat: leaderboard

Timothy J. Baek 6 months ago
parent
commit
bc95e62600

+ 158 - 0
backend/open_webui/apps/webui/models/feedbacks.py

@@ -0,0 +1,158 @@
+import logging
+import time
+import uuid
+from typing import Optional
+
+from open_webui.apps.webui.internal.db import Base, get_db
+from open_webui.apps.webui.models.chats import Chats
+
+from open_webui.env import SRC_LOG_LEVELS
+from pydantic import BaseModel, ConfigDict
+from sqlalchemy import BigInteger, Column, Text, JSON, Boolean
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+
+####################
+# Feedback DB Schema
+####################
+
+
+class Feedback(Base):
+    __tablename__ = "feedback"
+    id = Column(Text, primary_key=True)
+    user_id = Column(Text)
+    type = Column(Text)
+    data = Column(JSON, nullable=True)
+    meta = Column(JSON, nullable=True)
+    created_at = Column(BigInteger)
+    updated_at = Column(BigInteger)
+
+
+class FeedbackModel(BaseModel):
+    id: str
+    user_id: str
+    type: str
+    data: Optional[dict] = None
+    meta: Optional[dict] = None
+    created_at: int
+    updated_at: int
+
+    model_config = ConfigDict(from_attributes=True)
+
+
+####################
+# Forms
+####################
+
+
+class RatingData(BaseModel):
+    rating: str
+    comment: str
+    model_config = ConfigDict(extra="allow")
+
+
+class VoteData(BaseModel):
+    rating: str
+    model_id: str
+    model_ids: list[str]
+    model_config = ConfigDict(extra="allow")
+
+
+class MetaData(BaseModel):
+    chat: Optional[dict] = None
+    message_id: Optional[str] = None
+    tags: Optional[list[str]] = None
+    model_config = ConfigDict(extra="allow")
+
+
+class FeedbackForm(BaseModel):
+    type: str
+    data: Optional[RatingData | VoteData] = None
+    meta: Optional[dict] = None
+    model_config = ConfigDict(extra="allow")
+
+
+class FeedbackTable:
+    def insert_new_feedback(
+        self, user_id: str, form_data: FeedbackForm
+    ) -> Optional[FeedbackModel]:
+        with get_db() as db:
+            id = str(uuid.uuid4())
+            feedback = FeedbackModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "type": form_data.type,
+                    "data": form_data.data,
+                    "meta": form_data.meta,
+                    "created_at": int(time.time()),
+                }
+            )
+            try:
+                result = Feedback(**feedback.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return FeedbackModel.model_validate(result)
+                else:
+                    return None
+            except Exception as e:
+                print(e)
+                return None
+
+    def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
+        try:
+            with get_db() as db:
+                feedback = db.query(Feedback).filter_by(id=id).first()
+                if not feedback:
+                    return None
+                return FeedbackModel.model_validate(feedback)
+        except Exception:
+            return None
+
+    def get_feedbacks_by_type(self, type: str) -> list[FeedbackModel]:
+        with get_db() as db:
+            return [
+                FeedbackModel.model_validate(feedback)
+                for feedback in db.query(Feedback).filter_by(type=type).all()
+            ]
+
+    def get_feedbacks_by_user_id(self, user_id: str) -> list[FeedbackModel]:
+        with get_db() as db:
+            return [
+                FeedbackModel.model_validate(feedback)
+                for feedback in db.query(Feedback).filter_by(user_id=user_id).all()
+            ]
+
+    def update_feedback_by_id(
+        self, id: str, form_data: FeedbackForm
+    ) -> Optional[FeedbackModel]:
+        with get_db() as db:
+            feedback = db.query(Feedback).filter_by(id=id).first()
+            if not feedback:
+                return None
+
+            if form_data.data:
+                feedback.data = form_data.data
+            if form_data.meta:
+                feedback.meta = form_data.meta
+
+            feedback.updated_at = int(time.time())
+
+            db.commit()
+            return FeedbackModel.model_validate(feedback)
+
+    def delete_feedback_by_id(self, id: str) -> bool:
+        with get_db() as db:
+            feedback = db.query(Feedback).filter_by(id=id).first()
+            if not feedback:
+                return False
+            db.delete(feedback)
+            db.commit()
+            return True
+
+
+Feedbacks = FeedbackTable()

+ 45 - 0
backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py

@@ -0,0 +1,45 @@
+"""Add feedback table
+
+Revision ID: af906e964978
+Revises: c29facfe716b
+Create Date: 2024-10-20 17:02:35.241684
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+# Revision identifiers, used by Alembic.
+revision = "af906e964978"
+down_revision = "c29facfe716b"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### Create feedback table ###
+    op.create_table(
+        "feedback",
+        sa.Column(
+            "id", sa.Text(), primary_key=True
+        ),  # Unique identifier for each feedback (TEXT type)
+        sa.Column(
+            "user_id", sa.Text(), nullable=True
+        ),  # ID of the user providing the feedback (TEXT 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(
+            "created_at", sa.BigInteger(), nullable=False
+        ),  # Feedback creation timestamp (BIGINT representing epoch)
+        sa.Column(
+            "updated_at", sa.BigInteger(), nullable=False
+        ),  # Feedback update timestamp (BIGINT representing epoch)
+    )
+
+
+def downgrade():
+    # ### Drop feedback table ###
+    op.drop_table("feedback")

+ 109 - 10
src/lib/components/admin/Evaluations.svelte

@@ -1,27 +1,126 @@
 <script lang="ts">
 <script lang="ts">
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
-	import Tooltip from '../common/Tooltip.svelte';
-	import Plus from '../icons/Plus.svelte';
-	import Collapsible from '../common/Collapsible.svelte';
-	import Switch from '../common/Switch.svelte';
-	import ChevronUp from '../icons/ChevronUp.svelte';
-	import ChevronDown from '../icons/ChevronDown.svelte';
+
+	import { models } from '$lib/stores';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	let rankedModels = [];
 	let loaded = false;
 	let loaded = false;
-	let evaluationEnabled = true;
-
-	let showModels = false;
 
 
 	onMount(() => {
 	onMount(() => {
 		loaded = true;
 		loaded = true;
+
+		rankedModels = $models
+			.filter((m) => m?.owned_by !== 'arena' && (m?.info?.meta?.hidden ?? false) !== true)
+			.map((model) => {
+				return {
+					...model,
+					rating: '-',
+					stats: {
+						won: '-',
+						draw: '-',
+						lost: '-'
+					}
+				};
+			})
+			.sort((a, b) => {
+				// Handle sorting by rating ('-' goes to the end)
+				if (a.rating === '-' && b.rating !== '-') return 1;
+				if (b.rating === '-' && a.rating !== '-') return -1;
+
+				// If both have ratings (non '-'), sort by rating numerically (descending)
+				if (a.rating !== '-' && b.rating !== '-') return b.rating - a.rating;
+
+				// If both ratings are '-', sort alphabetically (by 'name')
+				return a.name.localeCompare(b.name);
+			});
 	});
 	});
 </script>
 </script>
 
 
 {#if loaded}
 {#if loaded}
-	<div class="my-0.5 gap-1 flex flex-col md:flex-row justify-between">
+	<div class="mt-0.5 mb-3 gap-1 flex flex-col md:flex-row justify-between">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 			{$i18n.t('Leaderboard')}
 			{$i18n.t('Leaderboard')}
+
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+
+			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{rankedModels.length}</span
+			>
+		</div>
+	</div>
+
+	<div
+		class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"
+	>
+		<table
+			class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
+		>
+			<thead
+				class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
+			>
+				<tr class="">
+					<th scope="col" class="px-3 py-1.5 cursor-pointer select-none">
+						{$i18n.t('Model')}
+					</th>
+					<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none">
+						{$i18n.t('Rating')}
+					</th>
+					<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
+						{$i18n.t('Won')}
+					</th>
+
+					<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
+						{$i18n.t('Draw')}
+					</th>
+					<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
+						{$i18n.t('Lost')}
+					</th>
+				</tr>
+			</thead>
+			<tbody class="">
+				{#each rankedModels as model (model.id)}
+					<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
+						<td class="px-3 py-1 flex flex-col justify-center">
+							<div class="flex items-center gap-2">
+								<div class="flex-shrink-0">
+									<img
+										src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
+										alt={model.name}
+										class="size-6 rounded-full object-cover shrink-0"
+									/>
+								</div>
+
+								<div class="font-medium text-gray-600 dark:text-gray-400 pr-4">
+									{model.name}
+								</div>
+							</div>
+						</td>
+						<td class="px-3 py-1 text-right font-medium text-gray-900 dark:text-white w-max">
+							{model.rating}
+						</td>
+
+						<td class=" px-3 py-1 text-right font-semibold text-green-500"> {model.stats.won} </td>
+
+						<td class=" px-3 py-1 text-right font-semibold">
+							{model.stats.draw}
+						</td>
+
+						<td class="px-3 py-1 text-right font-semibold text-red-500">
+							{model.stats.lost}
+						</td>
+					</tr>
+				{/each}
+			</tbody>
+		</table>
+	</div>
+
+	<div class="pb-4"></div>
+
+	<div class="mt-0.5 mb-3 gap-1 flex flex-col md:flex-row justify-between">
+		<div class="flex md:self-center text-lg font-medium px-0.5">
+			{$i18n.t('Rating History')}
 		</div>
 		</div>
 	</div>
 	</div>
+
+	<div class="pb-8"></div>
 {/if}
 {/if}

+ 1 - 1
src/lib/components/chat/Messages/RateComment.svelte

@@ -140,7 +140,7 @@
 	</div>
 	</div>
 
 
 	<div class="mt-2 gap-1.5 flex justify-end">
 	<div class="mt-2 gap-1.5 flex justify-end">
-		{#if $config?.features.enable_community_sharing}
+		{#if $config?.features.enable_community_sharing && selectedModel}
 			<button
 			<button
 				class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition"
 				class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition"
 				type="button"
 				type="button"

+ 1 - 1
src/lib/components/workspace/Functions.svelte

@@ -228,7 +228,7 @@
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 			{$i18n.t('Functions')}
 			{$i18n.t('Functions')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 				>{filteredItems.length}</span
 				>{filteredItems.length}</span
 			>
 			>

+ 1 - 1
src/lib/components/workspace/Knowledge.svelte

@@ -122,7 +122,7 @@
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 			{$i18n.t('Knowledge')}
 			{$i18n.t('Knowledge')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 				>{filteredItems.length}</span
 				>{filteredItems.length}</span
 			>
 			>

+ 1 - 1
src/lib/components/workspace/Models.svelte

@@ -348,7 +348,7 @@
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 			{$i18n.t('Models')}
 			{$i18n.t('Models')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-850" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 				>{filteredModels.length}</span
 				>{filteredModels.length}</span
 			>
 			>

+ 1 - 1
src/lib/components/workspace/Prompts.svelte

@@ -125,7 +125,7 @@
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 			{$i18n.t('Prompts')}
 			{$i18n.t('Prompts')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 				>{filteredItems.length}</span
 				>{filteredItems.length}</span
 			>
 			>

+ 1 - 1
src/lib/components/workspace/Tools.svelte

@@ -200,7 +200,7 @@
 	<div class="flex justify-between items-center">
 	<div class="flex justify-between items-center">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 		<div class="flex md:self-center text-base font-medium px-0.5">
 			{$i18n.t('Tools')}
 			{$i18n.t('Tools')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 			<span class="text-base font-medium text-gray-500 dark:text-gray-300"
 				>{filteredItems.length}</span
 				>{filteredItems.length}</span
 			>
 			>

+ 2 - 1
src/routes/(app)/admin/+page.svelte

@@ -141,7 +141,8 @@
 	<div class="mt-0.5 mb-3 gap-1 flex flex-col md:flex-row justify-between">
 	<div class="mt-0.5 mb-3 gap-1 flex flex-col md:flex-row justify-between">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 		<div class="flex md:self-center text-lg font-medium px-0.5">
 			{$i18n.t('Users')}
 			{$i18n.t('Users')}
-			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
+			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
+
 			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
 			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
 		</div>
 		</div>