diff --git a/matrixbot/commands.py b/matrixbot/commands.py index ba589a1..7387378 100644 --- a/matrixbot/commands.py +++ b/matrixbot/commands.py @@ -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