nikeshn commited on
Commit
9ac502a
Β·
verified Β·
1 Parent(s): f45f29c

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. src/api/agent.py +154 -36
src/api/agent.py CHANGED
@@ -1225,23 +1225,126 @@ def _contact_footer_for_query(question: str, context: Optional[SearchContextPayl
1225
 
1226
 
1227
  def _search_trace_block(question: str, context: SearchContextPayload) -> str:
 
 
 
 
1228
  topic = _escape(context.display_topic or context.topic)
1229
- question_html = _escape(question)
1230
  primo_boolean = _escape(context.primo_boolean_query or "")
1231
- found_via = "I identified the main topic, built a PRIMO search query, and looked for KU-accessible results."
1232
- if context.intent == "search_medical":
1233
- found_via += " Because this is a medical topic, PubMed is also recommended for broader evidence searching."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1234
  block = (
1235
- f'<div style="margin:10px 0 14px 0;padding:12px 14px;border:1px solid #dbe4ff;border-radius:12px;background:#f8fbff">'
1236
- f"<strong>Your question:</strong> {question_html}<br>"
1237
- f"<strong>Interpreted topic:</strong> {topic}<br>"
 
 
 
 
1238
  )
1239
  if primo_boolean:
1240
- block += f"<strong>PRIMO query used:</strong> <code>{primo_boolean}</code><br>"
1241
- block += f"<strong>How I found this:</strong> {found_via}</div>"
 
 
 
 
 
1242
  return block
1243
 
1244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1245
  def _is_database_recommendation_question(question: str) -> bool:
1246
  return bool(DATABASE_RECOMMEND_RE.search(question or ""))
1247
 
@@ -1548,35 +1651,42 @@ def _database_recommendation_answer(question: str) -> str:
1548
  async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
1549
  """
1550
  Research snapshot:
1551
- - Runs Semantic Scholar + PRIMO in parallel for speed.
1552
- - If Semantic Scholar returns papers: generate cited summary, show refs, then AI tools at bottom.
1553
- - If Semantic Scholar returns nothing: silently fall back to PRIMO results + AI tools.
1554
- Never mention Semantic Scholar failure to the user.
1555
- - AI tools panel always appears at the end.
1556
  """
1557
  import asyncio as _asyncio
1558
  topic = context.topic
1559
  references: List[dict] = []
1560
 
1561
- # Parallel fetch β€” don't wait for Semantic Scholar before starting PRIMO
1562
  papers, (primo_result, primo_url) = await _asyncio.gather(
1563
  _semantic_scholar_search(topic, limit=8),
1564
  _search_primo_catalog(context, limit=6),
1565
  )
1566
  search_results = primo_result.get("results", []) or []
1567
 
 
 
 
 
1568
  if papers:
1569
  summary = await _generate_cited_summary(topic, papers, model)
1570
  if not summary:
1571
  _safe_metrics_increment("summary_fallbacks")
1572
  summary = _extractive_snapshot_from_papers(topic, papers)
1573
 
1574
- answer = f"<strong>πŸ“– Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
 
 
1575
  if search_results:
1576
  answer += (
1577
  "<br><br><strong>KU Library results:</strong> "
1578
- "KU-accessible items are shown below β€” open them directly or use the AI tools for a deeper search."
1579
  )
 
1580
  answer += "<br><br><strong>Selected references:</strong><br>"
1581
  for i, paper in enumerate(papers[:6], 1):
1582
  answer += (
@@ -1586,29 +1696,29 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
1586
  f'{_escape(paper["authors"])} Β· {paper["year"]} Β· {_escape(paper["venue"] or "")}'
1587
  f"</span>"
1588
  )
1589
- # AI tools always at the bottom
1590
- answer += f"<br><br>{_database_guidance_for_topic(context)}"
1591
  answer += _source_badge("semantic", "https://www.semanticscholar.org/")
1592
  if search_results:
1593
  answer += _source_badge("primo", primo_url)
 
 
 
1594
  references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
1595
  return answer, search_results, references
1596
 
1597
- # Semantic Scholar returned nothing β€” silent fallback, no error message
1598
  if search_results:
1599
- answer = (
1600
  f"<strong>πŸ“š Research starting points: {_escape(topic)}</strong><br><br>"
1601
- "Here are KU-accessible results you can open immediately. "
1602
- "For a deeper literature synthesis, use the AI research tools below."
1603
- f"<br><br>{_database_guidance_for_topic(context)}"
1604
  )
1605
  else:
1606
- answer = (
1607
  f"<strong>πŸ“š Research starting points: {_escape(topic)}</strong><br><br>"
1608
- "I didn't find direct results right now β€” try the AI research tools below for a deep search on this topic."
1609
- f"<br><br>{_database_guidance_for_topic(context)}"
1610
  )
1611
  answer += _source_badge("primo", primo_url)
 
 
1612
  return answer, search_results, []
1613
 
1614
 
@@ -1910,7 +2020,6 @@ async def _run_search_mode(
1910
  question: str, context: SearchContextPayload, model: str, is_follow_up: bool
1911
  ) -> Tuple[str, List[dict], List[dict], str]:
1912
  context = await _prepare_queries(question, context, model, is_follow_up)
1913
- filters = _filters_summary(context)
1914
 
1915
  if context.source == "pubmed":
1916
  result, source_url = await _search_pubmed_articles(context, limit=6)
@@ -1922,20 +2031,29 @@ async def _run_search_mode(
1922
  search_results = result.get("results", []) or []
1923
  total = result.get("total", len(search_results))
1924
 
1925
- answer = _search_answer_intro(context, is_follow_up)
 
 
 
 
 
 
1926
  if filters:
1927
- answer += f"<br><br><strong>Current filters:</strong> {_escape(filters)}."
 
1928
  if search_results:
1929
- answer += f"<br><br>I found <strong>{total}</strong> result(s). The top items are shown below."
1930
  else:
1931
- answer += (
1932
- "<br><br>I couldn't retrieve matching results right now. "
1933
- "You can still try the direct library links and AI research tools below."
1934
- )
1935
  if context.intent == "search_medical" and context.source != "pubmed":
1936
- answer += "<br><br>For the strongest medical evidence trail, I also recommend checking <strong>PubMed</strong> or <strong>Embase</strong>."
1937
 
1938
  answer += _source_badge(source_name, source_url)
 
 
 
 
1939
  sources = []
1940
  for item in search_results[:6]:
1941
  link = item.get("primo_url") or item.get("link")
 
1225
 
1226
 
1227
  def _search_trace_block(question: str, context: SearchContextPayload) -> str:
1228
+ """
1229
+ 'How I searched' reasoning panel β€” shown at the top of every search response.
1230
+ Collapsible <details> so it doesn't dominate the answer.
1231
+ """
1232
  topic = _escape(context.display_topic or context.topic)
 
1233
  primo_boolean = _escape(context.primo_boolean_query or "")
1234
+ source_label = "PubMed" if context.source == "pubmed" else "KU PRIMO Library Discovery"
1235
+
1236
+ # Build reasoning sentence
1237
+ intent_label = {
1238
+ "search_academic": "academic literature search",
1239
+ "search_medical": "medical / clinical literature search",
1240
+ }.get(context.intent, "library search")
1241
+
1242
+ filters_parts = []
1243
+ if context.peer_reviewed:
1244
+ filters_parts.append("peer reviewed only")
1245
+ if context.open_access:
1246
+ filters_parts.append("open access")
1247
+ if context.year_from and context.year_to:
1248
+ filters_parts.append(f"{context.year_from}–{context.year_to}")
1249
+ elif context.year_from:
1250
+ filters_parts.append(f"from {context.year_from}")
1251
+ rtype = {"articles": "articles", "books": "books", "both": "articles and books"}.get(
1252
+ context.resource_type, "articles"
1253
+ )
1254
+ filters_parts.append(rtype)
1255
+ filters_str = ", ".join(filters_parts)
1256
+
1257
+ reasoning = (
1258
+ f"I identified this as a <strong>{intent_label}</strong>. "
1259
+ f"I extracted the topic <strong>{topic}</strong>, "
1260
+ f"built an LLM-optimised boolean query with quoted phrases and synonyms, "
1261
+ f"then searched <strong>{source_label}</strong> for <strong>{filters_str}</strong>."
1262
+ )
1263
+ if context.intent == "search_medical" and context.source != "pubmed":
1264
+ reasoning += " For medical topics, PubMed and Embase are also recommended."
1265
+
1266
  block = (
1267
+ f'<details style="margin:0 0 14px 0;border:1px solid #dbe4ff;border-radius:12px;background:#f8fbff;overflow:hidden">'
1268
+ f'<summary style="padding:10px 14px;cursor:pointer;font-size:.8rem;font-weight:700;color:#1e40af;list-style:none;display:flex;align-items:center;gap:6px">'
1269
+ f'<span style="font-size:.9rem">πŸ”</span> How I searched β€” click to expand'
1270
+ f'</summary>'
1271
+ f'<div style="padding:4px 14px 12px 14px;font-size:.82rem;line-height:1.7;border-top:1px solid #dbe4ff">'
1272
+ f'<strong>Your question:</strong> {_escape(question)}<br>'
1273
+ f'<strong>Interpreted topic:</strong> {topic}<br>'
1274
  )
1275
  if primo_boolean:
1276
+ block += (
1277
+ f'<strong>Boolean query sent to {source_label}:</strong><br>'
1278
+ f'<code style="display:block;margin:4px 0;padding:6px 10px;background:#1a1a2e;color:#C8A951;'
1279
+ f'border-radius:7px;font-size:.78rem;word-break:break-all">{primo_boolean}</code>'
1280
+ )
1281
+ block += f'<strong>Reasoning:</strong> {reasoning}'
1282
+ block += '</div></details>'
1283
  return block
1284
 
1285
 
1286
+ def _ai_tools_footer(context: SearchContextPayload) -> str:
1287
+ """
1288
+ AI tools recommendation panel β€” always rendered at the very end of the answer.
1289
+ Structured as KU-subscribed tools first, then freemium, then more results.
1290
+ """
1291
+ urls = _tool_urls(context)
1292
+ topic_enc = _escape(context.ai_tool_query or context.display_topic or context.topic)
1293
+ is_medical = context.intent == "search_medical"
1294
+
1295
+ ku_tools = [
1296
+ ("πŸ”¬ LeapSpace", urls["leapspace"], "leapspace", "#9a3412"),
1297
+ ("πŸ”­ Scopus AI", urls["scopus_ai"], "scopus", "#1e40af"),
1298
+ ("πŸ“° EBSCO AI", urls["ebsco_ai"], "ebscoai", "#9d174d"),
1299
+ ("⭐ PRIMO AI Assistant", urls["primo_ai"], "primoai", "#5b21b6"),
1300
+ ]
1301
+ free_tools = [
1302
+ ("🧬 Consensus", urls["consensus"], "consensus", "#86198f"),
1303
+ ("🌐 OpenAlex", f"https://openalex.org/works?page=1&sort=relevance_score:desc&search={topic_enc}", "openalex", "#0c4a6e"),
1304
+ ("πŸ”Ž Perplexity", f"https://www.perplexity.ai/search?q={topic_enc}", "perplexity", "#4c1d95"),
1305
+ ]
1306
+ more = [("πŸ“š PRIMO Library Discovery", urls["primo_discovery"], "primoai", "#003366")]
1307
+ if is_medical and urls.get("pubmed"):
1308
+ more.append(("πŸ₯ PubMed", urls["pubmed"], "pubmed_badge", "#065f46"))
1309
+
1310
+ def chip(label, url, cls, color):
1311
+ return (
1312
+ f'<a href="{url}" target="_blank" '
1313
+ f'style="display:inline-flex;align-items:center;padding:5px 11px;margin:3px;'
1314
+ f'border-radius:20px;font-size:.72rem;font-weight:700;text-decoration:none;'
1315
+ f'border:1.5px solid {color}33;color:{color};background:{color}11;'
1316
+ f'transition:all .2s;white-space:nowrap">{label}</a>'
1317
+ )
1318
+
1319
+ html = (
1320
+ '<div style="margin-top:18px;padding:12px 14px;border:1px solid #c4b5fd;'
1321
+ 'border-radius:12px;background:linear-gradient(135deg,#f5f3ff,#fdf4ff)">'
1322
+ '<div style="font-size:.75rem;font-weight:800;color:#5b21b6;margin-bottom:8px">'
1323
+ '✨ For deeper research β€” AI tools</div>'
1324
+ '<div style="font-size:.67rem;font-weight:700;color:#6b7280;text-transform:uppercase;'
1325
+ 'letter-spacing:.5px;margin-bottom:4px">πŸ” KU subscribed</div>'
1326
+ '<div style="line-height:2">'
1327
+ )
1328
+ for label, url, cls, color in ku_tools:
1329
+ html += chip(label, url, cls, color)
1330
+ html += (
1331
+ '</div><div style="font-size:.67rem;font-weight:700;color:#6b7280;text-transform:uppercase;'
1332
+ 'letter-spacing:.5px;margin:8px 0 4px">πŸ†“ Freemium tools</div>'
1333
+ '<div style="line-height:2">'
1334
+ )
1335
+ for label, url, cls, color in free_tools:
1336
+ html += chip(label, url, cls, color)
1337
+ html += (
1338
+ '</div><div style="font-size:.67rem;font-weight:700;color:#6b7280;text-transform:uppercase;'
1339
+ 'letter-spacing:.5px;margin:8px 0 4px">πŸ“š More results</div>'
1340
+ '<div style="line-height:2">'
1341
+ )
1342
+ for label, url, cls, color in more:
1343
+ html += chip(label, url, cls, color)
1344
+ html += '</div></div>'
1345
+ return html
1346
+
1347
+
1348
  def _is_database_recommendation_question(question: str) -> bool:
1349
  return bool(DATABASE_RECOMMEND_RE.search(question or ""))
1350
 
 
1651
  async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
1652
  """
1653
  Research snapshot:
1654
+ - Reasoning block at the top (collapsible).
1655
+ - Cited summary in the middle.
1656
+ - PRIMO results below that.
1657
+ - AI tools recommendation panel always at the very end.
1658
+ - Silent fallback if Semantic Scholar returns nothing.
1659
  """
1660
  import asyncio as _asyncio
1661
  topic = context.topic
1662
  references: List[dict] = []
1663
 
1664
+ # Parallel fetch
1665
  papers, (primo_result, primo_url) = await _asyncio.gather(
1666
  _semantic_scholar_search(topic, limit=8),
1667
  _search_primo_catalog(context, limit=6),
1668
  )
1669
  search_results = primo_result.get("results", []) or []
1670
 
1671
+ # ── Top: reasoning block ──────────────────────────────────────────────────
1672
+ q_display = context.ai_tool_query or f"Research on {topic}"
1673
+ answer = _search_trace_block(q_display, context)
1674
+
1675
  if papers:
1676
  summary = await _generate_cited_summary(topic, papers, model)
1677
  if not summary:
1678
  _safe_metrics_increment("summary_fallbacks")
1679
  summary = _extractive_snapshot_from_papers(topic, papers)
1680
 
1681
+ # ── Middle: cited summary ─────────────────────────────────────────────
1682
+ answer += f"<strong>πŸ“– Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
1683
+
1684
  if search_results:
1685
  answer += (
1686
  "<br><br><strong>KU Library results:</strong> "
1687
+ "KU-accessible items are shown below."
1688
  )
1689
+
1690
  answer += "<br><br><strong>Selected references:</strong><br>"
1691
  for i, paper in enumerate(papers[:6], 1):
1692
  answer += (
 
1696
  f'{_escape(paper["authors"])} Β· {paper["year"]} Β· {_escape(paper["venue"] or "")}'
1697
  f"</span>"
1698
  )
 
 
1699
  answer += _source_badge("semantic", "https://www.semanticscholar.org/")
1700
  if search_results:
1701
  answer += _source_badge("primo", primo_url)
1702
+
1703
+ # ── Bottom: AI tools ──────────────────────────���───────────────────────
1704
+ answer += _ai_tools_footer(context)
1705
  references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
1706
  return answer, search_results, references
1707
 
1708
+ # Semantic Scholar returned nothing β€” silent fallback
1709
  if search_results:
1710
+ answer += (
1711
  f"<strong>πŸ“š Research starting points: {_escape(topic)}</strong><br><br>"
1712
+ "Here are KU-accessible results you can open immediately."
 
 
1713
  )
1714
  else:
1715
+ answer += (
1716
  f"<strong>πŸ“š Research starting points: {_escape(topic)}</strong><br><br>"
1717
+ "I didn't find direct results right now β€” try the AI research tools below."
 
1718
  )
1719
  answer += _source_badge("primo", primo_url)
1720
+ # ── Bottom: AI tools ──────────────────────────────────────────────────────
1721
+ answer += _ai_tools_footer(context)
1722
  return answer, search_results, []
1723
 
1724
 
 
2020
  question: str, context: SearchContextPayload, model: str, is_follow_up: bool
2021
  ) -> Tuple[str, List[dict], List[dict], str]:
2022
  context = await _prepare_queries(question, context, model, is_follow_up)
 
2023
 
2024
  if context.source == "pubmed":
2025
  result, source_url = await _search_pubmed_articles(context, limit=6)
 
2031
  search_results = result.get("results", []) or []
2032
  total = result.get("total", len(search_results))
2033
 
2034
+ # ── Top: reasoning / how-I-searched block ────────────────────────────────
2035
+ answer = _search_trace_block(question, context)
2036
+
2037
+ # ── Middle: answer sentence + result count ────────────────────────────────
2038
+ answer += _search_answer_intro(context, is_follow_up)
2039
+
2040
+ filters = _filters_summary(context)
2041
  if filters:
2042
+ answer += f"<br><br><strong>Active filters:</strong> {_escape(filters)}."
2043
+
2044
  if search_results:
2045
+ answer += f"<br><br>Found <strong>{total:,}</strong> result(s) β€” top {len(search_results)} shown below."
2046
  else:
2047
+ answer += "<br><br>No matching results found right now β€” try broadening your terms or use the AI tools below."
2048
+
 
 
2049
  if context.intent == "search_medical" and context.source != "pubmed":
2050
+ answer += "<br><br>For the strongest medical evidence trail, also check <strong>PubMed</strong> or <strong>Embase</strong>."
2051
 
2052
  answer += _source_badge(source_name, source_url)
2053
+
2054
+ # ── Bottom: AI tools recommendation panel ─────────────────────────────────
2055
+ answer += _ai_tools_footer(context)
2056
+
2057
  sources = []
2058
  for item in search_results[:6]:
2059
  link = item.get("primo_url") or item.get("link")