sdm-learn 0.1.0.dev0__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,71 @@
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 and video codecs, 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 cycle::
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="gate")
37
+ print(skill)
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ from .data import (REPRESENTATIONS, Dataset, Report, content, dataset,
43
+ describe, from_jsonl, labeled)
44
+ from .formats import (DEFAULT_FORMAT_MD, PACKAGED_FORMAT_FILE, apply_edits, clean_condition, codecs,
45
+ credit, format_file, formats, get_format, load_formats,
46
+ new_state, parse_edits, parse_predictions, render,
47
+ validate_html, validate_svg, validate_text,
48
+ write_format_file)
49
+ from .gateway import SetupNeeded
50
+ from .learn import (ALGOS, Skill, cycle_points, decide, evaluate, load,
51
+ parse_paired, stratified_holdout, train)
52
+
53
+ __version__ = "0.1.0.dev0"
54
+
55
+ __all__ = [
56
+ # the API
57
+ "Dataset", "Report", "Skill", "SetupNeeded",
58
+ "dataset", "from_jsonl", "load", "train", "evaluate",
59
+ # the four components
60
+ "REPRESENTATIONS", "describe",
61
+ "formats", "get_format", "load_formats", "DEFAULT_FORMAT_MD",
62
+ "format_file", "write_format_file", "PACKAGED_FORMAT_FILE",
63
+ "ALGOS", "codecs",
64
+ # the pieces the README and the checks reach for
65
+ "render", "new_state", "parse_edits", "apply_edits", "clean_condition",
66
+ "parse_predictions", "parse_paired", "credit",
67
+ "validate_text", "validate_html", "validate_svg",
68
+ "stratified_holdout", "cycle_points", "decide",
69
+ "content", "labeled",
70
+ "__version__",
71
+ ]
sdm/__main__.py ADDED
@@ -0,0 +1,51 @@
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`` and confirm the install works without
5
+ an API key.
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
+ options = parser.parse_args(argv)
33
+ try:
34
+ if options.command == "init":
35
+ path = write_format_file(force=options.force)
36
+ print(f"wrote {path}")
37
+ print("Edit the 'format:' line to change the skill format.")
38
+ return 0
39
+
40
+ active = format_file()
41
+ print(f"reading {active}")
42
+ for name, skill_format in sorted(formats().items()):
43
+ print(f" {name:10s} mechanism: {skill_format.mechanism}")
44
+ return 0
45
+ except SetupNeeded as error:
46
+ print(f"sdm: {error}", file=sys.stderr)
47
+ return 1
48
+
49
+
50
+ if __name__ == "__main__":
51
+ raise SystemExit(main())
sdm/data.py ADDED
@@ -0,0 +1,238 @@
1
+ """The data half: what a row is, how it reaches the model, what comes back.
2
+
3
+ Two of the four components live here. The **input representation** decides how
4
+ one labeled row becomes prompt material: serialized into the text, or attached
5
+ as an image. The **dataset** is just rows with labels, plus the prose that
6
+ tells the model what a row means.
7
+
8
+ The row-rendering helpers at the bottom belong to the representation, not to
9
+ any skill format: every prompt in the project builds its input blocks through
10
+ them, so a new representation changes every prompt at once.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ from .gateway import _png_block, image_block
20
+
21
+
22
+ DEFAULT_TEXT_DESCRIPTION = (
23
+ "Each input is one example rendered as text. Infer the meaning of its "
24
+ "fields from the labeled examples you are shown."
25
+ )
26
+ DEFAULT_IMAGE_DESCRIPTION = (
27
+ "Each input is one image. Infer what distinguishes the classes from the "
28
+ "labeled examples you are shown."
29
+ )
30
+
31
+
32
+ class TextRepresentation:
33
+ """Rows are serialized into the prompt text.
34
+
35
+ ``inline`` picks which half of this interface the prompts consult:
36
+ ``serialize`` for a representation that becomes text, ``blocks`` for one
37
+ that becomes attachments. Never both.
38
+ """
39
+
40
+ name = "text"
41
+ inline = True
42
+ default_description = DEFAULT_TEXT_DESCRIPTION
43
+
44
+ @staticmethod
45
+ def serialize(row: dict) -> str:
46
+ if "text" in row:
47
+ return str(row["text"])
48
+ return "\n".join(
49
+ f"{key}={value}" for key, value in sorted(row.items())
50
+ if key not in {"id", "label", "index", "image"})
51
+
52
+ @staticmethod
53
+ def blocks(rows: list[dict], tag: str, image_size: int) -> list:
54
+ return []
55
+
56
+
57
+ class ImageRepresentation(TextRepresentation):
58
+ """Rows are attached as images, one content block each."""
59
+
60
+ name = "image"
61
+ inline = False
62
+ default_description = DEFAULT_IMAGE_DESCRIPTION
63
+
64
+ @staticmethod
65
+ def blocks(rows: list[dict], tag: str, image_size: int) -> list:
66
+ attached = []
67
+ for number, row in enumerate(rows, 1):
68
+ attached.append({"type": "text", "text": f"{tag} P{number:02d}"})
69
+ attached.append(image_block(row["image"], image_size))
70
+ return attached
71
+
72
+
73
+ REPRESENTATIONS = {"text": TextRepresentation, "image": ImageRepresentation}
74
+
75
+
76
+ # --------------------------------------------------------------------------
77
+
78
+ @dataclass
79
+ class Dataset:
80
+ """Labeled rows plus the little context the model needs to read them.
81
+
82
+ Every row needs a ``label``. Its input is ``text``, or ``image`` (a PIL
83
+ image, an RGB array, or a path), or any other scalar fields.
84
+ """
85
+
86
+ train: list[dict] = field(default_factory=list)
87
+ test: list[dict] = field(default_factory=list)
88
+ classes: tuple[str, ...] = ()
89
+ name: str = "custom"
90
+ title: str = ""
91
+ description: str = ""
92
+ representation: str = "text"
93
+ image_size: int = 224
94
+ modality: str | None = None
95
+
96
+ def __post_init__(self):
97
+ self.train = [dict(row) for row in self.train]
98
+ self.test = [dict(row) for row in self.test]
99
+ if self.modality is not None:
100
+ self.representation = self.modality
101
+ self.modality = self.representation
102
+ if not self.classes:
103
+ self.classes = tuple(sorted(
104
+ {str(row["label"]) for row in self.train + self.test
105
+ if "label" in row}
106
+ ))
107
+ self.classes = tuple(str(label) for label in self.classes)
108
+ if self.representation not in REPRESENTATIONS:
109
+ raise ValueError(
110
+ f"unknown representation {self.representation!r}; choose one "
111
+ f"of {', '.join(sorted(REPRESENTATIONS))}")
112
+ if self.image_size <= 0:
113
+ raise ValueError("image_size must be positive")
114
+ if not self.title:
115
+ self.title = f"{self.name} skill"
116
+ for rows in (self.train, self.test):
117
+ for position, row in enumerate(rows):
118
+ row.setdefault("index", position)
119
+
120
+ def __len__(self):
121
+ return len(self.train)
122
+
123
+ def __repr__(self):
124
+ return (f"Dataset(name={self.name!r}, train={len(self.train)}, "
125
+ f"test={len(self.test)}, classes={len(self.classes)})")
126
+
127
+
128
+ @dataclass
129
+ class Report:
130
+ """The outcome of scoring a frozen skill."""
131
+
132
+ correct: int
133
+ total: int
134
+ rows: list[dict] = field(default_factory=list)
135
+ requests: int = 0
136
+
137
+ @property
138
+ def accuracy(self) -> float:
139
+ return self.correct / self.total if self.total else 0.0
140
+
141
+ def __repr__(self):
142
+ return (f"Report(accuracy={self.accuracy:.1%}, "
143
+ f"{self.correct}/{self.total}, requests={self.requests})")
144
+
145
+
146
+
147
+ def dataset(train, test=(), **options) -> Dataset:
148
+ """Wrap in-memory labeled rows into a :class:`Dataset`."""
149
+ return Dataset(train=list(train), test=list(test), **options)
150
+
151
+
152
+ def from_jsonl(train, test=None, *, limit=None, test_limit=None,
153
+ **options) -> Dataset:
154
+ """Read JSONL rows; relative ``image`` paths resolve from each file."""
155
+ wants_images = (options.get("representation") == "image"
156
+ or options.get("modality") == "image")
157
+
158
+ def read(path, cap):
159
+ path = Path(path)
160
+ if not path.exists():
161
+ raise FileNotFoundError(f"missing {path}")
162
+ with open(path) as handle:
163
+ rows = [json.loads(line) for line in handle]
164
+ rows = rows if cap is None else rows[:cap]
165
+ if wants_images:
166
+ for row in rows:
167
+ image = row.get("image")
168
+ if isinstance(image, str) and not Path(image).is_absolute():
169
+ row["image"] = str((path.parent / image).resolve())
170
+ return rows
171
+
172
+ return Dataset(train=read(train, limit),
173
+ test=read(test, test_limit) if test else [], **options)
174
+
175
+ def describe(data: Dataset) -> str:
176
+ if data.description:
177
+ return data.description
178
+ return REPRESENTATIONS[data.representation].default_description
179
+
180
+
181
+ def labeled(label: str, pngs) -> list[tuple[str, bytes]]:
182
+ """Tag each rendering of one document so the model can tell them apart."""
183
+ return [(label, png) for png in pngs]
184
+
185
+
186
+ def content(prompt: str, rows: list[dict], representation: str, tag: str,
187
+ image_size: int, views=()):
188
+ """Prompt text, then the skill's own renderings, then the input rows.
189
+
190
+ ``views`` holds ``(label, png)`` pairs; each label is numbered separately
191
+ so a two-document gate prompt reads A 01, A 02, B 01 rather than 01..04.
192
+ """
193
+ attached = REPRESENTATIONS[representation].blocks(rows, tag, image_size)
194
+ if not attached and not views:
195
+ return prompt
196
+ blocks = [{"type": "text", "text": prompt}]
197
+ counts: dict[str, int] = {}
198
+ for label, png in views:
199
+ counts[label] = counts.get(label, 0) + 1
200
+ blocks.append({"type": "text",
201
+ "text": f"{label} {counts[label]:02d}"})
202
+ blocks.append(_png_block(png))
203
+ return blocks + attached
204
+
205
+
206
+ def _inputs(rows: list[dict], representation: str, tag: str = "input") -> str:
207
+ if not REPRESENTATIONS[representation].inline:
208
+ return ""
209
+ serialize = REPRESENTATIONS[representation].serialize
210
+ return "\n\n".join(
211
+ f'<{tag} id="P{number:02d}">\n{serialize(row)}\n</{tag}>'
212
+ for number, row in enumerate(rows, 1))
213
+
214
+
215
+ def _revealed(rows: list[dict], predictions: list[dict], representation: str,
216
+ cites_rules: bool = True) -> str:
217
+ """The labeled feedback block: truth, prediction, citations, evidence."""
218
+ def cited(prediction, attribute: bool):
219
+ if not cites_rules:
220
+ return ""
221
+ joined = ",".join(prediction["rules_used"]) or "none"
222
+ return f' rules="{joined}"' if attribute else f"rules={joined}; "
223
+
224
+ if not REPRESENTATIONS[representation].inline:
225
+ return "\n".join(
226
+ f'P{number:02d}: true={row["label"]}; '
227
+ f'predicted={prediction["pred"]}; '
228
+ f'{cited(prediction, False)}'
229
+ f'evidence={prediction["evidence"]}'
230
+ for number, (row, prediction)
231
+ in enumerate(zip(rows, predictions), 1))
232
+ serialize = REPRESENTATIONS[representation].serialize
233
+ return "\n\n".join(
234
+ f'<example id="P{number:02d}" true="{row["label"]}" '
235
+ f'predicted="{prediction["pred"]}"{cited(prediction, True)}>\n'
236
+ f'{serialize(row)}\n'
237
+ f'<evidence>{prediction["evidence"]}</evidence>\n</example>'
238
+ for number, (row, prediction) in enumerate(zip(rows, predictions), 1))
sdm/format.md ADDED
@@ -0,0 +1,84 @@
1
+ ---
2
+ format: markdown
3
+ ---
4
+
5
+ # Skill formats
6
+
7
+ Pick a format by editing the `format:` line above. Each section below defines
8
+ one. The `mechanism:` line says which machinery the harness uses:
9
+
10
+ - `edits` numbered rules revised by one bounded atomic diff
11
+ - `document` one text document replaced outright, or kept with NOOP
12
+ - `signal` a source the model writes but never re-reads, only its rendering
13
+
14
+ Sections using `edits` or `document` are pure configuration, so a new
15
+ text-based format needs nothing but another section here. Safety checks stay
16
+ in Python: `validator:` selects them.
17
+
18
+ ## markdown
19
+
20
+ mechanism: edits
21
+ max_words: 8000
22
+ max_rules: 100
23
+
24
+ Use one operation per line and never put "|" inside a condition. Rules are a
25
+ rulebook, not an example log: no input IDs, no labels copied from individual
26
+ examples, no memorized values. Prefer revising an existing rule over adding a
27
+ near-duplicate. Write only the predicate: do not start it with IF or end it
28
+ with a prediction. IDs and the reliability counters are maintained by the
29
+ harness; do not write them.
30
+
31
+ ## html
32
+
33
+ mechanism: document
34
+ validator: html
35
+ block: skill_html
36
+ max_words: 8000
37
+ max_chars: 30000
38
+
39
+ One standalone static page, ending with `</html>`. No scripts, no frames, no
40
+ forms, no event handler attributes, and no external references of any kind.
41
+ Inline SVG is encouraged where a sketch beats a sentence: small labelled
42
+ prototype drawings per class, a workflow diagram for the reading procedure,
43
+ and contrast pairs for classes that get confused. Track recurring mistakes in
44
+ an error-log section so the same confusion is not repeated.
45
+
46
+ ## image
47
+
48
+ mechanism: document
49
+ validator: svg
50
+ render: svg
51
+ block: skill_svg
52
+ max_words: 2000
53
+ max_chars: 20000
54
+
55
+ One standalone `<svg>` element, ending with `</svg>`. This is a diagram you
56
+ will be shown as a picture on every future prediction, so draw the decision
57
+ procedure rather than writing paragraphs: labelled class prototypes, a
58
+ flowchart of the reading procedure, and contrast pairs for confusable classes.
59
+ Text inside the SVG is fine, but anything that only makes sense as prose
60
+ belongs in a caption. No external references.
61
+
62
+ ## audio
63
+
64
+ mechanism: signal
65
+ signal: audio
66
+
67
+ Write a synthesis score, one event per line:
68
+ TONE <frequency_hz 50-4000> <seconds 0.05-2>
69
+ REST <seconds 0.05-2>
70
+ Lines starting with # are stripped before rendering and will NOT survive.
71
+ At most 400 events and 30s total.
72
+
73
+ ## video
74
+
75
+ mechanism: signal
76
+ signal: video
77
+
78
+ Write a standalone HTML storyboard. Each frame is:
79
+ <section class="frame" data-seconds="N">
80
+ <svg width="640" height="360" viewBox="0 0 640 360"> ... </svg>
81
+ </section>
82
+ with 1 <= N <= 10, at most 12 frames, 60s total. No scripts, no external
83
+ references. Anything you draw or write inside the SVGs survives as pixels;
84
+ nothing outside them does.