riddle/wyr: fix repeat shadow answers and truncate long WYR options
Lint / Shell (shellcheck) (push) Successful in 10s
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (ruff) (push) Successful in 11s
Lint / Python deps (pip-audit) (push) Successful in 1m26s
Lint / Secret scan (gitleaks) (push) Successful in 13s

riddle:
- Cache answers separately so the same answer (e.g. 'shadow') can't
  appear twice in a session even if the riddle text differs
- Explicitly ban 'shadow' in the prompt and append avoid-answers clause
- Ban question endings ('what am I?', 'what could it be?') more strictly

wyr:
- Hard-cap options at 10 words server-side so the model can't ignore
  the word limit and generate paragraph-length options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 21:34:03 -04:00
parent bc84507e64
commit 63f1bfda49
+20 -9
View File
@@ -1685,8 +1685,10 @@ async def _generate_wyr() -> dict | None:
parsed = json.loads(text) parsed = json.loads(text)
a = parsed.get("option_a", "").strip() a = parsed.get("option_a", "").strip()
b = parsed.get("option_b", "").strip() b = parsed.get("option_b", "").strip()
# Rebuild question from options so it's always well-formed # Hard cap: truncate options that are too long
if a and b: if a and b:
a = " ".join(a.split()[:10])
b = " ".join(b.split()[:10])
q = f"Would you rather {a.rstrip('.')} OR {b.rstrip('.')}?" q = f"Would you rather {a.rstrip('.')} OR {b.rstrip('.')}?"
return {"question": q, "option_a": a, "option_b": b} return {"question": q, "option_a": a, "option_b": b}
except Exception as e: except Exception as e:
@@ -1770,16 +1772,22 @@ async def cmd_wyr(client: AsyncClient, room_id: str, sender: str, args: str):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_RIDDLE_ACTIVE: dict[str, dict] = {} _RIDDLE_ACTIVE: dict[str, dict] = {}
_riddle_recent: list[str] = [] _riddle_recent: list[str] = [] # past riddle texts
_riddle_recent_answers: list[str] = [] # past answers (lowercase)
_RIDDLE_RECENT_MAX = 30 _RIDDLE_RECENT_MAX = 30
async def _generate_riddle() -> dict | None: async def _generate_riddle() -> dict | None:
avoid_clause = ( avoid_riddles = (
" Do NOT use any of these riddles that were recently asked: " " Do NOT reuse any of these recent riddles: "
+ "; ".join(f'"{r}"' for r in _riddle_recent[-15:]) + "; ".join(f'"{r}"' for r in _riddle_recent[-10:])
+ "." + "."
) if _riddle_recent else "" ) if _riddle_recent else ""
avoid_answers = (
" Do NOT use any of these answers that were recently used: "
+ ", ".join(f'"{a}"' for a in _riddle_recent_answers[-15:])
+ "."
) if _riddle_recent_answers else ""
system_msg = ( system_msg = (
"You are a riddle generator. Always respond with ONLY a JSON object — no markdown fences, no explanation. " "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"}\n' 'Format: {"riddle": "the riddle text", "answer": "short answer"}\n'
@@ -1787,11 +1795,11 @@ async def _generate_riddle() -> dict | None:
"- The answer must be a specific, unambiguous noun (1-3 words). Avoid abstract answers.\n" "- The answer must be a specific, unambiguous noun (1-3 words). Avoid abstract answers.\n"
"- The riddle must describe the answer through metaphor or wordplay — NOT by literally describing it.\n" "- The riddle must describe the answer through metaphor or wordplay — NOT by literally describing it.\n"
"- Do NOT include the answer word anywhere in the riddle text.\n" "- Do NOT include the answer word anywhere in the riddle text.\n"
"- Do NOT end the riddle with 'what am I?' or 'what could it possibly mean?' — the riddle should stand alone.\n" "- Do NOT end with 'what am I?', 'what could it be?', or any question — the riddle should stand alone as a statement.\n"
"- The clues must logically point to ONE specific answer. Test it: would most people agree this answer is correct?\n" "- The clues must logically point to ONE specific answer that most people would agree on.\n"
"- Avoid riddles about riddles, shadows, or abstract concepts. Prefer concrete things: candle, mirror, clock, river, echo, stamp, 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_clause}" user_msg = f"Generate a clever, original riddle with a clear unambiguous answer.{avoid_answers}{avoid_riddles}"
try: try:
timeout = aiohttp.ClientTimeout(total=60) timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
@@ -1820,6 +1828,9 @@ async def _generate_riddle() -> dict | None:
_riddle_recent.append(riddle) _riddle_recent.append(riddle)
if len(_riddle_recent) > _RIDDLE_RECENT_MAX: if len(_riddle_recent) > _RIDDLE_RECENT_MAX:
_riddle_recent.pop(0) _riddle_recent.pop(0)
_riddle_recent_answers.append(answer.lower())
if len(_riddle_recent_answers) > _RIDDLE_RECENT_MAX:
_riddle_recent_answers.pop(0)
return {"riddle": riddle, "answer": answer} return {"riddle": riddle, "answer": answer}
except Exception as e: except Exception as e:
logger.error(f"riddle generation error: {e}", exc_info=True) logger.error(f"riddle generation error: {e}", exc_info=True)