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 +21 -0
- bioclaim-0.7.0/PKG-INFO +144 -0
- bioclaim-0.7.0/README.md +124 -0
- bioclaim-0.7.0/bioclaim/__init__.py +10 -0
- bioclaim-0.7.0/bioclaim/__main__.py +33 -0
- bioclaim-0.7.0/bioclaim/api.py +110 -0
- bioclaim-0.7.0/bioclaim/cache.py +77 -0
- bioclaim-0.7.0/bioclaim/claims.py +254 -0
- bioclaim-0.7.0/bioclaim/patterns.py +28 -0
- bioclaim-0.7.0/bioclaim/sources.py +222 -0
- bioclaim-0.7.0/bioclaim/validator.py +56 -0
- bioclaim-0.7.0/bioclaim.egg-info/PKG-INFO +144 -0
- bioclaim-0.7.0/bioclaim.egg-info/SOURCES.txt +20 -0
- bioclaim-0.7.0/bioclaim.egg-info/dependency_links.txt +1 -0
- bioclaim-0.7.0/bioclaim.egg-info/entry_points.txt +2 -0
- bioclaim-0.7.0/bioclaim.egg-info/requires.txt +3 -0
- bioclaim-0.7.0/bioclaim.egg-info/top_level.txt +1 -0
- bioclaim-0.7.0/pyproject.toml +36 -0
- bioclaim-0.7.0/setup.cfg +4 -0
- bioclaim-0.7.0/tests/test_api.py +63 -0
- bioclaim-0.7.0/tests/test_claims.py +79 -0
- bioclaim-0.7.0/tests/test_validator.py +28 -0
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.
|
bioclaim-0.7.0/PKG-INFO
ADDED
|
@@ -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).
|
bioclaim-0.7.0/README.md
ADDED
|
@@ -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
|