"""Mock LLM for deterministic end-to-end tests (no external API needed). Substitutes for app.llm.LLMClient. Returns canned JSON for structured calls and simple replies for chat calls, so the full analyze→persona→chat→debrief flow runs. """ from __future__ import annotations import json from typing import Any SAMPLE_SALES_KIT = { "productName": "CloudPOS", "category": "POS software", "valueProps": ["faster checkout", "inventory sync"], "features": ["tablets", "reports"], "pricingAnchors": ["1,000 THB/month"], "targetAudience": {"segment": "SME restaurants", "demographics": "", "useCases": ["front counter"]}, "objectionHandlers": ["free trial", "setup included"], "initialPainFit": [ {"pain": "slow checkout queues", "fit": "strong", "evidence": "faster checkout"}, {"pain": "lost sales from stockouts", "fit": "partial", "evidence": "inventory sync"}, ], "scenarioFrame": "Cloud POS sold over LINE to Bangkok SME restaurants.", } def _sample_persona(idx: int, tier: str) -> dict[str, Any]: return { "id": f"persona-{idx:02d}", "name": f"Persona {idx}", "tier": tier, "channel": "line", "initiation_mode": "customer" if idx % 3 else "seller", "profession": "restaurant owner", "age_group": "30s", "location": "Bangkok", "product_context": "running a small noodle shop", "background": "Runs a family noodle shop for 8 years.", "income": "60k THB/month", "lifestyle": "works long hours", "personality": "practical and cautious", "communication_style": "short, direct, casual", "budget": "1,500 THB/month max", "decision_timeline": "within 2 weeks", "goal": "reduce lunch-rush queues", "objections": ["too expensive", "hard to learn"], "pains": [ {"id": "p1", "name": "slow checkout", "fit": "strong", "description": "Long queues at lunch", "rootCause": "manual order taking", "resolutionConditions": ["show faster checkout", "offer a trial"]}, {"id": "p2", "name": "stockouts", "fit": "partial", "description": "Runs out of ingredients", "rootCause": "no inventory tracking", "resolutionConditions": ["show inventory feature"]}, ], "negotiation_levers": ["price reduction", "free setup"], "opener": "Hi, I saw your POS ad. Does it work with small shops?", "special": "wrong_text" if (tier == "C" and idx % 5 == 4) else "", "difficulty": 2 if tier == "A" else (3 if tier == "B" else 4), "notes": "sample", } def make_personas() -> list[dict[str, Any]]: out = [] idx = 1 for tier in ["A", "B", "C"]: for _ in range(5): out.append(_sample_persona(idx, tier)) idx += 1 return out class MockLLM: """Drop-in for app.llm.LLMClient — reads config the same way.""" persona_count = 0 def __init__(self, **kwargs): pass def complete(self, system_prompt: str, user_prompt: str, **kw) -> str: if "Persona generation prompts" in system_prompt or "persona designer" in system_prompt.lower(): return json.dumps({"personas": make_personas()}, ensure_ascii=False) if "market-research persona designer" in system_prompt.lower(): return json.dumps({"personas": make_personas()}, ensure_ascii=False) return "ok" def complete_json(self, system_prompt: str, user_prompt: str, **kw) -> dict[str, Any]: sp = system_prompt.lower() if "ecommerce/b2b analyst" in sp: return dict(SAMPLE_SALES_KIT) if "market-research persona designer" in sp: return {"personas": make_personas()} if "sales-training simulator" in sp and "PRIVATE" in system_prompt: return {"persona": _sample_persona(99, "C")} if "judge" in sp and "sales-training chat" in sp: return { "outcome": "won", "score": 82, "pain": "slow checkout queues", "why": "resolved the pain and secured acceptance", "failurePoints": [], "coaching": [], "painProgress": {"slow checkout": 100}, } return {} def complete_conversation(self, messages, **kw) -> str: # persona chat: echo a short in-character reply return json.dumps({"reply": "I see. Tell me more about the price then."}, ensure_ascii=False)