Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions engines/engine_mk2.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class DayPlan:
day_name: str
day_type: str
activities: List[Activity]
friction: float = 1.0
target_minutes: Optional[Dict[str, int]] = None


OUTDOOR_ACTIVITIES = {"outdoor_run", "bike_ride", "park_visit", "hiking", "outdoor_walk"}
Expand Down Expand Up @@ -357,6 +359,10 @@ def _generate_week_activities(
special = self._calendar_provider.get_special_period_effects(current_date)
activities = self.apply_special_period_effects(activities, special)

target_minutes: Dict[str, int] = {}
for activity in activities:
target_minutes[activity.name] = target_minutes.get(activity.name, 0) + activity.base_duration_minutes

for activity in activities:
self._apply_friction(activity, daily_friction)
if activity.name == "sleep":
Expand All @@ -368,7 +374,16 @@ def _generate_week_activities(
activity.actual_duration,
)

week_schedule.append(DayPlan(current_date, day_name, day_type, activities))
week_schedule.append(
DayPlan(
current_date,
day_name,
day_type,
activities,
friction=daily_friction,
target_minutes=target_minutes,
)
)

return week_schedule

Expand Down Expand Up @@ -536,6 +551,7 @@ def generate_complete_week(
week_seed: int,
templates: Optional[Dict[str, ActivityTemplate]] = None,
yearly_budget: Optional[YearlyBudget] = None,
debug: bool = False,
) -> Dict[str, object]:
random.seed(week_seed)
templates = templates or DEFAULT_TEMPLATES
Expand All @@ -545,11 +561,50 @@ def generate_complete_week(
{f"{plan.day_name} ({plan.date.isoformat()})": plan.activities for plan in week_plans}
)

debug_trace: Optional[Dict[str, Any]] = None
debug_days: Dict[str, Dict[str, Any]] = {}
if debug:
for plan in week_plans:
debug_days[plan.day_name] = {
"date": plan.date.isoformat(),
"classification": plan.day_type,
"friction": plan.friction,
"target_minutes": dict(plan.target_minutes or {}),
"activities_before_compression": [
{
"name": activity.name,
"base": activity.base_duration_minutes,
"waste_multiplier": activity.waste_multiplier,
"optional": activity.optional,
"priority": activity.priority,
}
for activity in plan.activities
],
}
debug_trace = {
"profile": profile.name,
"budget": asdict(profile.budget),
"per_day": debug_days,
}

compression_metadata: Dict[str, Dict[str, object]] = {}
for plan in week_plans:
compressed, metadata = self._compress_day_if_needed(plan.activities)
plan.activities = compressed
compression_metadata[plan.date.isoformat()] = metadata
if debug:
day_debug = debug_days.get(plan.day_name)
if day_debug is not None:
day_debug["activities_after_compression"] = [
{
"name": activity.name,
"base": activity.base_duration_minutes,
"actual": activity.actual_duration,
"optional": activity.optional,
"priority": activity.priority,
}
for activity in plan.activities
]

normalized_inputs: List[Dict[str, Any]] = []
for plan in week_plans:
Expand All @@ -559,6 +614,20 @@ def generate_complete_week(
event.day = plan.day_name
events = self.fill_free_time(events)
events = self.apply_micro_jitter(events)
if debug:
day_debug = debug_days.get(plan.day_name)
if day_debug is not None:
day_debug["events"] = [
{
"activity": event.activity.name
if isinstance(event.activity, Activity)
else event.activity,
"start_minutes": event.start_minutes,
"end_minutes": event.end_minutes,
"duration": max(0, event.end_minutes - event.start_minutes),
}
for event in events
]
day_offset = (plan.date - start_date).days
weekday_index = plan.date.weekday()
for event in events:
Expand Down Expand Up @@ -609,7 +678,18 @@ def generate_complete_week(
)
summary_hours = self._generate_summary(events_payload)

return {
weekly_totals_minutes: Dict[str, int] = {}
if debug:
for event in events_payload:
activity_name = str(event.get("activity"))
duration = int(event.get("duration_minutes", 0) or 0)
weekly_totals_minutes[activity_name] = (
weekly_totals_minutes.get(activity_name, 0) + duration
)
if debug_trace is not None:
debug_trace["weekly_totals_from_events"] = weekly_totals_minutes

result: Dict[str, Any] = {
"person": profile.name,
"week_start": start_date.isoformat(),
"events": events_payload,
Expand All @@ -623,6 +703,11 @@ def generate_complete_week(
},
}

if debug and debug_trace is not None:
result["debug_trace"] = debug_trace

return result

def select_profile(self, archetype: str) -> Tuple[PersonProfile, Dict[str, ActivityTemplate]]:
archetype = archetype.lower()
if archetype not in self._profile_factory:
Expand Down
13 changes: 10 additions & 3 deletions engines/web_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,17 @@ def mk1_run_web(archetype: str, week_start: Optional[str], seed: Any) -> SchemaP
return _ensure_schema(payload, rig="default", seed=seed_value, archetype=archetype_key or "office")


def mk2_run_calendar_web(archetype: str, week_start: Optional[str], seed: Any) -> SchemaPayload:
def mk2_run_calendar_web(
archetype: str, week_start: Optional[str], seed: Any, debug: bool = False
) -> SchemaPayload:
archetype_key = str(archetype or "office").strip().lower()
seed_value = _coerce_seed(seed)
start_date = _coerce_start_date(week_start) or date.today()

profile, templates = _MK2_RIG.select_profile(archetype_key)
result = _MK2_RIG.generate_complete_week(profile, start_date, seed_value, templates, None)
result = _MK2_RIG.generate_complete_week(
profile, start_date, seed_value, templates, None, debug=debug
)

payload: MutableMapping[str, Any] = dict(result)
payload.setdefault("issues", [])
Expand All @@ -307,6 +311,7 @@ def mk2_run_workforce_web(
week_start: Optional[str],
seed: Any,
yearly_budget: Optional[Mapping[str, Any]],
debug: bool = False,
) -> SchemaPayload:
archetype_key = str(archetype or "office").strip().lower()
seed_value = _coerce_seed(seed)
Expand All @@ -315,7 +320,9 @@ def mk2_run_workforce_web(
profile, templates = _MK2_RIG.select_profile(archetype_key)
budget = _build_yearly_budget(yearly_budget)

result = _MK2_RIG.generate_complete_week(profile, start_date, seed_value, templates, budget)
result = _MK2_RIG.generate_complete_week(
profile, start_date, seed_value, templates, budget, debug=debug
)

payload: MutableMapping[str, Any] = dict(result)
payload.setdefault("issues", [])
Expand Down
3 changes: 2 additions & 1 deletion rigs/workforce_rig.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ def generate_complete_week(
week_seed: int,
templates: Optional[Dict[str, ActivityTemplate]] = None,
yearly_budget: Optional[YearlyBudget] = None,
debug: bool = False,
) -> Dict[str, object]:
"""Delegate generation to the configured engine."""

return self._engine.generate_complete_week(
profile, start_date, week_seed, templates, yearly_budget
profile, start_date, week_seed, templates, yearly_budget, debug=debug
)