crossref_tool.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """
  2. CrossRef 期刊论文检索工具
  3. 通过 CrossRef REST API 检索已发表的学术期刊论文。
  4. CrossRef 是学术出版物的 DOI 注册机构,覆盖 1.5 亿+ 记录。
  5. API 文档: https://api.crossref.org/
  6. """
  7. import urllib.request
  8. import urllib.parse
  9. import urllib.error
  10. import json
  11. from typing import Dict, Any, List
  12. from hello_agents.tools import Tool, ToolParameter, ToolResponse, ToolStatus
  13. class CrossRefSearchTool(Tool):
  14. """CrossRef 期刊论文检索工具
  15. 通过 CrossRef REST API 检索正式发表的期刊论文、会议论文、书籍等。
  16. 覆盖 1.5 亿+ 学术作品,拥有最完整的期刊论文元数据(DOI、ISSN、页码等)。
  17. 特别适合检索正式发表的期刊论文和获取引用元数据。
  18. """
  19. BASE_URL = "https://api.crossref.org/works"
  20. def __init__(self):
  21. super().__init__(
  22. name="crossref_search",
  23. description="通过 CrossRef API 检索正式发表的期刊论文和会议论文。"
  24. "覆盖 1.5 亿+ 记录,拥有最完整的引用元数据(DOI、期刊名、"
  25. "卷号、页码等)。特别适合按 DOI 查找论文或检索特定期刊的文献。"
  26. "当需要精确的引用信息时使用此工具。"
  27. )
  28. def run(self, parameters: Dict[str, Any]) -> ToolResponse:
  29. keyword = parameters.get("keyword", "")
  30. author = parameters.get("author", "")
  31. doi = parameters.get("doi", "")
  32. journal = parameters.get("journal", "")
  33. max_results = min(parameters.get("max_results", 5), 20)
  34. year_from = parameters.get("year_from", "")
  35. year_to = parameters.get("year_to", "")
  36. # DOI 精确查询(最高效)
  37. if doi:
  38. url = f"{self.BASE_URL}/{urllib.parse.quote(doi.strip(), safe='')}"
  39. else:
  40. if not keyword and not author and not journal:
  41. return ToolResponse.error(
  42. code="INVALID_PARAM",
  43. message="请提供关键词(keyword)、作者(author)、DOI(doi)或期刊名(journal)"
  44. )
  45. # 构建过滤条件
  46. filters = []
  47. if year_from or year_to:
  48. f = f"from-pub-date:{year_from or '1900'}"
  49. if year_to:
  50. f += f",until-pub-date:{year_to}"
  51. filters.append(f)
  52. # 查询字段
  53. query_parts = []
  54. if keyword:
  55. query_parts.append(keyword.strip())
  56. if author:
  57. query_parts.append(author.strip())
  58. if journal:
  59. query_parts.append(journal.strip())
  60. params = {
  61. "query": " ".join(query_parts),
  62. "rows": str(max_results),
  63. }
  64. if filters:
  65. params["filter"] = ",".join(filters)
  66. url = f"{self.BASE_URL}?{urllib.parse.urlencode(params)}"
  67. try:
  68. req = urllib.request.Request(
  69. url,
  70. headers={
  71. "User-Agent": "PaperAssistant/1.0 (mailto:1793636425@qq.com)",
  72. "Accept": "application/json"
  73. }
  74. )
  75. with urllib.request.urlopen(req, timeout=20) as resp:
  76. data = json.loads(resp.read().decode("utf-8"))
  77. # 解析结果
  78. if doi:
  79. # 单篇论文查询
  80. msg = data.get("message", {})
  81. items = [msg] if msg else []
  82. total = len(items)
  83. else:
  84. msg = data.get("message", {})
  85. items = msg.get("items", [])
  86. total = msg.get("total-results", 0)
  87. if not items:
  88. return ToolResponse.success(
  89. text=f"在 CrossRef 中未找到匹配的论文。"
  90. f"{' DOI 可能不正确。' if doi else ' 请尝试更换关键词。'}",
  91. data={"count": 0, "papers": []}
  92. )
  93. # 格式化输出
  94. lines = [f"找到 {total} 篇论文(显示前 {len(items)} 篇):\n"]
  95. for i, item in enumerate(items, 1):
  96. title_list = item.get("title", ["N/A"])
  97. title = title_list[0] if title_list else "N/A"
  98. # 作者
  99. authors = item.get("author", [])
  100. author_names = []
  101. for a in authors[:5]:
  102. given = a.get("given", "")
  103. family = a.get("family", "")
  104. if given or family:
  105. author_names.append(f"{family} {given}".strip())
  106. authors_str = ", ".join(author_names)
  107. if len(authors) > 5:
  108. authors_str += " et al."
  109. # 发表信息
  110. published = item.get("published-print", {}) or item.get("published-online", {})
  111. pub_date = "-".join(str(v) for v in published.get("date-parts", [["?"]])[0]) if published else "N/A"
  112. # 期刊
  113. container = item.get("container-title", [])
  114. venue = container[0] if container else item.get("publisher", "N/A")
  115. # 引用次数
  116. ref_count = item.get("is-referenced-by-count", 0)
  117. item_doi = item.get("DOI", "")
  118. lines.append(f"### {i}. {title}")
  119. if authors_str:
  120. lines.append(f"> 作者: {authors_str}")
  121. lines.append(f"> 发表: {pub_date} | {venue}")
  122. lines.append(f"> 引用: {ref_count} 次")
  123. if item_doi:
  124. lines.append(f"> DOI: [{item_doi}](https://doi.org/{item_doi})")
  125. lines.append("")
  126. lines.append(f"---")
  127. lines.append(f"*数据来源: CrossRef API*")
  128. return ToolResponse.success(
  129. text="\n".join(lines),
  130. data={"count": len(items), "total": total, "papers": items}
  131. )
  132. except urllib.error.HTTPError as e:
  133. return ToolResponse.error(
  134. code="NETWORK_ERROR",
  135. message=f"CrossRef API 请求失败 (HTTP {e.code})"
  136. )
  137. except json.JSONDecodeError:
  138. return ToolResponse.error(
  139. code="INVALID_FORMAT",
  140. message="解析 CrossRef 返回数据失败"
  141. )
  142. except Exception as e:
  143. return ToolResponse.error(
  144. code="INTERNAL_ERROR",
  145. message=f"CrossRef 检索出错: {str(e)}"
  146. )
  147. def get_parameters(self) -> List[ToolParameter]:
  148. return [
  149. ToolParameter(name="keyword", type="string",
  150. description="搜索关键词", required=False),
  151. ToolParameter(name="author", type="string",
  152. description="作者姓名", required=False),
  153. ToolParameter(name="doi", type="string",
  154. description="DOI 号码(精确查询,优先级最高)", required=False),
  155. ToolParameter(name="journal", type="string",
  156. description="期刊名称", required=False),
  157. ToolParameter(name="year_from", type="string",
  158. description="起始年份", required=False),
  159. ToolParameter(name="year_to", type="string",
  160. description="截止年份", required=False),
  161. ToolParameter(name="max_results", type="integer",
  162. description="最大返回结果数(默认5,最大20)", required=False),
  163. ]