| """ |
| Utility functions for time parsing, conflict detection, and metrics computation. |
| """ |
|
|
| from datetime import datetime, timedelta |
| from typing import List, Dict, Tuple, Optional |
|
|
|
|
| |
|
|
| TIME_SLOTS = [ |
| "08:00", "08:30", "09:00", "09:30", "10:00", "10:30", |
| "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", |
| "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", |
| "17:00", "17:30", "18:00" |
| ] |
|
|
| DURATIONS = [30, 60, 90, 120] |
|
|
|
|
| def parse_time(time_str: str) -> datetime: |
| """Parse a HH:MM time string into a datetime object (date fixed to today).""" |
| return datetime.strptime(time_str, "%H:%M") |
|
|
|
|
| def time_to_minutes(time_str: str) -> int: |
| """Convert HH:MM to total minutes since midnight.""" |
| dt = parse_time(time_str) |
| return dt.hour * 60 + dt.minute |
|
|
|
|
| def minutes_to_time(minutes: int) -> str: |
| """Convert total minutes since midnight to HH:MM string.""" |
| h = minutes // 60 |
| m = minutes % 60 |
| return f"{h:02d}:{m:02d}" |
|
|
|
|
| def get_end_time(start_time: str, duration_minutes: int) -> str: |
| """Calculate end time given start time and duration in minutes.""" |
| start_mins = time_to_minutes(start_time) |
| end_mins = start_mins + duration_minutes |
| return minutes_to_time(end_mins) |
|
|
|
|
| def time_ranges_overlap( |
| start1: str, dur1: int, start2: str, dur2: int |
| ) -> bool: |
| """Check if two time ranges overlap. |
| |
| Args: |
| start1: Start time of first range (HH:MM). |
| dur1: Duration of first range in minutes. |
| start2: Start time of second range (HH:MM). |
| dur2: Duration of second range in minutes. |
| |
| Returns: |
| True if the ranges overlap. |
| """ |
| s1 = time_to_minutes(start1) |
| e1 = s1 + dur1 |
| s2 = time_to_minutes(start2) |
| e2 = s2 + dur2 |
| return s1 < e2 and s2 < e1 |
|
|
|
|
| def advance_time_slot(current_time: str, steps: int = 1) -> str: |
| """Advance to the next time slot(s).""" |
| current_mins = time_to_minutes(current_time) |
| new_mins = current_mins + (30 * steps) |
| |
| new_mins = min(new_mins, time_to_minutes("18:00")) |
| return minutes_to_time(new_mins) |
|
|
|
|
| |
|
|
| def build_conflict_graph(tasks: List[Dict]) -> Dict[int, List[int]]: |
| """Build an adjacency list of task conflicts based on time overlaps. |
| |
| Args: |
| tasks: List of task dicts with 'id', 'time', 'duration', 'status'. |
| |
| Returns: |
| Dict mapping task_id β list of conflicting task_ids. |
| """ |
| scheduled = [ |
| t for t in tasks |
| if t["status"] in ("pending", "scheduled", "completed") |
| ] |
| conflicts: Dict[int, List[int]] = {t["id"]: [] for t in scheduled} |
|
|
| for i, t1 in enumerate(scheduled): |
| for t2 in scheduled[i + 1:]: |
| if time_ranges_overlap( |
| t1["time"], t1.get("duration", 30), |
| t2["time"], t2.get("duration", 30) |
| ): |
| conflicts[t1["id"]].append(t2["id"]) |
| conflicts[t2["id"]].append(t1["id"]) |
|
|
| return conflicts |
|
|
|
|
| def count_conflicts(tasks: List[Dict]) -> int: |
| """Count the total number of unique conflict pairs.""" |
| graph = build_conflict_graph(tasks) |
| count = 0 |
| seen = set() |
| for tid, neighbors in graph.items(): |
| for nid in neighbors: |
| pair = (min(tid, nid), max(tid, nid)) |
| if pair not in seen: |
| seen.add(pair) |
| count += 1 |
| return count |
|
|
|
|
| |
|
|
| def compute_metrics(state_dict: Dict) -> Dict[str, float]: |
| """Compute evaluation metrics from a terminal state. |
| |
| Metrics: |
| - task_completion_rate: fraction of tasks completed. |
| - high_priority_completion: fraction of high-priority tasks completed. |
| - conflict_count: number of scheduling conflicts remaining. |
| - message_response_rate: fraction of inbox messages replied to. |
| - efficiency_score: weighted composite score (0β100). |
| """ |
| tasks = state_dict.get("tasks", []) |
| inbox = state_dict.get("inbox", []) |
|
|
| |
| total_tasks = len(tasks) if tasks else 1 |
| completed = sum(1 for t in tasks if t["status"] == "completed") |
| task_completion_rate = completed / total_tasks |
|
|
| |
| high_tasks = [t for t in tasks if t["priority"] == "high"] |
| high_completed = sum(1 for t in high_tasks if t["status"] == "completed") |
| high_priority_completion = ( |
| high_completed / len(high_tasks) if high_tasks else 1.0 |
| ) |
|
|
| |
| conflict_count = count_conflicts(tasks) |
|
|
| |
| total_messages = len(inbox) if inbox else 1 |
| replied = sum(1 for m in inbox if m.get("replied", False)) |
| message_response_rate = replied / total_messages |
|
|
| |
| efficiency_score = ( |
| task_completion_rate * 35 |
| + high_priority_completion * 30 |
| + message_response_rate * 20 |
| + max(0, (1 - conflict_count / max(total_tasks, 1))) * 15 |
| ) |
|
|
| return { |
| "task_completion_rate": round(task_completion_rate, 3), |
| "high_priority_completion": round(high_priority_completion, 3), |
| "conflict_count": conflict_count, |
| "message_response_rate": round(message_response_rate, 3), |
| "efficiency_score": round(efficiency_score, 1), |
| } |
|
|
|
|
| |
|
|
| MEETING_TITLES = [ |
| "Q4 Strategy Review", "1:1 with Manager", "Sprint Planning", |
| "Client Call β Acme Corp", "Board Presentation Prep", |
| "Design Review", "Weekly Standup", "Investor Update", |
| "Product Roadmap Sync", "Cross-team Alignment", |
| "Budget Review Meeting", "Hiring Panel Interview", |
| "Vendor Negotiation", "Architecture Review", |
| "Marketing Campaign Kickoff" |
| ] |
|
|
| WORK_TITLES = [ |
| "Finalize Q3 Report", "Review PR #247", "Update Documentation", |
| "Prepare Slide Deck", "Analyze Sales Data", |
| "Write Technical Spec", "Code Review Session", |
| "Database Migration Plan", "Security Audit Follow-up", |
| "Performance Optimization", "Deploy Hotfix v2.3.1", |
| "Update CI/CD Pipeline", "Refactor Auth Module", |
| "Write Unit Tests", "API Integration Testing" |
| ] |
|
|
| PERSONAL_TITLES = [ |
| "Dentist Appointment", "Gym Session", "Lunch with Friend", |
| "Pick Up Dry Cleaning", "Car Service Appointment", |
| "Call Insurance Company", "Grocery Shopping", |
| "Yoga Class", "Parent-Teacher Conference", |
| "Home Repair β Plumber" |
| ] |
|
|
| MESSAGE_CONTENTS_URGENT = [ |
| "Need the quarterly figures ASAP for the board meeting.", |
| "Critical bug in production β customer-facing. Please advise.", |
| "Client escalation: contract renewal at risk. Call me.", |
| "Server outage alert: all hands on deck.", |
| "Investor meeting moved to tomorrow. Need deck by EOD.", |
| "Legal flagged compliance issue. Immediate review needed.", |
| "VP requesting project status update within the hour.", |
| ] |
|
|
| MESSAGE_CONTENTS_NORMAL = [ |
| "FYI: Updated the shared drive with new templates.", |
| "Team lunch this Friday β please RSVP.", |
| "Quick question about the API changes in v2.4.", |
| "Sharing meeting notes from yesterday's sync.", |
| "Reminder: timesheets due by Friday.", |
| "New onboarding docs are ready for review.", |
| "Coffee chat next week? Let me know your availability.", |
| ] |
|
|
| SENDERS = [ |
| "CEO", "VP Engineering", "Product Manager", "HR Director", |
| "CFO", "Team Lead", "Client β Acme Corp", "External Counsel", |
| "Marketing Director", "CTO", "Board Member", |
| "Direct Report β Alex", "Direct Report β Priya", |
| "Colleague β Jordan", "Colleague β Sam" |
| ] |
|
|