Files
B.Tech-Project-III/negot8/backend/features/scheduling.py
2026-04-05 00:43:23 +05:30

149 lines
6.0 KiB
Python

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)