From 82a3f24519bd1f86a0a6fea108b88946886f80e6 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 22 Apr 2026 00:55:45 -0400 Subject: [PATCH] fix: switch all JSON-returning game generators to api/chat + robust parsing hangman, scramble, riddle, and wyr all used api/generate which has no system role. The model would wrap JSON in prose or markdown fences, causing json.loads() to throw and the command to silently die after the 'Generating...' message. Fix for all four: switch to api/chat with a system message enforcing raw JSON output, strip markdown fences, and use regex to extract the JSON object even if surrounded by extra text. Co-Authored-By: Claude Sonnet 4.6 --- matrixbot/commands.py | 82 ++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/matrixbot/commands.py b/matrixbot/commands.py index 35fe219..49410ee 100644 --- a/matrixbot/commands.py +++ b/matrixbot/commands.py @@ -1231,24 +1231,32 @@ def _hangman_display(game: dict) -> str: async def _generate_hangman_word() -> dict | None: - prompt = ( - "Generate a hangman word game. Pick a common English word between 5 and 8 letters. " - "Respond with ONLY valid JSON, no markdown: " - '{"word": "example", "hint": "a short category or hint about the word"}. ' - "The word must be all lowercase letters only, no spaces or hyphens." + system_msg = ( + "You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. " + 'Format: {"word": "example", "hint": "short category or hint"}' ) + user_msg = "Pick a common English word between 5 and 8 letters (lowercase letters only, no hyphens or spaces) and give a short hint." try: timeout = aiohttp.ClientTimeout(total=20) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post( - f"{OLLAMA_URL}/api/generate", - json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, + f"{OLLAMA_URL}/api/chat", + json={ + "model": ASK_MODEL, + "stream": False, + "messages": [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": user_msg}, + ], + }, ) as response: data = await response.json() - text = data.get("response", "").strip() - # Strip markdown fences + text = data.get("message", {}).get("content", "").strip() if "```" in text: - text = text.split("```")[1].lstrip("json").strip() + 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) word = parsed.get("word", "").lower().strip() hint = parsed.get("hint", "").strip() @@ -1430,23 +1438,32 @@ _SCRAMBLE_GAMES: dict[str, dict] = {} async def _generate_scramble_word() -> dict | None: - prompt = ( - "Pick a common English word between 4 and 8 letters. " - "Respond with ONLY valid JSON, no markdown: " - '{"word": "example"}. ' - "The word must be all lowercase letters only, no spaces or hyphens." + system_msg = ( + "You are a word game generator. Always respond with ONLY a JSON object — no markdown, no explanation. " + 'Format: {"word": "example"}' ) + user_msg = "Pick a common English word between 4 and 8 letters (lowercase letters only, no hyphens or spaces)." try: timeout = aiohttp.ClientTimeout(total=20) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post( - f"{OLLAMA_URL}/api/generate", - json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, + f"{OLLAMA_URL}/api/chat", + json={ + "model": ASK_MODEL, + "stream": False, + "messages": [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": user_msg}, + ], + }, ) as response: data = await response.json() - text = data.get("response", "").strip() + text = data.get("message", {}).get("content", "").strip() if "```" in text: - text = text.split("```")[1].lstrip("json").strip() + 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) word = parsed.get("word", "").lower().strip() if word.isalpha() and 4 <= len(word) <= 8: @@ -1641,23 +1658,32 @@ _RIDDLE_ACTIVE: dict[str, dict] = {} async def _generate_riddle() -> dict | None: - prompt = ( - "Generate a clever riddle and its answer. " - "Respond with ONLY valid JSON, no markdown: " - '{"riddle": "...", "answer": "..."}. ' - "The answer should be a short word or phrase (1-4 words). Make it interesting!" + system_msg = ( + "You are a riddle generator. Always respond with ONLY a JSON object — no markdown fences, no explanation. " + 'Format: {"riddle": "the riddle text", "answer": "short answer"}' ) + user_msg = "Generate a clever riddle. The answer should be 1-4 words." try: timeout = aiohttp.ClientTimeout(total=20) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post( - f"{OLLAMA_URL}/api/generate", - json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, + f"{OLLAMA_URL}/api/chat", + json={ + "model": ASK_MODEL, + "stream": False, + "messages": [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": user_msg}, + ], + }, ) as response: data = await response.json() - text = data.get("response", "").strip() + text = data.get("message", {}).get("content", "").strip() if "```" in text: - text = text.split("```")[1].lstrip("json").strip() + 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()