Update src/api/agent.py
Browse files- src/api/agent.py +89 -108
src/api/agent.py
CHANGED
|
@@ -1410,27 +1410,24 @@ async def _semantic_scholar_search(topic: str, limit: int = 8) -> List[dict]:
|
|
| 1410 |
|
| 1411 |
async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
|
| 1412 |
"""
|
| 1413 |
-
Search OpenAlex using
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
uses
|
| 1418 |
"""
|
| 1419 |
try:
|
| 1420 |
from urllib.parse import quote as _oa_quote
|
| 1421 |
|
| 1422 |
-
# Strip boolean operators
|
|
|
|
| 1423 |
clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
|
| 1424 |
-
clean = re.sub(
|
| 1425 |
-
r'\b(find|search|show|get|give|research on|articles on'
|
| 1426 |
-
r'|papers on|literature on|studies on|overview of|summary of|review of)\b',
|
| 1427 |
-
' ', clean, flags=re.IGNORECASE,
|
| 1428 |
-
)
|
| 1429 |
-
clean = re.sub(r'\s+', ' ', clean).strip()[:150]
|
| 1430 |
encoded = _oa_quote(clean, safe='')
|
| 1431 |
|
| 1432 |
select = "id,title,authorships,publication_year,primary_location,abstract_inverted_index,doi,cited_by_count,open_access"
|
| 1433 |
|
|
|
|
| 1434 |
url = (
|
| 1435 |
f"https://api.openalex.org/works"
|
| 1436 |
f"?search={encoded}"
|
|
@@ -2377,29 +2374,22 @@ def _highly_cited_note(topic_escaped: str) -> str:
|
|
| 2377 |
|
| 2378 |
async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
|
| 2379 |
"""
|
| 2380 |
-
Research snapshot
|
| 2381 |
-
|
| 2382 |
-
|
| 2383 |
-
- Runs PRIMO in parallel for KU-accessible results.
|
| 2384 |
-
- Reasoning block at the top, cited summary in the middle, AI tools at the end.
|
| 2385 |
"""
|
| 2386 |
import asyncio as _asyncio
|
| 2387 |
topic = context.topic
|
| 2388 |
references: List[dict] = []
|
| 2389 |
|
| 2390 |
-
#
|
| 2391 |
-
papers
|
| 2392 |
-
_openalex_search(context.ai_tool_query or context.topic, limit=8),
|
| 2393 |
-
_search_primo_catalog(context, limit=6),
|
| 2394 |
-
)
|
| 2395 |
|
| 2396 |
# Silent fallback to Semantic Scholar if OpenAlex returned nothing
|
| 2397 |
if not papers:
|
| 2398 |
logger.info(f"OpenAlex returned no papers for '{topic}' β trying Semantic Scholar")
|
| 2399 |
papers = await _semantic_scholar_search(topic, limit=8)
|
| 2400 |
|
| 2401 |
-
search_results = primo_result.get("results", []) or []
|
| 2402 |
-
|
| 2403 |
# ββ Top: reasoning block ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2404 |
q_display = context.ai_tool_query or f"Research on {topic}"
|
| 2405 |
answer = _search_trace_block(q_display, context)
|
|
@@ -2412,24 +2402,18 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
|
|
| 2412 |
|
| 2413 |
source_label = papers[0].get("_source", "OpenAlex")
|
| 2414 |
if source_label == "OpenAlex":
|
| 2415 |
-
|
| 2416 |
source_key = "openalex_badge"
|
| 2417 |
elif source_label == "Consensus":
|
| 2418 |
-
|
| 2419 |
source_key = "consensus_badge"
|
| 2420 |
else:
|
| 2421 |
-
|
| 2422 |
source_key = "semantic"
|
| 2423 |
|
| 2424 |
# ββ Middle: cited summary βββββββββββββββββββββββββββββββββββββββββββββ
|
| 2425 |
answer += f"<strong>π Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
|
| 2426 |
|
| 2427 |
-
if search_results:
|
| 2428 |
-
answer += (
|
| 2429 |
-
"<br><br><strong>KU Library results:</strong> "
|
| 2430 |
-
"KU-accessible items are shown below."
|
| 2431 |
-
)
|
| 2432 |
-
|
| 2433 |
answer += "<br><br><strong>Selected references:</strong><br>"
|
| 2434 |
for i, paper in enumerate(papers[:6], 1):
|
| 2435 |
answer += (
|
|
@@ -2440,31 +2424,18 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
|
|
| 2440 |
f"</span>"
|
| 2441 |
)
|
| 2442 |
|
| 2443 |
-
|
| 2444 |
-
answer += _source_badge(source_key, source_url)
|
| 2445 |
-
if search_results:
|
| 2446 |
-
answer += _source_badge("primo", primo_url)
|
| 2447 |
-
|
| 2448 |
-
# ββ Bottom: AI tools ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2449 |
answer += _ai_tools_footer(context)
|
| 2450 |
references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
|
| 2451 |
-
return answer,
|
| 2452 |
|
| 2453 |
-
#
|
| 2454 |
-
|
| 2455 |
-
|
| 2456 |
-
|
| 2457 |
-
|
| 2458 |
-
)
|
| 2459 |
-
else:
|
| 2460 |
-
answer += (
|
| 2461 |
-
f"<strong>π Research starting points: {_escape(topic)}</strong><br><br>"
|
| 2462 |
-
"I didn't find direct results right now β try the AI research tools below."
|
| 2463 |
-
)
|
| 2464 |
-
answer += _source_badge("primo", primo_url)
|
| 2465 |
answer += _ai_tools_footer(context)
|
| 2466 |
-
return answer,
|
| 2467 |
-
|
| 2468 |
|
| 2469 |
# ββ Follow-up System ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2470 |
|
|
@@ -2812,48 +2783,46 @@ async def _run_search_mode(
|
|
| 2812 |
) -> Tuple[str, List[dict], List[dict], str]:
|
| 2813 |
context = await _prepare_queries(question, context, model, is_follow_up)
|
| 2814 |
|
| 2815 |
-
#
|
|
|
|
| 2816 |
# Skip OpenAlex for books-only searches β it only indexes journal articles.
|
| 2817 |
use_openalex = context.resource_type != "books"
|
| 2818 |
|
| 2819 |
import asyncio as _aio
|
| 2820 |
if context.source == "pubmed":
|
| 2821 |
if use_openalex:
|
| 2822 |
-
(result,
|
| 2823 |
_search_pubmed_articles(context, limit=6),
|
| 2824 |
_openalex_search(context.ai_tool_query or context.topic, limit=5),
|
| 2825 |
)
|
| 2826 |
else:
|
| 2827 |
-
result,
|
| 2828 |
oa_papers = []
|
| 2829 |
source_name = "pubmed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2830 |
else:
|
|
|
|
| 2831 |
if use_openalex:
|
| 2832 |
-
|
| 2833 |
-
_search_primo_catalog(context, limit=6),
|
| 2834 |
-
_openalex_search(context.ai_tool_query or context.topic, limit=5),
|
| 2835 |
-
)
|
| 2836 |
else:
|
| 2837 |
-
result, source_url = await _search_primo_catalog(context, limit=6)
|
| 2838 |
oa_papers = []
|
| 2839 |
-
source_name = "
|
| 2840 |
-
|
| 2841 |
-
|
| 2842 |
-
|
| 2843 |
-
|
| 2844 |
-
# Tag PRIMO/PubMed results with source
|
| 2845 |
-
for r in search_results:
|
| 2846 |
-
if "_source" not in r:
|
| 2847 |
-
r["_source"] = "PubMed" if context.source == "pubmed" else "PRIMO"
|
| 2848 |
|
| 2849 |
-
# Tag OpenAlex results
|
| 2850 |
for paper in oa_papers:
|
| 2851 |
paper["_source"] = "OpenAlex"
|
| 2852 |
paper["description"] = paper.get("abstract", "")[:300]
|
| 2853 |
paper["creator"] = paper.get("authors", "")
|
| 2854 |
paper["date"] = paper.get("year", "")
|
| 2855 |
paper["type"] = "Article"
|
| 2856 |
-
# PRIMO search URL for "Read more" β searches by title in PRIMO
|
| 2857 |
from urllib.parse import quote as _pq
|
| 2858 |
primo_title_url = (
|
| 2859 |
"https://khalifa.primo.exlibrisgroup.com/discovery/search"
|
|
@@ -2866,42 +2835,47 @@ async def _run_search_mode(
|
|
| 2866 |
# ββ Top: reasoning / how-I-searched block ββββββββββββββββββββββββββββββββ
|
| 2867 |
answer = _search_trace_block(question, context)
|
| 2868 |
|
| 2869 |
-
# ββ Middle: answer sentence
|
| 2870 |
answer += _search_answer_intro(context, is_follow_up)
|
| 2871 |
|
| 2872 |
filters = _filters_summary(context)
|
| 2873 |
if filters:
|
| 2874 |
answer += f"<br><br><strong>Active filters:</strong> {_escape(filters)}."
|
| 2875 |
|
| 2876 |
-
if
|
| 2877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2878 |
else:
|
| 2879 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2880 |
|
| 2881 |
if context.intent == "search_medical" and context.source != "pubmed":
|
| 2882 |
answer += "<br><br>For the strongest medical evidence trail, also check <strong>PubMed</strong> or <strong>Embase</strong>."
|
| 2883 |
|
| 2884 |
-
answer += _source_badge(source_name, source_url)
|
| 2885 |
-
|
| 2886 |
-
if oa_papers:
|
| 2887 |
-
answer += _source_badge("openalex_badge", "https://openalex.org")
|
| 2888 |
-
|
| 2889 |
# ββ Bottom: AI tools recommendation panel βββββββββββββββββββββββββββββββββ
|
| 2890 |
answer += _ai_tools_footer(context)
|
| 2891 |
|
| 2892 |
-
#
|
| 2893 |
-
combined_results = search_results + oa_papers[:
|
| 2894 |
|
| 2895 |
sources = []
|
| 2896 |
for item in search_results[:6]:
|
| 2897 |
link = item.get("primo_url") or item.get("link")
|
| 2898 |
if link:
|
| 2899 |
sources.append({"title": item.get("title", "Result"), "url": link})
|
| 2900 |
-
for paper in oa_papers[:
|
| 2901 |
if paper.get("link"):
|
| 2902 |
sources.append({"title": paper["title"], "url": paper["link"]})
|
| 2903 |
-
return answer, combined_results, sources, source_url
|
| 2904 |
|
|
|
|
| 2905 |
|
| 2906 |
def _sort_and_trim_contexts(contexts: List[SearchContextPayload]) -> List[SearchContextPayload]:
|
| 2907 |
dedup: Dict[str, SearchContextPayload] = {}
|
|
@@ -3508,40 +3482,47 @@ async def _agent_query_inner(
|
|
| 3508 |
_prepare_queries(question, _ctx_articles, model, False),
|
| 3509 |
_prepare_queries(question, _ctx_books, model, False),
|
| 3510 |
)
|
| 3511 |
-
|
| 3512 |
-
|
| 3513 |
-
|
| 3514 |
-
|
| 3515 |
-
|
| 3516 |
-
|
| 3517 |
-
|
| 3518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3519 |
|
| 3520 |
_dual_answer = _search_trace_block(question, _ctx_articles)
|
| 3521 |
_dual_answer += (
|
| 3522 |
-
f"<strong>π Articles on {_escape(topic)}</strong>
|
| 3523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3524 |
)
|
| 3525 |
-
if not _results_a:
|
| 3526 |
-
_dual_answer += "<em>No article results found β try broadening your search.</em><br>"
|
| 3527 |
-
_dual_answer += _source_badge("primo", _src_url_a)
|
| 3528 |
-
|
| 3529 |
-
_dual_answer += f"<br><br><strong>π Books on {_escape(topic)}</strong> "
|
| 3530 |
-
_dual_answer += f"β {_total_b:,} result(s) found, top {len(_results_b)} shown below.<br>"
|
| 3531 |
-
if not _results_b:
|
| 3532 |
-
_dual_answer += "<em>No book results found β try the KU Library catalogue directly.</em><br>"
|
| 3533 |
_dual_answer += _source_badge("primo", _src_url_b)
|
| 3534 |
_dual_answer += _ai_tools_footer(_ctx_articles)
|
| 3535 |
|
| 3536 |
-
_combined = _results_a
|
| 3537 |
follow_up_question, follow_up_suggestions = await _search_follow_up(_ctx_articles, model)
|
| 3538 |
return AgentResponse(
|
| 3539 |
answer=_dual_answer,
|
| 3540 |
intent=intent,
|
| 3541 |
-
tools_used=["
|
| 3542 |
search_results=_combined[:8],
|
| 3543 |
-
sources=[{"title": r.get("title", "Result"), "url": r.get("
|
| 3544 |
-
for r in _combined[:6] if r.get("
|
| 3545 |
model_used=model,
|
| 3546 |
response_time=time.time() - start_time,
|
| 3547 |
corrected_query=topic,
|
|
|
|
| 1410 |
|
| 1411 |
async def _openalex_search(topic: str, limit: int = 8) -> List[dict]:
|
| 1412 |
"""
|
| 1413 |
+
Search OpenAlex using semantic search β passes the corrected natural language
|
| 1414 |
+
query directly, matching the search.semantic format the user sees in the
|
| 1415 |
+
OpenAlex browser link:
|
| 1416 |
+
https://openalex.org/works?search.semantic=QUERY&sort=relevance_score:desc
|
| 1417 |
+
The API equivalent uses ?search=QUERY which triggers the same semantic ranking.
|
| 1418 |
"""
|
| 1419 |
try:
|
| 1420 |
from urllib.parse import quote as _oa_quote
|
| 1421 |
|
| 1422 |
+
# Strip only boolean operators β keep the full natural language query intact
|
| 1423 |
+
# including proper nouns, locations, and all keywords
|
| 1424 |
clean = re.sub(r'\b(AND|OR|NOT)\b|[()"\']', ' ', topic, flags=re.IGNORECASE)
|
| 1425 |
+
clean = re.sub(r'\s+', ' ', clean).strip()[:200]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1426 |
encoded = _oa_quote(clean, safe='')
|
| 1427 |
|
| 1428 |
select = "id,title,authorships,publication_year,primary_location,abstract_inverted_index,doi,cited_by_count,open_access"
|
| 1429 |
|
| 1430 |
+
# Use ?search= β API equivalent of search.semantic in the browser link
|
| 1431 |
url = (
|
| 1432 |
f"https://api.openalex.org/works"
|
| 1433 |
f"?search={encoded}"
|
|
|
|
| 2374 |
|
| 2375 |
async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
|
| 2376 |
"""
|
| 2377 |
+
Research snapshot using OpenAlex semantic search only.
|
| 2378 |
+
PRIMO API removed β PRIMO boolean is used only for the browser link shown to users.
|
| 2379 |
+
Falls back to Semantic Scholar if OpenAlex returns nothing.
|
|
|
|
|
|
|
| 2380 |
"""
|
| 2381 |
import asyncio as _asyncio
|
| 2382 |
topic = context.topic
|
| 2383 |
references: List[dict] = []
|
| 2384 |
|
| 2385 |
+
# OpenAlex semantic search β passes corrected natural language query
|
| 2386 |
+
papers = await _openalex_search(context.ai_tool_query or context.topic, limit=8)
|
|
|
|
|
|
|
|
|
|
| 2387 |
|
| 2388 |
# Silent fallback to Semantic Scholar if OpenAlex returned nothing
|
| 2389 |
if not papers:
|
| 2390 |
logger.info(f"OpenAlex returned no papers for '{topic}' β trying Semantic Scholar")
|
| 2391 |
papers = await _semantic_scholar_search(topic, limit=8)
|
| 2392 |
|
|
|
|
|
|
|
| 2393 |
# ββ Top: reasoning block ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2394 |
q_display = context.ai_tool_query or f"Research on {topic}"
|
| 2395 |
answer = _search_trace_block(q_display, context)
|
|
|
|
| 2402 |
|
| 2403 |
source_label = papers[0].get("_source", "OpenAlex")
|
| 2404 |
if source_label == "OpenAlex":
|
| 2405 |
+
oa_source_url = "https://openalex.org"
|
| 2406 |
source_key = "openalex_badge"
|
| 2407 |
elif source_label == "Consensus":
|
| 2408 |
+
oa_source_url = "https://consensus.app"
|
| 2409 |
source_key = "consensus_badge"
|
| 2410 |
else:
|
| 2411 |
+
oa_source_url = "https://www.semanticscholar.org/"
|
| 2412 |
source_key = "semantic"
|
| 2413 |
|
| 2414 |
# ββ Middle: cited summary βββββββββββββββββββββββββββββββββββββββββββββ
|
| 2415 |
answer += f"<strong>π Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
|
| 2416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2417 |
answer += "<br><br><strong>Selected references:</strong><br>"
|
| 2418 |
for i, paper in enumerate(papers[:6], 1):
|
| 2419 |
answer += (
|
|
|
|
| 2424 |
f"</span>"
|
| 2425 |
)
|
| 2426 |
|
| 2427 |
+
answer += _source_badge(source_key, oa_source_url)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2428 |
answer += _ai_tools_footer(context)
|
| 2429 |
references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
|
| 2430 |
+
return answer, [], references
|
| 2431 |
|
| 2432 |
+
# OpenAlex and Semantic Scholar both returned nothing
|
| 2433 |
+
answer += (
|
| 2434 |
+
f"<strong>π Research starting points: {_escape(topic)}</strong><br><br>"
|
| 2435 |
+
"I didn't find direct results right now β try the AI research tools below."
|
| 2436 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2437 |
answer += _ai_tools_footer(context)
|
| 2438 |
+
return answer, [], []
|
|
|
|
| 2439 |
|
| 2440 |
# ββ Follow-up System ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2441 |
|
|
|
|
| 2783 |
) -> Tuple[str, List[dict], List[dict], str]:
|
| 2784 |
context = await _prepare_queries(question, context, model, is_follow_up)
|
| 2785 |
|
| 2786 |
+
# OpenAlex semantic search only β PRIMO API removed.
|
| 2787 |
+
# PRIMO boolean is preserved for the browser link shown to users.
|
| 2788 |
# Skip OpenAlex for books-only searches β it only indexes journal articles.
|
| 2789 |
use_openalex = context.resource_type != "books"
|
| 2790 |
|
| 2791 |
import asyncio as _aio
|
| 2792 |
if context.source == "pubmed":
|
| 2793 |
if use_openalex:
|
| 2794 |
+
(result, pub_source_url), oa_papers = await _aio.gather(
|
| 2795 |
_search_pubmed_articles(context, limit=6),
|
| 2796 |
_openalex_search(context.ai_tool_query or context.topic, limit=5),
|
| 2797 |
)
|
| 2798 |
else:
|
| 2799 |
+
result, pub_source_url = await _search_pubmed_articles(context, limit=6)
|
| 2800 |
oa_papers = []
|
| 2801 |
source_name = "pubmed"
|
| 2802 |
+
source_url = pub_source_url
|
| 2803 |
+
search_results = result.get("results", []) or []
|
| 2804 |
+
total = result.get("total", len(search_results))
|
| 2805 |
+
for r in search_results:
|
| 2806 |
+
if "_source" not in r:
|
| 2807 |
+
r["_source"] = "PubMed"
|
| 2808 |
else:
|
| 2809 |
+
# PRIMO API removed β use OpenAlex only for article results
|
| 2810 |
if use_openalex:
|
| 2811 |
+
oa_papers = await _openalex_search(context.ai_tool_query or context.topic, limit=8)
|
|
|
|
|
|
|
|
|
|
| 2812 |
else:
|
|
|
|
| 2813 |
oa_papers = []
|
| 2814 |
+
source_name = "openalex"
|
| 2815 |
+
source_url = _primo_clean_url(context) # PRIMO link still shown to user
|
| 2816 |
+
search_results = []
|
| 2817 |
+
total = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2818 |
|
| 2819 |
+
# Tag OpenAlex results
|
| 2820 |
for paper in oa_papers:
|
| 2821 |
paper["_source"] = "OpenAlex"
|
| 2822 |
paper["description"] = paper.get("abstract", "")[:300]
|
| 2823 |
paper["creator"] = paper.get("authors", "")
|
| 2824 |
paper["date"] = paper.get("year", "")
|
| 2825 |
paper["type"] = "Article"
|
|
|
|
| 2826 |
from urllib.parse import quote as _pq
|
| 2827 |
primo_title_url = (
|
| 2828 |
"https://khalifa.primo.exlibrisgroup.com/discovery/search"
|
|
|
|
| 2835 |
# ββ Top: reasoning / how-I-searched block ββββββββββββββββββββββββββββββββ
|
| 2836 |
answer = _search_trace_block(question, context)
|
| 2837 |
|
| 2838 |
+
# ββ Middle: answer sentence ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2839 |
answer += _search_answer_intro(context, is_follow_up)
|
| 2840 |
|
| 2841 |
filters = _filters_summary(context)
|
| 2842 |
if filters:
|
| 2843 |
answer += f"<br><br><strong>Active filters:</strong> {_escape(filters)}."
|
| 2844 |
|
| 2845 |
+
if context.source == "pubmed":
|
| 2846 |
+
if search_results:
|
| 2847 |
+
answer += f"<br><br>Found <strong>{total:,}</strong> result(s) β top {len(search_results)} shown below."
|
| 2848 |
+
else:
|
| 2849 |
+
answer += "<br><br>No PubMed results found β try the AI tools below."
|
| 2850 |
+
answer += _source_badge(source_name, source_url)
|
| 2851 |
+
if oa_papers:
|
| 2852 |
+
answer += _source_badge("openalex_badge", "https://openalex.org")
|
| 2853 |
else:
|
| 2854 |
+
if oa_papers:
|
| 2855 |
+
answer += f"<br><br>Found <strong>{len(oa_papers)}</strong> open access result(s) via OpenAlex."
|
| 2856 |
+
else:
|
| 2857 |
+
answer += "<br><br>No results found right now β try the AI tools or the PRIMO link below."
|
| 2858 |
+
answer += _source_badge("openalex_badge", "https://openalex.org")
|
| 2859 |
|
| 2860 |
if context.intent == "search_medical" and context.source != "pubmed":
|
| 2861 |
answer += "<br><br>For the strongest medical evidence trail, also check <strong>PubMed</strong> or <strong>Embase</strong>."
|
| 2862 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2863 |
# ββ Bottom: AI tools recommendation panel βββββββββββββββββββββββββββββββββ
|
| 2864 |
answer += _ai_tools_footer(context)
|
| 2865 |
|
| 2866 |
+
# Combined results: PubMed (if medical) + OpenAlex
|
| 2867 |
+
combined_results = search_results + oa_papers[:8]
|
| 2868 |
|
| 2869 |
sources = []
|
| 2870 |
for item in search_results[:6]:
|
| 2871 |
link = item.get("primo_url") or item.get("link")
|
| 2872 |
if link:
|
| 2873 |
sources.append({"title": item.get("title", "Result"), "url": link})
|
| 2874 |
+
for paper in oa_papers[:6]:
|
| 2875 |
if paper.get("link"):
|
| 2876 |
sources.append({"title": paper["title"], "url": paper["link"]})
|
|
|
|
| 2877 |
|
| 2878 |
+
return answer, combined_results, sources, source_url
|
| 2879 |
|
| 2880 |
def _sort_and_trim_contexts(contexts: List[SearchContextPayload]) -> List[SearchContextPayload]:
|
| 2881 |
dedup: Dict[str, SearchContextPayload] = {}
|
|
|
|
| 3482 |
_prepare_queries(question, _ctx_articles, model, False),
|
| 3483 |
_prepare_queries(question, _ctx_books, model, False),
|
| 3484 |
)
|
| 3485 |
+
# PRIMO API removed β use OpenAlex for articles, PRIMO link for books
|
| 3486 |
+
_ctx_articles_prepared = await _prepare_queries(question, _ctx_articles, model, False)
|
| 3487 |
+
_oa_articles = await _openalex_search(_ctx_articles_prepared.ai_tool_query or topic, limit=6)
|
| 3488 |
+
for _p in _oa_articles:
|
| 3489 |
+
_p["_source"] = "OpenAlex"
|
| 3490 |
+
_p["description"] = _p.get("abstract", "")[:300]
|
| 3491 |
+
_p["creator"] = _p.get("authors", "")
|
| 3492 |
+
_p["date"] = _p.get("year", "")
|
| 3493 |
+
_p["type"] = "Article"
|
| 3494 |
+
_results_a = _oa_articles
|
| 3495 |
+
_results_b = [] # books: direct user to PRIMO link
|
| 3496 |
+
_src_url_a = _primo_clean_url(_ctx_articles)
|
| 3497 |
+
_src_url_b = _primo_clean_url(_ctx_books)
|
| 3498 |
|
| 3499 |
_dual_answer = _search_trace_block(question, _ctx_articles)
|
| 3500 |
_dual_answer += (
|
| 3501 |
+
f"<strong>π Articles on {_escape(topic)}</strong>"
|
| 3502 |
+
)
|
| 3503 |
+
if _results_a:
|
| 3504 |
+
_dual_answer += f" β {len(_results_a)} open access result(s) shown below.<br>"
|
| 3505 |
+
else:
|
| 3506 |
+
_dual_answer += "<br><em>No article results found β try the PRIMO link below.</em><br>"
|
| 3507 |
+
_dual_answer += _source_badge("openalex_badge", "https://openalex.org")
|
| 3508 |
+
|
| 3509 |
+
_dual_answer += f"<br><br><strong>π Books on {_escape(topic)}</strong><br>"
|
| 3510 |
+
_dual_answer += (
|
| 3511 |
+
"For books, search the KU Library catalogue directly: "
|
| 3512 |
+
f'<a href="{_src_url_b}" target="_blank" style="color:#003366;font-weight:700">Search PRIMO for books β</a><br>'
|
| 3513 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3514 |
_dual_answer += _source_badge("primo", _src_url_b)
|
| 3515 |
_dual_answer += _ai_tools_footer(_ctx_articles)
|
| 3516 |
|
| 3517 |
+
_combined = _results_a
|
| 3518 |
follow_up_question, follow_up_suggestions = await _search_follow_up(_ctx_articles, model)
|
| 3519 |
return AgentResponse(
|
| 3520 |
answer=_dual_answer,
|
| 3521 |
intent=intent,
|
| 3522 |
+
tools_used=["openalex_articles", "primo_books_link"],
|
| 3523 |
search_results=_combined[:8],
|
| 3524 |
+
sources=[{"title": r.get("title", "Result"), "url": r.get("link", "")}
|
| 3525 |
+
for r in _combined[:6] if r.get("link")],
|
| 3526 |
model_used=model,
|
| 3527 |
response_time=time.time() - start_time,
|
| 3528 |
corrected_query=topic,
|