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 000000000..c66e8204c Binary files /dev/null and b/ai4s_semantic_scholar/_assets/icon.png differ 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