nikeshn commited on
Commit
a78847b
Β·
verified Β·
1 Parent(s): b0dce90

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. src/api/agent.py +114 -27
src/api/agent.py CHANGED
@@ -31,7 +31,7 @@ class AgentRequest(BaseModel):
31
 
32
 
33
  class AgentResponse(BaseModel):
34
- model_config = ConfigDict(extra="ignore") # drop unknown fields silently
35
 
36
  answer: str
37
  intent: str = "general"
@@ -117,11 +117,14 @@ MEDICAL_SEARCH_RE = re.compile(
117
  r"|clinical trial|biomedical literature|medical literature)\b",
118
  re.IGNORECASE,
119
  )
 
 
120
  CURRENT_INFO_RE = re.compile(
121
  r"\b(current|currently|latest|today|recent|recently|now|this week|this month"
122
- r"|news|update|updated|president|prime minister|ceo|minister|king|ruler|who won)\b",
123
  re.IGNORECASE,
124
  )
 
125
  MEDICAL_KEYWORDS = [
126
  "medicine", "medical", "health", "clinical", "nursing", "pharmacy", "biomedical",
127
  "anatomy", "physiology", "epidemiology", "oncology", "cardiology", "neuroscience",
@@ -142,8 +145,8 @@ INTENTS β€” pick exactly one:
142
  "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
143
  "search_academic"β€” user wants to FIND articles / papers / books / literature on a specific academic topic
144
  "search_medical" β€” same as search_academic but topic is medical/clinical/nursing/pharmacy/health sciences
145
- "general_recent" β€” questions about current events, news, latest info, who holds a position now, real-time data
146
- "general" β€” factual/knowledge question NOT about KU Library and NOT a research search
147
 
148
  RULES:
149
  1. When in doubt between library_info and social, choose library_info if ANY library keyword is present.
@@ -152,6 +155,7 @@ RULES:
152
  4. "I need help finding articles on diabetes" = search_medical.
153
  5. KU = Khalifa University. Do NOT confuse with Kuwait University.
154
  6. ANY question about advances, approaches, methods, effectiveness, impact, trends on a specific academic topic = search_academic or search_medical.
 
155
 
156
  For "social" intent ONLY: include "casual_answer" β€” a warm 1-3 sentence LibBee response (no markdown, no bullets).
157
  For all other intents: omit "casual_answer".
@@ -258,14 +262,6 @@ def _campus_answer() -> str:
258
  )
259
 
260
 
261
- def _general_redirect_answer() -> str:
262
- return (
263
- "I'm <strong>LibBee</strong>, the Khalifa University <strong>Library</strong> AI Assistant β€” "
264
- "I'm best for library resources, services, databases, research help, and staff contacts.<br><br>"
265
- f'For general Khalifa University questions, please visit: <a href="{KU_MAIN_URL}" target="_blank">{KU_MAIN_URL}</a>.'
266
- )
267
-
268
-
269
  def _greeting_menu_clarify_answer() -> str:
270
  return (
271
  "Sure β€” what would you like help with?<br><br>"
@@ -281,7 +277,6 @@ def _greeting_menu_clarify_answer() -> str:
281
  # ── LLM Helpers ───────────────────────────────────────────────────────────────
282
 
283
  def _get_llm(model: str, temperature: float, max_tokens: int):
284
- """Instantiate LLM. Called once per request β€” move to singleton if latency is a concern."""
285
  settings = get_settings()
286
  if model == "claude" and settings.anthropic_api_key:
287
  from langchain_anthropic import ChatAnthropic
@@ -315,7 +310,11 @@ async def _llm_classify(question: str, history: List[ChatMessage], model: str) -
315
  return _rule_based_classify(question)
316
 
317
  try:
318
- ctx_parts = [f"{m.role}: {m.content[:100]}" for m in history[-HISTORY_WINDOW:] if m.role in ("user", "assistant")]
 
 
 
 
319
  ctx = " | ".join(ctx_parts) if ctx_parts else "none"
320
  user_msg = f"Message: {question[:400]}\nContext: {ctx}\nJSON:"
321
 
@@ -344,7 +343,6 @@ async def _llm_classify(question: str, history: List[ChatMessage], model: str) -
344
  async def _libbee_casual_response(
345
  question: str, history: List[ChatMessage], model: str, hint: str = ""
346
  ) -> str:
347
- # Use the classifier's hint directly if it's a real answer
348
  if hint and len(hint.strip()) > 25:
349
  return hint.strip()
350
 
@@ -376,7 +374,6 @@ async def _rag_answer(
376
  """Generate a grounded answer from RAG-retrieved chunks."""
377
  settings = get_settings()
378
  if not settings.openai_api_key:
379
- # No LLM β€” return top chunk directly
380
  return rag_results[0]["content"] if rag_results else ""
381
 
382
  context = "\n\n---\n\n".join(r["content"] for r in rag_results)
@@ -406,6 +403,98 @@ async def _rag_answer(
406
  )
407
 
408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  # ── Main Endpoint ──────────────────────────────────────────────────────────────
410
 
411
  @router.post("", response_model=AgentResponse)
@@ -502,7 +591,6 @@ async def agent_query(request: Request, req: AgentRequest):
502
  # ── LAYER 2: Intent Handlers ───────────────────────────────────────────
503
 
504
  if intent == "social":
505
- # Use classifier's casual_answer directly β€” avoids second LLM call
506
  answer = casual_hint if (casual_hint and len(casual_hint.strip()) > 25) else \
507
  await _libbee_casual_response(question, history, model, casual_hint)
508
  return AgentResponse(
@@ -514,7 +602,6 @@ async def agent_query(request: Request, req: AgentRequest):
514
  )
515
 
516
  elif intent == "library_info":
517
- # Import singleton from app.py
518
  try:
519
  from app import rag_service
520
  rag_results = await rag_service.hybrid_search(question, top_k=4)
@@ -568,24 +655,24 @@ async def agent_query(request: Request, req: AgentRequest):
568
  )
569
 
570
  elif intent == "general_recent":
 
 
 
571
  return AgentResponse(
572
- answer=(
573
- "I'm LibBee, a library assistant β€” I don't have access to real-time or current information.<br><br>"
574
- "For the latest news or live data, please use a web search engine.<br><br>"
575
- "If you're looking for <strong>recent academic publications</strong> on this topic, "
576
- "I can help with that β€” just ask me to search for articles."
577
- ),
578
  intent="general_recent",
579
- tools_used=[],
580
  model_used=model,
581
  response_time=time.time() - start_time,
582
  )
583
 
584
  else: # general
 
 
585
  return AgentResponse(
586
- answer=_general_redirect_answer(),
587
  intent="general",
588
- tools_used=["general_redirect"],
589
  model_used=model,
590
  response_time=time.time() - start_time,
591
  )
 
31
 
32
 
33
  class AgentResponse(BaseModel):
34
+ model_config = ConfigDict(extra="ignore")
35
 
36
  answer: str
37
  intent: str = "general"
 
117
  r"|clinical trial|biomedical literature|medical literature)\b",
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",
125
  re.IGNORECASE,
126
  )
127
+
128
  MEDICAL_KEYWORDS = [
129
  "medicine", "medical", "health", "clinical", "nursing", "pharmacy", "biomedical",
130
  "anatomy", "physiology", "epidemiology", "oncology", "cardiology", "neuroscience",
 
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
  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".
 
262
  )
263
 
264
 
 
 
 
 
 
 
 
 
265
  def _greeting_menu_clarify_answer() -> str:
266
  return (
267
  "Sure β€” what would you like help with?<br><br>"
 
277
  # ── LLM Helpers ───────────────────────────────────────────────────────────────
278
 
279
  def _get_llm(model: str, temperature: float, max_tokens: int):
 
280
  settings = get_settings()
281
  if model == "claude" and settings.anthropic_api_key:
282
  from langchain_anthropic import ChatAnthropic
 
310
  return _rule_based_classify(question)
311
 
312
  try:
313
+ ctx_parts = [
314
+ f"{m.role}: {m.content[:100]}"
315
+ for m in history[-HISTORY_WINDOW:]
316
+ if m.role in ("user", "assistant")
317
+ ]
318
  ctx = " | ".join(ctx_parts) if ctx_parts else "none"
319
  user_msg = f"Message: {question[:400]}\nContext: {ctx}\nJSON:"
320
 
 
343
  async def _libbee_casual_response(
344
  question: str, history: List[ChatMessage], model: str, hint: str = ""
345
  ) -> str:
 
346
  if hint and len(hint.strip()) > 25:
347
  return hint.strip()
348
 
 
374
  """Generate a grounded answer from RAG-retrieved chunks."""
375
  settings = get_settings()
376
  if not settings.openai_api_key:
 
377
  return rag_results[0]["content"] if rag_results else ""
378
 
379
  context = "\n\n---\n\n".join(r["content"] for r in rag_results)
 
403
  )
404
 
405
 
406
+ async def _llm_general_answer(
407
+ question: str,
408
+ history: List[ChatMessage],
409
+ model: str,
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:
417
+ return (
418
+ "I'm <strong>LibBee</strong>, the Khalifa University Library AI Assistant. "
419
+ "I can best help with library resources, databases, and research support. "
420
+ f'For general questions, please visit <a href="{KU_MAIN_URL}" target="_blank">ku.ac.ae</a>.'
421
+ )
422
+
423
+ system = (
424
+ "You are LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE). "
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:
433
+ llm = _get_llm(model, temperature=0.3, max_tokens=400)
434
+ msgs = [{"role": "system", "content": system}]
435
+ msgs.extend(_build_history_messages(history))
436
+ msgs.append({"role": "user", "content": question})
437
+ response = await llm.ainvoke(msgs)
438
+ return response.content.strip()
439
+ except Exception as e:
440
+ logger.error(f"_llm_general_answer error: {e}")
441
+ return (
442
+ "I'm having trouble answering that right now. "
443
+ "For general Khalifa University questions, please visit "
444
+ f'<a href="{KU_MAIN_URL}" target="_blank">ku.ac.ae</a>.'
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)
 
591
  # ── LAYER 2: Intent Handlers ───────────────────────────────────────────
592
 
593
  if intent == "social":
 
594
  answer = casual_hint if (casual_hint and len(casual_hint.strip()) > 25) else \
595
  await _libbee_casual_response(question, history, model, casual_hint)
596
  return AgentResponse(
 
602
  )
603
 
604
  elif intent == "library_info":
 
605
  try:
606
  from app import rag_service
607
  rag_results = await rag_service.hybrid_search(question, top_k=4)
 
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
  )