AI Atlas — Technical architecture
The global intelligence layer for artificial intelligence: first-party connectors → raw historical archive → deterministic and local-LLM extraction → temporal knowledge graph → search / compare / timeline → www.ai-atlas.co + API.
1. Principles that shape the code
- No critical dependence on third-party data APIs. Every source is read directly (HTML, embedded JSON, JSON-LD, RSS/Atom, sitemaps,
Markdown docs, raw Git files, public JSON/CSV files). Scrapfly / Firecrawl / a headless browser are optional escalation transports
(
Fetcher(escalate=True)), never the database. - Provenance first. Every claim, relation, price, benchmark result and event stores
source_id,snapshot_id,source_url,tier,confidence,extractorandobserved_at. Entity pages can always answer "where does this number come from?". - History is never disposable. Claims are temporal (
valid_from/valid_to, statusescurrent|superseded|conflicting|retracted), prices and benchmark results are append-only, raw snapshots are content-addressed and kept forever, migrations are forward-only. - Deterministic before LLM. Stage 1 (DOM, JSON-LD, tables, Markdown, feeds) runs on every document. The local LLM factory
(MacLustr llm-api.io through an OpenAI-compatible gateway) only sees documents flagged
needs_llm, through versioned pydantic schemas, with full token accounting. - Never fabricate. Missing = missing. Conflicts between sources are stored side by side, flagged and queued for review — never averaged.
2. Topology
┌──────────────── aia schedule (PM2 ai-atlas-scheduler) ─────────────────┐
public AI web ──► │ connectors (registry) → Fetcher (direct/escalate) → archive (raw+text) │
│ → parse → extract (Facts) → FactWriter (resolve, version, diff) │
│ → jobs (llm_extract, embed, reprocess) → worker → LLM gateway │
└──────────────────────────────────────────────────────────────────────────┘
│ Postgres 17 (+pgvector, pg_trgm) · Redis (cache/locks) · AIA_DATA_DIR
┌──────────────────────────────┴───────────────────────────────────────────┐
│ FastAPI /api/v1 (PM2 ai-atlas-api :8321) ◄── Next.js 16 (PM2 ai-atlas-web :8320) ◄── MacLustr Tunnel (BHS64 Caddy)
└──────────────────────────────────────────────────────────────────────────┘Production node: M2M32c (Mac Studio M2, 12 c / 32 GB, dedicated), Postgres 17 + pgvector + Redis via Homebrew, data in
~/ai-atlas-data/ (raw, text, backups, logs). Public route https://www.ai-atlas.co → M2M32c:8320 on the MacLustr Tunnel gateway BHS64.
Workers can run on any node with database access (aia worker), which is how the LLM factory scales across the cluster.
3. Repository layout
registry/ curated YAML: sources.yaml (domains, tiers, crawl policy), organizations.yaml (aliases, domains, hf/github orgs),
providers.yaml, benchmarks.yaml, hardware.yaml — every entry carries its source_url
migrations/versions/ forward-only SQL (0001_initial.py)
src/aiatlas/
config.py settings (AIA_*), ids.py (prefixed ULIDs, slugify, normalize_alias), logging.py, db/ (SQLAlchemy Core + asyncpg)
sdk/ connector SDK
fetch.py Fetcher: robots.txt, per-domain rate limit, ETag/If-Modified-Since, retries/backoff, escalation chain
archive.py content-addressed gzip store (raw/, text/) with dedupe
extract/ html (selectolax; meta, JSON-LD, embedded JSON, tables, links, text), feeds, sitemap, markdown (front matter, tables),
numbers (70B, 128K, $3/1M), dates (partial precision)
facts.py EntityRef, Claim, Relation, Event, PriceObs, ResultObs, Target, Facts (+ MATERIAL_PROPERTIES → event types)
resolution.py Resolver: identifiers → aliases (org-disambiguated) → slug → create; ambiguity → review queue
writer.py FactWriter: temporal claims, attributes/provenance materialisation, conflicts, prices, results, events
connector.py BaseConnector.run(): documents, snapshots, hash change detection, structural diff, DOCUMENT_CHANGED,
breakage detection, circuit breaker, adaptive interval, follow-up targets, --file overrides, reprocess mode
connectors/ one package per group (labs/, hub/, research/, code/, providers/, benchmarks/, hardware/); CONNECTORS = [cls]
schemas/extraction.py pydantic schemas: ModelPassport, PricingExtraction, CompanyPassport, PaperPassport, BenchmarkResultExtraction, HardwareSpec, …
services/
jobs.py Postgres queue (SKIP LOCKED), handlers.py (llm_extract, embed_entity, reprocess_snapshot, recompute_quality)
llm/gateway.py LLMGateway: stage cascade small→medium→large, OpenAI-compatible engine, JSON validation, llm_jobs accounting
embeddings.py local embeddings → pgvector · search.py FTS+trigram(+vector) and NL→filters compiler
scheduler.py APScheduler: due connectors (Redis lock), worker, hourly stats/quality/embeddings, nightly pg_dump
quality.py transparency scores (documented in metric_definitions) · stats.py live counters · backup.py · cache.py
api/ FastAPI application (see docs/API.md)
cli.py `aia`: migrate seed connectors run reprocess crawl status stats quality schedule worker api backup llm embed search review
apps/web/ Next.js 16 site (see docs/FRONTEND.md)
tests/ pytest with saved fixtures (tests/fixtures/<connector>/…); live tests behind `-m live`
deploy/ mld manifest (ai-atlas.mld.json), render-manifest.sh, first-run.sh — see docs/DEPLOY.md4. Data model (Postgres)
| table | role |
|---|---|
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 |
entity_aliases, entity_identifiers |
resolution keys (normalized aliases; (scheme, value) unique: hf_repo, github_repo, arxiv, doi, anthropic_model_id, openrouter, pypi, domain…) |
sources, connectors, connector_runs, connector_errors |
source registry with tiers; connector state (health, adaptive interval, circuit, parser_version, expected_min_records) |
documents |
one row per URL: validators (etag/last-modified), content_hash, counters, entity_id, needs_llm |
snapshots |
one row per changed fetch: raw_path, text_path, structured (deterministic summary), diff, transport, parser_version, processing_status |
claims |
temporal facts: (entity_id, property, value jsonb, unit, tier, confidence, status, valid_from, valid_to, snapshot_id, source_url, extractor) |
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 |
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 |
prices |
append-only provider pricing (valid_to closes a row): per-1M-token input/output/cached/cache-write/batch, per image/request, context, features |
benchmark_results |
append-only results with config (harness, prompting, judge…) — never compared blindly |
jobs, llm_jobs |
work queue; LLM cost accounting (stage, model, node, tokens, duration, status) |
review_queue |
merge candidates, conflicts, blocked sources, parser breakage, unusual changes |
entity_embeddings |
pgvector(1024) local embeddings |
domains, stats_snapshots, page_views, api_keys, metric_definitions |
trust graph, counters history, trending, developer keys, documented metrics |
Identifiers: prefixed ULIDs (model_01J…, company_…, paper_…, snap_…, evt_…). Slugs are stable, readable and unique.
Temporal rules (FactWriter)
no current claim → insert current, set attribute, NEW_<TYPE> event when the entity is new
same value → confirm (observed_at)
different, source ≥ tier → supersede (valid_to = observed), insert current, CHANGE event for material properties
different, worse source → store as `conflicting`, mark current `conflicted`, review-queue item — never overwrite
soft text (description…) → first statement wins until its own source changes; never an event
metric.* / stats.* → time series without eventsHistorical mode = claims filtered by valid_from <= date < coalesce(valid_to, ∞); "did it exist?" = entities.first_seen_at <= date.
Diff A→B = new/removed entities by first_seen_at, claims superseded between A and B, price rows opened/closed, results observed.
5. Connector lifecycle
discover() returns Targets (URL, doc_type, optional entity hint, needs_llm, key for fixtures). For each target run():
fetch (conditional) → unchanged? stop · changed → archive raw + text → snapshot row (+ structural diff vs previous) → parse() →
extract() → FactWriter.write(facts) → follow-up targets → optional llm_extract job. Blocked (401/403/451/robots) → document
blocked + review item; 404/410 twice → gone (never deletes entities). Fewer records than expected_min_records → run suspect
- review item. Intervals adapt: ×0.7 on change (≥ min), ×1.5 after 3 unchanged runs (≤ max). Three failures open the circuit 30 min.
Parser improvements: bump parser_version, then aia reprocess <connector> replays the latest stored snapshot of every document — no crawl.
6. LLM factory
LLMGateway.extract(task_type, document, schema, stage) → strict JSON validated by a pydantic schema; on schema errors the next stage
model is tried (small → medium → large); transport failures do not escalate. Every call is recorded in llm_jobs (tokens, duration,
node, status, output). facts_from_llm() maps outputs onto Facts with extractor='llm' and medium/low confidence, so deterministic
tier-1 claims always win conflicts. Embeddings use the same gateway (/v1/embeddings, Qwen3-Embedding 1024-d) into pgvector.
Without a configured gateway the platform degrades gracefully: deterministic extraction, FTS search, no embeddings.
7. Quality, search, events
quality.score= 100·(0.25 completeness + 0.25 primary-source ratio + 0.20 freshness + 0.15 agreement + 0.15 source diversity), versioned and explained inmetric_definitionsand/methodology. It measures how well AI Atlas knows an entity, not the entity.- Search: tsvector (name A, family B, type/description C) +
pg_trgmsimilarity + aliases (+ cosine on embeddings when present).compile_query()turns "open models released in 2026 with more than 100B parameters and 128k context" into structured filters. - Events power the homepage feed, entity timelines,
/changes, the daily digest and, later, watchlists/alerts.
8. Security & compliance
Admin routes require x-aia-admin-token. The API binds to loopback; Next.js rewrites /api/v1/* to it. No credentials or internal
addresses in responses. Crawling honours robots.txt, identifies itself (AIAtlasBot/0.1 … contact@spboucher.ai), rate-limits per domain,
uses conditional requests, never bypasses access controls; blocked sources go to the review queue. Raw archive content is never exposed
publicly — only extracted facts with links to the original page.
9. Backups & durability
Nightly pg_dump -Fc into AIA_DATA_DIR/backups (30 kept), off-node rsync (scripts/backup-offnode.sh), raw archive is append-only and
content-addressed (rsync-friendly). The dataset is the product: restore = pg_restore + point DATABASE_URL.