root commited on
Commit
9b9c562
·
1 Parent(s): 4477500

feat: implement SQL agent environment with OpenEnv integration, SQLite management, and automated task grading

Browse files
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
5
+
6
+ WORKDIR /app
7
+
8
+ # Install dependencies
9
+ COPY requirements.txt /tmp/requirements.txt
10
+ RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt
11
+
12
+ # Copy environment code
13
+ COPY . /app/
14
+
15
+ # Environment configurations setup
16
+ ENV PYTHONPATH="/app"
17
+
18
+ # Health check
19
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
20
+ CMD curl -f http://localhost:8000/docs || exit 1
21
+
22
+ EXPOSE 8000
23
+
24
+ # Run server with updated root OOP Path
25
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from models import SqlAction, SqlObservation, SqlState
2
+ from client import SqlEnvClient
3
+
4
+ __all__ = ["SqlAction", "SqlObservation", "SqlState", "SqlEnvClient"]
__pycache__/client.cpython-313.pyc ADDED
Binary file (1.48 kB). View file
 
__pycache__/models.cpython-313.pyc ADDED
Binary file (1.66 kB). View file
 
client.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openenv.core import EnvClient
2
+ from openenv.core.client_types import StepResult
3
+ from models import SqlAction, SqlObservation, SqlState
4
+
5
+ class SqlEnvClient(EnvClient[SqlAction, SqlObservation, SqlState]):
6
+ def _step_payload(self, action: SqlAction) -> dict:
7
+ return {"query": action.query}
8
+
9
+ def _parse_result(self, payload: dict) -> StepResult[SqlObservation]:
10
+ obs = SqlObservation(**payload["observation"])
11
+ return StepResult(
12
+ observation=obs,
13
+ reward=payload.get("reward"),
14
+ done=payload.get("done", False),
15
+ )
16
+
17
+ def _parse_state(self, payload: dict) -> SqlState:
18
+ return SqlState(**payload)
core/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Core components exports
2
+ from core.grader import DataFrameGrader
3
+ from core.tasks import SqlTask, EnvironmentTasks
4
+
5
+ __all__ = ["DataFrameGrader", "SqlTask", "EnvironmentTasks"]
core/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (317 Bytes). View file
 
core/__pycache__/grader.cpython-313.pyc ADDED
Binary file (2.39 kB). View file
 
core/__pycache__/tasks.cpython-313.pyc ADDED
Binary file (2.52 kB). View file
 
core/grader.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ class DataFrameGrader:
4
+ """Handles logic for comparing agent SQL result DataFrames against expected DataFrames."""
5
+
6
+ @staticmethod
7
+ def grade(agent_df: pd.DataFrame, expected_df: pd.DataFrame) -> float:
8
+ """
9
+ Grades a DataFrame result. Returns 0.0 to 1.0.
10
+ Checks for exact match (columns, rows, data types).
11
+ Falls back to unordered match if the structure is the same but sorting is off.
12
+ """
13
+ try:
14
+ agent_norm = agent_df.reset_index(drop=True).sort_index(axis=1)
15
+ expected_norm = expected_df.reset_index(drop=True).sort_index(axis=1)
16
+
17
+ # Fast exact comparison
18
+ if set(agent_norm.columns) == set(expected_norm.columns):
19
+
20
+ # Check directly first
21
+ if agent_norm.equals(expected_norm):
22
+ return 1.0
23
+
24
+ # Re-align column order if mismatched
25
+ agent_aligned = agent_norm[expected_norm.columns]
26
+ if agent_aligned.equals(expected_norm):
27
+ return 1.0
28
+
29
+ # Check unordered row set equivalence
30
+ agent_tuples = set(tuple(row) for row in agent_aligned.to_numpy())
31
+ expected_tuples = set(tuple(row) for row in expected_norm.to_numpy())
32
+
33
+ if len(agent_tuples) == len(expected_tuples) and agent_tuples == expected_tuples:
34
+ return 0.8 # Unordered match yields partial points
35
+
36
+ return 0.0
37
+ except Exception:
38
+ return 0.0
core/tasks.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ @dataclass
5
+ class SqlTask:
6
+ difficulty: int
7
+ instruction: str
8
+ expected_query: str
9
+
10
+ class EnvironmentTasks:
11
+ """Registry for all SQL challenges assigned to the OpenEnv agent."""
12
+ def __init__(self):
13
+ self.tasks: List[SqlTask] = [
14
+ SqlTask(
15
+ difficulty=1,
16
+ instruction="EASY: Find all names and salaries of employees, ordering by their salaries descending.",
17
+ expected_query="SELECT NAME, SALARY FROM EMPLOYEES ORDER BY SALARY DESC"
18
+ ),
19
+ SqlTask(
20
+ difficulty=2,
21
+ instruction="MEDIUM: Return the department names alongside the average salary per department. Columns must be 'DEPARTMENT_NAME' and 'AVG_SALARY'.",
22
+ expected_query="""
23
+ SELECT D.NAME AS DEPARTMENT_NAME, AVG(E.SALARY) AS AVG_SALARY
24
+ FROM EMPLOYEES E
25
+ JOIN DEPARTMENTS D ON E.DEPARTMENT_ID = D.ID
26
+ GROUP BY D.NAME
27
+ """
28
+ ),
29
+ SqlTask(
30
+ difficulty=3,
31
+ instruction="HARD: Find the name and title of all Engineering (DEPARTMENT_ID = 2) employees who earn a 'Senior' level salary (based on ROLES MIN_SALARY limit).",
32
+ expected_query="""
33
+ SELECT E.NAME, R.TITLE
34
+ FROM EMPLOYEES E
35
+ JOIN ROLES R ON E.SALARY >= R.MIN_SALARY
36
+ WHERE E.DEPARTMENT_ID = 2 AND R.TITLE = 'Senior'
37
+ """
38
+ )
39
+ ]
40
+
41
+ def get_total_tasks(self) -> int:
42
+ return len(self.tasks)
43
+
44
+ def get_task(self, index: int) -> SqlTask:
45
+ return self.tasks[index]
database/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Database package exports
2
+ from database.sqlite_manager import SqliteManager
3
+
4
+ __all__ = ["SqliteManager"]
database/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (250 Bytes). View file
 
database/__pycache__/sqlite_manager.cpython-313.pyc ADDED
Binary file (4.88 kB). View file
 
database/sqlite_manager.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import pandas as pd
3
+ from typing import List, Tuple
4
+
5
+ class SqliteManager:
6
+ """Manages the lifecycle and execution of an internal SQLite database."""
7
+ def __init__(self, db_path: str = ":memory:"):
8
+ self.db_path = db_path
9
+ self.conn = None
10
+
11
+ def __enter__(self):
12
+ self.connect()
13
+ return self
14
+
15
+ def __exit__(self, exc_type, exc_val, exc_tb):
16
+ self.close()
17
+
18
+ def connect(self):
19
+ """Establishes the connection and sets up initial dummy schema + data."""
20
+ self.conn = sqlite3.Connection(self.db_path)
21
+ self._initialize_schema()
22
+
23
+ def close(self):
24
+ if self.conn:
25
+ self.conn.close()
26
+
27
+ def _initialize_schema(self):
28
+ cursor = self.conn.cursor()
29
+
30
+ cursor.execute('''CREATE TABLE DEPARTMENTS (ID INTEGER PRIMARY KEY, NAME TEXT)''')
31
+ cursor.execute('''CREATE TABLE EMPLOYEES (ID INTEGER PRIMARY KEY, NAME TEXT, DEPARTMENT_ID INTEGER, SALARY REAL)''')
32
+ cursor.execute('''CREATE TABLE ROLES (ID INTEGER PRIMARY KEY, TITLE TEXT, MIN_SALARY REAL)''')
33
+
34
+ cursor.executemany("INSERT INTO DEPARTMENTS (ID, NAME) VALUES (?, ?)", [
35
+ (1, "HR"), (2, "Engineering"), (3, "Marketing"), (4, "Sales")
36
+ ])
37
+
38
+ cursor.executemany("INSERT INTO EMPLOYEES (ID, NAME, DEPARTMENT_ID, SALARY) VALUES (?, ?, ?, ?)", [
39
+ (1, "Alice", 2, 120000), (2, "Bob", 2, 110000), (3, "Charlie", 1, 80000),
40
+ (4, "David", 3, 90000), (5, "Eve", 4, 95000), (6, "Frank", 2, 105000),
41
+ (7, "Grace", 4, 100000), (8, "Hank", 2, 85000)
42
+ ])
43
+
44
+ cursor.executemany("INSERT INTO ROLES (ID, TITLE, MIN_SALARY) VALUES (?, ?, ?)", [
45
+ (1, "Associate", 60000), (2, "Senior", 100000), (3, "Staff", 130000)
46
+ ])
47
+
48
+ self.conn.commit()
49
+
50
+ def get_schema_summary(self) -> str:
51
+ return """
52
+ Database Schema:
53
+ 1. DEPARTMENTS (ID INTEGER PRIMARY KEY, NAME TEXT)
54
+ 2. EMPLOYEES (ID INTEGER PRIMARY KEY, NAME TEXT, DEPARTMENT_ID INTEGER REFERENCES DEPARTMENTS(ID), SALARY REAL)
55
+ 3. ROLES (ID INTEGER PRIMARY KEY, TITLE TEXT, MIN_SALARY REAL)
56
+ """
57
+
58
+ def is_safe_query(self, query: str) -> bool:
59
+ """Validates that a query isn't destructive."""
60
+ forbidden = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]
61
+ upper_query = query.upper()
62
+ return not any(f in upper_query for f in forbidden)
63
+
64
+ def execute_dataframe(self, query: str) -> pd.DataFrame:
65
+ """Executes a SQL query and returns its result as a Pandas DataFrame."""
66
+ if not self.conn:
67
+ raise RuntimeError("Database connection not initialized.")
68
+ if not self.is_safe_query(query):
69
+ raise ValueError("Destructive operations are not allowed.")
70
+ return pd.read_sql_query(query, self.conn)
inference.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import textwrap
3
+ from openai import OpenAI
4
+ from typing import List
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ from client import SqlEnvClient
10
+ from models import SqlAction
11
+
12
+ # OpenEnv execution environment config
13
+ ENV_CONTAINER_START = os.getenv("ENV_CONTAINER_START", "false").lower() == "true"
14
+ ENV_IMAGE_NAME = "sql-agent-env:latest"
15
+
16
+ # LLM inference config as per instructions
17
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
18
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY")
19
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
20
+ MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
21
+ MAX_STEPS = 10
22
+
23
+ SYSTEM_PROMPT = textwrap.dedent(
24
+ """
25
+ You are an expert Data Analyst and SQL Agent.
26
+ You will be provided with a SQL Schema and an instruction.
27
+ Reply with exactly one JSON block containing the SQL query to execute.
28
+ Do NOT provide markdown formatting or explanations. DO NOT wrap in ```sql.
29
+ The response should be the pure string of the query itself.
30
+ """
31
+ ).strip()
32
+
33
+ def build_user_prompt(step: int, observation, history: List[str]) -> str:
34
+ schema = observation.schema_info
35
+ instruction = observation.current_task_instruction
36
+ exec_result = observation.execution_result or "None"
37
+ error = observation.execution_error or "None"
38
+
39
+ prompt = textwrap.dedent(
40
+ f"""
41
+ Step: {step}
42
+ Database Schema:
43
+ {schema}
44
+
45
+ Task Instruction:
46
+ {instruction}
47
+
48
+ Previous Execution Result: {exec_result}
49
+ Previous Error: {error}
50
+
51
+ Write the precise SQL query string to accomplish the task instruction. Return nothing else.
52
+ """
53
+ ).strip()
54
+ return prompt
55
+
56
+ def parse_model_action(response_text: str) -> str:
57
+ query = response_text.strip()
58
+ if query.startswith("```sql"):
59
+ query = query[6:]
60
+ if query.startswith("```"):
61
+ query = query[3:]
62
+ if query.endswith("```"):
63
+ query = query[:-3]
64
+ return query.strip()
65
+
66
+ def main():
67
+ if not API_KEY and not GEMINI_API_KEY:
68
+ print("WARNING: API credentials might be missing. Using dummy environment flow for validation.")
69
+
70
+ # Client setup
71
+ client_type = "openai"
72
+ if GEMINI_API_KEY:
73
+ try:
74
+ from google import genai
75
+ client = genai.Client(api_key=GEMINI_API_KEY)
76
+ client_type = "gemini"
77
+ print("--- Using Google Gemini Client ---")
78
+ except ImportError:
79
+ print("WARNING: google-genai package not found. Falling back to OpenAI client.")
80
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY or "dummy-key")
81
+ else:
82
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY or "dummy-key")
83
+
84
+ print("--- SQL Data Analyst OpenEnv Baseline ---")
85
+
86
+ if ENV_CONTAINER_START:
87
+ print(f"Starting docker image: {ENV_IMAGE_NAME}")
88
+ env = SqlEnvClient.from_docker_image(ENV_IMAGE_NAME).sync()
89
+ else:
90
+ # Fallback to local memory-bound initialization
91
+ from server.environment import SqlEnvironment
92
+ base_env = SqlEnvironment()
93
+
94
+ # Direct wrapper
95
+ class DirectClient:
96
+ def __init__(self, target):
97
+ self.target = target
98
+ def reset(self):
99
+ obs = self.target.reset()
100
+ return type('StepResult', (), {'observation': obs, 'reward': 0.0, 'done': False})()
101
+ def step(self, action):
102
+ obs, reward, done = self.target.step(action)
103
+ return type('StepResult', (), {'observation': obs, 'reward': reward, 'done': done})()
104
+ def close(self):
105
+ pass
106
+ env = DirectClient(base_env)
107
+
108
+ history = []
109
+
110
+ try:
111
+ result = env.reset()
112
+ observation = result.observation
113
+ print(f"\n[Episode Start] Initial Task: {observation.current_task_instruction}")
114
+
115
+ for step in range(1, MAX_STEPS + 1):
116
+ if result.done:
117
+ print("Environment signalled done. Stopping early.")
118
+ break
119
+
120
+ user_prompt = build_user_prompt(step, observation, history)
121
+
122
+ messages = [
123
+ {"role": "system", "content": SYSTEM_PROMPT},
124
+ {"role": "user", "content": user_prompt},
125
+ ]
126
+
127
+ try:
128
+ if client_type == "gemini":
129
+ # For Gemini, we collapse System & User prompts into a single payload
130
+ gemini_prompt = f"{SYSTEM_PROMPT}\n\n{user_prompt}"
131
+ # Use provided model Name, fallback defaults to gemini-2.5-flash
132
+ gemini_model = MODEL_NAME if MODEL_NAME != "gpt-4o-mini" else "gemini-2.5-flash"
133
+
134
+ response = client.models.generate_content(
135
+ model=gemini_model,
136
+ contents=gemini_prompt,
137
+ config={"temperature": 0.1}
138
+ )
139
+ response_text = response.text or "SELECT * FROM DEPARTMENTS"
140
+ elif API_KEY or os.getenv("OPENAI_API_KEY"):
141
+ completion = client.chat.completions.create(
142
+ model=MODEL_NAME,
143
+ messages=messages,
144
+ temperature=0.1,
145
+ stream=False
146
+ )
147
+ response_text = completion.choices[0].message.content or "SELECT * FROM DEPARTMENTS"
148
+ else:
149
+ response_text = "SELECT * FROM DEPARTMENTS" # Dummy mode
150
+
151
+ except Exception as e:
152
+ print(f"Model Request failed: {e}. Using fallback action.")
153
+ response_text = "SELECT 1"
154
+
155
+ action_str = parse_model_action(response_text)
156
+ print(f"\n[Step {step}] Model suggested: {action_str}")
157
+
158
+ result = env.step(SqlAction(query=action_str))
159
+ observation = result.observation
160
+ reward = result.reward
161
+ done = result.done
162
+
163
+ print(f" Reward: {reward:+.2f} | Done: {done}")
164
+ if observation.execution_error:
165
+ print(f" Error: {observation.execution_error[:200]}")
166
+
167
+ history.append(f"Q: {action_str} -> R: {reward:+.2f}")
168
+
169
+ if done:
170
+ print("\nAll tasks complete!")
171
+ break
172
+
173
+ finally:
174
+ env.close()
175
+
176
+ if __name__ == "__main__":
177
+ main()
models.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field
2
+ from typing import Optional, List
3
+ from openenv.core.env_server import Action, Observation, State
4
+
5
+ class SqlAction(Action):
6
+ """Action representing a SQL query execution."""
7
+ query: str
8
+
9
+ class SqlObservation(Observation):
10
+ """Observation returned after executing a SQL query (or at reset)."""
11
+ current_task_instruction: str
12
+ schema_info: str
13
+ execution_result: Optional[str] = None
14
+ execution_error: Optional[str] = None
15
+
16
+ class SqlState(State):
17
+ """State tracking for the SQL agent environment."""
18
+ current_task_index: int = 0
19
+ total_tasks: int = 3
20
+ accumulated_reward: float = 0.0
21
+ task_scores: List[float] = Field(default_factory=list)
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: sql-agent-env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
readme.md CHANGED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SQL AI Data Analyst
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 8000
8
+ tags:
9
+ - openenv
10
+ ---
11
+ # OpenEnv SQL Data Analyst
12
+
13
+ ## The Goal
14
+ The objective is straightforward: the AI must interpret an internal database schema and translate human business instructions into valid SQL queries natively executed against the pipeline.
15
+
16
+ Tasks increase in difficulty, pushing models to understand complex relationships such as:
17
+ - Simple Table Retrievals (`SELECT`, `WHERE`)
18
+ - Aggregating related tables (`JOIN`, `GROUP BY`)
19
+ - Complex constraint execution bounds (Logic matching)
20
+
21
+ ## Fast Execution
22
+ 1. **Install:** `pip install -r requirements.txt`
23
+ 2. **Setup your preferred engine:**
24
+ ```powershell
25
+ $env:GEMINI_API_KEY="your-api-key"
26
+ # Or use OPENAI_API_KEY
27
+ ```
28
+ 3. **Run the Simulation Base:**
29
+ ```powershell
30
+ python inference.py
31
+ ```
32
+
33
+ ## Architecture Workflow
34
+
35
+ This environment follows strict [OpenEnv Gym interface](https://github.com/meta-pytorch/OpenEnv) standards, utilizing `step()`, `reset()`, and `state()` endpoints wrapped locally or exposed natively via Hugging Face.
36
+
37
+ ```mermaid
38
+ flowchart TD
39
+ Start([Episode Start]) --> EnvInit[Environment loads Task 1]
40
+
41
+ EnvInit --> Obs[Generate Observation]
42
+ Obs -.-> |1. Send Schema and Instruction| Agent((LLM Agent))
43
+
44
+ Agent -.-> |2. Predicts SQL Query| Step[Environment step function]
45
+
46
+ Step --> Exec{Execute SQL on SQLite Engine}
47
+
48
+ Exec -->|SQL Syntax Error| Pen[Return Negative Reward]
49
+ Exec -->|Valid SQL| Grader[Evaluate Pandas DataFrame]
50
+
51
+ Grader --> Match{Compare Expected DataFrame}
52
+ Match -->|Perfect Match| Rew1[Reward 1.0 Advance Level]
53
+ Match -->|Unordered Match| RewPartial[Reward 0.8]
54
+ Match -->|Incorrect Data| Rew0[Reward 0.0]
55
+
56
+ Pen --> StateUpdate
57
+ Rew1 --> StateUpdate
58
+ RewPartial --> StateUpdate
59
+ Rew0 --> StateUpdate
60
+
61
+ StateUpdate[Log History and Update Episode state] --> CheckDone{All Tasks Completed Yes or No}
62
+
63
+ CheckDone -->|No| Obs
64
+ CheckDone -->|Yes| Finish([Episode End])
65
+ ```
66
+
67
+ ## Internal Engine Design
68
+ To read more about exactly how the logic is handled under the hood, access the OOP components inside the root directory:
69
+ - `server/environment.py`: The orchestrator handling the OpenEnv lifecycle.
70
+ - `database/sqlite_manager.py`: Creates ephemeral SQLite sandboxes per session.
71
+ - `core/grader.py`: Evaluates logic correctness independent of string semantics by leveraging Pandas DataFrame alignments.
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi>=0.111.0
2
+ pydantic>=2.7.1
3
+ uvicorn>=0.30.1
4
+ openenv-core
5
+ pandas
6
+ google-genai
7
+ python-dotenv
server/__pycache__/environment.cpython-313.pyc ADDED
Binary file (4.58 kB). View file
 
server/app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from openenv.core.env_server import create_fastapi_app
2
+
3
+ from models import SqlAction, SqlObservation
4
+ from server.environment import SqlEnvironment
5
+
6
+ env = SqlEnvironment()
7
+ app = create_fastapi_app(env, SqlAction, SqlObservation)
server/environment.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import pandas as pd
3
+ from typing import Tuple
4
+
5
+ from openenv.core.env_server import Environment
6
+ from models import SqlAction, SqlObservation, SqlState
7
+ from database import SqliteManager
8
+ from core import EnvironmentTasks, DataFrameGrader
9
+
10
+ class SqlEnvironment(Environment):
11
+ """Orchestrates the SQL Agent execution matching the OpenEnv Gym API."""
12
+ def __init__(self):
13
+ super().__init__()
14
+ self._state = SqlState()
15
+ self.task_registry = EnvironmentTasks()
16
+
17
+ # Instantiate db connection immediately for the server loop
18
+ self.db = SqliteManager()
19
+ self.db.connect()
20
+
21
+ def reset(self) -> SqlObservation:
22
+ self._state = SqlState(
23
+ episode_id=str(uuid.uuid4()),
24
+ current_task_index=0,
25
+ total_tasks=self.task_registry.get_total_tasks(),
26
+ accumulated_reward=0.0,
27
+ task_scores=[]
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
+ )
34
+
35
+ def state(self) -> SqlState:
36
+ return self._state
37
+
38
+ def step(self, action: SqlAction) -> Tuple[SqlObservation, float, bool]:
39
+ self._state.step_count += 1
40
+ query = action.query
41
+ current_idx = self._state.current_task_index
42
+ task = self.task_registry.get_task(current_idx)
43
+
44
+ execution_result_str = None
45
+ error_str = None
46
+ reward = 0.0
47
+ done = False
48
+
49
+ try:
50
+ # 1. Gather expected vs agent dataframes
51
+ expected_df = self.db.execute_dataframe(task.expected_query)
52
+ agent_df = self.db.execute_dataframe(query)
53
+
54
+ # Serialize for observation
55
+ execution_result_str = agent_df.to_json(orient="records")
56
+
57
+ # 2. Grade execution
58
+ score = DataFrameGrader.grade(agent_df, expected_df)
59
+ self._state.task_scores.append(score)
60
+
61
+ # 3. Reward scaling
62
+ reward += 0.1 # Valid execution baseline
63
+ reward += score
64
+
65
+ # 4. Check level progression
66
+ if score == 1.0:
67
+ self._state.current_task_index += 1
68
+ if self._state.current_task_index >= self.task_registry.get_total_tasks():
69
+ done = True
70
+
71
+ except Exception as e:
72
+ error_str = str(e)
73
+ reward -= 0.5 # Heavy penalty for destructive/invalid SQL
74
+
75
+ # Global accumulation
76
+ self._state.accumulated_reward += reward
77
+
78
+ # Prepare observation response
79
+ if done:
80
+ total_score = sum(self._state.task_scores)
81
+ next_instruction = f"All tasks completed! Final SQL Capability Score: {total_score}"
82
+ schema = ""
83
+ else:
84
+ next_task = self.task_registry.get_task(self._state.current_task_index)
85
+ next_instruction = next_task.instruction
86
+ schema = self.db.get_schema_summary()
87
+
88
+ obs = SqlObservation(
89
+ current_task_instruction=next_instruction,
90
+ schema_info=schema,
91
+ execution_result=execution_result_str,
92
+ execution_error=error_str
93
+ )
94
+
95
+ return obs, reward, done