Abhishek Tiwari commited on
Commit
b6b4d3b
·
1 Parent(s): 755655b

Rewrite environment, app endpoints, and openenv.yaml for Phase 2 validation

Browse files
Files changed (3) hide show
  1. openenv.yaml +68 -19
  2. server/app.py +52 -22
  3. server/environment.py +249 -134
openenv.yaml CHANGED
@@ -1,24 +1,73 @@
1
- spec_version: 1
2
- name: sql-agent-env
3
- description: SQL-RL (OpenEnv) — schema → SQL tasks with deterministic graders
4
- type: space
5
- runtime: fastapi
6
- app: server.app:app
7
- port: 7860
8
 
9
  tasks:
10
- - id: employee_payroll_overview
11
- name: "Employee Payroll Overview"
12
- description: "EASY: As an HR analyst, list all employee names and salaries ordered by salary descending for payroll review."
13
  difficulty: easy
14
- grader: core.grader:DataFrameGrader.grade
15
- - id: department_budget_summary
16
- name: "Department Budget Summary"
17
- description: "MEDIUM: As a finance analyst, return each department name with average salary. Output columns must be 'DEPARTMENT_NAME' and 'AVG_SALARY'."
 
 
 
 
 
 
 
 
 
18
  difficulty: medium
19
- grader: core.grader:DataFrameGrader.grade
20
- - id: senior_engineering_comp_review
21
- name: "Senior Engineering Comp Review"
22
- description: "HARD: For compensation review, find Engineering employees whose salary qualifies for the 'Senior' role threshold. Return employee NAME and role TITLE."
 
 
 
 
 
 
 
 
 
23
  difficulty: hard
24
- grader: core.grader:DataFrameGrader.grade
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: sql-debugger-env
2
+ version: "1.0.0"
3
+ description: >
4
+ RL environment for training AI agents to write, debug, and optimize SQL queries
5
+ against a real SQLite database with employees, departments, and projects.
 
 
6
 
7
  tasks:
8
+ - id: easy_01
9
+ name: Filter high earners
 
10
  difficulty: easy
11
+ type: write_query
12
+ grader: true
13
+ description: "Find Engineering employees with salary above 90000"
14
+
15
+ - id: easy_02
16
+ name: Count per department
17
+ difficulty: easy
18
+ type: write_query
19
+ grader: true
20
+ description: "Count employees per department, return department and count ordered by count DESC."
21
+
22
+ - id: medium_01
23
+ name: Fix broken JOIN
24
  difficulty: medium
25
+ type: fix_query
26
+ grader: true
27
+ description: "Fix a broken JOIN query (missing ON keyword, wrong table alias in WHERE). Return name and department name."
28
+
29
+ - id: medium_02
30
+ name: Fix GROUP BY
31
+ difficulty: medium
32
+ type: fix_query
33
+ grader: true
34
+ description: "Fix wrong GROUP BY (should GROUP BY department not id) finding max salary per department."
35
+
36
+ - id: hard_01
37
+ name: Optimize correlated subquery
38
  difficulty: hard
39
+ type: optimize_query
40
+ grader: true
41
+ description: "Replace correlated subquery with window function/CTE to find top earner per department with total hours."
42
+
43
+ - id: hard_02
44
+ name: Eliminate N+1 subqueries
45
+ difficulty: hard
46
+ type: optimize_query
47
+ grader: true
48
+ description: "Eliminate N+1 subqueries using LEFT JOIN + GROUP BY to compute total hours per project."
49
+
50
+ action_space:
51
+ type: object
52
+ fields:
53
+ action_type: string
54
+ sql: string
55
+ explanation: string
56
+
57
+ observation_space:
58
+ type: object
59
+ fields:
60
+ task_id: string
61
+ task_type: string
62
+ task_description: string
63
+ schema_info: string
64
+ last_sql: string
65
+ last_result: string
66
+ last_error: string
67
+ step_count: integer
68
+ done: boolean
69
+ reward: float
70
+ feedback: string
71
+
72
+ reward_range: [0.0, 1.0]
73
+ max_steps_per_episode: 8
server/app.py CHANGED
@@ -1,33 +1,63 @@
1
- from pathlib import Path
 
 
2
 
3
- from openenv.core.env_server import create_fastapi_app
4
- from fastapi.responses import HTMLResponse
5
- import os
6
- import uvicorn
7
 
8
- from models import SqlAction, SqlObservation
9
- from server.environment import SqlEnvironment
10
 
11
- app = create_fastapi_app(SqlEnvironment, SqlAction, SqlObservation)
12
 
13
- _PLAYGROUND_HTML = Path(__file__).resolve().parent / "playground.html"
 
 
14
 
 
 
 
15
 
16
- @app.get("/")
17
- def root():
18
- html = _PLAYGROUND_HTML.read_text(encoding="utf-8")
19
- return HTMLResponse(html)
20
 
 
 
 
21
 
22
  @app.get("/health")
23
- def health():
24
  return {"status": "healthy"}
25
 
26
-
27
- def main():
28
- port = int(os.getenv("PORT", "7860"))
29
- uvicorn.run("server.app:app", host="0.0.0.0", port=port)
30
-
31
-
32
- if __name__ == "__main__":
33
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request
2
+ from pydantic import BaseModel
3
+ from typing import Optional, Dict, Any
4
 
5
+ from server.environment import SqlEnvironment, TASKS
 
 
 
6
 
7
+ app = FastAPI(title="SQL Agent Environment API")
 
8
 
9
+ env = SqlEnvironment()
10
 
11
+ class ResetRequest(BaseModel):
12
+ task_id: Optional[str] = None
13
+ difficulty: Optional[str] = None
14
 
15
+ class ActionRequest(BaseModel):
16
+ sql: str
17
+ action_type: Optional[str] = None
18
 
19
+ class GradeRequest(BaseModel):
20
+ task_id: str
21
+ action: ActionRequest
 
22
 
23
+ @app.get("/")
24
+ def read_root():
25
+ return {"info": "SQL Agent Environment API"}
26
 
27
  @app.get("/health")
28
+ def read_health():
29
  return {"status": "healthy"}
30
 
31
+ @app.get("/state")
32
+ def read_state():
33
+ return env.state
34
+
35
+ @app.get("/tasks")
36
+ def list_tasks():
37
+ return {"tasks": TASKS}
38
+
39
+ @app.post("/reset")
40
+ def do_reset(req: Optional[ResetRequest] = None):
41
+ if req:
42
+ obs = env.reset(task_id=req.task_id, difficulty=req.difficulty)
43
+ else:
44
+ obs = env.reset()
45
+ return obs
46
+
47
+ @app.post("/step")
48
+ def do_step(action: ActionRequest):
49
+ obs = env.step({"sql": action.sql, "action_type": action.action_type})
50
+ return obs
51
+
52
+ @app.post("/grade")
53
+ def do_grade(req: GradeRequest):
54
+ env.reset(task_id=req.task_id)
55
+ obs = env.step({"sql": req.action.sql, "action_type": req.action.action_type})
56
+ score = max(0.05, min(0.95, float(obs["reward"])))
57
+
58
+ return {
59
+ "task_id": req.task_id,
60
+ "score": score,
61
+ "feedback": obs["feedback"],
62
+ "done": obs["done"]
63
+ }
server/environment.py CHANGED
@@ -1,145 +1,260 @@
1
- import uuid
 
 
 
2
 
3
- from openenv.core.env_server import Environment
4
- from models import SqlAction, SqlObservation, SqlState, SqlReward
5
- from database import SqliteManager
6
- from core import EnvironmentTasks, DataFrameGrader
 
 
7
 
8
- class SqlEnvironment(Environment):
9
- """Orchestrates the SQL Agent execution matching the OpenEnv Gym API."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def __init__(self):
11
- super().__init__()
12
- self._state = SqlState()
13
- self.task_registry = EnvironmentTasks()
14
-
15
- # Instantiate db connection immediately for the server loop
16
- self.db = SqliteManager()
17
- self.db.connect()
18
- self._dangerous_keywords = ("drop ", "delete ", "truncate ", "alter ", "update ", "insert ")
19
-
20
- def reset(self) -> SqlObservation:
21
- self._state = SqlState(
22
- episode_id=str(uuid.uuid4()),
23
- current_task_index=0,
24
- total_tasks=self.task_registry.get_total_tasks(),
25
- accumulated_reward=0.0,
26
- task_scores=[],
27
- attempts_per_task=[0 for _ in range(self.task_registry.get_total_tasks())],
28
- )
29
- first_task = self.task_registry.get_task(0)
30
- return SqlObservation(
31
- current_task_instruction=first_task.instruction,
32
- schema_info=self.db.get_schema_summary(),
33
- task_id=first_task.task_id,
34
- difficulty=first_task.difficulty_label,
35
- )
36
 
37
- @property
38
- def state(self) -> SqlState:
39
- return self._state
40
-
41
- def step(self, action: SqlAction) -> SqlObservation:
42
- self._state.step_count += 1
43
- query = action.query.strip()
44
- current_idx = self._state.current_task_index
45
- n_tasks = self.task_registry.get_total_tasks()
46
- while len(self._state.attempts_per_task) < n_tasks:
47
- self._state.attempts_per_task.append(0)
48
- task = self.task_registry.get_task(current_idx)
49
- self._state.attempts_per_task[current_idx] += 1
50
-
51
- execution_result_str = None
52
- error_str = None
53
- score = 0.0001
54
- grader_feedback = ""
55
- reward = SqlReward(total=0.0, task_score=0.0001)
56
- done = False
57
-
58
- query_lower = query.lower()
59
- if any(keyword in query_lower for keyword in self._dangerous_keywords):
60
- reward.safety_penalty = -1.0
61
- reward.total += reward.safety_penalty
62
- error_str = "Destructive SQL command detected. Only read-only SELECT queries are allowed."
63
- else:
64
- reward.step_penalty = -0.02
65
- reward.total += reward.step_penalty
66
-
67
- try:
68
- if error_str:
69
- raise ValueError(error_str)
70
 
71
- # 1. Gather expected vs agent dataframes
72
- expected_df = self.db.execute_dataframe(task.expected_query)
73
- agent_df = self.db.execute_dataframe(query)
74
-
75
- # Serialize for observation
76
- execution_result_str = agent_df.to_json(orient="records")
77
-
78
- # 2. Grade execution
79
- grade_result = DataFrameGrader.grade_with_details(agent_df, expected_df)
80
- score = grade_result["score"]
81
- grader_feedback = grade_result["feedback"]
82
- reward.details = grade_result["details"]
83
-
84
- # 3. Reward shaping
85
- reward.task_score = score
86
- reward.valid_sql_bonus = 0.1
87
- reward.progress_bonus = 0.9 * score
88
- reward.total += reward.valid_sql_bonus + reward.progress_bonus
89
-
90
- # 4. Check level progression
91
- if score >= 0.95:
92
- self._state.task_scores.append(score)
93
- self._state.current_task_index += 1
94
- if self._state.current_task_index >= self.task_registry.get_total_tasks():
95
- done = True
96
- elif self._state.attempts_per_task[current_idx] >= self._state.max_attempts_per_task:
97
- # Episode ends if agent gets stuck on same task
98
- self._state.task_scores.append(score)
99
- done = True
100
-
101
  except Exception as e:
102
- error_str = str(e)
103
- reward.error_penalty = -0.5
104
- reward.total += reward.error_penalty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- # Global accumulation
107
- self._state.last_reward = reward.total
108
- reward_breakdown = reward.model_dump() if hasattr(reward, "model_dump") else reward.dict()
109
- self._state.last_info = {
110
- "task_id": task.task_id,
111
- "difficulty": task.difficulty_label,
112
- "task_score": score,
113
- "attempts_for_task": self._state.attempts_per_task[current_idx],
114
- "reward_breakdown": reward_breakdown,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
- self._state.accumulated_reward += reward.total
 
 
 
 
 
 
 
117
 
118
- # Prepare observation response
119
- if done:
120
- total_score = sum(self._state.task_scores)
121
- next_instruction = f"All tasks completed! Final SQL Capability Score: {total_score}"
122
- schema = ""
123
- task_id = task.task_id
124
- difficulty = task.difficulty_label
125
  else:
126
- next_task = self.task_registry.get_task(self._state.current_task_index)
127
- next_instruction = next_task.instruction
128
- schema = self.db.get_schema_summary()
129
- task_id = next_task.task_id
130
- difficulty = next_task.difficulty_label
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- obs = SqlObservation(
133
- current_task_instruction=next_instruction,
134
- schema_info=schema,
135
- task_id=task_id,
136
- difficulty=difficulty,
137
- execution_result=execution_result_str,
138
- execution_error=error_str,
139
- task_score=score,
140
- grader_feedback=grader_feedback,
141
- reward=reward.total,
142
- done=done,
143
- )
144
-
145
- return obs
 
1
+ import sqlite3
2
+ import re
3
+ from typing import Dict, Any, List, Optional
4
+ import json
5
 
6
+ SCHEMA_SQL = """
7
+ CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary INTEGER, hire_date TEXT, manager_id INTEGER);
8
+ CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT, budget INTEGER, location TEXT);
9
+ CREATE TABLE projects (id INTEGER PRIMARY KEY, name TEXT, department_id INTEGER, start_date TEXT, status TEXT);
10
+ CREATE TABLE project_assignments (employee_id INTEGER, project_id INTEGER, hours_worked INTEGER);
11
+ """
12
 
13
+ SEED_DATA_SQL = """
14
+ INSERT INTO employees VALUES (1, 'Alice', 'Engineering', 95000, '2020-01-15', 3);
15
+ INSERT INTO employees VALUES (2, 'Bob', 'Engineering', 85000, '2021-02-20', 3);
16
+ INSERT INTO employees VALUES (3, 'Charlie', 'Engineering', 120000, '2019-01-10', NULL);
17
+ INSERT INTO employees VALUES (4, 'David', 'Sales', 70000, '2022-03-05', 5);
18
+ INSERT INTO employees VALUES (5, 'Eve', 'Sales', 92000, '2018-11-20', NULL);
19
+ INSERT INTO employees VALUES (6, 'Frank', 'HR', 65000, '2023-05-12', NULL);
20
+
21
+ INSERT INTO departments VALUES (1, 'Engineering', 500000, 'Building A');
22
+ INSERT INTO departments VALUES (2, 'Sales', 300000, 'Building B');
23
+ INSERT INTO departments VALUES (3, 'HR', 150000, 'Building C');
24
+
25
+ INSERT INTO projects VALUES (1, 'Project Alpha', 1, '2023-01-01', 'Active');
26
+ INSERT INTO projects VALUES (2, 'Project Beta', 2, '2023-06-15', 'Completed');
27
+
28
+ INSERT INTO project_assignments VALUES (1, 1, 100);
29
+ INSERT INTO project_assignments VALUES (2, 1, 150);
30
+ INSERT INTO project_assignments VALUES (4, 2, 80);
31
+ """
32
+
33
+ TASKS = [
34
+ {
35
+ "id": "easy_01",
36
+ "name": "Filter high earners",
37
+ "difficulty": "easy",
38
+ "type": "write_query",
39
+ "grader": True,
40
+ "description": "Find Engineering employees with salary > 90000, return name and salary ordered by salary DESC.",
41
+ "expected_rows": 2
42
+ },
43
+ {
44
+ "id": "easy_02",
45
+ "name": "Count per department",
46
+ "difficulty": "easy",
47
+ "type": "write_query",
48
+ "grader": True,
49
+ "description": "Count employees per department, return department and count ordered by count DESC.",
50
+ "expected_rows": 3
51
+ },
52
+ {
53
+ "id": "medium_01",
54
+ "name": "Fix broken JOIN",
55
+ "difficulty": "medium",
56
+ "type": "fix_query",
57
+ "grader": True,
58
+ "description": "Fix a broken JOIN query (missing ON keyword, wrong table alias in WHERE). Return name and department name.",
59
+ "expected_rows": 6
60
+ },
61
+ {
62
+ "id": "medium_02",
63
+ "name": "Fix GROUP BY",
64
+ "difficulty": "medium",
65
+ "type": "fix_query",
66
+ "grader": True,
67
+ "description": "Fix wrong GROUP BY (should GROUP BY department not id) finding max salary per department.",
68
+ "expected_rows": 3
69
+ },
70
+ {
71
+ "id": "hard_01",
72
+ "name": "Optimize correlated subquery",
73
+ "difficulty": "hard",
74
+ "type": "optimize_query",
75
+ "grader": True,
76
+ "description": "Replace correlated subquery with window function/CTE to find top earner per department with total hours.",
77
+ "expected_rows": 3
78
+ },
79
+ {
80
+ "id": "hard_02",
81
+ "name": "Eliminate N+1 subqueries",
82
+ "difficulty": "hard",
83
+ "type": "optimize_query",
84
+ "grader": True,
85
+ "description": "Eliminate N+1 subqueries using LEFT JOIN + GROUP BY to compute total hours per project.",
86
+ "expected_rows": 2
87
+ }
88
+ ]
89
+
90
+ def build_db() -> sqlite3.Connection:
91
+ conn = sqlite3.connect(':memory:')
92
+ conn.row_factory = sqlite3.Row
93
+ conn.executescript(SCHEMA_SQL)
94
+ conn.executescript(SEED_DATA_SQL)
95
+ conn.commit()
96
+ return conn
97
+
98
+ class SqlEnvironment:
99
  def __init__(self):
100
+ self.db = build_db()
101
+ self.max_steps = 8
102
+ self.step_count = 0
103
+ self.current_task = TASKS[0]
104
+ self.done = False
105
+ self.last_sql = ""
106
+ self.last_result = ""
107
+ self.last_error = ""
108
+ self.reward = 0.05
109
+ self.feedback = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ def _clamp(self, score: float) -> float:
112
+ return round(max(0.05, min(0.95, score)), 4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ def _execute_sql(self, sql: str) -> tuple[Optional[List[Dict[str, Any]]], Optional[str]]:
115
+ upper_sql = sql.upper()
116
+ blocked = ["DROP", "DELETE", "INSERT", "UPDATE", "CREATE", "ALTER", "TRUNCATE"]
117
+ for b in blocked:
118
+ if re.search(rf"\b{b}\b", upper_sql):
119
+ return None, f"Blocked keyword {b} found. Only SELECT allowed."
120
+ try:
121
+ cursor = self.db.cursor()
122
+ cursor.execute(sql)
123
+ rows = cursor.fetchall()
124
+ return [dict(row) for row in rows], None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  except Exception as e:
126
+ return None, str(e)
127
+
128
+ def _grade_write(self, sql: str, rows: Optional[List[Dict]], expected_rows: int) -> float:
129
+ score = 0.0
130
+ upper_sql = sql.upper()
131
+ if "EMPLOYEES" in upper_sql or "DEPARTMENTS" in upper_sql:
132
+ score += 0.15
133
+ if "WHERE" in upper_sql:
134
+ score += 0.10
135
+ if "ORDER BY" in upper_sql:
136
+ score += 0.15
137
+ if rows is not None:
138
+ if len(rows) > 0:
139
+ score += 0.25
140
+ if len(rows) == expected_rows:
141
+ score += 0.25
142
+ elif abs(len(rows) - expected_rows) == 1:
143
+ score += 0.12
144
+ return self._clamp(score)
145
+
146
+ def _grade_fix(self, sql: str, rows: Optional[List[Dict]], expected_rows: int) -> float:
147
+ score = 0.0
148
+ upper_sql = sql.upper()
149
+ if rows is not None:
150
+ score += 0.35
151
+ if len(rows) == expected_rows:
152
+ score += 0.35
153
+ if re.search(r"\bJOIN\b.*\bON\b", upper_sql):
154
+ score += 0.15
155
+ if "WHERE" in upper_sql or "GROUP BY" in upper_sql:
156
+ score += 0.10
157
+ return self._clamp(score)
158
+
159
+ def _grade_optimize(self, sql: str, rows: Optional[List[Dict]], expected_rows: int) -> float:
160
+ score = 0.0
161
+ upper_sql = sql.upper()
162
+ if re.search(r"\b(WITH|ROW_NUMBER|RANK)\b", upper_sql):
163
+ score += 0.30
164
+ if "JOIN" in upper_sql:
165
+ score += 0.25
166
+ if upper_sql.count("SELECT") == 1:
167
+ score += 0.20
168
+ if rows is not None and len(rows) == expected_rows:
169
+ score += 0.20
170
+ return self._clamp(score)
171
+
172
+ def reset(self, task_id: Optional[str] = None, difficulty: Optional[str] = None) -> Dict[str, Any]:
173
+ self.step_count = 0
174
+ self.done = False
175
+ self.last_sql = ""
176
+ self.last_result = ""
177
+ self.last_error = ""
178
+ self.reward = self._clamp(0.0)
179
+ self.feedback = "Environment reset."
180
 
181
+ if task_id:
182
+ for t in TASKS:
183
+ if t["id"] == task_id:
184
+ self.current_task = t
185
+ break
186
+ elif difficulty:
187
+ for t in TASKS:
188
+ if t["difficulty"] == difficulty:
189
+ self.current_task = t
190
+ break
191
+ else:
192
+ self.current_task = TASKS[0]
193
+
194
+ return self.get_observation()
195
+
196
+ def get_observation(self) -> Dict[str, Any]:
197
+ return {
198
+ "task_id": self.current_task["id"],
199
+ "task_type": self.current_task["type"],
200
+ "task_description": self.current_task["description"],
201
+ "schema_info": SCHEMA_SQL.strip(),
202
+ "last_sql": self.last_sql,
203
+ "last_result": self.last_result,
204
+ "last_error": self.last_error,
205
+ "step_count": self.step_count,
206
+ "done": self.done,
207
+ "reward": self.reward,
208
+ "feedback": self.feedback
209
+ }
210
+
211
+ @property
212
+ def state(self) -> Dict[str, Any]:
213
+ return {
214
+ "task": self.current_task["id"],
215
+ "step": self.step_count,
216
+ "done": self.done,
217
+ "reward": self.reward
218
  }
219
+
220
+ def step(self, action: Dict[str, Any]) -> Dict[str, Any]:
221
+ if self.done:
222
+ return self.get_observation()
223
+
224
+ self.step_count += 1
225
+ sql = action.get("sql", "").strip()
226
+ self.last_sql = sql
227
 
228
+ if not sql:
229
+ self.last_error = "No SQL provided."
230
+ self.last_result = ""
231
+ self.reward = self._clamp(0.0)
232
+ self.feedback = "Empty query."
 
 
233
  else:
234
+ rows, err = self._execute_sql(sql)
235
+ if err:
236
+ self.last_error = err
237
+ self.last_result = ""
238
+ self.reward = self._clamp(0.0)
239
+ self.feedback = "Execution failed."
240
+ else:
241
+ self.last_error = ""
242
+ self.last_result = json.dumps(rows)
243
+ expected = self.current_task["expected_rows"]
244
+ ttype = self.current_task["type"]
245
+
246
+ raw_score = 0.0
247
+ if ttype == "write_query":
248
+ raw_score = self._grade_write(sql, rows, expected)
249
+ elif ttype == "fix_query":
250
+ raw_score = self._grade_fix(sql, rows, expected)
251
+ elif ttype == "optimize_query":
252
+ raw_score = self._grade_optimize(sql, rows, expected)
253
+
254
+ self.reward = raw_score # already clamped internally by the functions
255
+ self.feedback = "Query executed successfully."
256
+
257
+ if self.step_count >= self.max_steps:
258
+ self.done = True
259
 
260
+ return self.get_observation()