Spaces:
Sleeping
fix(welcome-back): KI-255 — RULE 4 only fires for actual returning users
Browse filesSmoke-3-personas showed all 3 fresh personas got "Welcome back, Rajesh!"
/ "Welcome back, Anita!" / "Welcome back, Vikram!" on FIRST session despite
having no prior conversation history. Gemini was reading the KNOWN PROFILE
block (populated by save_profile_field during the current conversation) as
"pre-populated from prior session" and triggering RULE 4 Welcome Back.
Root cause: `_system_instruction` always labelled the snapshot
"KNOWN PROFILE (already captured this session; do NOT re-ask):" regardless
of whether slots came from disk-load (returning) or in-conversation
save_profile_field (fresh). Gemini interpreted "already captured" liberally.
Fix:
- handle_turn now derives `is_returning_user` from session.turn_idx == 1
AND profile has any of the 7 required slots. turn_idx is incremented at
function entry; turn_idx == 1 means this is the very first /api/chat
call for this session_id. If slots exist at turn 1, they MUST have been
loaded from disk (returning user). If turn_idx > 1, slots came from
save_profile_field calls in this conversation (fresh user).
- `_system_instruction(profile, is_returning_user)` now emits TWO distinct
preambles:
RETURNING → "SESSION TYPE: RETURNING USER. Profile below was LOADED
FROM A PRIOR CONVERSATION ... RULE 4 applies — Welcome Back."
FRESH → "SESSION TYPE: FRESH SESSION ... RULE 4 does NOT apply —
do NOT greet with 'Welcome back'."
This makes the signal explicit and machine-readable rather than relying on
Gemini's interpretation of an ambiguous phrase.
Note: KI-254's auto-mark_recommendation safety net is unaffected since it
uses citations + brain_used signals, not session.profile labels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/single_brain.py +52 -7
|
@@ -480,17 +480,42 @@ def _build_contents(
|
|
| 480 |
return out
|
| 481 |
|
| 482 |
|
| 483 |
-
def _system_instruction(profile) -> dict:
|
| 484 |
"""Bake the profile snapshot into the system prompt so each turn the
|
| 485 |
LLM knows what's already captured. Returned in Gemini's expected
|
| 486 |
-
`systemInstruction` shape.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
snapshot = _profile_to_snapshot(profile)
|
| 488 |
extra = ""
|
| 489 |
if snapshot:
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
text = SYSTEM_PROMPT + extra
|
| 495 |
return {"parts": [{"text": text}]}
|
| 496 |
|
|
@@ -944,7 +969,27 @@ async def handle_turn(
|
|
| 944 |
|
| 945 |
model = _resolve_model()
|
| 946 |
language = _detect_language(user_text)
|
| 947 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
|
| 949 |
# The running `contents` list — we append model turns + function
|
| 950 |
# responses to it across loop iterations so Gemini sees the entire
|
|
|
|
| 480 |
return out
|
| 481 |
|
| 482 |
|
| 483 |
+
def _system_instruction(profile, is_returning_user: bool = False) -> dict:
|
| 484 |
"""Bake the profile snapshot into the system prompt so each turn the
|
| 485 |
LLM knows what's already captured. Returned in Gemini's expected
|
| 486 |
+
`systemInstruction` shape.
|
| 487 |
+
|
| 488 |
+
KI-255 (2026-05-15) — added `is_returning_user` so the LLM can
|
| 489 |
+
distinguish "profile loaded from prior conversation" (RULE 4 Welcome
|
| 490 |
+
Back fires) from "profile captured during THIS turn / earlier in
|
| 491 |
+
this conversation" (no Welcome Back). Smoke-3-personas showed RULE 4
|
| 492 |
+
firing on every first session because the snapshot label said only
|
| 493 |
+
"already captured this session" which Gemini reads as "pre-populated."
|
| 494 |
+
"""
|
| 495 |
snapshot = _profile_to_snapshot(profile)
|
| 496 |
extra = ""
|
| 497 |
if snapshot:
|
| 498 |
+
if is_returning_user:
|
| 499 |
+
extra = (
|
| 500 |
+
"\n\nSESSION TYPE: RETURNING USER. Profile below was LOADED FROM A "
|
| 501 |
+
"PRIOR CONVERSATION (the user is coming back). RULE 4 applies — "
|
| 502 |
+
"your first reply must greet by name, summarise, and ask if anything "
|
| 503 |
+
"has changed. After the user confirms or provides new data, proceed."
|
| 504 |
+
"\n\nKNOWN PROFILE (pre-populated from prior session; do NOT re-ask):\n"
|
| 505 |
+
+ json.dumps(snapshot, ensure_ascii=False, sort_keys=True)
|
| 506 |
+
)
|
| 507 |
+
else:
|
| 508 |
+
extra = (
|
| 509 |
+
"\n\nSESSION TYPE: FRESH SESSION. Profile below was CAPTURED IN THIS "
|
| 510 |
+
"CONVERSATION (current turn or earlier turns of this same chat). "
|
| 511 |
+
"RULE 4 does NOT apply — do NOT greet with 'Welcome back', the user "
|
| 512 |
+
"did not come from a prior session. Just continue the conversation "
|
| 513 |
+
"naturally and ask for the next missing slot, or recommend if 7 slots "
|
| 514 |
+
"are filled."
|
| 515 |
+
"\n\nPROFILE CAPTURED IN THIS CONVERSATION (do NOT re-ask, do NOT "
|
| 516 |
+
"say 'Welcome back'):\n"
|
| 517 |
+
+ json.dumps(snapshot, ensure_ascii=False, sort_keys=True)
|
| 518 |
+
)
|
| 519 |
text = SYSTEM_PROMPT + extra
|
| 520 |
return {"parts": [{"text": text}]}
|
| 521 |
|
|
|
|
| 969 |
|
| 970 |
model = _resolve_model()
|
| 971 |
language = _detect_language(user_text)
|
| 972 |
+
|
| 973 |
+
# KI-255 — detect "returning user" so RULE 4 (Welcome Back greeting)
|
| 974 |
+
# only fires when the profile was actually loaded from a prior
|
| 975 |
+
# session. Signal: session.turn_idx == 1 (we just incremented above,
|
| 976 |
+
# so this is the FIRST turn of this session_id) AND profile has any
|
| 977 |
+
# captured slot. If turn_idx > 1, slots were populated by prior
|
| 978 |
+
# save_profile_field calls within THIS conversation — not a
|
| 979 |
+
# returning user, do NOT trigger RULE 4 Welcome Back.
|
| 980 |
+
_current_turn = int(getattr(session, "turn_idx", 1) or 1)
|
| 981 |
+
_has_prior_profile = any(
|
| 982 |
+
getattr(session.profile, fld, None) not in (None, "", [])
|
| 983 |
+
for fld in (
|
| 984 |
+
"name", "age", "dependents", "location_tier",
|
| 985 |
+
"income_band", "primary_goal", "health_conditions",
|
| 986 |
+
)
|
| 987 |
+
)
|
| 988 |
+
is_returning_user = (_current_turn == 1) and _has_prior_profile
|
| 989 |
+
|
| 990 |
+
system_instruction = _system_instruction(
|
| 991 |
+
session.profile, is_returning_user=is_returning_user,
|
| 992 |
+
)
|
| 993 |
|
| 994 |
# The running `contents` list — we append model turns + function
|
| 995 |
# responses to it across loop iterations so Gemini sees the entire
|