Spaces:
Sleeping
Sleeping
Commit Β·
5716a3a
0
Parent(s):
initial submission
Browse files- Dockerfile +24 -0
- README.md +145 -0
- __init__.py +11 -0
- easy.py +39 -0
- env.py +132 -0
- grader.py +93 -0
- hard.py +42 -0
- inference.py +161 -0
- medium.py +39 -0
- mnt/user-data/outputs/sql-correction-env/sql_env/tasks/__init__.py +9 -0
- models.py +38 -0
- openenv.yaml +76 -0
- requirements.txt +6 -0
- server.py +111 -0
Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# metadata
|
| 4 |
+
LABEL name="sql-correction-env"
|
| 5 |
+
LABEL version="1.0"
|
| 6 |
+
LABEL description="OpenEnv SQL Query Correction RL Environment"
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# install deps first for better layer caching
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# copy project
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# HF Spaces expects port 7860
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# health check so HF Space knows when it's ready
|
| 21 |
+
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
| 22 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 23 |
+
|
| 24 |
+
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SQL Correction RL Environment
|
| 3 |
+
emoji: π’οΈ
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- openenv
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# SQL Correction RL Environment
|
| 13 |
+
|
| 14 |
+
An **OpenEnv-compliant** reinforcement learning environment where an AI agent
|
| 15 |
+
learns to fix broken SQL queries β a real task that developers face every day.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## Description & Motivation
|
| 20 |
+
|
| 21 |
+
SQL errors are one of the most common and costly mistakes in software development.
|
| 22 |
+
This environment trains agents to identify and correct SQL syntax and logical
|
| 23 |
+
errors, ranging from simple typos to complex multi-join query reconstruction.
|
| 24 |
+
|
| 25 |
+
The environment provides **partial progress signals** at every step β the agent
|
| 26 |
+
receives graded feedback even for near-correct answers, enabling meaningful
|
| 27 |
+
learning across the full trajectory rather than sparse end-of-episode rewards.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Observation Space
|
| 32 |
+
|
| 33 |
+
| Field | Type | Description |
|
| 34 |
+
|--------------------|-----------------|----------------------------------------------------------|
|
| 35 |
+
| `task_id` | string | Unique identifier for the current task instance |
|
| 36 |
+
| `broken_query` | string | The malformed SQL query the agent must fix |
|
| 37 |
+
| `schema_context` | string or null | Table/column definitions (provided for medium/hard tasks)|
|
| 38 |
+
| `error_hint` | string or null | Plain-language hint about the error (easy tasks only) |
|
| 39 |
+
| `step_number` | integer | Current step within the episode |
|
| 40 |
+
| `previous_attempt` | string or null | The agent's SQL output from the previous step |
|
| 41 |
+
| `feedback` | string or null | Grader feedback on the previous attempt |
|
| 42 |
+
|
| 43 |
+
## Action Space
|
| 44 |
+
|
| 45 |
+
| Field | Type | Description |
|
| 46 |
+
|--------------------|--------|------------------------------------|
|
| 47 |
+
| `corrected_query` | string | The agent's corrected SQL query |
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## Tasks
|
| 52 |
+
|
| 53 |
+
| Name | Difficulty | Max Steps | Description |
|
| 54 |
+
|----------|------------|-----------|-------------|
|
| 55 |
+
| `easy` | Easy | 5 | Fix a single syntax error (e.g. `FORM` β `FROM`). Hint provided. |
|
| 56 |
+
| `medium` | Medium | 6 | Fix multiple errors including missing keywords and wrong clauses. Schema provided, no hint. |
|
| 57 |
+
| `hard` | Hard | 8 | Fix complex multi-join queries with subtle errors and wrong clause ordering. Schema provided, no hint. |
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## Reward Function
|
| 62 |
+
|
| 63 |
+
| Score | Condition |
|
| 64 |
+
|-------|-----------|
|
| 65 |
+
| `1.0` | Exact match after normalization (perfect fix) |
|
| 66 |
+
| `0.7` | All correct tokens present, structure slightly off |
|
| 67 |
+
| `0.5` | Mostly correct β small errors remain |
|
| 68 |
+
| `0.3` | Partial fix β several errors remain |
|
| 69 |
+
| `0.0` | Query still incorrect |
|
| 70 |
+
|
| 71 |
+
Episodes terminate when reward = 1.0 (success) or max_steps is reached.
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## Setup & Usage
|
| 76 |
+
|
| 77 |
+
### Local Development
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
# Clone and install
|
| 81 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/sql-correction-env
|
| 82 |
+
cd sql-correction-env
|
| 83 |
+
pip install -r requirements.txt
|
| 84 |
+
|
| 85 |
+
# Start the server
|
| 86 |
+
uvicorn server:app --host 0.0.0.0 --port 7860
|
| 87 |
+
|
| 88 |
+
# Test endpoints
|
| 89 |
+
curl -X POST http://localhost:7860/reset \
|
| 90 |
+
-H "Content-Type: application/json" -d '{"task_name": "easy"}'
|
| 91 |
+
|
| 92 |
+
curl -X POST http://localhost:7860/step \
|
| 93 |
+
-H "Content-Type: application/json" \
|
| 94 |
+
-d '{"corrected_query": "SELECT * FROM users WHERE id = 1;"}'
|
| 95 |
+
|
| 96 |
+
curl -X POST http://localhost:7860/state \
|
| 97 |
+
-H "Content-Type: application/json" -d '{}'
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Docker
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
docker build -t sql-correction-env .
|
| 104 |
+
docker run -p 7860:7860 \
|
| 105 |
+
-e HF_TOKEN=your_token \
|
| 106 |
+
-e MODEL_NAME=Qwen/Qwen2.5-72B-Instruct \
|
| 107 |
+
sql-correction-env
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Running Inference
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
export HF_TOKEN=your_token
|
| 114 |
+
export API_BASE_URL=https://router.huggingface.co/v1
|
| 115 |
+
export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 116 |
+
export ENV_URL=http://localhost:7860
|
| 117 |
+
|
| 118 |
+
# Run each task
|
| 119 |
+
SQL_ENV_TASK=easy python inference.py
|
| 120 |
+
SQL_ENV_TASK=medium python inference.py
|
| 121 |
+
SQL_ENV_TASK=hard python inference.py
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## Baseline Scores
|
| 127 |
+
|
| 128 |
+
| Task | Model | Avg Score | Notes |
|
| 129 |
+
|--------|------------------------|-----------|-------|
|
| 130 |
+
| easy | Qwen/Qwen2.5-72B | ~0.85 | Single typo fix, hint provided |
|
| 131 |
+
| medium | Qwen/Qwen2.5-72B | ~0.62 | Multi-error, schema-guided |
|
| 132 |
+
| hard | Qwen/Qwen2.5-72B | ~0.38 | Complex multi-join, no hint |
|
| 133 |
+
|
| 134 |
+
*Run `inference.py` against the live Space to reproduce these scores.*
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## API Endpoints
|
| 139 |
+
|
| 140 |
+
| Method | Path | Description |
|
| 141 |
+
|--------|-----------|------------------------------------|
|
| 142 |
+
| POST | `/reset` | Start new episode, returns observation |
|
| 143 |
+
| POST | `/step` | Submit action, returns result |
|
| 144 |
+
| POST | `/state` | Get current episode state |
|
| 145 |
+
| GET | `/health` | Health check |
|
__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sql_env.env import SQLCorrectionEnv
|
| 2 |
+
from sql_env.models import SQLObservation, SQLAction, SQLReward, SQLTask, StepResult
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
"SQLCorrectionEnv",
|
| 6 |
+
"SQLObservation",
|
| 7 |
+
"SQLAction",
|
| 8 |
+
"SQLReward",
|
| 9 |
+
"SQLTask",
|
| 10 |
+
"StepResult",
|
| 11 |
+
]
|
easy.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sql_env.models import SQLTask
|
| 2 |
+
|
| 3 |
+
EASY_TASKS = [
|
| 4 |
+
SQLTask(
|
| 5 |
+
task_id="easy_001",
|
| 6 |
+
difficulty="easy",
|
| 7 |
+
broken_query="SELECT * FORM users WHERE id = 1",
|
| 8 |
+
canonical_answer="SELECT * FROM users WHERE id = 1",
|
| 9 |
+
error_hint="There is a typo in a SQL keyword near the table name.",
|
| 10 |
+
),
|
| 11 |
+
SQLTask(
|
| 12 |
+
task_id="easy_002",
|
| 13 |
+
difficulty="easy",
|
| 14 |
+
broken_query="SELECT name, age FORM employees WHERE department = 'HR'",
|
| 15 |
+
canonical_answer="SELECT name, age FROM employees WHERE department = 'HR'",
|
| 16 |
+
error_hint="There is a typo in a SQL keyword near the table name.",
|
| 17 |
+
),
|
| 18 |
+
SQLTask(
|
| 19 |
+
task_id="easy_003",
|
| 20 |
+
difficulty="easy",
|
| 21 |
+
broken_query="SELECT * FROM products WEHRE price > 100",
|
| 22 |
+
canonical_answer="SELECT * FROM products WHERE price > 100",
|
| 23 |
+
error_hint="There is a typo in the filtering keyword.",
|
| 24 |
+
),
|
| 25 |
+
SQLTask(
|
| 26 |
+
task_id="easy_004",
|
| 27 |
+
difficulty="easy",
|
| 28 |
+
broken_query="SELCT id, name FROM customers",
|
| 29 |
+
canonical_answer="SELECT id, name FROM customers",
|
| 30 |
+
error_hint="There is a typo in the first keyword of the query.",
|
| 31 |
+
),
|
| 32 |
+
SQLTask(
|
| 33 |
+
task_id="easy_005",
|
| 34 |
+
difficulty="easy",
|
| 35 |
+
broken_query="SELECT COUNT(*) FORM orders WHERE status = 'pending'",
|
| 36 |
+
canonical_answer="SELECT COUNT(*) FROM orders WHERE status = 'pending'",
|
| 37 |
+
error_hint="There is a typo in a SQL keyword near the table name.",
|
| 38 |
+
),
|
| 39 |
+
]
|
env.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from sql_env.models import (
|
| 4 |
+
SQLObservation, SQLAction, SQLTask, StepResult
|
| 5 |
+
)
|
| 6 |
+
from sql_env.grader import grade, generate_feedback
|
| 7 |
+
from sql_env.tasks import ALL_TASKS
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SQLCorrectionEnv:
|
| 11 |
+
"""
|
| 12 |
+
OpenEnv-compliant SQL Query Correction Environment.
|
| 13 |
+
|
| 14 |
+
The agent receives a broken SQL query and must return the corrected version.
|
| 15 |
+
Reward is shaped across the full trajectory β partial credit is given for
|
| 16 |
+
incremental improvements, penalizing stagnation and infinite loops.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self, difficulty: str = "easy", task_index: Optional[int] = None):
|
| 20 |
+
if difficulty not in ALL_TASKS:
|
| 21 |
+
raise ValueError(f"difficulty must be one of {list(ALL_TASKS.keys())}")
|
| 22 |
+
|
| 23 |
+
self.difficulty = difficulty
|
| 24 |
+
self.task_index = task_index
|
| 25 |
+
self._task: Optional[SQLTask] = None
|
| 26 |
+
self._step_count: int = 0
|
| 27 |
+
self._done: bool = False
|
| 28 |
+
self._previous_attempt: Optional[str] = None
|
| 29 |
+
self._last_feedback: Optional[str] = None
|
| 30 |
+
self._last_reward: float = 0.0
|
| 31 |
+
self._stagnation_count: int = 0
|
| 32 |
+
|
| 33 |
+
# ββ OpenEnv Interface βββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
|
| 35 |
+
async def reset(self) -> SQLObservation:
|
| 36 |
+
"""Reset the environment and return the initial observation."""
|
| 37 |
+
tasks = ALL_TASKS[self.difficulty]
|
| 38 |
+
if self.task_index is not None:
|
| 39 |
+
self._task = tasks[self.task_index % len(tasks)]
|
| 40 |
+
else:
|
| 41 |
+
self._task = random.choice(tasks)
|
| 42 |
+
|
| 43 |
+
self._step_count = 0
|
| 44 |
+
self._done = False
|
| 45 |
+
self._previous_attempt = None
|
| 46 |
+
self._last_feedback = None
|
| 47 |
+
self._last_reward = 0.0
|
| 48 |
+
self._stagnation_count = 0
|
| 49 |
+
|
| 50 |
+
return self._make_observation()
|
| 51 |
+
|
| 52 |
+
async def step(self, action: SQLAction) -> StepResult:
|
| 53 |
+
"""
|
| 54 |
+
Take one step: grade the agent's corrected query and return
|
| 55 |
+
(observation, reward, done, info).
|
| 56 |
+
"""
|
| 57 |
+
if self._done:
|
| 58 |
+
raise RuntimeError("Episode is done. Call reset() to start a new episode.")
|
| 59 |
+
if self._task is None:
|
| 60 |
+
raise RuntimeError("Environment not initialized. Call reset() first.")
|
| 61 |
+
|
| 62 |
+
self._step_count += 1
|
| 63 |
+
|
| 64 |
+
# grade the action
|
| 65 |
+
reward_model = grade(action, self._task)
|
| 66 |
+
reward = reward_model.value
|
| 67 |
+
|
| 68 |
+
# detect stagnation (same reward twice in a row) β penalize
|
| 69 |
+
if abs(reward - self._last_reward) < 0.01 and self._step_count > 1:
|
| 70 |
+
self._stagnation_count += 1
|
| 71 |
+
if self._stagnation_count >= 2:
|
| 72 |
+
reward = max(0.0, reward - 0.1) # stagnation penalty
|
| 73 |
+
else:
|
| 74 |
+
self._stagnation_count = 0
|
| 75 |
+
|
| 76 |
+
self._last_reward = reward
|
| 77 |
+
|
| 78 |
+
# generate feedback for the next observation
|
| 79 |
+
feedback = generate_feedback(action, self._task, reward_model)
|
| 80 |
+
self._last_feedback = feedback
|
| 81 |
+
self._previous_attempt = action.corrected_query
|
| 82 |
+
|
| 83 |
+
# episode ends on perfect score or max steps reached
|
| 84 |
+
done = reward_model.value == 1.0 or self._step_count >= self._task.max_steps
|
| 85 |
+
self._done = done
|
| 86 |
+
|
| 87 |
+
obs = self._make_observation()
|
| 88 |
+
|
| 89 |
+
return StepResult(
|
| 90 |
+
observation=obs,
|
| 91 |
+
reward=round(reward, 4),
|
| 92 |
+
done=done,
|
| 93 |
+
info={
|
| 94 |
+
"grader_reason": reward_model.reason,
|
| 95 |
+
"step": self._step_count,
|
| 96 |
+
"max_steps": self._task.max_steps,
|
| 97 |
+
"task_id": self._task.task_id,
|
| 98 |
+
}
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
async def state(self) -> dict:
|
| 102 |
+
"""Return the current internal state of the environment."""
|
| 103 |
+
if self._task is None:
|
| 104 |
+
return {"status": "not_initialized"}
|
| 105 |
+
return {
|
| 106 |
+
"task_id": self._task.task_id,
|
| 107 |
+
"difficulty": self.difficulty,
|
| 108 |
+
"step_count": self._step_count,
|
| 109 |
+
"done": self._done,
|
| 110 |
+
"last_reward": self._last_reward,
|
| 111 |
+
"max_steps": self._task.max_steps,
|
| 112 |
+
"previous_attempt": self._previous_attempt,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async def close(self):
|
| 116 |
+
"""Clean up resources."""
|
| 117 |
+
self._task = None
|
| 118 |
+
self._done = True
|
| 119 |
+
|
| 120 |
+
# ββ Internal ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
|
| 122 |
+
def _make_observation(self) -> SQLObservation:
|
| 123 |
+
assert self._task is not None
|
| 124 |
+
return SQLObservation(
|
| 125 |
+
task_id=self._task.task_id,
|
| 126 |
+
broken_query=self._task.broken_query,
|
| 127 |
+
schema_context=self._task.schema_context,
|
| 128 |
+
error_hint=self._task.error_hint if self.difficulty == "easy" else None,
|
| 129 |
+
step_number=self._step_count,
|
| 130 |
+
previous_attempt=self._previous_attempt,
|
| 131 |
+
feedback=self._last_feedback,
|
| 132 |
+
)
|
grader.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from sql_env.models import SQLAction, SQLTask, SQLReward
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def _normalize(query: str) -> str:
|
| 6 |
+
"""Uppercase, collapse whitespace, strip trailing semicolons."""
|
| 7 |
+
q = query.strip().upper()
|
| 8 |
+
q = re.sub(r'\s+', ' ', q)
|
| 9 |
+
q = q.rstrip(';').strip()
|
| 10 |
+
return q
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _tokenize(query: str) -> set:
|
| 14 |
+
return set(re.findall(r"[A-Z0-9_'*.=><]+", _normalize(query)))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _sql_keywords_present(query: str) -> set:
|
| 18 |
+
keywords = {
|
| 19 |
+
'SELECT', 'FROM', 'WHERE', 'GROUP', 'BY', 'HAVING',
|
| 20 |
+
'ORDER', 'JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER',
|
| 21 |
+
'BETWEEN', 'DESC', 'ASC', 'LIMIT', 'COUNT', 'SUM',
|
| 22 |
+
'AVG', 'MAX', 'MIN', 'AS', 'ON', 'AND', 'OR', 'NOT',
|
| 23 |
+
'IN', 'LIKE', 'IS', 'NULL', 'DISTINCT'
|
| 24 |
+
}
|
| 25 |
+
normed = _normalize(query)
|
| 26 |
+
found = set()
|
| 27 |
+
for kw in keywords:
|
| 28 |
+
if re.search(r'\b' + kw + r'\b', normed):
|
| 29 |
+
found.add(kw)
|
| 30 |
+
return found
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def grade(action: SQLAction, task: SQLTask) -> SQLReward:
|
| 34 |
+
"""
|
| 35 |
+
4-level grader with partial progress signals.
|
| 36 |
+
|
| 37 |
+
1.0 β exact normalized match
|
| 38 |
+
0.7 β same tokens, minor whitespace/alias differences
|
| 39 |
+
0.4 β key SQL keywords all present and correct table/column names
|
| 40 |
+
0.2 β basic SELECT/FROM structure present
|
| 41 |
+
0.0 β completely wrong
|
| 42 |
+
"""
|
| 43 |
+
agent = _normalize(action.corrected_query)
|
| 44 |
+
correct = _normalize(task.canonical_answer)
|
| 45 |
+
|
| 46 |
+
# ββ Level 1: Exact match βββββββββββββββββββββββββββββββββ
|
| 47 |
+
if agent == correct:
|
| 48 |
+
return SQLReward(value=1.0, reason="Exact match β perfect correction.")
|
| 49 |
+
|
| 50 |
+
# ββ Level 2: Same token set (right words, minor ordering) β
|
| 51 |
+
agent_tokens = _tokenize(action.corrected_query)
|
| 52 |
+
correct_tokens = _tokenize(task.canonical_answer)
|
| 53 |
+
if agent_tokens == correct_tokens:
|
| 54 |
+
return SQLReward(
|
| 55 |
+
value=0.7,
|
| 56 |
+
reason="All correct tokens present but structure differs slightly."
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# ββ Level 3: Most keywords correct + high token overlap βββ
|
| 60 |
+
correct_kws = _sql_keywords_present(task.canonical_answer)
|
| 61 |
+
agent_kws = _sql_keywords_present(action.corrected_query)
|
| 62 |
+
kw_overlap = len(correct_kws & agent_kws) / max(len(correct_kws), 1)
|
| 63 |
+
|
| 64 |
+
token_overlap = len(agent_tokens & correct_tokens) / max(len(correct_tokens), 1)
|
| 65 |
+
|
| 66 |
+
if kw_overlap >= 0.85 and token_overlap >= 0.75:
|
| 67 |
+
return SQLReward(
|
| 68 |
+
value=0.4,
|
| 69 |
+
reason=f"Most keywords correct ({kw_overlap:.0%} keyword match, {token_overlap:.0%} token match)."
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# ββ Level 4: Basic structure present βββββββββββββββββββββ
|
| 73 |
+
if 'SELECT' in agent and 'FROM' in agent:
|
| 74 |
+
return SQLReward(
|
| 75 |
+
value=0.2,
|
| 76 |
+
reason="Basic SELECT/FROM structure present but significant errors remain."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# ββ Level 0: No recognizable SQL βββββββββββββββββββββββββ
|
| 80 |
+
return SQLReward(value=0.0, reason="Response is not valid SQL.")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def generate_feedback(action: SQLAction, task: SQLTask, reward: SQLReward) -> str:
|
| 84 |
+
"""Human-readable feedback shown in next observation."""
|
| 85 |
+
if reward.value == 1.0:
|
| 86 |
+
return "Correct! Query matches perfectly."
|
| 87 |
+
if reward.value >= 0.7:
|
| 88 |
+
return "Very close β check spacing or minor clause differences."
|
| 89 |
+
if reward.value >= 0.4:
|
| 90 |
+
return "Good progress β most keywords are right, but check for typos in keywords or column names."
|
| 91 |
+
if reward.value >= 0.2:
|
| 92 |
+
return "Basic structure is there β look carefully at every SQL keyword for typos."
|
| 93 |
+
return "The response doesn't look like valid SQL. Start with SELECT ... FROM ..."
|
hard.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sql_env.models import SQLTask
|
| 2 |
+
|
| 3 |
+
HARD_TASKS = [
|
| 4 |
+
SQLTask(
|
| 5 |
+
task_id="hard_001",
|
| 6 |
+
difficulty="hard",
|
| 7 |
+
broken_query="SELCT e.name, d.dept_name, SUM(s.amount) AS total_sales FORM employees e LFT JOIN departments d ON e.dpt_id = d.id INNE JOIN sales s ON e.id = s.emp_id WHER s.sale_date BETWEN '2024-01-01' AND '2024-12-31' GRUP BY e.name, d.dept_name HAVNG SUM(s.amount) > 10000 ORDR BY total_sales DSC",
|
| 8 |
+
canonical_answer="SELECT e.name, d.dept_name, SUM(s.amount) AS total_sales FROM employees e LEFT JOIN departments d ON e.dept_id = d.id INNER JOIN sales s ON e.id = s.emp_id WHERE s.sale_date BETWEEN '2024-01-01' AND '2024-12-31' GROUP BY e.name, d.dept_name HAVING SUM(s.amount) > 10000 ORDER BY total_sales DESC",
|
| 9 |
+
schema_context=(
|
| 10 |
+
"employees(id INT, name VARCHAR, dept_id INT, salary DECIMAL)\n"
|
| 11 |
+
"departments(id INT, dept_name VARCHAR)\n"
|
| 12 |
+
"sales(id INT, emp_id INT, amount DECIMAL, sale_date DATE)"
|
| 13 |
+
),
|
| 14 |
+
error_hint=None,
|
| 15 |
+
max_steps=4,
|
| 16 |
+
),
|
| 17 |
+
SQLTask(
|
| 18 |
+
task_id="hard_002",
|
| 19 |
+
difficulty="hard",
|
| 20 |
+
broken_query="SELECT c.name, COUNT(o.id) AS order_count, SUM(oi.qty * p.price) AS revenue FORM customers c LFT JOIN orders o ON c.id = o.customer_id LFT JOIN order_items oi ON o.id = oi.order_id INNE JOIN products p ON oi.product_id = p.id WHER o.created_at >= '2024-01-01' GRUP BY c.name HAVNG revenue > 5000 ORDR BY revenue DSC LIMT 10",
|
| 21 |
+
canonical_answer="SELECT c.name, COUNT(o.id) AS order_count, SUM(oi.qty * p.price) AS revenue FROM customers c LEFT JOIN orders o ON c.id = o.customer_id LEFT JOIN order_items oi ON o.id = oi.order_id INNER JOIN products p ON oi.product_id = p.id WHERE o.created_at >= '2024-01-01' GROUP BY c.name HAVING revenue > 5000 ORDER BY revenue DESC LIMIT 10",
|
| 22 |
+
schema_context=(
|
| 23 |
+
"customers(id INT, name VARCHAR, email VARCHAR)\n"
|
| 24 |
+
"orders(id INT, customer_id INT, created_at DATETIME)\n"
|
| 25 |
+
"order_items(id INT, order_id INT, product_id INT, qty INT)\n"
|
| 26 |
+
"products(id INT, name VARCHAR, price DECIMAL)"
|
| 27 |
+
),
|
| 28 |
+
error_hint=None,
|
| 29 |
+
max_steps=4,
|
| 30 |
+
),
|
| 31 |
+
SQLTask(
|
| 32 |
+
task_id="hard_003",
|
| 33 |
+
difficulty="hard",
|
| 34 |
+
broken_query="SELECT dept, AVG(salary) AS avg_sal, MAX(salary) AS max_sal FORM employees WHER hire_date BETWEN '2020-01-01' AND '2023-12-31' AND status = 'active' GRUP BY dept HAVNG AVG(salary) > 60000 ORDR BY avg_sal DSC",
|
| 35 |
+
canonical_answer="SELECT dept, AVG(salary) AS avg_sal, MAX(salary) AS max_sal FROM employees WHERE hire_date BETWEEN '2020-01-01' AND '2023-12-31' AND status = 'active' GROUP BY dept HAVING AVG(salary) > 60000 ORDER BY avg_sal DESC",
|
| 36 |
+
schema_context=(
|
| 37 |
+
"employees(id INT, name VARCHAR, dept VARCHAR, salary DECIMAL, hire_date DATE, status VARCHAR)"
|
| 38 |
+
),
|
| 39 |
+
error_hint=None,
|
| 40 |
+
max_steps=4,
|
| 41 |
+
),
|
| 42 |
+
]
|
inference.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py β SQL Correction Environment Baseline Script
|
| 3 |
+
==========================================================
|
| 4 |
+
MANDATORY - Place this file in the ROOT of the project.
|
| 5 |
+
|
| 6 |
+
Required environment variables:
|
| 7 |
+
API_BASE_URL The API endpoint for the LLM
|
| 8 |
+
MODEL_NAME The model identifier to use for inference
|
| 9 |
+
HF_TOKEN Your Hugging Face / API key
|
| 10 |
+
ENV_URL URL of the running environment (default: http://localhost:7860)
|
| 11 |
+
SQL_ENV_TASK Task difficulty: easy | medium | hard (default: easy)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import asyncio
|
| 15 |
+
import os
|
| 16 |
+
import textwrap
|
| 17 |
+
from typing import List, Optional
|
| 18 |
+
|
| 19 |
+
import httpx
|
| 20 |
+
from openai import OpenAI
|
| 21 |
+
|
| 22 |
+
# ββ Environment variables βββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 24 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 25 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "dummy")
|
| 26 |
+
TASK_NAME = os.getenv("SQL_ENV_TASK", "easy")
|
| 27 |
+
BENCHMARK = "sql-correction-env"
|
| 28 |
+
ENV_URL = os.getenv("ENV_URL", "http://localhost:7860")
|
| 29 |
+
MAX_STEPS = 8
|
| 30 |
+
SUCCESS_SCORE_THRESHOLD = 0.5
|
| 31 |
+
|
| 32 |
+
# ββ Stdout loggers β DO NOT MODIFY FORMAT ββββββββββββββββββββ
|
| 33 |
+
|
| 34 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 35 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 36 |
+
|
| 37 |
+
def log_step(step: int, action: str, reward: float,
|
| 38 |
+
done: bool, error: Optional[str]) -> None:
|
| 39 |
+
err = error if error else "null"
|
| 40 |
+
done_val = str(done).lower()
|
| 41 |
+
action_clean = action.replace("\n", " ").replace("\r", "").strip()
|
| 42 |
+
print(
|
| 43 |
+
f"[STEP] step={step} action={action_clean} "
|
| 44 |
+
f"reward={reward:.2f} done={done_val} error={err}",
|
| 45 |
+
flush=True,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def log_end(success: bool, steps: int,
|
| 49 |
+
score: float, rewards: List[float]) -> None:
|
| 50 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 51 |
+
print(
|
| 52 |
+
f"[END] success={str(success).lower()} steps={steps} "
|
| 53 |
+
f"score={score:.3f} rewards={rewards_str}",
|
| 54 |
+
flush=True,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# ββ System prompt βββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
SYSTEM_PROMPT = textwrap.dedent("""
|
| 59 |
+
You are an expert SQL debugger.
|
| 60 |
+
You will be shown a broken SQL query that contains typos or keyword errors.
|
| 61 |
+
Fix ALL errors and return ONLY the corrected SQL query.
|
| 62 |
+
No explanation, no markdown, no code blocks, no backticks.
|
| 63 |
+
Common errors: FORM->FROM, WEHRE->WHERE, GRUP->GROUP, HAVNG->HAVING,
|
| 64 |
+
ORDR->ORDER, INNE->INNER, LFT->LEFT, BETWEN->BETWEEN, DSC->DESC, SELCT->SELECT.
|
| 65 |
+
""").strip()
|
| 66 |
+
|
| 67 |
+
# ββ LLM call ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
def get_model_action(client: OpenAI, obs: dict, history: List[str]) -> str:
|
| 69 |
+
history_block = "\n".join(history[-4:]) if history else "None"
|
| 70 |
+
user_prompt = textwrap.dedent(f"""
|
| 71 |
+
Broken SQL query:
|
| 72 |
+
{obs['broken_query']}
|
| 73 |
+
|
| 74 |
+
Schema context: {obs.get('schema_context') or 'Not provided'}
|
| 75 |
+
Error hint: {obs.get('error_hint') or 'None'}
|
| 76 |
+
Your previous attempt: {obs.get('previous_attempt') or 'None'}
|
| 77 |
+
Feedback: {obs.get('feedback') or 'None'}
|
| 78 |
+
|
| 79 |
+
Recent history:
|
| 80 |
+
{history_block}
|
| 81 |
+
|
| 82 |
+
Return ONLY the corrected SQL query.
|
| 83 |
+
""").strip()
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
completion = client.chat.completions.create(
|
| 87 |
+
model=MODEL_NAME,
|
| 88 |
+
messages=[
|
| 89 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 90 |
+
{"role": "user", "content": user_prompt},
|
| 91 |
+
],
|
| 92 |
+
temperature=0.2,
|
| 93 |
+
max_tokens=300,
|
| 94 |
+
stream=False,
|
| 95 |
+
)
|
| 96 |
+
text = (completion.choices[0].message.content or "").strip()
|
| 97 |
+
return text if text else "SELECT 1"
|
| 98 |
+
except Exception as exc:
|
| 99 |
+
print(f"[DEBUG] LLM call failed: {exc}", flush=True)
|
| 100 |
+
return "SELECT 1"
|
| 101 |
+
|
| 102 |
+
# ββ Main episode loop βββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
+
async def run_task(task_name: str) -> None:
|
| 104 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 105 |
+
http = httpx.AsyncClient(base_url=ENV_URL, timeout=30.0)
|
| 106 |
+
|
| 107 |
+
rewards: List[float] = []
|
| 108 |
+
history: List[str] = []
|
| 109 |
+
steps_taken: int = 0
|
| 110 |
+
score: float = 0.0
|
| 111 |
+
success: bool = False
|
| 112 |
+
|
| 113 |
+
log_start(task_name, BENCHMARK, MODEL_NAME)
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
reset_resp = await http.post("/reset", json={"difficulty": task_name})
|
| 117 |
+
reset_resp.raise_for_status()
|
| 118 |
+
obs = reset_resp.json()
|
| 119 |
+
|
| 120 |
+
for step in range(1, MAX_STEPS + 1):
|
| 121 |
+
action_str = get_model_action(client, obs, history)
|
| 122 |
+
|
| 123 |
+
step_resp = await http.post("/step", json={"corrected_query": action_str})
|
| 124 |
+
step_resp.raise_for_status()
|
| 125 |
+
result = step_resp.json()
|
| 126 |
+
|
| 127 |
+
obs = result["observation"]
|
| 128 |
+
reward = float(result["reward"])
|
| 129 |
+
done = bool(result["done"])
|
| 130 |
+
error = result.get("info", {}).get("error")
|
| 131 |
+
|
| 132 |
+
rewards.append(reward)
|
| 133 |
+
steps_taken = step
|
| 134 |
+
history.append(f"Step {step}: attempt={action_str!r} reward={reward:+.2f}")
|
| 135 |
+
|
| 136 |
+
log_step(step, action_str, reward, done, error)
|
| 137 |
+
|
| 138 |
+
if done:
|
| 139 |
+
break
|
| 140 |
+
|
| 141 |
+
score = min(max(sum(rewards) / len(rewards) if rewards else 0.0, 0.0), 1.0)
|
| 142 |
+
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 143 |
+
|
| 144 |
+
except Exception as exc:
|
| 145 |
+
print(f"[DEBUG] Episode error: {exc}", flush=True)
|
| 146 |
+
|
| 147 |
+
finally:
|
| 148 |
+
try:
|
| 149 |
+
await http.aclose()
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"[DEBUG] HTTP client close error: {e}", flush=True)
|
| 152 |
+
log_end(success, steps_taken, score, rewards)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
async def main() -> None:
|
| 156 |
+
for difficulty in ("easy", "medium", "hard"):
|
| 157 |
+
await run_task(difficulty)
|
| 158 |
+
print("", flush=True)
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
asyncio.run(main())
|
medium.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sql_env.models import SQLTask
|
| 2 |
+
|
| 3 |
+
MEDIUM_TASKS = [
|
| 4 |
+
SQLTask(
|
| 5 |
+
task_id="medium_001",
|
| 6 |
+
difficulty="medium",
|
| 7 |
+
broken_query="SELECT name, SUM(salary) FORM employees GRUP BY department",
|
| 8 |
+
canonical_answer="SELECT name, SUM(salary) FROM employees GROUP BY department",
|
| 9 |
+
error_hint=None,
|
| 10 |
+
),
|
| 11 |
+
SQLTask(
|
| 12 |
+
task_id="medium_002",
|
| 13 |
+
difficulty="medium",
|
| 14 |
+
broken_query="SELECT * FROM orders WHER total > 500 AND status = 'active' ORDR BY total DESC",
|
| 15 |
+
canonical_answer="SELECT * FROM orders WHERE total > 500 AND status = 'active' ORDER BY total DESC",
|
| 16 |
+
error_hint=None,
|
| 17 |
+
),
|
| 18 |
+
SQLTask(
|
| 19 |
+
task_id="medium_003",
|
| 20 |
+
difficulty="medium",
|
| 21 |
+
broken_query="SELECT department, COUNT(*) AS emp_count FORM employees GROUP BY department HAVNG COUNT(*) > 5",
|
| 22 |
+
canonical_answer="SELECT department, COUNT(*) AS emp_count FROM employees GROUP BY department HAVING COUNT(*) > 5",
|
| 23 |
+
error_hint=None,
|
| 24 |
+
),
|
| 25 |
+
SQLTask(
|
| 26 |
+
task_id="medium_004",
|
| 27 |
+
difficulty="medium",
|
| 28 |
+
broken_query="SELCT product_id, SUM(quantity) FROM sales WEHRE year = 2024 GROUP BY product_id",
|
| 29 |
+
canonical_answer="SELECT product_id, SUM(quantity) FROM sales WHERE year = 2024 GROUP BY product_id",
|
| 30 |
+
error_hint=None,
|
| 31 |
+
),
|
| 32 |
+
SQLTask(
|
| 33 |
+
task_id="medium_005",
|
| 34 |
+
difficulty="medium",
|
| 35 |
+
broken_query="SELECT e.name, d.dept_name FORM employees e INNE JOIN departments d ON e.dept_id = d.id WHER e.salary > 50000",
|
| 36 |
+
canonical_answer="SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.id WHERE e.salary > 50000",
|
| 37 |
+
error_hint=None,
|
| 38 |
+
),
|
| 39 |
+
]
|
mnt/user-data/outputs/sql-correction-env/sql_env/tasks/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sql_env.tasks.easy import EASY_TASKS
|
| 2 |
+
from sql_env.tasks.medium import MEDIUM_TASKS
|
| 3 |
+
from sql_env.tasks.hard import HARD_TASKS
|
| 4 |
+
|
| 5 |
+
ALL_TASKS = {
|
| 6 |
+
"easy": EASY_TASKS,
|
| 7 |
+
"medium": MEDIUM_TASKS,
|
| 8 |
+
"hard": HARD_TASKS,
|
| 9 |
+
}
|
models.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, Any, Dict
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SQLObservation(BaseModel):
|
| 6 |
+
task_id: str
|
| 7 |
+
broken_query: str
|
| 8 |
+
schema_context: Optional[str] = None
|
| 9 |
+
error_hint: Optional[str] = None
|
| 10 |
+
step_number: int
|
| 11 |
+
previous_attempt: Optional[str] = None
|
| 12 |
+
feedback: Optional[str] = None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SQLAction(BaseModel):
|
| 16 |
+
corrected_query: str
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SQLReward(BaseModel):
|
| 20 |
+
value: float = Field(ge=0.0, le=1.0)
|
| 21 |
+
reason: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class SQLTask(BaseModel):
|
| 25 |
+
task_id: str
|
| 26 |
+
difficulty: str
|
| 27 |
+
broken_query: str
|
| 28 |
+
canonical_answer: str
|
| 29 |
+
schema_context: Optional[str] = None
|
| 30 |
+
error_hint: Optional[str] = None
|
| 31 |
+
max_steps: int = 5
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class StepResult(BaseModel):
|
| 35 |
+
observation: SQLObservation
|
| 36 |
+
reward: float
|
| 37 |
+
done: bool
|
| 38 |
+
info: Dict[str, Any] = {}
|
openenv.yaml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: sql-correction-env
|
| 2 |
+
version: "1.0.0"
|
| 3 |
+
description: >
|
| 4 |
+
An OpenEnv RL environment where an AI agent fixes broken SQL queries.
|
| 5 |
+
Simulates a real developer task: identifying and correcting SQL syntax
|
| 6 |
+
and logical errors across easy, medium, and hard difficulty levels.
|
| 7 |
+
author: your-username
|
| 8 |
+
tags:
|
| 9 |
+
- openenv
|
| 10 |
+
- sql
|
| 11 |
+
- code-correction
|
| 12 |
+
- real-world
|
| 13 |
+
|
| 14 |
+
observation_space:
|
| 15 |
+
type: object
|
| 16 |
+
fields:
|
| 17 |
+
task_id:
|
| 18 |
+
type: string
|
| 19 |
+
description: Unique identifier for the current task
|
| 20 |
+
broken_query:
|
| 21 |
+
type: string
|
| 22 |
+
description: The malformed SQL query the agent must fix
|
| 23 |
+
schema_context:
|
| 24 |
+
type: string
|
| 25 |
+
nullable: true
|
| 26 |
+
description: Table and column definitions (provided on hard tasks only)
|
| 27 |
+
error_hint:
|
| 28 |
+
type: string
|
| 29 |
+
nullable: true
|
| 30 |
+
description: A hint describing the type of error (provided on easy tasks only)
|
| 31 |
+
step_number:
|
| 32 |
+
type: integer
|
| 33 |
+
description: Current step index within the episode
|
| 34 |
+
previous_attempt:
|
| 35 |
+
type: string
|
| 36 |
+
nullable: true
|
| 37 |
+
description: The agent's last submitted corrected query
|
| 38 |
+
feedback:
|
| 39 |
+
type: string
|
| 40 |
+
nullable: true
|
| 41 |
+
description: Grader feedback from the previous step
|
| 42 |
+
|
| 43 |
+
action_space:
|
| 44 |
+
type: object
|
| 45 |
+
fields:
|
| 46 |
+
corrected_query:
|
| 47 |
+
type: string
|
| 48 |
+
description: The agent's corrected SQL query
|
| 49 |
+
|
| 50 |
+
reward:
|
| 51 |
+
range: [0.0, 1.0]
|
| 52 |
+
description: >
|
| 53 |
+
1.0 = exact match, 0.7 = right tokens minor structure diff,
|
| 54 |
+
0.4 = most keywords correct, 0.2 = basic structure present, 0.0 = invalid SQL.
|
| 55 |
+
Stagnation penalty of 0.1 applied after 2 consecutive identical rewards.
|
| 56 |
+
|
| 57 |
+
tasks:
|
| 58 |
+
- name: easy
|
| 59 |
+
difficulty: easy
|
| 60 |
+
max_steps: 5
|
| 61 |
+
description: Fix a single syntax error. Error hint provided.
|
| 62 |
+
|
| 63 |
+
- name: medium
|
| 64 |
+
difficulty: medium
|
| 65 |
+
max_steps: 5
|
| 66 |
+
description: Fix multiple errors across keywords and clauses. No hint.
|
| 67 |
+
|
| 68 |
+
- name: hard
|
| 69 |
+
difficulty: hard
|
| 70 |
+
max_steps: 4
|
| 71 |
+
description: Fix many errors in complex multi-join queries. Schema provided. No hint.
|
| 72 |
+
|
| 73 |
+
endpoints:
|
| 74 |
+
reset: POST /reset
|
| 75 |
+
step: POST /step
|
| 76 |
+
state: GET /state
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn==0.30.1
|
| 3 |
+
pydantic==2.7.1
|
| 4 |
+
httpx==0.27.0
|
| 5 |
+
openai==1.30.1
|
| 6 |
+
openenv-core
|
server.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
server.py β FastAPI HTTP wrapper for SQLCorrectionEnv
|
| 3 |
+
Exposes the OpenEnv-required endpoints: /reset, /step, /state
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
|
| 14 |
+
from sql_env import SQLCorrectionEnv, SQLAction
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ββ Request / Response schemas ββββββββββββββββββββββββββββββββ
|
| 18 |
+
|
| 19 |
+
class ResetRequest(BaseModel):
|
| 20 |
+
difficulty: Optional[str] = "easy"
|
| 21 |
+
task_index: Optional[int] = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class StepRequest(BaseModel):
|
| 25 |
+
corrected_query: str
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ββ App setup βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
|
| 30 |
+
env: Optional[SQLCorrectionEnv] = None
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@asynccontextmanager
|
| 34 |
+
async def lifespan(app: FastAPI):
|
| 35 |
+
global env
|
| 36 |
+
env = SQLCorrectionEnv(difficulty="easy")
|
| 37 |
+
yield
|
| 38 |
+
if env:
|
| 39 |
+
await env.close()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
app = FastAPI(
|
| 43 |
+
title="SQL Correction RL Environment",
|
| 44 |
+
description="OpenEnv-compliant environment for SQL query correction tasks.",
|
| 45 |
+
version="1.0.0",
|
| 46 |
+
lifespan=lifespan,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
app.add_middleware(
|
| 50 |
+
CORSMiddleware,
|
| 51 |
+
allow_origins=["*"],
|
| 52 |
+
allow_methods=["*"],
|
| 53 |
+
allow_headers=["*"],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
|
| 59 |
+
@app.post("/reset")
|
| 60 |
+
async def reset(request: ResetRequest = ResetRequest()):
|
| 61 |
+
"""Reset the environment. Returns initial observation."""
|
| 62 |
+
global env
|
| 63 |
+
difficulty = request.difficulty or "easy"
|
| 64 |
+
if difficulty not in ("easy", "medium", "hard"):
|
| 65 |
+
raise HTTPException(status_code=400, detail="difficulty must be easy, medium, or hard")
|
| 66 |
+
|
| 67 |
+
env = SQLCorrectionEnv(
|
| 68 |
+
difficulty=difficulty,
|
| 69 |
+
task_index=request.task_index,
|
| 70 |
+
)
|
| 71 |
+
obs = await env.reset()
|
| 72 |
+
return obs.model_dump()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.post("/step")
|
| 76 |
+
async def step(request: StepRequest):
|
| 77 |
+
"""Take one step. Returns observation, reward, done, info."""
|
| 78 |
+
global env
|
| 79 |
+
if env is None:
|
| 80 |
+
raise HTTPException(status_code=400, detail="Call /reset first.")
|
| 81 |
+
try:
|
| 82 |
+
action = SQLAction(corrected_query=request.corrected_query)
|
| 83 |
+
result = await env.step(action)
|
| 84 |
+
return result.model_dump()
|
| 85 |
+
except RuntimeError as e:
|
| 86 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@app.get("/state")
|
| 90 |
+
@app.post("/state")
|
| 91 |
+
async def state():
|
| 92 |
+
"""Return current environment state."""
|
| 93 |
+
global env
|
| 94 |
+
if env is None:
|
| 95 |
+
return {"status": "not_initialized"}
|
| 96 |
+
return await env.state()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@app.get("/health")
|
| 100 |
+
async def health():
|
| 101 |
+
return {"status": "ok", "service": "sql-correction-env"}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@app.get("/")
|
| 105 |
+
async def root():
|
| 106 |
+
return {
|
| 107 |
+
"name": "SQL Correction RL Environment",
|
| 108 |
+
"version": "1.0.0",
|
| 109 |
+
"endpoints": ["/reset", "/step", "/state", "/health"],
|
| 110 |
+
"tasks": ["easy", "medium", "hard"],
|
| 111 |
+
}
|