feat: implement global memories toggle and permissions (#20462)

This commit is contained in:
G30
2026-01-07 23:50:04 +04:00
committed by GitHub
parent 48f1b2d547
commit b73d30b6df
10 changed files with 152 additions and 5 deletions
+11
View File
@@ -1470,6 +1470,10 @@ USER_PERMISSIONS_FEATURES_API_KEYS = (
os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true" 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 = ( USER_PERMISSIONS_SETTINGS_INTERFACE = (
os.environ.get("USER_PERMISSIONS_SETTINGS_INTERFACE", "True").lower() == "true" os.environ.get("USER_PERMISSIONS_SETTINGS_INTERFACE", "True").lower() == "true"
@@ -1533,6 +1537,7 @@ DEFAULT_USER_PERMISSIONS = {
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH, "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
"image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
"code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER, "code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
"memories": USER_PERMISSIONS_FEATURES_MEMORIES,
}, },
"settings": { "settings": {
"interface": USER_PERMISSIONS_SETTINGS_INTERFACE, "interface": USER_PERMISSIONS_SETTINGS_INTERFACE,
@@ -2083,6 +2088,12 @@ ENABLE_CODE_INTERPRETER = PersistentConfig(
os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true", 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 = PersistentConfig(
"CODE_INTERPRETER_ENGINE", "CODE_INTERPRETER_ENGINE",
"code_interpreter.engine", "code_interpreter.engine",
+3
View File
@@ -145,6 +145,7 @@ from open_webui.config import (
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
CODE_INTERPRETER_JUPYTER_TIMEOUT, CODE_INTERPRETER_JUPYTER_TIMEOUT,
ENABLE_MEMORIES,
# Image # Image
AUTOMATIC1111_API_AUTH, AUTOMATIC1111_API_AUTH,
AUTOMATIC1111_BASE_URL, 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.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_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_GENERATION_MODEL = IMAGE_GENERATION_MODEL
app.state.config.IMAGE_SIZE = IMAGE_SIZE 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_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
"enable_memories": app.state.config.ENABLE_MEMORIES,
**( **(
{ {
"enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL, "enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL,
+4
View File
@@ -991,6 +991,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS, "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS,
"FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "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_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
@@ -1015,6 +1016,7 @@ class AdminConfig(BaseModel):
ENABLE_FOLDERS: bool ENABLE_FOLDERS: bool
FOLDER_MAX_FILE_COUNT: Optional[int | str] = None FOLDER_MAX_FILE_COUNT: Optional[int | str] = None
ENABLE_CHANNELS: bool ENABLE_CHANNELS: bool
ENABLE_MEMORIES: bool
ENABLE_NOTES: bool ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool ENABLE_USER_WEBHOOKS: bool
PENDING_USER_OVERLAY_TITLE: Optional[str] = None PENDING_USER_OVERLAY_TITLE: Optional[str] = None
@@ -1046,6 +1048,7 @@ async def update_admin_config(
else None else None
) )
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS 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 request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]: 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, "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS,
"FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "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_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
+99 -4
View File
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel from pydantic import BaseModel
import logging import logging
import asyncio import asyncio
@@ -10,11 +10,16 @@ from open_webui.utils.auth import get_verified_user
from open_webui.internal.db import get_session from open_webui.internal.db import get_session
from sqlalchemy.orm import 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__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/ef") @router.get("/ef")
async def get_embeddings(request: Request): async def get_embeddings(request: Request):
return {"result": await request.app.state.EMBEDDING_FUNCTION("hello world")} 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]) @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) return Memories.get_memories_by_user_id(user.id, db=db)
@@ -50,6 +69,18 @@ async def add_memory(
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), 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) memory = Memories.insert_new_memory(user.id, form_data.content, db=db)
vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user)
@@ -83,6 +114,18 @@ class QueryMemoryForm(BaseModel):
async def query_memory( async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user), db: Session = Depends(get_session) 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) memories = Memories.get_memories_by_user_id(user.id, db=db)
if not memories: if not memories:
raise HTTPException(status_code=404, detail="No memories found for user") 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( async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user), db: Session = Depends(get_session) 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}") VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id, db=db) 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) @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) result = Memories.delete_memories_by_user_id(user.id, db=db)
if result: if result:
@@ -168,6 +237,18 @@ async def update_memory_by_id(
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), 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 = Memories.update_memory_by_id_and_user_id(
memory_id, user.id, form_data.content, db=db 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) @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) result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db)
if result: if result:
+1
View File
@@ -226,6 +226,7 @@ class FeaturesPermissions(BaseModel):
web_search: bool = True web_search: bool = True
image_generation: bool = True image_generation: bool = True
code_interpreter: bool = True code_interpreter: bool = True
memories: bool = True
class SettingsPermissions(BaseModel): class SettingsPermissions(BaseModel):
@@ -741,6 +741,14 @@
<Switch bind:state={adminConfig.ENABLE_CHANNELS} /> <Switch bind:state={adminConfig.ENABLE_CHANNELS} />
</div> </div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Memories')} ({$i18n.t('Beta')})
</div>
<Switch bind:state={adminConfig.ENABLE_MEMORIES} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('User Webhooks')} {$i18n.t('User Webhooks')}
@@ -782,6 +782,22 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-col w-full">
<div class="flex w-full justify-between my-1">
<div class=" self-center text-xs font-medium">
{$i18n.t('Memories')}
</div>
<Switch bind:state={permissions.features.memories} />
</div>
{#if defaultPermissions?.features?.memories && !permissions.features.memories}
<div>
<div class="text-xs text-gray-500">
{$i18n.t('This is a default user permission and will remain enabled.')}
</div>
</div>
{/if}
</div>
</div> </div>
<hr class=" border-gray-100/30 dark:border-gray-850/30" /> <hr class=" border-gray-100/30 dark:border-gray-850/30" />
@@ -491,6 +491,13 @@
return $user?.role === 'admin' || ($user?.permissions?.settings?.interface ?? true); 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; return true;
}); });
}; };
+2 -1
View File
@@ -52,7 +52,8 @@ export const DEFAULT_PERMISSIONS = {
direct_tool_servers: false, direct_tool_servers: false,
web_search: true, web_search: true,
image_generation: true, image_generation: true,
code_interpreter: true code_interpreter: true,
memories: true
}, },
settings: { settings: {
interface: true interface: true
+1
View File
@@ -273,6 +273,7 @@ type Config = {
enable_admin_export: boolean; enable_admin_export: boolean;
enable_admin_chat_access: boolean; enable_admin_chat_access: boolean;
enable_community_sharing: boolean; enable_community_sharing: boolean;
enable_memories: boolean;
enable_autocomplete_generation: boolean; enable_autocomplete_generation: boolean;
enable_direct_connections: boolean; enable_direct_connections: boolean;
enable_version_update_check: boolean; enable_version_update_check: boolean;