nikeshn commited on
Commit
f3e7ab2
Β·
verified Β·
1 Parent(s): 87b167e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -3
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
- return json.loads(m.group(0)) if m else {"error": "Failed to parse response"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if bad_category or (not category and not area):
 
 
 
 
 
 
 
 
 
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")