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%
12.9 KB · 203 lines python
Raw Blame History
1"""/changes (cursor feed) · /changes/daily ("Today in AI 2.0") · /changes/categories.23API 1.1: feeds default to `is_backfill = false` and are keyed on `occurred_at` (= coalesce(effective_at, observed_at)); `include_backfill=1`4restores the historical corpus, `date_field=observed` restores the v1 ordering. `/changes/daily` groups the events that share a `group_key`5(one release across several documents) into one item with `sources: n` and `documents: [urls]`."""6from __future__ import annotations78from datetime import UTC, datetime9from typing import Any1011from fastapi import APIRouter, Query, Request1213from aiatlas.api.common import (14    ENTITY_COLS,15    ENTITY_FROM,16    EVENT_COLS,17    EVENT_FROM,18    OPEN_CATEGORIES,19    ApiError,20    cached,21    change_event,22    csv,23    day_bounds,24    entity_summary,25    event_type_label,26    parse_date,27    parse_ts,28    resolve_id,29)30from aiatlas.db import connection, fetch_all, fetch_val3132router = APIRouter(prefix="/api/v1/changes", tags=["changes"])3334CATEGORY_LABELS = {"model": "Models", "price": "Pricing", "benchmark": "Benchmarks", "paper": "Research", "release": "Releases", "company": "Companies & labs",35                   "provider": "Providers", "hardware": "Hardware", "framework": "Frameworks", "dataset": "Datasets", "regulation": "Regulation",36                   "incident": "Incidents", "repository": "Repositories", "tool": "Tools", "source": "Sources & curation", "update": "Updates"}37CATEGORY_ORDER = list(CATEGORY_LABELS)38TOTAL_CAP = 10_0003940# "Today in AI 2.0" sections — (key, label, SQL predicate on ev/e)41SECTIONS: list[tuple[str, str, str]] = [42    ("MAJOR_RELEASES", "Major releases", "ev.event_type in ('NEW_MODEL','RELEASE') and ev.importance >= 2 and e.entity_type = 'model'"),43    ("OPEN_WEIGHT_RELEASES", "Open-weight releases", "ev.event_type = 'NEW_MODEL' and e.entity_type = 'model' and e.attributes->>'openness' in (" + ", ".join(f"'{c}'" for c in OPEN_CATEGORIES) + ")"),44    ("PRICE_MOVES", "Price moves", "ev.category = 'price' or ev.event_type = 'PRICE_CHANGED'"),45    ("BENCHMARK_MOVES", "Benchmark moves", "ev.event_type in ('BENCHMARK_UPDATED','BENCHMARK_LEADER_CHANGED','NEW_BENCHMARK_LEADER')"),46    ("MODEL_CHANGES", "Model changes", "ev.event_type in ('CONTEXT_CHANGED','MAX_OUTPUT_CHANGED','STATUS_CHANGED','CAPABILITIES_CHANGED','PARAMETERS_CHANGED','KNOWLEDGE_CUTOFF_CHANGED','LICENSE_CHANGED','OPENNESS_CHANGED')"),47    ("RESEARCH", "Research", "ev.event_type = 'NEW_PAPER'"),48    ("DEPRECATIONS", "Deprecations & retirements", "ev.event_type in ('DEPRECATION_ANNOUNCED','RETIREMENT_ANNOUNCED') or (ev.event_type = 'STATUS_CHANGED' and ev.new_value::text ~* 'deprecated|retired')"),49    ("PROVIDER_CHANGES", "Provider listings", "ev.event_type in ('PROVIDER_LISTED','PROVIDER_DELISTED','NEW_PROVIDER')"),50    ("HARDWARE", "Hardware", "ev.category = 'hardware' or ev.event_type = 'NEW_HARDWARE'"),51]525354def _filters(*, category: str | None, types: list[str], entity_type: str | None, importance_min: int | None, since: datetime | None, until: datetime | None,55             q: str | None, include_documents: bool, entity_id: str | None = None, before: datetime | None = None, include_backfill: bool = False,56             date_field: str = "occurred") -> tuple[list[str], dict[str, Any]]:57    col = "ev.observed_at" if date_field == "observed" else "ev.occurred_at"58    where: list[str] = []59    p: dict[str, Any] = {}60    if not include_backfill:61        where.append("ev.is_backfill = false")62    if category:63        where.append("ev.category = any(cast(:cats as text[]))")64        p["cats"] = csv(category)65    if types:66        where.append("ev.event_type = any(cast(:types as text[]))")67        p["types"] = [t.upper() for t in types]68    elif not include_documents:69        where.append("ev.event_type <> 'DOCUMENT_CHANGED'")70    if entity_type:71        where.append("e.entity_type = any(cast(:etypes as text[]))")72        p["etypes"] = csv(entity_type)73    if importance_min is not None:74        where.append("ev.importance >= :imp")75        p["imp"] = importance_min76    if since is not None:77        where.append(f"{col} >= :since")78        p["since"] = since79    if until is not None:80        where.append(f"{col} <= :until")81        p["until"] = until82    if before is not None:83        where.append(f"{col} < :before")84        p["before"] = before85    if q:86        where.append("(ev.summary ilike :qlike or e.canonical_name ilike :qlike)")87        p["qlike"] = f"%{q}%"88    if entity_id:89        where.append("ev.entity_id = :eid")90        p["eid"] = entity_id91    return where or ["true"], p929394def _event(r: dict[str, Any]) -> dict[str, Any]:95    ev = change_event(r)96    ev["occurred_at"] = r.get("occurred_at")97    ev["is_backfill"] = r.get("is_backfill")98    ev["group_key"] = r.get("group_key")99    return ev100101102@router.get("")103@cached(60)104async def list_changes(request: Request, category: str | None = None, type: str | None = Query(None, alias="type"), entity_type: str | None = None,105                       importance_min: int | None = Query(None, ge=0, le=3), since: str | None = None, until: str | None = None, q: str | None = Query(None, max_length=200),106                       entity: str | None = None, limit: int = Query(50, ge=1, le=200), before: str | None = None, offset: int = Query(0, ge=0, le=10000),107                       include_documents: int = Query(0, ge=0, le=1), include_backfill: int = Query(0, ge=0, le=1),108                       date_field: str = Query("occurred", pattern="^(occurred|observed)$")) -> dict[str, Any]:109    before_ts, since_ts, until_ts = parse_ts(before, "before"), parse_ts(since, "since"), parse_ts(until, "until")110    col = "ev.observed_at" if date_field == "observed" else "ev.occurred_at"111    async with connection() as conn:112        eid = await resolve_id(conn, entity) if entity else None113        where, params = _filters(category=category, types=csv(type), entity_type=entity_type, importance_min=importance_min, since=since_ts, until=until_ts, q=q,114                                 include_documents=bool(include_documents), entity_id=eid, before=before_ts, include_backfill=bool(include_backfill), date_field=date_field)115        where_sql = " and ".join(where)116        rows = await fetch_all(conn, f"select {EVENT_COLS}, ev.occurred_at, ev.is_backfill, ev.group_key from {EVENT_FROM} where {where_sql} order by {col} desc, ev.id desc limit :lim offset :off",117                               lim=limit, off=offset, **params)118        total = await fetch_val(conn, f"select count(*) from (select 1 from {EVENT_FROM} where {where_sql} limit {TOTAL_CAP}) t", **params)119    items = [_event(r) for r in rows]120    cursor_key = "observed_at" if date_field == "observed" else "occurred_at"121    return {"items": items, "total": int(total or 0), "limit": limit, "offset": offset, "next_before": items[-1][cursor_key] if len(items) == limit else None,122            "date_field": date_field, "include_backfill": bool(include_backfill)}123124125def _group(items: list[dict[str, Any]]) -> list[dict[str, Any]]:126    """Fold events that share a `group_key` (one release across documents) into one item: the most important event + sources/documents."""127    out: list[dict[str, Any]] = []128    by_key: dict[str, dict[str, Any]] = {}129    for ev in items:130        k = ev.get("group_key")131        if not k:132            out.append({**ev, "sources": 1, "documents": [ev["source_url"]] if ev.get("source_url") else [], "grouped_events": 1})133            continue134        g = by_key.get(k)135        if g is None:136            g = by_key[k] = {**ev, "sources": 0, "documents": [], "grouped_events": 0, "event_ids": []}137            out.append(g)138        g["grouped_events"] += 1139        g["event_ids"].append(ev["id"])140        if ev.get("source_url") and ev["source_url"] not in g["documents"]:141            g["documents"].append(ev["source_url"])142        if (ev.get("importance") or 0) > (g.get("importance") or 0):143            for f in ("id", "event_type", "summary", "importance", "new_value", "old_value", "property", "entity"):144                g[f] = ev.get(f)145    for g in out:146        g["sources"] = max(1, len(g["documents"]))147    return out148149150@router.get("/daily")151@cached(120)152async def changes_daily(request: Request, date: str | None = None, per_section: int = Query(30, ge=1, le=100), include_backfill: int = Query(0, ge=0, le=1),153                        date_field: str = Query("occurred", pattern="^(occurred|observed)$")) -> dict[str, Any]:154    d = parse_date(date, "date") or datetime.now(UTC).date()155    start, end = day_bounds(d)156    col = "ev.observed_at" if date_field == "observed" else "ev.occurred_at"157    bf = "" if include_backfill else " and ev.is_backfill = false"158    base = f"{col} >= :s and {col} <= :e and ev.event_type <> 'DOCUMENT_CHANGED'{bf}"159    async with connection() as conn:160        rows = await fetch_all(conn, f"""select {EVENT_COLS}, ev.occurred_at, ev.is_backfill, ev.group_key from (161                                            select ev.*, row_number() over (partition by ev.category order by ev.importance desc, ev.occurred_at desc) as rn162                                            from change_events ev where {base}) ev163                                        left join entities e on e.id = ev.entity_id left join entities eo on eo.id = e.organization_id164                                        where ev.rn <= :n order by ev.category, ev.rn""", s=start, e=end, n=per_section)165        counts = await fetch_all(conn, f"select ev.category, count(*) as n from change_events ev where {base} group by 1", s=start, e=end)166        backfill_excluded = 0 if include_backfill else int(await fetch_val(conn, f"select count(*) from change_events ev where {col} >= :s and {col} <= :e and ev.is_backfill and ev.event_type <> 'DOCUMENT_CHANGED'", s=start, e=end) or 0)167        new_models = await fetch_all(conn, f"select {ENTITY_COLS} from {ENTITY_FROM} where e.entity_type = 'model' and e.merged_into is null and e.first_seen_at >= :s and e.first_seen_at <= :e "168                                           f"order by e.first_seen_at desc limit 100", s=start, e=end)169        prev = await fetch_val(conn, f"select max(ev.occurred_at)::date from change_events ev where ev.occurred_at < :s and ev.event_type <> 'DOCUMENT_CHANGED'{bf}", s=start)170        nxt = await fetch_val(conn, f"select min(ev.occurred_at)::date from change_events ev where ev.occurred_at > :e and ev.event_type <> 'DOCUMENT_CHANGED'{bf}", e=end)171        sections2: list[dict[str, Any]] = []172        for key, label, pred in SECTIONS:173            srows = await fetch_all(conn, f"""select {EVENT_COLS}, ev.occurred_at, ev.is_backfill, ev.group_key from {EVENT_FROM} where {base} and ({pred})174                                              order by ev.importance desc, ev.occurred_at desc, ev.id limit :n""", s=start, e=end, n=per_section * 3)175            items = _group([_event(r) for r in srows])[:per_section]176            total_s = await fetch_val(conn, f"select count(*) from {EVENT_FROM} where {base} and ({pred})", s=start, e=end)177            if items:178                sections2.append({"key": key, "label": label, "items": items, "total": int(total_s or 0)})179    by_cat: dict[str, list[dict[str, Any]]] = {}180    for r in rows:181        by_cat.setdefault(r["category"], []).append(_event(r))182    order = {c: i for i, c in enumerate(CATEGORY_ORDER)}183    sections = [{"category": c, "label": CATEGORY_LABELS.get(c, c.title()), "items": items} for c, items in sorted(by_cat.items(), key=lambda kv: (order.get(kv[0], 99), kv[0]))]184    return {"date": d.isoformat(), "counts": {r["category"]: int(r["n"]) for r in counts}, "total": sum(int(r["n"]) for r in counts), "sections": sections,185            "today": sections2, "new_models": [entity_summary(r) for r in new_models], "labels": CATEGORY_LABELS, "backfill_excluded": backfill_excluded,186            "date_field": date_field, "previous_day": prev.isoformat() if prev else None, "next_day": nxt.isoformat() if nxt else None,187            "note": "Events that occurred on this UTC day (effective date when known, else observation date), excluding back-filled history and source-document "188                    "changes. `today` groups events sharing a group_key (one release seen in several documents)."}189190191@router.get("/categories")192@cached(300)193async def changes_categories(request: Request, days: int = Query(7, ge=1, le=365), include_backfill: int = Query(0, ge=0, le=1)) -> dict[str, Any]:194    bf = "" if include_backfill else " and is_backfill = false"195    async with connection() as conn:196        rows = await fetch_all(conn, f"""select category, event_type, count(*) as count from change_events where occurred_at > now() - make_interval(days => :d)197                                        and event_type <> 'DOCUMENT_CHANGED'{bf} group by 1, 2 order by 3 desc, 1, 2""", d=days)198    return {"days": days, "items": [{"category": r["category"], "label": CATEGORY_LABELS.get(r["category"], r["category"].title()), "event_type": r["event_type"],199                                     "event_label": event_type_label(r["event_type"]), "count": int(r["count"])} for r in rows]}200201202__all__ = ["CATEGORY_LABELS", "SECTIONS", "ApiError", "router"]203