mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
148
negot8/backend/features/scheduling.py
Normal file
148
negot8/backend/features/scheduling.py
Normal file
@@ -0,0 +1,148 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user