chatty / app /services /dictionary.py
GitHub Action
Deploy to Hugging Face Space
3470cf9
Raw
History Blame Contribute Delete
2.66 kB
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