Skip to content

Extending muteval (plugin API)

muteval has three stable extension points. Each is a small, documented contract with a test in tests/test_plugin_contract.py — a third-party plugin that satisfies the contract keeps working across releases.

The guiding rule (muteval's core thesis): extensions stay orthogonal. A custom operator only produces mutated systems; a custom eval only grades output; a custom probe only rates the suite. None of them reach across, so the mutation-score abstraction stays clean.

1. Custom mutation operators

An operator degrades the system under test. It is fn(target) -> list[Mutant], where target is a System or a bare prompt string (call as_system(target) to normalize).

from muteval import as_system
from muteval.mutators import Mutant, register_operator

def shout(target):
    sys = as_system(target)
    return [Mutant(
        operator="shout",
        description="UPPERCASED the whole prompt",
        system=sys.with_prompt(sys.prompt.upper()),
    )]

register_operator("shout", shout)   # now runs by default and via --operators shout

Mutant(operator, description, system, target="prompt")description is what users see in the survivor report, so make it actionable. Set target to "context", "tools", or "model" if you mutate those (affects severity).

You don't have to register globally: pass callables straight through — operators=[shout] in MutEvalConfig or generate_mutants(system, operators=[shout]).

2. Custom evals (checks / framework adapters)

An eval is any fn(output, case) -> bool | EvalOutcome. Return an EvalOutcome to carry a score/threshold (enables near-miss reporting). Tag fn.is_llm = True if it makes a paid model call, so the runner orders it AFTER cheap checks and counts it against --max-calls.

from muteval import EvalOutcome

def mentions_price(output, case):
    return EvalOutcome(passed="$" in output, name="mentions_price")

To wrap a third-party metric, use the adapter helpers in muteval.adapters.base:

from muteval.adapters.base import scorer_to_eval, case_get

# any (output, case) -> float scorer, compared to a threshold
faithfulness = scorer_to_eval(my_metric.score, threshold=0.7, name="faithfulness")

case_get(case, key) reads a field from a dict-or-object case. See adapters/deepeval.py and adapters/ragas.py for full examples.

3. Custom probes (eval-quality lenses)

A probe rates the eval SUITE along one dimension. It is fn(config) -> ProbeResult, registered like an operator.

from muteval.probes.base import ProbeResult, register_probe

def has_negative_case(config):
    ok = any(getattr(c, "get", lambda k: None)("unanswerable") for c in config.cases)
    return ProbeResult(
        name="has_negative_case", ok=ok,
        summary="suite includes an unanswerable case" if ok else "no negative case",
        detail="Add a case with no supported answer to test the abstain path.",
    )

register_probe("has_negative_case", has_negative_case)

ProbeResult(name, ok, summary, detail=None, metrics={}). There is deliberately no composite score — probes are a report card of separate signals.

Stability

The signatures above (register_operator, Mutant, register_probe, ProbeResult, scorer_to_eval, case_get, the eval (output, case) shape) are covered by tests/test_plugin_contract.py. Changes to them are breaking changes and will be called out in the changelog.