Spaces:
Sleeping
Sleeping
Commit ·
b930fd8
1
Parent(s): 1a04c35
fix: drop new_memories that echo Hollow's own reply (recall pollution)
Browse filesOn recall turns Hollow retells a memory in first person; extraction
sometimes captured that poetic line back as a new treasure entry.
Deterministic filter: >60% token overlap with the reply = Hollow's
words, not the visitor's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app.py +1 -1
- memory.py +16 -2
- tests/test_memory.py +24 -0
app.py
CHANGED
|
@@ -313,7 +313,7 @@ def chat(user_msg: str, state: dict, history: list):
|
|
| 313 |
turn_failed = True
|
| 314 |
|
| 315 |
treasure_before = len(state["treasure"])
|
| 316 |
-
state = apply_update(state, raw_json)
|
| 317 |
captured = len(state["treasure"]) > treasure_before
|
| 318 |
|
| 319 |
if do_recall and recall_memory and not turn_failed:
|
|
|
|
| 313 |
turn_failed = True
|
| 314 |
|
| 315 |
treasure_before = len(state["treasure"])
|
| 316 |
+
state = apply_update(state, raw_json, reply=reply)
|
| 317 |
captured = len(state["treasure"]) > treasure_before
|
| 318 |
|
| 319 |
if do_recall and recall_memory and not turn_failed:
|
memory.py
CHANGED
|
@@ -17,7 +17,21 @@ def get_tier(affinity: int) -> str:
|
|
| 17 |
return "Almost"
|
| 18 |
|
| 19 |
|
| 20 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
try:
|
| 22 |
start = raw_json.find("{")
|
| 23 |
end = raw_json.rfind("}") + 1
|
|
@@ -31,7 +45,7 @@ def apply_update(state: dict, raw_json: str) -> dict:
|
|
| 31 |
|
| 32 |
state["affinity"] = max(0, min(100, state["affinity"] + update.affinity_delta))
|
| 33 |
for mem in update.new_memories:
|
| 34 |
-
if mem not in state["treasure"]:
|
| 35 |
state["treasure"].append(mem)
|
| 36 |
state["tone"] = max(-100, min(100, state.get("tone", 0) + update.tone_delta))
|
| 37 |
if update.cruel_quote and update.tone_delta < 0:
|
|
|
|
| 17 |
return "Almost"
|
| 18 |
|
| 19 |
|
| 20 |
+
def _is_hollows_words(memory: str, reply: str) -> bool:
|
| 21 |
+
"""On recall turns Hollow retells a memory in first person; extraction
|
| 22 |
+
sometimes captures THAT poetic line as a new memory. Drop any candidate
|
| 23 |
+
whose words substantially overlap Hollow's reply (>60% shared tokens)."""
|
| 24 |
+
if not reply:
|
| 25 |
+
return False
|
| 26 |
+
mem_words = set(re.findall(r"\w+", memory.lower()))
|
| 27 |
+
if not mem_words:
|
| 28 |
+
return False
|
| 29 |
+
reply_words = set(re.findall(r"\w+", reply.lower()))
|
| 30 |
+
overlap = len(mem_words & reply_words) / len(mem_words)
|
| 31 |
+
return overlap > 0.6
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def apply_update(state: dict, raw_json: str, reply: str = "") -> dict:
|
| 35 |
try:
|
| 36 |
start = raw_json.find("{")
|
| 37 |
end = raw_json.rfind("}") + 1
|
|
|
|
| 45 |
|
| 46 |
state["affinity"] = max(0, min(100, state["affinity"] + update.affinity_delta))
|
| 47 |
for mem in update.new_memories:
|
| 48 |
+
if mem not in state["treasure"] and not _is_hollows_words(mem, reply):
|
| 49 |
state["treasure"].append(mem)
|
| 50 |
state["tone"] = max(-100, min(100, state.get("tone", 0) + update.tone_delta))
|
| 51 |
if update.cruel_quote and update.tone_delta < 0:
|
tests/test_memory.py
CHANGED
|
@@ -324,3 +324,27 @@ class TestSingleGateBranchesByTone:
|
|
| 324 |
def test_gate_at_70_branches_only_by_tone(self):
|
| 325 |
assert decide_ending(_end_state(affinity=70, tone=20)) == "good"
|
| 326 |
assert decide_ending(_end_state(affinity=70, tone=19)) == "loop"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
def test_gate_at_70_branches_only_by_tone(self):
|
| 325 |
assert decide_ending(_end_state(affinity=70, tone=20)) == "good"
|
| 326 |
assert decide_ending(_end_state(affinity=70, tone=19)) == "loop"
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
class TestHollowWordsFiltered:
|
| 330 |
+
def _s(self):
|
| 331 |
+
return {"affinity": 50, "treasure": [], "claimed": [], "tone": 0, "wounds": []}
|
| 332 |
+
|
| 333 |
+
def test_drops_memory_that_echoes_hollows_reply(self):
|
| 334 |
+
reply = "i remember a voice, soft like the wind through the trees."
|
| 335 |
+
s = apply_update(self._s(),
|
| 336 |
+
'{"affinity_delta": 3, "new_memories": ["a voice soft like the wind through the trees"]}',
|
| 337 |
+
reply=reply)
|
| 338 |
+
assert s["treasure"] == []
|
| 339 |
+
|
| 340 |
+
def test_keeps_genuine_visitor_memory(self):
|
| 341 |
+
reply = "that sounds lovely. tell me more about her."
|
| 342 |
+
s = apply_update(self._s(),
|
| 343 |
+
'{"affinity_delta": 3, "new_memories": ["had a grandmother who kept bees"]}',
|
| 344 |
+
reply=reply)
|
| 345 |
+
assert s["treasure"] == ["had a grandmother who kept bees"]
|
| 346 |
+
|
| 347 |
+
def test_no_reply_keeps_everything(self):
|
| 348 |
+
s = apply_update(self._s(),
|
| 349 |
+
'{"affinity_delta": 3, "new_memories": ["grew up near the sea"]}')
|
| 350 |
+
assert s["treasure"] == ["grew up near the sea"]
|