Skill: agents365-ai/semanticscholar-skill (version 0.3.0)

--- name: semanticscholar-skill description: Use when searching academic papers, looking up citations, finding authors, or getting paper recommendations using the Semantic Scholar API. Triggers on queries about research papers, academic search, citation analysis, or literature discovery. license: MIT homepage: https://github.com/Agents365-ai/semanticscholar-skill compatibility: Requires python3 and the `requests` package. Set S2_API_KEY for higher rate limits (request at https://www.semanticscholar.org/product/api#api-key). Works unauthenticated with strict rate limits. platforms: [macos, linux, windows] metadata: {"openclaw":{"requires":{"bins":["python3"],"env":["S2_API_KEY"]},"emoji":"📚"},"hermes":{"tags":["semantic-scholar","academic","paper-search","citation","literature","research"],"category":"research","requires_tools":["python3"],"related_skills":["asta-skill","zotero-research-assistant","literature-review","paper-reader"]},"author":"Agents365-ai","version":"0.3.0"} --- # Semantic Scholar Search Workflow Search academic papers via the Semantic Scholar API using a structured 4-phase workflow. **Critical rule:** NEVER make multiple sequential Bash calls for API requests. Always write ONE Python script that runs all searches, then execute it once. All rate limiting is handled inside `s2.py` automatically. ## Phase 1: Understand & Plan Parse the user's intent and choose a search strategy: ### Decision Tree | User wants... | Strategy | Function | |---------------|----------|----------| | Broad topic exploration | Relevance search | `search_relevance()` | | Precise technical terms, exact phrases | Bulk search with boolean operators | `search_bulk()` with `build_bool_query()` | | Specific passages or methods | Snippet search | `search_snippets()` | | Known paper by title | Title match | `match_title()` | | Known paper by DOI/PMID/ArXiv | Direct lookup | `get_paper()` | | Papers citing a known work | Citation traversal | `get_citations()` | | Related to one paper | Single-seed recommendations | `find_similar()` | | Related to multiple papers | Multi-seed recommendations | `recommend()` | | Find a researcher | Author search | `search_authors()` | | Researcher's profile | Author details | `get_author()` | | Researcher's publications | Author papers | `get_author_papers()` | ### Query Construction Rules - **Ambiguous terms** (e.g., "stem cells" could mean mesenchymal or stem-like T cells): Use `build_bool_query()` with exact phrases and exclusions - Example: `build_bool_query(phrases=["stem-like T cells"], required=["CD4", "TCF7"], excluded=["mesenchymal", "hematopoietic stem cell"])` - **Multi-context queries** (e.g., "topic X in cancer AND autoimmunity"): Plan separate searches, deduplicate with `deduplicate()` - **Broad topics**: Use `search_relevance()` with filters (year, venue, fieldsOfStudy, minCitationCount) ### Plan Filters | Filter | Use when | |--------|----------| | `year="2020-"` | Recent work only | | `publication_date="2024-01-01:2024-06-30"` | Precise date range (YYYY-MM-DD) | | `fields_of_study="Medicine"` | Restrict to domain | | `min_citations=10` | Only established papers | | `pub_types="Review"` | Find reviews/meta-analyses | | `pub_types="ClinicalTrial"` | Clinical trials only | | `open_access=True` | Only open access papers | **Checkpoint:** Before proceeding, verify: (1) search strategy matches user intent, (2) filters are appropriate, (3) query is specific enough to avoid irrelevant results. ## Phase 2: Execute Search Write ONE Python script. Example: ```python import sys, os SKILL_DIR = next((p for p in [ os.path.expanduser("~/.claude/skills/semanticscholar-skill"), os.path.expanduser("~/.openclaw/skills/semanticscholar-skill"), ] if os.path.isdir(p)), ".") sys.path.insert(0, SKILL_DIR) from s2 import * # Build precise query q = build_bool_query( phrases=["stem-like T cells"], required=["CD4", "IBD"], excluded=["mesenchymal"] ) papers = search_bulk(q, max_results=30, year="2018-", fields_of_study="Medicine") papers = deduplicate(papers) print(format_results(papers, "Stem-like CD4 T cells in IBD")) ``` Execute with: `python3 /tmp/s2_search.py` **Rules:** - Import everything from s2: `from s2 import *` - Write script to `/tmp/s2_search.py` (or similar temp path) - One Bash call to execute. Never chain multiple API calls via separate Bash invocations. - Rate limiting, retries, and backoff are automatic inside s2.py **Checkpoint:** Verify the script ran successfully (no exceptions) and returned results. If 0 results, broaden the query or relax filters before presenting. ### Worked Examples **Example 1: Author workflow** — "Find papers by Yann LeCun on self-supervised learning" ```python import sys, os SKILL_DIR = next((p for p in [ os.path.expanduser("~/.claude/skills/semanticscholar-skill"), os.path.expanduser("~/.openclaw/skills/semanticscholar-skill"), ] if os.path.isdir(p)), ".") sys.path.insert(0, SKILL_DIR) from s2 import * authors = search_authors("Yann LeCun", max_results=5) print(format_authors(authors)) # Use the first match's ID to get their papers author_id = authors[0]["authorId"] papers = get_author_papers(author_id, max_results=50) # Filter locally for topic ssl_papers = [p for p in papers if "self-supervised" in (p.get("title") or "").lower()] print(format_results(ssl_papers, "Yann LeCun - Self-Supervised Learning")) ``` **Example 2: Citation chain** — "Who cited the Transformer paper and what did they build on?" ```python import sys, os SKILL_DIR = next((p for p in [ os.path.expanduser("~/.claude/skills/semanticscholar-skill"), os.path.expanduser("~/.openclaw/skills/semanticscholar-skill"), ] if os.path.isdir(p)), ".") sys.path.insert(0, SKILL_DIR) from s2 import * paper = get_paper("DOI:10.48550/arXiv.1706.03762") print(f"Title: {paper['title']}, Citations: {paper['citationCount']}") # Get top-cited papers that cite this one citing = get_citations(paper["paperId"], max_results=50) citing_papers = [c["citingPaper"] for c in citing if c.get("citingPaper")] citing_papers.sort(key=lambda p: p.get("citationCount", 0), reverse=True) print(format_results(citing_papers, "Most-cited papers citing Attention Is All You Need")) ``` **Example 3: Multi-seed recommendations with BibTeX export** — "Find papers like these two but not about NLP" ```python import sys, os SKILL_DIR = next((p for p in [ os.path.expanduser("~/.claude/skills/semanticscholar-skill"), os.path.expanduser("~/.openclaw/skills/semanticscholar-skill"), ] if os.path.isdir(p)), ".") sys.path.insert(0, SKILL_DIR) from s2 import * recs = recommend( positive_ids=["DOI:10.1038/nature14539", "ARXIV:2010.11929"], negative_ids=["ARXIV:1706.03762"], limit=20 ) print(format_results(recs, "Vision papers like Deep Learning & ViT, excluding NLP")) # Export BibTeX for top results bib_data = batch_papers([r["paperId"] for r in recs[:10]], fields="title,citationStyles") print(export_bibtex(bib_data)) ``` ## Phase 3: Summarize & Present - Use `format_results()` for consistent output (summary table + top-10 details) - If user's language is Chinese, present summaries in Chinese - Always note total results count and search strategy used - Highlight most relevant papers based on the user's specific question ## Phase 4: User Interaction Loop After presenting results, **always offer these options:** 1. **Translate** — titles/summaries to Chinese (or other language) 2. **Details** — full abstract for specific paper numbers 3. **Refine** — narrow or expand search with different terms/filters 4. **Similar** — find papers similar to a specific result (`find_similar()`) 5. **Citations** — who cited a specific paper (`get_citations()`) 6. **Export** — save results via `export_bibtex()`, `export_markdown()`, or `export_json()` 7. **Done** — end search session Loop until user says done. Each follow-up uses the same single-script pattern. --- ## API Quick Reference ### Helper Module (`s2.py`) ```python import sys, os SKILL_DIR = next((p for p in [ os.path.expanduser("~/.claude/skills/semanticscholar-skill"), os.path.expanduser("~/.openclaw/skills/semanticscholar-skill"), ] if os.path.isdir(p)), ".") sys.path.insert(0, SKILL_DIR) from s2 import * ``` ### Paper Search Functions | Function | Purpose | Max Results | |----------|---------|-------------| | `search_relevance(query, **filters)` | Simple broad search | 1,000 | | `search_bulk(query, sort=..., **filters)` | Boolean precise search | 10,000,000 | | `search_snippets(query, **filters)` | Full-text passage search | 1,000 | | `match_title(title)` | Exact title match | 1 | | `get_paper(paper_id)` | Single paper details | — | | `get_citations(paper_id, max_results)` | Who cited this | 10,000 | | `get_references(paper_id, max_results)` | What this cites | 10,000 | | `find_similar(paper_id, limit, pool)` | Single-seed recommendations | 500 | | `recommend(positive_ids, negative_ids, limit)` | Multi-seed recommendations | 500 | | `batch_papers(ids, fields)` | Batch lookup (≤500) | — | ### Author Functions | Function | Purpose | Max Results | |----------|---------|-------------| | `search_authors(query, max_results)` | Find researchers by name | 1,000 | | `get_author(author_id)` | Author profile (affiliations, h-index) | — | | `get_author_papers(author_id, max_results)` | Author's publications | 10,000 | | `get_paper_authors(paper_id, max_results)` | Paper's author list | 1,000 | | `batch_authors(ids, fields)` | Batch author lookup (≤1000) | — | ### Filter Parameters (kwargs) `year`, `publication_date`, `venue`, `fields_of_study`, `min_citations`, `pub_types`, `open_access` - `year`: `"2020-"`, `"-2019"`, `"2016-2020"` - `publication_date`: `"2024-01-01:2024-06-30"` (YYYY-MM-DD range, open-ended OK) - `pub_types`: `Review`, `JournalArticle`, `Conference`, `ClinicalTrial`, `MetaAnalysis`, `Dataset`, `Book`, `CaseReport`, `Editorial`, `LettersAndComments`, `News`, `Study`, `BookSection` ### Boolean Query Syntax (bulk search only) | Syntax | Example | Meaning | |--------|---------|---------| | `"..."` | `"deep learning"` | Exact phrase | | `+` | `+transformer` | Must include | | `-` | `-survey` | Exclude | | `\|` | `CNN \| RNN` | OR | | `*` | `neuro*` | Prefix wildcard | | `()` | `(CNN \| RNN) +attention` | Grouping | Use `build_bool_query(phrases, required, excluded, or_terms)` to construct safely. ### Output Functions | Function | Purpose | |----------|---------| | `format_table(papers, max_rows=30)` | Markdown summary table | | `format_details(papers, max_papers=10)` | Detailed entries with TLDR/abstract | | `format_results(papers, query_desc)` | Combined: summary + table + details | | `format_authors(authors, max_rows=20)` | Author table (name, affiliations, h-index) | | `export_bibtex(papers)` | BibTeX entries (requires `citationStyles` field) | | `export_markdown(papers, query_desc)` | Full markdown report saved to file | | `export_json(papers, path)` | JSON export saved to file | | `deduplicate(papers)` | Remove duplicates by paperId | ### Supported ID Formats `DOI:10.1038/...`, `ARXIV:2106.15928`, `PMID:19872477`, `PMCID:PMC2323569`, `CorpusId:215416146`, `ACL:2020.acl-main.447`, `DBLP:conf/acl/...`, `MAG:3015453090`, `URL:https://...` ### Paper Fields Default: `title,year,citationCount,authors,venue,externalIds,tldr` Additional: `abstract`, `references`, `citations`, `openAccessPdf`, `publicationDate`, `publicationVenue`, `fieldsOfStudy`, `s2FieldsOfStudy`, `journal`, `isOpenAccess`, `referenceCount`, `influentialCitationCount`, `citationStyles`, `embedding`, `textAvailability` Author fields: `name`, `affiliations`, `paperCount`, `citationCount`, `hIndex`, `homepage`, `externalIds`, `papers` ### Rate Limiting Handled automatically by `s2.py`: 1.1s gap between requests, exponential backoff (2s→4s→8s→16s→32s, max 60s) on 429/504 errors, up to 5 retries. ### Troubleshooting | Error | Cause | Fix | |-------|-------|-----| | `HTTPError 403` | Missing or invalid API key | Verify `S2_API_KEY` is set: `echo $S2_API_KEY` | | `HTTPError 429` after 5 retries | Sustained rate limit exceeded | Wait 60s, reduce `max_results`, or split into smaller batches | | `ModuleNotFoundError: s2` | Skill directory not on path | Verify skill is installed at `~/.claude/skills/` or `~/.openclaw/skills/` | | `ModuleNotFoundError: requests` | `requests` not installed | `pip install requests` or `uv pip install requests` | | 0 results returned | Query too specific or filters too narrow | Broaden query, remove filters, try `search_relevance()` instead of `search_bulk()` | | `KeyError: 'data'` | Endpoint returned error object | Check `r.get("message")` for API error details | | `tldr` field is empty | Not all papers have TLDR | Fall back to `abstract` field; bulk search never returns `tldr` |

path: README.md
content: # semanticscholar-skill — Semantic Scholar from the Command Line 📚

[中文文档](README_CN.md) | [Semantic Scholar API Docs](https://api.semanticscholar.org/api-docs/) | [Request API Key](https://www.semanticscholar.org/product/api#api-key)

## What it does

- **Search** the Semantic Scholar corpus by keyword, boolean expression, full-text snippet, title, or author
- **Look up** a paper from any supported ID (DOI, arXiv, PMID, PMCID, CorpusId, MAG, ACL, SHA, URL)
- **Traverse citations** forward and backward with pagination
- **Recommend** similar papers from one seed (`find_similar`) or multi-seed positive/negative sets (`recommend`)
- **Find authors** by name, list their publications, pull h-index / affiliations
- **Batch lookup** up to 500 papers or 1000 authors in a single call
- **Filter** by year, date range, venue, fields of study, min citations, pub types, open access
- **Export** to BibTeX, Markdown, or JSON
- **Rate limiting + retries built in** — 1.1s gap between requests, exponential backoff on 429/504
- **Single-script execution pattern** — avoids sequential API ping-pong; all calls batched into one Python script
- Triggers automatically whenever the user mentions papers, citations, academic search, literature discovery

## Multi-Platform Support

Works with all major AI coding agents that support the [Agent Skills](https://agentskills.io) format:

| Platform | Status | Details |
|----------|--------|---------|
| **Claude Code** | ✅ Full support | Native SKILL.md format |
| **Codex** | ✅ Full support | Native SKILL.md + `agents/openai.yaml` UI metadata |
| **Hermes** | ✅ Full support | `metadata.hermes` tags / category / related_skills |
| **OpenClaw** | ✅ Full support | `metadata.openclaw` namespace |
| **ClawHub** | ✅ Published | `clawhub install semanticscholar-skill` |
| **SkillsMP** | ✅ Indexed | GitHub topics configured |

## Why Semantic Scholar?

Semantic Scholar is **not another preprint server** — it is an academic **graph + semantic index** over the corpus that already includes arXiv and bioRxiv. The question is never "S2 *or* arXiv" but "S2 *on top of* arXiv".

| Dimension | arXiv | bioRxiv | **Semantic Scholar** |
|---|---|---|---|
| What it is | Preprint host (physics / CS / math / stats) | Preprint host (biology / medicine) | Aggregated academic graph over 200M+ papers |
| Corpus scope | ~2.5M arXiv papers only | ~350k bioRxiv/medRxiv papers only | arXiv + bioRxiv + PubMed + conference proceedings + journals + more |
| Search | Keyword / classification code | Keyword / subject area | Keyword + **semantic relevance ranking** + boolean + full-text snippets |
| Citation graph | ❌ not provided | ❌ not provided | ✅ forward + backward citations, influential citation count |
| "Papers like this" | ❌ | ❌ | ✅ `find_similar` + multi-seed `recommend` |
| Author disambiguation | partial (no unified IDs) | partial | ✅ unified `authorId`, affiliations, h-index |
| Venue / year / citation filters | limited | limited | ✅ rich filters: venue, fields of study, min citations, open access, pub types |
| TLDR summaries | ❌ | ❌ | ✅ `tldr` field on most papers |
| BibTeX export | via arxiv.org page | via bioRxiv page | ✅ API `citationStyles` field |

**Practical rule of thumb:**
- Need a specific arXiv or bioRxiv paper by ID → hit the source directly
- Need to **discover** what's relevant / influential / cited / similar → use Semantic Scholar
- Need **both preprints and published literature** in one ranked search → use Semantic Scholar
- Doing literature review, related-work sections, citation analysis → use Semantic Scholar

This skill does not replace arXiv or bioRxiv; it sits above them and adds the graph / ranking / recommendation layer they lack.

## Comparison

### vs. `asta-skill` (our MCP-based sibling)

| Capability | `semanticscholar-skill` | `asta-skill` |
|---|---|---|
| Transport | Python + direct REST (`s2.py`) | MCP (streamable HTTP) |
| Host requirement | Python + `S2_API_KEY` | Host with MCP support |
| Best for | Scripted batch workflows, custom filters | Zero-code agent integration |
| Boolean query builder | ✅ `build_bool_query()` | ❌ |
| Multi-seed recommendations | ✅ `recommend(pos, neg)` | ❌ |
| BibTeX / Markdown / JSON export | ✅ built-in | ❌ |
| Works in Cursor / Windsurf out of the box | ❌ | ✅ |

### vs. no skill (native agent)

| Feature | Native agent | This skill |
|---|---|---|
| Knows S2 API base URL & auth header | Maybe | ✅ |
| Boolean query construction helper | ❌ | ✅ `build_bool_query()` |
| Single-script execution pattern (no ping-pong) | ❌ | ✅ enforced |
| Automatic rate limiting + exponential backoff | ❌ | ✅ |
| BibTeX / Markdown / JSON export | ❌ | ✅ |
| Deduplication helper | ❌ | ✅ `deduplicate()` |
| Phase-based workflow (Plan → Execute → Summarize → Loop) | ❌ | ✅ |

## Prerequisites

- `python3`
- `pip install requests`
- (Optional) A [Semantic Scholar API Key](https://www.semanticscholar.org/product/api#api-key) for higher rate limits:

  ```bash
  export S2_API_KEY=xxxxxxxxxxxxxxxx
  ```

Works unauthenticated with strict rate limits.

## Skill Installation

### Claude Code

```bash
# Global install (all projects)
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.claude/skills/semanticscholar-skill

# Project-level
git clone https://github.com/Agents365-ai/semanticscholar-skill.git .claude/skills/semanticscholar-skill
```

### Codex

```bash
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.codex/skills/semanticscholar-skill
```

### OpenClaw

```bash
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.openclaw/skills/semanticscholar-skill

# Project-level
git clone https://github.com/Agents365-ai/semanticscholar-skill.git skills/semanticscholar-skill
```

### ClawHub

```bash
clawhub install semanticscholar-skill
```

### SkillsMP

```bash
skills install semanticscholar-skill
```

### Installation paths summary

| Platform | Global path | Project path |
|----------|-------------|--------------|
| Claude Code | `~/.claude/skills/semanticscholar-skill/` | `.claude/skills/semanticscholar-skill/` |
| Codex | `~/.codex/skills/semanticscholar-skill/` | N/A |
| OpenClaw | `~/.openclaw/skills/semanticscholar-skill/` | `skills/semanticscholar-skill/` |
| ClawHub | via `clawhub install` | — |
| SkillsMP | via `skills install` | — |

## Usage

Just describe what you want:

```
> Use semanticscholar-skill to search for recent mixture-of-experts papers at NeurIPS since 2023

> Find papers citing "Attention Is All You Need" and rank by citation count

> Use build_bool_query to find "stem-like T cells" in IBD, excluding mesenchymal

> Recommend papers similar to DOI:10.1038/nature14539 and ARXIV:2010.11929, excluding NLP

> Export the top 10 results to BibTeX
```

The skill picks the right search function, applies filters, and runs everything in a single Python script with automatic rate limiting.

## Helper Module

`s2.py` — single-file module imported via `from s2 import *`:

| Category | Functions |
|---|---|
| Paper search | `search_relevance`, `search_bulk`, `search_snippets`, `match_title`, `get_paper`, `get_citations`, `get_references`, `find_similar`, `recommend`, `batch_papers` |
| Author search | `search_authors`, `get_author`, `get_author_papers`, `get_paper_authors`, `batch_authors` |
| Query building | `build_bool_query(phrases, required, excluded, or_terms)` |
| Output | `format_table`, `format_details`, `format_results`, `format_authors`, `export_bibtex`, `export_markdown`, `export_json`, `deduplicate` |

Full API reference in `SKILL.md`.

## Files

- `SKILL.md` — **the only required file**. Loaded by all platforms as the skill instructions.
- `s2.py` — Python helper module (rate limiting, retries, query building, output formatting)
- `agents/openai.yaml` — Codex UI metadata for skill discovery and prompt chips
- `README.md` — this file (English, displayed on GitHub homepage)
- `README_CN.md` — Chinese documentation

## Known Limitations

- **Semantic Scholar free tier is rate-limited** — unauthenticated is ~1 req/sec shared across all users; API key raises your personal ceiling but is not unlimited
- **Bulk search never returns TLDR** — fall back to `abstract` field
- **Not every paper has an abstract** — some only expose title + TLDR
- **Author disambiguation** — common names collide; inspect affiliations before calling `get_author_papers`
- **Fields with large payloads** — avoid `citations` / `references` in `get_paper` fields for highly-cited papers; use `get_citations` (paginated) instead

## License

MIT

## Support

If this skill helps you, consider supporting the author:

<table>
  <tr>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/wechat-pay.png" width="180" alt="WeChat Pay">
      <br>
      <b>WeChat Pay</b>
    </td>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/alipay.png" width="180" alt="Alipay">
      <br>
      <b>Alipay</b>
    </td>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/buymeacoffee.png" width="180" alt="Buy Me a Coffee">
      <br>
      <b>Buy Me a Coffee</b>
    </td>
  </tr>
</table>

## Author

**Agents365-ai**

- Bilibili: https://space.bilibili.com/441831884
- GitHub: https://github.com/Agents365-ai


path: README_CN.md
content: # semanticscholar-skill — 命令行下的 Semantic Scholar 📚

[English](README.md) | [Semantic Scholar API 文档](https://api.semanticscholar.org/api-docs/) | [申请 API Key](https://www.semanticscholar.org/product/api#api-key)

## 功能特性

- **搜索** Semantic Scholar 语料库 —— 支持关键词、布尔表达式、全文片段、标题、作者
- **查论文** —— 支持 DOI / arXiv / PMID / PMCID / CorpusId / MAG / ACL / SHA / URL 等任意 ID
- **引用遍历** —— 前向 / 后向引用,支持分页
- **相似论文推荐** —— 单种子 `find_similar`,或多种子正负样本 `recommend`
- **作者检索** —— 按姓名找研究者,列出发表论文、拉取 h-index / 单位
- **批量查询** —— 一次最多 500 篇论文 / 1000 位作者
- **过滤** —— 年份、日期范围、venue、学科、最小引用数、论文类型、开放获取
- **导出** —— BibTeX / Markdown / JSON
- **内置限流与重试** —— 请求间隔 1.1s,429/504 指数退避
- **单脚本执行模式** —— 避免顺序 API ping-pong,所有调用合并到一个 Python 脚本
- 当用户提到论文、引用、学术搜索、文献发现时自动触发

## 多平台支持

适配所有支持 [Agent Skills](https://agentskills.io) 格式的 AI 编程助手:

| 平台 | 状态 | 说明 |
|------|------|------|
| **Claude Code** | ✅ 完全支持 | 原生 SKILL.md 格式 |
| **Codex** | ✅ 完全支持 | 原生 SKILL.md + `agents/openai.yaml` 界面元数据 |
| **Hermes** | ✅ 完全支持 | `metadata.hermes` tags / category / related_skills |
| **OpenClaw** | ✅ 完全支持 | `metadata.openclaw` 命名空间 |
| **ClawHub** | ✅ 已发布 | `clawhub install semanticscholar-skill` |
| **SkillsMP** | ✅ 已收录 | GitHub topics 已配置 |

## 为什么选择 Semantic Scholar?

Semantic Scholar **不是又一个预印本服务器** —— 它是一个覆盖包括 arXiv 和 bioRxiv 在内整个学术语料库的**学术图谱 + 语义索引**。问题从来不是"S2 *还是* arXiv",而是"S2 *叠加在* arXiv *之上*"。

| 维度 | arXiv | bioRxiv | **Semantic Scholar** |
|---|---|---|---|
| 定位 | 预印本仓库(物理 / CS / 数学 / 统计) | 预印本仓库(生物 / 医学) | 覆盖 2 亿+ 论文的聚合学术图谱 |
| 语料范围 | 仅 arXiv(~250 万篇) | 仅 bioRxiv/medRxiv(~35 万篇) | arXiv + bioRxiv + PubMed + 会议论文 + 期刊 + 更多 |
| 搜索方式 | 关键词 / 分类号 | 关键词 / 学科 | 关键词 + **语义相关度排序** + 布尔 + 全文片段 |
| 引用图谱 | ❌ 无 | ❌ 无 | ✅ 前向 / 后向引用、influential citations |
| "相似论文" | ❌ | ❌ | ✅ `find_similar` + 多种子 `recommend` |
| 作者消歧 | 部分(无统一 ID) | 部分 | ✅ 统一 `authorId`、单位、h-index |
| venue / 年份 / 引用数过滤 | 有限 | 有限 | ✅ 丰富过滤:venue、学科、最小引用数、OA、论文类型 |
| TLDR 摘要 | ❌ | ❌ | ✅ 大多数论文有 `tldr` 字段 |
| BibTeX 导出 | 需去 arxiv.org | 需去 bioRxiv | ✅ API `citationStyles` 字段 |

**实操原则:**
- 已知具体 arXiv / bioRxiv 论文 ID → 直接访问源站
- 想**发现**相关 / 有影响力 / 被引 / 相似的论文 → 用 Semantic Scholar
- 想在**一次排序结果里同时看到预印本和已发表文献** → 用 Semantic Scholar
- 做文献综述、相关工作小节、引用分析 → 用 Semantic Scholar

这个技能不是取代 arXiv 或 bioRxiv,而是位于它们之上,补齐它们缺失的图谱 / 排序 / 推荐层。

## 对比

### vs. `asta-skill`(基于 MCP 的姊妹技能)

| 能力 | `semanticscholar-skill` | `asta-skill` |
|---|---|---|
| 传输方式 | Python + 直连 REST(`s2.py`) | MCP(streamable HTTP) |
| 运行依赖 | Python + `S2_API_KEY` | Host 支持 MCP |
| 最佳场景 | 脚本化批处理、复杂过滤 | 零代码 agent 集成 |
| 布尔查询构造器 | ✅ `build_bool_query()` | ❌ |
| 多种子推荐 | ✅ `recommend(pos, neg)` | ❌ |
| BibTeX / Markdown / JSON 导出 | ✅ 内置 | ❌ |
| Cursor / Windsurf 开箱即用 | ❌ | ✅ |

### vs. 原生 Agent(无技能)

| 能力 | 原生 Agent | 本技能 |
|---|---|---|
| 知道 S2 API 基础 URL 与认证头 | 或许 | ✅ |
| 布尔查询构造器 | ❌ | ✅ `build_bool_query()` |
| 单脚本执行模式(无 ping-pong) | ❌ | ✅ 强制 |
| 自动限流与指数退避 | ❌ | ✅ |
| BibTeX / Markdown / JSON 导出 | ❌ | ✅ |
| 去重辅助 | ❌ | ✅ `deduplicate()` |
| 阶段式工作流(Plan → Execute → Summarize → Loop) | ❌ | ✅ |

## 前置条件

- `python3`
- `pip install requests`
- (可选)[Semantic Scholar API Key](https://www.semanticscholar.org/product/api#api-key),用于提高速率上限:

  ```bash
  export S2_API_KEY=xxxxxxxxxxxxxxxx
  ```

未认证也可使用,但速率限制很严格。

## 技能安装

### Claude Code

```bash
# 全局安装(所有项目可用)
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.claude/skills/semanticscholar-skill

# 项目级
git clone https://github.com/Agents365-ai/semanticscholar-skill.git .claude/skills/semanticscholar-skill
```

### Codex

```bash
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.codex/skills/semanticscholar-skill
```

### OpenClaw

```bash
git clone https://github.com/Agents365-ai/semanticscholar-skill.git ~/.openclaw/skills/semanticscholar-skill

# 项目级
git clone https://github.com/Agents365-ai/semanticscholar-skill.git skills/semanticscholar-skill
```

### ClawHub

```bash
clawhub install semanticscholar-skill
```

### SkillsMP

```bash
skills install semanticscholar-skill
```

### 安装路径汇总

| 平台 | 全局路径 | 项目路径 |
|------|---------|---------|
| Claude Code | `~/.claude/skills/semanticscholar-skill/` | `.claude/skills/semanticscholar-skill/` |
| Codex | `~/.codex/skills/semanticscholar-skill/` | 暂无 |
| OpenClaw | `~/.openclaw/skills/semanticscholar-skill/` | `skills/semanticscholar-skill/` |
| ClawHub | 通过 `clawhub install` | — |
| SkillsMP | 通过 `skills install` | — |

## 使用方式

直接用自然语言描述需求:

```
> 用 semanticscholar-skill 搜索 2023 年以来 NeurIPS 的 mixture-of-experts 论文

> 找引用 "Attention Is All You Need" 的论文,按引用数排序

> 用 build_bool_query 查 IBD 中的 "stem-like T cells",排除 mesenchymal

> 推荐与 DOI:10.1038/nature14539 和 ARXIV:2010.11929 相似的论文,排除 NLP

> 将前 10 条结果导出为 BibTeX
```

技能会自动选对搜索函数、配置过滤器,并把全部调用放进一个 Python 脚本运行,限流由 `s2.py` 处理。

## 辅助模块

`s2.py` —— 单文件模块,通过 `from s2 import *` 导入:

| 类别 | 函数 |
|---|---|
| 论文搜索 | `search_relevance`, `search_bulk`, `search_snippets`, `match_title`, `get_paper`, `get_citations`, `get_references`, `find_similar`, `recommend`, `batch_papers` |
| 作者搜索 | `search_authors`, `get_author`, `get_author_papers`, `get_paper_authors`, `batch_authors` |
| 查询构造 | `build_bool_query(phrases, required, excluded, or_terms)` |
| 输出 | `format_table`, `format_details`, `format_results`, `format_authors`, `export_bibtex`, `export_markdown`, `export_json`, `deduplicate` |

完整 API 参考见 `SKILL.md`。

## 文件说明

- `SKILL.md` —— **唯一必需文件**。所有平台都以此作为技能指令加载
- `s2.py` —— Python 辅助模块(限流、重试、查询构造、格式化输出)
- `agents/openai.yaml` —— Codex 界面元数据
- `README.md` —— 英文文档(GitHub 主页显示)
- `README_CN.md` —— 本文件(中文文档)

## 已知限制

- **Semantic Scholar 免费层速率受限** —— 未认证约 1 req/sec,全体用户共享;API key 能提高个人上限但不是无限
- **bulk search 不返回 TLDR** —— 改用 `abstract` 字段
- **并非所有论文都有摘要** —— 有些只暴露标题 + TLDR
- **作者消歧** —— 常见姓名会重名,调用 `get_author_papers` 前先核对单位
- **大字段要小心** —— `get_paper` 的 `fields` 不要加 `citations` / `references`(高被引论文会炸上下文),改用分页的 `get_citations`

## License

MIT

## 支持

如果这个技能对你有帮助,欢迎打赏支持作者:

<table>
  <tr>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/wechat-pay.png" width="180" alt="微信支付">
      <br>
      <b>微信支付</b>
    </td>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/alipay.png" width="180" alt="支付宝">
      <br>
      <b>支付宝</b>
    </td>
    <td align="center">
      <img src="https://raw.githubusercontent.com/Agents365-ai/images_payment/main/qrcode/buymeacoffee.png" width="180" alt="Buy Me a Coffee">
      <br>
      <b>Buy Me a Coffee</b>
    </td>
  </tr>
</table>

## 作者

**Agents365-ai**

- Bilibili: https://space.bilibili.com/441831884
- GitHub: https://github.com/Agents365-ai


path: agents/openai.yaml
content: interface:
  display_name: "Semantic Scholar"
  short_description: "Search academic papers, traverse citations, find authors, and get recommendations via the Semantic Scholar API with built-in rate limiting and retries"
  brand_color: "#1857B6"

policy:
  allow_implicit_invocation: true

capabilities:
  - Keyword-based broad relevance search (search_relevance)
  - Boolean precise search with phrases, required, excluded, OR terms (search_bulk + build_bool_query)
  - Full-text snippet/passage search (search_snippets)
  - Single-paper lookup by DOI, arXiv, PMID, PMCID, CorpusId, MAG, ACL, SHA, or URL
  - Forward and backward citation traversal (get_citations, get_references)
  - Single-seed and multi-seed paper recommendations (find_similar, recommend)
  - Author search, profile lookup, and publication listing
  - Batch paper and author lookups (batch_papers, batch_authors)
  - Filter by year, publication date, venue, fields of study, min citations, pub types, open access
  - Export results to BibTeX, Markdown, or JSON
  - Automatic rate limiting (1.1s gap) and exponential backoff retries on 429/504
  - Single-script execution pattern that avoids sequential API call ping-pong

prerequisites:
  - python3
  - pip package `requests`
  - Optional S2_API_KEY for higher rate limits (request at https://www.semanticscholar.org/product/api#api-key)


path: s2.py
content: import time, os, json, requests, sys

GRAPH = "https://api.semanticscholar.org/graph/v1"
RECS = "https://api.semanticscholar.org/recommendations/v1"
DATASETS = "https://api.semanticscholar.org/datasets/v1"
HEADERS = {"x-api-key": os.environ.get("S2_API_KEY", "")}

# Module-level rate coordination
_last_request_time = 0
_MIN_GAP = 1.1  # seconds between requests


def _request(method, url, params=None, json_data=None, max_retries=5):
    global _last_request_time
    # Enforce minimum gap
    elapsed = time.time() - _last_request_time
    if elapsed < _MIN_GAP:
        time.sleep(_MIN_GAP - elapsed)

    for attempt in range(max_retries + 1):
        _last_request_time = time.time()
        try:
            if method == "GET":
                r = requests.get(url, params=params, headers=HEADERS, timeout=30)
            else:
                r = requests.post(url, params=params, json=json_data, headers=HEADERS, timeout=30)
        except (requests.ConnectionError, requests.Timeout) as e:
            if attempt < max_retries:
                wait = min(2 ** (attempt + 1), 60)
                print(f"  [conn-error] {type(e).__name__}, retry {attempt+1}/{max_retries} in {wait}s", file=sys.stderr)
                time.sleep(wait)
                continue
            raise

        if r.status_code in (429, 504):
            if attempt < max_retries:
                wait = min(2 ** (attempt + 1), 60)
                print(f"  [rate-limit] {r.status_code}, retry {attempt+1}/{max_retries} in {wait}s", file=sys.stderr)
                time.sleep(wait)
                continue
            r.raise_for_status()
        elif r.status_code >= 400:
            r.raise_for_status()
        return r.json()


def s2_get(url, params=None):
    return _request("GET", url, params=params)


def s2_post(url, params=None, json_data=None):
    return _request("POST", url, params=params, json_data=json_data)


# --- Pagination ---

def paginate(url, params=None, max_results=1000):
    params = dict(params or {})
    params.setdefault("limit", 100)
    params["offset"] = 0
    results = []
    while len(results) < max_results:
        r = s2_get(url, params)
        results.extend(r.get("data", []))
        if "next" not in r or len(results) >= max_results:
            break
        params["offset"] = r["next"]
    return results[:max_results]


def paginate_bulk(url, params=None, max_results=10000):
    params = dict(params or {})
    token = None
    results = []
    while len(results) < max_results:
        if [REDACTED_SECRET]"token"] = token
        r = s2_get(url, params)
        results.extend(r.get("data", []))
        [REDACTED_SECRET]"token")
        if not token or len(results) >= max_results:
            break
    return results[:max_results]


# --- Batch ---

def batch_papers(ids, fields="title,year,citationCount"):
    return s2_post(f"{GRAPH}/paper/batch", params={"fields": fields}, json_data={"ids": ids[:500]})


def batch_authors(ids, fields="name,hIndex,paperCount"):
    return s2_post(f"{GRAPH}/author/batch", params={"fields": fields}, json_data={"ids": ids[:1000]})


# --- High-level search functions ---

_DEFAULT_FIELDS = "title,year,citationCount,authors,venue,externalIds,tldr"
_BULK_FIELDS = "title,year,citationCount,authors,venue,externalIds"  # bulk search doesn't support tldr

def _add_filters(params, year=None, venue=None, fields_of_study=None,
                 min_citations=None, pub_types=None, open_access=False,
                 publication_date=None):
    if year: params["year"] = year
    if publication_date: params["publicationDateOrYear"] = publication_date
    if venue: params["venue"] = venue
    if fields_of_study: params["fieldsOfStudy"] = fields_of_study
    if min_citations: params["minCitationCount"] = str(min_citations)
    if pub_types: params["publicationTypes"] = pub_types
    if open_access: params["openAccessPdf"] = ""
    return params


def search_relevance(query, fields=_DEFAULT_FIELDS, max_results=20, **filters):
    params = _add_filters({"query": query, "fields": fields, "limit": min(max_results, 100)}, **filters)
    if max_results <= 100:
        r = s2_get(f"{GRAPH}/paper/search", params)
        return r.get("data", [])[:max_results]
    return paginate(f"{GRAPH}/paper/search", params, max_results)


def search_bulk(query, fields=_BULK_FIELDS, max_results=100, sort="citationCount:desc", **filters):
    params = _add_filters({"query": query, "fields": fields, "sort": sort}, **filters)
    return paginate_bulk(f"{GRAPH}/paper/search/bulk", params, max_results)


def search_snippets(query, fields="snippet.text,snippet.snippetKind,snippet.section",
                    max_results=10, **filters):
    params = _add_filters({"query": query, "fields": fields, "limit": min(max_results, 100)}, **filters)
    return s2_get(f"{GRAPH}/snippet/search", params).get("data", [])[:max_results]


def get_paper(paper_id, fields=_DEFAULT_FIELDS + ",abstract,references,openAccessPdf"):
    return s2_get(f"{GRAPH}/paper/{paper_id}", {"fields": fields})


def get_citations(paper_id, fields="title,year,citationCount,authors,venue", max_results=100):
    return paginate(f"{GRAPH}/paper/{paper_id}/citations",
                    {"fields": fields, "limit": min(max_results, 1000)}, max_results)


def get_references(paper_id, fields="title,year,citationCount,authors,venue", max_results=100):
    return paginate(f"{GRAPH}/paper/{paper_id}/references",
                    {"fields": fields, "limit": min(max_results, 1000)}, max_results)


def find_similar(paper_id, fields="title,year,citationCount,authors,venue", limit=10, pool="recent"):
    return s2_get(f"{RECS}/papers/forpaper/{paper_id}",
                  {"fields": fields, "limit": limit, "from": pool}).get("recommendedPapers", [])


def recommend(positive_ids, negative_ids=None, fields="title,year,citationCount,authors,venue", limit=10):
    body = {"positivePaperIds": positive_ids}
    if negative_ids:
        body["negativePaperIds"] = negative_ids
    return s2_post(f"{RECS}/papers/", params={"fields": fields, "limit": limit},
                   json_data=body).get("recommendedPapers", [])


# --- Author functions ---

_DEFAULT_AUTHOR_FIELDS = "name,affiliations,paperCount,citationCount,hIndex"


def search_authors(query, fields=_DEFAULT_AUTHOR_FIELDS, max_results=20):
    params = {"query": query, "fields": fields, "limit": min(max_results, 1000)}
    if max_results <= 1000:
        r = s2_get(f"{GRAPH}/author/search", params)
        return r.get("data", [])[:max_results]
    return paginate(f"{GRAPH}/author/search", params, max_results)


def get_author(author_id, fields=_DEFAULT_AUTHOR_FIELDS):
    return s2_get(f"{GRAPH}/author/{author_id}", {"fields": fields})


def get_author_papers(author_id, fields=_DEFAULT_FIELDS, max_results=100):
    return paginate(f"{GRAPH}/author/{author_id}/papers",
                    {"fields": fields, "limit": min(max_results, 1000)}, max_results)


def get_paper_authors(paper_id, fields=_DEFAULT_AUTHOR_FIELDS, max_results=100):
    return paginate(f"{GRAPH}/paper/{paper_id}/authors",
                    {"fields": fields, "limit": min(max_results, 1000)}, max_results)


def match_title(title, fields=_DEFAULT_FIELDS):
    return s2_get(f"{GRAPH}/paper/search/match", {"query": title, "fields": fields})


# --- Utilities ---

def deduplicate(papers):
    seen = set()
    out = []
    for p in papers:
        pid = p.get("paperId")
        if pid and pid not in seen:
            seen.add(pid)
            out.append(p)
    return out


def build_bool_query(phrases=None, required=None, excluded=None, or_terms=None):
    parts = []
    for p in (phrases or []):
        parts.append(f'"{p}"')
    for r in (required or []):
        parts.append(f"+{r}")
    for e in (excluded or []):
        parts.append(f"-{e}")
    if or_terms:
        parts.append("(" + " | ".join(or_terms) + ")")
    return " ".join(parts)


# --- Output formatting ---

def _doi(paper):
    ext = paper.get("externalIds") or {}
    return ext.get("DOI", "")


def _first_author(paper):
    authors = paper.get("authors") or []
    if not authors:
        return ""
    name = authors[0].get("name", "")
    return f"{name} et al." if len(authors) > 1 else name


def format_table(papers, max_rows=30):
    rows = ["| # | Title | Year | Cites | First Author | Venue |",
            "|---|-------|------|-------|-------------|-------|"]
    for i, p in enumerate(papers[:max_rows], 1):
        t = (p.get("title") or "")[:80]
        y = p.get("year") or ""
        c = p.get("citationCount") or 0
        a = _first_author(p)[:25]
        v = (p.get("venue") or "")[:30]
        rows.append(f"| {i} | {t} | {y} | {c} | {a} | {v} |")
    return "\n".join(rows)


def format_details(papers, max_papers=10):
    lines = []
    for i, p in enumerate(papers[:max_papers], 1):
        title = p.get("title") or "Untitled"
        year = p.get("year") or "?"
        cites = p.get("citationCount") or 0
        doi = _doi(p)
        authors = ", ".join(a.get("name", "") for a in (p.get("authors") or [])[:5])
        if len(p.get("authors") or []) > 5:
            authors += " et al."
        tldr = (p.get("tldr") or {}).get("text", "")
        abstract = (p.get("abstract") or "")[:300]
        summary = tldr or (abstract + "..." if len(p.get("abstract") or "") > 300 else abstract)

        lines.append(f"### {i}. {title} ({year})")
        lines.append(f"**Authors:** {authors}")
        lines.append(f"**Citations:** {cites} | **DOI:** {doi}" if doi else f"**Citations:** {cites}")
        if summary:
            lines.append(f"**Summary:** {summary}")
        lines.append("")
    return "\n".join(lines)


def format_results(papers, query_desc=""):
    n = len(papers)
    header = f"## Search Results: {query_desc}\n\n**{n} papers found.**\n" if query_desc else f"**{n} papers found.**\n"
    table = format_table(papers)
    details = format_details(papers[:10])
    return f"{header}\n{table}\n\n---\n\n{details}"


def format_authors(authors, max_rows=20):
    rows = ["| # | Name | Affiliations | Papers | Citations | h-index |",
            "|---|------|-------------|--------|-----------|---------|"]
    for i, a in enumerate(authors[:max_rows], 1):
        name = a.get("name", "")
        affs = ", ".join(a.get("affiliations") or [])[:40]
        pc = a.get("paperCount") or 0
        cc = a.get("citationCount") or 0
        h = a.get("hIndex") or 0
        rows.append(f"| {i} | {name} | {affs} | {pc} | {cc} | {h} |")
    return "\n".join(rows)


def export_bibtex(papers):
    entries = []
    for p in papers:
        bib = (p.get("citationStyles") or {}).get("bibtex")
        if bib:
            entries.append(bib)
    return "\n\n".join(entries)


def export_markdown(papers, query_desc="", path="/tmp/s2_results.md"):
    content = format_results(papers, query_desc)
    with open(path, "w") as f:
        f.write(content)
    print(f"Saved {len(papers)} papers to {path}")
    return path


def export_json(papers, path="/tmp/s2_results.json"):
    with open(path, "w") as f:
        json.dump(papers, f, indent=2, ensure_ascii=False)
    print(f"Saved {len(papers)} papers to {path}")
    return path


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)