sql-env / server /sql_env_environment.py
jankip20's picture
Upload folder using huggingface_hub
11840ba verified
Raw History Blame Contribute Delete
6.02 kB
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""
Sql Env Environment Implementation.
Executes SQL queries against a SQLite database and returns results
as structured observations.
"""
import os
import sqlite3
from uuid import uuid4
from tasks import get_random_task, get_task_by_difficulty, Task
from openenv.core.env_server.interfaces import Environment
from openenv.core.env_server.types import State
try:
from ..models import SqlAction, SqlObservation
except ImportError:
from models import SqlAction, SqlObservation
DB_PATH = os.environ.get(
"SQL_ENV_DB_PATH",
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "sql_env.db")
)
TASK_DIFFICULTY = os.environ.get("TASK_DIFFICULTY", None)
class SqlEnvironment(Environment):
SUPPORTS_CONCURRENT_SESSIONS: bool = True
def __init__(self):
self._state = State(episode_id=str(uuid4()), step_count=0)
self._reset_count = 0
self._conn: sqlite3.Connection | None = None
self._task: Task | None = None
self._connect()
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _connect(self):
"""Open (or reopen) a connection to the SQLite database."""
if self._conn:
try:
self._conn.close()
except Exception:
pass
self._conn = sqlite3.connect(DB_PATH, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
def _execute(self, query: str) -> dict:
try:
cursor = self._conn.cursor()
cursor.execute(query)
self._conn.commit()
if cursor.description:
columns = [desc[0] for desc in cursor.description]
rows = [list(row) for row in cursor.fetchall()]
return {
"columns": columns,
"rows": rows,
"rowcount": len(rows),
"error": None,
}
else:
return {
"columns": [],
"rows": [],
"rowcount": cursor.rowcount,
"error": None,
}
except sqlite3.Error as e:
return {
"columns": [],
"rows": [],
"rowcount": 0,
"error": str(e),
}
def _reward(self, result: dict, query: str) -> float:
if result["error"]:
return 0.0
score = 0.0
q = query.lower()
score += 0.3
if result["rowcount"] > 0:
score += 0.2
if "join" in q:
score += 0.2
if any(fn in q for fn in ["count(", "sum(", "avg(", "max(", "min("]):
score += 0.2
if "where" in q:
score += 0.1
if self._task and result["columns"]:
matched = sum(
1 for c in self._task.expected_columns
if any(c in col.lower() or col.lower() in c
for col in result["columns"])
)
score += 0.2 * (matched / len(self._task.expected_columns))
if self._task and self._task.grader and result["rows"]:
grader_score = self._task.grader(result["rows"])
score = min(score + (grader_score * 0.2), 1.0)
return min(score, 1.0)
# ------------------------------------------------------------------
# Environment interface
# ------------------------------------------------------------------
def reset(self) -> SqlObservation:
self._task = get_random_task()
self._state = State(episode_id=str(uuid4()), step_count=0)
self._reset_count += 1
self._connect()
print(f"[ENV] Task: {self._task.id} | Difficulty: {self._task.difficulty}", flush=True)
return SqlObservation(
echoed_message=f"Task: {self._task.description}",
message_length=0,
metadata={
"db_path": DB_PATH,
"task_id": self._task.id,
"task_difficulty": self._task.difficulty,
"task_description": self._task.description,
},
)
def step(self, action: SqlAction) -> SqlObservation:
self._state.step_count += 1
query = action.query.strip()
result = self._execute(query)
if result["error"]:
summary = f"ERROR: {result['error']}"
elif result["columns"]:
summary = (
f"{result['rowcount']} row(s) returned. "
f"Columns: {result['columns']}"
)
else:
summary = f"Query OK. {result['rowcount']} row(s) affected."
grader_score = None
if self._task and self._task.grader and not result["error"]:
grader_score = self._task.grader(result["rows"])
return SqlObservation(
echoed_message=summary,
message_length=len(query),
metadata={
"query": query,
"columns": result["columns"],
"rows": result["rows"],
"rowcount": result["rowcount"],
"error": result["error"],
"reward": self._reward(result, query),
"grader_score": grader_score,
"task_id": self._task.id if self._task else None,
"task_description": self._task.description if self._task else None,
},
)
@property
def state(self) -> State:
return self._state