HardToFindAGoodUserName commited on
Commit
0a1a9f9
Β·
verified Β·
1 Parent(s): 497c541

Two-view app (chat + DB dashboard), web search, rename, boosted links

Browse files
config/config.yaml CHANGED
@@ -1,4 +1,4 @@
1
- # Awesome-WAM system configuration.
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/your-org/Awesome-WAM"
10
- app_title: "Awesome-WAM"
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:f7e93e5fdd9460355f30b196fe25fbf8bcf4b0f2fc6e40354e4ad6a797175324
3
- size 1957888
 
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">πŸ€– WAM 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,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-WAM Β· '
292
- f'<a href="https://github.com/your-org/Awesome-WAM">repo</a></p></div>')
293
- subject = f"WAM Daily β€” {today}: {new_core} new" + ("" if not fallback else " (recap)")
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&amp;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&amp;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=60000)
61
- blob = (text or "") + "\n" + (r["abstract"] or "")
62
- if not blob.strip():
63
- continue
64
  changed = False
65
- if not links.get("code"):
66
- gh = _best(blob, _GH, _GH_DENY)
67
- if gh:
68
- links["code"] = gh
69
- changed = True
70
- if not links.get("hf"):
71
- hf = _best(blob, _HF)
72
- if hf:
73
- links["hf"] = hf
74
- changed = True
 
 
 
 
 
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-WAM
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-WAM](https://github.com/your-org/Awesome-WAM)._
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"# WAM 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:
 
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-WAM/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)
 
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
- (vk, row.model_name, row.training_dataset, row.benchmark, row.task, row.split,
50
- row.metric_name, row.metric_value, row.inference_speed, row.speed_unit,
51
- row.inference_cost, row.cost_unit, row.hardware, source_paper_id,
 
 
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 Q&A app β€” the WAM knowledge base front-end (long-context, no RAG).
 
 
 
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 found yet. Run the pipeline to populate data/wam.db.")
31
-
32
- examples = ["Which VLA models report real-time inference?",
33
- "What are the rising research directions?",
34
- "Who works on world models for autonomous driving?"]
35
- q = st.text_input("Your question", placeholder=examples[0])
36
- st.caption("Try: " + " Β· ".join(f"_{e}_" for e in examples))
37
-
38
- if q and db_exists:
39
- with st.spinner("Reading the corpus and composing an answer…"):
40
- res = answer(q, cfg=cfg)
41
- st.markdown(res["answer"])
42
- st.caption(f"Grounded in a {res['pack_chars']:,}-char knowledge pack.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 using ONLY the knowledge pack "
23
- "below (tracked papers with scores, a benchmark leaderboard, author directions, and "
24
- "trends). Cite papers by their id like (arxiv:2606.01234). If the pack doesn't contain "
25
- "the answer, say so β€” never invent papers, numbers, or citations. Be concise and concrete.")
 
 
 
 
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, cfg: Config | None = None, client: LLMClient | None = None) -> dict:
 
 
 
 
 
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
- # Generous ceiling: the qa-tier model may be a reasoning model (burns tokens before the
100
- # answer); too low a cap yields empty content.
101
- ans = client.complete("qa", SYSTEM, f"KNOWLEDGE PACK:\n{pack}\n\nQUESTION: {question}",
102
- label="qa", max_tokens=6000)
103
- return {"answer": ans, "pack_chars": len(pack)}
 
 
 
 
 
 
 
 
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)}