"""LinkedIn profile recipe — the first supported site. Target page: a person's profile (linkedin.com/in/...), typically reached from search. Anything else on LinkedIn (feed, search results, messaging) is not a target; non-LinkedIn pages don't match at all. """ from __future__ import annotations import re from typing import Any, Optional from core.models import Lead, PageContext, PageMatch from .base import SiteRecipe _PROFILE_RE = re.compile(r"linkedin\.com/in/[^/?#]+", re.I) _LINKEDIN_RE = re.compile(r"linkedin\.com", re.I) class LinkedInRecipe(SiteRecipe): site_id = "linkedin" display_name = "LinkedIn" enabled = True target_page_type = "profile" def matches(self, ctx: PageContext) -> Optional[PageMatch]: url = ctx.best_url if url: if _PROFILE_RE.search(url): return PageMatch(self.site_id, "profile", is_target=True) if _LINKEDIN_RE.search(url): return PageMatch(self.site_id, "other", is_target=False) return None # No URL available: fall back to the AI's visual read. if (ctx.ai_site or "").lower() == "linkedin": page_type = (ctx.ai_page_type or "other").lower() return PageMatch(self.site_id, page_type, is_target=(page_type == "profile")) return None @property def extraction_schema(self) -> dict[str, Any]: str_fields = [ "full_name", "headline", "current_title", "current_company", "location", "about", "email", "phone", "website", ] props: dict[str, Any] = {f: {"type": "string"} for f in str_fields} props["other_profiles"] = {"type": "array", "items": {"type": "string"}} props["confidence"] = {"type": "number"} return { "type": "object", "properties": props, "required": str_fields + ["other_profiles", "confidence"], "additionalProperties": False, } @property def extraction_prompt(self) -> str: return ( "These screenshots are a single LinkedIn profile page, captured top " "to bottom. Extract the person's details into the required JSON. " "Rules: use an empty string \"\" for any field not visible; do NOT " "guess or invent contact details. Email, phone and website usually " "only appear if the user opened the 'Contact info' panel — extract " "them only if they are actually shown. 'other_profiles' is any other " "social/web links visible (Twitter/X, GitHub, personal site). " "'confidence' is 0-1 for how legible the profile was." ) def to_canonical(self, raw: dict[str, Any]) -> Lead: socials: dict[str, str] = {} for link in raw.get("other_profiles") or []: low = link.lower() if "github.com" in low: socials["github"] = link elif "twitter.com" in low or "x.com" in low: socials["twitter"] = link else: socials.setdefault("other", link) return Lead( full_name=raw.get("full_name", "") or "", headline=raw.get("headline", "") or "", title=raw.get("current_title", "") or "", company=raw.get("current_company", "") or "", location=raw.get("location", "") or "", about=raw.get("about", "") or "", email=raw.get("email", "") or "", phone=raw.get("phone", "") or "", website=raw.get("website", "") or "", social_handles=socials, source_site=self.site_id, confidence=float(raw.get("confidence", 0) or 0), )