rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
3f15c03
·
1 Parent(s): fc0cd9e

fix(welcome-back): KI-255 — RULE 4 only fires for actual returning users

Browse files

Smoke-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>

Files changed (1) hide show
  1. backend/single_brain.py +52 -7
backend/single_brain.py CHANGED
@@ -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
- extra = (
491
- "\n\nKNOWN PROFILE (already captured this session; do NOT re-ask):\n"
492
- + json.dumps(snapshot, ensure_ascii=False, sort_keys=True)
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
- system_instruction = _system_instruction(session.profile)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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