Update src/api/agent.py
Browse files- src/api/agent.py +45 -39
src/api/agent.py
CHANGED
|
@@ -1400,57 +1400,64 @@ async def _semantic_scholar_search(topic: str, limit: int = 8) -> List[dict]:
|
|
| 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 |
-
|
| 1404 |
-
general `search` parameter which also hits author names, journals, institutions.
|
| 1405 |
Free, no API key, typically < 2s.
|
| 1406 |
"""
|
| 1407 |
try:
|
| 1408 |
-
from urllib.parse import
|
| 1409 |
|
| 1410 |
-
#
|
| 1411 |
clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
|
| 1412 |
clean = re.sub(r'\s+', ' ', clean).strip()[:200]
|
|
|
|
| 1413 |
|
| 1414 |
-
#
|
| 1415 |
-
|
| 1416 |
-
"
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
"
|
| 1422 |
-
"
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
}
|
| 1428 |
-
url = "https://api.openalex.org/works?" + urlencode(params)
|
| 1429 |
headers = {"User-Agent": "LibBee/3.2 (mailto:libse@ku.ac.ae)"}
|
| 1430 |
|
| 1431 |
async with httpx.AsyncClient(timeout=10) as client:
|
| 1432 |
r = await client.get(url, headers=headers)
|
| 1433 |
if r.status_code != 200:
|
| 1434 |
-
logger.warning(f"OpenAlex
|
| 1435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1436 |
|
| 1437 |
-
|
| 1438 |
-
works = data.get("results", [])
|
| 1439 |
|
| 1440 |
-
# If title+abstract
|
| 1441 |
if not works:
|
| 1442 |
-
logger.info(
|
| 1443 |
-
|
| 1444 |
-
"
|
| 1445 |
-
"
|
| 1446 |
-
"
|
| 1447 |
-
"
|
| 1448 |
-
"
|
| 1449 |
-
|
| 1450 |
-
r2 = await client.get(
|
| 1451 |
-
"https://api.openalex.org/works?" + urlencode(params_fallback),
|
| 1452 |
-
headers=headers,
|
| 1453 |
)
|
|
|
|
| 1454 |
if r2.status_code == 200:
|
| 1455 |
works = r2.json().get("results", [])
|
| 1456 |
|
|
@@ -1472,7 +1479,6 @@ async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
|
|
| 1472 |
if not abstract:
|
| 1473 |
continue
|
| 1474 |
|
| 1475 |
-
# Authors
|
| 1476 |
authorships = work.get("authorships") or []
|
| 1477 |
author_names = [
|
| 1478 |
a.get("author", {}).get("display_name", "")
|
|
@@ -1493,9 +1499,9 @@ async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
|
|
| 1493 |
or work.get("id", "https://openalex.org")
|
| 1494 |
)
|
| 1495 |
venue = (
|
| 1496 |
-
(work.get("primary_location") or {})
|
| 1497 |
-
.get("
|
| 1498 |
-
)
|
| 1499 |
|
| 1500 |
results.append({
|
| 1501 |
"title": title,
|
|
|
|
| 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}"
|
| 1426 |
+
)
|
|
|
|
|
|
|
| 1427 |
headers = {"User-Agent": "LibBee/3.2 (mailto:libse@ku.ac.ae)"}
|
| 1428 |
|
| 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 |
|
|
|
|
| 1479 |
if not abstract:
|
| 1480 |
continue
|
| 1481 |
|
|
|
|
| 1482 |
authorships = work.get("authorships") or []
|
| 1483 |
author_names = [
|
| 1484 |
a.get("author", {}).get("display_name", "")
|
|
|
|
| 1499 |
or work.get("id", "https://openalex.org")
|
| 1500 |
)
|
| 1501 |
venue = (
|
| 1502 |
+
((work.get("primary_location") or {}).get("source") or {})
|
| 1503 |
+
.get("display_name") or ""
|
| 1504 |
+
)
|
| 1505 |
|
| 1506 |
results.append({
|
| 1507 |
"title": title,
|