Plugin contract¶
Calibrated explanations stay extensible without sacrificing the calibrated-first workflow. Every plugin must honour the contract below so binary and multiclass classification plus probabilistic and interval regression remain aligned with the core library.
Overview: Plugin types¶
This document covers three complementary plugin types used throughout the calibrated explanations pipeline:
Interval calibrator plugins - Produce prediction intervals for uncertainty quantification
Explanation plugins - Generate factual, alternative, or fast explanations from calibrated models
Plot plugins - Render explanation visualizations in different styles and formats
Plugin override precedence¶
All plugin types follow a consistent override hierarchy for selection and configuration:
Explainer parameters (highest priority): Direct kwargs to
CalibratedExplainer(e.g.,factual_plugin,interval_plugin,plot_style).Environment variables: Mode-specific variables (e.g.,
CE_EXPLANATION_PLUGIN_FACTUAL,CE_INTERVAL_PLUGIN,CE_PLOT_STYLE).pyproject.toml: Project-level defaults under
[tool.calibrated_explanations.plugins]or mode sections.Plugin-declared dependencies (lowest): Automatic seeding from plugin metadata (e.g.,
interval_dependency,plot_dependency).
For detailed wiring examples, see Advanced plugin wiring and configuration.
Modality contract¶
ADR-033 defines the modality metadata required for entry-point plugins and the resolver behavior used by modality-aware selection helpers.
Canonical modalities and aliases¶
data_modalities values are normalised to canonical names:
tabularvision(aliases:image,images,img)audiotextmultimodal(aliases:multi-modal,multi_modal)
Custom extension names must use an x-<vendor>-<name> namespace.
Metadata requirements¶
Entry-point explanation plugins should publish these metadata fields:
data_modalities: tuple of canonical modality namesplugin_api_version:MAJOR.MINORorMAJOR.MINOR.PATCH
Compatibility policy is major-hard/minor-soft:
major mismatch is rejected at discovery time
higher minor/patch can load with
UserWarningplus governance logging
v0.11.1 deprecation timeline¶
v0.11.0: missingdata_modalitiesdefaults to("tabular",)silentlyv0.11.1: missingdata_modalitieson entry-point metadata emitsDeprecationWarningv0.11.3: explicitdata_modalitiesis required; fallback default is removed
See Modality plugin selection for user-facing selection examples and migration guidance.
Hello, calibrated plugin (minimal explanation plugin)¶
Use this minimal example when you want to wrap a model that already exposes
predict_proba and return a calibrated payload directly.
from __future__ import annotations
import numpy as np
def build_dummy_model():
class DummyModel:
def predict_proba(self, x):
return np.column_stack([1 - x, x])
return DummyModel()
class HelloCalibratedPlugin:
"""Minimal example that returns calibrated outputs for a wrapped model."""
plugin_meta = {
"schema_version": 1,
"name": "hello.calibrated.plugin",
"version": "0.1.0",
"provider": "example-team",
"capabilities": ("binary-classification", "probabilistic-regression"),
"dependencies": (),
"modes": ("factual", "alternative"),
"tasks": ("classification", "regression"),
"trusted": False,
}
def supports(self, model):
"""Return whether the plugin can work with *model*."""
return hasattr(model, "predict_proba")
def explain(self, model, x, **kwargs):
"""Produce a calibrated explanation payload for ``x``."""
probabilities = model.predict_proba(x)
return {
"prediction": probabilities[:, 1],
"uncertainty_interval": (probabilities[:, 0], probabilities[:, 1]),
"modes": self.plugin_meta["modes"],
}
Hello, interval calibrator plugin¶
Follow these steps to build an interval calibrator plugin that respects the contract defined by ADR-026.
1. Scaffold the interval calibrator class¶
Interval calibrators implement the :class:calibrated_explanations.plugins.intervals.IntervalCalibratorPlugin
protocol. The example below shows a minimal implementation that computes simple bounds; replace the
body with your calibration logic while preserving calibrated prediction intervals.
from __future__ import annotations
from typing import Any
from calibrated_explanations.plugins.intervals import IntervalCalibratorPlugin
class HelloIntervalCalibratorPlugin(IntervalCalibratorPlugin):
"""Minimal interval calibrator example that computes fixed-width bounds."""
plugin_meta = {
"schema_version": 1,
"name": "hello.interval.calibrator",
"version": "0.1.0",
"provider": "example-team",
"capabilities": ["interval:classification", "interval:regression"],
"modes": ("classification", "regression"),
"dependencies": (),
"trusted": False,
"trust": False,
"confidence_source": "hello",
"requires_bins": False,
"fast_compatible": False,
}
def create(self, context, **kwargs: Any) -> Any:
"""Return a list of calibrators for feature-wise and model-level intervals."""
task = str(context.metadata.get("task") or context.metadata.get("mode") or "")
if "classification" in task:
return []
else:
return []
Note: IntervalCalibratorContext is frozen by design. context.metadata is exposed as an immutable mapping so plugins cannot mutate shared explainer state, while context.plugin_state provides a mutable dictionary for storing transient, per-execution scratch data (e.g., caching intermediate summaries or heuristics) without leaking back into the explainer.
2. Validate and register the interval calibrator¶
from calibrated_explanations.plugins.base import validate_plugin_meta
from calibrated_explanations.plugins import register_interval_plugin
plugin = HelloIntervalCalibratorPlugin()
validate_plugin_meta(dict(plugin.plugin_meta))
register_interval_plugin("external.hello.interval", plugin)
Override precedence for interval calibrator plugins¶
Interval plugins are selected globally or per-mode:
Explainer parameters:
interval_plugin,fast_interval_pluginEnvironment variables:
CE_INTERVAL_PLUGIN,CE_INTERVAL_PLUGIN_FASTpyproject.toml: Under
[tool.calibrated_explanations.intervals]with keysdefault,fastDependencies: Seeded from explanation plugin metadata
interval_dependency
Hello, explanation plugin¶
Follow these steps to build an explanation plugin that respects the contract defined by ADR-026.
1. Scaffold the explanation plugin class¶
Explanation plugins implement the :class:calibrated_explanations.plugins.explanations.ExplanationPlugin
protocol. The example below emits a fixed explanation payload;
replace the body with your logic while preserving calibrated outputs.
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from calibrated_explanations.plugins.explanations import ExplanationPlugin
from calibrated_explanations.explanations import CalibratedExplanations
@dataclass
class HelloExplanationPlugin(ExplanationPlugin):
"""Minimal example that returns calibrated explanations for binary classification."""
plugin_meta = {
"schema_version": 1,
"name": "hello.explanation.plugin",
"version": "0.1.0",
"provider": "example-team",
"capabilities": ["explain", "explanation:factual", "task:classification"],
"modes": ("factual",),
"tasks": ("classification",),
"dependencies": (),
"interval_dependency": "core.interval.legacy",
"plot_dependency": "plot_spec.default",
"trusted": False,
}
def supports(self, model: Any) -> bool:
return hasattr(model, "predict_proba")
def initialize(self, context):
self.context = context
def explain_batch(self, x, request):
explanations = CalibratedExplanations(None, 0, x, {}, {}, {}, {})
return type(
"ExplanationBatch",
(),
{
"explanations": explanations,
"collection_metadata": {
"mode": self.plugin_meta.get("modes", ("factual",))[0],
"task": self.context.task,
},
},
)()
2. Validate and register the explanation plugin¶
from calibrated_explanations.plugins.base import validate_plugin_meta
from calibrated_explanations.plugins import register_explanation_plugin
plugin = HelloExplanationPlugin()
validate_plugin_meta(dict(plugin.plugin_meta))
register_explanation_plugin("external.hello.explanation", plugin)
Pitfall: two prediction paths — do not mix them¶
Inside explain_batch there are two separate prediction paths with different
responsibilities. Confusing them produces a TypeError that surfaces as an opaque
ENGINE_FAILURE to the caller.
Path |
How to call |
What it does |
|---|---|---|
|
Via |
Lifecycle contract check; uses default percentiles (5, 95) internally; return value is often discarded |
|
Via |
Full inference; honours any custom |
The PredictBridge protocol does not accept low_high_percentiles. If a
caller forwards request.low_high_percentiles into bridge.predict() it gets a
TypeError on every call where that field is non-None. The fix is to send
interval-shaping parameters to the explainer handle instead:
def explain_batch(self, x, request):
# Step 1 — honour the bridge lifecycle contract (protocol-defined params only)
self._bridge.predict(x, mode=self._mode, task=self.context.task, bins=request.bins)
# Step 2 — full inference with custom percentiles goes to the explainer handle
explainer = self.context.helper_handles["explainer"]
prediction, (low, high) = explainer.predict(
x,
uq_interval=True,
low_high_percentiles=request.low_high_percentiles, # safe here
bins=request.bins,
)
...
This distinction is not obvious because the bridge’s predict() return value
contains low and high arrays, which makes it look like the interval-shaping
surface. It is not — it is a narrowly-scoped lifecycle shim.
Override precedence for explanation plugins¶
Explanation plugins support mode-specific selection:
Explainer parameters:
factual_plugin,alternative_plugin,fast_pluginEnvironment variables:
CE_EXPLANATION_PLUGIN_FACTUAL,CE_EXPLANATION_PLUGIN_ALTERNATIVE,CE_EXPLANATION_PLUGIN_FASTpyproject.toml: Under
[tool.calibrated_explanations.explanations]with keysfactual,alternative,fastDependencies: Plugin metadata
interval_dependencyandplot_dependencyseed related plugins automatically
Hello, plot plugin¶
Follow these steps to build a plot plugin (builder and renderer pair) that respects the contract defined by ADR-037 and ADR-036.
1. Scaffold the plot builder and renderer¶
Plot builders and renderers implement the :class:calibrated_explanations.plugins.plots.PlotBuilder
and :class:calibrated_explanations.plugins.plots.PlotRenderer protocols.
from __future__ import annotations
from calibrated_explanations.plugins.plots import (
PlotBuilder,
PlotRenderer,
PlotRenderResult,
)
class HelloPlotBuilder(PlotBuilder):
"""Minimal plot builder that creates a simple PlotSpec payload."""
plugin_meta = {
"schema_version": 1,
"name": "hello.plot.builder",
"version": "0.1.0",
"provider": "example-team",
"style": "hello",
"output_formats": ("png", "svg"),
"capabilities": ["plot:builder"],
"dependencies": (),
"trusted": False,
"legacy_compatible": False,
}
def build(self, context):
"""Return a PlotSpec-compatible payload for the explanation."""
# Build a visualization specification from the explanation context
# This example returns a minimal structure; implement your visualization logic
return {
"plot_spec": {
"title": "Hello Plot",
"description": "A minimal plot builder example",
"primitives": [],
}
}
class HelloPlotRenderer(PlotRenderer):
"""Minimal plot renderer that materializes PlotSpec payloads."""
plugin_meta = {
"schema_version": 1,
"name": "hello.plot.renderer",
"version": "0.1.0",
"provider": "example-team",
"output_formats": ("png", "svg"),
"capabilities": ["plot:renderer"],
"supports_interactive": False,
"dependencies": (),
"trusted": False,
"legacy_compatible": False,
}
def render(self, artifact, *, context):
"""Render the PlotSpec artifact and return visualization result."""
# Materialize the plot using matplotlib, plotly, or other rendering backend
# This example returns a minimal result; implement your rendering logic
return PlotRenderResult(
artifact=artifact,
figure=None,
saved_paths=(),
extras={},
)
2. Register the plot builder, renderer, and style¶
from calibrated_explanations.plugins.base import validate_plugin_meta
from calibrated_explanations.plugins.plots import (
PlotBuilder,
PlotRenderer,
PlotRenderResult,
)
from calibrated_explanations.plugins import (
register_plot_builder,
register_plot_renderer,
register_plot_style,
)
class HelloPlotBuilder(PlotBuilder):
plugin_meta = {
"schema_version": 1,
"name": "hello.plot.builder.test",
"version": "0.1.0",
"provider": "example-team",
"style": "hello.test",
"output_formats": ("png", "svg"),
"capabilities": ["plot:builder"],
"dependencies": (),
"trusted": False,
"legacy_compatible": False,
}
def build(self, context):
return {"plot_spec": {"title": "Test", "primitives": []}}
class HelloPlotRenderer(PlotRenderer):
plugin_meta = {
"schema_version": 1,
"name": "hello.plot.renderer.test",
"version": "0.1.0",
"provider": "example-team",
"output_formats": ("png", "svg"),
"capabilities": ["plot:renderer"],
"supports_interactive": False,
"dependencies": (),
"trusted": False,
"legacy_compatible": False,
}
def render(self, artifact, *, context):
return PlotRenderResult(
artifact=artifact,
figure=None,
saved_paths=(),
extras={},
)
builder = HelloPlotBuilder()
renderer = HelloPlotRenderer()
validate_plugin_meta(dict(builder.plugin_meta))
validate_plugin_meta(dict(renderer.plugin_meta))
builder_id = "hello.plot.builder.test"
renderer_id = "hello.plot.renderer.test"
style_id = "hello.test"
register_plot_builder(builder_id, builder)
register_plot_renderer(renderer_id, renderer)
register_plot_style(
style_id,
metadata={
"style": style_id,
"builder_id": builder_id,
"renderer_id": renderer_id,
"fallbacks": ("plot_spec.default", "legacy"),
"is_default": False,
},
)
Override precedence for plot plugins¶
Plot styles control visualization rendering:
Explainer parameters:
plot_styleExplanation.plot() parameters:
style_overridefor dynamic selectionEnvironment variables:
CE_PLOT_STYLE,CE_PLOT_STYLE_FALLBACKSpyproject.toml: Under
[tool.calibrated_explanations.plots]with keystyleDependencies: Seeded from explanation plugin metadata
plot_dependency
Wiring plugins into your explainer and explanations¶
Once a plugin is registered, you can wire it into your workflow using two primary methods:
Method A: Via CalibratedExplainer parameter¶
Pass the desired plugin identifier (or style for plot plugins) to the explainer at construction time:
from tests.helpers.model_utils import get_classification_model
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from calibrated_explanations import CalibratedExplainer
# Prepare data
data = load_breast_cancer()
x = data.data
y = data.target
x_temp, x_test, y_temp, _ = train_test_split(x, y, test_size=0.2, random_state=42)
x_train, x_cal, y_train, y_cal = train_test_split(
x_temp, y_temp, test_size=0.4, random_state=42
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_cal = scaler.transform(x_cal)
x_test = scaler.transform(x_test)
model, _ = get_classification_model("RF", x_train, y_train)
# Parameter wiring
explainer = CalibratedExplainer(
model,
x_cal,
y_cal,
plot_style="plot_spec.default",
)
chain = explainer.plugin_manager.plot_style_chain
assert chain[0] == "plot_spec.default"
This approach ensures consistent plugin selection across all explanations generated by the explainer. The explainer stores the plugin identifiers and uses them during explanation generation.
Method B: Via explanation.plot() parameter¶
Pass the plot style override when calling .plot() on explanation batches:
from tests.helpers.model_utils import get_classification_model
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from calibrated_explanations import CalibratedExplainer
# Prepare data
data = load_breast_cancer()
x = data.data
y = data.target
x_temp, x_test, y_temp, _ = train_test_split(x, y, test_size=0.2, random_state=42)
x_train, x_cal, y_train, y_cal = train_test_split(
x_temp, y_temp, test_size=0.4, random_state=42
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_cal = scaler.transform(x_cal)
x_test = scaler.transform(x_test)
model, _ = get_classification_model("RF", x_train, y_train)
explainer = CalibratedExplainer(
model,
x_cal,
y_cal,
)
explanations = explainer.explain_factual(x_test[:3])
assert hasattr(explanations, "plot")
assert "style_override" in explanations.plot.__code__.co_varnames
This method allows dynamic plot style selection after explanations are generated, enabling comparison of different visualization styles for the same explanations.
Note: The style parameter in .plot() controls plot type (regular, triangular),
not the plugin style. Use style_override to select plot plugins.
Advanced wiring methods¶
For environment variable configuration, pyproject.toml settings, and plugin dependency propagation, see Advanced plugin wiring and configuration.
Packaging and entry points¶
To distribute your plugins with pip, expose entry points under calibrated_explanations.plugins:
[project.entry-points."calibrated_explanations.plugins"]
external.hello.explanation = "your_package.plugins:HelloExplanationPlugin"
external.hello.interval = "your_package.plugins:HelloIntervalCalibratorPlugin"
[project.entry-points."calibrated_explanations.plugins.plot_builders"]
hello.plot.builder = "your_package.plugins:HelloPlotBuilder"
[project.entry-points."calibrated_explanations.plugins.plot_renderers"]
hello.plot.renderer = "your_package.plugins:HelloPlotRenderer"
When your package is installed, these entry points are discovered automatically. Users can then register your plugins by importing your module:
import your_package.plugins # Triggers registration via entry points
# or register manually:
from your_package.plugins import HelloExplanationPlugin
from calibrated_explanations.plugins import register_explanation_plugin
register_explanation_plugin("external.hello.explanation", HelloExplanationPlugin())
Document how probabilistic and interval regression stay calibrated after your extension by linking back to the practitioner quickstarts and interpretation guides.
Guardrails and ADR references¶
Use these decision records when designing new plugins:
ADR-006 - plugin registry trust model defines explicit trust controls (
CE_TRUST_PLUGIN,CE_DENY_PLUGIN) and governance expectations for third-party plugins.ADR-013 - interval calibrator plugin strategy defines the architecture for interval calibrator plugins and their integration with core calibrators.
ADR-037 - visualization extension and rendering governance defines builder/renderer governance, deterministic metadata requirements, and default behavior.
ADR-015 - explanation plugin architecture specifies the plugin orchestration, resolution, and mode-aware selection for explanation plugins.
ADR-036 - PlotSpec canonical contract and validation boundary documents canonical PlotSpec semantics, validation boundaries, and compatibility rules.
ADR-026 – explanation plugin semantics captures the calibrated explanation contract for explanation and interval plugins.
Trust and governance checklist¶
Before shipping a plugin, verify all items:
Document trust onboarding (how operators should set
CE_TRUST_PLUGINor pyproject allowlists).Document emergency deny flow (
CE_DENY_PLUGIN) for incident mitigation.Verify plugin appears in discovery diagnostics (
python -m calibrated_explanations.plugins.cli report).Verify trusted-only views behave as expected (
python -m calibrated_explanations.plugins.cli list all --trusted-only).
Runtime performance toggles¶
v0.9.0 introduces an opt-in calibrator cache and parallel executor. Plugin authors should treat these as shared infrastructure:
Avoid mutating request-level objects captured by the cache key. Values stored in the cache must be deterministic, pickle-safe, and process independent.
When you spawn subprocesses (for example inside a plugin), call
explainer._perf_cache.forksafe_reset()or reuse the provided :class:~calibrated_explanations.parallel.ParallelExecutorto inherit the built-in fork guards. Thecalibrated_explanations.perf.parallelandcalibrated_explanations.perf.cacheshims remain temporarily for compatibility and will be removed after v1.1.0.Respect
CE_CACHE/CE_PARALLELoverrides documented in Tune runtime performance (opt-in). Plugins should not force their own worker settings—delegate to the executor attached to the explainer instead.Emit telemetry sparingly and honour the
perf_telemetrycallback when provided so operators can aggregate cache hit/miss data consistently.
Fast explanations and external bundles¶
The fast explanations implementation now ships as an external plugin. Install the aggregated extra and register the bundle explicitly when you need it:
pip install "calibrated-explanations[external-plugins]"
python -m external_plugins.fast_explanations register
See External plugins for community listings and governance notes.
Denylist and trust controls¶
The registry honours both CE_TRUST_PLUGIN and CE_DENY_PLUGIN environment
variables.
CE_TRUST_PLUGINlists identifiers that should be trusted on discovery.CE_DENY_PLUGINblocks specific identifiers while you iterate on them.
Use the CLI to inspect the resulting state after registration:
python -m calibrated_explanations.plugins.cli list all
The CLI always prints an opt-in reminder so core calibrated explanations remain usable without it.
CLI discovery (optional)¶
The :mod:calibrated_explanations.plugins.cli module surfaces plugin metadata
for auditing. It is optional tooling—set CE_DENY_PLUGIN during local testing
to simulate trust boundaries before promoting a plugin.
Telemetry instrumentation (optional)¶
If your plugin emits telemetry, document the signals and link back to Optional telemetry scaffolding. Keep emission disabled by default so calibrated explanations remain privacy-preserving out of the box.