nikeshn commited on
Commit
c281d32
·
verified ·
1 Parent(s): b265cdb

Upload 2 files

Browse files
Files changed (1) hide show
  1. src/api/agent.py +140 -50
src/api/agent.py CHANGED
@@ -1022,7 +1022,6 @@ async def _search_pubmed_articles(context: SearchContextPayload, limit: int = 5)
1022
 
1023
  def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
1024
  ai_query = quote(context.ai_tool_query or context.display_topic or context.topic)
1025
- clean_topic = _strip_resource_noise(context.topic)
1026
  return {
1027
  "leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}",
1028
  "scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}",
@@ -1031,34 +1030,119 @@ def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
1031
  }
1032
 
1033
 
1034
- def _ai_source_links_html(context: SearchContextPayload) -> str:
1035
- urls = _tool_urls(context)
1036
- return (
1037
- f'<a href="{urls["leapspace"]}" target="_blank" style="color:#9a3412;font-weight:700;text-decoration:none">LeapSpace</a> · '
1038
- f'<a href="{urls["scopus_ai"]}" target="_blank" style="color:#1e40af;font-weight:700;text-decoration:none">Scopus AI</a> · '
1039
- f'<a href="{urls["consensus"]}" target="_blank" style="color:#86198f;font-weight:700;text-decoration:none">Consensus</a> · '
1040
- f'<a href="{urls["primo_discovery"]}" target="_blank" style="color:#5b21b6;font-weight:700;text-decoration:none">PRIMO Library Discovery</a>'
1041
- )
 
 
 
 
 
 
1042
 
1043
 
1044
- def _database_guidance_for_topic(context: SearchContextPayload) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  topic = _escape(context.display_topic or context.topic)
1046
- links = _ai_source_links_html(context)
1047
- if context.intent == "search_medical":
1048
- return (
1049
- f"For a stronger evidence trail on <strong>{topic}</strong>, start with <strong>PubMed</strong> or <strong>Embase</strong> for the core medical literature, "
1050
- f"then continue with {links}."
 
 
 
 
 
1051
  )
1052
  return (
1053
- f"For deeper work on <strong>{topic}</strong>, continue with {links}. "
1054
- "Use Scopus AI for broader scholarly mapping, LeapSpace for guided evidence exploration, "
1055
- "Consensus for quick paper-grounded answers, and PRIMO Library Discovery for KU-accessible material."
 
 
 
1056
  )
1057
 
1058
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1059
  # ── Research Snapshot ─────────────────────────────────────────────────────────
1060
 
1061
- async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
1062
  """
1063
  Full research snapshot:
1064
  1. Semantic Scholar → cited summary with hyperlinked [Author, Year] citations
@@ -1069,6 +1153,7 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
1069
  references: List[dict] = []
1070
  primo_result, primo_url = await _search_primo_catalog(context, limit=6)
1071
  search_results = primo_result.get("results", []) or []
 
1072
 
1073
  if papers:
1074
  summary = await _generate_cited_summary(topic, papers, model)
@@ -1076,13 +1161,13 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
1076
  _safe_metrics_increment("summary_fallbacks")
1077
  summary = _extractive_snapshot_from_papers(topic, papers)
1078
 
1079
- answer = f"<strong>📖 Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
1080
  if search_results:
1081
  answer += (
1082
  "<br><br><strong>KU Library starting points:</strong> "
1083
  "I also pulled KU-accessible results below so you can move directly from the snapshot to subscribed material."
1084
  )
1085
- answer += f"<br><br><strong>AI sources:</strong> {_database_guidance_for_topic(context)}"
1086
  answer += "<br><br><strong>Selected references:</strong><br>"
1087
  for i, paper in enumerate(papers[:6], 1):
1088
  answer += (
@@ -1095,21 +1180,24 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
1095
  answer += _source_badge("semantic", "https://www.semanticscholar.org/")
1096
  if search_results:
1097
  answer += _source_badge("primo", primo_url)
 
1098
  references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
1099
  return answer, search_results, references
1100
 
1101
  if search_results:
1102
  answer = (
1103
- f"<strong>📚 KU Library starting points on {_escape(topic)}</strong><br><br>"
1104
- "I couldn't build a Semantic Scholar snapshot right now, but here are KU-accessible library results you can open immediately. "
1105
- f"<br><br><strong>AI sources:</strong> {_database_guidance_for_topic(context)}"
1106
  )
1107
  else:
1108
  answer = (
1109
- f"I couldn't retrieve a research snapshot for <strong>{_escape(topic)}</strong> right now.<br><br>"
1110
- f"<strong>AI sources:</strong> {_database_guidance_for_topic(context)}"
 
1111
  )
1112
  answer += _source_badge("primo", primo_url)
 
1113
  return answer, search_results, []
1114
 
1115
 
@@ -1244,9 +1332,8 @@ def _apply_follow_up_action(base_context: SearchContextPayload, action: Optional
1244
 
1245
  async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
1246
  """
1247
- FIX: Generate 3 topic-specific intellectual follow-up questions using LLM.
1248
- These are ASPECT-based (related subtopics, methodological angles, applications)
1249
- not generic filter suggestions like "peer reviewed only".
1250
  """
1251
  settings = get_settings()
1252
  if not settings.openai_api_key:
@@ -1257,15 +1344,11 @@ async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
1257
  {"role": "system", "content": (
1258
  "You are a research librarian helping a user explore a topic more deeply. "
1259
  "Given a research topic, generate exactly 3 short follow-up questions that explore "
1260
- "DIFFERENT ASPECTS of the topic — such as subtopics, methodological angles, "
1261
  "applications, comparisons, or related fields. "
1262
  "Do NOT suggest filter actions like peer-reviewed, date ranges, or format changes. "
1263
  "Return ONLY a JSON array of 3 strings, each under 12 words. "
1264
- "No explanation, no preamble, no markdown.\n"
1265
- "Example for 'quantum computing':\n"
1266
- '[\"What are the main hardware approaches in quantum computing?\", '
1267
- '"How does quantum error correction work?\", '
1268
- '"Applications of quantum computing in cryptography\"]'
1269
  )},
1270
  {"role": "user", "content": f"Topic: {topic}"},
1271
  ])
@@ -1275,11 +1358,19 @@ async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
1275
  questions = json.loads(raw)
1276
  if not isinstance(questions, list):
1277
  return []
1278
- return [
1279
- {"label": q.strip("?") + "?", "question": q.strip("?") + " — find articles", "action": "new_search"}
1280
- for q in questions[:3]
1281
- if isinstance(q, str) and q.strip()
1282
- ]
 
 
 
 
 
 
 
 
1283
  except Exception as e:
1284
  logger.warning(f"Topic follow-up generation failed: {e}")
1285
  return []
@@ -1436,20 +1527,19 @@ async def _run_search_mode(
1436
  search_results = result.get("results", []) or []
1437
  total = result.get("total", len(search_results))
1438
 
1439
- answer = _search_answer_intro(context, is_follow_up)
1440
  if filters:
1441
  answer += f"<br><br><strong>Current filters:</strong> {_escape(filters)}."
1442
  if search_results:
1443
  answer += f"<br><br>I found <strong>{total}</strong> result(s). The top items are shown below."
1444
  else:
1445
- answer += (
1446
- "<br><br>I couldn't retrieve matching results right now. "
1447
- "You can still try the direct library links and AI research tools below."
1448
- )
1449
  if context.intent == "search_medical" and context.source != "pubmed":
1450
  answer += "<br><br>For the strongest medical evidence trail, I also recommend checking <strong>PubMed</strong> or <strong>Embase</strong>."
1451
 
 
1452
  answer += _source_badge(source_name, source_url)
 
1453
  sources = []
1454
  for item in search_results[:6]:
1455
  link = item.get("primo_url") or item.get("link")
@@ -1579,7 +1669,7 @@ async def agent_query(request: Request, req: AgentRequest):
1579
  if follow_up_action in {"summarize_topic", "deep_research_tools", "best_databases"}:
1580
  context = await _prepare_queries(question, context, model, True)
1581
  if follow_up_action == "summarize_topic":
1582
- answer, search_results, sources = await _research_snapshot(context, model)
1583
  else:
1584
  answer = (
1585
  f"<strong>Research guide for {_escape(context.display_topic or context.topic)}</strong><br><br>"
@@ -1676,7 +1766,7 @@ async def agent_query(request: Request, req: AgentRequest):
1676
 
1677
  follow_up_question, follow_up_suggestions = _library_follow_up(question)
1678
  return AgentResponse(
1679
- answer=answer,
1680
  intent="library_info",
1681
  tools_used=tools_used,
1682
  sources=sources,
@@ -1712,7 +1802,7 @@ async def agent_query(request: Request, req: AgentRequest):
1712
  ))
1713
 
1714
  if summary_mode or deep_research_mode:
1715
- answer, search_results, sources = await _research_snapshot(context, model)
1716
  if deep_research_mode and "Research guide" not in answer:
1717
  answer += f"<br><br><strong>Research guide:</strong> {_database_guidance_for_topic(context)}"
1718
  follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
@@ -1761,7 +1851,7 @@ async def agent_query(request: Request, req: AgentRequest):
1761
  answer = await _web_search_answer(question, history, model) + _source_badge("web_live")
1762
  follow_up_question, follow_up_suggestions = _general_follow_up(question)
1763
  return AgentResponse(
1764
- answer=answer,
1765
  intent="general_recent",
1766
  tools_used=["web_search"],
1767
  model_used=model,
@@ -1773,7 +1863,7 @@ async def agent_query(request: Request, req: AgentRequest):
1773
  answer = await _web_search_answer(question, history, model) + _source_badge("web")
1774
  follow_up_question, follow_up_suggestions = _general_follow_up(question)
1775
  return AgentResponse(
1776
- answer=answer,
1777
  intent="general",
1778
  tools_used=["web_search"],
1779
  model_used=model,
 
1022
 
1023
  def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
1024
  ai_query = quote(context.ai_tool_query or context.display_topic or context.topic)
 
1025
  return {
1026
  "leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}",
1027
  "scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}",
 
1030
  }
1031
 
1032
 
1033
+ _BOOLEAN_PHRASE_SYNONYMS: Dict[str, List[str]] = {
1034
+ "global warming": ["climate change"],
1035
+ "artificial intelligence": ["AI", "machine learning"],
1036
+ "machine learning": ["artificial intelligence", "AI"],
1037
+ "healthcare": ["clinical", "medicine"],
1038
+ "energy transition": ["decarbonization", "low carbon"],
1039
+ }
1040
+ _BOOLEAN_TOKEN_SYNONYMS: Dict[str, List[str]] = {
1041
+ "india": ["indian"],
1042
+ "adaptation": ["resilience"],
1043
+ "mitigation": ["emissions reduction"],
1044
+ "transport": ["transportation"],
1045
+ "climate": ["warming"],
1046
+ }
1047
 
1048
 
1049
+ def _boolean_or_group(terms: List[str]) -> str:
1050
+ clean=[]
1051
+ seen=set()
1052
+ for term in terms:
1053
+ t=_normalize_whitespace(term).strip('"').strip()
1054
+ if not t:
1055
+ continue
1056
+ key=t.lower()
1057
+ if key in seen:
1058
+ continue
1059
+ seen.add(key)
1060
+ if re.search(r"\s", t):
1061
+ clean.append(f'"{t}"')
1062
+ else:
1063
+ clean.append(t)
1064
+ if not clean:
1065
+ return '"library search"'
1066
+ if len(clean)==1:
1067
+ return clean[0]
1068
+ return '(' + ' OR '.join(clean) + ')'
1069
+
1070
+
1071
+ def _make_primo_boolean_query(context: SearchContextPayload) -> str:
1072
+ topic = _strip_resource_noise(_title_case_topic(context.display_topic or context.topic))
1073
+ if not topic:
1074
+ return '"library search"'
1075
+ lower = topic.lower()
1076
+ groups: List[str] = []
1077
+ consumed = lower
1078
+ for phrase, syns in sorted(_BOOLEAN_PHRASE_SYNONYMS.items(), key=lambda kv: len(kv[0]), reverse=True):
1079
+ if phrase in consumed:
1080
+ groups.append(_boolean_or_group([phrase] + syns))
1081
+ consumed = consumed.replace(phrase, ' ')
1082
+ for token in re.findall(r"[a-z0-9-]+", consumed):
1083
+ if len(token) < 3:
1084
+ continue
1085
+ groups.append(_boolean_or_group([token] + _BOOLEAN_TOKEN_SYNONYMS.get(token, [])))
1086
+ if not groups:
1087
+ groups = [_boolean_or_group([topic])]
1088
+ return ' AND '.join(groups)
1089
+
1090
+
1091
+ def _primo_clean_url(context: SearchContextPayload) -> str:
1092
+ clean_topic = _strip_resource_noise(context.topic) or context.topic
1093
+ return f"{PRIMO_BASE_URL}&query=any,contains,{quote(clean_topic)}&lang=en&mode=advanced&offset=0"
1094
+
1095
+
1096
+ def _build_trace_block(user_question: str, context: SearchContextPayload, used_summary: bool) -> str:
1097
  topic = _escape(context.display_topic or context.topic)
1098
+ q = _escape(_normalize_whitespace(user_question))
1099
+ boolean_q = _escape(context.primo_boolean_query or _make_primo_boolean_query(context))
1100
+ if used_summary:
1101
+ how = (
1102
+ f"I interpreted your question as <strong>{topic}</strong>, used Semantic Scholar abstracts to prepare a brief literature snapshot when available, "
1103
+ f"and searched PRIMO for KU-accessible results."
1104
+ )
1105
+ else:
1106
+ how = (
1107
+ f"I interpreted your question as <strong>{topic}</strong> and searched PRIMO for KU-accessible results using the Boolean query below."
1108
  )
1109
  return (
1110
+ '<div style="margin:10px 0 14px 0;padding:12px 14px;background:#f8fafc;border:1px solid #dbeafe;border-radius:12px">'
1111
+ f"<strong>Your question:</strong> {q}<br>"
1112
+ f"<strong>Interpreted topic:</strong> {topic}<br>"
1113
+ f"<strong>PRIMO query used:</strong> <code>{boolean_q}</code><br>"
1114
+ f"<strong>How I found this:</strong> {how}"
1115
+ '</div>'
1116
  )
1117
 
1118
 
1119
+ def _research_end_note() -> str:
1120
+ return "<br><br><em>For deeper research, recommended AI research tools are listed at the end of this answer.</em>"
1121
+
1122
+
1123
+ def _contact_footer_for_query(intent: str, question: str, context: Optional[SearchContextPayload] = None) -> str:
1124
+ q = (question or '').lower()
1125
+ blocks = [
1126
+ '<br><br><strong>Library contact points</strong><br>'
1127
+ f'General help: <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a> · '
1128
+ '<a href="mailto:libse@ku.ac.ae">libse@ku.ac.ae</a>'
1129
+ ]
1130
+ if intent == 'search_medical' or 'pubmed' in q or 'clinical' in q or 'medical' in q or 'nursing' in q:
1131
+ blocks.append('Medical support: <strong>Jason Fetty</strong> · <a href="mailto:jason.fetty@ku.ac.ae">jason.fetty@ku.ac.ae</a> · <a href="tel:+97123124722">+971 2 312 4722</a>')
1132
+ elif any(tok in q for tok in ['database access', 'full text', 'proxy', 'login', 'eresource', 'e-resource', 'access problem']):
1133
+ blocks.append('E-resources support: <strong>Rani Anand</strong> · <a href="mailto:rani.anand@ku.ac.ae">rani.anand@ku.ac.ae</a> · <a href="tel:+97123123935">+971 2 312 3935</a>')
1134
+ elif any(tok in q for tok in ['website', 'system', 'technical', 'error', 'bug']):
1135
+ blocks.append('Systems support: <strong>Walter Brian Hall</strong> · <a href="mailto:walter.hall@ku.ac.ae">walter.hall@ku.ac.ae</a> · <a href="tel:+97123123163">+971 2 312 3163</a>')
1136
+ elif intent in {'search_academic', 'search_medical'} or any(tok in q for tok in ['research', 'review', 'literature', 'citation', 'orcid', 'scopus', 'bibliometric']):
1137
+ blocks.append('Research support: <strong>Nikesh Narayanan</strong> · <a href="mailto:nikesh.narayanan@ku.ac.ae">nikesh.narayanan@ku.ac.ae</a> · <a href="tel:+97123123980">+971 2 312 3980</a>')
1138
+ else:
1139
+ blocks.append('Public services: <strong>Muna Al Blooshi</strong> · <a href="mailto:muna.alblooshi@ku.ac.ae">muna.alblooshi@ku.ac.ae</a> · <a href="tel:+97123124212">+971 2 312 4212</a>')
1140
+ return '<div style="margin-top:14px;padding-top:10px;border-top:1px solid #e5e7eb;font-size:.95rem">' + '<br>'.join(blocks) + '</div>'
1141
+
1142
+
1143
  # ── Research Snapshot ─────────────────────────────────────────────────────────
1144
 
1145
+ async def _research_snapshot(question: str, context: SearchContextPayload, model: str) -> Tuple[str, List[dict], List[dict]]:
1146
  """
1147
  Full research snapshot:
1148
  1. Semantic Scholar → cited summary with hyperlinked [Author, Year] citations
 
1153
  references: List[dict] = []
1154
  primo_result, primo_url = await _search_primo_catalog(context, limit=6)
1155
  search_results = primo_result.get("results", []) or []
1156
+ trace = _build_trace_block(question, context, used_summary=bool(papers))
1157
 
1158
  if papers:
1159
  summary = await _generate_cited_summary(topic, papers, model)
 
1161
  _safe_metrics_increment("summary_fallbacks")
1162
  summary = _extractive_snapshot_from_papers(topic, papers)
1163
 
1164
+ answer = f"{trace}<strong>📖 Research Snapshot: {_escape(topic)}</strong><br><br>{summary}"
1165
  if search_results:
1166
  answer += (
1167
  "<br><br><strong>KU Library starting points:</strong> "
1168
  "I also pulled KU-accessible results below so you can move directly from the snapshot to subscribed material."
1169
  )
1170
+ answer += _research_end_note()
1171
  answer += "<br><br><strong>Selected references:</strong><br>"
1172
  for i, paper in enumerate(papers[:6], 1):
1173
  answer += (
 
1180
  answer += _source_badge("semantic", "https://www.semanticscholar.org/")
1181
  if search_results:
1182
  answer += _source_badge("primo", primo_url)
1183
+ answer += _contact_footer_for_query(context.intent, question, context)
1184
  references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
1185
  return answer, search_results, references
1186
 
1187
  if search_results:
1188
  answer = (
1189
+ f"{trace}<strong>📚 KU Library starting points on {_escape(topic)}</strong><br><br>"
1190
+ "I found KU-accessible library results you can open immediately."
1191
+ f"{_research_end_note()}"
1192
  )
1193
  else:
1194
  answer = (
1195
+ f"{trace}<strong>Research starting points for {_escape(topic)}</strong><br><br>"
1196
+ "I prepared a library discovery path for this topic."
1197
+ f"{_research_end_note()}"
1198
  )
1199
  answer += _source_badge("primo", primo_url)
1200
+ answer += _contact_footer_for_query(context.intent, question, context)
1201
  return answer, search_results, []
1202
 
1203
 
 
1332
 
1333
  async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
1334
  """
1335
+ Generate 3 topic-specific follow-up questions, but preserve the original anchor topic
1336
+ in the next clicked search query.
 
1337
  """
1338
  settings = get_settings()
1339
  if not settings.openai_api_key:
 
1344
  {"role": "system", "content": (
1345
  "You are a research librarian helping a user explore a topic more deeply. "
1346
  "Given a research topic, generate exactly 3 short follow-up questions that explore "
1347
+ "different aspects of the topic — such as subtopics, methodological angles, "
1348
  "applications, comparisons, or related fields. "
1349
  "Do NOT suggest filter actions like peer-reviewed, date ranges, or format changes. "
1350
  "Return ONLY a JSON array of 3 strings, each under 12 words. "
1351
+ "No explanation, no preamble, no markdown."
 
 
 
 
1352
  )},
1353
  {"role": "user", "content": f"Topic: {topic}"},
1354
  ])
 
1358
  questions = json.loads(raw)
1359
  if not isinstance(questions, list):
1360
  return []
1361
+ out=[]
1362
+ topic_l = topic.lower()
1363
+ for q in questions[:3]:
1364
+ if not isinstance(q, str) or not q.strip():
1365
+ continue
1366
+ label = q.strip().rstrip('?') + '?'
1367
+ q_clean = _normalize_whitespace(q.strip().rstrip('?'))
1368
+ if topic_l not in q_clean.lower():
1369
+ full_q = f"Find research articles on {q_clean} in the context of {topic}"
1370
+ else:
1371
+ full_q = f"Find research articles on {q_clean}"
1372
+ out.append({"label": label, "question": full_q, "action": None})
1373
+ return out
1374
  except Exception as e:
1375
  logger.warning(f"Topic follow-up generation failed: {e}")
1376
  return []
 
1527
  search_results = result.get("results", []) or []
1528
  total = result.get("total", len(search_results))
1529
 
1530
+ answer = _build_trace_block(question, context, used_summary=False) + _search_answer_intro(context, is_follow_up)
1531
  if filters:
1532
  answer += f"<br><br><strong>Current filters:</strong> {_escape(filters)}."
1533
  if search_results:
1534
  answer += f"<br><br>I found <strong>{total}</strong> result(s). The top items are shown below."
1535
  else:
1536
+ answer += "<br><br>I prepared a direct discovery path for this topic using the search shown above."
 
 
 
1537
  if context.intent == "search_medical" and context.source != "pubmed":
1538
  answer += "<br><br>For the strongest medical evidence trail, I also recommend checking <strong>PubMed</strong> or <strong>Embase</strong>."
1539
 
1540
+ answer += _research_end_note()
1541
  answer += _source_badge(source_name, source_url)
1542
+ answer += _contact_footer_for_query(context.intent, question, context)
1543
  sources = []
1544
  for item in search_results[:6]:
1545
  link = item.get("primo_url") or item.get("link")
 
1669
  if follow_up_action in {"summarize_topic", "deep_research_tools", "best_databases"}:
1670
  context = await _prepare_queries(question, context, model, True)
1671
  if follow_up_action == "summarize_topic":
1672
+ answer, search_results, sources = await _research_snapshot(question, context, model)
1673
  else:
1674
  answer = (
1675
  f"<strong>Research guide for {_escape(context.display_topic or context.topic)}</strong><br><br>"
 
1766
 
1767
  follow_up_question, follow_up_suggestions = _library_follow_up(question)
1768
  return AgentResponse(
1769
+ answer=answer + _contact_footer_for_query("library_info", question),
1770
  intent="library_info",
1771
  tools_used=tools_used,
1772
  sources=sources,
 
1802
  ))
1803
 
1804
  if summary_mode or deep_research_mode:
1805
+ answer, search_results, sources = await _research_snapshot(question, context, model)
1806
  if deep_research_mode and "Research guide" not in answer:
1807
  answer += f"<br><br><strong>Research guide:</strong> {_database_guidance_for_topic(context)}"
1808
  follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
 
1851
  answer = await _web_search_answer(question, history, model) + _source_badge("web_live")
1852
  follow_up_question, follow_up_suggestions = _general_follow_up(question)
1853
  return AgentResponse(
1854
+ answer=answer + _contact_footer_for_query("general_recent", question),
1855
  intent="general_recent",
1856
  tools_used=["web_search"],
1857
  model_used=model,
 
1863
  answer = await _web_search_answer(question, history, model) + _source_badge("web")
1864
  follow_up_question, follow_up_suggestions = _general_follow_up(question)
1865
  return AgentResponse(
1866
+ answer=answer + _contact_footer_for_query("general", question),
1867
  intent="general",
1868
  tools_used=["web_search"],
1869
  model_used=model,