Ship the scaffolding for #99 (phase B.3 of #81): - ml/serving: add /score/egreedy/v2, /reward/egreedy/v2, /stats/egreedy/v2 endpoints (D=12). New feature dims: completion/dismiss rates, mean dwell (clipped 10min), preferred-hour alignment (cosine, 1-dim), tip volume (log). Separate state file per user (_egreedy_v2.json). /reset clears v2 state too. - ADR-0012: documents D=7→12 dimension change, normalization choices, shadow rollout protocol, and promotion gate (offline sim win per ADR-0002). - recommender.ts: register egreedy-v2-shadow in shadow-policy map (disabled by default). When enabled, calls /score/egreedy/v2 fire-and-forget and publishes shadow:egreedy-v2-shadow serve signal. No reward to shadow — sim is the gate. - sim runner/personas: personas carry synthetic profile_features per persona; _call_score/_call_reward thread profile_features through (None-safe for v1/linucb). - 18 new Python tests; all 56 Python + 170 TS tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
"""Synthetic user personas for simulation."""
|
||
|
||
import math
|
||
from dataclasses import dataclass
|
||
|
||
|
||
@dataclass
|
||
class Persona:
|
||
name: str
|
||
description: str
|
||
# Feature preference weights — used by deterministic judge
|
||
prefers_high_priority: float # 0–1: scales response to priority
|
||
prefers_overdue: float # 0–1: scales response to overdue tasks
|
||
morning_active: bool # higher engagement hours 6–10
|
||
evening_active: bool # higher engagement hours 18–22
|
||
recency_bias: float # 0–1: prefers recently-due tasks
|
||
# Synthetic profile features for egreedy-v2 sim (ADR-0012).
|
||
# Values represent what a typical user of this persona would have
|
||
# accumulated after a few weeks of app use.
|
||
_completion_rate: float = 0.3
|
||
_dismiss_rate: float = 0.2
|
||
_mean_dwell_ms: float = 60_000.0 # ms
|
||
_preferred_hour: float = 12.0 # 0–23
|
||
_tip_volume_30d: float = 15.0
|
||
|
||
def profile_features(self, now_hour: int | None = None) -> dict:
|
||
"""Return profile_features dict compatible with the ml/serving API."""
|
||
return {
|
||
"completion_rate_30d": self._completion_rate,
|
||
"dismiss_rate_30d": self._dismiss_rate,
|
||
"mean_dwell_ms_30d": self._mean_dwell_ms,
|
||
"preferred_hour": self._preferred_hour,
|
||
"tip_volume_30d": self._tip_volume_30d,
|
||
}
|
||
|
||
|
||
PERSONAS: list[Persona] = [
|
||
Persona(
|
||
name="deadline-driven",
|
||
description=(
|
||
"Responds urgently to overdue and high-priority tasks. "
|
||
"Most active in the morning. Dismisses low-priority tips."
|
||
),
|
||
prefers_high_priority=0.9,
|
||
prefers_overdue=0.85,
|
||
morning_active=True,
|
||
evening_active=False,
|
||
recency_bias=0.3,
|
||
_completion_rate=0.55,
|
||
_dismiss_rate=0.10,
|
||
_mean_dwell_ms=45_000.0,
|
||
_preferred_hour=8.0,
|
||
_tip_volume_30d=22.0,
|
||
),
|
||
Persona(
|
||
name="evening-relaxed",
|
||
description=(
|
||
"Reviews tasks in the evenings. Neutral on priority. "
|
||
"Snoozes morning recommendations."
|
||
),
|
||
prefers_high_priority=0.5,
|
||
prefers_overdue=0.4,
|
||
morning_active=False,
|
||
evening_active=True,
|
||
recency_bias=0.5,
|
||
_completion_rate=0.30,
|
||
_dismiss_rate=0.25,
|
||
_mean_dwell_ms=90_000.0,
|
||
_preferred_hour=20.0,
|
||
_tip_volume_30d=12.0,
|
||
),
|
||
Persona(
|
||
name="low-priority-first",
|
||
description=(
|
||
"Clears small tasks first. Snoozes urgent items until deadline. "
|
||
"Morning person."
|
||
),
|
||
prefers_high_priority=0.2,
|
||
prefers_overdue=0.6,
|
||
morning_active=True,
|
||
evening_active=False,
|
||
recency_bias=0.7,
|
||
_completion_rate=0.40,
|
||
_dismiss_rate=0.15,
|
||
_mean_dwell_ms=30_000.0,
|
||
_preferred_hour=9.0,
|
||
_tip_volume_30d=18.0,
|
||
),
|
||
Persona(
|
||
name="consistent-responder",
|
||
description=(
|
||
"Engages consistently across hours and days. "
|
||
"Acts on helpful tips regardless of priority."
|
||
),
|
||
prefers_high_priority=0.6,
|
||
prefers_overdue=0.6,
|
||
morning_active=True,
|
||
evening_active=True,
|
||
recency_bias=0.5,
|
||
_completion_rate=0.50,
|
||
_dismiss_rate=0.10,
|
||
_mean_dwell_ms=60_000.0,
|
||
_preferred_hour=12.0,
|
||
_tip_volume_30d=30.0,
|
||
),
|
||
Persona(
|
||
name="overdue-ignorer",
|
||
description=(
|
||
"Avoids overdue tasks (stress avoidance). "
|
||
"Focuses on future-due, high-priority items. Evening person."
|
||
),
|
||
prefers_high_priority=0.8,
|
||
prefers_overdue=0.1,
|
||
morning_active=False,
|
||
evening_active=True,
|
||
recency_bias=0.2,
|
||
_completion_rate=0.20,
|
||
_dismiss_rate=0.40,
|
||
_mean_dwell_ms=120_000.0,
|
||
_preferred_hour=19.0,
|
||
_tip_volume_30d=10.0,
|
||
),
|
||
]
|