Two-view app (chat + DB dashboard), web search, rename, boosted links
Browse files- config/config.yaml +5 -3
- data/wam.db +2 -2
- requirements.txt +1 -0
- src/wam/llm/client.py +18 -3
- src/wam/notify/mailer.py +4 -4
- src/wam/pipeline/links.py +25 -17
- src/wam/render/readme.py +3 -3
- src/wam/sources/pdf.py +1 -1
- src/wam/store/benchmarks.py +5 -3
- src/wam/webapp/app.py +51 -23
- src/wam/webapp/dashboard.py +108 -0
- src/wam/webapp/qa.py +26 -10
config/config.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# Awesome-
|
| 2 |
# Everything domain/tuning-related lives here so behavior changes need no code edits.
|
| 3 |
|
| 4 |
provider:
|
|
@@ -6,8 +6,8 @@ provider:
|
|
| 6 |
base_url: "https://openrouter.ai/api/v1"
|
| 7 |
api_key_env: "OPENROUTER_API_KEY"
|
| 8 |
# Sent as OpenRouter attribution headers (optional but recommended).
|
| 9 |
-
http_referer: "https://github.com/
|
| 10 |
-
app_title: "Awesome-
|
| 11 |
|
| 12 |
models:
|
| 13 |
# Tiered routing. Cheap = filtering/first-pass (high volume); mid = analysis; strong =
|
|
@@ -124,6 +124,8 @@ qa:
|
|
| 124 |
# passed to the qa-tier model. See stage_tiers.qa for the model.
|
| 125 |
max_papers: 400 # cap papers in the pack (most-scored first) to bound context
|
| 126 |
include_dropped: true # include filtered-out papers (title-only) so the KB still covers them
|
|
|
|
|
|
|
| 127 |
|
| 128 |
trends:
|
| 129 |
sim_threshold: 0.62 # cosine edge threshold for the direction graph
|
|
|
|
| 1 |
+
# Awesome-Embodied&MM system configuration.
|
| 2 |
# Everything domain/tuning-related lives here so behavior changes need no code edits.
|
| 3 |
|
| 4 |
provider:
|
|
|
|
| 6 |
base_url: "https://openrouter.ai/api/v1"
|
| 7 |
api_key_env: "OPENROUTER_API_KEY"
|
| 8 |
# Sent as OpenRouter attribution headers (optional but recommended).
|
| 9 |
+
http_referer: "https://github.com/wzii/Awesome_Embodied_MM"
|
| 10 |
+
app_title: "Awesome-Embodied&MM"
|
| 11 |
|
| 12 |
models:
|
| 13 |
# Tiered routing. Cheap = filtering/first-pass (high volume); mid = analysis; strong =
|
|
|
|
| 124 |
# passed to the qa-tier model. See stage_tiers.qa for the model.
|
| 125 |
max_papers: 400 # cap papers in the pack (most-scored first) to bound context
|
| 126 |
include_dropped: true # include filtered-out papers (title-only) so the KB still covers them
|
| 127 |
+
web_search: true # also search the web (paper original text, related concepts)
|
| 128 |
+
web_max_results: 5 # web results per query (OpenRouter web plugin; ~$0.02/query)
|
| 129 |
|
| 130 |
trends:
|
| 131 |
sim_threshold: 0.62 # cosine edge threshold for the direction graph
|
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:afa5119bc335d2ad494cd871b265586d52be5b9803ebfd1e9c428e52b48812c7
|
| 3 |
+
size 1961984
|
requirements.txt
CHANGED
|
@@ -8,4 +8,5 @@ requests>=2.31.0
|
|
| 8 |
tenacity>=8.2.0
|
| 9 |
networkx>=3.2
|
| 10 |
streamlit>=1.36.0
|
|
|
|
| 11 |
# No embeddings/vector DB: Q&A is long-context over a knowledge pack built from data/wam.db.
|
|
|
|
| 8 |
tenacity>=8.2.0
|
| 9 |
networkx>=3.2
|
| 10 |
streamlit>=1.36.0
|
| 11 |
+
pandas>=2.0.0
|
| 12 |
# No embeddings/vector DB: Q&A is long-context over a knowledge pack built from data/wam.db.
|
src/wam/llm/client.py
CHANGED
|
@@ -77,7 +77,7 @@ class LLMClient:
|
|
| 77 |
|
| 78 |
# --- core call -------------------------------------------------------------
|
| 79 |
def _call(self, model: str, messages: list[dict], *, temperature: float, max_tokens: int,
|
| 80 |
-
json_mode: bool, label: str) -> tuple[str, int, int]:
|
| 81 |
@retry(
|
| 82 |
reraise=True,
|
| 83 |
stop=stop_after_attempt(self._max_retries),
|
|
@@ -90,6 +90,8 @@ class LLMClient:
|
|
| 90 |
kwargs["max_tokens"] = max_tokens
|
| 91 |
if json_mode:
|
| 92 |
kwargs["response_format"] = {"type": "json_object"}
|
|
|
|
|
|
|
| 93 |
t0 = time.monotonic()
|
| 94 |
resp = self.client.chat.completions.create(**kwargs)
|
| 95 |
dt = time.monotonic() - t0
|
|
@@ -109,13 +111,26 @@ class LLMClient:
|
|
| 109 |
# --- public API ------------------------------------------------------------
|
| 110 |
def complete(self, tier: str, system: str, user: str, *, temperature: float | None = None,
|
| 111 |
max_tokens: int | None = None, label: str = "complete",
|
| 112 |
-
model: str | None = None) -> str:
|
| 113 |
model = model or self.resolve_model(tier)
|
| 114 |
temperature = self.cfg.get("models.defaults.temperature", 0.2) if temperature is None else temperature
|
| 115 |
max_tokens = self.cfg.get("models.defaults.max_tokens", 4096) if max_tokens is None else max_tokens
|
| 116 |
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 117 |
content, _, _ = self._call(model, messages, temperature=temperature,
|
| 118 |
-
max_tokens=max_tokens, json_mode=False, label=label
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
return content
|
| 120 |
|
| 121 |
def complete_json(self, tier: str, system: str, user: str, schema: Type[T], *,
|
|
|
|
| 77 |
|
| 78 |
# --- core call -------------------------------------------------------------
|
| 79 |
def _call(self, model: str, messages: list[dict], *, temperature: float, max_tokens: int,
|
| 80 |
+
json_mode: bool, label: str, extra_body: dict | None = None) -> tuple[str, int, int]:
|
| 81 |
@retry(
|
| 82 |
reraise=True,
|
| 83 |
stop=stop_after_attempt(self._max_retries),
|
|
|
|
| 90 |
kwargs["max_tokens"] = max_tokens
|
| 91 |
if json_mode:
|
| 92 |
kwargs["response_format"] = {"type": "json_object"}
|
| 93 |
+
if extra_body:
|
| 94 |
+
kwargs["extra_body"] = extra_body
|
| 95 |
t0 = time.monotonic()
|
| 96 |
resp = self.client.chat.completions.create(**kwargs)
|
| 97 |
dt = time.monotonic() - t0
|
|
|
|
| 111 |
# --- public API ------------------------------------------------------------
|
| 112 |
def complete(self, tier: str, system: str, user: str, *, temperature: float | None = None,
|
| 113 |
max_tokens: int | None = None, label: str = "complete",
|
| 114 |
+
model: str | None = None, extra_body: dict | None = None) -> str:
|
| 115 |
model = model or self.resolve_model(tier)
|
| 116 |
temperature = self.cfg.get("models.defaults.temperature", 0.2) if temperature is None else temperature
|
| 117 |
max_tokens = self.cfg.get("models.defaults.max_tokens", 4096) if max_tokens is None else max_tokens
|
| 118 |
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 119 |
content, _, _ = self._call(model, messages, temperature=temperature,
|
| 120 |
+
max_tokens=max_tokens, json_mode=False, label=label,
|
| 121 |
+
extra_body=extra_body)
|
| 122 |
+
return content
|
| 123 |
+
|
| 124 |
+
def chat(self, tier: str, messages: list[dict], *, temperature: float | None = None,
|
| 125 |
+
max_tokens: int | None = None, label: str = "chat", model: str | None = None,
|
| 126 |
+
extra_body: dict | None = None) -> str:
|
| 127 |
+
"""Multi-turn: pass a full messages list (system + prior turns + new user message)."""
|
| 128 |
+
model = model or self.resolve_model(tier)
|
| 129 |
+
temperature = self.cfg.get("models.defaults.temperature", 0.2) if temperature is None else temperature
|
| 130 |
+
max_tokens = self.cfg.get("models.defaults.max_tokens", 4096) if max_tokens is None else max_tokens
|
| 131 |
+
content, _, _ = self._call(model, messages, temperature=temperature,
|
| 132 |
+
max_tokens=max_tokens, json_mode=False, label=label,
|
| 133 |
+
extra_body=extra_body)
|
| 134 |
return content
|
| 135 |
|
| 136 |
def complete_json(self, tier: str, system: str, user: str, schema: Type[T], *,
|
src/wam/notify/mailer.py
CHANGED
|
@@ -227,7 +227,7 @@ def build_html(cfg: Config, conn: sqlite3.Connection, today: str | None = None)
|
|
| 227 |
|
| 228 |
S = "font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif"
|
| 229 |
parts = [f'<div style="max-width:640px;margin:auto;{S};color:#1a1a1a">']
|
| 230 |
-
parts.append(f'<h1 style="font-size:20px;margin:0 0 8px">π€
|
| 231 |
new_adj = conn.execute("SELECT count(*) FROM papers WHERE track='adjacent' AND first_seen=?",
|
| 232 |
(today,)).fetchone()[0]
|
| 233 |
total_core, total_adj = counts.get("core", 0), counts.get("adjacent", 0)
|
|
@@ -288,9 +288,9 @@ def build_html(cfg: Config, conn: sqlite3.Connection, today: str | None = None)
|
|
| 288 |
'WAM (bold = weighted 2Γ): spd=inference speed, gen=generalist, spec=specialist, '
|
| 289 |
'cost=inference cost, trust=trustworthiness, collab=collaborative, '
|
| 290 |
'ctrl=controlled generation. βββ = the paper doesnβt address it.</p>')
|
| 291 |
-
parts.append(f'<p style="color:#999;font-size:12px;margin-top:8px">Awesome-
|
| 292 |
-
f'<a href="https://github.com/
|
| 293 |
-
subject = f"
|
| 294 |
return subject, "".join(parts)
|
| 295 |
|
| 296 |
|
|
|
|
| 227 |
|
| 228 |
S = "font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif"
|
| 229 |
parts = [f'<div style="max-width:640px;margin:auto;{S};color:#1a1a1a">']
|
| 230 |
+
parts.append(f'<h1 style="font-size:20px;margin:0 0 8px">π€ Embodied&MM Daily β {today}</h1>')
|
| 231 |
new_adj = conn.execute("SELECT count(*) FROM papers WHERE track='adjacent' AND first_seen=?",
|
| 232 |
(today,)).fetchone()[0]
|
| 233 |
total_core, total_adj = counts.get("core", 0), counts.get("adjacent", 0)
|
|
|
|
| 288 |
'WAM (bold = weighted 2Γ): spd=inference speed, gen=generalist, spec=specialist, '
|
| 289 |
'cost=inference cost, trust=trustworthiness, collab=collaborative, '
|
| 290 |
'ctrl=controlled generation. βββ = the paper doesnβt address it.</p>')
|
| 291 |
+
parts.append(f'<p style="color:#999;font-size:12px;margin-top:8px">Awesome-Embodied&MM Β· '
|
| 292 |
+
f'<a href="https://github.com/wzii/Awesome_Embodied_MM">repo</a></p></div>')
|
| 293 |
+
subject = f"Embodied&MM Daily β {today}: {new_core} new" + ("" if not fallback else " (recap)")
|
| 294 |
return subject, "".join(parts)
|
| 295 |
|
| 296 |
|
src/wam/pipeline/links.py
CHANGED
|
@@ -14,7 +14,7 @@ import sqlite3
|
|
| 14 |
|
| 15 |
from wam.config import Config
|
| 16 |
from wam.logging import get_logger
|
| 17 |
-
from wam.sources import pdf
|
| 18 |
|
| 19 |
log = get_logger("pipeline.links")
|
| 20 |
|
|
@@ -31,6 +31,11 @@ def _clean(url: str) -> str:
|
|
| 31 |
return url.rstrip(").,;:'\"]}").rstrip("/")
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def _best(text: str, pattern: re.Pattern, deny: tuple = ()) -> str | None:
|
| 35 |
cands = [(m.start(), _clean(m.group(0))) for m in pattern.finditer(text)]
|
| 36 |
cands = [(p, u) for p, u in cands if not any(d in u.lower() for d in deny)]
|
|
@@ -44,12 +49,12 @@ def _best(text: str, pattern: re.Pattern, deny: tuple = ()) -> str | None:
|
|
| 44 |
|
| 45 |
|
| 46 |
def run(cfg: Config, conn: sqlite3.Connection, limit: int | None = None,
|
| 47 |
-
download: bool = True) -> int:
|
| 48 |
q = ("SELECT id, abstract, links_json FROM papers WHERE track IN ('core','adjacent')")
|
| 49 |
if limit:
|
| 50 |
q += f" LIMIT {int(limit)}"
|
| 51 |
rows = conn.execute(q).fetchall()
|
| 52 |
-
log.info("links: scanning %d papers (download=%s)", len(rows), download)
|
| 53 |
found = 0
|
| 54 |
for i, r in enumerate(rows):
|
| 55 |
links = json.loads(r["links_json"] or "{}")
|
|
@@ -57,21 +62,24 @@ def run(cfg: Config, conn: sqlite3.Connection, limit: int | None = None,
|
|
| 57 |
continue
|
| 58 |
pdf_url = links.get("pdf") or (f"https://arxiv.org/pdf/{r['id'].split(':',1)[1]}"
|
| 59 |
if r["id"].startswith("arxiv:") else "")
|
| 60 |
-
text = pdf.get_text(cfg, r["id"], pdf_url if download else "", max_chars=
|
| 61 |
-
blob = (text or "") + "\n" + (r["abstract"] or "")
|
| 62 |
-
if not blob.strip():
|
| 63 |
-
continue
|
| 64 |
changed = False
|
| 65 |
-
if
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
if changed:
|
| 76 |
conn.execute("UPDATE papers SET links_json=? WHERE id=?",
|
| 77 |
(json.dumps(links), r["id"]))
|
|
|
|
| 14 |
|
| 15 |
from wam.config import Config
|
| 16 |
from wam.logging import get_logger
|
| 17 |
+
from wam.sources import papers_with_code, pdf
|
| 18 |
|
| 19 |
log = get_logger("pipeline.links")
|
| 20 |
|
|
|
|
| 31 |
return url.rstrip(").,;:'\"]}").rstrip("/")
|
| 32 |
|
| 33 |
|
| 34 |
+
def _prep(text: str) -> str:
|
| 35 |
+
"""Rejoin URLs broken across PDF lines: drop hyphenated breaks, unwrap newlines."""
|
| 36 |
+
return text.replace("-\n", "").replace("\n", " ")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
def _best(text: str, pattern: re.Pattern, deny: tuple = ()) -> str | None:
|
| 40 |
cands = [(m.start(), _clean(m.group(0))) for m in pattern.finditer(text)]
|
| 41 |
cands = [(p, u) for p, u in cands if not any(d in u.lower() for d in deny)]
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
def run(cfg: Config, conn: sqlite3.Connection, limit: int | None = None,
|
| 52 |
+
download: bool = True, use_pwc: bool = True) -> int:
|
| 53 |
q = ("SELECT id, abstract, links_json FROM papers WHERE track IN ('core','adjacent')")
|
| 54 |
if limit:
|
| 55 |
q += f" LIMIT {int(limit)}"
|
| 56 |
rows = conn.execute(q).fetchall()
|
| 57 |
+
log.info("links: scanning %d papers (download=%s, pwc=%s)", len(rows), download, use_pwc)
|
| 58 |
found = 0
|
| 59 |
for i, r in enumerate(rows):
|
| 60 |
links = json.loads(r["links_json"] or "{}")
|
|
|
|
| 62 |
continue
|
| 63 |
pdf_url = links.get("pdf") or (f"https://arxiv.org/pdf/{r['id'].split(':',1)[1]}"
|
| 64 |
if r["id"].startswith("arxiv:") else "")
|
| 65 |
+
text = pdf.get_text(cfg, r["id"], pdf_url if download else "", max_chars=80000)
|
| 66 |
+
blob = _prep((text or "") + "\n" + (r["abstract"] or "")) # rejoin line-broken URLs
|
|
|
|
|
|
|
| 67 |
changed = False
|
| 68 |
+
if blob.strip():
|
| 69 |
+
if not links.get("code"):
|
| 70 |
+
gh = _best(blob, _GH, _GH_DENY)
|
| 71 |
+
if gh:
|
| 72 |
+
links["code"], changed = gh, True
|
| 73 |
+
if not links.get("hf"):
|
| 74 |
+
hf = _best(blob, _HF)
|
| 75 |
+
if hf:
|
| 76 |
+
links["hf"], changed = hf, True
|
| 77 |
+
# Fallback: Papers-with-Code (canonical arXiv->repo) when still no code link.
|
| 78 |
+
if use_pwc and not links.get("code") and r["id"].startswith("arxiv:"):
|
| 79 |
+
url = papers_with_code._repo_for_arxiv(r["id"].split(":", 1)[1],
|
| 80 |
+
cfg.get("constants.request_timeout", 90))
|
| 81 |
+
if url:
|
| 82 |
+
links["code"], changed = _clean(url), True
|
| 83 |
if changed:
|
| 84 |
conn.execute("UPDATE papers SET links_json=? WHERE id=?",
|
| 85 |
(json.dumps(links), r["id"]))
|
src/wam/render/readme.py
CHANGED
|
@@ -180,7 +180,7 @@ def link_integrity(conn: sqlite3.Connection) -> list[str]:
|
|
| 180 |
def render_readme(cfg: Config, conn: sqlite3.Connection, today: str | None = None) -> str:
|
| 181 |
today = today or date.today().isoformat()
|
| 182 |
c = _counts(conn)
|
| 183 |
-
md = f"""# Awesome-
|
| 184 |
|
| 185 |
> Daily-updated intelligence on **World Action Models** β world models, vision-language-action
|
| 186 |
> (VLA) models, action-conditioned video/world generation, robot foundation models, and
|
|
@@ -210,7 +210,7 @@ _Not scored; surfaced for techniques transferable to WAM._
|
|
| 210 |
## π° Embodied / Physical-AI News
|
| 211 |
{_news(conn)}
|
| 212 |
---
|
| 213 |
-
_Generated by [Awesome-
|
| 214 |
"""
|
| 215 |
(cfg.root / "README.md").write_text(md, encoding="utf-8")
|
| 216 |
log.info("wrote README.md")
|
|
@@ -227,7 +227,7 @@ def render_digest(cfg: Config, conn: sqlite3.Connection, today: str | None = Non
|
|
| 227 |
"SELECT title, links_json, scores_json, summary_json FROM papers WHERE track='core' "
|
| 228 |
"AND first_seen=? AND scores_json IS NOT NULL "
|
| 229 |
"ORDER BY json_extract(scores_json,'$.weighted_total') DESC LIMIT 15", (today,)).fetchall()
|
| 230 |
-
lines = [f"#
|
| 231 |
f"**New today:** {new_core} core Β· {new_adj} adjacent papers\n",
|
| 232 |
"## Top new papers\n"]
|
| 233 |
if not rows:
|
|
|
|
| 180 |
def render_readme(cfg: Config, conn: sqlite3.Connection, today: str | None = None) -> str:
|
| 181 |
today = today or date.today().isoformat()
|
| 182 |
c = _counts(conn)
|
| 183 |
+
md = f"""# Awesome-Embodied&MM
|
| 184 |
|
| 185 |
> Daily-updated intelligence on **World Action Models** β world models, vision-language-action
|
| 186 |
> (VLA) models, action-conditioned video/world generation, robot foundation models, and
|
|
|
|
| 210 |
## π° Embodied / Physical-AI News
|
| 211 |
{_news(conn)}
|
| 212 |
---
|
| 213 |
+
_Generated by [Awesome-Embodied&MM](https://github.com/wzii/Awesome_Embodied_MM)._
|
| 214 |
"""
|
| 215 |
(cfg.root / "README.md").write_text(md, encoding="utf-8")
|
| 216 |
log.info("wrote README.md")
|
|
|
|
| 227 |
"SELECT title, links_json, scores_json, summary_json FROM papers WHERE track='core' "
|
| 228 |
"AND first_seen=? AND scores_json IS NOT NULL "
|
| 229 |
"ORDER BY json_extract(scores_json,'$.weighted_total') DESC LIMIT 15", (today,)).fetchall()
|
| 230 |
+
lines = [f"# Embodied&MM Daily Digest β {today}\n",
|
| 231 |
f"**New today:** {new_core} core Β· {new_adj} adjacent papers\n",
|
| 232 |
"## Top new papers\n"]
|
| 233 |
if not rows:
|
src/wam/sources/pdf.py
CHANGED
|
@@ -39,7 +39,7 @@ def get_text(cfg: Config, paper_id: str, pdf_url: str, *, max_chars: int = 60000
|
|
| 39 |
return None
|
| 40 |
try:
|
| 41 |
resp = requests.get(pdf_url, timeout=cfg.get("constants.request_timeout", 90),
|
| 42 |
-
headers={"User-Agent": "Awesome-
|
| 43 |
resp.raise_for_status()
|
| 44 |
with fitz.open(stream=resp.content, filetype="pdf") as doc:
|
| 45 |
text = "\n".join(page.get_text() for page in doc)
|
|
|
|
| 39 |
return None
|
| 40 |
try:
|
| 41 |
resp = requests.get(pdf_url, timeout=cfg.get("constants.request_timeout", 90),
|
| 42 |
+
headers={"User-Agent": "Awesome-Embodied-MM/0.1"})
|
| 43 |
resp.raise_for_status()
|
| 44 |
with fitz.open(stream=resp.content, filetype="pdf") as doc:
|
| 45 |
text = "\n".join(page.get_text() for page in doc)
|
src/wam/store/benchmarks.py
CHANGED
|
@@ -46,9 +46,11 @@ def insert_benchmark(conn: sqlite3.Connection, row: BenchmarkRow, source_paper_i
|
|
| 46 |
metric_value, inference_speed, speed_unit, inference_cost, cost_unit, hardware,
|
| 47 |
source_paper_id, claimed_by_authors, notes, extracted_on)
|
| 48 |
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
int(row.claimed_by_authors), row.notes, date.today().isoformat()),
|
| 53 |
)
|
| 54 |
|
|
|
|
| 46 |
metric_value, inference_speed, speed_unit, inference_cost, cost_unit, hardware,
|
| 47 |
source_paper_id, claimed_by_authors, notes, extracted_on)
|
| 48 |
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
| 49 |
+
# Coalesce the UNIQUE-key text cols to '' so SQLite dedups them (NULLs are all
|
| 50 |
+
# distinct in a UNIQUE index, which would let near-duplicate rows slip through).
|
| 51 |
+
(vk, row.model_name, row.training_dataset, row.benchmark, row.task or "",
|
| 52 |
+
row.split or "", row.metric_name or "", row.metric_value, row.inference_speed,
|
| 53 |
+
row.speed_unit, row.inference_cost, row.cost_unit, row.hardware, source_paper_id,
|
| 54 |
int(row.claimed_by_authors), row.notes, date.today().isoformat()),
|
| 55 |
)
|
| 56 |
|
src/wam/webapp/app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
"""Streamlit
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
Run locally: streamlit run src/wam/webapp/app.py
|
| 4 |
On HF Spaces: set OPENROUTER_API_KEY as a Space secret; this file is the entry point.
|
| 5 |
-
|
| 6 |
-
The OpenRouter key stays server-side. Answers are grounded in a knowledge pack built from the
|
| 7 |
-
committed data/wam.db (paper summaries + scores + leaderboard + authors + trends).
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
|
@@ -17,26 +17,54 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
| 17 |
import streamlit as st # noqa: E402
|
| 18 |
|
| 19 |
from wam.config import load_config # noqa: E402
|
| 20 |
-
from wam.webapp.qa import answer # noqa: E402
|
| 21 |
-
|
| 22 |
-
st.set_page_config(page_title="Awesome-WAM Knowledge Base", page_icon="π€", layout="centered")
|
| 23 |
-
st.title("π€ Awesome-WAM Knowledge Base")
|
| 24 |
-
st.caption("Ask about World Action Models β papers, benchmarks, authors, trends. "
|
| 25 |
-
"Answers are grounded in the tracked corpus and cite paper ids.")
|
| 26 |
|
|
|
|
| 27 |
cfg = load_config()
|
| 28 |
db_exists = cfg.path("db").exists()
|
|
|
|
|
|
|
|
|
|
| 29 |
if not db_exists:
|
| 30 |
-
st.warning("No data
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
st.caption("
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
with st.
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streamlit app β Awesome-Embodied&MM: a chat assistant + a database dashboard.
|
| 2 |
+
|
| 3 |
+
Two views (sidebar): π¬ Chat (multi-turn, long-context Q&A grounded in the corpus + live web
|
| 4 |
+
search) and π Database (visual browse of papers, scores, benchmarks, directions, authors).
|
| 5 |
|
| 6 |
Run locally: streamlit run src/wam/webapp/app.py
|
| 7 |
On HF Spaces: set OPENROUTER_API_KEY as a Space secret; this file is the entry point.
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
|
|
|
| 17 |
import streamlit as st # noqa: E402
|
| 18 |
|
| 19 |
from wam.config import load_config # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
st.set_page_config(page_title="Awesome-Embodied&MM", page_icon="π€", layout="wide")
|
| 22 |
cfg = load_config()
|
| 23 |
db_exists = cfg.path("db").exists()
|
| 24 |
+
|
| 25 |
+
st.sidebar.title("π€ Awesome-Embodied&MM")
|
| 26 |
+
view = st.sidebar.radio("View", ["π¬ Chat", "π Database"], label_visibility="collapsed")
|
| 27 |
if not db_exists:
|
| 28 |
+
st.sidebar.warning("No data yet β run the pipeline to populate data/wam.db.")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _chat() -> None:
|
| 32 |
+
from wam.webapp.qa import answer
|
| 33 |
+
st.title("π¬ Chat")
|
| 34 |
+
st.caption("Ask about World Action Models, VLA, world models & video generation. Grounded "
|
| 35 |
+
"in the tracked corpus (cited by paper id) + live web search for original text "
|
| 36 |
+
"and related concepts.")
|
| 37 |
+
with st.sidebar:
|
| 38 |
+
if st.button("π Clear chat"):
|
| 39 |
+
st.session_state.messages = []
|
| 40 |
+
st.rerun()
|
| 41 |
+
st.session_state.setdefault("messages", [])
|
| 42 |
+
for m in st.session_state.messages:
|
| 43 |
+
with st.chat_message(m["role"]):
|
| 44 |
+
st.markdown(m["content"])
|
| 45 |
+
if q := st.chat_input("Ask a questionβ¦", disabled=not db_exists):
|
| 46 |
+
st.session_state.messages.append({"role": "user", "content": q})
|
| 47 |
+
with st.chat_message("user"):
|
| 48 |
+
st.markdown(q)
|
| 49 |
+
with st.chat_message("assistant"):
|
| 50 |
+
with st.spinner("Thinking (corpus + web)β¦"):
|
| 51 |
+
res = answer(q, history=st.session_state.messages[:-1], cfg=cfg)
|
| 52 |
+
st.markdown(res["answer"])
|
| 53 |
+
web = " + live web" if res.get("web") else ""
|
| 54 |
+
st.caption(f"Grounded in a {res.get('pack_chars', 0):,}-char pack{web}.")
|
| 55 |
+
st.session_state.messages.append({"role": "assistant", "content": res["answer"]})
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _database() -> None:
|
| 59 |
+
from wam.webapp import dashboard
|
| 60 |
+
st.title("π Database")
|
| 61 |
+
if db_exists:
|
| 62 |
+
dashboard.render(cfg)
|
| 63 |
+
else:
|
| 64 |
+
st.info("No data yet.")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
if view.startswith("π¬"):
|
| 68 |
+
_chat()
|
| 69 |
+
else:
|
| 70 |
+
_database()
|
src/wam/webapp/dashboard.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Database dashboard β visual browse of the Awesome-Embodied&MM corpus (Streamlit).
|
| 2 |
+
|
| 3 |
+
Read-only views over data/wam.db: headline metrics, research directions (chart), top scored
|
| 4 |
+
papers with their two-layer dimensions, the benchmark leaderboard, and influential authors.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import sqlite3
|
| 11 |
+
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import streamlit as st
|
| 14 |
+
|
| 15 |
+
from wam.config import Config
|
| 16 |
+
|
| 17 |
+
_WAM_TOP4 = [("inference_speed", "spd"), ("generalist", "gen"),
|
| 18 |
+
("specialist", "spec"), ("inference_cost", "cost")]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _conn(cfg: Config) -> sqlite3.Connection:
|
| 22 |
+
return sqlite3.connect(f"file:{cfg.path('db')}?mode=ro", uri=True)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def render(cfg: Config) -> None:
|
| 26 |
+
con = _conn(cfg)
|
| 27 |
+
|
| 28 |
+
counts = dict(con.execute("SELECT track, count(*) FROM papers GROUP BY track").fetchall())
|
| 29 |
+
nbench = con.execute("SELECT count(*) FROM benchmarks").fetchone()[0]
|
| 30 |
+
nvar = con.execute("SELECT count(*) FROM model_variants").fetchone()[0]
|
| 31 |
+
nauth = con.execute("SELECT count(*) FROM authors").fetchone()[0]
|
| 32 |
+
c = st.columns(6)
|
| 33 |
+
c[0].metric("Core", counts.get("core", 0))
|
| 34 |
+
c[1].metric("Adjacent", counts.get("adjacent", 0))
|
| 35 |
+
c[2].metric("News", counts.get("news", 0))
|
| 36 |
+
c[3].metric("Benchmark rows", nbench)
|
| 37 |
+
c[4].metric("Model variants", nvar)
|
| 38 |
+
c[5].metric("Authors", nauth)
|
| 39 |
+
|
| 40 |
+
# --- Research directions ---
|
| 41 |
+
st.subheader("π Research directions")
|
| 42 |
+
fronts = pd.read_sql_query(
|
| 43 |
+
"SELECT name, size, momentum, summary FROM fronts WHERE snapshot_date="
|
| 44 |
+
"(SELECT max(snapshot_date) FROM fronts) ORDER BY size DESC", con)
|
| 45 |
+
if not fronts.empty:
|
| 46 |
+
st.bar_chart(fronts.set_index("name")["size"], height=320)
|
| 47 |
+
st.dataframe(fronts, use_container_width=True, hide_index=True)
|
| 48 |
+
else:
|
| 49 |
+
st.info("No trend snapshot yet.")
|
| 50 |
+
|
| 51 |
+
# --- Top papers ---
|
| 52 |
+
st.subheader("π Top scored papers")
|
| 53 |
+
track = st.selectbox("Track", ["core", "adjacent", "all"], index=0)
|
| 54 |
+
where = "" if track == "all" else f"AND track='{track}'"
|
| 55 |
+
rows = con.execute(
|
| 56 |
+
f"SELECT id, title, track, published, relevance, scores_json, links_json FROM papers "
|
| 57 |
+
f"WHERE scores_json IS NOT NULL {where} "
|
| 58 |
+
f"ORDER BY json_extract(scores_json,'$.weighted_total') DESC LIMIT 200").fetchall()
|
| 59 |
+
recs = []
|
| 60 |
+
for pid, title, trk, pub, rel, sj, lj in rows:
|
| 61 |
+
s = json.loads(sj or "{}")
|
| 62 |
+
wam = s.get("wam", {})
|
| 63 |
+
links = json.loads(lj or "{}")
|
| 64 |
+
rec = {"score": s.get("weighted_total"), "title": title, "track": trk,
|
| 65 |
+
"published": pub}
|
| 66 |
+
for key, lbl in _WAM_TOP4:
|
| 67 |
+
v = wam.get(key)
|
| 68 |
+
rec[lbl] = v if isinstance(v, int) else None
|
| 69 |
+
rec["code"] = links.get("code") or ""
|
| 70 |
+
rec["abs"] = links.get("abs") or ""
|
| 71 |
+
recs.append(rec)
|
| 72 |
+
if recs:
|
| 73 |
+
df = pd.DataFrame(recs)
|
| 74 |
+
st.dataframe(df, use_container_width=True, hide_index=True, column_config={
|
| 75 |
+
"abs": st.column_config.LinkColumn("abs"),
|
| 76 |
+
"code": st.column_config.LinkColumn("code")})
|
| 77 |
+
st.caption("Score = weighted total. spd/gen/spec/cost = top-4 WAM dims (blank = N/A).")
|
| 78 |
+
else:
|
| 79 |
+
st.info("No scored papers for this track yet.")
|
| 80 |
+
|
| 81 |
+
# --- Benchmark leaderboard ---
|
| 82 |
+
st.subheader("π Benchmark leaderboard")
|
| 83 |
+
bench = pd.read_sql_query(
|
| 84 |
+
"SELECT benchmark, task, model_name AS model, training_dataset AS trained_on, "
|
| 85 |
+
"metric_name AS metric, metric_value AS value, "
|
| 86 |
+
"CASE claimed_by_authors WHEN 1 THEN 'authors' ELSE '3rd-party' END AS source "
|
| 87 |
+
"FROM benchmarks WHERE metric_value IS NOT NULL ORDER BY benchmark, value DESC", con)
|
| 88 |
+
if not bench.empty:
|
| 89 |
+
st.caption("Model identity = (model, training data) β same name on different data is a "
|
| 90 |
+
"distinct row.")
|
| 91 |
+
st.dataframe(bench, use_container_width=True, hide_index=True)
|
| 92 |
+
else:
|
| 93 |
+
st.info("No benchmark rows yet.")
|
| 94 |
+
|
| 95 |
+
# --- Authors ---
|
| 96 |
+
st.subheader("π₯ Influential authors")
|
| 97 |
+
arows = con.execute(
|
| 98 |
+
"SELECT name, citations, h_index, paper_ids_json, directions, s2_url FROM authors "
|
| 99 |
+
"ORDER BY json_array_length(paper_ids_json) DESC").fetchall()
|
| 100 |
+
if arows:
|
| 101 |
+
adf = pd.DataFrame([{
|
| 102 |
+
"author": n, "papers": len(json.loads(pj or "[]")), "citations": cit,
|
| 103 |
+
"h_index": h, "directions": (d or "")[:200], "s2": url or ""}
|
| 104 |
+
for n, cit, h, pj, d, url in arows])
|
| 105 |
+
st.dataframe(adf, use_container_width=True, hide_index=True,
|
| 106 |
+
column_config={"s2": st.column_config.LinkColumn("s2")})
|
| 107 |
+
else:
|
| 108 |
+
st.info("No authors yet.")
|
src/wam/webapp/qa.py
CHANGED
|
@@ -19,10 +19,14 @@ from wam.store import Database
|
|
| 19 |
log = get_logger("webapp.qa")
|
| 20 |
|
| 21 |
SYSTEM = (
|
| 22 |
-
"You answer questions about World Action Models research
|
| 23 |
-
"below (tracked papers with scores, a
|
| 24 |
-
"
|
| 25 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
def build_pack(conn: sqlite3.Connection, max_papers: int = 400,
|
|
@@ -88,7 +92,12 @@ def build_pack(conn: sqlite3.Connection, max_papers: int = 400,
|
|
| 88 |
return "\n\n".join(sections)
|
| 89 |
|
| 90 |
|
| 91 |
-
def answer(question: str,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
cfg = cfg or load_config()
|
| 93 |
client = client or LLMClient(cfg)
|
| 94 |
with Database(cfg) as db:
|
|
@@ -96,8 +105,15 @@ def answer(question: str, cfg: Config | None = None, client: LLMClient | None =
|
|
| 96 |
include_dropped=bool(cfg.get("qa.include_dropped", True)))
|
| 97 |
if not pack.strip():
|
| 98 |
return {"answer": "The knowledge base is empty β run the pipeline first.", "pack_chars": 0}
|
| 99 |
-
#
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
log = get_logger("webapp.qa")
|
| 20 |
|
| 21 |
SYSTEM = (
|
| 22 |
+
"You answer questions about World Action Models research. You have two sources:\n"
|
| 23 |
+
"1) the KNOWLEDGE PACK below β our curated corpus (tracked papers with scores, a "
|
| 24 |
+
"benchmark leaderboard, author directions, trends). Use it for facts about what we track.\n"
|
| 25 |
+
"2) WEB SEARCH results (when provided) β use them for the original paper text, definitions "
|
| 26 |
+
"of related concepts, and anything not in the pack.\n"
|
| 27 |
+
"Cite tracked papers by id like (arxiv:2606.01234) and web sources by their URL. Prefer the "
|
| 28 |
+
"pack for our corpus facts; never invent papers, numbers, or citations. Be concise and "
|
| 29 |
+
"concrete, and write in English.")
|
| 30 |
|
| 31 |
|
| 32 |
def build_pack(conn: sqlite3.Connection, max_papers: int = 400,
|
|
|
|
| 92 |
return "\n\n".join(sections)
|
| 93 |
|
| 94 |
|
| 95 |
+
def answer(question: str, history: list[dict] | None = None, cfg: Config | None = None,
|
| 96 |
+
client: LLMClient | None = None) -> dict:
|
| 97 |
+
"""Answer a question, optionally with prior conversation turns for multi-turn chat.
|
| 98 |
+
|
| 99 |
+
``history`` is a list of {"role": "user"|"assistant", "content": ...} from earlier turns.
|
| 100 |
+
"""
|
| 101 |
cfg = cfg or load_config()
|
| 102 |
client = client or LLMClient(cfg)
|
| 103 |
with Database(cfg) as db:
|
|
|
|
| 105 |
include_dropped=bool(cfg.get("qa.include_dropped", True)))
|
| 106 |
if not pack.strip():
|
| 107 |
return {"answer": "The knowledge base is empty β run the pipeline first.", "pack_chars": 0}
|
| 108 |
+
# Optionally let the model also search the web (paper original text, related concepts)
|
| 109 |
+
# via OpenRouter's web plugin β works with any model.
|
| 110 |
+
extra_body = None
|
| 111 |
+
if bool(cfg.get("qa.web_search", True)):
|
| 112 |
+
extra_body = {"plugins": [{"id": "web",
|
| 113 |
+
"max_results": int(cfg.get("qa.web_max_results", 5))}]}
|
| 114 |
+
messages = [{"role": "system", "content": f"{SYSTEM}\n\nKNOWLEDGE PACK:\n{pack}"}]
|
| 115 |
+
messages += list(history or [])
|
| 116 |
+
messages.append({"role": "user", "content": question})
|
| 117 |
+
# Generous ceiling: the qa-tier model may be a reasoning model (burns tokens first).
|
| 118 |
+
ans = client.chat("qa", messages, label="qa", max_tokens=6000, extra_body=extra_body)
|
| 119 |
+
return {"answer": ans, "pack_chars": len(pack), "web": bool(extra_body)}
|