refac
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""add user variables
|
||||
|
||||
Revision ID: b0018471bbbe
|
||||
Revises: c49178636c78
|
||||
Create Date: 2026-07-24 01:21:46.457057
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b0018471bbbe'
|
||||
down_revision: Union[str, None] = 'c49178636c78'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = [col['name'] for col in inspector.get_columns('user')]
|
||||
|
||||
if 'variables' not in columns:
|
||||
op.add_column('user', sa.Column('variables', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('user', 'variables')
|
||||
@@ -9,7 +9,7 @@ from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
||||
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
||||
from open_webui.utils.misc import throttle
|
||||
from open_webui.utils.validate import validate_profile_image_url
|
||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
@@ -69,6 +69,7 @@ class User(Base): # identity & profile
|
||||
|
||||
# Metadata
|
||||
info = Column(JSON, nullable=True)
|
||||
variables = Column(JSON, nullable=True)
|
||||
settings = Column(JSON, nullable=True)
|
||||
oauth = Column(JSON, nullable=True)
|
||||
scim = Column(JSON, nullable=True)
|
||||
@@ -105,6 +106,7 @@ class UserModel(BaseModel):
|
||||
status_expires_at: int | None = None
|
||||
|
||||
info: dict | None = None
|
||||
variables: dict = Field(default_factory=dict, exclude=True)
|
||||
settings: UserSettings | None = None
|
||||
|
||||
oauth: dict | None = None
|
||||
@@ -126,6 +128,11 @@ class UserModel(BaseModel):
|
||||
self.profile_image_url = self.profile_image_url or _DEFAULT_PROFILE_IMAGE_URL.format(user_id=self.id)
|
||||
return self
|
||||
|
||||
@field_validator('variables', mode='before')
|
||||
@classmethod
|
||||
def normalize_variables(cls, value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
class UserStatusModel(UserModel):
|
||||
is_active: bool = False
|
||||
|
||||
@@ -44,6 +44,7 @@ from open_webui.utils.auth import (
|
||||
get_verified_user,
|
||||
validate_password,
|
||||
)
|
||||
from open_webui.utils.chat_variables import ChatVariablesError, normalize_user_variables, validate_user_variables
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -574,6 +575,52 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user), db: Asy
|
||||
return user.info
|
||||
|
||||
|
||||
class UserVariablesForm(BaseModel):
|
||||
variables: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class UserVariablesResponse(BaseModel):
|
||||
variables: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserVariablesBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.get('/user/variables', response_model=UserVariablesResponse)
|
||||
async def get_user_variables_by_session_user(user=Depends(get_verified_user)):
|
||||
return UserVariablesResponse(variables=normalize_user_variables(user.variables))
|
||||
|
||||
|
||||
############################
|
||||
# UpdateUserVariablesBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.post('/user/variables/update', response_model=UserVariablesResponse)
|
||||
async def update_user_variables_by_session_user(
|
||||
form_data: UserVariablesForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
try:
|
||||
variables = validate_user_variables(form_data.variables)
|
||||
except ChatVariablesError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
updated = await Users.update_user_by_id(user.id, {'variables': variables}, db=db)
|
||||
if not updated:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
return UserVariablesResponse(variables=variables)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateUserInfoBySessionUser
|
||||
############################
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
CHAT_VARIABLE_KEY_RE = re.compile(r'^[a-z][a-z0-9_]*$')
|
||||
CHAT_VARIABLE_ANY_RE = re.compile(r'{{\s*chat\.variables\.([^\s|}]+)(?:\s*\|\s*([^}]*))?\s*}}')
|
||||
USER_VARIABLE_ANY_RE = re.compile(r'{{\s*user\.variables\.([^\s|}]+)(?:\s*\|\s*([^}]*))?\s*}}')
|
||||
MAX_VARIABLE_VALUE_LENGTH = 20_000
|
||||
MAX_VARIABLES_JSON_LENGTH = 100_000
|
||||
|
||||
@@ -173,6 +174,36 @@ def normalize_chat_variables(variables: Any) -> dict[str, Any]:
|
||||
return variables
|
||||
|
||||
|
||||
def normalize_user_variables(variables: Any) -> dict[str, str]:
|
||||
if not isinstance(variables, dict):
|
||||
return {}
|
||||
return {key: value for key, value in variables.items() if isinstance(key, str) and isinstance(value, str)}
|
||||
|
||||
|
||||
def validate_user_variables(variables: Any) -> dict[str, str]:
|
||||
if not isinstance(variables, dict):
|
||||
raise ChatVariablesError('User variables must be an object.')
|
||||
|
||||
try:
|
||||
if len(json.dumps(variables)) > MAX_VARIABLES_JSON_LENGTH:
|
||||
raise ChatVariablesError('User variables are too large.')
|
||||
except TypeError:
|
||||
raise ChatVariablesError('User variables must be JSON serializable.')
|
||||
|
||||
validated: dict[str, str] = {}
|
||||
for key, value in variables.items():
|
||||
if not isinstance(key, str) or not CHAT_VARIABLE_KEY_RE.match(key):
|
||||
raise ChatVariablesError(f'Invalid user variable key: {key}')
|
||||
if not isinstance(value, str):
|
||||
raise ChatVariablesError(f'User variable must be a string: {key}')
|
||||
value = value.replace('\r\n', '\n')
|
||||
if len(value) > MAX_VARIABLE_VALUE_LENGTH:
|
||||
raise ChatVariablesError(f'User variable is too long: {key}')
|
||||
validated[key] = value
|
||||
|
||||
return validated
|
||||
|
||||
|
||||
def validate_chat_variables(
|
||||
system_prompt: str | None,
|
||||
variables: Any,
|
||||
@@ -240,3 +271,18 @@ def render_chat_variables(
|
||||
return '' if value is None else str(value)
|
||||
|
||||
return CHAT_VARIABLE_ANY_RE.sub(replace, system_prompt)
|
||||
|
||||
|
||||
def render_user_variables(system_prompt: str | None, variables: Any) -> str | None:
|
||||
if not system_prompt:
|
||||
return system_prompt
|
||||
|
||||
variables = normalize_user_variables(variables)
|
||||
|
||||
def replace(match: re.Match) -> str:
|
||||
key = match.group(1).strip()
|
||||
if not CHAT_VARIABLE_KEY_RE.match(key):
|
||||
return ''
|
||||
return variables.get(key, '')
|
||||
|
||||
return USER_VARIABLE_ANY_RE.sub(replace, system_prompt)
|
||||
|
||||
@@ -7,7 +7,7 @@ from open_webui.utils.misc import (
|
||||
deep_update,
|
||||
replace_system_message_content,
|
||||
)
|
||||
from open_webui.utils.chat_variables import render_chat_variables
|
||||
from open_webui.utils.chat_variables import render_chat_variables, render_user_variables
|
||||
from open_webui.utils.task import prompt_template, prompt_variables_template
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ async def resolve_system_prompt(
|
||||
required=False,
|
||||
)
|
||||
|
||||
system = render_user_variables(system, getattr(user, 'variables', {}) if user else {})
|
||||
|
||||
# Metadata (WebUI Usage)
|
||||
if metadata:
|
||||
variables = metadata.get('variables', {})
|
||||
|
||||
Reference in New Issue
Block a user