From c5eb0a973243d5f4e757e784b620d308364532c1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 1 Oct 2024 17:35:35 -0700 Subject: [PATCH] refac: documents -> projects --- backend/open_webui/apps/webui/main.py | 4 +- .../open_webui/apps/webui/models/projects.py | 142 +++++++++++++++ .../open_webui/apps/webui/routers/projects.py | 95 ++++++++++ src/lib/apis/projects/index.ts | 167 +++++++++++++++++ .../admin/Settings/Documents.svelte | 14 +- .../chat/MessageInput/Commands.svelte | 4 +- .../{Documents.svelte => Projects.svelte} | 117 +++++------- .../Models/Knowledge/Selector.svelte | 21 +-- src/lib/components/workspace/Projects.svelte | 168 ++++++++++++++++++ .../workspace/Projects/CreateProject.svelte | 0 .../workspace/Projects/EditProject.svelte | 0 .../workspace/Projects/ProjectMenu.svelte | 65 +++++++ src/lib/stores/index.ts | 2 +- src/routes/(app)/+layout.svelte | 56 +++--- src/routes/(app)/workspace/+layout.svelte | 8 +- .../(app)/workspace/documents/+page.svelte | 5 - .../(app)/workspace/projects/+page.svelte | 5 + .../workspace/projects/create/+page.svelte | 5 + .../workspace/projects/edit/+page.svelte | 5 + 19 files changed, 748 insertions(+), 135 deletions(-) create mode 100644 backend/open_webui/apps/webui/models/projects.py create mode 100644 backend/open_webui/apps/webui/routers/projects.py create mode 100644 src/lib/apis/projects/index.ts rename src/lib/components/chat/MessageInput/Commands/{Documents.svelte => Projects.svelte} (64%) create mode 100644 src/lib/components/workspace/Projects.svelte create mode 100644 src/lib/components/workspace/Projects/CreateProject.svelte create mode 100644 src/lib/components/workspace/Projects/EditProject.svelte create mode 100644 src/lib/components/workspace/Projects/ProjectMenu.svelte delete mode 100644 src/routes/(app)/workspace/documents/+page.svelte create mode 100644 src/routes/(app)/workspace/projects/+page.svelte create mode 100644 src/routes/(app)/workspace/projects/create/+page.svelte create mode 100644 src/routes/(app)/workspace/projects/edit/+page.svelte diff --git a/backend/open_webui/apps/webui/main.py b/backend/open_webui/apps/webui/main.py index 6c6f197dd..e58f83654 100644 --- a/backend/open_webui/apps/webui/main.py +++ b/backend/open_webui/apps/webui/main.py @@ -10,7 +10,7 @@ from open_webui.apps.webui.routers import ( auths, chats, configs, - documents, + projects, files, functions, memories, @@ -111,7 +111,7 @@ app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(chats.router, prefix="/chats", tags=["chats"]) -app.include_router(documents.router, prefix="/documents", tags=["documents"]) +app.include_router(projects.router, prefix="/projects", tags=["projects"]) app.include_router(models.router, prefix="/models", tags=["models"]) app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) diff --git a/backend/open_webui/apps/webui/models/projects.py b/backend/open_webui/apps/webui/models/projects.py new file mode 100644 index 000000000..5b9f07090 --- /dev/null +++ b/backend/open_webui/apps/webui/models/projects.py @@ -0,0 +1,142 @@ +import json +import logging +import time +from typing import Optional + +from open_webui.apps.webui.internal.db import Base, get_db +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text, JSON + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Projects DB Schema +#################### + + +class Project(Base): + __tablename__ = "project" + + id = Column(Text, unique=True, primary_key=True) + user_id = Column(Text) + + name = Column(Text) + description = Column(Text) + + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class ProjectModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + + name: str + description: str + + data: Optional[dict] = None + meta: Optional[dict] = None + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class ProjectResponse(BaseModel): + id: str + name: str + description: str + data: Optional[dict] = None + meta: Optional[dict] = None + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +class ProjectForm(BaseModel): + id: str + name: str + description: str + data: Optional[dict] = None + + +class ProjectTable: + def insert_new_project( + self, user_id: str, form_data: ProjectForm + ) -> Optional[ProjectModel]: + with get_db() as db: + project = ProjectModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + + try: + result = Project(**project.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return ProjectModel.model_validate(result) + else: + return None + except Exception: + return None + + def get_projects(self) -> list[ProjectModel]: + with get_db() as db: + return [ + ProjectModel.model_validate(project) + for project in db.query(Project).all() + ] + + def get_project_by_id(self, id: str) -> Optional[ProjectModel]: + try: + with get_db() as db: + project = db.query(Project).filter_by(id=id).first() + return ProjectModel.model_validate(project) if project else None + except Exception: + return None + + def update_project_by_id( + self, id: str, form_data: ProjectForm + ) -> Optional[ProjectModel]: + try: + with get_db() as db: + db.query(Project).filter_by(id=id).update( + { + "name": form_data.name, + "updated_id": int(time.time()), + } + ) + db.commit() + return self.get_project_by_id(id=form_data.id) + except Exception as e: + log.exception(e) + return None + + def delete_project_by_id(self, id: str) -> bool: + try: + with get_db() as db: + db.query(Project).filter_by(id=id).delete() + db.commit() + return True + except Exception: + return False + + +Projects = ProjectTable() diff --git a/backend/open_webui/apps/webui/routers/projects.py b/backend/open_webui/apps/webui/routers/projects.py new file mode 100644 index 000000000..ed47b41b2 --- /dev/null +++ b/backend/open_webui/apps/webui/routers/projects.py @@ -0,0 +1,95 @@ +import json +from typing import Optional, Union +from pydantic import BaseModel +from fastapi import APIRouter, Depends, HTTPException, status + + +from open_webui.apps.webui.models.projects import ( + Projects, + ProjectModel, + ProjectForm, + ProjectResponse, +) +from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.utils import get_admin_user, get_verified_user + +router = APIRouter() + +############################ +# GetProjects +############################ + + +@router.get("/", response_model=Optional[Union[list[ProjectResponse], ProjectResponse]]) +async def get_projects(id: Optional[str] = None, user=Depends(get_verified_user)): + if id: + project = Projects.get_project_by_id(id=id) + + if project: + return project + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + return [ + ProjectResponse(**project.model_dump()) + for project in Projects.get_projects() + ] + + +############################ +# CreateNewProject +############################ + + +@router.post("/create", response_model=Optional[ProjectResponse]) +async def create_new_project(form_data: ProjectForm, user=Depends(get_admin_user)): + project = Projects.get_project_by_id(form_data.id) + if project is None: + project = Projects.insert_new_project(user.id, form_data) + + if project: + return project + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_EXISTS, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# UpdateProjectById +############################ + + +@router.post("/update", response_model=Optional[ProjectResponse]) +async def update_project_by_id( + form_data: ProjectForm, + user=Depends(get_admin_user), +): + project = Projects.update_project_by_id(form_data) + if project: + return project + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# DeleteProjectById +############################ + + +@router.delete("/delete", response_model=bool) +async def delete_project_by_id(id: str, user=Depends(get_admin_user)): + result = Projects.delete_project_by_id(id=id) + return result diff --git a/src/lib/apis/projects/index.ts b/src/lib/apis/projects/index.ts new file mode 100644 index 000000000..8fad3ffd8 --- /dev/null +++ b/src/lib/apis/projects/index.ts @@ -0,0 +1,167 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewProject = async (token: string, id: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/projects/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + id: id, + name: name + }) + }) + .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 getProjects = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/projects/`, { + 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 getProjectById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/projects/${id}`, { + 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; +}; + +type ProjectForm = { + name: string; +}; + +export const updateProjectById = async (token: string, id: string, form: ProjectForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/projects/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: form.name + }) + }) + .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 deleteProjectById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/projects/${id}/delete`, { + 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(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index c10b60aa0..dfdc97511 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -1,10 +1,10 @@ -{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} +{#if filteredProjects.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
- {#each filteredItems as doc, docIdx} + {#each filteredProjects as project, idx} {/each} diff --git a/src/lib/components/workspace/Models/Knowledge/Selector.svelte b/src/lib/components/workspace/Models/Knowledge/Selector.svelte index 52d73540e..5dfd43ef9 100644 --- a/src/lib/components/workspace/Models/Knowledge/Selector.svelte +++ b/src/lib/components/workspace/Models/Knowledge/Selector.svelte @@ -1,11 +1,10 @@ diff --git a/src/lib/components/workspace/Projects.svelte b/src/lib/components/workspace/Projects.svelte new file mode 100644 index 000000000..1a37c8282 --- /dev/null +++ b/src/lib/components/workspace/Projects.svelte @@ -0,0 +1,168 @@ + + + + + {$i18n.t('Projects')} | {$WEBUI_NAME} + + + + { + deleteHandler(selectedProject); + }} +/> + +
+
+
+ {$i18n.t('Projects')} +
+ {$projects.length} +
+
+
+ +
+
+
+ + + +
+ +
+ +
+ +
+
+ +
+ +
+ {#each filteredProjects as project} + + {/each} +
+ +
+ ⓘ {$i18n.t("Use '#' in the prompt input to load and select your projects.")} +
diff --git a/src/lib/components/workspace/Projects/CreateProject.svelte b/src/lib/components/workspace/Projects/CreateProject.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/components/workspace/Projects/EditProject.svelte b/src/lib/components/workspace/Projects/EditProject.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/components/workspace/Projects/ProjectMenu.svelte b/src/lib/components/workspace/Projects/ProjectMenu.svelte new file mode 100644 index 000000000..b21308160 --- /dev/null +++ b/src/lib/components/workspace/Projects/ProjectMenu.svelte @@ -0,0 +1,65 @@ + + + { + if (e.detail === false) { + onClose(); + } + }} + align="end" +> + + + + + +
+ + { + dispatch('delete'); + }} + > + +
{$i18n.t('Delete')}
+
+
+
+
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index b96bf4a98..844eea514 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -29,7 +29,7 @@ export const tags = writable([]); export const models: Writable = writable([]); export const prompts: Writable = writable([]); -export const documents: Writable = writable([]); +export const projects: Writable = writable([]); export const tools = writable([]); export const functions = writable([]); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 83a53dffd..0556a87a1 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -3,50 +3,46 @@ import { onMount, tick, getContext } from 'svelte'; import { openDB, deleteDB } from 'idb'; import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; import mermaid from 'mermaid'; - const { saveAs } = fileSaver; - import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { fade } from 'svelte/transition'; + import { getProjects } from '$lib/apis/projects'; + import { getFunctions } from '$lib/apis/functions'; import { getModels as _getModels, getVersionUpdates } from '$lib/apis'; import { getAllChatTags } from '$lib/apis/chats'; - import { getPrompts } from '$lib/apis/prompts'; - import { getDocs } from '$lib/apis/documents'; import { getTools } from '$lib/apis/tools'; - import { getBanners } from '$lib/apis/configs'; import { getUserSettings } from '$lib/apis/users'; - import { - user, - showSettings, - settings, - models, - prompts, - documents, - tags, - banners, - showChangelog, - config, - showCallOverlay, - tools, - functions, - temporaryChatEnabled - } from '$lib/stores'; - - import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; - import Sidebar from '$lib/components/layout/Sidebar.svelte'; - import ChangelogModal from '$lib/components/ChangelogModal.svelte'; - import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte'; - import { getFunctions } from '$lib/apis/functions'; - import { page } from '$app/stores'; import { WEBUI_VERSION } from '$lib/constants'; import { compareVersion } from '$lib/utils'; + import { + config, + user, + settings, + models, + prompts, + projects, + tools, + functions, + tags, + banners, + showSettings, + showChangelog, + temporaryChatEnabled + } from '$lib/stores'; + + import Sidebar from '$lib/components/layout/Sidebar.svelte'; + import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; + import ChangelogModal from '$lib/components/ChangelogModal.svelte'; + import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte'; import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte'; - import { fade } from 'svelte/transition'; const i18n = getContext('i18n'); @@ -109,7 +105,7 @@ prompts.set(await getPrompts(localStorage.token)); })(), (async () => { - documents.set(await getDocs(localStorage.token)); + projects.set(await getProjects(localStorage.token)); })(), (async () => { tools.set(await getTools(localStorage.token)); diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte index 05ab80715..6f69cffec 100644 --- a/src/routes/(app)/workspace/+layout.svelte +++ b/src/routes/(app)/workspace/+layout.svelte @@ -69,14 +69,12 @@ > - {$i18n.t('Documents')} + {$i18n.t('Projects')} - import Documents from '$lib/components/workspace/Documents.svelte'; - - - diff --git a/src/routes/(app)/workspace/projects/+page.svelte b/src/routes/(app)/workspace/projects/+page.svelte new file mode 100644 index 000000000..9f3f25017 --- /dev/null +++ b/src/routes/(app)/workspace/projects/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/workspace/projects/create/+page.svelte b/src/routes/(app)/workspace/projects/create/+page.svelte new file mode 100644 index 000000000..d3744383a --- /dev/null +++ b/src/routes/(app)/workspace/projects/create/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/workspace/projects/edit/+page.svelte b/src/routes/(app)/workspace/projects/edit/+page.svelte new file mode 100644 index 000000000..121a5dfcf --- /dev/null +++ b/src/routes/(app)/workspace/projects/edit/+page.svelte @@ -0,0 +1,5 @@ + + +