Spaces:
Sleeping
fix(brain): don't re-ask recalled slots after returning-user confirm
Browse filesLive audit of the recall fix surfaced a real secondary bug: after the
user confirms 'yes' to 'are you the same <name>?', apply_pending_recall
correctly merged the stored profile (returning_user_recalled=True; a
recall probe proved income/city were present) β but the model still
RE-ASKED income band + pre-existing conditions, defeating 'pick up where
we left off'. The generic 'do not re-ask' snapshot was too weak on the
confirm turn.
Fix: _system_instruction now takes recall_applied (= _did_recall_this_turn)
and, on the apply turn, injects a high-priority 'RETURNING USER CONFIRMED
β PROFILE RESTORED' block listing the restored slots and forbidding any
re-ask/re-confirm of them ('Re-asking a RESTORED slot is a hard error'),
instructing a one-line welcome-back then resume at RULE 2.5 pricing /
recommendation.
Verified locally with a PRECISE interrogative-only detector (not the
naive substring checks that produced false alarms): turn-2 recaps the
full restored profile and asks only the optional RULE 2.5 pricing bundle
β zero re-ask of the 7. 8/8 recall regression tests pass; full pytest
suite exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@@ -712,7 +712,7 @@ def _affirm_or_deny(text: str):
|
|
| 712 |
|
| 713 |
def _system_instruction(
|
| 714 |
profile, is_returning_user: bool = False, shortlist_block: str = "",
|
| 715 |
-
pending_recall: "Optional[dict]" = None,
|
| 716 |
) -> dict:
|
| 717 |
"""Bake the profile snapshot into the system prompt so each turn the
|
| 718 |
LLM knows what's already captured. Returned in Gemini's expected
|
|
@@ -777,7 +777,35 @@ def _system_instruction(
|
|
| 777 |
"applies or discards the saved profile from their answer β you "
|
| 778 |
"never merge anything yourself."
|
| 779 |
)
|
| 780 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
return {"parts": [{"text": text}]}
|
| 782 |
|
| 783 |
|
|
@@ -1840,6 +1868,7 @@ async def handle_turn(
|
|
| 1840 |
is_returning_user=is_returning_user,
|
| 1841 |
shortlist_block=_shortlist_block,
|
| 1842 |
pending_recall=_pending_recall,
|
|
|
|
| 1843 |
)
|
| 1844 |
|
| 1845 |
# Bug #108 + #110 β if the user explicitly declines the pricing /
|
|
|
|
| 712 |
|
| 713 |
def _system_instruction(
|
| 714 |
profile, is_returning_user: bool = False, shortlist_block: str = "",
|
| 715 |
+
pending_recall: "Optional[dict]" = None, recall_applied: bool = False,
|
| 716 |
) -> dict:
|
| 717 |
"""Bake the profile snapshot into the system prompt so each turn the
|
| 718 |
LLM knows what's already captured. Returned in Gemini's expected
|
|
|
|
| 777 |
"applies or discards the saved profile from their answer β you "
|
| 778 |
"never merge anything yourself."
|
| 779 |
)
|
| 780 |
+
restored_block = ""
|
| 781 |
+
if recall_applied:
|
| 782 |
+
_rs = json.dumps(snapshot, ensure_ascii=False, sort_keys=True)
|
| 783 |
+
restored_block = (
|
| 784 |
+
"\n\nβββββββββββββββββββββββββββββββββββ\n"
|
| 785 |
+
"RETURNING USER CONFIRMED β PROFILE RESTORED "
|
| 786 |
+
"(HIGHEST PRIORITY THIS TURN)\n"
|
| 787 |
+
"βββββββββββββββββββββββββββββββββββ\n"
|
| 788 |
+
"The user just confirmed they are the SAME returning person. "
|
| 789 |
+
"Their saved profile is RESTORED and FINAL for every slot "
|
| 790 |
+
"present here:\n" + _rs + "\n"
|
| 791 |
+
"Do NOT re-ask, re-confirm, re-verify or 'just double-check' "
|
| 792 |
+
"ANY slot present above β name, age, dependents, city/location, "
|
| 793 |
+
"income band, primary goal, health / pre-existing conditions, "
|
| 794 |
+
"sum insured, existing cover, budget. Re-asking a RESTORED slot "
|
| 795 |
+
"is a hard error: the entire point of recall is that the user "
|
| 796 |
+
"does NOT repeat themselves.\n"
|
| 797 |
+
"Your reply this turn: (1) ONE warm 'welcome back' line, then "
|
| 798 |
+
"(2) resume exactly where a returning user continues β if the "
|
| 799 |
+
"RULE 2.5 pricing inputs (sum insured / premium budget / co-pay "
|
| 800 |
+
"/ smoker / family medical history) are NOT yet captured, ask "
|
| 801 |
+
"ONLY those via the single RULE 2.5 prompt; otherwise go "
|
| 802 |
+
"straight to retrieve_policies + recommendations. Ask ONLY for "
|
| 803 |
+
"a slot that is genuinely ABSENT above β never one present."
|
| 804 |
+
)
|
| 805 |
+
text = (
|
| 806 |
+
SYSTEM_PROMPT + extra + recall_block + restored_block
|
| 807 |
+
+ (shortlist_block or "")
|
| 808 |
+
)
|
| 809 |
return {"parts": [{"text": text}]}
|
| 810 |
|
| 811 |
|
|
|
|
| 1868 |
is_returning_user=is_returning_user,
|
| 1869 |
shortlist_block=_shortlist_block,
|
| 1870 |
pending_recall=_pending_recall,
|
| 1871 |
+
recall_applied=_did_recall_this_turn,
|
| 1872 |
)
|
| 1873 |
|
| 1874 |
# Bug #108 + #110 β if the user explicitly declines the pricing /
|
|
@@ -172,6 +172,13 @@ class TestHandleTurnIntegration(_StoredProfileFixture):
|
|
| 172 |
self.assertEqual(sess.profile.age, 41,
|
| 173 |
"stored profile was not merged on confirm")
|
| 174 |
self.assertIsNone(getattr(sess, "pending_profile_recall", None))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
def test_no_keeps_session_blank(self):
|
| 177 |
sess = SessionState(session_id=f"hti_{uuid.uuid4().hex[:8]}")
|
|
|
|
| 172 |
self.assertEqual(sess.profile.age, 41,
|
| 173 |
"stored profile was not merged on confirm")
|
| 174 |
self.assertIsNone(getattr(sess, "pending_profile_recall", None))
|
| 175 |
+
# The apply turn MUST carry the PROFILE RESTORED directive so the
|
| 176 |
+
# model does not re-ask the just-recalled slots (the live bug:
|
| 177 |
+
# recall merged but turn-2 still asked income + pre-existing).
|
| 178 |
+
self.assertIn("RETURNING USER CONFIRMED β PROFILE RESTORED",
|
| 179 |
+
self.sys_prompts[-1])
|
| 180 |
+
self.assertIn("Re-asking a RESTORED slot is a hard error",
|
| 181 |
+
self.sys_prompts[-1])
|
| 182 |
|
| 183 |
def test_no_keeps_session_blank(self):
|
| 184 |
sess = SessionState(session_id=f"hti_{uuid.uuid4().hex[:8]}")
|