Upload 2 files
Browse files- src/api/agent.py +192 -137
src/api/agent.py
CHANGED
|
@@ -114,8 +114,7 @@ class AgentResponse(BaseModel):
|
|
| 114 |
primo_search_url: Optional[str] = None
|
| 115 |
primo_ai_url: Optional[str] = None
|
| 116 |
pubmed_search_url: Optional[str] = None
|
| 117 |
-
|
| 118 |
-
pubmed_search_url: Optional[str] = None
|
| 119 |
|
| 120 |
|
| 121 |
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
@@ -204,6 +203,10 @@ CURRENT_INFO_RE = re.compile(
|
|
| 204 |
r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
|
| 205 |
re.IGNORECASE,
|
| 206 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
SUMMARY_RE = re.compile(
|
| 208 |
r"\b(summar(y|ize|ise|izing|ising)|overview|explain|what is|what are|describe"
|
| 209 |
r"|tell me about|give me a summary|give me an overview|give me a brief"
|
|
@@ -728,24 +731,27 @@ def _parse_year_filters(question: str) -> Tuple[Optional[str], Optional[str]]:
|
|
| 728 |
|
| 729 |
|
| 730 |
def _make_primo_boolean_query(context: SearchContextPayload) -> str:
|
| 731 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
topic = _title_case_topic(context.display_topic or context.topic)
|
|
|
|
| 733 |
topic = _strip_resource_noise(topic)
|
| 734 |
if not topic:
|
| 735 |
topic = context.topic or "library search"
|
| 736 |
-
return
|
| 737 |
|
| 738 |
|
| 739 |
def _primo_clean_url(context: SearchContextPayload) -> str:
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
context.
|
| 746 |
-
|
| 747 |
-
context.year_to,
|
| 748 |
-
)
|
| 749 |
|
| 750 |
|
| 751 |
def _topic_keywords(text: str) -> List[str]:
|
|
@@ -964,10 +970,7 @@ def _extractive_snapshot_from_papers(topic: str, papers: List[dict]) -> str:
|
|
| 964 |
|
| 965 |
# ── PRIMO / PubMed Search Wrappers ────────────────────────────────────────────
|
| 966 |
|
| 967 |
-
from src.api.search import
|
| 968 |
-
SearchRequest, search_primo, search_pubmed,
|
| 969 |
-
build_boolean_query, build_primo_discovery_url, build_pubmed_url,
|
| 970 |
-
)
|
| 971 |
|
| 972 |
|
| 973 |
async def _search_primo_catalog(context: SearchContextPayload, limit: int = 5) -> Tuple[dict, str]:
|
|
@@ -990,8 +993,7 @@ async def _search_primo_catalog(context: SearchContextPayload, limit: int = 5) -
|
|
| 990 |
open_access=context.open_access,
|
| 991 |
year_from=context.year_from,
|
| 992 |
year_to=context.year_to,
|
| 993 |
-
resource_type=context.resource_type,
|
| 994 |
-
boolean_query=context.primo_boolean_query,
|
| 995 |
),
|
| 996 |
settings.primo_api_key,
|
| 997 |
)
|
|
@@ -1017,7 +1019,9 @@ async def _search_pubmed_articles(context: SearchContextPayload, limit: int = 5)
|
|
| 1017 |
year_to=context.year_to, # FIX: now actually used in search.py
|
| 1018 |
)
|
| 1019 |
)
|
| 1020 |
-
pubmed_url =
|
|
|
|
|
|
|
| 1021 |
return result, pubmed_url
|
| 1022 |
|
| 1023 |
|
|
@@ -1026,102 +1030,134 @@ async def _search_pubmed_articles(context: SearchContextPayload, limit: int = 5)
|
|
| 1026 |
def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
|
| 1027 |
ai_query = quote(context.ai_tool_query or context.display_topic or context.topic)
|
| 1028 |
clean_topic = _strip_resource_noise(context.topic)
|
| 1029 |
-
semantic_q = quote((context.ai_tool_query or context.display_topic or context.topic).replace(" ", "+"))
|
| 1030 |
return {
|
| 1031 |
"leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}",
|
| 1032 |
"scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}",
|
| 1033 |
-
"ebsco_ai": f"https://research-ebsco-com.khalifa.idm.oclc.org/c/ntdlkp/search?q={ai_query}",
|
| 1034 |
-
"primo_ai": f"{PRIMO_AI_URL}{ai_query}",
|
| 1035 |
"consensus": f"https://consensus.app/results/?q={ai_query}",
|
| 1036 |
-
"openalex": f"https://openalex.org/works?page=1&sort=relevance_score:desc&search.semantic={semantic_q}",
|
| 1037 |
-
"scispace": f"https://scispace.com/search?q={ai_query}",
|
| 1038 |
-
"perplexity": f"https://www.perplexity.ai/search?q={ai_query}",
|
| 1039 |
"primo_discovery": _primo_clean_url(context),
|
| 1040 |
-
"pubmed": build_pubmed_url(clean_topic, context.year_from, context.year_to) if context.intent == "search_medical" else "",
|
| 1041 |
}
|
| 1042 |
|
| 1043 |
|
| 1044 |
def _database_guidance_for_topic(context: SearchContextPayload) -> str:
|
| 1045 |
topic = _escape(context.display_topic or context.topic)
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
f'<a href="{urls["consensus"]}" target="_blank" style="color:#86198f;font-weight:700;text-decoration:none">Consensus</a> · '
|
| 1053 |
-
f'<a href="{urls["openalex"]}" target="_blank" style="color:#0c4a6e;font-weight:700;text-decoration:none">OpenAlex</a>'
|
| 1054 |
-
)
|
| 1055 |
-
more_results = f'<a href="{urls["primo_discovery"]}" target="_blank" style="color:#5b21b6;font-weight:700;text-decoration:none">PRIMO Library Discovery</a>'
|
| 1056 |
-
if context.intent == "search_medical" and urls["pubmed"]:
|
| 1057 |
-
more_results += f' · <a href="{urls["pubmed"]}" target="_blank" style="color:#065f46;font-weight:700;text-decoration:none">PubMed</a>'
|
| 1058 |
return (
|
| 1059 |
-
f
|
| 1060 |
-
|
|
|
|
| 1061 |
)
|
| 1062 |
|
| 1063 |
|
|
|
|
|
|
|
| 1064 |
|
| 1065 |
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1071 |
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
# Public services for general library help
|
| 1076 |
-
if intent == "library_info":
|
| 1077 |
-
lines.append('Public services: Muna Al Blooshi · <a href="mailto:muna.alblooshi@ku.ac.ae">muna.alblooshi@ku.ac.ae</a> · +971 2 312 4212')
|
| 1078 |
-
# E-resources contact for access/search/database/full-text issues
|
| 1079 |
-
if intent in {"search_academic", "search_medical"} or any(t in q for t in ["database", "e-resource", "eresource", "access", "full text", "proxy", "login", "remote access", "primo", "pubmed"]):
|
| 1080 |
-
lines.append('E-resources: Rani Anand · <a href="mailto:rani.anand@ku.ac.ae">rani.anand@ku.ac.ae</a> · +971 2 312 3935')
|
| 1081 |
-
# Research support for scholarly/research questions
|
| 1082 |
-
if intent in {"search_academic", "search_medical"} or any(t in q for t in ["research", "literature review", "citation", "orcid", "open access", "refworks", "bibliometrics"]):
|
| 1083 |
-
lines.append('Research support: Nikesh Narayanan · <a href="mailto:nikesh.narayanan@ku.ac.ae">nikesh.narayanan@ku.ac.ae</a> · +971 2 312 3980')
|
| 1084 |
-
# Systems / technology contact
|
| 1085 |
-
if any(t in q for t in ["system", "systems", "website", "technology", "technical", "error", "broken"]):
|
| 1086 |
-
lines.append('Systems: Walter Brian Hall · <a href="mailto:walter.hall@ku.ac.ae">walter.hall@ku.ac.ae</a> · +971 2 312 3163')
|
| 1087 |
-
dedup = []
|
| 1088 |
-
seen = set()
|
| 1089 |
-
for line in lines:
|
| 1090 |
-
if line not in seen:
|
| 1091 |
-
seen.add(line)
|
| 1092 |
-
dedup.append(line)
|
| 1093 |
-
html_lines = ''.join(f'<div>{line}</div>' for line in dedup)
|
| 1094 |
return (
|
| 1095 |
-
'<div style="margin-top:
|
| 1096 |
-
'<strong>Library contact points</strong>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1097 |
)
|
| 1098 |
|
| 1099 |
|
| 1100 |
-
def
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
|
| 1126 |
# ── Research Snapshot ─────────────────────────────────────────────────────────
|
| 1127 |
|
|
@@ -1162,7 +1198,6 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
|
|
| 1162 |
answer += _source_badge("semantic", "https://www.semanticscholar.org/")
|
| 1163 |
if search_results:
|
| 1164 |
answer += _source_badge("primo", primo_url)
|
| 1165 |
-
answer = _scholarly_trace_html(topic, context, "snapshot") + answer + "<br><br>" + _database_guidance_for_topic(context) + _contact_footer(topic, context.intent, context)
|
| 1166 |
references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
|
| 1167 |
return answer, search_results, references
|
| 1168 |
|
|
@@ -1178,7 +1213,6 @@ async def _research_snapshot(context: SearchContextPayload, model: str) -> Tuple
|
|
| 1178 |
f"<strong>AI sources:</strong> {_database_guidance_for_topic(context)}"
|
| 1179 |
)
|
| 1180 |
answer += _source_badge("primo", primo_url)
|
| 1181 |
-
answer = _scholarly_trace_html(topic, context, "snapshot") + answer + "<br><br>" + _contact_footer(topic, context.intent, context)
|
| 1182 |
return answer, search_results, []
|
| 1183 |
|
| 1184 |
|
|
@@ -1269,8 +1303,6 @@ def _detect_follow_up(question: str, client_state: Optional[ClientStatePayload])
|
|
| 1269 |
return False, None, None
|
| 1270 |
explicit_action = (client_state.follow_up_action if client_state else None) or None
|
| 1271 |
if explicit_action:
|
| 1272 |
-
if explicit_action == "new_search":
|
| 1273 |
-
return False, None, None
|
| 1274 |
return True, explicit_action, base_context
|
| 1275 |
action = _parse_refinement_action(question)
|
| 1276 |
if not action:
|
|
@@ -1315,24 +1347,28 @@ def _apply_follow_up_action(base_context: SearchContextPayload, action: Optional
|
|
| 1315 |
|
| 1316 |
async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
| 1317 |
"""
|
| 1318 |
-
Generate 3 topic-specific follow-up questions
|
| 1319 |
-
These
|
|
|
|
| 1320 |
"""
|
| 1321 |
settings = get_settings()
|
| 1322 |
if not settings.openai_api_key:
|
| 1323 |
return []
|
| 1324 |
try:
|
| 1325 |
-
llm = _get_llm(model, temperature=0.5, max_tokens=
|
| 1326 |
response = await llm.ainvoke([
|
| 1327 |
{"role": "system", "content": (
|
| 1328 |
"You are a research librarian helping a user explore a topic more deeply. "
|
| 1329 |
-
"Given a research topic, generate exactly 3 short follow-up questions that explore
|
| 1330 |
-
"
|
| 1331 |
-
"
|
| 1332 |
-
"
|
| 1333 |
-
"
|
| 1334 |
-
"
|
| 1335 |
-
"
|
|
|
|
|
|
|
|
|
|
| 1336 |
)},
|
| 1337 |
{"role": "user", "content": f"Topic: {topic}"},
|
| 1338 |
])
|
|
@@ -1342,26 +1378,16 @@ async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
|
| 1342 |
questions = json.loads(raw)
|
| 1343 |
if not isinstance(questions, list):
|
| 1344 |
return []
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
if
|
| 1349 |
-
|
| 1350 |
-
clean = q.strip().rstrip('?')
|
| 1351 |
-
if topic_l not in clean.lower():
|
| 1352 |
-
clean = f"{clean} for {topic}"
|
| 1353 |
-
full_q = clean + "?"
|
| 1354 |
-
out.append({
|
| 1355 |
-
"label": full_q,
|
| 1356 |
-
"question": f"Find research articles on {full_q}",
|
| 1357 |
-
})
|
| 1358 |
-
return out
|
| 1359 |
except Exception as e:
|
| 1360 |
logger.warning(f"Topic follow-up generation failed: {e}")
|
| 1361 |
return []
|
| 1362 |
|
| 1363 |
|
| 1364 |
-
|
| 1365 |
async def _search_follow_up(
|
| 1366 |
context: SearchContextPayload, model: str, summary_mode: bool = False
|
| 1367 |
) -> Tuple[str, List[dict]]:
|
|
@@ -1513,7 +1539,7 @@ async def _run_search_mode(
|
|
| 1513 |
search_results = result.get("results", []) or []
|
| 1514 |
total = result.get("total", len(search_results))
|
| 1515 |
|
| 1516 |
-
answer =
|
| 1517 |
if filters:
|
| 1518 |
answer += f"<br><br><strong>Current filters:</strong> {_escape(filters)}."
|
| 1519 |
if search_results:
|
|
@@ -1526,9 +1552,7 @@ async def _run_search_mode(
|
|
| 1526 |
if context.intent == "search_medical" and context.source != "pubmed":
|
| 1527 |
answer += "<br><br>For the strongest medical evidence trail, I also recommend checking <strong>PubMed</strong> or <strong>Embase</strong>."
|
| 1528 |
|
| 1529 |
-
answer += "<br><br>" + _database_guidance_for_topic(context)
|
| 1530 |
answer += _source_badge(source_name, source_url)
|
| 1531 |
-
answer += _contact_footer(question, context.intent, context)
|
| 1532 |
sources = []
|
| 1533 |
for item in search_results[:6]:
|
| 1534 |
link = item.get("primo_url") or item.get("link")
|
|
@@ -1595,6 +1619,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1595 |
response_time=time.time() - start_time,
|
| 1596 |
follow_up_question=follow_up_question,
|
| 1597 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1598 |
)
|
| 1599 |
|
| 1600 |
staff_role_match = match_staff_role(question) if should_attempt_staff_lookup(question) else None
|
|
@@ -1609,6 +1634,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1609 |
response_time=time.time() - start_time,
|
| 1610 |
follow_up_question=follow_up_question,
|
| 1611 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1612 |
)
|
| 1613 |
|
| 1614 |
if _is_greeting_menu_followup(question, history):
|
|
@@ -1626,7 +1652,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1626 |
if _looks_library_hours_question(question):
|
| 1627 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 1628 |
return AgentResponse(
|
| 1629 |
-
answer=_hours_answer() + _source_badge("ku_library", LIBRARY_HOURS_URL)
|
| 1630 |
intent="library_info",
|
| 1631 |
tools_used=["hours_hardcoded"],
|
| 1632 |
ask_librarian=False,
|
|
@@ -1634,12 +1660,13 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1634 |
response_time=time.time() - start_time,
|
| 1635 |
follow_up_question=follow_up_question,
|
| 1636 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1637 |
)
|
| 1638 |
|
| 1639 |
if _looks_campus_question(question):
|
| 1640 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 1641 |
return AgentResponse(
|
| 1642 |
-
answer=_campus_answer() + _source_badge("ku_library", "https://library.ku.ac.ae/")
|
| 1643 |
intent="library_info",
|
| 1644 |
tools_used=["campus_hardcoded"],
|
| 1645 |
ask_librarian=False,
|
|
@@ -1647,6 +1674,27 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1647 |
response_time=time.time() - start_time,
|
| 1648 |
follow_up_question=follow_up_question,
|
| 1649 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1650 |
)
|
| 1651 |
|
| 1652 |
is_follow_up, follow_up_action, base_context = _detect_follow_up(question, client_state)
|
|
@@ -1683,12 +1731,13 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1683 |
ai_tool_query=context.ai_tool_query,
|
| 1684 |
primo_boolean_query=context.primo_boolean_query,
|
| 1685 |
primo_search_url=_primo_clean_url(context),
|
| 1686 |
-
primo_ai_url=_tool_urls(context).get("primo_ai"),
|
| 1687 |
-
pubmed_search_url=_tool_urls(context).get("pubmed"),
|
| 1688 |
follow_up_question=follow_up_question,
|
| 1689 |
follow_up_suggestions=follow_up_suggestions,
|
| 1690 |
search_context=_context_to_dict(context),
|
| 1691 |
is_follow_up=True,
|
|
|
|
|
|
|
|
|
|
| 1692 |
)
|
| 1693 |
|
| 1694 |
answer, search_results, sources, source_url = await _run_search_mode(question, context, model, True)
|
|
@@ -1707,12 +1756,13 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1707 |
ai_tool_query=context.ai_tool_query,
|
| 1708 |
primo_boolean_query=context.primo_boolean_query,
|
| 1709 |
primo_search_url=source_url,
|
| 1710 |
-
primo_ai_url=_tool_urls(context).get("primo_ai"),
|
| 1711 |
-
pubmed_search_url=_tool_urls(context).get("pubmed"),
|
| 1712 |
follow_up_question=follow_up_question,
|
| 1713 |
follow_up_suggestions=follow_up_suggestions,
|
| 1714 |
search_context=_context_to_dict(context),
|
| 1715 |
is_follow_up=True,
|
|
|
|
|
|
|
|
|
|
| 1716 |
)
|
| 1717 |
|
| 1718 |
cls_result = await _llm_classify(question, history, model)
|
|
@@ -1749,13 +1799,11 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1749 |
f'🔗 <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a>'
|
| 1750 |
)
|
| 1751 |
answer += _source_badge("ku_library", ASK_LIBRARIAN_URL)
|
| 1752 |
-
answer += _contact_footer(question, "library_info")
|
| 1753 |
sources = []
|
| 1754 |
tools_used = ["rag_no_results"]
|
| 1755 |
else:
|
| 1756 |
answer = await _rag_answer(question, rag_results, history, model)
|
| 1757 |
answer += _source_badge("rag", "https://library.ku.ac.ae/")
|
| 1758 |
-
answer += _contact_footer(question, "library_info")
|
| 1759 |
sources = [{"title": r.get("title", "Library KB"), "url": r.get("source", "")} for r in rag_results if r.get("source")]
|
| 1760 |
tools_used = ["rag_hybrid_search"]
|
| 1761 |
|
|
@@ -1770,6 +1818,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1770 |
response_time=time.time() - start_time,
|
| 1771 |
follow_up_question=follow_up_question,
|
| 1772 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1773 |
)
|
| 1774 |
|
| 1775 |
if intent in ("search_academic", "search_medical"):
|
|
@@ -1798,6 +1847,8 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1798 |
|
| 1799 |
if summary_mode or deep_research_mode:
|
| 1800 |
answer, search_results, sources = await _research_snapshot(context, model)
|
|
|
|
|
|
|
| 1801 |
follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
|
| 1802 |
_safe_metrics_increment("search_handoffs")
|
| 1803 |
return AgentResponse(
|
|
@@ -1814,11 +1865,12 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1814 |
ai_tool_query=context.ai_tool_query,
|
| 1815 |
primo_boolean_query=context.primo_boolean_query,
|
| 1816 |
primo_search_url=_primo_clean_url(context),
|
| 1817 |
-
primo_ai_url=_tool_urls(context).get("primo_ai"),
|
| 1818 |
-
pubmed_search_url=_tool_urls(context).get("pubmed"),
|
| 1819 |
follow_up_question=follow_up_question,
|
| 1820 |
follow_up_suggestions=follow_up_suggestions,
|
| 1821 |
search_context=_context_to_dict(context),
|
|
|
|
|
|
|
|
|
|
| 1822 |
)
|
| 1823 |
|
| 1824 |
answer, search_results, sources, source_url = await _run_search_mode(question, context, model, False)
|
|
@@ -1837,11 +1889,12 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1837 |
ai_tool_query=context.ai_tool_query,
|
| 1838 |
primo_boolean_query=context.primo_boolean_query,
|
| 1839 |
primo_search_url=source_url,
|
| 1840 |
-
primo_ai_url=_tool_urls(context).get("primo_ai"),
|
| 1841 |
-
pubmed_search_url=_tool_urls(context).get("pubmed"),
|
| 1842 |
follow_up_question=follow_up_question,
|
| 1843 |
follow_up_suggestions=follow_up_suggestions,
|
| 1844 |
search_context=_context_to_dict(context),
|
|
|
|
|
|
|
|
|
|
| 1845 |
)
|
| 1846 |
|
| 1847 |
if intent == "general_recent":
|
|
@@ -1855,6 +1908,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1855 |
response_time=time.time() - start_time,
|
| 1856 |
follow_up_question=follow_up_question,
|
| 1857 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1858 |
)
|
| 1859 |
|
| 1860 |
answer = await _web_search_answer(question, history, model) + _source_badge("web")
|
|
@@ -1867,6 +1921,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 1867 |
response_time=time.time() - start_time,
|
| 1868 |
follow_up_question=follow_up_question,
|
| 1869 |
follow_up_suggestions=follow_up_suggestions,
|
|
|
|
| 1870 |
)
|
| 1871 |
|
| 1872 |
|
|
|
|
| 114 |
primo_search_url: Optional[str] = None
|
| 115 |
primo_ai_url: Optional[str] = None
|
| 116 |
pubmed_search_url: Optional[str] = None
|
| 117 |
+
contact_html: Optional[str] = None
|
|
|
|
| 118 |
|
| 119 |
|
| 120 |
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
| 203 |
r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
|
| 204 |
re.IGNORECASE,
|
| 205 |
)
|
| 206 |
+
DATABASE_RECOMMENDATION_RE = re.compile(
|
| 207 |
+
r"\b(best database|best databases|which database|which databases|recommended databases?|database should i use|databases should i use|database for|databases for)\b",
|
| 208 |
+
re.IGNORECASE,
|
| 209 |
+
)
|
| 210 |
SUMMARY_RE = re.compile(
|
| 211 |
r"\b(summar(y|ize|ise|izing|ising)|overview|explain|what is|what are|describe"
|
| 212 |
r"|tell me about|give me a summary|give me an overview|give me a brief"
|
|
|
|
| 731 |
|
| 732 |
|
| 733 |
def _make_primo_boolean_query(context: SearchContextPayload) -> str:
|
| 734 |
+
"""
|
| 735 |
+
FIX: Build a clean boolean for display purposes ONLY.
|
| 736 |
+
Contains ONLY topic keywords — NO resource-type words.
|
| 737 |
+
Resource type is expressed as a PRIMO facet in search_primo(), never here.
|
| 738 |
+
"""
|
| 739 |
topic = _title_case_topic(context.display_topic or context.topic)
|
| 740 |
+
# Strip any remaining noise one final time
|
| 741 |
topic = _strip_resource_noise(topic)
|
| 742 |
if not topic:
|
| 743 |
topic = context.topic or "library search"
|
| 744 |
+
return f'"{_normalize_whitespace(topic)}"'
|
| 745 |
|
| 746 |
|
| 747 |
def _primo_clean_url(context: SearchContextPayload) -> str:
|
| 748 |
+
"""
|
| 749 |
+
Build the PRIMO discovery URL using the clean topic — matching exactly what the API searches.
|
| 750 |
+
"""
|
| 751 |
+
clean_topic = _strip_resource_noise(context.topic)
|
| 752 |
+
if not clean_topic:
|
| 753 |
+
clean_topic = context.topic
|
| 754 |
+
return f"{PRIMO_BASE_URL}&query=any,contains,{quote(clean_topic)}&lang=en&mode=advanced&offset=0"
|
|
|
|
|
|
|
| 755 |
|
| 756 |
|
| 757 |
def _topic_keywords(text: str) -> List[str]:
|
|
|
|
| 970 |
|
| 971 |
# ── PRIMO / PubMed Search Wrappers ────────────────────────────────────────────
|
| 972 |
|
| 973 |
+
from src.api.search import SearchRequest, search_primo, search_pubmed
|
|
|
|
|
|
|
|
|
|
| 974 |
|
| 975 |
|
| 976 |
async def _search_primo_catalog(context: SearchContextPayload, limit: int = 5) -> Tuple[dict, str]:
|
|
|
|
| 993 |
open_access=context.open_access,
|
| 994 |
year_from=context.year_from,
|
| 995 |
year_to=context.year_to,
|
| 996 |
+
resource_type=context.resource_type, # FIX: was missing — resource type now goes as facet
|
|
|
|
| 997 |
),
|
| 998 |
settings.primo_api_key,
|
| 999 |
)
|
|
|
|
| 1019 |
year_to=context.year_to, # FIX: now actually used in search.py
|
| 1020 |
)
|
| 1021 |
)
|
| 1022 |
+
pubmed_url = f"https://pubmed.ncbi.nlm.nih.gov/?term={quote(clean_topic)}"
|
| 1023 |
+
if context.year_from and context.year_to:
|
| 1024 |
+
pubmed_url += f"&filter=datesearch.y_custom&dates.from={context.year_from}&dates.to={context.year_to}"
|
| 1025 |
return result, pubmed_url
|
| 1026 |
|
| 1027 |
|
|
|
|
| 1030 |
def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
|
| 1031 |
ai_query = quote(context.ai_tool_query or context.display_topic or context.topic)
|
| 1032 |
clean_topic = _strip_resource_noise(context.topic)
|
|
|
|
| 1033 |
return {
|
| 1034 |
"leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}",
|
| 1035 |
"scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}",
|
|
|
|
|
|
|
| 1036 |
"consensus": f"https://consensus.app/results/?q={ai_query}",
|
|
|
|
|
|
|
|
|
|
| 1037 |
"primo_discovery": _primo_clean_url(context),
|
|
|
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
|
| 1041 |
def _database_guidance_for_topic(context: SearchContextPayload) -> str:
|
| 1042 |
topic = _escape(context.display_topic or context.topic)
|
| 1043 |
+
if context.intent == "search_medical":
|
| 1044 |
+
return (
|
| 1045 |
+
f"For deeper research on <strong>{topic}</strong>, the KU-subscribed AI research tools and freemium tools are listed at the end of this answer. "
|
| 1046 |
+
"Direct links to PRIMO Library Discovery and PubMed are also provided there. "
|
| 1047 |
+
"Some freemium tools may require account creation."
|
| 1048 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1049 |
return (
|
| 1050 |
+
f"For deeper research on <strong>{topic}</strong>, the KU-subscribed AI research tools and freemium tools are listed at the end of this answer. "
|
| 1051 |
+
"Direct links to PRIMO Library Discovery and, for medical topics, PubMed are also provided there. "
|
| 1052 |
+
"Some freemium tools may require account creation."
|
| 1053 |
)
|
| 1054 |
|
| 1055 |
|
| 1056 |
+
def _is_database_recommendation_question(question: str) -> bool:
|
| 1057 |
+
return bool(DATABASE_RECOMMENDATION_RE.search(question or ""))
|
| 1058 |
|
| 1059 |
|
| 1060 |
+
def _primary_contact_for_question(question: str, intent: str) -> dict:
|
| 1061 |
+
q = (question or '').lower()
|
| 1062 |
+
walter = {
|
| 1063 |
+
'label': 'Systems & website support',
|
| 1064 |
+
'name': 'Walter Brian Hall',
|
| 1065 |
+
'role': 'Digital & Technology Services Librarian / Systems Librarian',
|
| 1066 |
+
'email': 'walter.hall@ku.ac.ae',
|
| 1067 |
+
'phone': '+971 2 312 3163',
|
| 1068 |
+
}
|
| 1069 |
+
rani = {
|
| 1070 |
+
'label': 'E-resources support',
|
| 1071 |
+
'name': 'Rani Anand',
|
| 1072 |
+
'role': 'E-Resources Librarian',
|
| 1073 |
+
'email': 'rani.anand@ku.ac.ae',
|
| 1074 |
+
'phone': '+971 2 312 3935',
|
| 1075 |
+
}
|
| 1076 |
+
nikesh = {
|
| 1077 |
+
'label': 'Research support',
|
| 1078 |
+
'name': 'Nikesh Narayanan',
|
| 1079 |
+
'role': 'Research & Access Services Librarian',
|
| 1080 |
+
'email': 'nikesh.narayanan@ku.ac.ae',
|
| 1081 |
+
'phone': '+971 2 312 3980',
|
| 1082 |
+
}
|
| 1083 |
+
muna = {
|
| 1084 |
+
'label': 'Public services',
|
| 1085 |
+
'name': 'Muna Ahmad Mohammad Al Blooshi',
|
| 1086 |
+
'role': 'Manager, Public Services',
|
| 1087 |
+
'email': 'muna.alblooshi@ku.ac.ae',
|
| 1088 |
+
'phone': '+971 2 312 4212',
|
| 1089 |
+
}
|
| 1090 |
|
| 1091 |
+
if any(t in q for t in ['system', 'systems', 'website', 'web site', 'technical', 'technology', 'digital', 'error', 'bug', 'broken page', 'page not loading']):
|
| 1092 |
+
return walter
|
| 1093 |
+
if any(t in q for t in ['database access', 'off campus', 'remote access', 'proxy', 'login problem', 'login issue', 'cannot access', "can't access", 'full text', 'libkey', 'vendor', 'e-resource', 'eresource', 'subscription access']):
|
| 1094 |
+
return rani
|
| 1095 |
+
if intent in {'search_academic', 'search_medical'} or any(t in q for t in ['research', 'literature', 'systematic review', 'orcid', 'open access', 'apc', 'bibliometric', 'scopus', 'scival', 'impact', 'citation', 'ai tool', 'best database', 'best databases', 'which database', 'which databases']):
|
| 1096 |
+
return nikesh
|
| 1097 |
+
return muna
|
| 1098 |
|
| 1099 |
+
|
| 1100 |
+
def _contact_footer_html(question: str, intent: str) -> str:
|
| 1101 |
+
primary = _primary_contact_for_question(question, intent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1102 |
return (
|
| 1103 |
+
'<div style="margin-top:14px;padding:12px 14px;background:#f8fafc;border:1px solid #dbeafe;border-radius:12px;font-size:.8rem;line-height:1.6">'
|
| 1104 |
+
'<strong>Library contact points</strong><br>'
|
| 1105 |
+
f'General help: <a href="{ASK_LIBRARIAN_URL}" target="_blank" style="color:#003366;font-weight:700">Ask a Librarian</a> · '
|
| 1106 |
+
'<a href="mailto:libse@ku.ac.ae" style="color:#003366">libse@ku.ac.ae</a><br>'
|
| 1107 |
+
f'{_escape(primary["label"])}: <strong>{_escape(primary["name"])}</strong> — {_escape(primary["role"])} · '
|
| 1108 |
+
f'<a href="mailto:{primary["email"]}" style="color:#003366">{_escape(primary["email"])}</a> · '
|
| 1109 |
+
f'<a href="tel:{primary["phone"].replace(" ", "")}" style="color:#003366">{_escape(primary["phone"])}</a>'
|
| 1110 |
+
'</div>'
|
| 1111 |
)
|
| 1112 |
|
| 1113 |
|
| 1114 |
+
async def _database_recommendation_answer(question: str, history: List[ChatMessage], model: str) -> Tuple[str, List[dict]]:
|
| 1115 |
+
try:
|
| 1116 |
+
from app import rag_service
|
| 1117 |
+
rag_query = f"{question} recommended databases by subject Khalifa University Library databases"
|
| 1118 |
+
rag_results = await rag_service.hybrid_search(rag_query, top_k=5)
|
| 1119 |
+
except Exception as e:
|
| 1120 |
+
logger.error(f"Database recommendation search failed: {e}")
|
| 1121 |
+
rag_results = []
|
| 1122 |
+
|
| 1123 |
+
if not rag_results:
|
| 1124 |
+
answer = (
|
| 1125 |
+
'I could not pull the subject database guidance from the library knowledge base right now.<br><br>'
|
| 1126 |
+
'A good general starting point is <strong>PRIMO Library Discovery</strong>, then move to subject databases such as Scopus, Web of Science, IEEE Xplore, ACM Digital Library, PubMed, or Embase depending on your topic.'
|
| 1127 |
+
)
|
| 1128 |
+
return answer + _source_badge('ku_library', 'https://library.ku.ac.ae/eresources'), []
|
| 1129 |
+
|
| 1130 |
+
context_text = "\n\n---\n\n".join(r['content'] for r in rag_results)
|
| 1131 |
+
settings = get_settings()
|
| 1132 |
+
if not settings.openai_api_key:
|
| 1133 |
+
answer = 'I found KU Library database guidance for this subject in the knowledge base.'
|
| 1134 |
+
answer += _source_badge('rag', 'https://library.ku.ac.ae/eresources')
|
| 1135 |
+
sources = [{'title': r.get('title', 'Library guide'), 'url': r.get('source', '')} for r in rag_results if r.get('source')]
|
| 1136 |
+
return answer, sources
|
| 1137 |
+
|
| 1138 |
+
system = (
|
| 1139 |
+
'You are LibBee, the Khalifa University Library AI Assistant. '
|
| 1140 |
+
'Answer as an expert academic librarian using ONLY the context provided. '
|
| 1141 |
+
"Recommend the best KU databases for the user's subject. "
|
| 1142 |
+
'Start with one short sentence. Then list 3 to 6 databases using HTML <br>, with each database name in <strong> and a brief explanation of why or when to use it. '
|
| 1143 |
+
'Where appropriate, include PRIMO Library Discovery as a starting point and then subject-specific databases. '
|
| 1144 |
+
'End with one short sentence offering to search directly in PRIMO Library Discovery.'
|
| 1145 |
)
|
| 1146 |
+
try:
|
| 1147 |
+
llm = _get_llm(model, temperature=0.2, max_tokens=420)
|
| 1148 |
+
response = await llm.ainvoke([
|
| 1149 |
+
{'role': 'system', 'content': system + "\n\nCONTEXT:\n" + context_text},
|
| 1150 |
+
{'role': 'user', 'content': question},
|
| 1151 |
+
])
|
| 1152 |
+
answer = response.content.strip()
|
| 1153 |
+
except Exception as e:
|
| 1154 |
+
logger.error(f"Database recommendation answer failed: {e}")
|
| 1155 |
+
answer = 'I found subject-specific database guidance in the library knowledge base, but I had trouble formatting it into a recommendation right now.'
|
| 1156 |
+
|
| 1157 |
+
answer += _source_badge('rag', 'https://library.ku.ac.ae/eresources')
|
| 1158 |
+
sources = [{'title': r.get('title', 'Library guide'), 'url': r.get('source', '')} for r in rag_results if r.get('source')]
|
| 1159 |
+
return answer, sources
|
| 1160 |
+
|
| 1161 |
|
| 1162 |
# ── Research Snapshot ─────────────────────────────────────────────────────────
|
| 1163 |
|
|
|
|
| 1198 |
answer += _source_badge("semantic", "https://www.semanticscholar.org/")
|
| 1199 |
if search_results:
|
| 1200 |
answer += _source_badge("primo", primo_url)
|
|
|
|
| 1201 |
references = [{"title": p["title"], "url": p["link"]} for p in papers[:6]]
|
| 1202 |
return answer, search_results, references
|
| 1203 |
|
|
|
|
| 1213 |
f"<strong>AI sources:</strong> {_database_guidance_for_topic(context)}"
|
| 1214 |
)
|
| 1215 |
answer += _source_badge("primo", primo_url)
|
|
|
|
| 1216 |
return answer, search_results, []
|
| 1217 |
|
| 1218 |
|
|
|
|
| 1303 |
return False, None, None
|
| 1304 |
explicit_action = (client_state.follow_up_action if client_state else None) or None
|
| 1305 |
if explicit_action:
|
|
|
|
|
|
|
| 1306 |
return True, explicit_action, base_context
|
| 1307 |
action = _parse_refinement_action(question)
|
| 1308 |
if not action:
|
|
|
|
| 1347 |
|
| 1348 |
async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
| 1349 |
"""
|
| 1350 |
+
FIX: Generate 3 topic-specific intellectual follow-up questions using LLM.
|
| 1351 |
+
These are ASPECT-based (related subtopics, methodological angles, applications)
|
| 1352 |
+
not generic filter suggestions like "peer reviewed only".
|
| 1353 |
"""
|
| 1354 |
settings = get_settings()
|
| 1355 |
if not settings.openai_api_key:
|
| 1356 |
return []
|
| 1357 |
try:
|
| 1358 |
+
llm = _get_llm(model, temperature=0.5, max_tokens=200)
|
| 1359 |
response = await llm.ainvoke([
|
| 1360 |
{"role": "system", "content": (
|
| 1361 |
"You are a research librarian helping a user explore a topic more deeply. "
|
| 1362 |
+
"Given a research topic, generate exactly 3 short follow-up questions that explore "
|
| 1363 |
+
"DIFFERENT ASPECTS of the topic — such as subtopics, methodological angles, "
|
| 1364 |
+
"applications, comparisons, or related fields. "
|
| 1365 |
+
"Do NOT suggest filter actions like peer-reviewed, date ranges, or format changes. "
|
| 1366 |
+
"Return ONLY a JSON array of 3 strings, each under 12 words. "
|
| 1367 |
+
"No explanation, no preamble, no markdown.\n"
|
| 1368 |
+
"Example for 'quantum computing':\n"
|
| 1369 |
+
'[\"What are the main hardware approaches in quantum computing?\", '
|
| 1370 |
+
'"How does quantum error correction work?\", '
|
| 1371 |
+
'"Applications of quantum computing in cryptography\"]'
|
| 1372 |
)},
|
| 1373 |
{"role": "user", "content": f"Topic: {topic}"},
|
| 1374 |
])
|
|
|
|
| 1378 |
questions = json.loads(raw)
|
| 1379 |
if not isinstance(questions, list):
|
| 1380 |
return []
|
| 1381 |
+
return [
|
| 1382 |
+
{"label": q.strip("?") + "?", "question": f"Find research articles on {_normalize_whitespace(q.strip().rstrip("?"))} in the context of {topic}", "action": None}
|
| 1383 |
+
for q in questions[:3]
|
| 1384 |
+
if isinstance(q, str) and q.strip()
|
| 1385 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1386 |
except Exception as e:
|
| 1387 |
logger.warning(f"Topic follow-up generation failed: {e}")
|
| 1388 |
return []
|
| 1389 |
|
| 1390 |
|
|
|
|
| 1391 |
async def _search_follow_up(
|
| 1392 |
context: SearchContextPayload, model: str, summary_mode: bool = False
|
| 1393 |
) -> Tuple[str, List[dict]]:
|
|
|
|
| 1539 |
search_results = result.get("results", []) or []
|
| 1540 |
total = result.get("total", len(search_results))
|
| 1541 |
|
| 1542 |
+
answer = _search_answer_intro(context, is_follow_up)
|
| 1543 |
if filters:
|
| 1544 |
answer += f"<br><br><strong>Current filters:</strong> {_escape(filters)}."
|
| 1545 |
if search_results:
|
|
|
|
| 1552 |
if context.intent == "search_medical" and context.source != "pubmed":
|
| 1553 |
answer += "<br><br>For the strongest medical evidence trail, I also recommend checking <strong>PubMed</strong> or <strong>Embase</strong>."
|
| 1554 |
|
|
|
|
| 1555 |
answer += _source_badge(source_name, source_url)
|
|
|
|
| 1556 |
sources = []
|
| 1557 |
for item in search_results[:6]:
|
| 1558 |
link = item.get("primo_url") or item.get("link")
|
|
|
|
| 1619 |
response_time=time.time() - start_time,
|
| 1620 |
follow_up_question=follow_up_question,
|
| 1621 |
follow_up_suggestions=follow_up_suggestions,
|
| 1622 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1623 |
)
|
| 1624 |
|
| 1625 |
staff_role_match = match_staff_role(question) if should_attempt_staff_lookup(question) else None
|
|
|
|
| 1634 |
response_time=time.time() - start_time,
|
| 1635 |
follow_up_question=follow_up_question,
|
| 1636 |
follow_up_suggestions=follow_up_suggestions,
|
| 1637 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1638 |
)
|
| 1639 |
|
| 1640 |
if _is_greeting_menu_followup(question, history):
|
|
|
|
| 1652 |
if _looks_library_hours_question(question):
|
| 1653 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 1654 |
return AgentResponse(
|
| 1655 |
+
answer=_hours_answer() + _source_badge("ku_library", LIBRARY_HOURS_URL),
|
| 1656 |
intent="library_info",
|
| 1657 |
tools_used=["hours_hardcoded"],
|
| 1658 |
ask_librarian=False,
|
|
|
|
| 1660 |
response_time=time.time() - start_time,
|
| 1661 |
follow_up_question=follow_up_question,
|
| 1662 |
follow_up_suggestions=follow_up_suggestions,
|
| 1663 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1664 |
)
|
| 1665 |
|
| 1666 |
if _looks_campus_question(question):
|
| 1667 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 1668 |
return AgentResponse(
|
| 1669 |
+
answer=_campus_answer() + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 1670 |
intent="library_info",
|
| 1671 |
tools_used=["campus_hardcoded"],
|
| 1672 |
ask_librarian=False,
|
|
|
|
| 1674 |
response_time=time.time() - start_time,
|
| 1675 |
follow_up_question=follow_up_question,
|
| 1676 |
follow_up_suggestions=follow_up_suggestions,
|
| 1677 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1678 |
+
)
|
| 1679 |
+
|
| 1680 |
+
if _is_database_recommendation_question(question):
|
| 1681 |
+
answer, sources = await _database_recommendation_answer(question, history, model)
|
| 1682 |
+
follow_up_question = "Would you like me to search one of these databases directly or show the AI research tools for this topic?"
|
| 1683 |
+
follow_up_suggestions = [
|
| 1684 |
+
{"label": "Search PRIMO Library Discovery", "question": "Search PRIMO Library Discovery for this topic"},
|
| 1685 |
+
{"label": "Show AI research tools", "question": "Show the AI research tools for this topic"},
|
| 1686 |
+
]
|
| 1687 |
+
return AgentResponse(
|
| 1688 |
+
answer=answer,
|
| 1689 |
+
intent="library_info",
|
| 1690 |
+
tools_used=["database_recommendation_rag"],
|
| 1691 |
+
sources=sources,
|
| 1692 |
+
ask_librarian=False,
|
| 1693 |
+
model_used=model,
|
| 1694 |
+
response_time=time.time() - start_time,
|
| 1695 |
+
follow_up_question=follow_up_question,
|
| 1696 |
+
follow_up_suggestions=follow_up_suggestions,
|
| 1697 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1698 |
)
|
| 1699 |
|
| 1700 |
is_follow_up, follow_up_action, base_context = _detect_follow_up(question, client_state)
|
|
|
|
| 1731 |
ai_tool_query=context.ai_tool_query,
|
| 1732 |
primo_boolean_query=context.primo_boolean_query,
|
| 1733 |
primo_search_url=_primo_clean_url(context),
|
|
|
|
|
|
|
| 1734 |
follow_up_question=follow_up_question,
|
| 1735 |
follow_up_suggestions=follow_up_suggestions,
|
| 1736 |
search_context=_context_to_dict(context),
|
| 1737 |
is_follow_up=True,
|
| 1738 |
+
primo_ai_url=f"{PRIMO_AI_URL}{quote(context.ai_tool_query or context.topic)}",
|
| 1739 |
+
pubmed_search_url=(f"https://pubmed.ncbi.nlm.nih.gov/?term={quote(context.topic)}" if context.intent == "search_medical" else None),
|
| 1740 |
+
contact_html=_contact_footer_html(question, context.intent),
|
| 1741 |
)
|
| 1742 |
|
| 1743 |
answer, search_results, sources, source_url = await _run_search_mode(question, context, model, True)
|
|
|
|
| 1756 |
ai_tool_query=context.ai_tool_query,
|
| 1757 |
primo_boolean_query=context.primo_boolean_query,
|
| 1758 |
primo_search_url=source_url,
|
|
|
|
|
|
|
| 1759 |
follow_up_question=follow_up_question,
|
| 1760 |
follow_up_suggestions=follow_up_suggestions,
|
| 1761 |
search_context=_context_to_dict(context),
|
| 1762 |
is_follow_up=True,
|
| 1763 |
+
primo_ai_url=f"{PRIMO_AI_URL}{quote(context.ai_tool_query or context.topic)}",
|
| 1764 |
+
pubmed_search_url=(f"https://pubmed.ncbi.nlm.nih.gov/?term={quote(context.topic)}" if context.intent == "search_medical" else None),
|
| 1765 |
+
contact_html=_contact_footer_html(question, context.intent),
|
| 1766 |
)
|
| 1767 |
|
| 1768 |
cls_result = await _llm_classify(question, history, model)
|
|
|
|
| 1799 |
f'🔗 <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a>'
|
| 1800 |
)
|
| 1801 |
answer += _source_badge("ku_library", ASK_LIBRARIAN_URL)
|
|
|
|
| 1802 |
sources = []
|
| 1803 |
tools_used = ["rag_no_results"]
|
| 1804 |
else:
|
| 1805 |
answer = await _rag_answer(question, rag_results, history, model)
|
| 1806 |
answer += _source_badge("rag", "https://library.ku.ac.ae/")
|
|
|
|
| 1807 |
sources = [{"title": r.get("title", "Library KB"), "url": r.get("source", "")} for r in rag_results if r.get("source")]
|
| 1808 |
tools_used = ["rag_hybrid_search"]
|
| 1809 |
|
|
|
|
| 1818 |
response_time=time.time() - start_time,
|
| 1819 |
follow_up_question=follow_up_question,
|
| 1820 |
follow_up_suggestions=follow_up_suggestions,
|
| 1821 |
+
contact_html=_contact_footer_html(question, "library_info"),
|
| 1822 |
)
|
| 1823 |
|
| 1824 |
if intent in ("search_academic", "search_medical"):
|
|
|
|
| 1847 |
|
| 1848 |
if summary_mode or deep_research_mode:
|
| 1849 |
answer, search_results, sources = await _research_snapshot(context, model)
|
| 1850 |
+
if deep_research_mode and "Research guide" not in answer:
|
| 1851 |
+
answer += f"<br><br><strong>Research guide:</strong> {_database_guidance_for_topic(context)}"
|
| 1852 |
follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
|
| 1853 |
_safe_metrics_increment("search_handoffs")
|
| 1854 |
return AgentResponse(
|
|
|
|
| 1865 |
ai_tool_query=context.ai_tool_query,
|
| 1866 |
primo_boolean_query=context.primo_boolean_query,
|
| 1867 |
primo_search_url=_primo_clean_url(context),
|
|
|
|
|
|
|
| 1868 |
follow_up_question=follow_up_question,
|
| 1869 |
follow_up_suggestions=follow_up_suggestions,
|
| 1870 |
search_context=_context_to_dict(context),
|
| 1871 |
+
primo_ai_url=f"{PRIMO_AI_URL}{quote(context.ai_tool_query or context.topic)}",
|
| 1872 |
+
pubmed_search_url=(f"https://pubmed.ncbi.nlm.nih.gov/?term={quote(context.topic)}" if intent == "search_medical" else None),
|
| 1873 |
+
contact_html=_contact_footer_html(question, intent),
|
| 1874 |
)
|
| 1875 |
|
| 1876 |
answer, search_results, sources, source_url = await _run_search_mode(question, context, model, False)
|
|
|
|
| 1889 |
ai_tool_query=context.ai_tool_query,
|
| 1890 |
primo_boolean_query=context.primo_boolean_query,
|
| 1891 |
primo_search_url=source_url,
|
|
|
|
|
|
|
| 1892 |
follow_up_question=follow_up_question,
|
| 1893 |
follow_up_suggestions=follow_up_suggestions,
|
| 1894 |
search_context=_context_to_dict(context),
|
| 1895 |
+
primo_ai_url=f"{PRIMO_AI_URL}{quote(context.ai_tool_query or context.topic)}",
|
| 1896 |
+
pubmed_search_url=(f"https://pubmed.ncbi.nlm.nih.gov/?term={quote(context.topic)}" if intent == "search_medical" else None),
|
| 1897 |
+
contact_html=_contact_footer_html(question, intent),
|
| 1898 |
)
|
| 1899 |
|
| 1900 |
if intent == "general_recent":
|
|
|
|
| 1908 |
response_time=time.time() - start_time,
|
| 1909 |
follow_up_question=follow_up_question,
|
| 1910 |
follow_up_suggestions=follow_up_suggestions,
|
| 1911 |
+
contact_html=_contact_footer_html(question, "general_recent"),
|
| 1912 |
)
|
| 1913 |
|
| 1914 |
answer = await _web_search_answer(question, history, model) + _source_badge("web")
|
|
|
|
| 1921 |
response_time=time.time() - start_time,
|
| 1922 |
follow_up_question=follow_up_question,
|
| 1923 |
follow_up_suggestions=follow_up_suggestions,
|
| 1924 |
+
contact_html=_contact_footer_html(question, "general"),
|
| 1925 |
)
|
| 1926 |
|
| 1927 |
|