Update src/api/agent.py
Browse files- 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
|
| 1403 |
-
|
| 1404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1405 |
"""
|
| 1406 |
try:
|
| 1407 |
from urllib.parse import quote
|
| 1408 |
|
| 1409 |
-
# Strip boolean operators
|
|
|
|
| 1410 |
clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
|
| 1411 |
-
clean = re.sub(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"?
|
|
|
|
| 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
|
| 1433 |
-
|
| 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,
|
| 1604 |
-
boolean_query=boolean,
|
| 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:
|
| 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:
|
| 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:
|
| 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)
|