Source code for mllm_shap.pipeline.result
"""Explain pipeline result types.
@domain pipeline
@see [[knowledge/architecture/pipeline]]
@tested-by [[tests/pipeline/test_result.py]]
"""
from dataclasses import asdict, dataclass
from typing import TypedDict
from ..domain.estimands import Estimand, EstimandWire, estimand_to_wire
from ..domain.players import PlayerSet
from ..runtime.metrics import PerfSummary
from ..runtime.scheduler import SchedulerMetrics
from .manifest import RunManifest, RunManifestPayload
[docs]
class PlayerLabelPayload(TypedDict):
"""JSON form of one player: its id and, when known, where it sits."""
player_id: str
"""Player identifier in mask bit order."""
text: str | None
"""Text the player covers; ``None`` when the output has no player spans."""
message_index: int | None
"""Message index (turns flattened); ``None`` without player spans."""
start: int | None
"""Start character offset in the message; ``None`` without player spans."""
end: int | None
"""End character offset (exclusive); ``None`` without player spans."""
speaker: str | None
"""Speaker of the player's message; ``None`` when unset or unknown."""
label: str | None
"""Display label (``PlayerSpan.display_label``); ``None`` without spans."""
[docs]
class ExplainRunPayload(TypedDict):
"""JSON form of an explain or reanalyze run, keys in a stable order."""
values: list[float]
"""Per-player attribution values."""
labels: list[PlayerLabelPayload]
"""One entry per player, aligned with ``values``."""
base_value: float
"""Payoff of the empty coalition v(empty)."""
grand_value: float
"""Payoff of the grand coalition v(N)."""
estimand: EstimandWire
"""Estimand used to aggregate the coalitions."""
metrics: dict[str, int]
"""Scheduler counters (:class:`~mllm_shap.runtime.scheduler.SchedulerMetrics`)."""
manifest: RunManifestPayload
"""Run manifest, as written next to the archive."""
perf: dict[str, float] | None
"""Wall-clock breakdown in milliseconds, when measured."""
[docs]
@dataclass(frozen=True, slots=True)
class ExplainResult:
"""Attribution output for a single explain run."""
estimand: Estimand
"""Estimand used to aggregate coalition samples."""
values: tuple[float, ...]
"""Per-player attribution values."""
base_value: float = 0.0
"""Payoff of the empty coalition v(empty); Shapley values sum to v(N) - v(empty)."""
grand_value: float = 0.0
"""Payoff of the grand coalition v(N), scored against the base generation."""
[docs]
@dataclass(frozen=True, slots=True)
class ExplainRunOutput:
"""Explain pipeline output including scheduler metrics."""
result: ExplainResult
"""Aggregated and normalized attribution values."""
metrics: SchedulerMetrics
"""Coalition scheduler counters for the run."""
run_id: str
"""Stable identifier for the explain run."""
manifest: RunManifest
"""Manifest metadata persisted with optional archives."""
perf: PerfSummary | None = None
"""Optional wall-clock breakdown for the run."""
player_set: PlayerSet | None = None
"""Players of the explained snapshot; ``None`` for reanalyzed runs."""
[docs]
def to_dict(self) -> ExplainRunPayload:
"""Return a JSON-compatible payload with a stable key order.
Labels carry each player's text and span when :attr:`player_set` is
known (``explain``); after ``reanalyze`` only the archived player ids
are available and the span fields are ``None``.
"""
return {
"values": list(self.result.values),
"labels": self._labels(),
"base_value": self.result.base_value,
"grand_value": self.result.grand_value,
"estimand": estimand_to_wire(self.result.estimand),
"metrics": asdict(self.metrics),
"manifest": self.manifest.to_dict(),
"perf": None if self.perf is None else asdict(self.perf),
}
def _labels(self) -> list[PlayerLabelPayload]:
if self.player_set is not None and self.player_set.spans:
return [
{
"player_id": player_id,
"text": span.text,
"message_index": span.message_index,
"start": span.start,
"end": span.end,
"speaker": span.speaker,
"label": span.display_label,
}
for player_id, span in zip(
self.player_set.player_ids, self.player_set.spans, strict=True
)
]
return [
{
"player_id": player_id,
"text": None,
"message_index": None,
"start": None,
"end": None,
"speaker": None,
"label": None,
}
for player_id in self.manifest.player_ids
]