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.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
openinterp/syntax.py ADDED
@@ -0,0 +1,399 @@
1
+ """Label the tokens of source code from its syntax tree, for the scan's syntax label sets.
2
+
3
+ A document with a known programming language (a corpus's ``code_language`` field) goes
4
+ through tree-sitter. Tree-sitter tolerates the broken code that a cut at ``max_chars``
5
+ leaves. Every character of the document then has one label in each of these label sets.
6
+
7
+ * ``syntax node``: identifier, keyword, string, comment, number, operator, bracket,
8
+ punctuation, whitespace or other. A string or a comment counts as one node, so its
9
+ contents are not read as code.
10
+ * ``bracket depth``: how many round, square and curly brackets are open. A bracket has
11
+ the depth of what it holds. Angle brackets don't count, because most grammars do not
12
+ tell them from comparison operators. Depths of ``MAX_DEPTH`` or more share one label.
13
+ * ``round depth``: the same count for round brackets only. GPT-2's L2 N676+ follows this
14
+ count, and the count of all brackets hides it (papers/syntax-positive-controls/README.md).
15
+ * ``indent depth``: the indentation of the character's line, in levels. A blank line has
16
+ the level of the line before it.
17
+ * ``syntax construct``: the innermost function, type definition, parameter list, call's
18
+ arguments, import, loop or conditional around the character, or ``top level``.
19
+
20
+ The grammars name their nodes differently. So this module sorts node names into shared
21
+ labels by pattern. The sort is approximate and is not a language's own specification.
22
+ A language with no grammar in :data:`GRAMMARS` has no spans, so its tokens are unlabelled.
23
+ Tree-sitter gives offsets in UTF-8 bytes, and this module converts them to characters.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import importlib
29
+ import re
30
+ from collections import Counter
31
+ from collections.abc import Callable, Sequence
32
+ from dataclasses import dataclass
33
+ from functools import cache
34
+ from importlib.metadata import version
35
+ from itertools import accumulate, groupby
36
+
37
+ from openinterp.core.confounds import (
38
+ BRACKET_DEPTH,
39
+ INDENT_DEPTH,
40
+ ROUND_DEPTH,
41
+ SYNTAX_CONSTRUCT,
42
+ SYNTAX_NODE,
43
+ )
44
+ from openinterp.harness.annotations import Span
45
+
46
+ # The github-code preset's `language` values and their grammars: (package, module function).
47
+ # These are its most frequent programming languages. Markup and build files have no entry.
48
+ GRAMMARS = {
49
+ "C": ("tree-sitter-c", "language"),
50
+ "C#": ("tree-sitter-c-sharp", "language"),
51
+ "C++": ("tree-sitter-cpp", "language"),
52
+ "GO": ("tree-sitter-go", "language"),
53
+ "Haskell": ("tree-sitter-haskell", "language"),
54
+ "Java": ("tree-sitter-java", "language"),
55
+ "JavaScript": ("tree-sitter-javascript", "language"),
56
+ "Lua": ("tree-sitter-lua", "language"),
57
+ "PHP": ("tree-sitter-php", "language_php"),
58
+ "Python": ("tree-sitter-python", "language"),
59
+ "Ruby": ("tree-sitter-ruby", "language"),
60
+ "Rust": ("tree-sitter-rust", "language"),
61
+ "Scala": ("tree-sitter-scala", "language"),
62
+ "Shell": ("tree-sitter-bash", "language"),
63
+ "TypeScript": ("tree-sitter-typescript", "language_typescript"),
64
+ }
65
+
66
+ IDENTIFIER = "identifier"
67
+ KEYWORD = "keyword"
68
+ STRING = "string"
69
+ COMMENT = "comment"
70
+ NUMBER = "number"
71
+ OPERATOR = "operator"
72
+ BRACKET = "bracket"
73
+ PUNCTUATION = "punctuation"
74
+ WHITESPACE = "whitespace"
75
+ OTHER = "other"
76
+
77
+ TOP_LEVEL = "top level"
78
+ MAX_DEPTH = 8 # depths from here share a label: few tokens are deeper
79
+ DEFAULT_INDENT = 4 # the columns of one level, in a document whose lines don't show it
80
+
81
+ CLOSER = {"(": ")", "[": "]", "{": "}"}
82
+ OPENING, CLOSING = set(CLOSER), set(CLOSER.values())
83
+ # What can end a comment or a string that a cut leaves open: a line comment, a block
84
+ # comment, then the quotes. The first entry adds nothing.
85
+ _STARTS = ("", "\n", "*/", '"', "'", "`")
86
+ END_KEYWORD_LANGUAGES = {"Lua", "Ruby"} # a block closes with `end`
87
+ MAX_END_KEYWORDS = 12 # more blocks than a cut document leaves open
88
+ _SEPARATORS = {",", ";", ":", "::", "."}
89
+
90
+ _ATOMS = (
91
+ (COMMENT, re.compile(r"comment")),
92
+ (STRING, re.compile(r"(^|_)(string|char|character|heredoc|regex)(_|$)")),
93
+ )
94
+ _NUMBER = re.compile(r"(^|_)(number|integer|int|float|floating|decimal|hex|octal|binary|real)(_|$)")
95
+ _NAMED_KEYWORD = re.compile(
96
+ r"^(true|false|null|nil|none|this|self|super|primitive_type|predefined_type|void_type"
97
+ r"|boolean_type|boolean_literal|null_literal|integral_type|floating_point_type)$"
98
+ )
99
+ _NAME = re.compile(r"identifier|name|word|variable|constant|symbol|field|type")
100
+ # Innermost wins, because the walk paints a child's characters after its parent's.
101
+ _CONSTRUCTS = (
102
+ ("import", re.compile(r"import|include|using_directive|use_declaration|namespace_use")),
103
+ ("call arguments", re.compile(r"^(argument_list|arguments|actual_parameters)$")),
104
+ ("parameters", re.compile(r"parameter")),
105
+ (
106
+ "function",
107
+ re.compile(
108
+ r"^(function|method|lambda|closure_expression|func_literal|arrow_function"
109
+ r"|generator_function|anonymous_function)"
110
+ r"(_definition|_declaration|_item|_expression|_creation_expression)?$"
111
+ r"|^constructor_declaration$"
112
+ ),
113
+ ),
114
+ (
115
+ "type definition",
116
+ re.compile(
117
+ r"^(class|struct|interface|enum|trait|union|impl|object|module|namespace|type)"
118
+ r"_(definition|declaration|specifier|item)$|^(class|module)$"
119
+ ),
120
+ ),
121
+ (
122
+ "loop",
123
+ re.compile(
124
+ r"^(for|while|do|foreach|enhanced_for|for_in|for_range|repeat|loop|until)"
125
+ r"(_statement|_expression)?$"
126
+ ),
127
+ ),
128
+ ("conditional", re.compile(r"^(if|unless|match|case)(_statement|_expression)?$|switch_")),
129
+ )
130
+
131
+
132
+ def detector() -> str:
133
+ """Name the parser and grammar versions, which fix what the syntax label sets say."""
134
+ packages = ("tree-sitter", *sorted(package for package, _ in GRAMMARS.values()))
135
+ return f"openinterp.syntax ({', '.join(f'{p} {version(p)}' for p in packages)})"
136
+
137
+
138
+ @cache
139
+ def _parser(language: str):
140
+ """Return the parser of ``language``, a key of :data:`GRAMMARS`."""
141
+ import tree_sitter
142
+
143
+ package, function = GRAMMARS[language]
144
+ module = importlib.import_module(package.replace("-", "_"))
145
+ return tree_sitter.Parser(tree_sitter.Language(getattr(module, function)()))
146
+
147
+
148
+ def _char_offsets(text: str) -> list[int]:
149
+ """Return the map from each UTF-8 byte offset of ``text`` to its character offset."""
150
+ if text.isascii():
151
+ return list(range(len(text) + 1))
152
+ widths = (1 if c < "\x80" else 2 if c < "ࠀ" else 3 if c < "\U00010000" else 4 for c in text)
153
+ out = []
154
+ for i, width in enumerate(widths):
155
+ out += [i] * width
156
+ return [*out, len(text)]
157
+
158
+
159
+ def _encode(text: str) -> bytes:
160
+ """UTF-8 of ``text``. A corpus's JSON can hold a lone surrogate, so this keeps it."""
161
+ return text.encode("utf-8", "surrogatepass")
162
+
163
+
164
+ def _node_kind(node_type: str, named: bool, text: str) -> str:
165
+ """Return the ``syntax node`` label of a tree leaf (or of a string or comment)."""
166
+ if not named:
167
+ if text in OPENING or text in CLOSING:
168
+ return BRACKET
169
+ if text in _SEPARATORS:
170
+ return PUNCTUATION
171
+ return KEYWORD if text[:1].isalpha() or text[:1] == "_" else OPERATOR
172
+ for label, pattern in _ATOMS:
173
+ if pattern.search(node_type):
174
+ return label
175
+ if _NUMBER.search(node_type):
176
+ return NUMBER
177
+ if _NAMED_KEYWORD.match(node_type):
178
+ return KEYWORD
179
+ return IDENTIFIER if _NAME.search(node_type) else OTHER
180
+
181
+
182
+ def _atom(node) -> bool:
183
+ """Is this node a string or a comment, whose contents don't count as code?"""
184
+ return node.is_named and any(pattern.search(node.type) for _, pattern in _ATOMS)
185
+
186
+
187
+ def _construct(node) -> str | None:
188
+ """Return the ``syntax construct`` label of a node, or ``None`` if it opens none."""
189
+ if node.is_named:
190
+ for label, pattern in _CONSTRUCTS:
191
+ if pattern.search(node.type):
192
+ return label
193
+ return None
194
+
195
+
196
+ def _paint(text: str, language: str) -> tuple[list[str], list[str]]:
197
+ """Return each character's ``syntax node`` and ``syntax construct`` labels, from one parse."""
198
+ chars = _char_offsets(text)
199
+ nodes: list[str | None] = [None] * len(text)
200
+ constructs = [TOP_LEVEL] * len(text)
201
+ root = _parser(language).parse(_encode(text)).root_node
202
+ todo = [root]
203
+ while todo: # not recursive, because deep code would pass Python's recursion limit
204
+ node = todo.pop()
205
+ start, end = chars[node.start_byte], chars[node.end_byte]
206
+ # The root is the file. Python calls it `module`, which is not a Ruby `module`.
207
+ if node != root and (construct := _construct(node)) is not None:
208
+ constructs[start:end] = [construct] * (end - start)
209
+ if _atom(node) or not node.children:
210
+ kind = _node_kind(node.type, node.is_named, text[start:end])
211
+ nodes[start:end] = [kind] * (end - start)
212
+ else:
213
+ todo.extend(reversed(node.children))
214
+ return [_gap_kind(c) if n is None else n for n, c in zip(nodes, text, strict=True)], constructs
215
+
216
+
217
+ def _open_brackets(text: str, nodes: Sequence[str]) -> str:
218
+ """Return the closing brackets of the brackets still open at the end of ``text``."""
219
+ stack: list[str] = []
220
+ for char, node in zip(text, nodes, strict=True):
221
+ if node == BRACKET and char in OPENING:
222
+ stack.append(CLOSER[char])
223
+ elif node == BRACKET and char in CLOSING and stack:
224
+ stack.pop()
225
+ return "".join(reversed(stack))
226
+
227
+
228
+ def _missing_closers(text: str, nodes: Sequence[str]) -> str:
229
+ """Return what closes the brackets still open at the end of ``text``."""
230
+ # A cut also leaves a statement unfinished, and a block needs its statements to end.
231
+ return _open_brackets(text, nodes).replace("}", ";}")
232
+
233
+
234
+ def _error_chars(text: str, limit: int, language: str) -> int:
235
+ """Count the characters before ``limit`` that the parser puts in an error node.
236
+
237
+ A node that the parser had to make up (a missing token) counts as one character more.
238
+ A parse with no error gives 0.
239
+ """
240
+ root = _parser(language).parse(_encode(text)).root_node
241
+ if not root.has_error:
242
+ return 0
243
+ chars, count, todo = _char_offsets(text), 0, [root]
244
+ while todo:
245
+ node = todo.pop()
246
+ if node.type == "ERROR" or node.is_missing:
247
+ start, end = chars[node.start_byte], chars[node.end_byte]
248
+ count += max(0, min(end, limit) - min(start, limit)) + node.is_missing
249
+ elif node.has_error:
250
+ todo.extend(node.children)
251
+ return count
252
+
253
+
254
+ def _endings(text: str, nodes: Sequence[str], language: str) -> list[str]:
255
+ """Return the texts that can end a cut ``text``, shortest first.
256
+
257
+ A cut can stop inside a comment or a string. So each ending starts with nothing, a line
258
+ break, the end of a block comment or a quote. Then come the closing brackets that are
259
+ open after that start, with and without a ``;`` before each ``}``. In
260
+ :data:`END_KEYWORD_LANGUAGES`, a block closes with ``end`` and not with a bracket. So
261
+ there, endings also add up to ``MAX_END_KEYWORDS`` of them, before or after the brackets.
262
+ """
263
+ out: set[str] = set()
264
+ for start in _STARTS:
265
+ start_nodes = nodes if not start else _paint(text + start, language)[0][: len(text)]
266
+ brackets = _open_brackets(text, start_nodes)
267
+ tails = {brackets, brackets.replace("}", ";}")}
268
+ if language in END_KEYWORD_LANGUAGES:
269
+ for n in range(1, MAX_END_KEYWORDS + 1):
270
+ tails |= {brackets + "\nend" * n, "\nend" * n + brackets}
271
+ out |= {start + tail for tail in tails}
272
+ return sorted(out - {""}, key=lambda ending: (len(ending), ending))
273
+
274
+
275
+ def parse_labels(text: str, language: str) -> tuple[list[str], list[str]]:
276
+ """Return each character's ``syntax node`` and ``syntax construct`` labels.
277
+
278
+ The corpus cuts a document at ``max_chars``, usually inside a class or a function. The
279
+ parser then has no node for that class or function, so its contents read as top level.
280
+ So when the parse has errors, this parses again with an ending added, and labels only
281
+ ``text``. The ending is the one of :func:`_endings` that leaves the fewest characters
282
+ of ``text`` in error nodes, and the shortest of those. If no ending lowers that count,
283
+ the ending closes the open brackets as the first parse reads them.
284
+ """
285
+ nodes, constructs = _paint(text, language)
286
+ errors = _error_chars(text, len(text), language)
287
+ if not errors:
288
+ return nodes, constructs
289
+ scored = (
290
+ (_error_chars(text + e, len(text), language), e) for e in _endings(text, nodes, language)
291
+ )
292
+ fewest, ending = min(scored, key=lambda pair: pair[0], default=(errors, ""))
293
+ if fewest >= errors:
294
+ ending = _missing_closers(text, nodes)
295
+ if not ending:
296
+ return nodes, constructs
297
+ nodes, constructs = _paint(text + ending, language)
298
+ return nodes[: len(text)], constructs[: len(text)]
299
+
300
+
301
+ def _gap_kind(char: str) -> str:
302
+ """Return the ``syntax node`` label of a character that no tree leaf covers."""
303
+ return WHITESPACE if char.isspace() else OTHER
304
+
305
+
306
+ def _depth_label(depth: int) -> str:
307
+ return f"{MAX_DEPTH}+" if depth >= MAX_DEPTH else str(depth)
308
+
309
+
310
+ def bracket_depths(text: str, nodes: Sequence[str], kinds: str = "([{") -> list[str]:
311
+ """Return each character's depth of the brackets that ``kinds`` opens.
312
+
313
+ ``nodes`` holds the characters' ``syntax node`` labels. So a bracket in a string or
314
+ a comment doesn't count. A closing bracket with no opening bracket leaves the depth at 0.
315
+ """
316
+ openers, closers = set(kinds), {CLOSER[k] for k in kinds}
317
+ out, depth = [], 0
318
+ for char, node in zip(text, nodes, strict=True):
319
+ if node == BRACKET and char in openers:
320
+ depth += 1
321
+ out.append(_depth_label(depth))
322
+ if node == BRACKET and char in closers:
323
+ depth = max(0, depth - 1)
324
+ return out
325
+
326
+
327
+ def _indent_unit(widths: Sequence[int]) -> int:
328
+ """Return the columns of one level: the most frequent step between indented lines.
329
+
330
+ Lines' first indents can mislead. A `` * `` comment line has one space, and is not a
331
+ level. So this takes the step that the lines repeat most. ``DEFAULT_INDENT`` if there
332
+ is no step.
333
+ """
334
+ steps = Counter(b - a for a, b in zip(widths, widths[1:], strict=False) if b > a)
335
+ return steps.most_common(1)[0][0] if steps else DEFAULT_INDENT
336
+
337
+
338
+ def indent_depths(text: str) -> list[str]:
339
+ """Return each character's ``indent depth`` label, from its line's leading whitespace."""
340
+ lines = text.splitlines(keepends=True)
341
+ tabbed = [len(line) - len(line.lstrip(" \t")) for line in lines]
342
+ blank = [not line.strip() for line in lines]
343
+ tabs = [line[:n].count("\t") for line, n in zip(lines, tabbed, strict=True)]
344
+ spaces = [n - t for n, t in zip(tabbed, tabs, strict=True)]
345
+ solid = [s for s, b, t in zip(spaces, blank, tabs, strict=True) if not b and not t]
346
+ unit = _indent_unit(solid)
347
+ out, level = [], 0
348
+ for line, is_blank, tab, space in zip(lines, blank, tabs, spaces, strict=True):
349
+ if not is_blank:
350
+ level = tab + space // unit
351
+ out += [_depth_label(level)] * len(line)
352
+ return out
353
+
354
+
355
+ def _runs(labels: Sequence[str]) -> list[Span]:
356
+ """Join neighbouring characters of one label into spans."""
357
+ lengths = [(label, len(list(run))) for label, run in groupby(labels)]
358
+ ends = accumulate(n for _, n in lengths)
359
+ return [Span(end - n, end, label) for (label, n), end in zip(lengths, ends, strict=True)]
360
+
361
+
362
+ @dataclass(frozen=True)
363
+ class SyntaxTagger:
364
+ """Label a document's characters with ``read``, for documents that have a grammar."""
365
+
366
+ name: str
367
+ read: Callable[[int, str], list[Span]]
368
+
369
+ def spans(self, doc: int, text: str) -> list[Span]:
370
+ """The document's spans in this label set. A document without a grammar has none."""
371
+ return self.read(doc, text)
372
+
373
+
374
+ def syntax_taggers(languages: Sequence[str | None]) -> list[SyntaxTagger]:
375
+ """Return one tagger per syntax label set, over documents of these ``languages``.
376
+
377
+ The taggers share one parse of each document. ``languages`` holds one value per
378
+ document, or ``None``. A document with a language outside :data:`GRAMMARS` has no spans.
379
+ """
380
+
381
+ @cache
382
+ def parsed(doc: int, text: str) -> tuple[list[str], list[str]] | None:
383
+ language = languages[doc]
384
+ return parse_labels(text, language) if language in GRAMMARS else None
385
+
386
+ def tagger(name: str, labels: Callable[[str, list[str], list[str]], Sequence[str]]):
387
+ def read(doc: int, text: str) -> list[Span]:
388
+ found = parsed(doc, text)
389
+ return [] if found is None else _runs(labels(text, *found))
390
+
391
+ return SyntaxTagger(name, read)
392
+
393
+ return [
394
+ tagger(SYNTAX_NODE, lambda _, nodes, __: nodes),
395
+ tagger(BRACKET_DEPTH, lambda text, nodes, _: bracket_depths(text, nodes)),
396
+ tagger(ROUND_DEPTH, lambda text, nodes, _: bracket_depths(text, nodes, "(")),
397
+ tagger(INDENT_DEPTH, lambda text, _, __: indent_depths(text)),
398
+ tagger(SYNTAX_CONSTRUCT, lambda _, __, constructs: constructs),
399
+ ]
@@ -0,0 +1,76 @@
1
+ """Part of check 5 of ``open-interpretability check``: the takedown file.
2
+
3
+ The file's format is in :mod:`openinterp.core.takedowns`.
4
+
5
+ The file parses and has unique ids. A key in the allowed signers (from ``base_ref`` if
6
+ given) signed it, so a branch can't add a takedown of its own. With ``base_ref`` the file
7
+ is append-only, and must not be deleted.
8
+
9
+ :func:`page_redaction_ok` is the one exception to write-once experiment pages. A page may
10
+ change outside its Discussion section when the change only removes what the takedowns
11
+ cover.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import subprocess
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ from openinterp.core.takedowns import all_hashes, load_takedowns, parse_takedowns, redact
21
+ from openinterp.core.takedowns import takedowns_path as path_of
22
+ from openinterp.errata_checks import changed_file, check_signature
23
+ from openinterp.signing import allowed_signers_text, signature_path
24
+
25
+ if TYPE_CHECKING:
26
+ from openinterp.checks import CheckReport
27
+
28
+ WHAT = "takedown file"
29
+
30
+
31
+ def check_takedowns(
32
+ wiki_dir: Path,
33
+ report: CheckReport,
34
+ base_ref: str | None,
35
+ changes: list[tuple[str, Path]],
36
+ allowed_signers: Path,
37
+ ) -> None:
38
+ """Add a problem to ``report`` for each way the takedown file is not valid."""
39
+ path = path_of(wiki_dir)
40
+ if any(p == path and status == "D" for status, p in changes):
41
+ report.add(path, f"the {WHAT} is append-only and must not be deleted")
42
+ if not path.exists():
43
+ if signature_path(path).exists():
44
+ report.add(signature_path(path), f"signature of a {WHAT} that does not exist")
45
+ return
46
+ text = path.read_text(encoding="utf-8")
47
+ try:
48
+ takedowns = parse_takedowns(text)
49
+ except ValueError as exc:
50
+ report.add(path, f"invalid takedown: {exc}")
51
+ return
52
+ ids = [t.id for t in takedowns]
53
+ for dup in sorted({i for i in ids if ids.count(i) > 1}):
54
+ report.add(path, f"takedown id {dup!r} is used more than once")
55
+ changed = changed_file(path, base_ref, changes)
56
+ if base_ref and changed:
57
+ shown = subprocess.run(
58
+ ["git", "show", f"{base_ref}:{path.as_posix()}"], capture_output=True, text=True
59
+ )
60
+ if shown.returncode == 0 and not text.startswith(shown.stdout):
61
+ report.add(path, f"the {WHAT} is append-only: add lines at the end, change none")
62
+ signers = allowed_signers_text(allowed_signers, base_ref)
63
+ check_signature(path, report, signers, changed, WHAT)
64
+
65
+
66
+ def page_redaction_ok(before: str, after: str, wiki_dir: Path) -> bool:
67
+ """Whether ``after`` is ``before`` with exactly the text that the takedowns cover removed.
68
+
69
+ Both are a page's text before its Discussion section. The check that the takedown
70
+ file is signed is :func:`check_takedowns`, which runs in the same
71
+ ``open-interpretability check``.
72
+ """
73
+ from openinterp.builder.pii import REMOVED_TEXT
74
+
75
+ hashes = all_hashes(load_takedowns(wiki_dir))
76
+ return before != after and redact(before, hashes, REMOVED_TEXT) == after
openinterp/toxicity.py ADDED
@@ -0,0 +1,158 @@
1
+ """Find toxic text and hate speech in a document, for the scan's label sets.
2
+
3
+ Both labels are models' guesses, for experiments only. Nothing removes text because of
4
+ them. The site's spoiler guard uses a word list (``sensitive.is_offensive``), so that a
5
+ site build needs no model.
6
+
7
+ A model reads text, not tokens. So each sentence is read alone, and all of its tokens take
8
+ the sentence's label. A label therefore says where toxic text is, not which word made it so.
9
+
10
+ The two models do different work. We compared them on HateCheck (Röttger et al., 2021),
11
+ which neither trained on, at a score of at least 0.5:
12
+
13
+ - ``unitary/unbiased-toxic-roberta`` (Apache 2.0) gives ``toxicity``. It alone finds
14
+ obscenity, insults, threats and sexual text. Its identity outputs (``female``, ``black``)
15
+ say that a text mentions a group, so we leave them out. Built from them, a target
16
+ category found 0% to 76% of hateful cases, and 0% for immigrants, trans people and
17
+ disabled people.
18
+ - ``cardiffnlp/twitter-roberta-base-hate-multiclass-latest`` gives ``hate speech``. It
19
+ found 40% of hateful cases, and called 9% of non-hateful cases hate. It names the right
20
+ target in 13% to 62% of a group's cases, but in 1% for disabled people. Its model card
21
+ states no licence. It trained on tweets. We leave out its ``other`` output, because that
22
+ output called lines of Ruby code hate.
23
+
24
+ Both models are English models. Scoring runs on the CPU, at about 10 ms a sentence for each
25
+ model. Each document's result stays on disk. The models (about 500 MB each) are downloaded
26
+ once, pinned to a commit.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ import json
33
+ import os
34
+ import re
35
+ from collections.abc import Sequence
36
+ from dataclasses import dataclass
37
+ from functools import cache
38
+ from pathlib import Path
39
+
40
+ THRESHOLD = 0.5 # the score at which a sentence takes a label
41
+ _BATCH = 16
42
+ _MAX_CHARS = 1000 # a longer "sentence" (code, a table) is read in pieces of this length
43
+ _MAX_TOKENS = 512 # what the models accept
44
+ # On our corpus, the models called one-word lines hate or toxic ("Fluker.", "F.eks.").
45
+ _MIN_WORDS = 2
46
+ # Up to a stop followed by a space, else up to the end of the line.
47
+ _SENTENCE = re.compile(r"\S(?:[^\n]*?[.!?](?=\s)|[^\n]*)")
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class _Classifier:
52
+ repo: str
53
+ revision: str # a commit, so a later scan reads the same weights
54
+ name: str # part of the cache's path
55
+ labels: tuple[str, ...] # the outputs that can label a sentence, most specific first
56
+ softmax: bool = False # the outputs compete; else each output is its own yes or no
57
+ fallback: str | None = None # the label of a sentence with no other label
58
+
59
+
60
+ _TOXICITY = _Classifier(
61
+ "unitary/unbiased-toxic-roberta",
62
+ "36295dd80b422dc49f40052021430dae76241adc",
63
+ "toxicity",
64
+ ("threat", "identity_attack", "sexual_explicit", "obscene", "insult"),
65
+ fallback="toxicity",
66
+ )
67
+ _HATE = _Classifier(
68
+ "cardiffnlp/twitter-roberta-base-hate-multiclass-latest",
69
+ "b9a303f920f8527ac4151e65953c04505fdf0587",
70
+ "hate",
71
+ ("racism", "sexism", "religion", "sexual_orientation", "disability"),
72
+ softmax=True,
73
+ )
74
+ DETECTOR = "; ".join(f"{c.repo}@{c.revision[:12]}" for c in (_TOXICITY, _HATE))
75
+ DETECTOR = f"{DETECTOR} (score at least {THRESHOLD}, per sentence of {_MIN_WORDS}+ words)"
76
+
77
+
78
+ def sentences(text: str) -> list[tuple[int, int]]:
79
+ """Return the ``(start, end)`` of each sentence in ``text``, split to ``_MAX_CHARS``."""
80
+ return [
81
+ (start, min(start + _MAX_CHARS, match.end()))
82
+ for match in _SENTENCE.finditer(text)
83
+ for start in range(match.start(), match.end(), _MAX_CHARS)
84
+ ]
85
+
86
+
87
+ def _label(classifier: _Classifier, scores: dict[str, float]) -> str | None:
88
+ """The strongest of a sentence's labels, else the fallback when its score passes."""
89
+ passed = [label for label in classifier.labels if scores[label] >= THRESHOLD]
90
+ if passed:
91
+ return max(passed, key=scores.__getitem__)
92
+ fallback = classifier.fallback
93
+ return fallback if fallback and scores[fallback] >= THRESHOLD else None
94
+
95
+
96
+ @cache
97
+ def _model(classifier: _Classifier):
98
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
99
+
100
+ tokenizer = AutoTokenizer.from_pretrained(classifier.repo, revision=classifier.revision)
101
+ model = AutoModelForSequenceClassification.from_pretrained(
102
+ classifier.repo, revision=classifier.revision
103
+ )
104
+ return tokenizer, model.eval()
105
+
106
+
107
+ def _score(classifier: _Classifier, texts: Sequence[str]) -> list[dict[str, float]]:
108
+ """Return each text's score per output of the model."""
109
+ import torch
110
+
111
+ tokenizer, model = _model(classifier)
112
+ outputs = [model.config.id2label[i] for i in range(model.config.num_labels)]
113
+ found: list[dict[str, float]] = []
114
+ for first in range(0, len(texts), _BATCH):
115
+ batch = tokenizer(
116
+ list(texts[first : first + _BATCH]),
117
+ truncation=True,
118
+ max_length=_MAX_TOKENS,
119
+ padding=True,
120
+ return_tensors="pt",
121
+ )
122
+ with torch.no_grad():
123
+ logits = model(**batch).logits
124
+ scores = logits.softmax(-1) if classifier.softmax else logits.sigmoid()
125
+ found += [dict(zip(outputs, row, strict=True)) for row in scores.tolist()]
126
+ return found
127
+
128
+
129
+ def _cache_dir() -> Path:
130
+ return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "openinterp"
131
+
132
+
133
+ def _matches(classifier: _Classifier, text: str) -> tuple[tuple[int, int, str], ...]:
134
+ """Return ``(start, end, label)`` of each labelled sentence, kept on disk by the text."""
135
+ key = hashlib.sha256(text.encode()).hexdigest()
136
+ version = f"{classifier.revision[:12]}-{THRESHOLD}-{_MIN_WORDS}"
137
+ path = _cache_dir() / "toxicity" / classifier.name / version / f"{key}.json"
138
+ if path.exists():
139
+ return tuple((start, end, label) for start, end, label in json.loads(path.read_text()))
140
+ spans = [(a, b) for a, b in sentences(text) if len(text[a:b].split()) >= _MIN_WORDS]
141
+ scores = _score(classifier, [text[start:end] for start, end in spans]) if spans else []
142
+ labels = [_label(classifier, s) for s in scores]
143
+ found = [(*span, label) for span, label in zip(spans, labels, strict=True) if label]
144
+ path.parent.mkdir(parents=True, exist_ok=True)
145
+ partial = path.with_suffix(f".{os.getpid()}.part") # scans can run side by side
146
+ partial.write_text(json.dumps(found))
147
+ partial.replace(path)
148
+ return tuple(found)
149
+
150
+
151
+ def toxicity_matches(text: str) -> tuple[tuple[int, int, str], ...]:
152
+ """Return ``(start, end, label)`` of each toxic sentence in ``text``, sorted."""
153
+ return _matches(_TOXICITY, text)
154
+
155
+
156
+ def hate_matches(text: str) -> tuple[tuple[int, int, str], ...]:
157
+ """Return ``(start, end, label)`` of each sentence of hate speech in ``text``, sorted."""
158
+ return _matches(_HATE, text)