diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 680ecbb86..3fee61f6e 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1470,6 +1470,10 @@ USER_PERMISSIONS_FEATURES_API_KEYS = ( os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true" ) +USER_PERMISSIONS_FEATURES_MEMORIES = ( + os.environ.get("USER_PERMISSIONS_FEATURES_MEMORIES", "True").lower() == "true" +) + USER_PERMISSIONS_SETTINGS_INTERFACE = ( os.environ.get("USER_PERMISSIONS_SETTINGS_INTERFACE", "True").lower() == "true" @@ -1533,6 +1537,7 @@ DEFAULT_USER_PERMISSIONS = { "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH, "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, "code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER, + "memories": USER_PERMISSIONS_FEATURES_MEMORIES, }, "settings": { "interface": USER_PERMISSIONS_SETTINGS_INTERFACE, @@ -2083,6 +2088,12 @@ ENABLE_CODE_INTERPRETER = PersistentConfig( os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true", ) +ENABLE_MEMORIES = PersistentConfig( + "ENABLE_MEMORIES", + "memories.enable", + os.environ.get("ENABLE_MEMORIES", "True").lower() == "true", +) + CODE_INTERPRETER_ENGINE = PersistentConfig( "CODE_INTERPRETER_ENGINE", "code_interpreter.engine", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 6a1d79e72..2f384ee7d 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -145,6 +145,7 @@ from open_webui.config import ( CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, CODE_INTERPRETER_JUPYTER_TIMEOUT, + ENABLE_MEMORIES, # Image AUTOMATIC1111_API_AUTH, AUTOMATIC1111_BASE_URL, @@ -1106,6 +1107,7 @@ app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIM app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION +app.state.config.ENABLE_MEMORIES = ENABLE_MEMORIES app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL app.state.config.IMAGE_SIZE = IMAGE_SIZE @@ -1935,6 +1937,7 @@ async def get_app_config(request: Request): "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + "enable_memories": app.state.config.ENABLE_MEMORIES, **( { "enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL, diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index fb0c14137..2c6fc6108 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -991,6 +991,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)): "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS, "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, + "ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, @@ -1015,6 +1016,7 @@ class AdminConfig(BaseModel): ENABLE_FOLDERS: bool FOLDER_MAX_FILE_COUNT: Optional[int | str] = None ENABLE_CHANNELS: bool + ENABLE_MEMORIES: bool ENABLE_NOTES: bool ENABLE_USER_WEBHOOKS: bool PENDING_USER_OVERLAY_TITLE: Optional[str] = None @@ -1046,6 +1048,7 @@ async def update_admin_config( else None ) request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS + request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]: @@ -1091,6 +1094,7 @@ async def update_admin_config( "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS, "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, + "ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index ebb628074..6e98247ce 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel import logging import asyncio @@ -10,11 +10,16 @@ from open_webui.utils.auth import get_verified_user from open_webui.internal.db import get_session from sqlalchemy.orm import Session +from open_webui.utils.access_control import has_permission +from open_webui.constants import ERROR_MESSAGES + log = logging.getLogger(__name__) router = APIRouter() + + @router.get("/ef") async def get_embeddings(request: Request): return {"result": await request.app.state.EMBEDDING_FUNCTION("hello world")} @@ -26,7 +31,21 @@ async def get_embeddings(request: Request): @router.get("/", response_model=list[MemoryModel]) -async def get_memories(user=Depends(get_verified_user), db: Session = Depends(get_session)): +async def get_memories( + request: Request, user=Depends(get_verified_user), db: Session = Depends(get_session) +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return Memories.get_memories_by_user_id(user.id, db=db) @@ -50,6 +69,18 @@ async def add_memory( user=Depends(get_verified_user), db: Session = Depends(get_session), ): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + memory = Memories.insert_new_memory(user.id, form_data.content, db=db) vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) @@ -83,6 +114,18 @@ class QueryMemoryForm(BaseModel): async def query_memory( request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user), db: Session = Depends(get_session) ): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + memories = Memories.get_memories_by_user_id(user.id, db=db) if not memories: raise HTTPException(status_code=404, detail="No memories found for user") @@ -105,6 +148,18 @@ async def query_memory( async def reset_memory_from_vector_db( request: Request, user=Depends(get_verified_user), db: Session = Depends(get_session) ): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}") memories = Memories.get_memories_by_user_id(user.id, db=db) @@ -142,7 +197,21 @@ async def reset_memory_from_vector_db( @router.delete("/delete/user", response_model=bool) -async def delete_memory_by_user_id(user=Depends(get_verified_user), db: Session = Depends(get_session)): +async def delete_memory_by_user_id( + request: Request, user=Depends(get_verified_user), db: Session = Depends(get_session) +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + result = Memories.delete_memories_by_user_id(user.id, db=db) if result: @@ -168,6 +237,18 @@ async def update_memory_by_id( user=Depends(get_verified_user), db: Session = Depends(get_session), ): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + memory = Memories.update_memory_by_id_and_user_id( memory_id, user.id, form_data.content, db=db ) @@ -201,7 +282,21 @@ async def update_memory_by_id( @router.delete("/{memory_id}", response_model=bool) -async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)): +async def delete_memory_by_id( + memory_id: str, request: Request, user=Depends(get_verified_user), db: Session = Depends(get_session) +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not has_permission(user.id, "features.memories", request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db) if result: diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 3c3d22c61..f10448458 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -226,6 +226,7 @@ class FeaturesPermissions(BaseModel): web_search: bool = True image_generation: bool = True code_interpreter: bool = True + memories: bool = True class SettingsPermissions(BaseModel): diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 0b56de20e..c32793741 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -741,6 +741,14 @@ +
+
+ {$i18n.t('Memories')} ({$i18n.t('Beta')}) +
+ + +
+
{$i18n.t('User Webhooks')} diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 28b7b4c2c..3284f6171 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -782,6 +782,22 @@
{/if}
+ +
+
+
+ {$i18n.t('Memories')} +
+ +
+ {#if defaultPermissions?.features?.memories && !permissions.features.memories} +
+
+ {$i18n.t('This is a default user permission and will remain enabled.')} +
+
+ {/if} +

diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 9c601ca91..b9cdac92a 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -491,6 +491,13 @@ return $user?.role === 'admin' || ($user?.permissions?.settings?.interface ?? true); } + if (tab.id === 'personalization') { + return ( + $config?.features?.enable_memories && + ($user?.role === 'admin' || ($user?.permissions?.features?.memories ?? true)) + ); + } + return true; }); }; diff --git a/src/lib/constants/permissions.ts b/src/lib/constants/permissions.ts index d40cda090..8ef3426a7 100644 --- a/src/lib/constants/permissions.ts +++ b/src/lib/constants/permissions.ts @@ -52,7 +52,8 @@ export const DEFAULT_PERMISSIONS = { direct_tool_servers: false, web_search: true, image_generation: true, - code_interpreter: true + code_interpreter: true, + memories: true }, settings: { interface: true diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 9932dba05..547548019 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -273,6 +273,7 @@ type Config = { enable_admin_export: boolean; enable_admin_chat_access: boolean; enable_community_sharing: boolean; + enable_memories: boolean; enable_autocomplete_generation: boolean; enable_direct_connections: boolean; enable_version_update_check: boolean;