Spaces:
Sleeping
fix(profile): KI-271 — wire copay + family + desired_SI through profile endpoints
Browse filesE3 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>
- backend/main.py +18 -30
|
@@ -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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|