nikeshn commited on
Commit
cb99cc3
Β·
verified Β·
1 Parent(s): e8838b3

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. src/api/agent.py +26 -45
src/api/agent.py CHANGED
@@ -1399,27 +1399,35 @@ async def _semantic_scholar_search(topic: str, limit: int = 8) -> List[dict]:
1399
 
1400
  async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
1401
  """
1402
- Search OpenAlex using title.search + abstract.search filters for precision.
1403
- Targets only title and abstract fields β€” more relevant than general search.
1404
- Free, no API key, typically < 2s.
 
 
 
 
1405
  """
1406
  try:
1407
  from urllib.parse import quote
1408
 
1409
- # Strip boolean operators β€” OpenAlex filter search uses its own relevance engine
 
1410
  clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
1411
- clean = re.sub(r'\s+', ' ', clean).strip()[:200]
 
 
 
 
 
1412
  encoded = quote(clean, safe='')
1413
 
1414
- # Build URL manually β€” urlencode encodes | and : which breaks OpenAlex filter syntax
1415
- filter_str = (
1416
- f"title.search:{encoded}|abstract.search:{encoded},"
1417
- f"has_abstract:true,type:article,is_paratext:false"
1418
- )
1419
  select = "id,title,authorships,publication_year,primary_location,abstract_inverted_index,doi,cited_by_count,open_access"
 
 
1420
  url = (
1421
  f"https://api.openalex.org/works"
1422
- f"?filter={filter_str}"
 
1423
  f"&sort=relevance_score:desc"
1424
  f"&per_page={limit}"
1425
  f"&select={select}"
@@ -1429,38 +1437,11 @@ async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
1429
  async with httpx.AsyncClient(timeout=10) as client:
1430
  r = await client.get(url, headers=headers)
1431
  if r.status_code != 200:
1432
- logger.warning(f"OpenAlex title+abstract search {r.status_code} β€” retrying with general search")
1433
- # Fallback: general search
1434
- url_fb = (
1435
- f"https://api.openalex.org/works"
1436
- f"?search={encoded}"
1437
- f"&filter=has_abstract:true,type:article,is_paratext:false"
1438
- f"&sort=relevance_score:desc"
1439
- f"&per_page={limit}"
1440
- f"&select={select}"
1441
- )
1442
- r = await client.get(url_fb, headers=headers)
1443
- if r.status_code != 200:
1444
- logger.warning(f"OpenAlex fallback also failed: {r.status_code}")
1445
- return []
1446
 
1447
  works = r.json().get("results", [])
1448
 
1449
- # If title+abstract returns nothing, retry with general search
1450
- if not works:
1451
- logger.info("OpenAlex title+abstract returned 0 β€” retrying with general search")
1452
- url_fb = (
1453
- f"https://api.openalex.org/works"
1454
- f"?search={encoded}"
1455
- f"&filter=has_abstract:true,type:article,is_paratext:false"
1456
- f"&sort=relevance_score:desc"
1457
- f"&per_page={limit}"
1458
- f"&select={select}"
1459
- )
1460
- r2 = await client.get(url_fb, headers=headers)
1461
- if r2.status_code == 200:
1462
- works = r2.json().get("results", [])
1463
-
1464
  results = []
1465
  for work in works:
1466
  title = work.get("title") or "Untitled"
@@ -1600,8 +1581,8 @@ async def _search_primo_catalog(context: SearchContextPayload, limit: int = 5) -
1600
  boolean = clean_topic or context.topic
1601
  result = await search_primo(
1602
  SearchRequest(
1603
- query=context.topic, # plain topic as q= fallback
1604
- boolean_query=boolean, # FIX: boolean goes here so _boolean_to_primo_params() fires
1605
  source="primo",
1606
  limit=limit,
1607
  peer_reviewed=context.peer_reviewed,
@@ -2401,7 +2382,7 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
2401
 
2402
  # Parallel fetch: OpenAlex (fast, free) + PRIMO
2403
  papers, (primo_result, primo_url) = await _asyncio.gather(
2404
- _openalex_search(context.ai_tool_query or context.topic, limit=8), # FIX: use richer natural query
2405
  _search_primo_catalog(context, limit=6),
2406
  )
2407
 
@@ -2833,7 +2814,7 @@ async def _run_search_mode(
2833
  if use_openalex:
2834
  (result, source_url), oa_papers = await _aio.gather(
2835
  _search_pubmed_articles(context, limit=6),
2836
- _openalex_search(context.ai_tool_query or context.topic, limit=5), # FIX: use richer natural query
2837
  )
2838
  else:
2839
  result, source_url = await _search_pubmed_articles(context, limit=6)
@@ -2843,7 +2824,7 @@ async def _run_search_mode(
2843
  if use_openalex:
2844
  (result, source_url), oa_papers = await _aio.gather(
2845
  _search_primo_catalog(context, limit=6),
2846
- _openalex_search(context.ai_tool_query or context.topic, limit=5), # FIX: use richer natural query
2847
  )
2848
  else:
2849
  result, source_url = await _search_primo_catalog(context, limit=6)
 
1399
 
1400
  async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
1401
  """
1402
+ Search OpenAlex using the ?search= relevance engine β€” same approach as the
1403
+ clickable OpenAlex link shown to users, which consistently returns relevant results.
1404
+
1405
+ Previous approach used ?filter=title.search:X|abstract.search:X β€” a strict
1406
+ field-level filter requiring exact token matches in title/abstract only, which
1407
+ returned irrelevant or zero results. The ?search= parameter uses OpenAlex's
1408
+ full relevance engine across all fields, identical to the browser link.
1409
  """
1410
  try:
1411
  from urllib.parse import quote
1412
 
1413
+ # Strip boolean operators and retrieval filler β€” keep only keywords.
1414
+ # Matches what the user-facing OpenAlex link does.
1415
  clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
1416
+ clean = re.sub(
1417
+ r'\b(find|search|show|get|give|recent|research on|articles? on'
1418
+ r'|papers? on|literature on|studies on|overview of|summary of|review of)\b',
1419
+ ' ', clean, flags=re.IGNORECASE,
1420
+ )
1421
+ clean = re.sub(r'\s+', ' ', clean).strip()[:150]
1422
  encoded = quote(clean, safe='')
1423
 
 
 
 
 
 
1424
  select = "id,title,authorships,publication_year,primary_location,abstract_inverted_index,doi,cited_by_count,open_access"
1425
+
1426
+ # FIX: use ?search= β€” same as the clickable OpenAlex link
1427
  url = (
1428
  f"https://api.openalex.org/works"
1429
+ f"?search={encoded}"
1430
+ f"&filter=has_abstract:true,type:article,is_paratext:false"
1431
  f"&sort=relevance_score:desc"
1432
  f"&per_page={limit}"
1433
  f"&select={select}"
 
1437
  async with httpx.AsyncClient(timeout=10) as client:
1438
  r = await client.get(url, headers=headers)
1439
  if r.status_code != 200:
1440
+ logger.warning(f"OpenAlex search failed: {r.status_code}")
1441
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
1442
 
1443
  works = r.json().get("results", [])
1444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1445
  results = []
1446
  for work in works:
1447
  title = work.get("title") or "Untitled"
 
1581
  boolean = clean_topic or context.topic
1582
  result = await search_primo(
1583
  SearchRequest(
1584
+ query=context.topic, # plain topic as q= fallback
1585
+ boolean_query=boolean, # FIX: boolean goes here so _boolean_to_primo_params() fires
1586
  source="primo",
1587
  limit=limit,
1588
  peer_reviewed=context.peer_reviewed,
 
2382
 
2383
  # Parallel fetch: OpenAlex (fast, free) + PRIMO
2384
  papers, (primo_result, primo_url) = await _asyncio.gather(
2385
+ _openalex_search(context.ai_tool_query or context.topic, limit=8), # FIX: richer natural query
2386
  _search_primo_catalog(context, limit=6),
2387
  )
2388
 
 
2814
  if use_openalex:
2815
  (result, source_url), oa_papers = await _aio.gather(
2816
  _search_pubmed_articles(context, limit=6),
2817
+ _openalex_search(context.ai_tool_query or context.topic, limit=5), # FIX: richer natural query
2818
  )
2819
  else:
2820
  result, source_url = await _search_pubmed_articles(context, limit=6)
 
2824
  if use_openalex:
2825
  (result, source_url), oa_papers = await _aio.gather(
2826
  _search_primo_catalog(context, limit=6),
2827
+ _openalex_search(context.ai_tool_query or context.topic, limit=5), # FIX: richer natural query
2828
  )
2829
  else:
2830
  result, source_url = await _search_primo_catalog(context, limit=6)