Command line

Installing mllm-shap adds an mllm-shap command for explaining prompts without writing Python. It reads the same YAML configuration as ExplainRunner (see Configuration) and a snapshot in JSON:

mllm-shap config --example > config.yaml     # commented YAML skeleton
mllm-shap config --check config.yaml         # validate a configuration
mllm-shap explain --config config.yaml --snapshot snapshot.json --out result.json
mllm-shap explain --config config.yaml --snapshot snapshot.json \
    --render token_bar --png result.png
mllm-shap reanalyze --config config.yaml --archive runs/<run_id>
mllm-shap plugins                            # registered plugins per group

Add --log-level info (debug, info, warning or error, any case) before the command to see the pipeline’s stage events on stderr (see Runtime core). Exit codes: 0 success, 2 an invalid argument, configuration, snapshot or archive, or an input file that cannot be read, 3 the backend is unavailable (a missing extra, an unreachable server or a refused connection). Any other error is a bug and propagates with its traceback.

The snippets below call mllm_shap.cli.main() with the same arguments, which is what the command runs. They share one working directory.

Snapshot JSON

A snapshot file lists turns, each with ordered messages; roles are user, assistant and system; each message is one text, with an optional single-line "speaker" name for conversations where several people share a role. The snapshot id is derived from the content (the speaker included), so the same file always maps to the same run; a snapshot built with its own id carries it as a top-level "snapshot_id" key. to_json() and from_json() write and read this format:

from pathlib import Path

from mllm_shap import ConversationSnapshot, Message, Role, Turn

snapshot = ConversationSnapshot.from_turns((
    Turn(
        messages=(
            Message(role=Role.USER, text="Why is the sky blue", speaker="ana"),
        )
    ),
))
Path("snapshot.json").write_text(snapshot.to_json(), encoding="utf-8")
print(Path("snapshot.json").read_text(encoding="utf-8"))
restored = ConversationSnapshot.from_json(Path("snapshot.json").read_text())
print("same snapshot:", restored == snapshot)

Output

{
  "turns": [
    {
      "messages": [
        {
          "role": "user",
          "text": "Why is the sky blue",
          "speaker": "ana"
        }
      ]
    }
  ]
}
same snapshot: True

A message with audio, or with several parts, gives a "content" list instead of "text". An audio part names a file with "path" (relative to the snapshot file) or carries base64 bytes in "data":

{"turns": [{"messages": [{"role": "user", "content": [
  {"type": "text", "text": "I had that curiosity beside me at this moment"},
  {"type": "audio", "path": "voices_sp0307.wav", "sample_rate": 16000}
]}]}]}

Audio needs a backend that hears it (Audio input).

Explain

mllm-shap config --example prints a skeleton that runs as is with the mock backend; change backend to point at a real model. explain writes the result JSON to --out (or standard output). Its keys come in a fixed order: values, labels (one entry per player with its text, character span, speaker and display label), base_value, grand_value, estimand, metrics, manifest and perf:

import json

from mllm_shap.cli import EXAMPLE_CONFIG, main

Path("config.yaml").write_text(EXAMPLE_CONFIG, encoding="utf-8")
code = main([
    "explain",
    "--config", "config.yaml",
    "--snapshot", "snapshot.json",
    "--out", "result.json",
])
result = json.loads(Path("result.json").read_text(encoding="utf-8"))
print("exit code:", code)
print("keys:", list(result))
for label, value in zip(result["labels"], result["values"], strict=True):
    print(f"{label['text']:>5} {value:+.4f}")
print("sum == grand - base:", round(sum(result["values"]), 9)
      == round(result["grand_value"] - result["base_value"], 9))

Output

exit code: 0
keys: ['values', 'labels', 'base_value', 'grand_value', 'estimand', 'metrics', 'manifest', 'perf']
  Why +0.2000
   is +0.2000
  the +0.2000
  sky +0.2000
 blue +0.2000
sum == grand - base: True

--render token_text or --render token_bar together with --png also saves the chart of Visualization (needs the viz extra).

Reanalyze

reanalyze rescores an archived run without calling the model; edit value_fn or normalizer in the configuration first. The archive path comes from explanation.archive.path (./runs/{run_id} in the skeleton). A reanalyzed result has no snapshot, so its labels carry the player ids only:

run_dir = Path("runs") / result["manifest"]["run_id"]
rescored_config = EXAMPLE_CONFIG.replace(
    "normalizer: identity", "normalizer: abs_sum"
)
Path("abs_sum.yaml").write_text(rescored_config, encoding="utf-8")
main([
    "reanalyze",
    "--config", "abs_sum.yaml",
    "--archive", str(run_dir),
    "--out", "rescored.json",
])
rescored = json.loads(Path("rescored.json").read_text(encoding="utf-8"))
print("backend calls:", rescored["metrics"]["executed"])
print("abs_sum values:", [round(value, 4) for value in rescored["values"]])
print("label:", rescored["labels"][0])

Output

backend calls: 0
abs_sum values: [0.2, 0.2, 0.2, 0.2, 0.2]
label: {'player_id': '9319b488c2a98329:0:0:0', 'text': None, 'message_index': None, 'start': None, 'end': None, 'speaker': None, 'label': None}

Archives store generated text only, so embedding value functions (embedding_cosine, embedding_euclidean) score hash embeddings of that text on reanalysis. The manifest’s embedding_source says which embeddings the explain run scored: none when the value function read none, hash for the text-derived vectors and backend for model embeddings (the transformers backend; embedding_model names the model). Reanalyzing a backend run with an embedding value function logs a WARNING (shown with --log-level warning), because the values will differ from explain; add --strict to refuse it with exit code 2 instead. A missing, unreadable or older manifest or database also exits with 2.

Plugins

plugins lists every registered plugin, built-in and installed through entry points (see Extending mllm-shap):

main(["plugins"])

Output

mllm_shap.estimators: complementary, exact, loo, mc, neyman_cc
mllm_shap.estimands: banzhaf, shapley
mllm_shap.value_fns: embedding_cosine, embedding_euclidean, logprob, reference_wer, tfidf_cosine
mllm_shap.normalizers: abs_sum, identity, min_max, power_shift
mllm_shap.backends: api, lmstudio, mock, mock_audio, transformers
mllm_shap.partitions: messages, sgpa_segments, tokens
mllm_shap.absence_policies: delete, neutral, pad
mllm_shap.renderers: token_bar, token_text

Notebook

examples/cli.ipynb (also embedded on Examples) runs every subcommand in a temporary folder and shows each exit code with its message.