fix: switch all JSON-returning game generators to api/chat + robust parsing
Lint / Shell (shellcheck) (push) Successful in 10s
Lint / JS (eslint) (push) Successful in 6s
Lint / Python (ruff) (push) Successful in 5s
Lint / Python deps (pip-audit) (push) Successful in 1m0s
Lint / Secret scan (gitleaks) (push) Successful in 7s

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 00:55:45 -04:00
parent a47648435e
commit 82a3f24519
+54 -28
View File
@@ -1231,24 +1231,32 @@ def _hangman_display(game: dict) -> str:
async def _generate_hangman_word() -> dict | None: async def _generate_hangman_word() -> dict | None:
prompt = ( system_msg = (
"Generate a hangman word game. Pick a common English word between 5 and 8 letters. " "You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
"Respond with ONLY valid JSON, no markdown: " 'Format: {"word": "example", "hint": "short category or hint"}'
'{"word": "example", "hint": "a short category or hint about the word"}. '
"The word must be all lowercase letters only, no spaces or hyphens."
) )
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: try:
timeout = aiohttp.ClientTimeout(total=20) timeout = aiohttp.ClientTimeout(total=20)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( async with session.post(
f"{OLLAMA_URL}/api/generate", f"{OLLAMA_URL}/api/chat",
json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, json={
"model": ASK_MODEL,
"stream": False,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
},
) as response: ) as response:
data = await response.json() data = await response.json()
text = data.get("response", "").strip() text = data.get("message", {}).get("content", "").strip()
# Strip markdown fences
if "```" in text: 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) parsed = json.loads(text)
word = parsed.get("word", "").lower().strip() word = parsed.get("word", "").lower().strip()
hint = parsed.get("hint", "").strip() hint = parsed.get("hint", "").strip()
@@ -1430,23 +1438,32 @@ _SCRAMBLE_GAMES: dict[str, dict] = {}
async def _generate_scramble_word() -> dict | None: async def _generate_scramble_word() -> dict | None:
prompt = ( system_msg = (
"Pick a common English word between 4 and 8 letters. " "You are a word game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
"Respond with ONLY valid JSON, no markdown: " 'Format: {"word": "example"}'
'{"word": "example"}. '
"The word must be all lowercase letters only, no spaces or hyphens."
) )
user_msg = "Pick a common English word between 4 and 8 letters (lowercase letters only, no hyphens or spaces)."
try: try:
timeout = aiohttp.ClientTimeout(total=20) timeout = aiohttp.ClientTimeout(total=20)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( async with session.post(
f"{OLLAMA_URL}/api/generate", f"{OLLAMA_URL}/api/chat",
json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, json={
"model": ASK_MODEL,
"stream": False,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
},
) as response: ) as response:
data = await response.json() data = await response.json()
text = data.get("response", "").strip() text = data.get("message", {}).get("content", "").strip()
if "```" in text: 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) parsed = json.loads(text)
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:
@@ -1641,23 +1658,32 @@ _RIDDLE_ACTIVE: dict[str, dict] = {}
async def _generate_riddle() -> dict | None: async def _generate_riddle() -> dict | None:
prompt = ( system_msg = (
"Generate a clever riddle and its answer. " "You are a riddle generator. Always respond with ONLY a JSON object — no markdown fences, no explanation. "
"Respond with ONLY valid JSON, no markdown: " 'Format: {"riddle": "the riddle text", "answer": "short answer"}'
'{"riddle": "...", "answer": "..."}. '
"The answer should be a short word or phrase (1-4 words). Make it interesting!"
) )
user_msg = "Generate a clever riddle. The answer should be 1-4 words."
try: try:
timeout = aiohttp.ClientTimeout(total=20) timeout = aiohttp.ClientTimeout(total=20)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( async with session.post(
f"{OLLAMA_URL}/api/generate", f"{OLLAMA_URL}/api/chat",
json={"model": ASK_MODEL, "prompt": prompt, "stream": False}, json={
"model": ASK_MODEL,
"stream": False,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
},
) as response: ) as response:
data = await response.json() data = await response.json()
text = data.get("response", "").strip() text = data.get("message", {}).get("content", "").strip()
if "```" in text: 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) parsed = json.loads(text)
riddle = parsed.get("riddle", "").strip() riddle = parsed.get("riddle", "").strip()
answer = parsed.get("answer", "").strip() answer = parsed.get("answer", "").strip()