aminer_tool.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. """
  2. AMiner 中文学术检索工具
  3. 通过 AMiner API 检索中文学术论文,补充知网/万方无法免费接入的缺口。
  4. AMiner 由清华大学开发,覆盖 3.2 亿+ 论文和 1.3 亿+ 学者。
  5. 注册地址: https://open.aminer.cn/
  6. """
  7. import urllib.request
  8. import urllib.parse
  9. import urllib.error
  10. import json
  11. import os
  12. from typing import Dict, Any, List
  13. from hello_agents.tools import Tool, ToolParameter, ToolResponse, ToolStatus
  14. class AminerSearchTool(Tool):
  15. """AMiner 中文学术检索工具
  16. 通过 AMiner API 检索学术论文,特别擅长中文文献和中文作者。
  17. 覆盖 3.2 亿+ 论文,是 Semantic Scholar 的中文补充。
  18. """
  19. SEARCH_URL = "https://datacenter.aminer.cn/gateway/open_platform/api/paper/search"
  20. def __init__(self):
  21. super().__init__(
  22. name="aminer_search",
  23. description="通过 AMiner API 检索中英文学术论文。"
  24. "覆盖 3.2 亿+ 论文,擅长中文文献和中文作者搜索。"
  25. "当需要检索中文学术论文或中国学者的英文论文时使用此工具。"
  26. "需要先注册获取 API Key: https://open.aminer.cn/"
  27. )
  28. def _get_api_key(self) -> str:
  29. """获取 AMiner API Key"""
  30. key = os.getenv("AMINER_API_KEY", "")
  31. if not key:
  32. raise RuntimeError(
  33. "未配置 AMiner API Key。请前往 https://open.aminer.cn/ 注册获取,"
  34. "然后在 .env 中设置: AMINER_API_KEY=你的key"
  35. )
  36. return key
  37. def run(self, parameters: Dict[str, Any]) -> ToolResponse:
  38. keyword = parameters.get("keyword", "")
  39. author = parameters.get("author", "")
  40. max_results = min(parameters.get("max_results", 5), 20)
  41. if not keyword and not author:
  42. return ToolResponse.error(
  43. code="INVALID_PARAM",
  44. message="请至少提供关键词(keyword)或作者(author)"
  45. )
  46. # AMiner 用 title 参数做关键词搜索
  47. query = keyword or author
  48. params = {
  49. "title": query.strip(),
  50. "page": "1",
  51. "size": str(max_results)
  52. }
  53. url = f"{self.SEARCH_URL}?{urllib.parse.urlencode(params)}"
  54. try:
  55. api_key = self._get_api_key()
  56. req = urllib.request.Request(url, headers={
  57. "User-Agent": "PaperAssistant/1.0",
  58. "Authorization": f"Bearer {api_key}",
  59. "Accept": "application/json"
  60. })
  61. with urllib.request.urlopen(req, timeout=20) as resp:
  62. data = json.loads(resp.read().decode("utf-8"))
  63. code = data.get("code", -1)
  64. if code != 200 and code != 0:
  65. msg = data.get("msg", data.get("message", "未知错误"))
  66. return ToolResponse.error(
  67. code="API_ERROR",
  68. message=f"AMiner API 返回错误 (code={code}): {msg}"
  69. )
  70. papers = data.get("data", [])
  71. if isinstance(papers, dict):
  72. papers = papers.get("list", papers.get("results", []))
  73. total = data.get("total", len(papers))
  74. if not papers:
  75. return ToolResponse.success(
  76. text=f"在 AMiner 中未找到匹配的论文(共 {total} 条结果)。",
  77. data={"count": 0, "total": total, "papers": []}
  78. )
  79. # 格式化输出
  80. lines = [f"在 AMiner 中找到 {len(papers)} 篇论文(共 {total} 条结果):\n"]
  81. for i, paper in enumerate(papers, 1):
  82. title = paper.get("title") or paper.get("name") or "N/A"
  83. paper_id = paper.get("id") or paper.get("paper_id") or ""
  84. doi = paper.get("doi") or ""
  85. year = paper.get("year") or paper.get("pub_year") or "N/A"
  86. # 作者
  87. authors_raw = paper.get("authors") or paper.get("author") or []
  88. if isinstance(authors_raw, list):
  89. author_names = []
  90. for a in authors_raw:
  91. if isinstance(a, dict):
  92. author_names.append(a.get("name", ""))
  93. elif isinstance(a, str):
  94. author_names.append(a)
  95. authors_str = ", ".join(author_names[:5])
  96. if len(authors_raw) > 5:
  97. authors_str += " et al."
  98. elif isinstance(authors_raw, str):
  99. authors_str = authors_raw
  100. else:
  101. authors_str = "N/A"
  102. # 期刊/会议
  103. venue = paper.get("venue") or paper.get("journal") or ""
  104. if isinstance(venue, dict):
  105. venue = venue.get("name", "") or venue.get("raw", "")
  106. # 引用次数
  107. citations = paper.get("n_citation") or paper.get("citation_count") or 0
  108. lines.append(f"### {i}. {title}")
  109. if authors_str and authors_str != "N/A":
  110. lines.append(f"> 作者: {authors_str}")
  111. lines.append(f"> 发表: {year} | {venue or 'N/A'}")
  112. lines.append(f"> 引用: {citations} 次")
  113. if doi:
  114. lines.append(f"> DOI: [{doi}](https://doi.org/{doi})")
  115. if paper_id:
  116. lines.append(f"> AMiner ID: {paper_id}")
  117. lines.append("")
  118. return ToolResponse.success(
  119. text="\n".join(lines),
  120. data={
  121. "count": len(papers),
  122. "total": total,
  123. "source": "AMiner",
  124. "papers": [
  125. {
  126. "title": p.get("title", ""),
  127. "authors": p.get("authors", []),
  128. "year": p.get("year", ""),
  129. "doi": p.get("doi", ""),
  130. "venue": str(p.get("venue", "")),
  131. }
  132. for p in papers
  133. ]
  134. }
  135. )
  136. except urllib.error.HTTPError as e:
  137. if e.code == 401:
  138. return ToolResponse.error(
  139. code="ACCESS_DENIED",
  140. message="AMiner API Key 无效或已过期。请检查 .env 中的 AMINER_API_KEY。"
  141. )
  142. return ToolResponse.error(
  143. code="NETWORK_ERROR",
  144. message=f"AMiner API 请求失败 (HTTP {e.code})"
  145. )
  146. except RuntimeError as e:
  147. return ToolResponse.error(code="ACCESS_DENIED", message=str(e))
  148. except Exception as e:
  149. return ToolResponse.error(
  150. code="INTERNAL_ERROR",
  151. message=f"AMiner 检索出错: {str(e)}"
  152. )
  153. def get_parameters(self) -> List[ToolParameter]:
  154. return [
  155. ToolParameter(name="keyword", type="string",
  156. description="搜索关键词,支持中文和英文",
  157. required=False),
  158. ToolParameter(name="author", type="string",
  159. description="作者姓名,支持中文名和英文名",
  160. required=False),
  161. ToolParameter(name="max_results", type="integer",
  162. description="最大返回结果数(默认5,最大20)",
  163. required=False),
  164. ]