Download src/services/staff_service.py from nikeshn/LibBee: direct link, hf CLI and curl.
- Browser
- Download file 9.25 kB
-
https://huggingface.co/spaces/nikeshn/LibBee/resolve/main/src/services/staff_service.py
- Command line
-
hf download hf://spaces/nikeshn/LibBee/src/services/staff_service.py
-
curl -L -o staff_service.py https://huggingface.co/spaces/nikeshn/LibBee/resolve/main/src/services/staff_service.py
9.25 kB
| """Deterministic staff matching using the supplied KU directory snapshot. | |
| Live roles and contact details still require checking against the official | |
| staff page; these helpers do not perform a live directory lookup. | |
| """ | |
| import difflib | |
| import re | |
| from typing import Dict, List, Optional | |
| STAFF_DIRECTORY = [ | |
| { | |
| "full_name": "Dr. Abdulla Al Hefeiti", | |
| "role": "Library Director / Assistant Provost, Libraries", | |
| "email": "abdulla.alhefeiti@ku.ac.ae", | |
| "phone": "+971 2 312 3331", | |
| "expertise": "strategic and institutional matters, library leadership, and partnerships", | |
| "tokens": ["abdulla", "hefeiti", "abdulla al hefeiti", "al hefeiti"], | |
| "aliases": ["library director", "director", "assistant provost"], | |
| }, | |
| { | |
| "full_name": "Nikesh Narayanan", | |
| "role": "Research & Access Services Librarian", | |
| "email": "nikesh.narayanan@ku.ac.ae", | |
| "phone": "+971 2 312 3980", | |
| "expertise": "research support, Open Access publishing, Khazna repository, ORCID, Scopus, research impact, AI tools for research, bibliometrics, and scholarly communication", | |
| "tokens": ["nikesh", "narayanan", "nikesh narayanan"], | |
| "aliases": ["research librarian", "research support librarian"], | |
| }, | |
| { | |
| "full_name": "Rani Anand", | |
| "role": "E-Resources Librarian", | |
| "email": "rani.anand@ku.ac.ae", | |
| "phone": "+971 2 312 3935", | |
| "expertise": "database access problems, e-resources troubleshooting, remote access, vendor issues, patents, e-books, and bibliometrics", | |
| "tokens": ["rani", "anand", "rani anand"], | |
| "aliases": ["e-resources librarian", "eresources librarian", "database librarian"], | |
| }, | |
| { | |
| "full_name": "Jason Fetty", | |
| "role": "Medical Librarian", | |
| "email": "jason.fetty@ku.ac.ae", | |
| "phone": "+971 2 312 4722", | |
| "expertise": "medical and health sciences research, systematic reviews, PubMed, Embase, CINAHL, UpToDate, and clinical databases", | |
| "tokens": ["jason", "fetty", "jason fetty"], | |
| "aliases": ["medical librarian", "medical library", "health sciences librarian"], | |
| }, | |
| { | |
| "full_name": "Walter Brian Hall", | |
| "role": "Digital & Technology Services Librarian / Systems Librarian", | |
| "email": "walter.hall@ku.ac.ae", | |
| "phone": "+971 2 312 3163", | |
| "expertise": "library website, systems, technology, digital infrastructure, ORCID, STEAM, coding, and Open Access support", | |
| "tokens": ["walter", "brian", "hall", "walter brian hall", "walter hall", "brian hall"], | |
| "aliases": ["systems librarian", "technology librarian", "digital librarian"], | |
| }, | |
| { | |
| "full_name": "Alia Al-Harrasi", | |
| "role": "Manager, Technical Services", | |
| "email": "alia.alharrasi@ku.ac.ae", | |
| "phone": "+971 2 312 3180", | |
| "expertise": "cataloguing, metadata, acquisitions processing, and technical services", | |
| "tokens": ["alia", "alia al-harrasi", "alia alharrasi", "al harrasi", "alharrasi"], | |
| "aliases": ["technical services", "acquisitions processing", "cataloguing"], | |
| }, | |
| { | |
| "full_name": "Zainab Al Ali", | |
| "role": "Library Information Senior Specialist (Interlibrary Loan)", | |
| "email": "zainab.alali@ku.ac.ae", | |
| "phone": "+971 2 312 4321", | |
| "expertise": "Interlibrary Loan (ILL) requests and status, document delivery, and obtaining books or articles that KU does not hold (ILL mailbox: ill@ku.ac.ae)", | |
| "tokens": ["zainab", "zainab al ali", "zainab alali", "alali"], | |
| "aliases": [ | |
| "interlibrary loan", "inter library loan", "inter-library loan", | |
| "ill request", "ill requests", "document delivery", | |
| ], | |
| }, | |
| ] | |
| _STOP_WORDS = { | |
| "who", "is", "are", "the", "a", "an", "can", "help", "me", "tell", | |
| "about", "find", "contact", "email", "phone", "number", "i", "need", | |
| "to", "speak", "with", "please", "get", "in", "touch", "reach", "how", | |
| "do", "what", "which", "where", "for", "of", "best", "person", "librarian", | |
| } | |
| _CONTACT_INTENT_RE = re.compile( | |
| r"\b(contact|email|phone|number|who is|who's|who handles|who can help|best person|best librarian|" | |
| r"which librarian|talk to|speak to|reach|appointment|book an appointment|schedule)", | |
| re.IGNORECASE, | |
| ) | |
| _ROLE_INDEX: dict[str, Dict] = {} | |
| def build_staff_index() -> None: | |
| """Build role alias β staff dict. Called once from app.py lifespan.""" | |
| global _ROLE_INDEX | |
| _ROLE_INDEX = {} | |
| for staff in STAFF_DIRECTORY: | |
| for alias in [staff["full_name"], *staff.get("aliases", [])]: | |
| key = re.sub(r"[^a-z0-9]+", " ", alias.lower()).strip() | |
| _ROLE_INDEX[key] = staff | |
| def _normalize(text: str) -> List[str]: | |
| tokens = re.sub(r"[^a-z0-9 ]+", " ", (text or "").lower()).split() | |
| return [t for t in tokens if t not in _STOP_WORDS] | |
| def should_attempt_staff_lookup(question: str) -> bool: | |
| q = (question or "").strip().lower() | |
| if not q: | |
| return False | |
| if not _CONTACT_INTENT_RE.search(q): | |
| return False | |
| # Don't trigger on article/research queries that happen to contain contact words | |
| if re.search( | |
| r"\b(article|articles|paper|papers|study|studies|research article|research articles|literature)\b", q | |
| ) and not re.search( | |
| r"\b(who|contact|email|phone|which librarian|best person|who can help)\b", q | |
| ): | |
| return False | |
| return True | |
| def match_staff_name(question: str) -> Optional[Dict]: | |
| """Match alternative spellings independently, without combining aliases. | |
| A contact request can use a first name, surname or full-name alias. Common | |
| words such as "hall" cannot identify a person on their own. Ambiguous | |
| matches fall through to the KB instead of choosing the first staff entry. | |
| """ | |
| if not should_attempt_staff_lookup(question): | |
| return None | |
| question_tokens = set(_normalize(question)) | |
| weak = {"al", "dr", "hall", "brian"} | |
| candidates = [] | |
| for staff in STAFF_DIRECTORY: | |
| strength = 0 | |
| for alias in [staff["full_name"], *staff.get("tokens", [])]: | |
| tokens = set(_normalize(alias)) - {"dr"} | |
| if tokens and tokens.issubset(question_tokens): | |
| if len(tokens) >= 2 or next(iter(tokens)) not in weak: | |
| strength = max(strength, len(tokens)) | |
| if strength: | |
| candidates.append(staff) | |
| if len(candidates) == 1: | |
| return candidates[0] | |
| if candidates: | |
| return None | |
| return _fuzzy_staff_match(question) | |
| def _fuzzy_staff_match(question: str) -> Optional[Dict]: | |
| """Compare probable name words with name words, not full sentences.""" | |
| candidates = {} | |
| for token in _normalize(question): | |
| if len(token) < 6: | |
| continue | |
| for staff in STAFF_DIRECTORY: | |
| distinctive = { | |
| word for alias in staff.get("tokens", []) for word in _normalize(alias) | |
| if len(word) >= 6 and word not in {"librarian", "library"} | |
| } | |
| if difflib.get_close_matches(token, distinctive, n=1, cutoff=0.88): | |
| candidates[staff["email"]] = staff | |
| return next(iter(candidates.values())) if len(candidates) == 1 else None | |
| def match_staff_role(question: str) -> Optional[Dict]: | |
| """Match by role alias. Auto-builds index if empty (defensive guard).""" | |
| if not should_attempt_staff_lookup(question): | |
| return None | |
| if not _ROLE_INDEX: | |
| build_staff_index() | |
| q = re.sub(r"[^a-z0-9]+", " ", question.lower()).strip() | |
| # Longest alias first β prevents "librarian" matching before "medical librarian" | |
| for alias, staff in sorted(_ROLE_INDEX.items(), key=lambda item: len(item[0]), reverse=True): | |
| if len(alias) > 3 and re.search(r"(?<!\w)" + re.escape(alias) + r"(?!\w)", q): | |
| return staff | |
| return None | |
| def staff_name_answer(staff: Dict) -> str: | |
| return ( | |
| f"<strong>{staff['full_name']}</strong> is the <strong>{staff['role']}</strong>.<br><br>" | |
| f"They can help with: {staff['expertise']}.<br><br>" | |
| f"π§ <a href=\"mailto:{staff['email']}\">{staff['email']}</a><br>" | |
| f"π <a href=\"tel:{staff['phone'].replace(' ', '')}\">{staff['phone']}</a>" | |
| '<br><br>Contact details are from the supplied directory. ' | |
| '<a href="https://library.ku.ac.ae/librarystaff" target="_blank">Check the current staff directory</a>.' | |
| ) | |
| def staff_role_answer(staff: Dict, question: str) -> str: | |
| return ( | |
| f"For help with that, the best person to contact is <strong>{staff['full_name']}</strong> β " | |
| f"<strong>{staff['role']}</strong>.<br><br>" | |
| f"They can help with: {staff['expertise']}.<br><br>" | |
| f"π§ <a href=\"mailto:{staff['email']}\">{staff['email']}</a><br>" | |
| f"π <a href=\"tel:{staff['phone'].replace(' ', '')}\">{staff['phone']}</a>" | |
| '<br><br>Contact details are from the supplied directory. ' | |
| '<a href="https://library.ku.ac.ae/librarystaff" target="_blank">Check the current staff directory</a>.' | |
| ) | |
| # NOTE: build_staff_index() is NOT called here at module level. | |
| # It is called once from app.py lifespan to avoid double-build. | |