Update src/api/agent.py
Browse files- src/api/agent.py +33 -20
src/api/agent.py
CHANGED
|
@@ -114,6 +114,8 @@ 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 |
|
| 119 |
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
@@ -1024,12 +1026,16 @@ async def _search_pubmed_articles(context: SearchContextPayload, limit: int = 5)
|
|
| 1024 |
def _tool_urls(context: SearchContextPayload) -> Dict[str, str]:
|
| 1025 |
ai_query = quote(context.ai_tool_query or context.display_topic or context.topic)
|
| 1026 |
clean_topic = _strip_resource_noise(context.topic)
|
|
|
|
| 1027 |
return {
|
| 1028 |
"leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}",
|
| 1029 |
"scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}",
|
| 1030 |
"ebsco_ai": f"https://research-ebsco-com.khalifa.idm.oclc.org/c/ntdlkp/search?q={ai_query}",
|
| 1031 |
"primo_ai": f"{PRIMO_AI_URL}{ai_query}",
|
| 1032 |
"consensus": f"https://consensus.app/results/?q={ai_query}",
|
|
|
|
|
|
|
|
|
|
| 1033 |
"primo_discovery": _primo_clean_url(context),
|
| 1034 |
"pubmed": build_pubmed_url(clean_topic, context.year_from, context.year_to) if context.intent == "search_medical" else "",
|
| 1035 |
}
|
|
@@ -1043,7 +1049,8 @@ def _database_guidance_for_topic(context: SearchContextPayload) -> str:
|
|
| 1043 |
f'<a href="{urls["scopus_ai"]}" target="_blank" style="color:#1e40af;font-weight:700;text-decoration:none">Scopus AI</a> · '
|
| 1044 |
f'<a href="{urls["ebsco_ai"]}" target="_blank" style="color:#9d174d;font-weight:700;text-decoration:none">EBSCO AI</a> · '
|
| 1045 |
f'<a href="{urls["primo_ai"]}" target="_blank" style="color:#5b21b6;font-weight:700;text-decoration:none">PRIMO AI Assistant</a> · '
|
| 1046 |
-
f'<a href="{urls["consensus"]}" target="_blank" style="color:#86198f;font-weight:700;text-decoration:none">Consensus</a>'
|
|
|
|
| 1047 |
)
|
| 1048 |
more_results = f'<a href="{urls["primo_discovery"]}" target="_blank" style="color:#5b21b6;font-weight:700;text-decoration:none">PRIMO Library Discovery</a>'
|
| 1049 |
if context.intent == "search_medical" and urls["pubmed"]:
|
|
@@ -1308,28 +1315,24 @@ def _apply_follow_up_action(base_context: SearchContextPayload, action: Optional
|
|
| 1308 |
|
| 1309 |
async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
| 1310 |
"""
|
| 1311 |
-
|
| 1312 |
-
These
|
| 1313 |
-
not generic filter suggestions like "peer reviewed only".
|
| 1314 |
"""
|
| 1315 |
settings = get_settings()
|
| 1316 |
if not settings.openai_api_key:
|
| 1317 |
return []
|
| 1318 |
try:
|
| 1319 |
-
llm = _get_llm(model, temperature=0.5, max_tokens=
|
| 1320 |
response = await llm.ainvoke([
|
| 1321 |
{"role": "system", "content": (
|
| 1322 |
"You are a research librarian helping a user explore a topic more deeply. "
|
| 1323 |
-
"Given a research topic, generate exactly 3 short follow-up questions that explore "
|
| 1324 |
-
"
|
| 1325 |
-
"
|
| 1326 |
-
"
|
| 1327 |
-
"
|
| 1328 |
-
"
|
| 1329 |
-
"
|
| 1330 |
-
'[\"What are the main hardware approaches in quantum computing?\", '
|
| 1331 |
-
'"How does quantum error correction work?\", '
|
| 1332 |
-
'"Applications of quantum computing in cryptography\"]'
|
| 1333 |
)},
|
| 1334 |
{"role": "user", "content": f"Topic: {topic}"},
|
| 1335 |
])
|
|
@@ -1339,16 +1342,26 @@ async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
|
| 1339 |
questions = json.loads(raw)
|
| 1340 |
if not isinstance(questions, list):
|
| 1341 |
return []
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
if isinstance(q, str)
|
| 1346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1347 |
except Exception as e:
|
| 1348 |
logger.warning(f"Topic follow-up generation failed: {e}")
|
| 1349 |
return []
|
| 1350 |
|
| 1351 |
|
|
|
|
| 1352 |
async def _search_follow_up(
|
| 1353 |
context: SearchContextPayload, model: str, summary_mode: bool = False
|
| 1354 |
) -> Tuple[str, List[dict]]:
|
|
|
|
| 114 |
primo_search_url: Optional[str] = None
|
| 115 |
primo_ai_url: Optional[str] = None
|
| 116 |
pubmed_search_url: Optional[str] = None
|
| 117 |
+
primo_ai_url: Optional[str] = None
|
| 118 |
+
pubmed_search_url: Optional[str] = None
|
| 119 |
|
| 120 |
|
| 121 |
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
| 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 |
}
|
|
|
|
| 1049 |
f'<a href="{urls["scopus_ai"]}" target="_blank" style="color:#1e40af;font-weight:700;text-decoration:none">Scopus AI</a> · '
|
| 1050 |
f'<a href="{urls["ebsco_ai"]}" target="_blank" style="color:#9d174d;font-weight:700;text-decoration:none">EBSCO AI</a> · '
|
| 1051 |
f'<a href="{urls["primo_ai"]}" target="_blank" style="color:#5b21b6;font-weight:700;text-decoration:none">PRIMO AI Assistant</a> · '
|
| 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"]:
|
|
|
|
| 1315 |
|
| 1316 |
async def _generate_topic_follow_ups(topic: str, model: str) -> List[dict]:
|
| 1317 |
"""
|
| 1318 |
+
Generate 3 topic-specific follow-up questions that ALWAYS preserve the original topic anchor.
|
| 1319 |
+
These should start a fresh contextual search, not a refinement action.
|
|
|
|
| 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=220)
|
| 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 different aspects of the SAME topic. "
|
| 1330 |
+
"Every suggestion must keep the original topic explicitly present. "
|
| 1331 |
+
"For example, if the topic is 'global warming', acceptable suggestions include "
|
| 1332 |
+
"'What are the main adaptation strategies for global warming?' and "
|
| 1333 |
+
"'How does global warming affect water security?'. "
|
| 1334 |
+
"Do NOT suggest generic filters like peer reviewed or last 5 years. "
|
| 1335 |
+
"Return ONLY a JSON array of 3 strings."
|
|
|
|
|
|
|
|
|
|
| 1336 |
)},
|
| 1337 |
{"role": "user", "content": f"Topic: {topic}"},
|
| 1338 |
])
|
|
|
|
| 1342 |
questions = json.loads(raw)
|
| 1343 |
if not isinstance(questions, list):
|
| 1344 |
return []
|
| 1345 |
+
out = []
|
| 1346 |
+
topic_l = (topic or "").lower()
|
| 1347 |
+
for q in questions[:3]:
|
| 1348 |
+
if not isinstance(q, str) or not q.strip():
|
| 1349 |
+
continue
|
| 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]]:
|