SPB Git

spb/ai-atlas

Public
7.0 KB · 116 lines python
1"""/methodology · /trending · POST /views · /sitemap · /api-keys/me."""2from __future__ import annotations34import hashlib5from datetime import UTC, datetime6from typing import Any78from fastapi import APIRouter, Depends, Query, Request9from pydantic import BaseModel, Field1011from aiatlas.api.common import (12    ENTITY_COLS,13    EVENT_TYPE_LABELS,14    STATUS_VOCAB,15    ApiError,16    cached,17    entity_summary,18    event_type_importance,19    event_type_label,20    rate_limit,21)22from aiatlas.db import connection, execute, fetch_all, fetch_one, transaction23from aiatlas.ids import ENTITY_TYPES24from aiatlas.services.quality import EXPECTED_FIELDS, QUALITY_VERSION2526router = APIRouter(prefix="/api/v1", tags=["misc"])2728CONFIDENCE_LEVELS = [29    {"key": "verified", "label": "Verified", "description": "Confirmed by at least two independent sources, one of them tier 1."},30    {"key": "high", "label": "High", "description": "Stated explicitly by an official (tier 1) source and extracted deterministically."},31    {"key": "medium", "label": "Medium", "description": "Quality secondary source, curated registry, or LLM extraction from an official document."},32    {"key": "low", "label": "Low", "description": "Community or unverified source, or LLM extraction from a secondary document."},33    {"key": "conflicted", "label": "Conflicted", "description": "Another source states a different value; both claims are kept and flagged, never averaged."},34]35TIERS = [36    {"tier": 1, "label": "Official / primary", "description": "The organization's own site, docs, pricing pages, model cards, filings."},37    {"tier": 2, "label": "Quality secondary", "description": "Peer-reviewed venues, arXiv, curated registries, major leaderboards."},38    {"tier": 3, "label": "Community", "description": "Community-maintained hubs, forums, wikis."},39    {"tier": 4, "label": "Unverified", "description": "Anything else; never overrides a better tier."},40]41EXTRACTORS = [42    {"key": "deterministic", "description": "Rule-based parsers (tables, JSON-LD, meta tags, embedded JSON, regex) — always runs first."},43    {"key": "curated", "description": "Hand-maintained registries shipped with the code (organizations, providers, benchmarks, hardware)."},44    {"key": "llm", "description": "Local LLM extraction validated against a strict JSON schema; medium/low confidence, never overrides deterministic tier-1 claims."},45]464748@router.get("/methodology")49@cached(600)50async def methodology(request: Request) -> dict[str, Any]:51    async with connection() as conn:52        metrics = await fetch_all(conn, "select key, label, version, description, formula from metric_definitions order by key")53        event_types = await fetch_all(conn, "select event_type, category, count(*) as count, max(observed_at) as last_seen_at from change_events group by 1, 2 order by 3 desc")54    seen = {r["event_type"] for r in event_types}55    types = [{**r, "count": int(r["count"]), "label": event_type_label(r["event_type"]), "importance": event_type_importance(r["event_type"])} for r in event_types]56    types += [{"event_type": t, "category": None, "count": 0, "last_seen_at": None, "label": lbl, "importance": imp}57              for t, (lbl, imp) in EVENT_TYPE_LABELS.items() if t not in seen]58    return {"metrics": metrics, "quality_version": QUALITY_VERSION, "expected_fields": EXPECTED_FIELDS, "confidence_levels": CONFIDENCE_LEVELS, "tiers": TIERS,59            "event_types": types, "status_vocabulary": list(STATUS_VOCAB), "extractors": EXTRACTORS,60            "principles": ["Never fabricate: missing data is reported as unavailable.", "Every fact carries provenance (source, snapshot, URL, tier, confidence, extractor).",61                           "History is append-only: claims, prices and benchmark results are never overwritten.",62                           "Conflicts between sources are stored side by side and flagged for review.", "Live counters and feeds are computed from the database."]}636465@router.get("/trending")66@cached(300)67async def trending(request: Request, days: int = Query(7, ge=1, le=90), limit: int = Query(12, ge=1, le=60), type: str | None = Query(None, alias="type")) -> dict[str, Any]:68    async with connection() as conn:69        rows = await fetch_all(conn, f"""70            with v as (select split_part(regexp_replace(path, '[?#].*$', ''), '/', 3) as slug, sum(views) as views from page_views71                       where day >= ((now() at time zone 'UTC') - make_interval(days => :d))::date and path ~ '^/[a-z-]+/[^/?#]+' group by 1)72            select v.views, {ENTITY_COLS} from v join entities e on e.slug = v.slug left join entities eo on eo.id = e.organization_id73            where e.merged_into is null {"and e.entity_type = :t" if type else ""} order by v.views desc, e.updated_at desc limit :lim""", d=days, lim=limit, t=type)74    return {"days": days, "items": [{**(entity_summary(r) or {}), "views": int(r["views"] or 0)} for r in rows]}757677class ViewBeacon(BaseModel):78    path: str = Field(..., min_length=1, max_length=300)798081@router.post("/views", dependencies=[Depends(rate_limit("views"))])82async def record_view(body: ViewBeacon) -> dict[str, Any]:83    path = body.path.strip()84    if not path.startswith("/") or "\n" in path or "//" in path:85        raise ApiError(400, "path must be a site-relative path")86    path = path.split("?", 1)[0].split("#", 1)[0][:300]87    async with transaction() as conn:88        await execute(conn, "insert into page_views (path, day, views) values (:p, :d, 1) on conflict (path, day) do update set views = page_views.views + 1",89                      p=path, d=datetime.now(UTC).date())90    return {"ok": True}919293@router.get("/sitemap")94@cached(600)95async def sitemap(request: Request, type: str | None = Query(None, alias="type"), limit: int = Query(5000, ge=1, le=5000), offset: int = Query(0, ge=0)) -> dict[str, Any]:96    if type and type not in ENTITY_TYPES:97        raise ApiError(400, f"unknown entity type {type!r}")98    where = "e.merged_into is null" + (" and e.entity_type = :t" if type else "")99    async with connection() as conn:100        rows = await fetch_all(conn, f"select e.slug, e.entity_type, e.updated_at from entities e where {where} order by e.updated_at desc, e.id limit :lim offset :off", t=type, lim=limit, off=offset)101        total = await fetch_one(conn, f"select count(*) as n from entities e where {where}", t=type)102    return {"items": rows, "total": int(total["n"]) if total else 0, "limit": limit, "offset": offset}103104105@router.get("/api-keys/me")106async def api_key_me(request: Request) -> dict[str, Any]:107    key = request.headers.get("x-api-key") or ""108    if not key:109        raise ApiError(401, "x-api-key header required")110    digest = hashlib.sha256(key.encode()).hexdigest()111    async with transaction() as conn:112        row = await fetch_one(conn, "update api_keys set last_used_at = now(), usage_count = usage_count + 1 where key_hash = :h and enabled returning label, plan, rate_per_min, usage_count, created_at, last_used_at", h=digest)113    if not row:114        raise ApiError(401, "unknown or disabled API key")115    return row116
Files · c509d7e03d5aed8bc61fb24bc1c8bb3de1f4b9e2
Menu

Theme