Update src/api/agent.py
Browse files- src/api/agent.py +104 -65
src/api/agent.py
CHANGED
|
@@ -118,7 +118,7 @@ MEDICAL_SEARCH_RE = re.compile(
|
|
| 118 |
re.IGNORECASE,
|
| 119 |
)
|
| 120 |
|
| 121 |
-
# FIX: removed "president" β was catching KU
|
| 122 |
CURRENT_INFO_RE = re.compile(
|
| 123 |
r"\b(current|currently|latest|today|recent|recently|now|this week|this month"
|
| 124 |
r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
|
|
@@ -136,7 +136,8 @@ MEDICAL_KEYWORDS = [
|
|
| 136 |
"crispr", "gene therapy", "gene editing", "genomic medicine", "precision medicine",
|
| 137 |
]
|
| 138 |
|
| 139 |
-
# LLM Classifier
|
|
|
|
| 140 |
_CLASSIFIER_SYSTEM = """You are the routing brain of LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE).
|
| 141 |
Your only job: read the user message and return a single JSON object routing it to the right handler.
|
| 142 |
|
|
@@ -145,8 +146,8 @@ INTENTS β pick exactly one:
|
|
| 145 |
"library_info" β questions about KU Library services, hours, rooms, borrowing, fines, ILL, staff, databases, ORCID, RefWorks, open access, APC, account, printing, remote access, LibKey, acquisitions, Khazna, thesis submission, library policies, alumni access
|
| 146 |
"search_academic"β user wants to FIND articles / papers / books / literature on a specific academic topic
|
| 147 |
"search_medical" β same as search_academic but topic is medical/clinical/nursing/pharmacy/health sciences
|
| 148 |
-
"general_recent" β questions about TRULY
|
| 149 |
-
"general" β
|
| 150 |
|
| 151 |
RULES:
|
| 152 |
1. When in doubt between library_info and social, choose library_info if ANY library keyword is present.
|
|
@@ -155,7 +156,8 @@ RULES:
|
|
| 155 |
4. "I need help finding articles on diabetes" = search_medical.
|
| 156 |
5. KU = Khalifa University. Do NOT confuse with Kuwait University.
|
| 157 |
6. ANY question about advances, approaches, methods, effectiveness, impact, trends on a specific academic topic = search_academic or search_medical.
|
| 158 |
-
7. Questions about university leadership, presidents, provosts,
|
|
|
|
| 159 |
|
| 160 |
For "social" intent ONLY: include "casual_answer" β a warm 1-3 sentence LibBee response (no markdown, no bullets).
|
| 161 |
For all other intents: omit "casual_answer".
|
|
@@ -210,7 +212,7 @@ def _looks_research_question(question: str) -> bool:
|
|
| 210 |
|
| 211 |
|
| 212 |
def _rule_based_classify(question: str) -> Dict:
|
| 213 |
-
"""Fallback classifier
|
| 214 |
q = question.lower()
|
| 215 |
if SOCIAL_RE.match(question.strip()):
|
| 216 |
return {
|
|
@@ -403,6 +405,92 @@ async def _rag_answer(
|
|
| 403 |
)
|
| 404 |
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
async def _llm_general_answer(
|
| 407 |
question: str,
|
| 408 |
history: List[ChatMessage],
|
|
@@ -410,7 +498,7 @@ async def _llm_general_answer(
|
|
| 410 |
) -> str:
|
| 411 |
"""
|
| 412 |
Answer general factual questions from LLM training knowledge.
|
| 413 |
-
Used
|
| 414 |
"""
|
| 415 |
settings = get_settings()
|
| 416 |
if not settings.openai_api_key:
|
|
@@ -425,8 +513,9 @@ async def _llm_general_answer(
|
|
| 425 |
"Answer the user's question factually and concisely in 3-5 sentences. "
|
| 426 |
"Use HTML <br> for line breaks. "
|
| 427 |
"KU means Khalifa University β do NOT confuse with Kuwait University. "
|
| 428 |
-
"
|
| 429 |
-
"
|
|
|
|
| 430 |
)
|
| 431 |
|
| 432 |
try:
|
|
@@ -445,56 +534,6 @@ async def _llm_general_answer(
|
|
| 445 |
)
|
| 446 |
|
| 447 |
|
| 448 |
-
async def _web_search_answer(
|
| 449 |
-
question: str,
|
| 450 |
-
history: List[ChatMessage],
|
| 451 |
-
model: str,
|
| 452 |
-
) -> str:
|
| 453 |
-
"""
|
| 454 |
-
Answer current-events questions using web search (Claude) or
|
| 455 |
-
LLM knowledge fallback (GPT β no native web search tool).
|
| 456 |
-
Used for: breaking news, live scores, real-time data.
|
| 457 |
-
"""
|
| 458 |
-
settings = get_settings()
|
| 459 |
-
|
| 460 |
-
# Claude: use Anthropic web search tool
|
| 461 |
-
if model == "claude" and settings.anthropic_api_key:
|
| 462 |
-
try:
|
| 463 |
-
import anthropic
|
| 464 |
-
client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
|
| 465 |
-
|
| 466 |
-
system = (
|
| 467 |
-
"You are LibBee, the Khalifa University Library AI Assistant. "
|
| 468 |
-
"Answer the question using web search results. "
|
| 469 |
-
"Be concise and factual. Use HTML <br> for line breaks. "
|
| 470 |
-
"If the topic is academic, briefly mention you can help find "
|
| 471 |
-
"scholarly articles on it."
|
| 472 |
-
)
|
| 473 |
-
|
| 474 |
-
response = client.messages.create(
|
| 475 |
-
model="claude-3-haiku-20240307",
|
| 476 |
-
max_tokens=500,
|
| 477 |
-
system=system,
|
| 478 |
-
tools=[{"type": "web_search_20250305", "name": "web_search"}],
|
| 479 |
-
messages=[{"role": "user", "content": question}],
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
# Extract text blocks from response
|
| 483 |
-
text = ""
|
| 484 |
-
for block in response.content:
|
| 485 |
-
if hasattr(block, "text"):
|
| 486 |
-
text += block.text
|
| 487 |
-
|
| 488 |
-
if text.strip():
|
| 489 |
-
return text.strip()
|
| 490 |
-
|
| 491 |
-
except Exception as e:
|
| 492 |
-
logger.warning(f"Claude web search failed: {e} β falling back to LLM knowledge")
|
| 493 |
-
|
| 494 |
-
# GPT or Claude fallback: answer from training knowledge
|
| 495 |
-
return await _llm_general_answer(question, history, model)
|
| 496 |
-
|
| 497 |
-
|
| 498 |
# ββ Main Endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 499 |
|
| 500 |
@router.post("", response_model=AgentResponse)
|
|
@@ -655,24 +694,24 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 655 |
)
|
| 656 |
|
| 657 |
elif intent == "general_recent":
|
| 658 |
-
#
|
| 659 |
answer = await _web_search_answer(question, history, model)
|
| 660 |
-
tools_used = ["web_search" if model == "claude" else "llm_knowledge"]
|
| 661 |
return AgentResponse(
|
| 662 |
answer=answer,
|
| 663 |
intent="general_recent",
|
| 664 |
-
tools_used=
|
| 665 |
model_used=model,
|
| 666 |
response_time=time.time() - start_time,
|
| 667 |
)
|
| 668 |
|
| 669 |
else: # general
|
| 670 |
-
# Factual questions
|
| 671 |
-
|
|
|
|
| 672 |
return AgentResponse(
|
| 673 |
answer=answer,
|
| 674 |
intent="general",
|
| 675 |
-
tools_used=["
|
| 676 |
model_used=model,
|
| 677 |
response_time=time.time() - start_time,
|
| 678 |
)
|
|
|
|
| 118 |
re.IGNORECASE,
|
| 119 |
)
|
| 120 |
|
| 121 |
+
# FIX: removed "president" β was incorrectly catching KU leadership questions
|
| 122 |
CURRENT_INFO_RE = re.compile(
|
| 123 |
r"\b(current|currently|latest|today|recent|recently|now|this week|this month"
|
| 124 |
r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
|
|
|
|
| 136 |
"crispr", "gene therapy", "gene editing", "genomic medicine", "precision medicine",
|
| 137 |
]
|
| 138 |
|
| 139 |
+
# ββ LLM Classifier System Prompt ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
+
|
| 141 |
_CLASSIFIER_SYSTEM = """You are the routing brain of LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE).
|
| 142 |
Your only job: read the user message and return a single JSON object routing it to the right handler.
|
| 143 |
|
|
|
|
| 146 |
"library_info" β questions about KU Library services, hours, rooms, borrowing, fines, ILL, staff, databases, ORCID, RefWorks, open access, APC, account, printing, remote access, LibKey, acquisitions, Khazna, thesis submission, library policies, alumni access
|
| 147 |
"search_academic"β user wants to FIND articles / papers / books / literature on a specific academic topic
|
| 148 |
"search_medical" β same as search_academic but topic is medical/clinical/nursing/pharmacy/health sciences
|
| 149 |
+
"general_recent" β questions about TRULY live/real-time data: breaking news, live sports scores, today's weather, current stock prices, events happening right now
|
| 150 |
+
"general" β ALL other factual questions: university info, people, history, science, technology, definitions, explanations, stable facts like who holds a position
|
| 151 |
|
| 152 |
RULES:
|
| 153 |
1. When in doubt between library_info and social, choose library_info if ANY library keyword is present.
|
|
|
|
| 156 |
4. "I need help finding articles on diabetes" = search_medical.
|
| 157 |
5. KU = Khalifa University. Do NOT confuse with Kuwait University.
|
| 158 |
6. ANY question about advances, approaches, methods, effectiveness, impact, trends on a specific academic topic = search_academic or search_medical.
|
| 159 |
+
7. Questions about people, university leadership, presidents, provosts, history, science, technology = general (answered via web search).
|
| 160 |
+
8. Only use general_recent for truly live data that changes by the minute/hour.
|
| 161 |
|
| 162 |
For "social" intent ONLY: include "casual_answer" β a warm 1-3 sentence LibBee response (no markdown, no bullets).
|
| 163 |
For all other intents: omit "casual_answer".
|
|
|
|
| 212 |
|
| 213 |
|
| 214 |
def _rule_based_classify(question: str) -> Dict:
|
| 215 |
+
"""Fallback classifier when no API key is available."""
|
| 216 |
q = question.lower()
|
| 217 |
if SOCIAL_RE.match(question.strip()):
|
| 218 |
return {
|
|
|
|
| 405 |
)
|
| 406 |
|
| 407 |
|
| 408 |
+
async def _web_search_answer(
|
| 409 |
+
question: str,
|
| 410 |
+
history: List[ChatMessage],
|
| 411 |
+
model: str,
|
| 412 |
+
) -> str:
|
| 413 |
+
"""
|
| 414 |
+
Answer using live web search for BOTH models.
|
| 415 |
+
|
| 416 |
+
Claude β Anthropic web search tool (web_search_20250305)
|
| 417 |
+
GPT β OpenAI Responses API with web_search_preview tool
|
| 418 |
+
|
| 419 |
+
Both fall back to _llm_general_answer() if web search fails.
|
| 420 |
+
"""
|
| 421 |
+
settings = get_settings()
|
| 422 |
+
|
| 423 |
+
# ββ Claude: Anthropic web search tool βββββββββββββββββββββββββββββββββ
|
| 424 |
+
if model == "claude" and settings.anthropic_api_key:
|
| 425 |
+
try:
|
| 426 |
+
import anthropic
|
| 427 |
+
client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
|
| 428 |
+
|
| 429 |
+
response = client.messages.create(
|
| 430 |
+
model="claude-3-haiku-20240307",
|
| 431 |
+
max_tokens=600,
|
| 432 |
+
system=(
|
| 433 |
+
"You are LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE). "
|
| 434 |
+
"Answer the question using current web search results. "
|
| 435 |
+
"Be concise and factual. Use HTML <br> for line breaks. "
|
| 436 |
+
"KU = Khalifa University. "
|
| 437 |
+
"If the topic is academic, briefly mention you can help find scholarly articles."
|
| 438 |
+
),
|
| 439 |
+
tools=[{"type": "web_search_20250305", "name": "web_search"}],
|
| 440 |
+
messages=[{"role": "user", "content": question}],
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
text = ""
|
| 444 |
+
for block in response.content:
|
| 445 |
+
if hasattr(block, "text"):
|
| 446 |
+
text += block.text
|
| 447 |
+
|
| 448 |
+
if text.strip():
|
| 449 |
+
logger.info("Claude web search succeeded")
|
| 450 |
+
return text.strip()
|
| 451 |
+
|
| 452 |
+
except Exception as e:
|
| 453 |
+
logger.warning(f"Claude web search failed: {e} β trying GPT web search")
|
| 454 |
+
|
| 455 |
+
# ββ GPT: OpenAI Responses API with web_search_preview βββββββββββββββββ
|
| 456 |
+
if settings.openai_api_key:
|
| 457 |
+
try:
|
| 458 |
+
from openai import OpenAI
|
| 459 |
+
client = OpenAI(api_key=settings.openai_api_key)
|
| 460 |
+
|
| 461 |
+
response = client.responses.create(
|
| 462 |
+
model="gpt-4o-mini",
|
| 463 |
+
tools=[{"type": "web_search_preview"}],
|
| 464 |
+
instructions=(
|
| 465 |
+
"You are LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE). "
|
| 466 |
+
"Answer the question using current web search results. "
|
| 467 |
+
"Be concise and factual. Use HTML <br> for line breaks. "
|
| 468 |
+
"KU = Khalifa University β do NOT confuse with Kuwait University. "
|
| 469 |
+
"If the topic is academic, briefly mention you can help find scholarly articles."
|
| 470 |
+
),
|
| 471 |
+
input=question,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
# Extract text from Responses API output items
|
| 475 |
+
text = ""
|
| 476 |
+
for item in response.output:
|
| 477 |
+
if hasattr(item, "content"):
|
| 478 |
+
for block in item.content:
|
| 479 |
+
if hasattr(block, "text"):
|
| 480 |
+
text += block.text
|
| 481 |
+
|
| 482 |
+
if text.strip():
|
| 483 |
+
logger.info("GPT web search succeeded")
|
| 484 |
+
return text.strip()
|
| 485 |
+
|
| 486 |
+
except Exception as e:
|
| 487 |
+
logger.warning(f"GPT web search failed: {e} β falling back to LLM knowledge")
|
| 488 |
+
|
| 489 |
+
# ββ Final fallback: LLM training knowledge ββββββββββββββββββββββββββββ
|
| 490 |
+
logger.warning("Both web search tools failed β answering from LLM training knowledge")
|
| 491 |
+
return await _llm_general_answer(question, history, model)
|
| 492 |
+
|
| 493 |
+
|
| 494 |
async def _llm_general_answer(
|
| 495 |
question: str,
|
| 496 |
history: List[ChatMessage],
|
|
|
|
| 498 |
) -> str:
|
| 499 |
"""
|
| 500 |
Answer general factual questions from LLM training knowledge.
|
| 501 |
+
Used as fallback when web search is unavailable or fails.
|
| 502 |
"""
|
| 503 |
settings = get_settings()
|
| 504 |
if not settings.openai_api_key:
|
|
|
|
| 513 |
"Answer the user's question factually and concisely in 3-5 sentences. "
|
| 514 |
"Use HTML <br> for line breaks. "
|
| 515 |
"KU means Khalifa University β do NOT confuse with Kuwait University. "
|
| 516 |
+
"Note: your training data has a cutoff date so some facts may be outdated β "
|
| 517 |
+
"mention this only if the question is about something that changes frequently. "
|
| 518 |
+
"If the topic is academic, add one short sentence offering to find library resources."
|
| 519 |
)
|
| 520 |
|
| 521 |
try:
|
|
|
|
| 534 |
)
|
| 535 |
|
| 536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
# ββ Main Endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 538 |
|
| 539 |
@router.post("", response_model=AgentResponse)
|
|
|
|
| 694 |
)
|
| 695 |
|
| 696 |
elif intent == "general_recent":
|
| 697 |
+
# Truly live/real-time questions β web search (both models)
|
| 698 |
answer = await _web_search_answer(question, history, model)
|
|
|
|
| 699 |
return AgentResponse(
|
| 700 |
answer=answer,
|
| 701 |
intent="general_recent",
|
| 702 |
+
tools_used=["web_search"],
|
| 703 |
model_used=model,
|
| 704 |
response_time=time.time() - start_time,
|
| 705 |
)
|
| 706 |
|
| 707 |
else: # general
|
| 708 |
+
# Factual questions (people, places, science, university info, etc.)
|
| 709 |
+
# β web search for current accurate answers (both models)
|
| 710 |
+
answer = await _web_search_answer(question, history, model)
|
| 711 |
return AgentResponse(
|
| 712 |
answer=answer,
|
| 713 |
intent="general",
|
| 714 |
+
tools_used=["web_search"],
|
| 715 |
model_used=model,
|
| 716 |
response_time=time.time() - start_time,
|
| 717 |
)
|