open-interpretability 0.0.20__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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
openinterp/concepts.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Labelled concept sets from annotated Hugging Face datasets (`open-interpretability concepts`).
|
|
2
|
+
|
|
3
|
+
A concept set is JSON lines ``{"text": ..., "spans": [[start, end], ...]}``. It is the
|
|
4
|
+
labelled text that ``open-interpretability probe-compare`` reads. The spans are the characters where
|
|
5
|
+
the concept holds, marked by human annotators, never by the model. A concept set feeds
|
|
6
|
+
sparse-probing searches. It also gives reviewers positive holdouts from a source that
|
|
7
|
+
the proposer never saw. A
|
|
8
|
+
``<name>.source.json`` sidecar (:class:`~openinterp.core.probing.ConceptSource`) pins
|
|
9
|
+
the dataset commit, split, label and licence.
|
|
10
|
+
|
|
11
|
+
Two kinds of label are read:
|
|
12
|
+
|
|
13
|
+
* *word tags*: a span over each word with the tag. The tags are CoNLL-2003 entities and
|
|
14
|
+
Penn Treebank tags, and Universal Dependencies parts of speech and relations. A tag
|
|
15
|
+
matches a label
|
|
16
|
+
exactly, without its BIO prefix (``B-PER``, ``I-PER`` -> ``PER``) or without its
|
|
17
|
+
subtype (``nmod:poss`` -> ``nmod``). Sentences are joined back into documents.
|
|
18
|
+
* *document classes* (language, news topic): a span over the whole text of each
|
|
19
|
+
document of the class. The sample is balanced: half the documents are of the class,
|
|
20
|
+
half are of the others.
|
|
21
|
+
|
|
22
|
+
Splits are small (a few MB), so each is read whole, then sampled with a seed.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import random
|
|
30
|
+
import re
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from collections.abc import Sequence
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import pyarrow.parquet as pq
|
|
37
|
+
|
|
38
|
+
from openinterp.core.probing import ConceptSource
|
|
39
|
+
from openinterp.corpus import _license, source_path
|
|
40
|
+
from openinterp.harness.annotations import Span
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class ConceptPreset:
|
|
45
|
+
"""Where a concept preset's labels are, and how its rows become documents."""
|
|
46
|
+
|
|
47
|
+
dataset: str
|
|
48
|
+
files: str # glob within the dataset repo, with {split} and, for treebanks, {subset}
|
|
49
|
+
split: str # the default split: a held-out one, which is small
|
|
50
|
+
column: str # word tags, or one class per row
|
|
51
|
+
# How rows become documents. "conll": pre-tokenised words, joined with spaces. "ud":
|
|
52
|
+
# words found in each sentence's text. "document": one per row, with one class.
|
|
53
|
+
layout: str
|
|
54
|
+
revision: str | None = None
|
|
55
|
+
subset: str | None = None # default treebank, for "ud"
|
|
56
|
+
bio: bool = False # tags carry B-/I- prefixes
|
|
57
|
+
text_column: str = "text"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Licences differ, and some forbid redistribution: check before quoting excerpts in the wiki.
|
|
61
|
+
CONCEPT_PRESETS = {
|
|
62
|
+
# CoNLL-2003 English NER (PER, ORG, LOC, MISC) over Reuters news of 1996. Licence
|
|
63
|
+
# "other": the text is Reuters RCV1, for research use, so don't publish excerpts. The
|
|
64
|
+
# main branch is a loading script, so read the Hub's Parquet conversion.
|
|
65
|
+
"conll2003-ner": ConceptPreset(
|
|
66
|
+
"eriktks/conll2003",
|
|
67
|
+
"conll2003/{split}/*.parquet",
|
|
68
|
+
"validation",
|
|
69
|
+
"ner_tags",
|
|
70
|
+
"conll",
|
|
71
|
+
revision="refs/convert/parquet",
|
|
72
|
+
bio=True,
|
|
73
|
+
),
|
|
74
|
+
# The same sentences' Penn Treebank tags (NNP, VBD, ...): a second part-of-speech
|
|
75
|
+
# source, independent of Universal Dependencies' annotators and genres.
|
|
76
|
+
"conll2003-pos": ConceptPreset(
|
|
77
|
+
"eriktks/conll2003",
|
|
78
|
+
"conll2003/{split}/*.parquet",
|
|
79
|
+
"validation",
|
|
80
|
+
"pos_tags",
|
|
81
|
+
"conll",
|
|
82
|
+
revision="refs/convert/parquet",
|
|
83
|
+
),
|
|
84
|
+
# Universal Dependencies parts of speech (NOUN, VERB, ADP, ...) for any of its ~300
|
|
85
|
+
# treebanks; --subset picks one (en_ewt: English web text; en_gum: eight genres).
|
|
86
|
+
# Licences are per treebank (en_ewt CC BY-SA 4.0, en_gum CC BY-NC-SA 4.0).
|
|
87
|
+
"ud-upos": ConceptPreset(
|
|
88
|
+
"universal-dependencies/universal_dependencies",
|
|
89
|
+
"parquet/{subset}/{split}.parquet",
|
|
90
|
+
"dev",
|
|
91
|
+
"upos",
|
|
92
|
+
"ud",
|
|
93
|
+
subset="en_ewt",
|
|
94
|
+
),
|
|
95
|
+
# The same treebanks' dependency relations (nsubj, obj, amod, ...), for syntax neurons.
|
|
96
|
+
"ud-deprel": ConceptPreset(
|
|
97
|
+
"universal-dependencies/universal_dependencies",
|
|
98
|
+
"parquet/{subset}/{split}.parquet",
|
|
99
|
+
"dev",
|
|
100
|
+
"deprel",
|
|
101
|
+
"ud",
|
|
102
|
+
subset="en_ewt",
|
|
103
|
+
),
|
|
104
|
+
# Sentences and reviews in 20 languages. Their codes are ar, bg, de, el, en, es, fr,
|
|
105
|
+
# hi, it, ja, nl, pl, pt, ru, sw, th, tr, ur, vi and zh. No licence on the card; its
|
|
106
|
+
# sources include Amazon reviews and XNLI (CC BY-NC 4.0). The main branch is CSV.
|
|
107
|
+
"language-identification": ConceptPreset(
|
|
108
|
+
"papluca/language-identification",
|
|
109
|
+
"default/{split}/*.parquet",
|
|
110
|
+
"validation",
|
|
111
|
+
"labels",
|
|
112
|
+
"document",
|
|
113
|
+
revision="refs/convert/parquet",
|
|
114
|
+
),
|
|
115
|
+
# AG News headlines and lead sentences in four topics (World, Sports, Business,
|
|
116
|
+
# Sci/Tech). Licence "unknown": academic, non-commercial use.
|
|
117
|
+
"ag_news": ConceptPreset(
|
|
118
|
+
"fancyzhx/ag_news", "data/{split}-*.parquet", "test", "label", "document"
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Sentence:
|
|
125
|
+
"""One annotated sentence, with a span per tagged word, and where it starts a document."""
|
|
126
|
+
|
|
127
|
+
text: str
|
|
128
|
+
spans: list[Span] # one per tagged word
|
|
129
|
+
newdoc: bool = False
|
|
130
|
+
newpar: bool = True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Pre-tokenised CoNLL text: "lobster ," and "( AP )" are joined as written, "lobster,", "(AP)".
|
|
134
|
+
_ATTACH_LEFT = re.compile(r"[.,;:!?%)\]}]+|n't|'(?:s|re|ve|m|ll|d)", re.IGNORECASE)
|
|
135
|
+
_ATTACH_RIGHT = re.compile(r"[(\[{$]")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def join_words(words: Sequence[str]) -> tuple[str, list[tuple[int, int]]]:
|
|
139
|
+
"""Pre-tokenised words as text, with each word's characters [start, end)."""
|
|
140
|
+
text, offsets = "", []
|
|
141
|
+
for i, word in enumerate(words):
|
|
142
|
+
if i and not (_ATTACH_LEFT.fullmatch(word) or _ATTACH_RIGHT.fullmatch(words[i - 1])):
|
|
143
|
+
text += " "
|
|
144
|
+
offsets.append((len(text), len(text) + len(word)))
|
|
145
|
+
text += word
|
|
146
|
+
return text, offsets
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def locate_words(
|
|
150
|
+
text: str, words: Sequence[str], mwt: Sequence[dict] = ()
|
|
151
|
+
) -> list[tuple[int, int]] | None:
|
|
152
|
+
"""Return each word's characters in its sentence's ``text``.
|
|
153
|
+
|
|
154
|
+
The words of a multiword token (French "du" = "de" + "le") share its surface form's
|
|
155
|
+
characters. ``None`` if a word isn't where it should be, because that would misplace
|
|
156
|
+
every label after it.
|
|
157
|
+
"""
|
|
158
|
+
fused = {}
|
|
159
|
+
for token in mwt or ():
|
|
160
|
+
first, last = token["id"].split("-")
|
|
161
|
+
fused[int(first) - 1] = (int(last) - 1, token["form"])
|
|
162
|
+
out: list[tuple[int, int]] = []
|
|
163
|
+
cursor = i = 0
|
|
164
|
+
while i < len(words):
|
|
165
|
+
last, form = fused.get(i, (i, words[i]))
|
|
166
|
+
start = text.find(form, cursor) if form else -1
|
|
167
|
+
if start < 0 or text[cursor:start].strip():
|
|
168
|
+
return None
|
|
169
|
+
cursor = start + len(form)
|
|
170
|
+
out += [(start, cursor)] * (last - i + 1)
|
|
171
|
+
i = last + 1
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def group_sentences(sentences: Sequence[Sentence], max_chars: int) -> list[tuple[str, list[Span]]]:
|
|
176
|
+
"""Join consecutive sentences into documents of at most ``max_chars``.
|
|
177
|
+
|
|
178
|
+
A new document starts at each new source document, or when the next sentence wouldn't
|
|
179
|
+
fit. Newlines separate paragraphs, and a space separates the sentences in one. A
|
|
180
|
+
sentence longer than ``max_chars`` is cut, and its spans with it.
|
|
181
|
+
"""
|
|
182
|
+
docs: list[tuple[str, list[Span]]] = []
|
|
183
|
+
text, spans = "", []
|
|
184
|
+
for sent in sentences:
|
|
185
|
+
sep = "\n" if sent.newpar else " "
|
|
186
|
+
if text and (sent.newdoc or len(text) + len(sep) + len(sent.text) > max_chars):
|
|
187
|
+
docs.append((text, spans))
|
|
188
|
+
text, spans = "", []
|
|
189
|
+
shift = len(text) + len(sep) if text else 0
|
|
190
|
+
text = (text + sep if text else "") + sent.text
|
|
191
|
+
spans += [Span(s.start + shift, s.end + shift, s.label) for s in sent.spans]
|
|
192
|
+
if len(text) > max_chars:
|
|
193
|
+
text = text[:max_chars]
|
|
194
|
+
spans = [
|
|
195
|
+
Span(s.start, min(s.end, max_chars), s.label) for s in spans if s.start < max_chars
|
|
196
|
+
]
|
|
197
|
+
if text:
|
|
198
|
+
docs.append((text, spans))
|
|
199
|
+
return docs
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def tag_matches(tag: str, label: str) -> bool:
|
|
203
|
+
"""True when ``tag`` is ``label``, or ``label`` with a ``:subtype`` (``nmod:poss``)."""
|
|
204
|
+
return tag == label or tag.split(":")[0] == label
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _tag(tag, names: list[str] | None, bio: bool) -> str | None:
|
|
208
|
+
"""A word's or row's label as text, or ``None`` for none (``O``, ``_``)."""
|
|
209
|
+
if tag is None:
|
|
210
|
+
return None
|
|
211
|
+
tag = names[tag] if names is not None and isinstance(tag, int) else str(tag)
|
|
212
|
+
if tag in ("O", "_", ""):
|
|
213
|
+
return None
|
|
214
|
+
return re.sub(r"^[BI]-", "", tag) if bio else tag
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def class_names(schema, column: str) -> list[str] | None:
|
|
218
|
+
"""Return the names of a Hugging Face ClassLabel column, or a sequence of them.
|
|
219
|
+
|
|
220
|
+
Parquet stores them as integers. The names are in the file's ``huggingface`` schema
|
|
221
|
+
metadata.
|
|
222
|
+
"""
|
|
223
|
+
meta = (schema.metadata or {}).get(b"huggingface")
|
|
224
|
+
if meta is None:
|
|
225
|
+
return None
|
|
226
|
+
feature = json.loads(meta).get("info", {}).get("features", {}).get(column, {})
|
|
227
|
+
feature = feature.get("feature", feature) # Sequence(ClassLabel) holds it inside
|
|
228
|
+
return feature.get("names")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _read_rows(fs, paths: Sequence[str], columns: Sequence[str]) -> tuple[list[dict], list | None]:
|
|
232
|
+
rows, names = [], None
|
|
233
|
+
for path in sorted(paths):
|
|
234
|
+
with fs.open(path, "rb") as fh:
|
|
235
|
+
file = pq.ParquetFile(fh)
|
|
236
|
+
names = names or class_names(file.schema_arrow, columns[-1])
|
|
237
|
+
rows += file.read(columns=list(columns)).to_pylist()
|
|
238
|
+
return rows, names
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _documents(preset: ConceptPreset, column: str, rows, names, max_chars: int):
|
|
242
|
+
"""The split's documents, each with a span per tagged word or class."""
|
|
243
|
+
if preset.layout == "document":
|
|
244
|
+
docs = []
|
|
245
|
+
for row in rows:
|
|
246
|
+
text = (row[preset.text_column] or "").strip()[:max_chars]
|
|
247
|
+
label = _tag(row[column], names, preset.bio)
|
|
248
|
+
if text:
|
|
249
|
+
docs.append((text, [Span(0, len(text), label)] if label else []))
|
|
250
|
+
return docs, 0
|
|
251
|
+
sentences, skipped = [], 0
|
|
252
|
+
for row in rows:
|
|
253
|
+
words = row["tokens"] or []
|
|
254
|
+
if preset.layout == "conll":
|
|
255
|
+
text, offsets = join_words(words)
|
|
256
|
+
newdoc, newpar = False, True
|
|
257
|
+
else:
|
|
258
|
+
text = row[preset.text_column] or ""
|
|
259
|
+
offsets = locate_words(text, words, row.get("mwt"))
|
|
260
|
+
comments = row.get("comments") or []
|
|
261
|
+
newdoc = any(c.startswith("newdoc") for c in comments)
|
|
262
|
+
newpar = newdoc or any(c.startswith("newpar") for c in comments)
|
|
263
|
+
if not words or offsets is None:
|
|
264
|
+
skipped += 1
|
|
265
|
+
continue
|
|
266
|
+
spans = [
|
|
267
|
+
Span(start, end, label)
|
|
268
|
+
for (start, end), tag in zip(offsets, row[column], strict=True)
|
|
269
|
+
if (label := _tag(tag, names, preset.bio)) is not None
|
|
270
|
+
]
|
|
271
|
+
sentences.append(Sentence(text, spans, newdoc, newpar))
|
|
272
|
+
return group_sentences(sentences, max_chars), skipped
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class LoadedSplit:
|
|
277
|
+
"""One split read whole: its documents with spans, and where they came from."""
|
|
278
|
+
|
|
279
|
+
docs: list[tuple[str, list[Span]]]
|
|
280
|
+
dataset: str
|
|
281
|
+
revision: str
|
|
282
|
+
files: str
|
|
283
|
+
license: str | None
|
|
284
|
+
skipped_sentences: int
|
|
285
|
+
|
|
286
|
+
def counts(self) -> Counter:
|
|
287
|
+
"""How many words (or documents) carry each label."""
|
|
288
|
+
return Counter(span.label for _, spans in self.docs for span in spans)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def load_split(
|
|
292
|
+
name: str,
|
|
293
|
+
*,
|
|
294
|
+
split: str | None = None,
|
|
295
|
+
subset: str | None = None,
|
|
296
|
+
column: str | None = None,
|
|
297
|
+
max_chars: int = 3000,
|
|
298
|
+
) -> LoadedSplit:
|
|
299
|
+
"""A preset's split at a pinned commit, as labelled documents."""
|
|
300
|
+
from huggingface_hub import HfApi, HfFileSystem
|
|
301
|
+
|
|
302
|
+
preset = CONCEPT_PRESETS[name]
|
|
303
|
+
column = column or preset.column
|
|
304
|
+
api = HfApi()
|
|
305
|
+
info = api.dataset_info(preset.dataset, revision=preset.revision)
|
|
306
|
+
fs = HfFileSystem()
|
|
307
|
+
subset = subset or preset.subset
|
|
308
|
+
files = preset.files.format(split=split or preset.split, subset=subset)
|
|
309
|
+
paths = fs.glob(f"datasets/{preset.dataset}@{info.sha}/{files}")
|
|
310
|
+
if not paths:
|
|
311
|
+
raise FileNotFoundError(f"no files match {files!r} in {preset.dataset}@{info.sha}")
|
|
312
|
+
if preset.layout == "document":
|
|
313
|
+
columns = [preset.text_column, column]
|
|
314
|
+
elif preset.layout == "conll":
|
|
315
|
+
columns = ["tokens", column]
|
|
316
|
+
else:
|
|
317
|
+
columns = [preset.text_column, "tokens", "mwt", "comments", column]
|
|
318
|
+
rows, names = _read_rows(fs, paths, columns)
|
|
319
|
+
docs, skipped = _documents(preset, column, rows, names, max_chars)
|
|
320
|
+
if preset.layout == "ud":
|
|
321
|
+
# Universal Dependencies licenses each treebank separately.
|
|
322
|
+
meta = json.loads(fs.read_text(f"datasets/{preset.dataset}@{info.sha}/metadata.json"))
|
|
323
|
+
license_ = meta.get(subset, {}).get("license")
|
|
324
|
+
else:
|
|
325
|
+
license_ = _license(api, preset.dataset, info, preset.revision)
|
|
326
|
+
return LoadedSplit(
|
|
327
|
+
docs,
|
|
328
|
+
preset.dataset,
|
|
329
|
+
info.sha,
|
|
330
|
+
files,
|
|
331
|
+
", ".join(license_) if isinstance(license_, list) else license_,
|
|
332
|
+
skipped,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def sample_concept(
|
|
337
|
+
docs: Sequence[tuple[str, list[Span]]], label: str, documents: int, seed: int, balance: bool
|
|
338
|
+
) -> list[tuple[str, list[tuple[int, int]]]]:
|
|
339
|
+
"""Return up to ``documents`` documents with the spans of ``label``, in split order.
|
|
340
|
+
|
|
341
|
+
With ``balance``, half (rounded up) have a span and half don't, where there are enough.
|
|
342
|
+
"""
|
|
343
|
+
marked = [[(s.start, s.end) for s in spans if tag_matches(s.label, label)] for _, spans in docs]
|
|
344
|
+
rng = random.Random(seed)
|
|
345
|
+
if balance:
|
|
346
|
+
pos = [i for i, m in enumerate(marked) if m]
|
|
347
|
+
neg = [i for i, m in enumerate(marked) if not m]
|
|
348
|
+
take_pos = min(len(pos), (documents + 1) // 2)
|
|
349
|
+
picked = rng.sample(pos, take_pos) + rng.sample(neg, min(len(neg), documents - take_pos))
|
|
350
|
+
else:
|
|
351
|
+
picked = rng.sample(range(len(docs)), min(documents, len(docs)))
|
|
352
|
+
return [(docs[i][0], marked[i]) for i in sorted(picked)]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def build_concept(
|
|
356
|
+
name: str,
|
|
357
|
+
label: str,
|
|
358
|
+
out: Path,
|
|
359
|
+
*,
|
|
360
|
+
split: str | None = None,
|
|
361
|
+
subset: str | None = None,
|
|
362
|
+
column: str | None = None,
|
|
363
|
+
documents: int = 1000,
|
|
364
|
+
seed: int = 0,
|
|
365
|
+
max_chars: int = 3000,
|
|
366
|
+
) -> ConceptSource:
|
|
367
|
+
"""Write ``label``'s concept set from preset ``name`` and its source sidecar."""
|
|
368
|
+
preset = CONCEPT_PRESETS[name]
|
|
369
|
+
loaded = load_split(name, split=split, subset=subset, column=column, max_chars=max_chars)
|
|
370
|
+
counts = loaded.counts()
|
|
371
|
+
if not any(tag_matches(tag, label) for tag in counts):
|
|
372
|
+
known = ", ".join(tag for tag, _ in counts.most_common())
|
|
373
|
+
raise ValueError(f"no {label!r} labels in {name}; it has: {known}")
|
|
374
|
+
level = "document" if preset.layout == "document" else "token"
|
|
375
|
+
rows = sample_concept(loaded.docs, label, documents, seed, balance=level == "document")
|
|
376
|
+
data = "".join(
|
|
377
|
+
json.dumps({"text": text, "spans": [list(s) for s in spans]}, ensure_ascii=False) + "\n"
|
|
378
|
+
for text, spans in rows
|
|
379
|
+
).encode()
|
|
380
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
+
out.write_bytes(data)
|
|
382
|
+
source = ConceptSource(
|
|
383
|
+
preset=name,
|
|
384
|
+
dataset=loaded.dataset,
|
|
385
|
+
revision=loaded.revision,
|
|
386
|
+
files=loaded.files,
|
|
387
|
+
column=column or preset.column,
|
|
388
|
+
label=label,
|
|
389
|
+
level=level,
|
|
390
|
+
documents=len(rows),
|
|
391
|
+
positive_documents=sum(1 for _, spans in rows if spans),
|
|
392
|
+
spans=sum(len(spans) for _, spans in rows),
|
|
393
|
+
skipped_sentences=loaded.skipped_sentences,
|
|
394
|
+
seed=seed,
|
|
395
|
+
max_chars=max_chars,
|
|
396
|
+
license=loaded.license,
|
|
397
|
+
sha256=hashlib.sha256(data).hexdigest(),
|
|
398
|
+
)
|
|
399
|
+
source_path(out).write_text(source.model_dump_json(indent=2) + "\n", encoding="utf-8")
|
|
400
|
+
return source
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Schemas, identifiers and statistics that every record and command depends on."""
|
|
2
|
+
|
|
3
|
+
from openinterp.core.exceptions import (
|
|
4
|
+
AgentError,
|
|
5
|
+
ArchitectureNotSupportedError,
|
|
6
|
+
OpenInterpError,
|
|
7
|
+
PatchingError,
|
|
8
|
+
RegistryError,
|
|
9
|
+
ShapeMismatchError,
|
|
10
|
+
SyncError,
|
|
11
|
+
)
|
|
12
|
+
from openinterp.core.schemas import (
|
|
13
|
+
ActivatingExample,
|
|
14
|
+
AdversarialTestResult,
|
|
15
|
+
AuditRecord,
|
|
16
|
+
CausalMetrics,
|
|
17
|
+
ComponentIdentifier,
|
|
18
|
+
CounterfactualPair,
|
|
19
|
+
DictionaryProfile,
|
|
20
|
+
ExperimentProvenance,
|
|
21
|
+
FeatureIdentifier,
|
|
22
|
+
HeadIdentifier,
|
|
23
|
+
HeadProfile,
|
|
24
|
+
Histogram,
|
|
25
|
+
LookaheadProfile,
|
|
26
|
+
NeuronAuditSubmission,
|
|
27
|
+
NeuronIdentifier,
|
|
28
|
+
ReviewMetrics,
|
|
29
|
+
ReviewPolicy,
|
|
30
|
+
VerificationVerdict,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ActivatingExample",
|
|
35
|
+
"AdversarialTestResult",
|
|
36
|
+
"AgentError",
|
|
37
|
+
"ArchitectureNotSupportedError",
|
|
38
|
+
"AuditRecord",
|
|
39
|
+
"CausalMetrics",
|
|
40
|
+
"ComponentIdentifier",
|
|
41
|
+
"CounterfactualPair",
|
|
42
|
+
"DictionaryProfile",
|
|
43
|
+
"ExperimentProvenance",
|
|
44
|
+
"FeatureIdentifier",
|
|
45
|
+
"HeadIdentifier",
|
|
46
|
+
"HeadProfile",
|
|
47
|
+
"Histogram",
|
|
48
|
+
"LookaheadProfile",
|
|
49
|
+
"NeuronAuditSubmission",
|
|
50
|
+
"NeuronIdentifier",
|
|
51
|
+
"OpenInterpError",
|
|
52
|
+
"PatchingError",
|
|
53
|
+
"RegistryError",
|
|
54
|
+
"ReviewMetrics",
|
|
55
|
+
"ReviewPolicy",
|
|
56
|
+
"ShapeMismatchError",
|
|
57
|
+
"SyncError",
|
|
58
|
+
"VerificationVerdict",
|
|
59
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""One name per agent model, however a record spelled it.
|
|
2
|
+
|
|
3
|
+
``submitting_agent_model``, ``reviewer_agent_model`` and ``judge_model`` are free text.
|
|
4
|
+
Sessions spell one model in several ways. For example:
|
|
5
|
+
|
|
6
|
+
- ``external/deepseek-flash[1m]`` and ``external/deepseek-flash``.
|
|
7
|
+
- ``external/gemini/gemini-3.8-flash`` and ``external/gemini-3.8-flash``.
|
|
8
|
+
- ``anthropic/claude-opus-5-5 (Claude Code session)`` and ``external/claude-opus-5-5``.
|
|
9
|
+
|
|
10
|
+
Records are signed, so the site build does not rewrite them. It maps each spelling to one
|
|
11
|
+
name with :func:`canonical_agent`. :func:`spelling_problem` rejects new spellings at
|
|
12
|
+
submission.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
EXTERNAL_PREFIX = "external/"
|
|
20
|
+
|
|
21
|
+
# Spellings that name the same model, after the generic clean-up in canonical_agent.
|
|
22
|
+
# One 2026-10-26 session ran Grok Build but reported itself as Claude 3.5 Sonnet.
|
|
23
|
+
ALIASES: dict[str, str] = {
|
|
24
|
+
"claude-3-5-sonnet-20241022": "grok-build-0.1",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Model families whose sessions report their version inconsistently. canonical_agent maps
|
|
28
|
+
# every name that starts with the key to the value. Muse Spark sessions have recorded
|
|
29
|
+
# muse-spark, muse-spark-3, muse-spark-1-3-contributor and others. "-contributor" is the
|
|
30
|
+
# Muse Spark pricing tier (openinterp.core.pricing.FAMILIES), not part of the model's
|
|
31
|
+
# name. Every run so far was 1.3 at the Contributor tier.
|
|
32
|
+
FAMILY_ALIASES: dict[str, str] = {
|
|
33
|
+
"muse-spark": "muse-spark-1.3",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Shown on an agent's page when its recorded name is doubtful.
|
|
37
|
+
NOTES: dict[str, str] = {
|
|
38
|
+
"grok-build-0.1": (
|
|
39
|
+
"One session on 2026-10-26 ran this model but recorded itself as "
|
|
40
|
+
"claude-3-5-sonnet-20241022; its records are signed, so the name is remapped here."
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def canonical_agent(model: str) -> str:
|
|
46
|
+
"""The model's one name: ``external/deepseek-flash[1m]`` → ``deepseek-flash``.
|
|
47
|
+
|
|
48
|
+
Drops the route (``external/``, a LiteLLM provider such as ``anthropic/``), a context
|
|
49
|
+
suffix (``[1m]``), a parenthesised remark and case. Then applies :data:`FAMILY_ALIASES`
|
|
50
|
+
and :data:`ALIASES`.
|
|
51
|
+
"""
|
|
52
|
+
name = re.sub(r"\s*\(.*\)\s*$", "", model.strip()).removeprefix(EXTERNAL_PREFIX)
|
|
53
|
+
name = re.sub(r"\[[^\]]*\]$", "", name.rsplit("/", 1)[-1]).strip().lower()
|
|
54
|
+
for family, canonical in FAMILY_ALIASES.items():
|
|
55
|
+
if name.startswith(family):
|
|
56
|
+
return canonical
|
|
57
|
+
return ALIASES.get(name, name) or "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def same_agent(a: str | None, b: str | None) -> bool:
|
|
61
|
+
"""True when both names are set and name one model after ``ALIASES`` and spelling fixes."""
|
|
62
|
+
return a is not None and b is not None and canonical_agent(a) == canonical_agent(b)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def spelling_problem(model: str) -> str | None:
|
|
66
|
+
"""Why ``model`` isn't a name a new record may carry, or ``None`` when it is.
|
|
67
|
+
|
|
68
|
+
An external answerer must be ``external/<name>`` with ``<name>`` already canonical. A
|
|
69
|
+
LiteLLM string is what gets called, so it already names the model and passes.
|
|
70
|
+
"""
|
|
71
|
+
if not model.startswith(EXTERNAL_PREFIX):
|
|
72
|
+
return None
|
|
73
|
+
expected = EXTERNAL_PREFIX + canonical_agent(model)
|
|
74
|
+
if model != expected:
|
|
75
|
+
return f"spell it {expected} (no provider, context size, remark or version alias)"
|
|
76
|
+
return None
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Schemas for input attribution: which context tokens drive a pole at its peak.
|
|
2
|
+
|
|
3
|
+
Integrated gradients (Sundararajan et al., 2017) run over the input token embeddings.
|
|
4
|
+
They attribute the pole's activation at each top example's peak token to every token up
|
|
5
|
+
to the peak. The baseline replaces every token with one reference token. A second check
|
|
6
|
+
replaces each token alone with the reference token and measures the pole again.
|
|
7
|
+
Reported, not gated (docs/PROTOCOL.md §4).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from openinterp.core.strict import StrictModel, UnitFloat
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AttributedToken(StrictModel):
|
|
18
|
+
"""One context token's contribution to the pole at the peak.
|
|
19
|
+
|
|
20
|
+
Both numbers are fractions of one gap: the pole's activation on the example minus
|
|
21
|
+
its activation on the all-baseline input. So ``attribution`` sums to about 1 over
|
|
22
|
+
the tokens, and you can compare the two numbers directly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
offset: int = Field(le=0, description="Position relative to the peak token (0 = the peak).")
|
|
26
|
+
token: str
|
|
27
|
+
attribution: float = Field(description="Integrated-gradients share of the gap.")
|
|
28
|
+
ablation_effect: float | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="Drop in activation when only this token is replaced, as a share of the gap.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContextExample(StrictModel):
|
|
35
|
+
"""Input attribution for one top example."""
|
|
36
|
+
|
|
37
|
+
example: int = Field(ge=0, description="Index into the scan's top examples.")
|
|
38
|
+
peak_token: str
|
|
39
|
+
tokens: int = Field(gt=0, description="Tokens attributed: the example up to its peak.")
|
|
40
|
+
steps: int = Field(gt=0, description="Integration steps used.")
|
|
41
|
+
converged: bool = Field(
|
|
42
|
+
description="completeness_error within the tolerance; only these count in the shares."
|
|
43
|
+
)
|
|
44
|
+
completeness_error: float = Field(
|
|
45
|
+
ge=0.0, description="|Σ attribution − 1|: how far the path integral is from converged."
|
|
46
|
+
)
|
|
47
|
+
top: list[AttributedToken] = Field(description="Largest |attribution| first.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DistanceShare(StrictModel):
|
|
51
|
+
"""Share of total |attribution| on tokens ``min_distance``..``max_distance`` back."""
|
|
52
|
+
|
|
53
|
+
min_distance: int = Field(ge=0)
|
|
54
|
+
max_distance: int | None = Field(default=None, description="None: no upper bound.")
|
|
55
|
+
share: UnitFloat
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def label(self) -> str:
|
|
59
|
+
"""The distance band as the page shows it: ``0``, ``1–3`` or ``8+``."""
|
|
60
|
+
if self.max_distance is None:
|
|
61
|
+
return f"{self.min_distance}+"
|
|
62
|
+
if self.max_distance == self.min_distance:
|
|
63
|
+
return str(self.min_distance)
|
|
64
|
+
return f"{self.min_distance}–{self.max_distance}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InputAttribution(StrictModel):
|
|
68
|
+
"""Where in the context a pole's activation comes from, over its top examples.
|
|
69
|
+
|
|
70
|
+
``distances`` splits each example's |attribution| by how far back each token is from
|
|
71
|
+
the peak, then averages the shares over examples. A high share at distance 0 means
|
|
72
|
+
that the pole fires on the current token. A high share far back means that it fires
|
|
73
|
+
because of earlier context.
|
|
74
|
+
|
|
75
|
+
``ablation_agreement`` is Spearman's ρ between attribution and single-token ablation
|
|
76
|
+
effect. It uses every checked token: the peak, the tokens just before it and the most
|
|
77
|
+
attributed ones. A low value means that the linear path integral misreads this pole,
|
|
78
|
+
so trust the ablation effects.
|
|
79
|
+
|
|
80
|
+
Both use only the examples whose path integral converged. With none, they are empty.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
method: str = "integrated_gradients"
|
|
84
|
+
baseline_token: str
|
|
85
|
+
tolerance: float = Field(gt=0.0, description="Largest completeness error accepted.")
|
|
86
|
+
examples: list[ContextExample]
|
|
87
|
+
distances: list[DistanceShare]
|
|
88
|
+
mean_distance: float | None = Field(
|
|
89
|
+
default=None, ge=0.0, description="|attribution|-weighted mean tokens back from the peak."
|
|
90
|
+
)
|
|
91
|
+
ablation_agreement: float | None = Field(default=None, ge=-1.0, le=1.0)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def converged(self) -> int:
|
|
95
|
+
"""How many examples converged; only these count in the shares."""
|
|
96
|
+
return sum(ex.converged for ex in self.examples)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def current_token_share(self) -> float | None:
|
|
100
|
+
"""The share at distance 0, the first band, or ``None`` with no converged example."""
|
|
101
|
+
return self.distances[0].share if self.distances else None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Where the on-disk caches live."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cache_root(env: str, name: str) -> Path | None:
|
|
10
|
+
"""``$<env>``, else ``$XDG_CACHE_HOME/openinterp/<name>``; None if ``$<env>`` turns it off."""
|
|
11
|
+
value = os.environ.get(env)
|
|
12
|
+
if value is not None:
|
|
13
|
+
return None if value.lower() in ("", "0", "off", "false") else Path(value)
|
|
14
|
+
base = os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache"
|
|
15
|
+
return Path(base) / "openinterp" / name
|