from agentpro.tools import Tool from typing import Any import datetime import dateutil.parser import re class DailyEventSummaryTool(Tool): # ── 1) Class attributes (Pydantic fields) ─────────────────────────── name: str = "Daily Event Summary" description: str = ( "Provide a narrative summary of Google Calendar events for any requested day. " "You can ask for today, tomorrow, day after tomorrow, last day of this year, " "a weekday of the current week (e.g., 'Wednesday'), or an explicit date like 'June 10, 2025'." ) action_type: str = "daily_event_summary" input_format: str = ( "Natural language calendar query. Examples:\n" " • \"What’s on my calendar today?\"\n" " • \"Show me my schedule tomorrow.\"\n" " • \"Plan for day after tomorrow.\"\n" " • \"Events on the last day of this year.\"\n" " • \"What do I have on Wednesday?\"\n" " • \"What do I have on June 10, 2025?\"\n" ) # ── 2) We expect a Google Calendar “service” to be passed in at instantiation ── service: Any def run(self, input_text: Any) -> str: """ Determine which calendar day the user wants and return a single-sentence narrative describing each event's start time, end time, and duration. Supported queries: - “today” - “tomorrow” - “day after tomorrow” - “last day of this year” (Dec 31, current year) - any weekday of the current week (e.g., “Monday”, “Friday”) - explicit dates (e.g., “June 10, 2025” or “2025-06-10”) """ text = str(input_text).lower() # ──────────────────────────────────────────────────────────────────── # A) Handle relative-day keywords and weekdays of this week # ──────────────────────────────────────────────────────────────────── today_utc = datetime.datetime.utcnow().date() target_date = None # 1) “today” if "today" in text: target_date = today_utc # 2) “tomorrow” (but not “day after tomorrow”) elif "tomorrow" in text and "day after" not in text: target_date = today_utc + datetime.timedelta(days=1) # 3) “day after tomorrow” elif "day after tomorrow" in text: target_date = today_utc + datetime.timedelta(days=2) # 4) “last day of this year” or “last day of the year” elif "last day of this year" in text or "last day of the year" in text: year = today_utc.year target_date = datetime.date(year, 12, 31) else: # 5) Try to match a weekday name in the current week weekdays = { "monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4, "friday": 5, "saturday": 6, "sunday": 7 } for name, iso_num in weekdays.items(): if name in text: # Compute offset from today's ISO weekday to the requested one today_iso = today_utc.isoweekday() # Monday=1 ... Sunday=7 delta_days = iso_num - today_iso target_date = today_utc + datetime.timedelta(days=delta_days) break # 6) If still None, try to parse an explicit date if target_date is None and re.search(r"\d", text): try: parsed_dt = dateutil.parser.parse(text, fuzzy=True) target_date = parsed_dt.date() except (ValueError, OverflowError): target_date = None # If we still don't have a date, return fallback instructions if target_date is None: return ( "Sorry, I couldn’t figure out which day you mean.\n" "Please ask for:\n" " • “today”\n" " • “tomorrow”\n" " • “day after tomorrow”\n" " • “last day of this year”\n" " • a weekday this week (e.g., “Wednesday”)\n" " • or specify an explicit date (e.g., “June 10, 2025”)." ) # ──────────────────────────────────────────────────────────────────── # B) Build UTC‐based timestamps for that entire day: [00:00 → next 00:00) # ──────────────────────────────────────────────────────────────────── start_of_day = datetime.datetime.combine(target_date, datetime.time.min).isoformat() + "Z" end_of_day = ( datetime.datetime.combine(target_date, datetime.time.min) + datetime.timedelta(days=1) ).isoformat() + "Z" # ──────────────────────────────────────────────────────────────────── # C) Query Google Calendar API for events in that window # ──────────────────────────────────────────────────────────────────── events_res = ( self.service.events() .list( calendarId="primary", timeMin=start_of_day, timeMax=end_of_day, singleEvents=True, orderBy="startTime" ) .execute() ) items = events_res.get("items", []) if not items: return f"You have no events scheduled for {target_date.strftime('%B %d, %Y')}." # ──────────────────────────────────────────────────────────────────── # D) Build narrative: “On {date}, first meeting is ‘X’, which will start at {h:mm AM/PM} # and end at {h:mm AM/PM} (Duration: {N hours M minutes}), and second meeting is ….” # ──────────────────────────────────────────────────────────────────── ordinals = [ "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth" ] narrative_parts = [] for idx, ev in enumerate(items): start_raw = ev["start"].get("dateTime") end_raw = ev["end"].get("dateTime") summary = ev.get("summary", "(no title)") if start_raw and end_raw: # Timed event start_dt = datetime.datetime.fromisoformat(start_raw.replace("Z", "+00:00")) end_dt = datetime.datetime.fromisoformat(end_raw.replace("Z", "+00:00")) # Format “H:MM AM/PM” start_str = start_dt.strftime("%I:%M %p").lstrip("0") end_str = end_dt.strftime("%I:%M %p").lstrip("0") # Compute duration duration_ts = end_dt - start_dt total_seconds = int(duration_ts.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 if hours and minutes: duration_str = f"{hours} hours {minutes} minutes" elif hours: duration_str = f"{hours} hours" else: duration_str = f"{minutes} minutes" ordinal = ordinals[idx] if idx < len(ordinals) else f"{idx+1}th" part = ( f"{ordinal} meeting is “{summary},” which will start at {start_str} " f"and end at {end_str} (Duration: {duration_str})" ) else: # All-day event start_date = ev["start"].get("date") ordinal = ordinals[idx] if idx < len(ordinals) else f"{idx+1}th" part = f"{ordinal} event is “{summary},” which is an all-day event on {start_date}" narrative_parts.append(part) # Join all parts with “, and …” joined = ", and ".join(narrative_parts) date_str = target_date.strftime("%B %d, %Y") return f"On {date_str}, {joined}."