SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
5.1 KB · 116 lines python
Raw Blame History
1"""Meta AI — blog listing (ai.meta.com/blog) → ANNOUNCEMENT events, release posts queued for LLM extraction.23The listing has no RSS (404). It is server-rendered twice: React cards (title link + "Mon D, YYYY" date + category) and a `<noscript>`4list view (h4 title, "Month DD, YYYY" date, "Learn More" link). Both are parsed; items are deduplicated by URL.56llama.com (redirects to developer.meta.com/ai) is a client-rendered Comet app: the HTML contains no model list at all (SSR disabled,7`fail_bad_preloaders`), so no Llama model target is emitted — Llama model facts come from the release posts (LLM) and other connectors.8"""9from __future__ import annotations1011import re1213from selectolax.parser import HTMLParser, Node1415from aiatlas.registry import org_ref16from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext17from aiatlas.sdk.extract.dates import parse_datetime18from aiatlas.sdk.extract.feeds import FeedItem19from aiatlas.sdk.extract.html import node_text20from aiatlas.sdk.facts import Facts, Target21from aiatlas.sdk.fetch import FetchResult2223from ._common import announcement_events2425BLOG = "https://ai.meta.com/blog/"26POST_URL = re.compile(r"^https://ai\.meta\.com/blog/[^/?#]+/?$")27DATE = re.compile(r"\b(January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\.? \d{1,2}, \d{4}\b")28CATEGORIES = ("Research", "Open Source", "Computer Vision", "Product", "Responsible AI", "Developer", "Blog", "FEATURED", "Speech & Audio", "Large Language Models",29              "Generative AI", "Hardware", "Robotics", "Infrastructure")30NOISE = {"", "learn more", "featured", "read more"}313233class MetaAIConnector(BaseConnector):34    name = "meta_ai"35    label = "Meta AI — blog"36    description = "AI at Meta blog listing: announcements and research posts (no feed available; HTML listing parsed)."37    source_key = "ai.meta.com"38    version = "1"39    parser_version = "1"40    interval_seconds = 360041    min_interval_seconds = 180042    rate_per_min = 1043    tier = 144    priority = 145    expected_min_records = 146    concurrency = 14748    async def discover(self, ctx: RunContext) -> list[Target]:49        return [Target(url=BLOG, doc_type="listing", key="blog", min_bytes=5000)]5051    async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:52        facts = Facts()53        org = org_ref("meta-ai")54        facts.entities.append(org)55        if (target.key or "") == "blog" and parsed.html:56            # fresh parse: the SDK's parse_html decomposes <noscript> (where the server-rendered list view lives) from the tree it keeps57            items = listing_items(HTMLParser(res.content).body)58            announcement_events(facts, org, items, source_name="ai.meta.com/blog", max_follow=15)59            facts.document_title, facts.document_entity = "AI at Meta blog", org60        return facts616263def listing_items(root: Node | None) -> list[FeedItem]:64    if root is None:65        return []66    by_url: dict[str, FeedItem] = {}67    for a in root.css("a[href]"):68        href = (a.attributes.get("href") or "").strip()69        if not POST_URL.match(href):70            continue71        url = href if href.endswith("/") else href + "/"72        title = node_text(a)73        aria = a.attributes.get("aria-label") or ""74        if title.lower() in NOISE or len(title) < 8:75            title = re.sub(r"^Read\s+", "", aria).strip() if aria else ""76        date = None77        category = None78        node: Node | None = a79        # climb to the smallest container that still describes this single post (stop before a node holding other posts)80        for _ in range(8):81            node = node.parent if node else None82            if node is None or node.tag in ("body", "html"):83                break84            others = {(x.attributes.get("href") or "").rstrip("/") for x in node.css("a[href]") if POST_URL.match(x.attributes.get("href") or "")}85            if len(others) > 1:86                break87            text = node_text(node)88            if date is None:89                m = DATE.search(text)90                if m:91                    date = parse_datetime(m.group(0))92            if not title:93                h = node.css_first("h4, h3, h2")94                title = node_text(h) if h else ""95            if category is None:96                for c in CATEGORIES:97                    if c not in ("FEATURED", "Blog") and re.search(rf"(^|\n| ){re.escape(c)}(\n| |$)", text):98                        category = c99                        break100            if date is not None and title:101                break102        if not title:103            continue104        item = by_url.get(url)105        if item is None:106            by_url[url] = FeedItem(id=url, url=url, title=title[:300], summary=None, published_at=date, updated_at=None, categories=[category] if category else [])107        else:108            if item.published_at is None and date:109                item.published_at = date110            if not item.categories and category:111                item.categories = [category]112    return sorted(by_url.values(), key=lambda it: it.published_at.timestamp() if it.published_at else 0, reverse=True)113114115CONNECTORS = [MetaAIConnector]116