agharsallah commited on
Commit ·
a4ce257
1
Parent(s): 6ca7a5f
feat(tests): add tests for handling first-person openers in clean_clue function
Browse files- src/core/structured.py +39 -9
- 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 |
-
#
|
| 261 |
-
#
|
| 262 |
-
#
|
| 263 |
-
|
| 264 |
-
r"secret word|the word is|my word|
|
| 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
|
| 279 |
-
return
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|