sravaniamere commited on
Commit
5716a3a
Β·
0 Parent(s):

initial submission

Browse files
Files changed (14) hide show
  1. Dockerfile +24 -0
  2. README.md +145 -0
  3. __init__.py +11 -0
  4. easy.py +39 -0
  5. env.py +132 -0
  6. grader.py +93 -0
  7. hard.py +42 -0
  8. inference.py +161 -0
  9. medium.py +39 -0
  10. mnt/user-data/outputs/sql-correction-env/sql_env/tasks/__init__.py +9 -0
  11. models.py +38 -0
  12. openenv.yaml +76 -0
  13. requirements.txt +6 -0
  14. 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
+ }