Update app.py
Browse files
app.py
CHANGED
|
@@ -304,6 +304,136 @@ def _load_staff_directory_from_kb():
|
|
| 304 |
def _staff_lookup_candidates():
|
| 305 |
return kb_staff_directory or STAFF_DIRECTORY
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
def _match_staff_name(question: str):
|
| 308 |
tokens = _normalize_name_query(question)
|
| 309 |
if not tokens or len(tokens) > 5:
|
|
@@ -327,6 +457,12 @@ def _staff_name_answer(staff: dict, partial: str) -> str:
|
|
| 327 |
f"<strong>{staff['full_name']}</strong> is the <strong>{staff['role']}</strong>. {staff['details']}"
|
| 328 |
)
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
GROUNDED_LIBRARY_MAP = {
|
| 332 |
"ill": "interlibrary loan ILL document delivery full text unavailable article not available borrow from another library",
|
|
@@ -1357,7 +1493,7 @@ async def agent_query(req: AgentRequest):
|
|
| 1357 |
"source_mode": "social",
|
| 1358 |
}
|
| 1359 |
|
| 1360 |
-
# ----
|
| 1361 |
staff_match = _match_staff_name(question)
|
| 1362 |
if staff_match:
|
| 1363 |
answer = _staff_name_answer(staff_match, question)
|
|
@@ -1380,6 +1516,28 @@ async def agent_query(req: AgentRequest):
|
|
| 1380 |
"source_mode": "staff_kb" if kb_staff_directory else "staff_directory",
|
| 1381 |
}
|
| 1382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1383 |
# ---- Follow-up to the greeting menu ----
|
| 1384 |
if _is_greeting_menu_followup(question, history):
|
| 1385 |
answer = _greeting_menu_clarify_answer()
|
|
|
|
| 304 |
def _staff_lookup_candidates():
|
| 305 |
return kb_staff_directory or STAFF_DIRECTORY
|
| 306 |
|
| 307 |
+
def _role_key(name: str) -> str:
|
| 308 |
+
return re.sub(r"[^a-z0-9]+", " ", (name or "").lower()).strip()
|
| 309 |
+
|
| 310 |
+
def _build_role_aliases(staff: dict):
|
| 311 |
+
role = (staff.get("role") or "").strip()
|
| 312 |
+
details = (staff.get("details") or "").strip()
|
| 313 |
+
full_name = staff.get("full_name", "")
|
| 314 |
+
key = _role_key(full_name)
|
| 315 |
+
|
| 316 |
+
aliases = []
|
| 317 |
+
if role:
|
| 318 |
+
aliases.append(role)
|
| 319 |
+
role_l = role.lower()
|
| 320 |
+
parts = [p.strip() for p in re.split(r"[\/,]|\band\b", role_l) if p.strip()]
|
| 321 |
+
aliases.extend(parts)
|
| 322 |
+
if "manager" in role_l:
|
| 323 |
+
mgr_tail = re.sub(r"^manager\s*,?\s*", "", role_l).strip()
|
| 324 |
+
if mgr_tail:
|
| 325 |
+
aliases.extend([
|
| 326 |
+
mgr_tail,
|
| 327 |
+
f"{mgr_tail} librarian",
|
| 328 |
+
f"{mgr_tail.rstrip('s')} librarian",
|
| 329 |
+
f"{mgr_tail} manager",
|
| 330 |
+
])
|
| 331 |
+
if "librarian" in role_l:
|
| 332 |
+
base = role_l.replace("librarian", "").replace("/", " ").strip(" ,")
|
| 333 |
+
if base:
|
| 334 |
+
aliases.extend([
|
| 335 |
+
base,
|
| 336 |
+
f"{base} librarian",
|
| 337 |
+
f"{base.rstrip('s')} librarian",
|
| 338 |
+
])
|
| 339 |
+
|
| 340 |
+
best_for = ""
|
| 341 |
+
m = re.search(r"Best for:\s*([^|]+)", details, re.IGNORECASE)
|
| 342 |
+
if m:
|
| 343 |
+
best_for = m.group(1).strip()
|
| 344 |
+
if best_for:
|
| 345 |
+
aliases.extend([p.strip() for p in best_for.split(",") if p.strip()])
|
| 346 |
+
|
| 347 |
+
manual = {
|
| 348 |
+
"nikesh narayanan": [
|
| 349 |
+
"research librarian", "access services librarian", "research and access services librarian",
|
| 350 |
+
"research access librarian", "open access librarian", "orcid librarian",
|
| 351 |
+
"research impact librarian", "bibliometrics librarian", "scholarly communication librarian"
|
| 352 |
+
],
|
| 353 |
+
"walter brian hall": [
|
| 354 |
+
"systems librarian", "system librarian", "library systems librarian",
|
| 355 |
+
"digital services librarian", "technology services librarian", "website librarian",
|
| 356 |
+
"technology librarian", "digital librarian"
|
| 357 |
+
],
|
| 358 |
+
"rani anand": [
|
| 359 |
+
"e resources librarian", "e-resources librarian", "electronic resources librarian",
|
| 360 |
+
"database librarian", "database access librarian", "resource access librarian"
|
| 361 |
+
],
|
| 362 |
+
"jason fetty": [
|
| 363 |
+
"medical librarian", "health sciences librarian", "clinical librarian",
|
| 364 |
+
"systematic review librarian", "pubmed librarian"
|
| 365 |
+
],
|
| 366 |
+
"alia al harrasi": [
|
| 367 |
+
"acquisitions librarian", "acquisition librarian", "technical services librarian",
|
| 368 |
+
"technical service librarian", "collection development librarian", "cataloguing librarian",
|
| 369 |
+
"metadata librarian"
|
| 370 |
+
],
|
| 371 |
+
"muna ahmad mohammad al blooshi": [
|
| 372 |
+
"public services librarian", "public service librarian", "circulation librarian",
|
| 373 |
+
"access services manager", "service desk librarian"
|
| 374 |
+
],
|
| 375 |
+
"dr abdulla al hefeiti": [
|
| 376 |
+
"library director", "director of library", "director libraries", "assistant provost libraries"
|
| 377 |
+
],
|
| 378 |
+
"abdulla al hefeiti": [
|
| 379 |
+
"library director", "director of library", "director libraries", "assistant provost libraries"
|
| 380 |
+
],
|
| 381 |
+
}
|
| 382 |
+
aliases.extend(manual.get(key, []))
|
| 383 |
+
|
| 384 |
+
normalized = []
|
| 385 |
+
for a in aliases:
|
| 386 |
+
a = re.sub(r"[^a-z0-9&/ +()-]+", " ", (a or "").lower())
|
| 387 |
+
a = re.sub(r"\s+", " ", a).strip(" ,")
|
| 388 |
+
if a:
|
| 389 |
+
normalized.append(a)
|
| 390 |
+
if a.endswith(" services"):
|
| 391 |
+
normalized.append(a[:-1])
|
| 392 |
+
if a.endswith(" service"):
|
| 393 |
+
normalized.append(a + " librarian")
|
| 394 |
+
if a.endswith(" resources"):
|
| 395 |
+
normalized.append(a[:-1])
|
| 396 |
+
return _dedupe_keep_order(normalized)
|
| 397 |
+
|
| 398 |
+
def _match_staff_role(question: str):
|
| 399 |
+
ql = re.sub(r"[^a-z0-9 ]+", " ", (question or "").lower())
|
| 400 |
+
ql = re.sub(r"\s+", " ", ql).strip()
|
| 401 |
+
if not ql:
|
| 402 |
+
return None
|
| 403 |
+
|
| 404 |
+
role_trigger_re = re.compile(
|
| 405 |
+
r"\b(librarian|library director|director|manager|services?|service|systems?|technology|website|research|access|open access|orcid|bibliometric|bibliometrics|public services?|technical services?|acquisitions?|collection development|catalogu(?:e|ing)|metadata|database|e resources|e-resources|electronic resources|medical|clinical|systematic review|circulation)\b"
|
| 406 |
+
)
|
| 407 |
+
if not role_trigger_re.search(ql):
|
| 408 |
+
return None
|
| 409 |
+
|
| 410 |
+
best = None
|
| 411 |
+
best_score = 0
|
| 412 |
+
for staff in _staff_lookup_candidates():
|
| 413 |
+
score = 0
|
| 414 |
+
for alias in _build_role_aliases(staff):
|
| 415 |
+
if not alias:
|
| 416 |
+
continue
|
| 417 |
+
alias_words = alias.split()
|
| 418 |
+
if alias in ql:
|
| 419 |
+
score = max(score, 100 + len(alias_words))
|
| 420 |
+
elif len(alias_words) >= 2 and all(w in ql.split() for w in alias_words):
|
| 421 |
+
score = max(score, 70 + len(alias_words))
|
| 422 |
+
else:
|
| 423 |
+
overlap = sum(1 for w in alias_words if len(w) > 2 and w in ql.split())
|
| 424 |
+
if overlap >= 2:
|
| 425 |
+
score = max(score, 40 + overlap)
|
| 426 |
+
role_words = _normalize_name_query(staff.get("role", ""))
|
| 427 |
+
if role_words:
|
| 428 |
+
overlap = sum(1 for w in role_words if len(w) > 2 and w in ql.split())
|
| 429 |
+
if overlap >= 2:
|
| 430 |
+
score = max(score, 20 + overlap)
|
| 431 |
+
if score > best_score:
|
| 432 |
+
best_score = score
|
| 433 |
+
best = staff
|
| 434 |
+
|
| 435 |
+
return best if best_score >= 42 else None
|
| 436 |
+
|
| 437 |
def _match_staff_name(question: str):
|
| 438 |
tokens = _normalize_name_query(question)
|
| 439 |
if not tokens or len(tokens) > 5:
|
|
|
|
| 457 |
f"<strong>{staff['full_name']}</strong> is the <strong>{staff['role']}</strong>. {staff['details']}"
|
| 458 |
)
|
| 459 |
|
| 460 |
+
def _staff_role_answer(staff: dict, question: str) -> str:
|
| 461 |
+
return (
|
| 462 |
+
f"For <strong>{question}</strong>, the best match is <strong>{staff['full_name']}</strong> — "
|
| 463 |
+
f"<strong>{staff['role']}</strong>.<br><br>{staff['details']}"
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
|
| 467 |
GROUNDED_LIBRARY_MAP = {
|
| 468 |
"ill": "interlibrary loan ILL document delivery full text unavailable article not available borrow from another library",
|
|
|
|
| 1493 |
"source_mode": "social",
|
| 1494 |
}
|
| 1495 |
|
| 1496 |
+
# ---- Staff direct matching (name first, then role semantics) ----
|
| 1497 |
staff_match = _match_staff_name(question)
|
| 1498 |
if staff_match:
|
| 1499 |
answer = _staff_name_answer(staff_match, question)
|
|
|
|
| 1516 |
"source_mode": "staff_kb" if kb_staff_directory else "staff_directory",
|
| 1517 |
}
|
| 1518 |
|
| 1519 |
+
staff_role_match = _match_staff_role(question)
|
| 1520 |
+
if staff_role_match:
|
| 1521 |
+
answer = _staff_role_answer(staff_role_match, question)
|
| 1522 |
+
elapsed = time.time() - start
|
| 1523 |
+
source_title = staff_role_match.get("source_title", "")
|
| 1524 |
+
source_url = staff_role_match.get("source", "")
|
| 1525 |
+
return {
|
| 1526 |
+
"answer": answer,
|
| 1527 |
+
"intent": "library_info",
|
| 1528 |
+
"tools_used": ["staff_role_match"],
|
| 1529 |
+
"search_results": [],
|
| 1530 |
+
"sources": ([{"title": source_title, "source": source_url}] if source_title or source_url else []),
|
| 1531 |
+
"model_used": req.model,
|
| 1532 |
+
"response_time": round(elapsed, 2),
|
| 1533 |
+
"corrected_query": question,
|
| 1534 |
+
"natural_query": question,
|
| 1535 |
+
"database_query": question,
|
| 1536 |
+
"original_question": question,
|
| 1537 |
+
"is_follow_up": False,
|
| 1538 |
+
"source_mode": "staff_kb" if kb_staff_directory else "staff_directory",
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
# ---- Follow-up to the greeting menu ----
|
| 1542 |
if _is_greeting_menu_followup(question, history):
|
| 1543 |
answer = _greeting_menu_clarify_answer()
|