Source code for nlp_shap.domain.conversation

"""Immutable conversation snapshots without backend or IO."""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass

from .enums import Role


[docs] @dataclass(frozen=True, slots=True) class Message: """A single explainable text unit with an attached role.""" role: Role """Participant role for this text unit.""" text: str """Token or span text included in the explainability input."""
[docs] @dataclass(frozen=True, slots=True) class Turn: """One conversational turn composed of ordered messages.""" messages: tuple[Message, ...] """Ordered messages that make up this turn.""" def __post_init__(self) -> None: if not self.messages: msg = "turn must contain at least one message" raise ValueError(msg)
[docs] @dataclass(frozen=True, slots=True) class ConversationSnapshot: """Frozen conversation state used as the explainability input.""" turns: tuple[Turn, ...] """Ordered turns that define the conversation under study.""" snapshot_id: str """Stable identifier used for deduplication and run archives.""" def __post_init__(self) -> None: if not self.turns: msg = "snapshot must contain at least one turn" raise ValueError(msg) if not self.snapshot_id: msg = "snapshot_id must be non-empty" raise ValueError(msg)
[docs] @classmethod def from_turns(cls, turns: tuple[Turn, ...]) -> ConversationSnapshot: """Build a snapshot with a stable content-derived identifier.""" return cls(turns=turns, snapshot_id=_digest_turns(turns))
def _digest_turns(turns: tuple[Turn, ...]) -> str: payload = [ [ {"role": message.role.value, "text": message.text} for message in turn.messages ] for turn in turns ] encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")) return hashlib.sha256(encoded.encode()).hexdigest()[:16]