From 2b414d13b7fe27dafa06eb44732fca833476a759 Mon Sep 17 00:00:00 2001 From: Macky Date: Sun, 9 Aug 2026 12:23:15 +0700 Subject: [PATCH] fix(analyze): don't hard-fail when LLM generates <15 personas (retry + accept) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live test found: deepseek returned 14/15 personas -> whole analyze 500'd, breaking the 'กดสร้าง -> auto-analyze' flow. Now generate() retries up to 3x with a nudge, and accepts a short result (>=8 personas) instead of crashing — admin can top up the rest with 'สร้างบุคคลต้นแบบเพิ่มเติม'. Also default tolerance/recontact on generated personas. All backend suites pass. --- backend/app/services/persona_generator.py | 30 ++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/backend/app/services/persona_generator.py b/backend/app/services/persona_generator.py index b9d8e95..e24ce98 100644 --- a/backend/app/services/persona_generator.py +++ b/backend/app/services/persona_generator.py @@ -31,10 +31,23 @@ class PersonaGenerator: f"Sales Kit:\n{kit_json}\n\n" f"Generate exactly 15 personas (5 per tier A/B/C) as JSON." ) - result = self.llm.complete_json( - PERSONA_SYSTEM, user_prompt, temperature=0.8, max_tokens=14000 - ) - personas = result.get("personas") or [] + # Real LLMs sometimes return fewer than 15 (truncation / merge). Retry up to 2 extra + # times with a nudge; the body below already ACCEPTS short results (>= 8) instead of + # hard-failing, so these retries are just best-effort to reach a fuller set. + attempt = 0 + while True: + attempt += 1 + prompt = user_prompt + ( + "" + if attempt == 1 + else "\n\n(Note: you left some personas out — please output all 15, one JSON object per persona, no extra prose.)" + ) + result = self.llm.complete_json( + PERSONA_SYSTEM, prompt, temperature=0.8, max_tokens=14000 + ) + personas = result.get("personas") or [] + if (isinstance(personas, list) and len(personas) >= 15) or attempt >= 3: + break if not isinstance(personas, list) or not personas: raise ValueError("persona generator returned no personas") @@ -57,6 +70,8 @@ class PersonaGenerator: p.setdefault("pains", []) p.setdefault("negotiation_levers", []) p.setdefault("objections", []) + p.setdefault("tolerance", 3) + p.setdefault("recontact", False) normalized.append(p) # Wrap tier-C: ensure at least one wrong_text persona @@ -69,6 +84,9 @@ class PersonaGenerator: p["special"] = "wrong_text" break - if len(normalized) < 15: - raise ValueError(f"expected 15 personas, generated {len(normalized)}") + if len(normalized) < 8: + raise ValueError(f"expected ~15 personas, generated only {len(normalized)}") + # NOTE: if we're short of 15 (real LLMs occasionally return 14/13), we ACCEPT what we + # got rather than crashing the whole analyze — the caller/UI can top up with + # "create more personas". A retry loop lives in generate(). return normalized