"""Vesuvius Radar: a public Space that keeps collecting new work that could help read the Herculaneum scrolls, and serves it as a page, a JSON API, an RSS feed and an MCP server. A background collector checks each source on its own schedule (radar.json, every_min) and keeps every item from the last 30 days in one store, so a lookback from 1 minute to 30 days is a filter, not a new scan. Anyone may ask for a check now (POST /api/check), at most once every few minutes for the whole Space. The Space holds no keys and needs no login: every source is public and read logged out. GET / the page GET /api/items items in a lookback window, filtered and sorted (since=1m..30d, view, q, ...) GET /api/item?id=ID one item in full, with the items closest to it in meaning GET /api/overview counts behind the charts for a window and view GET /api/briefing the strongest items per topic for a window and view GET /api/sources the collector: each source's last check, next check and recent runs POST /api/check check every source now (one at a time for the whole Space, with a cooldown) GET /api/views, /api/config the reader views and the scoring settings GET /api/export.csv a window as CSV GET /feed.xml a window as RSS POST /mcp the same as an MCP server (radar_mcp.py) GET /docs the API's OpenAPI page """ from __future__ import annotations import csv import datetime as dt import io import json import os import re import sys import threading import time from pathlib import Path from xml.sax.saxutils import escape as xml_escape from fastapi import Depends, FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse, Response from fastapi.staticfiles import StaticFiles import scanner HERE = Path(__file__).resolve().parent SEED = HERE / "data" / "seed-store.json" DATA_DIR = Path(os.environ.get("RADAR_DATA") or ("/data/radar" if os.access("/data", os.W_OK) else "/tmp/radar")) DATA_DIR.mkdir(parents=True, exist_ok=True) STORE_FILE = DATA_DIR / "store.json" VECS_FILE = DATA_DIR / "vectors.npz" COLLECT = os.environ.get("RADAR_COLLECT", "1") != "0" # tests set 0: no background collector SEMANTIC = os.environ.get("RADAR_SEMANTIC", "1") != "0" PUBLIC_URL = "https://sharpmeow-vesuvius-radar.hf.space" VERSION = "1.2.2" CFG = scanner.load_config() SCORER = scanner.Scorer(CFG) SEM = scanner.Semantic(CFG) HEAD = scanner.Head() LOCK = threading.RLock() STORE: dict = {"saved": None, "items": {}, "sources": {}, "runs": [], "state": {}} CHECK = {"last": 0.0, "requested": None} BOOT = scanner.iso(scanner.utcnow()) # ---------------------------------------------------------------- views (who is reading) VIEWS = { "all": {"label": "Everyone", "about": "Everything the radar kept, from every source.", "any": []}, "research": {"label": "Researchers", "about": "New papers and methods: imaging, sheet and surface segmentation, ink detection, fibers, " "registration, 3D encoders and reading damaged text. arXiv, journals, Hugging Face daily " "papers and Zenodo publications.", "any": [{"sources": ["arxiv", "journals"]}, {"kinds": ["daily-paper", "publication", "preprint", "journal article", "conference paper", "book chapter", "report", "thesis", "presentation", "poster"]}]}, "team": {"label": "Challenge team", "about": "What the community shipped and said about the scrolls: villa pull requests, issues and releases, " "new repositories, Hugging Face uploads and Zenodo records that name the scrolls, and the " "Challenge blog.", "any": [{"sources": ["github"]}, {"sources": ["blog"], "kinds": ["post"]}, {"sources": ["hf", "zenodo", "journals", "arxiv"], "topics": ["Scroll work"]}]}, "builders": {"label": "Contributors", "about": "Things you can use today: new and updated repositories, releases, models, datasets and " "Spaces, package versions, and papers that link their code.", "any": [{"kinds": ["repo", "release", "model", "dataset", "space", "package", "software"]}, {"has_code": True}]}, } def in_view(i: dict, view: str) -> bool: v = VIEWS.get(view or "all") if v is None: raise KeyError(f"unknown view {view}; one of " + ", ".join(VIEWS)) if not v["any"]: return True for c in v["any"]: if "sources" in c and i["source"] not in c["sources"]: continue if "kinds" in c and i["kind"] not in c["kinds"]: continue if "topics" in c and not any(t in i["topics"] for t in c["topics"]): continue if c.get("has_code") and not i.get("code"): continue return True return False # ---------------------------------------------------------------- time windows UNITS = {"m": 60, "min": 60, "h": 3600, "d": 86400, "w": 604800} LOOKBACKS = ["1m", "5m", "15m", "30m", "1h", "3h", "6h", "12h", "1d", "2d", "3d", "7d", "14d", "30d"] def parse_since(s: str | None, default: str = "1d") -> dt.datetime: """A lookback such as 1m, 15m, 6h, 1d or 30d (or an ISO time) as the moment the window starts, at most 30 days ago. ValueError on anything else.""" s = (s or default).strip().lower() now = scanner.utcnow() m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(m|min|h|d|w)", s) if m: secs = float(m.group(1)) * UNITS[m.group(2)] if secs <= 0: raise ValueError("lookback must be above zero") t = now - dt.timedelta(seconds=secs) else: t = scanner.parse_time(s) if not t: raise ValueError(f"lookback '{s}' is not like 15m, 6h, 1d or an ISO time") return max(t, now - dt.timedelta(days=CFG["retention_days"])) # ---------------------------------------------------------------- store def _load(p: Path) -> dict | None: try: d = json.loads(p.read_text()) return d if isinstance(d.get("items"), dict) else None except (OSError, ValueError, AttributeError): return None def load_store(): """The newest of the saved store and the one built into the image.""" cands = [d for d in (_load(STORE_FILE), _load(SEED)) if d] if cands: best = max(cands, key=lambda d: d.get("saved") or "") with LOCK: STORE.update({k: best.get(k, STORE[k]) for k in STORE}) STORE.setdefault("state", {}) prune() def save_store(): with LOCK: STORE["saved"] = scanner.iso(scanner.utcnow()) text = json.dumps(STORE, ensure_ascii=False, separators=(",", ":")) tmp = STORE_FILE.with_suffix(".tmp") tmp.write_text(text) tmp.replace(STORE_FILE) def prune(): cut = scanner.iso(scanner.utcnow() - dt.timedelta(days=CFG["retention_days"] + 1)) with LOCK: for k in [k for k, i in STORE["items"].items() if i["date"] < cut and i.get("last_seen", "") < cut]: del STORE["items"][k] STORE["runs"] = STORE["runs"][-400:] def merge(items: list[dict]) -> int: """Add or update items; returns how many were new to the store.""" now = scanner.iso(scanner.utcnow()) new = 0 with LOCK: for i in items: old = STORE["items"].get(i["id"]) if old: i["first_seen"] = old.get("first_seen", now) if old.get("date") != i["date"]: i["updated"] = True else: i["first_seen"] = now new += 1 i["last_seen"] = now STORE["items"][i["id"]] = i return new # ---------------------------------------------------------------- collector RUNNING: dict[str, dict] = {} def _due(src: str, now: float) -> bool: s = STORE["sources"].get(src) or {} if src in RUNNING: return False if CHECK["requested"] and (s.get("finished") or "") < CHECK["requested"]: return True last = scanner.parse_time(s.get("finished")) return not last or now - last.timestamp() >= 60 * CFG["sources"][src]["every_min"] def run_source(src: str): sc = CFG["sources"][src] now = scanner.utcnow() prev = STORE["sources"].get(src) or {} last_ok = scanner.parse_time(prev.get("ok_since_started")) floor = now - dt.timedelta(days=sc["backfill_days"]) since = max(floor, last_ok - dt.timedelta(hours=6)) if last_ok else floor # overlap so late listings are not missed state = STORE["state"].setdefault(src, {}) RUNNING[src] = {"source": src, "label": sc["label"], "state": "running", "started": scanner.iso(now), "step": "starting"} def prog(row): RUNNING[src] = dict(row) try: items, row = scanner.collect(src, since, CFG, SCORER, SEM if SEM.ready else None, state, prog, HEAD) row["new"] = merge(items) with LOCK: if row["state"] != "error": row["ok_since_started"] = row["started"] else: row["ok_since_started"] = prev.get("ok_since_started") row["every_min"] = sc["every_min"] STORE["sources"][src] = row STORE["runs"].append({k: row[k] for k in ("source", "started", "finished", "since", "state", "read", "kept", "new", "ms")} | {"notes": row["notes"][:3]}) prune() save_store() finally: RUNNING.pop(src, None) warm_vectors() def collector_loop(): if SEMANTIC: SEM.load() SEM.load_cache(VECS_FILE) warm_vectors() while True: now = time.time() for src in scanner.SOURCES: if _due(src, now): threading.Thread(target=run_source, args=(src,), daemon=True, name=f"collect-{src}").start() if CHECK["requested"] and not RUNNING and all( (STORE["sources"].get(s) or {}).get("finished", "") >= CHECK["requested"] for s in scanner.SOURCES): CHECK["requested"] = None time.sleep(5) def request_check() -> tuple[int, dict]: with LOCK: wait = CFG["check_cooldown_s"] - (time.time() - CHECK["last"]) if CHECK["requested"] or RUNNING: return 409, {"ok": False, "reason": "a check is already running", "sources": sources_status()} if wait > 0: return 429, {"ok": False, "reason": f"the last check started under {CFG['check_cooldown_s'] // 60} minutes ago; " f"try again in {int(wait) + 1} s", "retry_after": int(wait) + 1} CHECK["last"] = time.time() CHECK["requested"] = scanner.iso(scanner.utcnow()) return 200, {"ok": True, "requested": CHECK["requested"], "next": "GET /api/sources until every source has finished"} def sources_status() -> dict: now = scanner.utcnow() out = {} for src in scanner.SOURCES: s = dict(STORE["sources"].get(src) or {}) last = scanner.parse_time(s.get("finished")) nxt = (last + dt.timedelta(minutes=CFG["sources"][src]["every_min"])) if last else now out[src] = {"label": scanner.LABELS[src], "every_min": CFG["sources"][src]["every_min"], "state": "running" if src in RUNNING else s.get("state", "waiting"), "step": (RUNNING.get(src) or {}).get("step", ""), "last_check": s.get("finished"), "next_check": scanner.iso(max(nxt, now)), "read": s.get("read", 0), "kept": s.get("kept", 0), "new": s.get("new", 0), "seconds": round(s.get("ms", 0) / 1000, 1), "notes": s.get("notes", []), "items_stored": sum(1 for i in STORE["items"].values() if i["source"] == src)} return out # ---------------------------------------------------------------- queries def _vec_text(i: dict) -> str: return scanner.embed_text(CFG, i["title"], i.get("summary", "")) def warm_vectors(): """Embed every stored item in the background, so similarity and meaning search answer at once.""" if not SEM.ready: return with LOCK: texts = [_vec_text(i) for i in STORE["items"].values()] for k in range(0, len(texts), 256): SEM.embed(texts[k:k + 256]) SEM.save_cache(VECS_FILE) def public(i: dict) -> dict: return named({k: v for k, v in i.items() if not k.startswith("_")}) class Filters: """One set of filters, shared by the page, the API and MCP. Lists are any-of; everything else must hold.""" FIELDS = ("since", "by", "view", "q", "by_meaning", "sources", "kinds", "topics", "bands", "min_score", "has_code", "ids") def __init__(self, since="1d", by="date", view="all", q="", by_meaning=False, sources=None, kinds=None, topics=None, bands=None, min_score=0, has_code=False, ids=None): if view not in VIEWS: raise KeyError(f"unknown view {view}; one of " + ", ".join(VIEWS)) if by not in ("date", "found"): raise ValueError("by must be date (published or updated) or found (first seen by the radar)") self.start = parse_since(since) self.since, self.by, self.view, self.q = since, by, view, (q or "").strip() self.by_meaning = bool(by_meaning) and bool(self.q) self.sources, self.kinds, self.topics, self.bands = sources or [], kinds or [], topics or [], bands or [] self.min_score, self.has_code, self.ids = int(min_score), bool(has_code), set(ids or []) def when(self, i: dict) -> str: return i.get("first_seen", i["date"]) if self.by == "found" else i["date"] def keep(self, i: dict, t0: str, words: list[str]) -> bool: if self.when(i) < t0 or i["score"] < self.min_score or not in_view(i, self.view): return False if self.ids and i["id"] not in self.ids: return False if self.sources and i["source"] not in self.sources: return False if self.kinds and i["kind"] not in self.kinds: return False if self.topics and not any(t in i["topics"] for t in self.topics): return False if self.bands and i["band"] not in self.bands: return False if self.has_code and not i.get("code"): return False if words: act = i.get("activity") or {} hay = " ".join([i["title"], i.get("summary", ""), " ".join(i.get("matched", [])), i.get("who", ""), " ".join(i["topics"]), i["id"], i.get("category", ""), act.get("who", ""), act.get("text", ""), " ".join(person(w) for w in i.get("who", "").split(",")), person(act.get("who", ""))]).lower() if not all(w in hay for w in words): return False return True def query(f: Filters, sort: str = "score") -> list[dict]: """Items that pass the filters. sort: score, date, popularity; a meaning search ranks by meaning instead and adds match (cosine similarity to the query) to each item.""" t0 = scanner.iso(f.start) meaning = f.by_meaning and SEM.ready words = [] if meaning else [w for w in f.q.lower().split() if w] with LOCK: out = [i for i in STORE["items"].values() if f.keep(i, t0, words)] if meaning and out: qv = SEM.embed_query(f.q) sims = SEM.embed([_vec_text(i) for i in out]) @ qv out = [dict(i, match=round(float(s), 3)) for i, s in zip(out, sims)] out.sort(key=lambda i: -i["match"]) return out if sort == "date": out.sort(key=lambda i: f.when(i), reverse=True) elif sort == "popularity": out.sort(key=lambda i: (-((i.get("stars") or 0) + (i.get("likes") or 0) + (i.get("upvotes") or 0)), -i["score"])) else: out.sort(key=lambda i: f.when(i), reverse=True) out.sort(key=lambda i: i["score"], reverse=True) return out def similar(item_id: str, limit: int = 8) -> list[dict]: with LOCK: it = STORE["items"].get(item_id) pool = [i for i in STORE["items"].values() if i["id"] != item_id] if not it: raise KeyError(f"no item {item_id}") if not SEM.ready or not pool: return [] import numpy as np v = SEM.embed([_vec_text(it)])[0] sims = SEM.embed([_vec_text(i) for i in pool]) @ v order = np.argsort(-sims)[:limit] return [{"id": pool[k]["id"], "title": pool[k]["title"], "url": pool[k]["url"], "source": pool[k]["source"], "kind": pool[k]["kind"], "date": pool[k]["date"], "score": pool[k]["score"], "band": pool[k]["band"], "similarity": round(float(sims[k]), 3)} for k in order] def tally(items, keys) -> dict: out: dict = {} for i in items: for k in keys(i): if k: out[k] = out.get(k, 0) + 1 return dict(sorted(out.items(), key=lambda kv: -kv[1])) BIN_STEPS = [(900, 60), (3600, 300), (6 * 3600, 900), (12 * 3600, 1800), (2 * 86400, 3600), (3 * 86400, 3 * 3600), (7 * 86400, 6 * 3600), (14 * 86400, 12 * 3600)] def timeline(f: Filters, items: list[dict]) -> dict: """Items per time bin, per source. The bin width grows with the window (1 minute up to 1 day).""" now = scanner.utcnow() span = (now - f.start).total_seconds() step = next((b for lim, b in BIN_STEPS if span <= lim + 1), 86400) # +1 s: the request itself takes time first = int(f.start.timestamp() // step * step) n = int((now.timestamp() - first) // step) + 1 bins = [{"t": scanner.iso(dt.datetime.fromtimestamp(first + k * step, dt.timezone.utc)), "n": {}} for k in range(n)] for i in items: t = scanner.parse_time(f.when(i)) k = int((t.timestamp() - first) // step) if t else -1 if 0 <= k < n: b = bins[k]["n"] b[i["source"]] = b.get(i["source"], 0) + 1 return {"bin_seconds": step, "bins": bins} WORK_SIM = 0.80 # two items by one person whose texts are this close in meaning are one piece of work # One name per person: an account or nickname listed under "people" in radar.json counts and shows as that person. PEOPLE = {a.strip().lower(): name for name, accounts in CFG.get("people", {}).items() for a in [name, *accounts]} def person(name: str) -> str: return PEOPLE.get(name.strip().lower(), name.strip()) def named(i: dict) -> dict: """A copy of the item with who_name (and activity.who_name) when its author is listed under another name; who keeps the account as the source gave it.""" out = dict(i) w = author(out) if "source" in out else "" if w and person(w) != w: out["who_name"] = person(w) a = out.get("activity") if a and a.get("who") and person(a["who"]) != a["who"]: out["activity"] = {**a, "who_name": person(a["who"])} return out def author(i: dict) -> str: return i.get("who", "").split(",")[0].strip() if i["source"] not in ("pypi", "blog") else "" def top_people(items: list[dict], limit: int = 10) -> list[dict]: """People and organizations ranked by distinct pieces of work, not raw item counts. A person's items are grouped when their texts are close in meaning (cosine >= WORK_SIM, same source), so twenty small related pull requests count as one piece of work; each piece is worth its best score, and the person's total is the sum. Without the meaning model, items from one source on the same day form one piece.""" by: dict[str, list[dict]] = {} names: dict[str, str] = {} for i in items: w = person(author(i)) if w: by.setdefault(w.lower(), []).append(i) names.setdefault(w.lower(), w) rows = [] for key, its in by.items(): n = len(its) parent = list(range(n)) def root(a): while parent[a] != a: parent[a] = parent[parent[a]] a = parent[a] return a if SEM.ready and n > 1: v = SEM.embed([_vec_text(i) for i in its]) sims = v @ v.T for a in range(n): for b in range(a + 1, n): if sims[a, b] >= WORK_SIM and its[a]["source"] == its[b]["source"]: parent[root(a)] = root(b) elif n > 1: first: dict = {} for a, i in enumerate(its): parent[a] = first.setdefault((i["source"], i["date"][:10]), a) groups: dict[int, list[dict]] = {} for a in range(n): groups.setdefault(root(a), []).append(its[a]) value = sum(max(i["score"] for i in g) for g in groups.values()) rows.append({"name": names[key], "value": value, "works": len(groups), "items": n}) rows.sort(key=lambda r: (-r["value"], -r["works"], r["name"].lower())) return rows[:limit] def overview(f: Filters) -> dict: items = query(f) people = top_people(items) hist = [0] * 20 for i in items: hist[min(19, i["score"] // 5)] += 1 matrix: dict = {} for i in items: for t in i["topics"]: row = matrix.setdefault(t, {}) row[i["source"]] = row.get(i["source"], 0) + 1 return {"since": scanner.iso(f.start), "until": scanner.iso(scanner.utcnow()), "view": f.view, "by": f.by, "items": len(items), "high": sum(1 for i in items if i["band"] == "high"), "medium": sum(1 for i in items if i["band"] == "medium"), "by_source": tally(items, lambda i: [i["source"]]), "by_band": tally(items, lambda i: [i["band"]]), "by_topic": tally(items, lambda i: i["topics"]), "by_kind": tally(items, lambda i: [i["kind"]]), "topic_source": matrix, "score_hist": hist, "top_people": {p["name"]: p["value"] for p in people}, "people": people, "community": {"villa_merged": sum(1 for i in items if i["kind"] == "pr" and i.get("state") == "merged"), "new_scroll_repos": sum(1 for i in items if i.get("new_repo") and "Scroll work" in i["topics"])}, "with_code": sum(1 for i in items if i.get("code")), "new_repos": sum(1 for i in items if i.get("new_repo")), "lifted_by_meaning": sum(1 for i in items if i.get("semantic") is not None and not i.get("learned") and i["score"] > i["keyword"]), "learned": {"ready": HEAD.ready, "items": sum(1 for i in items if i.get("learned")), "trained_on": HEAD.info.get("items"), "spearman": (HEAD.info.get("evaluation") or {}).get("full", {}).get("spearman")}, "timeline": timeline(f, items), "semantic": {"model": CFG["semantic"]["model"], "ready": SEM.ready, "error": SEM.error}, "store": {"items": len(STORE["items"]), "saved": STORE.get("saved"), "booted": BOOT}} def update_line(i: dict) -> str: """The latest update in words, for text outputs: 'Comment by X: what it said. Opened by Y. ' or ''.""" a = i.get("activity") or {} if not a.get("who"): return "" said = f": {a['text']}" if a.get("text") else "" end = "" if said[-1:] in (".", "!", "?") else "." return f"{a['what'][:1].upper()}{a['what'][1:]} by {a['who']}{said}{end} Opened by {i.get('who') or 'unknown'}. " def briefing(f: Filters, per_topic: int = 3) -> dict: items = query(f) used, topics = set(), {} for t in tally(items, lambda i: i["topics"]): pick = [i for i in items if t in i["topics"] and i["id"] not in used][:per_topic] if pick: topics[t] = [{k: i.get(k) for k in ("id", "title", "url", "source", "kind", "date", "score", "band", "who", "who_name", "activity", "summary", "code", "keyword", "semantic") if i.get(k) not in (None, "", [])} for i in map(named, pick)] used.update(i["id"] for i in pick) return {"since": scanner.iso(f.start), "view": f.view, "label": VIEWS[f.view]["label"], "about": VIEWS[f.view]["about"], "items": len(items), "topics": topics} # ---------------------------------------------------------------- app app = FastAPI(title="Vesuvius Radar API", version=VERSION, docs_url="/docs", redoc_url=None, description="New papers, code, models and data that could help read the Herculaneum scrolls, collected " "from public sources and scored for relevance. No key or login needed. See /CONNECT.md.") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"]) app.mount("/static", StaticFiles(directory=HERE / "static"), name="static") def _list(s: str | None) -> list[str]: return [x.strip() for x in (s or "").split(",") if x.strip()] @app.get("/", response_class=HTMLResponse, include_in_schema=False) def index(): return FileResponse(HERE / "index.html", headers={"Cache-Control": "no-cache"}) @app.get("/connect.json", include_in_schema=False) def connect_json(): return FileResponse(HERE / "connect.json") @app.get("/connect.html", response_class=HTMLResponse, include_in_schema=False) def connect_html(): return FileResponse(HERE / "connect.html") @app.get("/CONNECT.md", response_class=PlainTextResponse, include_in_schema=False) def connect_md(): return (HERE / "CONNECT.md").read_text() @app.get("/healthz", response_class=PlainTextResponse, include_in_schema=False) def healthz(): return "ok" def filters(since: str = Query("1d", description="Lookback: 1m, 5m, 15m, 30m, 1h, 3h, 6h, 12h, 1d, 2d, 3d, 7d, 14d, 30d " "(any number with m, h, d or w, at most 30 days) or an ISO time"), by: str = Query("date", description="date: when the item was published or last updated; found: when the radar " "first saw it"), view: str = Query("all", description="all, research, team or builders (see /api/views)"), q: str = Query("", description="Words that must all appear, or with by_meaning a phrase ranked by meaning"), by_meaning: bool = Query(False, description="Rank by closeness in meaning to q (sentence embeddings)"), sources: str = Query("", description="Comma list of " + ", ".join(scanner.SOURCES)), kinds: str = Query("", description="Comma list, such as paper,repo,pr,issue,release,model,dataset,space,package"), topics: str = Query("", description="Comma list of topics, such as Scroll work,Ink and imaging"), bands: str = Query("", description="Comma list of high, medium, low"), min_score: int = Query(0, ge=0, le=100), has_code: bool = Query(False, description="Only items that link code"), ids: str = Query("", description="Comma list of item ids (for example, your starred items)")) -> Filters: try: return Filters(since, by, view, q, by_meaning, _list(sources), _list(kinds), _list(topics), _list(bands), min_score, has_code, _list(ids)) except (KeyError, ValueError) as e: raise HTTPException(400, str(e).strip("'\"")) @app.get("/api/items", summary="Items in a lookback window, filtered and sorted") def api_items(f: Filters = Depends(filters), sort: str = Query("score", pattern="^(score|date|popularity)$"), limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), compact: bool = Query(False, description="Drop the score breakdown and cut summaries to 400 characters")): items = query(f, sort) page = [public(i) for i in items[offset:offset + limit]] if compact: page = [{**{k: v for k, v in i.items() if k != "parts"}, "summary": i.get("summary", "")[:400]} for i in page] return JSONResponse({"since": scanner.iso(f.start), "until": scanner.iso(scanner.utcnow()), "view": f.view, "by": f.by, "matched": len(items), "offset": offset, "returned": len(page), "by_meaning": f.by_meaning and SEM.ready, "items": page}, headers={"Cache-Control": "no-cache"}) @app.get("/api/counts", summary="How many items each standard lookback holds, for the same filters") def api_counts(f: Filters = Depends(filters)): items = query(f.__class__("30d", f.by, f.view, f.q if not f.by_meaning else "", False, f.sources, f.kinds, f.topics, f.bands, f.min_score, f.has_code, f.ids)) now = scanner.utcnow() whens = [f.when(i) for i in items] out = {} for lb in LOOKBACKS: t0 = scanner.iso(parse_since(lb)) out[lb] = sum(1 for w in whens if w >= t0) return JSONResponse({"lookbacks": out, "at": scanner.iso(now)}, headers={"Cache-Control": "no-cache"}) @app.get("/api/item", summary="One item in full, with the closest items in meaning") def api_item(id: str): with LOCK: it = STORE["items"].get(id) if not it: raise HTTPException(404, f"no item {id}") return {**public(it), "similar": similar(id)} @app.get("/api/overview", summary="Counts behind the charts, for the same filters as /api/items") def api_overview(f: Filters = Depends(filters)): return JSONResponse(overview(f), headers={"Cache-Control": "no-cache"}) @app.get("/api/briefing", summary="The strongest items per topic") def api_briefing(f: Filters = Depends(filters), per_topic: int = Query(3, ge=1, le=10)): if f.min_score == 0: f.min_score = 35 return briefing(f, per_topic) @app.get("/api/sources", summary="The collector: each source's last and next check") def api_sources(): return JSONResponse({"sources": sources_status(), "check_requested": CHECK["requested"], "check_cooldown_s": CFG["check_cooldown_s"], "runs": STORE["runs"][-60:][::-1], "semantic": {"model": CFG["semantic"]["model"], "ready": SEM.ready, "error": SEM.error}, "store": {"items": len(STORE["items"]), "saved": STORE.get("saved"), "booted": BOOT}}, headers={"Cache-Control": "no-store"}) @app.post("/api/check", summary="Check every source now (cooldown for the whole Space)") def api_check(): code, body = request_check() return JSONResponse(body, status_code=code) @app.get("/api/views", summary="The reader views") def api_views(): return {k: {"label": v["label"], "about": v["about"], "rule": v["any"]} for k, v in VIEWS.items()} def head_summary() -> dict: """What the learned head is, for the page and agents: no coefficients, just how it was fitted and how it did.""" if not HEAD.ready: return {"ready": False} i = HEAD.info return {"ready": True, "trained": i.get("trained"), "items": i.get("items"), "target": i.get("target"), "calibration": i.get("calibration"), "meta_features": i.get("meta_features"), "evaluation": i.get("evaluation")} @app.get("/api/config", summary="Sources, keywords, weights, anchor sentences and the learned head's summary") def api_config(): return {**CFG, "learned": head_summary()} @app.get("/api/export.csv", summary="A window as CSV, for the same filters as /api/items") def export_csv(f: Filters = Depends(filters), sort: str = Query("score", pattern="^(score|date|popularity)$")): items = query(f, sort) buf = io.StringIO() cols = ["score", "band", "keyword", "semantic", "rule", "source", "kind", "date", "first_seen", "title", "url", "who", "topics", "matched", "code", "summary", "update", "update_by", "update_said", "update_url"] w = csv.writer(buf) w.writerow(cols) for i in items: a = i.get("activity") or {} i = {**i, "update": a.get("what", ""), "update_by": a.get("who", ""), "update_said": a.get("text", ""), "update_url": a.get("url", "")} w.writerow([("; ".join(i.get(c) or []) if c in ("topics", "matched", "code") else i.get(c, "")) for c in cols]) name = f"vesuvius-radar-{f.view}-{re.sub(r'[^0-9a-z]', '', f.since.lower())[:12]}-{scanner.iso(scanner.utcnow())[:10]}.csv" return Response(buf.getvalue(), media_type="text/csv", headers={"Content-Disposition": f'attachment; filename="{name}"'}) @app.get("/feed.xml", summary="A window as RSS (default: 7 days, score 60 or more)") def feed(f: Filters = Depends(filters)): if f.since == "1d" and f.min_score == 0: # the defaults of /api/items are too loose for a feed f = Filters("7d", f.by, f.view, f.q, f.by_meaning, f.sources, f.kinds, f.topics, f.bands, 60, f.has_code) items = query(f, "date") rows = [] for i in items[:200]: d = scanner.parse_time(f.when(i)) or scanner.utcnow() desc = f"Score {i['score']} ({i['band']}), {scanner.LABELS.get(i['source'], i['source'])} {i['kind']}. " + update_line(i) + i.get("summary", "") rows.append(f"{xml_escape(i['title'])}{xml_escape(i['url'])}" f'{xml_escape(i["id"])}' f"{d.strftime('%a, %d %b %Y %H:%M:%S +0000')}" f"{xml_escape(scanner.LABELS.get(i['source'], i['source']))}" f"{xml_escape(desc)}") title = f"Vesuvius Radar: {VIEWS[f.view]['label']}, score {f.min_score} or more" body = ('' f"{xml_escape(title)}{PUBLIC_URL}/" "New work that could help read the Herculaneum scrolls, scored for relevance." + "".join(rows) + "") return Response(body, media_type="application/rss+xml") import radar_mcp # noqa: E402 (needs the functions above) radar_mcp.bind(sys.modules[__name__]) app.include_router(radar_mcp.router) @app.on_event("startup") def _start(): load_store() if COLLECT: threading.Thread(target=collector_loop, daemon=True, name="collector").start() elif SEMANTIC: threading.Thread(target=lambda: (SEM.load(), warm_vectors()), daemon=True).start()