From 5d292f5cf494b36cadedf04ec603f272608cfaa6 Mon Sep 17 00:00:00 2001 From: literaf Date: Wed, 31 Dec 2025 12:55:09 +0800 Subject: [PATCH] feat: Add AI4S Semantic Scholar plugin - 11 tools for academic paper search powered by ai4scholar.net - Paper search: semantic search, title search, bulk search - Paper details: single and batch paper details - Paper analysis: recommendations, citations, references - Author search: author lookup, details, and papers - Supports English and Chinese documentation --- ai4s_semantic_scholar/GUIDE.md | 104 ++++++++++++ ai4s_semantic_scholar/LICENSE | 21 +++ ai4s_semantic_scholar/PRIVACY.md | 90 +++++++++++ ai4s_semantic_scholar/README.md | 138 ++++++++++++++++ ai4s_semantic_scholar/README_ZH.md | 138 ++++++++++++++++ ai4s_semantic_scholar/_assets/icon.png | Bin 0 -> 22103 bytes ai4s_semantic_scholar/_assets/icon.svg | 17 ++ ai4s_semantic_scholar/main.py | 9 ++ ai4s_semantic_scholar/manifest.yaml | 51 ++++++ .../provider/semantic_scholar.py | 34 ++++ .../provider/semantic_scholar.yaml | 41 +++++ ai4s_semantic_scholar/requirements.txt | 2 + ai4s_semantic_scholar/tools/author_detail.py | 86 ++++++++++ .../tools/author_detail.yaml | 26 +++ ai4s_semantic_scholar/tools/author_papers.py | 87 ++++++++++ .../tools/author_papers.yaml | 38 +++++ ai4s_semantic_scholar/tools/author_search.py | 84 ++++++++++ .../tools/author_search.yaml | 39 +++++ ai4s_semantic_scholar/tools/bulk_search.py | 103 ++++++++++++ ai4s_semantic_scholar/tools/bulk_search.yaml | 39 +++++ .../tools/multiple_papers_detail.py | 112 +++++++++++++ .../tools/multiple_papers_detail.yaml | 27 ++++ .../tools/paper_citations.py | 89 +++++++++++ .../tools/paper_citations.yaml | 38 +++++ ai4s_semantic_scholar/tools/paper_detail.py | 149 ++++++++++++++++++ ai4s_semantic_scholar/tools/paper_detail.yaml | 51 ++++++ .../tools/paper_recommendations.py | 100 ++++++++++++ .../tools/paper_recommendations.yaml | 38 +++++ .../tools/paper_references.py | 89 +++++++++++ .../tools/paper_references.yaml | 38 +++++ .../tools/semantic_search.py | 111 +++++++++++++ .../tools/semantic_search.yaml | 73 +++++++++ ai4s_semantic_scholar/tools/title_search.py | 113 +++++++++++++ ai4s_semantic_scholar/tools/title_search.yaml | 38 +++++ 34 files changed, 2213 insertions(+) create mode 100644 ai4s_semantic_scholar/GUIDE.md create mode 100644 ai4s_semantic_scholar/LICENSE create mode 100644 ai4s_semantic_scholar/PRIVACY.md create mode 100644 ai4s_semantic_scholar/README.md create mode 100644 ai4s_semantic_scholar/README_ZH.md create mode 100644 ai4s_semantic_scholar/_assets/icon.png create mode 100644 ai4s_semantic_scholar/_assets/icon.svg create mode 100644 ai4s_semantic_scholar/main.py create mode 100644 ai4s_semantic_scholar/manifest.yaml create mode 100644 ai4s_semantic_scholar/provider/semantic_scholar.py create mode 100644 ai4s_semantic_scholar/provider/semantic_scholar.yaml create mode 100644 ai4s_semantic_scholar/requirements.txt create mode 100644 ai4s_semantic_scholar/tools/author_detail.py create mode 100644 ai4s_semantic_scholar/tools/author_detail.yaml create mode 100644 ai4s_semantic_scholar/tools/author_papers.py create mode 100644 ai4s_semantic_scholar/tools/author_papers.yaml create mode 100644 ai4s_semantic_scholar/tools/author_search.py create mode 100644 ai4s_semantic_scholar/tools/author_search.yaml create mode 100644 ai4s_semantic_scholar/tools/bulk_search.py create mode 100644 ai4s_semantic_scholar/tools/bulk_search.yaml create mode 100644 ai4s_semantic_scholar/tools/multiple_papers_detail.py create mode 100644 ai4s_semantic_scholar/tools/multiple_papers_detail.yaml create mode 100644 ai4s_semantic_scholar/tools/paper_citations.py create mode 100644 ai4s_semantic_scholar/tools/paper_citations.yaml create mode 100644 ai4s_semantic_scholar/tools/paper_detail.py create mode 100644 ai4s_semantic_scholar/tools/paper_detail.yaml create mode 100644 ai4s_semantic_scholar/tools/paper_recommendations.py create mode 100644 ai4s_semantic_scholar/tools/paper_recommendations.yaml create mode 100644 ai4s_semantic_scholar/tools/paper_references.py create mode 100644 ai4s_semantic_scholar/tools/paper_references.yaml create mode 100644 ai4s_semantic_scholar/tools/semantic_search.py create mode 100644 ai4s_semantic_scholar/tools/semantic_search.yaml create mode 100644 ai4s_semantic_scholar/tools/title_search.py create mode 100644 ai4s_semantic_scholar/tools/title_search.yaml diff --git a/ai4s_semantic_scholar/GUIDE.md b/ai4s_semantic_scholar/GUIDE.md new file mode 100644 index 000000000..8330f1794 --- /dev/null +++ b/ai4s_semantic_scholar/GUIDE.md @@ -0,0 +1,104 @@ +# AI4S Semantic Scholar 插件使用指南 + +## 快速开始 + +### 1. 获取 API Key + +1. 访问 [ai4scholar.net](https://ai4scholar.net) +2. 注册账号 +3. 在个人中心获取 API Key + +### 2. 配置插件 + +在 Dify 平台中: +1. 进入「插件」页面 +2. 找到 Semantic Scholar 插件 +3. 点击「配置」 +4. 输入你的 API Key + +### 3. 开始使用 + +插件提供以下工具,可在 Dify 工作流或聊天应用中使用: + +## 使用场景示例 + +### 场景一:文献调研 + +使用**语义搜索**找到相关论文: + +``` +查询:"deep learning for medical image analysis" +年份:2020-2024 +领域:Computer Science, Medicine +仅开放获取:是 +``` + +### 场景二:查找特定论文 + +使用**标题搜索**: + +``` +标题:Attention Is All You Need +年份:2017 +``` + +### 场景三:深入了解论文 + +使用**论文详情**获取完整信息: + +``` +论文ID:DOI:10.48550/arXiv.1706.03762 +包含引用:是 +包含参考文献:是 +``` + +### 场景四:比较多篇论文 + +使用**多篇论文详情**: + +``` +论文ID列表: +DOI:10.48550/arXiv.1706.03762 +DOI:10.48550/arXiv.1810.04805 +DOI:10.48550/arXiv.2005.14165 +``` + +### 场景五:调研作者 + +使用**作者搜索**: + +``` +作者姓名:Yann LeCun +返回数量:5 +``` + +## 常见问题 + +### Q: API Key 无效? + +A: 请确认: +1. API Key 复制完整,没有多余空格 +2. 账号积分充足 +3. API Key 没有被禁用 + +### Q: 搜索结果太少? + +A: 尝试: +1. 使用更通用的关键词 +2. 扩大年份范围 +3. 移除领域筛选 + +### Q: 如何获取论文 PDF? + +A: 搜索结果中会显示开放获取的 PDF 链接。勾选"仅开放获取"可只返回有免费 PDF 的论文。 + +## 积分消耗说明 + +每次 API 调用消耗 1 积分,批量操作按实际调用次数计算。 + +如需更多积分,请访问 [ai4scholar.net](https://ai4scholar.net) 充值。 + +## 联系支持 + +- 微信:literaf +- 网站:[ai4scholar.net](https://ai4scholar.net) diff --git a/ai4s_semantic_scholar/LICENSE b/ai4s_semantic_scholar/LICENSE new file mode 100644 index 000000000..101d10dfc --- /dev/null +++ b/ai4s_semantic_scholar/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 literaf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ai4s_semantic_scholar/PRIVACY.md b/ai4s_semantic_scholar/PRIVACY.md new file mode 100644 index 000000000..2b0214a09 --- /dev/null +++ b/ai4s_semantic_scholar/PRIVACY.md @@ -0,0 +1,90 @@ +# Privacy Policy + +**Last Updated**: December 31, 2024 + +This privacy policy describes how the AI4S Semantic Scholar plugin ("Plugin") collects, uses, and shares information. + +--- + +## 1. Data Collection + +### Data We Collect + +| Data Type | Description | Purpose | +|-----------|-------------|---------| +| **Search Queries** | Text queries entered by users | To perform paper/author searches | +| **Paper IDs** | S2 ID, DOI, arXiv ID provided by users | To retrieve paper details | +| **Author IDs** | Semantic Scholar author IDs | To retrieve author information | +| **API Key** | User's ai4scholar.net API key | To authenticate API requests | + +### Data We Do NOT Collect + +- Personal identification information (name, email, etc.) +- Device information +- Location data +- Usage analytics or tracking data + +--- + +## 2. Data Usage + +All collected data is used **solely** for the following purposes: + +1. **Processing API Requests**: Search queries and IDs are sent to ai4scholar.net API to retrieve academic paper information. +2. **Authentication**: API keys are used to authenticate requests and track credit usage. + +**We do NOT**: +- Store user queries or search history +- Use data for advertising or marketing +- Sell or share data with third parties (except as described below) + +--- + +## 3. Third-Party Services + +This plugin uses the following third-party services: + +| Service | Purpose | Privacy Policy | +|---------|---------|----------------| +| **ai4scholar.net** | Academic paper search API | [ai4scholar.net](https://ai4scholar.net) | +| **Semantic Scholar** | Underlying data source | [Semantic Scholar Privacy](https://www.semanticscholar.org/privacy-policy) | + +Data sent to these services is subject to their respective privacy policies. + +--- + +## 4. Data Retention + +- **No local storage**: The plugin does not store any user data locally. +- **API logs**: ai4scholar.net may retain API usage logs for billing and security purposes. See [ai4scholar.net](https://ai4scholar.net) for details. + +--- + +## 5. Data Security + +- API keys are transmitted securely and stored using Dify's credential management system. +- All API communications use HTTPS encryption. + +--- + +## 6. User Rights + +You have the right to: +- Stop using the plugin at any time +- Request deletion of your ai4scholar.net account and associated data + +--- + +## 7. Changes to This Policy + +We may update this privacy policy from time to time. Changes will be reflected in the "Last Updated" date above. + +--- + +## 8. Contact + +For privacy concerns or questions, please contact: + +- **GitHub Issues**: [dify-plugin-ai4s-semantic-scholar](https://github.com/literaf/dify-plugin-ai4s-semantic-scholar/issues) +- **WeChat**: literaf +- **Website**: [ai4scholar.net](https://ai4scholar.net) diff --git a/ai4s_semantic_scholar/README.md b/ai4s_semantic_scholar/README.md new file mode 100644 index 000000000..4fb12abbe --- /dev/null +++ b/ai4s_semantic_scholar/README.md @@ -0,0 +1,138 @@ +# AI4S Semantic Scholar + +Academic paper search plugin powered by [ai4scholar.net](https://ai4scholar.net) API for Dify platform. + +**Author**: [ai4scholar](https://ai4scholar.net) +**Repository**: [dify-plugin-ai4s-semantic-scholar](https://github.com/literaf/dify-plugin-ai4s-semantic-scholar) + +[English Documentation](./README.md) | [中文文档](./README_ZH.md) + +--- + +## Overview + +- **Plugin Type**: Tool Plugin (Python) +- **Tools**: 11 tools (Paper Search / Paper Details / Paper Analysis / Author Search) +- **Output**: text (Markdown format) + +This plugin wraps the Semantic Scholar API via ai4scholar.net proxy, providing stable service with unified credit billing. + +--- + +## Configuration (Provider Credentials) + +After installing the plugin in Dify, configure the following credentials: + +| Credential | Required | Description | +|-----------|----------|-------------| +| `api_key` | ✅ Yes | Your ai4scholar.net API Key | + +**Get API Key**: Visit [ai4scholar.net](https://ai4scholar.net) to register and obtain your API key. + +--- + +## Tools + +### Paper Search + +| Tool | API Endpoint | Description | +|------|-------------|-------------| +| **Semantic Search** (`semantic_search`) | `GET /graph/v1/paper/search` | Search papers by relevance using natural language | +| **Title Search** (`title_search`) | `GET /graph/v1/paper/search/match` | Search papers by exact or partial title | +| **Bulk Search** (`bulk_search`) | `GET /graph/v1/paper/search/bulk` | Execute multiple search queries at once | + +### Paper Details + +| Tool | API Endpoint | Description | +|------|-------------|-------------| +| **Paper Detail** (`paper_detail`) | `GET /graph/v1/paper/{paper_id}` | Get detailed information for a single paper | +| **Multiple Papers Detail** (`multiple_papers_detail`) | `POST /graph/v1/paper/batch` | Get details for multiple papers at once | + +### Paper Analysis + +| Tool | API Endpoint | Description | +|------|-------------|-------------| +| **Paper Recommendations** (`paper_recommendations`) | `GET /recommendations/v1/papers` | Get recommended papers based on a given paper | +| **Paper Citations** (`paper_citations`) | `GET /graph/v1/paper/{paper_id}/citations` | Get papers that cite the given paper | +| **Paper References** (`paper_references`) | `GET /graph/v1/paper/{paper_id}/references` | Get reference papers of the given paper | + +### Author Search + +| Tool | API Endpoint | Description | +|------|-------------|-------------| +| **Author Search** (`author_search`) | `GET /graph/v1/author/search` | Search authors and get publication statistics | +| **Author Detail** (`author_detail`) | `GET /graph/v1/author/{author_id}` | Get author details (h-index, citation count, etc.) | +| **Author Papers** (`author_papers`) | `GET /graph/v1/author/{author_id}/papers` | Get papers published by an author | + +--- + +## Parameters + +### Semantic Search + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | string | ✅ | Natural language search query | +| `limit` | number | ❌ | Number of results (1-100, default 10) | +| `year` | string | ❌ | Year filter (e.g., "2020", "2020-2024") | +| `fields_of_study` | string | ❌ | Field of study filter | +| `open_access_only` | boolean | ❌ | Only return open access papers | + +### Paper Detail + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `paper_id` | string | ✅ | Paper ID (supports S2 ID, DOI, arXiv ID) | +| `include_citations` | boolean | ❌ | Include citing papers | +| `include_references` | boolean | ❌ | Include reference papers | + +### Paper Recommendations / Citations / References + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `paper_id` | string | ✅ | Paper ID (supports S2 ID, DOI, arXiv ID) | +| `limit` | number | ❌ | Number of results (default 10-20) | + +### Author Search + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | string | ✅ | Author name | +| `limit` | number | ❌ | Number of results (1-20, default 5) | + +### Author Detail / Author Papers + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `author_id` | string | ✅ | Semantic Scholar author ID | +| `limit` | number | ❌ | Number of results (for author_papers only) | + +--- + +## Credits + +Each API call consumes 1 credit. Batch operations are charged per actual API call. + +To get more credits, visit [ai4scholar.net](https://ai4scholar.net). + +--- + +## Links + +- [ai4scholar.net](https://ai4scholar.net) - API Service +- [Semantic Scholar API Docs](https://api.semanticscholar.org/api-docs/) +- [Dify Plugin Documentation](https://docs.dify.ai/) + +--- + +## Support + +- **GitHub Issues**: [Report a bug or request a feature](https://github.com/literaf/dify-plugin-ai4s-semantic-scholar/issues) +- **WeChat**: literaf +- **Website**: [ai4scholar.net](https://ai4scholar.net) + +--- + +## License + +MIT License diff --git a/ai4s_semantic_scholar/README_ZH.md b/ai4s_semantic_scholar/README_ZH.md new file mode 100644 index 000000000..bb2925418 --- /dev/null +++ b/ai4s_semantic_scholar/README_ZH.md @@ -0,0 +1,138 @@ +# AI4S Semantic Scholar + +基于 [ai4scholar.net](https://ai4scholar.net) API 的学术论文搜索插件,为 Dify 平台提供强大的学术搜索能力。 + +**作者**: [ai4scholar](https://ai4scholar.net) +**项目地址**: [dify-plugin-ai4s-semantic-scholar](https://github.com/literaf/dify-plugin-ai4s-semantic-scholar) + +[English Documentation](./README.md) | [中文文档](./README_ZH.md) + +--- + +## 概述 + +- **插件类型**: 工具插件 (Python) +- **包含工具**: 11 个 (论文搜索 / 论文详情 / 论文分析 / 作者搜索) +- **输出**: text (Markdown 格式) + +本插件是 Semantic Scholar API 的包装层,通过 ai4scholar.net 代理访问,提供更稳定的服务和统一的积分计费。 + +--- + +## 配置 (Provider Credentials) + +在 Dify 中安装插件后,配置以下凭证: + +| 凭证 | 必填 | 说明 | +|-----|------|------| +| `api_key` | ✅ 是 | 你的 ai4scholar.net API Key | + +**获取 API Key**: 访问 [ai4scholar.net](https://ai4scholar.net) 注册并获取 + +--- + +## 工具 + +### 论文搜索 + +| 工具名称 | 接口 | 说明 | +|---------|------|------| +| **语义搜索** (`semantic_search`) | `GET /graph/v1/paper/search` | 使用自然语言按相关性搜索论文 | +| **标题搜索** (`title_search`) | `GET /graph/v1/paper/search/match` | 通过精确或部分标题搜索论文 | +| **批量搜索** (`bulk_search`) | `GET /graph/v1/paper/search/bulk` | 一次执行多个搜索查询 | + +### 论文详情 + +| 工具名称 | 接口 | 说明 | +|---------|------|------| +| **论文详情** (`paper_detail`) | `GET /graph/v1/paper/{paper_id}` | 获取单篇论文详细信息 | +| **多篇论文详情** (`multiple_papers_detail`) | `POST /graph/v1/paper/batch` | 批量获取多篇论文信息 | + +### 论文分析 + +| 工具名称 | 接口 | 说明 | +|---------|------|------| +| **论文推荐** (`paper_recommendations`) | `GET /recommendations/v1/papers` | 基于给定论文获取推荐论文 | +| **论文引用** (`paper_citations`) | `GET /graph/v1/paper/{paper_id}/citations` | 获取引用该论文的论文列表 | +| **论文参考文献** (`paper_references`) | `GET /graph/v1/paper/{paper_id}/references` | 获取论文的参考文献列表 | + +### 作者搜索 + +| 工具名称 | 接口 | 说明 | +|---------|------|------| +| **作者搜索** (`author_search`) | `GET /graph/v1/author/search` | 搜索作者并获取发表统计 | +| **作者详情** (`author_detail`) | `GET /graph/v1/author/{author_id}` | 获取作者详细信息(h-index、引用数等) | +| **作者论文** (`author_papers`) | `GET /graph/v1/author/{author_id}/papers` | 获取作者发表的论文列表 | + +--- + +## 参数说明 + +### 语义搜索 (semantic_search) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `query` | string | ✅ | 自然语言搜索查询 | +| `limit` | number | ❌ | 返回数量 (1-100, 默认 10) | +| `year` | string | ❌ | 年份筛选 (如 "2020", "2020-2024") | +| `fields_of_study` | string | ❌ | 研究领域筛选 | +| `open_access_only` | boolean | ❌ | 仅返回开放获取论文 | + +### 论文详情 (paper_detail) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `paper_id` | string | ✅ | 论文 ID (支持 S2 ID, DOI, arXiv ID) | +| `include_citations` | boolean | ❌ | 包含引用论文 | +| `include_references` | boolean | ❌ | 包含参考文献 | + +### 论文推荐 / 引用 / 参考文献 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `paper_id` | string | ✅ | 论文 ID (支持 S2 ID, DOI, arXiv ID) | +| `limit` | number | ❌ | 返回数量 (默认 10-20) | + +### 作者搜索 (author_search) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `query` | string | ✅ | 作者姓名 | +| `limit` | number | ❌ | 返回数量 (1-20, 默认 5) | + +### 作者详情 / 作者论文 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `author_id` | string | ✅ | Semantic Scholar 作者 ID | +| `limit` | number | ❌ | 返回数量 (仅 author_papers) | + +--- + +## 积分消耗 + +每次 API 调用消耗 1 积分,批量操作按实际调用次数计算。 + +如需更多积分,请访问 [ai4scholar.net](https://ai4scholar.net) 充值。 + +--- + +## 相关链接 + +- [ai4scholar.net](https://ai4scholar.net) - API 服务 +- [Semantic Scholar API 文档](https://api.semanticscholar.org/api-docs/) +- [Dify 插件开发文档](https://docs.dify.ai/zh/use-dify/workspace/plugins) + +--- + +## 联系支持 + +- **GitHub Issues**: [提交问题或建议](https://github.com/literaf/dify-plugin-ai4s-semantic-scholar/issues) +- **微信**: literaf +- **网站**: [ai4scholar.net](https://ai4scholar.net) + +--- + +## 许可证 + +MIT License diff --git a/ai4s_semantic_scholar/_assets/icon.png b/ai4s_semantic_scholar/_assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c66e8204c62d77ab4294a67bb68bc1fa608cd387 GIT binary patch literal 22103 zcmdpegvNkPc~7BqXJ4bf=`!4N4>3tstG!-QBtOe1F#) z|HFIj+Rn3`v*(;W&v`y^-}h%>Z&l>+aj0-WAQ1j*1%x^X1O}R55EcfoF>oyU_}@Dx zb$J<3*$~YR@W$5qqGf^q_tC&-S&;v8G#HeH_W#=deeZd&kruGQwpY+|0)e=h{(FMm z@+DnBASuvmgtVp`_`nd;OS5Mx^G&{Mp6gDy?k5~F*a-&~ndV!fCrzu>ce9?pNZm|B zdXuA>xSS$(+C14$*;g4-@i&hsCw|u>TS11|goQUBeHt2EkJ3Fps-*MtE6$oQ{QvuS zOM!Kb^L3`+7c5j8>r?5T#ryYPLL(xq^EI_uO1-Vu*48Ypy;|i)^)24<;DT}eWMn|0 zzA_&__TJUku8P2saGL%O6_u&Nkn8EVuj3&M%T9 z&pQxO9GSKwS=oyBwP=6Txr)4MrXuq~Z;Zb)m=ic(r6)Z|^5@Sic6n zY3aAE7vKFR%eXB14~?Sg0NnITVnkT|plU-OOBag<142o;gXVXi(&t)m724c}dTg?t zuYXOj&Xz3ia@&c^GbZ_P{?+IHq|aGK#(xG~f%*FLNS5?qCkt~0E}n$)lYI+ZzZri} zudAE^8`8-ra8GeDThg_!38UVu7nihumcGAjDdJudbzfa(*yMt>uG8qa!Fd}cv?xXL zkgZcbzEHm{w@~|8G?gw79LK=4wmeWd*;HFg25FE2sqo?Z@v2HU8wP_9m5~-}GPV6p zXn`jBA2+it>?NOM{@IGx>Nd{x5%;@ps;as-LlH{eXZ_GAKV+;SdQj6dEL1TRH|-oA z(`T$xd-5uhVx{-$X0=F4uaIGft?VsJA8N*vR7U<>w$5yvH))kq4D zTWku2c1RRqXZr1BM7t7g1Y;7=d-glG= znnPIpAzB5nI5k@_uX9P$n}TP+JYt+!eMh_D=w)MN-653xBHTc}(=yC#^RCVo47Lmu zqY*&zHmpU9=B28L)bP>lYK4}}<&i<+{6Mc9@Zkp@j6CH}xU}14d=LbIe2mo%U#W9? z9e-JlYfGLD@_RgZ5HT{-Z}<4arLvwXpwV}|vj0;54Dm`UE2jn$pB~4ziapB8#J!A9UP-;Q`{ddGQG8Tx8<&vEr>zH^_zd+)!$Ss z=oq=@e@`MT(m~nJS&ZvpA@G28W@+I75=~RyyE|%IqTSApT?p;iq&_NYHi)7U7>g0V zOmX|er^*-;hjrXcjnUP2(x`6~)pO+r_q2vjuqg0T?G%CGJufD80u+Q@B2DtH9zRlF?$4IK`(<^o*>g!it5X3<-<%r9fF8_oO7o%0nFH$3#o_2QM9L`z>ZfJ5VUdv5h2J(IsF?NpH6J-Ep@x^`_(~G%2FWfMLFzE zsikK3>N4XtFsyIX`S-#17nV7e(ri`v`LrVuWY35lGKY9SnUpE?+8L;b`rHfovm3pa z)_aRf(2CbJTPbzz8GH2`S_QLf>~o-|Cd#M z5d*#NeWi+`qGRc$4`gH9d2ebwf}AJGCLm6H!fwisSZW}tbiTIgvqi=Yj}W0m*0bau zM97??K9(B;QkutuzrjLLF}zH>)C2kJ?l)Uzf}d@D`F*?O?le!!dA#kvnpAl3aIzCR z?H@34?@mVm<{q&0Hv2I@P)B5Q`3W1$a|DI<&4#K<+~`W)Zs&UR$1*fp3>!7dhM2=Y zydZyH;GE&S*(34@TPfLjznu_>3+DVmSV_8vRi?e3sX&bQDZ?(FEN{PqA_=+LbPPtr~)T4w6w zH|w%QGv$WG%k|4a7u&g7IU%bF3ioRZ@Z$v!U%DHXR4+Pv7smg11g!QKX6SLe6JA1v zHx!Ptu&zqqn+iz=PqeYLQG?Dnin?6Tl?+dMkrYp)ut4S+xKOJYe z$-2b6ogzbxp*=Vj`-TM)I1r4#*YjpZ33PjP#G^;Vjdz|CS(;Jz>lX|J%?gN6zwOJeccI!EgL0*A)=q>k~|CN&d$#(3~sSK|6aE}hM{QQWQ9W`eVcAdq~McuG&vOaKVu zaGIt+n8{^#Vph(Gaj&#)@y~Dw3kX5S^e2XrpLN|zG@yMwZ*Z;%Gr0QOQI9j5gK0yv zig5Nv399A4j)Q><=0eFbbX(B zF|Q>flVdesuO_8L&TYWF1Toq?MH{!mi4SaCk5`%q#3mvA2wr!!|3Xuhja)zd=K2UY z9xbUq=qqacAC+}xt~E?OG+M3}n~E%wY9uwqc|R0oaiV+qcZ@##bwTs7JdAjLYJx4r zvHdA@??>}rTnA1 zEkPXOM0YpQp^?s~rUHaR;+%YZ(&AV1PP_9?!|3FH@whVM6%aD989r}K6Ej~voGLD>SO0`P8`GRAh0i?rR2l)t6z9IWouEGW@7DrY`0ncVc?_pqWjisBdIc)A(7=qwY#A~%{1S~8+(Y=x(C^* zcZe#%nU+)=BXX zj`QWL_vBWlFE>WreZgNHFbga!xV^p}dNFH}Sv)b}OvhBD?Yv8qU7}sOtN426>irvR zO^)60{-sZ$jaG9wK}e9`uk(m|woI?*U#{|yJ8dUxZTN9dx40N3EeM7rn&{8B+t6>E zdc&iH((sCr(c_tZ{R}dKP#y@y-w2=ch^1M2X zOGm0*?av~G^{j{Sg`2Cv3IR)cxBslmoh=A*W*d!3o!17j>WGc~x};}ZIzhYv{39JI zl_S$~TJl_A(kINhaC*GmL^x!e#uxcitI~``zy#@=spROsAI+uhP%f95^Z3rLrTFn~ z3AI^5hmbxCufYisct1%e!nVt3{djdS$7T?t^ufK;76=FUR1YOZ3fb#zYPP_EeK;MbR1x7wR2eB! zK=#9y7l;d-!0eJFrRU%fw#M-1kh!jKj+^}4{ud3tA>R6>@O!ifuh!%(*^uWGpX(N~ zYd_DCY0PKR{oXtk79+#zd;v031q*nbD=BP|P-BNe$YFR~Z(s4z%n~Ch4x8U<^<^1h zreCs!*OOXX9yi{fGJw@?9`lopl3b5kt}>MHAdGS+ghL0eHiZ+E$IdUYEp9Y>!hD!9 zK56@*X=RtU-r{Iv4g|r+k|F-EI-Jw;C*1aBf__0#PzEHrm<1&heU#r^?dL>8|Hx3) zQS!5g8RfM$x1|@G2w%6W9nUhQ5s%OQL;4w6$tubt(*|is7kgBve-mr&j~WryLACUn zUec8YqJ5cnH9=C>Fwhleh+i|(qm**wYSGgU8I*}YenH&6Zeo%%U)YG!}SZ$;5rbVv;o zXNYY|cN3YENQZG2F$!ei7YIp9+ z1|IF(0t%Bdg}5veFEsJGuO}uc80E*S$2?GbiT86572qc}N*CL)`46&izy$N-IoX{j z+nXKn9-(Ez z3R3Hk$9{-eJpXiX>QBnI%rnHA3o+QkamvRPL;lkx&6pk`jvsX=+(-Uxb~QPBe!;m z&0eZ8mI*u)+8t9!6szm6;Y>QTi#vBLAN{yatC-&9Dy@eJKJXlYcrGFxn!(jvY z2>f-jYZhPHBGw*;N)^42->CQ}EIE#k zzz=;tF@s`aHU!FX4S&Rk_+2-8?mk5IBj61vc=Zjm42Md0;<_Sq5`KWY@xarrt5@vhR z5ez}iw-g57iB#9Ai>QYmb&5BBplQX=G7^ z6^&M3pQ~?GUup@uTI<-6=g^9j_Fz3afb-JBuett z-w2B$g_$7&3<7~Thg>xnd6!_1Ipy=w+2>$QsKTip4zhn?iGuTcBjm(FSm zG|#zpRPy@-bW~e(4Go>>Rrzs@?=2d%N;DHK)m3SAt%?cE7`P{&JR#t(> z^Zk66jH8ksgwC@velKZZF5y$C{)CrBzKk9deDg@1`0bhvapCQ=-sBQ%UOx~h$=Klf zN55F6U2-a4|JN=M&hp{N$D8-8hFQ&2{P^HZN6wqt-d887wOdE2Xc*w7x>pic7XHt* zDErpaD9yuqo>ky;FF_G@S}lKZXVY04s4OW^wBkKdPMW%F;CDe4ER+|z-0eu$ZRPr` zeS0+bcOmY^m8#^OP-ftgx0U~p?y7-@mjC1FFX%#!gMXPOgY=S{p1(x>3u%khKl({o z>VVI=sgLZ9y|HF>OGrD56c0!n4tQ$Ns{1KfA%|*r!XzEbcEiDPG|Dags0>D({nGa3OXG zcg`kbRfWujT*L!0Wu6#PT9%X(f&@M_3drTcw z6hDVt*H|QoT#N`??So-{w_p#+uy)JB^nwEA)StGkHy>>*wWaX_nJHLdC#m#Da!-n) z?eA*VVk8Ol{!E!PmxmadxvM3bo>juw)v(^b&m+tevra;bp%Mru&3em;B&y^H*O+fx z+l96qvlT&LS0D7alQvJ?d#AB)nd5|;<;Wm!oUMCtI=2t z`=5H^ulsJ%E+X>q3!XLeF9ATJDYm`GrJ!Zt9D^>MO$97e4J$7{KV6#nIn-o-rV>bZ z(b{;24Ptl#v&{StJ#742>oc94cT%zjSVWhaZ%7=#ir#BcLO40n-9vuY9f8OT3xzjt zChb}-k|eg$9%p{nMXz@?ySWcwU8UZL0U5eF*&Zk?Akc)^1|(a--|jFYRztixW5NIv zg|)v}=A-DQ=o-Qx|1Sli4v5DMiD1tjqm(g;{L<{%ATbWIY6Qa9+a7Xb(xFVW8-51t zPcUfuPFvMIv{L~ZOtJh^(i?zp_CEgoW^R#t{~6Tjf@%DddTAw;SKMh6FBwFU^PAia zL%d&?K&mxj&!E}A$~+)QDnZs{I73|CnHoeU7E|hrT5my4!|9<{+ND~P^ zH1q-C0V@iab1|(oNmoHRrK)7sOBJFT6$%S&y6fgA(Kdqw|L9@x{=-(|-^jLxqtGObXt? z3ZxRSD)}$#N_zHgb*0iDJ*zf}vppO3!pA(#L0RaHU`#tpiuG%+4l)p{+- z(`DB7G=E2}n?rn%CYGsZO>^xxZov->>L0~qZ>$+!!NFO%BS6SYz?M_#Uu2QtUa&j) zBYAh`ghJYbE%{z(C5ZeL2gPd#+f_STMq)hTz@c=}0Q-z+m$bBWA#!F1sxck>eqS!E zF8AYxdJ^hVuT|FQY|PHNV09eu_D?kAQ)Ado)=`OE1euwXE0LY3W9KT?dYQP#`P(I* zyV4TP;`lwSt!D$6kP4?!D*>l-w%rNsK7W2TrbTIZn`$gdQZiC2(=NTd^~R^UL>v2# z!lRbYTtZ61W=TL^_>`~j3sSyf?X*3A=!bTx|Mc2R`^&$b3w1WLjANa^m99uus3zUX zyo}Es|Dp8*$1?lLsR*=FSTDbzOan;Z`og6KtufFk=r<}R2YA|OG!hGCh!qlQPY4)6 zU8YmwDT3XOEYwe-IZy}yj`#W{hX{^fNOh4Rw7m}-w!(dA{$%OQxW2*+VJ{Ooz#0{M zyEU9~iY7f~nc~=V7`*2^{p$01!n;?N4}a_&#mA4V3|l_pi#tszk2dKYy-im8gSxS3 z^#Mls90^A2X2&vv<)JX=&>+5D_h)TvLu4L!

M4ei^lMCd!E@8lz8H!w??{h-9 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 literal 0 HcmV?d00001 diff --git a/ai4s_semantic_scholar/_assets/icon.svg b/ai4s_semantic_scholar/_assets/icon.svg new file mode 100644 index 000000000..a7beeb125 --- /dev/null +++ b/ai4s_semantic_scholar/_assets/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ai4s_semantic_scholar/main.py b/ai4s_semantic_scholar/main.py new file mode 100644 index 000000000..854496a02 --- /dev/null +++ b/ai4s_semantic_scholar/main.py @@ -0,0 +1,9 @@ +from dify_plugin import Plugin +from dify_plugin.config.config import DifyPluginEnv + +# 从 .env 文件自动加载配置 +config = DifyPluginEnv() +plugin = Plugin(config) + +if __name__ == "__main__": + plugin.run() diff --git a/ai4s_semantic_scholar/manifest.yaml b/ai4s_semantic_scholar/manifest.yaml new file mode 100644 index 000000000..07b810ae6 --- /dev/null +++ b/ai4s_semantic_scholar/manifest.yaml @@ -0,0 +1,51 @@ +version: 0.0.1 +type: plugin +author: ai4scholar +name: ai4s_semantic_scholar +label: + en_US: AI4S Semantic Scholar + zh_Hans: AI4S Semantic Scholar +description: + en_US: Academic paper search plugin powered by ai4scholar.net API. Search papers, get details, and find authors. + zh_Hans: 基于 ai4scholar.net API 的学术论文搜索插件。支持论文搜索、详情获取、作者查询等功能。 +icon: icon.png +tags: + - search +resource: + memory: 268435456 + permission: + tool: + enabled: true + model: + enabled: false + llm: false + text_embedding: false + rerank: false + tts: false + speech2text: false + moderation: false + node: + enabled: false + endpoint: + enabled: true + app: + enabled: false + storage: + enabled: true + size: 10485760 +plugins: + tools: + - provider/semantic_scholar.yaml +meta: + version: 0.0.1 + arch: + - amd64 + - arm64 + runner: + language: python + version: "3.12" + entrypoint: main + minimum_dify_version: 1.0.0 +created_at: 2024-12-31T00:00:00.000000+08:00 +privacy: PRIVACY.md +verified: false diff --git a/ai4s_semantic_scholar/provider/semantic_scholar.py b/ai4s_semantic_scholar/provider/semantic_scholar.py new file mode 100644 index 000000000..b69ae49f7 --- /dev/null +++ b/ai4s_semantic_scholar/provider/semantic_scholar.py @@ -0,0 +1,34 @@ +from dify_plugin import ToolProvider + + +class SemanticScholarProvider(ToolProvider): + def validate_credentials(self, credentials: dict) -> None: + """ + Validate the API key by making a test request + """ + import requests + + api_key = credentials.get("api_key", "") + if not api_key: + raise Exception("API key is required") + + # Test the API key with a simple search + try: + response = requests.get( + "https://ai4scholar.net/graph/v1/paper/search", + headers={"Authorization": f"Bearer {api_key}"}, + params={"query": "test", "limit": 1}, + timeout=10 + ) + + if response.status_code == 401: + raise Exception("Invalid API key") + elif response.status_code == 402: + raise Exception("Insufficient credits. Please recharge at ai4scholar.net") + elif response.status_code != 200: + raise Exception(f"API error: {response.status_code}") + + except requests.exceptions.Timeout: + raise Exception("API request timeout") + except requests.exceptions.RequestException as e: + raise Exception(f"Network error: {str(e)}") diff --git a/ai4s_semantic_scholar/provider/semantic_scholar.yaml b/ai4s_semantic_scholar/provider/semantic_scholar.yaml new file mode 100644 index 000000000..b579e9c90 --- /dev/null +++ b/ai4s_semantic_scholar/provider/semantic_scholar.yaml @@ -0,0 +1,41 @@ +identity: + author: ai4scholar + name: ai4s_semantic_scholar + label: + en_US: AI4S Semantic Scholar + zh_Hans: AI4S Semantic Scholar + description: + en_US: Search academic papers via ai4scholar.net API + zh_Hans: 通过 ai4scholar.net API 搜索学术论文 + icon: icon.png + tags: + - search +credentials_for_provider: + api_key: + type: secret-input + required: true + label: + en_US: API Key + zh_Hans: API 密钥 + placeholder: + en_US: Enter your ai4scholar.net API Key + zh_Hans: 输入您的 ai4scholar.net API 密钥 + help: + en_US: Get your API key from ai4scholar.net + zh_Hans: 从 ai4scholar.net 获取 API 密钥 + url: https://ai4scholar.net +tools: + - tools/semantic_search.yaml + - tools/title_search.yaml + - tools/paper_detail.yaml + - tools/bulk_search.yaml + - tools/multiple_papers_detail.yaml + - tools/author_search.yaml + - tools/author_detail.yaml + - tools/author_papers.yaml + - tools/paper_recommendations.yaml + - tools/paper_citations.yaml + - tools/paper_references.yaml +extra: + python: + source: provider/semantic_scholar.py \ No newline at end of file diff --git a/ai4s_semantic_scholar/requirements.txt b/ai4s_semantic_scholar/requirements.txt new file mode 100644 index 000000000..2a2cfe8cf --- /dev/null +++ b/ai4s_semantic_scholar/requirements.txt @@ -0,0 +1,2 @@ +dify_plugin>=0.0.1 +requests>=2.31.0 diff --git a/ai4s_semantic_scholar/tools/author_detail.py b/ai4s_semantic_scholar/tools/author_detail.py new file mode 100644 index 000000000..cf9079680 --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_detail.py @@ -0,0 +1,86 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class AuthorDetailTool(Tool): + """ + Get detailed information about a specific author + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + author_id = tool_parameters.get("author_id", "") + if not author_id: + yield self.create_text_message("Error: Author ID is required") + return + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/author/{author_id}", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "authorId,name,affiliations,paperCount,citationCount,hIndex,homepage,externalIds" + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Author not found with ID: {author_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + author = response.json() + + # Format result + name = author.get("name", "N/A") + affiliations = author.get("affiliations", []) + affiliation_str = ", ".join(affiliations) if affiliations else "N/A" + paper_count = author.get("paperCount", 0) + citation_count = author.get("citationCount", 0) + h_index = author.get("hIndex", 0) + homepage = author.get("homepage", "") + + external_ids = author.get("externalIds", {}) + orcid = external_ids.get("ORCID", "") + dblp = external_ids.get("DBLP", "") + + result_lines = [f"# {name}"] + result_lines.append(f"\n**Affiliations:** {affiliation_str}") + result_lines.append(f"**Papers:** {paper_count} | **Citations:** {citation_count} | **h-index:** {h_index}") + + if homepage: + result_lines.append(f"**Homepage:** {homepage}") + if orcid: + result_lines.append(f"**ORCID:** {orcid}") + if dblp: + result_lines.append(f"**DBLP:** {dblp}") + + result_lines.append(f"\n**Author ID:** {author.get('authorId', author_id)}") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/author_detail.yaml b/ai4s_semantic_scholar/tools/author_detail.yaml new file mode 100644 index 000000000..08e7d6e56 --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_detail.yaml @@ -0,0 +1,26 @@ +identity: + name: author_detail + author: ai4scholar + label: + en_US: Author Detail + zh_Hans: 作者详情 +description: + human: + en_US: Get detailed information about a specific author by their ID + zh_Hans: 通过作者 ID 获取特定作者的详细信息 + llm: Get detailed information about an academic author using their Semantic Scholar Author ID, including h-index, citation count, and affiliations. +parameters: + - name: author_id + type: string + required: true + label: + en_US: Author ID + zh_Hans: 作者 ID + human_description: + en_US: Semantic Scholar Author ID + zh_Hans: Semantic Scholar 作者 ID + llm_description: The Semantic Scholar Author ID to look up + form: llm +extra: + python: + source: tools/author_detail.py diff --git a/ai4s_semantic_scholar/tools/author_papers.py b/ai4s_semantic_scholar/tools/author_papers.py new file mode 100644 index 000000000..048e2fa65 --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_papers.py @@ -0,0 +1,87 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class AuthorPapersTool(Tool): + """ + Get papers published by a specific author + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + author_id = tool_parameters.get("author_id", "") + if not author_id: + yield self.create_text_message("Error: Author ID is required") + return + + limit = min(max(int(tool_parameters.get("limit", 20)), 1), 100) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/author/{author_id}/papers", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "paperId,title,year,citationCount,venue,openAccessPdf", + "limit": limit + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Author not found with ID: {author_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + papers = data.get("data", []) + + if not papers: + yield self.create_text_message(f"No papers found for author ID: {author_id}") + return + + result_lines = [f"# Papers by Author\n**Author ID:** {author_id} | **Showing:** {len(papers)} papers\n"] + + for i, paper in enumerate(papers, 1): + title = paper.get("title", "N/A") + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + venue = paper.get("venue", "") + paper_id = paper.get("paperId", "") + + open_access = paper.get("openAccessPdf") + has_pdf = "📄" if open_access else "" + + result_lines.append(f"### {i}. {has_pdf} {title}") + result_lines.append(f"**Year:** {year} | **Citations:** {citations}") + if venue: + result_lines.append(f"**Venue:** {venue}") + result_lines.append(f"**Paper ID:** {paper_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/author_papers.yaml b/ai4s_semantic_scholar/tools/author_papers.yaml new file mode 100644 index 000000000..7f5d70166 --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_papers.yaml @@ -0,0 +1,38 @@ +identity: + name: author_papers + author: ai4scholar + label: + en_US: Author Papers + zh_Hans: 作者论文 +description: + human: + en_US: Get all papers published by a specific author + zh_Hans: 获取特定作者发表的所有论文 + llm: Get a list of papers published by an author using their Semantic Scholar Author ID. +parameters: + - name: author_id + type: string + required: true + label: + en_US: Author ID + zh_Hans: 作者 ID + human_description: + en_US: Semantic Scholar Author ID + zh_Hans: Semantic Scholar 作者 ID + llm_description: The Semantic Scholar Author ID to look up papers for + form: llm + - name: limit + type: number + required: false + default: 20 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of papers to return (1-100, default 20) + zh_Hans: 返回的最大论文数量(1-100,默认20) + llm_description: Maximum number of papers to return + form: form +extra: + python: + source: tools/author_papers.py diff --git a/ai4s_semantic_scholar/tools/author_search.py b/ai4s_semantic_scholar/tools/author_search.py new file mode 100644 index 000000000..7f5b541dd --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_search.py @@ -0,0 +1,84 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class AuthorSearchTool(Tool): + """ + Search for authors and get their publication information + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + query = tool_parameters.get("query", "") + if not query: + yield self.create_text_message("Error: Author name is required") + return + + limit = min(max(int(tool_parameters.get("limit", 5)), 1), 20) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/author/search", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "query": query, + "limit": limit, + "fields": "authorId,name,affiliations,paperCount,citationCount,hIndex" + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + authors = data.get("data", []) + total = data.get("total", 0) + + if not authors: + yield self.create_text_message(f"No authors found for: {query}") + return + + result_lines = [f"# Author Search Results\n**Query:** \"{query}\" | **Found:** {total} authors (showing {len(authors)})\n"] + + for i, author in enumerate(authors, 1): + author_id = author.get("authorId", "N/A") + name = author.get("name", "N/A") + affiliations = author.get("affiliations", []) + affiliation_str = ", ".join(affiliations) if affiliations else "N/A" + paper_count = author.get("paperCount", 0) + citation_count = author.get("citationCount", 0) + h_index = author.get("hIndex", 0) + + result_lines.append(f"## {i}. {name}") + result_lines.append(f"**Affiliations:** {affiliation_str}") + result_lines.append(f"**Papers:** {paper_count} | **Citations:** {citation_count} | **h-index:** {h_index}") + result_lines.append(f"**Author ID:** {author_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/author_search.yaml b/ai4s_semantic_scholar/tools/author_search.yaml new file mode 100644 index 000000000..3079e8ac6 --- /dev/null +++ b/ai4s_semantic_scholar/tools/author_search.yaml @@ -0,0 +1,39 @@ +identity: + name: author_search + author: ai4scholar + label: + en_US: Author Search + zh_Hans: 作者搜索 +description: + human: + en_US: Search for authors and get their publication information + zh_Hans: 搜索作者并获取其发表信息 + llm: Search for academic authors by name and get information about their publications, citations, and h-index. +parameters: + - name: query + type: string + required: true + label: + en_US: Author Name + zh_Hans: 作者姓名 + human_description: + en_US: Name of the author to search for + zh_Hans: 要搜索的作者姓名 + llm_description: The name of the author to search for + form: llm + - name: limit + type: number + required: false + default: 5 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of authors to return (1-20, default 5) + zh_Hans: 返回的最大作者数量(1-20,默认5) + llm_description: Maximum number of authors to return + form: form + +extra: + python: + source: tools/author_search.py diff --git a/ai4s_semantic_scholar/tools/bulk_search.py b/ai4s_semantic_scholar/tools/bulk_search.py new file mode 100644 index 000000000..4201658b0 --- /dev/null +++ b/ai4s_semantic_scholar/tools/bulk_search.py @@ -0,0 +1,103 @@ +from typing import Any, Generator +import requests +import re +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class BulkSearchTool(Tool): + """ + Execute multiple search queries at once + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + queries_raw = tool_parameters.get("queries", "") + if not queries_raw: + yield self.create_text_message("Error: Search queries are required") + return + + # Parse queries (split by newlines or semicolons) + queries = [q.strip() for q in re.split(r'[\n;]', queries_raw) if q.strip()] + + if not queries: + yield self.create_text_message("Error: No valid queries found") + return + + if len(queries) > 10: + queries = queries[:10] + yield self.create_text_message("Note: Limited to first 10 queries\n") + + limit_per_query = min(max(int(tool_parameters.get("limit_per_query", 5)), 1), 20) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + result_lines = [f"# Bulk Search Results\n**Queries:** {len(queries)} | **Results per query:** {limit_per_query}\n"] + + for i, query in enumerate(queries, 1): + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/search", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "query": query, + "limit": limit_per_query, + "fields": "paperId,title,authors,year,citationCount,openAccessPdf" + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code != 200: + result_lines.append(f"\n## Query {i}: \"{query}\"") + result_lines.append(f"Error: API returned status {response.status_code}") + continue + + data = response.json() + papers = data.get("data", []) + total = data.get("total", 0) + + result_lines.append(f"\n## Query {i}: \"{query}\"") + result_lines.append(f"Found {total} papers (showing {len(papers)})\n") + + if not papers: + result_lines.append("No papers found.") + continue + + for j, paper in enumerate(papers, 1): + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:2]]) + if len(paper.get("authors", [])) > 2: + authors += " et al." + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + paper_id = paper.get("paperId", "") + + open_access = paper.get("openAccessPdf") + has_pdf = "📄" if open_access else "" + + result_lines.append(f"{j}. {has_pdf} **{title}**") + result_lines.append(f" {authors} ({year}) | Citations: {citations}") + result_lines.append(f" ID: {paper_id}") + result_lines.append("") + + except requests.exceptions.Timeout: + result_lines.append(f"\n## Query {i}: \"{query}\"") + result_lines.append("Error: Request timeout") + except Exception as e: + result_lines.append(f"\n## Query {i}: \"{query}\"") + result_lines.append(f"Error: {str(e)}") + + yield self.create_text_message("\n".join(result_lines)) diff --git a/ai4s_semantic_scholar/tools/bulk_search.yaml b/ai4s_semantic_scholar/tools/bulk_search.yaml new file mode 100644 index 000000000..b36b97744 --- /dev/null +++ b/ai4s_semantic_scholar/tools/bulk_search.yaml @@ -0,0 +1,39 @@ +identity: + name: bulk_search + author: ai4scholar + label: + en_US: Bulk Search + zh_Hans: 批量搜索 +description: + human: + en_US: Execute multiple search queries at once and get combined results + zh_Hans: 一次执行多个搜索查询并获取合并结果 + llm: Execute multiple search queries in a single request. Useful when you need to search for papers on multiple different topics. +parameters: + - name: queries + type: string + required: true + label: + en_US: Search Queries + zh_Hans: 搜索查询 + human_description: + en_US: Multiple queries separated by newlines or semicolons + zh_Hans: 多个查询,用换行符或分号分隔 + llm_description: Multiple search queries, separated by newlines or semicolons. Each query will be searched separately. + form: llm + - name: limit_per_query + type: number + required: false + default: 5 + label: + en_US: Results per Query + zh_Hans: 每个查询的结果数 + human_description: + en_US: Maximum papers per query (1-20, default 5) + zh_Hans: 每个查询的最大论文数(1-20,默认5) + llm_description: Maximum number of papers to return per query + form: form + +extra: + python: + source: tools/bulk_search.py diff --git a/ai4s_semantic_scholar/tools/multiple_papers_detail.py b/ai4s_semantic_scholar/tools/multiple_papers_detail.py new file mode 100644 index 000000000..bf971e1b8 --- /dev/null +++ b/ai4s_semantic_scholar/tools/multiple_papers_detail.py @@ -0,0 +1,112 @@ +from typing import Any, Generator +import requests +import re +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class MultiplePapersDetailTool(Tool): + """ + Get details for multiple papers at once + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + paper_ids_raw = tool_parameters.get("paper_ids", "") + if not paper_ids_raw: + yield self.create_text_message("Error: Paper IDs are required") + return + + # Parse paper IDs (split by commas or newlines) + paper_ids = [p.strip() for p in re.split(r'[,\n]', paper_ids_raw) if p.strip()] + + if not paper_ids: + yield self.create_text_message("Error: No valid paper IDs found") + return + + if len(paper_ids) > 20: + paper_ids = paper_ids[:20] + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + result_lines = [f"# Multiple Papers Detail\n**Requesting:** {len(paper_ids)} papers\n"] + + for i, paper_id in enumerate(paper_ids, 1): + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/{paper_id}", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "paperId,title,authors,year,abstract,citationCount,openAccessPdf,venue,externalIds,tldr" + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + result_lines.append(f"\n## Paper {i}: Not Found") + result_lines.append(f"ID: {paper_id}") + continue + elif response.status_code != 200: + result_lines.append(f"\n## Paper {i}: Error") + result_lines.append(f"API returned status {response.status_code}") + continue + + paper = response.json() + + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:5]]) + if len(paper.get("authors", [])) > 5: + authors += " et al." + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + venue = paper.get("venue", "") + + external_ids = paper.get("externalIds", {}) + doi = external_ids.get("DOI", "") + + tldr = paper.get("tldr", {}) + tldr_text = tldr.get("text", "") if tldr else "" + + abstract = paper.get("abstract", "") + if abstract and len(abstract) > 300: + abstract = abstract[:300] + "..." + + open_access = paper.get("openAccessPdf") + pdf_url = open_access.get("url", "") if open_access else "" + + result_lines.append(f"\n---\n## {i}. {title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {citations}") + if venue: + result_lines.append(f"**Venue:** {venue}") + if doi: + result_lines.append(f"**DOI:** {doi}") + if tldr_text: + result_lines.append(f"**TL;DR:** {tldr_text}") + elif abstract: + result_lines.append(f"**Abstract:** {abstract}") + if pdf_url: + result_lines.append(f"**PDF:** {pdf_url}") + result_lines.append(f"**Paper ID:** {paper.get('paperId', paper_id)}") + + except requests.exceptions.Timeout: + result_lines.append(f"\n## Paper {i}: Timeout") + result_lines.append(f"ID: {paper_id}") + except Exception as e: + result_lines.append(f"\n## Paper {i}: Error") + result_lines.append(f"Error: {str(e)}") + + yield self.create_text_message("\n".join(result_lines)) diff --git a/ai4s_semantic_scholar/tools/multiple_papers_detail.yaml b/ai4s_semantic_scholar/tools/multiple_papers_detail.yaml new file mode 100644 index 000000000..7256a14e2 --- /dev/null +++ b/ai4s_semantic_scholar/tools/multiple_papers_detail.yaml @@ -0,0 +1,27 @@ +identity: + name: multiple_papers_detail + author: ai4scholar + label: + en_US: Multiple Papers Detail + zh_Hans: 多篇论文详情 +description: + human: + en_US: Get details for multiple papers at once by their IDs + zh_Hans: 通过 ID 一次获取多篇论文的详情 + llm: Get detailed information about multiple papers in a single request. Provide paper IDs separated by commas or newlines. +parameters: + - name: paper_ids + type: string + required: true + label: + en_US: Paper IDs + zh_Hans: 论文 ID 列表 + human_description: + en_US: Paper IDs separated by commas or newlines (Semantic Scholar IDs, DOIs, or arXiv IDs) + zh_Hans: 论文 ID,用逗号或换行符分隔(Semantic Scholar ID、DOI 或 arXiv ID) + llm_description: Multiple paper identifiers separated by commas or newlines. Can be Semantic Scholar IDs, DOIs (prefix with "DOI:"), or arXiv IDs (prefix with "arXiv:") + form: llm + +extra: + python: + source: tools/multiple_papers_detail.py diff --git a/ai4s_semantic_scholar/tools/paper_citations.py b/ai4s_semantic_scholar/tools/paper_citations.py new file mode 100644 index 000000000..11bd3adbb --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_citations.py @@ -0,0 +1,89 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class PaperCitationsTool(Tool): + """ + Get papers that cite a specific paper + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + paper_id = tool_parameters.get("paper_id", "") + if not paper_id: + yield self.create_text_message("Error: Paper ID is required") + return + + limit = min(max(int(tool_parameters.get("limit", 20)), 1), 100) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/{paper_id}/citations", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "paperId,title,authors,year,citationCount,venue", + "limit": limit + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Paper not found with ID: {paper_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + citations = data.get("data", []) + + if not citations: + yield self.create_text_message(f"No citations found for paper: {paper_id}") + return + + result_lines = [f"# Papers Citing This Paper\n**Paper ID:** {paper_id} | **Showing:** {len(citations)} citations\n"] + + for i, item in enumerate(citations, 1): + paper = item.get("citingPaper", {}) + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:3]]) + if len(paper.get("authors", [])) > 3: + authors += " et al." + year = paper.get("year", "N/A") + cite_count = paper.get("citationCount", 0) + venue = paper.get("venue", "") + citing_paper_id = paper.get("paperId", "") + + result_lines.append(f"### {i}. {title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {cite_count}") + if venue: + result_lines.append(f"**Venue:** {venue}") + result_lines.append(f"**Paper ID:** {citing_paper_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/paper_citations.yaml b/ai4s_semantic_scholar/tools/paper_citations.yaml new file mode 100644 index 000000000..72ca5d882 --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_citations.yaml @@ -0,0 +1,38 @@ +identity: + name: paper_citations + author: ai4scholar + label: + en_US: Paper Citations + zh_Hans: 论文引用 +description: + human: + en_US: Get papers that cite a specific paper + zh_Hans: 获取引用特定论文的论文列表 + llm: Get a list of papers that cite the given paper. Useful for understanding the impact and follow-up research. +parameters: + - name: paper_id + type: string + required: true + label: + en_US: Paper ID + zh_Hans: 论文 ID + human_description: + en_US: Paper ID to get citations for (Semantic Scholar ID, DOI, or arXiv ID) + zh_Hans: 获取引用的论文 ID(Semantic Scholar ID、DOI 或 arXiv ID) + llm_description: The paper ID to get citations for. Can be Semantic Scholar ID, DOI (prefix with "DOI:"), or arXiv ID (prefix with "arXiv:") + form: llm + - name: limit + type: number + required: false + default: 20 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of citations to return (1-100, default 20) + zh_Hans: 返回的最大引用数量(1-100,默认20) + llm_description: Maximum number of citing papers to return + form: form +extra: + python: + source: tools/paper_citations.py diff --git a/ai4s_semantic_scholar/tools/paper_detail.py b/ai4s_semantic_scholar/tools/paper_detail.py new file mode 100644 index 000000000..fe2fe984a --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_detail.py @@ -0,0 +1,149 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class PaperDetailTool(Tool): + """ + Get detailed information about a specific paper + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + paper_id = tool_parameters.get("paper_id", "") + if not paper_id: + yield self.create_text_message("Error: Paper ID is required") + return + + include_citations = tool_parameters.get("include_citations", False) + include_references = tool_parameters.get("include_references", False) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + # Build fields parameter + fields = [ + "paperId", "title", "authors", "year", "abstract", + "citationCount", "referenceCount", "openAccessPdf", + "venue", "publicationDate", "externalIds", "tldr", + "fieldsOfStudy", "publicationTypes" + ] + + if include_citations: + fields.append("citations.paperId") + fields.append("citations.title") + fields.append("citations.year") + fields.append("citations.citationCount") + + if include_references: + fields.append("references.paperId") + fields.append("references.title") + fields.append("references.year") + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/{paper_id}", + headers={"Authorization": f"Bearer {api_key}"}, + params={"fields": ",".join(fields)}, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Paper not found with ID: {paper_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + paper = response.json() + + # Format result + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])]) + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + ref_count = paper.get("referenceCount", 0) + venue = paper.get("venue", "") + abstract = paper.get("abstract", "") + + external_ids = paper.get("externalIds", {}) + doi = external_ids.get("DOI", "") + arxiv = external_ids.get("ArXiv", "") + + tldr = paper.get("tldr", {}) + tldr_text = tldr.get("text", "") if tldr else "" + + fields_of_study = paper.get("fieldsOfStudy", []) + pub_types = paper.get("publicationTypes", []) + + open_access = paper.get("openAccessPdf") + pdf_url = open_access.get("url", "") if open_access else "" + + result_lines = [f"# {title}"] + result_lines.append(f"\n**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {citations} | **References:** {ref_count}") + + if venue: + result_lines.append(f"**Venue:** {venue}") + if fields_of_study: + result_lines.append(f"**Fields:** {', '.join(fields_of_study)}") + if pub_types: + result_lines.append(f"**Type:** {', '.join(pub_types)}") + if doi: + result_lines.append(f"**DOI:** {doi}") + if arxiv: + result_lines.append(f"**arXiv:** {arxiv}") + + if tldr_text: + result_lines.append(f"\n**TL;DR:** {tldr_text}") + + if abstract: + result_lines.append(f"\n**Abstract:**\n{abstract}") + + if pdf_url: + result_lines.append(f"\n**Open Access PDF:** {pdf_url}") + + result_lines.append(f"\n**Paper ID:** {paper.get('paperId', paper_id)}") + + # Citations + if include_citations: + citations_list = paper.get("citations", []) + if citations_list: + result_lines.append(f"\n---\n## Recent Citations ({len(citations_list)} shown)") + for c in citations_list[:10]: + c_title = c.get("title", "N/A") + c_year = c.get("year", "N/A") + c_cites = c.get("citationCount", 0) + result_lines.append(f"- {c_title} ({c_year}, {c_cites} citations)") + + # References + if include_references: + references_list = paper.get("references", []) + if references_list: + result_lines.append(f"\n---\n## References ({len(references_list)} shown)") + for r in references_list[:10]: + r_title = r.get("title", "N/A") + r_year = r.get("year", "N/A") + result_lines.append(f"- {r_title} ({r_year})") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/paper_detail.yaml b/ai4s_semantic_scholar/tools/paper_detail.yaml new file mode 100644 index 000000000..edf3337d1 --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_detail.yaml @@ -0,0 +1,51 @@ +identity: + name: paper_detail + author: ai4scholar + label: + en_US: Paper Detail + zh_Hans: 论文详情 +description: + human: + en_US: Get detailed information about a specific paper by ID, DOI, or arXiv ID + zh_Hans: 通过 ID、DOI 或 arXiv ID 获取特定论文的详细信息 + llm: Get detailed information about an academic paper using its Semantic Scholar Paper ID, DOI, or arXiv ID. +parameters: + - name: paper_id + type: string + required: true + label: + en_US: Paper ID + zh_Hans: 论文 ID + human_description: + en_US: Paper identifier (Semantic Scholar ID, DOI like "DOI:10.1234/xxx", or arXiv ID like "arXiv:2301.00001") + zh_Hans: 论文标识符(Semantic Scholar ID、DOI 如 "DOI:10.1234/xxx"、或 arXiv ID 如 "arXiv:2301.00001") + llm_description: The paper identifier. Can be Semantic Scholar Paper ID, DOI (prefix with "DOI:"), or arXiv ID (prefix with "arXiv:") + form: llm + - name: include_citations + type: boolean + required: false + default: false + label: + en_US: Include Citations + zh_Hans: 包含引用 + human_description: + en_US: Include recent citing papers + zh_Hans: 包含最近引用此论文的论文 + llm_description: If true, include recent papers that cite this paper + form: form + - name: include_references + type: boolean + required: false + default: false + label: + en_US: Include References + zh_Hans: 包含参考文献 + human_description: + en_US: Include papers referenced by this paper + zh_Hans: 包含此论文引用的论文 + llm_description: If true, include papers that this paper references + form: form + +extra: + python: + source: tools/paper_detail.py diff --git a/ai4s_semantic_scholar/tools/paper_recommendations.py b/ai4s_semantic_scholar/tools/paper_recommendations.py new file mode 100644 index 000000000..17a6e1017 --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_recommendations.py @@ -0,0 +1,100 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class PaperRecommendationsTool(Tool): + """ + Get paper recommendations based on a given paper + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + paper_id = tool_parameters.get("paper_id", "") + if not paper_id: + yield self.create_text_message("Error: Paper ID is required") + return + + limit = min(max(int(tool_parameters.get("limit", 10)), 1), 100) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + # Use the recommendations endpoint + response = requests.get( + f"{self.BASE_URL}/recommendations/v1/papers/forpaper/{paper_id}", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "paperId,title,authors,year,citationCount,venue,abstract,openAccessPdf", + "limit": limit + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Paper not found or no recommendations available for: {paper_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + papers = data.get("recommendedPapers", []) + + if not papers: + yield self.create_text_message(f"No recommendations found for paper: {paper_id}") + return + + result_lines = [f"# Paper Recommendations\n**Based on:** {paper_id} | **Found:** {len(papers)} recommendations\n"] + + for i, paper in enumerate(papers, 1): + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:3]]) + if len(paper.get("authors", [])) > 3: + authors += " et al." + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + venue = paper.get("venue", "") + rec_paper_id = paper.get("paperId", "") + + abstract = paper.get("abstract", "") + if abstract and len(abstract) > 200: + abstract = abstract[:200] + "..." + + open_access = paper.get("openAccessPdf") + pdf_url = open_access.get("url", "") if open_access else "" + + result_lines.append(f"### {i}. {title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {citations}") + if venue: + result_lines.append(f"**Venue:** {venue}") + if abstract: + result_lines.append(f"**Abstract:** {abstract}") + if pdf_url: + result_lines.append(f"**PDF:** {pdf_url}") + result_lines.append(f"**Paper ID:** {rec_paper_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/paper_recommendations.yaml b/ai4s_semantic_scholar/tools/paper_recommendations.yaml new file mode 100644 index 000000000..5cb3c8b5c --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_recommendations.yaml @@ -0,0 +1,38 @@ +identity: + name: paper_recommendations + author: ai4scholar + label: + en_US: Paper Recommendations + zh_Hans: 论文推荐 +description: + human: + en_US: Get paper recommendations based on a given paper + zh_Hans: 基于给定论文获取推荐论文 + llm: Get recommended papers similar to a given paper. Useful for finding related work and expanding literature review. +parameters: + - name: paper_id + type: string + required: true + label: + en_US: Paper ID + zh_Hans: 论文 ID + human_description: + en_US: Paper ID to get recommendations for (Semantic Scholar ID, DOI, or arXiv ID) + zh_Hans: 获取推荐的论文 ID(Semantic Scholar ID、DOI 或 arXiv ID) + llm_description: The paper ID to base recommendations on. Can be Semantic Scholar ID, DOI (prefix with "DOI:"), or arXiv ID (prefix with "arXiv:") + form: llm + - name: limit + type: number + required: false + default: 10 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of recommendations (1-100, default 10) + zh_Hans: 推荐论文数量(1-100,默认10) + llm_description: Maximum number of recommended papers to return + form: form +extra: + python: + source: tools/paper_recommendations.py diff --git a/ai4s_semantic_scholar/tools/paper_references.py b/ai4s_semantic_scholar/tools/paper_references.py new file mode 100644 index 000000000..fb537e153 --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_references.py @@ -0,0 +1,89 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class PaperReferencesTool(Tool): + """ + Get the references of a specific paper + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + paper_id = tool_parameters.get("paper_id", "") + if not paper_id: + yield self.create_text_message("Error: Paper ID is required") + return + + limit = min(max(int(tool_parameters.get("limit", 20)), 1), 100) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/{paper_id}/references", + headers={"Authorization": f"Bearer {api_key}"}, + params={ + "fields": "paperId,title,authors,year,citationCount,venue", + "limit": limit + }, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code == 404: + yield self.create_text_message(f"Error: Paper not found with ID: {paper_id}") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + references = data.get("data", []) + + if not references: + yield self.create_text_message(f"No references found for paper: {paper_id}") + return + + result_lines = [f"# References of This Paper\n**Paper ID:** {paper_id} | **Showing:** {len(references)} references\n"] + + for i, item in enumerate(references, 1): + paper = item.get("citedPaper", {}) + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:3]]) + if len(paper.get("authors", [])) > 3: + authors += " et al." + year = paper.get("year", "N/A") + cite_count = paper.get("citationCount", 0) + venue = paper.get("venue", "") + ref_paper_id = paper.get("paperId", "") + + result_lines.append(f"### {i}. {title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {cite_count}") + if venue: + result_lines.append(f"**Venue:** {venue}") + result_lines.append(f"**Paper ID:** {ref_paper_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/paper_references.yaml b/ai4s_semantic_scholar/tools/paper_references.yaml new file mode 100644 index 000000000..b4c11e084 --- /dev/null +++ b/ai4s_semantic_scholar/tools/paper_references.yaml @@ -0,0 +1,38 @@ +identity: + name: paper_references + author: ai4scholar + label: + en_US: Paper References + zh_Hans: 论文参考文献 +description: + human: + en_US: Get the references (bibliography) of a specific paper + zh_Hans: 获取特定论文的参考文献列表 + llm: Get a list of papers referenced by the given paper. Useful for understanding the background and related work. +parameters: + - name: paper_id + type: string + required: true + label: + en_US: Paper ID + zh_Hans: 论文 ID + human_description: + en_US: Paper ID to get references for (Semantic Scholar ID, DOI, or arXiv ID) + zh_Hans: 获取参考文献的论文 ID(Semantic Scholar ID、DOI 或 arXiv ID) + llm_description: The paper ID to get references for. Can be Semantic Scholar ID, DOI (prefix with "DOI:"), or arXiv ID (prefix with "arXiv:") + form: llm + - name: limit + type: number + required: false + default: 20 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of references to return (1-100, default 20) + zh_Hans: 返回的最大参考文献数量(1-100,默认20) + llm_description: Maximum number of referenced papers to return + form: form +extra: + python: + source: tools/paper_references.py diff --git a/ai4s_semantic_scholar/tools/semantic_search.py b/ai4s_semantic_scholar/tools/semantic_search.py new file mode 100644 index 000000000..c2fdea362 --- /dev/null +++ b/ai4s_semantic_scholar/tools/semantic_search.py @@ -0,0 +1,111 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class SemanticSearchTool(Tool): + """ + Semantic search tool for finding academic papers by relevance + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + query = tool_parameters.get("query", "") + if not query: + yield self.create_text_message("Error: Search query is required") + return + + limit = min(max(int(tool_parameters.get("limit", 10)), 1), 100) + year = tool_parameters.get("year", "") + fields_of_study = tool_parameters.get("fields_of_study", "") + open_access_only = tool_parameters.get("open_access_only", False) + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + # Build request parameters + params = { + "query": query, + "limit": limit, + "fields": "paperId,title,authors,year,abstract,citationCount,openAccessPdf,venue,publicationDate,externalIds" + } + + if year: + params["year"] = year + if fields_of_study: + params["fieldsOfStudy"] = fields_of_study + if open_access_only: + params["openAccessPdf"] = "" + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/search", + headers={"Authorization": f"Bearer {api_key}"}, + params=params, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + papers = data.get("data", []) + total = data.get("total", 0) + + if not papers: + yield self.create_text_message(f"No papers found for query: {query}") + return + + # Format results + result_lines = [f"Found {total} papers for query: \"{query}\" (showing {len(papers)})\n"] + + for i, paper in enumerate(papers, 1): + paper_id = paper.get("paperId", "N/A") + title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])[:3]]) + if len(paper.get("authors", [])) > 3: + authors += " et al." + year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + venue = paper.get("venue", "") + abstract = paper.get("abstract", "") + if abstract and len(abstract) > 300: + abstract = abstract[:300] + "..." + + open_access = paper.get("openAccessPdf") + pdf_url = open_access.get("url", "") if open_access else "" + + result_lines.append(f"### {i}. {title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {year} | **Citations:** {citations}") + if venue: + result_lines.append(f"**Venue:** {venue}") + if abstract: + result_lines.append(f"**Abstract:** {abstract}") + if pdf_url: + result_lines.append(f"**PDF:** {pdf_url}") + result_lines.append(f"**Paper ID:** {paper_id}") + result_lines.append("") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/semantic_search.yaml b/ai4s_semantic_scholar/tools/semantic_search.yaml new file mode 100644 index 000000000..e78876319 --- /dev/null +++ b/ai4s_semantic_scholar/tools/semantic_search.yaml @@ -0,0 +1,73 @@ +identity: + name: semantic_search + author: ai4scholar + label: + en_US: Semantic Search + zh_Hans: 语义搜索 +description: + human: + en_US: Search academic papers by relevance using natural language queries + zh_Hans: 使用自然语言查询按相关性搜索学术论文 + llm: Search for academic papers on Semantic Scholar using natural language queries. Returns papers ranked by relevance to the query. +parameters: + - name: query + type: string + required: true + label: + en_US: Search Query + zh_Hans: 搜索查询 + human_description: + en_US: Natural language search query for finding relevant papers + zh_Hans: 用于查找相关论文的自然语言搜索查询 + llm_description: The search query in natural language to find relevant academic papers + form: llm + - name: limit + type: number + required: false + default: 10 + label: + en_US: Result Limit + zh_Hans: 结果数量 + human_description: + en_US: Maximum number of papers to return (1-100, default 10) + zh_Hans: 返回的最大论文数量(1-100,默认10) + llm_description: Maximum number of papers to return, between 1 and 100 + form: form + - name: year + type: string + required: false + label: + en_US: Year Filter + zh_Hans: 年份筛选 + human_description: + en_US: Filter by publication year (e.g., "2020", "2020-2024", "2020-") + zh_Hans: 按发表年份筛选(如 "2020"、"2020-2024"、"2020-") + llm_description: Filter papers by publication year. Can be a single year, range (2020-2024), or open-ended (2020-) + form: form + - name: fields_of_study + type: string + required: false + label: + en_US: Fields of Study + zh_Hans: 研究领域 + human_description: + en_US: Filter by field (e.g., "Computer Science", "Medicine") + zh_Hans: 按领域筛选(如 "Computer Science"、"Medicine") + llm_description: Filter papers by field of study, comma-separated for multiple fields + form: form + - name: open_access_only + type: boolean + required: false + default: false + label: + en_US: Open Access Only + zh_Hans: 仅开放获取 + human_description: + en_US: Only return papers with free PDF available + zh_Hans: 仅返回有免费PDF的论文 + llm_description: If true, only return papers that have open access PDFs available + form: form + +extra: + python: + source: tools/semantic_search.py diff --git a/ai4s_semantic_scholar/tools/title_search.py b/ai4s_semantic_scholar/tools/title_search.py new file mode 100644 index 000000000..bfa9f3e2d --- /dev/null +++ b/ai4s_semantic_scholar/tools/title_search.py @@ -0,0 +1,113 @@ +from typing import Any, Generator +import requests +from dify_plugin.entities.tool import ToolInvokeMessage +from dify_plugin import Tool + + +class TitleSearchTool(Tool): + """ + Search for papers by title + """ + + BASE_URL = "https://ai4scholar.net" + + def _invoke( + self, tool_parameters: dict[str, Any] + ) -> Generator[ToolInvokeMessage, None, None]: + + title = tool_parameters.get("title", "") + if not title: + yield self.create_text_message("Error: Paper title is required") + return + + year = tool_parameters.get("year") + + api_key = self.runtime.credentials.get("api_key", "") + if not api_key: + yield self.create_text_message("Error: API key is required") + return + + params = { + "query": title, + "limit": 5, + "fields": "paperId,title,authors,year,abstract,citationCount,openAccessPdf,venue,publicationDate,externalIds" + } + + if year: + params["year"] = str(int(year)) + + try: + response = requests.get( + f"{self.BASE_URL}/graph/v1/paper/search", + headers={"Authorization": f"Bearer {api_key}"}, + params=params, + timeout=30 + ) + + if response.status_code == 401: + yield self.create_text_message("Error: Invalid API key") + return + elif response.status_code == 402: + yield self.create_text_message("Error: Insufficient credits. Please recharge at ai4scholar.net") + return + elif response.status_code != 200: + yield self.create_text_message(f"Error: API returned status {response.status_code}") + return + + data = response.json() + papers = data.get("data", []) + + if not papers: + yield self.create_text_message(f"No papers found with title: {title}") + return + + # Find best match by title similarity + best_match = papers[0] + + result_lines = [f"Found paper matching title: \"{title}\"\n"] + + paper = best_match + paper_id = paper.get("paperId", "N/A") + paper_title = paper.get("title", "N/A") + authors = ", ".join([a.get("name", "") for a in paper.get("authors", [])]) + paper_year = paper.get("year", "N/A") + citations = paper.get("citationCount", 0) + venue = paper.get("venue", "") + abstract = paper.get("abstract", "") + + external_ids = paper.get("externalIds", {}) + doi = external_ids.get("DOI", "") + arxiv = external_ids.get("ArXiv", "") + + open_access = paper.get("openAccessPdf") + pdf_url = open_access.get("url", "") if open_access else "" + + result_lines.append(f"## {paper_title}") + result_lines.append(f"**Authors:** {authors}") + result_lines.append(f"**Year:** {paper_year} | **Citations:** {citations}") + if venue: + result_lines.append(f"**Venue:** {venue}") + if doi: + result_lines.append(f"**DOI:** {doi}") + if arxiv: + result_lines.append(f"**arXiv:** {arxiv}") + if abstract: + result_lines.append(f"\n**Abstract:**\n{abstract}") + if pdf_url: + result_lines.append(f"\n**PDF:** {pdf_url}") + result_lines.append(f"\n**Paper ID:** {paper_id}") + + # Show other matches if any + if len(papers) > 1: + result_lines.append("\n---\n**Other possible matches:**") + for p in papers[1:]: + result_lines.append(f"- {p.get('title', 'N/A')} ({p.get('year', 'N/A')})") + + yield self.create_text_message("\n".join(result_lines)) + + except requests.exceptions.Timeout: + yield self.create_text_message("Error: Request timeout. Please try again.") + except requests.exceptions.RequestException as e: + yield self.create_text_message(f"Error: Network error - {str(e)}") + except Exception as e: + yield self.create_text_message(f"Error: {str(e)}") diff --git a/ai4s_semantic_scholar/tools/title_search.yaml b/ai4s_semantic_scholar/tools/title_search.yaml new file mode 100644 index 000000000..368448ac6 --- /dev/null +++ b/ai4s_semantic_scholar/tools/title_search.yaml @@ -0,0 +1,38 @@ +identity: + name: title_search + author: ai4scholar + label: + en_US: Title Search + zh_Hans: 标题搜索 +description: + human: + en_US: Search for a specific paper by its exact or partial title + zh_Hans: 通过精确或部分标题搜索特定论文 + llm: Search for a specific academic paper by its title. Use this when you know the paper's title and want to find it directly. +parameters: + - name: title + type: string + required: true + label: + en_US: Paper Title + zh_Hans: 论文标题 + human_description: + en_US: The title of the paper to search for + zh_Hans: 要搜索的论文标题 + llm_description: The title of the paper to search for + form: llm + - name: year + type: number + required: false + label: + en_US: Publication Year + zh_Hans: 发表年份 + human_description: + en_US: Filter by specific publication year + zh_Hans: 按特定发表年份筛选 + llm_description: Filter results by publication year + form: form + +extra: + python: + source: tools/title_search.py