rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
531dfbd
·
1 Parent(s): 9813994

fix(fact-find): KI-092 — lenient name parser when slot explicitly awaiting name

Browse files

User screenshot: bot asked "First — what should I call you?", user
replied "rohit sar" (plain lowercase name, no intro phrase). Bot
RE-ASKED the same question because `_normalize_for_slot("name", "rohit sar")`
returned None — KI-074's strict regex required an explicit intro phrase
("I'm X" / "my name is X" / "this is X") which a direct reply doesn't have.

Root cause: KI-074 conflated TWO different name-extraction modes:

Mode A (lenient) — bot explicitly asked for name; the reply IS the
name by construction. Should accept any plain alphabetic input.

Mode B (strict) — greedy multi-slot capture; the user's message could
be about anything. Need intro phrase to avoid false positives like
"this is correct" (KI-069) or "29 years old".

Fix: split into two paths via a synthetic slot_id="name__awaiting" tag.
`_canonical_fallback` checks `session.awaiting_question_id == "name"` and
calls the lenient path first. Greedy multi-slot capture continues to use
the strict path. Both paths share validation: 1-50 chars, ≥50% alpha,
no digits, 1-4 word tokens.

Verified 9 cases including the user's exact failing input:
✓ 'rohit sar' → 'Rohit Sar' (lenient mode)
✓ 'Anjali' → 'Anjali'
✓ 'my name is Rohit' → 'Rohit'
✓ '29 years old' → None (digit-reject still works)
✓ 'this is correct' → None (strict-mode KI-069 guard intact)

Tests: routing_regression 15/15, credits_election 12/12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. backend/fact_find_brain.py +63 -7
backend/fact_find_brain.py CHANGED
@@ -527,17 +527,55 @@ def _normalize_for_slot(slot_id: str, raw_text: str) -> Any:
527
  return None
528
  text = raw_text.strip()
529
 
530
- # NAME slot — KI-074 (2026-05-15) — explicit intro patterns ONLY.
531
- # Previous version accepted "29 years old" as a name because it passed
532
- # the alphabetic-ratio check. Now we require an explicit "I am X" /
533
- # "my name is X" / "this is X" / "call me X" pattern, AND the captured
534
- # span must look like a name (no digits, 1-4 short tokens).
535
- if slot_id == "name":
 
 
 
 
 
 
 
 
 
536
  import re as _re
537
  # Strip leading greeting first
538
  s = text.strip().strip(".,!?")
539
  s = _re.sub(r"^(hi|hello|hey|namaste|yo)[,!.\s]+", "", s, flags=_re.IGNORECASE)
540
- # Require an explicit intro phrase
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  m = _re.search(
542
  r"\b(?:i'?m|i\s+am|this\s+is|my\s+name\s+is|name\s+is|call\s+me|name'?s)\s+"
543
  r"([a-zA-Z][a-zA-Z'\-]{1,30}(?:\s+[a-zA-Z][a-zA-Z'\-]{1,30}){0,3})\b",
@@ -667,6 +705,24 @@ def _canonical_fallback(session, user_text: str, *, reason: str) -> FactFindOutc
667
  try:
668
  from backend.needs_finder import GRAPH
669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  # Build the prioritised slot order — try high-signal slots first
671
  # (numbers, enums) before name (which has explicit-intro guard).
672
  _GREEDY_ORDER = [
 
527
  return None
528
  text = raw_text.strip()
529
 
530
+ # NAME slot — KI-074 + KI-092 (2026-05-15) — two-mode parsing.
531
+ # MODE A (lenient, when caller explicitly tagged awaiting=name): accept
532
+ # any plain alphabetic name. Used when fallback fires after the bot
533
+ # asked "what should I call you?" the reply IS the name by
534
+ # construction; no intro phrase needed. Avoids the KI-074 false-
535
+ # reject on "rohit sar" (plain reply to the name prompt).
536
+ # MODE B (strict, default — used by greedy multi-slot capture): requires
537
+ # explicit "I'm X" / "my name is X" / "this is X" / "call me X" intro
538
+ # to avoid false positives on "this is correct" / "29 years old" etc.
539
+ # The caller signals MODE A by passing slot_id="name__awaiting" (a
540
+ # synthetic tag); the canonical fallback maps awaiting_question_id=name
541
+ # to this tag.
542
+ if slot_id in ("name", "name__awaiting"):
543
+ lenient_mode = (slot_id == "name__awaiting")
544
+ slot_id = "name" # normalise so downstream logic still sees "name"
545
  import re as _re
546
  # Strip leading greeting first
547
  s = text.strip().strip(".,!?")
548
  s = _re.sub(r"^(hi|hello|hey|namaste|yo)[,!.\s]+", "", s, flags=_re.IGNORECASE)
549
+
550
+ # KI-092 — LENIENT MODE: the bot just asked "what should I call you?"
551
+ # so any plain alphabetic reply should be accepted as the name.
552
+ # Examples to accept: "rohit sar" / "Anjali" / "Dr. Priya" / "Sam"
553
+ # Examples to reject: empty, all-digit, mostly-symbol, way too long.
554
+ if lenient_mode:
555
+ # Drop common polite-prefix scraps if present.
556
+ s_lc = s.lower()
557
+ for prefix in ("i'm ", "i am ", "this is ", "my name is ", "name is ",
558
+ "name's ", "call me ", "im ", "mr ", "mrs ", "ms ", "dr "):
559
+ if s_lc.startswith(prefix):
560
+ s = s[len(prefix):].strip()
561
+ break
562
+ # Validation: 1-50 chars, ≥50% alphabetic, no embedded digits
563
+ if not s or len(s) > 50 or any(c.isdigit() for c in s):
564
+ return None
565
+ alpha = sum(1 for c in s if c.isalpha())
566
+ if alpha < 2 or alpha / max(1, len(s)) < 0.5:
567
+ return None
568
+ # Limit to 1-4 word tokens
569
+ tokens = s.split()
570
+ if not (1 <= len(tokens) <= 4):
571
+ return None
572
+ # Capitalise if all-lower
573
+ if not any(c.isupper() for c in s):
574
+ s = " ".join(w.capitalize() for w in tokens)
575
+ return s
576
+
577
+ # STRICT MODE (greedy multi-slot capture) — original KI-074 behaviour:
578
+ # require an explicit intro phrase to avoid false positives.
579
  m = _re.search(
580
  r"\b(?:i'?m|i\s+am|this\s+is|my\s+name\s+is|name\s+is|call\s+me|name'?s)\s+"
581
  r"([a-zA-Z][a-zA-Z'\-]{1,30}(?:\s+[a-zA-Z][a-zA-Z'\-]{1,30}){0,3})\b",
 
705
  try:
706
  from backend.needs_finder import GRAPH
707
 
708
+ # KI-092 — if the bot was explicitly awaiting the name slot,
709
+ # try the LENIENT name parser FIRST so a plain "rohit sar"
710
+ # reply gets captured instead of falling through to the strict
711
+ # intro-phrase requirement and never matching.
712
+ awaiting = getattr(session, "awaiting_question_id", None)
713
+ if awaiting == "name" and not getattr(profile, "name", None):
714
+ try:
715
+ lenient_name = _normalize_for_slot("name__awaiting", user_text)
716
+ except Exception:
717
+ lenient_name = None
718
+ if lenient_name:
719
+ q_obj = next((q for q in GRAPH if q.id == "name"), None)
720
+ if q_obj is not None:
721
+ captured[q_obj.field] = lenient_name
722
+ setattr(profile, q_obj.field, lenient_name)
723
+ if "name" not in profile.asked:
724
+ profile.asked.append("name")
725
+
726
  # Build the prioritised slot order — try high-signal slots first
727
  # (numbers, enums) before name (which has explicit-intro guard).
728
  _GREEDY_ORDER = [