nikeshn commited on
Commit
2c2a7a2
Β·
verified Β·
1 Parent(s): 628312d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -3
app.py CHANGED
@@ -1,18 +1,24 @@
1
  """
2
  Khalifa University Library AI Agent
3
- MCP-style tool-calling backend with RAG, PRIMO, PubMed, Google Scholar, Consensus
4
 
5
  Tools:
6
  - search_primo: Search KU Library catalog
7
  - search_pubmed: Search biomedical literature
8
  - search_scholar: Search Google Scholar
9
  - search_consensus: Search Consensus (research papers)
 
10
  - get_library_info: RAG from KU library knowledge base
11
 
12
  Environment variables (HF Space Secrets):
13
  OPENAI_API_KEY β€” required (embeddings + ChatGPT)
14
  ANTHROPIC_API_KEY β€” optional (Claude answers)
15
  PRIMO_API_KEY β€” required (PRIMO search)
 
 
 
 
 
16
  """
17
 
18
  import os
@@ -1006,6 +1012,166 @@ async def tool_search_pubmed(query, limit=5):
1006
  return {"error": f"PubMed: {str(e)}", "results": [], "source": "PubMed"}
1007
 
1008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  # ===== TOOL: SEARCH CONSENSUS (via Semantic Scholar with consensus framing) =====
1010
  async def tool_search_consensus(query, limit=5):
1011
  """
@@ -1235,7 +1401,7 @@ class ChatMessage(BaseModel):
1235
 
1236
  class SearchRequest(BaseModel):
1237
  query: str
1238
- source: str = "primo" # primo, pubmed, scholar, consensus, all
1239
  model: str = "gpt"
1240
  limit: int = 5
1241
  peer_reviewed: bool = False
@@ -1260,7 +1426,7 @@ def health():
1260
  return {
1261
  "status": "ok",
1262
  "vectorstore_ready": vectorstore is not None,
1263
- "tools": ["search_primo", "search_pubmed", "search_scholar", "search_consensus", "get_library_info"],
1264
  "endpoints": ["/rag", "/search", "/agent", "/general", "/config", "/year"],
1265
  "models": {
1266
  "gpt": bool(os.environ.get("OPENAI_API_KEY")),
@@ -1712,6 +1878,8 @@ async def search(req: SearchRequest):
1712
  result = await tool_search_scholar(req.query, req.limit)
1713
  elif source == "consensus":
1714
  result = await tool_search_consensus(req.query, req.limit)
 
 
1715
  elif source == "all":
1716
  import asyncio
1717
  tasks = [
 
1
  """
2
  Khalifa University Library AI Agent
3
+ MCP-style tool-calling backend with RAG, PRIMO, PubMed, Google Scholar, Consensus, EBSCO EDS
4
 
5
  Tools:
6
  - search_primo: Search KU Library catalog
7
  - search_pubmed: Search biomedical literature
8
  - search_scholar: Search Google Scholar
9
  - search_consensus: Search Consensus (research papers)
10
+ - search_eds: Search EBSCO Discovery Service
11
  - get_library_info: RAG from KU library knowledge base
12
 
13
  Environment variables (HF Space Secrets):
14
  OPENAI_API_KEY β€” required (embeddings + ChatGPT)
15
  ANTHROPIC_API_KEY β€” optional (Claude answers)
16
  PRIMO_API_KEY β€” required (PRIMO search)
17
+ EDS_USER_ID β€” required (EBSCO EDS API)
18
+ EDS_PASSWORD β€” required (EBSCO EDS API)
19
+ EDS_PROFILE β€” optional (default: edsapi)
20
+ EDS_INTERFACE β€” optional (default: wsapi)
21
+ EDS_ORG β€” optional (default: ns174973)
22
  """
23
 
24
  import os
 
1012
  return {"error": f"PubMed: {str(e)}", "results": [], "source": "PubMed"}
1013
 
1014
 
1015
+ # ===== TOOL: SEARCH EDS (EBSCO Discovery Service) =====
1016
+ async def tool_search_eds(query: str, limit: int = 5) -> dict:
1017
+ """
1018
+ Search EBSCO EDS API.
1019
+ Auth flow: UIDAuth β†’ CreateSession β†’ Search β†’ EndSession
1020
+ Credentials from HF Space secrets: EDS_USER_ID, EDS_PASSWORD,
1021
+ EDS_PROFILE, EDS_INTERFACE, EDS_ORG
1022
+ """
1023
+ user_id = os.environ.get("EDS_USER_ID")
1024
+ password = os.environ.get("EDS_PASSWORD")
1025
+ profile = os.environ.get("EDS_PROFILE", "edsapi")
1026
+ interface = os.environ.get("EDS_INTERFACE", "wsapi")
1027
+ org = os.environ.get("EDS_ORG", "ns174973")
1028
+
1029
+ if not user_id or not password:
1030
+ return {"error": "EDS credentials not configured", "results": [], "source": "EBSCO EDS"}
1031
+
1032
+ auth_url = "https://eds-api.ebscohost.com/authservice/rest/UIDAuth"
1033
+ session_url = "https://eds-api.ebscohost.com/edsapi/rest/CreateSession"
1034
+ search_url = "https://eds-api.ebscohost.com/edsapi/rest/Search"
1035
+ end_url = "https://eds-api.ebscohost.com/edsapi/rest/EndSession"
1036
+
1037
+ headers_base = {"Content-Type": "application/json", "Accept": "application/json"}
1038
+
1039
+ try:
1040
+ async with httpx.AsyncClient(timeout=20) as client:
1041
+
1042
+ # ── Step 1: Authentication token ──
1043
+ auth_r = await client.post(auth_url, json={
1044
+ "UserId": user_id,
1045
+ "Password": password,
1046
+ "InterfaceId": interface,
1047
+ }, headers=headers_base)
1048
+ if auth_r.status_code != 200:
1049
+ return {"error": f"EDS auth failed: {auth_r.status_code}", "results": [], "source": "EBSCO EDS"}
1050
+ auth_token = auth_r.json().get("AuthToken")
1051
+ if not auth_token:
1052
+ return {"error": "EDS: no AuthToken in response", "results": [], "source": "EBSCO EDS"}
1053
+
1054
+ # ── Step 2: Session token ──
1055
+ sess_headers = {**headers_base, "x-authenticationToken": auth_token}
1056
+ sess_r = await client.post(session_url, json={
1057
+ "Profile": profile,
1058
+ "Guest": "n",
1059
+ "Org": org,
1060
+ }, headers=sess_headers)
1061
+ if sess_r.status_code != 200:
1062
+ return {"error": f"EDS session failed: {sess_r.status_code}", "results": [], "source": "EBSCO EDS"}
1063
+ session_token = sess_r.json().get("SessionToken")
1064
+ if not session_token:
1065
+ return {"error": "EDS: no SessionToken in response", "results": [], "source": "EBSCO EDS"}
1066
+
1067
+ # ── Step 3: Search ──
1068
+ search_headers = {
1069
+ **headers_base,
1070
+ "x-authenticationToken": auth_token,
1071
+ "x-sessionToken": session_token,
1072
+ }
1073
+ params = {
1074
+ "query-1": f"AND,{query}",
1075
+ "resultsperpage": str(limit),
1076
+ "pagenumber": "1",
1077
+ "highlight": "n",
1078
+ "includefacets": "n",
1079
+ "sort": "relevance",
1080
+ }
1081
+ search_r = await client.get(search_url, params=params, headers=search_headers)
1082
+
1083
+ results = []
1084
+ total = 0
1085
+ if search_r.status_code == 200:
1086
+ data = search_r.json()
1087
+ search_res = data.get("SearchResult", {})
1088
+ statistics = search_res.get("Statistics", {})
1089
+ total = statistics.get("TotalHits", 0)
1090
+ records = search_res.get("Data", {}).get("Records", []) or []
1091
+
1092
+ for rec in records[:limit]:
1093
+ # Title
1094
+ title_items = rec.get("RecordInfo", {}).get("BibRecord", {}).get("BibEntity", {}).get("Titles", [])
1095
+ title = next((t.get("TitleFull", "") for t in title_items if t.get("Type") == "main"), "") or \
1096
+ (title_items[0].get("TitleFull", "") if title_items else "Untitled")
1097
+
1098
+ # Authors
1099
+ contributors = rec.get("RecordInfo", {}).get("BibRecord", {}).get("BibRelationships", {}) \
1100
+ .get("HasContributorRelationships", [])
1101
+ authors = []
1102
+ for c in contributors[:3]:
1103
+ parts = c.get("PersonEntity", {}).get("Name", {})
1104
+ name = parts.get("NameFull") or \
1105
+ f"{parts.get('NameLast','')}, {parts.get('NameFirst','')}".strip(", ")
1106
+ if name:
1107
+ authors.append(name)
1108
+ if len(contributors) > 3:
1109
+ authors.append("et al.")
1110
+ creator = "; ".join(authors) if authors else "Unknown"
1111
+
1112
+ # Date / source / volume / issue
1113
+ pub_info = rec.get("RecordInfo", {}).get("BibRecord", {}).get("BibRelationships", {}) \
1114
+ .get("IsPartOfRelationships", [])
1115
+ date = volume = issue = source_title = ""
1116
+ if pub_info:
1117
+ pub = pub_info[0]
1118
+ bib_entity = pub.get("BibEntity", {})
1119
+ dates = bib_entity.get("Dates", [])
1120
+ if dates:
1121
+ d = dates[0]
1122
+ date = d.get("Y", "") + ("-" + d.get("M", "") if d.get("M") else "")
1123
+ nums = bib_entity.get("Numbering", [])
1124
+ for n in nums:
1125
+ if n.get("Type") == "volume": volume = n.get("Value", "")
1126
+ if n.get("Type") == "issue": issue = n.get("Value", "")
1127
+ titles = bib_entity.get("Titles", [])
1128
+ source_title = titles[0].get("TitleFull", "") if titles else ""
1129
+
1130
+ # Abstract
1131
+ abstract = ""
1132
+ items = rec.get("Items", [])
1133
+ for item in items:
1134
+ if item.get("Name") == "Abstract":
1135
+ abstract = re.sub(r"<[^>]+>", "", item.get("Data", ""))[:400]
1136
+ break
1137
+
1138
+ # Persistent link
1139
+ plink = rec.get("PLink", "")
1140
+
1141
+ # DOI from custom links
1142
+ doi = None
1143
+ for link in rec.get("CustomLinks", []):
1144
+ url_val = link.get("Url", "")
1145
+ if "doi.org" in url_val:
1146
+ doi = url_val.split("doi.org/")[-1]
1147
+ break
1148
+
1149
+ results.append({
1150
+ "title": title,
1151
+ "creator": creator,
1152
+ "date": date,
1153
+ "source": source_title,
1154
+ "volume": volume,
1155
+ "issue": issue,
1156
+ "description": abstract,
1157
+ "doi": doi,
1158
+ "link": plink,
1159
+ "type": "Journal Article",
1160
+ "_source": "EBSCO EDS",
1161
+ })
1162
+
1163
+ # ── Step 4: End session (cleanup) ──
1164
+ try:
1165
+ await client.get(end_url, headers=search_headers)
1166
+ except Exception:
1167
+ pass # non-critical
1168
+
1169
+ return {"total": total, "results": results, "source": "EBSCO EDS"}
1170
+
1171
+ except Exception as e:
1172
+ return {"error": f"EDS: {str(e)}", "results": [], "source": "EBSCO EDS"}
1173
+
1174
+
1175
  # ===== TOOL: SEARCH CONSENSUS (via Semantic Scholar with consensus framing) =====
1176
  async def tool_search_consensus(query, limit=5):
1177
  """
 
1401
 
1402
  class SearchRequest(BaseModel):
1403
  query: str
1404
+ source: str = "primo" # primo, pubmed, scholar, consensus, eds, all
1405
  model: str = "gpt"
1406
  limit: int = 5
1407
  peer_reviewed: bool = False
 
1426
  return {
1427
  "status": "ok",
1428
  "vectorstore_ready": vectorstore is not None,
1429
+ "tools": ["search_primo", "search_pubmed", "search_scholar", "search_consensus", "search_eds", "get_library_info"],
1430
  "endpoints": ["/rag", "/search", "/agent", "/general", "/config", "/year"],
1431
  "models": {
1432
  "gpt": bool(os.environ.get("OPENAI_API_KEY")),
 
1878
  result = await tool_search_scholar(req.query, req.limit)
1879
  elif source == "consensus":
1880
  result = await tool_search_consensus(req.query, req.limit)
1881
+ elif source == "eds":
1882
+ result = await tool_search_eds(req.query, req.limit)
1883
  elif source == "all":
1884
  import asyncio
1885
  tasks = [