fix: 20q answer dedup cache — prevent repeated answers
Lint / Shell (shellcheck) (push) Successful in 10s
Lint / JS (eslint) (push) Successful in 10s
Lint / Python (ruff) (push) Failing after 10s
Lint / Python deps (pip-audit) (push) Successful in 57s
Lint / Secret scan (gitleaks) (push) Successful in 6s

Add a rolling cache of the last 30 answers (persisted to
twentyq_cache.json) and pass the recent list to the model as an
explicit avoid clause. Also prompt the model to vary categories
each round. If the model still returns a cached answer it is
rejected and one retry is attempted automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 19:57:47 -04:00
parent ad09286e27
commit a7a3891d1c
+35 -1
View File
@@ -2730,13 +2730,39 @@ async def cmd_ac(client: AsyncClient, room_id: str, sender: str, args: str):
# ===========================================================================
_TWENTYQ_GAMES: dict[str, dict] = {}
_TWENTYQ_RECENT_MAX = 30
_TWENTYQ_CACHE_FILE = Path("twentyq_cache.json")
def _load_20q_cache() -> list[str]:
try:
data = json.loads(_TWENTYQ_CACHE_FILE.read_text())
return data.get("things", [])
except Exception:
return []
def _save_20q_cache(things: list[str]) -> None:
try:
_TWENTYQ_CACHE_FILE.write_text(json.dumps({"things": things}, indent=2))
except Exception as e:
logger.warning("Failed to save 20q cache: %s", e)
_20q_recent: list[str] = _load_20q_cache()
async def _generate_20q_thing() -> dict | None:
avoid_clause = (
f" Do NOT use any of these recently used answers: {', '.join(_20q_recent[-20:])}."
if _20q_recent else ""
)
system_msg = (
"You are generating a subject for a game of 20 questions. "
"Pick a specific, well-known, concrete thing. Avoid overly obscure topics. "
"Good categories: animal, famous person, place, everyday object, food, movie/show, fictional character. "
"Choose a DIFFERENT category each time — vary between animals, people, objects, places, food, etc."
+ avoid_clause + " "
"Respond with ONLY a JSON object — no markdown, no explanation. "
'{"thing": "elephant", "category": "animal", "hint": "it\'s a living creature"}'
)
@@ -2763,6 +2789,14 @@ async def _generate_20q_thing() -> dict | None:
category = parsed.get("category", "thing").strip()
hint = parsed.get("hint", f"it's a {category}").strip()
if thing and len(thing) > 1:
# Check it's not a repeat (case-insensitive)
if thing.lower() in [r.lower() for r in _20q_recent]:
logger.warning("20q generated a cached answer '%s', regenerating", thing)
return None
_20q_recent.append(thing)
if len(_20q_recent) > _TWENTYQ_RECENT_MAX:
_20q_recent.pop(0)
_save_20q_cache(_20q_recent)
return {"thing": thing, "category": category, "hint": hint}
except Exception as e:
logger.error("20q generation error: %s", e, exc_info=True)
@@ -2807,7 +2841,7 @@ async def cmd_20q(client: AsyncClient, room_id: str, sender: str, args: str):
return
await send_text(client, room_id, "🤔 I'm thinking of something...")
thing_data = await _generate_20q_thing()
thing_data = await _generate_20q_thing() or await _generate_20q_thing()
if not thing_data:
await send_text(client, room_id, "Failed to think of something. Try again!")
return