| """HTML fragment renderers: source badges, reasoning trace blocks, AI-tools footer, contacts.""" |
| import asyncio |
| import html |
| import json |
| import logging |
| import os |
| import re |
| import time |
| import uuid |
| from datetime import datetime |
| from typing import Any, Dict, List, Literal, Optional, Tuple |
| from urllib.parse import quote |
|
|
| import httpx |
| from pydantic import BaseModel, ConfigDict, Field |
|
|
| from src.config import get_settings, LIBBEE_VERSION |
| from src.services.staff_service import ( |
| STAFF_DIRECTORY, |
| match_staff_name, |
| match_staff_role, |
| should_attempt_staff_lookup, |
| staff_name_answer, |
| staff_role_answer, |
| ) |
|
|
| from src.agentcore.models import SearchContextPayload |
| from src.agentcore.constants import ASK_LIBRARIAN_URL, PRIMO_AI_URL |
| from src.agentcore.utils import ( |
| _escape, |
| _find_staff_by_token, |
| _primo_clean_url, |
| _shared_build_pubmed_url, |
| ) |
|
|
| logger = logging.getLogger(__name__) |
|
|
| def _source_badge(source: str, url: str = "") -> str: |
| styles = { |
| "rag": ("📖", "KU Library Knowledge Base", "#fef3c7", "#92400e"), |
| "primo": ("📚", "KU Library PRIMO", "#eff6ff", "#1e40af"), |
| "semantic": ("🎓", "Semantic Scholar", "#f0fdf4", "#166534"), |
| "consensus_badge": ("🧬", "Consensus", "#fdf4ff", "#86198f"), |
| "semantic_badge": ("🎓", "Semantic Scholar", "#f0fdf4", "#166534"), |
| "pubmed": ("🏥", "PubMed", "#d1fae5", "#065f46"), |
| "web": ("🌐", "Web Search", "#f5f3ff", "#4c1d95"), |
| "web_live": ("🌐", "Web Search · Live", "#fdf4ff", "#86198f"), |
| "libbee": ("🐝", "LibBee", "#fef9c3", "#713f12"), |
| "ku_library": ("📋", "KU Library", "#eff6ff", "#1e40af"), |
| "llm": ("🤖", "AI Knowledge Base", "#f3f4f6", "#374151"), |
| } |
| icon, label, bg, fg = styles.get(source, ("ℹ️", source, "#f3f4f6", "#374151")) |
| inner = f"{icon} Source: {label}" |
| if url: |
| inner = f'<a href="{url}" target="_blank" style="color:{fg};text-decoration:none">{inner}</a>' |
| return ( |
| f'<div style="margin-top:10px;display:inline-flex;align-items:center;gap:5px;' |
| f'padding:4px 10px;background:{bg};color:{fg};border-radius:20px;' |
| f'font-size:.72rem;font-weight:600;border:1px solid {fg}33">{inner}</div>' |
| ) |
|
|
|
|
| def _tool_urls(context: SearchContextPayload) -> Dict[str, str]: |
| ai_query = quote(context.ai_tool_query or context.display_topic or context.topic) |
| primo_ai_query = quote(context.ai_tool_query or context.display_topic or context.topic) |
| urls: Dict[str, str] = { |
| "leapspace": f"https://www.sciencedirect.com/leapspace?query={ai_query}", |
| "scopus_ai": f"https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query={ai_query}", |
| "ebsco_ai": ( |
| f"https://research-ebsco-com.khalifa.idm.oclc.org/c/yst6t2/search/results" |
| f"?q={ai_query}" |
| f"&autocorrect=y" |
| f"&expanders=concept" |
| f"&limiters=None" |
| f"&searchMode=enhanced" |
| f"&searchSegment=all-results" |
| ), |
| "primo_ai": f"{PRIMO_AI_URL}{primo_ai_query}", |
| "consensus": f"https://consensus.app/results/?q={ai_query}", |
| "primo_discovery": _primo_clean_url(context), |
| } |
| if context.intent == "search_medical": |
| urls["pubmed"] = _shared_build_pubmed_url( |
| context.topic, context.year_from, context.year_to, context.peer_reviewed |
| ) |
| return urls |
|
|
|
|
| def _contact_footer_for_query(question: str, context: Optional[SearchContextPayload]) -> str: |
| q = (question or "").lower() |
| specialist: Optional[dict] = None |
| if any(t in q for t in ["interlibrary", "ill request", "ill form", "borrow from another", |
| "request article", "request a book", "not available at ku", |
| "not in ku library", "inter library", "inter-library"]): |
| footer = ( |
| "<br><br><strong>Library contact points</strong><br>" |
| f'General help: <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a> · ' |
| '<a href="mailto:libse@ku.ac.ae">libse@ku.ac.ae</a><br>' |
| 'ILL requests: <a href="mailto:ill@ku.ac.ae">ill@ku.ac.ae</a> · ' |
| '<strong>Suaad Al Jneibi</strong> · ' |
| '<a href="mailto:suaad.aljneibi@ku.ac.ae">suaad.aljneibi@ku.ac.ae</a> · ' |
| '<a href="tel:+97123124278">+971 2 312 4278</a>' |
| ) |
| return footer |
| elif any(t in q for t in [ |
| "apc", "article processing charge", "article processing fee", |
| "open access fee", "oa fee", "oa fund", "oa publishing", |
| "publish open access", "open access publish", "how to publish open access", |
| "open access journal", "open access mandate", "open access policy", |
| "waiver", "gold open access", "diamond open access", "hybrid journal", |
| "creative commons", "cc by", "cc licence", "openaccess", "open access", |
| ]): |
| |
| specialist = _find_staff_by_token("walter") |
| elif any(t in q for t in [ |
| "research support", "research service", "research consultation", |
| "research librarian", "research help", "research advice", |
| "scholarly communication", "bibliometric", "research impact", |
| "citation analysis", "h-index", "journal ranking", "impact factor", |
| "orcid", "khazna", "repository", "scival", "scopus profile", |
| "author profile", "research data", "data management", |
| "libguide", "research guide", "ai tool", "ai for research", |
| "publication advice", "journal selection", "where to publish", |
| ]): |
| |
| specialist = _find_staff_by_token("nikesh") |
| elif any(t in q for t in ["database access", "proxy", "eresource", |
| "e-resource", "remote access", "access problem", "off campus"]): |
| specialist = _find_staff_by_token("rani") |
| elif any(t in q for t in ["website", "system", "technical", "login issue", "page not loading"]): |
| specialist = _find_staff_by_token("walter") |
| elif context and context.intent == "search_medical": |
| |
| specialist = _find_staff_by_token("jason") |
| elif context and context.intent == "search_academic": |
| specialist = _find_staff_by_token("nikesh") |
| footer = ( |
| "<br><br><strong>Library contact points</strong><br>" |
| f'General help: <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a> · ' |
| '<a href="mailto:libse@ku.ac.ae">libse@ku.ac.ae</a>' |
| ) |
| if specialist: |
| footer += ( |
| f'<br>Relevant contact: <strong>{_escape(specialist.get("full_name", ""))}</strong> — ' |
| f'{_escape(specialist.get("role", ""))}<br>' |
| f'<a href="mailto:{_escape(specialist.get("email", ""))}">{_escape(specialist.get("email", ""))}</a>' |
| ) |
| phone = specialist.get("phone") or specialist.get("work_phone") |
| if phone: |
| footer += f' · <a href="tel:{_escape(str(phone).replace(" ", ""))}">{_escape(phone)}</a>' |
| return footer |
|
|
|
|
| def _thought_block(intent: str, tool: str, question: str, extra: str = "") -> str: |
| labels = { |
| "library_info": ("📋", "Library information lookup", "I searched the KU Library knowledge base for relevant content."), |
| "general": ("🌐", "General knowledge / web search", "I answered using a web search for current information."), |
| "general_recent": ("🔴", "Live web search", "I ran a live web search for the most up-to-date information."), |
| "social_greeting": ("🐝", "Conversational response", "I responded directly as LibBee, the KU Library AI Assistant."), |
| "social": ("🐝", "Conversational response", "I responded directly as LibBee, the KU Library AI Assistant."), |
| "staff_lookup": ("👤", "Staff directory lookup", "I matched your request to the KU Library staff directory."), |
| "hours": ("⏰", "Library hours lookup", "I fetched live library hours from the KU LibCal API."), |
| "libcal_live": ("⏰", "Live library hours", "I fetched real-time hours from the KU LibCal API."), |
| "campus": ("📍", "Campus information lookup", "I retrieved KU Library campus information from the knowledge base."), |
| "database_rec": ("🗄️", "Database recommendation", "I matched your subject to the KU Library e-resources database."), |
| "rag_search": ("📖", "Knowledge base search", "I ran a hybrid semantic + keyword search across the KU Library knowledge base."), |
| } |
| icon, label, reasoning = labels.get(intent, ("🤖", "AI response", "I processed your question and generated a response.")) |
| if extra: |
| reasoning += " " + extra |
| return ( |
| f'<details style="margin:0 0 12px 0;border:1px solid #e5e7eb;border-radius:10px;' |
| f'background:#fafafa;overflow:hidden">' |
| f'<summary style="padding:8px 12px;cursor:pointer;font-size:.75rem;font-weight:700;' |
| f'color:#6b7280;list-style:none;display:flex;align-items:center;gap:5px">' |
| f'<span>{icon}</span> My approach — click to see</summary>' |
| f'<div style="padding:4px 12px 10px 12px;font-size:.79rem;line-height:1.7;border-top:1px solid #e5e7eb">' |
| f'<strong>Question:</strong> {_escape(question)}<br>' |
| f'<strong>Classified as:</strong> {label}<br>' |
| f'<strong>How I answered:</strong> {reasoning}' |
| f'</div></details>' |
| ) |
|
|
|
|
| def _search_trace_block(question: str, context: SearchContextPayload) -> str: |
| topic = _escape(context.display_topic or context.topic) |
| primo_boolean = _escape(context.primo_boolean_query or "") |
| source_label = "PubMed" if context.source == "pubmed" else "KU PRIMO Library Discovery" |
| intent_label = { |
| "search_academic": "academic literature search", |
| "search_medical": "medical / clinical literature search", |
| }.get(context.intent, "library search") |
| filters_parts = [] |
| if context.peer_reviewed: |
| filters_parts.append("peer reviewed only") |
| if context.open_access: |
| filters_parts.append("open access") |
| if context.year_from and context.year_to: |
| filters_parts.append(f"{context.year_from}–{context.year_to}") |
| elif context.year_from: |
| filters_parts.append(f"from {context.year_from}") |
| rtype = {"articles": "articles", "books": "books", "both": "articles and books"}.get( |
| context.resource_type, "articles" |
| ) |
| filters_parts.append(rtype) |
| filters_str = ", ".join(filters_parts) |
| reasoning = ( |
| f"I classified this as a <strong>{intent_label}</strong>, " |
| f"extracted the topic <strong>{topic}</strong>, " |
| f"built an LLM-optimised boolean query with quoted phrases and OR synonyms, " |
| f"then searched <strong>{source_label}</strong> for <strong>{filters_str}</strong>." |
| ) |
| if context.intent == "search_medical" and context.source != "pubmed": |
| reasoning += " For medical topics, PubMed and Embase are also recommended." |
| block = ( |
| f'<details style="margin:0 0 14px 0;border:1px solid #dbe4ff;border-radius:12px;background:#f8fbff;overflow:hidden">' |
| f'<summary style="padding:10px 14px;cursor:pointer;font-size:.8rem;font-weight:700;color:#1e40af;list-style:none;display:flex;align-items:center;gap:6px">' |
| f'<span style="font-size:.9rem">🔍</span> How I searched — click to expand' |
| f'</summary>' |
| f'<div style="padding:4px 14px 12px 14px;font-size:.82rem;line-height:1.7;border-top:1px solid #dbe4ff">' |
| f'<strong>Your question:</strong> {_escape(question)}<br>' |
| f'<strong>Interpreted topic:</strong> {topic}<br>' |
| ) |
| if primo_boolean: |
| block += ( |
| f'<strong>Boolean query sent to {source_label}:</strong><br>' |
| f'<code style="display:block;margin:4px 0;padding:6px 10px;background:#1a1a2e;color:#C8A951;' |
| f'border-radius:7px;font-size:.78rem;word-break:break-all">{primo_boolean}</code>' |
| ) |
| block += f'<strong>Reasoning:</strong> {reasoning}<br>' |
| block += '<span style="color:#5b21b6;font-size:.78rem">✨ AI research tools for deeper exploration are listed at the end of this answer.</span>' |
| block += '</div></details>' |
| return block |
|
|
|
|
| def _ai_tools_footer(context): |
| from urllib.parse import quote as _q |
|
|
| ai_q = _q(context.ai_tool_query or context.display_topic or context.topic) |
| bool_q = context.primo_boolean_query or "" |
| primo_url = _primo_clean_url(context) |
| primo_ai = PRIMO_AI_URL + ai_q |
| is_med = context.intent == "search_medical" |
| pubmed_url = _shared_build_pubmed_url( |
| context.topic, context.year_from, context.year_to, context.peer_reviewed |
| ) if is_med else "" |
|
|
| KU = "https://khalifa.idm.oclc.org/login?url=" |
|
|
| platforms = [ |
| ("PRIMO AI Assistant", primo_ai, "primo"), |
| ("LeapSpace", "https://www.sciencedirect.com/leapspace?query=" + ai_q, "leapspace"), |
| ("Scopus AI", "https://www-scopus-com.khalifa.idm.oclc.org/pages/ai?query=" + ai_q, "scopus"), |
| ("EBSCO Research AI", |
| "https://research-ebsco-com.khalifa.idm.oclc.org/c/yst6t2/search/results?q=" + ai_q |
| + "&autocorrect=y&expanders=concept&limiters=None&searchMode=enhanced&searchSegment=all-results", "ebsco"), |
| ("Consensus", "https://consensus.app/results/?q=" + ai_q, "consensus"), |
| ("Semantic Scholar", "https://www.semanticscholar.org/search?q=" + ai_q + "&sort=relevance", "semanticscholar"), |
| ("Perplexity", "https://www.perplexity.ai/search?q=" + ai_q, "perplexity"), |
| ] |
| if is_med and pubmed_url: |
| platforms.append(("PubMed", pubmed_url, "pubmed")) |
|
|
| _SVGS = { |
| "bee": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="16" height="16" style="flex-shrink:0">' |
| '<ellipse cx="5.5" cy="9" rx="5" ry="2.5" fill="#a8d4f0" opacity=".85"/>' |
| '<ellipse cx="14.5" cy="9" rx="5" ry="2.5" fill="#a8d4f0" opacity=".85"/>' |
| '<ellipse cx="10" cy="13" rx="5" ry="6" fill="#1a3a6b"/>' |
| '<ellipse cx="10" cy="15" rx="3" ry="1.5" fill="#243f7a"/>' |
| '<circle cx="10" cy="8" r="5" fill="#1a3a6b"/>' |
| '<rect x="6" y="6.5" width="8" height="3.5" rx="1.5" fill="#001a40"/>' |
| '<ellipse cx="8.2" cy="8.2" rx="1.5" ry="1.2" fill="#00ccee" opacity=".9"/>' |
| '<ellipse cx="11.8" cy="8.2" rx="1.5" ry="1.2" fill="#00ccee" opacity=".9"/>' |
| '<path d="M8 10 Q10 11.5 12 10" fill="none" stroke="#00ccee" stroke-width=".8" stroke-linecap="round"/>' |
| '<line x1="8.5" y1="3.5" x2="6.5" y2="1.5" stroke="#1a3a6b" stroke-width="1.2" stroke-linecap="round"/>' |
| '<circle cx="6.5" cy="1.5" r="1.2" fill="#00ccee" opacity=".8"/>' |
| '<line x1="11.5" y1="3.5" x2="13.5" y2="1.5" stroke="#1a3a6b" stroke-width="1.2" stroke-linecap="round"/>' |
| '<ellipse cx="13.5" cy="1.2" rx="1.5" ry="1.8" fill="#e8e8e8" stroke="#aaa" stroke-width=".5"/>' |
| '<text x="10" y="17.5" font-family="Arial,sans-serif" font-size="3" font-weight="bold" fill="#C8A951" text-anchor="middle">LB</text>' |
| '</svg>' |
| ), |
| "book": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="2" y="2" width="9" height="12" rx="1" fill="#2563eb"/>' |
| '<rect x="3" y="2" width="1.5" height="12" fill="#1d4ed8"/>' |
| '<rect x="4" y="4" width="5" height="1" rx=".5" fill="#bfdbfe"/>' |
| '<rect x="4" y="6" width="5" height="1" rx=".5" fill="#bfdbfe"/>' |
| '<rect x="4" y="8" width="3" height="1" rx=".5" fill="#bfdbfe"/>' |
| '<rect x="11" y="3" width="3" height="10" rx="1" fill="#1e40af"/>' |
| '</svg>' |
| ), |
| "microscope": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="7" y="1" width="2" height="5" rx="1" fill="#6b7280"/>' |
| '<ellipse cx="8" cy="1.5" rx="2.5" ry="1.5" fill="#9ca3af"/>' |
| '<rect x="6.5" y="5" width="3" height="5" rx=".5" fill="#374151"/>' |
| '<rect x="5" y="9.5" width="6" height="1.5" rx=".5" fill="#374151"/>' |
| '<rect x="3" y="13" width="10" height="1.5" rx=".5" fill="#374151"/>' |
| '<line x1="8" y1="11" x2="8" y2="13" stroke="#374151" stroke-width="1.5"/>' |
| '<circle cx="8" cy="6" r="1.5" fill="#60a5fa" opacity=".8"/>' |
| '</svg>' |
| ), |
| "circuit": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#1e293b"/>' |
| '<circle cx="4" cy="4" r="1.5" fill="#22d3ee"/>' |
| '<circle cx="12" cy="4" r="1.5" fill="#22d3ee"/>' |
| '<circle cx="4" cy="12" r="1.5" fill="#22d3ee"/>' |
| '<circle cx="12" cy="12" r="1.5" fill="#22d3ee"/>' |
| '<circle cx="8" cy="8" r="2" fill="#06b6d4"/>' |
| '<line x1="4" y1="4" x2="8" y2="8" stroke="#22d3ee" stroke-width=".8"/>' |
| '<line x1="12" y1="4" x2="8" y2="8" stroke="#22d3ee" stroke-width=".8"/>' |
| '<line x1="4" y1="12" x2="8" y2="8" stroke="#22d3ee" stroke-width=".8"/>' |
| '<line x1="12" y1="12" x2="8" y2="8" stroke="#22d3ee" stroke-width=".8"/>' |
| '</svg>' |
| ), |
| "leaf": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<path d="M8 14 C8 14 2 10 2 5 C2 2 5 1 8 1 C11 1 14 2 14 5 C14 10 8 14 8 14Z" fill="#16a34a"/>' |
| '<line x1="8" y1="1" x2="8" y2="14" stroke="#bbf7d0" stroke-width=".8"/>' |
| '<line x1="8" y1="5" x2="5" y2="7" stroke="#bbf7d0" stroke-width=".6"/>' |
| '<line x1="8" y1="7" x2="5" y2="9" stroke="#bbf7d0" stroke-width=".6"/>' |
| '<line x1="8" y1="5" x2="11" y2="7" stroke="#bbf7d0" stroke-width=".6"/>' |
| '<line x1="8" y1="7" x2="11" y2="9" stroke="#bbf7d0" stroke-width=".6"/>' |
| '</svg>' |
| ), |
| "gear": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<path d="M6.5 1h3l.5 1.5a5 5 0 011.2.7l1.5-.5 1.5 2.6-1.2 1a5 5 0 010 1.4l1.2 1-1.5 2.6-1.5-.5a5 5 0 01-1.2.7L9.5 15h-3l-.5-1.5a5 5 0 01-1.2-.7l-1.5.5L1.8 10.8l1.2-1a5 5 0 010-1.4l-1.2-1L3.3 4.8l1.5.5A5 5 0 016 4.5L6.5 1z" fill="#64748b"/>' |
| '<circle cx="8" cy="8" r="2.5" fill="#f1f5f9"/>' |
| '<circle cx="8" cy="8" r="1.2" fill="#64748b"/>' |
| '</svg>' |
| ), |
| "chart": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="1.5" fill="#f0fdf4"/>' |
| '<rect x="3" y="9" width="2" height="5" rx=".5" fill="#16a34a"/>' |
| '<rect x="7" y="6" width="2" height="8" rx=".5" fill="#15803d"/>' |
| '<rect x="11" y="3" width="2" height="11" rx=".5" fill="#166534"/>' |
| '<line x1="1" y1="13.5" x2="15" y2="13.5" stroke="#166534" stroke-width=".8"/>' |
| '</svg>' |
| ), |
| "atom": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<ellipse cx="8" cy="8" rx="7" ry="3" fill="none" stroke="#7c3aed" stroke-width=".9"/>' |
| '<ellipse cx="8" cy="8" rx="7" ry="3" fill="none" stroke="#7c3aed" stroke-width=".9" transform="rotate(60 8 8)"/>' |
| '<ellipse cx="8" cy="8" rx="7" ry="3" fill="none" stroke="#7c3aed" stroke-width=".9" transform="rotate(120 8 8)"/>' |
| '<circle cx="8" cy="8" r="1.8" fill="#7c3aed"/>' |
| '</svg>' |
| ), |
| "medical": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#dc2626"/>' |
| '<rect x="6.5" y="3" width="3" height="10" rx="1" fill="white"/>' |
| '<rect x="3" y="6.5" width="10" height="3" rx="1" fill="white"/>' |
| '</svg>' |
| ), |
| "wrench": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<path d="M13 2a3 3 0 00-3 3c0 .4.1.8.2 1.1L2.6 13.7a1 1 0 001.4 1.4L11.7 7.8c.3.1.7.2 1.1.2a3 3 0 000-6z" fill="#d97706"/>' |
| '<circle cx="12" cy="4" r="1.2" fill="#fef3c7"/>' |
| '</svg>' |
| ), |
| "primo": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#003366"/>' |
| '<rect x="3" y="4" width="10" height="1.5" rx=".5" fill="#C8A951"/>' |
| '<rect x="3" y="7" width="7" height="1.5" rx=".5" fill="white" opacity=".9"/>' |
| '<rect x="3" y="10" width="9" height="1.5" rx=".5" fill="white" opacity=".7"/>' |
| '<circle cx="12" cy="11" r="2.5" fill="#C8A951"/>' |
| '<line x1="11" y1="11" x2="13" y2="11" stroke="#003366" stroke-width=".8"/>' |
| '<line x1="12" y1="10" x2="12" y2="12" stroke="#003366" stroke-width=".8"/>' |
| '</svg>' |
| ), |
| "scopus": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#e8650a"/>' |
| '<text x="8" y="11.5" font-family="Arial,sans-serif" font-size="8" font-weight="bold" fill="white" text-anchor="middle">Sc</text>' |
| '</svg>' |
| ), |
| "ebsco": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#00427a"/>' |
| '<text x="8" y="11.5" font-family="Arial,sans-serif" font-size="7" font-weight="bold" fill="white" text-anchor="middle">EBS</text>' |
| '</svg>' |
| ), |
| "consensus": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="7" fill="#6c47ff"/>' |
| '<circle cx="6" cy="7" r="1.2" fill="white"/>' |
| '<circle cx="10" cy="7" r="1.2" fill="white"/>' |
| '<path d="M5 10 Q8 12.5 11 10" fill="none" stroke="white" stroke-width="1.2" stroke-linecap="round"/>' |
| '</svg>' |
| ), |
| "leapspace": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#ff6b00"/>' |
| '<path d="M4 12 L4 7 L8 4 L12 7 L12 12 Z" fill="white" opacity=".9"/>' |
| '<rect x="6.5" y="9" width="3" height="3" fill="#ff6b00"/>' |
| '</svg>' |
| ), |
| "perplexity": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#1a1a2e"/>' |
| '<circle cx="8" cy="6.5" r="3" fill="none" stroke="#20b2aa" stroke-width="1.2"/>' |
| '<line x1="8" y1="9.5" x2="8" y2="13" stroke="#20b2aa" stroke-width="1.2" stroke-linecap="round"/>' |
| '<circle cx="8" cy="13" r=".8" fill="#20b2aa"/>' |
| '</svg>' |
| ), |
| "semanticscholar": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#1857b6"/>' |
| '<path d="M4 11 Q8 4 12 11" fill="none" stroke="white" stroke-width="1.5" stroke-linecap="round"/>' |
| '<circle cx="8" cy="7" r="1.8" fill="#4fc3f7"/>' |
| '</svg>' |
| ), |
| "pubmed": ( |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" style="flex-shrink:0">' |
| '<rect x="1" y="1" width="14" height="14" rx="2" fill="#336699"/>' |
| '<text x="8" y="11" font-family="Arial,sans-serif" font-size="6.5" font-weight="bold" fill="white" text-anchor="middle">PubMed</text>' |
| '</svg>' |
| ), |
| } |
| _DB_MASCOT = { |
| "PubMed@KU": "microscope", "Embase": "medical", "CINAHL Ultimate": "medical", |
| "Cochrane Library": "microscope", "UpToDate": "medical", |
| "IEEE Xplore": "circuit", "ACM Digital Library": "circuit", |
| "ScienceDirect": "microscope", "Scopus": "microscope", |
| "Web of Science": "microscope", "SpringerLink": "book", |
| "ACS Publications": "atom", "APS Journals": "atom", "Knovel": "wrench", |
| "Business Source Complete": "chart", "Emerald Insight": "chart", |
| "Taylor & Francis": "book", "EBSCO": "book", |
| } |
| _BEE = _SVGS["bee"] |
|
|
| def _btn(label, url, mascot_key="bee"): |
| icon = _SVGS.get(mascot_key, _SVGS["bee"]) |
| style = ( |
| 'display:inline-flex;align-items:center;gap:7px;' |
| 'padding:8px 14px;border-radius:10px;text-decoration:none;' |
| 'font-size:.8rem;font-weight:600;color:#1a1a2e;' |
| 'background:#fff;border:1.5px solid #d1d5db;' |
| 'transition:box-shadow .2s;width:100%;box-sizing:border-box' |
| ) |
| return '<a href="' + url + '" target="_blank" style="' + style + '">' + icon + label + '</a>' |
|
|
| btn_rows = "" |
| for k in range(0, len(platforms), 2): |
| pair = platforms[k:k+2] |
| cells = "" |
| for lbl, url, mascot_key in pair: |
| cells += '<td style="width:50%;padding:3px 4px 3px 0">' + _btn(lbl, url, mascot_key) + '</td>' |
| if len(pair) == 1: |
| cells += '<td style="width:50%"></td>' |
| btn_rows += "<tr>" + cells + "</tr>" |
|
|
| platforms_html = ( |
| '<div style="margin-top:18px;padding:14px 16px;border:1px solid #c4b5fd;' |
| 'border-radius:12px;background:linear-gradient(135deg,#f5f3ff,#fdf4ff)">' |
| '<div style="font-size:.8rem;font-weight:800;color:#5b21b6;margin-bottom:4px">' |
| '🤖 Search your query across AI platforms</div>' |
| '<div style="font-size:.73rem;color:#6b7280;margin-bottom:10px">' |
| 'Your query is pre-loaded. Click any platform to search instantly.</div>' |
| '<table style="width:100%;border-collapse:collapse">' + btn_rows + '</table>' |
| '</div>' |
| ) |
|
|
| if bool_q: |
| and_count = bool_q.upper().count(" AND ") |
| if and_count >= 1: |
| bool_label = "Use this in PRIMO Advanced Search or any database for precise results:" |
| else: |
| bool_label = "Try adding synonyms with OR to find more relevant results:" |
| bool_html = ( |
| '<div style="margin-top:12px;padding:12px 14px;border:1px solid #fde68a;' |
| 'border-radius:10px;background:#fffbeb">' |
| '<div style="font-size:.78rem;font-weight:700;color:#92400e;margin-bottom:6px">' |
| '\U0001f4a1 Boolean search tip</div>' |
| '<div style="font-size:.74rem;color:#6b7280;margin-bottom:6px">' + bool_label + '</div>' |
| '<code style="display:block;padding:8px 10px;background:#1a1a2e;color:#C8A951;' |
| 'border-radius:7px;font-size:.76rem;word-break:break-all;white-space:pre-wrap">' |
| + _escape(bool_q) + |
| '</code>' |
| '<div style="font-size:.71rem;color:#92400e;margin-top:6px">' |
| 'Paste into the Advanced Search box of any database below.</div>' |
| '</div>' |
| ) |
| else: |
| bool_html = "" |
|
|
| all_lower = ((context.ai_tool_query or "") + " " + (context.topic or "")).lower() |
|
|
| def _db_row(name, url, desc, favicon=None): |
| icon = _SVGS.get(_DB_MASCOT.get(name, "bee"), _SVGS["bee"]) |
| return ( |
| '<div style="display:flex;align-items:center;gap:8px;' |
| 'padding:7px 0;border-bottom:1px solid #f3f4f6">' |
| + icon + |
| '<div><a href="' + url + '" target="_blank" ' |
| 'style="font-size:.8rem;font-weight:700;color:#003366;text-decoration:none">' |
| + name + '</a>' |
| '<span style="font-size:.73rem;color:#6b7280"> — ' + desc + '</span></div>' |
| '</div>' |
| ) |
|
|
| if any(k in all_lower for k in ["medicine","medical","clinical","nursing","pharmacy","health","pubmed","embase","cinahl","cochrane","uptodate"]): |
| db_subject = "Medicine & Health Sciences" |
| dbs = [ |
| ("PubMed@KU","http://www.ncbi.nlm.nih.gov/pubmed/?otool=iaekustlib","Core biomedical literature"), |
| ("Embase",KU+"https://www.embase.com","Biomedical, drugs, devices, clinical medicine"), |
| ("CINAHL Ultimate",KU+"https://search.ebscohost.com/login.aspx?authtype=ip,uid&profile=ehost&defaultdb=cul","Nursing and allied health"), |
| ("Cochrane Library",KU+"https://www.cochranelibrary.com","Systematic reviews and evidence-based medicine"), |
| ("UpToDate",KU+"https://www.uptodate.com","Point-of-care clinical decision support"), |
| ] |
| elif any(k in all_lower for k in ["machine learning","deep learning","artificial intelligence","neural network","computer science","software","algorithm","cybersecurity","robotics","nlp"]): |
| db_subject = "Computer Science & AI" |
| dbs = [ |
| ("IEEE Xplore",KU+"https://ieeexplore.ieee.org","Core CS, AI, electronics and systems"), |
| ("ACM Digital Library",KU+"https://dl.acm.org","Computer science journals, proceedings and books"), |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Full text across CS and applied computing"), |
| ("Scopus",KU+"https://www.scopus.com","Discovery and citation tracking"), |
| ("Web of Science",KU+"https://www.webofscience.com","Citation index"), |
| ] |
| elif any(k in all_lower for k in ["climate","global warming","environment","sustainability","ecology","carbon","emission","renewable","sea level","maldives","island"]): |
| db_subject = "Environment & Climate" |
| dbs = [ |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Full text across science, environment and engineering"), |
| ("Scopus",KU+"https://www.scopus.com","Broad discovery and citation tracking"), |
| ("Web of Science",KU+"https://www.webofscience.com","High-impact journal coverage and citation index"), |
| ("SpringerLink",KU+"https://link.springer.com","Springer journals and books"), |
| ] |
| elif any(k in all_lower for k in ["engineering","mechanical","civil","electrical","aerospace","structural","petroleum","chemical","materials"]): |
| db_subject = "Engineering" |
| dbs = [ |
| ("IEEE Xplore",KU+"https://ieeexplore.ieee.org","Electrical, electronics and systems engineering"), |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Full text across engineering disciplines"), |
| ("Knovel",KU+"https://app.knovel.com","Engineering handbooks and reference data"), |
| ("Scopus",KU+"https://www.scopus.com","Discovery and citation tracking"), |
| ("Web of Science",KU+"https://www.webofscience.com","Citation index"), |
| ] |
| elif any(k in all_lower for k in ["business","management","finance","economics","leadership","marketing","strategy","entrepreneurship"]): |
| db_subject = "Business & Management" |
| dbs = [ |
| ("Business Source Complete",KU+"https://search.ebscohost.com","Core business and management literature"), |
| ("Emerald Insight",KU+"https://www.emerald.com/insight","Management, strategy and leadership"), |
| ("Scopus",KU+"https://www.scopus.com","Broad discovery and citation tracking"), |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Business and social sciences full text"), |
| ] |
| elif any(k in all_lower for k in ["physics","chemistry","quantum","nuclear","materials science","nanotechnology","optics"]): |
| db_subject = "Physics, Chemistry & Materials" |
| dbs = [ |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Full text across physical sciences"), |
| ("ACS Publications",KU+"https://pubs.acs.org","Core chemistry and chemical engineering"), |
| ("APS Journals",KU+"https://journals.aps.org","American Physical Society core physics journals"), |
| ("Scopus",KU+"https://www.scopus.com","Discovery and citation tracking"), |
| ("Web of Science",KU+"https://www.webofscience.com","Citation index"), |
| ] |
| else: |
| db_subject = "General" |
| dbs = [ |
| ("EBSCO",KU+"https://search.ebscohost.com","Multidisciplinary full text"), |
| ("ScienceDirect",KU+"https://www.sciencedirect.com","Science, engineering and medicine"), |
| ("SpringerLink",KU+"https://link.springer.com","Journals and books across all disciplines"), |
| ("Taylor & Francis",KU+"https://www.tandfonline.com","Humanities, social sciences and STEM"), |
| ("Emerald Insight",KU+"https://www.emerald.com/insight","Management and social sciences"), |
| ("Scopus",KU+"https://www.scopus.com","Citation discovery, all disciplines"), |
| ("Web of Science",KU+"https://www.webofscience.com","Citation index, all disciplines"), |
| ] |
|
|
| dbs_html_rows = "" |
| for name, url, desc in dbs: |
| dbs_html_rows += _db_row(name, url, desc) |
|
|
| db_section = ( |
| '<div style="margin-top:12px;padding:12px 14px;border:1px solid #d1d5db;' |
| 'border-radius:10px;background:#f9fafb">' |
| '<div style="font-size:.78rem;font-weight:700;color:#003366;margin-bottom:8px">' |
| '\U0001f4c2 Recommended KU databases \u2014 ' + db_subject + '</div>' |
| + dbs_html_rows + |
| '<div style="padding-top:8px;font-size:.74rem">' |
| '<a href="https://library.ku.ac.ae/eresources" target="_blank" ' |
| 'style="color:#003366;font-weight:700">' |
| '\U0001f5c4\ufe0f Browse all 50+ KU databases \u2192</a></div>' |
| '</div>' |
| ) |
|
|
| return platforms_html + bool_html + db_section |
|
|
|
|
|
|