nikeshn commited on
Commit
a74c0f9
Β·
verified Β·
1 Parent(s): f7907fd

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. 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-specific questions incorrectly
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 system prompt
 
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 current/live events: breaking news, stock prices, live sports scores, today's weather, real-time data that changes by the hour. NOT questions about stable facts like university leadership.
149
- "general" β€” factual/knowledge question NOT about KU Library and NOT a research search β€” including questions about universities, people, places, history, science, technology
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, staff = general (not general_recent β€” these are stable facts).
 
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 used when no API key is available."""
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 for: university info, people, places, science, technology, etc.
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
- "If the topic is academic, add one short sentence at the end offering to find "
429
- "library resources or articles on the topic."
 
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
- # Real-time/live questions β†’ Claude web search or GPT knowledge fallback
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=tools_used,
665
  model_used=model,
666
  response_time=time.time() - start_time,
667
  )
668
 
669
  else: # general
670
- # Factual questions β†’ LLM answers from training knowledge
671
- answer = await _llm_general_answer(question, history, model)
 
672
  return AgentResponse(
673
  answer=answer,
674
  intent="general",
675
- tools_used=["llm_knowledge"],
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
  )