sdm-learn 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sdm/__init__.py ADDED
@@ -0,0 +1,77 @@
1
+ """Self-documenting models: the document *is* the model.
2
+
3
+ There are no weights and no classifier head. A document holds the learned
4
+ hypothesis, an LLM reads it to classify inputs, and then rewrites it from the
5
+ revealed labels. What you train is readable, and what you read is what runs.
6
+
7
+ Four components plug into one interface, one file each, and the imports only
8
+ point downward:
9
+
10
+ gateway.py the wire to an LLM, and SetupNeeded
11
+ data.py the dataset, the input representation, row rendering
12
+ formats.py how the hypothesis is written down and revised
13
+ signals.py the audio codec, imported only on demand
14
+ learn.py run artifacts, the gate, the baseline, train / evaluate
15
+
16
+ ``import sdm`` is the only entry point: everything public is re-exported here,
17
+ so callers never see the split.
18
+
19
+ The gate algorithm runs this loop per epoch::
20
+
21
+ predict the feedback batches from the current document (labels hidden)
22
+ reveal the labels and ask for one bounded revision
23
+ score the current and revised documents head to head on protected rows
24
+ keep the revision only if it wins
25
+
26
+ ``state.json`` is the truth; the skill file is rendered from it.
27
+
28
+ import sdm
29
+
30
+ data = sdm.dataset(
31
+ train=[{"text": "wire me $500 today", "label": "spam"},
32
+ {"text": "lunch at one?", "label": "ham"}],
33
+ classes=("spam", "ham"),
34
+ description="Each input is one email body.",
35
+ )
36
+ skill = sdm.train(data, algo="sdm")
37
+ print(skill)
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from . import datasets, science
43
+ from .data import (REPRESENTATIONS, Dataset, Report, content, dataset,
44
+ describe, from_jsonl, labeled)
45
+ from .formats import (DEFAULT_FORMAT_MD, PACKAGED_FORMAT_FILE, apply_edits, clean_condition, codecs,
46
+ credit, format_file, formats, get_format, load_formats,
47
+ new_state, parse_edits, parse_predictions, render,
48
+ validate_html, validate_svg, validate_text,
49
+ write_format_file)
50
+ from .gateway import SetupNeeded
51
+ from .learn import (ALGOS, GATE_SCORING, Skill, epoch_points, decide,
52
+ evaluate, load, parse_paired, proposal_history_text,
53
+ split_holdout, stratified_holdout, train)
54
+ from .experiment import run_experiment
55
+ from .prauc import average_precision, evaluate_prauc
56
+
57
+ __version__ = "0.1.0"
58
+
59
+ __all__ = [
60
+ # the API
61
+ "Dataset", "Report", "Skill", "SetupNeeded",
62
+ "dataset", "datasets", "science", "from_jsonl", "load", "train", "evaluate",
63
+ "run_experiment", "evaluate_prauc", "average_precision",
64
+ # the four components
65
+ "REPRESENTATIONS", "describe",
66
+ "formats", "get_format", "load_formats", "DEFAULT_FORMAT_MD",
67
+ "format_file", "write_format_file", "PACKAGED_FORMAT_FILE",
68
+ "ALGOS", "GATE_SCORING", "codecs",
69
+ # the pieces the README and the checks reach for
70
+ "render", "new_state", "parse_edits", "apply_edits", "clean_condition",
71
+ "parse_predictions", "parse_paired", "credit",
72
+ "validate_text", "validate_html", "validate_svg",
73
+ "stratified_holdout", "split_holdout", "epoch_points", "decide",
74
+ "proposal_history_text",
75
+ "content", "labeled",
76
+ "__version__",
77
+ ]
sdm/__main__.py ADDED
@@ -0,0 +1,58 @@
1
+ """``python -m sdm``, and the ``sdm`` console script.
2
+
3
+ Small on purpose. The library is the product; this exists so a pip-installed
4
+ user can get an editable ``format.md``, confirm the install works without an
5
+ API key, and run the shipped benchmarks.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+
13
+ from . import __version__, format_file, formats, write_format_file
14
+ from .gateway import SetupNeeded
15
+
16
+
17
+ def main(argv=None) -> int:
18
+ parser = argparse.ArgumentParser(
19
+ prog="sdm", description="Self-documenting models.")
20
+ parser.add_argument("--version", action="version",
21
+ version=f"sdm {__version__}")
22
+ commands = parser.add_subparsers(dest="command", required=True)
23
+
24
+ initialize = commands.add_parser(
25
+ "init", help="write an editable format.md into this directory")
26
+ initialize.add_argument("--force", action="store_true",
27
+ help="overwrite an existing format.md")
28
+
29
+ commands.add_parser(
30
+ "formats", help="list the skill formats format.md defines")
31
+
32
+ from . import benchmarks
33
+ benchmarks.add_arguments(commands.add_parser(
34
+ "run", help="run a shipped benchmark: --dataset, --algo, --format"))
35
+
36
+ options = parser.parse_args(argv)
37
+ try:
38
+ if options.command == "run":
39
+ return benchmarks.run(options)
40
+
41
+ if options.command == "init":
42
+ path = write_format_file(force=options.force)
43
+ print(f"wrote {path}")
44
+ print("Edit the 'format:' line to change the skill format.")
45
+ return 0
46
+
47
+ active = format_file()
48
+ print(f"reading {active}")
49
+ for name, skill_format in sorted(formats().items()):
50
+ print(f" {name:10s} mechanism: {skill_format.mechanism}")
51
+ return 0
52
+ except SetupNeeded as error:
53
+ print(f"sdm: {error}", file=sys.stderr)
54
+ return 1
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(main())
sdm/benchmarks.py ADDED
@@ -0,0 +1,221 @@
1
+ """The shipped benchmarks, and the one command that runs them.
2
+
3
+ sdm run --dataset pods --algo sdm --format html
4
+ sdm run --dataset data/mine # train.jsonl and test.jsonl inside
5
+
6
+ Each benchmark is a loader plus the gate settings its protocol calls for. The
7
+ command line only overrides those defaults; the lifecycle (zero-training
8
+ control, gated training, evaluation, resume, result files) is the shared
9
+ :func:`sdm.run_experiment`. PODS and DF2 add the original paper's one-vs-all
10
+ PR-AUC after top-1 accuracy. A directory of JSONL rows is a benchmark too,
11
+ with the generic defaults.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ from . import datasets, science
22
+ from .data import from_jsonl
23
+ from .experiment import run_experiment
24
+ from .learn import ALGOS, GATE_SCORING, load
25
+ from .prauc import evaluate_prauc
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Benchmark:
30
+ """One dataset and the training settings its protocol uses."""
31
+
32
+ load: object
33
+ protocol: str
34
+ settings: dict = field(default_factory=dict)
35
+ train: int | None = None
36
+ test: int | None = None
37
+ image_size: int | None = None
38
+ prauc: bool = False
39
+
40
+
41
+ PERSONAL = {"holdout_per_class": 1, "epochs": 7, "gate_scoring": "separate",
42
+ "proposal_history": True, "max_semantic_edits": 60}
43
+ SCIENCE = {"epochs": 4}
44
+
45
+ BENCHMARKS = {
46
+ "cifar10": Benchmark(
47
+ load=lambda o: datasets.cifar10(o.train, o.test, root=o.data_dir or "data/cifar"),
48
+ protocol="direct RGB images, 10 classes",
49
+ settings={"holdout_per_class": 1, "epochs": 5,
50
+ "max_semantic_edits": 100, "max_support_edits": 100},
51
+ train=100, test=100),
52
+ "pods": Benchmark(
53
+ load=lambda o: datasets.pods("test", o.test, image_size=o.image_size,
54
+ root=o.data_dir or "data/pods"),
55
+ protocol="PODS paper class split: 70 test objects, 3 training photos "
56
+ "each, test photos per object from the full test split",
57
+ settings=PERSONAL, test=10, image_size=512, prauc=True),
58
+ "df2": Benchmark(
59
+ load=lambda o: datasets.df2("train", o.test, image_size=o.image_size,
60
+ val_per_class=0,
61
+ root=o.data_dir or "data/personal/df2/df2"),
62
+ protocol="PODS paper protocol on DF2: 139 test shirts, 3 training "
63
+ "photos each, test photos per shirt",
64
+ settings=PERSONAL, test=10, image_size=512, prauc=True),
65
+ "camelyon17": Benchmark(
66
+ load=lambda o: datasets.camelyon17(
67
+ o.train, o.test, image_size=o.image_size, seed=o.seed,
68
+ root=o.data_dir or "data/wilds/camelyon17"),
69
+ protocol="WILDS in-distribution training patches, official "
70
+ "out-of-distribution test patches",
71
+ settings={"holdout_per_class": 10, "epochs": 4,
72
+ "gate_scoring": "separate"},
73
+ train=100, test=500, image_size=288),
74
+ # The four science benchmarks of reproduce/ai4science. Their reported
75
+ # numbers were produced with the SLMD-dev learner (see that README); here
76
+ # they run under the generic gate settings.
77
+ "gravityspy": Benchmark(
78
+ load=lambda o: science.gravityspy(
79
+ o.train, o.test, image_size=o.image_size, seed=o.seed,
80
+ root=o.data_dir or "data/gravityspy"),
81
+ protocol="Gravity Spy v1.0 official train / test samples, the ten "
82
+ "largest glitch classes, four-window spectrograms with a Hz "
83
+ "ruler and time axis",
84
+ settings=SCIENCE, train=300, test=400, image_size=412),
85
+ "galaxy10": Benchmark(
86
+ load=lambda o: science.galaxy10(
87
+ o.train, o.test, image_size=o.image_size, seed=o.seed,
88
+ root=o.data_dir or "data/galaxy10"),
89
+ protocol="Galaxy10 DECaLS colour cutouts, ten Galaxy Zoo morphology "
90
+ "classes, class-balanced seeded draw (no official split)",
91
+ settings=SCIENCE, train=100, test=100, image_size=224),
92
+ "bloodmnist": Benchmark(
93
+ load=lambda o: science.bloodmnist(
94
+ o.train, o.test, image_size=o.image_size, seed=o.seed,
95
+ root=o.data_dir or "data/bloodmnist"),
96
+ protocol="BloodMNIST 224 px official train / test split, eight "
97
+ "peripheral blood cell types",
98
+ settings=SCIENCE, train=300, test=160, image_size=224),
99
+ "eclipsing": Benchmark(
100
+ load=lambda o: science.eclipsing(
101
+ o.train, o.test, seed=o.seed, root=o.data_dir or "data/ogle3"),
102
+ protocol="OGLE-III LMC eclipsing binaries as order-4 Fourier "
103
+ "coefficients of the folded I-band light curve; contact / "
104
+ "semidetached / detached",
105
+ settings=SCIENCE, train=60, test=150),
106
+ }
107
+
108
+
109
+ def custom(path: Path) -> Benchmark:
110
+ """A directory holding ``train.jsonl`` and ``test.jsonl`` as a benchmark.
111
+
112
+ Rows need a ``label`` and ``text`` or ``image`` (paths relative to the
113
+ file); classes are the labels seen. ``--description`` tells the model
114
+ what one input is.
115
+ """
116
+ train, test = path / "train.jsonl", path / "test.jsonl"
117
+ for file in (train, test):
118
+ if not file.is_file():
119
+ raise FileNotFoundError(
120
+ f"{file} is missing; a custom --dataset is a directory with "
121
+ "train.jsonl and test.jsonl")
122
+ with open(train) as handle:
123
+ first = json.loads(next(handle))
124
+ representation = "image" if "image" in first else "text"
125
+
126
+ def load(options):
127
+ return from_jsonl(train, test, limit=options.train,
128
+ test_limit=options.test, name=path.name,
129
+ representation=representation,
130
+ description=options.description or "",
131
+ **({"image_size": options.image_size}
132
+ if options.image_size else {}))
133
+
134
+ return Benchmark(load=load, protocol=f"JSONL rows under {path}",
135
+ settings={"epochs": 4})
136
+
137
+
138
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
139
+ parser.add_argument("--dataset", required=True,
140
+ help=f"one of {', '.join(sorted(BENCHMARKS))}, or a "
141
+ "directory with train.jsonl and test.jsonl")
142
+ parser.add_argument("--description",
143
+ help="custom dataset: what one input is")
144
+ parser.add_argument("--algo", choices=tuple(ALGOS), default="sdm",
145
+ help="sdm: learn a document under the gate; "
146
+ "base: the zero-training control")
147
+ parser.add_argument("--format", default="markdown",
148
+ help="a section of format.md: markdown, text, html, "
149
+ "image, audio, or your own")
150
+ parser.add_argument("--style",
151
+ help="what the document should contain, in words, "
152
+ "e.g. \"decision tree, tables, illustrations\"")
153
+ parser.add_argument("--train", type=int, help="training rows")
154
+ parser.add_argument("--test", type=int,
155
+ help="test rows (PODS, DF2: per object)")
156
+ parser.add_argument("--image-size", type=int, help="pixels per side")
157
+ parser.add_argument("--batch-size", type=int, default=10)
158
+ parser.add_argument("--epochs", type=int, help="gated revisions")
159
+ parser.add_argument("--holdout-per-class", type=int,
160
+ help="rows per class the gate protects")
161
+ parser.add_argument("--gate-scoring", choices=GATE_SCORING)
162
+ parser.add_argument("--max-semantic-edits", type=int,
163
+ help="rule edits one candidate may make")
164
+ parser.add_argument("--no-proposal-history", action="store_true",
165
+ help="hide earlier candidates' verdicts from the writer")
166
+ parser.add_argument("--no-baseline", action="store_true",
167
+ help="skip the zero-training control")
168
+ parser.add_argument("--seed", type=int, default=0)
169
+ parser.add_argument("--data-dir", help="where the dataset lives")
170
+ parser.add_argument("--run-dir",
171
+ help="default: runs/<dataset>-<algo>-<format>")
172
+
173
+
174
+ def settings_for(benchmark: Benchmark, options) -> dict:
175
+ """The benchmark's defaults, with explicit command-line values winning."""
176
+ settings = dict(benchmark.settings)
177
+ for key in ("epochs", "holdout_per_class", "gate_scoring",
178
+ "max_semantic_edits"):
179
+ value = getattr(options, key, None)
180
+ if value is not None:
181
+ settings[key] = value
182
+ if options.no_proposal_history:
183
+ settings.pop("proposal_history", None)
184
+ if options.style:
185
+ settings["style"] = options.style
186
+ return settings
187
+
188
+
189
+ def run(options) -> int:
190
+ if options.dataset in BENCHMARKS:
191
+ benchmark = BENCHMARKS[options.dataset]
192
+ elif Path(options.dataset).is_dir():
193
+ benchmark = custom(Path(options.dataset))
194
+ else:
195
+ raise SystemExit(f"sdm run: unknown dataset {options.dataset!r}; choose "
196
+ f"{', '.join(sorted(BENCHMARKS))} or a directory with "
197
+ "train.jsonl and test.jsonl")
198
+ name = Path(options.dataset).name
199
+ for key in ("train", "test", "image_size"):
200
+ if getattr(options, key) is None:
201
+ setattr(options, key, getattr(benchmark, key))
202
+ data = benchmark.load(options)
203
+ run_dir = options.run_dir or f"runs/{name}-{options.algo}-{options.format}"
204
+ settings = settings_for(benchmark, options)
205
+ print(f"{name}: {len(data.classes)} classes, {len(data.train)} "
206
+ f"training rows, {len(data.test)} test rows; {benchmark.protocol}")
207
+ print(f"algo={options.algo}; format={options.format}", flush=True)
208
+ run_experiment(
209
+ data, run_dir=run_dir, algo=options.algo, format=options.format,
210
+ batch_size=options.batch_size,
211
+ run_baseline=options.algo == "sdm" and not options.no_baseline,
212
+ metadata={"protocol": benchmark.protocol, "seed": options.seed},
213
+ seed=options.seed, **settings)
214
+ if benchmark.prauc:
215
+ skill = load(run_dir)
216
+ result = evaluate_prauc(skill, data, run_dir=run_dir, tag="prauc")
217
+ print(f"PR-AUC {100 * result['mean_prauc']:.1f}, top-1 from the same "
218
+ f"scores {100 * result['top1_from_scores']:.1f}% "
219
+ f"({result['unparsed_inputs']} unparsed inputs); saved to "
220
+ f"{run_dir}/prauc.json")
221
+ return 0
sdm/data.py ADDED
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from .gateway import _png_block, image_block
8
+
9
+
10
+ DEFAULT_TEXT_DESCRIPTION = (
11
+ "Each input is one example rendered as text. Infer the meaning of its "
12
+ "fields from the labeled examples you are shown."
13
+ )
14
+ DEFAULT_IMAGE_DESCRIPTION = (
15
+ "Each input is one image. Infer what distinguishes the classes from the "
16
+ "labeled examples you are shown."
17
+ )
18
+
19
+
20
+ class TextRepresentation:
21
+ """Rows are serialized into the prompt text.
22
+
23
+ ``inline`` picks which half of this interface the prompts consult:
24
+ ``serialize`` for a representation that becomes text, ``blocks`` for one
25
+ that becomes attachments. Never both.
26
+ """
27
+
28
+ name = "text"
29
+ inline = True
30
+ default_description = DEFAULT_TEXT_DESCRIPTION
31
+
32
+ @staticmethod
33
+ def serialize(row: dict) -> str:
34
+ if "text" in row:
35
+ return str(row["text"])
36
+ return "\n".join(
37
+ f"{key}={value}" for key, value in sorted(row.items())
38
+ if key not in {"id", "label", "index", "image"})
39
+
40
+ @staticmethod
41
+ def blocks(rows: list[dict], tag: str, image_size: int) -> list:
42
+ return []
43
+
44
+
45
+ class ImageRepresentation(TextRepresentation):
46
+ """Rows are attached as images, one content block each."""
47
+
48
+ name = "image"
49
+ inline = False
50
+ default_description = DEFAULT_IMAGE_DESCRIPTION
51
+
52
+ @staticmethod
53
+ def blocks(rows: list[dict], tag: str, image_size: int) -> list:
54
+ attached = []
55
+ for number, row in enumerate(rows, 1):
56
+ attached.append({"type": "text", "text": f"{tag} P{number:02d}"})
57
+ attached.append(image_block(row["image"], image_size))
58
+ return attached
59
+
60
+
61
+ REPRESENTATIONS = {"text": TextRepresentation, "image": ImageRepresentation}
62
+
63
+
64
+ # --------------------------------------------------------------------------
65
+
66
+ @dataclass
67
+ class Dataset:
68
+ """Labeled rows plus the little context the model needs to read them.
69
+
70
+ Every row needs a ``label``. Its input is ``text``, or ``image`` (a PIL
71
+ image, an RGB array, or a path), or any other scalar fields.
72
+ """
73
+
74
+ train: list[dict] = field(default_factory=list)
75
+ test: list[dict] = field(default_factory=list)
76
+ classes: tuple[str, ...] = ()
77
+ name: str = "custom"
78
+ title: str = ""
79
+ description: str = ""
80
+ representation: str = "text"
81
+ image_size: int = 224
82
+ modality: str | None = None
83
+
84
+ def __post_init__(self):
85
+ self.train = [dict(row) for row in self.train]
86
+ self.test = [dict(row) for row in self.test]
87
+ if self.modality is not None:
88
+ self.representation = self.modality
89
+ self.modality = self.representation
90
+ if not self.classes:
91
+ self.classes = tuple(sorted(
92
+ {str(row["label"]) for row in self.train + self.test
93
+ if "label" in row}
94
+ ))
95
+ self.classes = tuple(str(label) for label in self.classes)
96
+ if self.representation not in REPRESENTATIONS:
97
+ raise ValueError(
98
+ f"unknown representation {self.representation!r}; choose one "
99
+ f"of {', '.join(sorted(REPRESENTATIONS))}")
100
+ if self.image_size <= 0:
101
+ raise ValueError("image_size must be positive")
102
+ if not self.title:
103
+ self.title = f"{self.name} skill"
104
+ for rows in (self.train, self.test):
105
+ for position, row in enumerate(rows):
106
+ row.setdefault("index", position)
107
+
108
+ def __len__(self):
109
+ return len(self.train)
110
+
111
+ def __repr__(self):
112
+ return (f"Dataset(name={self.name!r}, train={len(self.train)}, "
113
+ f"test={len(self.test)}, classes={len(self.classes)})")
114
+
115
+
116
+
117
+ @dataclass
118
+ class Report:
119
+ """The outcome of scoring a frozen skill."""
120
+
121
+ correct: int
122
+ total: int
123
+ rows: list[dict] = field(default_factory=list)
124
+ requests: int = 0
125
+
126
+ @property
127
+ def accuracy(self) -> float:
128
+ return self.correct / self.total if self.total else 0.0
129
+
130
+ def __repr__(self):
131
+ return (f"Report(accuracy={self.accuracy:.1%}, "
132
+ f"{self.correct}/{self.total}, requests={self.requests})")
133
+
134
+
135
+
136
+ def dataset(train, test=(), **options) -> Dataset:
137
+ """Wrap in-memory labeled rows into a :class:`Dataset`."""
138
+ return Dataset(train=list(train), test=list(test), **options)
139
+
140
+
141
+ def from_jsonl(train, test=None, *, limit=None, test_limit=None,
142
+ **options) -> Dataset:
143
+ """Read JSONL rows; relative ``image`` paths resolve from each file."""
144
+ wants_images = (options.get("representation") == "image"
145
+ or options.get("modality") == "image")
146
+
147
+ def read(path, cap):
148
+ path = Path(path)
149
+ if not path.exists():
150
+ raise FileNotFoundError(f"missing {path}")
151
+ with open(path) as handle:
152
+ rows = [json.loads(line) for line in handle]
153
+ rows = rows if cap is None else rows[:cap]
154
+ if wants_images:
155
+ for row in rows:
156
+ image = row.get("image")
157
+ if isinstance(image, str) and not Path(image).is_absolute():
158
+ row["image"] = str((path.parent / image).resolve())
159
+ return rows
160
+
161
+ return Dataset(train=read(train, limit),
162
+ test=read(test, test_limit) if test else [], **options)
163
+
164
+ def describe(data: Dataset) -> str:
165
+ if data.description:
166
+ return data.description
167
+ return REPRESENTATIONS[data.representation].default_description
168
+
169
+
170
+ def labeled(label: str, pngs) -> list[tuple[str, bytes]]:
171
+ """Tag each rendering of one document so the model can tell them apart."""
172
+ return [(label, png) for png in pngs]
173
+
174
+
175
+ def content(prompt: str, rows: list[dict], representation: str, tag: str,
176
+ image_size: int, views=()):
177
+ """Prompt text, the skill's renderings, then the inputs.
178
+
179
+ ``views`` holds ``(label, png)`` pairs; each label is numbered separately
180
+ so a two-document gate prompt reads A 01, A 02, B 01 rather than 01..04.
181
+ """
182
+ attached = REPRESENTATIONS[representation].blocks(rows, tag, image_size)
183
+ if not attached and not views:
184
+ return prompt
185
+ blocks = [{"type": "text", "text": prompt}]
186
+ counts: dict[str, int] = {}
187
+ for label, png in views:
188
+ counts[label] = counts.get(label, 0) + 1
189
+ blocks.append({"type": "text",
190
+ "text": f"{label} {counts[label]:02d}"})
191
+ blocks.append(_png_block(png))
192
+ return blocks + attached
193
+
194
+
195
+ def _inputs(rows: list[dict], representation: str, tag: str = "input") -> str:
196
+ if not REPRESENTATIONS[representation].inline:
197
+ return ""
198
+ serialize = REPRESENTATIONS[representation].serialize
199
+ return "\n\n".join(
200
+ f'<{tag} id="P{number:02d}">\n{serialize(row)}\n</{tag}>'
201
+ for number, row in enumerate(rows, 1))
202
+
203
+
204
+ def _revealed(rows: list[dict], predictions: list[dict], representation: str,
205
+ cites_rules: bool = True) -> str:
206
+ """The labeled feedback block: truth, prediction, citations, evidence."""
207
+ def cited(prediction, attribute: bool):
208
+ if not cites_rules:
209
+ return ""
210
+ joined = ",".join(prediction["rules_used"]) or "none"
211
+ return f' rules="{joined}"' if attribute else f"rules={joined}; "
212
+
213
+ if not REPRESENTATIONS[representation].inline:
214
+ return "\n".join(
215
+ f'P{number:02d}: true={row["label"]}; '
216
+ f'predicted={prediction["pred"]}; '
217
+ f'{cited(prediction, False)}'
218
+ f'evidence={prediction["evidence"]}'
219
+ for number, (row, prediction)
220
+ in enumerate(zip(rows, predictions), 1))
221
+ serialize = REPRESENTATIONS[representation].serialize
222
+ return "\n\n".join(
223
+ f'<example id="P{number:02d}" true="{row["label"]}" '
224
+ f'predicted="{prediction["pred"]}"{cited(prediction, True)}>\n'
225
+ f'{serialize(row)}\n'
226
+ f'<evidence>{prediction["evidence"]}</evidence>\n</example>'
227
+ for number, (row, prediction) in enumerate(zip(rows, predictions), 1))