[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:
@@ -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", ""),
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user