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.
Files changed (91) hide show
  1. bmlib-0.4.0/CHANGELOG.md +195 -0
  2. bmlib-0.4.0/LICENSE +17 -0
  3. bmlib-0.4.0/MANIFEST.in +10 -0
  4. bmlib-0.4.0/PKG-INFO +263 -0
  5. bmlib-0.4.0/README.md +213 -0
  6. bmlib-0.4.0/bmlib/__init__.py +19 -0
  7. bmlib-0.4.0/bmlib/agents/__init__.py +21 -0
  8. bmlib-0.4.0/bmlib/agents/base.py +269 -0
  9. bmlib-0.4.0/bmlib/db/__init__.py +57 -0
  10. bmlib-0.4.0/bmlib/db/connection.py +100 -0
  11. bmlib-0.4.0/bmlib/db/migrations.py +141 -0
  12. bmlib-0.4.0/bmlib/db/operations.py +213 -0
  13. bmlib-0.4.0/bmlib/db/transactions.py +84 -0
  14. bmlib-0.4.0/bmlib/fulltext/__init__.py +60 -0
  15. bmlib-0.4.0/bmlib/fulltext/cache.py +172 -0
  16. bmlib-0.4.0/bmlib/fulltext/jats_parser.py +1029 -0
  17. bmlib-0.4.0/bmlib/fulltext/models.py +198 -0
  18. bmlib-0.4.0/bmlib/fulltext/pdf_converter.py +288 -0
  19. bmlib-0.4.0/bmlib/fulltext/service.py +390 -0
  20. bmlib-0.4.0/bmlib/llm/__init__.py +79 -0
  21. bmlib-0.4.0/bmlib/llm/client.py +393 -0
  22. bmlib-0.4.0/bmlib/llm/data_types.py +198 -0
  23. bmlib-0.4.0/bmlib/llm/json_repair.py +548 -0
  24. bmlib-0.4.0/bmlib/llm/providers/__init__.py +139 -0
  25. bmlib-0.4.0/bmlib/llm/providers/anthropic.py +467 -0
  26. bmlib-0.4.0/bmlib/llm/providers/base.py +182 -0
  27. bmlib-0.4.0/bmlib/llm/providers/deepseek.py +56 -0
  28. bmlib-0.4.0/bmlib/llm/providers/gemini.py +78 -0
  29. bmlib-0.4.0/bmlib/llm/providers/mistral.py +71 -0
  30. bmlib-0.4.0/bmlib/llm/providers/ollama.py +511 -0
  31. bmlib-0.4.0/bmlib/llm/providers/openai_compat.py +417 -0
  32. bmlib-0.4.0/bmlib/llm/providers/openai_provider.py +81 -0
  33. bmlib-0.4.0/bmlib/llm/text_utils.py +361 -0
  34. bmlib-0.4.0/bmlib/llm/token_tracker.py +167 -0
  35. bmlib-0.4.0/bmlib/llm/utils.py +84 -0
  36. bmlib-0.4.0/bmlib/publications/__init__.py +67 -0
  37. bmlib-0.4.0/bmlib/publications/fetchers/__init__.py +37 -0
  38. bmlib-0.4.0/bmlib/publications/fetchers/biorxiv.py +202 -0
  39. bmlib-0.4.0/bmlib/publications/fetchers/openalex.py +280 -0
  40. bmlib-0.4.0/bmlib/publications/fetchers/pubmed.py +400 -0
  41. bmlib-0.4.0/bmlib/publications/fetchers/registry.py +187 -0
  42. bmlib-0.4.0/bmlib/publications/models.py +321 -0
  43. bmlib-0.4.0/bmlib/publications/schema.py +84 -0
  44. bmlib-0.4.0/bmlib/publications/storage.py +384 -0
  45. bmlib-0.4.0/bmlib/publications/sync.py +442 -0
  46. bmlib-0.4.0/bmlib/py.typed +0 -0
  47. bmlib-0.4.0/bmlib/quality/__init__.py +81 -0
  48. bmlib-0.4.0/bmlib/quality/cochrane_formatter.py +380 -0
  49. bmlib-0.4.0/bmlib/quality/cochrane_models.py +599 -0
  50. bmlib-0.4.0/bmlib/quality/data_models.py +361 -0
  51. bmlib-0.4.0/bmlib/quality/extractors.py +487 -0
  52. bmlib-0.4.0/bmlib/quality/manager.py +175 -0
  53. bmlib-0.4.0/bmlib/quality/metadata_filter.py +161 -0
  54. bmlib-0.4.0/bmlib/quality/quality_agent.py +168 -0
  55. bmlib-0.4.0/bmlib/quality/scoring_models.py +140 -0
  56. bmlib-0.4.0/bmlib/quality/study_classifier.py +123 -0
  57. bmlib-0.4.0/bmlib/templates/__init__.py +36 -0
  58. bmlib-0.4.0/bmlib/templates/engine.py +119 -0
  59. bmlib-0.4.0/bmlib/transparency/__init__.py +38 -0
  60. bmlib-0.4.0/bmlib/transparency/analyzer.py +766 -0
  61. bmlib-0.4.0/bmlib/transparency/models.py +187 -0
  62. bmlib-0.4.0/bmlib.egg-info/PKG-INFO +263 -0
  63. bmlib-0.4.0/bmlib.egg-info/SOURCES.txt +89 -0
  64. bmlib-0.4.0/bmlib.egg-info/dependency_links.txt +1 -0
  65. bmlib-0.4.0/bmlib.egg-info/requires.txt +30 -0
  66. bmlib-0.4.0/bmlib.egg-info/top_level.txt +1 -0
  67. bmlib-0.4.0/pyproject.toml +94 -0
  68. bmlib-0.4.0/setup.cfg +4 -0
  69. bmlib-0.4.0/tests/test_agents.py +301 -0
  70. bmlib-0.4.0/tests/test_cochrane.py +245 -0
  71. bmlib-0.4.0/tests/test_db.py +244 -0
  72. bmlib-0.4.0/tests/test_extractors.py +243 -0
  73. bmlib-0.4.0/tests/test_fulltext_cache.py +143 -0
  74. bmlib-0.4.0/tests/test_fulltext_models.py +143 -0
  75. bmlib-0.4.0/tests/test_fulltext_service.py +545 -0
  76. bmlib-0.4.0/tests/test_jats_parser.py +157 -0
  77. bmlib-0.4.0/tests/test_json_repair.py +164 -0
  78. bmlib-0.4.0/tests/test_llm.py +300 -0
  79. bmlib-0.4.0/tests/test_llm_tools.py +926 -0
  80. bmlib-0.4.0/tests/test_migrations.py +153 -0
  81. bmlib-0.4.0/tests/test_openai_compat.py +299 -0
  82. bmlib-0.4.0/tests/test_openalex_fetcher.py +596 -0
  83. bmlib-0.4.0/tests/test_pdf_converter.py +182 -0
  84. bmlib-0.4.0/tests/test_publications.py +1102 -0
  85. bmlib-0.4.0/tests/test_pubmed_fetcher.py +591 -0
  86. bmlib-0.4.0/tests/test_quality.py +257 -0
  87. bmlib-0.4.0/tests/test_registry.py +189 -0
  88. bmlib-0.4.0/tests/test_sync.py +518 -0
  89. bmlib-0.4.0/tests/test_templates.py +115 -0
  90. bmlib-0.4.0/tests/test_text_utils.py +191 -0
  91. bmlib-0.4.0/tests/test_transparency.py +869 -0
@@ -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/>.
@@ -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
+ [![CI](https://github.com/hherb/bmlib/actions/workflows/ci.yml/badge.svg)](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