feat(personas): 'create persona from this persona' (variant) — practice the same challenge, new identity

Each persona is one-shot (chat once = win/lose locked). To keep training repeatable:
- New endpoint POST /api/groups/<gid>/personas/<pid>/variant creates a NEW persona that is
  a fresh incarnation of the source: LOCKS pain points, objections, negotiation levers,
  tolerance, special/recontact, goal, budget, difficulty, tier, product_context — but VARYS
  name/profession/age/location/background/personality/opener so it isn't an identical copy.
- Added to the same group as a distinct persona (fresh not_tried, so chat-able again).
- UI: on the Personas page, a finished (won/lost) persona gets a
  'สร้างบุคคลต้นแบบจากต้นแบบนี้' button; reload shows the variant.
All 10 backend suites pass. Rebuilt dist.
This commit is contained in:
Macky
2026-08-09 12:43:00 +07:00
parent 8771438aef
commit 10c7d01236
29 changed files with 217 additions and 30 deletions

View File

@@ -338,6 +338,53 @@ def update_persona(gid: str, pid: str):
return jsonify({"persona": strip_secret_fields(full)})
@groups_bp.post("/<gid>/personas/<pid>/variant")
@require_auth
def create_persona_variant(gid: str, pid: str):
"""Create a NEW persona cloned from an existing one (fresh identity, same core traits).
Lets a trainee practice the SAME selling challenge repeatedly even though each persona
can only be chatted once — the variant is a different person with the same pain points /
personality / temperament, so the training repeats but never as an identical copy.
Anyone who has access to the group can create a variant (admin or trainee).
"""
s = _stores()
group = _get_owned_group(s, gid)
if group.get("status") != "ready":
raise ApiError("group not ready", 403)
src = next((p for p in group.get("personas", []) if p.get("id") == pid), None)
if src is None:
raise ApiError("persona not found", 404)
lang = (group.get("input") or {}).get("language", "th")
try:
from ..services.persona_generator import PersonaGenerator
variant = PersonaGenerator(s["llm"]).generate_variant(
source=src,
sales_kit=group.get("sales_kit") or {},
language=lang,
)
except Exception as exc:
raise ApiError(f"variant failed: {exc}", 500)
# Assign a unique id and append to the group (keeps existing personas/sessions intact).
import uuid as _uuid
variant["id"] = f"persona-{_uuid.uuid4().hex[:10]}"
variant["source_persona_id"] = pid
personas = group.get("personas", []) + [variant]
s["groups"].update(gid, personas=personas)
full = ensure_persona_shape(variant)
actor = current_user()
if actor.get("role") == "super_admin":
persona_out = full
elif actor.get("role") == "user":
persona_out = revealable_view(full)
else:
persona_out = strip_secret_fields(full)
return jsonify({"persona": persona_out}), 201
@groups_bp.delete("/<gid>")
@require_auth
@require_roles("admin")