nikeshn commited on
Commit
31edef1
Β·
verified Β·
1 Parent(s): d949433

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. 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 the ?search= relevance engine.
1414
- This matches the clickable OpenAlex link shown to users.
1415
- The previous ?filter=title.search:X|abstract.search:X approach was a strict
1416
- field-level filter that returned irrelevant results. The ?search= parameter
1417
- uses OpenAlex full relevance ranking across all fields.
1418
  """
1419
  try:
1420
  from urllib.parse import quote as _oa_quote
1421
 
1422
- # Strip boolean operators and common retrieval filler words.
 
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
- - Uses Consensus MCP for peer-reviewed papers with abstracts (primary).
2382
- - Falls back to Semantic Scholar silently if Consensus returns nothing.
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
- # Parallel fetch: OpenAlex (fast, free) + PRIMO
2391
- papers, (primo_result, primo_url) = await _asyncio.gather(
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
- source_url = "https://openalex.org"
2416
  source_key = "openalex_badge"
2417
  elif source_label == "Consensus":
2418
- source_url = "https://consensus.app"
2419
  source_key = "consensus_badge"
2420
  else:
2421
- source_url = "https://www.semanticscholar.org/"
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
- # Source badge β€” show Consensus or Semantic Scholar depending on which fired
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, search_results, references
2452
 
2453
- # Both Consensus and Semantic Scholar returned nothing β€” PRIMO-only fallback
2454
- if search_results:
2455
- answer += (
2456
- f"<strong>πŸ“š Research starting points: {_escape(topic)}</strong><br><br>"
2457
- "Here are KU-accessible results you can open immediately."
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, search_results, []
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
- # Run PRIMO (or PubMed) and OpenAlex in parallel.
 
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, source_url), oa_papers = await _aio.gather(
2823
  _search_pubmed_articles(context, limit=6),
2824
  _openalex_search(context.ai_tool_query or context.topic, limit=5),
2825
  )
2826
  else:
2827
- result, source_url = await _search_pubmed_articles(context, limit=6)
2828
  oa_papers = []
2829
  source_name = "pubmed"
 
 
 
 
 
 
2830
  else:
 
2831
  if use_openalex:
2832
- (result, source_url), oa_papers = await _aio.gather(
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 = "primo"
2840
-
2841
- search_results = result.get("results", []) or []
2842
- total = result.get("total", len(search_results))
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 and add abstract as description
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 + result count ────────────────────────────────
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 search_results:
2877
- answer += f"<br><br>Found <strong>{total:,}</strong> result(s) β€” top {len(search_results)} shown below."
 
 
 
 
 
 
2878
  else:
2879
- answer += "<br><br>No matching results found right now β€” try broadening your terms or use the AI tools below."
 
 
 
 
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
- # Merge results: PRIMO first, then OpenAlex β€” frontend splits by _source
2893
- combined_results = search_results + oa_papers[:5]
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[:3]:
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
- (_res_a, _src_url_a), (_res_b, _src_url_b) = await _aio.gather(
3512
- _search_primo_catalog(_ctx_articles, limit=4),
3513
- _search_primo_catalog(_ctx_books, limit=3),
3514
- )
3515
- _results_a = _res_a.get("results", [])
3516
- _results_b = _res_b.get("results", [])
3517
- _total_a = _res_a.get("total", len(_results_a))
3518
- _total_b = _res_b.get("total", len(_results_b))
 
 
 
 
 
3519
 
3520
  _dual_answer = _search_trace_block(question, _ctx_articles)
3521
  _dual_answer += (
3522
- f"<strong>πŸ“„ Articles on {_escape(topic)}</strong> "
3523
- f"β€” {_total_a:,} result(s) found, top {len(_results_a)} shown below.<br>"
 
 
 
 
 
 
 
 
 
 
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 + _results_b
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=["primo_dual_resource"],
3542
  search_results=_combined[:8],
3543
- sources=[{"title": r.get("title", "Result"), "url": r.get("primo_url") or r.get("link", "")}
3544
- for r in _combined[:6] if r.get("primo_url") or r.get("link")],
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,