"""Per-user UI preferences (account-bound, follow the user across devices). ``hide_admin_prompt_prefix`` hides the admin-configured prompt-prefix switch. ``followup_suggestions_enabled`` opts in to recommended questions after answers. Kept as a small key-value (JSON) store so new toggles do not each need a schema change. """ from __future__ import annotations import logging from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel, Field from app.gateway.api_read_cache import get_cache_version, get_cached_json, set_cached_json from deerflow.runtime.user_context import get_effective_user_id logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/user-preferences", tags=["user-preferences"]) class UserPreferencesResponse(BaseModel): hide_admin_prompt_prefix: bool = False followup_suggestions_enabled: bool = False class UserPreferencesUpdateRequest(BaseModel): hide_admin_prompt_prefix: bool | None = Field(default=None) followup_suggestions_enabled: bool | None = Field(default=None) def _current_user_id(request: Request) -> str: user = getattr(request.state, "user", None) if user is not None: return str(user.id) return get_effective_user_id() def _get_store(request: Request): store = getattr(request.app.state, "user_preferences_store", None) if store is None: raise HTTPException(status_code=503, detail="User preferences store not available") return store def _to_response(prefs: dict) -> UserPreferencesResponse: return UserPreferencesResponse( hide_admin_prompt_prefix=bool(prefs.get("hide_admin_prompt_prefix", False)), followup_suggestions_enabled=bool(prefs.get("followup_suggestions_enabled", False)), ) @router.get("", response_model=UserPreferencesResponse) async def get_user_preferences(request: Request) -> UserPreferencesResponse: user_id = _current_user_id(request) config = getattr(request.app.state, "config", None) cache_key = f"user:{user_id}" version = await get_cache_version(config, "user_preferences") cached = await get_cached_json(config, "user_preferences", cache_key, version=version) if cached is not None: return UserPreferencesResponse.model_validate(cached) store = _get_store(request) prefs = await store.get_for_user(user_id) response = _to_response(prefs) await set_cached_json(config, "user_preferences", cache_key, response, expected_version=version) return response @router.put("", response_model=UserPreferencesResponse) async def update_user_preferences(request: Request, body: UserPreferencesUpdateRequest) -> UserPreferencesResponse: store = _get_store(request) user_id = _current_user_id(request) patch = body.model_dump(exclude_unset=True, exclude_none=True) if not patch: return _to_response(await store.get_for_user(user_id)) prefs = await store.merge_for_user(user_id, patch) return _to_response(prefs)