ek#
ek – a framework for building Knowledge Evaluation systems.
ek evaluates the outputs of information-extraction systems. OCR is treated as
the noisiest special case of a general problem, so the core is source-agnostic
(PDF, DOCX, tables, DB responses, LLM extractors) and the OCR specifics live in
ek.ocr.
Two facades cover the two halves of evaluation, both operating on the same typed schema:
score()/evaluate()– reference-based (offline): compare against a gold answer, one item or a whole corpus, the metric chosen by output type.estimate_quality()– reference-free (online): gather signals, calibrate, validate, and decide accept/flag/block with no gold answer.
Everything swappable is a typing.Protocol strategy resolved from
ek.registry and injected as a keyword-only argument with a smart default –
so the simple path Just Works and every layer stays replaceable (open-closed).
Quickstart:
>>> import ek
>>> round(ek.score("hello wrld", "hello world").value, 3) # CER by default
0.091
>>> ek.evaluate([("ct", "cat"), ("dg", "dog")], metric="cer").n
2
The two-layer data model (GraphGrammar for the schema + cost weights,
AnnotatedExtraction for per-extraction verification metadata) is the SSOT
every component plugs into – see ek.base. Persistence is local-file dol
stores under ~/.local/share/ek/ – see ek.stores.
Functions
|
Score one prediction against one gold reference (reference-based). |
|
Aggregate many comparisons into a |
|
Estimate the quality of an extraction with no gold reference. |
|
Run |
|
Freeze a report's aggregate + per-slice scores as a named baseline. |
|
Load a named baseline (or |
|
Fail if |
|
Cohen's kappa for two raters on nominal labels (chance-corrected agreement). |
|
Raw fraction of items two raters labelled identically. |
|
Krippendorff's alpha -- the general inter-annotator agreement coefficient. |
|
The default |
A conservative default: NFC -> casefold -> collapse whitespace. |
|
|
Extract a normalized |
|
Whether |
|
Resolve a table parser reference (a registered name or a callable) to a callable. |
|
Whether |
|
Align N hypotheses, vote per slot, and emit consensus + per-position agreement. |
|
Expected Calibration Error: weighted mean gap between confidence and accuracy. |
|
Per-bin |
|
Persist a fitted calibrator's parameters to the |
|
Reconstruct a persisted calibrator by name (dispatched on its |
|
Fuse calibrated per-family probabilities by their mean (the default combiner). |
|
Fuse calibrated per-family probabilities by their minimum (conservative). |
|
The risk-coverage trade-off: |
|
The split-conformal threshold: the |
|
A |
|
A |
|
A |
|
A |
|
A spec-driven |
|
Flag when |
|
Luhn (mod-10) checksum: credit cards, IMEIs, some national IDs. |
|
IBAN validity via the ISO 13616 / ISO 7064 mod-97 rule (must equal 1). |
|
Validate an ISBN-10 or ISBN-13 by length (digits/hyphens/spaces ignored). |
|
Compose validators/correctors into a cheapest -> most-expensive pipeline. |
|
L0: fold a value to canonical form, emitting a CORRECT finding when it changes. |
|
L2: resolve a value against a closed vocabulary by fuzzy match ( |
|
FLAG a record when |
|
FLAG a record whose comparable values at |
|
FLAG a numeric field whose first-digit distribution deviates from Benford's law. |
|
FLAG numeric values that are robust-z-score outliers (median + MAD based). |
|
L3: FLAG a value whose language-model surprisal crosses |
|
L5: a gated neural/LLM corrector -- the only layer that invents content. |
|
Stop policy: end the pass as soon as any layer proposes a correction. |
|
Stop policy: end the pass at the first FLAG (fail fast). |
|
Register |
|
Resolve a registered strategy by name (discovering entry points first). |
|
Coerce a strategy reference to a callable. |
|
All registered names in a namespace (after entry-point discovery). |
|
Decorator: fail with an actionable install hint if an extra is missing. |
|
Report what an OCR engine or feature extra needs, without raising. |
|
A JSON |
|
The full stores-of-stores mall (one JSON store per |
The root |
|
|
Memoize a plain function's result in a |
|
Unified caching decorator for properties and methods with persistent storage support. |
|
Run |
|
Unbiased |
|
Unbiased |
Classes
|
Outcome of a |
|
The frozen schema SSOT: typed nodes/edges plus their importance weights. |
|
A leaf attribute of a node: its type, validation domain and cost weight. |
|
A node kind in the typed graph (e.g. an "invoice" or a "line item"). |
|
A directed relation kind between two |
|
A reference into the schema, so an injected cost function can read weights. |
|
Layer B: the verification metadata, referencing a frozen Layer-A grammar. |
|
One extracted value plus all of its reference-free verification metadata. |
|
Addresses a node (or one of its fields) in an extracted graph; keys Layer B. |
|
Where an extracted value came from, for audit and click-to-source review. |
|
One validator observation about a value: a flag, optionally a correction. |
|
Terminal action a selective-prediction gate emits for an extracted value. |
|
Whether a |
|
The result of comparing one prediction against one reference. |
|
Aggregate of many |
|
The result of a reference-free quality estimate for one extraction. |
|
Compares one prediction to one gold reference, returning a |
|
Reference-free check on a value, yielding zero or more |
|
Maps a raw scalar signal to a probability after being |
|
Turns a (calibrated) confidence into an accept/flag/block |
|
A reference-free quality signal: extractor output -> raw scalar score. |
|
An ordered, named, versioned pipeline of |
|
CER ( |
|
Per-field precision/recall/F1 for two dict records. |
|
Cost-weighted, type-aware typed-graph edit distance as a |
|
A typed graph: typed nodes plus directed typed edges between them. |
|
A node in a typed graph: an id, a node-type tag, and its field values. |
|
A directed, typed relation between two node ids. |
|
ANLS / ANLS* (nested-JSON) similarity as a |
|
Entity/slot precision/recall/F1 under an explicit |
|
The (backend, scheme) contract a span/slot F1 is computed under. |
|
TEDS / TEDS-Struct as a |
|
GriTS-Top / GriTS-Con as a |
|
A table as an ordered list of rows of |
|
One logical table cell: its text plus its row/column span (>=1 each). |
|
The result of a ROVER pass over N hypotheses. |
|
ROVER multi-engine agreement as a reference-free |
|
Deterministic verifier evidence as a reference-free |
|
Aggregate token log-probabilities into one field score (a |
|
Aggregate an extractor's per-unit confidences into a field score. |
|
Platt scaling: |
|
Isotonic (monotonic non-decreasing) calibration via pool-adjacent-violators. |
|
Temperature scaling: |
|
Per-group (Mondrian) calibration: one calibrator per |
|
One calibrator per signal family, fused by a documented combination rule. |
|
Accept/flag/block from the cost ratio |
|
Split-conformal accept/flag with a finite-sample marginal guarantee. |
|
Mondrian (class-conditional) conformal: one |
|
Conformal Risk Control: accept |
|
The outcome of running a value through a |
|
A |
|
Layer B: one agent run over one task -- the object agent evaluation scores. |
|
One task in a suite: its input, its oracle, its value, and its allowed tools. |
|
One tool the agent may call: its argument schema and its error costs. |
- class ek.AgreementSignal(cost_tier=3, use_confidence=True, conf_weight=0.5, null_conf=0.7, tokenize=None, max_tokens=5000)[source]#
Bases:
objectROVER multi-engine agreement as a reference-free
Signal.Cost tier 3 (
Nengine runs): try the deterministic verifier layer and any free intrinsic confidence first. Called on a collection of hypotheses, it runsrover()and returns the mean per-position agreement as the raw signal – uncalibrated, like every signal, so it must pass through aCalibratorbefore any gate reads it.Example
>>> sig = AgreementSignal() >>> round(sig(["the cat sat", "the cat sit", "the bat sat"]), 3) 0.778
- class ek.AnlsMetric(*, threshold=0.5)[source]#
Bases:
objectANLS / ANLS* (nested-JSON) similarity as a
Metric.Handles a bare string (classic ANLS) or an arbitrarily nested dict/list/tuple/
Nonestructure (ANLS*); the backend dispatches on type.- Parameters:
threshold (
float) – ANLS zeroes a per-leaf similarity below this value before averaging (the classic ANLS 0.5 cut tolerates minor OCR/spelling noise).anls_starexposes no parameter for this – it reads a class constantANLSTree.THRESHOLD(default0.5) – so the metric applies the threshold by setting that constant around the call and restoring it. Caveat: this mutates a process-global, so it is not thread-safe; concurrentAnlsMetriccalls with different thresholds in the same process can interfere. Defaults to0.5.
- aggregate(scores)[source]#
Corpus ANLS = mean of per-item ANLS (each is already length-normalized).
Unlike CER/WER (which accumulate global edit counts) or field-F1 (micro-TP/ FP/FN), ANLS is defined as the average normalized similarity over the evaluation set – the “A” in ANLS – so the corpus statistic is the plain mean of the per-item scores.
- Return type:
- class ek.AnnotatedExtraction(grammar, estimates=<factory>)[source]#
Bases:
objectLayer B: the verification metadata, referencing a frozen Layer-A grammar.
The grammar is held by reference and never mutated;
estimatesmaps aNodePathto theFieldEstimatefor that node/field.
- class ek.Calibrator(*args, **kwargs)[source]#
Bases:
ProtocolMaps a raw scalar signal to a probability after being
fiton labels.
- class ek.Canonicalizer(steps=(), *, version='', audit_marks=False)[source]#
Bases:
objectAn ordered, named, versioned pipeline of
str -> strsteps.- Parameters:
steps (
Sequence[Union[str,Callable[[str],str]]]) – Step references – names registered under"normalizers"or plain callables. Applied left to right.version (
str) – Override the auto-derived version string (the+-joined names).audit_marks (
bool) – If true, warn when a step would run on text carrying combining marks (the cross-script damage guard).
- class ek.Cell(text='', rowspan=1, colspan=1)[source]#
Bases:
objectOne logical table cell: its text plus its row/column span (>=1 each).
- class ek.ConformalGate(alpha=0.1, nonconformity=<function ConformalGate.<lambda>>, q=inf, _cal=<factory>)[source]#
Bases:
objectSplit-conformal accept/flag with a finite-sample marginal guarantee.
Fit on
(calibrated_prob, correct): the nonconformity scores of the correct calibration items set a thresholdqsuch that, among truly-correct test items exchangeable with them, at mostalphaare flagged. The guarantee is marginal, not per-field-type – useGroupConformalGatefor that.- Parameters:
- class ek.Corrector(*args, **kwargs)[source]#
Bases:
ProtocolA
Validatorthat may also CORRECT.Same callable shape as a Validator, but it can emit a
severity=CORRECTFindingwhosesuggestionis the proposed replacement value. Thelayerattribute names its place on the cheap->expensive spine.
- class ek.CostSensitiveGate(rho=1.0, block_threshold=0.0, accept_threshold=None)[source]#
Bases:
objectAccept/flag/block from the cost ratio
rho = c_FN / c_FPon calibratedp.Accept when the expected cost of accepting is below that of routing to a human. Accepting risks an undetected error: cost
(1 - p) * c_FN. Flagging a value that was actually correct wastes a review: costp * c_FP(the false-positive cost is incurred only with probabilityp, not unconditionally). So accept iff(1 - p) * c_FN <= p * c_FP, i.e. the Bayes-optimal thresholdp >= c_FN / (c_FN + c_FP) = rho / (1 + rho). A largerrho(misses much costlier than reviews) raises the bar to auto-accept (tau -> 1asrho -> inf);rho = 1gives the symmetrictau = 0.5.block_thresholdadds an optional hard-fail band at the bottom.- Parameters:
rho (
float) – Cost ratioc_FN / c_FP(>= 0). The single lever; no magic numbers.block_threshold (
float) – Calibrated probability at or below which toblockrather thanflag(default0-> never block on probability alone; a failed verifier is what forces a block, upstream).accept_threshold (
Optional[float]) – Override the derived accept threshold explicitly.
- class ek.Decision(*values)[source]#
-
Terminal action a selective-prediction gate emits for an extracted value.
- class ek.DecisionPolicy(*args, **kwargs)[source]#
Bases:
ProtocolTurns a (calibrated) confidence into an accept/flag/block
Decision.
- class ek.EdgeType(name, src, dst, importance=1.0)[source]#
Bases:
objectA directed relation kind between two
NodeTypenames.
- class ek.Episode(task_id='', trajectory=<factory>, output=None, final_state=None, cost=None, success=None, run=None, provenance=None, meta=<factory>)[source]#
Bases:
objectLayer B: one agent run over one task – the object agent evaluation scores.
The direct analog of
AnnotatedExtraction: it rides alongside the frozen Layer-A grammar and carries the run’s metadata. It is scored offline against gold (ek.score()/ek.evaluate()) and estimated online against a judge or a consensus (ek.estimate_quality()) – the same object, both halves.- Parameters:
task_id (
str) – Which task this episode answers (the k-trial grouping key).trajectory (
Trajectory) – The tool calls taken.output (
Any) – The agent’s final user-facing answer.final_state (
Any) – The end state of the world (the DB, the filesystem) – what a state-based oracle actually grades. Success is a final-state check, not a surface-text check.success (
Optional[bool]) – Filled in by a checker;Noneuntil graded.run (
Optional[RunProvenance]) – TheRunProvenance(seed, model, simulator, suite version).provenance (
Optional[Provenance]) – Optional coreProvenancefor click-to-source audit.meta (
dict) – Anything else the bridge captured.
- class ek.FieldEstimate(value, raw_signals=<factory>, confidence=None, findings=(), provenance=None, decision=None)[source]#
Bases:
objectOne extracted value plus all of its reference-free verification metadata.
- class ek.FieldMetric(*, canonicalizer=None)[source]#
Bases:
objectPer-field precision/recall/F1 for two dict records.
- Parameters:
canonicalizer – Optional
str -> strapplied to string values before comparison (e.g. casefold/whitespace folding).
- class ek.FieldSpec(name, type='string', importance=1.0, domain=(), normalizer=None)[source]#
Bases:
objectA leaf attribute of a node: its type, validation domain and cost weight.
- Parameters:
name (
str) – Attribute name.type (
str) – A free-form type tag (e.g."string","number","date");ekdoes not impose a type system – the tag drives metric/validator dispatch and is yours to define.importance (
float) – Per-field cost weight; the lever that makes “two extra digits on a donation amount” outweigh “a misspelled city”. Defaults to1.0.domain (
tuple) – Optional allowed values: an enum tuple, a(lo, hi)range, or a regex string – consumed by validators, never by metrics.normalizer (
Optional[str]) – Optional key naming a canonicalizer (resolved from the registry) to apply to this field before comparison.
- class ek.Finding(field, layer, severity=Severity.FLAG, message='', suggestion=None)[source]#
Bases:
objectOne validator observation about a value: a flag, optionally a correction.
A
suggestionis present iff the producing layer can correct (only the deterministic canonicalizer and the gated LLM corrector freely do so). The original value is always retained elsewhere; a Finding never mutates.
- class ek.GateResult(passed, metric, higher_is_better, tolerance, aggregate_current=None, aggregate_baseline=None, regressions=<factory>)[source]#
Bases:
objectOutcome of a
regression_gate()check.
- class ek.GraphGrammar(node_types=<factory>, edge_types=<factory>)[source]#
Bases:
objectThe frozen schema SSOT: typed nodes/edges plus their importance weights.
Methods return the cost weight for a referenced type, defaulting to
1.0for anything not declared, so a partial grammar is always usable.
- class ek.GritsMetric(*, structure_only=False)[source]#
Bases:
objectGriTS-Top / GriTS-Con as a
Metric(pure-Python, P/R aware).- Parameters:
structure_only (
bool) – WhenTrue, score cell topology only (GriTS-Top); whenFalse(default), average topology with cell-content similarity (GriTS-Con).
- aggregate(scores)[source]#
Corpus GriTS F1: pool overlap and per-table cell counts, then divide once.
GriTS precision = overlap / |pred cells| and recall = overlap / |gold cells|; the corpus statistic micro-averages by summing the numerators and denominators globally – never a mean of per-table F1s.
- Return type:
- class ek.GroupCalibrator(factory=<class 'ek.qe.calibrate.PlattCalibrator'>, by_group=<factory>, pooled=None, kind='group')[source]#
Bases:
objectPer-group (Mondrian) calibration: one calibrator per
NodeType/FieldSpec.Distribution-free conditional (per-field-type) coverage is impossible in general (Barber et al. 2019); calibrating separately per group restores it approximately.
fittakes a parallelgroupssequence;__call__routes by group key, falling back to a pooled calibrator for unseen groups.- Parameters:
factory (
Callable[[],Any]) – Zero-arg callable producing a fresh per-group calibrator (defaultPlattCalibrator).
- factory#
alias of
PlattCalibrator
- class ek.GroupConformalGate(alpha=0.1, nonconformity=<function GroupConformalGate.<lambda>>, by_group=<factory>, pooled=None)[source]#
Bases:
objectMondrian (class-conditional) conformal: one
ConformalGateper group.Restores approximate per-field-type validity by calibrating separately per group key (e.g.
NodeType/FieldSpec), with a pooled fallback for unseen groups.
- class ek.IntrinsicConfidenceSignal(pool='min', cost_tier=2)[source]#
Bases:
objectAggregate an extractor’s per-unit confidences into a field score.
- Parameters:
Example
>>> IntrinsicConfidenceSignal(pool="min")([0.99, 0.4, 0.95]) 0.4
- class ek.IsotonicCalibrator(x=<factory>, y=<factory>, kind='isotonic')[source]#
Bases:
objectIsotonic (monotonic non-decreasing) calibration via pool-adjacent-violators.
More flexible than Platt; needs more calibration data and can overfit small sets. Predicts by linear interpolation between fitted points, clipped at the ends.
- class ek.LogprobSignal(aggregator=<function geo_mean>, alpha=0.6, cost_tier=2)[source]#
Bases:
objectAggregate token log-probabilities into one field score (a
Signal).- Parameters:
- class ek.MatchScheme(*values)[source]#
-
The (backend, scheme) contract a span/slot F1 is computed under.
The two backends legitimately disagree (seqeval ignores other-type tags at the tag level; nervaluate includes them), so the scheme is always explicit. The
SEQEVAL_*members consume BIO/IOBES tag sequences; the others consume SemEval-2013 span lists via nervaluate.- EXACT = 'exact'#
exact span boundaries, type ignored.
- Type:
nervaluate exact
- PARTIAL = 'partial'#
boundary overlap (half credit), type ignored.
- Type:
nervaluate partial
- SEQEVAL_CONLL = 'seqeval_conll'#
seqeval, CoNLL
conlleval-compatible entity F1 (the default seqeval mode).
- SEQEVAL_STRICT = 'seqeval_strict'#
span boundaries AND type must match, keyed to a scheme.
- Type:
seqeval strict mode
- STRICT = 'strict'#
exact span boundaries AND correct type.
- Type:
nervaluate SemEval-2013 strict
- TYPE = 'type'#
correct type with at least some span overlap.
- Type:
nervaluate type
- class ek.Metric(*args, **kwargs)[source]#
Bases:
ProtocolCompares one prediction to one gold reference, returning a
Score.
- class ek.NodePath(node_id, node_type, field=None)[source]#
Bases:
objectAddresses a node (or one of its fields) in an extracted graph; keys Layer B.
- class ek.NodeType(name, fields=<factory>, importance=1.0)[source]#
Bases:
objectA node kind in the typed graph (e.g. an “invoice” or a “line item”).
- class ek.PlattCalibrator(a=1.0, b=0.0, max_iter=100, kind='platt')[source]#
Bases:
objectPlatt scaling:
sigmoid(a * score + b), fit by Newton/IRLS on labels.The default calibrator: works on any scalar (aggregated confidence/logprob), no logits required. Uses Platt’s target smoothing so it does not overfit small calibration sets.
- Parameters:
max_iter (
int) – Newton iterations (converges in a handful for a 2-parameter model).
- class ek.Provenance(engine='', source_span=None, bbox=None, raw_transcripts=())[source]#
Bases:
objectWhere an extracted value came from, for audit and click-to-source review.
bboxis intentionally typedAnysoekcore need not import any OCR package; the OCR instance stores normalized[0, 1]top-left-origin geometry.
- class ek.QualityReport(calibrated_confidence=None, decision=None, findings=(), raw_signals=<factory>, provenance=<factory>, per_field=None)[source]#
Bases:
objectThe result of a reference-free quality estimate for one extraction.
For a single value the scalar fields carry the verdict. For a whole
AnnotatedExtraction,per_fieldmaps eachNodePathto its updatedFieldEstimate(confidence/findings/decision filled in), and the scalar fields summarize:calibrated_confidenceis the weakest field, anddecisionthe most severe (block>flag>accept).
- class ek.Report(metric='', aggregate=None, n=0, scores=<factory>, per_slice=<factory>, detail=<factory>)[source]#
Bases:
objectAggregate of many
Scores over a corpus, with optional per-slice cuts.aggregateis the corpus-level headline (computed by the metric’s own aggregator – e.g. globally accumulated WER – not a naive mean of per-item values).per_slicemaps a slice label to its own aggregate.
- class ek.RiskControlGate(target=0.05, loss=<function RiskControlGate.<lambda>>, loss_bound=1.0, lam=1.0)[source]#
Bases:
objectConformal Risk Control: accept
p >= lambdabounding the accepted-error rate.Picks the smallest threshold
lambda(the most coverage) whose finite-sample risk bound is at or belowtarget. The controlled quantity isE[ loss(item) * 1(accepted) ]– with the default 0/1 loss, the population rate of accepted-yet-wrong items.- Parameters:
target (
float) – Upper bound on the accepted-error rate (e.g.0.05).loss (
Callable[[bool],float]) –correct -> lossin[0, loss_bound]; default0if correct else1. Any monotone loss bounded byloss_boundworks.loss_bound (
float) – The loss’s upper boundB– the CRC finite-sample slack is+B/(n+1). Must matchloss’s range or the guarantee is invalid; keep the default1.0for the 0/1 loss.
- class ek.RoverConsensus(tokens=<factory>, slots=<factory>, agreement=<factory>, n_engines=0)[source]#
Bases:
objectThe result of a ROVER pass over N hypotheses.
- property mean_agreement: float#
Mean per-position agreement over consensus tokens, a reference-free confidence in
[0, 1]:1.0means every engine agreed at every emitted position; lower means at least one engine dissented.An empty consensus (no token survived voting – NULL won every slot) means the engines agreed on nothing, so this returns
0.0when 2+ engines were fused, NOT a misleading1.0(which would feed a maximal raw confidence to the calibrator/gate). A single engine – or none – is vacuously1.0.
- class ek.Score(value, precision=None, recall=None, f1=None, metric='', detail=<factory>)[source]#
Bases:
objectThe result of comparing one prediction against one reference.
valueis the headline number (higher is better unless a metric documents otherwise). The optional decomposition fields let structured metrics carry precision/recall/F1, an alignment, and raw counts (e.g. edit operations) so that corpus aggregation can be done correctly (e.g. global WER accumulation) rather than by averaging per-item scores.float(score)returnsvalue.
- class ek.Signal(*args, **kwargs)[source]#
Bases:
ProtocolA reference-free quality signal: extractor output -> raw scalar score.
The
cost_tierattribute (lower is cheaper) lets an escalation policy run cheap signals first and pay for expensive ones only on residual uncertainty.
- class ek.SignalFamilyCalibrator(calibrators=<factory>, combine=<function mean_probability>, factory=<class 'ek.qe.calibrate.PlattCalibrator'>, fusion=None, kind='signal_family')[source]#
Bases:
objectOne calibrator per signal family, fused by a documented combination rule.
Averaging heterogeneous raw signals (an LLM-judge score, a token logprob, a ROVER agreement) and calibrating the mean discards exactly what a calibrator needs: each family has its own scale, reliability and direction of miscalibration. This calibrates every signal by name with its own calibrator, then fuses the calibrated probabilities with
combine.Pass it (or simply a
Mapping[str, Calibrator]) asestimate_quality(..., calibrator=...).The fused value must itself be calibrated before it is gated. Fusing calibrated probabilities (by mean, min, …) does not yield a calibrated probability: agreeing families make the mean under-confident, a weak family drags it toward the base rate. So
fit()also learns a second-stagefusioncalibrator oncombine’s output, and__call__applies it. Without afusioncalibrator (e.g. a hand-built mapping),ek.estimate_quality()refuses to gate a value fused from two or more families unless toldassume_calibrated=True(Hard Rule 1). A value from a single family is that family’s calibrated probability and gates as is.A field’s own upstream
confidenceis just one more family here, named"confidence": include it in the training rows under that name.- Parameters:
calibrators (
dict) –signal name -> Calibrator. A"*"entry is the fallback for any signal without its own entry. A signal with neither is refused (ValueError) rather than silently dropped or passed through raw (Hard Rule 1).combine (
Callable[[Mapping[str,float]],float]) –Mapping[str, float] -> floatfusing the calibrated values;mean_probability()(default) ormin_probability(), or any callable of that shape (e.g. a learned stacker).factory (
Callable[[],Any]) – Zero-arg callable producing a fresh calibrator, used byfit().fusion (
Any) – Second-stage calibrator applied tocombine’s output (learned byfit());Noneleaves the fused value uncalibrated.
Example
>>> rows = [{"judge": j, "logprob": l} for j, l in ... [(0.9, -0.1), (0.8, -0.2), (0.85, -2.0), (0.3, -0.1), (0.2, -2.5), (0.7, -1.8)]] >>> correct = [True, True, False, True, False, False] >>> cal = SignalFamilyCalibrator().fit(rows, correct) >>> sorted(cal.calibrators) ['judge', 'logprob'] >>> 0.0 <= cal({"judge": 0.9, "logprob": -0.1}) <= 1.0 True
- calibrate_signals(raw_signals, *, group=None)[source]#
{name: calibrated probability}for each raw signal (before fusion).- Return type:
- combine()#
Fuse calibrated per-family probabilities by their mean (the default combiner).
Each family’s calibrated value estimates the same event (“this field is correct”), so the mean is a defensible, assumption-light pooling rule.
- Return type:
>>> mean_probability({"judge": 0.9, "logprob": 0.7}) 0.8
- factory#
alias of
PlattCalibrator
- fit(signals, correct)[source]#
Fit one calibrator per signal name, then the
fusioncalibrator.Each family is fit over the rows where it appears with a finite value. A family with fewer than two such rows, or only one label class, cannot be calibrated and is refused (
ValueError) rather than turned into a constant that would then dilute the fusion.- Return type:
- class ek.SpanF1Metric(scheme=None, *, tagging_scheme='IOB2')[source]#
Bases:
objectEntity/slot precision/recall/F1 under an explicit
MatchScheme.- Parameters:
scheme (
Optional[MatchScheme]) – REQUIRED. The (backend, scheme) contract – there is deliberately no default, because seqeval and nervaluate disagree and a bare F1 is ambiguous (misc/docs/ek_02§2.2). Pass aMatchScheme.tagging_scheme (
str) – ForMatchScheme.SEQEVAL_STRICTonly: the BIO/IOBES tagging scheme name (e.g."IOB2","IOBES","BILOU") that strict-mode entity decoding is keyed to. Defaults to"IOB2".
- Raises:
TypeError – if
schemeis omitted (the whole point of this metric).
- aggregate(scores)[source]#
Micro-averaged F1 over the corpus (sum the raw counts, then divide once).
seqeval schemes carry TP/FP/FN; nervaluate schemes carry COR/INC/PAR/MIS/SPU (PAR worth half). Both micro-average by summing counts globally – never a mean of per-document F1s. The two backends’ counts are not commensurable, so a corpus mixing them is rejected rather than silently dropping one.
- Return type:
- class ek.StringMetric(mode='cer', *, canonicalizer=None)[source]#
Bases:
objectCER (
mode='cer') or WER (mode='wer') as aMetric.- Parameters:
mode (
str) –"cer"(character) or"wer"(word) error rate.canonicalizer – Optional
str -> strapplied to both sides before scoring (seeek.canonicalize). Usually supplied by the facade instead.
- class ek.Table(rows=())[source]#
Bases:
objectA table as an ordered list of rows of
Cells (the logical cells).rows[i]is the i-th row’s logical cells in reading order; spans are carried on eachCell. Usefrom_html()/from_grid()to build one, andas_grid()for the dense 2-D occupancy view GriTS scores.- as_grid()[source]#
Expand spans into a dense 2-D grid; a spanned position repeats its cell.
Each physical grid position holds the (shared)
Cellcovering it, so two tables with the same topology have grids of the same shape – the view GriTS aligns position-by-position.
- classmethod coerce(obj)[source]#
Coerce a
Table, an HTML string, or a 2-D grid into a Table.- Return type:
- class ek.TaskSpec(task_id, input=None, gold=None, value=1.0, tools=(), slice=None)[source]#
Bases:
objectOne task in a suite: its input, its oracle, its value, and its allowed tools.
- Parameters:
task_id (
str) – Stable identifier (the grouping key for k-trial reliability).input (
Any) – Whatever the agent under test is called with.gold (
Any) – The reference the checker grades against (a goal state, an expected answer, a gold trajectory – the checker decides how to read it).value (
float) – Task-level cost weight: how much a completed task is worth. The task-level analog ofFieldSpec.importance; feeds value-weighted cost accounting.tools (
Sequence[ToolSpec]) – The tools this task permits (the Layer-A grammar for the task).slice (
Optional[str]) – Optional slice label (domain, difficulty, language) – per-slice cuts are mandatory, not optional (a low pass^k concentrated in one hard slice is a different problem from uniform flakiness).
- class ek.TedsMetric(*, structure_only=False)[source]#
Bases:
objectTEDS / TEDS-Struct as a
Metric(clean-room onapted).- Parameters:
structure_only (
bool) – WhenTrue, ignore cell text and score structure alone (TEDS-Struct), isolating table-structure accuracy from OCR noise. Defaults toFalse(full TEDS: structure + content).
- aggregate(scores)[source]#
Corpus TEDS = mean of per-table TEDS (the PubTabNet/OmniDocBench convention). Each per-table TEDS is already node-count-normalized, so the reported corpus statistic is the average, not a pooled edit-rate. The pooled
1 - sum(dist)/sum(nodes)is available from the per-itemdetailif a size-weighted variant is wanted instead.- Return type:
- class ek.TemperatureCalibrator(T=1.0, kind='temperature', t_min=0.05, t_max=10.0, max_iter=60)[source]#
Bases:
objectTemperature scaling:
sigmoid(logit / T)with oneTfit on a holdout.Use only when you have logits:
__call__expects a logit, not a probability.T > 1softens overconfidence; the argmax is unchanged.
- class ek.ToolSpec(name, params=<factory>, importance=None, destructive=False)[source]#
Bases:
objectOne tool the agent may call: its argument schema and its error costs.
- Parameters:
name (
str) – The tool/function name (the AST-match ground truth).params (
Mapping[str,FieldSpec]) – Argument name ->FieldSpec. Each spec’simportanceis the cost of getting that argument wrong, and itsdomainfeeds validators – the same contract as an extracted field.importance (
Optional[float]) – Cost weight of the call itself (a missing or spurious call). Defaults toDESTRUCTIVE_WEIGHTwhendestructiveis set, else 1.0.destructive (
bool) – Whether calling this tool mutates the world irreversibly (a refund, a delete, a send). Drives the default weight and the safety validators.
- class ek.TypeRef(kind, name, field=None)[source]#
Bases:
objectA reference into the schema, so an injected cost function can read weights.
kindis one of"node","edge"or"field"; for a field,nameis the node-type name andfieldthe field name.
- class ek.TypedEdge(src, dst, edge_type='')[source]#
Bases:
objectA directed, typed relation between two node ids.
- class ek.TypedGraph(nodes=(), edges=())[source]#
Bases:
objectA typed graph: typed nodes plus directed typed edges between them.
- class ek.TypedGraphMetric(grammar=None, weights=None, *, canonicalizer=None, timeout=10.0, max_nodes=60)[source]#
Bases:
objectCost-weighted, type-aware typed-graph edit distance as a
Metric.- Parameters:
grammar (
Optional[GraphGrammar]) – Layer-AGraphGrammarsupplying cost weights (may also be passed per-call to__call__).weights (
Optional[Callable[[GraphGrammar,TypeRef],float]]) – OptionalCostWeightoverriding the schema weights.canonicalizer – Optional
str -> strapplied to field values before the equality check (the facade passesnormalize=through here); a schema’s per-fieldFieldSpec.normalizertakes precedence for the fields it names.timeout (
float) – Seconds budget for the (NP-hard) GED search; on timeout the best cost found so far is used (an approximation). Default 10.0.
- class ek.TypedNode(node_id, node_type, fields=<factory>)[source]#
Bases:
objectA node in a typed graph: an id, a node-type tag, and its field values.
- class ek.ValidationResult(original, value, findings=())[source]#
Bases:
objectThe outcome of running a value through a
validation_pipeline().valueis the final value after any applied corrections;originalis the input;findingsis the full audit trail (FLAG and CORRECT), in layer order.
- class ek.Validator(*args, **kwargs)[source]#
Bases:
ProtocolReference-free check on a value, yielding zero or more
Findings.
- class ek.VerifierSignal(validators=<factory>, cost_tier=1)[source]#
Bases:
objectDeterministic verifier evidence as a reference-free
Signal.Cost tier 1 – the free, first-to-run layer. Runs a list of
Validators on a value and returns the fraction that passed in[0, 1]as the raw signal (1.0= every check passed). It also exposes theFindings it produced viafindings(), so the same object feeds both the score path (-> calibrate) and the audit path (-> review). Like every signal it is uncalibrated: a verifier failing is strong evidence, not a probability, so aCalibratorstill runs before any gate.Example
>>> v = VerifierSignal([checksum_validator("luhn")]) >>> v("79927398713") # valid -> all checks pass 1.0 >>> v("79927398710") # invalid -> the one check fails 0.0
- ek.benford_findings(numbers, *, field='', tol=0.15, min_n=30, layer='anomaly')[source]#
FLAG a numeric field whose first-digit distribution deviates from Benford’s law.
A reference-free anomaly check for naturally-occurring magnitudes (amounts, populations, counts): the first significant digit should follow
P(d)=log10(1+1/d). A large deviation flags fabricated or systematically-wrong data. Corpus-level (takes the whole column of values) and FLAG-only – it routes a field to review, never auto-edits, and can false-positive on legitimately bounded/skewed fields. Skipped (returns[]) belowmin_nusable values, since the law is asymptotic.- Return type:
- ek.cache_this(func=None, *, cache=None, key=None, pre_cache=False, as_property=None, ignore=None, serialize=None, deserialize=None)[source]#
Unified caching decorator for properties and methods with persistent storage support.
cache_thisextends the capabilities of Python’s built-infunctools.cached_propertyandfunctools.lru_cacheby providing:Persistent caching: Store cached values in files, databases, or any MutableMapping
Flexible cache backends: Use instance attributes, external stores, or cache factories
Smart key generation: Automatic argument-based keys for methods with parameter filtering
Serialization support: Custom serialize/deserialize functions for complex data
Auto-detection: Automatically chooses property vs method caching based on signature
No LRU eviction: Unlike lru_cache, values persist until explicitly removed
Unlike functools.cached_property (properties only) and lru_cache (memory-only with eviction), cache_this provides a unified interface for both use cases with persistent storage options.
- Parameters:
func (
Callable[[Any],TypeVar(VT)]) – The function to be decorated (usually left empty).cache (
Union[str,MutableMapping[TypeVar(KT),TypeVar(VT)],None]) –The cache storage. Can be:
A MutableMapping instance (shared across instances)
A string naming an instance attribute containing a MutableMapping
A callable taking (instance) and returning a MutableMapping This enables instance-specific caching, e.g.: cache=lambda self: Files(f’/cache/{self.user_id}/’)
key (
Union[TypeVar(KT),Callable[[str],TypeVar(KT)],None]) – For properties: the key to store the cache value, can be a callable that will be applied to the method name to make a key, or an explicit string. For methods: a callable that takes(self, *args, **kwargs)and returns a cache key.pre_cache (
bool|MutableMapping) – Default is False. If True, adds an in-memory cache to the method to (also) cache the results in memory. If a MutableMapping is given, it will be used as the pre-cache. This is useful when you want a persistent cache but also want to speed up access to the method in the same session.as_property (
bool|None) – If True, force use of CachedProperty. If False, force use of CachedMethod. If None (default), auto-detect based on function signature.ignore (
str|list[str] |None) – Parameter name(s) to exclude from cache key computation. Can be a string (single parameter) or list of strings (multiple parameters). Commonly used to ignore ‘self’ or parameters like ‘verbose’ that don’t affect the result.serialize (
Callable[[Any],Any] |None) – Optional function to serialize values before caching (e.g.pickle.dumpsfor binary file storage).deserialize (
Callable[[Any],Any] |None) – Optional function to deserialize cached values (e.g.pickle.loads).
- Returns:
The decorated function.
Comprehensive Example
Here’s a complete example showcasing all major features of cache_this:
>>> import tempfile >>> import os >>> from pathlib import Path >>> >>> class DataProcessor: ... def __init__(self, user_id="user123"): ... self.user_id = user_id ... self.memory_cache = {} # In-memory cache ... self.call_counts = {} # Track function calls for demo ... ... # 1. Basic property caching (like functools.cached_property) ... @cache_this ... def basic_property(self): ... '''Cached in instance.__dict__ by default''' ... self.call_counts['basic_property'] = self.call_counts.get('basic_property', 0) + 1 ... return f"computed_value_{self.call_counts['basic_property']}" ... ... # 2. Property with custom cache and key ... @cache_this(cache='memory_cache', key='custom_prop_key') ... def custom_cached_property(self): ... '''Cached in instance.memory_cache with custom key''' ... self.call_counts['custom_cached_property'] = self.call_counts.get('custom_cached_property', 0) + 1 ... return f"custom_value_{self.call_counts['custom_cached_property']}" ... ... # 3. Method caching with argument-based keys ... @cache_this(cache='memory_cache') ... def compute_result(self, x, y, mode='fast'): ... '''Cached based on arguments (x, y, mode)''' ... key = ('compute_result', x, y, mode) ... self.call_counts[key] = self.call_counts.get(key, 0) + 1 ... return x * y * (2 if mode == 'fast' else 3) ... ... # 4. Method caching with ignored parameters ... @cache_this(cache='memory_cache', ignore={'verbose', 'debug'}) ... def process_data(self, data, algorithm='default', verbose=False, debug=False): ... '''Cache ignores verbose and debug parameters''' ... key = ('process_data', tuple(data), algorithm) ... self.call_counts[key] = self.call_counts.get(key, 0) + 1 ... if verbose: print(f"Processing {data} with {algorithm}") ... return sum(data) * (2 if algorithm == 'default' else 3) ... ... # 5. Instance-specific cache factory ... @cache_this(cache=lambda self: {f'{self.user_id}_cache': {}}.get(f'{self.user_id}_cache')) ... def user_specific_computation(self, value): ... '''Each instance gets its own cache based on user_id''' ... key = ('user_specific_computation', value) ... self.call_counts[key] = self.call_counts.get(key, 0) + 1 ... return value ** 2
Now let’s test all the features:
>>> processor = DataProcessor("alice") >>> >>> # Test basic property caching >>> result1 = processor.basic_property >>> result2 = processor.basic_property # Should use cache >>> assert result1 == result2 == "computed_value_1" >>> assert 'basic_property' in processor.__dict__ # Cached in instance dict >>> >>> # Test custom cache and key >>> result1 = processor.custom_cached_property >>> result2 = processor.custom_cached_property # Should use cache >>> assert result1 == result2 == "custom_value_1" >>> assert 'custom_prop_key' in processor.memory_cache >>> >>> # Test method caching with arguments >>> result1 = processor.compute_result(3, 4, 'fast') >>> result2 = processor.compute_result(3, 4, 'fast') # Should use cache >>> result3 = processor.compute_result(3, 4, 'slow') # Different args, new computation >>> assert result1 == result2 == 24 # 3 * 4 * 2 >>> assert result3 == 36 # 3 * 4 * 3 >>> >>> # Test parameter ignoring >>> result1 = processor.process_data([1, 2, 3], verbose=True) Processing [1, 2, 3] with default >>> result2 = processor.process_data([1, 2, 3], verbose=False) # Should use same cache >>> result3 = processor.process_data([1, 2, 3], debug=True) # Should use same cache >>> assert result1 == result2 == result3 == 12 # sum([1,2,3]) * 2 >>> >>> # Test instance-specific caching >>> result1 = processor.user_specific_computation(5) >>> result2 = processor.user_specific_computation(5) # Should use cache >>> assert result1 == result2 == 25 # 5 ** 2 >>> >>> # Different instance should have separate cache >>> processor2 = DataProcessor("bob") >>> result3 = processor2.user_specific_computation(5) # Fresh computation >>> assert result3 == 25
Used with no arguments,
cache_thiswill cache just as the builtincached_propertydoes – in the instance’s__dict__attribute.>>> class SameAsCachedProperty: ... @cache_this ... def foo(self): ... print("In SameAsCachedProperty.foo...") ... return 42 ... >>> obj = SameAsCachedProperty() >>> obj.__dict__ # the cache is empty {} >>> obj.foo # when we access foo, it's computed and returned... In SameAsCachedProperty.foo... 42 >>> obj.__dict__ # ... but also cached {'foo': 42} >>> obj.foo # so that the next time we access foo, it's returned from the cache. 42
Not that if you specify
cache=False, you get a property that is computed every time it’s accessed:>>> class NoCache: ... @cache_this(cache=False) ... def foo(self): ... print("In NoCache.foo...") ... return 42 ... >>> obj = NoCache() >>> obj.foo In NoCache.foo... 42 >>> obj.foo In NoCache.foo... 42
Specify the cache as a dictionary that lives outside the instance:
>>> external_cache = {} >>> >>> class CacheWithExternalMapping: ... @cache_this(cache=external_cache) ... def foo(self): ... print("In CacheWithExternalMapping.foo...") ... return 42 ... >>> obj = CacheWithExternalMapping() >>> external_cache {} >>> obj.foo In CacheWithExternalMapping.foo... 42 >>> external_cache {'foo': 42} >>> obj.foo 42
Specify the cache as an attribute of the instance, and an explicit key:
>>> class WithCacheInInstanceAttribute: ... ... def __init__(self): ... self.my_cache = {} ... ... @cache_this(cache='my_cache', key='key_for_foo') ... def foo(self): ... print("In WithCacheInInstanceAttribute.foo...") ... return 42 ... >>> obj = WithCacheInInstanceAttribute() >>> obj.my_cache {} >>> obj.foo In WithCacheInInstanceAttribute.foo... 42 >>> obj.my_cache {'key_for_foo': 42} >>> obj.foo 42
Now let’s see a more involved example that exhibits how
cache_thiswould be used in real life. Note two things in the example below.First, that we use
functools.partialto fix the parameters of ourcache_this. This enables us to reuse the samecache_thisin multiple places without all the verbosity. We fix that the cache is the attributecacheof the instance, and that the key is a function that will be computed from the name of the method adding a'.pkl'extension to it.Secondly, we use the
ValueCodecsfromdolto provide a pickle codec for storying values. The backend store used here is a dictionary, so we don’t really need a codec to store values, but in real life you would use a persistent storage that would require a codec, such as files or a database.Thirdly, we’ll use a
pre_cacheto store the values in a different cache “before” (setting and getting) them in the main cache. This is useful, for instance, when you want to persist the values (in the main cache), but keep them in memory for faster access in the same session (the pre-cache, a dict() instance usually). It can also be used to store and use things locally (pre-cache) while sharing them with others by storing them in a remote store (main cache).Finally, we’ll use a dict that logs any setting and getting of values to show how the caches are being used.
>>> from dol import cache_this >>> >>> from functools import partial >>> from dol import ValueCodecs >>> from collections import UserDict >>> >>> >>> class LoggedCache(UserDict): ... name = 'cache' ... ... def __setitem__(self, key, value): ... print(f"In {self.name}: setting {key} to {value}") ... return super().__setitem__(key, value) ... ... def __getitem__(self, key): ... print(f"In {self.name}: getting value of {key}") ... return super().__getitem__(key) ... >>> >>> class CacheA(LoggedCache): ... name = 'CacheA' ... >>> >>> class CacheB(LoggedCache): ... name = 'CacheB' ... >>> >>> cache_with_pickle = partial( ... cache_this, ... cache='cache', # the cache can be found on the instance attribute `cache` ... key=lambda x: f"{x}.pkl", # the key is the method name with a '.pkl' extension ... pre_cache=CacheB(), ... ) >>> >>> >>> class PickleCached: ... def __init__(self, backend_store_factory=CacheA): ... # usually this would be a mapping interface to persistent storage: ... self._backend_store = backend_store_factory() ... self.cache = ValueCodecs.default.pickle(self._backend_store) ... ... @cache_with_pickle ... def foo(self): ... print("In PickleCached.foo...") ... return 42 ...
>>> obj = PickleCached() >>> list(obj.cache) []
>>> obj.foo In CacheA: getting value of foo.pkl In CacheA: getting value of foo.pkl In PickleCached.foo... In CacheA: setting foo.pkl to b'\x80\x04K*.' 42 >>> obj.foo In CacheA: getting value of foo.pkl In CacheB: setting foo.pkl to 42 42
As usual, it’s because the cache now holds something that has to do with
foo:>>> list(obj.cache) ['foo.pkl'] >>> # == ['foo.pkl']
The value of
'foo.pkl'is indeed42:>>> obj.cache['foo.pkl'] In CacheA: getting value of foo.pkl 42
But note that the actual way it’s stored in the
_backend_storeis as pickle bytes:>>> obj._backend_store['foo.pkl'] In CacheA: getting value of foo.pkl b'\x80\x04K*.' >>> # == b'\x80\x04K*.'
- ek.canonicalize_corrector(normalize, *, field_name='', layer='canonicalize')[source]#
L0: fold a value to canonical form, emitting a CORRECT finding when it changes.
normalizeis anythingek.canonicalize.resolve_canonicalizer()accepts (a registered name, a callable, a step list). The narrowest, safest corrector – run it first so later layers compare canonical forms.- Return type:
- ek.check_requirements(*, engine=None, extra=None)[source]#
Report what an OCR engine or feature extra needs, without raising.
For an OCR
engineand an installedocracy, defers toocracy’s own doctor (which knows each backend’s pip package, system binaries and credentials). Otherwise returns a generic hint.- Return type:
- Returns:
A dict with at least
{"ok": bool, "hint": str}.
- ek.checksum_validator(kind='luhn', *, field_name='', layer='checksum')[source]#
A
Validatorthat flags a value failing a named checksum.
- ek.cohen_kappa(rater_a, rater_b)[source]#
Cohen’s kappa for two raters on nominal labels (chance-corrected agreement).
Use only for exactly two raters, nominal labels, complete data; prefer
krippendorff_alpha()for the general case (missing data, any measurement level).- Return type:
- ek.cross_field_validator(predicate, *, message, fields=(), layer='cross_field')[source]#
FLAG a record when
predicate(record)is False (a general cross-field check).Skips silently when
predicateraises on a partial record (a missing field is not itself a cross-field violation).
- ek.default_canonicalizer()[source]#
A conservative default: NFC -> casefold -> collapse whitespace.
- Return type:
- ek.default_cost_weight(grammar, ref)[source]#
The default
CostWeight: read the importance weight off the schema.- Return type:
- ek.engine_yields_tables(engine)[source]#
Whether
engine’s capability profile claims scoreable table structure.A convenience over
ek.ocr.profile(): returns the engine profile’stablesflag. ek treats the profile as a prior (Azure DI, AWS Textract, PaddleOCR and the markdown engines are expected to yield scoreable tables; Google Vision is not), to be verified empirically by actually runningtable_from_ocr(). Use it to skip table scoring for engines that cannot emit cells in the first place.- Return type:
- ek.enum_validator(members, *, field_name='', layer='enum')[source]#
A
Validatorthat flags a value not in an allowed set.
- ek.estimate_quality(extraction, *, sources=(), signals=(), calibrator=None, validators=(), policy=None, agreement=True, assume_calibrated=False)[source]#
Estimate the quality of an extraction with no gold reference.
Composes the strict
signal -> calibrate -> validate -> decidepipeline (misc/docs/ek_03) over a single value, aFieldEstimate, or a wholeAnnotatedExtraction(scored per field, with per-field specs feeding the validators and the node type as the Mondrian group key). The simple callestimate_quality(value)Just Works; every stage is injectable.- Parameters:
extraction (
Any) – A raw value, aFieldEstimate, or anAnnotatedExtraction.sources (
Iterable) – Additional hypotheses of the same content (strings orOcrResult-shaped objects) to fuse with ROVER – their mean agreement becomes araw_signals["agreement"]entry (the flagship online signal).signals (
Iterable) – ExplicitSignalcallablestarget -> float | Mappingproducing further raw signals.calibrator – A
Calibratormapping raw score -> probability, applied to the field’s confidence (else the mean of its raw signals). When the signals are heterogeneous (e.g. an LLM-judge score next to a logprob), pass aMapping[str, Calibrator]keyed by signal name (a"*"entry is the fallback) or aSignalFamilyCalibrator: each signal family is then calibrated on its own and the calibrated probabilities are fused by itscombinerule (mean by default) followed by its learnedfusioncalibrator. A field’s ownconfidenceis the"confidence"family. The per-family values are reported inprovenance["calibrated_signals"]. Calibration is non-optional before gating (Hard Rule 1): passing apolicywith no calibrator andassume_calibratedfalse raises – a raw, uncalibrated score must never reach a DecisionPolicy.validators (
Iterable) –Validatorcallables yielding findings. A flat iterable runs on every field (use spec-driven validators likeschema_validator()). For per-field scoping pass aMappingkeyed by field name or node type ("*"runs on all).policy – A
DecisionPolicyproducing accept/flag/block from the calibrated confidence.agreement (
bool) – Auto-run ROVER oversourceswhen any are given (default true).assume_calibrated (
bool) – Treat the incoming confidence as already calibrated (silences the uncalibrated-gating warning). With a per-family calibrator it also waives the check that a value fused from two or more families went through a learnedfusioncalibrator.
- Return type:
- ek.evaluate(cases, *, metric=None, grammar=None, normalize=None, weights=None)[source]#
Aggregate many comparisons into a
Report(corpus level).- Parameters:
cases (
Iterable) – Iterable of(pred, gold)or(pred, gold, slice_label)tuples.metric (
Union[None,str,Metric]) – As inscore()(resolved once from the first case’s types).grammar (
Optional[GraphGrammar]) – Optional Layer-A grammar passed to every comparison.normalize (
Any) – Optional canonicalizer applied before every comparison.
- Return type:
- Returns:
A Report whose
aggregateis computed by the metric’s own aggregator (e.g. globally accumulated CER/WER, micro-F1) – never a naive mean.
- ek.evaluate_store(predict, gold, *, metric=None, grammar=None, normalize=None, weights=None, input_key='input', reference_key='reference', slice_key='slice', persist=False, run_id=None, rootdir=None)[source]#
Run
predictover a gold store and score it per slice.- Parameters:
predict (
Callable[[Any],Any]) –input -> predictioncallable (the system under test).gold (
Mapping) – Mappingkey -> {input, reference, [slice], ...}(a dict or aekgold store).metric (
Optional[str]) – Metric name/callable (defaults to type-dispatch on the first case).grammar/normalize/weights – forwarded to scoring.
input_key/reference_key/slice_key – field names within each gold record.
persist/run_id/rootdir – persist the run to the
results/runsstores.
- Returns:
A
Report;detail['per_item']maps each key to its prediction, reference, slice, and score.
- ek.expected_calibration_error(probs, correct, *, n_bins=10)[source]#
Expected Calibration Error: weighted mean gap between confidence and accuracy.
Bins predictions by confidence into
n_binsequal-width bins and averages|mean_confidence - accuracy|weighted by bin population.0is perfect. Non-finite probs are skipped; out-of-range probs are clamped into[0, 1].- Return type:
- ek.get(namespace, name)[source]#
Resolve a registered strategy by name (discovering entry points first).
- ek.has_table_structure(ocr_result, *, parser=None)[source]#
Whether
table_from_ocr()recovers a non-empty table fromocr_result.A thin predicate over
table_from_ocr()(sameparsersemantics): true iff a table with at least one cell can be recovered. Use it to route an OCR result to the table metrics vs the text-only CER/WER fallback.- Return type:
- ek.iban_check(iban)[source]#
IBAN validity via the ISO 13616 / ISO 7064 mod-97 rule (must equal 1).
- Return type:
- ek.isbn_check(value)[source]#
Validate an ISBN-10 or ISBN-13 by length (digits/hyphens/spaces ignored).
- Return type:
- ek.json_store(kind, *, rootdir=None)[source]#
A JSON
MutableMappingfor onekindof artifact.Keys are plain names (no extension); values are JSON-serializable objects.
- Return type:
- ek.krippendorff_alpha(reliability_data, *, level='nominal')[source]#
Krippendorff’s alpha – the general inter-annotator agreement coefficient.
Pure-Python (no dependency, permissive core): any number of raters, any measurement
level("nominal","ordinal","interval","ratio"), and missing data (useNoneorfloat('nan')for an unlabelled cell).reliability_datais one row per rater, one column per item. Alpha is1.0for perfect agreement,0.0at chance, and goes negative for systematic disagreement. Computed via the coincidence-matrix method (Krippendorff 2011): observed vs expected disagreement under the level-specific difference metric.- Return type:
Example
>>> data = [[1, 1, 1, 1], [1, 1, 1, 1]] # two raters, perfect agreement >>> krippendorff_alpha(data) 1.0
- ek.lexicon_corrector(vocabulary, *, threshold=0.8, scorer=None, field_name='', layer='lexicon', flag_unmatched=True)[source]#
L2: resolve a value against a closed vocabulary by fuzzy match (
rapidfuzz).The safest corrector in the stack when the candidate set is closed (country codes, SKUs, enums): an exact member passes untouched; a single close match (similarity
>= threshold) is applied as a CORRECT finding; otherwise the value is FLAGged as out-of-vocabulary (whenflag_unmatched). Against an open vocabulary, setflag_unmatched=Falseand treat matches as suggestions only.- Parameters:
- Return type:
- ek.llm_corrector(correct_fn, *, only_flagged=True, field_name='', layer='llm_correct')[source]#
L5: a gated neural/LLM corrector – the only layer that invents content.
correct_fnis an injected(value) -> Optional[str](bring your own LLM/seq2seq call; return a corrected string, orNoneto leave the value unchanged). It is the most expensive and stochastic layer, so by default (only_flagged) it fires **only on a value that a cheaper layer already FLAGged** – gate it to the residual, never let it silently rewrite clean fields (this layer reads the pipeline’s accumulated findings viawants_findings). Emits a CORRECT finding; the pipeline applies the rewrite and keeps the original for audit. Always verify its output downstream – it can make text worse.Recipe (Anthropic):
correct_fn=lambda v: client.messages.create(model=..., messages=[{"role": "user", "content": prompt(v)}]).content[0].text.strip()withclient = anthropic.Anthropic()(MIT). Constrain/verify the result and keep an audit trail.- Return type:
- ek.lm_surprisal_validator(scorer, *, threshold, higher_is_worse=True, field_name='', layer='lm_prior')[source]#
L3: FLAG a value whose language-model surprisal crosses
threshold.scoreris an injected(str) -> floatreturning a surprisal / perplexity / negative-log-likelihood (dependency injection: bring your own in-domain n-gram or masked-LM scorer – an in-domain prior is what makes this useful). FLAG-only on its own; pair it with a candidate generator (lexicon_corrector/llm_corrector) to actually correct. Sethigher_is_worse=Falseif your scorer returns a probability (higher = better).Recipe (masked-LM pseudo-log-likelihood): wrap
minicons–from minicons import scorer; m = scorer.MaskedLMScorer("bert-base-uncased", "cpu")– and passscorer=lambda s: -m.sequence_score([s])[0](surprisal = negative PLL).miniconsis MIT; install your own LM backend (it pulls torch).
- ek.load_baseline(name, *, rootdir=None)[source]#
Load a named baseline (or
Noneif it does not exist).
- ek.load_calibrator(name, *, rootdir=None)[source]#
Reconstruct a persisted calibrator by name (dispatched on its
kind).Validates the stored record so a malformed or unknown
kindfails with an actionable error rather than a rawKeyErroror a load-then-crash-later.- Return type:
- ek.luhn_check(number)[source]#
Luhn (mod-10) checksum: credit cards, IMEIs, some national IDs.
- Return type:
- ek.mall(*, rootdir=None)[source]#
The full stores-of-stores mall (one JSON store per
KINDS).- Return type:
- ek.mean_probability(probs)[source]#
Fuse calibrated per-family probabilities by their mean (the default combiner).
Each family’s calibrated value estimates the same event (“this field is correct”), so the mean is a defensible, assumption-light pooling rule.
- Return type:
>>> mean_probability({"judge": 0.9, "logprob": 0.7}) 0.8
- ek.min_probability(probs)[source]#
Fuse calibrated per-family probabilities by their minimum (conservative).
Use when any one family flagging a problem should be enough to lower the gate.
- Return type:
>>> min_probability({"judge": 0.9, "logprob": 0.7}) 0.7
- ek.ordering_validator(keys, *, strict=True, layer='cross_field')[source]#
FLAG a record whose comparable values at
keysare not ascending.The canonical date/sequence check (
start <= end, issue before due). Only the keys actually present and mutually comparable are checked, so it is safe on partial records.
- ek.pass_at_k(*, n, c, k)[source]#
Unbiased
pass@k: probability that at least one of k samples succeeds.The HumanEval estimator
1 - C(n-c, k) / C(n, k)overntrials withcsuccesses. This is the capability metric – right when a single success is enough (offline candidate generation behind a verifier).- Return type:
Example
>>> round(pass_at_k(n=10, c=1, k=1), 3) 0.1 >>> pass_at_k(n=10, c=10, k=5) 1.0
- ek.pass_hat_k(*, n, c, k)[source]#
Unbiased
pass^k: probability that all k independent trials succeed.The tau-bench estimator
C(c, k) / C(n, k); for a per-trial success probabilitypit decays top**k. This is the reliability metric – the one that matters when consistency is the product.- Return type:
Example
>>> pass_hat_k(n=8, c=8, k=8) 1.0 >>> pass_hat_k(n=8, c=4, k=2) 0.21428571428571427
- ek.percent_agreement(rater_a, rater_b)[source]#
Raw fraction of items two raters labelled identically.
- Return type:
- ek.persistent_cache(func=None, *, kind='runs', key=None, rootdir=None)[source]#
Memoize a plain function’s result in a
kindJSON store, across sessions.Use as
@persistent_cache(kind="runs", key=...). Returned values must be JSON-serializable.keymay be a constant or a callable receiving the call arguments; it defaults to the function’s qualified name (so provide akeyfor functions that take arguments). For caching a class property/method, prefercache_this(dol’s descriptor) instead.Example
>>> import tempfile; root = tempfile.mkdtemp() >>> calls = [] >>> @persistent_cache(kind="runs", key="answer", rootdir=root) ... def compute(): ... calls.append(1) ... return {"answer": 42} >>> compute(), compute(), len(calls) ({'answer': 42}, {'answer': 42}, 1)
- ek.range_validator(lo, hi, *, field_name='', layer='range')[source]#
A
Validatorthat flags a number outside[lo, hi].
- ek.regex_validator(pattern, *, field_name='', layer='format')[source]#
A
Validatorthat flags a value not fully matchingpattern.
- ek.register(namespace, name, obj=None)[source]#
Register
objundernamespace/name(usable as a decorator).- Parameters:
- Return type:
- Returns:
obj(so it can be used as a decorator).
- ek.regression_gate(report, baseline, *, tolerance=0.0, higher_is_better=None, rootdir=None)[source]#
Fail if
reportregresses beyondtolerancevs a baseline, per slice.- Parameters:
report – The current
Report.baseline (
Any) – A baseline name (loaded from thebaselinesstore) or a baseline dict fromsave_baseline().tolerance (
float) – Allowed drift before a change counts as a regression.higher_is_better (
Optional[bool]) – Override metric-direction inference (CER/WER/graph are lower-is-better; F1/similarity are higher-is-better).rootdir (
Optional[str]) – data root (whenbaselineis a name).
- Return type:
- Returns:
A
GateResult(falsy if any regression was found).
- ek.reliability_curve(probs, correct, *, n_bins=10)[source]#
Per-bin
{confidence, accuracy, count}for a reliability diagram.
- ek.requires_extra(extra, *, packages=None)[source]#
Decorator: fail with an actionable install hint if an extra is missing.
The wrapped callable runs only once every package in
packagesimports; the common case (everything installed) adds a single cheap import check.- Parameters:
- Return type:
Example
>>> @requires_extra("ocr", packages=["definitely_not_installed_pkg"]) ... def run(): ... return "ran" >>> try: ... run() ... except MissingExtraError as e: ... print("ek[ocr]" in str(e)) True
- ek.resolve(namespace, ref, *, default=None)[source]#
Coerce a strategy reference to a callable.
refmay be a registered name (str), an already-resolved object, orNone(in which casedefault, itself a name or object, is used).- Return type:
- ek.resolve_table_parser(ref)[source]#
Resolve a table parser reference (a registered name or a callable) to a callable.
Mirrors how other ek strategies resolve from the registry (see
ek.registry.resolve()). Astris looked up under the"table_parsers"namespace; an already-callable parser is returned unchanged.
- ek.risk_coverage_curve(probs, correct, *, thresholds=None)[source]#
The risk-coverage trade-off:
{threshold, coverage, selective_risk}per point.Coverage is the fraction auto-accepted at each threshold; selective risk is the error rate among the accepted. Lower-left is better; pick the operating point against a target risk or coverage.
- ek.rover(hypotheses, *, use_confidence=True, conf_weight=0.5, null_conf=0.7, tokenize=None, max_tokens=5000)[source]#
Align N hypotheses, vote per slot, and emit consensus + per-position agreement.
- Parameters:
hypotheses (
Iterable[Any]) – The recognizer/extractor outputs to fuse. Each may be a string, anOcrResult-shaped object (.text/.mean_confidence), a list of token strings, or a list of(token, confidence)pairs.use_confidence (
bool) – Blend confidence into the vote (True) or vote purely by frequency (False, which forcesconf_weightto0).conf_weight (
float) – Weight on average confidence vs vote frequency in the slot score (Fiscus’s1 - alpha);0is frequency-only,1is confidence-only. Ignored whenuse_confidenceisFalse.null_conf (
float) – Confidence credited to a NULL vote (an engine that produced no token at a slot) – the lever for how readily a deletion wins.tokenize (
Optional[Callable[[Any],List[Tuple[Optional[str],float]]]]) – Optionalhypothesis -> [(token, confidence), ...]override; by default_as_units()handles strings/OcrResults/token lists.max_tokens (
Optional[int]) – Reject any hypothesis longer than this (the aligner isO(N*l*L*L'); an unbounded input is a quadratic time/memory DoS).Nonedisables the guard.
- Return type:
- Returns:
A
RoverConsensuswith the consensustokens/text, the per-slot breakdown, and the per-consensus-tokenagreementusable as a raw signal.
- ek.run_suite(agent, tasks, *, k=1, check=None, metrics=None, price=None, prices=None, run=None, seed=None, persist=False, run_id=None, rootdir=None)[source]#
Run
agentover a task suite,ktrials per task, and report reliability + cost.- Parameters:
agent (
Callable[[TaskSpec],Any]) – The system under test:TaskSpec -> Episode(a bare answer is wrapped). It receives the whole spec –.input,.tools,.gold– not just the input. If it accepts a keywordseed, the per-trial seed is passed to it.k (
int) – Trials per task.k > 1is what makespass^kmeaningful – and it only means anything if the agent is genuinely stochastic (the report warns if it is not).check (
Any) – The success oracle – a registered checker name or an(episode, gold) -> boolcallable. Defaults to"output"; inject a state-based/executable oracle for a real suite, and run it isolated from the agent.metrics (
Optional[Mapping]) – Optional{name: Metric}scored on every episode againsttask.gold, with the suite’s Layer-A grammar injected – this is how the tool/argument cost weights reach atool_call/trajectorymetric. Results land inreport.detail["metrics"].price/prices – Rates for costing the episodes (see
ek.agents.cost).run (
Optional[RunProvenance]) –RunProvenancefor this run (model, simulator, suite version, scaffold). Recorded on every episode and on any baseline saved from it.seed (
Optional[int]) – Base RNG seed. Trial i of each task usesseed + i: it is recorded on the episode’s provenance and passed to the agent when the agent accepts aseedkeyword.persist/run_id/rootdir – Persist the run to the
runs/resultsstores.
- Return type:
- Returns:
A
ReliabilityReportcarryingpass@k,pass^k(with a bootstrap CI), the success rate (with a Wilson CI), the per-slice cuts, and the cost report – because reliability without cost is only half the answer.
- ek.save_baseline(report, name, *, rootdir=None)[source]#
Freeze a report’s aggregate + per-slice scores as a named baseline.
- Return type:
- ek.save_calibrator(calibrator, name, *, rootdir=None)[source]#
Persist a fitted calibrator’s parameters to the
calibratorsstore.- Return type:
- ek.schema_validator(value, *, spec=None)[source]#
A spec-driven
Validator: type anddomainchecks from a FieldSpec.Reads the
FieldSpectypeanddomain– a(lo, hi)numeric range, an enum tuple, or a regex string – and applies the matching check. With no spec it is a no-op, so it composes safely in any validator list.
- ek.score(pred, gold, *, grammar=None, metric=None, normalize=None, weights=None)[source]#
Score one prediction against one gold reference (reference-based).
- Parameters:
pred (
Any) – The predicted output (string, record dict, or anything with.text).gold (
Any) – The gold reference, same shape aspred.grammar (
Optional[GraphGrammar]) – Optional Layer-AGraphGrammar(carries cost weights).metric (
Union[None,str,Metric]) – A registered metric name ("cer","wer","fields", …), a callableMetric, orNoneto dispatch by type.normalize (
Any) – Optional canonicalizer (name, callable, step list, orCanonicalizer) applied before comparison.weights (
Any) – ACostWeightfor cost-weighted metrics (the typed-graph distance); overrides the schema’s importance weights.
- Return type:
- Returns:
A
Score.
- ek.split_conformal_quantile(scores, alpha)[source]#
The split-conformal threshold: the
ceil((n+1)(1-alpha)) / nempirical quantile.scoresare nonconformity scores on an exchangeable calibration set. Returns+infwhennis too small to guarantee1 - alpha(so nothing is flagged – the honest behaviour at that sample size), and-infat thealpha -> 1limit (flag everything).- Return type:
- ek.stop_on_correction(findings)[source]#
Stop policy: end the pass as soon as any layer proposes a correction.
- Return type:
- ek.stop_on_flag(findings)[source]#
Stop policy: end the pass at the first FLAG (fail fast).
- Return type:
- ek.table_from_ocr(ocr_result, *, parser=None)[source]#
Extract a normalized
Tablefrom anOcrResult.The single seam ek’s table metrics (TEDS / GriTS) use to score OCR table output uniformly. Returns the recovered Table, or
Nonewhen the result carries no recoverable table structure.- Parameters:
ocr_result (
Any) – AnyOcrResult-shaped object. ek reads only its.raw(the engine-specific payload),.markdown(VLM/markdown engines), and.meta– never ocracy types, so anyimage -> OcrResultcallable’s output works.parser (
Union[None,str,Callable[[Any],Any]]) –How to turn the raw payload into a table (dependency injection / open-closed). One of:
None(default): try the built-in safe heuristics –.rawis already aTable, a 2-D grid, or a<table>...</table>HTML string; or.raw/.metais a mapping carrying one of{tables, table, cells, grid, html}; or.markdowncontains an HTML<table>. ReturnsNoneif none match – it never guesses wildly.a
str: the name of a parser registered under"table_parsers"(per-engine extractor), resolved fromek.registry.a callable
(raw) -> Table | grid | html | None: an inline per-engine extractor. It receives the result’s.raw(falling back to the result itself when there is no.raw).
through (Any non-None value an injected parser returns is fed)
- Return type:
Table | None
:param
Table.coerce(): :param so a parser may yield a Table: :param a 2-D grid: :param or HTML.:- Return type:
- Returns:
A non-empty
Table, orNonewhen no table structure is recoverable. An empty Table (no rows/cells) is normalized toNoneso the predicatehas_table_structure()and downstream metrics see “no table”, not a degenerate one.
- ek.totals_consistent(record, *, total_key, item_keys, tol=0.01, layer='cross_field')[source]#
Flag when
record[total_key]does not equal the sum ofitem_keys(±tol).The canonical cross-field check (invoice total = Σ line items). Skipped (rather than raising or flagging) when the total or all item keys are absent or non-numeric, so it is safe on partial records.
- ek.validation_pipeline(*layers, apply_corrections=True, stop_when=<function _never_stop>)[source]#
Compose validators/correctors into a cheapest -> most-expensive pipeline.
layersrun in the given order (the order encodes the cost spine). Each layer is a(value, *, spec) -> Iterable[Finding]callable. Whenapply_corrections(default), the firstCORRECTfinding a layer emits is applied to the value before the next layer runs – so a cheap deterministic fix is what the expensive layers then see (the noisy-channel chain).stop_whenis an injectable early-exit policy over the findings accumulated so far (seestop_on_correction()/stop_on_flag()); the default runs every layer.- Return type:
- ek.zscore_anomaly_findings(numbers, *, field='', threshold=3.5, min_n=30, layer='anomaly')[source]#
FLAG numeric values that are robust-z-score outliers (median + MAD based).
A reference-free, dependency-free anomaly check that complements
benford_findings()(which checks the leading-digit distribution; this checks individual magnitudes). For a numeric column it computes each value’s modified z-score –0.6745 * (x - median) / MAD– which is robust to the very outliers it is looking for, and FLAGs those whose absolute score exceedsthreshold. One FLAG per outlying value (carrying its index); FLAG-only – it routes a value to review, never edits. Skipped belowmin_n(robust statistics on a handful of points are noise). When the MAD is 0 (a near-constant column, e.g. many identical values plus one outlier) it falls back to the mean-absolute-deviation scale (Iglewicz & Hoaglin), so a lone outlier is still caught; a truly constant column (no spread at all) yields no findings. For multivariate outliers, plug an isolation forest viapyod(already inek[validation]) as a custom validator.- Return type:
Modules
OCR evaluation -- |
|
|
|
Reference-based (offline) metrics: the |
|
Versioned, composable canonicalization applied before scoring. |
|
Reference-free quality estimation: the |
|
Foundational data structures and strategy protocols for |
|
The two top-level facades: |
|
The offline evaluation harness: run a predictor over gold, gate regressions. |
|
Strategy registry, optional-dependency guarding, and requirement checks. |
|
Local-file persistence for |
|
Small, CLI-friendly functions over the |
|
Reference-free post-extraction validation & correction (issue #7). |