diff --git a/backend/open_webui/apps/webui/main.py b/backend/open_webui/apps/webui/main.py index 6c6f197ddb..e58f83654a 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 0000000000..5b9f070903 --- /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 0000000000..ed47b41b2b --- /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 0000000000..8fad3ffd89 --- /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 c10b60aa06..dfdc975114 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')}