CoolFace
Apppublic

Tsah00/sql-env

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py390 linesDownload Raw Back to server
1"""2server/app.py - FastAPI server exposing the SQL Environment over HTTP and WebSocket.3 4Endpoints:5  POST /reset             - Start a new episode6  POST /step              - Execute a SQL action7  GET  /state             - Get current environment state8  GET  /health            - Health check (returns HTTP 200)9  WebSocket /ws           - Real-time bidirectional communication10 11Compatible with the OpenEnv client interface.12 13Usage:14  uvicorn server.app:app --host 0.0.0.0 --port 786015"""16 17from __future__ import annotations18 19import asyncio20import json21import logging22import uuid23from contextlib import asynccontextmanager24from typing import Any as _Any25from typing import Any, Dict, Optional26 27from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Body28from fastapi.middleware.cors import CORSMiddleware29from fastapi.responses import HTMLResponse, JSONResponse30from pydantic import BaseModel31 32import sys33import os34sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))35 36from models import SQLAction, SQLObservation, SQLState37from server.sql_environment import SQLEnvironment38 39logging.basicConfig(level=logging.INFO)40logger = logging.getLogger(__name__)41 42# ---------------------------------------------------------------------------43# Pydantic request/response models (for FastAPI schema generation)44# ---------------------------------------------------------------------------45 46class ResetRequest(BaseModel):47    difficulty: str = "easy"48    task_id: Optional[str] = None49 50 51class StepRequest(BaseModel):52    query: str53    task_id: Optional[str] = None54    difficulty: str = "easy"55 56 57# ---------------------------------------------------------------------------58# Session management59# ---------------------------------------------------------------------------60 61# Map episode_id -> SQLEnvironment instance62_sessions: Dict[str, SQLEnvironment] = {}63_default_session_id = "default"64 65 66def _get_or_create_session(session_id: str = _default_session_id) -> SQLEnvironment:67    if session_id not in _sessions:68        _sessions[session_id] = SQLEnvironment()69    return _sessions[session_id]70 71 72# ---------------------------------------------------------------------------73# Application lifecycle74# ---------------------------------------------------------------------------75 76@asynccontextmanager77async def lifespan(app: FastAPI):78    """Startup: initialise default session. Shutdown: close all sessions."""79    env = _get_or_create_session(_default_session_id)80    env.reset()81    logger.info("SQL Environment server started. Default session ready.")82    yield83    for env in _sessions.values():84        env.close()85    logger.info("SQL Environment server stopped.")86 87 88app = FastAPI(89    title="SQL Query Learning Environment",90    description=(91        "An OpenEnv-compatible reinforcement learning environment where "92        "agents learn to write correct and efficient SQL queries."93    ),94    version="1.0.0",95    lifespan=lifespan,96)97 98app.add_middleware(99    CORSMiddleware,100    allow_origins=["*"],101    allow_methods=["*"],102    allow_headers=["*"],103)104 105 106# ---------------------------------------------------------------------------107# HTTP Endpoints108# ---------------------------------------------------------------------------109 110@app.get("/health")111async def health():112    """Health check - required by OpenEnv deployment validation."""113    return JSONResponse({"status": "ok", "environment": "sql_env", "version": "1.0.0"})114 115 116@app.post("/reset")117async def reset(118    request: Optional[ResetRequest] = Body(default=None),119    session_id: str = _default_session_id,120):121    """122    Initialize a new episode.123 124    Returns:125        SQLObservation as JSON with schema_info and first task description.126    """127    env = _get_or_create_session(session_id)128    difficulty = request.difficulty if request else "easy"129    task_id = request.task_id if request else None130    obs: SQLObservation = env.reset(difficulty=difficulty, task_id=task_id)131    return _obs_to_dict(obs)132 133 134@app.post("/step")135async def step(request: StepRequest, session_id: str = _default_session_id):136    """137    Execute a SQL query action.138 139    Returns:140        SQLObservation as JSON with result rows, reward, and feedback.141    """142    env = _get_or_create_session(session_id)143    action = SQLAction(144        query=request.query,145        task_id=request.task_id,146        difficulty=request.difficulty,147    )148    obs: SQLObservation = env.step(action)149    return _obs_to_dict(obs)150 151 152@app.get("/state")153async def get_state(session_id: str = _default_session_id):154    """155    Return current episode state metadata.156 157    Returns:158        SQLState as JSON with episode_id, step_count, total_reward, etc.159    """160    env = _get_or_create_session(session_id)161    return env.state.model_dump()162 163 164@app.get("/tasks")165async def list_tasks():166    """List all available tasks grouped by difficulty."""167    from server.tasks import TASKS168    grouped: Dict[str, list] = {"easy": [], "medium": [], "hard": []}169    for task in TASKS.values():170        entry = {171            "id": task["id"],172            "description": task["description"],173            "expected_columns": task["expected_columns"],174        }175        grouped[task["difficulty"]].append(entry)176    return grouped177 178 179@app.get("/schema")180async def get_schema():181    """Return the database schema description."""182    from server.tasks import SCHEMA_INFO183    return {"schema": SCHEMA_INFO}184 185 186@app.get("/", response_class=HTMLResponse)187async def root():188    """Simple web interface for manual testing."""189    return HTMLResponse(content=_web_ui_html(), status_code=200)190 191 192# ---------------------------------------------------------------------------193# WebSocket Endpoint194# ---------------------------------------------------------------------------195 196@app.websocket("/ws")197async def websocket_endpoint(websocket: WebSocket):198    """199    WebSocket endpoint for real-time agent interaction.200 201    Message format (JSON):202      { "action": "reset", "difficulty": "easy" }203      { "action": "step",  "query": "SELECT ...", "difficulty": "easy" }204      { "action": "state" }205 206    Response format (JSON):207      { "type": "observation"|"state"|"error", "data": {...} }208    """209    await websocket.accept()210    session_id = str(uuid.uuid4())211    env = SQLEnvironment()212    _sessions[session_id] = env213    logger.info(f"WebSocket session opened: {session_id}")214 215    try:216        while True:217            raw = await websocket.receive_text()218            try:219                msg = json.loads(raw)220            except json.JSONDecodeError:221                await websocket.send_text(json.dumps({222                    "type": "error",223                    "data": {"message": "Invalid JSON"},224                }))225                continue226 227            action_type = msg.get("action", "")228 229            if action_type == "reset":230                obs = env.reset(231                    difficulty=msg.get("difficulty", "easy"),232                    task_id=msg.get("task_id"),233                )234                await websocket.send_text(json.dumps({235                    "type": "observation",236                    "data": _obs_to_dict(obs),237                }))238 239            elif action_type == "step":240                query = msg.get("query", "")241                if not query:242                    await websocket.send_text(json.dumps({243                        "type": "error",244                        "data": {"message": "Missing 'query' field"},245                    }))246                    continue247                action = SQLAction(248                    query=query,249                    task_id=msg.get("task_id"),250                    difficulty=msg.get("difficulty", "easy"),251                )252                obs = env.step(action)253                await websocket.send_text(json.dumps({254                    "type": "observation",255                    "data": _obs_to_dict(obs),256                }))257 258            elif action_type == "state":259                await websocket.send_text(json.dumps({260                    "type": "state",261                    "data": env.state.model_dump(),262                }))263 264            else:265                await websocket.send_text(json.dumps({266                    "type": "error",267                    "data": {"message": f"Unknown action: {action_type}"},268                }))269 270    except WebSocketDisconnect:271        logger.info(f"WebSocket session closed: {session_id}")272    finally:273        env.close()274        _sessions.pop(session_id, None)275 276 277# ---------------------------------------------------------------------------278# Helpers279# ---------------------------------------------------------------------------280 281def _obs_to_dict(obs: SQLObservation) -> Dict[str, Any]:282    return obs.model_dump()283 284 285def _web_ui_html() -> str:286    return """287<!DOCTYPE html>288<html lang="en">289<head>290  <meta charset="UTF-8">291  <title>SQL Query Learning Environment</title>292  <style>293    body { font-family: monospace; max-width: 900px; margin: 40px auto; padding: 0 20px; background: #1e1e2e; color: #cdd6f4; }294    h1 { color: #89b4fa; }295    h2 { color: #a6e3a1; font-size: 1em; margin-top: 20px; }296    textarea { width: 100%; height: 120px; background: #313244; color: #cdd6f4; border: 1px solid #585b70; padding: 8px; font-family: monospace; font-size: 13px; }297    button { background: #89b4fa; color: #1e1e2e; border: none; padding: 8px 16px; cursor: pointer; margin: 4px; font-weight: bold; }298    button:hover { background: #74c7ec; }299    select { background: #313244; color: #cdd6f4; border: 1px solid #585b70; padding: 6px; }300    pre { background: #313244; padding: 12px; overflow: auto; font-size: 12px; max-height: 400px; border: 1px solid #585b70; }301    .reward { color: #a6e3a1; font-size: 1.2em; font-weight: bold; }302    .error  { color: #f38ba8; }303    .task   { background: #2a273f; padding: 10px; border-left: 3px solid #cba6f7; margin: 10px 0; }304  </style>305</head>306<body>307  <h1>SQL Query Learning Environment</h1>308  <p>An OpenEnv RL environment for learning SQL against an e-commerce database.</p>309 310  <h2>1. Start Episode</h2>311  Difficulty:312  <select id="difficulty">313    <option value="easy">Easy</option>314    <option value="medium">Medium</option>315    <option value="hard">Hard</option>316  </select>317  <button onclick="doReset()">Reset / New Episode</button>318 319  <div id="taskBox" class="task" style="display:none">320    <b>Task:</b> <span id="taskDesc"></span><br>321    <b>Expected columns:</b> <span id="taskCols"></span>322  </div>323 324  <h2>2. Submit SQL Query</h2>325  <textarea id="query" placeholder="SELECT name, email FROM customers WHERE country = 'USA'"></textarea>326  <br>327  <button onclick="doStep()">Execute Query</button>328  <button onclick="doState()">Get State</button>329 330  <h2>3. Result</h2>331  <div class="reward">Reward: <span id="reward">-</span></div>332  <div id="message"></div>333  <pre id="output">Response will appear here...</pre>334 335  <script>336    const base = window.location.origin;337 338    async function doReset() {339      const diff = document.getElementById('difficulty').value;340      const res = await fetch(base + '/reset', {341        method: 'POST',342        headers: {'Content-Type': 'application/json'},343        body: JSON.stringify({ difficulty: diff })344      });345      const data = await res.json();346      document.getElementById('output').textContent = JSON.stringify(data, null, 2);347      document.getElementById('reward').textContent = data.reward ?? '-';348      document.getElementById('message').textContent = data.message ?? '';349      document.getElementById('taskDesc').textContent = data.task_description ?? '';350      document.getElementById('taskCols').textContent = (data.expected_columns ?? []).join(', ');351      document.getElementById('taskBox').style.display = 'block';352    }353 354    async function doStep() {355      const query = document.getElementById('query').value;356      const diff  = document.getElementById('difficulty').value;357      const res = await fetch(base + '/step', {358        method: 'POST',359        headers: {'Content-Type': 'application/json'},360        body: JSON.stringify({ query, difficulty: diff })361      });362      const data = await res.json();363      document.getElementById('output').textContent = JSON.stringify(data, null, 2);364      document.getElementById('reward').textContent = data.reward ?? '-';365      document.getElementById('message').textContent = data.message ?? '';366      if (data.task_description) {367        document.getElementById('taskDesc').textContent = data.task_description;368        document.getElementById('taskCols').textContent = (data.expected_columns ?? []).join(', ');369      }370    }371 372    async function doState() {373      const res = await fetch(base + '/state');374      const data = await res.json();375      document.getElementById('output').textContent = JSON.stringify(data, null, 2);376    }377  </script>378</body>379</html>380""".strip()381 382 383def main():384    import uvicorn385    uvicorn.run(app, host="0.0.0.0", port=7860)386 387 388if __name__ == "__main__":389    main()390