Coverage for src/local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py: 97%

284 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-20 01:24 +0000

1import re 

2from typing import Any, Dict, List, Optional, Tuple 

3 

4import requests 

5from langchain_core.language_models import BaseLLM 

6from ...security.secure_logging import logger 

7from requests.adapters import HTTPAdapter 

8from urllib3.util import Retry 

9 

10from ...constants import SNIPPET_LENGTH_SHORT 

11from ..rate_limiting import RateLimitError 

12from ..search_engine_base import BaseSearchEngine, Exposure, Sensitivity 

13from ...security import SafeSession 

14 

15 

16class SemanticScholarSearchEngine(BaseSearchEngine): 

17 """ 

18 Semantic Scholar search engine implementation with two-phase approach. 

19 Provides efficient access to scientific literature across all fields. 

20 """ 

21 

22 # Mark as public search engine 

23 is_public = True 

24 egress_sensitivity = Sensitivity.NON_SENSITIVE 

25 egress_exposure = Exposure.EXPOSING 

26 # Scientific/academic search engine 

27 is_scientific = True 

28 is_lexical = True 

29 needs_llm_relevance_filter = True 

30 

31 def __init__( 

32 self, 

33 max_results: int = 10, 

34 api_key: Optional[str] = None, 

35 year_range: Optional[Tuple[int, int]] = None, 

36 get_abstracts: bool = True, 

37 get_references: bool = False, 

38 get_citations: bool = False, 

39 get_embeddings: bool = False, 

40 get_tldr: bool = True, 

41 citation_limit: int = 10, 

42 reference_limit: int = 10, 

43 llm: Optional[BaseLLM] = None, 

44 max_filtered_results: Optional[int] = None, 

45 optimize_queries: bool = True, 

46 max_retries: int = 5, 

47 retry_backoff_factor: float = 1.0, 

48 fields_of_study: Optional[List[str]] = None, 

49 publication_types: Optional[List[str]] = None, 

50 settings_snapshot: Optional[Dict[str, Any]] = None, 

51 **kwargs, 

52 ): 

53 """ 

54 Initialize the Semantic Scholar search engine. 

55 

56 Args: 

57 max_results: Maximum number of search results 

58 api_key: Semantic Scholar API key for higher rate limits (optional) 

59 year_range: Optional tuple of (start_year, end_year) to filter results 

60 get_abstracts: Whether to fetch abstracts for all results 

61 get_references: Whether to fetch references for papers 

62 get_citations: Whether to fetch citations for papers 

63 get_embeddings: Whether to fetch SPECTER embeddings for papers 

64 get_tldr: Whether to fetch TLDR summaries for papers 

65 citation_limit: Maximum number of citations to fetch per paper 

66 reference_limit: Maximum number of references to fetch per paper 

67 llm: Language model for relevance filtering 

68 max_filtered_results: Maximum number of results to keep after filtering 

69 optimize_queries: Whether to optimize natural language queries 

70 max_retries: Maximum number of retries for API requests 

71 retry_backoff_factor: Backoff factor for retries 

72 fields_of_study: List of fields of study to filter results 

73 publication_types: List of publication types to filter results 

74 settings_snapshot: Settings snapshot for configuration 

75 **kwargs: Additional parameters to pass to parent class 

76 """ 

77 # Journal filter runs before LLM relevance (Tiers 1-3 are instant) 

78 preview_filters = [] 

79 journal_filter = self._create_journal_filter( 

80 "semantic_scholar", llm, settings_snapshot 

81 ) 

82 if journal_filter is not None: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true

83 preview_filters.append(journal_filter) 

84 

85 super().__init__( 

86 llm=llm, 

87 max_filtered_results=max_filtered_results, 

88 max_results=max_results, 

89 preview_filters=preview_filters, # type: ignore[arg-type] 

90 settings_snapshot=settings_snapshot, 

91 **kwargs, 

92 ) 

93 

94 # Get API key from settings if not provided 

95 if not api_key and settings_snapshot: 

96 from ...config.search_config import get_setting_from_snapshot 

97 

98 try: 

99 api_key = get_setting_from_snapshot( 

100 "search.engine.web.semantic_scholar.api_key", 

101 settings_snapshot=settings_snapshot, 

102 ) 

103 except Exception: 

104 logger.debug( 

105 "Failed to read semantic_scholar.api_key from settings snapshot", 

106 exc_info=True, 

107 ) 

108 

109 self.api_key = api_key 

110 self.year_range = year_range 

111 self.get_abstracts = get_abstracts 

112 self.get_references = get_references 

113 self.get_citations = get_citations 

114 self.get_embeddings = get_embeddings 

115 self.get_tldr = get_tldr 

116 self.citation_limit = citation_limit 

117 self.reference_limit = reference_limit 

118 self.optimize_queries = optimize_queries 

119 self.max_retries = max_retries 

120 self.retry_backoff_factor = retry_backoff_factor 

121 self.fields_of_study = ( 

122 self._ensure_list(fields_of_study) 

123 if fields_of_study is not None 

124 else None 

125 ) 

126 self.publication_types = ( 

127 self._ensure_list(publication_types) 

128 if publication_types is not None 

129 else None 

130 ) 

131 

132 # Base API URLs 

133 self.base_url = "https://api.semanticscholar.org/graph/v1" 

134 self.paper_search_url = f"{self.base_url}/paper/search" 

135 self.paper_details_url = f"{self.base_url}/paper" 

136 

137 # Create a session with retry capabilities 

138 self.session: SafeSession | None = self._create_session() 

139 

140 # Log API key status 

141 if self.api_key: 

142 logger.info( 

143 "Using Semantic Scholar with API key (higher rate limits)" 

144 ) 

145 else: 

146 logger.info( 

147 "Using Semantic Scholar without API key (lower rate limits)" 

148 ) 

149 

150 def _create_session(self) -> SafeSession: 

151 """Create and configure a requests session with retry capabilities""" 

152 session = SafeSession() 

153 

154 # Configure automatic retries with exponential backoff 

155 retry_strategy = Retry( 

156 total=self.max_retries, 

157 backoff_factor=self.retry_backoff_factor, 

158 status_forcelist=[429, 500, 502, 503, 504], 

159 allowed_methods={"HEAD", "GET", "POST", "OPTIONS"}, 

160 ) 

161 

162 adapter = HTTPAdapter(max_retries=retry_strategy) 

163 session.mount("https://", adapter) 

164 

165 # Set up headers 

166 headers = {"Accept": "application/json"} 

167 if self.api_key: 

168 headers["x-api-key"] = self.api_key 

169 

170 session.headers.update(headers) 

171 

172 return session 

173 

174 def close(self): 

175 """ 

176 Close the HTTP session and clean up resources. 

177 

178 Call this method when done using the search engine to prevent 

179 connection/file descriptor leaks. 

180 """ 

181 if hasattr(self, "session") and self.session: 

182 try: 

183 self.session.close() 

184 except Exception as e: 

185 safe_msg = self._scrub_error(e) 

186 logger.warning( 

187 f"Error closing SemanticScholar session: {safe_msg}" 

188 ) 

189 finally: 

190 self.session = None 

191 # Close content filters (JournalReputationFilter) via parent 

192 super().close() 

193 

194 def __del__(self): 

195 """Destructor to ensure session is closed.""" 

196 self.close() 

197 

198 def __enter__(self): 

199 """Context manager entry.""" 

200 return self 

201 

202 def __exit__(self, exc_type, exc_val, exc_tb): 

203 """Context manager exit - ensures session cleanup.""" 

204 self.close() 

205 return False 

206 

207 def _respect_rate_limit(self): 

208 """Apply rate limiting between requests""" 

209 # Apply rate limiting before request 

210 self._last_wait_time = self.rate_tracker.apply_rate_limit( 

211 self.engine_type 

212 ) 

213 logger.debug(f"Applied rate limit wait: {self._last_wait_time:.2f}s") 

214 

215 def _make_request( 

216 self, 

217 url: str, 

218 params: Optional[Dict] = None, 

219 data: Optional[Dict] = None, 

220 method: str = "GET", 

221 ) -> Dict: 

222 """ 

223 Make a request to the Semantic Scholar API. 

224 

225 Args: 

226 url: API endpoint URL 

227 params: Query parameters 

228 data: JSON data for POST requests 

229 method: HTTP method (GET or POST) 

230 

231 Returns: 

232 API response as dictionary 

233 """ 

234 self._respect_rate_limit() 

235 

236 try: 

237 if self.session is None: 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true

238 raise RuntimeError("Session is not initialized") 

239 if method.upper() == "GET": 

240 response = self.session.get(url, params=params, timeout=30) 

241 elif method.upper() == "POST": 

242 response = self.session.post( 

243 url, params=params, json=data, timeout=30 

244 ) 

245 else: 

246 raise ValueError(f"Unsupported HTTP method: {method}") 

247 

248 # Handle rate limiting 

249 if response.status_code == 429: 

250 logger.warning("Semantic Scholar rate limit exceeded") 

251 raise RateLimitError("Semantic Scholar rate limit exceeded") 

252 

253 response.raise_for_status() 

254 return response.json() # type: ignore[no-any-return] 

255 except requests.RequestException as e: 

256 safe_msg = self._scrub_error(e) 

257 logger.warning(f"API request failed: {safe_msg}") 

258 return {} 

259 

260 def _optimize_query(self, query: str) -> str: 

261 """ 

262 Optimize a natural language query for Semantic Scholar search. 

263 If LLM is available, uses it to extract key terms and concepts. 

264 

265 Args: 

266 query: Natural language query 

267 

268 Returns: 

269 Optimized query string 

270 """ 

271 if not self.llm or not self.optimize_queries: 

272 return query 

273 

274 try: 

275 prompt = f"""Transform this natural language question into an optimized academic search query. 

276 

277Original query: "{query}" 

278 

279INSTRUCTIONS: 

2801. Extract key academic concepts, technical terms, and proper nouns 

2812. Remove generic words, filler words, and non-technical terms 

2823. Add quotation marks around specific phrases that should be kept together 

2834. Return ONLY the optimized search query with no explanation 

2845. Keep it under 100 characters if possible 

285 

286EXAMPLE TRANSFORMATIONS: 

287"What are the latest findings about mRNA vaccines and COVID-19?" → "mRNA vaccines COVID-19 recent findings" 

288"How does machine learning impact climate change prediction?" → "machine learning "climate change" prediction" 

289"Tell me about quantum computing approaches for encryption" → "quantum computing encryption" 

290 

291Return ONLY the optimized search query with no explanation. 

292""" 

293 

294 response = self.llm.invoke(prompt) 

295 optimized_query = ( 

296 str(response.content) 

297 if hasattr(response, "content") 

298 else str(response) 

299 ).strip() 

300 

301 # Clean up the query - remove any explanations 

302 lines = optimized_query.split("\n") 

303 optimized_query = lines[0].strip() 

304 

305 # Safety check - if query looks too much like an explanation, use original 

306 if len(optimized_query.split()) > 15 or ":" in optimized_query: 

307 logger.warning( 

308 "Query optimization result looks too verbose, using original" 

309 ) 

310 return query 

311 

312 logger.info(f"Original query: '{query}'") 

313 logger.info(f"Optimized for search: '{optimized_query}'") 

314 

315 return optimized_query 

316 except Exception as e: 

317 safe_msg = self._scrub_error(e) 

318 logger.warning(f"Error optimizing query: {safe_msg}") 

319 return query # Fall back to original query on error 

320 

321 def _direct_search(self, query: str) -> List[Dict[str, Any]]: 

322 """ 

323 Make a direct search request to the Semantic Scholar API. 

324 

325 Args: 

326 query: The search query 

327 

328 Returns: 

329 List of paper dictionaries 

330 """ 

331 try: 

332 # Configure fields to retrieve 

333 fields = [ 

334 "paperId", 

335 "externalIds", 

336 "url", 

337 "title", 

338 "abstract", 

339 "venue", 

340 "publicationVenue", # Structured venue with name/type/ISSN 

341 "year", 

342 "authors", 

343 "citationCount", # Add citation count for ranking 

344 "openAccessPdf", # PDF URL for open access papers 

345 ] 

346 

347 if self.get_tldr: 

348 fields.append("tldr") 

349 

350 params = { 

351 "query": query, 

352 "limit": min( 

353 self.max_results, 100 

354 ), # API limit is 100 per request 

355 "fields": ",".join(fields), 

356 } 

357 

358 # Add year filter if specified 

359 if self.year_range: 

360 start_year, end_year = self.year_range 

361 params["year"] = f"{start_year}-{end_year}" 

362 

363 # Add fields of study filter if specified 

364 if self.fields_of_study: 

365 params["fieldsOfStudy"] = ",".join(self.fields_of_study) 

366 

367 # Add publication types filter if specified 

368 if self.publication_types: 

369 params["publicationTypes"] = ",".join(self.publication_types) 

370 

371 response = self._make_request(self.paper_search_url, params) 

372 

373 if "data" in response: 

374 papers = response["data"] 

375 logger.info( 

376 f"Found {len(papers)} papers with direct search for query: '{query}'" 

377 ) 

378 return papers # type: ignore[no-any-return] 

379 logger.warning( 

380 f"No data in response for direct search query: '{query}'" 

381 ) 

382 return [] 

383 

384 except Exception as e: 

385 safe_msg = self._scrub_error(e) 

386 logger.warning(f"Error in direct search: {safe_msg}") 

387 return [] 

388 

389 def _adaptive_search(self, query: str) -> Tuple[List[Dict[str, Any]], str]: 

390 """ 

391 Perform an adaptive search that adjusts based on result volume. 

392 Uses LLM to generate better fallback queries when available. 

393 

394 Args: 

395 query: The search query 

396 

397 Returns: 

398 Tuple of (list of paper results, search strategy used) 

399 """ 

400 # Start with a standard search 

401 papers = self._direct_search(query) 

402 strategy = "standard" 

403 

404 # If no results, try different variations 

405 if not papers: 

406 # Try removing quotes to broaden search 

407 if '"' in query: 

408 unquoted_query = query.replace('"', "") 

409 logger.info( 

410 "No results with quoted terms, trying without quotes: {}", 

411 unquoted_query, 

412 ) 

413 papers = self._direct_search(unquoted_query) 

414 

415 if papers: 

416 strategy = "unquoted" 

417 return papers, strategy 

418 

419 # If LLM is available, use it to generate better fallback queries 

420 if self.llm: 

421 try: 

422 # Generate alternate search queries focusing on core concepts 

423 prompt = f"""You are helping refine a search query that returned no results. 

424 

425Original query: "{query}" 

426 

427The query might be too specific or use natural language phrasing that doesn't match academic paper keywords. 

428 

429Please provide THREE alternative search queries that: 

4301. Focus on the core academic concepts 

4312. Use precise terminology commonly found in academic papers 

4323. Break down complex queries into more searchable components 

4334. Format each as a concise keyword-focused search term (not a natural language question) 

434 

435Format each query on a new line with no numbering or explanation. Keep each query under 8 words and very focused. 

436""" 

437 # Get the LLM's response 

438 response = self.llm.invoke(prompt) 

439 

440 # Extract the alternative queries 

441 alt_queries = [] 

442 if hasattr( 

443 response, "content" 

444 ): # Handle various LLM response formats 

445 content = response.content 

446 alt_queries = [ 

447 q.strip() 

448 for q in content.strip().split("\n") 

449 if q.strip() 

450 ] 

451 elif isinstance(response, str): 451 ↛ 459line 451 didn't jump to line 459 because the condition on line 451 was always true

452 alt_queries = [ 

453 q.strip() 

454 for q in response.strip().split("\n") 

455 if q.strip() 

456 ] 

457 

458 # Try each alternative query 

459 for alt_query in alt_queries[ 

460 :3 

461 ]: # Limit to first 3 alternatives 

462 logger.info("Trying LLM-suggested query: {}", alt_query) 

463 alt_papers = self._direct_search(alt_query) 

464 

465 if alt_papers: 

466 logger.info( 

467 "Found {} papers using LLM-suggested query: {}", 

468 len(alt_papers), 

469 alt_query, 

470 ) 

471 strategy = "llm_alternative" 

472 return alt_papers, strategy 

473 except Exception as e: 

474 safe_msg = self._scrub_error(e) 

475 logger.warning( 

476 f"Error using LLM for query refinement: {safe_msg}" 

477 ) 

478 # Fall through to simpler strategies 

479 

480 # Fallback: Try with the longest words (likely specific terms) 

481 words = re.findall(r"\w+", query) 

482 longer_words = [word for word in words if len(word) > 6] 

483 if longer_words: 

484 # Use up to 3 of the longest words 

485 longer_words = sorted(longer_words, key=len, reverse=True)[:3] 

486 key_terms_query = " ".join(longer_words) 

487 logger.info("Trying with key terms: {}", key_terms_query) 

488 papers = self._direct_search(key_terms_query) 

489 

490 if papers: 

491 strategy = "key_terms" 

492 return papers, strategy 

493 

494 # Final fallback: Try with just the longest word 

495 if words: 495 ↛ 505line 495 didn't jump to line 505 because the condition on line 495 was always true

496 longest_word = max(words, key=len) 

497 if len(longest_word) > 5: # Only use if it's reasonably long 

498 logger.info("Trying with single key term: {}", longest_word) 

499 papers = self._direct_search(longest_word) 

500 

501 if papers: 

502 strategy = "single_term" 

503 return papers, strategy 

504 

505 return papers, strategy 

506 

507 def _get_paper_details(self, paper_id: str) -> Dict[str, Any]: 

508 """ 

509 Get detailed information about a specific paper. 

510 

511 Args: 

512 paper_id: Semantic Scholar Paper ID 

513 

514 Returns: 

515 Dictionary with paper details 

516 """ 

517 try: 

518 # Construct fields parameter 

519 fields = [ 

520 "paperId", 

521 "externalIds", 

522 "corpusId", 

523 "url", 

524 "title", 

525 "abstract", 

526 "venue", 

527 "publicationVenue", # Structured venue with name/type/ISSN 

528 "year", 

529 "authors", 

530 "fieldsOfStudy", 

531 "citationCount", # Add citation count 

532 ] 

533 

534 if self.get_tldr: 

535 fields.append("tldr") 

536 

537 if self.get_embeddings: 

538 fields.append("embedding") 

539 

540 # Add citation and reference fields if requested 

541 if self.get_citations: 

542 fields.append(f"citations.limit({self.citation_limit})") 

543 

544 if self.get_references: 

545 fields.append(f"references.limit({self.reference_limit})") 

546 

547 # Make the request 

548 url = f"{self.paper_details_url}/{paper_id}" 

549 params = {"fields": ",".join(fields)} 

550 

551 return self._make_request(url, params) 

552 

553 except Exception as e: 

554 safe_msg = self._scrub_error(e) 

555 logger.warning(f"Error getting paper details: {safe_msg}") 

556 return {} 

557 

558 def _get_previews(self, query: str) -> List[Dict[str, Any]]: 

559 """ 

560 Get preview information for Semantic Scholar papers. 

561 

562 Args: 

563 query: The search query 

564 

565 Returns: 

566 List of preview dictionaries 

567 """ 

568 logger.info(f"Getting Semantic Scholar previews for query: {query}") 

569 

570 # Optimize the query if LLM is available 

571 optimized_query = self._optimize_query(query) 

572 

573 # Use the adaptive search approach 

574 papers, strategy = self._adaptive_search(optimized_query) 

575 

576 if not papers: 

577 logger.warning("No Semantic Scholar results found") 

578 return [] 

579 

580 # Format as previews 

581 previews = [] 

582 for paper in papers: 

583 try: 

584 # Format authors - ensure we have a valid list with string values 

585 authors = [] 

586 if paper.get("authors"): 

587 authors = [ 

588 author.get("name", "") 

589 for author in paper["authors"] 

590 if author and author.get("name") 

591 ] 

592 

593 # Ensure we have valid strings for all fields 

594 paper_id = paper.get("paperId", "") 

595 title = paper.get("title", "") 

596 url = paper.get("url", "") 

597 

598 # Handle abstract safely, ensuring we always have a string 

599 abstract = paper.get("abstract") 

600 snippet = "" 

601 if abstract: 

602 snippet = ( 

603 abstract[:SNIPPET_LENGTH_SHORT] + "..." 

604 if len(abstract) > SNIPPET_LENGTH_SHORT 

605 else abstract 

606 ) 

607 

608 # Prefer publicationVenue (structured, with ISSN) over 

609 # venue (plain string, often empty for many papers). 

610 pub_venue = paper.get("publicationVenue") or {} 

611 venue_name = pub_venue.get("name") or paper.get("venue", "") 

612 venue_issn = pub_venue.get("issn") 

613 

614 year = paper.get("year") 

615 external_ids = paper.get("externalIds", {}) 

616 

617 # Handle TLDR safely 

618 tldr_text = "" 

619 if paper.get("tldr") and isinstance(paper.get("tldr"), dict): 

620 tldr_text = paper.get("tldr", {}).get("text", "") 

621 

622 # Create preview with basic information, ensuring no None values 

623 preview = { 

624 "id": paper_id if paper_id else "", 

625 "title": title if title else "", 

626 "link": url if url else "", 

627 "snippet": snippet, 

628 "authors": authors, 

629 "venue": venue_name if venue_name else "", 

630 "journal_ref": venue_name if venue_name else None, 

631 "issn": venue_issn, 

632 "year": year, 

633 "external_ids": external_ids if external_ids else {}, 

634 "source": "Semantic Scholar", 

635 "_paper_id": paper_id if paper_id else "", 

636 "_search_strategy": strategy, 

637 "tldr": tldr_text, 

638 } 

639 

640 # Store the full paper object for later reference 

641 preview["_full_paper"] = paper 

642 

643 previews.append(preview) 

644 except Exception as e: 

645 safe_msg = self._scrub_error(e) 

646 logger.warning(f"Error processing paper preview: {safe_msg}") 

647 # Continue with the next paper 

648 

649 # Sort by year (newer first) if available 

650 def _year_key(p: dict[str, Any]) -> int: 

651 year = p.get("year") 

652 try: 

653 return int(year) if year is not None else 0 

654 except (TypeError, ValueError): 

655 return 0 

656 

657 previews = sorted(previews, key=_year_key, reverse=True) 

658 

659 logger.info( 

660 f"Found {len(previews)} Semantic Scholar previews using strategy: {strategy}" 

661 ) 

662 return previews 

663 

664 def _get_full_content( 

665 self, relevant_items: List[Dict[str, Any]] 

666 ) -> List[Dict[str, Any]]: 

667 """ 

668 Get full content for the relevant Semantic Scholar papers. 

669 Gets additional details like citations, references, and full metadata. 

670 

671 Args: 

672 relevant_items: List of relevant preview dictionaries 

673 

674 Returns: 

675 List of result dictionaries with full content 

676 """ 

677 # For Semantic Scholar, we already have most content from the preview 

678 # Additional API calls are only needed for citations/references 

679 

680 logger.info( 

681 f"Getting content for {len(relevant_items)} Semantic Scholar papers" 

682 ) 

683 

684 results = [] 

685 for item in relevant_items: 

686 result = item.copy() 

687 paper_id = item.get("_paper_id", "") 

688 

689 # Skip if no paper ID 

690 if not paper_id: 

691 results.append(result) 

692 continue 

693 

694 # Get paper details if citations or references are requested 

695 if self.get_citations or self.get_references or self.get_embeddings: 

696 paper_details = self._get_paper_details(paper_id) 

697 

698 if paper_details: 

699 # Add citation information 

700 if self.get_citations and "citations" in paper_details: 

701 result["citations"] = paper_details["citations"] 

702 

703 # Add reference information 

704 if self.get_references and "references" in paper_details: 

705 result["references"] = paper_details["references"] 

706 

707 # Add embedding if available 

708 if self.get_embeddings and "embedding" in paper_details: 

709 result["embedding"] = paper_details["embedding"] 

710 

711 # Add fields of study 

712 if "fieldsOfStudy" in paper_details: 

713 result["fields_of_study"] = paper_details[ 

714 "fieldsOfStudy" 

715 ] 

716 

717 # Promote useful fields from _full_paper to top level before 

718 # dropping the raw paper (consistent with NASA ADS/OpenAlex which 

719 # expose citations/journal_ref at the top level). 

720 full_paper = result.get("_full_paper") or {} 

721 if ( 721 ↛ 725line 721 didn't jump to line 725 because the condition on line 721 was never true

722 full_paper.get("citationCount") is not None 

723 and "citations" not in result 

724 ): 

725 result["citations"] = full_paper.get("citationCount") 

726 

727 # Remove temporary fields 

728 if "_paper_id" in result: 728 ↛ 730line 728 didn't jump to line 730 because the condition on line 728 was always true

729 del result["_paper_id"] 

730 if "_search_strategy" in result: 730 ↛ 732line 730 didn't jump to line 732 because the condition on line 730 was always true

731 del result["_search_strategy"] 

732 if "_full_paper" in result: 732 ↛ 735line 732 didn't jump to line 735 because the condition on line 732 was always true

733 del result["_full_paper"] 

734 

735 results.append(result) 

736 

737 return results