fix: robust JSON extraction with try/except + retry in all AI commands
- Added _extract_riddle_answer() with dual fallback: JSON parse first, then regex extraction of quoted riddle/answer values directly from text - _generate_riddle() now retries up to 2 times on parse/network failure - Hangman, scramble, WYR, and trivia now catch JSONDecodeError and log the raw model output instead of letting the exception propagate silently Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-50
@@ -1071,12 +1071,15 @@ async def _generate_trivia_question(category: str) -> dict | None:
|
|||||||
) as response:
|
) as response:
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
text = data.get("message", {}).get("content", "").strip()
|
text = data.get("message", {}).get("content", "").strip()
|
||||||
# Strip markdown code fences if present
|
if "```" in text:
|
||||||
if text.startswith("```"):
|
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||||
text = text.split("```")[1]
|
m = re.search(r"\{.+\}", text, re.DOTALL)
|
||||||
if text.startswith("json"):
|
candidate = m.group(0) if m else text
|
||||||
text = text[4:]
|
try:
|
||||||
parsed = json.loads(text)
|
parsed = json.loads(candidate)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("trivia: JSON parse failed, raw: %.200s", text)
|
||||||
|
parsed = {}
|
||||||
# Validate structure
|
# Validate structure
|
||||||
if (
|
if (
|
||||||
isinstance(parsed.get("q"), str)
|
isinstance(parsed.get("q"), str)
|
||||||
@@ -1398,9 +1401,12 @@ async def _generate_hangman_word() -> dict | None:
|
|||||||
if "```" in text:
|
if "```" in text:
|
||||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||||
if m:
|
candidate = m.group(0) if m else text
|
||||||
text = m.group(0)
|
try:
|
||||||
parsed = json.loads(text)
|
parsed = json.loads(candidate)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("hangman: JSON parse failed, raw: %.200s", text)
|
||||||
|
parsed = {}
|
||||||
word = parsed.get("word", "").lower().strip()
|
word = parsed.get("word", "").lower().strip()
|
||||||
hint = parsed.get("hint", "").strip()
|
hint = parsed.get("hint", "").strip()
|
||||||
if word.isalpha() and 5 <= len(word) <= 8 and hint:
|
if word.isalpha() and 5 <= len(word) <= 8 and hint:
|
||||||
@@ -1559,9 +1565,12 @@ async def _generate_scramble_word() -> dict | None:
|
|||||||
if "```" in text:
|
if "```" in text:
|
||||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||||
if m:
|
candidate = m.group(0) if m else text
|
||||||
text = m.group(0)
|
try:
|
||||||
parsed = json.loads(text)
|
parsed = json.loads(candidate)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("scramble: JSON parse failed, raw: %.200s", text)
|
||||||
|
parsed = {}
|
||||||
word = parsed.get("word", "").lower().strip()
|
word = parsed.get("word", "").lower().strip()
|
||||||
if word.isalpha() and 4 <= len(word) <= 8:
|
if word.isalpha() and 4 <= len(word) <= 8:
|
||||||
return {"word": word}
|
return {"word": word}
|
||||||
@@ -1706,9 +1715,12 @@ async def _generate_wyr() -> dict | None:
|
|||||||
if "```" in text:
|
if "```" in text:
|
||||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||||
if m:
|
candidate = m.group(0) if m else text
|
||||||
text = m.group(0)
|
try:
|
||||||
parsed = json.loads(text)
|
parsed = json.loads(candidate)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning("WYR: JSON parse failed, raw: %.200s", text)
|
||||||
|
parsed = {}
|
||||||
a = parsed.get("option_a", "").strip()
|
a = parsed.get("option_a", "").strip()
|
||||||
b = parsed.get("option_b", "").strip()
|
b = parsed.get("option_b", "").strip()
|
||||||
_HANGING = {"but", "and", "or", "with", "for", "in", "on", "at",
|
_HANGING = {"but", "and", "or", "with", "for", "in", "on", "at",
|
||||||
@@ -1824,6 +1836,28 @@ def _save_riddle_cache(riddles: list[str], answers: list[str]) -> None:
|
|||||||
_riddle_recent, _riddle_recent_answers = _load_riddle_cache()
|
_riddle_recent, _riddle_recent_answers = _load_riddle_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_riddle_answer(text: str) -> tuple[str, str] | None:
|
||||||
|
"""Try JSON parse, then fall back to regex extraction of riddle/answer values."""
|
||||||
|
if "```" in text:
|
||||||
|
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||||
|
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||||
|
candidate = m.group(0) if m else text
|
||||||
|
try:
|
||||||
|
parsed = json.loads(candidate)
|
||||||
|
riddle = parsed.get("riddle", "").strip()
|
||||||
|
answer = parsed.get("answer", "").strip()
|
||||||
|
if riddle and answer:
|
||||||
|
return riddle, answer
|
||||||
|
except (json.JSONDecodeError, AttributeError):
|
||||||
|
pass
|
||||||
|
# Fallback: extract quoted values for "riddle" and "answer" keys
|
||||||
|
rm = re.search(r'"riddle"\s*[:\s]+["“]([^"”]{10,})["”]', text)
|
||||||
|
am = re.search(r'"answer"\s*[:\s]+["“]([^"”]{1,50})["”]', text)
|
||||||
|
if rm and am:
|
||||||
|
return rm.group(1).strip(), am.group(1).strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _generate_riddle() -> dict | None:
|
async def _generate_riddle() -> dict | None:
|
||||||
avoid_riddles = (
|
avoid_riddles = (
|
||||||
" Do NOT reuse any of these recent riddles: "
|
" Do NOT reuse any of these recent riddles: "
|
||||||
@@ -1847,41 +1881,37 @@ async def _generate_riddle() -> dict | None:
|
|||||||
"- Avoid 'shadow' as an answer. Prefer concrete things: candle, mirror, clock, river, echo, stamp, key, glove, envelope, etc."
|
"- Avoid 'shadow' as an answer. Prefer concrete things: candle, mirror, clock, river, echo, stamp, key, glove, envelope, etc."
|
||||||
)
|
)
|
||||||
user_msg = f"Generate a clever, original riddle with a clear unambiguous answer.{avoid_answers}{avoid_riddles}"
|
user_msg = f"Generate a clever, original riddle with a clear unambiguous answer.{avoid_answers}{avoid_riddles}"
|
||||||
try:
|
for attempt in range(2):
|
||||||
timeout = aiohttp.ClientTimeout(total=60)
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
timeout = aiohttp.ClientTimeout(total=60)
|
||||||
async with session.post(
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
f"{OLLAMA_URL}/api/chat",
|
async with session.post(
|
||||||
json={
|
f"{OLLAMA_URL}/api/chat",
|
||||||
"model": CREATIVE_MODEL,
|
json={
|
||||||
"stream": False,
|
"model": CREATIVE_MODEL,
|
||||||
"messages": [
|
"stream": False,
|
||||||
{"role": "system", "content": system_msg},
|
"messages": [
|
||||||
{"role": "user", "content": user_msg},
|
{"role": "system", "content": system_msg},
|
||||||
],
|
{"role": "user", "content": user_msg},
|
||||||
},
|
],
|
||||||
) as response:
|
},
|
||||||
data = await response.json()
|
) as response:
|
||||||
text = data.get("message", {}).get("content", "").strip()
|
data = await response.json()
|
||||||
if "```" in text:
|
text = data.get("message", {}).get("content", "").strip()
|
||||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
result = _extract_riddle_answer(text)
|
||||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
if result:
|
||||||
if m:
|
riddle, answer = result
|
||||||
text = m.group(0)
|
_riddle_recent.append(riddle)
|
||||||
parsed = json.loads(text)
|
if len(_riddle_recent) > _RIDDLE_RECENT_MAX:
|
||||||
riddle = parsed.get("riddle", "").strip()
|
_riddle_recent.pop(0)
|
||||||
answer = parsed.get("answer", "").strip()
|
_riddle_recent_answers.append(answer.lower())
|
||||||
if riddle and answer:
|
if len(_riddle_recent_answers) > _RIDDLE_RECENT_MAX:
|
||||||
_riddle_recent.append(riddle)
|
_riddle_recent_answers.pop(0)
|
||||||
if len(_riddle_recent) > _RIDDLE_RECENT_MAX:
|
_save_riddle_cache(_riddle_recent, _riddle_recent_answers)
|
||||||
_riddle_recent.pop(0)
|
return {"riddle": riddle, "answer": answer}
|
||||||
_riddle_recent_answers.append(answer.lower())
|
logger.warning("riddle attempt %d: could not extract from: %.200s", attempt + 1, text)
|
||||||
if len(_riddle_recent_answers) > _RIDDLE_RECENT_MAX:
|
except Exception as e:
|
||||||
_riddle_recent_answers.pop(0)
|
logger.error(f"riddle generation error (attempt {attempt + 1}): {e}", exc_info=True)
|
||||||
_save_riddle_cache(_riddle_recent, _riddle_recent_answers)
|
|
||||||
return {"riddle": riddle, "answer": answer}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"riddle generation error: {e}", exc_info=True)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user