File size: 8,209 Bytes
62851e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""
Utility functions for time parsing, conflict detection, and metrics computation.
"""

from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Optional


# ─── Time Utilities ───────────────────────────────────────────────────────────

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]  # minutes


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)
    # Clamp to end of day
    new_mins = min(new_mins, time_to_minutes("18:00"))
    return minutes_to_time(new_mins)


# ─── Conflict Detection ──────────────────────────────────────────────────────

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


# ─── Metrics Computation ─────────────────────────────────────────────────────

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", [])

    # Task completion
    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-priority completion
    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
    )

    # Conflicts
    conflict_count = count_conflicts(tasks)

    # Message response
    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 (weighted composite)
    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),
    }


# ─── Task/Message Titles ─────────────────────────────────────────────────────

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"
]