Upload 7 files
Browse files- Dockerfile +26 -0
- README.md +18 -12
- env.py +98 -0
- inference.py +74 -0
- models.py +17 -0
- pyproject.toml +20 -0
- uv.lock +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight python image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Install system dependencies (SQLite is built-in, but we need curl for healthchecks)
|
| 5 |
+
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
# Install uv (the fast python package manager)
|
| 8 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 9 |
+
|
| 10 |
+
# Set the working directory
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
# Copy the dependency files first to cache the layer
|
| 14 |
+
COPY pyproject.toml uv.lock ./
|
| 15 |
+
|
| 16 |
+
# Install dependencies using uv
|
| 17 |
+
RUN uv sync --frozen
|
| 18 |
+
|
| 19 |
+
# Copy the rest of the application code
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
# Expose the port the FastAPI server runs on
|
| 23 |
+
EXPOSE 8000
|
| 24 |
+
|
| 25 |
+
# Start the OpenEnv server
|
| 26 |
+
CMD ["uv", "run", "server/app.py"]
|
README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SQLite Rescue Environment
|
| 2 |
+
|
| 3 |
+
## Description
|
| 4 |
+
A data engineering environment where an AI agent must clean, normalize, and manipulate a messy SQLite database using raw SQL queries. This simulates the real-world task of data cleaning and database refactoring.
|
| 5 |
+
|
| 6 |
+
## Action & Observation Spaces
|
| 7 |
+
* **Action Space:** The agent submits an SQL `query` (string) to execute, and a `submit` (boolean) flag when they are ready for their final database state to be graded.
|
| 8 |
+
* **Observation Space:** The environment returns the current `schema_info` (string), `rows_affected` (int), any SQL execution `error` (string), and a `query_result` (list of dicts) if the action was a SELECT query.
|
| 9 |
+
|
| 10 |
+
## Tasks
|
| 11 |
+
1. **easy_data_cleaning:** Clean inconsistent dates and trailing whitespaces in a single table.
|
| 12 |
+
2. **medium_schema_normalization:** Split a denormalized monolithic table into two related tables with a foreign key.
|
| 13 |
+
3. **hard_complex_reconciliation:** Write a complex query/view to generate a financial reconciliation report.
|
| 14 |
+
|
| 15 |
+
## Setup Instructions
|
| 16 |
+
1. Install dependencies: `pip install -r requirements.txt` (or via pyproject.toml)
|
| 17 |
+
2. Generate the starting databases: `python generate_templates.py`
|
| 18 |
+
3. Run the baseline: `python inference.py`
|
env.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import shutil
|
| 3 |
+
import os
|
| 4 |
+
from typing import Tuple, Dict, Any
|
| 5 |
+
|
| 6 |
+
from models import RescueAction, RescueObservation, RescueState
|
| 7 |
+
from graders import grade_easy_task, grade_medium_task, grade_hard_task
|
| 8 |
+
|
| 9 |
+
class DatabaseRescueEnv:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.task_name = None
|
| 12 |
+
self.steps_taken = 0
|
| 13 |
+
self.working_db = "working.db"
|
| 14 |
+
self.template_dir = "templates"
|
| 15 |
+
|
| 16 |
+
def _get_schema(self, cursor: sqlite3.Cursor) -> str:
|
| 17 |
+
"""Retrieves the current database schema as a string."""
|
| 18 |
+
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table';")
|
| 19 |
+
tables = cursor.fetchall()
|
| 20 |
+
return "\n".join([t[0] for t in tables if t[0] is not None])
|
| 21 |
+
|
| 22 |
+
def reset(self, task_name: str) -> RescueObservation:
|
| 23 |
+
"""Resets the environment by copying the task's template DB."""
|
| 24 |
+
self.task_name = task_name
|
| 25 |
+
self.steps_taken = 0
|
| 26 |
+
|
| 27 |
+
# Copy the messy template DB to our working path
|
| 28 |
+
template_path = os.path.join(self.template_dir, f"{task_name}.db")
|
| 29 |
+
if not os.path.exists(template_path):
|
| 30 |
+
raise ValueError(f"Template DB for task '{task_name}' not found.")
|
| 31 |
+
|
| 32 |
+
shutil.copyfile(template_path, self.working_db)
|
| 33 |
+
|
| 34 |
+
# Return initial observation
|
| 35 |
+
with sqlite3.connect(self.working_db) as conn:
|
| 36 |
+
schema = self._get_schema(conn.cursor())
|
| 37 |
+
|
| 38 |
+
return RescueObservation(
|
| 39 |
+
schema_info=schema,
|
| 40 |
+
query_result=None,
|
| 41 |
+
rows_affected=0,
|
| 42 |
+
error=None
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def step(self, action: RescueAction) -> Tuple[RescueObservation, float, bool, Dict[str, Any]]:
|
| 46 |
+
self.steps_taken += 1
|
| 47 |
+
reward = 0.0
|
| 48 |
+
done = False
|
| 49 |
+
info = {}
|
| 50 |
+
|
| 51 |
+
# If the agent submits, trigger the grader and end the episode
|
| 52 |
+
if action.submit:
|
| 53 |
+
reward = self._grade_task()
|
| 54 |
+
done = True
|
| 55 |
+
with sqlite3.connect(self.working_db) as conn:
|
| 56 |
+
schema = self._get_schema(conn.cursor())
|
| 57 |
+
return RescueObservation(schema_info=schema), reward, done, info
|
| 58 |
+
|
| 59 |
+
# Otherwise, execute the query
|
| 60 |
+
obs = RescueObservation(schema_info="", rows_affected=0)
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
with sqlite3.connect(self.working_db) as conn:
|
| 64 |
+
conn.row_factory = sqlite3.Row
|
| 65 |
+
cursor = conn.cursor()
|
| 66 |
+
|
| 67 |
+
cursor.execute(action.query)
|
| 68 |
+
conn.commit()
|
| 69 |
+
|
| 70 |
+
obs.schema_info = self._get_schema(cursor)
|
| 71 |
+
obs.rows_affected = cursor.rowcount
|
| 72 |
+
|
| 73 |
+
# If it was a SELECT query, fetch results
|
| 74 |
+
if action.query.strip().upper().startswith("SELECT"):
|
| 75 |
+
rows = cursor.fetchmany(50) # Limit to avoid context bloat
|
| 76 |
+
obs.query_result = [dict(row) for row in rows]
|
| 77 |
+
|
| 78 |
+
except sqlite3.Error as e:
|
| 79 |
+
obs.error = str(e)
|
| 80 |
+
|
| 81 |
+
return obs, reward, done, info
|
| 82 |
+
|
| 83 |
+
def state(self) -> RescueState:
|
| 84 |
+
return RescueState(
|
| 85 |
+
task_name=self.task_name,
|
| 86 |
+
steps_taken=self.steps_taken,
|
| 87 |
+
db_path=self.working_db
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def _grade_task(self) -> float:
|
| 91 |
+
"""Routes the current database to the correct scoring logic."""
|
| 92 |
+
if self.task_name == "easy_data_cleaning":
|
| 93 |
+
return grade_easy_task(self.working_db)
|
| 94 |
+
elif self.task_name == "medium_schema_normalization":
|
| 95 |
+
return grade_medium_task(self.working_db)
|
| 96 |
+
elif self.task_name == "hard_complex_reconciliation":
|
| 97 |
+
return grade_hard_task(self.working_db)
|
| 98 |
+
return 0.0
|
inference.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from openai import OpenAI
|
| 3 |
+
from env import DatabaseRescueEnv
|
| 4 |
+
from models import RescueAction
|
| 5 |
+
|
| 6 |
+
# --- CONFIGURATION (Mapped to Hackathon Requirements) ---
|
| 7 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY")
|
| 8 |
+
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 9 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 10 |
+
TASK_NAME = "easy_data_cleaning"
|
| 11 |
+
MAX_STEPS = 5
|
| 12 |
+
|
| 13 |
+
def run_baseline():
|
| 14 |
+
# Initialize the client and the environment
|
| 15 |
+
client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
|
| 16 |
+
env = DatabaseRescueEnv()
|
| 17 |
+
|
| 18 |
+
# 1. Print the mandatory [START] log
|
| 19 |
+
print(f"[START] task={TASK_NAME} env=sqlite-rescue-env model={MODEL_NAME}")
|
| 20 |
+
|
| 21 |
+
# Reset the environment
|
| 22 |
+
obs = env.reset(TASK_NAME)
|
| 23 |
+
rewards = []
|
| 24 |
+
success = False
|
| 25 |
+
error_msg = "null"
|
| 26 |
+
|
| 27 |
+
# The hardcoded solution to the Easy task for the baseline agent
|
| 28 |
+
solution_queries = [
|
| 29 |
+
"UPDATE customers SET name = TRIM(name);",
|
| 30 |
+
"UPDATE customers SET signup_date = substr(signup_date, 7, 4) || '-' || substr(signup_date, 1, 2) || '-' || substr(signup_date, 4, 2) WHERE signup_date LIKE '%/%';",
|
| 31 |
+
"UPDATE customers SET signup_date = substr(signup_date, 7, 4) || '-' || substr(signup_date, 1, 2) || '-' || substr(signup_date, 4, 2) WHERE signup_date LIKE '%-%' AND length(signup_date) = 10 AND substr(signup_date, 3, 1) = '-';",
|
| 32 |
+
"SELECT * FROM customers;"
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
steps_taken = 0
|
| 36 |
+
for i in range(MAX_STEPS):
|
| 37 |
+
steps_taken += 1
|
| 38 |
+
|
| 39 |
+
# Decide the action
|
| 40 |
+
if i < len(solution_queries):
|
| 41 |
+
query = solution_queries[i]
|
| 42 |
+
action = RescueAction(query=query, submit=False)
|
| 43 |
+
action_str = f"execute_sql('{query}')"
|
| 44 |
+
else:
|
| 45 |
+
action = RescueAction(query="", submit=True)
|
| 46 |
+
action_str = "submit(True)"
|
| 47 |
+
|
| 48 |
+
# Step the environment
|
| 49 |
+
obs, reward, done, info = env.step(action)
|
| 50 |
+
rewards.append(reward)
|
| 51 |
+
|
| 52 |
+
if obs.error:
|
| 53 |
+
error_msg = f"'{obs.error}'"
|
| 54 |
+
else:
|
| 55 |
+
error_msg = "null"
|
| 56 |
+
|
| 57 |
+
# 2. Print the mandatory [STEP] log
|
| 58 |
+
print(f"[STEP] step={steps_taken} action={action_str} reward={reward:.2f} done={str(done).lower()} error={error_msg}")
|
| 59 |
+
|
| 60 |
+
if done:
|
| 61 |
+
success = (reward == 1.0)
|
| 62 |
+
break
|
| 63 |
+
|
| 64 |
+
# 3. Print the mandatory [END] log
|
| 65 |
+
rewards_str = ",".join([f"{r:.2f}" for r in rewards])
|
| 66 |
+
final_score = rewards[-1] if rewards else 0.00
|
| 67 |
+
print(f"[END] success={str(success).lower()} steps={steps_taken} score={final_score:.2f} rewards={rewards_str}")
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
# Ensure the user has an API key set
|
| 71 |
+
if not API_KEY:
|
| 72 |
+
print("Error: Please set HF_TOKEN or OPENAI_API_KEY environment variable.")
|
| 73 |
+
else:
|
| 74 |
+
run_baseline()
|
models.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, List, Dict, Any
|
| 3 |
+
|
| 4 |
+
class RescueAction(BaseModel):
|
| 5 |
+
query: str = Field(description="The SQL query to execute against the SQLite database.")
|
| 6 |
+
submit: bool = Field(default=False, description="Set to True ONLY when you have completed the task and are ready for grading.")
|
| 7 |
+
|
| 8 |
+
class RescueObservation(BaseModel):
|
| 9 |
+
schema_info: str = Field(description="The current schema of the database.")
|
| 10 |
+
query_result: Optional[List[Dict[str, Any]]] = Field(default=None, description="Results from a SELECT query, limited to 50 rows.")
|
| 11 |
+
rows_affected: int = Field(default=0, description="Number of rows modified by INSERT/UPDATE/DELETE.")
|
| 12 |
+
error: Optional[str] = Field(default=None, description="SQL execution error message, if any.")
|
| 13 |
+
|
| 14 |
+
class RescueState(BaseModel):
|
| 15 |
+
task_name: str
|
| 16 |
+
steps_taken: int
|
| 17 |
+
db_path: str
|
pyproject.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61.0"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "sqlite-rescue-env"
|
| 7 |
+
version = "1.0.0"
|
| 8 |
+
description = "A data engineering environment where an agent must rescue a messy SQLite database."
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.9"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"openenv-core>=0.1.0",
|
| 13 |
+
"pydantic>=2.0.0",
|
| 14 |
+
"openai>=1.0.0",
|
| 15 |
+
"fastapi>=0.100.0",
|
| 16 |
+
"uvicorn>=0.23.0"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[project.scripts]
|
| 20 |
+
server = "server.app:main"
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|