from __future__ import annotations import json from sqlalchemy import select from sqlalchemy.exc import IntegrityError from services.shared.core import new_id, utc_now_iso from services.shared.sql_models import VoiceTranscriptSegmentRow _SEQUENCE_CONFLICT_MARKERS = ( "uq_voice_transcript_segments_session_sequence", "voice_transcript_segments.session_id, voice_transcript_segments.sequence_no", "voice_transcript_segments.session_id, sequence_no", ) def next_transcript_sequence(session, session_id: str) -> int: row = session.execute( select(VoiceTranscriptSegmentRow) .where(VoiceTranscriptSegmentRow.session_id == session_id) .order_by(VoiceTranscriptSegmentRow.sequence_no.desc()) .limit(1) ).scalar_one_or_none() if row is None: return 1 return int(row.sequence_no or 0) + 1 def _is_sequence_conflict(exc: IntegrityError) -> bool: message = str(exc).lower() return any(marker in message for marker in _SEQUENCE_CONFLICT_MARKERS) def add_transcript_segment( session, *, session_id: str, call_id: str, interaction_id: str | None, speaker: str, source_type: str, text: str, confidence: float | None = None, payload: dict | None = None, is_final: bool = True, barge_in_interrupted: bool = False, sequence_no: int | None = None, created_at: str | None = None, max_attempts: int = 5, ) -> VoiceTranscriptSegmentRow: for attempt in range(max_attempts): candidate_sequence = sequence_no if attempt == 0 and sequence_no is not None else next_transcript_sequence(session, session_id) row = VoiceTranscriptSegmentRow( segment_id=new_id("vts"), session_id=session_id, call_id=call_id, interaction_id=interaction_id, speaker=speaker, source_type=source_type, sequence_no=candidate_sequence, text=text, confidence=confidence, is_final=is_final, barge_in_interrupted=barge_in_interrupted, payload_json=json.dumps(payload or {}, ensure_ascii=False), created_at=created_at or utc_now_iso(), ) try: with session.begin_nested(): session.add(row) session.flush() return row except IntegrityError as exc: if not _is_sequence_conflict(exc) or attempt >= max_attempts - 1: raise session.expire_all() raise RuntimeError("Voice transcript segment insert retry exhausted")