Upload 2 files
Browse files- 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 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
|
| 1043 |
|
| 1044 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
topic = _escape(context.display_topic or context.topic)
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
)
|
| 1052 |
return (
|
| 1053 |
-
|
| 1054 |
-
"
|
| 1055 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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 +=
|
| 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
|
| 1105 |
-
f"
|
| 1106 |
)
|
| 1107 |
else:
|
| 1108 |
answer = (
|
| 1109 |
-
f"
|
| 1110 |
-
|
|
|
|
| 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 |
-
|
| 1248 |
-
|
| 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 |
-
"
|
| 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.
|
| 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 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
if isinstance(q, str)
|
| 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,
|