Spaces:
Sleeping
Sleeping
| # tests/simulate_lifecycle.py | |
| import json | |
| import datetime | |
| import random | |
| from typing import Dict | |
| from unittest.mock import MagicMock | |
| # Import the logic | |
| import sys | |
| import os | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from app.services.rl_engine import ( | |
| get_behavior_signal, | |
| update_reward, | |
| _load_bandit_params, | |
| _calculate_weekly_strain | |
| ) | |
| from app.models.progress import Progress | |
| def simulate_archetype(name: str, days: int, behavior_fn): | |
| print(f"\n{'='*40}") | |
| print(f" Simulating Archetype: {name} ({days} Days)") | |
| print(f"{'='*40}") | |
| user = MagicMock() | |
| user.user_id = f"sim_user_{name}" | |
| user.competence = 0.7 | |
| user.fatigue = 0.0 | |
| user.mood = 0.5 | |
| user.sleep_quality = 0.8 | |
| user.stress = 0.2 | |
| user.current_streak = 0 | |
| user.rl_bandit_params = None | |
| user.rl_history_count = 0 | |
| user.days_since_last_login = 0 | |
| # We mock the DB and _compute_historical_features to simulate a rolling 7-day progress. | |
| progress_history = [] | |
| def mock_db_query(*args, **kwargs): | |
| mock_q = MagicMock() | |
| def filter_fn(*f_args, **f_kwargs): | |
| return mock_q | |
| mock_q.filter = filter_fn | |
| mock_q.order_by = lambda x: mock_q | |
| # We need to return exactly the mock progress history | |
| def all_fn(): | |
| return progress_history | |
| mock_q.all = all_fn | |
| return mock_q | |
| db = MagicMock() | |
| db.query = mock_db_query | |
| for day in range(1, days + 1): | |
| # 1. Update user state based on the archetype's behavior | |
| behavior_fn(user, day, progress_history) | |
| # 2. Get Behavior Signal | |
| state_override = [user.competence, user.mood, user.fatigue, user.sleep_quality, user.stress, 0.0, 0.0] | |
| # Simulate getting historical features by replacing the DB query inside rl_engine | |
| import app.services.rl_engine as rl | |
| original_compute = rl._compute_historical_features | |
| def fake_compute(*args): | |
| return { | |
| "consecutive_hard_days": sum(1 for r in progress_history[-3:] if r.actual_duration_min >= 60), | |
| "recent_completion_rate": 0.8, | |
| "avg_satisfaction_7d": sum(r.satisfaction for r in progress_history[-7:] if r.satisfaction) / max(1, len(progress_history[-7:])), | |
| "fatigue_trend": 0.05 if user.fatigue > 0.7 else 0.0, | |
| "overwork_signal": 0.8 if user.fatigue > 0.8 else 0.0, | |
| "deep_work_streak": sum(1 for r in progress_history[-3:] if r.action_label == "Deep Work"), | |
| "avg_daily_minutes": 60.0, | |
| "consecutive_same_action": 3 if len(progress_history) >= 3 and all(r.action_label == "Deep Work" for r in progress_history[-3:]) else 0, | |
| "last_action": progress_history[-1].action_label if progress_history else "Normal", | |
| "ema_fatigue": user.fatigue, | |
| "ema_mood": user.mood, | |
| "burnout_warning": user.fatigue > 0.8 and json.loads(user.rl_bandit_params).get("_recovery_debt", 0) > 10.0 if user.rl_bandit_params else False | |
| } | |
| rl._compute_historical_features = fake_compute | |
| try: | |
| signal = get_behavior_signal(user, state_override=state_override, db=db) | |
| finally: | |
| rl._compute_historical_features = original_compute | |
| action = signal["action_label"] | |
| params = _load_bandit_params(user) | |
| debt = params.get("_recovery_debt", 0.0) | |
| # 3. Apply consequence of the action (Mocking the user performing the task) | |
| # Deep Work generates high fatigue. Recovery reduces it. | |
| reward = 1.0 | |
| duration = 60 | |
| if action == "Deep Work": | |
| user.fatigue = min(1.0, user.fatigue + 0.15) | |
| user.mood = max(0.1, user.mood - 0.05) | |
| reward = 0.9 if user.fatigue < 0.8 else 0.4 # Reward drops if exhausted | |
| elif action in ("Recovery", "Light Review", "Rest"): | |
| user.fatigue = max(0.0, user.fatigue - 0.2) | |
| user.mood = min(1.0, user.mood + 0.1) | |
| duration = 30 | |
| reward = 0.8 | |
| else: | |
| user.fatigue = min(1.0, user.fatigue + 0.05) | |
| # 4. Save to history | |
| p = MagicMock() | |
| p.date = (datetime.date.today() - datetime.timedelta(days=days-day)).isoformat() | |
| p.completed = True | |
| p.action_label = action | |
| p.actual_duration_min = duration | |
| p.satisfaction = reward | |
| p.fatigue = user.fatigue | |
| p.mood = user.mood | |
| progress_history.append(p) | |
| if len(progress_history) > 14: | |
| progress_history.pop(0) | |
| # 5. Update Reward | |
| update_reward(db, user, action, reward) | |
| print(f"Day {day:02d} | Fatigue: {user.fatigue:.2f} | Mood: {user.mood:.2f} | Debt: {debt:5.2f} | Action: {action:12s} | Rew: {reward:.2f} | Warning: {signal.get('burnout_warning', False)}") | |
| # --- Behaviors --- | |
| def overachiever_behavior(user, day, hist): | |
| # Ignores fatigue, always tries to stay motivated. Doesn't sleep well if stressed. | |
| user.sleep_quality = 0.6 if user.fatigue > 0.6 else 0.9 | |
| user.stress = 0.5 | |
| if day == 1: | |
| user.fatigue = 0.1 | |
| user.mood = 0.9 | |
| def erratic_behavior(user, day, hist): | |
| # High variance in sleep and mood | |
| user.sleep_quality = random.uniform(0.3, 1.0) | |
| user.mood += random.uniform(-0.3, 0.3) | |
| user.mood = max(0.1, min(1.0, user.mood)) | |
| if day == 1: | |
| user.fatigue = 0.3 | |
| def normal_behavior(user, day, hist): | |
| # Standard user, sleeps okay, mood fluctuates mildly | |
| user.sleep_quality = 0.8 | |
| if day % 7 in (6, 0): # Weekend | |
| user.fatigue = max(0.0, user.fatigue - 0.3) | |
| user.mood = min(1.0, user.mood + 0.2) | |
| if day == 1: | |
| user.fatigue = 0.2 | |
| user.mood = 0.7 | |
| if __name__ == "__main__": | |
| simulate_archetype("The Overachiever", 30, overachiever_behavior) | |
| simulate_archetype("The Erratic", 30, erratic_behavior) | |
| simulate_archetype("The Normal", 30, normal_behavior) | |