agharsallah commited on
Commit
a4ce257
·
1 Parent(s): 6ca7a5f

feat(tests): add tests for handling first-person openers in clean_clue function

Browse files
Files changed (2) hide show
  1. src/core/structured.py +39 -9
  2. tests/test_structured.py +14 -0
src/core/structured.py CHANGED
@@ -257,13 +257,22 @@ def _salvage_text(cleaned: str) -> dict[str, str]:
257
  return {"text": "…"}
258
 
259
 
260
- # Meta-commentary / instruction-echo / reasoning-preamble a weak model leaks when asked
261
- # for JSON: drop any sentence matching this so the model's scratchpad never becomes the
262
- # spoken line ("Alright, the user wants me to play as CARA…", "Looking at the clues…").
263
- _META = re.compile(
264
- r"secret word|the word is|my word|need to|have to|also include|must (?:be|include|output|name|provide)|"
265
  r"\bjson\b|\bschema\b|\bmood\b|\bthought\b|one or two sentence|vivid and specific|"
266
- r"\bagent\.\w+|brief,? evocative|output format|\bfield\b|"
 
 
 
 
 
 
 
 
 
267
  r"\balright\b|\bokay\b|the user\b|looking at|let me\b|my clue\b|play as\b|"
268
  r"\bas (?:cara|bex|nil|ovo)\b|i (?:should|need|must|will|think|'ll|'m|am)\b|the (?:scenario|game|prompt)\b",
269
  re.IGNORECASE,
@@ -274,9 +283,15 @@ _CAPS_TOKEN = re.compile(r"\b[A-Z]{3,}\b")
274
  _EXAMPLE_ECHO = "a brief, evocative response"
275
 
276
 
 
 
 
 
 
 
277
  def _is_meta(sentence: str) -> bool:
278
- """True if *sentence* is scratchpad/meta or names a secret word in caps."""
279
- return bool(_META.search(sentence)) or bool(_CAPS_TOKEN.search(sentence))
280
 
281
 
282
  def clean_clue(raw: str) -> tuple[str, str]:
@@ -304,11 +319,26 @@ def clean_clue(raw: str) -> tuple[str, str]:
304
  cleaned = tail
305
 
306
  kept: list[str] = []
 
307
  for s in _SENTENCE_SPLIT.split(cleaned):
308
  sentence = s.strip()
309
  if len(sentence) < 6 or sentence.startswith("{"):
310
  continue
311
- (residue if _is_meta(sentence) else kept).append(sentence.strip(" \"'"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
  return " ".join(kept)[:300].strip(), " ".join(p for p in residue if p)[:600].strip()
314
 
 
257
  return {"text": "…"}
258
 
259
 
260
+ # HARD meta a secret-word leak or an echo of the JSON/format instruction. These must
261
+ # NEVER become the spoken line; they always go to the (private) residue, even when nothing
262
+ # else survives (better to skip the turn than ship the secret or the schema).
263
+ _HARD_META = re.compile(
264
+ r"secret word|the word is|my word|also include|must (?:include|output|name|provide)|"
265
  r"\bjson\b|\bschema\b|\bmood\b|\bthought\b|one or two sentence|vivid and specific|"
266
+ r"\bagent\.\w+|brief,? evocative|output format|\bfield\b",
267
+ re.IGNORECASE,
268
+ )
269
+ # SOFT meta — a scratchpad/planning preamble or a first-person opener ("Alright, the user
270
+ # wants me to play as CARA…", "Let me… I should…", "I think mine is…"). Dropped to residue
271
+ # when real speech survives alongside it, but PROMOTED to the spoken line when it is all the
272
+ # model gave: on the prose fallback an over-thinker's "I think mine is…" is in-character
273
+ # speech, not reasoning, so shipping it beats raising "no usable line" and skipping the turn.
274
+ _SOFT_META = re.compile(
275
+ r"need to|have to|must be\b|"
276
  r"\balright\b|\bokay\b|the user\b|looking at|let me\b|my clue\b|play as\b|"
277
  r"\bas (?:cara|bex|nil|ovo)\b|i (?:should|need|must|will|think|'ll|'m|am)\b|the (?:scenario|game|prompt)\b",
278
  re.IGNORECASE,
 
283
  _EXAMPLE_ECHO = "a brief, evocative response"
284
 
285
 
286
+ def _is_hard_meta(sentence: str) -> bool:
287
+ """True if *sentence* leaks the secret (caps token) or echoes the format instruction —
288
+ never a spoken line, no matter what."""
289
+ return bool(_HARD_META.search(sentence)) or bool(_CAPS_TOKEN.search(sentence))
290
+
291
+
292
  def _is_meta(sentence: str) -> bool:
293
+ """True if *sentence* is hard meta or a soft scratchpad/planning preamble."""
294
+ return _is_hard_meta(sentence) or bool(_SOFT_META.search(sentence))
295
 
296
 
297
  def clean_clue(raw: str) -> tuple[str, str]:
 
319
  cleaned = tail
320
 
321
  kept: list[str] = []
322
+ soft: list[str] = []
323
  for s in _SENTENCE_SPLIT.split(cleaned):
324
  sentence = s.strip()
325
  if len(sentence) < 6 or sentence.startswith("{"):
326
  continue
327
+ clean_sentence = sentence.strip(" \"'")
328
+ if _is_hard_meta(sentence):
329
+ residue.append(clean_sentence)
330
+ elif _SOFT_META.search(sentence):
331
+ soft.append(clean_sentence)
332
+ else:
333
+ kept.append(clean_sentence)
334
+
335
+ # Soft meta rides as private residue when real speech survives — but when it's all the
336
+ # model gave, it IS the line: a "thinking out loud" opener still passed every hard guard
337
+ # (no secret word, no schema echo), so shipping it beats skipping the turn.
338
+ if kept:
339
+ residue.extend(soft)
340
+ else:
341
+ kept = soft
342
 
343
  return " ".join(kept)[:300].strip(), " ".join(p for p in residue if p)[:600].strip()
344
 
tests/test_structured.py CHANGED
@@ -174,6 +174,20 @@ class TestCleanClue:
174
  assert "TEA" not in clue.upper()
175
  assert clue == "A pale gold warmth fills the cup."
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  class TestIsUsableLine:
179
  def test_rejects_empty_placeholder_and_example(self):
 
174
  assert "TEA" not in clue.upper()
175
  assert clue == "A pale gold warmth fills the cup."
176
 
177
+ def test_first_person_opener_survives_when_it_is_all_there_is(self):
178
+ # The spy-bex failure: an over-thinker's whole line is a first-person opener. Soft
179
+ # meta alone must be PROMOTED to the clue, not stripped to nothing (no usable line).
180
+ clue, _ = clean_clue("I think mine is something rooted, tall, and still.")
181
+ assert clue == "I think mine is something rooted, tall, and still."
182
+ assert is_usable_line(clue)
183
+
184
+ def test_first_person_opener_yields_to_real_speech(self):
185
+ # When a plain clue survives alongside the first-person preamble, the preamble drops
186
+ # to residue and the clean spoken line wins.
187
+ clue, residue = clean_clue("I think mine is the odd one. A pale gold warmth fills the cup.")
188
+ assert clue == "A pale gold warmth fills the cup."
189
+ assert "I think" in residue
190
+
191
 
192
  class TestIsUsableLine:
193
  def test_rejects_empty_placeholder_and_example(self):