bioclaim 0.7.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.
bioclaim-0.7.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sohil Ananth
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,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: bioclaim
3
+ Version: 0.7.0
4
+ Summary: A grounding firewall for biomedical LLMs: verifies biological identifiers and claims against authoritative databases, and flags fabrications.
5
+ Author-email: Sohil Ananth <sohil.r@icloud.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/SxR24/bioclaim
8
+ Project-URL: Issues, https://github.com/SxR24/bioclaim/issues
9
+ Keywords: bioinformatics,llm,hallucination,grounding,genomics,ontology
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # bioclaim
22
+
23
+ **A grounding firewall for biomedical LLMs.** Wrap it around any model's output and it
24
+ verifies the biological identifiers against authoritative databases — UniProtKB, Ensembl,
25
+ and EBI's Ontology Lookup Service — flagging the ones that are fabricated.
26
+
27
+ Large language models constantly emit identifiers that *look* real but don't exist
28
+ (`GO:9999999`, `HP:9999999`), alongside false gene–function and gene–disease claims. In
29
+ research and clinical settings that is a documented safety risk. `bioclaim` is a
30
+ deterministic, low-latency layer that catches it.
31
+
32
+ > **Real-model finding:** across three models, **48–68% of specialist biology
33
+ > answers contained a wrong biomedical identifier** (Llama-3.1-8B 68%, gpt-oss-120B
34
+ > 60%, Llama-3.3-70B 48%). The dominant error is a real, valid accession pointing at
35
+ > the **wrong gene** — invisible to existence checks, independently verified.
36
+ > Full study: [RESULTS.md](RESULTS.md).
37
+
38
+ ## Benchmark result
39
+
40
+ On a labeled set of **500 LLM-style answers** (394 injected fabrications):
41
+
42
+ | Metric | Score |
43
+ | --- | --- |
44
+ | Precision (flagged that were truly fake) | **100.0%** |
45
+ | Recall (fabrications caught) | **95.7%** (377 / 394) |
46
+ | F1 | **0.978** |
47
+ | False accusations | **0** |
48
+
49
+ Every uncaught fabrication was a network-throttled lookup that safely returned
50
+ `UNVERIFIED` — not a single fake was checked and missed. Reproduce with:
51
+
52
+ ```bash
53
+ python scripts/generate_benchmark.py 500
54
+ python scripts/benchmark.py data/benchmark_large.jsonl
55
+ ```
56
+
57
+ ## How it works
58
+
59
+ Two layers, both designed to **never falsely accuse** (if a claim can't be verified it is
60
+ marked `UNVERIFIED`, never `NOT_FOUND`):
61
+
62
+ 1. **FORMAT** — offline, deterministic. Rejects malformed identifiers.
63
+ 2. **EXISTS** — live existence check against the source database. Well-formed but absent
64
+ identifiers are flagged `NOT_FOUND`. Rate-limit/network errors are retried with backoff,
65
+ then degrade to `UNVERIFIED`.
66
+ 3. **CLAIM (v0.5)** — label consistency. A *real* identifier with a *wrong* description
67
+ (`GO:0006281 (photosynthesis)`, actually "DNA repair") is flagged
68
+ `SUPPORTED_LABEL_MISMATCH`. Synonym-aware, so paraphrases aren't falsely flagged.
69
+ 4. **ENTITY (v0.6)** — correspondence. A *real* UniProt/Ensembl ID attached to the
70
+ *wrong* gene ("the UniProt for TP53 is P38398", actually BRCA1) is flagged
71
+ `SUPPORTED_ENTITY_MISMATCH` — the hardest class, invisible to existence checks.
72
+
73
+ Supported identifier types: GO, HP, MONDO, DOID, CHEBI, Ensembl gene (ENSG), UniProtKB.
74
+
75
+ Every lookup is **cached to disk** ($BIOCLAIM_CACHE, else `~/.cache/bioclaim/`), so
76
+ after a warm-up bioclaim runs fast, offline-capable, and immune to rate limits.
77
+
78
+ ## Install & use
79
+
80
+ ```bash
81
+ pip install -e . # from a clone; PyPI release: pip install bioclaim
82
+ ```
83
+
84
+ **One-call API:**
85
+
86
+ ```python
87
+ from bioclaim import check
88
+
89
+ result = check("TP53 (P04637) is annotated with GO:9999999 (apoptosis).")
90
+ print(result.ok) # False
91
+ for p in result.problems:
92
+ print(p) # GO:9999999: fabricated (does not exist)
93
+ ```
94
+
95
+ **Guard any model call** (raises if a fabrication slips through):
96
+
97
+ ```python
98
+ from bioclaim import Firewall
99
+ guarded = Firewall(raise_on_flag=True).guard(call_my_llm)
100
+ answer = guarded(prompt) # BioclaimFlag raised if the answer cites a fake ID
101
+ ```
102
+
103
+ **Command line:**
104
+
105
+ ```bash
106
+ bioclaim "TP53 is P04637 and the fake GO:9999999"
107
+ echo "some model output" | bioclaim --entity BRCA1
108
+ ```
109
+
110
+ ## Project layout
111
+
112
+ ```
113
+ bioclaim/ core package (patterns, live sources, validator)
114
+ scripts/ runnable CLIs: demo, batch, benchmark, generate_benchmark
115
+ data/ sample_answers.jsonl (labeled demo set)
116
+ tests/ offline unit tests
117
+ ```
118
+
119
+ Run the pieces (from the repo root):
120
+
121
+ ```bash
122
+ python scripts/demo.py # catch fakes in one example answer
123
+ python scripts/benchmark.py # precision/recall on the sample set
124
+ python scripts/batch.py your_answers.jsonl # scan your own AI answers
125
+ python -m pytest # run tests
126
+ ```
127
+
128
+ ## Roadmap (land-and-expand)
129
+
130
+ - **v0.1–0.3** Identifier validation across ontologies, genes, and proteins *(done)*
131
+ - **v0.4** Local database snapshots — microsecond lookups, fully offline, true 100% recall
132
+ - **v0.5** Claim verification — relationships, not just IDs (gene–disease, gene–function)
133
+ - **v0.6** Calibrated confidence per claim
134
+ - **v1.0** Public leaderboard + LLM-framework integrations
135
+
136
+ ## Why this design wins
137
+
138
+ Generality (all identifier types, one layer) · Deployability (pure standard library,
139
+ 3-line integration) · Speed (deterministic lookups, not agent loops) · Trust (never a
140
+ false accusation).
141
+
142
+ ## License
143
+
144
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,124 @@
1
+ # bioclaim
2
+
3
+ **A grounding firewall for biomedical LLMs.** Wrap it around any model's output and it
4
+ verifies the biological identifiers against authoritative databases — UniProtKB, Ensembl,
5
+ and EBI's Ontology Lookup Service — flagging the ones that are fabricated.
6
+
7
+ Large language models constantly emit identifiers that *look* real but don't exist
8
+ (`GO:9999999`, `HP:9999999`), alongside false gene–function and gene–disease claims. In
9
+ research and clinical settings that is a documented safety risk. `bioclaim` is a
10
+ deterministic, low-latency layer that catches it.
11
+
12
+ > **Real-model finding:** across three models, **48–68% of specialist biology
13
+ > answers contained a wrong biomedical identifier** (Llama-3.1-8B 68%, gpt-oss-120B
14
+ > 60%, Llama-3.3-70B 48%). The dominant error is a real, valid accession pointing at
15
+ > the **wrong gene** — invisible to existence checks, independently verified.
16
+ > Full study: [RESULTS.md](RESULTS.md).
17
+
18
+ ## Benchmark result
19
+
20
+ On a labeled set of **500 LLM-style answers** (394 injected fabrications):
21
+
22
+ | Metric | Score |
23
+ | --- | --- |
24
+ | Precision (flagged that were truly fake) | **100.0%** |
25
+ | Recall (fabrications caught) | **95.7%** (377 / 394) |
26
+ | F1 | **0.978** |
27
+ | False accusations | **0** |
28
+
29
+ Every uncaught fabrication was a network-throttled lookup that safely returned
30
+ `UNVERIFIED` — not a single fake was checked and missed. Reproduce with:
31
+
32
+ ```bash
33
+ python scripts/generate_benchmark.py 500
34
+ python scripts/benchmark.py data/benchmark_large.jsonl
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ Two layers, both designed to **never falsely accuse** (if a claim can't be verified it is
40
+ marked `UNVERIFIED`, never `NOT_FOUND`):
41
+
42
+ 1. **FORMAT** — offline, deterministic. Rejects malformed identifiers.
43
+ 2. **EXISTS** — live existence check against the source database. Well-formed but absent
44
+ identifiers are flagged `NOT_FOUND`. Rate-limit/network errors are retried with backoff,
45
+ then degrade to `UNVERIFIED`.
46
+ 3. **CLAIM (v0.5)** — label consistency. A *real* identifier with a *wrong* description
47
+ (`GO:0006281 (photosynthesis)`, actually "DNA repair") is flagged
48
+ `SUPPORTED_LABEL_MISMATCH`. Synonym-aware, so paraphrases aren't falsely flagged.
49
+ 4. **ENTITY (v0.6)** — correspondence. A *real* UniProt/Ensembl ID attached to the
50
+ *wrong* gene ("the UniProt for TP53 is P38398", actually BRCA1) is flagged
51
+ `SUPPORTED_ENTITY_MISMATCH` — the hardest class, invisible to existence checks.
52
+
53
+ Supported identifier types: GO, HP, MONDO, DOID, CHEBI, Ensembl gene (ENSG), UniProtKB.
54
+
55
+ Every lookup is **cached to disk** ($BIOCLAIM_CACHE, else `~/.cache/bioclaim/`), so
56
+ after a warm-up bioclaim runs fast, offline-capable, and immune to rate limits.
57
+
58
+ ## Install & use
59
+
60
+ ```bash
61
+ pip install -e . # from a clone; PyPI release: pip install bioclaim
62
+ ```
63
+
64
+ **One-call API:**
65
+
66
+ ```python
67
+ from bioclaim import check
68
+
69
+ result = check("TP53 (P04637) is annotated with GO:9999999 (apoptosis).")
70
+ print(result.ok) # False
71
+ for p in result.problems:
72
+ print(p) # GO:9999999: fabricated (does not exist)
73
+ ```
74
+
75
+ **Guard any model call** (raises if a fabrication slips through):
76
+
77
+ ```python
78
+ from bioclaim import Firewall
79
+ guarded = Firewall(raise_on_flag=True).guard(call_my_llm)
80
+ answer = guarded(prompt) # BioclaimFlag raised if the answer cites a fake ID
81
+ ```
82
+
83
+ **Command line:**
84
+
85
+ ```bash
86
+ bioclaim "TP53 is P04637 and the fake GO:9999999"
87
+ echo "some model output" | bioclaim --entity BRCA1
88
+ ```
89
+
90
+ ## Project layout
91
+
92
+ ```
93
+ bioclaim/ core package (patterns, live sources, validator)
94
+ scripts/ runnable CLIs: demo, batch, benchmark, generate_benchmark
95
+ data/ sample_answers.jsonl (labeled demo set)
96
+ tests/ offline unit tests
97
+ ```
98
+
99
+ Run the pieces (from the repo root):
100
+
101
+ ```bash
102
+ python scripts/demo.py # catch fakes in one example answer
103
+ python scripts/benchmark.py # precision/recall on the sample set
104
+ python scripts/batch.py your_answers.jsonl # scan your own AI answers
105
+ python -m pytest # run tests
106
+ ```
107
+
108
+ ## Roadmap (land-and-expand)
109
+
110
+ - **v0.1–0.3** Identifier validation across ontologies, genes, and proteins *(done)*
111
+ - **v0.4** Local database snapshots — microsecond lookups, fully offline, true 100% recall
112
+ - **v0.5** Claim verification — relationships, not just IDs (gene–disease, gene–function)
113
+ - **v0.6** Calibrated confidence per claim
114
+ - **v1.0** Public leaderboard + LLM-framework integrations
115
+
116
+ ## Why this design wins
117
+
118
+ Generality (all identifier types, one layer) · Deployability (pure standard library,
119
+ 3-line integration) · Speed (deterministic lookups, not agent loops) · Trust (never a
120
+ false accusation).
121
+
122
+ ## License
123
+
124
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ from .validator import scan, report, Verdict
2
+ from .claims import check_claims, report_claims, ClaimVerdict, extract_target_entity
3
+ from .api import check, Result, Problem, Firewall, BioclaimFlag
4
+
5
+ __all__ = [
6
+ "check", "Result", "Problem", "Firewall", "BioclaimFlag", # primary API
7
+ "scan", "report", "Verdict",
8
+ "check_claims", "report_claims", "ClaimVerdict", "extract_target_entity",
9
+ ]
10
+ __version__ = "0.7.0"
@@ -0,0 +1,33 @@
1
+ """Command-line interface: bioclaim "text with GO:9999999 ..."
2
+
3
+ Reads text from the arguments or stdin, verifies every biomedical identifier,
4
+ and prints the problems. Exit code is non-zero if any identifier is flagged
5
+ (handy in CI / guardrail scripts).
6
+ """
7
+ import sys
8
+ import argparse
9
+ from . import check, __version__
10
+
11
+
12
+ def main(argv=None):
13
+ ap = argparse.ArgumentParser(prog="bioclaim",
14
+ description="Verify biomedical identifiers in text.")
15
+ ap.add_argument("text", nargs="*", help="text to check (or pipe via stdin)")
16
+ ap.add_argument("--entity", default=None,
17
+ help="the gene/protein the text is about (enables wrong-gene check)")
18
+ ap.add_argument("--offline", action="store_true",
19
+ help="format check only; skip database lookups")
20
+ ap.add_argument("--version", action="version", version=f"bioclaim {__version__}")
21
+ args = ap.parse_args(argv)
22
+
23
+ text = " ".join(args.text) if args.text else sys.stdin.read()
24
+ if not text.strip():
25
+ ap.error("no text provided (pass as arguments or via stdin)")
26
+
27
+ result = check(text, entity_hint=args.entity, online=not args.offline)
28
+ print(result)
29
+ return 1 if not result.ok else 0
30
+
31
+
32
+ if __name__ == "__main__":
33
+ sys.exit(main())
@@ -0,0 +1,110 @@
1
+ """The one-call public API for bioclaim.
2
+
3
+ from bioclaim import check
4
+ result = check("TP53 (P04637) is annotated with GO:9999999.")
5
+ print(result.ok, result.problems)
6
+
7
+ Also a Firewall wrapper to guard any function that returns LLM text.
8
+ """
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional
11
+ from .claims import check_claims, FLAGGED_STATUSES
12
+
13
+ _LABELS = {
14
+ "NOT_FOUND": "fabricated (does not exist)",
15
+ "INVALID_FORMAT": "malformed identifier",
16
+ "SUPPORTED_LABEL_MISMATCH": "wrong description",
17
+ "SUPPORTED_ENTITY_MISMATCH": "wrong gene/entity",
18
+ "SUPPORTED_OBSOLETE": "obsolete / deprecated",
19
+ }
20
+
21
+
22
+ @dataclass
23
+ class Problem:
24
+ curie: str
25
+ kind: str # human-readable problem type
26
+ status: str
27
+ claimed: Optional[str]
28
+ actual: Optional[str]
29
+
30
+ def __str__(self):
31
+ extra = ""
32
+ if self.claimed and self.actual:
33
+ extra = f' (said "{self.claimed}", actually "{self.actual}")'
34
+ elif self.actual:
35
+ extra = f' (actually "{self.actual}")'
36
+ return f"{self.curie}: {self.kind}{extra}"
37
+
38
+
39
+ @dataclass
40
+ class Result:
41
+ text: str
42
+ n_ids: int
43
+ problems: List[Problem]
44
+
45
+ @property
46
+ def ok(self) -> bool:
47
+ """True if no problems were found."""
48
+ return not self.problems
49
+
50
+ def __bool__(self):
51
+ return self.ok
52
+
53
+ def __str__(self):
54
+ if self.ok:
55
+ return f"OK - {self.n_ids} identifier(s) checked, none flagged"
56
+ lines = [f"{len(self.problems)} problem(s) in {self.n_ids} identifier(s):"]
57
+ lines += [f" - {p}" for p in self.problems]
58
+ return "\n".join(lines)
59
+
60
+
61
+ def check(text, entity_hint=None, online=True) -> Result:
62
+ """Verify every biomedical identifier in `text`. Returns a Result.
63
+
64
+ entity_hint: the gene/protein the text is about, if known (enables the
65
+ wrong-gene check on free text).
66
+ """
67
+ verdicts = check_claims(text, online=online, entity_hint=entity_hint)
68
+ problems = [
69
+ Problem(v.curie, _LABELS.get(v.status, v.status), v.status,
70
+ v.claimed_label, v.canonical_label)
71
+ for v in verdicts if v.status in FLAGGED_STATUSES
72
+ ]
73
+ return Result(text=text, n_ids=len(verdicts), problems=problems)
74
+
75
+
76
+ class Firewall:
77
+ """Guard any function that returns model text.
78
+
79
+ fw = Firewall()
80
+ answer = fw.guard(call_my_llm)(prompt) # raises if a fabrication slips through
81
+
82
+ or non-raising:
83
+
84
+ result = fw(answer_text)
85
+ """
86
+
87
+ def __init__(self, entity_hint=None, online=True, raise_on_flag=False):
88
+ self.entity_hint = entity_hint
89
+ self.online = online
90
+ self.raise_on_flag = raise_on_flag
91
+
92
+ def __call__(self, text) -> Result:
93
+ r = check(text, entity_hint=self.entity_hint, online=self.online)
94
+ if self.raise_on_flag and not r.ok:
95
+ raise BioclaimFlag(r)
96
+ return r
97
+
98
+ def guard(self, fn):
99
+ """Decorator: run fn, then verify its returned text."""
100
+ def wrapped(*args, **kwargs):
101
+ text = fn(*args, **kwargs)
102
+ self(text) # may raise if raise_on_flag
103
+ return text
104
+ return wrapped
105
+
106
+
107
+ class BioclaimFlag(Exception):
108
+ def __init__(self, result: Result):
109
+ self.result = result
110
+ super().__init__(str(result))
@@ -0,0 +1,77 @@
1
+ """Transparent, persistent on-disk cache for database lookups.
2
+
3
+ Every existence / entity lookup is cached to disk, so:
4
+ - repeat lookups are instant,
5
+ - a warmed cache works fully offline,
6
+ - rate limits stop mattering (each id is fetched at most once, ever).
7
+
8
+ Only *definitive* results are cached (a real answer from the database). Transient
9
+ failures (network/None) are never cached, so a hiccup can't poison the cache.
10
+
11
+ Cache location: $BIOCLAIM_CACHE, else ~/.cache/bioclaim/. Disable with
12
+ BIOCLAIM_CACHE=off.
13
+ """
14
+ import os
15
+ import json
16
+ import atexit
17
+ import pathlib
18
+ import threading
19
+
20
+
21
+ def _cache_dir():
22
+ override = os.environ.get("BIOCLAIM_CACHE")
23
+ if override and override.lower() == "off":
24
+ return None
25
+ base = pathlib.Path(override) if override else pathlib.Path.home() / ".cache" / "bioclaim"
26
+ try:
27
+ base.mkdir(parents=True, exist_ok=True)
28
+ return base
29
+ except Exception:
30
+ return None
31
+
32
+
33
+ class DiskCache:
34
+ """A tiny JSON-backed dict with atomic, batched persistence."""
35
+
36
+ def __init__(self, name):
37
+ self._dir = _cache_dir()
38
+ self._path = (self._dir / f"{name}.json") if self._dir else None
39
+ self._lock = threading.Lock()
40
+ self._data = {}
41
+ self._writes = 0
42
+ if self._path and self._path.exists():
43
+ try:
44
+ self._data = json.loads(self._path.read_text())
45
+ except Exception:
46
+ self._data = {}
47
+ atexit.register(self.flush)
48
+
49
+ _MISS = object()
50
+
51
+ def get(self, key):
52
+ return self._data.get(key, DiskCache._MISS)
53
+
54
+ def set(self, key, value):
55
+ with self._lock:
56
+ self._data[key] = value
57
+ self._writes += 1
58
+ due = self._writes % 25 == 0
59
+ if due:
60
+ self.flush()
61
+
62
+ def flush(self):
63
+ if not self._path:
64
+ return
65
+ with self._lock:
66
+ try:
67
+ tmp = self._path.with_suffix(".tmp")
68
+ tmp.write_text(json.dumps(self._data))
69
+ tmp.replace(self._path)
70
+ except Exception:
71
+ pass
72
+
73
+ def __len__(self):
74
+ return len(self._data)
75
+
76
+
77
+ MISS = DiskCache._MISS