nikeshn commited on
Commit
e9b2d8c
·
verified ·
1 Parent(s): fd79e98

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -0
app.py CHANGED
@@ -487,6 +487,7 @@ def health():
487
  "status": "ok",
488
  "vectorstore_ready": vectorstore is not None,
489
  "tools": ["search_primo", "search_pubmed", "search_scholar", "search_consensus", "get_library_info"],
 
490
  "models": {
491
  "gpt": bool(os.environ.get("OPENAI_API_KEY")),
492
  "claude": bool(os.environ.get("ANTHROPIC_API_KEY")),
@@ -585,6 +586,129 @@ async def rag_query(req: RAGRequest):
585
  return {"answer": "Error processing your question.", "sources": [], "error": str(e)}
586
 
587
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  # ---- Rebuild index (protected) ----
589
  @app.post("/rebuild")
590
  async def rebuild_index(request: Request):
 
487
  "status": "ok",
488
  "vectorstore_ready": vectorstore is not None,
489
  "tools": ["search_primo", "search_pubmed", "search_scholar", "search_consensus", "get_library_info"],
490
+ "endpoints": ["/rag", "/search", "/agent", "/config", "/year"],
491
  "models": {
492
  "gpt": bool(os.environ.get("OPENAI_API_KEY")),
493
  "claude": bool(os.environ.get("ANTHROPIC_API_KEY")),
 
586
  return {"answer": "Error processing your question.", "sources": [], "error": str(e)}
587
 
588
 
589
+ # ---- Agent endpoint (Batch 3: tool-calling agent) ----
590
+ @app.post("/agent")
591
+ async def agent_query(req: AgentRequest):
592
+ """
593
+ Multi-tool agent. Given a question it:
594
+ 1. Classifies intent (library_info | search_academic | search_medical | general)
595
+ 2. Calls the right combination of tools in parallel
596
+ 3. Synthesises results into a single answer with sources
597
+ """
598
+ start = time.time()
599
+ question = req.question
600
+ history = [{"role": m.role, "content": m.content} for m in req.history] if req.history else []
601
+
602
+ # ---- Step 1: classify intent ----
603
+ try:
604
+ classifier_prompt = f"""Classify this library chatbot question into ONE category.
605
+ Question: "{question}"
606
+ Categories:
607
+ - library_info: library hours, services, accounts, policies, databases, staff
608
+ - search_academic: find articles, books, papers on a research topic
609
+ - search_medical: biomedical, clinical, health sciences research
610
+ - general: factual/general knowledge question
611
+
612
+ Return ONLY valid JSON: {{"intent":"<category>","search_query":"<2-5 keyword search query if searching, else empty>"}}"""
613
+
614
+ use_claude = req.model == "claude" and os.environ.get("ANTHROPIC_API_KEY")
615
+ if use_claude:
616
+ from langchain_anthropic import ChatAnthropic
617
+ clf_llm = ChatAnthropic(model="claude-haiku-4-5-20251001", temperature=0, max_tokens=100)
618
+ else:
619
+ clf_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, max_tokens=100)
620
+
621
+ clf_resp = clf_llm.invoke(classifier_prompt)
622
+ clf_text = clf_resp.content.strip()
623
+ # Extract JSON safely
624
+ s, e = clf_text.find("{"), clf_text.rfind("}")
625
+ clf = json.loads(clf_text[s:e+1]) if s != -1 else {}
626
+ intent = clf.get("intent", "general")
627
+ search_query = clf.get("search_query", question)
628
+ except Exception:
629
+ intent = "general"
630
+ search_query = question
631
+
632
+ # ---- Step 2: run tools based on intent ----
633
+ tool_results = {}
634
+ tools_used = []
635
+
636
+ if intent == "library_info":
637
+ # RAG from KU knowledge base
638
+ rag = await tool_library_info(question, history[-5:] if history else None, model=req.model)
639
+ tool_results["rag"] = rag
640
+ tools_used.append("get_library_info")
641
+
642
+ elif intent in ("search_academic", "search_medical"):
643
+ # Run PRIMO + PubMed (if medical) in parallel
644
+ import asyncio
645
+ tasks = [tool_search_primo(search_query, limit=5)]
646
+ if intent == "search_medical":
647
+ tasks.append(tool_search_pubmed(search_query, limit=3))
648
+ else:
649
+ tasks.append(tool_search_scholar(search_query, limit=3))
650
+
651
+ raw_results = await asyncio.gather(*tasks, return_exceptions=True)
652
+ combined = []
653
+ for r in raw_results:
654
+ if isinstance(r, dict) and r.get("results"):
655
+ combined.extend(r["results"])
656
+ tools_used.append(r.get("source", "unknown"))
657
+
658
+ tool_results["search"] = {"results": combined[:8], "total": len(combined)}
659
+ tools_used = list(set(tools_used))
660
+
661
+ # Also get RAG context for library-specific guidance
662
+ rag = await tool_library_info(question, history[-3:] if history else None, model=req.model)
663
+ tool_results["rag"] = rag
664
+ tools_used.append("get_library_info")
665
+
666
+ else:
667
+ # General question — use RAG + LLM
668
+ rag = await tool_library_info(question, history[-5:] if history else None, model=req.model)
669
+ tool_results["rag"] = rag
670
+ tools_used.append("get_library_info")
671
+
672
+ # ---- Step 3: synthesise answer ----
673
+ context_parts = []
674
+ if "rag" in tool_results and tool_results["rag"].get("answer"):
675
+ context_parts.append(f"Library Knowledge Base:\n{tool_results['rag']['answer']}")
676
+ if "search" in tool_results and tool_results["search"].get("results"):
677
+ top = tool_results["search"]["results"][:3]
678
+ res_text = "\n".join(f"- {r.get('title','')} by {r.get('creator','')} ({r.get('date','')})" for r in top)
679
+ context_parts.append(f"Search Results:\n{res_text}")
680
+
681
+ synthesis_prompt = f"""You are the Khalifa University Library AI Assistant (Abu Dhabi, UAE). KU = Khalifa University.
682
+ Be concise (3-5 sentences). Include relevant URLs. If search results are present, mention the top 2-3.
683
+
684
+ Context:
685
+ {chr(10).join(context_parts) if context_parts else 'No additional context.'}
686
+
687
+ Question: {question}
688
+ Answer:"""
689
+
690
+ try:
691
+ if use_claude:
692
+ from langchain_anthropic import ChatAnthropic
693
+ synth_llm = ChatAnthropic(model="claude-haiku-4-5-20251001", temperature=0.2, max_tokens=600)
694
+ else:
695
+ synth_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2, max_tokens=600)
696
+ answer = synth_llm.invoke(synthesis_prompt).content
697
+ except Exception as ex:
698
+ answer = tool_results.get("rag", {}).get("answer", f"Error: {str(ex)}")
699
+
700
+ elapsed = time.time() - start
701
+ return {
702
+ "answer": answer,
703
+ "intent": intent,
704
+ "tools_used": tools_used,
705
+ "search_results": tool_results.get("search", {}).get("results", []),
706
+ "sources": tool_results.get("rag", {}).get("sources", []),
707
+ "model_used": req.model,
708
+ "response_time": round(elapsed, 2),
709
+ }
710
+
711
+
712
  # ---- Rebuild index (protected) ----
713
  @app.post("/rebuild")
714
  async def rebuild_index(request: Request):