SPB Git

spb/ai-atlas

Public

AI Atlas foundation: schema, connector SDK, archive, temporal writer, LLM gateway, scheduler, registry, CLI, Anthropic connector

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 11, 2026) Browse files

72 changed files +8,918 −0

72 changed files
added .env.example +25 −0
@@ -0,0 +1,25 @@
1+# AI Atlas — local development. Copy to .env (never commit .env).
2+APP_ENV=development
3+AIA_SITE_URL=http://localhost:8320
4+DATABASE_URL=postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas
5+REDIS_URL=redis://127.0.0.1:6379/5
6+AIA_DATA_DIR=./data
7+AIA_API_HOST=127.0.0.1
8+AIA_API_PORT=8321
9+AIA_ADMIN_TOKEN=dev-admin-token
10+AIA_LOG_JSON=0
11+# Crawler identity (robots.txt is honoured; contact address is public)
12+AIA_USER_AGENT="AIAtlasBot/0.1 (+https://www.ai-atlas.co/bot; contact@spboucher.ai)"
13+# Local LLM factory (OpenAI-compatible; MacLustr llm-api.io). Optional — deterministic extraction works without it.
14+AIA_LLM_BASE_URL=https://www.llm-api.io/v1
15+AIA_LLM_API_KEY=
16+AIA_LLM_SMALL_MODEL=qwen3-4b-instruct-2507-4bit
17+AIA_LLM_MEDIUM_MODEL=qwen3.6-35b-a3b-4bit
18+AIA_LLM_LARGE_MODEL=qwen3.8-27b-4bit
19+AIA_EMBEDDING_MODEL=qwen3-embedding-0.6b-4bit
20+# Optional escalation transports (never required)
21+SCRAPFLY_API_KEY=
22+FIRECRAWL_API_KEY=
23+# Web
24+API_URL=http://127.0.0.1:8321
25+NEXT_PUBLIC_SITE_URL=http://localhost:8320
added .gitignore +22 −0
@@ -0,0 +1,22 @@
1+.env
2+.env.*
3+!.env.example
4+.venv/
5+__pycache__/
6+*.pyc
7+/data/
8+node_modules/
9+.next/
10+tmp/
11+.DS_Store
12+deploy/.admin-token
13+deploy/.llm-key
14+deploy/rendered/
15+*.tsbuildinfo
16+apps/web/next-env.d.ts
17+apps/web/qa/screens/
18+apps/web/AGENTS.md
19+apps/web/CLAUDE.md
20+.pytest_cache/
21+.ruff_cache/
22+*.egg-info/
added CLAUDE.md +52 −0
@@ -0,0 +1,52 @@
1+# AI Atlas — project guide (condensed from the founding spec; the full architecture is in docs/ARCHITECTURE.md)
2+
3+**Mission**: the most comprehensive, structured, searchable and continuously updated map of the global AI ecosystem — Bloomberg × Wikipedia ×
4+Crunchbase × Hugging Face × Papers With Code, purpose-built for AI. Not a tools directory, not a news aggregator, not an API wrapper.
5+**The dataset is the product; the website and the API are interfaces to it.**
6+
7+## Non-negotiables
8+
9+1. No critical dependence on external data APIs — direct crawling first; Scrapfly/Firecrawl/browser are optional escalation only.
10+2. Store provenance (source, snapshot, URL, tier, confidence, extractor) on every fact; store history (temporal claims, append-only prices/results, raw snapshots).
11+3. Local LLM (MacLustr llm-api.io via the gateway) for large-scale extraction; deterministic extraction always runs first.
12+4. **Never fabricate data.** Missing → "Unavailable". Conflicts → both claims stored and flagged; never a compromise value.
13+5. Primary sources first (tier 1 official > 2 quality secondary > 3 community > 4 unverified). Connectors are owned IP with fixture tests.
14+6. Live counters, stats, feeds and timelines come from the database — never hardcoded.
15+7. Public information stays public (SEO); login only for watchlists/alerts/API keys later.
16+8. Respectful crawling: robots.txt, per-domain rate limits, conditional requests, identified UA, no bypassing access controls, no private data.
17+
18+## Stack
19+
20+- Backend `src/aiatlas/` — Python 3.12, FastAPI, SQLAlchemy Core + asyncpg, Alembic (SQL, forward-only), Redis, APScheduler, selectolax, feedparser, pydantic.
21+ CLI `aia` (`.venv/bin/aia --help`). Connector SDK in `sdk/` (docs/CONNECTORS.md). API contract in docs/API.md.
22+- Database: Postgres 17 + pgvector + pg_trgm. Internal ids = prefixed ULIDs (`model_…`); slugs stable and unique.
23+- Frontend `apps/web/` — Next.js 16 (App Router, React 19, TypeScript strict, Tailwind v4). Guide: docs/FRONTEND.md. Dev on :8320, API on :8321.
24+- Data outside the repo: `AIA_DATA_DIR` (`./data` in dev, `~/ai-atlas-data` in prod): raw/, text/, backups/, logs/, cache/, seed/.
25+
26+## Local development
27+
28+```bash
29+uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python -e ".[dev]"
30+cp .env.example .env # local Postgres `aiatlas` (role aiatlas/aiatlas), Redis db 5
31+.venv/bin/aia migrate && .venv/bin/aia seed
32+.venv/bin/aia run anthropic # one connector, live · `--file key=path` uses a fixture · `aia crawl --priority 0` runs all P0 connectors
33+.venv/bin/aia api --reload # http://127.0.0.1:8321/api/v1/docs
34+pnpm install && pnpm dev:web # http://localhost:8320
35+.venv/bin/pytest -q # fixtures only; `-m live` hits the network
36+```
37+
38+## Production (MacLustr)
39+
40+Node **M2M32c** (dedicated), deployed by `mld` — manifest `deploy/ai-atlas.mld.json` → `M1M32:~/dispatch/apps/ai-atlas.json`. PM2 processes
41+`ai-atlas-api` (uvicorn 127.0.0.1:8321), `ai-atlas-scheduler` (`aia schedule`), `ai-atlas-web` (next start :8320). Public route through the
42+MacLustr Tunnel: `https://www.ai-atlas.co → M2M32c:8320`. See docs/DEPLOY.md. Secrets live only in the rendered manifest (`deploy/.admin-token`,
43+`deploy/.llm-key`, git-ignored). LLM factory: `AIA_LLM_BASE_URL=https://www.llm-api.io/v1` (private MacLustr server on M1M64).
44+
45+## Conventions
46+
47+- Properties and relations use the shared vocabulary in docs/CONNECTORS.md; metrics use the `metric.` prefix (no events).
48+- Every connector: `source_key` registered in `registry/sources.yaml`, organization in `registry/organizations.yaml`, fixture test in `tests/`.
49+- Parser improved? bump `parser_version` and `aia reprocess <connector>` — never re-crawl for a code change.
50+- Frontend: mobile-first (390/430/768/1440), DOM order = visual order, ≥ 44 px targets, no horizontal overflow, dark + light, premium
51+ institutional design (no generic SaaS cards). Every page: real data, loading + error + empty states, source attribution, `generateMetadata`.
52+- Units: tokens, USD per 1M tokens, GB, GB/s, W, ISO dates (UTC). Openness vocabulary: open-weights | open-source | proprietary | restricted.
added README.md +39 −0
@@ -0,0 +1,39 @@
1+# AI Atlas
2+
3+**www.ai-atlas.co — Explore the entire AI ecosystem.**
4+
5+AI Atlas is a continuously updated, historically versioned, machine-readable map of the global artificial intelligence ecosystem: models,
6+companies, research, providers and prices, benchmarks, hardware, frameworks, datasets, tools — connected in one temporal knowledge graph,
7+with provenance on every fact.
8+
9+- **First-party connectors** read official pages, docs, feeds, model cards and public files directly. No commercial data API is required.
10+- **Raw historical archive**: every changed page is stored (content-addressed, compressed) and can be reprocessed when parsers improve.
11+- **Temporal knowledge graph**: claims carry `valid_from`/`valid_to`, prices and benchmark results are append-only, every change is an event.
12+- **Local LLM factory**: large-scale extraction runs on MacLustr's own inference servers through an engine-agnostic gateway.
13+- **Interfaces**: the website, the public API (`/api/v1`), and — later — datasets and alerts.
14+
15+## Layout
16+
17+```
18+registry/ curated YAML (sources, organizations, providers, benchmarks, hardware) — every entry cites its source
19+src/aiatlas/ Python platform: sdk/ (connector SDK), connectors/, services/ (jobs, scheduler, LLM gateway, search, quality), api/, cli.py
20+migrations/ forward-only SQL migrations
21+apps/web/ Next.js 16 website
22+tests/ pytest with saved fixtures
23+deploy/ MacLustr (mld) manifest and scripts
24+docs/ ARCHITECTURE.md · CONNECTORS.md · API.md · FRONTEND.md · DEPLOY.md
25+```
26+
27+## Quick start
28+
29+```bash
30+uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python -e ".[dev]"
31+cp .env.example .env && createdb -O aiatlas aiatlas
32+.venv/bin/aia migrate && .venv/bin/aia seed
33+.venv/bin/aia crawl --priority 0 # initial corpus: labs, Hugging Face, arXiv, providers, benchmarks…
34+.venv/bin/aia api # http://127.0.0.1:8321/api/v1/docs
35+pnpm install && pnpm dev:web # http://localhost:8320
36+```
37+
38+© 2026 Simon-Pierre Boucher · contact@spboucher.ai · hosted on [MacLustr](https://www.maclustr.io). AI Atlas links to original sources and
39+never republishes documents in full; extracted facts are provided as-is with their provenance.
added alembic.ini +31 −0
@@ -0,0 +1,31 @@
1+[alembic]
2+script_location = migrations
3+prepend_sys_path = src
4+path_separator = os
5+
6+[loggers]
7+keys = root,alembic
8+
9+[handlers]
10+keys = console
11+
12+[formatters]
13+keys = generic
14+
15+[logger_root]
16+level = WARN
17+handlers = console
18+
19+[logger_alembic]
20+level = INFO
21+handlers =
22+qualname = alembic
23+
24+[handler_console]
25+class = StreamHandler
26+args = (sys.stderr,)
27+level = NOTSET
28+formatter = generic
29+
30+[formatter_generic]
31+format = %(levelname)-5.5s [%(name)s] %(message)s
added docs/API.md +119 −0
@@ -0,0 +1,119 @@
1+# AI Atlas API — contract (v1)
2+
3+Base: `/api/v1` (FastAPI, `src/aiatlas/api`). JSON, UTF-8, ISO-8601 UTC timestamps, numbers as numbers (Postgres aggregates may arrive as
4+strings — the web layer normalises). Public routes are cached in Redis (`aia:api:*`, 60–600 s) and rate-limited per IP on search.
5+Admin routes require header `x-aia-admin-token: <AIA_ADMIN_TOKEN>`. OpenAPI at `/api/v1/docs`. Errors: `{"detail": "..."}` with 400/404/429/503.
6+Never expose credentials, internal hostnames, raw archive content or `raw_path`s.
7+
8+## Shared shapes
9+
10+```ts
11+type Num = number | string | null
12+type Org = { id: string; slug: string; name: string } | null
13+type EntitySummary = { id: string; entity_type: string; slug: string; name: string; description: string | null; status: string;
14+ organization: Org; attributes: Record<string, unknown>; quality: { score?: number; completeness?: number; primary_source_ratio?: number;
15+ freshness?: number; source_count?: number; conflicts?: number }; counts: { relations?: number; events?: number; claims?: number };
16+ first_seen_at: string; last_seen_at: string; updated_at: string }
17+type Page<T> = { items: T[]; total: number; limit: number; offset: number }
18+type Provenance = Record<string, { source_id: string | null; source_name?: string; url: string | null; observed_at: string; tier: number;
19+ confidence: string; extractor: string; unit?: string }>
20+type ChangeEvent = { id: string; event_type: string; category: string; property: string | null; old_value: unknown; new_value: unknown;
21+ summary: string; importance: 0 | 1 | 2 | 3; observed_at: string; effective_at: string | null; source_url: string | null; connector_name: string | null;
22+ entity: EntitySummary | null; meta: Record<string, unknown> }
23+type Price = { id: string; model: EntitySummary; provider: EntitySummary; provider_model_id: string | null; input_per_mtok: Num; output_per_mtok: Num;
24+ cached_input_per_mtok: Num; cache_write_per_mtok: Num; batch_input_per_mtok: Num; batch_output_per_mtok: Num; per_image: Num; currency: string;
25+ context_length: Num; max_output_tokens: Num; features: Record<string, unknown>; observed_at: string; valid_from: string; valid_to: string | null; source_url: string | null; tier: number }
26+type BenchmarkResult = { id: string; model: EntitySummary; benchmark: EntitySummary; score: number; metric: string | null; unit: string | null;
27+ higher_is_better: boolean; config: Record<string, unknown>; evaluated_at: string | null; observed_at: string; source_url: string | null; tier: number; confidence: string }
28+type Claim = { id: string; property: string; value: unknown; unit: string | null; tier: number; confidence: string; status: string; extractor: string;
29+ observed_at: string; effective_at: string | null; valid_from: string; valid_to: string | null; source_url: string | null; source_name: string | null }
30+type SourceRef = { source_id: string | null; source_name: string | null; domain: string | null; tier: number | null; url: string; doc_type: string; last_observed_at: string | null; snapshots: number }
31+```
32+
33+## Public routes
34+
35+| route | returns |
36+|---|---|
37+| `GET /health` | `{ status: "ok"|"degraded", version, db: bool, redis: bool, llm: { available, reachable? }, time }` |
38+| `GET /stats` | `{ entities: Record<type, number>, entities_total, sources, connectors, connectors_enabled, documents, snapshots, claims, claims_current, relations, change_events, change_events_24h, change_events_7d, benchmark_results, prices_current, prices_total, review_pending, llm_jobs, llm_tokens, last_snapshot_at, last_event_at, first_entity_at, archive: { raw_bytes, raw_files, text_bytes, text_files }, computed_at }` — **always live from the DB** |
39+| `GET /stats/history?days=90` | `{ items: { day: string; counts: object }[] }` |
40+| `GET /search?q=&type=&limit=30&offset=0` | `{ query: CompiledQuery, items: (EntitySummary & { rank: number })[], total: number }` — uses `services.search.compile_query` + `search_entities` (+ embedding when the gateway is up) |
41+| `GET /search/suggest?q=` | `{ items: { id, entity_type, slug, name, organization_name }[] }` (≤ 8, prefix, fast) |
42+| `GET /entities/{slug_or_id}` | `EntityDetail` (below). Also mounted as `/models/{slug}`, `/companies/{slug}`, `/papers/{slug}`, `/providers/{slug}`, `/benchmarks/{slug}`, `/hardware/{slug}`, `/frameworks/{slug}`, `/datasets/{slug}`, `/tools/{slug}` (404 if the type doesn't match) |
43+| `GET /entities/{slug}/timeline?limit=50&before=` | `{ items: ChangeEvent[] }` (entity's own events + events of entities it develops for companies) |
44+| `GET /entities/{slug}/history?property=` | `{ items: Claim[] }` — full claim history (all statuses), newest first |
45+| `GET /entities/{slug}/asof?date=YYYY-MM-DD` | `{ existed: bool, first_seen_at, date, attributes: Record<string, unknown>, claims: Claim[] }` — state as known at that date |
46+| `GET /entities/{slug}/graph?depth=1&limit=80` | `{ nodes: { id, slug, name, entity_type, organization_name? }[], edges: { source, target, predicate }[] }` (depth ≤ 2, cap nodes) |
47+| `GET /entities/{slug}/sources` | `{ items: SourceRef[] }` |
48+| `GET /entities/{slug}/related?limit=` | `{ items: EntitySummary[] }` (same org / same family / shared relations) |
49+| `GET /models?q=&org=&family=&openness=&modality=&status=&min_params=&max_params=&min_context=&year_from=&year_to=&license=&sort=(updated|name|params|context|release|quality|downloads)&order=&limit=&offset=` | `Page<EntitySummary>` + `facets` when `facets=1`: `{ organizations: {slug,name,count}[], openness: {value,count}[], modalities, families, years, licenses, status }` |
50+| `GET /companies?q=&country=&kind=&sort=(models|name|updated|quality)&limit=&offset=` | `Page<EntitySummary & { model_count: number; paper_count: number }>` (+`facets`: countries, kinds) |
51+| `GET /papers?q=&category=&org=&since=&until=&sort=(published|updated)&limit=&offset=` | `Page<EntitySummary>` |
52+| `GET /providers` | `{ items: (EntitySummary & { model_count: number; price_count: number; min_input_per_mtok: Num; min_output_per_mtok: Num })[] }` |
53+| `GET /prices?model=&provider=&sort=(input|output|model|provider|observed)&limit=&offset=&current=1` | `Page<Price>` |
54+| `GET /prices/history?model=&provider=` | `{ items: Price[] }` (all rows incl. closed, oldest first) |
55+| `GET /prices/index?days=180` | `{ series: { day: string; median_input: Num; median_output: Num; min_input: Num; models: number }[]; movers: ChangeEvent[] }` computed from `prices` history |
56+| `GET /benchmarks` | `{ items: (EntitySummary & { result_count: number; model_count: number; top: { model: EntitySummary; score: number } | null })[] }` |
57+| `GET /benchmarks/{slug}/results?limit=&offset=&config=` | `Page<BenchmarkResult>` sorted by score (respecting `higher_is_better`), current rows only unless `history=1` |
58+| `GET /benchmarks/{slug}/history?model=` | `{ items: BenchmarkResult[] }` over time |
59+| `GET /hardware?kind=&manufacturer=&min_memory=&sort=` | `Page<EntitySummary>` |
60+| `GET /hardware/fit?memory_gb=&quant=(4bit|8bit|fp16)&context=8192&limit=` | `{ inputs, assumptions: string[], items: { model: EntitySummary; parameter_count: number; estimated_memory_gb: number; fits: boolean; headroom_gb: number; quantization: string; note: string }[] }` — **ESTIMATED**: bytes/param (4bit 0.5+overhead 1.15, 8bit 1.0, fp16 2.0) + KV cache estimate; label as estimated |
61+| `GET /explore/types` | `{ items: { entity_type, count, label }[] }` |
62+| `GET /explore/{type}?q=&org=&sort=&limit=&offset=` | `Page<EntitySummary>` generic listing for any type (datasets, frameworks, tools, repositories, regulation…) |
63+| `GET /changes?category=&type=&entity_type=&importance_min=&since=&until=&q=&limit=50&before=<observed_at cursor>` | `Page<ChangeEvent>` newest first; `type` = comma list of event types; excludes `DOCUMENT_CHANGED` unless `include_documents=1` |
64+| `GET /changes/daily?date=YYYY-MM-DD` | `{ date, counts: Record<category, number>, sections: { category, label, items: ChangeEvent[] }[], new_models: EntitySummary[] }` ("What changed in AI today", DB-generated only) |
65+| `GET /changes/categories?days=7` | `{ items: { category, event_type, count }[] }` |
66+| `GET /timeline?entity=&year=&category=&limit=` | `{ items: { month: string; events: ChangeEvent[] }[] }` grouped by month; global when no entity |
67+| `GET /compare?ids=slug,slug,…` (2–6) | `{ entity_type, dimensions: { key, label, unit?, kind: "number"|"text"|"list"|"bool"|"date" }[], items: { entity: EntitySummary; values: Record<key, unknown>; provenance: Provenance; prices?: Price[]; results?: BenchmarkResult[] }[] }` — dimensions per type: model (params, active params, context, max output, openness, license, modalities, release date, knowledge cutoff, best price in/out, benchmark scores shared by all), provider (models, min prices, features), hardware (memory, bandwidth, tdp, runtimes), framework (version, license, stars), company (country, founded, models, papers) |
68+| `GET /diff?a=YYYY-MM-DD&b=YYYY-MM-DD&scope=(all|models|org:<slug>|family:<name>)` | `{ a, b, new_entities: EntitySummary[], gone_entities: EntitySummary[], property_changes: ChangeEvent[], price_changes: ChangeEvent[], benchmark_changes: ChangeEvent[], counts }` |
69+| `GET /sources` | `{ items: { key, name, domain, tier, kind, category, organization: Org, enabled, documents, last_crawled_at, connectors: { name, label, health, last_success_at, interval_seconds }[] }[] }` (public transparency page) |
70+| `GET /methodology` | `{ metrics: metric_definitions[], confidence_levels, tiers, event_types, extractors }` |
71+| `GET /trending?days=7&limit=12` | `{ items: (EntitySummary & { views: number })[] }` from `page_views` |
72+| `POST /views` `{ path }` | `{ ok: true }` (beacon; 1 req/s/IP) |
73+| `GET /sitemap?type=&limit=5000&offset=0` | `{ items: { slug, entity_type, updated_at }[], total }` |
74+| `GET /api-keys/me` (header `x-api-key`) | `{ label, plan, rate_per_min, usage_count }` — developer keys (later); public routes work without a key |
75+
76+### `EntityDetail`
77+
78+```ts
79+EntitySummary & {
80+ attributes: Record<string, unknown> // full
81+ provenance: Provenance
82+ aliases: string[]; identifiers: { scheme: string; value: string }[]
83+ relations: { predicate: string; direction: "out" | "in"; items: EntitySummary[]; total: number }[] // grouped, ≤ 24 per group
84+ sources: SourceRef[] // documents describing this entity (deduped by URL)
85+ timeline: ChangeEvent[] // latest 30
86+ quality: {...}; counts: {...}
87+ // type-specific blocks (present only when relevant)
88+ prices?: Price[] // current rows across providers (model) or for this provider (provider)
89+ price_history?: Price[] // closed + current, oldest first (model)
90+ results?: BenchmarkResult[] // model: its results · benchmark: leaderboard top 100
91+ lineage?: { ancestors: EntitySummary[]; descendants: EntitySummary[]; quantizations: EntitySummary[] } // model
92+ providers?: EntitySummary[] // model
93+ hardware_fit?: { hardware: EntitySummary; quantization: string; estimated_memory_gb: number; fits: boolean }[] // model, ESTIMATED
94+ models?: Page<EntitySummary> // company / provider / hardware(runnable)
95+ papers?: EntitySummary[] // company / model
96+ repositories?: EntitySummary[] // company / model / framework
97+}
98+```
99+
100+## Admin routes (`x-aia-admin-token`)
101+
102+| route | returns / action |
103+|---|---|
104+| `GET /admin/overview` | `{ stats, queue: Record<kind, Record<status, n>>, heartbeats, connectors: { ok, degraded, failing, disabled }, review_pending, recent_errors: n, llm: { jobs_24h, tokens_24h, by_stage } , archive }` |
105+| `GET /admin/connectors` | full `connectors` rows + last run + docs/snapshots counts + source tier |
106+| `POST /admin/connectors/{name}/run` `{ force?: bool }` | enqueue an immediate run (Redis flag `aia:run-now:<name>` consumed by the scheduler tick) → `{ queued: true }` |
107+| `PATCH /admin/connectors/{name}` `{ enabled?, interval_seconds?, priority? }` | update |
108+| `GET /admin/runs?connector=&limit=` · `GET /admin/errors?connector=&limit=` | rows |
109+| `GET /admin/documents?connector=&status=&q=&limit=&offset=` · `GET /admin/documents/{id}` | documents (+ snapshots list); `GET /admin/snapshots/{id}` → metadata + `structured` + `diff` + first 20 kB of cleaned text (never raw HTML) |
110+| `GET /admin/jobs?status=&kind=` · `POST /admin/jobs/{id}/retry` · `POST /admin/jobs/requeue-dead` | queue |
111+| `GET /admin/llm-jobs?limit=` · `GET /admin/llm/health` | LLM factory accounting |
112+| `GET /admin/review?status=pending&kind=` · `POST /admin/review/{id}` `{ action: "approve"|"reject"|"edit", resolution?: object }` | review queue; approving a `merge_candidate` calls `merge_entities(source, target)` (aliases/identifiers/claims/relations/events moved, `merged_into` set) |
113+| `GET /admin/entities/duplicates?type=&limit=` | candidate pairs by normalized name similarity (`pg_trgm`) within a type |
114+| `POST /admin/entities/merge` `{ source_id, target_id }` · `POST /admin/entities/{id}/claims/{claim_id}/retract` | curation |
115+| `POST /admin/reprocess` `{ connector, url? }` | enqueue `reprocess_snapshot` |
116+| `POST /admin/llm/enqueue` `{ limit?, task? }` | queue `llm_extract` for pending snapshots |
117+| `GET /admin/infrastructure` | heartbeats, archive size, DB size (`pg_database_size`), table sizes, node hostname, uptime |
118+| `POST /admin/cache/flush` | `{ flushed: n }` |
119+| `POST /admin/stats/recompute` · `POST /admin/quality/recompute` | maintenance |
added docs/ARCHITECTURE.md +142 −0
@@ -0,0 +1,142 @@
1+# AI Atlas — Technical architecture
2+
3+> The global intelligence layer for artificial intelligence: first-party connectors → raw historical archive → deterministic and
4+> local-LLM extraction → temporal knowledge graph → search / compare / timeline → www.ai-atlas.co + API.
5+
6+## 1. Principles that shape the code
7+
8+1. **No critical dependence on third-party data APIs.** Every source is read directly (HTML, embedded JSON, JSON-LD, RSS/Atom, sitemaps,
9+ Markdown docs, raw Git files, public JSON/CSV files). Scrapfly / Firecrawl / a headless browser are *optional escalation transports*
10+ (`Fetcher(escalate=True)`), never the database.
11+2. **Provenance first.** Every claim, relation, price, benchmark result and event stores `source_id`, `snapshot_id`, `source_url`, `tier`,
12+ `confidence`, `extractor` and `observed_at`. Entity pages can always answer "where does this number come from?".
13+3. **History is never disposable.** Claims are temporal (`valid_from`/`valid_to`, statuses `current|superseded|conflicting|retracted`),
14+ prices and benchmark results are append-only, raw snapshots are content-addressed and kept forever, migrations are forward-only.
15+4. **Deterministic before LLM.** Stage 1 (DOM, JSON-LD, tables, Markdown, feeds) runs on every document. The local LLM factory
16+ (MacLustr llm-api.io through an OpenAI-compatible gateway) only sees documents flagged `needs_llm`, through versioned pydantic schemas,
17+ with full token accounting.
18+5. **Never fabricate.** Missing = missing. Conflicts between sources are stored side by side, flagged and queued for review — never averaged.
19+
20+## 2. Topology
21+
22+```
23+ ┌──────────────── aia schedule (PM2 ai-atlas-scheduler) ─────────────────┐
24+ public AI web ──► │ connectors (registry) → Fetcher (direct/escalate) → archive (raw+text) │
25+ │ → parse → extract (Facts) → FactWriter (resolve, version, diff) │
26+ │ → jobs (llm_extract, embed, reprocess) → worker → LLM gateway │
27+ └──────────────────────────────────────────────────────────────────────────┘
28+ │ Postgres 17 (+pgvector, pg_trgm) · Redis (cache/locks) · AIA_DATA_DIR
29+ ┌──────────────────────────────┴───────────────────────────────────────────┐
30+ │ FastAPI /api/v1 (PM2 ai-atlas-api :8321) ◄── Next.js 16 (PM2 ai-atlas-web :8320) ◄── MacLustr Tunnel (BHS64 Caddy)
31+ └──────────────────────────────────────────────────────────────────────────┘
32+```
33+
34+Production node: **M2M32c** (Mac Studio M2, 12 c / 32 GB, dedicated), Postgres 17 + pgvector + Redis via Homebrew, data in
35+`~/ai-atlas-data/` (raw, text, backups, logs). Public route `https://www.ai-atlas.co → M2M32c:8320` on the MacLustr Tunnel gateway BHS64.
36+Workers can run on any node with database access (`aia worker`), which is how the LLM factory scales across the cluster.
37+
38+## 3. Repository layout
39+
40+```
41+registry/ curated YAML: sources.yaml (domains, tiers, crawl policy), organizations.yaml (aliases, domains, hf/github orgs),
42+ providers.yaml, benchmarks.yaml, hardware.yaml — every entry carries its source_url
43+migrations/versions/ forward-only SQL (0001_initial.py)
44+src/aiatlas/
45+ config.py settings (AIA_*), ids.py (prefixed ULIDs, slugify, normalize_alias), logging.py, db/ (SQLAlchemy Core + asyncpg)
46+ sdk/ connector SDK
47+ fetch.py Fetcher: robots.txt, per-domain rate limit, ETag/If-Modified-Since, retries/backoff, escalation chain
48+ archive.py content-addressed gzip store (raw/, text/) with dedupe
49+ extract/ html (selectolax; meta, JSON-LD, embedded JSON, tables, links, text), feeds, sitemap, markdown (front matter, tables),
50+ numbers (70B, 128K, $3/1M), dates (partial precision)
51+ facts.py EntityRef, Claim, Relation, Event, PriceObs, ResultObs, Target, Facts (+ MATERIAL_PROPERTIES → event types)
52+ resolution.py Resolver: identifiers → aliases (org-disambiguated) → slug → create; ambiguity → review queue
53+ writer.py FactWriter: temporal claims, attributes/provenance materialisation, conflicts, prices, results, events
54+ connector.py BaseConnector.run(): documents, snapshots, hash change detection, structural diff, DOCUMENT_CHANGED,
55+ breakage detection, circuit breaker, adaptive interval, follow-up targets, --file overrides, reprocess mode
56+ connectors/ one package per group (labs/, hub/, research/, code/, providers/, benchmarks/, hardware/); CONNECTORS = [cls]
57+ schemas/extraction.py pydantic schemas: ModelPassport, PricingExtraction, CompanyPassport, PaperPassport, BenchmarkResultExtraction, HardwareSpec, …
58+ services/
59+ jobs.py Postgres queue (SKIP LOCKED), handlers.py (llm_extract, embed_entity, reprocess_snapshot, recompute_quality)
60+ llm/gateway.py LLMGateway: stage cascade small→medium→large, OpenAI-compatible engine, JSON validation, llm_jobs accounting
61+ embeddings.py local embeddings → pgvector · search.py FTS+trigram(+vector) and NL→filters compiler
62+ scheduler.py APScheduler: due connectors (Redis lock), worker, hourly stats/quality/embeddings, nightly pg_dump
63+ quality.py transparency scores (documented in metric_definitions) · stats.py live counters · backup.py · cache.py
64+ api/ FastAPI application (see docs/API.md)
65+ cli.py `aia`: migrate seed connectors run reprocess crawl status stats quality schedule worker api backup llm embed search review
66+apps/web/ Next.js 16 site (see docs/FRONTEND.md)
67+tests/ pytest with saved fixtures (tests/fixtures/<connector>/…); live tests behind `-m live`
68+deploy/ mld manifest (ai-atlas.mld.json), render-manifest.sh, first-run.sh — see docs/DEPLOY.md
69+```
70+
71+## 4. Data model (Postgres)
72+
73+| table | role |
74+|---|---|
75+| `entities` | unified graph node: `entity_type`, `canonical_name`, `slug`, `organization_id`, `attributes` (current value per property), `provenance` (per property), `quality`, `counts`, `first_seen_at`, `search` tsvector |
76+| `entity_aliases`, `entity_identifiers` | resolution keys (normalized aliases; `(scheme, value)` unique: hf_repo, github_repo, arxiv, doi, anthropic_model_id, openrouter, pypi, domain…) |
77+| `sources`, `connectors`, `connector_runs`, `connector_errors` | source registry with tiers; connector state (health, adaptive interval, circuit, parser_version, expected_min_records) |
78+| `documents` | one row per URL: validators (etag/last-modified), `content_hash`, counters, `entity_id`, `needs_llm` |
79+| `snapshots` | one row per *changed* fetch: `raw_path`, `text_path`, `structured` (deterministic summary), `diff`, `transport`, `parser_version`, `processing_status` |
80+| `claims` | temporal facts: `(entity_id, property, value jsonb, unit, tier, confidence, status, valid_from, valid_to, snapshot_id, source_url, extractor)` |
81+| `relations` | typed edges with validity: `develops, owns, operates, available_through, evaluated_on, described_by, derived_from, fine_tuned_from, quantized_from, superseded_by, runs_on, uses, manufactures, funded_by, acquired, authored, works_at, uses_dataset, evaluates_on, integrates` |
82+| `change_events` | the event engine: `NEW_MODEL, MODEL_UPDATED, PRICE_CHANGED, PROVIDER_LISTED, CONTEXT_CHANGED, STATUS_CHANGED, LICENSE_CHANGED, DEPRECATION_ANNOUNCED, RETIREMENT_ANNOUNCED, BENCHMARK_RESULT, BENCHMARK_UPDATED, NEW_PAPER, ANNOUNCEMENT, RELEASE, VERSION_RELEASED, DOCUMENT_CHANGED, …` with `category`, `importance 0–3`, `dedupe_key` |
83+| `prices` | append-only provider pricing (`valid_to` closes a row): per-1M-token input/output/cached/cache-write/batch, per image/request, context, features |
84+| `benchmark_results` | append-only results with `config` (harness, prompting, judge…) — never compared blindly |
85+| `jobs`, `llm_jobs` | work queue; LLM cost accounting (stage, model, node, tokens, duration, status) |
86+| `review_queue` | merge candidates, conflicts, blocked sources, parser breakage, unusual changes |
87+| `entity_embeddings` | pgvector(1024) local embeddings |
88+| `domains`, `stats_snapshots`, `page_views`, `api_keys`, `metric_definitions` | trust graph, counters history, trending, developer keys, documented metrics |
89+
90+Identifiers: prefixed ULIDs (`model_01J…`, `company_…`, `paper_…`, `snap_…`, `evt_…`). Slugs are stable, readable and unique.
91+
92+### Temporal rules (FactWriter)
93+
94+```
95+no current claim → insert current, set attribute, NEW_<TYPE> event when the entity is new
96+same value → confirm (observed_at)
97+different, source ≥ tier → supersede (valid_to = observed), insert current, CHANGE event for material properties
98+different, worse source → store as `conflicting`, mark current `conflicted`, review-queue item — never overwrite
99+soft text (description…) → first statement wins until its own source changes; never an event
100+metric.* / stats.* → time series without events
101+```
102+
103+Historical mode = `claims` filtered by `valid_from <= date < coalesce(valid_to, ∞)`; "did it exist?" = `entities.first_seen_at <= date`.
104+Diff A→B = new/removed entities by `first_seen_at`, claims superseded between A and B, price rows opened/closed, results observed.
105+
106+## 5. Connector lifecycle
107+
108+`discover()` returns `Target`s (URL, doc_type, optional entity hint, `needs_llm`, `key` for fixtures). For each target `run()`:
109+fetch (conditional) → unchanged? stop · changed → archive raw + text → snapshot row (+ structural diff vs previous) → `parse()` →
110+`extract()` → `FactWriter.write(facts)` → follow-up targets → optional `llm_extract` job. Blocked (401/403/451/robots) → document
111+`blocked` + review item; 404/410 twice → `gone` (never deletes entities). Fewer records than `expected_min_records` → run `suspect`
112++ review item. Intervals adapt: ×0.7 on change (≥ min), ×1.5 after 3 unchanged runs (≤ max). Three failures open the circuit 30 min.
113+
114+Parser improvements: bump `parser_version`, then `aia reprocess <connector>` replays the latest stored snapshot of every document — no crawl.
115+
116+## 6. LLM factory
117+
118+`LLMGateway.extract(task_type, document, schema, stage)` → strict JSON validated by a pydantic schema; on schema errors the next stage
119+model is tried (small → medium → large); transport failures do not escalate. Every call is recorded in `llm_jobs` (tokens, duration,
120+node, status, output). `facts_from_llm()` maps outputs onto `Facts` with `extractor='llm'` and medium/low confidence, so deterministic
121+tier-1 claims always win conflicts. Embeddings use the same gateway (`/v1/embeddings`, Qwen3-Embedding 1024-d) into pgvector.
122+Without a configured gateway the platform degrades gracefully: deterministic extraction, FTS search, no embeddings.
123+
124+## 7. Quality, search, events
125+
126+* `quality.score` = 100·(0.25 completeness + 0.25 primary-source ratio + 0.20 freshness + 0.15 agreement + 0.15 source diversity),
127+ versioned and explained in `metric_definitions` and `/methodology`. It measures how well AI Atlas knows an entity, not the entity.
128+* Search: tsvector (name A, family B, type/description C) + `pg_trgm` similarity + aliases (+ cosine on embeddings when present).
129+ `compile_query()` turns "open models released in 2026 with more than 100B parameters and 128k context" into structured filters.
130+* Events power the homepage feed, entity timelines, `/changes`, the daily digest and, later, watchlists/alerts.
131+
132+## 8. Security & compliance
133+
134+Admin routes require `x-aia-admin-token`. The API binds to loopback; Next.js rewrites `/api/v1/*` to it. No credentials or internal
135+addresses in responses. Crawling honours robots.txt, identifies itself (`AIAtlasBot/0.1 … contact@spboucher.ai`), rate-limits per domain,
136+uses conditional requests, never bypasses access controls; blocked sources go to the review queue. Raw archive content is never exposed
137+publicly — only extracted facts with links to the original page.
138+
139+## 9. Backups & durability
140+
141+Nightly `pg_dump -Fc` into `AIA_DATA_DIR/backups` (30 kept), off-node rsync (`scripts/backup-offnode.sh`), raw archive is append-only and
142+content-addressed (rsync-friendly). The dataset is the product: restore = `pg_restore` + point `DATABASE_URL`.
added docs/CONNECTORS.md +97 −0
@@ -0,0 +1,97 @@
1+# Writing a connector
2+
3+A connector is a Python class under `src/aiatlas/connectors/<group>/<name>.py`, listed in that module's `CONNECTORS = [Cls]`.
4+The registry auto-discovers it; `aia seed` registers it in the database; `aia run <name>` runs it; the scheduler runs it on its interval.
5+
6+## Skeleton
7+
8+```python
9+from aiatlas.registry import org_ref, provider_ref
10+from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
11+from aiatlas.sdk.facts import Facts, Target
12+from aiatlas.sdk.fetch import FetchResult
13+
14+class ExampleConnector(BaseConnector):
15+ name = "example" # unique, snake_case, stable (used in documents/claims/events)
16+ label = "Example Lab — models & news"
17+ description = "Official docs and blog of Example Lab."
18+ source_key = "example.com" # must exist in registry/sources.yaml (tier, rate limit, org)
19+ version = "1"; parser_version = "1" # bump parser_version when extraction improves → `aia reprocess example`
20+ interval_seconds = 3600; min_interval_seconds = 1800; max_interval_seconds = 86400
21+ rate_per_min = 15; tier = 1; priority = 0 | 1 | 2
22+ expected_min_records = 5 # breakage detection: fewer entities/prices/results ⇒ run "suspect", nothing deleted
23+ concurrency = 2
24+
25+ async def discover(self, ctx: RunContext) -> list[Target]:
26+ return [Target(url="https://example.com/models", doc_type="model_docs", key="models", min_bytes=2000),
27+ Target(url="https://example.com/blog/rss.xml", doc_type="feed", key="feed")]
28+
29+ async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:
30+ facts = Facts()
31+ org = org_ref("example") # registry organization → deterministic identifiers
32+ facts.entities.append(org)
33+ if target.key == "models" and parsed.html:
34+ for row in parsed.html.tables[0]["rows"]: ...
35+ model = facts.entity("model", name, identifiers={"example_model_id": api_id}, organization=org)
36+ facts.relate(org, "develops", model)
37+ facts.claim(model, "context_length", 128000, unit="tokens")
38+ facts.price(model=model, provider=provider_ref("example"), input_per_mtok=1.0, output_per_mtok=4.0)
39+ facts.follow(detail_url, doc_type="model_page", entity=model, needs_llm=True, meta={"llm_task": "model_passport"})
40+ elif target.key == "feed" and parsed.kind == "feed":
41+ announcement_events(facts, org, parsed.feed_items, source_name="example.com/blog") # from connectors/labs/_common.py
42+ return facts
43+
44+CONNECTORS = [ExampleConnector]
45+```
46+
47+## What `Parsed` gives you
48+
49+| `parsed.kind` | fields |
50+|---|---|
51+| `html` | `parsed.html: HtmlDoc` — `title, canonical, description, meta, og, json_ld, embedded_json (__NEXT_DATA__, data-props…), headings, tables [{headers, rows}], links [(href, text)], text`, `css()`, `links_matching()` ; `find_in_json(obj, key)` helper |
52+| `markdown` | `parsed.markdown: MarkdownDoc` — `front_matter, headings, tables, links, text, section(pattern)` |
53+| `feed` | `parsed.feed_items: list[FeedItem]` (`url, title, summary, published_at, categories, authors`) |
54+| `json` | `parsed.json` |
55+| `pdf` / `text` / `xml` | `parsed.text` |
56+
57+Helpers: `aiatlas.sdk.extract.numbers` (`parse_param_count("70B")`, `parse_active_params("235B-A22B")`, `parse_context_length("128K")`,
58+`parse_money_per_mtok("$3 / 1M tokens")`, `parse_percent`), `aiatlas.sdk.extract.dates` (`parse_datetime`, `parse_date` with precision),
59+`connectors/labs/_common.py` (`announcement_events`, `transpose_feature_table`, `kv_tables`, `clean_cell`, `money`, `tokens`, `month_year`, `parse_retirement`, `model_ref`).
60+
61+## Rules
62+
63+1. **Direct mode first.** Use official pages, feeds, sitemaps, Markdown/raw files, embedded JSON. Public JSON files (e.g. `openrouter.ai/api/v1/models`,
64+ `pypi.org/pypi/<pkg>/json`, arXiv Atom) are fine — they are public documents, not commercial APIs. Never require an API key.
65+ Set `Target(escalate=True)` only for pages known to block bots *and* only if a key is configured; otherwise let the document be `blocked`.
66+2. **Never fabricate.** Only emit a claim when the page states it. Unknown → no claim. Don't infer parameter counts from names unless the
67+ name literally contains them (`Qwen3-235B-A22B` → 235e9 / 22e9 is fine; "Large" is not).
68+3. **Identifiers make resolution deterministic.** Give models the provider's API id (`{provider}_model_id`), HF repo (`hf_repo`), arXiv id
69+ (`arxiv`), GitHub repo (`github_repo`), PyPI name (`pypi`). Organizations always come from `org_ref(<registry key>)` (add missing orgs to
70+ `registry/organizations.yaml` with a `source_url`).
71+4. **Properties are shared vocabulary** (see below). Add new ones sparingly; prefix metrics with `metric.` (downloads, likes, stars) so they
72+ never generate events.
73+5. **Events**: NEW_*, PRICE_CHANGED, CONTEXT_CHANGED… are emitted automatically by the writer. Emit `ANNOUNCEMENT`/`RELEASE` events yourself for
74+ feed items (`announcement_events`) with `effective_at` = publication date and a `dedupe_key` (URL).
75+6. **Follow-ups** (`facts.follow`) let a listing discover detail pages; keep `max_targets` reasonable (`ctx.max_targets`, default 2000).
76+7. **Every connector has a fixture test**: save real responses under `tests/fixtures/<connector>/…` and assert extracted facts
77+ (see `tests/test_anthropic.py`). Run connectors with `--file key=path` to use fixtures instead of the network.
78+8. Respect `rate_per_min` from `registry/sources.yaml`; arXiv ≤ 4/min; Hugging Face ≤ 30/min; GitHub ≤ 20/min.
79+
80+## Property vocabulary (entities.attributes)
81+
82+**model**: `family, version, release_date (ISO, may be YYYY-MM), status (active|preview|deprecated|retired|announced|limited-availability), openness
83+(open-weights|open-source|proprietary|restricted), license, architecture, parameter_count (int), active_parameter_count, is_moe, modalities
84+(list of text|image|audio|video|code|embedding), modalities_input, modalities_output, context_length (tokens), max_output_tokens, knowledge_cutoff
85+(YYYY-MM), training_data_cutoff, languages, tool_calling, structured_output, reasoning, vision, audio, fine_tuning_available, tokenizer,
86+api_model_id, api_alias, official_url, model_card_url, paper_url, repository_url, hf_repo, pipeline_tag, base_model, quantization, quant_format
87+(gguf|mlx|awq|gptq|fp8), file_size_gb, deprecation_date, retirement_date, retirement_tentative, metric.downloads, metric.likes`
88+
89+**company / organization / lab**: `country (ISO-2), headquarters, founded, website, domains, hf_org, github_org, org_kind, legal_name, founders, leadership, employee_count`
90+
91+**provider**: `website, pricing_url, docs_url, regions, features` · **paper**: `authors, published_at, updated_at, abstract, arxiv_id, doi, categories, primary_category, pdf_url, code_url, venue`
92+· **benchmark**: `category, task, metric, unit, creator, website, paper, known_limitations, methodology` · **hardware**: `kind, architecture, release_date, memory_gb,
93+memory_type, memory_bandwidth_gbs, tdp_watts, runtimes, manufacturer, spec_url, price_usd, compute_fp16_tflops` · **framework / repository**: `repository_url, latest_version,
94+latest_release_at, license, language, description, topics, pypi, metric.stars, metric.forks` · **dataset**: `license, modality, size, publisher, task, hf_repo`
95+
96+Relations: `develops, owns, operates, available_through, evaluated_on, described_by, derived_from, fine_tuned_from, quantized_from, distilled_from,
97+merged_from, superseded_by, runs_on, uses, manufactures, funded_by, acquired, authored, works_at, uses_dataset, evaluates_on, integrates, published_by`.
added migrations/env.py +45 −0
@@ -0,0 +1,45 @@
1+"""Alembic environment (async engine). Migrations are plain SQL executed through `op.execute`."""
2+from __future__ import annotations
3+
4+import asyncio
5+from logging.config import fileConfig
6+
7+from alembic import context
8+from sqlalchemy.ext.asyncio import create_async_engine
9+
10+from aiatlas.config import settings
11+
12+config = context.config
13+if config.config_file_name is not None:
14+ fileConfig(config.config_file_name)
15+
16+target_metadata = None
17+
18+
19+def run_migrations_offline() -> None:
20+ context.configure(url=settings.sync_database_url, literal_binds=True, dialect_opts={"paramstyle": "named"})
21+ with context.begin_transaction():
22+ context.run_migrations()
23+
24+
25+def do_run_migrations(connection) -> None: # type: ignore[no-untyped-def]
26+ context.configure(connection=connection, target_metadata=target_metadata, transaction_per_migration=True)
27+ with context.begin_transaction():
28+ context.run_migrations()
29+
30+
31+async def run_async_migrations() -> None:
32+ engine = create_async_engine(settings.database_url, poolclass=None)
33+ async with engine.connect() as connection:
34+ await connection.run_sync(do_run_migrations)
35+ await engine.dispose()
36+
37+
38+def run_migrations_online() -> None:
39+ asyncio.run(run_async_migrations())
40+
41+
42+if context.is_offline_mode():
43+ run_migrations_offline()
44+else:
45+ run_migrations_online()
added migrations/script.py.mako +21 −0
@@ -0,0 +1,21 @@
1+"""${message}
2+
3+Revision ID: ${up_revision}
4+Revises: ${down_revision | comma,n}
5+"""
6+from __future__ import annotations
7+
8+from alembic import op
9+
10+revision = ${repr(up_revision)}
11+down_revision = ${repr(down_revision)}
12+branch_labels = ${repr(branch_labels)}
13+depends_on = ${repr(depends_on)}
14+
15+
16+def upgrade() -> None:
17+ ${upgrades if upgrades else "pass"}
18+
19+
20+def downgrade() -> None:
21+ raise RuntimeError("forward-only")
added migrations/versions/0001_initial.py +502 −0
@@ -0,0 +1,502 @@
1+"""AI Atlas initial schema — entities, aliases, identifiers, sources, connectors, documents, snapshots, claims, relations,
2+change events, benchmark results, prices, jobs, LLM jobs, review queue, stats.
3+
4+Revision ID: 0001
5+"""
6+from __future__ import annotations
7+
8+from alembic import op
9+
10+revision = "0001"
11+down_revision = None
12+branch_labels = None
13+depends_on = None
14+
15+SQL = r"""
16+create extension if not exists pg_trgm;
17+create extension if not exists "uuid-ossp";
18+do $$ begin
19+ create extension if not exists vector;
20+exception when others then
21+ raise notice 'pgvector unavailable: %', sqlerrm;
22+end $$;
23+
24+-- ---------------------------------------------------------------------------------------------- reference: sources & connectors
25+create table if not exists sources (
26+ id text primary key,
27+ key text not null unique, -- e.g. 'openai.com', 'huggingface.co', 'arxiv.org'
28+ name text not null,
29+ domain text not null,
30+ organization_id text, -- entity id of the owning organization (domain trust graph)
31+ tier smallint not null default 2, -- 1 official primary, 2 quality secondary, 3 community, 4 unverified
32+ kind text not null default 'website', -- website|docs|feed|repository|registry|dataset|leaderboard|regulator
33+ category text not null default 'lab',
34+ base_url text,
35+ robots_policy text not null default 'respect', -- respect|documented-exception
36+ rate_limit_per_min integer not null default 30,
37+ crawl_interval_s integer not null default 86400,
38+ enabled boolean not null default true,
39+ priority smallint not null default 2, -- 0 = P0
40+ notes text,
41+ meta jsonb not null default '{}'::jsonb,
42+ created_at timestamptz not null default now(),
43+ updated_at timestamptz not null default now()
44+);
45+
46+create table if not exists connectors (
47+ name text primary key,
48+ source_id text references sources(id),
49+ label text not null,
50+ description text,
51+ enabled boolean not null default true,
52+ priority smallint not null default 2,
53+ interval_seconds integer not null default 3600,
54+ min_interval_seconds integer not null default 900,
55+ max_interval_seconds integer not null default 604800,
56+ parser_version text not null default '1',
57+ rate_limit_per_min integer not null default 30,
58+ owner text not null default 'ai-atlas',
59+ expected_min_records integer not null default 0, -- breakage detection: fewer records than this = suspected failure
60+ last_attempt_at timestamptz,
61+ last_success_at timestamptz,
62+ last_change_at timestamptz,
63+ next_run_at timestamptz,
64+ last_duration_ms integer,
65+ consecutive_failures integer not null default 0,
66+ consecutive_unchanged integer not null default 0,
67+ circuit_open_until timestamptz,
68+ health text not null default 'unknown', -- ok|degraded|failing|disabled|unknown
69+ meta jsonb not null default '{}'::jsonb,
70+ created_at timestamptz not null default now(),
71+ updated_at timestamptz not null default now()
72+);
73+
74+create table if not exists connector_runs (
75+ id text primary key,
76+ connector_name text not null references connectors(name),
77+ started_at timestamptz not null,
78+ finished_at timestamptz,
79+ status text not null, -- running|success|unchanged|failed|skipped|suspect
80+ duration_ms integer,
81+ docs_discovered integer not null default 0,
82+ docs_fetched integer not null default 0,
83+ docs_changed integer not null default 0,
84+ docs_unchanged integer not null default 0,
85+ docs_failed integer not null default 0,
86+ entities_created integer not null default 0,
87+ entities_updated integer not null default 0,
88+ claims_written integer not null default 0,
89+ relations_written integer not null default 0,
90+ events_emitted integer not null default 0,
91+ error text,
92+ meta jsonb not null default '{}'::jsonb
93+);
94+create index if not exists connector_runs_name_idx on connector_runs (connector_name, started_at desc);
95+
96+create table if not exists connector_errors (
97+ id bigserial primary key,
98+ connector_name text not null,
99+ run_id text,
100+ url text,
101+ error_type text not null,
102+ message text not null,
103+ created_at timestamptz not null default now()
104+);
105+create index if not exists connector_errors_name_idx on connector_errors (connector_name, created_at desc);
106+
107+-- ---------------------------------------------------------------------------------------------- entities (unified graph)
108+create table if not exists entities (
109+ id text primary key,
110+ entity_type text not null,
111+ canonical_name text not null,
112+ slug text not null unique,
113+ description text,
114+ status text not null default 'active', -- active|deprecated|retired|announced|unknown|merged
115+ organization_id text references entities(id),
116+ attributes jsonb not null default '{}'::jsonb, -- current value per property (materialised from claims)
117+ provenance jsonb not null default '{}'::jsonb, -- property -> {source, snapshot_id, url, observed_at, tier, confidence}
118+ quality jsonb not null default '{}'::jsonb, -- source_count, primary_source_ratio, freshness, completeness, agreement, score
119+ counts jsonb not null default '{}'::jsonb, -- cached relation / claim / event counts
120+ first_seen_at timestamptz not null default now(),
121+ last_seen_at timestamptz not null default now(),
122+ merged_into text references entities(id),
123+ search tsvector,
124+ created_at timestamptz not null default now(),
125+ updated_at timestamptz not null default now()
126+);
127+create index if not exists entities_type_idx on entities (entity_type, canonical_name);
128+create index if not exists entities_org_idx on entities (organization_id);
129+create index if not exists entities_search_idx on entities using gin (search);
130+create index if not exists entities_name_trgm_idx on entities using gin (canonical_name gin_trgm_ops);
131+create index if not exists entities_attrs_idx on entities using gin (attributes jsonb_path_ops);
132+create index if not exists entities_updated_idx on entities (updated_at desc);
133+create index if not exists entities_first_seen_idx on entities (first_seen_at desc);
134+
135+create table if not exists entity_aliases (
136+ id bigserial primary key,
137+ entity_id text not null references entities(id) on delete cascade,
138+ alias text not null,
139+ alias_norm text not null,
140+ kind text not null default 'alias', -- alias|former_name|abbreviation|localized|typo
141+ snapshot_id text,
142+ created_at timestamptz not null default now(),
143+ unique (entity_id, alias_norm)
144+);
145+create index if not exists entity_aliases_norm_idx on entity_aliases (alias_norm);
146+
147+create table if not exists entity_identifiers (
148+ id bigserial primary key,
149+ entity_id text not null references entities(id) on delete cascade,
150+ scheme text not null, -- hf_repo|github_repo|arxiv|doi|openrouter|domain|pypi|wikidata|url|provider_model_id
151+ value text not null,
152+ snapshot_id text,
153+ created_at timestamptz not null default now(),
154+ unique (scheme, value)
155+);
156+create index if not exists entity_identifiers_entity_idx on entity_identifiers (entity_id);
157+
158+-- ---------------------------------------------------------------------------------------------- documents & snapshots (raw archive)
159+create table if not exists documents (
160+ id text primary key,
161+ source_id text references sources(id),
162+ connector_name text references connectors(name),
163+ url text not null unique,
164+ canonical_url text,
165+ doc_type text not null default 'page', -- page|feed|feed_item|model_card|docs|pricing|release|paper|pdf|json|sitemap|repo|readme|leaderboard
166+ title text,
167+ entity_id text references entities(id),
168+ status text not null default 'active', -- active|gone|blocked|error
169+ first_seen_at timestamptz not null default now(),
170+ last_fetched_at timestamptz,
171+ last_changed_at timestamptz,
172+ last_status integer,
173+ etag text,
174+ last_modified text,
175+ content_hash text,
176+ fetch_count integer not null default 0,
177+ change_count integer not null default 0,
178+ fail_count integer not null default 0,
179+ fetch_interval_s integer,
180+ next_fetch_at timestamptz,
181+ priority smallint not null default 2,
182+ needs_llm boolean not null default false,
183+ meta jsonb not null default '{}'::jsonb
184+);
185+create index if not exists documents_connector_idx on documents (connector_name, last_fetched_at desc);
186+create index if not exists documents_entity_idx on documents (entity_id);
187+create index if not exists documents_next_idx on documents (next_fetch_at) where status = 'active';
188+
189+create table if not exists snapshots (
190+ id text primary key,
191+ document_id text not null references documents(id) on delete cascade,
192+ run_id text,
193+ url text not null,
194+ final_url text,
195+ observed_at timestamptz not null default now(),
196+ http_status integer,
197+ headers jsonb not null default '{}'::jsonb,
198+ content_type text,
199+ content_hash text not null,
200+ byte_size integer not null default 0,
201+ raw_path text, -- content-addressed gzip under AIA_DATA_DIR/raw
202+ text_path text, -- cleaned text (gzip) under AIA_DATA_DIR/text
203+ text_hash text,
204+ structured jsonb, -- deterministic extraction (json-ld, meta, tables, embedded json)
205+ parser_version text not null default '1',
206+ connector_version text not null default '1',
207+ transport text not null default 'direct', -- direct|browser|scrapfly|firecrawl|file|git
208+ changed boolean not null default true,
209+ diff jsonb, -- structural diff vs previous changed snapshot
210+ processing_status text not null default 'stored', -- stored|extracted|llm_pending|llm_done|failed
211+ created_at timestamptz not null default now()
212+);
213+create index if not exists snapshots_doc_idx on snapshots (document_id, observed_at desc);
214+create index if not exists snapshots_hash_idx on snapshots (content_hash);
215+create index if not exists snapshots_status_idx on snapshots (processing_status) where processing_status in ('stored','llm_pending');
216+
217+-- ---------------------------------------------------------------------------------------------- temporal facts
218+create table if not exists claims (
219+ id text primary key,
220+ entity_id text not null references entities(id) on delete cascade,
221+ property text not null,
222+ value jsonb not null,
223+ value_text text,
224+ value_num double precision,
225+ unit text,
226+ source_id text references sources(id),
227+ snapshot_id text references snapshots(id),
228+ source_url text,
229+ tier smallint not null default 2,
230+ confidence text not null default 'medium', -- verified|high|medium|low|conflicted
231+ status text not null default 'current', -- current|superseded|conflicting|retracted
232+ extractor text not null default 'deterministic',
233+ extractor_version text not null default '1',
234+ observed_at timestamptz not null default now(),
235+ effective_at timestamptz,
236+ valid_from timestamptz not null default now(),
237+ valid_to timestamptz,
238+ created_at timestamptz not null default now()
239+);
240+create index if not exists claims_entity_prop_idx on claims (entity_id, property, valid_from desc);
241+create index if not exists claims_current_idx on claims (entity_id, property) where status = 'current';
242+create index if not exists claims_snapshot_idx on claims (snapshot_id);
243+create index if not exists claims_observed_idx on claims (observed_at desc);
244+
245+create table if not exists relations (
246+ id text primary key,
247+ subject_id text not null references entities(id) on delete cascade,
248+ predicate text not null,
249+ object_id text not null references entities(id) on delete cascade,
250+ attributes jsonb not null default '{}'::jsonb,
251+ source_id text references sources(id),
252+ snapshot_id text references snapshots(id),
253+ source_url text,
254+ tier smallint not null default 2,
255+ confidence text not null default 'medium',
256+ observed_at timestamptz not null default now(),
257+ valid_from timestamptz not null default now(),
258+ valid_to timestamptz,
259+ created_at timestamptz not null default now()
260+);
261+create unique index if not exists relations_live_uniq on relations (subject_id, predicate, object_id) where valid_to is null;
262+create index if not exists relations_subject_idx on relations (subject_id, predicate);
263+create index if not exists relations_object_idx on relations (object_id, predicate);
264+
265+create table if not exists change_events (
266+ id text primary key,
267+ entity_id text references entities(id) on delete cascade,
268+ event_type text not null, -- NEW_MODEL|MODEL_UPDATED|PRICE_CHANGED|CONTEXT_CHANGED|NEW_PAPER|RELEASE|BENCHMARK_RESULT|NEW_PROVIDER|DOCUMENT_CHANGED|…
269+ category text not null default 'update', -- model|price|benchmark|paper|release|company|hardware|framework|provider|dataset|regulation|incident|repository
270+ property text,
271+ old_value jsonb,
272+ new_value jsonb,
273+ summary text,
274+ importance smallint not null default 2, -- 0 minor … 3 major
275+ observed_at timestamptz not null default now(),
276+ effective_at timestamptz,
277+ source_id text references sources(id),
278+ snapshot_id text references snapshots(id),
279+ source_url text,
280+ connector_name text,
281+ dedupe_key text unique,
282+ meta jsonb not null default '{}'::jsonb
283+);
284+create index if not exists change_events_observed_idx on change_events (observed_at desc);
285+create index if not exists change_events_entity_idx on change_events (entity_id, observed_at desc);
286+create index if not exists change_events_type_idx on change_events (event_type, observed_at desc);
287+create index if not exists change_events_category_idx on change_events (category, observed_at desc);
288+
289+-- ---------------------------------------------------------------------------------------------- domain tables
290+create table if not exists benchmark_results (
291+ id text primary key,
292+ model_id text not null references entities(id) on delete cascade,
293+ benchmark_id text not null references entities(id) on delete cascade,
294+ score double precision not null,
295+ metric text,
296+ unit text,
297+ higher_is_better boolean not null default true,
298+ config jsonb not null default '{}'::jsonb, -- prompting, judge, tool use, sampling, harness, model variant string
299+ evaluated_at timestamptz,
300+ observed_at timestamptz not null default now(),
301+ source_id text references sources(id),
302+ snapshot_id text references snapshots(id),
303+ source_url text,
304+ tier smallint not null default 2,
305+ confidence text not null default 'medium',
306+ dedupe_key text unique,
307+ valid_to timestamptz
308+);
309+create index if not exists benchmark_results_model_idx on benchmark_results (model_id, benchmark_id, observed_at desc);
310+create index if not exists benchmark_results_bench_idx on benchmark_results (benchmark_id, score desc);
311+
312+create table if not exists prices (
313+ id text primary key,
314+ model_id text not null references entities(id) on delete cascade,
315+ provider_id text not null references entities(id) on delete cascade,
316+ provider_model_id text,
317+ input_per_mtok double precision,
318+ output_per_mtok double precision,
319+ cached_input_per_mtok double precision,
320+ cache_write_per_mtok double precision,
321+ batch_input_per_mtok double precision,
322+ batch_output_per_mtok double precision,
323+ per_image double precision,
324+ per_request double precision,
325+ currency text not null default 'USD',
326+ context_length integer,
327+ max_output_tokens integer,
328+ features jsonb not null default '{}'::jsonb,
329+ observed_at timestamptz not null default now(),
330+ valid_from timestamptz not null default now(),
331+ valid_to timestamptz,
332+ source_id text references sources(id),
333+ snapshot_id text references snapshots(id),
334+ source_url text,
335+ tier smallint not null default 2,
336+ meta jsonb not null default '{}'::jsonb
337+);
338+create unique index if not exists prices_live_uniq on prices (model_id, provider_id, coalesce(provider_model_id, '')) where valid_to is null;
339+create index if not exists prices_provider_idx on prices (provider_id, valid_from desc);
340+create index if not exists prices_model_idx on prices (model_id, valid_from desc);
341+
342+create table if not exists domains (
343+ domain text primary key,
344+ organization_id text references entities(id),
345+ trust_tier smallint not null default 3,
346+ category text,
347+ notes text,
348+ created_at timestamptz not null default now()
349+);
350+
351+-- ---------------------------------------------------------------------------------------------- work queues
352+create table if not exists jobs (
353+ id text primary key,
354+ kind text not null, -- fetch_document|llm_extract|embed_entity|reprocess_snapshot|recompute_quality|resolve_entity
355+ payload jsonb not null default '{}'::jsonb,
356+ priority smallint not null default 5, -- 0 highest
357+ status text not null default 'queued', -- queued|running|done|failed|dead
358+ attempts integer not null default 0,
359+ max_attempts integer not null default 3,
360+ run_after timestamptz not null default now(),
361+ locked_by text,
362+ locked_at timestamptz,
363+ started_at timestamptz,
364+ finished_at timestamptz,
365+ error text,
366+ batch_id text,
367+ dedupe_key text,
368+ created_at timestamptz not null default now()
369+);
370+create index if not exists jobs_pick_idx on jobs (status, priority, run_after) where status = 'queued';
371+create unique index if not exists jobs_dedupe_idx on jobs (dedupe_key) where status in ('queued','running');
372+create index if not exists jobs_batch_idx on jobs (batch_id);
373+
374+create table if not exists llm_jobs (
375+ id text primary key,
376+ job_id text,
377+ task_type text not null,
378+ stage text not null, -- small|medium|large
379+ engine text not null,
380+ model text not null,
381+ node text,
382+ schema_name text,
383+ snapshot_id text references snapshots(id),
384+ entity_id text references entities(id),
385+ input_tokens integer,
386+ output_tokens integer,
387+ duration_ms integer,
388+ status text not null, -- ok|failed|invalid_json|schema_error
389+ output jsonb,
390+ error text,
391+ created_at timestamptz not null default now()
392+);
393+create index if not exists llm_jobs_created_idx on llm_jobs (created_at desc);
394+
395+create table if not exists review_queue (
396+ id text primary key,
397+ kind text not null, -- merge_candidate|conflict|blocked_source|parser_breakage|unusual_change|new_source
398+ entity_ids text[] not null default '{}',
399+ payload jsonb not null default '{}'::jsonb,
400+ reason text not null,
401+ status text not null default 'pending', -- pending|approved|rejected|edited
402+ resolution jsonb,
403+ created_at timestamptz not null default now(),
404+ resolved_at timestamptz,
405+ dedupe_key text unique
406+);
407+create index if not exists review_queue_status_idx on review_queue (status, created_at desc);
408+
409+-- ---------------------------------------------------------------------------------------------- embeddings (optional; local models)
410+do $$ begin
411+ if exists (select 1 from pg_extension where extname = 'vector') then
412+ execute 'create table if not exists entity_embeddings (
413+ entity_id text primary key references entities(id) on delete cascade,
414+ model text not null,
415+ embedding vector(1024) not null,
416+ text_hash text not null,
417+ created_at timestamptz not null default now())';
418+ end if;
419+end $$;
420+
421+-- ---------------------------------------------------------------------------------------------- stats, views, api keys
422+create table if not exists stats_snapshots (
423+ id bigserial primary key,
424+ computed_at timestamptz not null default now(),
425+ counts jsonb not null
426+);
427+
428+create table if not exists page_views (
429+ path text not null,
430+ day date not null,
431+ views integer not null default 0,
432+ primary key (path, day)
433+);
434+
435+create table if not exists api_keys (
436+ id text primary key,
437+ key_hash text not null unique,
438+ label text not null,
439+ owner_email text,
440+ plan text not null default 'developer',
441+ rate_per_min integer not null default 60,
442+ enabled boolean not null default true,
443+ created_at timestamptz not null default now(),
444+ last_used_at timestamptz,
445+ usage_count bigint not null default 0
446+);
447+
448+create table if not exists metric_definitions (
449+ key text primary key,
450+ label text not null,
451+ version text not null,
452+ description text not null,
453+ formula text,
454+ created_at timestamptz not null default now()
455+);
456+
457+-- ---------------------------------------------------------------------------------------------- search vector maintenance
458+create or replace function entities_search_update() returns trigger language plpgsql as $$
459+begin
460+ new.search :=
461+ setweight(to_tsvector('simple', coalesce(new.canonical_name, '')), 'A') ||
462+ setweight(to_tsvector('simple', coalesce(new.attributes->>'family', '')), 'B') ||
463+ setweight(to_tsvector('simple', coalesce(new.entity_type, '')), 'C') ||
464+ setweight(to_tsvector('english', left(coalesce(new.description, ''), 4000)), 'C');
465+ new.updated_at := now();
466+ return new;
467+end $$;
468+drop trigger if exists entities_search_trg on entities;
469+create trigger entities_search_trg before insert or update of canonical_name, description, attributes, entity_type on entities
470+ for each row execute function entities_search_update();
471+"""
472+
473+
474+def upgrade() -> None:
475+ for statement in _split(SQL):
476+ op.execute(statement)
477+
478+
479+def downgrade() -> None:
480+ raise RuntimeError("AI Atlas migrations are forward-only: historical data is never disposable")
481+
482+
483+def _split(sql: str) -> list[str]:
484+ """Split on semicolons outside `$$ … $$` blocks."""
485+ out: list[str] = []
486+ buf: list[str] = []
487+ in_dollar = False
488+ for line in sql.splitlines():
489+ stripped = line.strip()
490+ if stripped.count("$$") % 2 == 1:
491+ in_dollar = not in_dollar
492+ buf.append(line)
493+ if not in_dollar and stripped.endswith(";"):
494+ body = [ln for ln in buf if not ln.strip().startswith("--") or in_dollar]
495+ stmt = "\n".join(body).strip()
496+ if stmt:
497+ out.append(stmt)
498+ buf = []
499+ tail = "\n".join(ln for ln in buf if not ln.strip().startswith("--")).strip()
500+ if tail:
501+ out.append(tail)
502+ return out
added package.json +15 −0
@@ -0,0 +1,15 @@
1+{
2+ "name": "ai-atlas",
3+ "version": "0.1.0",
4+ "private": true,
5+ "description": "AI Atlas — explore the entire AI ecosystem. Web workspace (backend is Python, see pyproject.toml).",
6+ "packageManager": "pnpm@11.1.2",
7+ "engines": { "node": ">=22" },
8+ "scripts": {
9+ "dev:web": "pnpm --filter @ai-atlas/web run dev",
10+ "build": "pnpm --filter @ai-atlas/web run build",
11+ "start:web": "pnpm --filter @ai-atlas/web run start",
12+ "typecheck": "pnpm -r run typecheck",
13+ "qa": "pnpm --filter @ai-atlas/web run qa"
14+ }
15+}
added pnpm-workspace.yaml +2 −0
@@ -0,0 +1,2 @@
1+packages:
2+ - apps/*
added pyproject.toml +55 −0
@@ -0,0 +1,55 @@
1+[project]
2+name = "aiatlas"
3+version = "0.1.0"
4+description = "AI Atlas — the global intelligence layer for artificial intelligence: first-party connectors, raw historical archive, temporal knowledge graph, local LLM extraction factory and public API"
5+requires-python = ">=3.12"
6+dependencies = [
7+ "fastapi>=0.115",
8+ "uvicorn[standard]>=0.30",
9+ "pydantic>=2.8",
10+ "pydantic-settings>=2.4",
11+ "sqlalchemy[asyncio]>=2.0.35",
12+ "asyncpg>=0.30",
13+ "alembic>=1.13",
14+ "httpx[http2]>=0.27",
15+ "orjson>=3.10",
16+ "typer>=0.12",
17+ "rich>=13",
18+ "redis>=5.1",
19+ "apscheduler>=3.10,<4",
20+ "python-ulid>=2.7",
21+ "pyyaml>=6",
22+ "python-dateutil>=2.9",
23+ "tenacity>=9",
24+ "python-slugify>=8",
25+ "selectolax>=0.3.21",
26+ "feedparser>=6.0.11",
27+ "markdown-it-py>=3.0",
28+ "pypdf>=5.0",
29+ "rapidfuzz>=3.9",
30+ "pgvector>=0.3",
31+ "numpy>=2",
32+]
33+
34+[project.optional-dependencies]
35+dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6", "respx>=0.21"]
36+browser = ["playwright>=1.47"]
37+
38+[project.scripts]
39+aia = "aiatlas.cli:app"
40+
41+[build-system]
42+requires = ["hatchling"]
43+build-backend = "hatchling.build"
44+
45+[tool.hatch.build.targets.wheel]
46+packages = ["src/aiatlas"]
47+
48+[tool.ruff]
49+line-length = 120
50+target-version = "py312"
51+
52+[tool.pytest.ini_options]
53+testpaths = ["tests"]
54+asyncio_mode = "auto"
55+markers = ["live: hits real external endpoints (skipped unless -m live)"]
added registry/benchmarks.d/.gitkeep +0 −0
added registry/benchmarks.yaml +173 −0
@@ -0,0 +1,173 @@
1+# Benchmark definitions (entities of type `benchmark`). Results come from connectors; definitions are curated with sources.
2+benchmarks:
3+ - key: mmlu
4+ name: MMLU
5+ aliases: [Massive Multitask Language Understanding]
6+ category: knowledge
7+ task: 57-subject multiple-choice questions
8+ metric: accuracy
9+ unit: "%"
10+ creator: Hendrycks et al.
11+ website: https://github.com/hendrycks/test
12+ paper: https://arxiv.org/abs/2009.03300
13+ known_limitations: Widespread training-data contamination; saturated at the frontier.
14+ source_url: https://arxiv.org/abs/2009.03300
15+ - key: mmlu-pro
16+ name: MMLU-Pro
17+ category: knowledge
18+ task: harder, 10-option MMLU variant
19+ metric: accuracy
20+ unit: "%"
21+ website: https://github.com/TIGER-AI-Lab/MMLU-Pro
22+ paper: https://arxiv.org/abs/2406.01574
23+ source_url: https://arxiv.org/abs/2406.01574
24+ - key: gpqa
25+ name: GPQA
26+ aliases: [GPQA Diamond]
27+ category: reasoning
28+ task: graduate-level science questions
29+ metric: accuracy
30+ unit: "%"
31+ website: https://github.com/idavidrein/gpqa
32+ paper: https://arxiv.org/abs/2311.12022
33+ source_url: https://arxiv.org/abs/2311.12022
34+ - key: humaneval
35+ name: HumanEval
36+ category: coding
37+ task: Python function synthesis from docstrings
38+ metric: pass@1
39+ unit: "%"
40+ website: https://github.com/openai/human-eval
41+ paper: https://arxiv.org/abs/2107.03374
42+ known_limitations: Saturated; small (164 problems).
43+ source_url: https://arxiv.org/abs/2107.03374
44+ - key: swe-bench-verified
45+ name: SWE-bench Verified
46+ aliases: [SWE-bench]
47+ category: coding
48+ task: resolve real GitHub issues (500 human-validated instances)
49+ metric: resolved
50+ unit: "%"
51+ website: https://www.swebench.com
52+ paper: https://arxiv.org/abs/2310.06770
53+ known_limitations: Scaffold/agent dependent; results are not comparable across harnesses.
54+ source_url: https://www.swebench.com/
55+ - key: aider-polyglot
56+ name: Aider polyglot
57+ aliases: [Aider polyglot coding leaderboard]
58+ category: coding
59+ task: 225 Exercism exercises in 6 languages, edit-format aware
60+ metric: pass rate (2 attempts)
61+ unit: "%"
62+ website: https://aider.chat/docs/leaderboards/
63+ known_limitations: Depends on aider's edit format and prompting; cost column depends on provider pricing.
64+ source_url: https://aider.chat/docs/leaderboards/
65+ - key: livebench
66+ name: LiveBench
67+ category: general
68+ task: contamination-limited, monthly refreshed questions across 6 categories
69+ metric: average score
70+ unit: "%"
71+ website: https://livebench.ai
72+ paper: https://arxiv.org/abs/2406.19314
73+ source_url: https://livebench.ai/
74+ - key: math-500
75+ name: MATH-500
76+ aliases: [MATH]
77+ category: math
78+ task: competition mathematics
79+ metric: accuracy
80+ unit: "%"
81+ paper: https://arxiv.org/abs/2103.03874
82+ source_url: https://arxiv.org/abs/2103.03874
83+ - key: aime-2025
84+ name: AIME 2025
85+ aliases: [AIME]
86+ category: math
87+ task: American Invitational Mathematics Examination problems
88+ metric: accuracy
89+ unit: "%"
90+ known_limitations: 30 problems per year; high variance; often reported with majority voting.
91+ source_url: https://maa.org/maa-invitational-competitions/
92+ - key: arc-agi
93+ name: ARC-AGI
94+ aliases: [ARC-AGI-1, ARC-AGI-2, Abstraction and Reasoning Corpus]
95+ category: reasoning
96+ task: novel visual abstraction puzzles
97+ metric: accuracy
98+ unit: "%"
99+ website: https://arcprize.org
100+ source_url: https://arcprize.org/
101+ - key: humanitys-last-exam
102+ name: Humanity's Last Exam
103+ aliases: [HLE]
104+ category: knowledge
105+ task: expert-written frontier questions
106+ metric: accuracy
107+ unit: "%"
108+ website: https://lastexam.ai
109+ paper: https://arxiv.org/abs/2501.14249
110+ source_url: https://lastexam.ai/
111+ - key: lmarena-text
112+ name: LMArena text leaderboard
113+ aliases: [Chatbot Arena, LMSYS Chatbot Arena, Arena Elo]
114+ category: preference
115+ task: crowdsourced pairwise human preference
116+ metric: Elo / Bradley–Terry score
117+ unit: ""
118+ website: https://lmarena.ai
119+ known_limitations: Style bias; sampling of prompts by users.
120+ source_url: https://lmarena.ai/
121+ - key: mmmu
122+ name: MMMU
123+ category: multimodal
124+ task: college-level multimodal understanding
125+ metric: accuracy
126+ unit: "%"
127+ website: https://mmmu-benchmark.github.io
128+ paper: https://arxiv.org/abs/2311.16502
129+ source_url: https://arxiv.org/abs/2311.16502
130+ - key: tau-bench
131+ name: τ-bench
132+ aliases: [tau-bench, TAU-bench]
133+ category: agentic
134+ task: tool-agent-user interaction in retail/airline domains
135+ metric: pass^1
136+ unit: "%"
137+ paper: https://arxiv.org/abs/2406.12045
138+ source_url: https://arxiv.org/abs/2406.12045
139+ - key: terminal-bench
140+ name: Terminal-Bench
141+ category: agentic
142+ task: terminal tasks solved by agents
143+ metric: accuracy
144+ unit: "%"
145+ website: https://www.tbench.ai
146+ source_url: https://www.tbench.ai/
147+ - key: ifeval
148+ name: IFEval
149+ category: instruction-following
150+ task: verifiable instruction following
151+ metric: prompt-level strict accuracy
152+ unit: "%"
153+ paper: https://arxiv.org/abs/2311.07911
154+ source_url: https://arxiv.org/abs/2311.07911
155+ - key: mteb
156+ name: MTEB
157+ aliases: [Massive Text Embedding Benchmark]
158+ category: embeddings
159+ task: embedding tasks across retrieval, classification, clustering…
160+ metric: mean score
161+ unit: ""
162+ website: https://huggingface.co/spaces/mteb/leaderboard
163+ paper: https://arxiv.org/abs/2210.07316
164+ source_url: https://arxiv.org/abs/2210.07316
165+ - key: artificial-analysis-intelligence-index
166+ name: Artificial Analysis Intelligence Index
167+ category: composite
168+ task: composite of several evaluations run by Artificial Analysis
169+ metric: index
170+ unit: ""
171+ website: https://artificialanalysis.ai
172+ known_limitations: Proprietary composite; methodology versions change.
173+ source_url: https://artificialanalysis.ai/methodology
added registry/hardware.d/.gitkeep +0 −0
added registry/hardware.yaml +259 −0
@@ -0,0 +1,259 @@
1+# Curated hardware registry (entities of type `hardware`), used by the hardware compatibility engine.
2+# Only manufacturer-published figures; each entry cites the spec page it was read from. Fields not published are omitted.
3+# memory_gb may be a list when the product ships with several configurations.
4+hardware:
5+ # ---------------------------------------------------------------- Apple Silicon (unified memory)
6+ - key: apple-m1
7+ name: Apple M1
8+ manufacturer: apple
9+ kind: soc
10+ architecture: Apple Silicon (Firestorm/Icestorm)
11+ release_date: "2020-11"
12+ memory_gb: [8, 16]
13+ memory_bandwidth_gbs: 68.25
14+ runtimes: [mlx, llama.cpp, coreml]
15+ source_url: https://support.apple.com/en-us/111883
16+ - key: apple-m1-pro
17+ name: Apple M1 Pro
18+ manufacturer: apple
19+ kind: soc
20+ release_date: "2021-10"
21+ memory_gb: [16, 32]
22+ memory_bandwidth_gbs: 200
23+ runtimes: [mlx, llama.cpp, coreml]
24+ source_url: https://support.apple.com/en-us/111902
25+ - key: apple-m1-max
26+ name: Apple M1 Max
27+ manufacturer: apple
28+ kind: soc
29+ release_date: "2021-10"
30+ memory_gb: [32, 64]
31+ memory_bandwidth_gbs: 400
32+ runtimes: [mlx, llama.cpp, coreml]
33+ source_url: https://support.apple.com/en-us/111902
34+ - key: apple-m1-ultra
35+ name: Apple M1 Ultra
36+ manufacturer: apple
37+ kind: soc
38+ release_date: "2022-03"
39+ memory_gb: [64, 128]
40+ memory_bandwidth_gbs: 800
41+ runtimes: [mlx, llama.cpp, coreml]
42+ source_url: https://support.apple.com/en-us/111900
43+ - key: apple-m2
44+ name: Apple M2
45+ manufacturer: apple
46+ kind: soc
47+ release_date: "2022-06"
48+ memory_gb: [8, 16, 24]
49+ memory_bandwidth_gbs: 100
50+ runtimes: [mlx, llama.cpp, coreml]
51+ source_url: https://support.apple.com/en-us/111837
52+ - key: apple-m2-pro
53+ name: Apple M2 Pro
54+ manufacturer: apple
55+ kind: soc
56+ release_date: "2023-01"
57+ memory_gb: [16, 32]
58+ memory_bandwidth_gbs: 200
59+ runtimes: [mlx, llama.cpp, coreml]
60+ source_url: https://support.apple.com/en-us/111837
61+ - key: apple-m2-max
62+ name: Apple M2 Max
63+ manufacturer: apple
64+ kind: soc
65+ release_date: "2023-01"
66+ memory_gb: [32, 64, 96]
67+ memory_bandwidth_gbs: 400
68+ runtimes: [mlx, llama.cpp, coreml]
69+ source_url: https://support.apple.com/en-us/111835
70+ - key: apple-m2-ultra
71+ name: Apple M2 Ultra
72+ manufacturer: apple
73+ kind: soc
74+ release_date: "2023-06"
75+ memory_gb: [64, 128, 192]
76+ memory_bandwidth_gbs: 800
77+ runtimes: [mlx, llama.cpp, coreml]
78+ source_url: https://support.apple.com/en-us/111835
79+ - key: apple-m3
80+ name: Apple M3
81+ manufacturer: apple
82+ kind: soc
83+ release_date: "2023-10"
84+ memory_gb: [8, 16, 24]
85+ memory_bandwidth_gbs: 100
86+ runtimes: [mlx, llama.cpp, coreml]
87+ source_url: https://support.apple.com/en-us/117735
88+ - key: apple-m3-pro
89+ name: Apple M3 Pro
90+ manufacturer: apple
91+ kind: soc
92+ release_date: "2023-10"
93+ memory_gb: [18, 36]
94+ memory_bandwidth_gbs: 150
95+ runtimes: [mlx, llama.cpp, coreml]
96+ source_url: https://support.apple.com/en-us/117736
97+ - key: apple-m3-max
98+ name: Apple M3 Max
99+ manufacturer: apple
100+ kind: soc
101+ release_date: "2023-10"
102+ memory_gb: [36, 48, 64, 96, 128]
103+ memory_bandwidth_gbs: 400
104+ runtimes: [mlx, llama.cpp, coreml]
105+ source_url: https://support.apple.com/en-us/117736
106+ - key: apple-m3-ultra
107+ name: Apple M3 Ultra
108+ manufacturer: apple
109+ kind: soc
110+ release_date: "2025-03"
111+ memory_gb: [96, 256, 512]
112+ memory_bandwidth_gbs: 819
113+ runtimes: [mlx, llama.cpp, coreml]
114+ source_url: https://www.apple.com/mac-studio/specs/
115+ - key: apple-m4
116+ name: Apple M4
117+ manufacturer: apple
118+ kind: soc
119+ release_date: "2024-05"
120+ memory_gb: [16, 24, 32]
121+ memory_bandwidth_gbs: 120
122+ runtimes: [mlx, llama.cpp, coreml]
123+ source_url: https://www.apple.com/mac-mini/specs/
124+ - key: apple-m4-pro
125+ name: Apple M4 Pro
126+ manufacturer: apple
127+ kind: soc
128+ release_date: "2024-10"
129+ memory_gb: [24, 48, 64]
130+ memory_bandwidth_gbs: 273
131+ runtimes: [mlx, llama.cpp, coreml]
132+ source_url: https://www.apple.com/mac-mini/specs/
133+ - key: apple-m4-max
134+ name: Apple M4 Max
135+ manufacturer: apple
136+ kind: soc
137+ release_date: "2024-10"
138+ memory_gb: [36, 48, 64, 128]
139+ memory_bandwidth_gbs: 546
140+ runtimes: [mlx, llama.cpp, coreml]
141+ source_url: https://www.apple.com/mac-studio/specs/
142+ # ---------------------------------------------------------------- NVIDIA data center
143+ - key: nvidia-a100-80gb
144+ name: NVIDIA A100 80GB
145+ manufacturer: nvidia
146+ kind: gpu
147+ architecture: Ampere
148+ release_date: "2020-11"
149+ memory_gb: 80
150+ memory_type: HBM2e
151+ memory_bandwidth_gbs: 2039
152+ tdp_watts: 400
153+ runtimes: [cuda, tensorrt-llm, vllm, sglang, llama.cpp]
154+ source_url: https://www.nvidia.com/en-us/data-center/a100/
155+ - key: nvidia-h100-sxm
156+ name: NVIDIA H100 SXM
157+ manufacturer: nvidia
158+ kind: gpu
159+ architecture: Hopper
160+ release_date: "2022-09"
161+ memory_gb: 80
162+ memory_type: HBM3
163+ memory_bandwidth_gbs: 3350
164+ tdp_watts: 700
165+ runtimes: [cuda, tensorrt-llm, vllm, sglang, llama.cpp]
166+ source_url: https://www.nvidia.com/en-us/data-center/h100/
167+ - key: nvidia-h200
168+ name: NVIDIA H200
169+ manufacturer: nvidia
170+ kind: gpu
171+ architecture: Hopper
172+ release_date: "2024"
173+ memory_gb: 141
174+ memory_type: HBM3e
175+ memory_bandwidth_gbs: 4800
176+ tdp_watts: 700
177+ runtimes: [cuda, tensorrt-llm, vllm, sglang, llama.cpp]
178+ source_url: https://www.nvidia.com/en-us/data-center/h200/
179+ - key: nvidia-b200
180+ name: NVIDIA B200
181+ manufacturer: nvidia
182+ kind: gpu
183+ architecture: Blackwell
184+ release_date: "2024-03"
185+ memory_gb: 180
186+ memory_type: HBM3e
187+ runtimes: [cuda, tensorrt-llm, vllm, sglang]
188+ source_url: https://www.nvidia.com/en-us/data-center/dgx-b200/
189+ - key: nvidia-rtx-4090
190+ name: NVIDIA GeForce RTX 4090
191+ manufacturer: nvidia
192+ kind: gpu
193+ architecture: Ada Lovelace
194+ release_date: "2022-10"
195+ memory_gb: 24
196+ memory_type: GDDR6X
197+ memory_bandwidth_gbs: 1008
198+ tdp_watts: 450
199+ runtimes: [cuda, vllm, llama.cpp, exllama]
200+ source_url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4090/
201+ - key: nvidia-rtx-5090
202+ name: NVIDIA GeForce RTX 5090
203+ manufacturer: nvidia
204+ kind: gpu
205+ architecture: Blackwell
206+ release_date: "2025-01"
207+ memory_gb: 32
208+ memory_type: GDDR7
209+ memory_bandwidth_gbs: 1792
210+ tdp_watts: 575
211+ runtimes: [cuda, vllm, llama.cpp, exllama]
212+ source_url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/
213+ - key: nvidia-rtx-3090
214+ name: NVIDIA GeForce RTX 3090
215+ manufacturer: nvidia
216+ kind: gpu
217+ architecture: Ampere
218+ release_date: "2020-09"
219+ memory_gb: 24
220+ memory_type: GDDR6X
221+ memory_bandwidth_gbs: 936
222+ tdp_watts: 350
223+ runtimes: [cuda, vllm, llama.cpp, exllama]
224+ source_url: https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3090-3090ti/
225+ - key: nvidia-dgx-spark
226+ name: NVIDIA DGX Spark
227+ manufacturer: nvidia
228+ kind: system
229+ architecture: Grace Blackwell (GB10)
230+ release_date: "2025"
231+ memory_gb: 128
232+ memory_type: LPDDR5x
233+ memory_bandwidth_gbs: 273
234+ runtimes: [cuda, tensorrt-llm, vllm, llama.cpp]
235+ source_url: https://www.nvidia.com/en-us/products/workstations/dgx-spark/
236+ # ---------------------------------------------------------------- AMD
237+ - key: amd-instinct-mi300x
238+ name: AMD Instinct MI300X
239+ manufacturer: amd
240+ kind: gpu
241+ architecture: CDNA 3
242+ release_date: "2023-12"
243+ memory_gb: 192
244+ memory_type: HBM3
245+ memory_bandwidth_gbs: 5300
246+ tdp_watts: 750
247+ runtimes: [rocm, vllm, sglang, llama.cpp]
248+ source_url: https://www.amd.com/en/products/accelerators/instinct/mi300/mi300x.html
249+ - key: amd-instinct-mi325x
250+ name: AMD Instinct MI325X
251+ manufacturer: amd
252+ kind: gpu
253+ architecture: CDNA 3
254+ release_date: "2024-10"
255+ memory_gb: 256
256+ memory_type: HBM3e
257+ memory_bandwidth_gbs: 6000
258+ runtimes: [rocm, vllm, sglang]
259+ source_url: https://www.amd.com/en/products/accelerators/instinct/mi300/mi325x.html
added registry/organizations.d/.gitkeep +0 −0
Large diff (0 lines) is not loaded by default to keep the page fast.Load diff
added registry/organizations.yaml +619 −0
Large diff (619 lines) is not loaded by default to keep the page fast.Load diff
added registry/providers.d/.gitkeep +0 −0
Large diff (0 lines) is not loaded by default to keep the page fast.Load diff
added registry/providers.yaml +176 −0
Large diff (176 lines) is not loaded by default to keep the page fast.Load diff
added registry/sources.d/.gitkeep +0 −0
Large diff (0 lines) is not loaded by default to keep the page fast.Load diff
added registry/sources.yaml +387 −0
Large diff (387 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/__init__.py +3 −0
Large diff (3 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/cli.py +295 −0
Large diff (295 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/config.py +94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/connectors/__init__.py +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/connectors/labs/__init__.py +2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/connectors/labs/_common.py +153 −0
Large diff (153 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/connectors/labs/anthropic.py +312 −0
Large diff (312 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/db/__init__.py +75 −0
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/ids.py +93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/logging.py +56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/registry/__init__.py +103 −0
Large diff (103 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/registry/seed.py +168 −0
Large diff (168 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/schemas/__init__.py +17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/schemas/extraction.py +174 −0
Large diff (174 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/__init__.py +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/archive.py +74 −0
Large diff (74 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/connector.py +483 −0
Large diff (483 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/__init__.py +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/dates.py +71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/feeds.py +76 −0
Large diff (76 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/html.py +236 −0
Large diff (236 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/markdown.py +103 −0
Large diff (103 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/numbers.py +106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/extract/sitemap.py +42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/facts.py +222 −0
Large diff (222 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/fetch.py +329 −0
Large diff (329 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/resolution.py +169 −0
Large diff (169 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/sdk/writer.py +357 −0
Large diff (357 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/__init__.py +1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/backup.py +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/cache.py +99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/embeddings.py +83 −0
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/handlers.py +228 −0
Large diff (228 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/jobs.py +124 −0
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/llm/__init__.py +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/llm/gateway.py +231 −0
Large diff (231 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/quality.py +72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/scheduler.py +119 −0
Large diff (119 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/search.py +156 −0
Large diff (156 lines) is not loaded by default to keep the page fast.Load diff
added src/aiatlas/services/stats.py +53 −0
Large diff (53 lines) is not loaded by default to keep the page fast.Load diff
added tests/__init__.py +0 −0
Large diff (0 lines) is not loaded by default to keep the page fast.Load diff
added tests/conftest.py +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/anthropic/model-deprecations.md +223 −0
Large diff (223 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/anthropic/models-overview.md +110 −0
Large diff (110 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/anthropic/news.html +1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/anthropic/pricing.md +526 −0
Large diff (526 lines) is not loaded by default to keep the page fast.Load diff
added tests/test_anthropic.py +63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added tests/test_extract.py +76 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme