| from __future__ import annotations |
|
|
| from collections import Counter, defaultdict |
| from statistics import mean |
|
|
| from core.schemas import ( |
| AggregatedReport, |
| BrutalQuote, |
| DeadliestStep, |
| ExcludedPersona, |
| FunnelStep, |
| IssueCluster, |
| Persona, |
| SegmentFailure, |
| SimulationResult, |
| SummaryMetrics, |
| ) |
|
|
|
|
| def _normalize_issue(issue: str) -> str: |
| lowered = issue.lower() |
| if "pricing" in lowered or "cost" in lowered or "free" in lowered: |
| return "Pricing unclear" |
| if "jargon" in lowered or "headline" in lowered or "word" in lowered: |
| return "Headline jargon" |
| if "visual" in lowered or "screenshot" in lowered or "demo" in lowered: |
| return "No product visual" |
| if "cta" in lowered or "button" in lowered or "sign" in lowered: |
| return "CTA unclear" |
| if "trust" in lowered or "proof" in lowered or "review" in lowered: |
| return "Missing trust signals" |
| if "contrast" in lowered or "accessibility" in lowered: |
| return "Accessibility risk" |
| return issue.strip().capitalize() |
|
|
|
|
| def aggregate_results( |
| results: list[SimulationResult], |
| personas: list[Persona], |
| flow_steps: list[str], |
| ) -> AggregatedReport: |
| valid_count = len(results) |
| total_personas = len(personas) |
| valid_persona_ids = {result.persona_id for result in results} |
| excluded_personas = [ |
| ExcludedPersona( |
| persona_id=persona.persona_id, |
| archetype=persona.archetype, |
| reason="simulation result failed validation", |
| ) |
| for persona in personas |
| if persona.persona_id not in valid_persona_ids |
| ] |
| completed = sum(1 for result in results if result.completed_task) |
| failed = valid_count - completed |
| success_rate = completed / valid_count if valid_count else 0.0 |
| avg_confusion = mean([result.overall_confusion_score for result in results]) if results else 0.0 |
| avg_trust = mean([result.overall_trust_score for result in results]) if results else 0.0 |
| max_steps = max([result.total_steps for result in results], default=len(flow_steps) or 1) |
|
|
| funnel: list[FunnelStep] = [] |
| for step in range(1, max_steps + 1): |
| label = flow_steps[step - 1] if step <= len(flow_steps) else f"Step {step}" |
| survival = ( |
| sum(1 for result in results if result.final_step_reached >= step) / valid_count |
| if valid_count |
| else 0.0 |
| ) |
| funnel.append(FunnelStep(step=step, label=label, survival_rate=survival)) |
|
|
| deadliest_step = None |
| if len(funnel) > 1: |
| drops = [ |
| (current.step, current.label, current.survival_rate - nxt.survival_rate) |
| for current, nxt in zip(funnel, funnel[1:], strict=False) |
| ] |
| step, label, dropoff = max(drops, key=lambda item: item[2]) |
| deadliest_step = DeadliestStep( |
| step=step, |
| label=label, |
| dropoff=max(dropoff, 0.0), |
| primary_reason=_primary_reason(results, step), |
| ) |
|
|
| by_segment: dict[str, list[SimulationResult]] = defaultdict(list) |
| for result in results: |
| by_segment[result.archetype].append(result) |
| worst_segments = sorted( |
| [ |
| SegmentFailure( |
| archetype=archetype, |
| failure_rate=1 |
| - (sum(r.completed_task for r in segment_results) / len(segment_results)), |
| ) |
| for archetype, segment_results in by_segment.items() |
| ], |
| key=lambda item: item.failure_rate, |
| reverse=True, |
| )[:5] |
|
|
| issue_counts = Counter( |
| _normalize_issue(issue) |
| for result in results |
| for issue in result.top_issues |
| if issue.strip() |
| ) |
| top_issues = [ |
| IssueCluster( |
| issue=issue, |
| frequency=frequency, |
| severity="high" if frequency >= 3 else "medium" if frequency == 2 else "low", |
| ) |
| for issue, frequency in issue_counts.most_common(5) |
| ] |
| brutal_quotes = [ |
| BrutalQuote( |
| quote=result.brutal_quote, |
| persona=result.persona_id, |
| archetype=result.archetype, |
| ) |
| for result in sorted(results, key=lambda item: item.overall_confusion_score, reverse=True) |
| if not result.completed_task |
| ][:3] |
|
|
| return AggregatedReport( |
| summary=SummaryMetrics( |
| total_personas=total_personas, |
| valid_personas=valid_count, |
| excluded=len(excluded_personas), |
| completed=completed, |
| failed=failed, |
| success_rate=success_rate, |
| avg_confusion=avg_confusion, |
| avg_trust=avg_trust, |
| ), |
| failure_funnel=funnel, |
| deadliest_step=deadliest_step, |
| worst_segments=worst_segments, |
| top_issues=top_issues, |
| brutal_quotes=brutal_quotes, |
| excluded_personas=excluded_personas, |
| persona_results=results, |
| ) |
|
|
|
|
| def _primary_reason(results: list[SimulationResult], step: int) -> str: |
| issues = Counter( |
| _normalize_issue(issue) |
| for result in results |
| for log in result.step_log |
| if log.step == step |
| for issue in log.issues |
| ) |
| return issues.most_common(1)[0][0] if issues else "No dominant issue" |
|
|