mnawfal29 commited on
Commit
b89c27d
·
verified ·
1 Parent(s): e290bbe

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -15,9 +15,16 @@ FROM ${BASE_IMAGE} AS builder
15
 
16
  WORKDIR /app
17
 
18
- # Ensure git is available (required for installing dependencies from VCS)
19
  RUN apt-get update && \
20
- apt-get install -y --no-install-recommends git && \
 
 
 
 
 
 
 
21
  rm -rf /var/lib/apt/lists/*
22
 
23
  # Build argument to control whether we're building standalone or in-repo
@@ -54,6 +61,13 @@ RUN --mount=type=cache,target=/root/.cache/uv \
54
  uv sync --no-editable; \
55
  fi
56
 
 
 
 
 
 
 
 
57
  # Final runtime stage
58
  FROM ${BASE_IMAGE}
59
 
 
15
 
16
  WORKDIR /app
17
 
18
+ # Ensure git + Node are available. Node is needed to build the React frontend.
19
  RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git curl ca-certificates gnupg && \
21
+ mkdir -p /etc/apt/keyrings && \
22
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
23
+ | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
24
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
25
+ > /etc/apt/sources.list.d/nodesource.list && \
26
+ apt-get update && \
27
+ apt-get install -y --no-install-recommends nodejs && \
28
  rm -rf /var/lib/apt/lists/*
29
 
30
  # Build argument to control whether we're building standalone or in-repo
 
61
  uv sync --no-editable; \
62
  fi
63
 
64
+ # Build the React frontend
65
+ WORKDIR /app/env/frontend
66
+ RUN --mount=type=cache,target=/root/.npm \
67
+ npm install --no-audit --no-fund --prefer-offline && \
68
+ npm run build
69
+ WORKDIR /app/env
70
+
71
  # Final runtime stage
72
  FROM ${BASE_IMAGE}
73
 
demo/ui.py CHANGED
@@ -21,6 +21,30 @@ import numpy as np
21
  import plotly.graph_objects as go
22
  from plotly.subplots import make_subplots
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
  from ..arena import auto_test_draft, run_arena
26
  from ..landscapes import BUILDERS, build_landscape, structural_hints
@@ -1418,7 +1442,7 @@ def _api_reset(tier, seed):
1418
  obs = env.reset()
1419
  _API_ENV_STATE["env"] = env
1420
  return (
1421
- obs.model_dump(exclude_none=True),
1422
  f"✓ Reset complete · landscape: **{obs.landscape_description}** · "
1423
  f"dim = {obs.dim} · budget = {obs.budget_remaining}",
1424
  )
@@ -1445,7 +1469,7 @@ def _api_step(kind, baseline_name, code, draft_idx, step_start, step_end):
1445
  return {"error": str(e)}, f"❌ Invalid action: {e}"
1446
 
1447
  obs = env.step(action)
1448
- dump = obs.model_dump(exclude_none=True)
1449
  banner = (
1450
  f"✓ {kind} executed · budget remaining = {obs.budget_remaining}"
1451
  + (" · **episode done**" if obs.done else "")
@@ -1513,7 +1537,7 @@ def _llm_auto_run(endpoint_choice, custom_url, api_key, model_name,
1513
  f"**Dim:** {obs.dim} · **Initial budget:** {obs.budget_remaining}",
1514
  "",
1515
  ]
1516
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1517
 
1518
  for turn in range(1, int(max_turns) + 1):
1519
  messages = build_prompt(obs)
@@ -1528,12 +1552,12 @@ def _llm_auto_run(endpoint_choice, custom_url, api_key, model_name,
1528
  }, timeout=180)
1529
  if r.status_code >= 400:
1530
  log_lines.append(f"**[LLM error {r.status_code}]** {r.text[:300]}")
1531
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1532
  return
1533
  raw = r.json()["choices"][0]["message"]["content"]
1534
  except Exception as e:
1535
  log_lines.append(f"**[request failed]** `{type(e).__name__}: {e}`")
1536
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1537
  return
1538
 
1539
  dt = _time.time() - t0
@@ -1545,7 +1569,7 @@ def _llm_auto_run(endpoint_choice, custom_url, api_key, model_name,
1545
  f"**[turn {turn}] parse error:** `{e}`"
1546
  f"\n```\n{raw[:500]}\n```\n"
1547
  )
1548
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1549
  return
1550
 
1551
  obs = env.step(action)
@@ -1619,7 +1643,7 @@ def _llm_auto_run(endpoint_choice, custom_url, api_key, model_name,
1619
  log_lines.append(f"```python\n{action.code.strip()}\n```")
1620
  log_lines.append(f"")
1621
 
1622
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1623
 
1624
  if obs.done:
1625
  bk = obs.r_optcoder_breakdown or {}
@@ -1676,12 +1700,12 @@ def _llm_auto_run(endpoint_choice, custom_url, api_key, model_name,
1676
  "-r_eval_fail": -bk.get("r_eval_failures", 0),
1677
  }, reward_val)
1678
  yield ("\n".join(log_lines),
1679
- obs.model_dump(exclude_none=True),
1680
  reward_plot)
1681
  return
1682
 
1683
  log_lines.append("\n**[!] Reached max turns without commit** — episode unfinished.")
1684
- yield ("\n".join(log_lines), obs.model_dump(exclude_none=True), None)
1685
 
1686
 
1687
  # ----------------- top-level UI -----------------
@@ -1789,8 +1813,8 @@ def build_ui(*args, **kwargs) -> gr.Blocks:
1789
  gr.HTML(HERO_HTML)
1790
 
1791
  with gr.Tabs():
1792
- # --- Tab 0: OpenEnv (primary — LLM auto-run) ---
1793
- with gr.Tab("OpenEnv"):
1794
  with gr.Row(equal_height=False):
1795
  # -------- MAIN PANE (left, wider) --------
1796
  with gr.Column(scale=4, min_width=640):
@@ -1805,8 +1829,9 @@ def build_ui(*args, **kwargs) -> gr.Blocks:
1805
  llm_reward_plot = gr.Plot(
1806
  label="Reward breakdown (on episode end)")
1807
  with gr.Column(scale=1):
1808
- latest_obs = gr.JSON(label="Latest observation",
1809
- height=240, open=False)
 
1810
 
1811
  # -------- SIDEBAR (right, narrower) --------
1812
  with gr.Column(scale=1, min_width=300, elem_classes="lf-sidebar"):
@@ -1849,6 +1874,18 @@ def build_ui(*args, **kwargs) -> gr.Blocks:
1849
  run_btn = gr.Button("▶ Run episode", variant="primary",
1850
  size="lg")
1851
 
 
 
 
 
 
 
 
 
 
 
 
 
1852
  run_btn.click(
1853
  _llm_auto_run,
1854
  [ep_choice, custom_url_in, key_in, model_name_in,
@@ -1857,7 +1894,7 @@ def build_ui(*args, **kwargs) -> gr.Blocks:
1857
  )
1858
 
1859
  # --- Tab: Manual stepping (raw /reset + /step) ---
1860
- with gr.Tab("Manual"):
1861
  with gr.Row(equal_height=False):
1862
  with gr.Column(scale=1, min_width=340, elem_classes="lf-sidebar"):
1863
  gr.Markdown("### Manual stepping")
@@ -1896,10 +1933,13 @@ def build_ui(*args, **kwargs) -> gr.Blocks:
1896
  with gr.Column(scale=2, min_width=580):
1897
  status4 = gr.Markdown(
1898
  "*No active env — hit **Reset env** to begin.*")
1899
- obs4_reset = gr.JSON(label="Initial observation",
1900
- height=280)
 
1901
  status4b = gr.Markdown()
1902
- obs4 = gr.JSON(label="Step observation", height=320)
 
 
1903
 
1904
  reset_btn.click(_api_reset, [tier4, seed4],
1905
  [obs4_reset, status4])
 
21
  import plotly.graph_objects as go
22
  from plotly.subplots import make_subplots
23
 
24
+
25
+ def _fmt_obs(obs_dict: dict) -> str:
26
+ """Pretty-print an observation as indented JSON for gr.Code display.
27
+
28
+ Shrinks very long arrays (baseline trajectories etc.) so the rendered view
29
+ stays readable. `json.dumps(indent=2)` gives one value per line which
30
+ looks much cleaner than gr.JSON's component-per-field tree.
31
+ """
32
+ def _shrink(v):
33
+ if isinstance(v, list):
34
+ if len(v) > 8:
35
+ return (
36
+ [_shrink(x) for x in v[:3]]
37
+ + [f"... ({len(v)-6} more) ..."]
38
+ + [_shrink(x) for x in v[-3:]]
39
+ )
40
+ return [_shrink(x) for x in v]
41
+ if isinstance(v, dict):
42
+ return {k: _shrink(x) for k, x in v.items()}
43
+ if isinstance(v, float):
44
+ return round(v, 6)
45
+ return v
46
+ return json.dumps(_shrink(obs_dict), indent=2, default=str)
47
+
48
  try:
49
  from ..arena import auto_test_draft, run_arena
50
  from ..landscapes import BUILDERS, build_landscape, structural_hints
 
1442
  obs = env.reset()
1443
  _API_ENV_STATE["env"] = env
1444
  return (
1445
+ _fmt_obs(obs.model_dump(exclude_none=True)),
1446
  f"✓ Reset complete · landscape: **{obs.landscape_description}** · "
1447
  f"dim = {obs.dim} · budget = {obs.budget_remaining}",
1448
  )
 
1469
  return {"error": str(e)}, f"❌ Invalid action: {e}"
1470
 
1471
  obs = env.step(action)
1472
+ dump = _fmt_obs(obs.model_dump(exclude_none=True))
1473
  banner = (
1474
  f"✓ {kind} executed · budget remaining = {obs.budget_remaining}"
1475
  + (" · **episode done**" if obs.done else "")
 
1537
  f"**Dim:** {obs.dim} · **Initial budget:** {obs.budget_remaining}",
1538
  "",
1539
  ]
1540
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1541
 
1542
  for turn in range(1, int(max_turns) + 1):
1543
  messages = build_prompt(obs)
 
1552
  }, timeout=180)
1553
  if r.status_code >= 400:
1554
  log_lines.append(f"**[LLM error {r.status_code}]** {r.text[:300]}")
1555
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1556
  return
1557
  raw = r.json()["choices"][0]["message"]["content"]
1558
  except Exception as e:
1559
  log_lines.append(f"**[request failed]** `{type(e).__name__}: {e}`")
1560
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1561
  return
1562
 
1563
  dt = _time.time() - t0
 
1569
  f"**[turn {turn}] parse error:** `{e}`"
1570
  f"\n```\n{raw[:500]}\n```\n"
1571
  )
1572
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1573
  return
1574
 
1575
  obs = env.step(action)
 
1643
  log_lines.append(f"```python\n{action.code.strip()}\n```")
1644
  log_lines.append(f"")
1645
 
1646
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1647
 
1648
  if obs.done:
1649
  bk = obs.r_optcoder_breakdown or {}
 
1700
  "-r_eval_fail": -bk.get("r_eval_failures", 0),
1701
  }, reward_val)
1702
  yield ("\n".join(log_lines),
1703
+ _fmt_obs(obs.model_dump(exclude_none=True)),
1704
  reward_plot)
1705
  return
1706
 
1707
  log_lines.append("\n**[!] Reached max turns without commit** — episode unfinished.")
1708
+ yield ("\n".join(log_lines), _fmt_obs(obs.model_dump(exclude_none=True)), None)
1709
 
1710
 
1711
  # ----------------- top-level UI -----------------
 
1813
  gr.HTML(HERO_HTML)
1814
 
1815
  with gr.Tabs():
1816
+ # --- Tab 0: Run with LLM (primary — auto-run) ---
1817
+ with gr.Tab("Run with LLM"):
1818
  with gr.Row(equal_height=False):
1819
  # -------- MAIN PANE (left, wider) --------
1820
  with gr.Column(scale=4, min_width=640):
 
1829
  llm_reward_plot = gr.Plot(
1830
  label="Reward breakdown (on episode end)")
1831
  with gr.Column(scale=1):
1832
+ latest_obs = gr.Code(
1833
+ language="json", interactive=False,
1834
+ label="Latest observation", lines=14)
1835
 
1836
  # -------- SIDEBAR (right, narrower) --------
1837
  with gr.Column(scale=1, min_width=300, elem_classes="lf-sidebar"):
 
1874
  run_btn = gr.Button("▶ Run episode", variant="primary",
1875
  size="lg")
1876
 
1877
+ with gr.Accordion("System prompt (sent to LLM)",
1878
+ open=False):
1879
+ try:
1880
+ from ..prompts import SYSTEM as _SYS, ACTION_SPEC as _ACT
1881
+ except ImportError:
1882
+ from prompts import SYSTEM as _SYS, ACTION_SPEC as _ACT # type: ignore
1883
+ gr.Code(
1884
+ value=f"# SYSTEM\n\n{_SYS}\n\n# ACTION_SPEC\n\n{_ACT}",
1885
+ language="markdown", interactive=False,
1886
+ lines=14,
1887
+ )
1888
+
1889
  run_btn.click(
1890
  _llm_auto_run,
1891
  [ep_choice, custom_url_in, key_in, model_name_in,
 
1894
  )
1895
 
1896
  # --- Tab: Manual stepping (raw /reset + /step) ---
1897
+ with gr.Tab("API playground"):
1898
  with gr.Row(equal_height=False):
1899
  with gr.Column(scale=1, min_width=340, elem_classes="lf-sidebar"):
1900
  gr.Markdown("### Manual stepping")
 
1933
  with gr.Column(scale=2, min_width=580):
1934
  status4 = gr.Markdown(
1935
  "*No active env — hit **Reset env** to begin.*")
1936
+ obs4_reset = gr.Code(
1937
+ language="json", interactive=False,
1938
+ label="Initial observation", lines=12)
1939
  status4b = gr.Markdown()
1940
+ obs4 = gr.Code(
1941
+ language="json", interactive=False,
1942
+ label="Step observation", lines=14)
1943
 
1944
  reset_btn.click(_api_reset, [tier4, seed4],
1945
  [obs4_reset, status4])
frontend/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>LandscapeForge</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ html, body, #root { height: 100%; background: #1f1d1a; color: #f3f0e8; }
12
+ body { margin: 0; font-family: 'Inter', system-ui, sans-serif; }
13
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
14
+ *::-webkit-scrollbar-track { background: #1f1d1a; }
15
+ *::-webkit-scrollbar-thumb { background: #403b34; border-radius: 5px; }
16
+ *::-webkit-scrollbar-thumb:hover { background: #857d72; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ <script type="module" src="/src/main.jsx"></script>
22
+ </body>
23
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "landscapeforge-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "lucide-react": "^0.460.0",
13
+ "plotly.js-basic-dist-min": "^2.35.0",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "react-plotly.js": "^2.6.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "autoprefixer": "^10.4.20",
21
+ "postcss": "^8.4.49",
22
+ "tailwindcss": "^3.4.15",
23
+ "vite": "^5.4.11"
24
+ }
25
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { TopBar } from './components/TopBar.jsx'
3
+ import { TabNav } from './components/TabNav.jsx'
4
+ import { RunWithLlm } from './pages/RunWithLlm.jsx'
5
+ import { ApiPlayground } from './pages/ApiPlayground.jsx'
6
+ import { LandscapeExplorer } from './pages/LandscapeExplorer.jsx'
7
+ import { BaselineRace } from './pages/BaselineRace.jsx'
8
+ import { OptimizerArena } from './pages/OptimizerArena.jsx'
9
+ import { About } from './pages/About.jsx'
10
+
11
+ const TABS = [
12
+ { id: 'llm', label: 'Run with LLM' },
13
+ { id: 'api', label: 'API playground' },
14
+ { id: 'landscape', label: 'Landscape' },
15
+ { id: 'race', label: 'Baseline race' },
16
+ { id: 'arena', label: 'Optimizer arena' },
17
+ { id: 'about', label: 'About' },
18
+ ]
19
+
20
+ export default function App() {
21
+ const [active, setActive] = useState('llm')
22
+
23
+ return (
24
+ <div className="min-h-screen bg-bg text-ink">
25
+ <div className="max-w-[1400px] mx-auto px-6 py-6">
26
+ <TopBar />
27
+ <section className="py-4">
28
+ <h1 className="text-[2rem] leading-tight max-w-[820px]">
29
+ An LLM designs optimizers, through a probe–draft–commit REPL.
30
+ </h1>
31
+ <p className="text-muted text-base mt-2 max-w-[720px]">
32
+ Two agents co-evolve: one writes optimizer code, the other picks
33
+ adversarial landscapes. Connect any OpenAI-compatible endpoint
34
+ and watch a model play, or explore the landscape library interactively.
35
+ </p>
36
+ </section>
37
+
38
+ <TabNav tabs={TABS} active={active} onChange={setActive} />
39
+
40
+ <main className="mt-6">
41
+ {active === 'llm' && <RunWithLlm />}
42
+ {active === 'api' && <ApiPlayground />}
43
+ {active === 'landscape' && <LandscapeExplorer />}
44
+ {active === 'race' && <BaselineRace />}
45
+ {active === 'arena' && <OptimizerArena />}
46
+ {active === 'about' && <About />}
47
+ </main>
48
+ </div>
49
+ </div>
50
+ )
51
+ }
frontend/src/components/KpiCard.jsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function KpiCard({ label, value, sub, tone, sign = '' }) {
2
+ const toneClass =
3
+ tone === 'good' ? 'text-good border-good/35'
4
+ : tone === 'warn' ? 'text-warn border-warn/35'
5
+ : tone === 'bad' ? 'text-bad border-bad/35'
6
+ : 'text-ink border-border-soft'
7
+ return (
8
+ <div className={`rounded-xl border bg-elevated p-4 ${toneClass}`}>
9
+ <div className="text-[0.68rem] font-semibold uppercase tracking-[0.1em]
10
+ text-subtle mb-1">{label}</div>
11
+ <div className="font-serif font-medium text-[1.9rem] leading-[1.05]
12
+ tracking-tight">{sign}{value}</div>
13
+ {sub && (
14
+ <div className="text-[0.72rem] mt-1.5 text-subtle font-mono">
15
+ {sub}
16
+ </div>
17
+ )}
18
+ </div>
19
+ )
20
+ }
frontend/src/components/RewardBreakdown.jsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Plot from 'react-plotly.js'
2
+
3
+ const PLOTLY_BASE = {
4
+ font: { family: 'Inter', color: '#f3f0e8', size: 12 },
5
+ paper_bgcolor: '#2a2824',
6
+ plot_bgcolor: '#1f1d1a',
7
+ hoverlabel: { bgcolor: '#f3f0e8', font: { color: '#1f1d1a' } },
8
+ }
9
+ const AXIS = {
10
+ gridcolor: '#403b34', zerolinecolor: '#554e45',
11
+ showline: true, linecolor: '#554e45',
12
+ tickfont: { color: '#b5ada0' },
13
+ }
14
+
15
+ export function RewardBreakdown({ breakdown, total }) {
16
+ const components = {
17
+ r_regret: breakdown.r_regret,
18
+ r_convergence: breakdown.r_convergence,
19
+ r_robustness: breakdown.r_robustness,
20
+ r_novelty: breakdown.r_novelty,
21
+ '-r_budget': -breakdown.r_budget,
22
+ '-r_eval_fail': -breakdown.r_eval_failures,
23
+ }
24
+ const names = Object.keys(components)
25
+ const vs = names.map(n => components[n])
26
+ const colors = vs.map(v => v >= 0 ? '#3d6b4c' : '#a0483a')
27
+ const labels = vs.map(v => (v >= 0 ? '+' : '') + v.toFixed(3))
28
+
29
+ return (
30
+ <Plot
31
+ data={[{
32
+ type: 'bar', orientation: 'h',
33
+ y: names, x: vs,
34
+ marker: { color: colors, line: { color: '#1f1d1a', width: 1 } },
35
+ text: labels, textposition: 'outside', cliponaxis: false,
36
+ textfont: { color: '#f3f0e8', size: 11 },
37
+ hovertemplate: '%{y}<br>contribution=%{x:+.3f}<extra></extra>',
38
+ }]}
39
+ layout={{
40
+ ...PLOTLY_BASE,
41
+ title: {
42
+ text: `Reward breakdown · total = ${(total >= 0 ? '+' : '') + total.toFixed(3)}`,
43
+ x: 0.02, xanchor: 'left',
44
+ font: { size: 14, color: '#f3f0e8' },
45
+ },
46
+ height: 260, margin: { l: 110, r: 50, t: 50, b: 30 },
47
+ xaxis: {
48
+ title: 'weighted contribution',
49
+ range: [Math.min(...vs, 0) - 0.15, Math.max(...vs, 0) + 0.15],
50
+ ...AXIS,
51
+ },
52
+ yaxis: { autorange: 'reversed', ...AXIS },
53
+ showlegend: false, bargap: 0.25,
54
+ shapes: [{
55
+ type: 'line', x0: 0, x1: 0, y0: -0.5, y1: names.length - 0.5,
56
+ line: { color: '#554e45', width: 1 },
57
+ }],
58
+ }}
59
+ config={{ displayModeBar: false, responsive: true }}
60
+ style={{ width: '100%' }}
61
+ useResizeHandler
62
+ />
63
+ )
64
+ }
frontend/src/components/TabNav.jsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function TabNav({ tabs, active, onChange }) {
2
+ return (
3
+ <nav className="flex gap-1 border-b border-border">
4
+ {tabs.map(t => (
5
+ <button
6
+ key={t.id}
7
+ onClick={() => onChange(t.id)}
8
+ className={
9
+ 'px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px ' +
10
+ (t.id === active
11
+ ? 'text-accent border-accent'
12
+ : 'text-muted hover:text-ink border-transparent')
13
+ }
14
+ >
15
+ {t.label}
16
+ </button>
17
+ ))}
18
+ </nav>
19
+ )
20
+ }
frontend/src/components/TopBar.jsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function TopBar() {
2
+ return (
3
+ <header className="flex items-center justify-between border-b border-border pb-4">
4
+ <div className="flex items-center gap-3">
5
+ <div className="relative w-8 h-8 rounded-[8px] shadow-card"
6
+ style={{ background: 'linear-gradient(135deg, #e28763 0%, #c96442 100%)' }}>
7
+ <div className="absolute inset-[5px] border-[1.5px] rounded-[4px] border-white/60"
8
+ style={{ clipPath: 'polygon(0 0, 100% 0, 100% 70%, 30% 100%, 0 100%)' }} />
9
+ </div>
10
+ <div>
11
+ <div className="font-semibold text-base leading-tight">LandscapeForge</div>
12
+ <div className="text-[0.7rem] uppercase tracking-wider text-subtle mt-[1px]">
13
+ OpenEnv · Hackathon Apr '26
14
+ </div>
15
+ </div>
16
+ </div>
17
+ <nav className="flex items-center gap-1">
18
+ {[
19
+ ['Space', 'https://huggingface.co/spaces/mnawfal29/landscapeforge'],
20
+ ['API schema', '/schema'],
21
+ ['OpenAPI', '/openapi.json'],
22
+ ].map(([label, href]) => (
23
+ <a
24
+ key={label}
25
+ href={href}
26
+ target="_blank"
27
+ rel="noreferrer"
28
+ className="text-sm text-muted hover:text-ink px-3 py-1.5 rounded-md
29
+ hover:bg-surface border border-transparent
30
+ hover:border-border transition-colors"
31
+ >
32
+ {label}
33
+ </a>
34
+ ))}
35
+ </nav>
36
+ </header>
37
+ )
38
+ }
frontend/src/components/TurnCard.jsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const KIND_STYLES = {
2
+ draft: 'text-accent border-accent',
3
+ run_baseline: 'text-[#7ecfc5] border-[#5a9c94]',
4
+ inspect: 'text-[#b5a5e0] border-[#7e6ea8]',
5
+ commit: 'text-[#7ab68c] border-[#4e7c5c]',
6
+ }
7
+
8
+ export function TurnCard({ turn, kind, action_str, output, duration_s,
9
+ budget_remaining, code }) {
10
+ const chipClass = KIND_STYLES[kind] || 'text-ink border-border'
11
+
12
+ return (
13
+ <div className="rounded-lg border border-border bg-surface p-4 shadow-card">
14
+ <div className="flex items-center gap-3 pb-2.5 mb-2.5
15
+ border-b border-dashed border-border/60">
16
+ <span className="font-serif font-semibold text-[0.98rem] tracking-tight">
17
+ Turn {turn}
18
+ </span>
19
+ <span className={`chip ${chipClass}`}>{kind}</span>
20
+ <span className="ml-auto font-mono text-xs text-subtle">
21
+ {duration_s.toFixed(1)}s · budget <strong className="text-ink">{budget_remaining}</strong>
22
+ </span>
23
+ </div>
24
+
25
+ <Row label="Action">{action_str}</Row>
26
+ <Row label="Output">
27
+ {output.length === 0 ? 'ok' : output.map((o, i) => (
28
+ <StatusChip key={i} {...o} />
29
+ ))}
30
+ </Row>
31
+
32
+ {code && (
33
+ <pre className="mt-3 rounded-lg border border-border bg-[#14120f]
34
+ px-4 py-3 overflow-x-auto text-[0.82rem] leading-relaxed">
35
+ <code className="font-mono text-[#e8e3d6]">{code.trim()}</code>
36
+ </pre>
37
+ )}
38
+ </div>
39
+ )
40
+ }
41
+
42
+ function Row({ label, children }) {
43
+ return (
44
+ <div className="grid grid-cols-[70px_1fr] gap-3 items-baseline py-1">
45
+ <div className="text-[0.68rem] font-semibold uppercase tracking-[0.1em]
46
+ text-subtle pt-0.5">{label}</div>
47
+ <div className="text-[0.9rem] leading-[1.55] flex flex-wrap gap-2 items-center">
48
+ {children}
49
+ </div>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ function StatusChip({ kind, text }) {
55
+ const cls = {
56
+ good: 'text-good border-good/40 bg-good/10',
57
+ warn: 'text-warn border-warn/40 bg-warn/10',
58
+ bad: 'text-bad border-bad/40 bg-bad/10',
59
+ info: 'text-ink border-border bg-transparent',
60
+ }[kind] || 'text-ink border-border'
61
+ return (
62
+ <span className={`inline-block px-1.5 py-0.5 rounded border text-xs font-medium ${cls}`}
63
+ dangerouslySetInnerHTML={{ __html: text }} />
64
+ )
65
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ h1, h2, h3, h4 { @apply font-serif font-medium tracking-tight; }
7
+ h1 { @apply text-3xl; }
8
+ h2 { @apply text-xl; }
9
+ h3 { @apply text-base font-semibold; }
10
+ code { @apply font-mono text-[0.85em]; }
11
+ }
12
+
13
+ @layer components {
14
+ .card {
15
+ @apply bg-surface border border-border rounded-xl p-5;
16
+ }
17
+ .btn-primary {
18
+ @apply inline-flex items-center justify-center gap-2
19
+ bg-accent hover:bg-accent-dark text-white font-semibold text-sm
20
+ rounded-lg px-4 py-2 transition-colors
21
+ shadow-[0_1px_2px_rgba(201,100,66,0.15)];
22
+ }
23
+ .btn-secondary {
24
+ @apply inline-flex items-center justify-center gap-2
25
+ bg-surface border border-border hover:border-accent text-ink
26
+ font-medium text-sm rounded-lg px-4 py-2 transition-colors;
27
+ }
28
+ .input {
29
+ @apply w-full bg-elevated border border-border text-ink rounded-lg
30
+ px-3 py-2 text-sm placeholder:text-subtle
31
+ focus:border-accent focus:outline-none focus:ring-[3px]
32
+ focus:ring-accent/20 transition;
33
+ }
34
+ .label {
35
+ @apply text-xs font-medium text-muted tracking-wide mb-1.5 block;
36
+ }
37
+ .chip {
38
+ @apply inline-block px-2 py-0.5 rounded border font-mono text-[0.78rem]
39
+ font-medium;
40
+ }
41
+ .kbd {
42
+ @apply inline-block px-1.5 py-0.5 rounded border border-border bg-elevated
43
+ font-mono text-xs text-muted;
44
+ }
45
+ }
frontend/src/lib/api.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Thin wrappers around the FastAPI endpoints.
2
+
3
+ export async function envReset(params = {}) {
4
+ const r = await fetch('/reset', {
5
+ method: 'POST',
6
+ headers: { 'Content-Type': 'application/json' },
7
+ body: JSON.stringify(params),
8
+ })
9
+ if (!r.ok) throw new Error(`reset failed: ${r.status}`)
10
+ return r.json()
11
+ }
12
+
13
+ export async function envStep(action) {
14
+ const r = await fetch('/step', {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ action }),
18
+ })
19
+ if (!r.ok) throw new Error(`step failed: ${r.status}`)
20
+ return r.json()
21
+ }
22
+
23
+ export async function getLandscape(params) {
24
+ const r = await fetch('/api/landscape', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(params),
28
+ })
29
+ if (!r.ok) throw new Error(`landscape failed: ${r.status}`)
30
+ return r.json()
31
+ }
32
+
33
+ export async function getBaselineRace(params) {
34
+ const r = await fetch('/api/baseline_race', {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(params),
38
+ })
39
+ if (!r.ok) throw new Error(`baseline_race failed: ${r.status}`)
40
+ return r.json()
41
+ }
42
+
43
+ export async function runArena(params) {
44
+ const r = await fetch('/api/arena', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify(params),
48
+ })
49
+ if (!r.ok) throw new Error(`arena failed: ${r.status}`)
50
+ return r.json()
51
+ }
52
+
53
+ export function llmRunStream(params, onEvent) {
54
+ // SSE stream: each chunk is a JSON event
55
+ const url = '/api/llm_run?' + new URLSearchParams(params).toString()
56
+ const es = new EventSource(url)
57
+ es.onmessage = ev => {
58
+ try {
59
+ onEvent(JSON.parse(ev.data))
60
+ } catch (e) {
61
+ console.error('bad SSE payload', ev.data, e)
62
+ }
63
+ }
64
+ es.addEventListener('end', () => es.close())
65
+ es.onerror = () => es.close()
66
+ return () => es.close()
67
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend/src/pages/About.jsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function About() {
2
+ return (
3
+ <article className="card prose prose-invert max-w-[820px] text-[0.95rem]
4
+ [&_code]:bg-elevated [&_code]:border [&_code]:border-border
5
+ [&_code]:px-1 [&_code]:rounded [&_a]:text-accent">
6
+ <h2>How the environment works</h2>
7
+ <p>
8
+ <strong>OptCoder</strong> (the LLM policy) designs an
9
+ <code> Optimizer </code>class that minimizes a hidden loss landscape.
10
+ Each episode:
11
+ </p>
12
+ <ol>
13
+ <li>
14
+ <strong>LandscapeForge</strong> (v1: internal template picker)
15
+ chooses a landscape at a tier-appropriate difficulty — convex
16
+ quadratic, Rosenbrock, Gaussian mix, Himmelblau, stiff quadratic,
17
+ cliff.
18
+ </li>
19
+ <li>
20
+ <strong>OptCoder runs a 4-action REPL</strong> with a 12-unit budget:
21
+ <ul>
22
+ <li><code>run_baseline(name)</code> — run SGD / Momentum / Adam /
23
+ L-BFGS, observe trajectory (cost 2)</li>
24
+ <li><code>draft(code)</code> — submit <code>Optimizer</code> class,
25
+ env auto-tests 20 steps (cost 2)</li>
26
+ <li><code>inspect(draft_idx, step_range)</code> — per-step detail
27
+ for a prior draft (cost 1)</li>
28
+ <li><code>commit</code> — run the full 10-seed × 200-step arena
29
+ (cost 0)</li>
30
+ </ul>
31
+ </li>
32
+ <li>
33
+ <strong>Reward</strong> is Adam-relative progress —
34
+ <code>my_progress / tuned_adam_progress − 1</code>, clipped to
35
+ <code>[−1, +1]</code>. No <code>f_min</code> dependency, so this
36
+ extends to NN training as a drop-in.
37
+ </li>
38
+ <li>
39
+ <strong>GRPO</strong> trains the policy against this reward; arena
40
+ cost is ~50 ms so ~36k episodes/hour on one H100.
41
+ </li>
42
+ </ol>
43
+
44
+ <h2>Research anchors</h2>
45
+ <ul>
46
+ <li><strong>Thread 1</strong> · LLMs as optimizer designers:{' '}
47
+ <a href="https://arxiv.org/abs/2302.06675">Lion</a>,{' '}
48
+ <a href="https://www.nature.com/articles/s41586-023-06924-6">FunSearch</a>
49
+ </li>
50
+ <li><strong>Thread 2</strong> · Co-evolutionary LLM-env: Coevolve,{' '}
51
+ <a href="https://arxiv.org/html/2512.19682v1">GenEnv</a>
52
+ </li>
53
+ <li><strong>Thread 3</strong> · Iterative code refinement:{' '}
54
+ <a href="https://arxiv.org/abs/2303.17651">Self-Refine</a>
55
+ </li>
56
+ <li><strong>Thread 4</strong> · GRPO with measurable rewards:{' '}
57
+ <a href="https://arxiv.org/abs/2602.12049v1">HPC GFLOPS reward</a>
58
+ </li>
59
+ <li><strong>Thread 5</strong> · Analytical landscape benchmarks:{' '}
60
+ <a href="https://inria.hal.science/hal-00362649/document">BBOB/COCO</a>,{' '}
61
+ <a href="https://arxiv.org/abs/1901.01753">POET</a>
62
+ </li>
63
+ </ul>
64
+ </article>
65
+ )
66
+ }
frontend/src/pages/ApiPlayground.jsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { envReset, envStep } from '../lib/api.js'
3
+
4
+ const SAMPLE_CODE = `class Optimizer:
5
+ def __init__(self, dim):
6
+ self.lr = 0.05
7
+ self.beta = 0.9
8
+ self.v = np.zeros(dim)
9
+
10
+ def step(self, x, f_val, grad):
11
+ self.v = self.beta * self.v - self.lr * grad
12
+ return x + self.v
13
+ `
14
+
15
+ export function ApiPlayground() {
16
+ const [tier, setTier] = useState('T0')
17
+ const [seed, setSeed] = useState(42)
18
+ const [obs, setObs] = useState(null)
19
+ const [status, setStatus] = useState('No active env — hit Reset to begin.')
20
+ const [kind, setKind] = useState('run_baseline')
21
+ const [baselineName, setBaselineName] = useState('adam')
22
+ const [code, setCode] = useState(SAMPLE_CODE)
23
+ const [draftIdx, setDraftIdx] = useState(0)
24
+ const [stepStart, setStepStart] = useState(0)
25
+ const [stepEnd, setStepEnd] = useState(20)
26
+
27
+ async function reset() {
28
+ try {
29
+ const result = await envReset({ tier, seed })
30
+ setObs(result.observation || result)
31
+ setStatus(`✓ Reset · ${result.observation?.landscape_description ?? 'landscape ready'}`)
32
+ } catch (e) { setStatus(`❌ ${e.message}`) }
33
+ }
34
+ async function step() {
35
+ const action = { kind }
36
+ if (kind === 'run_baseline') action.baseline_name = baselineName
37
+ if (kind === 'draft') action.code = code
38
+ if (kind === 'inspect') {
39
+ action.draft_idx = draftIdx
40
+ action.step_range_start = stepStart
41
+ action.step_range_end = stepEnd
42
+ }
43
+ try {
44
+ const result = await envStep(action)
45
+ setObs(result.observation || result)
46
+ setStatus(`✓ Stepped · budget ${result.observation?.budget_remaining ?? '—'}`)
47
+ } catch (e) { setStatus(`❌ ${e.message}`) }
48
+ }
49
+
50
+ return (
51
+ <div className="grid grid-cols-[320px_1fr] gap-5">
52
+ <aside className="card space-y-4 h-fit">
53
+ <div>
54
+ <h3 className="mb-1">Manual stepping</h3>
55
+ <p className="text-xs text-muted">
56
+ Drive the env one action at a time — same contract as HTTP
57
+ <code> /reset </code> + <code> /step </code>.
58
+ </p>
59
+ </div>
60
+ <div>
61
+ <label className="label">Tier</label>
62
+ <select className="input" value={tier} onChange={e => setTier(e.target.value)}>
63
+ <option>T0</option><option>T1</option><option>T2</option>
64
+ </select>
65
+ </div>
66
+ <div>
67
+ <label className="label">Seed: {seed}</label>
68
+ <input type="range" min={0} max={100} step={1} value={seed}
69
+ onChange={e => setSeed(Number(e.target.value))}
70
+ className="w-full accent-accent" />
71
+ </div>
72
+ <button className="btn-primary w-full" onClick={reset}>Reset env</button>
73
+
74
+ <hr className="border-border/50" />
75
+
76
+ <div>
77
+ <label className="label">Action kind</label>
78
+ <div className="grid grid-cols-2 gap-1.5">
79
+ {['run_baseline', 'draft', 'inspect', 'commit'].map(k => (
80
+ <button key={k}
81
+ onClick={() => setKind(k)}
82
+ className={`text-xs font-mono px-2 py-1.5 rounded border transition ${
83
+ kind === k
84
+ ? 'bg-accent/20 text-accent border-accent'
85
+ : 'bg-elevated text-muted border-border hover:text-ink'}`}>
86
+ {k}
87
+ </button>
88
+ ))}
89
+ </div>
90
+ </div>
91
+
92
+ {kind === 'run_baseline' && (
93
+ <div>
94
+ <label className="label">Reference optimizer</label>
95
+ <select className="input" value={baselineName}
96
+ onChange={e => setBaselineName(e.target.value)}>
97
+ <option>sgd</option><option>momentum</option>
98
+ <option>adam</option><option>lbfgs</option>
99
+ </select>
100
+ </div>
101
+ )}
102
+ {kind === 'draft' && (
103
+ <div>
104
+ <label className="label">Optimizer class</label>
105
+ <textarea className="input font-mono text-xs h-40"
106
+ value={code} onChange={e => setCode(e.target.value)} />
107
+ </div>
108
+ )}
109
+ {kind === 'inspect' && (
110
+ <div className="space-y-2">
111
+ <div>
112
+ <label className="label">draft_idx</label>
113
+ <input type="number" className="input" value={draftIdx}
114
+ onChange={e => setDraftIdx(Number(e.target.value))} />
115
+ </div>
116
+ <div>
117
+ <label className="label">step_range_start</label>
118
+ <input type="number" className="input" value={stepStart}
119
+ onChange={e => setStepStart(Number(e.target.value))} />
120
+ </div>
121
+ <div>
122
+ <label className="label">step_range_end</label>
123
+ <input type="number" className="input" value={stepEnd}
124
+ onChange={e => setStepEnd(Number(e.target.value))} />
125
+ </div>
126
+ </div>
127
+ )}
128
+ <button className="btn-primary w-full" onClick={step}>Step</button>
129
+ </aside>
130
+
131
+ <div className="space-y-3">
132
+ <div className="text-sm text-muted font-mono">{status}</div>
133
+ <pre className="card overflow-x-auto max-h-[640px] overflow-y-auto">
134
+ <code className="font-mono text-xs text-ink leading-relaxed whitespace-pre-wrap">
135
+ {obs ? JSON.stringify(obs, null, 2) : '// hit Reset to begin'}
136
+ </code>
137
+ </pre>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
frontend/src/pages/BaselineRace.jsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import Plot from 'react-plotly.js'
3
+ import { Play } from 'lucide-react'
4
+ import { getBaselineRace } from '../lib/api.js'
5
+
6
+ const TEMPLATES = [
7
+ 'quadratic', 'rosenbrock', 'styblinski_tang', 'huber',
8
+ 'gaussian_mix', 'himmelblau', 'plateau', 'cliff',
9
+ ]
10
+
11
+ export function BaselineRace() {
12
+ const [template, setTemplate] = useState('rosenbrock')
13
+ const [seed, setSeed] = useState(1)
14
+ const [data, setData] = useState(null)
15
+ const [loading, setLoading] = useState(false)
16
+
17
+ async function race() {
18
+ setLoading(true)
19
+ try { setData(await getBaselineRace({ template, seed })) }
20
+ finally { setLoading(false) }
21
+ }
22
+
23
+ return (
24
+ <div className="grid grid-cols-[320px_1fr] gap-5">
25
+ <aside className="card space-y-4 h-fit">
26
+ <div>
27
+ <h3 className="mb-1">Baseline race</h3>
28
+ <p className="text-xs text-muted">
29
+ SGD / Momentum / Adam / L-BFGS each with per-landscape LR tuning.
30
+ Tuned Adam is the bar the trained OptCoder has to beat.
31
+ </p>
32
+ </div>
33
+ <div>
34
+ <label className="label">Template</label>
35
+ <select className="input" value={template}
36
+ onChange={e => setTemplate(e.target.value)}>
37
+ {TEMPLATES.map(t => <option key={t}>{t}</option>)}
38
+ </select>
39
+ </div>
40
+ <div>
41
+ <label className="label">Seed: {seed}</label>
42
+ <input type="range" min={0} max={100} step={1} value={seed}
43
+ onChange={e => setSeed(Number(e.target.value))}
44
+ className="w-full accent-accent" />
45
+ </div>
46
+ <button className="btn-primary w-full" onClick={race} disabled={loading}>
47
+ <Play size={14} /> {loading ? 'Racing…' : 'Race'}
48
+ </button>
49
+ </aside>
50
+
51
+ <div className="space-y-4">
52
+ {data?.contour && (
53
+ <div className="card">
54
+ <Plot data={data.contour.data} layout={data.contour.layout}
55
+ config={{ displayModeBar: false, responsive: true }}
56
+ style={{ width: '100%' }} useResizeHandler />
57
+ </div>
58
+ )}
59
+ {data && (
60
+ <div className="grid grid-cols-2 gap-4">
61
+ {data.curves && (
62
+ <div className="card"><Plot data={data.curves.data} layout={data.curves.layout}
63
+ config={{ displayModeBar: false }} style={{ width: '100%' }} useResizeHandler /></div>
64
+ )}
65
+ {data.finals && (
66
+ <div className="card"><Plot data={data.finals.data} layout={data.finals.layout}
67
+ config={{ displayModeBar: false }} style={{ width: '100%' }} useResizeHandler /></div>
68
+ )}
69
+ </div>
70
+ )}
71
+ {data?.summary_md && (
72
+ <div className="card prose prose-invert max-w-none text-sm
73
+ [&_code]:bg-elevated [&_code]:border [&_code]:border-border
74
+ [&_code]:px-1 [&_code]:rounded">
75
+ <div dangerouslySetInnerHTML={{ __html: data.summary_md }} />
76
+ </div>
77
+ )}
78
+ </div>
79
+ </div>
80
+ )
81
+ }
frontend/src/pages/LandscapeExplorer.jsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import Plot from 'react-plotly.js'
3
+ import { getLandscape } from '../lib/api.js'
4
+
5
+ const TEMPLATES = [
6
+ 'quadratic', 'rosenbrock', 'styblinski_tang', 'huber',
7
+ 'gaussian_mix', 'himmelblau', 'plateau', 'cliff',
8
+ ]
9
+
10
+ export function LandscapeExplorer() {
11
+ const [template, setTemplate] = useState('rosenbrock')
12
+ const [dim, setDim] = useState(2)
13
+ const [seed, setSeed] = useState(0)
14
+ const [data, setData] = useState(null)
15
+ const [loading, setLoading] = useState(false)
16
+
17
+ async function build() {
18
+ setLoading(true)
19
+ try {
20
+ const d = await getLandscape({ template, dim, seed })
21
+ setData(d)
22
+ } finally { setLoading(false) }
23
+ }
24
+ useEffect(() => { build() /* initial load */ // eslint-disable-next-line
25
+ }, [])
26
+
27
+ return (
28
+ <div className="grid grid-cols-[320px_1fr] gap-5">
29
+ <aside className="card space-y-4 h-fit">
30
+ <div>
31
+ <h3 className="mb-1">Landscape explorer</h3>
32
+ <p className="text-xs text-muted">
33
+ Pick a template and see what the agent sees at reset.
34
+ </p>
35
+ </div>
36
+ <div>
37
+ <label className="label">Template</label>
38
+ <select className="input" value={template}
39
+ onChange={e => setTemplate(e.target.value)}>
40
+ {TEMPLATES.map(t => <option key={t}>{t}</option>)}
41
+ </select>
42
+ </div>
43
+ <div>
44
+ <label className="label">Dim: {dim}</label>
45
+ <input type="range" min={2} max={10} step={1} value={dim}
46
+ onChange={e => setDim(Number(e.target.value))}
47
+ className="w-full accent-accent" />
48
+ </div>
49
+ <div>
50
+ <label className="label">Seed: {seed}</label>
51
+ <input type="range" min={0} max={100} step={1} value={seed}
52
+ onChange={e => setSeed(Number(e.target.value))}
53
+ className="w-full accent-accent" />
54
+ </div>
55
+ <button className="btn-primary w-full"
56
+ onClick={build} disabled={loading}>
57
+ {loading ? 'Building…' : 'Build landscape'}
58
+ </button>
59
+ </aside>
60
+
61
+ <div className="space-y-4">
62
+ {data?.contour && (
63
+ <div className="card">
64
+ <Plot data={data.contour.data} layout={data.contour.layout}
65
+ config={{ displayModeBar: false, responsive: true }}
66
+ style={{ width: '100%' }} useResizeHandler />
67
+ </div>
68
+ )}
69
+ {data?.hints && (
70
+ <div className="card">
71
+ <h3 className="mb-2">Structural hints</h3>
72
+ <table className="w-full text-sm">
73
+ <tbody>
74
+ {data.hints.map(([k, v], i) => (
75
+ <tr key={i} className="border-b border-border-soft">
76
+ <td className="py-2 pr-3 text-muted font-mono text-xs">{k}</td>
77
+ <td className="py-2 text-ink font-mono text-xs">{v}</td>
78
+ </tr>
79
+ ))}
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ )}
84
+ </div>
85
+ </div>
86
+ )
87
+ }
frontend/src/pages/OptimizerArena.jsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import Plot from 'react-plotly.js'
3
+ import { Swords } from 'lucide-react'
4
+ import { runArena } from '../lib/api.js'
5
+ import { RewardBreakdown } from '../components/RewardBreakdown.jsx'
6
+
7
+ const SAMPLE = `class Optimizer:
8
+ def __init__(self, dim):
9
+ self.lr = 0.05
10
+ self.beta = 0.9
11
+ self.v = np.zeros(dim)
12
+
13
+ def step(self, x, f_val, grad):
14
+ # SGD with heavy-ball momentum
15
+ self.v = self.beta * self.v - self.lr * grad
16
+ return x + self.v
17
+ `
18
+
19
+ const TEMPLATES = [
20
+ 'quadratic', 'rosenbrock', 'styblinski_tang', 'huber',
21
+ 'gaussian_mix', 'himmelblau', 'plateau', 'cliff',
22
+ ]
23
+
24
+ export function OptimizerArena() {
25
+ const [template, setTemplate] = useState('quadratic')
26
+ const [dim, setDim] = useState(5)
27
+ const [seed, setSeed] = useState(42)
28
+ const [code, setCode] = useState(SAMPLE)
29
+ const [data, setData] = useState(null)
30
+ const [loading, setLoading] = useState(false)
31
+ const [err, setErr] = useState(null)
32
+
33
+ async function run() {
34
+ setLoading(true); setErr(null)
35
+ try { setData(await runArena({ template, dim, seed, code })) }
36
+ catch (e) { setErr(e.message) }
37
+ finally { setLoading(false) }
38
+ }
39
+
40
+ return (
41
+ <div className="grid grid-cols-[340px_1fr] gap-5">
42
+ <aside className="card space-y-4 h-fit">
43
+ <div>
44
+ <h3 className="mb-1">Optimizer arena</h3>
45
+ <p className="text-xs text-muted">
46
+ Paste an <code>Optimizer</code> class. We run it through the full
47
+ Phase-D arena vs tuned Adam on the chosen landscape.
48
+ <br /><span className="text-subtle">np is pre-injected — no import lines.</span>
49
+ </p>
50
+ </div>
51
+ <div>
52
+ <label className="label">Template</label>
53
+ <select className="input" value={template}
54
+ onChange={e => setTemplate(e.target.value)}>
55
+ {TEMPLATES.map(t => <option key={t}>{t}</option>)}
56
+ </select>
57
+ </div>
58
+ <div>
59
+ <label className="label">Dim: {dim}</label>
60
+ <input type="range" min={2} max={10} step={1} value={dim}
61
+ onChange={e => setDim(Number(e.target.value))}
62
+ className="w-full accent-accent" />
63
+ </div>
64
+ <div>
65
+ <label className="label">Seed: {seed}</label>
66
+ <input type="range" min={0} max={100} step={1} value={seed}
67
+ onChange={e => setSeed(Number(e.target.value))}
68
+ className="w-full accent-accent" />
69
+ </div>
70
+ <button className="btn-primary w-full" onClick={run} disabled={loading}>
71
+ <Swords size={14} /> {loading ? 'Running…' : 'Run arena'}
72
+ </button>
73
+ </aside>
74
+
75
+ <div className="space-y-4">
76
+ <div className="card">
77
+ <h3 className="mb-2">Your Optimizer class</h3>
78
+ <textarea className="input font-mono text-xs h-72 leading-relaxed"
79
+ value={code} onChange={e => setCode(e.target.value)} spellCheck={false} />
80
+ </div>
81
+ {err && (
82
+ <div className="card border-bad/40 bg-bad/10 text-bad">
83
+ <strong>Compile error:</strong>
84
+ <pre className="mt-1 text-xs whitespace-pre-wrap">{err}</pre>
85
+ </div>
86
+ )}
87
+ {data?.summary_md && (
88
+ <div className="card prose prose-invert max-w-none text-sm"
89
+ dangerouslySetInnerHTML={{ __html: data.summary_md }} />
90
+ )}
91
+ {data && (
92
+ <div className="grid grid-cols-2 gap-4">
93
+ {data.contour && (
94
+ <div className="card"><Plot data={data.contour.data} layout={data.contour.layout}
95
+ config={{ displayModeBar: false }} style={{ width: '100%' }} useResizeHandler /></div>
96
+ )}
97
+ {data.progress && (
98
+ <div className="card"><Plot data={data.progress.data} layout={data.progress.layout}
99
+ config={{ displayModeBar: false }} style={{ width: '100%' }} useResizeHandler /></div>
100
+ )}
101
+ </div>
102
+ )}
103
+ {data?.breakdown && (
104
+ <div className="card">
105
+ <RewardBreakdown breakdown={data.breakdown} total={data.total} />
106
+ </div>
107
+ )}
108
+ </div>
109
+ </div>
110
+ )
111
+ }
frontend/src/pages/RunWithLlm.jsx ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from 'react'
2
+ import { Play, Terminal, Activity } from 'lucide-react'
3
+ import { llmRunStream } from '../lib/api.js'
4
+ import { RewardBreakdown } from '../components/RewardBreakdown.jsx'
5
+ import { TurnCard } from '../components/TurnCard.jsx'
6
+ import { KpiCard } from '../components/KpiCard.jsx'
7
+
8
+ const PRESET_ENDPOINTS = [
9
+ { label: 'Ollama (localhost:11434)', url: 'http://localhost:11434/v1' },
10
+ { label: 'Hugging Face Router', url: 'https://router.huggingface.co/v1' },
11
+ { label: 'OpenAI', url: 'https://api.openai.com/v1' },
12
+ { label: 'Custom', url: '' },
13
+ ]
14
+ const MODELS = [
15
+ 'qwen2.5:3b', 'qwen2.5:7b', 'qwen2.5:1.5b',
16
+ 'Qwen/Qwen2.5-7B-Instruct', 'Qwen/Qwen2.5-3B-Instruct',
17
+ 'meta-llama/Llama-3.2-3B-Instruct', 'gpt-4o-mini',
18
+ ]
19
+
20
+ export function RunWithLlm() {
21
+ const [endpoint, setEndpoint] = useState(PRESET_ENDPOINTS[0].label)
22
+ const [customUrl, setCustomUrl] = useState('')
23
+ const [apiKey, setApiKey] = useState('')
24
+ const [model, setModel] = useState(MODELS[0])
25
+ const [tier, setTier] = useState('T0')
26
+ const [seed, setSeed] = useState(42)
27
+ const [temperature, setTemperature] = useState(0.7)
28
+ const [maxTurns, setMaxTurns] = useState(10)
29
+
30
+ const [running, setRunning] = useState(false)
31
+ const [turns, setTurns] = useState([])
32
+ const [header, setHeader] = useState(null)
33
+ const [done, setDone] = useState(null)
34
+ const stopRef = useRef(null)
35
+
36
+ function run() {
37
+ setTurns([]); setHeader(null); setDone(null); setRunning(true)
38
+ const preset = PRESET_ENDPOINTS.find(p => p.label === endpoint)
39
+ const base_url = (customUrl || preset.url).replace(/\/$/, '')
40
+
41
+ if (stopRef.current) stopRef.current()
42
+ stopRef.current = llmRunStream(
43
+ {
44
+ base_url, api_key: apiKey, model,
45
+ tier, seed, temperature, max_turns: maxTurns,
46
+ },
47
+ (ev) => {
48
+ if (ev.kind === 'header') {
49
+ setHeader(ev)
50
+ } else if (ev.kind === 'turn') {
51
+ // SSE event uses `kind` for event type and `kind_of` for action kind;
52
+ // TurnCard expects `kind` = action kind, so rename here.
53
+ setTurns(prev => [...prev, { ...ev, kind: ev.kind_of }])
54
+ } else if (ev.kind === 'done') {
55
+ setDone(ev)
56
+ setRunning(false)
57
+ } else if (ev.kind === 'error') {
58
+ setHeader(h => ({ ...(h || {}), error: ev.message }))
59
+ setRunning(false)
60
+ }
61
+ },
62
+ )
63
+ }
64
+
65
+ return (
66
+ <div className="grid grid-cols-[1fr_320px] gap-5">
67
+ {/* ─── MAIN PANE ─── */}
68
+ <div className="space-y-5">
69
+ <div className="card">
70
+ <div className="flex items-center gap-2 mb-3">
71
+ <Terminal size={18} className="text-subtle" />
72
+ <h3>Transcript</h3>
73
+ </div>
74
+ {!header && !turns.length && (
75
+ <p className="text-muted italic text-sm">
76
+ Configure the LLM on the right and hit <strong>Run episode</strong>.
77
+ Each turn streams here as the model plays.
78
+ </p>
79
+ )}
80
+ {header && (
81
+ <div className="border-b border-border/50 pb-3 mb-3">
82
+ <div className="text-sm text-muted">
83
+ Model <span className="chip border-border text-ink">{header.model}</span>
84
+ &nbsp;via&nbsp;
85
+ <span className="chip border-border text-ink">{header.base_url}</span>
86
+ </div>
87
+ <div className="text-sm mt-2">
88
+ <strong>Landscape:</strong> {header.landscape}
89
+ </div>
90
+ <div className="text-sm text-muted mt-1">
91
+ Dim: <strong>{header.dim}</strong> · Initial budget:{' '}
92
+ <strong>{header.budget}</strong>
93
+ </div>
94
+ {header.error && (
95
+ <div className="mt-2 p-3 rounded border border-bad/40 bg-bad/10 text-bad">
96
+ {header.error}
97
+ </div>
98
+ )}
99
+ </div>
100
+ )}
101
+ <div className="space-y-3">
102
+ {turns.map((t, i) => <TurnCard key={i} {...t} />)}
103
+ </div>
104
+ </div>
105
+
106
+ {done && <EpisodeDone done={done} />}
107
+ </div>
108
+
109
+ {/* ─── SIDEBAR ─── */}
110
+ <aside className="card space-y-4 h-fit sticky top-4">
111
+ <div>
112
+ <h3 className="mb-1">Connect an LLM</h3>
113
+ <p className="text-xs text-muted">
114
+ Point at any OpenAI-compatible <code>/v1/chat/completions</code> endpoint.
115
+ </p>
116
+ </div>
117
+
118
+ <div>
119
+ <label className="label">Endpoint</label>
120
+ <select
121
+ className="input"
122
+ value={endpoint}
123
+ onChange={e => setEndpoint(e.target.value)}
124
+ >
125
+ {PRESET_ENDPOINTS.map(p => (
126
+ <option key={p.label} value={p.label}>{p.label}</option>
127
+ ))}
128
+ </select>
129
+ </div>
130
+
131
+ <div>
132
+ <label className="label">Model</label>
133
+ <input
134
+ className="input font-mono text-xs"
135
+ list="model-list"
136
+ value={model}
137
+ onChange={e => setModel(e.target.value)}
138
+ />
139
+ <datalist id="model-list">
140
+ {MODELS.map(m => <option key={m} value={m} />)}
141
+ </datalist>
142
+ </div>
143
+
144
+ <div>
145
+ <label className="label">Custom base URL (optional)</label>
146
+ <input
147
+ className="input font-mono text-xs"
148
+ placeholder="http://localhost:8080/v1"
149
+ value={customUrl}
150
+ onChange={e => setCustomUrl(e.target.value)}
151
+ />
152
+ </div>
153
+
154
+ <div>
155
+ <label className="label">API key (optional)</label>
156
+ <input
157
+ type="password"
158
+ className="input font-mono text-xs"
159
+ placeholder="Bearer <key>"
160
+ value={apiKey}
161
+ onChange={e => setApiKey(e.target.value)}
162
+ />
163
+ </div>
164
+
165
+ <hr className="border-border/50" />
166
+
167
+ <h3>Episode config</h3>
168
+
169
+ <div>
170
+ <label className="label">Tier</label>
171
+ <select className="input" value={tier} onChange={e => setTier(e.target.value)}>
172
+ <option>T0</option><option>T1</option><option>T2</option>
173
+ </select>
174
+ </div>
175
+
176
+ <RangeRow label={`Seed: ${seed}`} min={0} max={100} step={1}
177
+ value={seed} onChange={setSeed} />
178
+ <RangeRow label={`Temperature: ${temperature.toFixed(2)}`}
179
+ min={0} max={1.5} step={0.05}
180
+ value={temperature} onChange={setTemperature} />
181
+ <RangeRow label={`Max turns: ${maxTurns}`} min={3} max={15} step={1}
182
+ value={maxTurns} onChange={setMaxTurns} />
183
+
184
+ <button
185
+ className="btn-primary w-full py-3"
186
+ disabled={running}
187
+ onClick={run}
188
+ >
189
+ {running
190
+ ? <><Activity size={16} className="animate-pulse" /> Running…</>
191
+ : <><Play size={16} /> Run episode</>}
192
+ </button>
193
+ </aside>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ function RangeRow({ label, min, max, step, value, onChange }) {
199
+ return (
200
+ <div>
201
+ <label className="label">{label}</label>
202
+ <input
203
+ type="range"
204
+ min={min} max={max} step={step} value={value}
205
+ onChange={e => onChange(Number(e.target.value))}
206
+ className="w-full accent-accent"
207
+ />
208
+ </div>
209
+ )
210
+ }
211
+
212
+ function EpisodeDone({ done }) {
213
+ const reward = done.reward
214
+ const speedup = done.speedup_vs_adam
215
+ const rewardTone =
216
+ reward >= 0.5 ? 'good' : reward >= 0 ? 'warn' : 'bad'
217
+ const speedupTone = speedup >= 1.0 ? 'good' : 'warn'
218
+ const speedupDisplay = speedup < 100
219
+ ? `${speedup.toFixed(2)}×`
220
+ : `${Math.round(speedup)}×`
221
+
222
+ return (
223
+ <div className="card"
224
+ style={{ background: 'linear-gradient(180deg, rgba(226,135,99,0.07) 0%, rgba(42,40,36,0) 60%)' }}>
225
+ <div className="flex items-baseline gap-3 mb-4">
226
+ <span className="chip border-accent text-accent uppercase tracking-wider text-[0.7rem]">
227
+ Episode complete
228
+ </span>
229
+ <span className="text-subtle text-sm">
230
+ ended by <code className="text-muted">{done.reason}</code>
231
+ </span>
232
+ </div>
233
+
234
+ <div className="grid grid-cols-3 gap-3 mb-5">
235
+ <KpiCard label="Terminal reward" value={reward.toFixed(3)}
236
+ sub="GRPO training scalar" tone={rewardTone} sign={reward >= 0 ? '+' : ''}/>
237
+ <KpiCard label="Speedup vs tuned Adam" value={speedupDisplay}
238
+ sub={`my ${done.my_progress.toFixed(3)} · adam ${done.adam_progress.toFixed(3)}`}
239
+ tone={speedupTone} />
240
+ <KpiCard label="Adam shortfall" value={done.final_regret.toFixed(3)}
241
+ sub="0 = matched/beat Adam" />
242
+ </div>
243
+
244
+ <RewardBreakdown breakdown={done.breakdown} total={reward} />
245
+ </div>
246
+ )
247
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{js,jsx}'],
4
+ darkMode: 'class',
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ // Warm Claude-inspired dark palette
9
+ bg: '#1f1d1a',
10
+ surface: '#2a2824',
11
+ elevated: '#332f2a',
12
+ border: { DEFAULT: '#403b34', soft: '#332f2a' },
13
+ ink: '#f3f0e8',
14
+ muted: '#b5ada0',
15
+ subtle: '#857d72',
16
+ accent: { DEFAULT: '#e28763', dark: '#c96442', soft: '#4a2f22' },
17
+ good: '#7ab68c',
18
+ warn: '#e4b264',
19
+ bad: '#d47d6a',
20
+ },
21
+ fontFamily: {
22
+ sans: ['Inter', 'system-ui', 'sans-serif'],
23
+ serif: ['"Source Serif 4"', 'Georgia', 'serif'],
24
+ mono: ['"JetBrains Mono"', 'ui-monospace', 'monospace'],
25
+ },
26
+ boxShadow: {
27
+ card: '0 1px 0 rgba(0,0,0,0.2)',
28
+ glow: '0 0 0 3px rgba(226,135,99,0.18)',
29
+ },
30
+ },
31
+ },
32
+ plugins: [],
33
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ outDir: 'dist',
8
+ assetsDir: 'assets',
9
+ },
10
+ server: {
11
+ port: 5173,
12
+ proxy: {
13
+ // Proxy API calls to FastAPI during development
14
+ '/reset': 'http://localhost:8000',
15
+ '/step': 'http://localhost:8000',
16
+ '/schema': 'http://localhost:8000',
17
+ '/health': 'http://localhost:8000',
18
+ '/api': 'http://localhost:8000',
19
+ },
20
+ },
21
+ })
server/api_routes.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI endpoints used by the React frontend.
2
+
3
+ Provides:
4
+ - /api/landscape build a template and return a Plotly contour + hints
5
+ - /api/baseline_race run 4 LR-tuned baselines and return plots + summary
6
+ - /api/arena full Phase-D evaluation of a user optimizer vs Adam
7
+ - /api/llm_run SSE-streamed LLM-driven episode
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import re
15
+ import time
16
+ from typing import Any, Optional
17
+
18
+ import numpy as np
19
+ import requests
20
+ from fastapi import APIRouter, Query
21
+ from fastapi.responses import StreamingResponse
22
+ from pydantic import BaseModel
23
+
24
+ try:
25
+ from ..arena import auto_test_draft, run_arena, ArenaResult
26
+ from ..landscapes import BUILDERS, build_landscape, structural_hints
27
+ from ..reference_optimizers import (
28
+ run_baseline_tuned, tune_adam_lr,
29
+ )
30
+ from ..rewards import ast_novelty_score, compute_optcoder_reward
31
+ from ..sandbox import SandboxError, compile_optimizer
32
+ from ..models import LandscapeforgeAction
33
+ from ..prompts import build_prompt, parse_action
34
+ from .landscapeforge_environment import LandscapeforgeEnvironment
35
+ except ImportError: # flat layout
36
+ from arena import auto_test_draft, run_arena, ArenaResult # type: ignore
37
+ from landscapes import BUILDERS, build_landscape, structural_hints # type: ignore
38
+ from reference_optimizers import ( # type: ignore
39
+ run_baseline_tuned, tune_adam_lr,
40
+ )
41
+ from rewards import ast_novelty_score, compute_optcoder_reward # type: ignore
42
+ from sandbox import SandboxError, compile_optimizer # type: ignore
43
+ from models import LandscapeforgeAction # type: ignore
44
+ from prompts import build_prompt, parse_action # type: ignore
45
+ from server.landscapeforge_environment import LandscapeforgeEnvironment # type: ignore
46
+
47
+
48
+ router = APIRouter(prefix="/api", tags=["lf-frontend"])
49
+
50
+
51
+ # ---------- palette constants for Plotly layouts ----------
52
+
53
+ _PLOTLY_LAYOUT = dict(
54
+ font=dict(family="Inter", color="#f3f0e8", size=12),
55
+ paper_bgcolor="#2a2824", plot_bgcolor="#1f1d1a",
56
+ hoverlabel=dict(bgcolor="#f3f0e8", font_color="#1f1d1a"),
57
+ legend=dict(bgcolor="rgba(31,29,26,0.85)",
58
+ bordercolor="#403b34", borderwidth=1,
59
+ font=dict(color="#f3f0e8")),
60
+ )
61
+ _AXIS = dict(gridcolor="#403b34", zerolinecolor="#554e45",
62
+ showline=True, linecolor="#554e45",
63
+ tickfont=dict(color="#b5ada0"))
64
+ _DEFAULT_MARGIN = dict(l=60, r=30, t=60, b=55)
65
+ _TITLE = dict(x=0.02, xanchor="left", font=dict(size=14, color="#f3f0e8"))
66
+
67
+ OPT_COLORS = {
68
+ "sgd": "#c05450",
69
+ "momentum": "#d9865b",
70
+ "adam": "#5b7a6b",
71
+ "lbfgs": "#556b99",
72
+ "custom": "#e28763",
73
+ }
74
+
75
+
76
+ # ---------- shared plot helpers ----------
77
+
78
+ def _color(name: str) -> str:
79
+ return OPT_COLORS.get(name.split("(")[0].strip(), "#e28763")
80
+
81
+
82
+ def _contour_fig(ls, trajectories=None, title=None):
83
+ import numpy as np
84
+ if ls.dim != 2:
85
+ return _empty_fig(f"{ls.name} · dim={ls.dim}\nContour is 2-D only", 480)
86
+ CLIP = 8.0
87
+ xs_all, ys_all = [0.0], [0.0]
88
+ for traj in (trajectories or {}).values():
89
+ arr = np.asarray(traj)
90
+ if arr.size == 0:
91
+ continue
92
+ mask = (np.abs(arr) <= CLIP).all(axis=1) & np.isfinite(arr).all(axis=1)
93
+ good = arr[mask]
94
+ if good.size:
95
+ xs_all.extend(good[:, 0].tolist())
96
+ ys_all.extend(good[:, 1].tolist())
97
+ x_min = max(min(xs_all) - 1.5, -CLIP); x_max = min(max(xs_all) + 1.5, CLIP)
98
+ y_min = max(min(ys_all) - 1.5, -CLIP); y_max = min(max(ys_all) + 1.5, CLIP)
99
+ x_min, x_max = min(x_min, -3.5), max(x_max, 3.5)
100
+ y_min, y_max = min(y_min, -3.5), max(y_max, 3.5)
101
+
102
+ g = 70
103
+ xs = np.linspace(x_min, x_max, g)
104
+ ys = np.linspace(y_min, y_max, g)
105
+ X, Y = np.meshgrid(xs, ys)
106
+ Z = np.empty_like(X)
107
+ for i in range(g):
108
+ for j in range(g):
109
+ Z[i, j] = ls.f(np.array([X[i, j], Y[i, j]]))
110
+ finite = Z[np.isfinite(Z)]
111
+ lo, hi = map(float, np.percentile(finite, [2, 95]))
112
+
113
+ data = [dict(
114
+ type="contour", x=xs.tolist(), y=ys.tolist(), z=Z.tolist(),
115
+ zmin=lo, zmax=hi,
116
+ colorscale=[
117
+ [0.0, "#1f1d1a"], [0.15, "#2f2a22"], [0.3, "#4a2f22"],
118
+ [0.5, "#7a4229"], [0.7, "#c25a3a"], [0.85, "#e28763"],
119
+ [1.0, "#f4d6c5"],
120
+ ],
121
+ contours=dict(coloring="heatmap", showlabels=False),
122
+ line=dict(width=0.5, color="rgba(243,240,232,0.12)"),
123
+ colorbar=dict(title=dict(text="f(x)",
124
+ font=dict(size=11, color="#f3f0e8")),
125
+ thickness=12, len=0.85,
126
+ tickfont=dict(size=10, color="#b5ada0"),
127
+ outlinewidth=0),
128
+ hovertemplate="x₁=%{x:.3f}<br>x₂=%{y:.3f}<br>f=%{z:.3f}<extra></extra>",
129
+ )]
130
+ if trajectories:
131
+ for name, traj in trajectories.items():
132
+ arr = np.asarray(traj)
133
+ if not arr.size:
134
+ continue
135
+ mask = (np.abs(arr) <= CLIP).all(axis=1) & np.isfinite(arr).all(axis=1)
136
+ diverged = not mask.all()
137
+ arr = arr[mask]
138
+ if arr.shape[0] == 0:
139
+ continue
140
+ color = _color(name)
141
+ label = f"{name} · diverged" if diverged else name
142
+ data.append(dict(
143
+ type="scatter", mode="lines+markers",
144
+ x=arr[:, 0].tolist(), y=arr[:, 1].tolist(),
145
+ name=label,
146
+ line=dict(color=color, width=2.5, dash="dash" if diverged else "solid"),
147
+ marker=dict(size=4, color=color,
148
+ line=dict(color="#ffffff", width=0.8)),
149
+ hovertemplate="step %{pointNumber}<br>x₁=%{x:.3f}<br>x₂=%{y:.3f}"
150
+ "<extra>" + label + "</extra>",
151
+ ))
152
+ data.append(dict(type="scatter", mode="markers",
153
+ x=[arr[0, 0].item()], y=[arr[0, 1].item()],
154
+ showlegend=False,
155
+ marker=dict(size=12, color=color, symbol="circle-open",
156
+ line=dict(color=color, width=2.5)),
157
+ hoverinfo="skip"))
158
+ end_sym = "x" if diverged else "star"
159
+ data.append(dict(type="scatter", mode="markers",
160
+ x=[arr[-1, 0].item()], y=[arr[-1, 1].item()],
161
+ showlegend=False,
162
+ marker=dict(size=14 if diverged else 16,
163
+ color=color, symbol=end_sym,
164
+ line=dict(color="#ffffff", width=1.2)),
165
+ hoverinfo="skip"))
166
+
167
+ layout = {
168
+ **_PLOTLY_LAYOUT,
169
+ "title": {"text": title or f"{ls.name} (dim=2)", **_TITLE},
170
+ "height": 480, "margin": _DEFAULT_MARGIN,
171
+ "xaxis": {"title": "x₁", "range": [x_min, x_max], **_AXIS},
172
+ "yaxis": {"title": "x₂", "range": [y_min, y_max],
173
+ "scaleanchor": "x", "scaleratio": 1, **_AXIS},
174
+ }
175
+ return {"data": data, "layout": layout}
176
+
177
+
178
+ def _empty_fig(msg: str, h: int = 480):
179
+ return {"data": [], "layout": {
180
+ **_PLOTLY_LAYOUT, "height": h, "margin": _DEFAULT_MARGIN,
181
+ "xaxis": {"visible": False}, "yaxis": {"visible": False},
182
+ "annotations": [{"text": msg, "showarrow": False,
183
+ "x": 0.5, "y": 0.5, "xref": "paper", "yref": "paper",
184
+ "font": {"size": 14, "color": "#b5ada0"}}],
185
+ }}
186
+
187
+
188
+ def _curves_fig(curves, title):
189
+ data = []
190
+ for name, fs in curves.items():
191
+ if not fs:
192
+ continue
193
+ color = _color(name)
194
+ data.append(dict(
195
+ type="scatter", mode="lines+markers", name=name,
196
+ x=list(range(len(fs))),
197
+ y=[v if np.isfinite(v) else None for v in fs],
198
+ line=dict(color=color, width=2.2, shape="spline"),
199
+ marker=dict(size=4, color=color),
200
+ hovertemplate="step=%{x}<br>f=%{y:.4g}<extra>" + name + "</extra>",
201
+ connectgaps=False,
202
+ ))
203
+ layout = {
204
+ **_PLOTLY_LAYOUT, "title": {"text": title, **_TITLE},
205
+ "height": 360, "margin": _DEFAULT_MARGIN,
206
+ "xaxis": {"title": "optimizer step", **_AXIS},
207
+ "yaxis": {"title": "f(x) (symlog)", "type": "log", **_AXIS},
208
+ }
209
+ return {"data": data, "layout": layout}
210
+
211
+
212
+ def _bar_fig(values, title, ylabel):
213
+ names = list(values.keys())
214
+ vs = [values[n] for n in names]
215
+ data = [dict(type="bar", x=names, y=vs,
216
+ marker=dict(color=[_color(n) for n in names],
217
+ line=dict(color="#ffffff", width=1)),
218
+ text=[f"{v:.3g}" for v in vs], textposition="outside",
219
+ textfont=dict(size=11, color="#f3f0e8"),
220
+ hovertemplate="%{x}<br>" + ylabel + "=%{y:.4g}<extra></extra>")]
221
+ layout = {
222
+ **_PLOTLY_LAYOUT, "title": {"text": title, **_TITLE},
223
+ "height": 280, "margin": _DEFAULT_MARGIN,
224
+ "xaxis": {**_AXIS},
225
+ "yaxis": {"title": ylabel, **_AXIS},
226
+ "showlegend": False,
227
+ }
228
+ return {"data": data, "layout": layout}
229
+
230
+
231
+ # ---------- request/response models ----------
232
+
233
+ class LandscapeReq(BaseModel):
234
+ template: str
235
+ dim: int = 2
236
+ seed: int = 0
237
+
238
+
239
+ class BaselineReq(BaseModel):
240
+ template: str
241
+ seed: int = 1
242
+
243
+
244
+ class ArenaReq(BaseModel):
245
+ template: str
246
+ dim: int = 5
247
+ seed: int = 42
248
+ code: str
249
+
250
+
251
+ # ---------- /api/landscape ----------
252
+
253
+ def _landscape_params(template: str) -> dict:
254
+ if template == "quadratic": return {"cond": 10.0}
255
+ if template == "gaussian_mix": return {"k": 3, "sigma": 0.5, "spread": 2.0}
256
+ return {}
257
+
258
+
259
+ @router.post("/landscape")
260
+ def api_landscape(req: LandscapeReq):
261
+ rng = np.random.default_rng(req.seed)
262
+ dim = 2 if req.template == "himmelblau" else req.dim
263
+ ls = build_landscape(template=req.template, dim=dim,
264
+ params=_landscape_params(req.template), rng=rng)
265
+ hints = structural_hints(ls, rng=rng)
266
+ hints_rows = [[k, f"{v:.4g}" if isinstance(v, float) else str(v)]
267
+ for k, v in hints.items()]
268
+ hints_rows.append(["dim", str(ls.dim)])
269
+ hints_rows.append(["f_min (known)", f"{ls.f_min:.4g}"])
270
+ hints_rows.append(["description", ls.description])
271
+ return {
272
+ "contour": _contour_fig(ls, title=f"{req.template} · dim={ls.dim}"),
273
+ "hints": hints_rows,
274
+ }
275
+
276
+
277
+ # ---------- /api/baseline_race ----------
278
+
279
+ @router.post("/baseline_race")
280
+ def api_baseline_race(req: BaselineReq):
281
+ rng = np.random.default_rng(req.seed)
282
+ ls = build_landscape(template=req.template, dim=2,
283
+ params=_landscape_params(req.template), rng=rng)
284
+ x0 = np.random.default_rng(req.seed + 999).normal(0.0, 0.5, size=2)
285
+
286
+ traj_2d, curves, finals, lrs = {}, {}, {}, {}
287
+ for name in ["sgd", "momentum", "adam", "lbfgs"]:
288
+ r = run_baseline_tuned(name, ls.f, ls.grad, x0, steps=50)
289
+ lrs[name] = r["lr"]
290
+ traj = [s for s in r["trajectory"] if s.get("x") is not None]
291
+ traj_2d[name] = [(s["x"][0], s["x"][1]) for s in traj]
292
+ curves[name] = [s["f"] for s in traj if s.get("f") is not None]
293
+ finals[name] = curves[name][-1] if curves[name] else float("inf")
294
+
295
+ lr_list = " · ".join(f"<code>{n}</code>: <code>{lr:g}</code>"
296
+ for n, lr in lrs.items())
297
+ best = min(finals, key=finals.get)
298
+ return {
299
+ "contour": _contour_fig(ls, trajectories=traj_2d,
300
+ title=f"{req.template} — baselines racing (LR-tuned)"),
301
+ "curves": _curves_fig(curves, "f(x) vs step"),
302
+ "finals": _bar_fig(finals, "Final f after 50 steps",
303
+ "f(x) at step 50"),
304
+ "summary_md": (
305
+ f"<p><strong>{ls.description}</strong></p>"
306
+ f"<p>Tuned LR per baseline (7-point sweep, 30 steps): {lr_list}</p>"
307
+ f"<p>Best baseline: <code>{best}</code> at f = "
308
+ f"<code>{finals[best]:.4f}</code></p>"
309
+ ),
310
+ }
311
+
312
+
313
+ # ---------- /api/arena ----------
314
+
315
+ ADAM_TEMPLATE = """\
316
+ class Optimizer:
317
+ def __init__(self, dim):
318
+ self.lr = {lr}
319
+ self.b1, self.b2, self.eps = 0.9, 0.999, 1e-8
320
+ self.m = np.zeros(dim); self.v = np.zeros(dim); self.t = 0
321
+ def step(self, x, f_val, grad):
322
+ self.t += 1
323
+ self.m = self.b1*self.m + (1-self.b1)*grad
324
+ self.v = self.b2*self.v + (1-self.b2)*grad*grad
325
+ mh = self.m/(1-self.b1**self.t); vh = self.v/(1-self.b2**self.t)
326
+ return x - self.lr * mh / (np.sqrt(vh) + self.eps)
327
+ """
328
+
329
+ ARENA_SEEDS = [101, 202, 303, 404, 505, 606, 707, 808, 909, 1010]
330
+
331
+
332
+ @router.post("/arena")
333
+ def api_arena(req: ArenaReq):
334
+ rng = np.random.default_rng(req.seed)
335
+ dim = 2 if req.template == "himmelblau" else req.dim
336
+ ls = build_landscape(template=req.template, dim=dim,
337
+ params=_landscape_params(req.template), rng=rng)
338
+
339
+ tune_x0 = np.random.default_rng(0).normal(0.0, 0.5, size=dim)
340
+ best_lr = tune_adam_lr(ls.f, ls.grad, tune_x0, sweep_steps=30)
341
+ adam_src = ADAM_TEMPLATE.format(lr=best_lr)
342
+
343
+ try:
344
+ opt = compile_optimizer(req.code, dim=dim)
345
+ except SandboxError as e:
346
+ return {"error": str(e)}
347
+
348
+ test = auto_test_draft(opt, ls, seed=req.seed, steps=20)
349
+ user_arena = run_arena(opt, ls, seeds=ARENA_SEEDS, steps=200)
350
+ adam_opt = compile_optimizer(adam_src, dim=dim)
351
+ adam_arena = run_arena(adam_opt, ls, seeds=ARENA_SEEDS, steps=200)
352
+
353
+ reward = compute_optcoder_reward(
354
+ arena=user_arena, adam_arena=adam_arena,
355
+ actions_used_cost=0, budget_total=12,
356
+ novelty_score=ast_novelty_score(req.code, [adam_src]),
357
+ convergence_step=None, arena_steps=200,
358
+ )
359
+
360
+ # 2-D contour if applicable
361
+ contour = None
362
+ if dim == 2:
363
+ from .reference_optimizers import run_baseline
364
+ user_traj = [(s["x"][0], s["x"][1]) for s in test["detail"]]
365
+ adam_run_raw = []
366
+ try:
367
+ from ..reference_optimizers import run_baseline as _rb
368
+ except ImportError:
369
+ from reference_optimizers import run_baseline as _rb # type: ignore
370
+ adam_run = _rb("adam", ls.f, ls.grad,
371
+ np.random.default_rng(req.seed).normal(0.0, 0.5, 2),
372
+ steps=50)
373
+ adam_traj = [(s["x"][0], s["x"][1]) for s in adam_run["trajectory"]
374
+ if s.get("x") is not None]
375
+ contour = _contour_fig(ls,
376
+ trajectories={"custom": user_traj, "adam": adam_traj},
377
+ title=f"{req.template} — your optimizer vs tuned Adam")
378
+
379
+ bk = reward.breakdown
380
+ return {
381
+ "contour": contour or _empty_fig(f"{req.template} · dim={dim}\nContour is 2-D only"),
382
+ "progress": _bar_fig(
383
+ {"custom": user_arena.mean_progress,
384
+ "adam (tuned)": adam_arena.mean_progress},
385
+ "Arena mean progress",
386
+ "mean(f₀ − f_N) over 10 seeds",
387
+ ),
388
+ "breakdown": bk,
389
+ "total": reward.r_total,
390
+ "summary_md": (
391
+ f"<h3>Results</h3>"
392
+ f"<ul>"
393
+ f"<li>Your mean progress: <code>{user_arena.mean_progress:.4g}</code></li>"
394
+ f"<li>Tuned Adam progress: <code>{adam_arena.mean_progress:.4g}</code>"
395
+ f" (lr=<code>{best_lr:g}</code>)</li>"
396
+ f"<li>Speedup vs Adam: <code>{bk.get('speedup_vs_adam', 0):.3g}×</code></li>"
397
+ f"<li>Your crash fraction: <code>{user_arena.crash_fraction:.0%}</code></li>"
398
+ f"<li><strong>Total reward: <code>{reward.r_total:+.3f}</code></strong></li>"
399
+ f"</ul>"
400
+ ),
401
+ }
402
+
403
+
404
+ # ---------- /api/llm_run (SSE stream) ----------
405
+
406
+ def _sse(event: str, data: dict) -> str:
407
+ return f"event: {event}\ndata: {json.dumps(data, default=str)}\n\n"
408
+
409
+
410
+ @router.get("/llm_run")
411
+ def api_llm_run(
412
+ base_url: str = Query(...),
413
+ api_key: str = "",
414
+ model: str = Query(...),
415
+ tier: str = "T0",
416
+ seed: int = 42,
417
+ temperature: float = 0.7,
418
+ max_turns: int = 10,
419
+ ):
420
+ """SSE-streamed LLM-driven episode. One event per turn."""
421
+
422
+ def gen():
423
+ url = base_url.rstrip("/") + "/chat/completions"
424
+ headers = {"Content-Type": "application/json"}
425
+ if api_key:
426
+ headers["Authorization"] = f"Bearer {api_key}"
427
+
428
+ env = LandscapeforgeEnvironment(tier=tier, seed=int(seed))
429
+ obs = env.reset()
430
+
431
+ yield _sse("message", {
432
+ "kind": "header", "model": model, "base_url": base_url,
433
+ "landscape": obs.landscape_description,
434
+ "dim": obs.dim, "budget": obs.budget_remaining,
435
+ })
436
+
437
+ for turn in range(1, int(max_turns) + 1):
438
+ messages = build_prompt(obs)
439
+ t0 = time.time()
440
+ try:
441
+ r = requests.post(url, headers=headers, json={
442
+ "model": model, "messages": messages,
443
+ "temperature": float(temperature),
444
+ "max_tokens": 1200, "stream": False,
445
+ }, timeout=180)
446
+ if r.status_code >= 400:
447
+ yield _sse("message", {
448
+ "kind": "error",
449
+ "message": f"LLM {r.status_code}: {r.text[:400]}",
450
+ })
451
+ return
452
+ raw = r.json()["choices"][0]["message"]["content"]
453
+ except Exception as e:
454
+ yield _sse("message", {
455
+ "kind": "error",
456
+ "message": f"request failed: {type(e).__name__}: {e}",
457
+ })
458
+ return
459
+ dt = time.time() - t0
460
+
461
+ try:
462
+ action = parse_action(raw)
463
+ except Exception as e:
464
+ yield _sse("message", {
465
+ "kind": "error",
466
+ "message": f"parse error: {e}. Raw: {raw[:400]}",
467
+ })
468
+ return
469
+
470
+ obs = env.step(action)
471
+ lar = obs.last_action_result or {}
472
+
473
+ output_chips = []
474
+ if lar.get("compile_error"):
475
+ output_chips.append({"kind": "bad", "text": "compile error"})
476
+ if lar.get("summary"):
477
+ s = lar["summary"]
478
+ if s.get("converged"):
479
+ output_chips.append({"kind": "good", "text": "auto-test converged"})
480
+ elif s.get("diverged"):
481
+ output_chips.append({"kind": "warn", "text": "auto-test diverged"})
482
+ if s.get("final_f") is not None:
483
+ output_chips.append({
484
+ "kind": "info",
485
+ "text": f"<code>final_f</code>=<b>{s['final_f']:.3g}</b>",
486
+ })
487
+ if action.kind == "run_baseline" and lar.get("final_f") is not None:
488
+ output_chips.append({
489
+ "kind": "info",
490
+ "text": f"<code>final_f</code>=<b>{lar['final_f']:.3g}</b>",
491
+ })
492
+ for k, v in (lar.get("feedback") or {}).items():
493
+ output_chips.append({
494
+ "kind": "good" if v >= 0 else "warn",
495
+ "text": f"<code>{k}</code> <b>{v:+.3f}</b>",
496
+ })
497
+
498
+ if action.kind == "draft":
499
+ action_str = f"draft ({len(action.code or '')} chars)"
500
+ elif action.kind == "run_baseline":
501
+ action_str = f"run_baseline({action.baseline_name})"
502
+ elif action.kind == "inspect":
503
+ action_str = (f"inspect(draft={action.draft_idx}, "
504
+ f"[{action.step_range_start},{action.step_range_end}])")
505
+ else:
506
+ action_str = "commit"
507
+
508
+ yield _sse("message", {
509
+ "kind": "turn",
510
+ "turn": turn, "kind_of": action.kind,
511
+ "action_str": action_str, "output": output_chips,
512
+ "duration_s": dt,
513
+ "budget_remaining": obs.budget_remaining,
514
+ "code": action.code if action.kind == "draft" else None,
515
+ })
516
+
517
+ if obs.done:
518
+ bk = obs.r_optcoder_breakdown or {}
519
+ yield _sse("message", {
520
+ "kind": "done",
521
+ "reason": (obs.last_action_result or {}).get("reason"),
522
+ "reward": obs.r_optcoder or 0.0,
523
+ "final_regret": obs.final_regret or 0.0,
524
+ "my_progress": bk.get("my_progress", 0.0),
525
+ "adam_progress": bk.get("adam_progress", 0.0),
526
+ "speedup_vs_adam": bk.get("speedup_vs_adam", 0.0),
527
+ "breakdown": bk,
528
+ })
529
+ yield "event: end\ndata: {}\n\n"
530
+ return
531
+
532
+ yield _sse("message", {
533
+ "kind": "error",
534
+ "message": f"reached MAX_TURNS ({max_turns}) without commit",
535
+ })
536
+ yield "event: end\ndata: {}\n\n"
537
+
538
+ return StreamingResponse(gen(), media_type="text/event-stream")
server/app.py CHANGED
@@ -1,53 +1,30 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
 
 
 
 
 
7
  """
8
- FastAPI application for the Landscapeforge Environment.
9
 
10
- This module creates an HTTP server that exposes the LandscapeforgeEnvironment
11
- over HTTP and WebSocket endpoints, compatible with EnvClient.
12
-
13
- Endpoints:
14
- - POST /reset: Reset the environment
15
- - POST /step: Execute an action
16
- - GET /state: Get current environment state
17
- - GET /schema: Get action/observation schemas
18
- - WS /ws: WebSocket endpoint for persistent sessions
19
-
20
- Usage:
21
- # Development (with auto-reload):
22
- uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
-
24
- # Production:
25
- uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
-
27
- # Or run directly:
28
- python -m server.app
29
- """
30
 
31
  try:
32
  from openenv.core.env_server.http_server import create_app
33
  except Exception as e: # pragma: no cover
34
  raise ImportError(
35
- "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
  ) from e
37
 
38
  try:
39
  from ..models import LandscapeforgeAction, LandscapeforgeObservation
40
  from .landscapeforge_environment import LandscapeforgeEnvironment
41
- from ..demo.ui import build_ui as _build_demo_ui
42
  except (ModuleNotFoundError, ImportError):
43
  from models import LandscapeforgeAction, LandscapeforgeObservation # type: ignore
44
  from server.landscapeforge_environment import LandscapeforgeEnvironment # type: ignore
45
- from demo.ui import build_ui as _build_demo_ui # type: ignore
46
 
47
 
48
- # Create the core FastAPI app (without OpenEnv's built-in web UI, which has a
49
- # theme-kwarg incompatibility with Gradio 5.x). We mount our custom Gradio
50
- # demo manually at /web below.
51
  app = create_app(
52
  LandscapeforgeEnvironment,
53
  LandscapeforgeAction,
@@ -56,34 +33,51 @@ app = create_app(
56
  max_concurrent_envs=4,
57
  )
58
 
59
- # Mount the Gradio demo at the root path.
60
- #
61
- # Mounting at "/" lets HF Spaces iframe the app directly without needing
62
- # base_path in the README, and avoids the Gradio 5.x SSR sidecar that 502s
63
- # under HF's Docker reverse proxy. FastAPI routes registered by create_app
64
- # (/reset, /step, /schema, /openapi.json, /health, /ws) keep priority
65
- # because they're exact-match and were registered BEFORE this mount.
66
  try:
67
- import gradio as gr
68
- _demo = _build_demo_ui()
69
- app = gr.mount_gradio_app(
70
- app, _demo,
71
- path="/",
72
- ssr_mode=False,
73
- )
74
- except Exception as _e: # pragma: no cover
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  import logging
76
- logging.getLogger(__name__).warning(
77
- "Gradio demo failed to mount (%s); FastAPI endpoints still available.", _e,
78
- )
79
 
80
 
81
  def main():
82
- """Entry point for direct execution.
83
-
84
- Parses --host / --port from the command line (also honours $PORT),
85
- defaulting to 0.0.0.0:8000 for container-friendly launches.
86
- """
87
  import argparse
88
  import os
89
  import uvicorn
 
1
+ """FastAPI application for LandscapeForge.
 
 
 
 
2
 
3
+ Mounts:
4
+ - OpenEnv endpoints (`/reset`, `/step`, `/schema`, `/ws`, etc.) via create_app
5
+ - Frontend API helpers at `/api/*` (see `api_routes.py`)
6
+ - React SPA at `/` (built to `/app/env/frontend/dist` via Vite)
7
  """
 
8
 
9
+ from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  try:
12
  from openenv.core.env_server.http_server import create_app
13
  except Exception as e: # pragma: no cover
14
  raise ImportError(
15
+ "openenv is required for the web interface. Install dependencies via 'uv sync'."
16
  ) from e
17
 
18
  try:
19
  from ..models import LandscapeforgeAction, LandscapeforgeObservation
20
  from .landscapeforge_environment import LandscapeforgeEnvironment
21
+ from .api_routes import router as lf_api_router
22
  except (ModuleNotFoundError, ImportError):
23
  from models import LandscapeforgeAction, LandscapeforgeObservation # type: ignore
24
  from server.landscapeforge_environment import LandscapeforgeEnvironment # type: ignore
25
+ from server.api_routes import router as lf_api_router # type: ignore
26
 
27
 
 
 
 
28
  app = create_app(
29
  LandscapeforgeEnvironment,
30
  LandscapeforgeAction,
 
33
  max_concurrent_envs=4,
34
  )
35
 
36
+ # Frontend-facing API (landscape, baseline_race, arena, llm_run)
37
+ app.include_router(lf_api_router)
38
+
39
+
40
+ # ---- React SPA serving ----
41
+ # The Dockerfile builds the frontend into `frontend/dist/`. Locate it relative
42
+ # to this file, and serve it under `/` with a SPA fallback to index.html.
43
  try:
44
+ from fastapi.staticfiles import StaticFiles
45
+ from fastapi.responses import FileResponse
46
+
47
+ _here = Path(__file__).resolve().parent.parent
48
+ _dist = _here / "frontend" / "dist"
49
+
50
+ if _dist.is_dir():
51
+ # Assets (hashed css/js) at /assets, vite dist root items at /
52
+ app.mount("/assets", StaticFiles(directory=_dist / "assets"),
53
+ name="lf-assets")
54
+
55
+ @app.get("/")
56
+ def _index():
57
+ return FileResponse(_dist / "index.html")
58
+
59
+ # SPA fallback: any route the FastAPI router doesn't own, serve
60
+ # index.html so client-side routing (if any) works.
61
+ @app.get("/{path:path}")
62
+ def _spa_fallback(path: str):
63
+ # Don't swallow API/OpenEnv routes — FastAPI matches those first
64
+ # because they're registered before this catch-all.
65
+ candidate = _dist / path
66
+ if candidate.is_file():
67
+ return FileResponse(candidate)
68
+ return FileResponse(_dist / "index.html")
69
+ else:
70
+ import logging
71
+ logging.getLogger(__name__).warning(
72
+ "Frontend dist not found at %s — SPA routes disabled.", _dist
73
+ )
74
+ except Exception as _e:
75
  import logging
76
+ logging.getLogger(__name__).warning("SPA mount failed: %s", _e)
 
 
77
 
78
 
79
  def main():
80
+ """Entry point for direct execution."""
 
 
 
 
81
  import argparse
82
  import os
83
  import uvicorn