opencode-bioresearcher 1.8.0 → 1.9.0

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.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """Shared NCBI PubMed esummary client for the bioresearcher-deep-research scripts.
3
+
4
+ Extracted verbatim from vet-references.py (DRY): both vet-references.py and
5
+ evidence-ledger.py import fetch_ncbi_summaries from here.
6
+
7
+ Zero external dependencies (pure Python standard library). Fail-safe: on
8
+ network or API failure, returns whatever was fetched (possibly {}); callers
9
+ treat missing records as "unverified" and never block on network errors.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ import time
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+
20
+ NCBI_ESUMMARY_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
21
+ USER_AGENT = "bioresearcher-skills/1.2 (evidence tools; +https://github.com/bioresearcher-agent)"
22
+
23
+
24
+ def fetch_ncbi_summaries(pmids: list, timeout: float = 15.0) -> dict:
25
+ """Fetch esummary JSON for a list of PMIDs with rate-limiting and exponential backoff."""
26
+ if not pmids:
27
+ return {}
28
+
29
+ api_key = os.environ.get("NCBI_API_KEY", "").strip()
30
+ email = os.environ.get("NCBI_EMAIL", "bioresearcher-agent@noreply.github.com").strip()
31
+ min_interval = 0.100 if api_key else 0.334
32
+
33
+ results: dict = {}
34
+ batch_size = 100
35
+
36
+ for i in range(0, len(pmids), batch_size):
37
+ if i > 0:
38
+ time.sleep(min_interval)
39
+
40
+ batch = pmids[i : i + batch_size]
41
+ params = {
42
+ "db": "pubmed",
43
+ "id": ",".join(batch),
44
+ "retmode": "json",
45
+ "tool": "bioresearcher",
46
+ "email": email,
47
+ }
48
+ if api_key:
49
+ params["api_key"] = api_key
50
+
51
+ data = urllib.parse.urlencode(params).encode("utf-8")
52
+ req = urllib.request.Request(
53
+ NCBI_ESUMMARY_URL,
54
+ data=data,
55
+ headers={"User-Agent": USER_AGENT},
56
+ )
57
+
58
+ for attempt in range(4):
59
+ try:
60
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
61
+ payload = json.loads(resp.read().decode("utf-8"))
62
+ result_data = payload.get("result", {})
63
+ for uid in batch:
64
+ doc = result_data.get(uid)
65
+ if doc and isinstance(doc, dict) and "error" not in doc:
66
+ results[uid] = doc
67
+ break
68
+ except urllib.error.HTTPError as e:
69
+ if e.code in (429, 500, 502, 503, 504) and attempt < 3:
70
+ retry_after_hdr = e.headers.get("Retry-After")
71
+ try:
72
+ retry_after = float(retry_after_hdr) if retry_after_hdr else float(1.5 * (2**attempt))
73
+ except ValueError:
74
+ retry_after = float(1.5 * (2**attempt))
75
+ time.sleep(retry_after)
76
+ continue
77
+ sys.stderr.write(f"[ncbi-esummary] HTTP {e.code} querying NCBI for batch {i}: {e.reason}\n")
78
+ break
79
+ except Exception as e:
80
+ if attempt < 3:
81
+ time.sleep(1.0 * (2**attempt))
82
+ continue
83
+ sys.stderr.write(f"[ncbi-esummary] Network error querying NCBI: {e}\n")
84
+ break
85
+
86
+ return results
@@ -16,14 +16,10 @@ import json
16
16
  import os
17
17
  import re
18
18
  import sys
19
- import time
20
- import urllib.error
21
- import urllib.parse
22
- import urllib.request
23
19
  from pathlib import Path
24
20
 
25
- NCBI_ESUMMARY_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
26
- USER_AGENT = "bioresearcher-skills/1.2 (vet-references; +https://github.com/bioresearcher-agent)"
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+ from ncbi_esummary import fetch_ncbi_summaries # noqa: E402,F401 (shared module, extracted verbatim)
27
23
 
28
24
  REF_SECTION_RE = re.compile(
29
25
  r'(?m)^(#{2,3}\s+(?:\d+[\.\s]+)?(?:References?|Bibliography|Literature Cited|Citations)\b.*?)(?=\n#{1,2}\s+|\Z)',
@@ -37,71 +33,6 @@ PMID_RE = re.compile(r'\bPMID[:\s]+\[?(\d{4,9})\]?', re.IGNORECASE)
37
33
  DOI_RE = re.compile(r'(?:DOI[:\s]+|https?://(?:dx\.)?doi\.org/)?\b(10\.\d{4,9}/[^\s\]\)]+)', re.IGNORECASE)
38
34
 
39
35
 
40
- def fetch_ncbi_summaries(pmids: list[str], timeout: float = 15.0) -> dict[str, dict]:
41
- """Fetch esummary JSON for a list of PMIDs with rate-limiting and exponential backoff."""
42
- if not pmids:
43
- return {}
44
-
45
- api_key = os.environ.get("NCBI_API_KEY", "").strip()
46
- email = os.environ.get("NCBI_EMAIL", "bioresearcher-agent@noreply.github.com").strip()
47
- min_interval = 0.100 if api_key else 0.334
48
-
49
- results: dict[str, dict] = {}
50
- batch_size = 100
51
-
52
- for i in range(0, len(pmids), batch_size):
53
- if i > 0:
54
- time.sleep(min_interval)
55
-
56
- batch = pmids[i : i + batch_size]
57
- params = {
58
- "db": "pubmed",
59
- "id": ",".join(batch),
60
- "retmode": "json",
61
- "tool": "bioresearcher",
62
- "email": email,
63
- }
64
- if api_key:
65
- params["api_key"] = api_key
66
-
67
- data = urllib.parse.urlencode(params).encode("utf-8")
68
- req = urllib.request.Request(
69
- NCBI_ESUMMARY_URL,
70
- data=data,
71
- headers={"User-Agent": USER_AGENT},
72
- )
73
-
74
- for attempt in range(4):
75
- try:
76
- with urllib.request.urlopen(req, timeout=timeout) as resp:
77
- payload = json.loads(resp.read().decode("utf-8"))
78
- result_data = payload.get("result", {})
79
- for uid in batch:
80
- doc = result_data.get(uid)
81
- if doc and isinstance(doc, dict) and "error" not in doc:
82
- results[uid] = doc
83
- break
84
- except urllib.error.HTTPError as e:
85
- if e.code in (429, 500, 502, 503, 504) and attempt < 3:
86
- retry_after_hdr = e.headers.get("Retry-After")
87
- try:
88
- retry_after = float(retry_after_hdr) if retry_after_hdr else float(1.5 * (2**attempt))
89
- except ValueError:
90
- retry_after = float(1.5 * (2**attempt))
91
- time.sleep(retry_after)
92
- continue
93
- sys.stderr.write(f"[vet-references] HTTP {e.code} querying NCBI for batch {i}: {e.reason}\n")
94
- break
95
- except Exception as e:
96
- if attempt < 3:
97
- time.sleep(1.0 * (2**attempt))
98
- continue
99
- sys.stderr.write(f"[vet-references] Network error querying NCBI: {e}\n")
100
- break
101
-
102
- return results
103
-
104
-
105
36
  def build_pub_locator(doc: dict) -> str:
106
37
  """Build canonical Year;Volume(Issue):Pages string from NCBI esummary."""
107
38
  pubdate = doc.get("pubdate", "")