"""SiteRecipe interface. Each supported site implements one of these; adding a new site is a drop-in module registered in ai/recipes/__init__.py — the core capture loop never changes. """ from __future__ import annotations from abc import ABC, abstractmethod from typing import Any, Optional from core.models import Lead, PageContext, PageMatch class SiteRecipe(ABC): site_id: str = "" display_name: str = "" enabled: bool = True # Which extracted page_type is a capturable target (e.g. a person's profile). target_page_type: str = "profile" @abstractmethod def matches(self, ctx: PageContext) -> Optional[PageMatch]: """Return a PageMatch if this recipe recognises the page, else None.""" @property @abstractmethod def extraction_schema(self) -> dict[str, Any]: """JSON schema (structured-output format) for the raw extraction.""" @property @abstractmethod def extraction_prompt(self) -> str: """Instruction given to the vision model alongside the screenshots.""" @abstractmethod def to_canonical(self, raw: dict[str, Any]) -> Lead: """Map this site's raw extraction to the canonical Lead schema."""