Spaces:
Sleeping
Sleeping
| """ | |
| PubMed E-utilities client. No API key needed (3 req/sec). | |
| Falls back gracefully when rate-limited. | |
| """ | |
| import time | |
| import requests | |
| import xml.etree.ElementTree as ET | |
| from typing import Optional | |
| from src.cache import get_cached, set_cached | |
| EUTILS_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils" | |
| _last_req = 0.0 | |
| def _rate_limit(): | |
| global _last_req | |
| wait = 0.35 - (time.time() - _last_req) | |
| if wait > 0: | |
| time.sleep(wait) | |
| _last_req = time.time() | |
| def _search(query: str, max_results: int = 3) -> Optional[list[str]]: | |
| _rate_limit() | |
| try: | |
| resp = requests.get(f"{EUTILS_BASE}/esearch.fcgi", params={ | |
| "db": "pubmed", "retmode": "json", "retmax": max_results, | |
| "sort": "relevance", "term": query, | |
| }, timeout=10) | |
| if resp.status_code == 429: | |
| return None | |
| resp.raise_for_status() | |
| return resp.json().get("esearchresult", {}).get("idlist", []) | |
| except Exception as e: | |
| print(f"PubMed search error: {e}") | |
| return None | |
| def _fetch(pmids: list[str]) -> list[dict]: | |
| if not pmids: | |
| return [] | |
| _rate_limit() | |
| try: | |
| resp = requests.get(f"{EUTILS_BASE}/efetch.fcgi", params={ | |
| "db": "pubmed", "id": ",".join(pmids), | |
| "retmode": "xml", "rettype": "abstract", | |
| }, timeout=10) | |
| resp.raise_for_status() | |
| articles = [] | |
| root = ET.fromstring(resp.text) | |
| for elem in root.findall(".//PubmedArticle"): | |
| a = _parse(elem) | |
| if a: | |
| articles.append(a) | |
| return articles | |
| except Exception as e: | |
| print(f"PubMed fetch error: {e}") | |
| return [] | |
| def _parse(elem) -> Optional[dict]: | |
| try: | |
| med = elem.find(".//MedlineCitation") | |
| art = med.find(".//Article") | |
| pmid = med.findtext("PMID", "") | |
| title = art.findtext("ArticleTitle", "") | |
| authors = [] | |
| al = art.find("AuthorList") | |
| if al is not None: | |
| for a in al.findall("Author")[:3]: | |
| last = a.findtext("LastName", "") | |
| init = a.findtext("Initials", "") | |
| if last: | |
| authors.append(f"{last} {init}".strip()) | |
| author_str = ", ".join(authors) | |
| if len(al.findall("Author") if al is not None else []) > 3: | |
| author_str += " et al." | |
| j = art.find("Journal") | |
| journal = j.findtext("ISOAbbreviation", "") if j is not None else "" | |
| pd = j.find(".//PubDate") if j is not None else None | |
| year = pd.findtext("Year", "") if pd is not None else "" | |
| abstract_parts = [] | |
| ab = art.find("Abstract") | |
| if ab is not None: | |
| for t in ab.findall("AbstractText"): | |
| label = t.get("Label", "") | |
| content = (t.text or "")[:400] | |
| abstract_parts.append(f"{label}: {content}" if label else content) | |
| doi = "" | |
| for id_el in elem.findall(".//ArticleId"): | |
| if id_el.get("IdType") == "doi": | |
| doi = id_el.text or "" | |
| break | |
| return { | |
| "pmid": pmid, "title": title, "authors": author_str, | |
| "journal": journal, "year": year, | |
| "abstract": " ".join(abstract_parts)[:800], | |
| "doi": doi, "url": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/", | |
| } | |
| except Exception: | |
| return None | |
| def search_food_health(ingredient: str, health_aspect: str = "", | |
| max_papers: int = 2) -> list[dict]: | |
| """Search PubMed for studies on a food's health effects. Cached.""" | |
| if health_aspect: | |
| query = f'({ingredient}) AND ({health_aspect}) AND (health OR nutrition)' | |
| else: | |
| query = f'({ingredient}) AND (health effects OR nutritional)' | |
| query += ' AND (Review[pt] OR Meta-Analysis[pt])' | |
| cache_key = f"lit:{ingredient}:{health_aspect}" | |
| cached = get_cached("literature_cache", cache_key) | |
| if cached is not None: | |
| return cached | |
| pmids = _search(query, max_results=max_papers) | |
| if not pmids: | |
| # broader retry without review filter | |
| pmids = _search(query.replace(' AND (Review[pt] OR Meta-Analysis[pt])', ''), | |
| max_results=max_papers) | |
| if not pmids: | |
| return [] | |
| articles = _fetch(pmids) | |
| set_cached("literature_cache", cache_key, articles, ttl_days=7) | |
| return articles | |
| def format_citation(a: dict) -> str: | |
| parts = [] | |
| if a.get("authors"): parts.append(a["authors"]) | |
| if a.get("year"): parts.append(f"({a['year']})") | |
| if a.get("title"): parts.append(f"{a['title'].rstrip('.')}.") | |
| if a.get("journal"): parts.append(f"{a['journal']}.") | |
| if a.get("doi"): parts.append(f"DOI: {a['doi']}") | |
| return " ".join(parts) | |
| def lookup_literature(ingredients: list[str], health_goal: str = "", | |
| papers_per: int = 2) -> tuple[dict, int]: | |
| """Look up literature for ingredients. Returns (results, failure_count).""" | |
| results = {} | |
| failures = 0 | |
| for ing in ingredients: | |
| articles = search_food_health(ing.strip(), health_goal, papers_per) | |
| if articles: | |
| results[ing] = articles | |
| else: | |
| failures += 1 | |
| results[ing] = [] | |
| return results, failures | |