"""SchedulingProvider interface (ยง8) and per-tenant provider resolution. Adding a third provider = one new module registering itself here. No changes to the agent, tools, or proposal engine are needed. """ from __future__ import annotations from datetime import date from typing import Protocol, runtime_checkable from sqlalchemy.ext.asyncio import AsyncSession from gogo.domain import BookingRequestData, DeliveryResult, ServiceInfo, Slot from gogo.models import ProviderConfig, Tenant @runtime_checkable class SchedulingProvider(Protocol): """Availability lookup + proposal delivery for one tenant.""" async def get_services(self) -> list[ServiceInfo] | None: """Optional catalog sync; None = provider has no catalog.""" ... async def get_availability( self, service_id: str, date_from: date, date_to: date, home_visit: bool = False ) -> list[Slot]: """Ready-to-offer free slots. The agent picks up to 3.""" ... async def deliver_request(self, booking_request: BookingRequestData) -> DeliveryResult: """Deliver the proposal to the owner (email / partner push). Outcome is signaled back via webhook/polling (partner) or the owner's email/dashboard actions (google_calendar, mock). """ ... _REGISTRY: dict[str, type] = {} def register_provider(name: str): def deco(cls): _REGISTRY[name] = cls return cls return deco async def get_provider(session: AsyncSession, tenant: Tenant) -> SchedulingProvider: """Instantiate the tenant's configured provider.""" # Imports here to avoid circulars; modules self-register on import. from gogo.scheduling import google_calendar, mock, partner_api # noqa: F401 cls = _REGISTRY.get(tenant.scheduling_provider) if cls is None: raise ValueError(f"Unknown scheduling provider: {tenant.scheduling_provider}") config = await _load_config(session, tenant) return cls(session=session, tenant=tenant, config=config) async def _load_config(session: AsyncSession, tenant: Tenant) -> dict: from sqlalchemy import select row = ( await session.execute( select(ProviderConfig).where(ProviderConfig.tenant_id == tenant.id) ) ).scalar_one_or_none() return row.config if row else {}