Update app.py
Browse files
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):
|