| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- """
- PubMed 生物医学文献检索工具
- 通过 NCBI Entrez API (E-utilities) 检索 PubMed 数据库中的生物医学论文。
- 覆盖 3600 万+ 论文,是生物医学领域最权威的数据库。
- API 文档: https://www.ncbi.nlm.nih.gov/books/NBK25501/
- """
- import urllib.request
- import urllib.parse
- import urllib.error
- import ssl
- import xml.etree.ElementTree as ET
- from typing import Dict, Any, List
- from hello_agents.tools import Tool, ToolParameter, ToolResponse, ToolStatus
- # Windows SSL 兼容
- _ssl_ctx = ssl.create_default_context()
- _ssl_ctx.check_hostname = False
- _ssl_ctx.verify_mode = ssl.CERT_NONE
- class PubMedSearchTool(Tool):
- """PubMed 生物医学文献检索工具
- 通过 NCBI Entrez API 检索 PubMed/PMC 数据库。
- 覆盖医学、生物学、药学、护理学、公共卫生等生物医学全领域。
- """
- SEARCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
- FETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
- SUMMARY_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
- def __init__(self):
- super().__init__(
- name="pubmed_search",
- description="在 PubMed 数据库中检索生物医学论文。"
- "覆盖 3600 万+ 论文,涵盖医学、生物学、药学、护理学、"
- "公共卫生等所有生物医学领域。"
- "支持 MeSH 主题词搜索、作者、期刊、年份等筛选。"
- "适合医学研究、药物研发、临床实践等场景。"
- )
- def _search_pmids(self, query: str, max_results: int = 5,
- year_from: str = "", year_to: str = "") -> List[str]:
- """搜索返回 PMID 列表"""
- # 构建查询条件
- search_terms = [query.strip()]
- if year_from or year_to:
- from_year = year_from or "1900"
- to_year = year_to or "2026"
- search_terms.append(f"{from_year}:{to_year}[dp]")
- full_query = " AND ".join(search_terms)
- params = {
- "db": "pubmed",
- "term": full_query,
- "retmax": str(max_results),
- "retmode": "xml",
- "sort": "relevance"
- }
- url = f"{self.SEARCH_URL}?{urllib.parse.urlencode(params)}"
- req = urllib.request.Request(url, headers={"User-Agent": "PaperAssistant/1.0"})
- with urllib.request.urlopen(req, timeout=15, context=_ssl_ctx) as resp:
- root = ET.fromstring(resp.read().decode("utf-8"))
- id_list = root.find(".//IdList")
- if id_list is None:
- return []
- return [elem.text for elem in id_list.findall("Id")]
- def _fetch_summaries(self, pmids: List[str]) -> List[Dict[str, Any]]:
- """获取论文摘要信息"""
- if not pmids:
- return []
- params = {
- "db": "pubmed",
- "id": ",".join(pmids),
- "retmode": "xml"
- }
- url = f"{self.SUMMARY_URL}?{urllib.parse.urlencode(params)}"
- req = urllib.request.Request(url, headers={"User-Agent": "PaperAssistant/1.0"})
- with urllib.request.urlopen(req, timeout=15, context=_ssl_ctx) as resp:
- root = ET.fromstring(resp.read().decode("utf-8"))
- papers = []
- for doc in root.findall(".//DocSum"):
- paper = {
- "pmid": doc.find("Id").text if doc.find("Id") is not None else "",
- "title": "N/A",
- "authors": [],
- "pubdate": "N/A",
- "source": "N/A",
- "doi": "",
- }
- for item in doc.findall("Item"):
- name = item.get("Name", "")
- if name == "Title":
- paper["title"] = item.text or "N/A"
- elif name == "AuthorList":
- paper["authors"] = [a.text for a in item.findall("Item")
- if a.text]
- elif name == "PubDate":
- paper["pubdate"] = item.text or "N/A"
- elif name == "Source":
- paper["source"] = item.text or "N/A"
- elif name == "DOI":
- paper["doi"] = item.text or ""
- papers.append(paper)
- return papers
- def run(self, parameters: Dict[str, Any]) -> ToolResponse:
- keyword = parameters.get("keyword", "")
- author = parameters.get("author", "")
- max_results = min(parameters.get("max_results", 5), 20)
- year_from = parameters.get("year_from", "")
- year_to = parameters.get("year_to", "")
- if not keyword and not author:
- return ToolResponse.error(
- code="INVALID_PARAM",
- message="请至少提供关键词(keyword)或作者(author)"
- )
- # 构建查询
- query_parts = []
- if keyword:
- query_parts.append(keyword.strip())
- if author:
- query_parts.append(f'{author.strip()}[Author]')
- query = " AND ".join(query_parts)
- try:
- pmids = self._search_pmids(query, max_results, year_from, year_to)
- if not pmids:
- return ToolResponse.success(
- text=f"在 PubMed 中未找到匹配的论文。\n"
- f"建议:尝试更简短的关键词、使用 MeSH 主题词、"
- f"或检查拼写。查询: {query}",
- data={"count": 0, "papers": []}
- )
- papers = self._fetch_summaries(pmids)
- # 格式化输出
- lines = [f"在 PubMed 中找到 {len(papers)} 篇论文:\n"]
- for i, p in enumerate(papers, 1):
- authors_str = ", ".join(p["authors"][:3])
- if len(p["authors"]) > 3:
- authors_str += " et al."
- lines.append(f"### {i}. {p['title']}")
- if authors_str:
- lines.append(f"> 作者: {authors_str}")
- lines.append(f"> PMID: {p['pmid']} | 发表: {p['pubdate']} | {p['source']}")
- if p.get("doi"):
- lines.append(f"> [DOI](https://doi.org/{p['doi']}) | "
- f"[PubMed](https://pubmed.ncbi.nlm.nih.gov/{p['pmid']}/)")
- lines.append("")
- lines.append(f"---")
- lines.append(f"*数据来源: PubMed/NCBI*")
- return ToolResponse.success(
- text="\n".join(lines),
- data={"count": len(papers), "papers": papers, "query": query}
- )
- except urllib.error.HTTPError as e:
- return ToolResponse.error(
- code="NETWORK_ERROR",
- message=f"PubMed API 请求失败 (HTTP {e.code})"
- )
- except Exception as e:
- return ToolResponse.error(
- code="INTERNAL_ERROR",
- message=f"PubMed 检索出错: {str(e)}"
- )
- def get_parameters(self) -> List[ToolParameter]:
- return [
- ToolParameter(name="keyword", type="string",
- description="搜索关键词,支持 MeSH 主题词,如 'diabetes treatment metformin'",
- required=False),
- ToolParameter(name="author", type="string",
- description="作者姓名,如 'Anthony Fauci'",
- required=False),
- ToolParameter(name="year_from", type="string",
- description="起始年份", required=False),
- ToolParameter(name="year_to", type="string",
- description="截止年份", required=False),
- ToolParameter(name="max_results", type="integer",
- description="最大返回结果数(默认5,最大20)", required=False),
- ]
|