"""User profile service — structured per-user preferences. Post-pivot the LLM-driven observation/consolidation surface is gone (curator deleted in Phase 8). The profile model is preserved as user-managed metadata (name, job, expertise, style, tone, interests, work schedule) — Claude can read it via the upcoming MCP profile tools. """ import logging from sqlalchemy import select from scribe.models import async_session from scribe.models.user_profile import UserProfile logger = logging.getLogger(__name__) VALID_EXPERTISE = {"novice", "intermediate", "expert"} VALID_STYLES = {"concise", "balanced", "detailed"} VALID_TONES = {"casual", "professional", "technical"} async def get_profile(user_id: int) -> UserProfile: """Get or create the profile row for a user.""" async with async_session() as session: result = await session.execute( select(UserProfile).where(UserProfile.user_id == user_id) ) profile = result.scalar_one_or_none() if profile is None: profile = UserProfile(user_id=user_id) session.add(profile) await session.commit() await session.refresh(profile) return profile async def update_profile(user_id: int, data: dict) -> UserProfile: """Upsert structured profile fields from a validated dict.""" allowed = { "display_name", "job_title", "industry", "expertise_level", "response_style", "tone", "interests", "work_schedule", } async with async_session() as session: result = await session.execute( select(UserProfile).where(UserProfile.user_id == user_id) ) profile = result.scalar_one_or_none() if profile is None: profile = UserProfile(user_id=user_id) session.add(profile) for key, value in data.items(): if key in allowed: setattr(profile, key, value) await session.commit() await session.refresh(profile) return profile