from __future__ import annotations import time from asgiref.sync import sync_to_async from django.conf import settings from core.events.manticore import get_event_ledger_backend from core.models import ConversationEvent from core.observability.tracing import ensure_trace_id from core.util import logs log = logs.get_logger("event-ledger") def event_ledger_enabled() -> bool: return bool( getattr(settings, "EVENT_LEDGER_DUAL_WRITE", False) or getattr(settings, "EVENT_PRIMARY_WRITE_PATH", False) ) def event_ledger_status() -> dict: return { "event_ledger_dual_write": bool( getattr(settings, "EVENT_LEDGER_DUAL_WRITE", False) ), "event_primary_write_path": bool( getattr(settings, "EVENT_PRIMARY_WRITE_PATH", False) ), } def _normalize_direction(value: str) -> str: direction = str(value or "system").strip().lower() if direction not in {"in", "out", "system"}: return "system" return direction def _safe_ts(value: int | None) -> int: if value is None: return int(time.time() * 1000) try: parsed = int(value) except Exception: return int(time.time() * 1000) if parsed <= 0: return int(time.time() * 1000) return parsed def append_event_sync( *, user, session, event_type: str, direction: str, actor_identifier: str = "", origin_transport: str = "", origin_message_id: str = "", origin_chat_id: str = "", payload: dict | None = None, raw_payload: dict | None = None, trace_id: str = "", ts: int | None = None, ): if not event_ledger_enabled(): return None normalized_type = str(event_type or "").strip().lower() if not normalized_type: raise ValueError("event_type is required") candidates = {str(choice[0]) for choice in ConversationEvent.EVENT_TYPE_CHOICES} if normalized_type not in candidates: raise ValueError(f"unsupported event_type: {normalized_type}") normalized_direction = _normalize_direction(direction) normalized_trace = ensure_trace_id(trace_id, payload or {}) safe_ts = _safe_ts(ts) transport = str(origin_transport or "").strip().lower() message_id = str(origin_message_id or "").strip() actor_identifier = str(actor_identifier or "").strip() origin_chat_id = str(origin_chat_id or "").strip() payload = dict(payload or {}) raw_payload = dict(raw_payload or {}) dual_write = bool(getattr(settings, "EVENT_LEDGER_DUAL_WRITE", False)) primary_write = bool(getattr(settings, "EVENT_PRIMARY_WRITE_PATH", False)) write_django = dual_write and not primary_write row = None if write_django: dedup_row = None if transport and message_id: dedup_row = ( ConversationEvent.objects.filter( user=user, session=session, event_type=normalized_type, origin_transport=transport, origin_message_id=message_id, ) .order_by("-created_at") .first() ) if dedup_row is not None: row = dedup_row else: row = ConversationEvent.objects.create( user=user, session=session, ts=safe_ts, event_type=normalized_type, direction=normalized_direction, actor_identifier=actor_identifier, origin_transport=transport, origin_message_id=message_id, origin_chat_id=origin_chat_id, payload=payload, raw_payload=raw_payload, trace_id=normalized_trace, ) try: get_event_ledger_backend().upsert_event( user_id=int(user.id), person_id=str(session.identifier.person_id), session_id=str(session.id), event_type=normalized_type, direction=normalized_direction, ts=safe_ts, actor_identifier=actor_identifier, origin_transport=transport, origin_message_id=message_id, origin_chat_id=origin_chat_id, payload=payload, raw_payload=raw_payload, trace_id=normalized_trace, ) except Exception as exc: if primary_write: raise log.warning( "Event ledger manticore dual-write failed session=%s event_type=%s err=%s", getattr(session, "id", "-"), normalized_type, exc, ) return row async def append_event(**kwargs): return await sync_to_async(append_event_sync, thread_sensitive=True)(**kwargs)