Spaces:
Running
Running
Upload app.py
Browse files
app.py
CHANGED
|
@@ -935,6 +935,129 @@ async def chat(req: ChatRequest, request: Request):
|
|
| 935 |
)
|
| 936 |
|
| 937 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 938 |
# ββ Static frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 939 |
@app.get("/")
|
| 940 |
async def root():
|
|
|
|
| 935 |
)
|
| 936 |
|
| 937 |
|
| 938 |
+
# ββ Journal meta lookup β ISSN + publisher auto-fill βββββββββββββββββββββ
|
| 939 |
+
@app.get("/api/lookup-journal-meta")
|
| 940 |
+
async def lookup_journal_meta(request: Request, name: str):
|
| 941 |
+
"""
|
| 942 |
+
Two-layer journal lookup:
|
| 943 |
+
Layer 1: SCImago (exact) β OpenAlex (title search) β returns ISSN + publisher
|
| 944 |
+
Layer 2: If not found, LLM corrects spelling only β never returns ISSN
|
| 945 |
+
"""
|
| 946 |
+
ip = get_client_ip(request)
|
| 947 |
+
if not check_rate_limit(ip, "global"):
|
| 948 |
+
raise HTTPException(429, "Too many requests.")
|
| 949 |
+
|
| 950 |
+
name = name.strip()
|
| 951 |
+
if not name or len(name) < 3:
|
| 952 |
+
return {"found": False}
|
| 953 |
+
|
| 954 |
+
# ββ Layer 1a: SCImago fuzzy title match βββββββββββββββββββββββββββββββ
|
| 955 |
+
from scimago_index import load_subject_index
|
| 956 |
+
rows = load_subject_index()
|
| 957 |
+
name_lower = name.lower()
|
| 958 |
+
|
| 959 |
+
# Exact match first
|
| 960 |
+
exact = next((r for r in rows if r["title"].lower() == name_lower), None)
|
| 961 |
+
if exact:
|
| 962 |
+
return {
|
| 963 |
+
"found": True,
|
| 964 |
+
"issn": exact.get("issn", ""),
|
| 965 |
+
"publisher": exact.get("publisher", ""),
|
| 966 |
+
"title": exact["title"],
|
| 967 |
+
"source": "SCImago",
|
| 968 |
+
"confidence": "high",
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
# Partial match β title contains all words
|
| 972 |
+
words = [w for w in name_lower.split() if len(w) > 2]
|
| 973 |
+
partial = next((r for r in rows if all(w in r["title"].lower() for w in words)), None)
|
| 974 |
+
if partial:
|
| 975 |
+
return {
|
| 976 |
+
"found": True,
|
| 977 |
+
"issn": partial.get("issn", ""),
|
| 978 |
+
"publisher": partial.get("publisher", ""),
|
| 979 |
+
"title": partial["title"],
|
| 980 |
+
"source": "SCImago",
|
| 981 |
+
"confidence": "medium",
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
# ββ Layer 1b: OpenAlex title search βββββββββββββββββββββββββββββββββββ
|
| 985 |
+
try:
|
| 986 |
+
oa_client = openalex._get_client()
|
| 987 |
+
r = await oa_client.get(
|
| 988 |
+
"https://api.openalex.org/sources",
|
| 989 |
+
params={"search": name, "per_page": 1, "filter": "type:journal"},
|
| 990 |
+
timeout=5.0,
|
| 991 |
+
)
|
| 992 |
+
if r.is_success:
|
| 993 |
+
results = r.json().get("results", [])
|
| 994 |
+
if results:
|
| 995 |
+
s = results[0]
|
| 996 |
+
title = s.get("display_name", "")
|
| 997 |
+
issns = s.get("issn", [])
|
| 998 |
+
issn_l = s.get("issn_l", "")
|
| 999 |
+
publisher = s.get("host_organization_name", "")
|
| 1000 |
+
best_issn = issn_l or (issns[0] if issns else "")
|
| 1001 |
+
ni = norm_issn(best_issn)
|
| 1002 |
+
if title:
|
| 1003 |
+
return {
|
| 1004 |
+
"found": True,
|
| 1005 |
+
"issn": ni or best_issn,
|
| 1006 |
+
"publisher": publisher,
|
| 1007 |
+
"title": title,
|
| 1008 |
+
"source": "OpenAlex",
|
| 1009 |
+
"confidence": "high" if title.lower() == name_lower else "medium",
|
| 1010 |
+
}
|
| 1011 |
+
except Exception as e:
|
| 1012 |
+
logger.warning(f"[LookupJournal] OpenAlex error: {e}")
|
| 1013 |
+
|
| 1014 |
+
# ββ Layer 2: LLM spelling correction only β NO ISSN ββββββββββββββββββ
|
| 1015 |
+
try:
|
| 1016 |
+
client = get_client()
|
| 1017 |
+
resp = await client.chat.completions.create(
|
| 1018 |
+
model=MODEL,
|
| 1019 |
+
response_format={"type": "json_object"},
|
| 1020 |
+
messages=[
|
| 1021 |
+
{
|
| 1022 |
+
"role": "system",
|
| 1023 |
+
"content": (
|
| 1024 |
+
"You are an academic journal name expert. "
|
| 1025 |
+
"The user has typed a journal name that may be misspelled or abbreviated. "
|
| 1026 |
+
"Return ONLY valid JSON: "
|
| 1027 |
+
"{corrected_name: string, confidence: high|medium|low, known: bool}. "
|
| 1028 |
+
"corrected_name: the most likely correct full journal name. "
|
| 1029 |
+
"If you do not recognise it, return the input unchanged. "
|
| 1030 |
+
"confidence: high if certain, medium if likely, low if guessing. "
|
| 1031 |
+
"known: true only if you are confident this journal exists. "
|
| 1032 |
+
"Do NOT return ISSN or publisher. Return JSON only."
|
| 1033 |
+
),
|
| 1034 |
+
},
|
| 1035 |
+
{"role": "user", "content": f"Journal name input: {name}"},
|
| 1036 |
+
],
|
| 1037 |
+
max_tokens=100,
|
| 1038 |
+
temperature=0.1,
|
| 1039 |
+
)
|
| 1040 |
+
raw = resp.choices[0].message.content or "{}"
|
| 1041 |
+
parsed = json.loads(raw)
|
| 1042 |
+
corrected = parsed.get("corrected_name", name).strip()
|
| 1043 |
+
confidence = parsed.get("confidence", "low")
|
| 1044 |
+
known = parsed.get("known", False)
|
| 1045 |
+
|
| 1046 |
+
# Only suggest if different from input and model is confident it exists
|
| 1047 |
+
if known and corrected.lower() != name_lower and confidence in ("high", "medium"):
|
| 1048 |
+
return {
|
| 1049 |
+
"found": False,
|
| 1050 |
+
"suggestion": corrected,
|
| 1051 |
+
"suggestion_confidence": confidence,
|
| 1052 |
+
"source": "LLM",
|
| 1053 |
+
"message": f"Did you mean: {corrected}?",
|
| 1054 |
+
}
|
| 1055 |
+
except Exception as e:
|
| 1056 |
+
logger.warning(f"[LookupJournal] LLM error: {e}")
|
| 1057 |
+
|
| 1058 |
+
return {"found": False}
|
| 1059 |
+
|
| 1060 |
+
|
| 1061 |
# ββ Static frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1062 |
@app.get("/")
|
| 1063 |
async def root():
|