[verified] IP-protect pain (hide from all roles) + persona summary button + topbar user dropdown

- Backend: strip pain/painProgress/revealed_persona.pains from debrief
  serializer and remove pain/initialPainFit from admin report so the
  coaching formula never leaks to any user-facing role (persona keeps
  pains internally to drive the judge/training)
- Fix [object Object] array-of-objects rendering in Chat/SessionDetail
- Personas: trained persona shows 'สรุปผล/Summary' button -> chat page
  with past result (chat already loads finished session)
- Top bar: username dropdown containing Settings + Logout (was separate
  logout button); variant-from-base already preserves tier/difficulty
- Updated debrief allowlist tests to reflect new redaction
This commit is contained in:
Macky
2026-08-19 07:26:21 +07:00
parent 711e058b24
commit ab5fff0bd9
9 changed files with 82 additions and 46 deletions

View File

@@ -108,30 +108,19 @@ def _safe_judge_debrief(verdict: object, outcome: str, persona: dict) -> dict:
score = max(0, min(100, int(raw.get("score", 0))))
except (TypeError, ValueError):
score = 0
progress = {}
raw_progress = raw.get("painProgress")
if isinstance(raw_progress, dict):
for key, value in list(raw_progress.items())[:20]:
if not isinstance(key, str):
continue
try:
progress[key[:100]] = max(0, min(100, int(value)))
except (TypeError, ValueError):
continue
return {
"outcome": outcome,
"score": score,
"pain": _bounded_text(raw.get("pain")),
# IP protection: do NOT surface the customer's pain (prose or raw list) to any
# user-facing role. Pain drives the judge/training internally but is the core
# "formula" of the coaching product, so it must not leak through the debrief.
"why": _bounded_text(raw.get("why")),
"failurePoints": _string_list(raw.get("failurePoints")),
"coaching": _string_list(raw.get("coaching")),
"painProgress": progress,
"revealed_persona": {
"pains": persona.get("pains", []),
"income": persona.get("income", ""),
"personality": persona.get("personality", ""),
"budget": persona.get("budget", ""),
"negotiation_levers": persona.get("negotiation_levers", []),
"opener": persona.get("opener", ""),
"background": persona.get("background", ""),
},

View File

@@ -51,10 +51,7 @@ def _render_sales_kit(kit: dict[str, Any], thai: bool) -> str:
ta = kit.get("targetAudience") or {}
if ta.get("segment"):
lines.append(f"**{'กลุ่มเป้าหมาย' if thai else 'Target segment'}:** {ta['segment']}")
if kit.get("initialPainFit"):
lines.append(f"**{'Pain ที่สินค้าแก้ได้เบื้องต้น' if thai else 'Initial pain-fit'}:**")
for p in kit["initialPainFit"]:
lines.append(f"- ({p.get('fit', '?')}) {p.get('pain', '')}")
# IP protection: hide pain-fit details from the admin report (the "formula").
return "\n".join(lines)
@@ -79,13 +76,6 @@ def _render_persona(p: dict[str, Any], thai: bool) -> str:
lines.append(f"- {'รายได้' if thai else 'Income'}: {p.get('income', '-')} | "
f"{'ไลฟ์สไตล์' if thai else 'Lifestyle'}: {p.get('lifestyle', '-')}")
lines.append(f"- {'นิสัย' if thai else 'Personality'}: {p.get('personality', '-')}")
if p.get("pains"):
lines.append(f"- {'Pain points (latent)' if thai else 'Pains (latent)'}:")
for pain in p.get("pains", []):
conds = "; ".join(pain.get("resolutionConditions", [])) if isinstance(pain, dict) else ""
lines.append(f" - [{pain.get('fit', '?') if isinstance(pain, dict) else '?'}] "
f"{pain.get('name', pain) if isinstance(pain, dict) else pain}"
f"{' — resolve: ' + conds if conds else ''}")
if p.get("negotiation_levers"):
levers = p.get("negotiation_levers") or []
lines.append(f"- {'ต่อรอง' if thai else 'Negotiation levers'}: " + ", ".join(str(x) for x in levers))

View File

@@ -81,9 +81,11 @@ def test_final_judge_supplies_score_and_debrief_for_automatic_buy(monkeypatch):
assert updated["status"] == "finished"
assert updated["outcome"] == "won"
assert debrief["score"] == 88
assert debrief["pain"] == "qualified pain"
assert debrief["coaching"] == ["Keep the close concise"]
assert debrief["revealed_persona"]["pains"]
# IP protection: pain is hidden from the debrief (prose + raw list).
assert "pain" not in debrief
assert "painProgress" not in debrief
assert "pains" not in debrief["revealed_persona"]
assert sessions.updates[0]["messages"]

View File

@@ -148,12 +148,14 @@ def test_judge_debrief_uses_closed_allowlist():
)
assert set(debrief) == {
"outcome", "score", "pain", "why", "failurePoints", "coaching",
"painProgress", "revealed_persona",
"outcome", "score", "why", "failurePoints", "coaching", "revealed_persona",
}
assert debrief["score"] == 100
assert debrief["failurePoints"] == ["missed discovery"]
assert debrief["painProgress"] == {"main": 100}
# IP protection: pain (prose + list) and painProgress are stripped from the debrief.
assert "pain" not in debrief
assert "painProgress" not in debrief
assert "pains" not in debrief["revealed_persona"]
assert "password_hash" not in str(debrief)
assert "provider_path" not in str(debrief)