bmlib 0.4.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.
- bmlib-0.4.0/CHANGELOG.md +195 -0
- bmlib-0.4.0/LICENSE +17 -0
- bmlib-0.4.0/MANIFEST.in +10 -0
- bmlib-0.4.0/PKG-INFO +263 -0
- bmlib-0.4.0/README.md +213 -0
- bmlib-0.4.0/bmlib/__init__.py +19 -0
- bmlib-0.4.0/bmlib/agents/__init__.py +21 -0
- bmlib-0.4.0/bmlib/agents/base.py +269 -0
- bmlib-0.4.0/bmlib/db/__init__.py +57 -0
- bmlib-0.4.0/bmlib/db/connection.py +100 -0
- bmlib-0.4.0/bmlib/db/migrations.py +141 -0
- bmlib-0.4.0/bmlib/db/operations.py +213 -0
- bmlib-0.4.0/bmlib/db/transactions.py +84 -0
- bmlib-0.4.0/bmlib/fulltext/__init__.py +60 -0
- bmlib-0.4.0/bmlib/fulltext/cache.py +172 -0
- bmlib-0.4.0/bmlib/fulltext/jats_parser.py +1029 -0
- bmlib-0.4.0/bmlib/fulltext/models.py +198 -0
- bmlib-0.4.0/bmlib/fulltext/pdf_converter.py +288 -0
- bmlib-0.4.0/bmlib/fulltext/service.py +390 -0
- bmlib-0.4.0/bmlib/llm/__init__.py +79 -0
- bmlib-0.4.0/bmlib/llm/client.py +393 -0
- bmlib-0.4.0/bmlib/llm/data_types.py +198 -0
- bmlib-0.4.0/bmlib/llm/json_repair.py +548 -0
- bmlib-0.4.0/bmlib/llm/providers/__init__.py +139 -0
- bmlib-0.4.0/bmlib/llm/providers/anthropic.py +467 -0
- bmlib-0.4.0/bmlib/llm/providers/base.py +182 -0
- bmlib-0.4.0/bmlib/llm/providers/deepseek.py +56 -0
- bmlib-0.4.0/bmlib/llm/providers/gemini.py +78 -0
- bmlib-0.4.0/bmlib/llm/providers/mistral.py +71 -0
- bmlib-0.4.0/bmlib/llm/providers/ollama.py +511 -0
- bmlib-0.4.0/bmlib/llm/providers/openai_compat.py +417 -0
- bmlib-0.4.0/bmlib/llm/providers/openai_provider.py +81 -0
- bmlib-0.4.0/bmlib/llm/text_utils.py +361 -0
- bmlib-0.4.0/bmlib/llm/token_tracker.py +167 -0
- bmlib-0.4.0/bmlib/llm/utils.py +84 -0
- bmlib-0.4.0/bmlib/publications/__init__.py +67 -0
- bmlib-0.4.0/bmlib/publications/fetchers/__init__.py +37 -0
- bmlib-0.4.0/bmlib/publications/fetchers/biorxiv.py +202 -0
- bmlib-0.4.0/bmlib/publications/fetchers/openalex.py +280 -0
- bmlib-0.4.0/bmlib/publications/fetchers/pubmed.py +400 -0
- bmlib-0.4.0/bmlib/publications/fetchers/registry.py +187 -0
- bmlib-0.4.0/bmlib/publications/models.py +321 -0
- bmlib-0.4.0/bmlib/publications/schema.py +84 -0
- bmlib-0.4.0/bmlib/publications/storage.py +384 -0
- bmlib-0.4.0/bmlib/publications/sync.py +442 -0
- bmlib-0.4.0/bmlib/py.typed +0 -0
- bmlib-0.4.0/bmlib/quality/__init__.py +81 -0
- bmlib-0.4.0/bmlib/quality/cochrane_formatter.py +380 -0
- bmlib-0.4.0/bmlib/quality/cochrane_models.py +599 -0
- bmlib-0.4.0/bmlib/quality/data_models.py +361 -0
- bmlib-0.4.0/bmlib/quality/extractors.py +487 -0
- bmlib-0.4.0/bmlib/quality/manager.py +175 -0
- bmlib-0.4.0/bmlib/quality/metadata_filter.py +161 -0
- bmlib-0.4.0/bmlib/quality/quality_agent.py +168 -0
- bmlib-0.4.0/bmlib/quality/scoring_models.py +140 -0
- bmlib-0.4.0/bmlib/quality/study_classifier.py +123 -0
- bmlib-0.4.0/bmlib/templates/__init__.py +36 -0
- bmlib-0.4.0/bmlib/templates/engine.py +119 -0
- bmlib-0.4.0/bmlib/transparency/__init__.py +38 -0
- bmlib-0.4.0/bmlib/transparency/analyzer.py +766 -0
- bmlib-0.4.0/bmlib/transparency/models.py +187 -0
- bmlib-0.4.0/bmlib.egg-info/PKG-INFO +263 -0
- bmlib-0.4.0/bmlib.egg-info/SOURCES.txt +89 -0
- bmlib-0.4.0/bmlib.egg-info/dependency_links.txt +1 -0
- bmlib-0.4.0/bmlib.egg-info/requires.txt +30 -0
- bmlib-0.4.0/bmlib.egg-info/top_level.txt +1 -0
- bmlib-0.4.0/pyproject.toml +94 -0
- bmlib-0.4.0/setup.cfg +4 -0
- bmlib-0.4.0/tests/test_agents.py +301 -0
- bmlib-0.4.0/tests/test_cochrane.py +245 -0
- bmlib-0.4.0/tests/test_db.py +244 -0
- bmlib-0.4.0/tests/test_extractors.py +243 -0
- bmlib-0.4.0/tests/test_fulltext_cache.py +143 -0
- bmlib-0.4.0/tests/test_fulltext_models.py +143 -0
- bmlib-0.4.0/tests/test_fulltext_service.py +545 -0
- bmlib-0.4.0/tests/test_jats_parser.py +157 -0
- bmlib-0.4.0/tests/test_json_repair.py +164 -0
- bmlib-0.4.0/tests/test_llm.py +300 -0
- bmlib-0.4.0/tests/test_llm_tools.py +926 -0
- bmlib-0.4.0/tests/test_migrations.py +153 -0
- bmlib-0.4.0/tests/test_openai_compat.py +299 -0
- bmlib-0.4.0/tests/test_openalex_fetcher.py +596 -0
- bmlib-0.4.0/tests/test_pdf_converter.py +182 -0
- bmlib-0.4.0/tests/test_publications.py +1102 -0
- bmlib-0.4.0/tests/test_pubmed_fetcher.py +591 -0
- bmlib-0.4.0/tests/test_quality.py +257 -0
- bmlib-0.4.0/tests/test_registry.py +189 -0
- bmlib-0.4.0/tests/test_sync.py +518 -0
- bmlib-0.4.0/tests/test_templates.py +115 -0
- bmlib-0.4.0/tests/test_text_utils.py +191 -0
- bmlib-0.4.0/tests/test_transparency.py +869 -0
bmlib-0.4.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to bmlib are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); bmlib follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.4.0] — 2026-07-19
|
|
8
|
+
|
|
9
|
+
### Changed — breaking
|
|
10
|
+
|
|
11
|
+
- **`bmlib.db.transaction()` no longer commits when joining an open
|
|
12
|
+
transaction** (SQLite savepoint path). Previously, a `transaction(conn)`
|
|
13
|
+
block entered while the connection already held uncommitted writes would
|
|
14
|
+
call `conn.commit()` on success, committing the caller's pending writes
|
|
15
|
+
along with its own. Now the block joins via a savepoint and the owner of
|
|
16
|
+
the enclosing transaction commits. Code that relied on `transaction()` as
|
|
17
|
+
a durability checkpoint after bare `execute()` writes must commit
|
|
18
|
+
explicitly (or wrap the whole batch in an outer `transaction()`). The same
|
|
19
|
+
applies to `run_migrations()` when called with a transaction already open.
|
|
20
|
+
On PostgreSQL the old connection-wide commit behaviour is unchanged (no
|
|
21
|
+
savepoint nesting is implemented there).
|
|
22
|
+
- **`bmlib.publications.sync()` buffers each day's records and stores them
|
|
23
|
+
after the fetch.** The `on_record` callback now fires while the fetcher
|
|
24
|
+
streams, *before* the record is stored — callbacks must not expect to read
|
|
25
|
+
the record back from the database. Writes cost one commit per day instead
|
|
26
|
+
of one per statement, and SQLite's write lock is no longer held across
|
|
27
|
+
network I/O; in exchange, a day's records are held in memory during the
|
|
28
|
+
fetch.
|
|
29
|
+
- `TransparencyAnalyzer._check_europepmc()` now returns a 6-tuple (adds
|
|
30
|
+
`industry_coi`).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- transparency: industry conflict-of-interest detection in full-text
|
|
35
|
+
COI/disclosure statements — negation-aware, scoped to the COI region, with
|
|
36
|
+
a guard for non-industry contexts (university/government employment,
|
|
37
|
+
editorial boards). ORs into `industry_funding_detected` at moderate
|
|
38
|
+
confidence (#7).
|
|
39
|
+
- llm: embedding support in the LLM abstraction layer (`LLMClient.embed()`,
|
|
40
|
+
`EmbeddingResponse`). Implemented by the Ollama provider; other providers
|
|
41
|
+
inherit `BaseProvider.embed()`, which raises `NotImplementedError`.
|
|
42
|
+
- llm: tool calling — `LLMClient.chat()` accepts `tools` and `tool_choice`,
|
|
43
|
+
with the new `LLMToolDefinition` and `LLMToolCall` data types,
|
|
44
|
+
`LLMResponse.tool_calls`, and `LLMMessage.tool_calls` / `tool_call_id` for
|
|
45
|
+
multi-turn tool conversations. Implemented for Anthropic, Ollama, and the
|
|
46
|
+
OpenAI-compatible providers (OpenAI, DeepSeek, Mistral, Gemini). Passing
|
|
47
|
+
`tools` to a provider that does not support them raises
|
|
48
|
+
`NotImplementedError` before any network call. Ollama accepts but ignores
|
|
49
|
+
`tool_choice` — its native API has no equivalent.
|
|
50
|
+
- llm: `supports_tools()` — public probe for the tool-calling allowlist, so
|
|
51
|
+
callers can test support for a provider name or `"provider:model"` string
|
|
52
|
+
without catching `NotImplementedError`.
|
|
53
|
+
- db: nested `transaction()` blocks on SQLite are now composable (savepoint
|
|
54
|
+
join; the outer block owns the commit).
|
|
55
|
+
- llm: `bmlib.llm.json_repair` — repairs malformed LLM JSON (single quotes,
|
|
56
|
+
trailing/missing commas, unescaped control chars, truncation, unquoted
|
|
57
|
+
keys) via `repair_json()`, `safe_json_loads()`, `extract_and_repair_json()`.
|
|
58
|
+
`BaseAgent.parse_json()` now uses it as a last-resort fallback. Ported from
|
|
59
|
+
bmlibrarian.
|
|
60
|
+
- llm: `bmlib.llm.text_utils` — boundary-aware text chunking (`TextChunk`,
|
|
61
|
+
`TextChunker`, `chunk_text`) that never drops text, plus map-reduce /
|
|
62
|
+
rolling-summary long-document processing and document-text helpers. Ported
|
|
63
|
+
and consolidated from bmlibrarian's two chunkers.
|
|
64
|
+
- quality: `bmlib.quality.cochrane_models` and `cochrane_formatter` —
|
|
65
|
+
Cochrane-aligned nine-domain Risk-of-Bias models with judgement + rationale,
|
|
66
|
+
the full study-characteristics table, and Markdown/HTML renderers. A strict
|
|
67
|
+
superset of `BiasRisk`. Ported from bmlibrarian.
|
|
68
|
+
- quality: `bmlib.quality.extractors` and `scoring_models` — rule-based
|
|
69
|
+
(LLM-free) study-type detection with exclusion-context guarding and
|
|
70
|
+
sample-size scoring, producing `DimensionScore` audit trails. Ported from
|
|
71
|
+
bmlibrarian's paper_weight.
|
|
72
|
+
- fulltext: `bmlib.fulltext.pdf_converter` — pluggable PDF→text conversion
|
|
73
|
+
(`ConversionResult`, `PDFConverter`, `get_converter`, `list_converters`)
|
|
74
|
+
with a PyMuPDF backend behind the new optional `bmlib[pdf]` extra. Ported
|
|
75
|
+
from bmlibrarian.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- transparency: a JATS-tagged COI section now counts as `coi_disclosed=True`
|
|
80
|
+
even when its wording contains no cue phrase — the tag is structural proof
|
|
81
|
+
of a disclosure; the cue-phrase scan remains the fallback for untagged text
|
|
82
|
+
(#13).
|
|
83
|
+
- llm: `list_models()` on the Anthropic and OpenAI-compatible providers now
|
|
84
|
+
returns a copy of the cached model list; mutating a returned list no longer
|
|
85
|
+
corrupts the cache for subsequent callers (#12).
|
|
86
|
+
- publications: batched database commits — one commit per stored publication
|
|
87
|
+
and one per synced day instead of one per statement (#8).
|
|
88
|
+
- llm: `get_llm_client()` singleton creation is now thread-safe; the
|
|
89
|
+
openai-compat `list_models()` caches a successful-but-empty response for
|
|
90
|
+
the TTL instead of re-hitting the API every call; the Anthropic provider
|
|
91
|
+
warns (once per model per instance) when an unknown model id falls back to
|
|
92
|
+
estimated pricing (#9).
|
|
93
|
+
- fulltext: `FullTextCache` sanitizes identifiers internally, so a raw DOI or
|
|
94
|
+
path-traversal string cannot write outside the cache directory;
|
|
95
|
+
already-safe identifiers keep their exact filenames (#9).
|
|
96
|
+
- publications: the OpenAlex fetcher tolerates a `"meta": null` page instead
|
|
97
|
+
of raising `AttributeError` (#9).
|
|
98
|
+
- agents: `chat_json()` now fails fast with the real cause when a response is
|
|
99
|
+
truncated at the `max_tokens` ceiling, instead of reporting a generic
|
|
100
|
+
"unparseable response". At `temperature == 0.0` it raises immediately —
|
|
101
|
+
greedy sampling reproduces the identical truncation, so retrying only pays
|
|
102
|
+
for it again; above 0.0 it retries, since a different sample may fit. A
|
|
103
|
+
response that is complete JSON despite hitting the ceiling is returned
|
|
104
|
+
rather than rejected. Truncation detection covers Anthropic's
|
|
105
|
+
`stop_reason="max_tokens"` and the OpenAI-compatible `"length"`, and empty
|
|
106
|
+
responses are now treated as retryable transport errors.
|
|
107
|
+
- fulltext: cache keys are now `{sanitized}_{sha1[:10]}`, so DOIs that
|
|
108
|
+
differed only in characters the sanitizer collapsed (for example
|
|
109
|
+
`10.1/a:b` and `10.1/a/b`) no longer share a cache file and serve each
|
|
110
|
+
other's full text.
|
|
111
|
+
- fulltext: JATS parsing no longer drops abstract sections, mislabels table
|
|
112
|
+
headers, or loses figure and table captions.
|
|
113
|
+
- fulltext: the final fallback result is labelled `source="pubmed"` rather
|
|
114
|
+
than `"doi"` when it resolves to a PubMed URL.
|
|
115
|
+
- db: `create_tables()` no longer uses SQLite's `executescript()`, whose
|
|
116
|
+
implicit `COMMIT` broke a surrounding `transaction()` block and left
|
|
117
|
+
migrations non-atomic. Statements are split and executed individually.
|
|
118
|
+
- llm: provider names are normalised to lowercase in client routing, so
|
|
119
|
+
`"Anthropic:claude-..."` resolves like `"anthropic:claude-..."`.
|
|
120
|
+
- llm: JSON extraction handles responses containing multiple objects and
|
|
121
|
+
braces inside strings.
|
|
122
|
+
- llm: OpenAI reasoning models receive `max_completion_tokens` instead of the
|
|
123
|
+
rejected `max_tokens`.
|
|
124
|
+
- llm: the Ollama provider no longer clobbers a legitimate zero token count
|
|
125
|
+
when recording usage.
|
|
126
|
+
- quality: the Tier 1 metadata filter no longer misclassifies study designs
|
|
127
|
+
from ambiguous PubMed publication types, and `QualityAssessment` records
|
|
128
|
+
`is_randomized` from the new `DESIGN_TO_RANDOMIZED` mapping, so
|
|
129
|
+
`QualityFilter.require_randomization` recognises a Tier 1/2 RCT instead of
|
|
130
|
+
rejecting it.
|
|
131
|
+
- transparency: conflict-of-interest detection and the ClinicalTrials.gov
|
|
132
|
+
posted-results check were both under-detecting — the latter requested
|
|
133
|
+
`ResultsSection` but read `resultsSection`. The analyzer now returns an
|
|
134
|
+
`UNKNOWN` risk level with score 0 when no external API was reachable,
|
|
135
|
+
rather than letting an all-zero score read as HIGH risk.
|
|
136
|
+
- publications: full-text sources are no longer silently dropped during sync.
|
|
137
|
+
- publications: the bioRxiv fetcher records the correct PDF version, and the
|
|
138
|
+
PubMed fetcher handles non-numeric month names in publication dates.
|
|
139
|
+
- publications: `fetch_pubmed()` now populates `publication_types` from
|
|
140
|
+
`PublicationTypeList`. It never did, yet the free Tier 1 quality filter
|
|
141
|
+
classifies study design from exactly that field — so every synced PubMed
|
|
142
|
+
record skipped the free tier and fell through to the paid LLM classifier.
|
|
143
|
+
- publications: `register_source()` now registers the built-ins before
|
|
144
|
+
writing its entry, so registering under a built-in name actually overrides
|
|
145
|
+
it. Previously an override installed before the first lookup was silently
|
|
146
|
+
reverted the moment lazy registration ran.
|
|
147
|
+
- publications: the three built-in fetchers annotated `on_record` as
|
|
148
|
+
`Callable[[dict], None]` while passing a `FetchedRecord`; the annotations
|
|
149
|
+
now match the behaviour, which is unchanged.
|
|
150
|
+
- transparency: `TransparencyAnalyzer` is now safe to share across threads,
|
|
151
|
+
which is what makes `settings.max_concurrent_analyses` usable. Rate-limit
|
|
152
|
+
state is mutex-guarded (the interval throttles a shared remote API, so it
|
|
153
|
+
must apply across threads); reachability is held per-thread, since it
|
|
154
|
+
describes a single analysis. Previously two concurrent `analyze()` calls
|
|
155
|
+
contaminated each other: a thread whose APIs were all down inherited a
|
|
156
|
+
concurrent thread's success and was scored 0 / HIGH instead of UNKNOWN,
|
|
157
|
+
wrongly triggering a tier downgrade.
|
|
158
|
+
- transparency: `settings.enabled` is now honoured. `enabled=False`
|
|
159
|
+
short-circuits `analyze()` before any HTTP — and before the `httpx` import,
|
|
160
|
+
so a disabled analyzer does not require the optional extra. It was
|
|
161
|
+
previously ignored and analysis ran regardless.
|
|
162
|
+
- transparency: `TransparencyResult.to_dict()` now round-trips
|
|
163
|
+
`full_text_analyzed`. Dropping it made a persisted `coi_disclosed=False`
|
|
164
|
+
uninterpretable, since that value only means "scanned and absent" when the
|
|
165
|
+
full text really was read.
|
|
166
|
+
- transparency: removed the unreachable `resultsSection` fallback in
|
|
167
|
+
`_check_trial_results()`. The request is narrowed to `fields=hasResults`,
|
|
168
|
+
so no other key can come back; the fallback implied a robustness it could
|
|
169
|
+
not provide.
|
|
170
|
+
- db: `create_tables()` now parses `CREATE TRIGGER ... BEGIN ... END;`.
|
|
171
|
+
Splitting on the semicolons inside a trigger body handed SQLite a fragment
|
|
172
|
+
and raised `OperationalError: incomplete input`. Nesting counts
|
|
173
|
+
`BEGIN`/`CASE` against `END`, so a `CASE ... END` inside a body does not
|
|
174
|
+
close it early and a bare `BEGIN;` is not mistaken for one.
|
|
175
|
+
|
|
176
|
+
### Documented
|
|
177
|
+
|
|
178
|
+
- transparency: `TransparencySettings` now states which fields the analyzer
|
|
179
|
+
honours and which are orchestration hints for the calling application
|
|
180
|
+
(`filtering_enabled`, `max_concurrent_analyses`, `cache_results` — the
|
|
181
|
+
library analyses one document per call and does no filtering, threading,
|
|
182
|
+
or caching of its own).
|
|
183
|
+
- transparency: `outcome_switching_detected` is documented as reserved and
|
|
184
|
+
always `False`. Deciding it means comparing a trial's pre-registered
|
|
185
|
+
primary outcomes against those reported; it is kept in the schema so
|
|
186
|
+
persisted results need no migration when detection lands.
|
|
187
|
+
|
|
188
|
+
## [0.3.0]
|
|
189
|
+
|
|
190
|
+
Never released. The version string was bumped in-tree when embedding support
|
|
191
|
+
landed, but no release was cut; those changes ship as part of 0.4.0 above.
|
|
192
|
+
|
|
193
|
+
## [0.2.1] and earlier
|
|
194
|
+
|
|
195
|
+
No changelog was kept; see the git history.
|
bmlib-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2024-2026 Dr Horst Herb
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
bmlib-0.4.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# README and LICENSE are included automatically via `readme` and
|
|
2
|
+
# `license-files` in pyproject.toml. The changelog is not, and a source
|
|
3
|
+
# distribution should carry the release history it corresponds to.
|
|
4
|
+
include CHANGELOG.md
|
|
5
|
+
|
|
6
|
+
# Never ship caches or build leftovers that happen to be in the tree.
|
|
7
|
+
global-exclude *.py[cod]
|
|
8
|
+
global-exclude __pycache__
|
|
9
|
+
prune .github
|
|
10
|
+
prune .claude
|
bmlib-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bmlib
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Shared library for biomedical literature tools — LLM abstraction, quality assessment, transparency analysis, and database utilities
|
|
5
|
+
Author-email: Horst Herb <my.list.subscriptions@gmail.com>
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/hherb/bmlib
|
|
8
|
+
Project-URL: Repository, https://github.com/hherb/bmlib
|
|
9
|
+
Project-URL: Documentation, https://github.com/hherb/bmlib/blob/main/docs/manual/index.md
|
|
10
|
+
Project-URL: Changelog, https://github.com/hherb/bmlib/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/hherb/bmlib/issues
|
|
12
|
+
Keywords: biomedical,llm,quality-assessment,literature,systematic-review,pubmed,evidence-based-medicine
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: jinja2>=3.1
|
|
29
|
+
Provides-Extra: anthropic
|
|
30
|
+
Requires-Dist: anthropic>=0.30; extra == "anthropic"
|
|
31
|
+
Provides-Extra: ollama
|
|
32
|
+
Requires-Dist: ollama>=0.3; extra == "ollama"
|
|
33
|
+
Provides-Extra: openai
|
|
34
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
35
|
+
Provides-Extra: postgresql
|
|
36
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == "postgresql"
|
|
37
|
+
Provides-Extra: transparency
|
|
38
|
+
Requires-Dist: httpx>=0.25; extra == "transparency"
|
|
39
|
+
Provides-Extra: publications
|
|
40
|
+
Requires-Dist: httpx>=0.25; extra == "publications"
|
|
41
|
+
Provides-Extra: pdf
|
|
42
|
+
Requires-Dist: pymupdf>=1.23; extra == "pdf"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff; extra == "dev"
|
|
47
|
+
Provides-Extra: all
|
|
48
|
+
Requires-Dist: bmlib[anthropic,ollama,openai,pdf,postgresql,publications,transparency]; extra == "all"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# bmlib
|
|
52
|
+
|
|
53
|
+
[](https://github.com/hherb/bmlib/actions/workflows/ci.yml)
|
|
54
|
+
|
|
55
|
+
Shared Python library for biomedical literature tools — LLM abstraction, quality assessment, transparency analysis, full-text retrieval, publication ingestion, and database utilities.
|
|
56
|
+
|
|
57
|
+
**Version:** 0.4.0 | **License:** AGPL-3.0-or-later | **Python:** >=3.11
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Core (only jinja2 dependency)
|
|
63
|
+
pip install bmlib
|
|
64
|
+
|
|
65
|
+
# Editable install with all extras
|
|
66
|
+
uv pip install -e ".[all,dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Optional dependency groups
|
|
70
|
+
|
|
71
|
+
| Group | Install command | Provides |
|
|
72
|
+
|-------|----------------|----------|
|
|
73
|
+
| `anthropic` | `pip install bmlib[anthropic]` | Anthropic Claude LLM provider |
|
|
74
|
+
| `ollama` | `pip install bmlib[ollama]` | Ollama local LLM provider |
|
|
75
|
+
| `openai` | `pip install bmlib[openai]` | OpenAI, DeepSeek, Mistral, Gemini, and OpenAI-compatible providers |
|
|
76
|
+
| `postgresql` | `pip install bmlib[postgresql]` | PostgreSQL database backend |
|
|
77
|
+
| `transparency` | `pip install bmlib[transparency]` | Transparency analysis (httpx) |
|
|
78
|
+
| `publications` | `pip install bmlib[publications]` | Publication ingestion and sync (httpx) |
|
|
79
|
+
| `pdf` | `pip install bmlib[pdf]` | PDF → text conversion (pymupdf) |
|
|
80
|
+
| `dev` | `pip install bmlib[dev]` | pytest, pytest-cov, ruff |
|
|
81
|
+
| `all` | `pip install bmlib[all]` | Every runtime extra above (**not** `dev`) |
|
|
82
|
+
|
|
83
|
+
## Modules
|
|
84
|
+
|
|
85
|
+
| Module | Description |
|
|
86
|
+
|--------|-------------|
|
|
87
|
+
| **bmlib.db** | Thin database abstraction (SQLite + PostgreSQL) with pure functions over DB-API connections |
|
|
88
|
+
| **bmlib.llm** | Unified LLM client with pluggable providers (Anthropic, OpenAI, Ollama, DeepSeek, Mistral, Gemini) — chat, tool calling, embeddings, JSON repair, and text chunking |
|
|
89
|
+
| **bmlib.templates** | Jinja2-based prompt template engine with user-override directory fallback |
|
|
90
|
+
| **bmlib.agents** | Base agent class for LLM-driven tasks with template rendering and JSON parsing |
|
|
91
|
+
| **bmlib.quality** | 3-tier quality assessment pipeline for biomedical publications (metadata → LLM classifier → deep assessment), plus Cochrane risk-of-bias models and rule-based extractors |
|
|
92
|
+
| **bmlib.transparency** | Multi-API transparency and bias analysis (CrossRef, Europe PMC, OpenAlex, ClinicalTrials.gov) |
|
|
93
|
+
| **bmlib.publications** | Publication ingestion from PubMed, bioRxiv, medRxiv, and OpenAlex with deduplication and sync |
|
|
94
|
+
| **bmlib.fulltext** | Full-text retrieval (Europe PMC → Unpaywall → DOI), JATS XML parsing, PDF → text conversion, and disk-based caching |
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
### Database
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from bmlib.db import connect_sqlite, execute, fetch_all, transaction
|
|
102
|
+
|
|
103
|
+
conn = connect_sqlite("~/.myapp/data.db")
|
|
104
|
+
with transaction(conn):
|
|
105
|
+
execute(conn, "INSERT INTO papers (doi, title) VALUES (?, ?)", ("10.1101/x", "A paper"))
|
|
106
|
+
rows = fetch_all(conn, "SELECT * FROM papers")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### LLM
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from bmlib.llm import LLMClient, LLMMessage
|
|
113
|
+
|
|
114
|
+
client = LLMClient(default_provider="ollama")
|
|
115
|
+
response = client.chat(
|
|
116
|
+
messages=[LLMMessage(role="user", content="Summarise this paper.")],
|
|
117
|
+
model="ollama:medgemma4B_it_q8",
|
|
118
|
+
)
|
|
119
|
+
print(response.content)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Model strings use the format `"provider:model_name"`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
"anthropic:claude-sonnet-4-20250514"
|
|
126
|
+
"openai:gpt-4o"
|
|
127
|
+
"ollama:medgemma4B_it_q8"
|
|
128
|
+
"deepseek:deepseek-chat"
|
|
129
|
+
"mistral:mistral-large-latest"
|
|
130
|
+
"gemini:gemini-2.0-flash"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Tool Calling
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from bmlib.llm import LLMClient, LLMMessage, LLMToolDefinition
|
|
137
|
+
|
|
138
|
+
search = LLMToolDefinition(
|
|
139
|
+
name="search_pubmed",
|
|
140
|
+
description="Search PubMed for articles matching a query.",
|
|
141
|
+
parameters={
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {"query": {"type": "string"}},
|
|
144
|
+
"required": ["query"],
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
client = LLMClient()
|
|
149
|
+
response = client.chat(
|
|
150
|
+
messages=[LLMMessage(role="user", content="Find recent trials on statins.")],
|
|
151
|
+
model="anthropic:claude-sonnet-4-20250514",
|
|
152
|
+
tools=[search],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for call in response.tool_calls or []:
|
|
156
|
+
print(call.name, call.arguments) # arguments is already a parsed dict
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To continue the conversation, append the assistant message (carrying
|
|
160
|
+
`tool_calls`) and one `role="tool"` message per call, each with the matching
|
|
161
|
+
`tool_call_id`, then send the whole list again.
|
|
162
|
+
|
|
163
|
+
### Long Documents
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from bmlib.llm import chunk_text, process_with_map_reduce
|
|
167
|
+
|
|
168
|
+
for chunk in chunk_text(paper_text, chunk_size=8000, overlap=200):
|
|
169
|
+
print(chunk.chunk_index, chunk.size)
|
|
170
|
+
|
|
171
|
+
summary = process_with_map_reduce(
|
|
172
|
+
paper_text,
|
|
173
|
+
map_fn=lambda part: summarise(part),
|
|
174
|
+
reduce_fn=lambda parts: summarise("\n".join(parts)),
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Publication Sync
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from datetime import date
|
|
182
|
+
from bmlib.db import connect_sqlite
|
|
183
|
+
from bmlib.publications import sync
|
|
184
|
+
|
|
185
|
+
conn = connect_sqlite("publications.db")
|
|
186
|
+
report = sync(
|
|
187
|
+
conn,
|
|
188
|
+
sources=["pubmed", "biorxiv"],
|
|
189
|
+
date_from=date(2025, 1, 1),
|
|
190
|
+
date_to=date(2025, 1, 7),
|
|
191
|
+
email="researcher@example.com",
|
|
192
|
+
)
|
|
193
|
+
print(f"Added: {report.records_added}, Merged: {report.records_merged}")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Full-Text Retrieval
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from bmlib.fulltext import FullTextService
|
|
200
|
+
|
|
201
|
+
service = FullTextService(email="researcher@example.com")
|
|
202
|
+
|
|
203
|
+
# Passing identifier= enables the built-in disk cache (platform default dir).
|
|
204
|
+
result = service.fetch_fulltext(
|
|
205
|
+
pmc_id="PMC7614751", doi="10.1234/example", identifier="PMC7614751"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result.html:
|
|
209
|
+
print(result.html[:200])
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Quality Assessment
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from bmlib.llm import LLMClient
|
|
216
|
+
from bmlib.quality import QualityManager
|
|
217
|
+
|
|
218
|
+
llm = LLMClient()
|
|
219
|
+
manager = QualityManager(
|
|
220
|
+
llm=llm,
|
|
221
|
+
classifier_model="anthropic:claude-3-haiku-20240307",
|
|
222
|
+
assessor_model="anthropic:claude-sonnet-4-20250514",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
assessment = manager.assess(
|
|
226
|
+
title="A Randomized Controlled Trial of ...",
|
|
227
|
+
abstract="We conducted a double-blind RCT ...",
|
|
228
|
+
publication_types=["Randomized Controlled Trial"],
|
|
229
|
+
)
|
|
230
|
+
print(assessment.study_design, assessment.quality_tier)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Transparency Analysis
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from bmlib.transparency import TransparencyAnalyzer
|
|
237
|
+
|
|
238
|
+
analyzer = TransparencyAnalyzer(email="researcher@example.com")
|
|
239
|
+
result = analyzer.analyze("doc-001", doi="10.1038/s41586-024-00001-0")
|
|
240
|
+
print(result.transparency_score, result.risk_level)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Install with dev dependencies
|
|
247
|
+
uv pip install -e ".[all,dev]"
|
|
248
|
+
|
|
249
|
+
# Run tests
|
|
250
|
+
uv run pytest tests/ -v
|
|
251
|
+
|
|
252
|
+
# Lint and format
|
|
253
|
+
uv run ruff check .
|
|
254
|
+
uv run ruff format --check .
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Documentation
|
|
258
|
+
|
|
259
|
+
Full API documentation is available in [docs/manual/](docs/manual/index.md).
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
AGPL-3.0-or-later
|