contextattr 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ """ToyModel: a compatible wrapper with known answers and no real model behind it, for tests and a first try."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+ import random
8
+ import re
9
+ from collections.abc import Mapping, Sequence
10
+
11
+ from .contract import Rendered, Sampling, Scored
12
+
13
+ _TOKEN = re.compile(r"\s*\S+|\s+$") # each word with the spaces before it; trailing spaces as a last token
14
+ _EDGE = re.compile(r"^\W+|\W+$")
15
+
16
+
17
+ class ToyModel:
18
+ """A stand-in model whose answers are known exactly.
19
+
20
+ A response is cut into words, each with the spaces before it. Word w gets the log-probability
21
+ log σ(bias + Σ rules[cue][w] + Σ pairs[(cue1, cue2)][w]), summed over the cues present in the prompt, and
22
+ over the pairs whose two cues are both present. A cue is present when its text occurs in the prompt, case
23
+ ignored (a chat prompt's message contents are joined with newlines); a word is matched lower-cased, without
24
+ punctuation around it.
25
+
26
+ `generate` at temperature 0 gives the reply of the first cue in `replies` that is present, else `default_reply`,
27
+ sampling.n times. Above temperature 0 each reply is drawn uniformly from the replies of all the cues present (the
28
+ default when none is), so its rate is known exactly; the draw is seeded by the seed and the prompt's text, and
29
+ seed None draws afresh."""
30
+
31
+ def __init__(self, rules: Mapping[str, Mapping[str, float]] | None = None, *,
32
+ pairs: Mapping[tuple[str, str], Mapping[str, float]] | None = None, bias: float = 0.0,
33
+ replies: Mapping[str, str] | None = None, default_reply: str = "I do not know.",
34
+ name: str = "toy"):
35
+ self.rules = {_cue(c): _weights(w, c) for c, w in (rules or {}).items()}
36
+ self.pairs = {}
37
+ for key, w in (pairs or {}).items():
38
+ if not (isinstance(key, tuple) and len(key) == 2):
39
+ raise TypeError(f"ToyModel: a pair is a tuple of two cues, not {key!r}")
40
+ self.pairs[(_cue(key[0]), _cue(key[1]))] = _weights(w, key)
41
+ if isinstance(bias, bool) or not isinstance(bias, (int, float)) or not math.isfinite(bias):
42
+ raise TypeError("ToyModel: bias must be a finite number")
43
+ self.bias = float(bias)
44
+ self.replies = {_cue(c): r for c, r in (replies or {}).items()}
45
+ if not all(isinstance(r, str) for r in self.replies.values()) or not isinstance(default_reply, str):
46
+ raise TypeError("ToyModel: replies must be strings")
47
+ self.default_reply = default_reply
48
+ self.name = name
49
+
50
+ @classmethod
51
+ def demo(cls) -> ToyModel:
52
+ """A toy model of the interface's Rio example: Brazil's winter makes "cold" likely, its summer "hot" and
53
+ "humid", more so when the prompt also says Carnival falls in February or March; it replies "cold" when it
54
+ reads only of the winter."""
55
+ return cls(rules={"winter in brazil": {"cold": 2.0, "winter": 2.0},
56
+ "summer in brazil": {"hot": 2.0, "humid": 1.0, "summer": 2.0}},
57
+ pairs={("february or march", "summer in brazil"): {"hot": 1.5, "summer": 1.5}},
58
+ bias=-1.0,
59
+ replies={"summer in brazil": "It is hot and humid: it is summer in Brazil.",
60
+ "winter in brazil": "It is cold, because Carnival is in winter."},
61
+ name="toy-demo")
62
+
63
+ def score(self, prompts: Sequence[Rendered], responses: Sequence[str]) -> list[Scored]:
64
+ if len(prompts) != len(responses):
65
+ raise ValueError(f"ToyModel.score: {len(prompts)} prompts but {len(responses)} responses")
66
+ out = []
67
+ for prompt, response in zip(prompts, responses):
68
+ if not isinstance(response, str):
69
+ raise TypeError(f"ToyModel.score: a response is a string, not {type(response).__name__}")
70
+ text = _text(prompt)
71
+ tokens = _TOKEN.findall(response)
72
+ out.append(Scored(tuple(self._logprob(text, t) for t in tokens), tuple(tokens)))
73
+ return out
74
+
75
+ def generate(self, prompts: Sequence[Rendered], sampling: Sampling) -> list[list[str]]:
76
+ out = []
77
+ for prompt in prompts:
78
+ text = _text(prompt)
79
+ present = [r for c, r in self.replies.items() if c in text] or [self.default_reply]
80
+ if sampling.temperature == 0 or len(present) == 1:
81
+ out.append([present[0]] * sampling.n)
82
+ continue
83
+ rng = random.Random(None if sampling.seed is None else _seed(text, sampling.seed))
84
+ out.append([rng.choice(present) for _ in range(sampling.n)])
85
+ return out
86
+
87
+ def _logprob(self, text: str, token: str) -> float:
88
+ word = _EDGE.sub("", token.strip()).lower()
89
+ x = self.bias
90
+ x += sum(w.get(word, 0.0) for c, w in self.rules.items() if c in text)
91
+ x += sum(w.get(word, 0.0) for (c1, c2), w in self.pairs.items() if c1 in text and c2 in text)
92
+ return log_sigmoid(x)
93
+
94
+
95
+ def _seed(text: str, seed: int) -> int:
96
+ return int.from_bytes(hashlib.sha256(f"{seed}\0{text}".encode()).digest()[:8], "big")
97
+
98
+
99
+ def log_sigmoid(x: float) -> float:
100
+ """log σ(x) = log(1 / (1 + e^-x)), without overflow."""
101
+ return min(x, 0.0) - math.log1p(math.exp(-abs(x)))
102
+
103
+
104
+ def _text(prompt: Rendered) -> str:
105
+ """The prompt's text, lower-cased; a chat prompt's contents joined with newlines. Checks the rendered shape."""
106
+ if isinstance(prompt, str):
107
+ return prompt.lower()
108
+ if not isinstance(prompt, list) or not all(isinstance(m, dict) and set(m) == {"role", "content"} and
109
+ isinstance(m["role"], str) and isinstance(m["content"], str)
110
+ for m in prompt):
111
+ raise TypeError("ToyModel: a prompt is a string or a list of {'role', 'content'} string dicts")
112
+ return "\n".join(m["content"] for m in prompt).lower()
113
+
114
+
115
+ def _cue(cue) -> str:
116
+ if not isinstance(cue, str) or not cue:
117
+ raise TypeError(f"ToyModel: a cue is a non-empty string, not {cue!r}")
118
+ return cue.lower()
119
+
120
+
121
+ def _weights(w, cue) -> dict[str, float]:
122
+ if not isinstance(w, Mapping):
123
+ raise TypeError(f"ToyModel: the weights of {cue!r} are a mapping word → number")
124
+ out = {}
125
+ for word, x in w.items():
126
+ if not isinstance(word, str) or isinstance(x, bool) or not isinstance(x, (int, float)) or not math.isfinite(x):
127
+ raise TypeError(f"ToyModel: weight {word!r}: {x!r} of {cue!r} must be a word and a finite number")
128
+ out[word.lower()] = float(x)
129
+ return out
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: contextattr
3
+ Version: 0.1.0
4
+ Summary: Context attribution: which parts of a prompt make a model give an output, for a model hosted anywhere
5
+ Author: Surya Rayala
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/Surya-Rayala/contextattr
8
+ Project-URL: Documentation, https://github.com/Surya-Rayala/contextattr/tree/main/docs
9
+ Project-URL: Issues, https://github.com/Surya-Rayala/contextattr/issues
10
+ Keywords: context attribution,explainability,interpretability,LLM,feature interactions,Shapley values,SPEX
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: scikit-learn>=1.9
26
+ Provides-Extra: interactions
27
+ Requires-Dist: shapiq[sparse]<1.9,>=1.8; extra == "interactions"
28
+ Requires-Dist: sparse-transform<0.3,>=0.2.1; extra == "interactions"
29
+ Requires-Dist: lightgbm>=4; extra == "interactions"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Requires-Dist: hypothesis>=6.100; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ # ContextAttr
36
+
37
+ Find out which parts of a prompt make a language model give an answer.
38
+
39
+ You split a prompt into parts: sentences, documents, words, or any pieces you choose. ContextAttr asks your model about many copies of the prompt with some parts removed. It then tells you how much each part, and each small group of parts working together, pushes the model towards an answer, and how far to trust that result. It works with any model, wherever it runs: you connect yours with a short wrapper.
40
+
41
+ Version 0.1.0 is an early release: the interface may still change.
42
+
43
+ ## Install
44
+
45
+ ```
46
+ pip install contextattr
47
+ ```
48
+
49
+ Add `[interactions]` (`pip install "contextattr[interactions]"`) for SPEX and the other methods that find parts which matter only together. With uv: `uv add contextattr`. Needs Python 3.12 or later.
50
+
51
+ ## Try it
52
+
53
+ This runs anywhere in seconds: `ToyModel` stands in for a real model.
54
+
55
+ ```python
56
+ import contextattr as ca
57
+
58
+ prompt = ca.Prompt("rio", messages=[{"role": "user", "content": [
59
+ ca.Unit("d1", "Rio Carnival takes place in February or March.", sep="\n"),
60
+ ca.Unit("d2", "Summer in Brazil runs from December to March.", sep="\n"),
61
+ ca.Unit("d3", "Winter in Brazil runs from June to September.", sep="\n"),
62
+ "Question: What is the weather like during Rio Carnival?"]}],
63
+ responses=[ca.Response("bad", "It is cold, because Carnival is in winter.", label="negative"),
64
+ ca.Response("good", "It is hot and humid: it is summer in Brazil.", label="positive")])
65
+
66
+ model = ca.ToyModel.demo() # a stand-in: put your own model's wrapper here
67
+ e = ca.explain(prompt, model, ca.Contrast(), method="kernelshap", budget=8)
68
+ print(e)
69
+ ```
70
+
71
+ It prints:
72
+
73
+ ```
74
+ rio · KernelSHAP(paired=True) · 8 masks · R² 0.991 on 128 test masks · noise 0 · 22 requests
75
+ d3 +0.2500 Winter in Brazil runs from June to September.
76
+ d1 -0.0234 Rio Carnival takes place in February or March.
77
+ d2 -0.2854 Summer in Brazil runs from December to March.
78
+ ```
79
+
80
+ **What the example says:**
81
+
82
+ | In the code | Means |
83
+ |---|---|
84
+ | `ca` | the package, under the short name all the docs use |
85
+ | `ca.Prompt("rio", messages=...)` | one prompt, named `rio`, as chat messages |
86
+ | `ca.Unit("d1", "...", sep="\n")` | a part that may be removed, named `d1`. `sep` is text that goes with it, here the line break. Everything else, like the question, always stays |
87
+ | `ca.Response("bad", "...", label="negative")` | an answer the model could give. `negative` marks the answer you do not want, `positive` the one you do |
88
+ | `model` | the wrapper around your model |
89
+ | `ca.Contrast()` | what to explain: how much more likely the model finds the unwanted answers than the wanted ones |
90
+ | `method="kernelshap"`, `budget=8` | how the scores are worked out, and how many copies of the prompt, each with some parts removed, the model is asked about |
91
+ | `e` | the result |
92
+
93
+ **How to read the output:**
94
+
95
+ - The first line names the prompt and the method and says how far to trust the result. R² close to 1 means the scores describe the model well; close to 0, do not use them.
96
+ - Then each part with its score, highest first. A positive score pushes the model towards the unwanted answer: `d3`, the sentence about winter, is what makes this model say "cold". A negative score pushes it away: `d2` protects against it. A score near zero means the part hardly matters.
97
+ - `e.scores` gives the scores as a dictionary, and `e.to_html("report.html")` writes a page with each part shaded by its score.
98
+
99
+ [Reading a result](https://github.com/Surya-Rayala/contextattr/blob/main/docs/results.md) shows the output of every method on this example and what its scores mean.
100
+
101
+ ## Use it with your own model
102
+
103
+ ContextAttr reaches your model through a small class that you write, a *wrapper*. [Writing a wrapper](https://github.com/Surya-Rayala/contextattr/blob/main/docs/wrapper.md) is the guide, with two complete wrappers to copy from: one for an API and one for a model you run yourself.
104
+
105
+ 1. **Get the file to fill in.** `contextattr template my_wrapper.py` writes it. It needs one or both of:
106
+ - `generate`: writes replies to a prompt. Every model can do this, including the OpenAI, Gemini and Anthropic APIs.
107
+ - `score`: gives the log-probability of each token of a given answer. Possible when you run the model yourself, in your own code or on a vLLM or SGLang server.
108
+ 2. **Check it.** `python my_wrapper.py` runs `ca.check_model` on it and says what, if anything, is wrong.
109
+ 3. **Use it.** Put your wrapper where the example has `model`.
110
+
111
+ ## Choose what to explain, and how
112
+
113
+ - **Parts:** mark them with `ca.Unit`, as above, or cut a text automatically: `ca.split(text, "sentence", id="s")` (or `"word"`, `"line"`, `"paragraph"`).
114
+ - **Answer:** `ca.LogProb()` explains one answer. `ca.Contrast()` explains what moves the model from the answers you want to the ones you don't. `ca.Sampled(check)` works when your model can only generate text.
115
+ - **Method:** `contextcite`, the default, takes a quick and cheap look, even at long prompts. `spex` finds parts that matter only together. `budget` sets how many copies of the prompt the model is asked about: more is steadier and costs more.
116
+ - **Many prompts:** `contextattr run run.toml` explains a whole file of prompts and writes a report.
117
+
118
+ ## Learn more
119
+
120
+ | Document | Covers |
121
+ |---|---|
122
+ | [docs/wrapper.md](https://github.com/Surya-Rayala/contextattr/blob/main/docs/wrapper.md) | connecting your model: what to write, two complete wrappers, the rules, and what to do when the check fails |
123
+ | [docs/results.md](https://github.com/Surya-Rayala/contextattr/blob/main/docs/results.md) | reading a result: the output of every method on one example, what its scores mean, and how far to trust them |
124
+ | [docs/methods.md](https://github.com/Surya-Rayala/contextattr/blob/main/docs/methods.md) | which method to use, how many masks to give it, and every setting with its default |
125
+ | [docs/interface.md](https://github.com/Surya-Rayala/contextattr/blob/main/docs/interface.md) | the reference: prompts and the prompt file, answers and labels, what can be explained, costs, runs and the command line |
126
+
127
+ ## Licence
128
+
129
+ ContextAttr is released under the MIT licence: see [LICENSE](https://github.com/Surya-Rayala/contextattr/blob/main/LICENSE).
@@ -0,0 +1,38 @@
1
+ contextattr/__init__.py,sha256=4nyU5K4kR14X4qybjSQYifex2DykprwCseegxdC7xGs,1247
2
+ contextattr/__main__.py,sha256=csk-ut-m00mlCsYR1xRESKPb4qfCxaWwn87vxJzrhvU,109
3
+ contextattr/cache.py,sha256=MGoAyvplajOjZMepLZesGc9Uig8G7An2LyV7W4jp0V8,2752
4
+ contextattr/cli.py,sha256=MDV-L3zJjnR4piZazuJ5CrnKaDo71TbxoL6t-3Wcn4k,8590
5
+ contextattr/evaluate.py,sha256=iPIkWkc45ZnnTpuIGA49GrRKCKkjp302SZSTwmSZd30,7103
6
+ contextattr/report.py,sha256=pUDAlxLjP1omdHCagh4PkeLTUTQpQN_JKXqdatPO-rA,5682
7
+ contextattr/result.py,sha256=ZgxpYoeYzRxL8Nxrqhz5slJKHYoILpR8BfPNBxreYuo,5947
8
+ contextattr/run.py,sha256=6iSypAPfC0n4Bo_QQQXuOg8ZiVZLIkt-keOffKUAEi4,17517
9
+ contextattr/value.py,sha256=K11gSSACqUTfboanHo4ZhImUwtfVkbfqxvoBr-x_2uc,11068
10
+ contextattr/methods/__init__.py,sha256=QEeQvEbAdLZrnzQfQpU1vRxY0zxoDn0Cev2BIkiZLJY,1336
11
+ contextattr/methods/base.py,sha256=xvhk-rR7_-S6HGvW9cLhZuu4B1GVQQ8HAeAbbelocBM,3587
12
+ contextattr/methods/indices.py,sha256=-KaXDhqb5Io1f0g-w5SDzDXYvv8kR8n8zIZIR-7EUdQ,1986
13
+ contextattr/methods/interactions.py,sha256=4wUDWCpVw-LHAN3t7Bl7exq03htATEWceM_xHuDH5tc,11284
14
+ contextattr/methods/kernelshap.py,sha256=56MUsb14hAIPnsGMdxBt2Ta-9yrv4iSZ4d_qPT5Cmdo,5830
15
+ contextattr/methods/linear.py,sha256=5iQnHESFy2yeLy8IA_68Llz11PZdShshAbsNhifw5TY,5195
16
+ contextattr/methods/loo.py,sha256=R3blTynnnImRq_OtIFSvwQhL3EsFZi58ydveTQnrRQo,1344
17
+ contextattr/prompt/__init__.py,sha256=P7j6cR5CEpRpVHBrwD0IYT0gNZjZMf5koLAZ3VGP8MI,517
18
+ contextattr/prompt/io.py,sha256=1-VcVpzyTp_rAzv3M81aT1maynEFdiY5GxWhUxpXgfc,8875
19
+ contextattr/prompt/render.py,sha256=yzeodc5-_d_A55DDoo6woCTmO-uLaBBF9B22OxI30rg,2964
20
+ contextattr/prompt/split.py,sha256=wEZFORXpzJgHsL-gp0JrGOqbb0yRbFjW7ZI5TwWu8SM,6892
21
+ contextattr/prompt/units.py,sha256=jvBOt7AXnSSieC7GpnluGSmPSCihE7fHvcIYeTwB6DM,11209
22
+ contextattr/responses/__init__.py,sha256=wr1dp5MiGSEXzSnjIF6FwXwFo1sOupSQVMjFfDoiEHo,313
23
+ contextattr/responses/checks.py,sha256=oXHbYZ82M_bdlHXYmEWCmqAUqc-hLibG-yeOFEPb_9k,7024
24
+ contextattr/responses/sample.py,sha256=ARNb0jzmfW4ZIx8darHNhYfQKSL4Wd0hsugjP7EeS94,1886
25
+ contextattr/targets/__init__.py,sha256=i4JxFaHGQqi9mrsgBR6ceYuYdgAsRP5yUL9rarqjdis,362
26
+ contextattr/targets/sampled.py,sha256=3u60m_NCHdOxUWWz1dXmsHwn7upZ8MitDV8rsmBklhA,2813
27
+ contextattr/targets/scored.py,sha256=n7p6iaNdn051bqHz8ZI17K2vxDGsgxgfsadVY_U84Og,6371
28
+ contextattr/wrapper/__init__.py,sha256=jFzFVUwRW-ECkuQ8bxZT6ySuvuHC_bWqy8aJuuosrek,552
29
+ contextattr/wrapper/check.py,sha256=nxkVF1dUEdvW1YnK0XV6l_CKg793J7TsucsNbydItKY,8190
30
+ contextattr/wrapper/contract.py,sha256=d3Qjml5rRNRKvu6HIRX24rn8OCKHhlVs7ZdLU_0Sf7I,7206
31
+ contextattr/wrapper/template.py,sha256=lskvy8hwqSd7ZxhwEunZpN14QWgxky_pOGiqVP27oLM,3047
32
+ contextattr/wrapper/toy.py,sha256=oyQX-Lu3IlJv1z9N9tlH1ALjC27Vi43fIjPW79lvPvk,6610
33
+ contextattr-0.1.0.dist-info/licenses/LICENSE,sha256=lsxcqFqfXFICgMR2fHHX8MrOD8p2cH-GPv5AffW1gDg,1069
34
+ contextattr-0.1.0.dist-info/METADATA,sha256=YpkRmuSWa8z12CVgBLc93GG5tzP-wlx-7_rAQpiHz4c,7846
35
+ contextattr-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
36
+ contextattr-0.1.0.dist-info/entry_points.txt,sha256=IyucNr2RurEFZpf0-NqwjKxCV2DCiks3p7VkC-I-Y3w,53
37
+ contextattr-0.1.0.dist-info/top_level.txt,sha256=tqF9BghHlsVfEpUVelZMxQWkSxQ6ZMJMktUqSRjuqG8,12
38
+ contextattr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ contextattr = contextattr.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Surya Rayala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ contextattr