Spaces:
Sleeping
Sleeping
root commited on
Commit ·
38819be
1
Parent(s): 14de67e
hf
Browse files- README.md +1 -0
- __pycache__/inference.cpython-313.pyc +0 -0
- core/__pycache__/__init__.cpython-313.pyc +0 -0
- database/__pycache__/__init__.cpython-313.pyc +0 -0
- database/__pycache__/sqlite_manager.cpython-313.pyc +0 -0
- inference.py +10 -2
- server/__pycache__/app.cpython-313.pyc +0 -0
- server/__pycache__/environment.cpython-313.pyc +0 -0
- server/app.py +6 -16
- server/environment.py +9 -4
- server/playground.html +477 -0
README.md
CHANGED
|
@@ -94,6 +94,7 @@ This creates reproducible benchmark records for easy/medium/hard tasks.
|
|
| 94 |
- `Dockerfile` is included and starts `uvicorn server.app:app`.
|
| 95 |
- Space metadata is configured in this `README.md` frontmatter with `sdk: docker`.
|
| 96 |
- Health endpoint: `/health`
|
|
|
|
| 97 |
|
| 98 |
Local container run:
|
| 99 |
```bash
|
|
|
|
| 94 |
- `Dockerfile` is included and starts `uvicorn server.app:app`.
|
| 95 |
- Space metadata is configured in this `README.md` frontmatter with `sdk: docker`.
|
| 96 |
- Health endpoint: `/health`
|
| 97 |
+
- **Web playground:** open the Space URL (`/`). Click **Connect session** to open a WebSocket to `/ws`, then **Start episode (reset)** and **Run query (step)**. OpenEnv’s HTTP `POST /reset` and `POST /step` each use a fresh environment instance (stateless); the WebSocket session keeps one episode alive for the UI.
|
| 98 |
|
| 99 |
Local container run:
|
| 100 |
```bash
|
__pycache__/inference.cpython-313.pyc
CHANGED
|
Binary files a/__pycache__/inference.cpython-313.pyc and b/__pycache__/inference.cpython-313.pyc differ
|
|
|
core/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/core/__pycache__/__init__.cpython-313.pyc and b/core/__pycache__/__init__.cpython-313.pyc differ
|
|
|
database/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/database/__pycache__/__init__.cpython-313.pyc and b/database/__pycache__/__init__.cpython-313.pyc differ
|
|
|
database/__pycache__/sqlite_manager.cpython-313.pyc
CHANGED
|
Binary files a/database/__pycache__/sqlite_manager.cpython-313.pyc and b/database/__pycache__/sqlite_manager.cpython-313.pyc differ
|
|
|
inference.py
CHANGED
|
@@ -87,8 +87,16 @@ def main():
|
|
| 87 |
obs = self.target.reset()
|
| 88 |
return type('StepResult', (), {'observation': obs, 'reward': 0.0, 'done': False})()
|
| 89 |
def step(self, action):
|
| 90 |
-
obs
|
| 91 |
-
return type(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
def close(self):
|
| 93 |
pass
|
| 94 |
env = DirectClient(base_env)
|
|
|
|
| 87 |
obs = self.target.reset()
|
| 88 |
return type('StepResult', (), {'observation': obs, 'reward': 0.0, 'done': False})()
|
| 89 |
def step(self, action):
|
| 90 |
+
obs = self.target.step(action)
|
| 91 |
+
return type(
|
| 92 |
+
'StepResult',
|
| 93 |
+
(),
|
| 94 |
+
{
|
| 95 |
+
'observation': obs,
|
| 96 |
+
'reward': obs.reward if obs.reward is not None else 0.0,
|
| 97 |
+
'done': obs.done,
|
| 98 |
+
},
|
| 99 |
+
)()
|
| 100 |
def close(self):
|
| 101 |
pass
|
| 102 |
env = DirectClient(base_env)
|
server/__pycache__/app.cpython-313.pyc
CHANGED
|
Binary files a/server/__pycache__/app.cpython-313.pyc and b/server/__pycache__/app.cpython-313.pyc differ
|
|
|
server/__pycache__/environment.cpython-313.pyc
CHANGED
|
Binary files a/server/__pycache__/environment.cpython-313.pyc and b/server/__pycache__/environment.cpython-313.pyc differ
|
|
|
server/app.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
from openenv.core.env_server import create_fastapi_app
|
| 2 |
from fastapi.responses import HTMLResponse
|
| 3 |
import os
|
|
@@ -8,25 +10,13 @@ from server.environment import SqlEnvironment
|
|
| 8 |
|
| 9 |
app = create_fastapi_app(SqlEnvironment, SqlAction, SqlObservation)
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
@app.get("/")
|
| 13 |
def root():
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
<html>
|
| 17 |
-
<head><title>SQL OpenEnv Space</title></head>
|
| 18 |
-
<body style="font-family: sans-serif; margin: 2rem;">
|
| 19 |
-
<h2>SQL Data Analyst OpenEnv Environment</h2>
|
| 20 |
-
<p>Environment is running.</p>
|
| 21 |
-
<ul>
|
| 22 |
-
<li>Health check: <a href="/health">/health</a></li>
|
| 23 |
-
<li>OpenAPI docs: <a href="/docs">/docs</a></li>
|
| 24 |
-
</ul>
|
| 25 |
-
<p>Use the OpenEnv client in <code>inference.py</code> to run baseline episodes.</p>
|
| 26 |
-
</body>
|
| 27 |
-
</html>
|
| 28 |
-
"""
|
| 29 |
-
)
|
| 30 |
|
| 31 |
|
| 32 |
@app.get("/health")
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
from openenv.core.env_server import create_fastapi_app
|
| 4 |
from fastapi.responses import HTMLResponse
|
| 5 |
import os
|
|
|
|
| 10 |
|
| 11 |
app = create_fastapi_app(SqlEnvironment, SqlAction, SqlObservation)
|
| 12 |
|
| 13 |
+
_PLAYGROUND_HTML = Path(__file__).resolve().parent / "playground.html"
|
| 14 |
+
|
| 15 |
|
| 16 |
@app.get("/")
|
| 17 |
def root():
|
| 18 |
+
html = _PLAYGROUND_HTML.read_text(encoding="utf-8")
|
| 19 |
+
return HTMLResponse(html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
@app.get("/health")
|
server/environment.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import uuid
|
| 2 |
-
from typing import Tuple
|
| 3 |
|
| 4 |
from openenv.core.env_server import Environment
|
| 5 |
from models import SqlAction, SqlObservation, SqlState, SqlReward
|
|
@@ -35,13 +34,17 @@ class SqlEnvironment(Environment):
|
|
| 35 |
difficulty=first_task.difficulty_label,
|
| 36 |
)
|
| 37 |
|
|
|
|
| 38 |
def state(self) -> SqlState:
|
| 39 |
return self._state
|
| 40 |
|
| 41 |
-
def step(self, action: SqlAction) ->
|
| 42 |
self._state.step_count += 1
|
| 43 |
query = action.query.strip()
|
| 44 |
current_idx = self._state.current_task_index
|
|
|
|
|
|
|
|
|
|
| 45 |
task = self.task_registry.get_task(current_idx)
|
| 46 |
self._state.attempts_per_task[current_idx] += 1
|
| 47 |
|
|
@@ -135,6 +138,8 @@ class SqlEnvironment(Environment):
|
|
| 135 |
execution_error=error_str,
|
| 136 |
task_score=score,
|
| 137 |
grader_feedback=grader_feedback,
|
|
|
|
|
|
|
| 138 |
)
|
| 139 |
-
|
| 140 |
-
return obs
|
|
|
|
| 1 |
import uuid
|
|
|
|
| 2 |
|
| 3 |
from openenv.core.env_server import Environment
|
| 4 |
from models import SqlAction, SqlObservation, SqlState, SqlReward
|
|
|
|
| 34 |
difficulty=first_task.difficulty_label,
|
| 35 |
)
|
| 36 |
|
| 37 |
+
@property
|
| 38 |
def state(self) -> SqlState:
|
| 39 |
return self._state
|
| 40 |
|
| 41 |
+
def step(self, action: SqlAction) -> SqlObservation:
|
| 42 |
self._state.step_count += 1
|
| 43 |
query = action.query.strip()
|
| 44 |
current_idx = self._state.current_task_index
|
| 45 |
+
n_tasks = self.task_registry.get_total_tasks()
|
| 46 |
+
while len(self._state.attempts_per_task) < n_tasks:
|
| 47 |
+
self._state.attempts_per_task.append(0)
|
| 48 |
task = self.task_registry.get_task(current_idx)
|
| 49 |
self._state.attempts_per_task[current_idx] += 1
|
| 50 |
|
|
|
|
| 138 |
execution_error=error_str,
|
| 139 |
task_score=score,
|
| 140 |
grader_feedback=grader_feedback,
|
| 141 |
+
reward=reward.total,
|
| 142 |
+
done=done,
|
| 143 |
)
|
| 144 |
+
|
| 145 |
+
return obs
|
server/playground.html
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>SQL Analyst OpenEnv · Playground</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=JetBrains+Mono:wght@400;600&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,700;1,6..72,400&display=swap" rel="stylesheet" />
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--bg: #0f1419;
|
| 13 |
+
--surface: #1a222d;
|
| 14 |
+
--border: #2d3a4a;
|
| 15 |
+
--text: #e7ecf3;
|
| 16 |
+
--muted: #8b9cb3;
|
| 17 |
+
--accent: #3ddc97;
|
| 18 |
+
--warn: #f5a623;
|
| 19 |
+
--err: #ff6b6b;
|
| 20 |
+
--sql-bg: #121820;
|
| 21 |
+
}
|
| 22 |
+
* { box-sizing: border-box; }
|
| 23 |
+
body {
|
| 24 |
+
margin: 0;
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
font-family: "Newsreader", Georgia, serif;
|
| 27 |
+
background: var(--bg);
|
| 28 |
+
color: var(--text);
|
| 29 |
+
background-image:
|
| 30 |
+
radial-gradient(ellipse 120% 80% at 100% -20%, rgba(61, 220, 151, 0.08), transparent),
|
| 31 |
+
radial-gradient(ellipse 80% 50% at 0% 100%, rgba(45, 58, 74, 0.5), transparent);
|
| 32 |
+
}
|
| 33 |
+
.wrap {
|
| 34 |
+
max-width: 1180px;
|
| 35 |
+
margin: 0 auto;
|
| 36 |
+
padding: 1.75rem 1.25rem 3rem;
|
| 37 |
+
}
|
| 38 |
+
header {
|
| 39 |
+
display: flex;
|
| 40 |
+
flex-wrap: wrap;
|
| 41 |
+
align-items: baseline;
|
| 42 |
+
justify-content: space-between;
|
| 43 |
+
gap: 1rem;
|
| 44 |
+
margin-bottom: 1.5rem;
|
| 45 |
+
border-bottom: 1px solid var(--border);
|
| 46 |
+
padding-bottom: 1rem;
|
| 47 |
+
}
|
| 48 |
+
h1 {
|
| 49 |
+
font-size: 1.65rem;
|
| 50 |
+
font-weight: 700;
|
| 51 |
+
margin: 0;
|
| 52 |
+
letter-spacing: -0.02em;
|
| 53 |
+
}
|
| 54 |
+
.tag {
|
| 55 |
+
font-family: "JetBrains Mono", monospace;
|
| 56 |
+
font-size: 0.7rem;
|
| 57 |
+
text-transform: uppercase;
|
| 58 |
+
letter-spacing: 0.12em;
|
| 59 |
+
color: var(--accent);
|
| 60 |
+
border: 1px solid var(--border);
|
| 61 |
+
padding: 0.25rem 0.5rem;
|
| 62 |
+
border-radius: 4px;
|
| 63 |
+
}
|
| 64 |
+
.links a {
|
| 65 |
+
color: var(--muted);
|
| 66 |
+
font-size: 0.9rem;
|
| 67 |
+
margin-left: 1rem;
|
| 68 |
+
}
|
| 69 |
+
.links a:hover { color: var(--accent); }
|
| 70 |
+
.grid {
|
| 71 |
+
display: grid;
|
| 72 |
+
grid-template-columns: 1fr 1fr;
|
| 73 |
+
gap: 1rem;
|
| 74 |
+
}
|
| 75 |
+
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
| 76 |
+
.panel {
|
| 77 |
+
background: var(--surface);
|
| 78 |
+
border: 1px solid var(--border);
|
| 79 |
+
border-radius: 10px;
|
| 80 |
+
padding: 1rem 1.1rem;
|
| 81 |
+
}
|
| 82 |
+
.panel h2 {
|
| 83 |
+
font-size: 0.85rem;
|
| 84 |
+
font-weight: 700;
|
| 85 |
+
text-transform: uppercase;
|
| 86 |
+
letter-spacing: 0.06em;
|
| 87 |
+
color: var(--muted);
|
| 88 |
+
margin: 0 0 0.75rem;
|
| 89 |
+
}
|
| 90 |
+
.task-meta {
|
| 91 |
+
display: flex;
|
| 92 |
+
gap: 0.5rem;
|
| 93 |
+
flex-wrap: wrap;
|
| 94 |
+
margin-bottom: 0.75rem;
|
| 95 |
+
}
|
| 96 |
+
.pill {
|
| 97 |
+
font-family: "JetBrains Mono", monospace;
|
| 98 |
+
font-size: 0.72rem;
|
| 99 |
+
padding: 0.2rem 0.5rem;
|
| 100 |
+
border-radius: 4px;
|
| 101 |
+
background: var(--sql-bg);
|
| 102 |
+
border: 1px solid var(--border);
|
| 103 |
+
}
|
| 104 |
+
.pill.diff-easy { color: var(--accent); }
|
| 105 |
+
.pill.diff-medium { color: var(--warn); }
|
| 106 |
+
.pill.diff-hard { color: var(--err); }
|
| 107 |
+
pre.schema, .instruction {
|
| 108 |
+
font-size: 0.95rem;
|
| 109 |
+
line-height: 1.55;
|
| 110 |
+
white-space: pre-wrap;
|
| 111 |
+
word-break: break-word;
|
| 112 |
+
margin: 0;
|
| 113 |
+
color: var(--muted);
|
| 114 |
+
}
|
| 115 |
+
.instruction { color: var(--text); }
|
| 116 |
+
textarea#sql {
|
| 117 |
+
width: 100%;
|
| 118 |
+
min-height: 140px;
|
| 119 |
+
font-family: "JetBrains Mono", monospace;
|
| 120 |
+
font-size: 0.82rem;
|
| 121 |
+
line-height: 1.5;
|
| 122 |
+
background: var(--sql-bg);
|
| 123 |
+
color: var(--text);
|
| 124 |
+
border: 1px solid var(--border);
|
| 125 |
+
border-radius: 8px;
|
| 126 |
+
padding: 0.85rem;
|
| 127 |
+
resize: vertical;
|
| 128 |
+
}
|
| 129 |
+
textarea#sql:focus {
|
| 130 |
+
outline: none;
|
| 131 |
+
border-color: var(--accent);
|
| 132 |
+
box-shadow: 0 0 0 1px rgba(61, 220, 151, 0.25);
|
| 133 |
+
}
|
| 134 |
+
.actions {
|
| 135 |
+
display: flex;
|
| 136 |
+
flex-wrap: wrap;
|
| 137 |
+
gap: 0.6rem;
|
| 138 |
+
margin-top: 0.75rem;
|
| 139 |
+
}
|
| 140 |
+
button {
|
| 141 |
+
font-family: "JetBrains Mono", monospace;
|
| 142 |
+
font-size: 0.78rem;
|
| 143 |
+
padding: 0.55rem 1rem;
|
| 144 |
+
border-radius: 6px;
|
| 145 |
+
border: 1px solid var(--border);
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
background: var(--surface);
|
| 148 |
+
color: var(--text);
|
| 149 |
+
}
|
| 150 |
+
button.primary {
|
| 151 |
+
background: rgba(61, 220, 151, 0.15);
|
| 152 |
+
border-color: var(--accent);
|
| 153 |
+
color: var(--accent);
|
| 154 |
+
}
|
| 155 |
+
button:hover { filter: brightness(1.12); }
|
| 156 |
+
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
| 157 |
+
.metrics {
|
| 158 |
+
display: grid;
|
| 159 |
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
| 160 |
+
gap: 0.5rem;
|
| 161 |
+
margin-top: 0.75rem;
|
| 162 |
+
}
|
| 163 |
+
.metric {
|
| 164 |
+
background: var(--sql-bg);
|
| 165 |
+
border: 1px solid var(--border);
|
| 166 |
+
border-radius: 6px;
|
| 167 |
+
padding: 0.5rem 0.65rem;
|
| 168 |
+
}
|
| 169 |
+
.metric label {
|
| 170 |
+
display: block;
|
| 171 |
+
font-size: 0.65rem;
|
| 172 |
+
text-transform: uppercase;
|
| 173 |
+
letter-spacing: 0.08em;
|
| 174 |
+
color: var(--muted);
|
| 175 |
+
}
|
| 176 |
+
.metric span {
|
| 177 |
+
font-family: "JetBrains Mono", monospace;
|
| 178 |
+
font-size: 1rem;
|
| 179 |
+
}
|
| 180 |
+
.error { color: var(--err); font-size: 0.9rem; margin-top: 0.5rem; }
|
| 181 |
+
.feedback { color: var(--warn); font-size: 0.9rem; margin-top: 0.35rem; }
|
| 182 |
+
table.result {
|
| 183 |
+
width: 100%;
|
| 184 |
+
border-collapse: collapse;
|
| 185 |
+
font-family: "JetBrains Mono", monospace;
|
| 186 |
+
font-size: 0.75rem;
|
| 187 |
+
margin-top: 0.5rem;
|
| 188 |
+
}
|
| 189 |
+
table.result th, table.result td {
|
| 190 |
+
border: 1px solid var(--border);
|
| 191 |
+
padding: 0.35rem 0.5rem;
|
| 192 |
+
text-align: left;
|
| 193 |
+
}
|
| 194 |
+
table.result th { background: var(--sql-bg); color: var(--muted); }
|
| 195 |
+
.log {
|
| 196 |
+
font-family: "JetBrains Mono", monospace;
|
| 197 |
+
font-size: 0.72rem;
|
| 198 |
+
line-height: 1.5;
|
| 199 |
+
max-height: 220px;
|
| 200 |
+
overflow-y: auto;
|
| 201 |
+
background: var(--sql-bg);
|
| 202 |
+
border: 1px solid var(--border);
|
| 203 |
+
border-radius: 8px;
|
| 204 |
+
padding: 0.65rem;
|
| 205 |
+
color: var(--muted);
|
| 206 |
+
white-space: pre-wrap;
|
| 207 |
+
word-break: break-all;
|
| 208 |
+
}
|
| 209 |
+
.full { grid-column: 1 / -1; }
|
| 210 |
+
.status-bar {
|
| 211 |
+
font-family: "JetBrains Mono", monospace;
|
| 212 |
+
font-size: 0.72rem;
|
| 213 |
+
color: var(--muted);
|
| 214 |
+
margin-top: 0.5rem;
|
| 215 |
+
}
|
| 216 |
+
.status-bar.ok { color: var(--accent); }
|
| 217 |
+
.status-bar.bad { color: var(--err); }
|
| 218 |
+
</style>
|
| 219 |
+
</head>
|
| 220 |
+
<body>
|
| 221 |
+
<div class="wrap">
|
| 222 |
+
<header>
|
| 223 |
+
<div>
|
| 224 |
+
<span class="tag">OpenEnv</span>
|
| 225 |
+
<h1>SQL data analyst playground</h1>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="links">
|
| 228 |
+
<a href="/docs">API docs</a>
|
| 229 |
+
<a href="/health">Health</a>
|
| 230 |
+
</div>
|
| 231 |
+
</header>
|
| 232 |
+
<p class="status-bar" style="margin:-0.5rem 0 1.25rem;">
|
| 233 |
+
Interactive mode uses a persistent <strong>WebSocket</strong> session at <code>/ws</code>
|
| 234 |
+
(OpenEnv HTTP <code>/step</code> is stateless). Connect, then run reset → SQL steps.
|
| 235 |
+
</p>
|
| 236 |
+
|
| 237 |
+
<div class="grid">
|
| 238 |
+
<div class="panel">
|
| 239 |
+
<h2>Current task</h2>
|
| 240 |
+
<div class="task-meta">
|
| 241 |
+
<span id="taskId" class="pill">—</span>
|
| 242 |
+
<span id="difficulty" class="pill">—</span>
|
| 243 |
+
</div>
|
| 244 |
+
<p id="instruction" class="instruction">Click “Start episode” to load the first task.</p>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="panel">
|
| 247 |
+
<h2>Schema</h2>
|
| 248 |
+
<pre id="schema" class="schema">—</pre>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div class="panel full">
|
| 252 |
+
<h2>Your SQL</h2>
|
| 253 |
+
<textarea id="sql" placeholder="SELECT ..."></textarea>
|
| 254 |
+
<div class="actions">
|
| 255 |
+
<button type="button" class="primary" id="btnConnect">Connect session</button>
|
| 256 |
+
<button type="button" class="primary" id="btnReset" disabled>Start episode (reset)</button>
|
| 257 |
+
<button type="button" class="primary" id="btnStep" disabled>Run query (step)</button>
|
| 258 |
+
<button type="button" id="btnState" disabled>Refresh state</button>
|
| 259 |
+
</div>
|
| 260 |
+
<div id="reqStatus" class="status-bar">Disconnected — click “Connect session”.</div>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<div class="panel">
|
| 264 |
+
<h2>Last step</h2>
|
| 265 |
+
<div class="metrics">
|
| 266 |
+
<div class="metric"><label>Reward</label><span id="reward">—</span></div>
|
| 267 |
+
<div class="metric"><label>Task score</label><span id="score">—</span></div>
|
| 268 |
+
<div class="metric"><label>Episode done</label><span id="done">—</span></div>
|
| 269 |
+
</div>
|
| 270 |
+
<p id="graderFb" class="feedback" hidden></p>
|
| 271 |
+
<p id="execErr" class="error" hidden></p>
|
| 272 |
+
</div>
|
| 273 |
+
<div class="panel">
|
| 274 |
+
<h2>Result preview</h2>
|
| 275 |
+
<div id="resultTable"></div>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
<div class="panel full">
|
| 279 |
+
<h2>Episode log</h2>
|
| 280 |
+
<div id="log" class="log"></div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<script>
|
| 286 |
+
const $ = (id) => document.getElementById(id);
|
| 287 |
+
const logEl = $("log");
|
| 288 |
+
function log(msg) {
|
| 289 |
+
const t = new Date().toISOString().slice(11, 19);
|
| 290 |
+
logEl.textContent += "[" + t + "] " + msg + "\n";
|
| 291 |
+
logEl.scrollTop = logEl.scrollHeight;
|
| 292 |
+
}
|
| 293 |
+
function setDifficultyClass(el, d) {
|
| 294 |
+
el.className = "pill diff-" + (d || "").toLowerCase();
|
| 295 |
+
}
|
| 296 |
+
function renderObservation(obs) {
|
| 297 |
+
if (!obs) return;
|
| 298 |
+
$("taskId").textContent = obs.task_id || "—";
|
| 299 |
+
const diff = obs.difficulty || "";
|
| 300 |
+
const diffEl = $("difficulty");
|
| 301 |
+
diffEl.textContent = diff || "—";
|
| 302 |
+
setDifficultyClass(diffEl, diff);
|
| 303 |
+
$("instruction").textContent = obs.current_task_instruction || "";
|
| 304 |
+
$("schema").textContent = obs.schema_info || "—";
|
| 305 |
+
}
|
| 306 |
+
function renderTableFromJson(jsonStr) {
|
| 307 |
+
const host = $("resultTable");
|
| 308 |
+
host.innerHTML = "";
|
| 309 |
+
if (!jsonStr) {
|
| 310 |
+
host.textContent = "No rows yet.";
|
| 311 |
+
return;
|
| 312 |
+
}
|
| 313 |
+
let rows;
|
| 314 |
+
try { rows = JSON.parse(jsonStr); } catch (e) {
|
| 315 |
+
host.textContent = String(jsonStr).slice(0, 2000);
|
| 316 |
+
return;
|
| 317 |
+
}
|
| 318 |
+
if (!Array.isArray(rows) || rows.length === 0) {
|
| 319 |
+
host.textContent = "0 rows.";
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
+
const cols = Object.keys(rows[0]);
|
| 323 |
+
const table = document.createElement("table");
|
| 324 |
+
table.className = "result";
|
| 325 |
+
const thead = document.createElement("thead");
|
| 326 |
+
const trh = document.createElement("tr");
|
| 327 |
+
cols.forEach((c) => {
|
| 328 |
+
const th = document.createElement("th");
|
| 329 |
+
th.textContent = c;
|
| 330 |
+
trh.appendChild(th);
|
| 331 |
+
});
|
| 332 |
+
thead.appendChild(trh);
|
| 333 |
+
table.appendChild(thead);
|
| 334 |
+
const tbody = document.createElement("tbody");
|
| 335 |
+
rows.slice(0, 50).forEach((row) => {
|
| 336 |
+
const tr = document.createElement("tr");
|
| 337 |
+
cols.forEach((c) => {
|
| 338 |
+
const td = document.createElement("td");
|
| 339 |
+
const v = row[c];
|
| 340 |
+
td.textContent = v === null || v === undefined ? "" : String(v);
|
| 341 |
+
tr.appendChild(td);
|
| 342 |
+
});
|
| 343 |
+
tbody.appendChild(tr);
|
| 344 |
+
});
|
| 345 |
+
table.appendChild(tbody);
|
| 346 |
+
host.appendChild(table);
|
| 347 |
+
if (rows.length > 50) {
|
| 348 |
+
const note = document.createElement("p");
|
| 349 |
+
note.className = "status-bar";
|
| 350 |
+
note.textContent = "Showing first 50 rows.";
|
| 351 |
+
host.appendChild(note);
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
function setStatus(el, ok, text) {
|
| 355 |
+
el.textContent = text;
|
| 356 |
+
el.className = "status-bar " + (ok ? "ok" : "bad");
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
let socket = null;
|
| 360 |
+
|
| 361 |
+
function setConnected(connected) {
|
| 362 |
+
$("btnReset").disabled = !connected;
|
| 363 |
+
$("btnStep").disabled = !connected;
|
| 364 |
+
$("btnState").disabled = !connected;
|
| 365 |
+
$("btnConnect").textContent = connected ? "Reconnect" : "Connect session";
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function handleWsMessage(raw) {
|
| 369 |
+
let msg;
|
| 370 |
+
try {
|
| 371 |
+
msg = JSON.parse(raw);
|
| 372 |
+
} catch (e) {
|
| 373 |
+
log("parse error: " + e);
|
| 374 |
+
return;
|
| 375 |
+
}
|
| 376 |
+
const status = $("reqStatus");
|
| 377 |
+
if (msg.type === "error") {
|
| 378 |
+
setStatus(status, false, msg.data && msg.data.message ? msg.data.message : "Server error");
|
| 379 |
+
log("error: " + JSON.stringify(msg.data));
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
if (msg.type === "observation") {
|
| 383 |
+
const pack = msg.data || {};
|
| 384 |
+
const obs = pack.observation;
|
| 385 |
+
const rew = pack.reward;
|
| 386 |
+
const done = pack.done;
|
| 387 |
+
setStatus(status, true, "OK");
|
| 388 |
+
log("observation reward=" + rew + " done=" + done);
|
| 389 |
+
$("reward").textContent = rew != null ? Number(rew).toFixed(3) : "—";
|
| 390 |
+
$("done").textContent = String(!!done);
|
| 391 |
+
$("score").textContent = obs && obs.task_score != null ? Number(obs.task_score).toFixed(3) : "—";
|
| 392 |
+
renderObservation(obs);
|
| 393 |
+
renderTableFromJson(obs && obs.execution_result);
|
| 394 |
+
const fb = $("graderFb");
|
| 395 |
+
if (obs && obs.grader_feedback) {
|
| 396 |
+
fb.textContent = obs.grader_feedback;
|
| 397 |
+
fb.hidden = false;
|
| 398 |
+
} else { fb.hidden = true; }
|
| 399 |
+
const er = $("execErr");
|
| 400 |
+
if (obs && obs.execution_error) {
|
| 401 |
+
er.textContent = obs.execution_error;
|
| 402 |
+
er.hidden = false;
|
| 403 |
+
} else { er.hidden = true; }
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
+
if (msg.type === "state") {
|
| 407 |
+
setStatus(status, true, "State received (see log).");
|
| 408 |
+
log("state: " + JSON.stringify(msg.data).slice(0, 2000));
|
| 409 |
+
return;
|
| 410 |
+
}
|
| 411 |
+
log("unknown msg: " + raw.slice(0, 500));
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
function connectWs() {
|
| 415 |
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
| 416 |
+
socket.close();
|
| 417 |
+
}
|
| 418 |
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
| 419 |
+
const url = proto + "//" + location.host + "/ws";
|
| 420 |
+
setStatus($("reqStatus"), true, "Connecting…");
|
| 421 |
+
socket = new WebSocket(url);
|
| 422 |
+
socket.onopen = function () {
|
| 423 |
+
setConnected(true);
|
| 424 |
+
setStatus($("reqStatus"), true, "WebSocket connected. Run reset to start.");
|
| 425 |
+
log("WebSocket open " + url);
|
| 426 |
+
};
|
| 427 |
+
socket.onclose = function () {
|
| 428 |
+
setConnected(false);
|
| 429 |
+
setStatus($("reqStatus"), false, "Disconnected.");
|
| 430 |
+
log("WebSocket closed");
|
| 431 |
+
};
|
| 432 |
+
socket.onerror = function () {
|
| 433 |
+
setStatus($("reqStatus"), false, "WebSocket error.");
|
| 434 |
+
log("WebSocket error");
|
| 435 |
+
};
|
| 436 |
+
socket.onmessage = function (ev) {
|
| 437 |
+
handleWsMessage(ev.data);
|
| 438 |
+
};
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
function wsSend(obj) {
|
| 442 |
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
| 443 |
+
setStatus($("reqStatus"), false, "Not connected.");
|
| 444 |
+
return;
|
| 445 |
+
}
|
| 446 |
+
socket.send(JSON.stringify(obj));
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
$("btnConnect").addEventListener("click", connectWs);
|
| 450 |
+
|
| 451 |
+
$("btnReset").addEventListener("click", function () {
|
| 452 |
+
wsSend({ type: "reset", data: {} });
|
| 453 |
+
$("graderFb").hidden = true;
|
| 454 |
+
$("execErr").hidden = true;
|
| 455 |
+
renderTableFromJson(null);
|
| 456 |
+
log("sent reset");
|
| 457 |
+
});
|
| 458 |
+
|
| 459 |
+
$("btnStep").addEventListener("click", function () {
|
| 460 |
+
const q = $("sql").value.trim();
|
| 461 |
+
if (!q) {
|
| 462 |
+
setStatus($("reqStatus"), false, "Enter a SQL query.");
|
| 463 |
+
return;
|
| 464 |
+
}
|
| 465 |
+
wsSend({ type: "step", data: { query: q } });
|
| 466 |
+
log("sent step");
|
| 467 |
+
});
|
| 468 |
+
|
| 469 |
+
$("btnState").addEventListener("click", function () {
|
| 470 |
+
wsSend({ type: "state" });
|
| 471 |
+
log("sent state");
|
| 472 |
+
});
|
| 473 |
+
|
| 474 |
+
log("Open the Space, click Connect session, then Reset → run SQL.");
|
| 475 |
+
</script>
|
| 476 |
+
</body>
|
| 477 |
+
</html>
|