Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -694,14 +694,39 @@ async def call_openai(client: AsyncOpenAI, system: str, user: str, max_tokens: i
|
|
| 694 |
temperature=0.3,
|
| 695 |
)
|
| 696 |
content = resp.choices[0].message.content or "{}"
|
|
|
|
| 697 |
try:
|
| 698 |
result = json.loads(content)
|
| 699 |
if "error" in result and isinstance(result["error"], dict):
|
| 700 |
raise ValueError(f"OpenAI returned error: {result['error']}")
|
| 701 |
return result
|
| 702 |
except json.JSONDecodeError:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
m = re.search(r"\{[\s\S]*\}", content)
|
| 704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
except RateLimitError as e:
|
| 706 |
err_msg = str(e).lower()
|
| 707 |
# Quota exceeded = billing cap, not a rate spike β don't retry
|
|
@@ -1075,7 +1100,8 @@ async def analyze_journal(req: JournalRequest, request: Request):
|
|
| 1075 |
"IMPORTANT: Return EXACTLY 10 detailed journals in journals array and EXACTLY 20 quick matches in extended_list (30 total).\n\n"
|
| 1076 |
"Return JSON only."
|
| 1077 |
)
|
| 1078 |
-
parsed = await call_openai(client, JOURNAL_SYSTEM_PROMPT, user_msg
|
|
|
|
| 1079 |
if "error" in parsed:
|
| 1080 |
raise HTTPException(500, parsed["error"])
|
| 1081 |
|
|
@@ -1416,6 +1442,22 @@ async def browse_subject(req: SubjectRequest, request: Request):
|
|
| 1416 |
area, req.subject)
|
| 1417 |
area = ""
|
| 1418 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1419 |
# Suggestions the user can act on: the model's nearest names, hard-filtered
|
| 1420 |
# to real ones, topped up deterministically. Never invented.
|
| 1421 |
def _suggestions() -> list:
|
|
@@ -1439,7 +1481,16 @@ async def browse_subject(req: SubjectRequest, request: Request):
|
|
| 1439 |
# Refusing and naming the nearest real categories is both the honest answer
|
| 1440 |
# and the more useful one: Law, Sociology and Political Science, and
|
| 1441 |
# Pathology and Forensic Medicine are where criminology research is indexed.
|
| 1442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1443 |
usage.usage.record_gap(
|
| 1444 |
"journal_subject",
|
| 1445 |
"category_invented" if bad_category else "subject_unrecognised")
|
|
|
|
| 694 |
temperature=0.3,
|
| 695 |
)
|
| 696 |
content = resp.choices[0].message.content or "{}"
|
| 697 |
+
finish = getattr(resp.choices[0], "finish_reason", None)
|
| 698 |
try:
|
| 699 |
result = json.loads(content)
|
| 700 |
if "error" in result and isinstance(result["error"], dict):
|
| 701 |
raise ValueError(f"OpenAI returned error: {result['error']}")
|
| 702 |
return result
|
| 703 |
except json.JSONDecodeError:
|
| 704 |
+
# A truncated answer is not malformed JSON in the usual sense:
|
| 705 |
+
# it is a correct answer that ran out of room, and the two need
|
| 706 |
+
# opposite fixes (raise max_tokens vs. fix the prompt). The
|
| 707 |
+
# previous version re-parsed a brace-matched slice with no
|
| 708 |
+
# guard, so a truncation raised JSONDecodeError from INSIDE the
|
| 709 |
+
# JSONDecodeError handler, escaped through `except Exception:
|
| 710 |
+
# raise`, and reached the browser as a bare HTTP 500 with no
|
| 711 |
+
# explanation anywhere.
|
| 712 |
m = re.search(r"\{[\s\S]*\}", content)
|
| 713 |
+
if m:
|
| 714 |
+
try:
|
| 715 |
+
return json.loads(m.group(0))
|
| 716 |
+
except json.JSONDecodeError:
|
| 717 |
+
pass
|
| 718 |
+
if finish == "length":
|
| 719 |
+
logger.error(
|
| 720 |
+
"[OpenAI] %s hit max_tokens=%s (%d chars returned) β response truncated",
|
| 721 |
+
_model, max_tokens, len(content))
|
| 722 |
+
return {"error": (
|
| 723 |
+
f"The model's answer was cut off at the {max_tokens}-token "
|
| 724 |
+
f"limit before it could be completed. Try a shorter abstract, "
|
| 725 |
+
f"or ask for fewer journals.")}
|
| 726 |
+
logger.error(
|
| 727 |
+
"[OpenAI] %s returned unparseable JSON (finish_reason=%s): %.300s",
|
| 728 |
+
_model, finish, content)
|
| 729 |
+
return {"error": "The model returned a malformed response. Please try again."}
|
| 730 |
except RateLimitError as e:
|
| 731 |
err_msg = str(e).lower()
|
| 732 |
# Quota exceeded = billing cap, not a rate spike β don't retry
|
|
|
|
| 1100 |
"IMPORTANT: Return EXACTLY 10 detailed journals in journals array and EXACTLY 20 quick matches in extended_list (30 total).\n\n"
|
| 1101 |
"Return JSON only."
|
| 1102 |
)
|
| 1103 |
+
parsed = await call_openai(client, JOURNAL_SYSTEM_PROMPT, user_msg,
|
| 1104 |
+
max_tokens=16000)
|
| 1105 |
if "error" in parsed:
|
| 1106 |
raise HTTPException(500, parsed["error"])
|
| 1107 |
|
|
|
|
| 1442 |
area, req.subject)
|
| 1443 |
area = ""
|
| 1444 |
|
| 1445 |
+
# The user's own words, checked against the closed vocabulary directly.
|
| 1446 |
+
# This depends on nothing the model got right, and it is what makes an AREA
|
| 1447 |
+
# query work at all: "Chemistry" is one of the 27 SCImago areas (ASJC 1600)
|
| 1448 |
+
# and is NOT a category β the nearest category name is "Chemistry
|
| 1449 |
+
# (miscellaneous)". A model that echoes "chemistry" into scimago_category
|
| 1450 |
+
# therefore produces an unlisted name, and the refusal below used to fire
|
| 1451 |
+
# on a subject the taxonomy covers perfectly well.
|
| 1452 |
+
raw_subject = req.subject.strip().lower()
|
| 1453 |
+
typed_category = raw_subject if is_real_category(raw_subject) else ""
|
| 1454 |
+
typed_area = raw_subject if is_real_area(raw_subject) else ""
|
| 1455 |
+
if not category and typed_category:
|
| 1456 |
+
logger.info("[BrowseSubject] recovered category %r from the user's input", typed_category)
|
| 1457 |
+
category = typed_category
|
| 1458 |
+
if not area and typed_area:
|
| 1459 |
+
area = typed_area
|
| 1460 |
+
|
| 1461 |
# Suggestions the user can act on: the model's nearest names, hard-filtered
|
| 1462 |
# to real ones, topped up deterministically. Never invented.
|
| 1463 |
def _suggestions() -> list:
|
|
|
|
| 1481 |
# Refusing and naming the nearest real categories is both the honest answer
|
| 1482 |
# and the more useful one: Law, Sociology and Political Science, and
|
| 1483 |
# Pathology and Forensic Medicine are where criminology research is indexed.
|
| 1484 |
+
#
|
| 1485 |
+
# The test for "did the user ask for an area" is the USER'S INPUT against
|
| 1486 |
+
# the closed vocabulary β never the model's output, which is exactly what
|
| 1487 |
+
# used to conflate "chemistry" (a real area, a real request) with "crime"
|
| 1488 |
+
# (no category, no area, nothing to answer with). Keying the refusal on
|
| 1489 |
+
# bad_category alone meant a model that echoed a valid area name into the
|
| 1490 |
+
# category field could veto its own correct answer.
|
| 1491 |
+
have_category = bool(category)
|
| 1492 |
+
have_area = bool(area) and bool(typed_area)
|
| 1493 |
+
if not have_category and not have_area:
|
| 1494 |
usage.usage.record_gap(
|
| 1495 |
"journal_subject",
|
| 1496 |
"category_invented" if bad_category else "subject_unrecognised")
|