Files
bhushanct c1eef4c6aa Initial commit: Screen Leads app
Screen-capture lead tool: capture agent, Claude vision extractor with
pluggable site recipes (LinkedIn), canonical funnel model, SQLite storage,
FastAPI backend, dashboard, and setup/run scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 07:43:33 +05:30

92 lines
3.6 KiB
Python

"""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),
)