aastikny commited on
Commit
1b629b2
·
verified ·
1 Parent(s): f7f80d5

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +26 -0
  2. README.md +18 -12
  3. env.py +98 -0
  4. inference.py +74 -0
  5. models.py +17 -0
  6. pyproject.toml +20 -0
  7. 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
- title: Sqlite Rescue Env
3
- emoji: 📉
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: AI agents rescue, clean, and normalize a messy SQLite dbs
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
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