Edit on GitHub

agent_search_gateway.providers.academic.semantic_scholar

Semantic Scholar Graph API discovery adapter.

  1"""Semantic Scholar Graph API discovery adapter."""
  2
  3from __future__ import annotations
  4
  5from ...observability import SecretValue
  6from ..contracts import PaperSearchHit
  7from .common import (
  8    AcademicHttpExecutor,
  9    as_list,
 10    as_mapping,
 11    join_url,
 12    nonnegative_int,
 13    parse_iso_date,
 14    protocol_failure,
 15    reject_item,
 16    text,
 17)
 18
 19_DEFAULT_API_URL = "https://api.semanticscholar.org/graph/v1"
 20_FIELDS = (
 21    "title,abstract,citationCount,authors,url,publicationDate,"
 22    "externalIds,fieldsOfStudy,openAccessPdf"
 23)
 24
 25
 26class SemanticScholarProvider:
 27    name = "semantic_scholar"
 28
 29    def __init__(
 30        self,
 31        executor: AcademicHttpExecutor,
 32        *,
 33        api_url: str = _DEFAULT_API_URL,
 34        api_key: SecretValue | None = None,
 35    ) -> None:
 36        self._executor = executor
 37        self._api_url = api_url
 38        self._credential = api_key
 39
 40    async def search(self, query: str) -> list[PaperSearchHit]:
 41        payload = await self._executor.request_json(
 42            "GET",
 43            join_url(self._api_url, "paper/search"),
 44            stage="paper_search",
 45            headers=self._request_headers(),
 46            params={"query": query, "limit": 10, "fields": _FIELDS},
 47        )
 48        envelope = as_mapping(payload)
 49        if envelope is None:
 50            reason = "invalid_data_envelope"
 51            raise protocol_failure(self.name, reason, reason=reason)
 52        if "data" not in envelope:
 53            reason = "missing_data_envelope"
 54            raise protocol_failure(self.name, reason, reason=reason)
 55        data = as_list(envelope.get("data"))
 56        if data is None:
 57            reason = "invalid_data_envelope"
 58            raise protocol_failure(self.name, reason, reason=reason)
 59        hits: list[PaperSearchHit] = []
 60        for item in data:
 61            mapped = self._map_item(item)
 62            if mapped is None:
 63                reject_item(self.name)
 64            else:
 65                hits.append(mapped)
 66        return hits
 67
 68    def _request_headers(self) -> dict[str, str] | None:
 69        if self._credential is None:
 70            return None
 71        reveal = self._credential.reveal
 72        return {"x-api-key": reveal()}
 73
 74    def _map_item(self, value: object) -> PaperSearchHit | None:
 75        item = as_mapping(value)
 76        if item is None:
 77            return None
 78        paper_id = text(item.get("paperId"))
 79        title = text(item.get("title"))
 80        url = text(item.get("url"))
 81        if not paper_id or not title or not url:
 82            return None
 83        external = as_mapping(item.get("externalIds")) or {}
 84        authors_raw = as_list(item.get("authors")) or []
 85        authors = tuple(
 86            name
 87            for author in authors_raw
 88            if (mapping := as_mapping(author)) is not None and (name := text(mapping.get("name")))
 89        )
 90        topics = tuple(
 91            topic
 92            for raw_topic in (as_list(item.get("fieldsOfStudy")) or [])
 93            if (topic := text(raw_topic))
 94        )
 95        oa = as_mapping(item.get("openAccessPdf"))
 96        pdf_url = text(oa.get("url")) if oa is not None else ""
 97        return PaperSearchHit(
 98            source=self.name,
 99            source_id=paper_id,
100            title=title,
101            authors=authors,
102            abstract=text(item.get("abstract")),
103            doi=text(external.get("DOI")),
104            arxiv_id=text(external.get("ArXiv")),
105            published_date=parse_iso_date(item.get("publicationDate")),
106            url=url,
107            pdf_url=pdf_url,
108            topics=topics,
109            citation_count=nonnegative_int(item.get("citationCount")),
110            is_open_access=True if pdf_url else None,
111        )
class SemanticScholarProvider:
 27class SemanticScholarProvider:
 28    name = "semantic_scholar"
 29
 30    def __init__(
 31        self,
 32        executor: AcademicHttpExecutor,
 33        *,
 34        api_url: str = _DEFAULT_API_URL,
 35        api_key: SecretValue | None = None,
 36    ) -> None:
 37        self._executor = executor
 38        self._api_url = api_url
 39        self._credential = api_key
 40
 41    async def search(self, query: str) -> list[PaperSearchHit]:
 42        payload = await self._executor.request_json(
 43            "GET",
 44            join_url(self._api_url, "paper/search"),
 45            stage="paper_search",
 46            headers=self._request_headers(),
 47            params={"query": query, "limit": 10, "fields": _FIELDS},
 48        )
 49        envelope = as_mapping(payload)
 50        if envelope is None:
 51            reason = "invalid_data_envelope"
 52            raise protocol_failure(self.name, reason, reason=reason)
 53        if "data" not in envelope:
 54            reason = "missing_data_envelope"
 55            raise protocol_failure(self.name, reason, reason=reason)
 56        data = as_list(envelope.get("data"))
 57        if data is None:
 58            reason = "invalid_data_envelope"
 59            raise protocol_failure(self.name, reason, reason=reason)
 60        hits: list[PaperSearchHit] = []
 61        for item in data:
 62            mapped = self._map_item(item)
 63            if mapped is None:
 64                reject_item(self.name)
 65            else:
 66                hits.append(mapped)
 67        return hits
 68
 69    def _request_headers(self) -> dict[str, str] | None:
 70        if self._credential is None:
 71            return None
 72        reveal = self._credential.reveal
 73        return {"x-api-key": reveal()}
 74
 75    def _map_item(self, value: object) -> PaperSearchHit | None:
 76        item = as_mapping(value)
 77        if item is None:
 78            return None
 79        paper_id = text(item.get("paperId"))
 80        title = text(item.get("title"))
 81        url = text(item.get("url"))
 82        if not paper_id or not title or not url:
 83            return None
 84        external = as_mapping(item.get("externalIds")) or {}
 85        authors_raw = as_list(item.get("authors")) or []
 86        authors = tuple(
 87            name
 88            for author in authors_raw
 89            if (mapping := as_mapping(author)) is not None and (name := text(mapping.get("name")))
 90        )
 91        topics = tuple(
 92            topic
 93            for raw_topic in (as_list(item.get("fieldsOfStudy")) or [])
 94            if (topic := text(raw_topic))
 95        )
 96        oa = as_mapping(item.get("openAccessPdf"))
 97        pdf_url = text(oa.get("url")) if oa is not None else ""
 98        return PaperSearchHit(
 99            source=self.name,
100            source_id=paper_id,
101            title=title,
102            authors=authors,
103            abstract=text(item.get("abstract")),
104            doi=text(external.get("DOI")),
105            arxiv_id=text(external.get("ArXiv")),
106            published_date=parse_iso_date(item.get("publicationDate")),
107            url=url,
108            pdf_url=pdf_url,
109            topics=topics,
110            citation_count=nonnegative_int(item.get("citationCount")),
111            is_open_access=True if pdf_url else None,
112        )
SemanticScholarProvider( executor: agent_search_gateway.providers.academic.common.AcademicHttpExecutor, *, api_url: str = 'https://api.semanticscholar.org/graph/v1', api_key: agent_search_gateway.observability.SecretValue | None = None)
30    def __init__(
31        self,
32        executor: AcademicHttpExecutor,
33        *,
34        api_url: str = _DEFAULT_API_URL,
35        api_key: SecretValue | None = None,
36    ) -> None:
37        self._executor = executor
38        self._api_url = api_url
39        self._credential = api_key
name = 'semantic_scholar'
async def search( self, query: str) -> list[agent_search_gateway.providers.contracts.PaperSearchHit]:
41    async def search(self, query: str) -> list[PaperSearchHit]:
42        payload = await self._executor.request_json(
43            "GET",
44            join_url(self._api_url, "paper/search"),
45            stage="paper_search",
46            headers=self._request_headers(),
47            params={"query": query, "limit": 10, "fields": _FIELDS},
48        )
49        envelope = as_mapping(payload)
50        if envelope is None:
51            reason = "invalid_data_envelope"
52            raise protocol_failure(self.name, reason, reason=reason)
53        if "data" not in envelope:
54            reason = "missing_data_envelope"
55            raise protocol_failure(self.name, reason, reason=reason)
56        data = as_list(envelope.get("data"))
57        if data is None:
58            reason = "invalid_data_envelope"
59            raise protocol_failure(self.name, reason, reason=reason)
60        hits: list[PaperSearchHit] = []
61        for item in data:
62            mapped = self._map_item(item)
63            if mapped is None:
64                reject_item(self.name)
65            else:
66                hits.append(mapped)
67        return hits