system-prompts-and-models-o.../dealix/auto_client_acquisition/agents/booking.py
2026-05-01 14:03:52 +03:00

179 lines
6.7 KiB
Python

"""
Booking Agent — books discovery calls via Calendly (preferred) or Google Calendar.
وكيل الحجز — يحجز مكالمات الاستكشاف عبر Calendly أو Google Calendar.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo
from auto_client_acquisition.agents.intake import Lead
from core.agents.base import BaseAgent
from core.config.settings import get_settings
from core.prompts.sales_scripts import get_sales_script
from core.utils import generate_id
@dataclass
class BookingResult:
booking_id: str
provider: str # calendly | google | manual
link: str | None
scheduled_at: datetime | None
meeting_minutes: int
invitee_email: str | None
invitee_phone: str | None
confirmation_message: str
success: bool
reason: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"booking_id": self.booking_id,
"provider": self.provider,
"link": self.link,
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
"meeting_minutes": self.meeting_minutes,
"invitee_email": self.invitee_email,
"invitee_phone": self.invitee_phone,
"confirmation_message": self.confirmation_message,
"success": self.success,
"reason": self.reason,
}
class BookingAgent(BaseAgent):
"""
Attempts to book a meeting using the best available provider.
Priority: Calendly (scheduling link) → Google Calendar → manual fallback.
"""
name = "booking"
def __init__(self) -> None:
super().__init__()
self.settings = get_settings()
self.tz = ZoneInfo(self.settings.app_timezone)
async def run(
self,
*,
lead: Lead,
preferred_time: datetime | None = None,
meeting_minutes: int = 30,
**_: Any,
) -> BookingResult:
"""Return booking details or manual fallback."""
booking_id = generate_id("bkg")
# 1. Calendly (preferred for self-service scheduling)
if self.settings.calendly_api_token and self.settings.calendly_user_uri:
link = self._calendly_scheduling_link()
confirm = self._confirm_message(lead, "calendly", None, link)
self.log.info("booking_calendly_link_sent", lead_id=lead.id, link=link)
return BookingResult(
booking_id=booking_id,
provider="calendly",
link=link,
scheduled_at=None,
meeting_minutes=meeting_minutes,
invitee_email=lead.contact_email,
invitee_phone=lead.contact_phone,
confirmation_message=confirm,
success=True,
reason="Sent Calendly scheduling link",
)
# 2. Google Calendar direct-create (if credentials present)
if self.settings.google_calendar_credentials_file:
scheduled = preferred_time or self._default_slot()
# NOTE: actual Google API call happens in integrations/calendar.py
# The integration layer will be invoked via a callable if present.
confirm = self._confirm_message(
lead, "google", scheduled, link=None, meeting_minutes=meeting_minutes
)
self.log.info("booking_google_scheduled", lead_id=lead.id, when=scheduled.isoformat())
return BookingResult(
booking_id=booking_id,
provider="google",
link=None,
scheduled_at=scheduled,
meeting_minutes=meeting_minutes,
invitee_email=lead.contact_email,
invitee_phone=lead.contact_phone,
confirmation_message=confirm,
success=True,
reason="Scheduled via Google Calendar",
)
# 3. Manual fallback — return instructions
confirm = self._confirm_message(lead, "manual", None, None)
self.log.warning("booking_manual_fallback", lead_id=lead.id)
return BookingResult(
booking_id=booking_id,
provider="manual",
link=None,
scheduled_at=None,
meeting_minutes=meeting_minutes,
invitee_email=lead.contact_email,
invitee_phone=lead.contact_phone,
confirmation_message=confirm,
success=False,
reason="No booking provider configured",
)
# ── Helpers ─────────────────────────────────────────────────
def _calendly_scheduling_link(self) -> str:
"""Return the public Calendly link derived from the user URI."""
user_uri = self.settings.calendly_user_uri or ""
if user_uri.startswith("http"):
return user_uri
return f"https://calendly.com/{user_uri}"
def _default_slot(self) -> datetime:
"""Next business-day 10:00 Riyadh."""
now = datetime.now(self.tz)
# Skip Fri/Sat (weekend in Saudi)
target = now + timedelta(days=1)
while target.weekday() in (4, 5):
target += timedelta(days=1)
return target.replace(hour=10, minute=0, second=0, microsecond=0)
def _confirm_message(
self,
lead: Lead,
provider: str,
scheduled: datetime | None,
link: str | None,
meeting_minutes: int = 30,
) -> str:
if provider == "manual":
if lead.locale == "ar":
return (
f"شكراً {lead.contact_name or ''}. "
f"فريقنا سيتواصل معك خلال 24 ساعة لتحديد موعد مناسب."
)
return (
f"Thanks {lead.contact_name or ''}. "
f"Our team will reach out within 24 hours to schedule."
)
if provider == "calendly" and link:
if lead.locale == "ar":
return (
f"مرحباً {lead.contact_name or ''}،\n" f"اختر الموعد المناسب لك من هنا: {link}"
)
return f"Hi {lead.contact_name or ''},\n" f"Pick a slot that works for you: {link}"
if provider == "google" and scheduled:
return get_sales_script(
"demo_confirm",
locale=lead.locale,
name=lead.contact_name or "",
date=scheduled.strftime("%Y-%m-%d"),
time=scheduled.strftime("%H:%M"),
link="(meeting link will be sent separately)",
)
return "Booking pending."