"""Mistral AI — docs models overview (Next.js, server-rendered cards + deprecation table), model pages (embedded pricing JSON), news RSS. Sources (tier 1): * models overview → docs.mistral.ai/models (the old /getting-started/models/models_overview/ URL redirects here): cards `a[href^=/models/]` (name, weights-license badge, description, version) and the "Deprecated & retired models" table (name, version, API id, deprecation / retirement dates, alternative) * model pages → docs.mistral.ai/models/ : context window, "Released as open weights under a … license", features, RSC payload `"pricing":{input:[{price}], output:[{price}]}` in USD per 1M tokens (EUR kept in features), `isRetired` * news → mistral.ai/news/rss (RSS feed linked from the news listing) → ANNOUNCEMENT events mistral.ai/pricing is server-rendered without any per-model price (plans only; model prices load client-side), so it is not crawled: provider prices come from the docs model pages instead. """ from __future__ import annotations import re from datetime import datetime from typing import Any from selectolax.parser import HTMLParser from aiatlas.connectors._identity import family_ref from aiatlas.registry import org_ref, provider_ref from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext from aiatlas.sdk.extract.dates import parse_datetime from aiatlas.sdk.extract.html import node_text from aiatlas.sdk.facts import EntityRef, Facts, Target from aiatlas.sdk.fetch import FetchResult from ._common import ( announcement_events, claim_api_aliases, claim_license, claim_status, json_after, model_ref, next_flight_payload, normalize_capabilities, slug_of, tokens, ) DOCS = "https://docs.mistral.ai" NEWS_RSS = "https://mistral.ai/news/rss" PROVIDER_KEY = "mistral" SLUG_SCHEME = "mistral_docs_slug" ID_SCHEME = "mistral_model_id" MAX_MODEL_PAGES = 50 API_ID = re.compile(r"^[a-z0-9][a-z0-9.\-]*$") OPEN_LICENSES = re.compile(r"apache|mit\b|mrl|research licen[cs]e|open[- ]weight|cc-by|gpl|bsd", re.IGNORECASE) LICENSE_SENTENCE = re.compile(r"(?:released|available|distributed)\s+(?:as\s+)?open[- ]weights?\s+under\s+(?:a\s+|an\s+|the\s+)?(.+?)\s+licen[cs]e", re.IGNORECASE) US_DATE = re.compile(r"\b(\d{1,2})/(\d{1,2})/(\d{4})\b") class MistralConnector(BaseConnector): name = "mistral" label = "Mistral AI — models, model pages (pricing), news" description = "Mistral docs models overview (cards + deprecation table), model pages with embedded pricing, and the Mistral news RSS." source_key = "docs.mistral.ai" version = "1" parser_version = "1" interval_seconds = 3600 min_interval_seconds = 1800 rate_per_min = 12 tier = 1 priority = 0 expected_min_records = 40 concurrency = 2 async def discover(self, ctx: RunContext) -> list[Target]: return [ Target(url=f"{DOCS}/models", doc_type="model_docs", key="models", min_bytes=20000), Target(url=NEWS_RSS, doc_type="feed", key="news", min_bytes=1000), ] async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts: facts = Facts() org = org_ref("mistral") facts.entities.append(org) key = target.key or "" if key == "models" and parsed.html: self._models(facts, org, parsed, res) elif key == "news" and parsed.kind == "feed": announcement_events(facts, org, parsed.feed_items, source_name="mistral.ai/news", max_follow=15) facts.document_title, facts.document_entity = "Mistral AI news", org elif target.doc_type == "model_page" and parsed.html: self._model_page(facts, org, target, res, parsed) return facts # ------------------------------------------------------------------------------------------ models overview def _models(self, facts: Facts, org: EntityRef, parsed: Parsed, res: FetchResult) -> None: observed: datetime = res.fetched_at html = parsed.html assert html facts.document_title, facts.document_entity = "Mistral models overview", org # the SDK's parse_html strips