Phase 2.2-2.3: add doi to Paper, populate from S2 externalIds, integrate OpenAlex into retriever
Browse files- src/agents/retriever.py +46 -0
- src/retriever_utils.py +3 -1
- src/state.py +1 -0
src/agents/retriever.py
CHANGED
|
@@ -7,6 +7,7 @@ from src.retriever_utils import (
|
|
| 7 |
search_semantic_scholar,
|
| 8 |
search_web,
|
| 9 |
build_citation_graph,
|
|
|
|
| 10 |
)
|
| 11 |
|
| 12 |
load_dotenv()
|
|
@@ -95,6 +96,51 @@ def retriever_node(state: ResearchState) -> ResearchState:
|
|
| 95 |
if i < len(sub_questions) - 1:
|
| 96 |
time.sleep(1)
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
# Sort papers by hybrid score descending
|
| 99 |
all_papers.sort(key=lambda p: p.hybrid_score, reverse=True)
|
| 100 |
|
|
|
|
| 7 |
search_semantic_scholar,
|
| 8 |
search_web,
|
| 9 |
build_citation_graph,
|
| 10 |
+
hybrid_score,
|
| 11 |
)
|
| 12 |
|
| 13 |
load_dotenv()
|
|
|
|
| 96 |
if i < len(sub_questions) - 1:
|
| 97 |
time.sleep(1)
|
| 98 |
|
| 99 |
+
# --- Phase 2.2: OpenAlex augmentation ---
|
| 100 |
+
from src.openalex_utils import search_openalex
|
| 101 |
+
existing_dois = {p.doi.lower() for p in all_papers if p.doi}
|
| 102 |
+
existing_ids = {p.paper_id for p in all_papers}
|
| 103 |
+
|
| 104 |
+
if len(all_papers) < 12:
|
| 105 |
+
for question in sub_questions[:2]: # only first 2 sub-questions
|
| 106 |
+
try:
|
| 107 |
+
oa_results = search_openalex(question, max_results=3)
|
| 108 |
+
for r in oa_results:
|
| 109 |
+
doi_lower = (r.get("doi") or "").lower()
|
| 110 |
+
pid = r.get("paper_id") or ""
|
| 111 |
+
# Skip if we already have this paper by DOI or paper_id
|
| 112 |
+
if doi_lower and doi_lower in existing_dois:
|
| 113 |
+
continue
|
| 114 |
+
if pid in existing_ids:
|
| 115 |
+
continue
|
| 116 |
+
# Build Paper object from OpenAlex result
|
| 117 |
+
p = Paper(
|
| 118 |
+
title=r.get("title") or "",
|
| 119 |
+
abstract=r.get("abstract") or "",
|
| 120 |
+
year=r.get("year") or 0,
|
| 121 |
+
citation_count=r.get("citation_count") or 0,
|
| 122 |
+
paper_id=pid,
|
| 123 |
+
authors=[a.strip() for a in (r.get("authors") or "").split(",") if a.strip()],
|
| 124 |
+
references=[],
|
| 125 |
+
doi=r.get("doi") or "",
|
| 126 |
+
source="openalex",
|
| 127 |
+
)
|
| 128 |
+
if not p.title or not p.year:
|
| 129 |
+
continue
|
| 130 |
+
p.hybrid_score = hybrid_score(
|
| 131 |
+
semantic_sim=0.3, # conservative default — no query embedding for OA papers
|
| 132 |
+
year=p.year,
|
| 133 |
+
citation_count=p.citation_count,
|
| 134 |
+
decay_config=decay_config,
|
| 135 |
+
)
|
| 136 |
+
all_papers.append(p)
|
| 137 |
+
existing_dois.add(doi_lower)
|
| 138 |
+
existing_ids.add(pid)
|
| 139 |
+
time.sleep(0.5) # be polite to OpenAlex
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.warning(f"OpenAlex augmentation failed for '{question[:40]}': {e}")
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
# Sort papers by hybrid score descending
|
| 145 |
all_papers.sort(key=lambda p: p.hybrid_score, reverse=True)
|
| 146 |
|
src/retriever_utils.py
CHANGED
|
@@ -129,7 +129,7 @@ def search_semantic_scholar(
|
|
| 129 |
params = {
|
| 130 |
"query": query,
|
| 131 |
"limit": limit,
|
| 132 |
-
"fields": "title,abstract,year,citationCount,authors,references,paperId",
|
| 133 |
}
|
| 134 |
|
| 135 |
time.sleep(3) # rate limit guard
|
|
@@ -170,6 +170,7 @@ def search_semantic_scholar(
|
|
| 170 |
if ref.get("paperId")
|
| 171 |
]
|
| 172 |
|
|
|
|
| 173 |
paper = Paper(
|
| 174 |
title=r.get("title") or "Untitled",
|
| 175 |
abstract=abstract,
|
|
@@ -178,6 +179,7 @@ def search_semantic_scholar(
|
|
| 178 |
paper_id=r.get("paperId") or "",
|
| 179 |
authors=authors,
|
| 180 |
references=references,
|
|
|
|
| 181 |
hybrid_score=hybrid_score(sim, year, citations, decay_config),
|
| 182 |
source="semantic_scholar",
|
| 183 |
)
|
|
|
|
| 129 |
params = {
|
| 130 |
"query": query,
|
| 131 |
"limit": limit,
|
| 132 |
+
"fields": "title,abstract,year,citationCount,authors,references,paperId,externalIds",
|
| 133 |
}
|
| 134 |
|
| 135 |
time.sleep(3) # rate limit guard
|
|
|
|
| 170 |
if ref.get("paperId")
|
| 171 |
]
|
| 172 |
|
| 173 |
+
doi = (r.get("externalIds") or {}).get("DOI", "") or ""
|
| 174 |
paper = Paper(
|
| 175 |
title=r.get("title") or "Untitled",
|
| 176 |
abstract=abstract,
|
|
|
|
| 179 |
paper_id=r.get("paperId") or "",
|
| 180 |
authors=authors,
|
| 181 |
references=references,
|
| 182 |
+
doi=doi,
|
| 183 |
hybrid_score=hybrid_score(sim, year, citations, decay_config),
|
| 184 |
source="semantic_scholar",
|
| 185 |
)
|
src/state.py
CHANGED
|
@@ -15,6 +15,7 @@ class Paper:
|
|
| 15 |
paper_id: str
|
| 16 |
authors: list[str] = field(default_factory=list)
|
| 17 |
references: list[str] = field(default_factory=list) # list of paper_ids
|
|
|
|
| 18 |
hybrid_score: float = 0.0
|
| 19 |
source: str = "semantic_scholar" # or "web"
|
| 20 |
|
|
|
|
| 15 |
paper_id: str
|
| 16 |
authors: list[str] = field(default_factory=list)
|
| 17 |
references: list[str] = field(default_factory=list) # list of paper_ids
|
| 18 |
+
doi: str = ""
|
| 19 |
hybrid_score: float = 0.0
|
| 20 |
source: str = "semantic_scholar" # or "web"
|
| 21 |
|