contextattr 0.1.0__tar.gz

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 (75) hide show
  1. contextattr-0.1.0/LICENSE +21 -0
  2. contextattr-0.1.0/MANIFEST.in +7 -0
  3. contextattr-0.1.0/PKG-INFO +129 -0
  4. contextattr-0.1.0/README.md +95 -0
  5. contextattr-0.1.0/examples/README.md +8 -0
  6. contextattr-0.1.0/examples/quickstart.py +36 -0
  7. contextattr-0.1.0/examples/rio.jsonl +2 -0
  8. contextattr-0.1.0/examples/rio.toml +9 -0
  9. contextattr-0.1.0/pyproject.toml +57 -0
  10. contextattr-0.1.0/setup.cfg +4 -0
  11. contextattr-0.1.0/src/contextattr/README.md +19 -0
  12. contextattr-0.1.0/src/contextattr/__init__.py +22 -0
  13. contextattr-0.1.0/src/contextattr/__main__.py +7 -0
  14. contextattr-0.1.0/src/contextattr/cache.py +64 -0
  15. contextattr-0.1.0/src/contextattr/cli.py +181 -0
  16. contextattr-0.1.0/src/contextattr/evaluate.py +137 -0
  17. contextattr-0.1.0/src/contextattr/methods/README.md +17 -0
  18. contextattr-0.1.0/src/contextattr/methods/__init__.py +27 -0
  19. contextattr-0.1.0/src/contextattr/methods/base.py +86 -0
  20. contextattr-0.1.0/src/contextattr/methods/indices.py +59 -0
  21. contextattr-0.1.0/src/contextattr/methods/interactions.py +215 -0
  22. contextattr-0.1.0/src/contextattr/methods/kernelshap.py +114 -0
  23. contextattr-0.1.0/src/contextattr/methods/linear.py +104 -0
  24. contextattr-0.1.0/src/contextattr/methods/loo.py +31 -0
  25. contextattr-0.1.0/src/contextattr/prompt/README.md +10 -0
  26. contextattr-0.1.0/src/contextattr/prompt/__init__.py +9 -0
  27. contextattr-0.1.0/src/contextattr/prompt/io.py +222 -0
  28. contextattr-0.1.0/src/contextattr/prompt/render.py +70 -0
  29. contextattr-0.1.0/src/contextattr/prompt/split.py +153 -0
  30. contextattr-0.1.0/src/contextattr/prompt/units.py +271 -0
  31. contextattr-0.1.0/src/contextattr/report.py +120 -0
  32. contextattr-0.1.0/src/contextattr/responses/README.md +8 -0
  33. contextattr-0.1.0/src/contextattr/responses/__init__.py +6 -0
  34. contextattr-0.1.0/src/contextattr/responses/checks.py +156 -0
  35. contextattr-0.1.0/src/contextattr/responses/sample.py +36 -0
  36. contextattr-0.1.0/src/contextattr/result.py +120 -0
  37. contextattr-0.1.0/src/contextattr/run.py +306 -0
  38. contextattr-0.1.0/src/contextattr/targets/README.md +8 -0
  39. contextattr-0.1.0/src/contextattr/targets/__init__.py +7 -0
  40. contextattr-0.1.0/src/contextattr/targets/sampled.py +58 -0
  41. contextattr-0.1.0/src/contextattr/targets/scored.py +177 -0
  42. contextattr-0.1.0/src/contextattr/value.py +201 -0
  43. contextattr-0.1.0/src/contextattr/wrapper/README.md +10 -0
  44. contextattr-0.1.0/src/contextattr/wrapper/__init__.py +9 -0
  45. contextattr-0.1.0/src/contextattr/wrapper/check.py +163 -0
  46. contextattr-0.1.0/src/contextattr/wrapper/contract.py +166 -0
  47. contextattr-0.1.0/src/contextattr/wrapper/template.py +56 -0
  48. contextattr-0.1.0/src/contextattr/wrapper/toy.py +129 -0
  49. contextattr-0.1.0/src/contextattr.egg-info/PKG-INFO +129 -0
  50. contextattr-0.1.0/src/contextattr.egg-info/SOURCES.txt +73 -0
  51. contextattr-0.1.0/src/contextattr.egg-info/dependency_links.txt +1 -0
  52. contextattr-0.1.0/src/contextattr.egg-info/entry_points.txt +2 -0
  53. contextattr-0.1.0/src/contextattr.egg-info/requires.txt +11 -0
  54. contextattr-0.1.0/src/contextattr.egg-info/top_level.txt +1 -0
  55. contextattr-0.1.0/tests/README.md +24 -0
  56. contextattr-0.1.0/tests/conftest.py +22 -0
  57. contextattr-0.1.0/tests/prompt_helpers.py +85 -0
  58. contextattr-0.1.0/tests/test_cache.py +45 -0
  59. contextattr-0.1.0/tests/test_explain.py +92 -0
  60. contextattr-0.1.0/tests/test_interactions.py +270 -0
  61. contextattr-0.1.0/tests/test_methods.py +208 -0
  62. contextattr-0.1.0/tests/test_package.py +58 -0
  63. contextattr-0.1.0/tests/test_prompt_io.py +94 -0
  64. contextattr-0.1.0/tests/test_prompt_render.py +185 -0
  65. contextattr-0.1.0/tests/test_prompt_split.py +189 -0
  66. contextattr-0.1.0/tests/test_prompt_units.py +146 -0
  67. contextattr-0.1.0/tests/test_review_fixes.py +182 -0
  68. contextattr-0.1.0/tests/test_run.py +136 -0
  69. contextattr-0.1.0/tests/test_sampled.py +118 -0
  70. contextattr-0.1.0/tests/test_shapiq_methods.py +128 -0
  71. contextattr-0.1.0/tests/test_targets_scored.py +83 -0
  72. contextattr-0.1.0/tests/test_value.py +202 -0
  73. contextattr-0.1.0/tests/test_wrapper_check.py +120 -0
  74. contextattr-0.1.0/tests/test_wrapper_contract.py +99 -0
  75. contextattr-0.1.0/tests/test_wrapper_toy.py +80 -0
@@ -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,7 @@
1
+ # The source archive: the package (found by setuptools), README.md, LICENSE, and the tests with what they need, which
2
+ # include the package folders' READMEs (tests/test_package.py). The wheel takes none of these (include-package-data).
3
+ recursive-include src/contextattr README.md
4
+ graft tests
5
+ graft examples
6
+ prune examples/runs
7
+ global-exclude __pycache__ *.py[cod] *.sqlite
@@ -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,95 @@
1
+ # ContextAttr
2
+
3
+ Find out which parts of a prompt make a language model give an answer.
4
+
5
+ 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.
6
+
7
+ Version 0.1.0 is an early release: the interface may still change.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ pip install contextattr
13
+ ```
14
+
15
+ 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.
16
+
17
+ ## Try it
18
+
19
+ This runs anywhere in seconds: `ToyModel` stands in for a real model.
20
+
21
+ ```python
22
+ import contextattr as ca
23
+
24
+ prompt = ca.Prompt("rio", messages=[{"role": "user", "content": [
25
+ ca.Unit("d1", "Rio Carnival takes place in February or March.", sep="\n"),
26
+ ca.Unit("d2", "Summer in Brazil runs from December to March.", sep="\n"),
27
+ ca.Unit("d3", "Winter in Brazil runs from June to September.", sep="\n"),
28
+ "Question: What is the weather like during Rio Carnival?"]}],
29
+ responses=[ca.Response("bad", "It is cold, because Carnival is in winter.", label="negative"),
30
+ ca.Response("good", "It is hot and humid: it is summer in Brazil.", label="positive")])
31
+
32
+ model = ca.ToyModel.demo() # a stand-in: put your own model's wrapper here
33
+ e = ca.explain(prompt, model, ca.Contrast(), method="kernelshap", budget=8)
34
+ print(e)
35
+ ```
36
+
37
+ It prints:
38
+
39
+ ```
40
+ rio · KernelSHAP(paired=True) · 8 masks · R² 0.991 on 128 test masks · noise 0 · 22 requests
41
+ d3 +0.2500 Winter in Brazil runs from June to September.
42
+ d1 -0.0234 Rio Carnival takes place in February or March.
43
+ d2 -0.2854 Summer in Brazil runs from December to March.
44
+ ```
45
+
46
+ **What the example says:**
47
+
48
+ | In the code | Means |
49
+ |---|---|
50
+ | `ca` | the package, under the short name all the docs use |
51
+ | `ca.Prompt("rio", messages=...)` | one prompt, named `rio`, as chat messages |
52
+ | `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 |
53
+ | `ca.Response("bad", "...", label="negative")` | an answer the model could give. `negative` marks the answer you do not want, `positive` the one you do |
54
+ | `model` | the wrapper around your model |
55
+ | `ca.Contrast()` | what to explain: how much more likely the model finds the unwanted answers than the wanted ones |
56
+ | `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 |
57
+ | `e` | the result |
58
+
59
+ **How to read the output:**
60
+
61
+ - 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.
62
+ - 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.
63
+ - `e.scores` gives the scores as a dictionary, and `e.to_html("report.html")` writes a page with each part shaded by its score.
64
+
65
+ [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.
66
+
67
+ ## Use it with your own model
68
+
69
+ 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.
70
+
71
+ 1. **Get the file to fill in.** `contextattr template my_wrapper.py` writes it. It needs one or both of:
72
+ - `generate`: writes replies to a prompt. Every model can do this, including the OpenAI, Gemini and Anthropic APIs.
73
+ - `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.
74
+ 2. **Check it.** `python my_wrapper.py` runs `ca.check_model` on it and says what, if anything, is wrong.
75
+ 3. **Use it.** Put your wrapper where the example has `model`.
76
+
77
+ ## Choose what to explain, and how
78
+
79
+ - **Parts:** mark them with `ca.Unit`, as above, or cut a text automatically: `ca.split(text, "sentence", id="s")` (or `"word"`, `"line"`, `"paragraph"`).
80
+ - **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.
81
+ - **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.
82
+ - **Many prompts:** `contextattr run run.toml` explains a whole file of prompts and writes a report.
83
+
84
+ ## Learn more
85
+
86
+ | Document | Covers |
87
+ |---|---|
88
+ | [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 |
89
+ | [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 |
90
+ | [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 |
91
+ | [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 |
92
+
93
+ ## Licence
94
+
95
+ ContextAttr is released under the MIT licence: see [LICENSE](https://github.com/Surya-Rayala/contextattr/blob/main/LICENSE).
@@ -0,0 +1,8 @@
1
+ # examples/
2
+
3
+ | File | Shows |
4
+ |---|---|
5
+ | `quickstart.py` | the Rio prompt explained through `ToyModel.demo()`: `check_model`, `explain` with `Contrast`, the removal check and an intervention; runs anywhere in seconds (`python examples/quickstart.py report.html`) |
6
+ | `rio.toml`, `rio.jsonl` | the command line on two prompts: `python -m contextattr plan examples/rio.toml`, then `run`, then `report examples/runs/rio` |
7
+
8
+ `contextattr template my_wrapper.py` writes a wrapper to fill in around your own model or service; running it runs `check_model` on it. The guide, with two complete wrappers to copy from, is [docs/wrapper.md](../docs/wrapper.md).
@@ -0,0 +1,36 @@
1
+ """A first try, on any machine in seconds: the interface's Rio prompt explained through ToyModel.demo(), a stand-in
2
+ model whose answers are known. Swap in your own wrapper to explain a real model (`contextattr template my_wrapper.py`
3
+ writes one to fill in).
4
+
5
+ python examples/quickstart.py [report.html]
6
+ """
7
+
8
+ import sys
9
+
10
+ import contextattr as ca
11
+
12
+ prompt = ca.Prompt("rio-01", messages=[
13
+ {"role": "system", "content": "Answer in one sentence."},
14
+ {"role": "user", "content": [
15
+ "Documents:\n",
16
+ ca.Unit("d1", "Rio Carnival takes place before Lent, in February or March.", sep="\n", tag="doc"),
17
+ ca.Unit("d2", "Summer in Brazil runs from December to March.", sep="\n", tag="doc"),
18
+ ca.Unit("d3", "Winter in Brazil runs from June to September.", sep="\n", tag="doc"),
19
+ "Question: What is the weather like during Rio Carnival?"]}],
20
+ responses=[ca.Response("bad-1", "It is cold, because Carnival is in winter.", label="negative"),
21
+ ca.Response("good-1", "It is hot and humid: it is summer in Brazil.", label="positive")])
22
+
23
+ model = ca.ToyModel.demo()
24
+ print(ca.check_model(model), "\n")
25
+
26
+ # What moves the model from the good answer to the bad one? Three units have only 8 masks: exact Shapley values.
27
+ e = ca.explain(prompt, model, ca.Contrast(), method="kernelshap", budget=8, removal=(1,))
28
+ print(e, "\n")
29
+
30
+ # The behaviour itself: how often the model says "cold", sampled afresh, with d3 and without it.
31
+ cold = ca.checks.regex(r"\bcold\b")
32
+ print("removing", e.top(1)[0][0], "→", ca.intervene(prompt, model, cold, [u for u, _ in e.top(1)], samples=200))
33
+
34
+ if len(sys.argv) > 1:
35
+ e.to_html(sys.argv[1])
36
+ print("report:", sys.argv[1])
@@ -0,0 +1,2 @@
1
+ {"id": "rio-01", "messages": [{"role": "system", "content": "Answer in one sentence."}, {"role": "user", "content": ["Documents:\n", {"unit": "d1", "text": "Rio Carnival takes place before Lent, in February or March.", "sep": "\n", "tag": "doc"}, {"unit": "d2", "text": "Summer in Brazil runs from December to March.", "sep": "\n", "tag": "doc"}, {"unit": "d3", "text": "Winter in Brazil runs from June to September.", "sep": "\n", "tag": "doc"}, "Question: What is the weather like during Rio Carnival?"]}], "reference": "Hot and humid: it is summer in Brazil.", "responses": [{"id": "bad-1", "text": "It is cold, because Carnival is in winter.", "label": "negative"}, {"id": "good-1", "text": "It is hot and humid: it is summer in Brazil.", "label": "positive"}]}
2
+ {"id": "rio-02", "messages": [{"role": "user", "content": [{"unit": "rule", "text": "Answer in one word.", "sep": "\n", "tag": "instruction"}, {"unit": "docs", "text": "Winter in Brazil runs from June to September. Summer in Brazil runs from December to March.", "split": "sentence", "sep": "\n", "tag": "doc"}, "Is Rio warm in July?"]}], "responses": [{"id": "bad-1", "text": "Yes, it is hot in July.", "label": "negative"}, {"id": "good-1", "text": "No, it is winter then, so it is cold.", "label": "positive"}]}
@@ -0,0 +1,9 @@
1
+ # The command line on the Rio example (examples/README.md): python -m contextattr run examples/rio.toml
2
+ prompts = "rio.jsonl"
3
+ out = "runs/rio"
4
+ [model]
5
+ use = "contextattr:ToyModel.demo" # your own: "my_wrappers.py:MyModel", with args = { ... }
6
+ [run]
7
+ method = "kernelshap"
8
+ budget = 8
9
+ removal = [1]
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"] # 77: the licence as an SPDX expression (PEP 639)
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "contextattr"
7
+ version = "0.1.0"
8
+ description = "Context attribution: which parts of a prompt make a model give an output, for a model hosted anywhere"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Surya Rayala" }]
13
+ keywords = ["context attribution", "explainability", "interpretability", "LLM", "feature interactions",
14
+ "Shapley values", "SPEX"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ ]
27
+ # Tested on 3.12, 3.13 and 3.14, from a fresh install each (log, 2026-11-10); design §5 aims at 3.11, not yet tried.
28
+ requires-python = ">=3.12"
29
+ # Both floors are tested (log, 2026-11-10). scikit-learn 1.9 is the first whose RidgeCV fits exactly: before it, the
30
+ # terms of a noise-free fit were off by up to 0.2 (Faith-Banzhaf's default fit, methods/interactions.py).
31
+ dependencies = ["numpy>=1.26", "scikit-learn>=1.9"]
32
+
33
+ [project.urls]
34
+ Repository = "https://github.com/Surya-Rayala/contextattr"
35
+ Documentation = "https://github.com/Surya-Rayala/contextattr/tree/main/docs"
36
+ Issues = "https://github.com/Surya-Rayala/contextattr/issues"
37
+
38
+ [project.scripts]
39
+ contextattr = "contextattr.cli:main"
40
+
41
+ [project.optional-dependencies]
42
+ # The SPEX adapter reaches into shapiq's and sparse-transform's internals (methods/interactions.py), so both stay in
43
+ # the series tested (shapiq 1.8.0, sparse-transform 0.2.1) until a newer one is checked.
44
+ interactions = ["shapiq[sparse]>=1.8,<1.9", "sparse-transform>=0.2.1,<0.3", "lightgbm>=4"]
45
+ test = ["pytest>=8", "hypothesis>=6.100"]
46
+
47
+ [tool.setuptools]
48
+ include-package-data = false # the wheel holds the code only; MANIFEST.in adds the rest to the source archive
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = ["src"]
56
+ markers = ["gpu: needs a GPU",
57
+ "shapiq: runs shapiq, slow to import; only with CA_TEST_SHAPIQ=1"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ # src/contextattr: the package
2
+
3
+ One folder or file per role. The package reaches a model only through a wrapper its user writes ([docs/wrapper.md](../../docs/wrapper.md)) and imports no model library, which `tests/test_package.py` checks.
4
+
5
+ | Role | Holds |
6
+ |---|---|
7
+ | [`prompt/`](prompt/README.md) | the input: prompts, units, splitting, masking, `prompts.jsonl` |
8
+ | [`wrapper/`](wrapper/README.md) | what a user's wrapper must do, `check_model`, and `ToyModel` |
9
+ | [`targets/`](targets/README.md) | the number read for each masked prompt: `LogProb`, `Pooled`, `Contrast`, `Choice`; `Sampled`, which needs only generate |
10
+ | [`responses/`](responses/README.md) | checks that label a response (`regex`, `reference`, any function) and replies sampled from the model |
11
+ | `value.py` | `Value`: masks → numbers, each distinct request asked once, in batches, cached; or a user's own function; the noise check |
12
+ | `cache.py` | `Cache`: every answer stored once in one SQLite file (or memory), so a run resumes |
13
+ | [`methods/`](methods/README.md) | which masks and which fit: `LeaveOneOut`, `Banzhaf`, `LIME`, `ContextCite`, `KernelSHAP`; `FaithBanzhaf`, `FaithShap`, `SPEX`, `ProxySPEX` through shapiq, and the indices of App. C |
14
+ | `evaluate.py` | the faithfulness R² on held-out masks (SPEX eq. 7) and those masks; top-r removal (eq. 8) and Recovery@r (eq. 9); `intervene`, removing units and sampling again |
15
+ | `result.py` | `Explanation`: the scores, R², noise and costs; `top`, `table`, `as_dict`, `from_dict`, `to_html`; Shapley, Banzhaf and Shapley-Taylor read from its surrogate |
16
+ | `run.py` | `explain` (a prompt through a model and a target, or a function of masked prompts), `attribute` (any function of masks), and `Run` (a prompt file: `plan`, `execute` into a run directory, resumable) with `Results` (`summary` by tag) |
17
+ | `report.py` | one self-contained HTML page: each prompt with its units shaded by score, the scores, interactions and removal |
18
+ | `cli.py`, `__main__.py` | `contextattr plan | run | report` (also `python -m contextattr`), from a TOML file naming the wrapper by import path |
19
+ | `__init__.py` | the public names |
@@ -0,0 +1,22 @@
1
+ """ContextAttr: which parts of a prompt make a model give an output (docs/interface.md, docs/methods.md)."""
2
+
3
+ from .cache import Cache
4
+ from . import responses
5
+ from .evaluate import faithfulness, intervene
6
+ from .methods import LIME, Banzhaf, ContextCite, KernelSHAP, LeaveOneOut
7
+ from .prompt import Group, Message, Prompt, Response, Unit, read_prompts, split, write_prompts
8
+ from .responses import Verdict, sample
9
+ from .result import Explanation
10
+ from .run import Results, Run, attribute, explain
11
+ from .targets import Choice, Contrast, LogProb, Pooled, Sampled
12
+ from .value import Value
13
+ from .wrapper import ModelReport, Sampling, Scored, ToyModel, WrapperError, check_model
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ checks = responses # ca.checks.regex(...), ca.checks.reference(...)
18
+
19
+ __all__ = ["LIME", "Banzhaf", "Cache", "Choice", "ContextCite", "Contrast", "Explanation", "Group", "KernelSHAP",
20
+ "LeaveOneOut", "LogProb", "Message", "ModelReport", "Pooled", "Prompt", "Response", "Results", "Run", "Sampled", "Sampling",
21
+ "Scored", "ToyModel", "Unit", "Value", "Verdict", "WrapperError", "attribute", "check_model", "checks",
22
+ "explain", "faithfulness", "intervene", "read_prompts", "responses", "sample", "split", "write_prompts"]
@@ -0,0 +1,7 @@
1
+ """python -m contextattr: the command line (cli.py)."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,64 @@
1
+ """The cache: every model answer stored once, under a key made from its request (docs/interface.md §7).
2
+
3
+ One SQLite file per run, or memory when no path is given. A run that stops resumes from it, another method on the
4
+ same masks reuses its answers, and nothing is asked of the model twice. The first answer stored for a key stays.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import sqlite3
13
+ from collections.abc import Iterable, Mapping
14
+
15
+ _CHUNK = 500 # keys per SELECT, under SQLite's limit on query parameters
16
+
17
+
18
+ class Cache:
19
+ """Answers by key. `key(request)` makes the key from a JSON-able request; answers are JSON-able values."""
20
+
21
+ def __init__(self, path: str | os.PathLike | None = None):
22
+ self.path = None if path is None else os.fspath(path)
23
+ if self.path is not None and os.path.dirname(self.path):
24
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
25
+ self._db = sqlite3.connect(":memory:" if self.path is None else self.path)
26
+ self._db.execute("CREATE TABLE IF NOT EXISTS answers "
27
+ "(key TEXT PRIMARY KEY, kind TEXT NOT NULL, answer TEXT NOT NULL)")
28
+ self._db.commit()
29
+
30
+ @staticmethod
31
+ def key(request: Mapping) -> str:
32
+ """sha256 of the request as canonical JSON: the same request always gives the same key."""
33
+ text = json.dumps(request, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
34
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
35
+
36
+ def get_many(self, keys: Iterable[str]) -> dict[str, object]:
37
+ """The stored answers among `keys`."""
38
+ keys = list(dict.fromkeys(keys))
39
+ found = {}
40
+ for i in range(0, len(keys), _CHUNK):
41
+ chunk = keys[i:i + _CHUNK]
42
+ rows = self._db.execute(f"SELECT key, answer FROM answers WHERE key IN ({','.join('?' * len(chunk))})",
43
+ chunk)
44
+ found.update((k, json.loads(a)) for k, a in rows)
45
+ return found
46
+
47
+ def put_many(self, kind: str, answers: Mapping[str, object]) -> None:
48
+ """Store answers and commit at once; a key already stored keeps its first answer."""
49
+ self._db.executemany("INSERT OR IGNORE INTO answers (key, kind, answer) VALUES (?, ?, ?)",
50
+ [(k, kind, json.dumps(a, ensure_ascii=False, allow_nan=False))
51
+ for k, a in answers.items()])
52
+ self._db.commit()
53
+
54
+ def __len__(self) -> int:
55
+ return self._db.execute("SELECT COUNT(*) FROM answers").fetchone()[0]
56
+
57
+ def close(self) -> None:
58
+ self._db.close()
59
+
60
+ def __enter__(self) -> Cache:
61
+ return self
62
+
63
+ def __exit__(self, *exc) -> None:
64
+ self.close()