from features.base_feature import BaseFeature from datetime import datetime from urllib.parse import quote as _url_quote class SchedulingFeature(BaseFeature): async def get_context( self, preferences_a: dict, preferences_b: dict, user_a_id: int = None, user_b_id: int = None, ) -> str: """ Compute overlapping time windows. If a user hasn't provided any times in their message AND they have Google Calendar connected, automatically fetch their free slots from the calendar instead of leaving it empty. """ windows_a = self._extract_windows(preferences_a) windows_b = self._extract_windows(preferences_b) # ── Google Calendar fallback: fetch free slots when no times given ── if not windows_a and user_a_id: windows_a = await self._fetch_calendar_slots(user_a_id, tag="A") if not windows_b and user_b_id: windows_b = await self._fetch_calendar_slots(user_b_id, tag="B") overlap_lines = [] if windows_a and windows_b: for wa in windows_a: for wb in windows_b: if wa.lower() == wb.lower(): overlap_lines.append(f" • {wa}") # Simple keyword matching for day/time overlap elif any( word in wa.lower() for word in wb.lower().split() if len(word) > 3 ): overlap_lines.append(f" • {wa} (aligns with {wb})") location_a = preferences_a.get("raw_details", {}).get("location", "") location_b = preferences_b.get("raw_details", {}).get("location", "") lines = ["SCHEDULING DOMAIN RULES:"] lines.append("• Only propose times that appear in BOTH parties' available windows.") lines.append("• Duration is non-negotiable — respect it.") lines.append("• If no overlap exists, escalate immediately with closest alternatives.") if windows_a: lines.append(f"\nPerson A available: {', '.join(windows_a)}") if windows_b: lines.append(f"Person B available: {', '.join(windows_b)}") if overlap_lines: lines.append(f"\nDetected overlapping windows:\n" + "\n".join(overlap_lines)) else: lines.append("\nNo clear overlap detected — propose closest alternatives and offer to adjust.") if location_a or location_b: loc = location_a or location_b lines.append(f"\nMeeting location preference: {loc}") return "\n".join(lines) async def _fetch_calendar_slots( self, user_id: int, tag: str = "" ) -> list[str]: """ Query Google Calendar for the user's free slots over the next 7 days. Returns a list of human-readable strings like "Mon Mar 2 10:00-11:00 AM". Returns [] silently on any error so the negotiation always continues even without calendar access. """ try: from tools.google_calendar import GoogleCalendarTool tool = GoogleCalendarTool() slots = await tool.get_free_slots(user_id) if slots: label = f" (from Google Calendar{' — Person ' + tag if tag else ''})" print(f"[Calendar] Fetched {len(slots)} free slots for user {user_id}") # Attach the source label only to the first entry for readability return [slots[0] + label] + slots[1:] return [] except Exception as e: print(f"[Calendar] Could not fetch slots for user {user_id}: {e}") return [] def _extract_windows(self, preferences: dict) -> list: windows = ( preferences.get("raw_details", {}).get("available_windows") or preferences.get("constraints", []) ) if isinstance(windows, list): return [str(w) for w in windows] if isinstance(windows, str): return [windows] return [] def format_resolution( self, resolution: dict, preferences_a: dict, preferences_b: dict ) -> str: status = resolution.get("status", "resolved") final = resolution.get("final_proposal", {}) details = final.get("details", {}) rounds = resolution.get("rounds_taken", "?") summary = resolution.get("summary", "") if status == "escalated": return ( f"⚠️ *Meeting — Human Decision Needed*\n\n" f"_{summary}_\n\n" f"Our agents couldn't find a perfect time in {rounds} round(s). " f"Please compare calendars directly." ) proposed_time = ( details.get("proposed_datetime") or details.get("date_time") or details.get("time") or final.get("summary", "") ) duration = details.get("duration") or preferences_a.get("raw_details", {}).get("duration", "") location = ( details.get("location") or preferences_a.get("raw_details", {}).get("location") or preferences_b.get("raw_details", {}).get("location") or "TBD" ) meeting_type = details.get("meeting_type") or details.get("type") or "Meeting" reasoning = resolution.get("summary", "") lines = [ "✅ *Meeting Scheduled!*\n", f"📅 *When:* {proposed_time}", ] if duration: lines.append(f"⏱ *Duration:* {duration}") if location and location != "TBD": maps_url = f"https://maps.google.com/?q={_url_quote(str(location))}" lines.append(f"📍 *Location:* [{location}]({maps_url})") else: lines.append(f"📍 *Location:* {location}") lines.append(f"📋 *Type:* {meeting_type}") lines.append(f"\n⏱ Agreed in {rounds} round(s)") if reasoning and reasoning != "Agreement reached": lines.append(f"_{reasoning}_") return "\n".join(lines)