P-Karthik-Mohan commited on
Commit
11d33cf
Β·
1 Parent(s): 4423519

agent setup

Browse files
Files changed (8) hide show
  1. .gitignore +15 -0
  2. README.md +208 -34
  3. __pycache__/main.cpython-313.pyc +0 -0
  4. inference.py +37 -94
  5. main.py +105 -189
  6. make_awesome.py +398 -0
  7. requirements.txt +4 -1
  8. ui.py +86 -0
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Security / Secrets
3
+ .env
4
+
5
+ # Virtual Environments
6
+ venv/
7
+ env/
8
+
9
+ # Python Cache
10
+ __pycache__/
11
+ *.pyc
12
+
13
+ # OS generated files
14
+ .DS_Store
15
+ Thumbs.db
README.md CHANGED
@@ -7,66 +7,240 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # SQL Analyst OpenEnv
11
 
12
- A real-world OpenEnv environment where an AI agent writes SQL queries against an e-commerce database to answer business questions.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  ## Tasks
15
 
16
- | ID | Difficulty | Description |
17
- |----|-----------|-------------|
18
- | 1 | Easy | Count completed orders in 2024 |
19
- | 2 | Medium | Top 5 customers by revenue (JOIN + GROUP BY) |
20
- | 3 | Hard | Category revenue ranking (CTE + window function) |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  ## API Endpoints
23
 
24
- - `POST /reset` β€” load a task
25
- - `POST /step` β€” submit a SQL query, get reward 0.0–1.0
26
- - `GET /state` β€” current session state
27
- - `GET /docs` β€” interactive API documentation
28
 
29
- ## Quick Start
 
30
 
31
- ```bash
32
- # Reset to task 1
33
- curl -X POST https://p-karthik-mohan-sql-analyst-env.hf.space/reset \
34
- -H "Content-Type: application/json" \
35
- -d '{"task_id": 1}'
36
-
37
- # Submit a SQL query
38
- curl -X POST https://p-karthik-mohan-sql-analyst-env.hf.space/step \
39
- -H "Content-Type: application/json" \
40
- -d '{"action": "SELECT COUNT(*) AS total_orders FROM orders WHERE status = 'completed'"}'
 
 
 
 
 
 
 
41
  ```
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  ## Reward Function
44
 
45
- Partial credit scoring β€” 0.0 to 1.0:
46
 
47
- - **0.30** β€” correct column names
48
- - **0.30** β€” correct row count
49
- - **0.40** β€” correct cell values
 
 
50
 
51
- ## Running inference.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  ```bash
 
 
54
  export API_BASE_URL=https://api.openai.com/v1
55
  export MODEL_NAME=gpt-4o-mini
56
  export HF_TOKEN=your_api_key_here
 
57
  python inference.py
58
  ```
59
 
60
- ## Database Schema
 
 
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
62
  ```
63
- customers (customer_id, first_name, last_name, email, city, signup_date) β€” 100 rows
64
- products (product_id, product_name, category, price, stock) β€” 30 rows
65
- orders (order_id, customer_id, product_id, quantity, total_amount, order_date, status) β€” 600 rows
 
 
 
 
 
66
  ```
67
 
 
 
68
  ## Hardware Requirements
69
 
70
- - 2 vCPU, 8GB RAM minimum
71
- - No GPU required
72
- - SQLite β€” no external database needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  pinned: false
8
  ---
9
 
10
+ # πŸ“Š SQL Analyst OpenEnv
11
 
12
+ > A real-world OpenEnv environment where an AI agent must write correct SQL queries
13
+ > against a live e-commerce database to answer business analytics questions.
14
+
15
+ **Live Demo:** https://p-karthik-mohan-sql-analyst-env.hf.space/docs
16
+
17
+ ---
18
+
19
+ ## What Is This?
20
+
21
+ This environment simulates the daily work of a **data analyst at an e-commerce company**.
22
+ The AI agent receives a natural language business question, explores the database schema,
23
+ and must produce a correct SQL query to answer it.
24
+
25
+ Unlike toy environments, this is a task that real analysts perform every day β€”
26
+ making it a meaningful benchmark for AI reasoning and code generation.
27
+
28
+ ---
29
 
30
  ## Tasks
31
 
32
+ Three tasks of increasing difficulty, each graded by an automated SQL result comparator.
33
+
34
+ ### Task 1 β€” Easy
35
+ **"How many completed orders were placed in 2024?"**
36
+ - Requires: COUNT, WHERE, date filtering
37
+ - Tests: basic aggregation and filtering
38
+ - Expected output: single row, single column
39
+
40
+ ### Task 2 β€” Medium
41
+ **"Find the top 5 customers by total revenue from completed orders."**
42
+ - Requires: JOIN, GROUP BY, SUM, ORDER BY, LIMIT
43
+ - Tests: multi-table joins and aggregation
44
+ - Expected output: 5 rows with first_name, last_name, total_revenue
45
+
46
+ ### Task 3 β€” Hard
47
+ **"Rank product categories by total revenue using a window function."**
48
+ - Requires: CTE (WITH), JOIN, GROUP BY, RANK() OVER (...)
49
+ - Tests: advanced SQL β€” CTEs and window functions
50
+ - Expected output: all categories with total_revenue and revenue_rank
51
+
52
+ ---
53
 
54
  ## API Endpoints
55
 
56
+ Base URL: https://p-karthik-mohan-sql-analyst-env.hf.space
 
 
 
57
 
58
+ ### POST /reset
59
+ Load a task. Always call this first.
60
 
61
+ Request:
62
+ ```json
63
+ {"task_id": 1}
64
+ ```
65
+
66
+ Response:
67
+ ```json
68
+ {
69
+ "observation": {
70
+ "task_id": 1,
71
+ "difficulty": "easy",
72
+ "task_description": "Find the total number of completed orders...",
73
+ "schema": "Tables:\n customers (...)\n products (...)\n orders (...)",
74
+ "hint": "Use COUNT with WHERE filters on status and order_date"
75
+ },
76
+ "info": {"message": "Task 1 loaded. Use POST /step with your SQL query."}
77
+ }
78
  ```
79
 
80
+ ### POST /step
81
+ Submit a SQL query. Returns reward and feedback.
82
+
83
+ Request:
84
+ ```json
85
+ {"action": "SELECT COUNT(*) AS total_orders FROM orders WHERE status = 'completed'"}
86
+ ```
87
+
88
+ Response:
89
+ ```json
90
+ {
91
+ "observation": {
92
+ "result_preview": [{"total_orders": 312}],
93
+ "reward_breakdown": {
94
+ "column_score": 0.30,
95
+ "row_score": 0.30,
96
+ "value_score": 0.40,
97
+ "total_reward": 1.0
98
+ }
99
+ },
100
+ "reward": 1.0,
101
+ "done": true
102
+ }
103
+ ```
104
+
105
+ ### GET /state
106
+ Get current session β€” task info, all attempts, best score.
107
+
108
+ ---
109
+
110
+ ## Observation Space
111
+
112
+ | Field | Type | Description |
113
+ |---|---|---|
114
+ | task_description | string | Natural language business question |
115
+ | schema | string | All table names, columns, types, row counts |
116
+ | hint | string | Guidance on which SQL constructs to use |
117
+ | result_preview | array | First 5 rows of the agent query result |
118
+ | result_row_count | integer | Total rows returned by agent query |
119
+ | reward_breakdown | object | Sub-scores for columns, rows, values |
120
+
121
+ ---
122
+
123
+ ## Action Space
124
+
125
+ | Property | Value |
126
+ |---|---|
127
+ | Type | string |
128
+ | Format | Valid SQLite SELECT or WITH statement |
129
+ | Restrictions | No INSERT, UPDATE, DELETE, DROP |
130
+
131
+ Example actions:
132
+ ```sql
133
+ -- Easy
134
+ SELECT COUNT(*) AS total_orders FROM orders WHERE status = 'completed'
135
+
136
+ -- Medium
137
+ SELECT c.first_name, c.last_name, SUM(o.total_amount) AS total_revenue
138
+ FROM orders o JOIN customers c ON o.customer_id = c.customer_id
139
+ WHERE o.status = 'completed'
140
+ GROUP BY o.customer_id ORDER BY total_revenue DESC LIMIT 5
141
+
142
+ -- Hard
143
+ WITH rev AS (
144
+ SELECT p.category, SUM(o.total_amount) AS total_revenue
145
+ FROM orders o JOIN products p ON o.product_id = p.product_id
146
+ WHERE o.status = 'completed' GROUP BY p.category
147
+ )
148
+ SELECT category, total_revenue, RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank
149
+ FROM rev ORDER BY revenue_rank ASC
150
+ ```
151
+
152
+ ---
153
+
154
  ## Reward Function
155
 
156
+ Partial credit score from 0.0 to 1.0 with three components:
157
 
158
+ | Component | Weight | How it is measured |
159
+ |---|---|---|
160
+ | Column names | 0.30 | Fraction of expected column names present |
161
+ | Row count | 0.30 | Ratio of returned rows vs expected rows |
162
+ | Cell values | 0.40 | Fraction of cells matching expected values |
163
 
164
+ Even an imperfect query receives meaningful feedback β€” not just pass/fail.
165
+ This gives the agent a gradient signal to improve from.
166
+
167
+ ---
168
+
169
+ ## Database Schema
170
+
171
+ A realistic e-commerce dataset with 600 orders, 100 customers, 30 products.
172
+
173
+ ```
174
+ customers (customer_id, first_name, last_name, email, city, signup_date)
175
+ products (product_id, product_name, category, price, stock)
176
+ orders (order_id, customer_id, product_id, quantity, total_amount, order_date, status)
177
+ ```
178
+
179
+ All orders are dated in 2024. Status values: completed, pending, cancelled.
180
+
181
+ ---
182
+
183
+ ## Running the Baseline Agent
184
 
185
  ```bash
186
+ pip install openai requests
187
+
188
  export API_BASE_URL=https://api.openai.com/v1
189
  export MODEL_NAME=gpt-4o-mini
190
  export HF_TOKEN=your_api_key_here
191
+
192
  python inference.py
193
  ```
194
 
195
+ Expected output:
196
+ ```
197
+ Tasks solved : 3 / 3
198
+ Average reward : 1.000 / 1.000
199
+ Results saved to results.json
200
+ ```
201
 
202
+ ---
203
+
204
+ ## Local Setup
205
+
206
+ ```bash
207
+ git clone https://huggingface.co/spaces/P-Karthik-Mohan/sql-analyst-env
208
+ cd sql-analyst-env
209
+ pip install -r requirements.txt
210
+ python seed.py
211
+ uvicorn main:app --host 0.0.0.0 --port 7860
212
  ```
213
+
214
+ ---
215
+
216
+ ## Docker
217
+
218
+ ```bash
219
+ docker build -t sql-analyst-env .
220
+ docker run -p 7860:7860 sql-analyst-env
221
  ```
222
 
223
+ ---
224
+
225
  ## Hardware Requirements
226
 
227
+ - CPU: 1-2 vCPU
228
+ - RAM: 8GB
229
+ - GPU: Not required
230
+ - Inference time: Under 5 minutes for all 3 tasks
231
+
232
+ ---
233
+
234
+ ## Project Structure
235
+
236
+ ```
237
+ sql-analyst-env/
238
+ β”œβ”€β”€ main.py # FastAPI server
239
+ β”œβ”€β”€ seed.py # Database seeder
240
+ β”œβ”€β”€ inference.py # Baseline AI agent
241
+ β”œβ”€β”€ openenv.yaml # OpenEnv specification
242
+ β”œβ”€β”€ Dockerfile # Container for HF Spaces
243
+ β”œβ”€β”€ requirements.txt # Python dependencies
244
+ └── data/
245
+ └── ecommerce.db # SQLite database
246
+ ```
__pycache__/main.cpython-313.pyc CHANGED
Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ
 
inference.py CHANGED
@@ -3,32 +3,23 @@ inference.py β€” Baseline AI agent for SQL Analyst OpenEnv
3
  ---------------------------------------------------------
4
  Uses the OpenAI client (pointed at any compatible LLM via API_BASE_URL)
5
  to solve all 3 tasks by interacting with the running FastAPI environment.
6
-
7
- Environment variables required:
8
- API_BASE_URL β€” LLM API base URL (e.g. https://api.openai.com/v1)
9
- MODEL_NAME β€” model to use (e.g. gpt-4o-mini)
10
- HF_TOKEN β€” Hugging Face token (used as the API key)
11
-
12
- Usage:
13
- python inference.py
14
  """
15
-
16
  import os
17
  import sys
18
- import json
19
  import time
20
  import requests
21
  from openai import OpenAI
22
-
 
23
  # ── Configuration ─────────────────────────────────────────────────────────────
24
 
25
- ENV_BASE_URL = "http://127.0.0.1:7860" # where FastAPI server is running
26
  MAX_ATTEMPTS = 5 # max SQL attempts per task
27
- TASK_IDS = [1, 2, 3] # tasks to solve
28
 
29
  API_BASE_URL = "https://api.groq.com/openai/v1"
30
  MODEL_NAME = "llama-3.1-8b-instant"
31
- HF_TOKEN = "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"
32
 
33
  # ── OpenAI Client ─────────────────────────────────────────────────────────────
34
 
@@ -44,33 +35,29 @@ def env_reset(task_id: int) -> dict:
44
  r.raise_for_status()
45
  return r.json()
46
 
47
-
48
  def env_step(sql: str) -> dict:
49
  r = requests.post(f"{ENV_BASE_URL}/step", json={"action": sql})
50
  r.raise_for_status()
51
  return r.json()
52
 
53
-
54
  def env_state() -> dict:
55
  r = requests.get(f"{ENV_BASE_URL}/state")
56
  r.raise_for_status()
57
  return r.json()
58
 
59
-
60
  def wait_for_server(retries: int = 10, delay: float = 2.0):
61
  """Wait until the FastAPI server is ready."""
62
  print("Waiting for environment server...")
63
  for i in range(retries):
64
  try:
65
- r = requests.get(f"{ENV_BASE_URL}/health", timeout=3)
66
  if r.status_code == 200:
67
- print("Server is ready.\n")
68
  return
69
- except requests.exceptions.ConnectionError:
70
  pass
71
- print(f" Not ready yet, retrying in {delay}s... ({i+1}/{retries})")
72
  time.sleep(delay)
73
- print("ERROR: Server did not start in time. Is uvicorn running?")
74
  sys.exit(1)
75
 
76
  # ── LLM SQL Generator ─────────────────────────────────────────────────────────
@@ -86,7 +73,6 @@ Rules:
86
  - If a previous attempt scored less than 1.0, study the feedback and fix the query.
87
  """
88
 
89
-
90
  def build_user_prompt(
91
  task_description: str,
92
  schema: str,
@@ -105,22 +91,13 @@ Hint: {hint}
105
  Attempt number: {attempt}
106
  """
107
  if previous_attempts:
108
- prompt += "\nYour previous attempts and their scores:\n"
109
- for prev in previous_attempts[-3:]: # show last 3 only
110
- prompt += f"""
111
- Attempt {prev['attempt']}:
112
- SQL: {prev['sql']}
113
- Reward: {prev['reward']} / 1.0
114
- Columns expected : {prev['details'].get('expected_columns', [])}
115
- Columns you gave : {prev['details'].get('agent_columns', [])}
116
- Rows expected : {prev['details'].get('expected_row_count', '?')}
117
- Rows you gave : {prev['details'].get('agent_row_count', '?')}
118
- Sub-scores : columns={prev['details'].get('column_score', 0):.2f} rows={prev['details'].get('row_score', 0):.2f} values={prev['details'].get('value_score', 0):.2f}
119
- """
120
  prompt += "\nWrite the corrected SQL query now:"
121
  return prompt
122
 
123
-
124
  def ask_llm(task_description: str, schema: str, hint: str,
125
  attempt: int, previous_attempts: list) -> str:
126
  """Call the LLM and return a SQL string."""
@@ -133,18 +110,14 @@ def ask_llm(task_description: str, schema: str, hint: str,
133
  response = client.chat.completions.create(
134
  model=MODEL_NAME,
135
  messages=messages,
136
- temperature=0.0, # deterministic β€” we want correct SQL, not creative SQL
137
  max_tokens=512,
138
  )
139
  sql = response.choices[0].message.content.strip()
140
 
141
  # Strip markdown fences if model wraps in ```sql ... ```
142
  if sql.startswith("```"):
143
- lines = sql.split("\n")
144
- sql = "\n".join(
145
- line for line in lines
146
- if not line.strip().startswith("```")
147
- ).strip()
148
 
149
  return sql
150
 
@@ -172,47 +145,28 @@ def solve_task(task_id: int) -> dict:
172
  final_sql = ""
173
 
174
  for attempt in range(1, MAX_ATTEMPTS + 1):
175
- print(f" Attempt {attempt}/{MAX_ATTEMPTS} β€” asking LLM...")
176
-
177
- # Get SQL from LLM
178
  sql = ask_llm(task_desc, schema, hint, attempt, previous_attempts)
179
- print(f" SQL: {sql[:120]}{'...' if len(sql) > 120 else ''}")
180
-
181
- # Submit to environment
182
  step_resp = env_step(sql)
183
- reward = step_resp["reward"]
184
- done = step_resp["done"]
185
- details = step_resp["observation"].get("reward_breakdown", {})
186
-
187
- print(f" Reward: {reward:.3f} "
188
- f"(cols={details.get('column_score',0):.2f} "
189
- f"rows={details.get('row_score',0):.2f} "
190
- f"vals={details.get('value_score',0):.2f})")
191
-
192
  best_reward = max(best_reward, reward)
193
- final_sql = sql
194
-
195
- # Store attempt for LLM feedback
196
- previous_attempts.append({
197
- "attempt": attempt,
198
- "sql": sql,
199
- "reward": reward,
200
- "details": details,
201
- })
202
-
203
- if done:
204
- print(f" PERFECT SCORE on attempt {attempt}!")
205
  break
206
- elif reward >= 0.8:
207
- print(f" Score is close ({reward:.3f}). Trying to improve...")
208
- else:
209
- print(f" Score is low ({reward:.3f}). Refining query...")
210
 
211
  return {
212
  "task_id": task_id,
213
  "difficulty": difficulty,
214
  "best_reward": best_reward,
215
- "attempts": len(previous_attempts),
216
  "final_sql": final_sql,
217
  "solved": best_reward >= 1.0,
218
  }
@@ -228,8 +182,8 @@ def main():
228
 
229
  results = []
230
  for task_id in TASK_IDS:
231
- result = solve_task(task_id)
232
- results.append(result)
233
 
234
  # ── Final Summary ─────────────────────────────────────────────────────────
235
  print(f"\n{'='*60}")
@@ -237,27 +191,16 @@ def main():
237
  print('='*60)
238
 
239
  total_score = 0.0
 
240
  for r in results:
241
- status = "SOLVED" if r["solved"] else f"best={r['best_reward']:.3f}"
242
- print(f" Task {r['task_id']} ({r['difficulty']:6s}) {status} "
243
- f"in {r['attempts']} attempt(s)")
244
  total_score += r["best_reward"]
 
 
 
245
 
246
- avg_score = total_score / len(results)
247
- print(f"\n Average reward : {avg_score:.3f} / 1.000")
248
- print(f" Tasks solved : {sum(1 for r in results if r['solved'])} / {len(results)}")
249
-
250
- # Save results to file (useful for judges / CI)
251
- output_path = "results.json"
252
- with open(output_path, "w") as f:
253
- json.dump({
254
- "results": results,
255
- "avg_score": round(avg_score, 3),
256
- "tasks_solved": sum(1 for r in results if r["solved"]),
257
- }, f, indent=2)
258
- print(f"\n Results saved to {output_path}")
259
-
260
- return avg_score
261
 
262
 
263
  if __name__ == "__main__":
 
3
  ---------------------------------------------------------
4
  Uses the OpenAI client (pointed at any compatible LLM via API_BASE_URL)
5
  to solve all 3 tasks by interacting with the running FastAPI environment.
 
 
 
 
 
 
 
 
6
  """
 
7
  import os
8
  import sys
 
9
  import time
10
  import requests
11
  from openai import OpenAI
12
+ from dotenv import load_dotenv
13
+ load_dotenv()
14
  # ── Configuration ─────────────────────────────────────────────────────────────
15
 
16
+ ENV_BASE_URL = "https://p-karthik-mohan-sql-analyst-env.hf.space" # live HF Space
17
  MAX_ATTEMPTS = 5 # max SQL attempts per task
18
+ TASK_IDS = [1, 2, 3] # tasks to solve
19
 
20
  API_BASE_URL = "https://api.groq.com/openai/v1"
21
  MODEL_NAME = "llama-3.1-8b-instant"
22
+ HF_TOKEN = os.environ.get("HF_TOKEN")
23
 
24
  # ── OpenAI Client ─────────────────────────────────────────────────────────────
25
 
 
35
  r.raise_for_status()
36
  return r.json()
37
 
 
38
  def env_step(sql: str) -> dict:
39
  r = requests.post(f"{ENV_BASE_URL}/step", json={"action": sql})
40
  r.raise_for_status()
41
  return r.json()
42
 
 
43
  def env_state() -> dict:
44
  r = requests.get(f"{ENV_BASE_URL}/state")
45
  r.raise_for_status()
46
  return r.json()
47
 
 
48
  def wait_for_server(retries: int = 10, delay: float = 2.0):
49
  """Wait until the FastAPI server is ready."""
50
  print("Waiting for environment server...")
51
  for i in range(retries):
52
  try:
53
+ r = requests.get(f"{ENV_BASE_URL}/docs")
54
  if r.status_code == 200:
55
+ print("Server is up!")
56
  return
57
+ except requests.ConnectionError:
58
  pass
 
59
  time.sleep(delay)
60
+ print("ERROR: Server did not start in time.")
61
  sys.exit(1)
62
 
63
  # ── LLM SQL Generator ─────────────────────────────────────────────────────────
 
73
  - If a previous attempt scored less than 1.0, study the feedback and fix the query.
74
  """
75
 
 
76
  def build_user_prompt(
77
  task_description: str,
78
  schema: str,
 
91
  Attempt number: {attempt}
92
  """
93
  if previous_attempts:
94
+ prompt += "\nPrevious attempts:\n"
95
+ for i, prev in enumerate(previous_attempts):
96
+ prompt += f"--- Attempt {i+1} ---\nSQL: {prev['sql']}\nReward: {prev['reward']}\n\n"
97
+
 
 
 
 
 
 
 
 
98
  prompt += "\nWrite the corrected SQL query now:"
99
  return prompt
100
 
 
101
  def ask_llm(task_description: str, schema: str, hint: str,
102
  attempt: int, previous_attempts: list) -> str:
103
  """Call the LLM and return a SQL string."""
 
110
  response = client.chat.completions.create(
111
  model=MODEL_NAME,
112
  messages=messages,
113
+ temperature=0.0,
114
  max_tokens=512,
115
  )
116
  sql = response.choices[0].message.content.strip()
117
 
118
  # Strip markdown fences if model wraps in ```sql ... ```
119
  if sql.startswith("```"):
120
+ sql = sql.split("\n", 1)[-1].rsplit("\n", 1)[0].replace("```", "").strip()
 
 
 
 
121
 
122
  return sql
123
 
 
145
  final_sql = ""
146
 
147
  for attempt in range(1, MAX_ATTEMPTS + 1):
148
+ print(f"Attempt {attempt}/{MAX_ATTEMPTS}...")
 
 
149
  sql = ask_llm(task_desc, schema, hint, attempt, previous_attempts)
150
+ print(f"Generated SQL: {sql}")
151
+
 
152
  step_resp = env_step(sql)
153
+ reward = step_resp["reward"]
154
+ print(f"Reward: {reward}")
155
+
 
 
 
 
 
 
156
  best_reward = max(best_reward, reward)
157
+ final_sql = sql
158
+
159
+ if reward >= 1.0:
160
+ print("Task solved successfully!")
 
 
 
 
 
 
 
 
161
  break
162
+
163
+ previous_attempts.append({"sql": sql, "reward": reward})
 
 
164
 
165
  return {
166
  "task_id": task_id,
167
  "difficulty": difficulty,
168
  "best_reward": best_reward,
169
+ "attempts": len(previous_attempts) + (1 if best_reward >= 1.0 else 0),
170
  "final_sql": final_sql,
171
  "solved": best_reward >= 1.0,
172
  }
 
182
 
183
  results = []
184
  for task_id in TASK_IDS:
185
+ res = solve_task(task_id)
186
+ results.append(res)
187
 
188
  # ── Final Summary ─────────────────────────────────────────────────────────
189
  print(f"\n{'='*60}")
 
191
  print('='*60)
192
 
193
  total_score = 0.0
194
+ solved_count = 0
195
  for r in results:
 
 
 
196
  total_score += r["best_reward"]
197
+ if r["solved"]:
198
+ solved_count += 1
199
+ print(f"Task {r['task_id']} ({r['difficulty']}): Reward = {r['best_reward']:.2f} | Solved = {r['solved']}")
200
 
201
+ avg_score = total_score / len(results) if results else 0
202
+ print(f"\nTasks solved : {solved_count} / {len(results)}")
203
+ print(f"Average reward : {avg_score:.3f} / 1.000")
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
 
206
  if __name__ == "__main__":
main.py CHANGED
@@ -4,128 +4,119 @@ import json
4
  import re
5
  from datetime import datetime
6
  from typing import Any, Optional
 
7
 
8
  from fastapi import FastAPI, HTTPException
9
- from pydantic import BaseModel
 
 
10
 
11
  # ── Config ────────────────────────────────────────────────────────────────────
12
  DB_PATH = os.path.join("data", "ecommerce.db")
13
- app = FastAPI(title="SQL Analyst OpenEnv", version="1.0.0")
 
 
 
 
 
 
 
 
14
 
15
  # ── Pydantic Models ───────────────────────────────────────────────────────────
16
 
17
  class StepRequest(BaseModel):
18
- action: str # The SQL query the agent submits
 
19
 
20
  class StepResponse(BaseModel):
21
- observation: dict
22
- reward: float # 0.0 – 1.0
23
- done: bool
24
- info: dict
25
 
26
  class ResetRequest(BaseModel):
27
- task_id: int # 1 = Easy, 2 = Medium, 3 = Hard
 
28
 
29
  class ResetResponse(BaseModel):
30
- observation: dict
31
- info: dict
 
 
 
32
 
33
  class StateResponse(BaseModel):
34
- task_id: int
35
- task_description: str
36
- schema_info: str
37
- attempts: int
38
- best_reward: float
39
- history: list
 
40
 
41
  # ── Task Definitions ──────────────────────────────────────────────────────────
42
 
43
  TASKS = {
44
  1: {
45
- "description": (
46
- "Find the total number of completed orders placed in the year 2024. "
47
- "Return a single number with column name: total_orders"
48
- ),
49
  "difficulty": "easy",
50
  "hint": "Use COUNT with WHERE filters on status and order_date",
51
- "answer_query": """
52
- SELECT COUNT(*) AS total_orders
53
- FROM orders
54
- WHERE status = 'completed'
55
- AND order_date LIKE '2024%'
56
- """,
57
  },
58
  2: {
59
- "description": (
60
- "Find the top 5 customers by total revenue (sum of total_amount for completed orders only). "
61
- "Return columns: first_name, last_name, total_revenue. "
62
- "Order by total_revenue descending."
63
- ),
64
  "difficulty": "medium",
65
  "hint": "JOIN orders with customers, GROUP BY customer, filter completed, ORDER and LIMIT",
66
- "answer_query": """
67
- SELECT c.first_name, c.last_name,
68
- ROUND(SUM(o.total_amount), 2) AS total_revenue
69
- FROM orders o
70
- JOIN customers c ON o.customer_id = c.customer_id
71
- WHERE o.status = 'completed'
72
- GROUP BY o.customer_id
73
- ORDER BY total_revenue DESC
74
- LIMIT 5
75
- """,
76
  },
77
  3: {
78
- "description": (
79
- "For each product category, calculate the total revenue (completed orders only) "
80
- "and rank categories by revenue using a window function. "
81
- "Return columns: category, total_revenue, revenue_rank. "
82
- "Order by revenue_rank ascending."
83
- ),
84
  "difficulty": "hard",
85
  "hint": "Use SUM with GROUP BY inside a CTE, then apply RANK() OVER (ORDER BY ...) on the result",
86
- "answer_query": """
87
- WITH category_revenue AS (
88
- SELECT p.category,
89
- SUM(o.total_amount) AS total_revenue
90
- FROM orders o
91
- JOIN products p ON o.product_id = p.product_id
92
- WHERE o.status = 'completed'
93
- GROUP BY p.category
94
- )
95
- SELECT category,
96
- total_revenue,
97
- RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank
98
- FROM category_revenue
99
- ORDER BY revenue_rank ASC
100
- """,
101
  },
102
  }
103
 
104
- # ── In-Memory Session State ───────────────────────────────────────────────────
105
 
106
- session = {
107
- "task_id": None,
108
- "task": None,
109
- "expected_rows": None,
110
- "expected_columns": None,
111
- "attempts": 0,
112
- "best_reward": 0.0,
113
- "history": [],
114
- }
 
115
 
116
  # ── Database Helpers ──────────────────────────────────────────────────────────
117
 
 
 
 
118
  def get_connection():
119
  if not os.path.exists(DB_PATH):
120
- raise HTTPException(
121
- status_code=500,
122
- detail=f"Database not found at {DB_PATH}. Run seed.py first."
123
- )
124
  conn = sqlite3.connect(DB_PATH)
125
  conn.row_factory = sqlite3.Row
 
 
126
  return conn
127
 
128
-
129
  def get_schema_info() -> str:
130
  conn = get_connection()
131
  cur = conn.cursor()
@@ -142,9 +133,7 @@ def get_schema_info() -> str:
142
  conn.close()
143
  return "Tables:\n" + "\n".join(schema_parts)
144
 
145
-
146
  def run_query(sql: str) -> tuple[list[dict], list[str]]:
147
- """Run a SQL query and return (rows_as_dicts, column_names)."""
148
  conn = get_connection()
149
  cur = conn.cursor()
150
  cur.execute(sql)
@@ -153,9 +142,8 @@ def run_query(sql: str) -> tuple[list[dict], list[str]]:
153
  conn.close()
154
  return rows, columns
155
 
156
-
157
- def compute_expected():
158
- """Run the answer query and cache the expected result."""
159
  task = session["task"]
160
  rows, columns = run_query(task["answer_query"])
161
  session["expected_rows"] = rows
@@ -163,29 +151,18 @@ def compute_expected():
163
 
164
  # ── Reward Function ───────────────────────────────────────────────────────────
165
 
166
- def compute_reward(agent_rows: list[dict], agent_cols: list[str]) -> tuple[float, dict]:
167
- """
168
- Partial scoring reward function β€” 0.0 to 1.0.
169
-
170
- Breakdown:
171
- - 0.30 correct column names
172
- - 0.30 correct number of rows
173
- - 0.40 correct values (cell-level match)
174
- """
175
  expected_rows = session["expected_rows"]
176
  expected_cols = session["expected_columns"]
177
  details = {}
178
 
179
- # ── Column score (0.30) ──────────────────────────────────────────────────
180
  agent_cols_lower = [c.lower() for c in agent_cols]
181
  expected_cols_lower = [c.lower() for c in expected_cols]
182
  col_matches = sum(1 for c in expected_cols_lower if c in agent_cols_lower)
183
  col_score = (col_matches / len(expected_cols_lower)) * 0.30 if expected_cols_lower else 0.0
184
  details["column_score"] = round(col_score, 3)
185
- details["expected_columns"] = expected_cols
186
- details["agent_columns"] = agent_cols
187
 
188
- # ── Row count score (0.30) ───────────────────────────────────────────────
189
  expected_count = len(expected_rows)
190
  agent_count = len(agent_rows)
191
  if expected_count == 0:
@@ -194,21 +171,14 @@ def compute_reward(agent_rows: list[dict], agent_cols: list[str]) -> tuple[float
194
  row_ratio = min(agent_count, expected_count) / max(agent_count, expected_count)
195
  row_score = row_ratio * 0.30
196
  details["row_score"] = round(row_score, 3)
197
- details["expected_row_count"] = expected_count
198
- details["agent_row_count"] = agent_count
199
 
200
- # ── Value match score (0.40) ─────────────────────────────────────────────
201
  if not expected_rows or not agent_rows:
202
  value_score = 0.0
203
  else:
204
  def normalize(v):
205
- if v is None:
206
- return ""
207
- try:
208
- return str(round(float(v), 1))
209
- except (ValueError, TypeError):
210
- return str(v).strip().lower()
211
-
212
 
213
  matched_cells = 0
214
  total_cells = len(expected_rows) * len(expected_cols_lower)
@@ -216,10 +186,8 @@ def compute_reward(agent_rows: list[dict], agent_cols: list[str]) -> tuple[float
216
  for exp_row, agt_row in zip(expected_rows, agent_rows):
217
  for col in expected_cols_lower:
218
  exp_val = normalize(exp_row.get(col) or exp_row.get(col.upper()))
219
- # Try matching by column name first, then by position
220
  agt_val = normalize(agt_row.get(col) or agt_row.get(col.upper()))
221
  if not agt_val:
222
- # Fall back to positional match
223
  exp_idx = expected_cols_lower.index(col)
224
  if exp_idx < len(agent_cols):
225
  pos_col = agent_cols[exp_idx]
@@ -236,132 +204,80 @@ def compute_reward(agent_rows: list[dict], agent_cols: list[str]) -> tuple[float
236
 
237
  # ── Endpoints ─────────────────────────────────────────────────────────────────
238
 
239
- @app.post("/reset", response_model=ResetResponse)
240
  def reset(req: ResetRequest):
241
- """Start a new task. Call this before /step."""
242
- if req.task_id not in TASKS:
243
- raise HTTPException(status_code=400, detail="task_id must be 1, 2, or 3")
244
-
245
  session["task_id"] = req.task_id
246
  session["task"] = TASKS[req.task_id]
247
  session["attempts"] = 0
248
  session["best_reward"] = 0.0
249
  session["history"] = []
250
-
251
- compute_expected()
252
-
253
- schema = get_schema_info()
254
  observation = {
255
  "task_id": req.task_id,
256
  "difficulty": session["task"]["difficulty"],
257
  "task_description": session["task"]["description"],
258
- "schema": schema,
259
  "hint": session["task"]["hint"],
260
  }
261
- return ResetResponse(
262
- observation=observation,
263
- info={"message": f"Task {req.task_id} loaded. Use POST /step with your SQL query."}
264
- )
265
-
266
 
267
- @app.post("/step", response_model=StepResponse)
268
  def step(req: StepRequest):
269
- """Submit a SQL query. Returns reward 0.0–1.0 and feedback."""
270
- if session["task_id"] is None:
271
- raise HTTPException(status_code=400, detail="Call /reset first to load a task.")
272
-
273
  session["attempts"] += 1
274
  sql = req.action.strip()
275
 
276
- # ── Safety: only allow SELECT statements ─────────────────────────────────
277
  if not re.match(r"^\s*(SELECT|WITH)\b", sql, re.IGNORECASE):
278
  return StepResponse(
279
- observation={"error": "Only SELECT or WITH (CTE) statements are allowed."},
280
- reward=0.0,
281
- done=False,
282
- info={"attempt": session["attempts"], "message": "Rejected: not a SELECT/WITH query."}
283
  )
284
 
285
- # ── Run the agent's query ─────────────────────────────────────────────────
286
  try:
287
  agent_rows, agent_cols = run_query(sql)
288
  except Exception as e:
289
- entry = {
290
- "attempt": session["attempts"],
291
- "sql": sql,
292
- "reward": 0.0,
293
- "error": str(e),
294
- }
295
- session["history"].append(entry)
296
  return StepResponse(
297
- observation={"error": str(e), "sql_submitted": sql},
298
- reward=0.0,
299
- done=False,
300
- info={"attempt": session["attempts"], "message": "SQL execution error."}
301
  )
302
 
303
- # ── Score it ──────────────────────────────────────────────────────────────
304
- reward, details = compute_reward(agent_rows, agent_cols)
305
  session["best_reward"] = max(session["best_reward"], reward)
306
-
307
  done = reward >= 1.0
308
-
309
- entry = {
310
- "attempt": session["attempts"],
311
- "sql": sql,
312
- "reward": reward,
313
- "details": details,
314
- "timestamp": datetime.now().isoformat(),
315
- }
316
- session["history"].append(entry)
317
 
318
  observation = {
319
- "task_id": session["task_id"],
320
- "task_description": session["task"]["description"],
321
- "sql_submitted": sql,
322
- "result_preview": agent_rows[:5], # show first 5 rows
323
- "result_row_count": len(agent_rows),
324
  "reward_breakdown": details,
325
  }
326
-
327
  return StepResponse(
328
- observation=observation,
329
- reward=reward,
330
- done=done,
331
- info={
332
- "attempt": session["attempts"],
333
- "best_reward": session["best_reward"],
334
- "message": "Perfect score! Task complete." if done else "Keep refining your query.",
335
- }
336
  )
337
 
338
-
339
- @app.get("/state", response_model=StateResponse)
340
- def state():
341
- """Get current session state β€” task info, attempts, history."""
342
- if session["task_id"] is None:
343
- raise HTTPException(status_code=400, detail="No active task. Call /reset first.")
344
-
345
  return StateResponse(
 
346
  task_id=session["task_id"],
347
- task_description=session["task"]["description"],
348
  schema_info=get_schema_info(),
349
  attempts=session["attempts"],
350
  best_reward=session["best_reward"],
351
  history=session["history"],
352
  )
353
 
354
-
355
- @app.get("/")
356
  def root():
357
- return {
358
- "name": "SQL Analyst OpenEnv",
359
- "version": "1.0.0",
360
- "tasks": {k: {"difficulty": v["difficulty"], "description": v["description"]} for k, v in TASKS.items()},
361
- "endpoints": ["/reset", "/step", "/state"],
362
- }
363
 
 
364
 
365
- @app.get("/health")
366
- def health():
367
- return {"status": "ok", "db_exists": os.path.exists(DB_PATH)}
 
4
  import re
5
  from datetime import datetime
6
  from typing import Any, Optional
7
+ import uuid
8
 
9
  from fastapi import FastAPI, HTTPException
10
+ from pydantic import BaseModel, Field
11
+ import gradio as gr
12
+ from ui import build_ui
13
 
14
  # ── Config ────────────────────────────────────────────────────────────────────
15
  DB_PATH = os.path.join("data", "ecommerce.db")
16
+ # Under /api for direct agent access, root for UI
17
+ api_app = FastAPI(
18
+ title="SQL Analyst OpenEnv API",
19
+ version="1.1.0",
20
+ docs_url="/docs",
21
+ redoc_url="/redoc",
22
+ description="A real-world benchmark environment for AI agents writing SQL. Use `/reset` to select a task, `/step` to submit a query and receive partial-credit feedback, and `/state` to track history."
23
+ )
24
+
25
 
26
  # ── Pydantic Models ───────────────────────────────────────────────────────────
27
 
28
  class StepRequest(BaseModel):
29
+ session_id: str = Field("default", description="Unique session ID to prevent collisions.")
30
+ action: str = Field(..., description="A valid SQLite SELECT or WITH statement.", examples=["SELECT COUNT(*) AS total_orders FROM orders;"])
31
 
32
  class StepResponse(BaseModel):
33
+ observation: dict = Field(description="Contains result_preview (first 5 rows), reward_breakdown, and any sql execution errors.")
34
+ reward: float = Field(description="Partial credit reward from 0.0 (wrong) to 1.0 (perfect).")
35
+ done: bool = Field(description="True if reward == 1.0 (Task solved).")
36
+ info: dict = Field(description="System message and attempt count.")
37
 
38
  class ResetRequest(BaseModel):
39
+ task_id: int = Field(..., description="ID of the task to load (1 to 5).", examples=[1])
40
+ session_id: str = Field("default", description="Unique session ID.")
41
 
42
  class ResetResponse(BaseModel):
43
+ observation: dict = Field(description="Contains task_description, schema, and hint to be passed to the agent.")
44
+ info: dict = Field(description="System message.")
45
+
46
+ class StateRequest(BaseModel):
47
+ session_id: str = Field("default", description="Unique session ID.")
48
 
49
  class StateResponse(BaseModel):
50
+ session_id: str
51
+ task_id: Optional[int] = Field(description="Currently active Task ID.")
52
+ task_description: Optional[str]
53
+ schema_info: str = Field(description="Raw string representation of DB Schema.")
54
+ attempts: int = Field(description="Number of queries submitted so far on active task.")
55
+ best_reward: float = Field(description="Highest partial credit achieved.")
56
+ history: list = Field(description="Log of all queries executed.")
57
 
58
  # ── Task Definitions ──────────────────────────────────────────────────────────
59
 
60
  TASKS = {
61
  1: {
62
+ "description": "Find the total number of completed orders placed in the year 2024. Return a single number with column name: total_orders",
 
 
 
63
  "difficulty": "easy",
64
  "hint": "Use COUNT with WHERE filters on status and order_date",
65
+ "answer_query": "SELECT COUNT(*) AS total_orders FROM orders WHERE status = 'completed' AND order_date LIKE '2024%'",
 
 
 
 
 
66
  },
67
  2: {
68
+ "description": "Find the top 5 customers by total revenue (sum of total_amount for completed orders only). Return columns: first_name, last_name, total_revenue. Order by total_revenue descending.",
 
 
 
 
69
  "difficulty": "medium",
70
  "hint": "JOIN orders with customers, GROUP BY customer, filter completed, ORDER and LIMIT",
71
+ "answer_query": "SELECT c.first_name, c.last_name, ROUND(SUM(o.total_amount), 2) AS total_revenue FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE o.status = 'completed' GROUP BY o.customer_id ORDER BY total_revenue DESC LIMIT 5",
 
 
 
 
 
 
 
 
 
72
  },
73
  3: {
74
+ "description": "For each product category, calculate the total revenue (completed orders only) and rank categories by revenue using a window function. Return columns: category, total_revenue, revenue_rank. Order by revenue_rank ascending.",
 
 
 
 
 
75
  "difficulty": "hard",
76
  "hint": "Use SUM with GROUP BY inside a CTE, then apply RANK() OVER (ORDER BY ...) on the result",
77
+ "answer_query": "WITH category_revenue AS ( SELECT p.category, SUM(o.total_amount) AS total_revenue FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.status = 'completed' GROUP BY p.category ) SELECT category, total_revenue, RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank FROM category_revenue ORDER BY revenue_rank ASC",
78
+ },
79
+ 4: {
80
+ "description": "Find the average price of products in each category, but only for categories that have more than 2 products. Return columns: category, avg_price.",
81
+ "difficulty": "medium",
82
+ "hint": "Use GROUP BY with HAVING COUNT(...) > 2.",
83
+ "answer_query": "SELECT category, ROUND(AVG(price), 2) AS avg_price FROM products GROUP BY category HAVING COUNT(product_id) > 2",
84
+ },
85
+ 5: {
86
+ "description": "Identify customers who have ordered products from both the 'Electronics' and 'Clothing' categories. Return columns: customer_id, first_name.",
87
+ "difficulty": "hard",
88
+ "hint": "Use INTERSECT on two queries, or GROUP BY customer HAVING COUNT(DISTINCT category) = 2.",
89
+ "answer_query": "SELECT DISTINCT c.customer_id, c.first_name FROM customers c JOIN orders o ON c.customer_id = o.customer_id JOIN products p ON o.product_id = p.product_id WHERE p.category = 'Electronics' INTERSECT SELECT DISTINCT c.customer_id, c.first_name FROM customers c JOIN orders o ON c.customer_id = o.customer_id JOIN products p ON o.product_id = p.product_id WHERE p.category = 'Clothing'",
 
 
90
  },
91
  }
92
 
93
+ # ── Sessions ──────────────────────────────────────────────────────────────────
94
 
95
+ sessions = {}
96
+
97
+ def get_session(sid: str):
98
+ if sid not in sessions:
99
+ sessions[sid] = {
100
+ "task_id": None, "task": None,
101
+ "expected_rows": None, "expected_columns": None,
102
+ "attempts": 0, "best_reward": 0.0, "history": []
103
+ }
104
+ return sessions[sid]
105
 
106
  # ── Database Helpers ──────────────────────────────────────────────────────────
107
 
108
+ def progress_handler():
109
+ raise sqlite3.OperationalError("Query execution aborted: Timed out or exceeded instruction limits. Hint: Too complex CROSS JOIN?")
110
+
111
  def get_connection():
112
  if not os.path.exists(DB_PATH):
113
+ raise HTTPException(status_code=500, detail=f"Database not found at {DB_PATH}.")
 
 
 
114
  conn = sqlite3.connect(DB_PATH)
115
  conn.row_factory = sqlite3.Row
116
+ # Security feature: Prevent DOS
117
+ conn.set_progress_handler(progress_handler, 500000)
118
  return conn
119
 
 
120
  def get_schema_info() -> str:
121
  conn = get_connection()
122
  cur = conn.cursor()
 
133
  conn.close()
134
  return "Tables:\n" + "\n".join(schema_parts)
135
 
 
136
  def run_query(sql: str) -> tuple[list[dict], list[str]]:
 
137
  conn = get_connection()
138
  cur = conn.cursor()
139
  cur.execute(sql)
 
142
  conn.close()
143
  return rows, columns
144
 
145
+ def compute_expected(sid: str):
146
+ session = get_session(sid)
 
147
  task = session["task"]
148
  rows, columns = run_query(task["answer_query"])
149
  session["expected_rows"] = rows
 
151
 
152
  # ── Reward Function ───────────────────────────────────────────────────────────
153
 
154
+ def compute_reward(sid: str, agent_rows: list[dict], agent_cols: list[str]) -> tuple[float, dict]:
155
+ session = get_session(sid)
 
 
 
 
 
 
 
156
  expected_rows = session["expected_rows"]
157
  expected_cols = session["expected_columns"]
158
  details = {}
159
 
 
160
  agent_cols_lower = [c.lower() for c in agent_cols]
161
  expected_cols_lower = [c.lower() for c in expected_cols]
162
  col_matches = sum(1 for c in expected_cols_lower if c in agent_cols_lower)
163
  col_score = (col_matches / len(expected_cols_lower)) * 0.30 if expected_cols_lower else 0.0
164
  details["column_score"] = round(col_score, 3)
 
 
165
 
 
166
  expected_count = len(expected_rows)
167
  agent_count = len(agent_rows)
168
  if expected_count == 0:
 
171
  row_ratio = min(agent_count, expected_count) / max(agent_count, expected_count)
172
  row_score = row_ratio * 0.30
173
  details["row_score"] = round(row_score, 3)
 
 
174
 
 
175
  if not expected_rows or not agent_rows:
176
  value_score = 0.0
177
  else:
178
  def normalize(v):
179
+ if v is None: return ""
180
+ try: return str(round(float(v), 1))
181
+ except: return str(v).strip().lower()
 
 
 
 
182
 
183
  matched_cells = 0
184
  total_cells = len(expected_rows) * len(expected_cols_lower)
 
186
  for exp_row, agt_row in zip(expected_rows, agent_rows):
187
  for col in expected_cols_lower:
188
  exp_val = normalize(exp_row.get(col) or exp_row.get(col.upper()))
 
189
  agt_val = normalize(agt_row.get(col) or agt_row.get(col.upper()))
190
  if not agt_val:
 
191
  exp_idx = expected_cols_lower.index(col)
192
  if exp_idx < len(agent_cols):
193
  pos_col = agent_cols[exp_idx]
 
204
 
205
  # ── Endpoints ─────────────────────────────────────────────────────────────────
206
 
207
+ @api_app.post("/reset", response_model=ResetResponse, tags=["Agent Environment"], summary="Load Task", description="Initializes the environment with a random task, or resets the current task, returning the description, hints, and database schema.")
208
  def reset(req: ResetRequest):
209
+ if req.task_id not in TASKS: raise HTTPException(status_code=400, detail="Invalid task_id")
210
+ session = get_session(req.session_id)
 
 
211
  session["task_id"] = req.task_id
212
  session["task"] = TASKS[req.task_id]
213
  session["attempts"] = 0
214
  session["best_reward"] = 0.0
215
  session["history"] = []
216
+ compute_expected(req.session_id)
 
 
 
217
  observation = {
218
  "task_id": req.task_id,
219
  "difficulty": session["task"]["difficulty"],
220
  "task_description": session["task"]["description"],
221
+ "schema": get_schema_info(),
222
  "hint": session["task"]["hint"],
223
  }
224
+ return ResetResponse(observation=observation, info={"message": f"Task {req.task_id} loaded."})
 
 
 
 
225
 
226
+ @api_app.post("/step", response_model=StepResponse, tags=["Agent Environment"], summary="Execute SQL", description="Executes the valid SQLite `action` against the ecommerce database, evaluating the correctness using a partial 0.0-1.0 Reward function.")
227
  def step(req: StepRequest):
228
+ session = get_session(req.session_id)
229
+ if session["task_id"] is None: raise HTTPException(status_code=400, detail="Call /reset first.")
 
 
230
  session["attempts"] += 1
231
  sql = req.action.strip()
232
 
 
233
  if not re.match(r"^\s*(SELECT|WITH)\b", sql, re.IGNORECASE):
234
  return StepResponse(
235
+ observation={"error": "Only SELECT or WITH allowed."}, reward=0.0, done=False,
236
+ info={"attempt": session["attempts"], "message": "Rejected"}
 
 
237
  )
238
 
 
239
  try:
240
  agent_rows, agent_cols = run_query(sql)
241
  except Exception as e:
242
+ session["history"].append({"attempt": session["attempts"], "sql": sql, "reward": 0.0, "error": str(e)})
 
 
 
 
 
 
243
  return StepResponse(
244
+ observation={"error": str(e), "sql_submitted": sql}, reward=0.0, done=False,
245
+ info={"attempt": session["attempts"], "message": "SQL Error"}
 
 
246
  )
247
 
248
+ reward, details = compute_reward(req.session_id, agent_rows, agent_cols)
 
249
  session["best_reward"] = max(session["best_reward"], reward)
 
250
  done = reward >= 1.0
251
+ session["history"].append({"attempt": session["attempts"], "sql": sql, "reward": reward, "details": details})
 
 
 
 
 
 
 
 
252
 
253
  observation = {
254
+ "task_id": session["task_id"], "task_description": session["task"]["description"],
255
+ "sql_submitted": sql, "result_preview": agent_rows[:5], "result_row_count": len(agent_rows),
 
 
 
256
  "reward_breakdown": details,
257
  }
 
258
  return StepResponse(
259
+ observation=observation, reward=reward, done=done,
260
+ info={"attempt": session["attempts"], "best_reward": session["best_reward"]}
 
 
 
 
 
 
261
  )
262
 
263
+ @api_app.get("/state", response_model=StateResponse, tags=["Diagnostics"], summary="Get Current State", description="Returns attempts, rewards, parsed tasks, and historical executed sql queries array.")
264
+ def state(req: StateRequest):
265
+ session = get_session(req.session_id)
 
 
 
 
266
  return StateResponse(
267
+ session_id=req.session_id,
268
  task_id=session["task_id"],
269
+ task_description=session["task"]["description"] if session["task"] else None,
270
  schema_info=get_schema_info(),
271
  attempts=session["attempts"],
272
  best_reward=session["best_reward"],
273
  history=session["history"],
274
  )
275
 
276
+ @api_app.get("/")
 
277
  def root():
278
+ return {"message": "API running at /api. Try the UI at root (handled by wrapper)!"}
 
 
 
 
 
279
 
280
+ # ── Server Setup ──────────────────────────────────────────────────────────────
281
 
282
+ demo = build_ui()
283
+ app = gr.mount_gradio_app(api_app, demo, path="/")
 
make_awesome.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+
4
+ # 1. Update requirements.txt
5
+ req_txt = "requirements.txt"
6
+ with open(req_txt, "r") as f:
7
+ reqs = f.read()
8
+ if "gradio" not in reqs:
9
+ with open(req_txt, "a") as f:
10
+ f.write("\ngradio==4.44.0\npandas\n")
11
+
12
+ # 2. Create the Gradio UI file (ui.py)
13
+ ui_code = """
14
+ import gradio as gr
15
+ import requests
16
+ import pandas as pd
17
+ import json
18
+ import uuid
19
+
20
+ ENV_BASE_URL = "http://127.0.0.1:7860"
21
+
22
+ def new_session():
23
+ return str(uuid.uuid4())
24
+
25
+ def load_task(task_id, sid):
26
+ try:
27
+ task_num = int(task_id.split()[1])
28
+ r = requests.post(f"{ENV_BASE_URL}/api/reset", json={"task_id": task_num, "session_id": sid})
29
+ if r.status_code != 200:
30
+ return f"Error: {r.text}", "", "", pd.DataFrame(), f"Error loading task {task_num}"
31
+
32
+ data = r.json()
33
+ obs = data["observation"]
34
+ return obs["task_description"], obs["schema"], obs["hint"], pd.DataFrame(), "Task loaded. Write SQL below."
35
+ except Exception as e:
36
+ return str(e), "", "", pd.DataFrame(), "Error loading task"
37
+
38
+ def run_sql(sql, sid):
39
+ if not sql.strip():
40
+ return pd.DataFrame(), "Please enter a SQL query.", "Error"
41
+ try:
42
+ r = requests.post(f"{ENV_BASE_URL}/api/step", json={"action": sql, "session_id": sid})
43
+ data = r.json()
44
+ obs = data.get("observation", {})
45
+ reward = data.get("reward", 0.0)
46
+ done = data.get("done", False)
47
+
48
+ df = pd.DataFrame(obs.get("result_preview", []))
49
+ breakdown = obs.get("reward_breakdown", {})
50
+
51
+ feedback = f"🎯 Reward: {reward:.2f} / 1.0\\n"
52
+ if breakdown:
53
+ feedback += f"Columns: {breakdown.get('column_score',0):.2f}, Rows: {breakdown.get('row_score',0):.2f}, Values: {breakdown.get('value_score',0):.2f}"
54
+ if "error" in obs:
55
+ feedback += f"\\n\\n⚠️ Error: {obs['error']}"
56
+
57
+ return df, feedback, "βœ… SOLVED!" if done else "Keep trying!"
58
+ except Exception as e:
59
+ return pd.DataFrame(), str(e), "Error"
60
+
61
+ def build_ui():
62
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo")) as demo:
63
+ gr.Markdown("# πŸ“Š SQL Analyst OpenEnv - Hackathon Edition")
64
+ gr.Markdown("Test Human or AI performance on realistic E-Commerce SQL Data tasks. [API served at `/api`]")
65
+
66
+ sid = gr.State(new_session)
67
+
68
+ with gr.Row():
69
+ with gr.Column(scale=1):
70
+ task_dropdown = gr.Dropdown(choices=["Task 1 (Easy)", "Task 2 (Medium)", "Task 3 (Hard)", "Task 4 (Medium)", "Task 5 (Hard)"], value="Task 1 (Easy)", label="Select Task")
71
+ btn_load = gr.Button("πŸ”„ Load Task")
72
+
73
+ desc = gr.Textbox(label="Business Question", interactive=False, lines=2)
74
+ hint = gr.Textbox(label="Hint", interactive=False)
75
+ schema = gr.Code(label="Database Schema", language="sql", interactive=False)
76
+
77
+ with gr.Column(scale=2):
78
+ sql_input = gr.Code(label="SQL Editor", language="sql", lines=10)
79
+ btn_run = gr.Button("πŸš€ Run SQL", variant="primary")
80
+
81
+ status_out = gr.Markdown("Ready.")
82
+ feedback_out = gr.Textbox(label="Feedback & Score", interactive=False)
83
+ grid_out = gr.Dataframe(label="Result Preview (First 5 Rows)")
84
+
85
+ btn_load.click(load_task, inputs=[task_dropdown, sid], outputs=[desc, schema, hint, grid_out, feedback_out])
86
+ btn_run.click(run_sql, inputs=[sql_input, sid], outputs=[grid_out, feedback_out, status_out])
87
+
88
+ return demo
89
+ """
90
+ with open("ui.py", "w", encoding="utf-8") as f:
91
+ f.write(ui_code.strip() + "\n")
92
+
93
+ # 3. Rewrite main.py (adding concurrency fixes, security, and mounting gradio)
94
+ main_py_code = """
95
+ import sqlite3
96
+ import os
97
+ import json
98
+ import re
99
+ from datetime import datetime
100
+ from typing import Any, Optional
101
+ import uuid
102
+
103
+ from fastapi import FastAPI, HTTPException
104
+ from pydantic import BaseModel, Field
105
+ import gradio as gr
106
+ from ui import build_ui
107
+
108
+ # ── Config ────────────────────────────────────────────────────────────────────
109
+ DB_PATH = os.path.join("data", "ecommerce.db")
110
+ # Under /api for direct agent access, root for UI
111
+ api_app = FastAPI(title="SQL Analyst OpenEnv API", version="1.1.0")
112
+
113
+ # ── Pydantic Models ───────────────────────────────────────────────────────────
114
+
115
+ class StepRequest(BaseModel):
116
+ session_id: str = "default"
117
+ action: str
118
+
119
+ class StepResponse(BaseModel):
120
+ observation: dict
121
+ reward: float
122
+ done: bool
123
+ info: dict
124
+
125
+ class ResetRequest(BaseModel):
126
+ task_id: int
127
+ session_id: str = "default"
128
+
129
+ class ResetResponse(BaseModel):
130
+ observation: dict
131
+ info: dict
132
+
133
+ class StateRequest(BaseModel):
134
+ session_id: str = "default"
135
+
136
+ class StateResponse(BaseModel):
137
+ session_id: str
138
+ task_id: Optional[int]
139
+ task_description: Optional[str]
140
+ schema_info: str
141
+ attempts: int
142
+ best_reward: float
143
+ history: list
144
+
145
+ # ── Task Definitions ──────────────────────────────────────────────────────────
146
+
147
+ TASKS = {
148
+ 1: {
149
+ "description": "Find the total number of completed orders placed in the year 2024. Return a single number with column name: total_orders",
150
+ "difficulty": "easy",
151
+ "hint": "Use COUNT with WHERE filters on status and order_date",
152
+ "answer_query": "SELECT COUNT(*) AS total_orders FROM orders WHERE status = 'completed' AND order_date LIKE '2024%'",
153
+ },
154
+ 2: {
155
+ "description": "Find the top 5 customers by total revenue (sum of total_amount for completed orders only). Return columns: first_name, last_name, total_revenue. Order by total_revenue descending.",
156
+ "difficulty": "medium",
157
+ "hint": "JOIN orders with customers, GROUP BY customer, filter completed, ORDER and LIMIT",
158
+ "answer_query": "SELECT c.first_name, c.last_name, ROUND(SUM(o.total_amount), 2) AS total_revenue FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE o.status = 'completed' GROUP BY o.customer_id ORDER BY total_revenue DESC LIMIT 5",
159
+ },
160
+ 3: {
161
+ "description": "For each product category, calculate the total revenue (completed orders only) and rank categories by revenue using a window function. Return columns: category, total_revenue, revenue_rank. Order by revenue_rank ascending.",
162
+ "difficulty": "hard",
163
+ "hint": "Use SUM with GROUP BY inside a CTE, then apply RANK() OVER (ORDER BY ...) on the result",
164
+ "answer_query": "WITH category_revenue AS ( SELECT p.category, SUM(o.total_amount) AS total_revenue FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.status = 'completed' GROUP BY p.category ) SELECT category, total_revenue, RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank FROM category_revenue ORDER BY revenue_rank ASC",
165
+ },
166
+ 4: {
167
+ "description": "Find the average price of products in each category, but only for categories that have more than 2 products. Return columns: category, avg_price.",
168
+ "difficulty": "medium",
169
+ "hint": "Use GROUP BY with HAVING COUNT(...) > 2.",
170
+ "answer_query": "SELECT category, ROUND(AVG(price), 2) AS avg_price FROM products GROUP BY category HAVING COUNT(product_id) > 2",
171
+ },
172
+ 5: {
173
+ "description": "Identify customers who have ordered products from both the 'Electronics' and 'Clothing' categories. Return columns: customer_id, first_name.",
174
+ "difficulty": "hard",
175
+ "hint": "Use INTERSECT on two queries, or GROUP BY customer HAVING COUNT(DISTINCT category) = 2.",
176
+ "answer_query": "SELECT DISTINCT c.customer_id, c.first_name FROM customers c JOIN orders o ON c.customer_id = o.customer_id JOIN products p ON o.product_id = p.product_id WHERE p.category = 'Electronics' INTERSECT SELECT DISTINCT c.customer_id, c.first_name FROM customers c JOIN orders o ON c.customer_id = o.customer_id JOIN products p ON o.product_id = p.product_id WHERE p.category = 'Clothing'",
177
+ },
178
+ }
179
+
180
+ # ── Sessions ──────────────────────────────────────────────────────────────────
181
+
182
+ sessions = {}
183
+
184
+ def get_session(sid: str):
185
+ if sid not in sessions:
186
+ sessions[sid] = {
187
+ "task_id": None, "task": None,
188
+ "expected_rows": None, "expected_columns": None,
189
+ "attempts": 0, "best_reward": 0.0, "history": []
190
+ }
191
+ return sessions[sid]
192
+
193
+ # ── Database Helpers ──────────────────────────────────────────────────────────
194
+
195
+ def progress_handler():
196
+ raise sqlite3.OperationalError("Query execution aborted: Timed out or exceeded instruction limits. Hint: Too complex CROSS JOIN?")
197
+
198
+ def get_connection():
199
+ if not os.path.exists(DB_PATH):
200
+ raise HTTPException(status_code=500, detail=f"Database not found at {DB_PATH}.")
201
+ conn = sqlite3.connect(DB_PATH)
202
+ conn.row_factory = sqlite3.Row
203
+ # Security feature: Prevent DOS
204
+ conn.set_progress_handler(progress_handler, 500000)
205
+ return conn
206
+
207
+ def get_schema_info() -> str:
208
+ conn = get_connection()
209
+ cur = conn.cursor()
210
+ schema_parts = []
211
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
212
+ tables = [r["name"] for r in cur.fetchall()]
213
+ for table in tables:
214
+ cur.execute(f"PRAGMA table_info({table})")
215
+ cols = cur.fetchall()
216
+ col_defs = ", ".join(f"{c['name']} {c['type']}" for c in cols)
217
+ cur.execute(f"SELECT COUNT(*) AS n FROM {table}")
218
+ count = cur.fetchone()["n"]
219
+ schema_parts.append(f" {table} ({col_defs}) -- {count} rows")
220
+ conn.close()
221
+ return "Tables:\\n" + "\\n".join(schema_parts)
222
+
223
+ def run_query(sql: str) -> tuple[list[dict], list[str]]:
224
+ conn = get_connection()
225
+ cur = conn.cursor()
226
+ cur.execute(sql)
227
+ columns = [d[0] for d in cur.description] if cur.description else []
228
+ rows = [dict(zip(columns, row)) for row in cur.fetchall()]
229
+ conn.close()
230
+ return rows, columns
231
+
232
+ def compute_expected(sid: str):
233
+ session = get_session(sid)
234
+ task = session["task"]
235
+ rows, columns = run_query(task["answer_query"])
236
+ session["expected_rows"] = rows
237
+ session["expected_columns"] = columns
238
+
239
+ # ── Reward Function ───────────────────────────────────────────────────────────
240
+
241
+ def compute_reward(sid: str, agent_rows: list[dict], agent_cols: list[str]) -> tuple[float, dict]:
242
+ session = get_session(sid)
243
+ expected_rows = session["expected_rows"]
244
+ expected_cols = session["expected_columns"]
245
+ details = {}
246
+
247
+ agent_cols_lower = [c.lower() for c in agent_cols]
248
+ expected_cols_lower = [c.lower() for c in expected_cols]
249
+ col_matches = sum(1 for c in expected_cols_lower if c in agent_cols_lower)
250
+ col_score = (col_matches / len(expected_cols_lower)) * 0.30 if expected_cols_lower else 0.0
251
+ details["column_score"] = round(col_score, 3)
252
+
253
+ expected_count = len(expected_rows)
254
+ agent_count = len(agent_rows)
255
+ if expected_count == 0:
256
+ row_score = 0.30 if agent_count == 0 else 0.0
257
+ else:
258
+ row_ratio = min(agent_count, expected_count) / max(agent_count, expected_count)
259
+ row_score = row_ratio * 0.30
260
+ details["row_score"] = round(row_score, 3)
261
+
262
+ if not expected_rows or not agent_rows:
263
+ value_score = 0.0
264
+ else:
265
+ def normalize(v):
266
+ if v is None: return ""
267
+ try: return str(round(float(v), 1))
268
+ except: return str(v).strip().lower()
269
+
270
+ matched_cells = 0
271
+ total_cells = len(expected_rows) * len(expected_cols_lower)
272
+
273
+ for exp_row, agt_row in zip(expected_rows, agent_rows):
274
+ for col in expected_cols_lower:
275
+ exp_val = normalize(exp_row.get(col) or exp_row.get(col.upper()))
276
+ agt_val = normalize(agt_row.get(col) or agt_row.get(col.upper()))
277
+ if not agt_val:
278
+ exp_idx = expected_cols_lower.index(col)
279
+ if exp_idx < len(agent_cols):
280
+ pos_col = agent_cols[exp_idx]
281
+ agt_val = normalize(agt_row.get(pos_col))
282
+ if exp_val == agt_val:
283
+ matched_cells += 1
284
+
285
+ value_score = (matched_cells / total_cells) * 0.40 if total_cells > 0 else 0.0
286
+ details["value_score"] = round(value_score, 3)
287
+
288
+ total = round(col_score + row_score + value_score, 3)
289
+ details["total_reward"] = total
290
+ return total, details
291
+
292
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
293
+
294
+ @api_app.post("/reset", response_model=ResetResponse)
295
+ def reset(req: ResetRequest):
296
+ if req.task_id not in TASKS: raise HTTPException(status_code=400, detail="Invalid task_id")
297
+ session = get_session(req.session_id)
298
+ session["task_id"] = req.task_id
299
+ session["task"] = TASKS[req.task_id]
300
+ session["attempts"] = 0
301
+ session["best_reward"] = 0.0
302
+ session["history"] = []
303
+ compute_expected(req.session_id)
304
+ observation = {
305
+ "task_id": req.task_id,
306
+ "difficulty": session["task"]["difficulty"],
307
+ "task_description": session["task"]["description"],
308
+ "schema": get_schema_info(),
309
+ "hint": session["task"]["hint"],
310
+ }
311
+ return ResetResponse(observation=observation, info={"message": f"Task {req.task_id} loaded."})
312
+
313
+ @api_app.post("/step", response_model=StepResponse)
314
+ def step(req: StepRequest):
315
+ session = get_session(req.session_id)
316
+ if session["task_id"] is None: raise HTTPException(status_code=400, detail="Call /reset first.")
317
+ session["attempts"] += 1
318
+ sql = req.action.strip()
319
+
320
+ if not re.match(r"^\\s*(SELECT|WITH)\\b", sql, re.IGNORECASE):
321
+ return StepResponse(
322
+ observation={"error": "Only SELECT or WITH allowed."}, reward=0.0, done=False,
323
+ info={"attempt": session["attempts"], "message": "Rejected"}
324
+ )
325
+
326
+ try:
327
+ agent_rows, agent_cols = run_query(sql)
328
+ except Exception as e:
329
+ session["history"].append({"attempt": session["attempts"], "sql": sql, "reward": 0.0, "error": str(e)})
330
+ return StepResponse(
331
+ observation={"error": str(e), "sql_submitted": sql}, reward=0.0, done=False,
332
+ info={"attempt": session["attempts"], "message": "SQL Error"}
333
+ )
334
+
335
+ reward, details = compute_reward(req.session_id, agent_rows, agent_cols)
336
+ session["best_reward"] = max(session["best_reward"], reward)
337
+ done = reward >= 1.0
338
+ session["history"].append({"attempt": session["attempts"], "sql": sql, "reward": reward, "details": details})
339
+
340
+ observation = {
341
+ "task_id": session["task_id"], "task_description": session["task"]["description"],
342
+ "sql_submitted": sql, "result_preview": agent_rows[:5], "result_row_count": len(agent_rows),
343
+ "reward_breakdown": details,
344
+ }
345
+ return StepResponse(
346
+ observation=observation, reward=reward, done=done,
347
+ info={"attempt": session["attempts"], "best_reward": session["best_reward"]}
348
+ )
349
+
350
+ @api_app.get("/state", response_model=StateResponse)
351
+ def state(req: StateRequest):
352
+ session = get_session(req.session_id)
353
+ return StateResponse(
354
+ session_id=req.session_id,
355
+ task_id=session["task_id"],
356
+ task_description=session["task"]["description"] if session["task"] else None,
357
+ schema_info=get_schema_info(),
358
+ attempts=session["attempts"],
359
+ best_reward=session["best_reward"],
360
+ history=session["history"],
361
+ )
362
+
363
+ @api_app.get("/")
364
+ def root():
365
+ return {"message": "API running at /api. Try the UI at root (handled by wrapper)!"}
366
+
367
+ # ── Server Setup ──────────────────────────────────────────────────────────────
368
+
369
+ demo = build_ui()
370
+ app = gr.mount_gradio_app(api_app, demo, path="/")
371
+ """
372
+ with open("main.py", "w", encoding="utf-8") as f:
373
+ f.write(main_py_code.strip() + "\n")
374
+
375
+ # 4. Modify inference.py to use session IDs
376
+ with open("inference.py", "r", encoding="utf-8") as f:
377
+ inf = f.read()
378
+
379
+ inf = inf.replace('ENV_BASE_URL = "http://127.0.0.1:7860"', 'ENV_BASE_URL = "http://127.0.0.1:7860/api"')
380
+ inf = inf.replace("TASK_IDS = [1, 2, 3]", "TASK_IDS = [1, 2, 3, 4, 5]")
381
+ inf = inf.replace('def env_reset(task_id: int) -> dict:', 'def env_reset(task_id: int, session_id: str) -> dict:')
382
+ inf = inf.replace('json={"task_id": task_id}', 'json={"task_id": task_id, "session_id": session_id}')
383
+
384
+ inf = inf.replace('def env_step(sql: str) -> dict:', 'def env_step(sql: str, session_id: str) -> dict:')
385
+ inf = inf.replace('json={"action": sql}', 'json={"action": sql, "session_id": session_id}')
386
+
387
+ inf = inf.replace("reset_resp = env_reset(task_id)", 'session_id = f"baseline_{task_id}"\n reset_resp = env_reset(task_id, session_id)')
388
+ inf = inf.replace("step_resp = env_step(sql)", 'step_resp = env_step(sql, session_id)')
389
+
390
+ inf = inf.replace('r = requests.get(f"{ENV_BASE_URL}/state")', 'r = requests.get(f"{ENV_BASE_URL}/state", json={"session_id": "baseline_1"})')
391
+
392
+ # health check fix for inference wait_for_server
393
+ inf = re.sub(r'requests\.get\(f"\{ENV_BASE_URL\}/health", timeout=3\)', 'requests.get(f"{ENV_BASE_URL}/", timeout=3)', inf)
394
+
395
+ with open("inference.py", "w", encoding="utf-8") as f:
396
+ f.write(inf)
397
+
398
+ print("Done generating make_awesome.")
requirements.txt CHANGED
@@ -3,4 +3,7 @@ uvicorn==0.30.6
3
  pydantic==2.9.2
4
  requests==2.32.3
5
  openai==1.51.0
6
- pyyaml==6.0.2
 
 
 
 
3
  pydantic==2.9.2
4
  requests==2.32.3
5
  openai==1.51.0
6
+ python-dotenv==1.0.1
7
+ pyyaml==6.0.2
8
+ gradio==4.44.0
9
+ pandas
ui.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import pandas as pd
4
+ import json
5
+ import uuid
6
+
7
+ ENV_BASE_URL = "http://127.0.0.1:7860"
8
+
9
+ def new_session():
10
+ return str(uuid.uuid4())
11
+
12
+ def load_task(task_id, sid):
13
+ try:
14
+ task_num = int(task_id.split()[1])
15
+ r = requests.post(f"{ENV_BASE_URL}/api/reset", json={"task_id": task_num, "session_id": sid})
16
+ if r.status_code != 200:
17
+ return f"Error: {r.text}", "", "", pd.DataFrame(), f"Error loading task {task_num}"
18
+
19
+ data = r.json()
20
+ obs = data["observation"]
21
+ return obs["task_description"], obs["schema"], obs["hint"], pd.DataFrame(), "Task loaded. Write SQL below."
22
+ except Exception as e:
23
+ return str(e), "", "", pd.DataFrame(), "Error loading task"
24
+
25
+ def run_sql(sql, sid):
26
+ if not sql.strip():
27
+ return pd.DataFrame(), "Please enter a SQL query.", "Error"
28
+ try:
29
+ r = requests.post(f"{ENV_BASE_URL}/api/step", json={"action": sql, "session_id": sid})
30
+ data = r.json()
31
+ obs = data.get("observation", {})
32
+ reward = data.get("reward", 0.0)
33
+ done = data.get("done", False)
34
+
35
+ df = pd.DataFrame(obs.get("result_preview", []))
36
+ breakdown = obs.get("reward_breakdown", {})
37
+
38
+ feedback = f"🎯 Reward: {reward:.2f} / 1.0\n"
39
+ if breakdown:
40
+ feedback += f"Columns: {breakdown.get('column_score',0):.2f}, Rows: {breakdown.get('row_score',0):.2f}, Values: {breakdown.get('value_score',0):.2f}"
41
+ if "error" in obs:
42
+ feedback += f"\n\n⚠️ Error: {obs['error']}"
43
+
44
+ return df, feedback, "βœ… SOLVED!" if done else "Keep trying!"
45
+ except Exception as e:
46
+ return pd.DataFrame(), str(e), "Error"
47
+
48
+ def build_ui():
49
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo")) as demo:
50
+ gr.Markdown("# πŸ“Š SQL Analyst OpenEnv")
51
+ gr.Markdown("An interactive environment to test SQL generation. Load a task, read the schema, and write a query answering the business question.")
52
+
53
+ sid = gr.State(new_session)
54
+
55
+ with gr.Group():
56
+ gr.Markdown("### Step 1: Select a Task")
57
+ with gr.Row():
58
+ task_dropdown = gr.Dropdown(choices=["Task 1 (Easy)", "Task 2 (Medium)", "Task 3 (Hard)", "Task 4 (Medium)", "Task 5 (Hard)"], value="Task 1 (Easy)", label="Available Tasks", show_label=False)
59
+ btn_load = gr.Button("πŸ”„ Load Task", variant="primary")
60
+
61
+ with gr.Row():
62
+ desc = gr.Textbox(label="🎯 Business Question", interactive=False, lines=2)
63
+
64
+ with gr.Row():
65
+ with gr.Column():
66
+ gr.Markdown("### Step 2: Understand the Data")
67
+ schema = gr.Code(label="Database Schema", language="sql", interactive=False)
68
+ with gr.Accordion("πŸ’‘ Need a hint?", open=False):
69
+ hint = gr.Textbox(show_label=False, interactive=False)
70
+
71
+ with gr.Column():
72
+ gr.Markdown("### Step 3: Write & Execute SQL")
73
+ sql_input = gr.Code(label="SQL Editor", language="sql", lines=12)
74
+ btn_run = gr.Button("πŸš€ Execute Query", variant="primary")
75
+
76
+ status_out = gr.Markdown("Waiting for query...")
77
+ feedback_out = gr.Textbox(label="Evaluation Score & Feedback", interactive=False, lines=3)
78
+
79
+ with gr.Group():
80
+ gr.Markdown("### Step 4: Review Results")
81
+ grid_out = gr.Dataframe(label="Result Preview (First 5 Rows)", interactive=False)
82
+
83
+ btn_load.click(load_task, inputs=[task_dropdown, sid], outputs=[desc, schema, hint, grid_out, feedback_out])
84
+ btn_run.click(run_sql, inputs=[sql_input, sid], outputs=[grid_out, feedback_out, status_out])
85
+
86
+ return demo