1
0

pubmed_tool.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. PubMed 生物医学文献检索工具
  3. 通过 NCBI Entrez API (E-utilities) 检索 PubMed 数据库中的生物医学论文。
  4. 覆盖 3600 万+ 论文,是生物医学领域最权威的数据库。
  5. API 文档: https://www.ncbi.nlm.nih.gov/books/NBK25501/
  6. """
  7. import urllib.request
  8. import urllib.parse
  9. import urllib.error
  10. import ssl
  11. import xml.etree.ElementTree as ET
  12. from typing import Dict, Any, List
  13. from hello_agents.tools import Tool, ToolParameter, ToolResponse, ToolStatus
  14. # Windows SSL 兼容
  15. _ssl_ctx = ssl.create_default_context()
  16. _ssl_ctx.check_hostname = False
  17. _ssl_ctx.verify_mode = ssl.CERT_NONE
  18. class PubMedSearchTool(Tool):
  19. """PubMed 生物医学文献检索工具
  20. 通过 NCBI Entrez API 检索 PubMed/PMC 数据库。
  21. 覆盖医学、生物学、药学、护理学、公共卫生等生物医学全领域。
  22. """
  23. SEARCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
  24. FETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
  25. SUMMARY_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
  26. def __init__(self):
  27. super().__init__(
  28. name="pubmed_search",
  29. description="在 PubMed 数据库中检索生物医学论文。"
  30. "覆盖 3600 万+ 论文,涵盖医学、生物学、药学、护理学、"
  31. "公共卫生等所有生物医学领域。"
  32. "支持 MeSH 主题词搜索、作者、期刊、年份等筛选。"
  33. "适合医学研究、药物研发、临床实践等场景。"
  34. )
  35. def _search_pmids(self, query: str, max_results: int = 5,
  36. year_from: str = "", year_to: str = "") -> List[str]:
  37. """搜索返回 PMID 列表"""
  38. # 构建查询条件
  39. search_terms = [query.strip()]
  40. if year_from or year_to:
  41. from_year = year_from or "1900"
  42. to_year = year_to or "2026"
  43. search_terms.append(f"{from_year}:{to_year}[dp]")
  44. full_query = " AND ".join(search_terms)
  45. params = {
  46. "db": "pubmed",
  47. "term": full_query,
  48. "retmax": str(max_results),
  49. "retmode": "xml",
  50. "sort": "relevance"
  51. }
  52. url = f"{self.SEARCH_URL}?{urllib.parse.urlencode(params)}"
  53. req = urllib.request.Request(url, headers={"User-Agent": "PaperAssistant/1.0"})
  54. with urllib.request.urlopen(req, timeout=15, context=_ssl_ctx) as resp:
  55. root = ET.fromstring(resp.read().decode("utf-8"))
  56. id_list = root.find(".//IdList")
  57. if id_list is None:
  58. return []
  59. return [elem.text for elem in id_list.findall("Id")]
  60. def _fetch_summaries(self, pmids: List[str]) -> List[Dict[str, Any]]:
  61. """获取论文摘要信息"""
  62. if not pmids:
  63. return []
  64. params = {
  65. "db": "pubmed",
  66. "id": ",".join(pmids),
  67. "retmode": "xml"
  68. }
  69. url = f"{self.SUMMARY_URL}?{urllib.parse.urlencode(params)}"
  70. req = urllib.request.Request(url, headers={"User-Agent": "PaperAssistant/1.0"})
  71. with urllib.request.urlopen(req, timeout=15, context=_ssl_ctx) as resp:
  72. root = ET.fromstring(resp.read().decode("utf-8"))
  73. papers = []
  74. for doc in root.findall(".//DocSum"):
  75. paper = {
  76. "pmid": doc.find("Id").text if doc.find("Id") is not None else "",
  77. "title": "N/A",
  78. "authors": [],
  79. "pubdate": "N/A",
  80. "source": "N/A",
  81. "doi": "",
  82. }
  83. for item in doc.findall("Item"):
  84. name = item.get("Name", "")
  85. if name == "Title":
  86. paper["title"] = item.text or "N/A"
  87. elif name == "AuthorList":
  88. paper["authors"] = [a.text for a in item.findall("Item")
  89. if a.text]
  90. elif name == "PubDate":
  91. paper["pubdate"] = item.text or "N/A"
  92. elif name == "Source":
  93. paper["source"] = item.text or "N/A"
  94. elif name == "DOI":
  95. paper["doi"] = item.text or ""
  96. papers.append(paper)
  97. return papers
  98. def run(self, parameters: Dict[str, Any]) -> ToolResponse:
  99. keyword = parameters.get("keyword", "")
  100. author = parameters.get("author", "")
  101. max_results = min(parameters.get("max_results", 5), 20)
  102. year_from = parameters.get("year_from", "")
  103. year_to = parameters.get("year_to", "")
  104. if not keyword and not author:
  105. return ToolResponse.error(
  106. code="INVALID_PARAM",
  107. message="请至少提供关键词(keyword)或作者(author)"
  108. )
  109. # 构建查询
  110. query_parts = []
  111. if keyword:
  112. query_parts.append(keyword.strip())
  113. if author:
  114. query_parts.append(f'{author.strip()}[Author]')
  115. query = " AND ".join(query_parts)
  116. try:
  117. pmids = self._search_pmids(query, max_results, year_from, year_to)
  118. if not pmids:
  119. return ToolResponse.success(
  120. text=f"在 PubMed 中未找到匹配的论文。\n"
  121. f"建议:尝试更简短的关键词、使用 MeSH 主题词、"
  122. f"或检查拼写。查询: {query}",
  123. data={"count": 0, "papers": []}
  124. )
  125. papers = self._fetch_summaries(pmids)
  126. # 格式化输出
  127. lines = [f"在 PubMed 中找到 {len(papers)} 篇论文:\n"]
  128. for i, p in enumerate(papers, 1):
  129. authors_str = ", ".join(p["authors"][:3])
  130. if len(p["authors"]) > 3:
  131. authors_str += " et al."
  132. lines.append(f"### {i}. {p['title']}")
  133. if authors_str:
  134. lines.append(f"> 作者: {authors_str}")
  135. lines.append(f"> PMID: {p['pmid']} | 发表: {p['pubdate']} | {p['source']}")
  136. if p.get("doi"):
  137. lines.append(f"> [DOI](https://doi.org/{p['doi']}) | "
  138. f"[PubMed](https://pubmed.ncbi.nlm.nih.gov/{p['pmid']}/)")
  139. lines.append("")
  140. lines.append(f"---")
  141. lines.append(f"*数据来源: PubMed/NCBI*")
  142. return ToolResponse.success(
  143. text="\n".join(lines),
  144. data={"count": len(papers), "papers": papers, "query": query}
  145. )
  146. except urllib.error.HTTPError as e:
  147. return ToolResponse.error(
  148. code="NETWORK_ERROR",
  149. message=f"PubMed API 请求失败 (HTTP {e.code})"
  150. )
  151. except Exception as e:
  152. return ToolResponse.error(
  153. code="INTERNAL_ERROR",
  154. message=f"PubMed 检索出错: {str(e)}"
  155. )
  156. def get_parameters(self) -> List[ToolParameter]:
  157. return [
  158. ToolParameter(name="keyword", type="string",
  159. description="搜索关键词,支持 MeSH 主题词,如 'diabetes treatment metformin'",
  160. required=False),
  161. ToolParameter(name="author", type="string",
  162. description="作者姓名,如 'Anthony Fauci'",
  163. required=False),
  164. ToolParameter(name="year_from", type="string",
  165. description="起始年份", required=False),
  166. ToolParameter(name="year_to", type="string",
  167. description="截止年份", required=False),
  168. ToolParameter(name="max_results", type="integer",
  169. description="最大返回结果数(默认5,最大20)", required=False),
  170. ]