"""Moment CRUD, entity linking, and embedding sync. Moments are small structured extractions from journal conversations. Stored separately from Notes — they have their own embedding index and the isolation between notes-RAG and journal-RAG is enforced at the API boundary, not just the schema. """ from __future__ import annotations import datetime from typing import Sequence from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from fabledassistant.models import ( Moment, MomentEmbedding, async_session, moment_notes, moment_people, moment_places, moment_tasks, ) from fabledassistant.services.embeddings import get_embedding async def create_moment( *, user_id: int, content: str, occurred_at: datetime.datetime, day_date: datetime.date, conversation_id: int | None = None, source_message_id: int | None = None, raw_excerpt: str | None = None, tags: Sequence[str] = (), person_ids: Sequence[int] = (), place_ids: Sequence[int] = (), task_ids: Sequence[int] = (), note_ids: Sequence[int] = (), ) -> Moment: """Insert a Moment, write its embedding, create junction rows.""" async with async_session() as session: moment = Moment( user_id=user_id, conversation_id=conversation_id, source_message_id=source_message_id, day_date=day_date, occurred_at=occurred_at, content=content, raw_excerpt=raw_excerpt, tags=list(tags), ) session.add(moment) await session.flush() await _set_links( session, moment_id=moment.id, person_ids=person_ids, place_ids=place_ids, task_ids=task_ids, note_ids=note_ids, ) embedding_vector = await get_embedding(content) session.add( MomentEmbedding( moment_id=moment.id, user_id=user_id, embedding=embedding_vector, ) ) await session.commit() await session.refresh(moment, ["people", "places", "tasks", "notes"]) return moment async def get_moment(*, user_id: int, moment_id: int) -> Moment | None: async with async_session() as session: result = await session.execute( select(Moment).where(Moment.id == moment_id, Moment.user_id == user_id) ) return result.scalar_one_or_none() async def update_moment( *, user_id: int, moment_id: int, content: str | None = None, tags: Sequence[str] | None = None, pinned: bool | None = None, person_ids: Sequence[int] | None = None, place_ids: Sequence[int] | None = None, task_ids: Sequence[int] | None = None, note_ids: Sequence[int] | None = None, ) -> Moment | None: """Update fields and (optionally) replace junction sets. None for a junction parameter ⇒ leave unchanged. Empty sequence ⇒ clear. Mirrors the Notes update convention. """ async with async_session() as session: result = await session.execute( select(Moment).where(Moment.id == moment_id, Moment.user_id == user_id) ) moment = result.scalar_one_or_none() if moment is None: return None if content is not None: moment.content = content embedding_vector = await get_embedding(content) await session.execute( delete(MomentEmbedding).where(MomentEmbedding.moment_id == moment_id) ) session.add( MomentEmbedding( moment_id=moment_id, user_id=user_id, embedding=embedding_vector, ) ) if tags is not None: moment.tags = list(tags) if pinned is not None: moment.pinned = pinned await _set_links( session, moment_id=moment_id, person_ids=person_ids, place_ids=place_ids, task_ids=task_ids, note_ids=note_ids, ) await session.commit() await session.refresh(moment, ["people", "places", "tasks", "notes"]) return moment async def delete_moment(*, user_id: int, moment_id: int) -> bool: async with async_session() as session: result = await session.execute( select(Moment).where(Moment.id == moment_id, Moment.user_id == user_id) ) moment = result.scalar_one_or_none() if moment is None: return False await session.delete(moment) await session.commit() return True async def _set_links( session: AsyncSession, *, moment_id: int, person_ids: Sequence[int] | None, place_ids: Sequence[int] | None, task_ids: Sequence[int] | None, note_ids: Sequence[int] | None, ) -> None: """Replace junction-table rows. None ⇒ leave existing rows alone. Empty sequence ⇒ clear all rows. """ for ids, table, fk_col in [ (person_ids, moment_people, "person_id"), (place_ids, moment_places, "place_id"), (task_ids, moment_tasks, "task_id"), (note_ids, moment_notes, "note_id"), ]: if ids is None: continue await session.execute(delete(table).where(table.c.moment_id == moment_id)) if ids: await session.execute( table.insert(), [{"moment_id": moment_id, fk_col: i} for i in ids], )