from __future__ import annotations import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import SavedWord from app.db.session import get_session_factory from app.pipelines.vocab import extract_vocab log = logging.getLogger(__name__) async def save_word( session: AsyncSession, user_id: int, word: str, meaning: str, context: str | None = None, ) -> None: """Insert or update a vocabulary word for the user.""" stmt = select(SavedWord).where(SavedWord.user_id == user_id, SavedWord.word == word) existing = (await session.execute(stmt)).scalar_one_or_none() if existing is not None: existing.meaning = meaning if context is not None: existing.context = context else: session.add(SavedWord(user_id=user_id, word=word, meaning=meaning, context=context)) await session.commit() async def save_reply_vocab(user_id: int, reply_text: str, level: str) -> None: """Extract 1-2 vocabulary words from a tutor reply and save them. Never raises. Fire-and-forget: runs after the voice reply is sent, so the Gemini turn no longer pauses to call a save_word tool mid-generation. Swallows its own errors so it can be spawned as a background task without breaking the reply path. """ try: words = await extract_vocab(reply_text, level) if not words: return factory = get_session_factory() async with factory() as session: for w in words: word = w.word.strip().lower() await save_word(session, user_id, word, w.meaning.strip()) log.info("auto-saved word '%s' for user %d", word, user_id) except Exception: log.warning("save reply vocab failed for user %d", user_id, exc_info=True) async def list_words(session: AsyncSession, user_id: int, limit: int = 50) -> list[SavedWord]: """Return saved words for a user, most recent first.""" stmt = ( select(SavedWord) .where(SavedWord.user_id == user_id) .order_by(SavedWord.id.desc()) .limit(limit) ) result = await session.execute(stmt) return list(result.scalars().all()) async def delete_word(session: AsyncSession, user_id: int, word: str) -> bool: """Delete a saved word. Returns True if found and deleted.""" stmt = select(SavedWord).where(SavedWord.user_id == user_id, SavedWord.word == word) existing = (await session.execute(stmt)).scalar_one_or_none() if existing is None: return False await session.delete(existing) await session.commit() return True