Claude Skill

semantic-scholar-deep

Deep research over the Semantic Scholar Graph API. Covers endpoints missing from allenai's lookup skill — paper references (backward citations), recommendations, batch paper lookup (up to 500 IDs), snippet search, and multi-hop citation graph traversal (BFS forward/backward). Use

LLM Mart · 0 points · 2 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download codealive-ai-ai-driven-development-skills_semantic-scholar-deep-68a302a.zip · 20 KB
Part of codealive-ai/ai-driven-development — 21 skills

Install

skills CLI npx skills add https://github.com/CodeAlive-AI/ai-driven-development/tree/main/skills/semantic-scholar-deep
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install codealive-ai-ai-driven-development@llmmart
Git git clone https://github.com/CodeAlive-AI/ai-driven-development.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole codealive-ai/ai-driven-development collection as a plugin from our marketplace. Git is the plain clone.

README

semantic-scholar-deep

Deep research over the Semantic Scholar Graph API. Covers the endpoints missing from the official allenai/asta-plugins semantic-scholar-lookup skill — backward references, recommendations, batch lookup (up to 500 IDs per call), snippet full-text search, and multi-hop citation-graph BFS.

Install

npx skills add CodeAlive-AI/ai-driven-development@semantic-scholar-deep -g -y

Works anonymously out of the box — most endpoints (paper lookup, references, recommendations, batch) respond without an API key. The keyword /paper/search endpoint is heavily rate-limited on the anonymous tier, so for large sweeps set SEMANTIC_SCHOLAR_API_KEY (request one at https://www.semanticscholar.org/product/api#api-key).

Prerequisites

This skill stands alone for scripts-only usage (invoke the Python CLIs directly). For the full deep-research pipeline (orchestrated by the bundled subagent, see below), you also need:

Tool Why How
Exa MCP Neural paper discovery across arXiv / OpenReview / PubMed / bioRxiv — the S2 search endpoint is too rate-limited to be the primary discovery path claude mcp add --transport http exa "https://mcp.exa.ai/mcp?tools=web_search_advanced_exa"
allenai semantic-scholar-lookup skill First-party asta papers CLI for fast metadata + forward citations (complements our backward-references / recommendations coverage) npx skills add "allenai/asta-plugins@Semantic Scholar Lookup" -g -y
Python 3.8+ Scripts are stdlib-only, no pip install Usually already present

Without the above, the bundled subagent falls back to what it can reach (S2-only), but Exa dramatically improves discovery quality and freshness.

Quick start

Inline usage (one specific endpoint):

> Get the references that DOI:10.18653/v1/N18-3011 cites
> Recommend 20 papers related to this paperId
> Batch-resolve these 30 arXiv IDs: 2404.18496 2502.02757 ...
> Build a citation graph of depth 2 around paperId X

Delegated usage (multi-step research via the bundled subagent):

> Find recent papers on LLM code review
> Do a literature review on retrieval-augmented generation since 2024
> Novelty check: is my idea <X> already published?

What it ships

Python CLIs (scripts/)

  • ss_client.py — stdlib-only S2 client with exponential backoff on HTTP 429/5xx. Subcommands: search, paper, citations, references, recommendations, batch, author-search, author, author-papers, snippets.
  • citation_graph.py — BFS traversal around a seed paper. Options: --direction forward|backward|both, --depth N, --max-nodes N. Outputs JSON with nodes + edges, designed for summarization rather than in-context dumping.

References (references/)

  • endpoints.md — complete field reference, query-parameter list, and ID-format matrix for every endpoint.
  • workflows.md — 5 ready-made deep-research patterns: literature review from a topic, citation graph around a seed, novelty check, author trajectory, evidence for a claim.

Bundled subagent (agents/deep-paper-researcher.md, optional)

A paired subagent definition for token-isolated research. Manually install once:

cp ~/.agents/skills/semantic-scholar-deep/agents/deep-paper-researcher.md ~/.claude/agents/

Then restart the session. Features:

  • Input validation — today's date anchoring + caller-paraphrased-window detection (catches cases where the calling agent translates "recent" into an invented "2024-2026" window)
  • Freshness Mode classifier — RECENT (default last 6 months, sort by publication date), FOUNDATIONAL (sort by citations, no date floor), MIXED (two slices side-by-side)
  • Sort-then-tiebreak ranking — never multiplies citations × recency into a single score, so fresh papers with zero citations aren't buried under older high-citation ones
  • Compact report format with explicit Anchor date / Mode / Window header — readers can see and redirect the choice

Key design decisions

  • Stdlib only — no requests dependency. Works on any Python 3.8+ install.
  • Backoff over hard-failure — HTTP 429 / 5xx get exponential retry up to 30s, honoring Retry-After. Anonymous tier is usable for small graphs.
  • Progressive disclosure — SKILL.md stays under 150 lines; deep endpoint-by-endpoint docs and workflow templates live in references/.
  • Token hygiene — scripts emit raw JSON to stdout by design. For graphs >50 nodes, use --output to write to disk and summarize via jq/python3 rather than piping full payloads into the agent's context.
  • Not a discovery engine — keyword search works but is the weakest endpoint on the anonymous tier; the bundled subagent delegates discovery to Exa MCP and uses S2 for structured expansion (references, recommendations, batch).

Rate limits

Endpoint Anonymous tier With API key
paper by ID ~1 RPS, reliable Much higher
references / recommendations ~1 RPS, reliable Much higher
batch (up to 500 IDs) ~1 RPS Much higher
search (keyword) Frequently 429 Reliable

The client respects Retry-After and does up to 5 retries with exponential backoff (1→30s).

File structure

semantic-scholar-deep/
├── SKILL.md                       # Agent-facing instructions (dispatch rule, when to use)
├── README.md                      # This file
├── scripts/
│   ├── ss_client.py              # Full S2 API client (stdlib only)
│   └── citation_graph.py         # BFS traversal
├── references/
│   ├── endpoints.md              # Per-endpoint field and parameter reference
│   └── workflows.md              # 5 deep-research patterns
└── agents/
    └── deep-paper-researcher.md  # Optional paired subagent definition

License

MIT

Skill manifest

Semantic Scholar — Deep Research

Purpose: fill the gaps that semantic-scholar-lookup (allenai) leaves — references, recommendations, batch, and multi-hop citation-graph traversal.

Contents

Dispatch Rule (read first)

Two execution modes:

Inline (run the Bash scripts yourself)

Use when the user asks for one specific endpoint:

  • "get references of paper X" → ss_client.py references <id>
  • "recommendations for paper Y" → ss_client.py recommendations <id>
  • "batch-resolve these 30 DOIs" → ss_client.py batch ...
  • "find the snippet where X is said" → ss_client.py snippets "..."

Fast, cheap, no orchestration overhead.

Delegate to deep-paper-researcher subagent

Use when the task is multi-step or would otherwise flood the context:

  • Literature review on a topic
  • Citation graph / network analysis around a seed paper
  • Novelty check for an idea
  • State-of-the-art survey
  • Anything that requires merging Exa discovery + S2 graph + ranking

Mandatory prompt contents. The subagent runs in isolated context with no access to this conversation's system reminders. Include exactly these two things:

  1. Today's date — inline as Today is YYYY-MM-DD. Pull from the currentDate system-reminder field, or run date -I via Bash before delegating if it's missing. Never rely on training-data intuitions about the current year.
  2. User's request, verbatim — pass the user's original phrasing (topic + any freshness words like "современные / recent / классические / seminal" and any explicit dates like "since 2024"). Translate language if needed but do not paraphrase trigger words into date windows.

Do NOT do any of these:

  • Do NOT classify freshness yourself (RECENT/FOUNDATIONAL/MIXED). The subagent does that from the verbatim user request.
  • Do NOT invent a date window. If the user said "современные / recent / latest" without a year, the subagent defaults to last 6 months — don't preempt it with "2024-2026".
  • Do NOT drop the trigger words. The subagent relies on them to pick the right mode.

Call:

Agent(
  subagent_type="deep-paper-researcher",
  description="<3–5 word task>",
  prompt="Today is 2026-04-22.\n\nUser's request: найди современные 10 статей про AI Code Review на arXiv.\n\n<optional: output format hints, language preference>"
  # model: "opus"  ← add only when the user opts in (see below)
)

The subagent's Freshness Mode section handles classification; keep this layer thin.

Model selection (Sonnet default, Opus on demand)

The subagent's model frontmatter is sonnet — that's the default.

Override to Opus by passing model: "opus" to the Agent tool only if the user explicitly requests deeper reasoning. Triggers (any of):

  • English: "deep dive", "thorough", "rigorous", "use Opus", "high quality", "comprehensive", "exhaustive"
  • Russian: "глубокий/глубже", "тщательный/тщательно", "подробно", "в режиме Опус/Opus", "максимально качественно", "серьёзный ресерч"

Never auto-upgrade to Opus without a user signal — Sonnet handles the default literature-review workflow fine and costs less.

When to Use

Trigger this skill for:

  • Citation graph / network over a seed paper or topic
  • Backward references (what does this paper cite?) — not covered by allenai
  • Forward citations with pagination beyond 1000 results
  • Recommendations — related-paper discovery from a seed
  • Batch lookup — resolve 50-500 DOI/arXiv/CorpusId/S2 IDs in one call
  • Snippet search — find specific passages across the S2 corpus

Do NOT use for:

  • Simple "get paper by ID" or "who cited this" — use semantic-scholar-lookup (faster, no Python)
  • Broad topical discovery — use web_search_advanced_exa with category: "research paper" (Exa MCP)
  • Consumer-level literature questions — use the deep-paper-researcher subagent, which orchestrates all three tools

Scripts

Located under ${SKILL_DIR}/scripts/.

ss_client.py — raw API client

Subcommands (all output JSON on stdout):

Command Endpoint Notes
search <query> /graph/v1/paper/search --bulk switches to /search/bulk (up to 1000/page)
paper <id> /graph/v1/paper/{id} ID forms: raw, DOI:, ARXIV:, CorpusId:, PMID:, URL:
citations <id> /graph/v1/paper/{id}/citations paginated; up to 1000 per page
references <id> /graph/v1/paper/{id}/references paginated; up to 1000 per page
recommendations <id> /recommendations/v1/papers/forpaper/{id} --pool recent|all-cs
batch <id1> <id2> ... POST /graph/v1/paper/batch up to 500 IDs
author-search <query> /graph/v1/author/search
author <id> /graph/v1/author/{id}
author-papers <id> /graph/v1/author/{id}/papers
snippets <query> /graph/v1/snippet/search Full-text snippets

Common flags: --limit, --offset, --fields, --year, --fields-of-study, --venue, --min-citation-count.

citation_graph.py — BFS traversal

python3 ${SKILL_DIR}/scripts/citation_graph.py <paperId> \
    --direction both \
    --depth 2 \
    --max-nodes 200 \
    --per-hop-limit 50 \
    --output graph.json

Directions: forward (citations), backward (references), both. Output schema described in the script docstring — nodes: {paperId → metadata+depth}, edges: [{src, dst, direction}].

Authentication & Rate Limits

  • Without API key: ~1 RPS shared, 100 queries/5min bursts. Fine for small graphs.
  • With SEMANTIC_SCHOLAR_API_KEY env var: much higher limits.
  • Apply: https://www.semanticscholar.org/product/api#api-key
  • The client does exponential backoff (1→30s) on HTTP 429/5xx, respects Retry-After.

Progressive Disclosure

  • references/endpoints.md — complete field list per endpoint + query examples
  • references/workflows.md — lit-review, novelty-check, seed-expansion patterns

Output Hygiene

Scripts emit raw JSON — redirect to files for anything beyond ~20 results. For graphs >50 nodes always pass --output graph.json to avoid flooding the conversation context.

Integration

Typical pipeline inside the deep-paper-researcher subagent:

  1. Discovery — mcp__exa__web_search_advanced_exa (neural + multi-source)
  2. ID resolution — ss_client.py search / batch to get paperId from titles or DOIs
  3. Graph expansion — citation_graph.py with the top 3-5 seeds
  4. Synthesis — distill nodes/edges into a ranked report

Optional: Bundled Subagent

A paired subagent definition ships alongside the skill at agents/deep-paper-researcher.md. It orchestrates Exa MCP + allenai semantic-scholar-lookup + this skill's scripts into a token-isolated research agent with:

  • Mandatory input validation (today's date anchoring + caller-paraphrased-window detection)
  • Freshness Mode classifier (RECENT / FOUNDATIONAL / MIXED)
  • Sort-then-tiebreak ranking (never multiplies citations × recency into a single score)
  • Compact report format with explicit Anchor date / Mode / Window header

To install for Claude Code (manual, one-time):

cp ~/.agents/skills/semantic-scholar-deep/agents/deep-paper-researcher.md ~/.claude/agents/

(Path may differ on other agents — copy to the agent's subagents directory, then restart the session.)

Prerequisites for full pipeline: Exa MCP connected, allenai/asta-plugins@"Semantic Scholar Lookup" skill installed.

Files (ai-driven-development)
  • agents
    • deep-paper-researcher.md 10.4 KB
      ---
      name: deep-paper-researcher
      description: "Token-isolated deep research agent for academic papers. Orchestrates Exa MCP (neural multi-source discovery), allenai's semantic-scholar-lookup skill (fast metadata + forward citations via asta CLI), and the semantic-scholar-deep skill (references, recommendations, batch, citation-graph BFS). Use when the user asks for a literature review, a citation graph around a seed paper, novelty checks, state-of-the-art surveys, or any multi-step paper research that would otherwise flood the main context. Returns a compact ranked report, not raw API output. MANDATORY when delegating: (1) include `Today is YYYY-MM-DD.` inline; (2) include the user's original request verbatim (keep trigger words like 'современные / recent / latest / seminal'). DO NOT paraphrase freshness words into date windows ('2024-2026', 'last 12-18 months', etc.) — the subagent classifies and chooses the window itself based on the verbatim request."
      tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, mcp__exa__web_search_advanced_exa
      model: sonnet
      ---
      
      You are a deep research specialist for academic papers. Your job is to take an open-ended research question and return a focused, trustworthy, ranked report — while keeping the caller's context clean.
      
      ## Input Validation (mandatory first step)
      
      Before any search, validate the caller's prompt:
      
      1. **Today's date**: Find `Today is YYYY-MM-DD` in the caller's prompt. If absent, run `date -I` via Bash. Never guess from training data — AI/LLM fields move fast enough that a 12-month offset can make the report actively wrong.
      2. **Caller-paraphrased window detection**: If the caller's prompt mentions BOTH a RECENT trigger word ("recent / latest / modern / new / SOTA / current / свежий / последний / современный / актуальный / новые") AND an explicit date window wider than 6 months (e.g. "2024-2026", "last 12-18 months", "past 2 years") **and** the original user phrase quoted in the prompt does NOT literally contain that range — treat it as caller over-translation. Ignore the caller's window, apply the 6-month default, flag the override in the report.
      3. **Self-anchoring fallback**: If you had to run `date -I` yourself (step 1), say so in one line of the report so the caller knows the date wasn't passed in.
      
      ## Freshness Mode
      
      Pick exactly one mode from the caller's request before searching:
      
      - **RECENT** — caller's request contains "recent / latest / modern / new / state-of-the-art / SOTA / current" or Russian equivalents ("свежий / свежие / последний / последние / недавний / современный / актуальный / новые").
        - Default date window: **today minus 6 months**.
        - Primary sort: **publication date descending**. Citation count is a **tiebreaker only**, never the primary key.
        - Explicit-window override: honor it **only if** the user literally mentioned a specific range ("since 2024", "last year", "Q1 2026", "в 2025"). Do NOT honor ranges that look like paraphrases from the caller (e.g. caller wrote "Prefer papers from 2024-2026" or "last 12-18 months" when the user said only "современные" / "recent" — that's a paraphrase, not a user-specified range). **Heuristic: any window wider than 6 months present alongside RECENT triggers is caller over-translation unless the user literally named that range.** Revert to the 6-month default, note the override in the report ("Window adjusted from caller's paraphrase '<their range>' to last 6 months because user request was just '<trigger word>'").
      
      - **FOUNDATIONAL** — user said "seminal / foundational / classic / most-cited / highly-cited / canonical" or Russian ("классический / основополагающий / ключевой / самые цитируемые").
        - No date floor.
        - Primary sort: **citation count descending**.
      
      - **MIXED** — ambiguous request with no freshness or foundational signal.
        - Return two ranked slices: top 5 by recency (within last 12 months), top 5 by all-time citations.
        - Do not blend them into one list — the two halves answer different questions.
      
      **Never multiply citation count with recency into a single score.** Log-citations grow to hundreds while a recency bonus caps at ~2–3×, so the older paper always wins. Use sort-then-tiebreak per the mode above.
      
      **Always state the mode and date window in the report** (see Output Format). Transparency lets the caller redirect if you chose wrong.
      
      ## Tool Stack
      
      You have three complementary sources. Use them in this order unless the task obviously needs only one:
      
      1. **Exa MCP** — `mcp__exa__web_search_advanced_exa` with `category: "research paper"`.
         - Best for: initial discovery, recent/fresh work, multi-source (arXiv, OpenReview, PubMed, bioRxiv), semantic/neural matching when keywords are fuzzy.
         - Restrictions: `includeText` / `excludeText` accept **single-item arrays only**; put multiple terms in `query`.
         - Tune: `type: "deep"` for thorough, `"fast"` for ideation, `startPublishedDate: "YYYY-01-01"` for recency, `enableSummary: true` + `summaryQuery` for distilled abstracts.
      
      2. **semantic-scholar-lookup skill** (`asta papers` CLI, installed via `npx skills add allenai/asta-plugins@...`).
         - Best for: fast targeted metadata lookup, forward citations (who cited paper X), author search, venue/year filtering.
         - Invoke via `Bash`: `asta papers get <id>`, `asta papers search <q>`, `asta papers citations <id>`, `asta papers author-search <name>`.
         - Honors `ASTA_TOOL_KEY` env var for rate limits.
      
      3. **semantic-scholar-deep skill** (`~/.claude/skills/semantic-scholar-deep/scripts/`).
         - Best for: **references** (what a paper cites — NOT covered by allenai), **recommendations** (related papers), **batch** lookup of up to 500 IDs, **citation graph** BFS, **snippet** full-text search.
         - Invoke via `Bash`:
           ```
           python3 ~/.claude/skills/semantic-scholar-deep/scripts/ss_client.py <subcommand> ...
           python3 ~/.claude/skills/semantic-scholar-deep/scripts/citation_graph.py <seed> --depth 2 --max-nodes 150 --output /tmp/graph.json
           ```
         - Honors `SEMANTIC_SCHOLAR_API_KEY` env var for higher rate limits.
         - Reference docs: `~/.claude/skills/semantic-scholar-deep/references/endpoints.md` and `workflows.md`.
      
      ## Default Workflow
      
      Unless the user asks for something narrower:
      
      1. **Scope** — parse the request into (a) topic keywords, (b) constraints (year, venue, domain), (c) output shape (review? graph? novelty check?).
      2. **Discovery** — Exa query with `numResults: 15–25`, `type: "deep"`. Set `startPublishedDate` per Freshness Mode: RECENT → today-6mo (or explicit user window); FOUNDATIONAL → no floor (or a decade window); MIXED → run **two** queries, one fresh (last 12mo) and one all-time. Skim titles+summaries.
      3. **Shortlist** — pick 5–10 most promising candidates by title relevance; extract DOI/arXiv/CorpusId from Exa URLs.
      4. **Resolve** — `ss_client.py batch` on the extracted IDs to get `paperId`, `citationCount`, `year`, `tldr`, `venue`.
      5. **Expand** — for top 3 seeds by citation count, run one of:
         - `ss_client.py recommendations <id> --limit 20` for related-work discovery
         - `ss_client.py references <id> --limit 30` for backward grounding
         - `citation_graph.py <id> --depth 2 --max-nodes 100 --output /tmp/graph_<id>.json` for network view
      6. **De-duplicate + rank** — merge everything by `paperId`, then apply the mode-specific ordering:
         - RECENT: sort by `publicationDate` DESC; `citationCount` is tiebreaker only. Drop anything older than the window.
         - FOUNDATIONAL: sort by `citationCount` DESC; publication date is tiebreaker only.
         - MIXED: produce two separate slices (top 5 recent + top 5 foundational) — do not merge.
         Pick final top 10–15 total.
      7. **Synthesize** — produce the report (see Output Format).
      
      Deviate from this only when the user's ask is simpler (e.g. "just build the graph around DOI:X" — go directly to step 5).
      
      ## Token Hygiene (Critical)
      
      - **Never print raw JSON from API calls into your response.** Save to `/tmp/<name>.json` via `--output` or shell redirection, then `jq`/`python3` for extraction.
      - **Cap per-call result sizes.** `--limit 30` for citations/references in discovery; bump only when doing graph traversal.
      - **Strip fields aggressively.** Default to `paperId,title,year,authors,citationCount,venue,tldr` unless a field is needed. (Use bare `authors`, not `authors.name` — nested projection is rejected on /citations and /references.)
      - **Summarize graphs, never dump them.** For `citation_graph.py` output, read the file with `jq` and emit only: hubs (top in-degree), influencers (top citationCount), clusters by year.
      
      ## Citation Discipline
      
      Cite only `paperId`, DOI, arXiv ID, or URL that you actually retrieved from S2/Exa/tools in this session. Never invent identifiers.
      
      ## Output Format
      
      Respond with a compact markdown report, ≤600 words unless the user explicitly asked for something long:
      
      ```
      ## Research Report: <topic>
      
      **Anchor date:** <YYYY-MM-DD> · **Mode:** <RECENT | FOUNDATIONAL | MIXED> · **Window:** <YYYY-MM-DD> → <YYYY-MM-DD or "all-time">
      
      ### Top Papers (ranked)
      1. **<title>** — <authors, year>, <venue> — <citationCount> cites
         DOI: `10.xxxx` · S2: `<paperId>` · [TLDR] one-line summary
         Why it matters: <one sentence>
      
      2. ...
      
      ### Citation Landscape (if graph built)
      - Hubs: <paperId-short:title> (N incoming edges)
      - Influencers: <title> (M total cites)
      - Temporal cluster: dense in <year range>
      
      ### Gaps / Observations
      - <one sentence each — methodology splits, under-explored angles, conflicting findings>
      
      ### Sources inspected
      - Exa: <N results, date window>
      - S2 batch: <N IDs resolved>
      - Graph: <N nodes, M edges> (if built)
      ```
      
      Do not paste API responses. Do not paste code. Link to saved artifacts (`/tmp/graph.json`) only if the caller needs them.
      
      ## Failure Modes
      
      - **HTTP 429 from S2** — retry backoff is built-in; if it still fails after 5 tries, flag the limit and suggest setting `SEMANTIC_SCHOLAR_API_KEY`.
      - **Paper not in S2** — fall back to Exa-only for that entry; note it in the report.
      - **`asta` CLI not installed** — skip it, use `ss_client.py` directly.
      - **Exa rate limit / error** — note it; proceed with S2-only discovery via `ss_client.py search`.
      
      ## When Not to Use This Agent
      
      If the caller just needs a single paper lookup or one citation list, point them at the `semantic-scholar-lookup` skill directly — it's faster and doesn't need orchestration.
      
  • references
    • endpoints.md 4.7 KB
      # Semantic Scholar API — Endpoint Reference
      
      ## Contents
      
      - [Paper IDs accepted everywhere](#paper-ids-accepted-everywhere)
      - [Paper Fields](#paper-fields-subset-pass-comma-separated-via-fields)
      - [`/paper/search`](#endpoint-papersearch) — keyword search
      - [`/paper/search/bulk`](#endpoint-papersearchbulk) — large sweeps
      - [`/paper/{id}`](#endpoint-paperpaper_id) — single paper
      - [`/paper/{id}/citations`](#endpoint-paperpaper_idcitations) — forward
      - [`/paper/{id}/references`](#endpoint-paperpaper_idreferences) — backward
      - [`POST /paper/batch`](#endpoint-post-paperbatch) — up to 500 IDs
      - [`/recommendations/v1/papers/forpaper/{id}`](#endpoint-recommendationsv1papersforpaperpaper_id)
      - [`/author/*`](#endpoint-authorsearch-authorid-authoridpapers)
      - [`/snippet/search`](#endpoint-snippetsearch)
      - [Citation contexts / intents](#citation-contexts-and-intents)
      - [Rate Limits](#rate-limits)
      
      Base URLs:
      - Graph API: `https://api.semanticscholar.org/graph/v1`
      - Recommendations: `https://api.semanticscholar.org/recommendations/v1`
      
      ## Paper IDs accepted everywhere
      
      | Form | Example |
      |------|---------|
      | S2 paper ID | `204e3073870fae3d05bcbc2f6a8e263d9b72e776` |
      | CorpusId | `CorpusId:215416146` |
      | DOI | `DOI:10.18653/v1/N18-3011` |
      | arXiv | `ARXIV:2106.15928` |
      | MAG | `MAG:112218234` |
      | ACL | `ACL:W12-3903` |
      | PubMed | `PMID:19872477` |
      | PubMed Central | `PMCID:2323736` |
      | URL | `URL:https://arxiv.org/abs/2106.15928v1` |
      
      ## Paper Fields (subset; pass comma-separated via `fields=`)
      
      - `paperId`
      - `externalIds` — `{DOI, ArXiv, CorpusId, MAG, ACL, PubMed, DBLP}`
      - `url`, `openAccessPdf` — `{url, status}`
      - `title`, `abstract`, `tldr` — `{model, text}`
      - `venue`, `publicationVenue`, `publicationDate`, `year`
      - `publicationTypes` — `JournalArticle | Conference | Review | ...`
      - `journal` — `{name, volume, pages}`
      - `authors` — list of `{authorId, name, affiliations, hIndex}`
      - `citationCount`, `referenceCount`, `influentialCitationCount`
      - `citationStyles` — `{bibtex}`
      - `fieldsOfStudy`, `s2FieldsOfStudy`
      - `embedding` — SPECTER / SPECTER2 (`--fields embedding.specter_v2`)
      
      ## Endpoint: `/paper/search`
      
      Query a keyword. Relevance-ranked, up to 100 per page.
      
      Query params:
      - `query` (required)
      - `limit`, `offset` (max `limit+offset` = 1000)
      - `fields`
      - `year` — `2019`, `2016-2020`, `-2015`, `2016-`
      - `venue` — e.g. `Nature,Radiology`
      - `fieldsOfStudy` — e.g. `Computer Science,Medicine`
      - `publicationTypes` — e.g. `Review,JournalArticle`
      - `openAccessPdf` — presence flag
      - `minCitationCount` — int
      - `publicationDateOrYear` — `YYYY-MM-DD:YYYY-MM-DD`
      
      ## Endpoint: `/paper/search/bulk`
      
      Up to 1000 per page, continuation via `token`. Sorted by relevance/year — use for large sweeps.
      
      ## Endpoint: `/paper/{paper_id}`
      
      Get single paper. All fields available. Use `fields=references.title,references.paperId` to embed shallow lists.
      
      ## Endpoint: `/paper/{paper_id}/citations`
      
      Forward citations. Returns `{data: [{citingPaper, contexts, intents, isInfluential}], offset, next}`.
      - `limit` up to 1000
      - `fields` applied to the `citingPaper` sub-object
      
      ## Endpoint: `/paper/{paper_id}/references`
      
      Backward references. Same shape as citations but `citedPaper` instead of `citingPaper`.
      
      ## Endpoint: `POST /paper/batch`
      
      Body: `{"ids": ["<id>", ...]}` — up to 500 IDs per request.
      Query param: `fields` (comma-separated).
      Returns an array aligned with input order; entries can be `null` when unresolved.
      
      ## Endpoint: `/recommendations/v1/papers/forpaper/{paper_id}`
      
      Related papers.
      - `limit` up to 500
      - `from=recent` (default) or `from=all-cs`
      - `fields` applied to recommended papers
      
      POST variant: `/recommendations/v1/papers` with `{positivePaperIds, negativePaperIds}` — better signal for curated seeds.
      
      ## Endpoint: `/author/search`, `/author/{id}`, `/author/{id}/papers`
      
      Author fields: `authorId`, `name`, `affiliations`, `aliases`, `homepage`,
      `paperCount`, `citationCount`, `hIndex`, `papers.{...}`.
      
      ## Endpoint: `/snippet/search`
      
      Full-text snippet search across the S2 corpus.
      Params: `query`, `limit`. Returns snippets with `section`, `text`, `paperId`, `score`.
      
      ## Citation `contexts` and `intents`
      
      When listing citations/references include `fields=contexts,intents,isInfluential`:
      - `contexts` — array of text snippets where citation appears
      - `intents` — `background | method | result`
      - `isInfluential` — boolean from S2's influence model
      
      These are the payload for fine-grained citation analysis (who cites *for what purpose*).
      
      ## Rate Limits
      
      - Anonymous: ~1 RPS shared, 100 req / 5min bursts.
      - Authenticated (`x-api-key` header): ~1 RPS per key, higher sustained throughput.
      - On `429` the server sets `Retry-After`. The client honors it with exponential fallback.
      
    • workflows.md 4.1 KB
      # Deep-Research Workflows
      
      ## Contents
      
      - [Workflow 1: Literature Review from a Topic](#workflow-1-literature-review-from-a-topic)
      - [Workflow 2: Citation Graph around a Seed Paper](#workflow-2-citation-graph-around-a-seed-paper)
      - [Workflow 3: Novelty Check for an Idea](#workflow-3-novelty-check-for-an-idea)
      - [Workflow 4: Author Trajectory](#workflow-4-author-trajectory)
      - [Workflow 5: Evidence for a Claim](#workflow-5-evidence-for-a-claim)
      - [Token Hygiene](#token-hygiene)
      - [Failure Modes](#failure-modes)
      
      ## Workflow 1: Literature Review from a Topic
      
      1. **Discovery** via Exa MCP:
         ```
         web_search_advanced_exa(
           query="retrieval augmented generation 2024",
           category="research paper",
           startPublishedDate="2024-01-01",
           numResults=20,
           type="deep",
           enableSummary=true
         )
         ```
      2. Extract DOI/arXiv IDs from Exa results (domain URL parsing).
      3. **Batch-resolve** to S2 IDs:
         ```
         python3 ss_client.py batch DOI:10.1145/... ARXIV:2401.12345 ... \
             --fields paperId,title,year,citationCount,tldr
         ```
      4. Sort by `citationCount`, pick top 5 seeds.
      5. **Expand each seed** via recommendations:
         ```
         python3 ss_client.py recommendations <seedId> --limit 30
         ```
      6. Merge + de-dup by `paperId`.
      7. Produce report: ranked list with year, venue, citation count, one-line TLDR.
      
      ## Workflow 2: Citation Graph around a Seed Paper
      
      Given a single anchor paper (user provides DOI or title):
      
      1. Resolve to S2 ID:
         ```
         python3 ss_client.py paper DOI:10.xxxx --fields paperId,title,year,citationCount
         ```
      2. Build the graph:
         ```
         python3 citation_graph.py <paperId> \
             --direction both --depth 2 --max-nodes 150 --output graph.json
         ```
      3. Analyze `graph.json`:
         - **Hubs** — nodes with highest in-degree (most-referenced by others in the graph)
         - **Influencers** — highest `citationCount` within the graph
         - **Clusters** — group by year or venue
      4. Present top 10 papers per category + one-sentence rationale per pick.
      
      ## Workflow 3: Novelty Check for an Idea
      
      User describes an idea in ≤3 sentences; confirm prior art exists.
      
      1. Extract 3-5 keyword groups from the description.
      2. For each group, run:
         ```
         python3 ss_client.py search "<group>" --limit 10 --year 2020- \
             --min-citation-count 5
         ```
      3. Deduplicate across groups by `paperId`.
      4. For top 3 candidates by relevance+citations, pull references + citations:
         ```
         python3 ss_client.py references <id> --limit 20
         python3 ss_client.py citations <id> --limit 20
         ```
      5. Report: "Closest prior work is X (year, citations) — overlap with the idea is [method/domain/etc]. Gap to user's idea: [...]".
      
      ## Workflow 4: Author Trajectory
      
      1. `author-search` to resolve a name to `authorId`:
         ```
         python3 ss_client.py author-search "Geoffrey Hinton"
         ```
      2. `author-papers` with `--limit 200` to pull career history.
      3. Group by year; plot topical drift via `fieldsOfStudy`.
      
      ## Workflow 5: Evidence for a Claim
      
      1. `snippets` to find exact passages:
         ```
         python3 ss_client.py snippets "attention scales quadratically with sequence length"
         ```
      2. For each hit, fetch full paper metadata:
         ```
         python3 ss_client.py paper <paperId> --fields paperId,title,year,venue,tldr,authors
         ```
      3. Rank by venue quality + recency.
      
      ## Token Hygiene
      
      - Never return raw `graph.json` (>50 nodes) into the main conversation — summarize or extract the top-N.
      - Always pipe large outputs through `--output` or redirect to files, not stdout-into-context.
      - When feeding back to main context, keep only: `paperId` (for stable linking), title, year, citation count, one-line why.
      
      ## Failure Modes
      
      | Symptom | Cause | Fix |
      |---------|-------|-----|
      | `HTTP 404` on paper lookup | wrong ID prefix | try `DOI:`, `ARXIV:`, `CorpusId:`, or URL form |
      | `HTTP 429` persistently | rate limit or missing key | set `SEMANTIC_SCHOLAR_API_KEY` env var |
      | Empty `data` for citations | paper too new or not cited yet | confirm with `paper <id>` that `citationCount > 0` |
      | `references.data[i].citedPaper == null` | external paper not in S2 corpus | filter nulls before traversal |
      | Graph explodes past `max-nodes` | seed is a very popular paper | lower `--per-hop-limit` and `--depth` |
      
  • scripts
    • citation_graph.py 4.8 KB
      #!/usr/bin/env python3
      """BFS traversal of the Semantic Scholar citation graph.
      
      Example:
          python3 citation_graph.py <paperId> --depth 2 --direction both --max-nodes 200 \
              --output graph.json
      
      Output JSON schema:
          {
            "seed": "<paperId>",
            "direction": "forward|backward|both",
            "depth": 2,
            "nodes": {
              "<paperId>": {
                "paperId": "...",
                "title": "...",
                "year": 2022,
                "citationCount": 123,
                "authors": [{"name": "..."}],
                "venue": "...",
                "depth": 1
              }
            },
            "edges": [
              {"src": "<paperId>", "dst": "<paperId>", "direction": "citation|reference"}
            ]
          }
      
      Directions:
          forward   — follow *citations* (who cites this paper). Good for tracking impact.
          backward  — follow *references* (what this paper cites). Good for literature grounding.
          both      — union of both per hop.
      """
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      from collections import deque
      from pathlib import Path
      
      sys.path.insert(0, str(Path(__file__).parent))
      import ss_client  # type: ignore
      
      
      def _extract_neighbor(entry: dict, key: str) -> dict | None:
          node = entry.get(key)
          if not node or not isinstance(node, dict):
              return None
          if not node.get("paperId"):
              return None
          return node
      
      
      def traverse(
          seed: str,
          *,
          direction: str = "both",
          depth: int = 2,
          max_nodes: int = 200,
          per_hop_limit: int = 50,
      ) -> dict:
          assert direction in {"forward", "backward", "both"}
          seed_info = ss_client.paper(
              seed,
              fields="paperId,title,year,authors,citationCount,venue,externalIds",
          )
          seed_id = seed_info["paperId"]
      
          nodes: dict[str, dict] = {seed_id: {**seed_info, "depth": 0}}
          edges: list[dict] = []
          queue: deque[tuple[str, int]] = deque([(seed_id, 0)])
      
          while queue and len(nodes) < max_nodes:
              paper_id, cur_depth = queue.popleft()
              if cur_depth >= depth:
                  continue
      
              if direction in {"forward", "both"}:
                  resp = ss_client.citations(paper_id, limit=per_hop_limit)
                  for entry in resp.get("data", []):
                      nb = _extract_neighbor(entry, "citingPaper")
                      if not nb:
                          continue
                      edges.append({"src": nb["paperId"], "dst": paper_id, "direction": "citation"})
                      if nb["paperId"] not in nodes and len(nodes) < max_nodes:
                          nodes[nb["paperId"]] = {**nb, "depth": cur_depth + 1}
                          queue.append((nb["paperId"], cur_depth + 1))
      
              if direction in {"backward", "both"} and len(nodes) < max_nodes:
                  resp = ss_client.references(paper_id, limit=per_hop_limit)
                  for entry in resp.get("data", []):
                      nb = _extract_neighbor(entry, "citedPaper")
                      if not nb:
                          continue
                      edges.append({"src": paper_id, "dst": nb["paperId"], "direction": "reference"})
                      if nb["paperId"] not in nodes and len(nodes) < max_nodes:
                          nodes[nb["paperId"]] = {**nb, "depth": cur_depth + 1}
                          queue.append((nb["paperId"], cur_depth + 1))
      
          return {
              "seed": seed_id,
              "direction": direction,
              "depth": depth,
              "nodes": nodes,
              "edges": edges,
              "stats": {
                  "total_nodes": len(nodes),
                  "total_edges": len(edges),
                  "truncated": len(nodes) >= max_nodes,
              },
          }
      
      
      def _main(argv: list[str]) -> int:
          p = argparse.ArgumentParser(description="Citation graph BFS on Semantic Scholar")
          p.add_argument("seed", help="paperId (supports DOI:, ARXIV:, CorpusId: prefixes)")
          p.add_argument("--direction", choices=["forward", "backward", "both"], default="both")
          p.add_argument("--depth", type=int, default=2)
          p.add_argument("--max-nodes", type=int, default=200)
          p.add_argument("--per-hop-limit", type=int, default=50)
          p.add_argument("--output", help="write JSON here instead of stdout")
      
          args = p.parse_args(argv)
      
          try:
              graph = traverse(
                  args.seed,
                  direction=args.direction,
                  depth=args.depth,
                  max_nodes=args.max_nodes,
                  per_hop_limit=args.per_hop_limit,
              )
          except ss_client.SemanticScholarError as e:
              print(f"error: {e}", file=sys.stderr)
              return 1
      
          dump = json.dumps(graph, ensure_ascii=False, indent=2)
          if args.output:
              Path(args.output).write_text(dump, encoding="utf-8")
              print(
                  f"wrote {graph['stats']['total_nodes']} nodes, {graph['stats']['total_edges']} edges → {args.output}",
                  file=sys.stderr,
              )
          else:
              print(dump)
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(_main(sys.argv[1:]))
      
    • ss_client.py 10.2 KB
      #!/usr/bin/env python3
      """Semantic Scholar Graph + Recommendations API client (stdlib only).
      
      CLI:
          python3 ss_client.py search "attention is all you need" --limit 10
          python3 ss_client.py paper 204e3073870fae3d05bcbc2f6a8e263d9b72e776
          python3 ss_client.py citations <paperId> --limit 100
          python3 ss_client.py references <paperId> --limit 100
          python3 ss_client.py recommendations <paperId> --limit 20
          python3 ss_client.py batch <id1> <id2> ... --fields paperId,title,year,citationCount
          python3 ss_client.py author-search "Ashish Vaswani"
          python3 ss_client.py author <authorId>
          python3 ss_client.py snippets "retrieval augmented generation"
      
      Env:
          SEMANTIC_SCHOLAR_API_KEY — optional; higher rate limits when set.
      
      All commands print JSON to stdout. Non-zero exit on terminal errors.
      """
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import sys
      import time
      import urllib.error
      import urllib.parse
      import urllib.request
      from typing import Any
      
      GRAPH_BASE = "https://api.semanticscholar.org/graph/v1"
      RECS_BASE = "https://api.semanticscholar.org/recommendations/v1"
      
      DEFAULT_PAPER_FIELDS = (
          "paperId,title,abstract,year,venue,authors,citationCount,"
          "referenceCount,influentialCitationCount,externalIds,openAccessPdf,tldr"
      )
      # Use bare `authors` (not `authors.name`): nested projection is rejected on
      # /citations and /references with HTTP 400 "Unrecognized or unsupported fields".
      LIGHT_PAPER_FIELDS = "paperId,title,year,authors,citationCount,venue"
      
      
      class SemanticScholarError(RuntimeError):
          pass
      
      
      def _headers() -> dict[str, str]:
          h = {"User-Agent": "semantic-scholar-deep/1.0"}
          api_key = os.environ.get("SEMANTIC_SCHOLAR_API_KEY")
          if api_key:
              h["x-api-key"] = api_key
          return h
      
      
      def _request(
          method: str,
          url: str,
          *,
          params: dict[str, Any] | None = None,
          json_body: Any = None,
          timeout: int = 30,
          max_retries: int = 5,
      ) -> Any:
          if params:
              cleaned = {k: v for k, v in params.items() if v is not None}
              if cleaned:
                  url = f"{url}?{urllib.parse.urlencode(cleaned, doseq=True)}"
      
          data: bytes | None = None
          headers = _headers()
          if json_body is not None:
              data = json.dumps(json_body).encode("utf-8")
              headers["Content-Type"] = "application/json"
      
          req = urllib.request.Request(url, data=data, method=method, headers=headers)
      
          backoff = 1.0
          for attempt in range(max_retries + 1):
              try:
                  with urllib.request.urlopen(req, timeout=timeout) as resp:
                      body = resp.read()
                      return json.loads(body) if body else None
              except urllib.error.HTTPError as e:
                  if e.code == 429 or 500 <= e.code < 600:
                      if attempt == max_retries:
                          raise SemanticScholarError(
                              f"HTTP {e.code} after {max_retries} retries: {url}"
                          ) from e
                      retry_after = e.headers.get("Retry-After")
                      wait = float(retry_after) if retry_after else backoff
                      time.sleep(wait)
                      backoff = min(backoff * 2, 30)
                      continue
                  raise SemanticScholarError(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e
              except urllib.error.URLError as e:
                  if attempt == max_retries:
                      raise SemanticScholarError(f"Network error: {e.reason}") from e
                  time.sleep(backoff)
                  backoff = min(backoff * 2, 30)
      
      
      def search(
          query: str,
          *,
          limit: int = 20,
          offset: int = 0,
          fields: str = DEFAULT_PAPER_FIELDS,
          year: str | None = None,
          fields_of_study: str | None = None,
          venue: str | None = None,
          min_citation_count: int | None = None,
          open_access_pdf: bool = False,
          bulk: bool = False,
      ) -> dict:
          path = "/paper/search/bulk" if bulk else "/paper/search"
          params = {
              "query": query,
              "limit": min(limit, 100) if not bulk else min(limit, 1000),
              "offset": offset,
              "fields": fields,
              "year": year,
              "fieldsOfStudy": fields_of_study,
              "venue": venue,
              "minCitationCount": min_citation_count,
          }
          if open_access_pdf:
              params["openAccessPdf"] = ""
          return _request("GET", f"{GRAPH_BASE}{path}", params=params)
      
      
      def paper(paper_id: str, *, fields: str = DEFAULT_PAPER_FIELDS) -> dict:
          return _request("GET", f"{GRAPH_BASE}/paper/{paper_id}", params={"fields": fields})
      
      
      def citations(
          paper_id: str, *, limit: int = 100, offset: int = 0, fields: str = LIGHT_PAPER_FIELDS
      ) -> dict:
          return _request(
              "GET",
              f"{GRAPH_BASE}/paper/{paper_id}/citations",
              params={"limit": min(limit, 1000), "offset": offset, "fields": fields},
          )
      
      
      def references(
          paper_id: str, *, limit: int = 100, offset: int = 0, fields: str = LIGHT_PAPER_FIELDS
      ) -> dict:
          return _request(
              "GET",
              f"{GRAPH_BASE}/paper/{paper_id}/references",
              params={"limit": min(limit, 1000), "offset": offset, "fields": fields},
          )
      
      
      def recommendations(
          paper_id: str, *, limit: int = 100, fields: str = LIGHT_PAPER_FIELDS, pool: str = "recent"
      ) -> dict:
          return _request(
              "GET",
              f"{RECS_BASE}/papers/forpaper/{paper_id}",
              params={"limit": min(limit, 500), "fields": fields, "from": pool},
          )
      
      
      def batch(ids: list[str], *, fields: str = DEFAULT_PAPER_FIELDS) -> list:
          if len(ids) > 500:
              raise ValueError("batch supports up to 500 ids")
          return _request(
              "POST",
              f"{GRAPH_BASE}/paper/batch",
              params={"fields": fields},
              json_body={"ids": ids},
          )
      
      
      def author_search(query: str, *, limit: int = 20, fields: str = "authorId,name,affiliations,paperCount,citationCount,hIndex") -> dict:
          return _request(
              "GET",
              f"{GRAPH_BASE}/author/search",
              params={"query": query, "limit": limit, "fields": fields},
          )
      
      
      def author(author_id: str, *, fields: str = "authorId,name,affiliations,paperCount,citationCount,hIndex") -> dict:
          return _request("GET", f"{GRAPH_BASE}/author/{author_id}", params={"fields": fields})
      
      
      def author_papers(
          author_id: str, *, limit: int = 100, offset: int = 0, fields: str = LIGHT_PAPER_FIELDS
      ) -> dict:
          return _request(
              "GET",
              f"{GRAPH_BASE}/author/{author_id}/papers",
              params={"limit": limit, "offset": offset, "fields": fields},
          )
      
      
      def snippet_search(query: str, *, limit: int = 10) -> dict:
          return _request(
              "GET",
              f"{GRAPH_BASE}/snippet/search",
              params={"query": query, "limit": limit},
          )
      
      
      def _emit(obj: Any) -> None:
          json.dump(obj, sys.stdout, ensure_ascii=False, indent=2)
          sys.stdout.write("\n")
      
      
      def _main(argv: list[str]) -> int:
          p = argparse.ArgumentParser(description="Semantic Scholar API client")
          sub = p.add_subparsers(dest="cmd", required=True)
      
          sp = sub.add_parser("search")
          sp.add_argument("query")
          sp.add_argument("--limit", type=int, default=20)
          sp.add_argument("--offset", type=int, default=0)
          sp.add_argument("--fields", default=DEFAULT_PAPER_FIELDS)
          sp.add_argument("--year")
          sp.add_argument("--fields-of-study")
          sp.add_argument("--venue")
          sp.add_argument("--min-citation-count", type=int)
          sp.add_argument("--open-access-pdf", action="store_true")
          sp.add_argument("--bulk", action="store_true")
      
          pp = sub.add_parser("paper")
          pp.add_argument("paper_id")
          pp.add_argument("--fields", default=DEFAULT_PAPER_FIELDS)
      
          for name in ("citations", "references"):
              cp = sub.add_parser(name)
              cp.add_argument("paper_id")
              cp.add_argument("--limit", type=int, default=100)
              cp.add_argument("--offset", type=int, default=0)
              cp.add_argument("--fields", default=LIGHT_PAPER_FIELDS)
      
          rp = sub.add_parser("recommendations")
          rp.add_argument("paper_id")
          rp.add_argument("--limit", type=int, default=100)
          rp.add_argument("--fields", default=LIGHT_PAPER_FIELDS)
          rp.add_argument("--pool", choices=["recent", "all-cs"], default="recent")
      
          bp = sub.add_parser("batch")
          bp.add_argument("ids", nargs="+")
          bp.add_argument("--fields", default=DEFAULT_PAPER_FIELDS)
      
          asp = sub.add_parser("author-search")
          asp.add_argument("query")
          asp.add_argument("--limit", type=int, default=20)
      
          ap = sub.add_parser("author")
          ap.add_argument("author_id")
      
          app = sub.add_parser("author-papers")
          app.add_argument("author_id")
          app.add_argument("--limit", type=int, default=100)
          app.add_argument("--offset", type=int, default=0)
      
          snp = sub.add_parser("snippets")
          snp.add_argument("query")
          snp.add_argument("--limit", type=int, default=10)
      
          args = p.parse_args(argv)
      
          try:
              if args.cmd == "search":
                  _emit(search(
                      args.query, limit=args.limit, offset=args.offset, fields=args.fields,
                      year=args.year, fields_of_study=args.fields_of_study, venue=args.venue,
                      min_citation_count=args.min_citation_count,
                      open_access_pdf=args.open_access_pdf, bulk=args.bulk,
                  ))
              elif args.cmd == "paper":
                  _emit(paper(args.paper_id, fields=args.fields))
              elif args.cmd == "citations":
                  _emit(citations(args.paper_id, limit=args.limit, offset=args.offset, fields=args.fields))
              elif args.cmd == "references":
                  _emit(references(args.paper_id, limit=args.limit, offset=args.offset, fields=args.fields))
              elif args.cmd == "recommendations":
                  _emit(recommendations(args.paper_id, limit=args.limit, fields=args.fields, pool=args.pool))
              elif args.cmd == "batch":
                  _emit(batch(args.ids, fields=args.fields))
              elif args.cmd == "author-search":
                  _emit(author_search(args.query, limit=args.limit))
              elif args.cmd == "author":
                  _emit(author(args.author_id))
              elif args.cmd == "author-papers":
                  _emit(author_papers(args.author_id, limit=args.limit, offset=args.offset))
              elif args.cmd == "snippets":
                  _emit(snippet_search(args.query, limit=args.limit))
              else:
                  p.error(f"unknown command: {args.cmd}")
          except SemanticScholarError as e:
              print(f"error: {e}", file=sys.stderr)
              return 1
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(_main(sys.argv[1:]))
      
  • README.md 5.9 KB
    # semantic-scholar-deep
    
    Deep research over the Semantic Scholar Graph API. Covers the endpoints missing from the official [allenai/asta-plugins](https://github.com/allenai/asta-plugins) `semantic-scholar-lookup` skill — backward **references**, **recommendations**, **batch** lookup (up to 500 IDs per call), **snippet** full-text search, and multi-hop **citation-graph BFS**.
    
    ## Install
    
    ```bash
    npx skills add CodeAlive-AI/ai-driven-development@semantic-scholar-deep -g -y
    ```
    
    Works anonymously out of the box — most endpoints (paper lookup, references, recommendations, batch) respond without an API key. The keyword `/paper/search` endpoint is heavily rate-limited on the anonymous tier, so for large sweeps set `SEMANTIC_SCHOLAR_API_KEY` (request one at <https://www.semanticscholar.org/product/api#api-key>).
    
    ## Prerequisites
    
    This skill stands alone for scripts-only usage (invoke the Python CLIs directly). For the full **deep-research pipeline** (orchestrated by the bundled subagent, see below), you also need:
    
    | Tool | Why | How |
    |------|-----|-----|
    | **Exa MCP** | Neural paper discovery across arXiv / OpenReview / PubMed / bioRxiv — the S2 `search` endpoint is too rate-limited to be the primary discovery path | `claude mcp add --transport http exa "https://mcp.exa.ai/mcp?tools=web_search_advanced_exa"` |
    | **allenai `semantic-scholar-lookup` skill** | First-party `asta papers` CLI for fast metadata + forward citations (complements our backward-references / recommendations coverage) | `npx skills add "allenai/asta-plugins@Semantic Scholar Lookup" -g -y` |
    | **Python 3.8+** | Scripts are stdlib-only, no pip install | Usually already present |
    
    Without the above, the bundled subagent falls back to what it can reach (S2-only), but Exa dramatically improves discovery quality and freshness.
    
    ## Quick start
    
    Inline usage (one specific endpoint):
    
    ```
    > Get the references that DOI:10.18653/v1/N18-3011 cites
    > Recommend 20 papers related to this paperId
    > Batch-resolve these 30 arXiv IDs: 2404.18496 2502.02757 ...
    > Build a citation graph of depth 2 around paperId X
    ```
    
    Delegated usage (multi-step research via the bundled subagent):
    
    ```
    > Find recent papers on LLM code review
    > Do a literature review on retrieval-augmented generation since 2024
    > Novelty check: is my idea <X> already published?
    ```
    
    ## What it ships
    
    ### Python CLIs (`scripts/`)
    
    - **`ss_client.py`** — stdlib-only S2 client with exponential backoff on HTTP 429/5xx. Subcommands: `search`, `paper`, `citations`, `references`, `recommendations`, `batch`, `author-search`, `author`, `author-papers`, `snippets`.
    - **`citation_graph.py`** — BFS traversal around a seed paper. Options: `--direction forward|backward|both`, `--depth N`, `--max-nodes N`. Outputs JSON with `nodes` + `edges`, designed for summarization rather than in-context dumping.
    
    ### References (`references/`)
    
    - **`endpoints.md`** — complete field reference, query-parameter list, and ID-format matrix for every endpoint.
    - **`workflows.md`** — 5 ready-made deep-research patterns: literature review from a topic, citation graph around a seed, novelty check, author trajectory, evidence for a claim.
    
    ### Bundled subagent (`agents/deep-paper-researcher.md`, optional)
    
    A paired subagent definition for token-isolated research. Manually install once:
    
    ```bash
    cp ~/.agents/skills/semantic-scholar-deep/agents/deep-paper-researcher.md ~/.claude/agents/
    ```
    
    Then restart the session. Features:
    
    - Input validation — today's date anchoring + caller-paraphrased-window detection (catches cases where the calling agent translates "recent" into an invented "2024-2026" window)
    - Freshness Mode classifier — `RECENT` (default last 6 months, sort by publication date), `FOUNDATIONAL` (sort by citations, no date floor), `MIXED` (two slices side-by-side)
    - Sort-then-tiebreak ranking — never multiplies `citations × recency` into a single score, so fresh papers with zero citations aren't buried under older high-citation ones
    - Compact report format with explicit `Anchor date` / `Mode` / `Window` header — readers can see and redirect the choice
    
    ## Key design decisions
    
    - **Stdlib only** — no `requests` dependency. Works on any Python 3.8+ install.
    - **Backoff over hard-failure** — HTTP 429 / 5xx get exponential retry up to 30s, honoring `Retry-After`. Anonymous tier is usable for small graphs.
    - **Progressive disclosure** — `SKILL.md` stays under 150 lines; deep endpoint-by-endpoint docs and workflow templates live in `references/`.
    - **Token hygiene** — scripts emit raw JSON to stdout by design. For graphs >50 nodes, use `--output` to write to disk and summarize via `jq`/`python3` rather than piping full payloads into the agent's context.
    - **Not a discovery engine** — keyword `search` works but is the weakest endpoint on the anonymous tier; the bundled subagent delegates discovery to Exa MCP and uses S2 for structured expansion (references, recommendations, batch).
    
    ## Rate limits
    
    | Endpoint | Anonymous tier | With API key |
    |----------|----------------|--------------|
    | `paper` by ID | ~1 RPS, reliable | Much higher |
    | `references` / `recommendations` | ~1 RPS, reliable | Much higher |
    | `batch` (up to 500 IDs) | ~1 RPS | Much higher |
    | `search` (keyword) | Frequently 429 | Reliable |
    
    The client respects `Retry-After` and does up to 5 retries with exponential backoff (1→30s).
    
    ## File structure
    
    ```
    semantic-scholar-deep/
    ├── SKILL.md                       # Agent-facing instructions (dispatch rule, when to use)
    ├── README.md                      # This file
    ├── scripts/
    │   ├── ss_client.py              # Full S2 API client (stdlib only)
    │   └── citation_graph.py         # BFS traversal
    ├── references/
    │   ├── endpoints.md              # Per-endpoint field and parameter reference
    │   └── workflows.md              # 5 deep-research patterns
    └── agents/
        └── deep-paper-researcher.md  # Optional paired subagent definition
    ```
    
    ## License
    
    MIT
    
  • SKILL.md 8.8 KB
    ---
    name: semantic-scholar-deep
    description: Deep research over the Semantic Scholar Graph API. Covers endpoints missing from allenai's lookup skill — paper references (backward citations), recommendations, batch paper lookup (up to 500 IDs), snippet search, and multi-hop citation graph traversal (BFS forward/backward). Use when the user asks to build a citation graph, expand a literature seed, find related work, run a reference network traversal, explore what a paper cites or what cites it beyond simple lookup, or batch-resolve many DOI/arXiv/S2 IDs. For multi-step research questions, delegate to the deep-paper-researcher subagent to keep the main context clean. Not for single paper-by-ID lookups (use semantic-scholar-lookup) or topical discovery (use web_search_advanced_exa).
    allowed-tools: Bash(python3:*), Read, Write, Edit, Glob, Grep, Agent
    ---
    
    # Semantic Scholar — Deep Research
    
    Purpose: fill the gaps that `semantic-scholar-lookup` (allenai) leaves — `references`, `recommendations`, `batch`, and multi-hop citation-graph traversal.
    
    ## Contents
    
    - [Dispatch Rule](#dispatch-rule-read-first) — inline vs delegate; model selection
    - [When to Use](#when-to-use) — trigger scenarios
    - [Scripts](#scripts) — `ss_client.py` + `citation_graph.py`
    - [Authentication & Rate Limits](#authentication--rate-limits)
    - [Progressive Disclosure](#progressive-disclosure) — deeper references
    - [Output Hygiene](#output-hygiene)
    - [Integration](#integration) — typical pipeline with the subagent
    
    ## Dispatch Rule (read first)
    
    Two execution modes:
    
    ### Inline (run the Bash scripts yourself)
    
    Use when the user asks for **one specific endpoint**:
    - "get references of paper X" → `ss_client.py references <id>`
    - "recommendations for paper Y" → `ss_client.py recommendations <id>`
    - "batch-resolve these 30 DOIs" → `ss_client.py batch ...`
    - "find the snippet where X is said" → `ss_client.py snippets "..."`
    
    Fast, cheap, no orchestration overhead.
    
    ### Delegate to `deep-paper-researcher` subagent
    
    Use when the task is **multi-step** or would otherwise flood the context:
    - Literature review on a topic
    - Citation graph / network analysis around a seed paper
    - Novelty check for an idea
    - State-of-the-art survey
    - Anything that requires merging Exa discovery + S2 graph + ranking
    
    **Mandatory prompt contents.** The subagent runs in isolated context with no access to this conversation's system reminders. Include exactly these two things:
    
    1. **Today's date** — inline as `Today is YYYY-MM-DD.` Pull from the `currentDate` system-reminder field, or run `date -I` via Bash before delegating if it's missing. Never rely on training-data intuitions about the current year.
    2. **User's request, verbatim** — pass the user's original phrasing (topic + any freshness words like "современные / recent / классические / seminal" and any explicit dates like "since 2024"). Translate language if needed but do not paraphrase trigger words into date windows.
    
    **Do NOT do any of these:**
    - Do NOT classify freshness yourself (RECENT/FOUNDATIONAL/MIXED). The subagent does that from the verbatim user request.
    - Do NOT invent a date window. If the user said "современные / recent / latest" without a year, the subagent defaults to last 6 months — don't preempt it with "2024-2026".
    - Do NOT drop the trigger words. The subagent relies on them to pick the right mode.
    
    Call:
    ```
    Agent(
      subagent_type="deep-paper-researcher",
      description="<3–5 word task>",
      prompt="Today is 2026-04-22.\n\nUser's request: найди современные 10 статей про AI Code Review на arXiv.\n\n<optional: output format hints, language preference>"
      # model: "opus"  ← add only when the user opts in (see below)
    )
    ```
    
    The subagent's Freshness Mode section handles classification; keep this layer thin.
    
    ### Model selection (Sonnet default, Opus on demand)
    
    The subagent's `model` frontmatter is `sonnet` — that's the default.
    
    Override to Opus by passing `model: "opus"` to the `Agent` tool **only if the user explicitly requests deeper reasoning**. Triggers (any of):
    - English: "deep dive", "thorough", "rigorous", "use Opus", "high quality", "comprehensive", "exhaustive"
    - Russian: "глубокий/глубже", "тщательный/тщательно", "подробно", "в режиме Опус/Opus", "максимально качественно", "серьёзный ресерч"
    
    Never auto-upgrade to Opus without a user signal — Sonnet handles the default literature-review workflow fine and costs less.
    
    ## When to Use
    
    Trigger this skill for:
    - **Citation graph / network** over a seed paper or topic
    - **Backward references** (what does this paper cite?) — *not* covered by allenai
    - **Forward citations** with pagination beyond 1000 results
    - **Recommendations** — related-paper discovery from a seed
    - **Batch lookup** — resolve 50-500 DOI/arXiv/CorpusId/S2 IDs in one call
    - **Snippet search** — find specific passages across the S2 corpus
    
    **Do NOT use** for:
    - Simple "get paper by ID" or "who cited this" — use `semantic-scholar-lookup` (faster, no Python)
    - Broad topical discovery — use `web_search_advanced_exa` with `category: "research paper"` (Exa MCP)
    - Consumer-level literature questions — use the `deep-paper-researcher` subagent, which orchestrates all three tools
    
    ## Scripts
    
    Located under `${SKILL_DIR}/scripts/`.
    
    ### `ss_client.py` — raw API client
    
    Subcommands (all output JSON on stdout):
    
    | Command | Endpoint | Notes |
    |---------|----------|-------|
    | `search <query>` | `/graph/v1/paper/search` | `--bulk` switches to `/search/bulk` (up to 1000/page) |
    | `paper <id>` | `/graph/v1/paper/{id}` | ID forms: raw, `DOI:`, `ARXIV:`, `CorpusId:`, `PMID:`, `URL:` |
    | `citations <id>` | `/graph/v1/paper/{id}/citations` | paginated; up to 1000 per page |
    | `references <id>` | `/graph/v1/paper/{id}/references` | paginated; up to 1000 per page |
    | `recommendations <id>` | `/recommendations/v1/papers/forpaper/{id}` | `--pool recent|all-cs` |
    | `batch <id1> <id2> ...` | `POST /graph/v1/paper/batch` | up to 500 IDs |
    | `author-search <query>` | `/graph/v1/author/search` | |
    | `author <id>` | `/graph/v1/author/{id}` | |
    | `author-papers <id>` | `/graph/v1/author/{id}/papers` | |
    | `snippets <query>` | `/graph/v1/snippet/search` | Full-text snippets |
    
    Common flags: `--limit`, `--offset`, `--fields`, `--year`, `--fields-of-study`, `--venue`, `--min-citation-count`.
    
    ### `citation_graph.py` — BFS traversal
    
    ```
    python3 ${SKILL_DIR}/scripts/citation_graph.py <paperId> \
        --direction both \
        --depth 2 \
        --max-nodes 200 \
        --per-hop-limit 50 \
        --output graph.json
    ```
    
    Directions: `forward` (citations), `backward` (references), `both`. Output schema described in the script docstring — `nodes: {paperId → metadata+depth}`, `edges: [{src, dst, direction}]`.
    
    ## Authentication & Rate Limits
    
    - Without API key: ~1 RPS shared, 100 queries/5min bursts. Fine for small graphs.
    - With `SEMANTIC_SCHOLAR_API_KEY` env var: much higher limits.
    - Apply: https://www.semanticscholar.org/product/api#api-key
    - The client does exponential backoff (1→30s) on HTTP 429/5xx, respects `Retry-After`.
    
    ## Progressive Disclosure
    
    - `references/endpoints.md` — complete field list per endpoint + query examples
    - `references/workflows.md` — lit-review, novelty-check, seed-expansion patterns
    
    ## Output Hygiene
    
    Scripts emit raw JSON — redirect to files for anything beyond ~20 results. For graphs >50 nodes always pass `--output graph.json` to avoid flooding the conversation context.
    
    ## Integration
    
    Typical pipeline inside the `deep-paper-researcher` subagent:
    
    1. **Discovery** — `mcp__exa__web_search_advanced_exa` (neural + multi-source)
    2. **ID resolution** — `ss_client.py search` / `batch` to get `paperId` from titles or DOIs
    3. **Graph expansion** — `citation_graph.py` with the top 3-5 seeds
    4. **Synthesis** — distill nodes/edges into a ranked report
    
    ## Optional: Bundled Subagent
    
    A paired subagent definition ships alongside the skill at `agents/deep-paper-researcher.md`. It orchestrates Exa MCP + allenai `semantic-scholar-lookup` + this skill's scripts into a token-isolated research agent with:
    
    - Mandatory input validation (today's date anchoring + caller-paraphrased-window detection)
    - Freshness Mode classifier (RECENT / FOUNDATIONAL / MIXED)
    - Sort-then-tiebreak ranking (never multiplies citations × recency into a single score)
    - Compact report format with explicit `Anchor date` / `Mode` / `Window` header
    
    To install for Claude Code (manual, one-time):
    
    ```bash
    cp ~/.agents/skills/semantic-scholar-deep/agents/deep-paper-researcher.md ~/.claude/agents/
    ```
    
    (Path may differ on other agents — copy to the agent's subagents directory, then restart the session.)
    
    Prerequisites for full pipeline: Exa MCP connected, `allenai/asta-plugins@"Semantic Scholar Lookup"` skill installed.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related