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:
|
||||
data = await response.json()
|
||||
text = data.get("message", {}).get("content", "").strip()
|
||||
# Strip markdown code fences if present
|
||||
if text.startswith("```"):
|
||||
text = text.split("```")[1]
|
||||
if text.startswith("json"):
|
||||
text = text[4:]
|
||||
parsed = json.loads(text)
|
||||
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)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("trivia: JSON parse failed, raw: %.200s", text)
|
||||
parsed = {}
|
||||
# Validate structure
|
||||
if (
|
||||
isinstance(parsed.get("q"), str)
|
||||
@@ -1398,9 +1401,12 @@ async def _generate_hangman_word() -> dict | None:
|
||||
if "```" in text:
|
||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||
if m:
|
||||
text = m.group(0)
|
||||
parsed = json.loads(text)
|
||||
candidate = m.group(0) if m else text
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("hangman: JSON parse failed, raw: %.200s", text)
|
||||
parsed = {}
|
||||
word = parsed.get("word", "").lower().strip()
|
||||
hint = parsed.get("hint", "").strip()
|
||||
if word.isalpha() and 5 <= len(word) <= 8 and hint:
|
||||
@@ -1559,9 +1565,12 @@ async def _generate_scramble_word() -> dict | None:
|
||||
if "```" in text:
|
||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||
if m:
|
||||
text = m.group(0)
|
||||
parsed = json.loads(text)
|
||||
candidate = m.group(0) if m else text
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("scramble: JSON parse failed, raw: %.200s", text)
|
||||
parsed = {}
|
||||
word = parsed.get("word", "").lower().strip()
|
||||
if word.isalpha() and 4 <= len(word) <= 8:
|
||||
return {"word": word}
|
||||
@@ -1706,9 +1715,12 @@ async def _generate_wyr() -> dict | None:
|
||||
if "```" in text:
|
||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||
if m:
|
||||
text = m.group(0)
|
||||
parsed = json.loads(text)
|
||||
candidate = m.group(0) if m else text
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("WYR: JSON parse failed, raw: %.200s", text)
|
||||
parsed = {}
|
||||
a = parsed.get("option_a", "").strip()
|
||||
b = parsed.get("option_b", "").strip()
|
||||
_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()
|
||||
|
||||
|
||||
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:
|
||||
avoid_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."
|
||||
)
|
||||
user_msg = f"Generate a clever, original riddle with a clear unambiguous answer.{avoid_answers}{avoid_riddles}"
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(
|
||||
f"{OLLAMA_URL}/api/chat",
|
||||
json={
|
||||
"model": CREATIVE_MODEL,
|
||||
"stream": False,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
},
|
||||
) as response:
|
||||
data = await response.json()
|
||||
text = data.get("message", {}).get("content", "").strip()
|
||||
if "```" in text:
|
||||
text = re.sub(r"```[a-z]*\n?", "", text).strip()
|
||||
m = re.search(r"\{[^{}]+\}", text, re.DOTALL)
|
||||
if m:
|
||||
text = m.group(0)
|
||||
parsed = json.loads(text)
|
||||
riddle = parsed.get("riddle", "").strip()
|
||||
answer = parsed.get("answer", "").strip()
|
||||
if riddle and answer:
|
||||
_riddle_recent.append(riddle)
|
||||
if len(_riddle_recent) > _RIDDLE_RECENT_MAX:
|
||||
_riddle_recent.pop(0)
|
||||
_riddle_recent_answers.append(answer.lower())
|
||||
if len(_riddle_recent_answers) > _RIDDLE_RECENT_MAX:
|
||||
_riddle_recent_answers.pop(0)
|
||||
_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)
|
||||
for attempt in range(2):
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(
|
||||
f"{OLLAMA_URL}/api/chat",
|
||||
json={
|
||||
"model": CREATIVE_MODEL,
|
||||
"stream": False,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
},
|
||||
) as response:
|
||||
data = await response.json()
|
||||
text = data.get("message", {}).get("content", "").strip()
|
||||
result = _extract_riddle_answer(text)
|
||||
if result:
|
||||
riddle, answer = result
|
||||
_riddle_recent.append(riddle)
|
||||
if len(_riddle_recent) > _RIDDLE_RECENT_MAX:
|
||||
_riddle_recent.pop(0)
|
||||
_riddle_recent_answers.append(answer.lower())
|
||||
if len(_riddle_recent_answers) > _RIDDLE_RECENT_MAX:
|
||||
_riddle_recent_answers.pop(0)
|
||||
_save_riddle_cache(_riddle_recent, _riddle_recent_answers)
|
||||
return {"riddle": riddle, "answer": answer}
|
||||
logger.warning("riddle attempt %d: could not extract from: %.200s", attempt + 1, text)
|
||||
except Exception as e:
|
||||
logger.error(f"riddle generation error (attempt {attempt + 1}): {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user