rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
6d84a76
·
1 Parent(s): dab7341

fix(profile): KI-271 — wire copay + family + desired_SI through profile endpoints

Browse files

E3 live smoke (Priya: copay=20, family=[cancer, heart]) caught a wiring bug:
brain captures the slots correctly + premium_calculator supports the
loadings, but 3 main.py endpoints hand-rolled a stale 12-key profile_dict
that omitted the new fields. Result: identical premium bands regardless of
copay/family input.

Affected endpoints (all hand-rolling 12 keys, missing
desired_sum_insured_inr / copay_pct / family_medical_history):
- POST /api/profile/save → response profile + RAG chunk
- GET /api/profile/completeness → fields_collected / fields_missing
- GET /api/profile/predicted-premium-band → estimate_premium_band input

Fix: replace all 3 hand-rolled dicts with a SLOT_UNION-driven dict-comp:
profile_dict = {slot: getattr(p, slot, None) for slot in brain_tools.SLOT_UNION}

SLOT_UNION (15 fields, defined in brain_tools.py per B6/KI-260):
name, age, dependents, location_tier, income_band, primary_goal,
health_conditions, budget_band, desired_sum_insured_inr,
existing_cover_inr, parents_to_insure, parents_age_max,
parents_has_ped, copay_pct, family_medical_history

Added top-level `from backend import brain_tools` import so the 3
endpoints share one source of truth.

Expected behavior after deploy:
- /api/profile/completeness fields_collected now includes copay_pct +
family_medical_history when captured.
- /api/profile/predicted-premium-band — band shifts when user changes
copay (e.g. 0% → 20% drops band by ~12%) or family history (e.g. adding
cancer + heart raises band by ~5%).
- /api/profile/save response + RAG chunk persist all 15 fields.

Verification:
- python -m py_compile clean
- SLOT_UNION import confirmed (15 fields)

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

Files changed (1) hide show
  1. backend/main.py +18 -30
backend/main.py CHANGED
@@ -27,6 +27,7 @@ from pydantic import BaseModel, Field
27
 
28
  from backend.config import settings
29
  from backend import nim_fallback
 
30
  from backend.providers.sarvam_stt import SarvamSTT
31
  from backend.providers.sarvam_tts import SarvamTTS
32
 
@@ -1620,13 +1621,11 @@ async def profile_update(req: ProfileUpdateRequest):
1620
  print(f"[profile_store] save failed for {req.name}: {type(e).__name__}: {e}")
1621
 
1622
  p = sess.profile
 
 
 
1623
  profile_dict = {
1624
- "name": p.name, # KI-077
1625
- "age": p.age, "dependents": p.dependents, "income_band": p.income_band,
1626
- "existing_cover_inr": p.existing_cover_inr, "primary_goal": p.primary_goal,
1627
- "location_tier": p.location_tier, "parents_to_insure": p.parents_to_insure,
1628
- "parents_age_max": p.parents_age_max, "parents_has_ped": p.parents_has_ped,
1629
- "health_conditions": p.health_conditions, "budget_band": p.budget_band,
1630
  }
1631
  # KI-196 (ADR-041) — same answered-only gate as profile_completeness_view.
1632
  answered = set(getattr(p, "asked", []) or [])
@@ -1682,20 +1681,16 @@ async def profile_completeness_view(session_id: Optional[str] = None):
1682
  )
1683
  sess = get_session(session_id)
1684
  p = sess.profile
 
 
 
 
 
 
1685
  profile_dict = {
1686
- "name": p.name, # KI-252 Path B 7-slot alignment with completeness bar
1687
- "age": p.age, "dependents": p.dependents, "income_band": p.income_band,
1688
- "existing_cover_inr": p.existing_cover_inr, "primary_goal": p.primary_goal,
1689
- "location_tier": p.location_tier, "parents_to_insure": p.parents_to_insure,
1690
- "parents_age_max": p.parents_age_max, "parents_has_ped": p.parents_has_ped,
1691
- "health_conditions": p.health_conditions, "budget_band": p.budget_band,
1692
  }
1693
- # KI-196 (ADR-041) — Profile completeness must reflect fields the user
1694
- # EXPLICITLY answered, not defaults that were never touched. Default
1695
- # `dependents="self"` pre-populated in the builder UI used to count as
1696
- # "done" and produced the misleading "25% DONE" badge on a zero-input
1697
- # session. Gate every field on Profile.asked containing the field name
1698
- # before exposing it to the completeness scorer.
1699
  answered = set(getattr(p, "asked", []) or [])
1700
  completeness_input = {
1701
  k: (v if k in answered else None) for k, v in profile_dict.items()
@@ -3077,19 +3072,12 @@ async def predicted_premium_band(session_id: Optional[str] = None):
3077
 
3078
  sess = get_session(session_id)
3079
  p = sess.profile
 
 
 
 
3080
  profile_dict = {
3081
- "name": p.name,
3082
- "age": p.age,
3083
- "dependents": p.dependents,
3084
- "income_band": p.income_band,
3085
- "existing_cover_inr": p.existing_cover_inr,
3086
- "primary_goal": p.primary_goal,
3087
- "location_tier": p.location_tier,
3088
- "parents_to_insure": p.parents_to_insure,
3089
- "parents_age_max": p.parents_age_max,
3090
- "parents_has_ped": p.parents_has_ped,
3091
- "health_conditions": p.health_conditions,
3092
- "budget_band": p.budget_band,
3093
  }
3094
  # Same answered-only gate as profile_completeness_view (KI-196 / ADR-041) —
3095
  # only feed slots the user has actually answered, not pre-populated
 
27
 
28
  from backend.config import settings
29
  from backend import nim_fallback
30
+ from backend import brain_tools # KI-271 — SLOT_UNION-driven profile_dict in 3 endpoints
31
  from backend.providers.sarvam_stt import SarvamSTT
32
  from backend.providers.sarvam_tts import SarvamTTS
33
 
 
1621
  print(f"[profile_store] save failed for {req.name}: {type(e).__name__}: {e}")
1622
 
1623
  p = sess.profile
1624
+ # KI-271 — SLOT_UNION-driven profile_dict (15 fields) so copay_pct +
1625
+ # family_medical_history + desired_sum_insured_inr propagate to the
1626
+ # save endpoint's response + RAG chunk.
1627
  profile_dict = {
1628
+ slot: getattr(p, slot, None) for slot in brain_tools.SLOT_UNION
 
 
 
 
 
1629
  }
1630
  # KI-196 (ADR-041) — same answered-only gate as profile_completeness_view.
1631
  answered = set(getattr(p, "asked", []) or [])
 
1681
  )
1682
  sess = get_session(session_id)
1683
  p = sess.profile
1684
+ # KI-271 — profile_dict now built from brain_tools.SLOT_UNION (15 fields)
1685
+ # so every captured slot (including B5 desired_sum_insured_inr, D2 copay_pct,
1686
+ # D2 family_medical_history) propagates through to /api/profile/completeness,
1687
+ # /api/profile/predicted-premium-band, /api/profile/recall-by-name. Prior
1688
+ # 12-key hand-roll caused E3 to discover the band endpoint ignoring copay
1689
+ # + family entirely.
1690
  profile_dict = {
1691
+ slot: getattr(p, slot, None) for slot in brain_tools.SLOT_UNION
 
 
 
 
 
1692
  }
1693
+ # KI-196 (ADR-041) — Profile completeness gates on Profile.asked.
 
 
 
 
 
1694
  answered = set(getattr(p, "asked", []) or [])
1695
  completeness_input = {
1696
  k: (v if k in answered else None) for k, v in profile_dict.items()
 
3072
 
3073
  sess = get_session(session_id)
3074
  p = sess.profile
3075
+ # KI-271 — band endpoint now drives off SLOT_UNION so copay_pct +
3076
+ # family_medical_history (D2/KI-269) actually shift the band. Prior
3077
+ # 12-key hand-roll silently omitted both → E3 smoke caught identical
3078
+ # bands with/without copay+family input.
3079
  profile_dict = {
3080
+ slot: getattr(p, slot, None) for slot in brain_tools.SLOT_UNION
 
 
 
 
 
 
 
 
 
 
 
3081
  }
3082
  # Same answered-only gate as profile_completeness_view (KI-196 / ADR-041) —
3083
  # only feed slots the user has actually answered, not pre-populated