Benchmark leaderboard by family + author region/institution
Browse files- data/wam.db +2 -2
- src/wam/pipeline/extract.py +49 -30
- src/wam/pipeline/people.py +26 -8
- src/wam/render/readme.py +32 -9
- src/wam/store/benchmarks.py +33 -0
- src/wam/store/db.py +3 -0
- src/wam/store/papers.py +4 -3
- src/wam/store/people.py +7 -5
- src/wam/store/schema.sql +1 -0
- src/wam/webapp/dashboard.py +6 -5
data/wam.db
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:18a23de4441d7a5fb39a67aa4cf714137d9d373f573c5e0119f9796660bfd241
|
| 3 |
+
size 2027520
|
src/wam/pipeline/extract.py
CHANGED
|
@@ -22,43 +22,62 @@ from wam.store import papers as ps
|
|
| 22 |
log = get_logger("pipeline.extract")
|
| 23 |
|
| 24 |
SYSTEM = (
|
| 25 |
-
"Extract structured experimental results from this
|
|
|
|
| 26 |
"- Only report numbers ACTUALLY stated in the text — never estimate or invent.\n"
|
| 27 |
-
"-
|
| 28 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
| 31 |
-
"
|
| 32 |
-
"-
|
| 33 |
-
"
|
| 34 |
-
"-
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
"-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection,
|
| 41 |
limit: int | None = None) -> int:
|
|
|
|
| 42 |
cap = limit or int(cfg.get("constants.analyze_cap", 40))
|
| 43 |
todo = ps.needs_extract(conn, limit=cap)
|
| 44 |
-
|
|
|
|
| 45 |
done = 0
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
done += 1
|
| 61 |
-
log.debug("%s -> %d variants, %d benchmark rows", row["id"], n_models, n_rows)
|
| 62 |
-
conn.commit()
|
| 63 |
log.info("extracted benchmarks from %d papers", done)
|
| 64 |
return done
|
|
|
|
| 22 |
log = get_logger("pipeline.extract")
|
| 23 |
|
| 24 |
SYSTEM = (
|
| 25 |
+
"Extract structured experimental results from this embodied-AI / World-Action-Model paper. "
|
| 26 |
+
"Rules:\n"
|
| 27 |
"- Only report numbers ACTUALLY stated in the text — never estimate or invent.\n"
|
| 28 |
+
"- Use the benchmark's STANDARD NAME and the variant when given, e.g. LIBERO, LIBERO-Long, "
|
| 29 |
+
"LIBERO-Goal, CALVIN (ABC->D), RoboTwin, SimplerEnv, RLBench, Meta-World, ManiSkill, "
|
| 30 |
+
"RoboCasa, Open-X, ALFWorld, VBench/VBench-Long. NEVER use a table caption ('Table 2'), a "
|
| 31 |
+
"section title, or a single task name ('Put Cube into Cup') as the benchmark — those tasks "
|
| 32 |
+
"go in the 'task' field, with the benchmark in 'benchmark'.\n"
|
| 33 |
+
"- Prefer the HEADLINE/aggregate number per (model, benchmark) (e.g. overall success rate "
|
| 34 |
+
"/ average), not every per-task row. Avoid generic names like 'Average' or 'Table N'.\n"
|
| 35 |
+
"- Each model is identified by (model_name, training_dataset). The SAME name trained/"
|
| 36 |
+
"finetuned on a different dataset is a DIFFERENT variant — capture the training dataset.\n"
|
| 37 |
+
"- Give metric name+value and any inference speed/cost with units + hardware as reported.\n"
|
| 38 |
+
"- claimed_by_authors=false for numbers quoted from OTHER papers (baselines), true for the "
|
| 39 |
+
"paper's own results.\n"
|
| 40 |
+
"- BE CONCISE: at most ~15 rows and ~8 model variants (proposed method + key baselines on "
|
| 41 |
+
"the main benchmarks). Notes under 12 words or omit. Empty lists are fine.")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _extract_one(cfg: Config, client: LLMClient, row) -> tuple[str, object | None]:
|
| 45 |
+
"""Worker: fetch PDF text + extract. Returns (paper_id, ExtractionResult|None). No DB."""
|
| 46 |
+
links = json.loads(row["links_json"] or "{}")
|
| 47 |
+
text = pdf.get_text(cfg, row["id"], links.get("pdf") or "", max_chars=35000)
|
| 48 |
+
body = text or row["abstract"] or ""
|
| 49 |
+
user = (f"Title: {row['title']}\n\nAbstract: {row['abstract'] or ''}\n\n"
|
| 50 |
+
f"Paper text (may be truncated):\n{body}")
|
| 51 |
+
try:
|
| 52 |
+
res = client.complete_json("extract", SYSTEM, user, ExtractionResult,
|
| 53 |
+
label="extract", max_tokens=6000)
|
| 54 |
+
return row["id"], res
|
| 55 |
+
except Exception as e: # noqa: BLE001
|
| 56 |
+
log.warning("extract failed for %s: %s", row["id"], e)
|
| 57 |
+
return row["id"], None
|
| 58 |
|
| 59 |
|
| 60 |
def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection,
|
| 61 |
limit: int | None = None) -> int:
|
| 62 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 63 |
cap = limit or int(cfg.get("constants.analyze_cap", 40))
|
| 64 |
todo = ps.needs_extract(conn, limit=cap)
|
| 65 |
+
workers = int(cfg.get("constants.extract_workers", 6))
|
| 66 |
+
log.info("extracting benchmarks for %d papers (%d workers)", len(todo), workers)
|
| 67 |
done = 0
|
| 68 |
+
# LLM calls run concurrently (I/O-bound); each result is committed in the main thread AS
|
| 69 |
+
# IT COMPLETES (as_completed, not map) so a slow/stuck paper never blocks committing others.
|
| 70 |
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
| 71 |
+
futs = {ex.submit(_extract_one, cfg, client, r): r["id"] for r in todo}
|
| 72 |
+
for fut in as_completed(futs):
|
| 73 |
+
pid, res = fut.result()
|
| 74 |
+
if res is not None:
|
| 75 |
+
bm.store_extraction(conn, pid, res.models, res.benchmarks)
|
| 76 |
+
else:
|
| 77 |
+
conn.execute("UPDATE papers SET benchmarks_extracted=1 WHERE id=?", (pid,))
|
| 78 |
+
conn.commit()
|
| 79 |
+
done += 1
|
| 80 |
+
if done % 20 == 0:
|
| 81 |
+
log.info("extract progress: %d/%d", done, len(todo))
|
|
|
|
|
|
|
|
|
|
| 82 |
log.info("extracted benchmarks from %d papers", done)
|
| 83 |
return done
|
src/wam/pipeline/people.py
CHANGED
|
@@ -25,9 +25,13 @@ from wam.store.people import slug
|
|
| 25 |
log = get_logger("pipeline.people")
|
| 26 |
|
| 27 |
|
| 28 |
-
class
|
| 29 |
directions: str = Field(description="1-2 sentences on this author's main WAM-related "
|
| 30 |
"research directions, grounded in the listed papers")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection,
|
|
@@ -72,21 +76,35 @@ def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection,
|
|
| 72 |
conn.execute("DELETE FROM authors")
|
| 73 |
conn.execute("DELETE FROM groups")
|
| 74 |
|
|
|
|
| 75 |
kept = set(ranked)
|
| 76 |
for aid in ranked:
|
| 77 |
e = influential[aid]
|
| 78 |
titles = "\n".join(f"- {meta[p]['title']}: {meta[p]['tldr']}" for p in e["pids"]
|
| 79 |
if p in meta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
try:
|
| 81 |
-
|
| 82 |
-
"cheap", "
|
| 83 |
-
"
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
except Exception as ex: # noqa: BLE001
|
| 86 |
-
log.warning("
|
| 87 |
-
directions = None
|
| 88 |
store.upsert_author(
|
| 89 |
-
conn, author_id=aid, name=e["name"], affiliation=
|
| 90 |
s2_url=f"https://www.semanticscholar.org/author/{aid}", citations=e["citations"],
|
| 91 |
h_index=e["h_index"], paper_ids=sorted(e["pids"]), directions=directions)
|
| 92 |
conn.commit()
|
|
|
|
| 25 |
log = get_logger("pipeline.people")
|
| 26 |
|
| 27 |
|
| 28 |
+
class AuthorProfile(BaseModel):
|
| 29 |
directions: str = Field(description="1-2 sentences on this author's main WAM-related "
|
| 30 |
"research directions, grounded in the listed papers")
|
| 31 |
+
affiliation: str | None = Field(default=None, description="the author's institution as "
|
| 32 |
+
"stated in the papers' author/affiliation block, else null")
|
| 33 |
+
region: str | None = Field(default=None, description="country/region of that institution "
|
| 34 |
+
"(e.g. 'China', 'USA', 'Singapore'), else null")
|
| 35 |
|
| 36 |
|
| 37 |
def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection,
|
|
|
|
| 76 |
conn.execute("DELETE FROM authors")
|
| 77 |
conn.execute("DELETE FROM groups")
|
| 78 |
|
| 79 |
+
from wam.sources import pdf
|
| 80 |
kept = set(ranked)
|
| 81 |
for aid in ranked:
|
| 82 |
e = influential[aid]
|
| 83 |
titles = "\n".join(f"- {meta[p]['title']}: {meta[p]['tldr']}" for p in e["pids"]
|
| 84 |
if p in meta)
|
| 85 |
+
# Author/affiliation block lives on the PDF first page — give the model that text
|
| 86 |
+
# (cached only, no download) so institution + region are grounded, not guessed.
|
| 87 |
+
blocks = []
|
| 88 |
+
for p in sorted(e["pids"])[:2]:
|
| 89 |
+
txt = pdf.get_text(cfg, p, "", max_chars=2500)
|
| 90 |
+
if txt:
|
| 91 |
+
blocks.append(f"[{meta.get(p,{}).get('title','')}]\n{txt}")
|
| 92 |
+
ctx = (f"Author: {e['name']}\nTracked papers:\n{titles}\n\n"
|
| 93 |
+
f"Author/affiliation blocks (first pages):\n" + "\n---\n".join(blocks))
|
| 94 |
+
aff, region, directions = e["affiliation"], None, None
|
| 95 |
try:
|
| 96 |
+
prof = client.complete_json(
|
| 97 |
+
"cheap", "For the given author, summarize their WAM research directions and, "
|
| 98 |
+
"FROM the author/affiliation blocks only, extract their institution and "
|
| 99 |
+
"country/region. If a field isn't in the text, use null. Write in English.",
|
| 100 |
+
ctx, AuthorProfile, label="author-profile", max_tokens=2000)
|
| 101 |
+
directions = prof.directions
|
| 102 |
+
aff = prof.affiliation or e["affiliation"]
|
| 103 |
+
region = prof.region
|
| 104 |
except Exception as ex: # noqa: BLE001
|
| 105 |
+
log.warning("profile failed for %s: %s", e["name"], ex)
|
|
|
|
| 106 |
store.upsert_author(
|
| 107 |
+
conn, author_id=aid, name=e["name"], affiliation=aff, region=region,
|
| 108 |
s2_url=f"https://www.semanticscholar.org/author/{aid}", citations=e["citations"],
|
| 109 |
h_index=e["h_index"], paper_ids=sorted(e["pids"]), directions=directions)
|
| 110 |
conn.commit()
|
src/wam/render/readme.py
CHANGED
|
@@ -79,20 +79,43 @@ def _core_table(conn: sqlite3.Connection, limit: int = 50) -> str:
|
|
| 79 |
return "\n".join(out) + "\n"
|
| 80 |
|
| 81 |
|
| 82 |
-
def _leaderboard(conn: sqlite3.Connection,
|
|
|
|
|
|
|
|
|
|
| 83 |
rows = conn.execute(
|
| 84 |
"SELECT model_name, training_dataset, benchmark, task, metric_name, metric_value, "
|
| 85 |
-
"claimed_by_authors FROM benchmarks WHERE metric_value IS NOT NULL
|
| 86 |
-
"ORDER BY benchmark, metric_value DESC LIMIT ?", (limit,)).fetchall()
|
| 87 |
if not rows:
|
| 88 |
return "_No benchmark results extracted yet._\n"
|
| 89 |
-
|
| 90 |
-
"|-----------|------|-----------------------|--------|------:|:------:|"]
|
| 91 |
for r in rows:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return "\n".join(out) + "\n"
|
| 97 |
|
| 98 |
|
|
|
|
| 79 |
return "\n".join(out) + "\n"
|
| 80 |
|
| 81 |
|
| 82 |
+
def _leaderboard(conn: sqlite3.Connection, top_per: int = 10) -> str:
|
| 83 |
+
"""Per-benchmark leaderboards grouped by canonical family (key embodied benchmarks first)."""
|
| 84 |
+
from wam.store.benchmarks import BENCH_FAMILIES, normalize_benchmark
|
| 85 |
+
|
| 86 |
rows = conn.execute(
|
| 87 |
"SELECT model_name, training_dataset, benchmark, task, metric_name, metric_value, "
|
| 88 |
+
"claimed_by_authors FROM benchmarks WHERE metric_value IS NOT NULL").fetchall()
|
|
|
|
| 89 |
if not rows:
|
| 90 |
return "_No benchmark results extracted yet._\n"
|
| 91 |
+
fam: dict[str, list] = {}
|
|
|
|
| 92 |
for r in rows:
|
| 93 |
+
f = normalize_benchmark(r["benchmark"])
|
| 94 |
+
if f:
|
| 95 |
+
fam.setdefault(f, []).append(r)
|
| 96 |
+
key = [f for f in BENCH_FAMILIES if f in fam]
|
| 97 |
+
other = sorted((f for f in fam if f not in BENCH_FAMILIES), key=lambda f: -len(fam[f]))
|
| 98 |
+
out = ["_Model identity = (model, training data); same name on different data is a distinct "
|
| 99 |
+
"row. `authors` = self-reported, `3rd-party` = quoted. Higher is better for "
|
| 100 |
+
"success-rate-style metrics._\n"]
|
| 101 |
+
for f in key + other[:12]:
|
| 102 |
+
seen, items = set(), []
|
| 103 |
+
for r in sorted(fam[f], key=lambda x: x["metric_value"], reverse=True):
|
| 104 |
+
k = (r["model_name"], r["training_dataset"], r["metric_name"])
|
| 105 |
+
if k in seen:
|
| 106 |
+
continue
|
| 107 |
+
seen.add(k)
|
| 108 |
+
items.append(r)
|
| 109 |
+
if len(items) >= top_per:
|
| 110 |
+
break
|
| 111 |
+
out.append(f"\n#### {f} · _{len(fam[f])} results_\n")
|
| 112 |
+
out.append("| Model (training data) | Task | Metric | Value | Source |")
|
| 113 |
+
out.append("|-----------------------|------|--------|------:|:------:|")
|
| 114 |
+
for r in items:
|
| 115 |
+
td = f" _({r['training_dataset']})_" if r["training_dataset"] else ""
|
| 116 |
+
src = "authors" if r["claimed_by_authors"] else "3rd-party"
|
| 117 |
+
out.append(f"| {r['model_name']}{td} | {r['task'] or '—'} | {r['metric_name'] or '—'} "
|
| 118 |
+
f"| {r['metric_value']} | {src} |")
|
| 119 |
return "\n".join(out) + "\n"
|
| 120 |
|
| 121 |
|
src/wam/store/benchmarks.py
CHANGED
|
@@ -15,6 +15,39 @@ from datetime import date, datetime
|
|
| 15 |
from wam.pipeline.schemas import BenchmarkRow, ModelVariant
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def variant_key(model_name: str, training_dataset: str | None) -> str:
|
| 19 |
base = f"{model_name}|{training_dataset or 'unknown'}".lower()
|
| 20 |
return re.sub(r"[^a-z0-9]+", "-", base).strip("-")
|
|
|
|
| 15 |
from wam.pipeline.schemas import BenchmarkRow, ModelVariant
|
| 16 |
|
| 17 |
|
| 18 |
+
# Canonical embodied-AI benchmark families (raw name -> family via alias substring match).
|
| 19 |
+
BENCH_FAMILIES: dict[str, list[str]] = {
|
| 20 |
+
"LIBERO": ["libero"],
|
| 21 |
+
"CALVIN": ["calvin"],
|
| 22 |
+
"RoboTwin": ["robotwin", "robo twin"],
|
| 23 |
+
"SimplerEnv": ["simpler"],
|
| 24 |
+
"RLBench": ["rlbench"],
|
| 25 |
+
"Meta-World": ["meta-world", "metaworld", "meta world"],
|
| 26 |
+
"ManiSkill": ["maniskill", "mani-skill", "mani skill"],
|
| 27 |
+
"RoboCasa": ["robocasa"],
|
| 28 |
+
"Open-X / RT": ["open-x", "openx", "open x", "rt-1", "rt-2", "rt-x", "bridgedata", "bridge data"],
|
| 29 |
+
"ALFWorld": ["alfworld"],
|
| 30 |
+
"VBench": ["vbench"],
|
| 31 |
+
"AgiBot / GENIE": ["agibot", "genie"],
|
| 32 |
+
"Habitat": ["habitat"],
|
| 33 |
+
"BEHAVIOR": ["behavior-1k", "behavior1k", "behavior "],
|
| 34 |
+
}
|
| 35 |
+
# Junk that the extractor sometimes emits as a "benchmark".
|
| 36 |
+
_BENCH_JUNK = re.compile(r"^(table\s*\d|fig\.?\s*\d|average|overall|search|real[\s-]?robot|"
|
| 37 |
+
r"main results?|ablation|results?|n/?a|simulation|sim)\b", re.I)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def normalize_benchmark(raw: str | None) -> str | None:
|
| 41 |
+
"""Map a raw benchmark string to a canonical family; return None for junk/empty."""
|
| 42 |
+
if not raw or not raw.strip() or _BENCH_JUNK.match(raw.strip()):
|
| 43 |
+
return None
|
| 44 |
+
low = raw.lower()
|
| 45 |
+
for family, aliases in BENCH_FAMILIES.items():
|
| 46 |
+
if any(a in low for a in aliases):
|
| 47 |
+
return family
|
| 48 |
+
return raw.strip()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
def variant_key(model_name: str, training_dataset: str | None) -> str:
|
| 52 |
base = f"{model_name}|{training_dataset or 'unknown'}".lower()
|
| 53 |
return re.sub(r"[^a-z0-9]+", "-", base).strip("-")
|
src/wam/store/db.py
CHANGED
|
@@ -36,6 +36,9 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|
| 36 |
cols = {r["name"] for r in conn.execute("PRAGMA table_info(papers)")}
|
| 37 |
if "benchmarks_extracted" not in cols:
|
| 38 |
conn.execute("ALTER TABLE papers ADD COLUMN benchmarks_extracted INTEGER DEFAULT 0")
|
|
|
|
|
|
|
|
|
|
| 39 |
conn.commit()
|
| 40 |
|
| 41 |
|
|
|
|
| 36 |
cols = {r["name"] for r in conn.execute("PRAGMA table_info(papers)")}
|
| 37 |
if "benchmarks_extracted" not in cols:
|
| 38 |
conn.execute("ALTER TABLE papers ADD COLUMN benchmarks_extracted INTEGER DEFAULT 0")
|
| 39 |
+
acols = {r["name"] for r in conn.execute("PRAGMA table_info(authors)")}
|
| 40 |
+
if "region" not in acols:
|
| 41 |
+
conn.execute("ALTER TABLE authors ADD COLUMN region TEXT")
|
| 42 |
conn.commit()
|
| 43 |
|
| 44 |
|
src/wam/store/papers.py
CHANGED
|
@@ -87,10 +87,11 @@ def needs_innovation(conn: sqlite3.Connection, limit: int | None = None) -> list
|
|
| 87 |
|
| 88 |
|
| 89 |
def needs_extract(conn: sqlite3.Connection, limit: int | None = None) -> list[sqlite3.Row]:
|
| 90 |
-
# core
|
|
|
|
| 91 |
return conn.execute(_limit_clause(
|
| 92 |
-
"SELECT id, source, title, abstract, links_json FROM papers WHERE
|
| 93 |
-
"
|
| 94 |
|
| 95 |
|
| 96 |
# --- stage writers -----------------------------------------------------------
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def needs_extract(conn: sqlite3.Connection, limit: int | None = None) -> list[sqlite3.Row]:
|
| 90 |
+
# core + adjacent papers not yet benchmark-extracted (VLA/world-model papers report
|
| 91 |
+
# standard embodied benchmarks too). No analysis requirement — extraction reads the PDF.
|
| 92 |
return conn.execute(_limit_clause(
|
| 93 |
+
"SELECT id, source, title, abstract, links_json FROM papers WHERE "
|
| 94 |
+
"track IN ('core','adjacent') AND benchmarks_extracted=0", limit)).fetchall()
|
| 95 |
|
| 96 |
|
| 97 |
# --- stage writers -----------------------------------------------------------
|
src/wam/store/people.py
CHANGED
|
@@ -14,21 +14,23 @@ def slug(name: str) -> str:
|
|
| 14 |
|
| 15 |
def upsert_author(conn: sqlite3.Connection, *, author_id: str, name: str,
|
| 16 |
affiliation: str | None, s2_url: str | None, citations: int | None,
|
| 17 |
-
h_index: int | None, paper_ids: list[str], directions: str | None
|
|
|
|
| 18 |
conn.execute(
|
| 19 |
"""INSERT INTO authors
|
| 20 |
-
(id, name, affiliation, s2_url, citations, h_index, paper_ids_json,
|
| 21 |
-
updated_at)
|
| 22 |
-
VALUES (?,?,?,?,?,?,?,?,?)
|
| 23 |
ON CONFLICT(id) DO UPDATE SET
|
| 24 |
affiliation=COALESCE(excluded.affiliation, affiliation),
|
|
|
|
| 25 |
s2_url=COALESCE(excluded.s2_url, s2_url),
|
| 26 |
citations=COALESCE(excluded.citations, citations),
|
| 27 |
h_index=COALESCE(excluded.h_index, h_index),
|
| 28 |
paper_ids_json=excluded.paper_ids_json,
|
| 29 |
directions=COALESCE(excluded.directions, directions),
|
| 30 |
updated_at=excluded.updated_at""",
|
| 31 |
-
(author_id, name, affiliation, s2_url, citations, h_index, json.dumps(paper_ids),
|
| 32 |
directions, datetime.now().isoformat(timespec="seconds")),
|
| 33 |
)
|
| 34 |
|
|
|
|
| 14 |
|
| 15 |
def upsert_author(conn: sqlite3.Connection, *, author_id: str, name: str,
|
| 16 |
affiliation: str | None, s2_url: str | None, citations: int | None,
|
| 17 |
+
h_index: int | None, paper_ids: list[str], directions: str | None,
|
| 18 |
+
region: str | None = None) -> None:
|
| 19 |
conn.execute(
|
| 20 |
"""INSERT INTO authors
|
| 21 |
+
(id, name, affiliation, region, s2_url, citations, h_index, paper_ids_json,
|
| 22 |
+
directions, updated_at)
|
| 23 |
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
| 24 |
ON CONFLICT(id) DO UPDATE SET
|
| 25 |
affiliation=COALESCE(excluded.affiliation, affiliation),
|
| 26 |
+
region=COALESCE(excluded.region, region),
|
| 27 |
s2_url=COALESCE(excluded.s2_url, s2_url),
|
| 28 |
citations=COALESCE(excluded.citations, citations),
|
| 29 |
h_index=COALESCE(excluded.h_index, h_index),
|
| 30 |
paper_ids_json=excluded.paper_ids_json,
|
| 31 |
directions=COALESCE(excluded.directions, directions),
|
| 32 |
updated_at=excluded.updated_at""",
|
| 33 |
+
(author_id, name, affiliation, region, s2_url, citations, h_index, json.dumps(paper_ids),
|
| 34 |
directions, datetime.now().isoformat(timespec="seconds")),
|
| 35 |
)
|
| 36 |
|
src/wam/store/schema.sql
CHANGED
|
@@ -75,6 +75,7 @@ CREATE TABLE IF NOT EXISTS authors (
|
|
| 75 |
id TEXT PRIMARY KEY, -- semantic scholar id or slug
|
| 76 |
name TEXT NOT NULL,
|
| 77 |
affiliation TEXT,
|
|
|
|
| 78 |
s2_url TEXT,
|
| 79 |
citations INTEGER,
|
| 80 |
h_index INTEGER,
|
|
|
|
| 75 |
id TEXT PRIMARY KEY, -- semantic scholar id or slug
|
| 76 |
name TEXT NOT NULL,
|
| 77 |
affiliation TEXT,
|
| 78 |
+
region TEXT, -- country/region of the affiliation
|
| 79 |
s2_url TEXT,
|
| 80 |
citations INTEGER,
|
| 81 |
h_index INTEGER,
|
src/wam/webapp/dashboard.py
CHANGED
|
@@ -95,13 +95,14 @@ def render(cfg: Config) -> None:
|
|
| 95 |
# --- Authors ---
|
| 96 |
st.subheader("👥 Influential authors")
|
| 97 |
arows = con.execute(
|
| 98 |
-
"SELECT name, citations, h_index, paper_ids_json, directions,
|
| 99 |
-
"ORDER BY json_array_length(paper_ids_json) DESC").fetchall()
|
| 100 |
if arows:
|
| 101 |
adf = pd.DataFrame([{
|
| 102 |
-
"author": n, "
|
| 103 |
-
"
|
| 104 |
-
|
|
|
|
| 105 |
st.dataframe(adf, use_container_width=True, hide_index=True,
|
| 106 |
column_config={"s2": st.column_config.LinkColumn("s2")})
|
| 107 |
else:
|
|
|
|
| 95 |
# --- Authors ---
|
| 96 |
st.subheader("👥 Influential authors")
|
| 97 |
arows = con.execute(
|
| 98 |
+
"SELECT name, region, affiliation, citations, h_index, paper_ids_json, directions, "
|
| 99 |
+
"s2_url FROM authors ORDER BY json_array_length(paper_ids_json) DESC").fetchall()
|
| 100 |
if arows:
|
| 101 |
adf = pd.DataFrame([{
|
| 102 |
+
"author": n, "region": reg or "", "institution": aff or "",
|
| 103 |
+
"papers": len(json.loads(pj or "[]")), "citations": cit, "h_index": h,
|
| 104 |
+
"directions": (d or "")[:200], "s2": url or ""}
|
| 105 |
+
for n, reg, aff, cit, h, pj, d, url in arows])
|
| 106 |
st.dataframe(adf, use_container_width=True, hide_index=True,
|
| 107 |
column_config={"s2": st.column_config.LinkColumn("s2")})
|
| 108 |
else:
|