File size: 9,664 Bytes
a97839e
 
e106b6e
aad228d
 
 
 
 
 
 
 
 
a97839e
a772363
a97839e
 
949c1e6
a97839e
 
aad228d
 
a97839e
e106b6e
 
949c1e6
aad228d
a772363
a97839e
949c1e6
 
e106b6e
a97839e
aad228d
 
 
 
 
 
 
 
a97839e
6025d0d
a772363
aad228d
a772363
 
aad228d
6025d0d
a772363
 
 
 
aad228d
a772363
aad228d
e106b6e
aad228d
e106b6e
a772363
 
 
aad228d
6025d0d
a97839e
 
 
aad228d
6025d0d
a97839e
 
 
aad228d
e106b6e
a97839e
 
6025d0d
a97839e
e106b6e
a97839e
6025d0d
a97839e
e106b6e
a97839e
e106b6e
a97839e
 
a772363
 
aad228d
a97839e
 
 
 
 
949c1e6
 
a772363
 
a97839e
 
 
 
949c1e6
a97839e
 
 
 
 
 
 
 
 
 
 
949c1e6
 
 
 
 
 
 
 
 
 
 
a97839e
 
 
aad228d
a97839e
 
a772363
a97839e
 
 
 
11d33cf
a97839e
 
 
 
949c1e6
a772363
a97839e
 
aad228d
a97839e
aad228d
 
 
 
 
 
 
 
 
6025d0d
 
 
aad228d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a97839e
aad228d
a97839e
aad228d
949c1e6
aad228d
 
 
 
 
 
949c1e6
aad228d
 
 
 
 
 
 
a97839e
aad228d
 
 
a772363
aad228d
 
 
 
 
 
6025d0d
aad228d
6025d0d
aad228d
e106b6e
 
 
 
a97839e
5e910ae
a97839e
aad228d
 
a97839e
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"""
inference.py β€” Baseline AI agent for SQL Analyst OpenEnv

Required environment variables:
    API_BASE_URL  LLM API endpoint
    MODEL_NAME    Model identifier
    HF_TOKEN      HuggingFace / API key

Stdout format:
    [START] task=<task> env=<benchmark> model=<model>
    [STEP]  step=<n> action=<action> reward=<0.00> done=<true|false> error=<msg|null>
    [END]   success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...>
"""

import os
import sys
import json
import time
import requests
from typing import List, Optional
from openai import OpenAI

# ── Configuration ─────────────────────────────────────────────────────────────

ENV_BASE_URL = "https://p-karthik-mohan-sql-analyst-env.hf.space"
MAX_ATTEMPTS = 5
BENCHMARK    = "sql-analyst-env"

API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.groq.com/openai/v1")
MODEL_NAME   = os.environ.get("MODEL_NAME",   "llama-3.1-8b-instant")
HF_TOKEN     = os.environ.get("HF_TOKEN")

TASKS = [1, 2, 3, 4, 5, 6, 7, 8]
SUCCESS_SCORE_THRESHOLD = 0.5

# Initialize client at top level like the sample
client = OpenAI(
    base_url=API_BASE_URL,
    api_key=HF_TOKEN if HF_TOKEN else "no-key-needed",
)

# ── Stdout log functions (mandatory format) ───────────────────────────────────

def log_start(task: str, env: str, model: str) -> None:
    print(f"[START] task={task} env={env} model={model}", flush=True)

def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
    action_clean = str(action).replace("\n", " ").strip()[:120]
    error_val    = error if error else "null"
    done_val     = str(done).lower()
    print(f"[STEP] step={step} action={action_clean} reward={reward:.2f} done={done_val} error={error_val}", flush=True)

def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
    rewards_str = ",".join(f"{r:.2f}" for r in rewards)
    print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)

def debug(msg: str) -> None:
    print(msg, file=sys.stderr, flush=True)

# ── Environment helpers ───────────────────────────────────────────────────────

def env_reset(task_id: int) -> dict:
    r = requests.post(f"{ENV_BASE_URL}/reset", json={"task_id": task_id}, timeout=30)
    r.raise_for_status()
    return r.json()

def env_step(sql: str) -> dict:
    r = requests.post(f"{ENV_BASE_URL}/step", json={"action": sql}, timeout=30)
    r.raise_for_status()
    return r.json()

def wait_for_server(retries: int = 10, delay: float = 3.0) -> None:
    debug("Waiting for environment server...")
    for i in range(retries):
        try:
            r = requests.get(f"{ENV_BASE_URL}/health", timeout=5)
            if r.status_code == 200:
                debug("Server is ready.")
                return
        except Exception:
            pass
        debug(f"  Not ready yet... ({i+1}/{retries})")
        time.sleep(delay)
    debug("ERROR: Server did not start in time.")
    sys.exit(1)

# ── LLM ───────────────────────────────────────────────────────────────────────

def build_system_prompt() -> str:
    return """You are an expert SQL analyst. Your job is to write correct SQLite queries.

Rules:
- Only write SELECT or WITH (CTE) statements. Never INSERT, UPDATE, DELETE, or DROP.
- Always match the exact column names specified in the task.
- Always filter WHERE status = 'completed' unless told otherwise.
- Use STRFTIME('%Y', order_date) for year filtering in SQLite.
- Use STRFTIME('%Y-%m', order_date) for year-month formatting.
- RANK() OVER (...) and LAG() OVER (...) are supported in SQLite 3.25+.
- Return ONLY the raw SQL query β€” no explanation, no markdown, no backticks.
- If a previous attempt scored less than 1.0, study the feedback and fix the query.
"""

def build_user_prompt(task_description, schema, hint, attempt, previous_attempts):
    prompt = f"""Task:
{task_description}

Database schema:
{schema}

Hint: {hint}

Attempt number: {attempt}
"""
    if previous_attempts:
        prompt += "\nYour previous attempts and their scores:\n"
        for prev in previous_attempts[-3:]:
            prompt += f"""
  Attempt {prev['attempt']}:
    SQL:    {prev['sql']}
    Reward: {prev['reward']} / 1.0
    Columns expected : {prev['details'].get('expected_columns', [])}
    Columns you gave : {prev['details'].get('agent_columns', [])}
    Rows expected    : {prev['details'].get('expected_row_count', '?')}
    Rows you gave    : {prev['details'].get('agent_row_count', '?')}
"""
    prompt += "\nWrite the corrected SQL query now:"
    return prompt

def ask_llm(task_description, schema, hint, attempt, previous_attempts) -> str:
    messages = [
        {"role": "system", "content": build_system_prompt()},
        {"role": "user",   "content": build_user_prompt(task_description, schema, hint, attempt, previous_attempts)},
    ]
    response = client.chat.completions.create(
        model=MODEL_NAME,
        messages=messages,
        temperature=0.0,
        max_tokens=512,
    )
    sql = response.choices[0].message.content.strip()
    if sql.startswith("```"):
        lines = sql.split("\n")
        sql = "\n".join(line for line in lines if not line.strip().startswith("```")).strip()
    return sql

# ── Task runner ───────────────────────────────────────────────────────────────

def run_task(task_id: int) -> None:
    task_name  = f"sql-task-{task_id}"
    rewards: List[float] = []
    steps_taken = 0
    score       = 0.0
    success     = False
    last_error: Optional[str] = None

    log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)

    try:
        reset_resp = env_reset(task_id)
        obs        = reset_resp["observation"]
        task_desc  = obs["task_description"]
        schema     = obs["schema"]
        hint       = obs["hint"]
        difficulty = obs["difficulty"]

        debug(f"\n{'='*60}")
        debug(f"TASK {task_id} ({difficulty.upper()})")
        debug(f"Task: {task_desc}\n")

        previous_attempts = []

        for attempt in range(1, MAX_ATTEMPTS + 1):
            debug(f"  Attempt {attempt}/{MAX_ATTEMPTS} β€” asking LLM...")
            last_error = None
            sql = ""

            try:
                sql = ask_llm(task_desc, schema, hint, attempt, previous_attempts)
                debug(f"  SQL: {sql[:120]}{'...' if len(sql) > 120 else ''}")
            except Exception as e:
                last_error = str(e)
                debug(f"  LLM error: {last_error}")
                log_step(step=attempt, action="", reward=0.0, done=False, error=last_error)
                rewards.append(0.0)
                steps_taken = attempt
                continue

            try:
                step_resp = env_step(sql)
                reward    = step_resp["reward"]
                done      = step_resp["done"]
                details   = step_resp["observation"].get("reward_breakdown", {})
            except Exception as e:
                last_error = str(e)
                debug(f"  Step error: {last_error}")
                log_step(step=attempt, action=sql, reward=0.0, done=False, error=last_error)
                rewards.append(0.0)
                steps_taken = attempt
                continue

            rewards.append(reward)
            steps_taken = attempt

            debug(f"  Reward: {reward:.3f}  (cols={details.get('column_score',0):.2f}  rows={details.get('row_score',0):.2f}  vals={details.get('value_score',0):.2f})")

            log_step(step=attempt, action=sql, reward=reward, done=done, error=last_error)

            previous_attempts.append({
                "attempt": attempt,
                "sql":     sql,
                "reward":  reward,
                "details": details,
            })

            if done:
                debug(f"  PERFECT SCORE on attempt {attempt}!")
                break
            elif reward >= 0.8:
                debug(f"  Score is close ({reward:.3f}). Trying to improve...")
            else:
                debug(f"  Score is low ({reward:.3f}). Refining query...")

    except Exception as e:
        last_error = str(e)
        debug(f"ERROR in task {task_id}: {last_error}")

    finally:
        # Clamp score strictly between 0 and 1 β€” required by OpenEnv spec
        score = sum(rewards) / len(rewards) if rewards else 0.0
        score = max(1e-6, min(score, 1 - 1e-6))
        success = score >= SUCCESS_SCORE_THRESHOLD
        log_end(success=success, steps=steps_taken, score=score, rewards=rewards)

# ── Main ──────────────────────────────────────────────────────────────────────

def main() -> None:
    debug("SQL Analyst OpenEnv β€” Baseline Inference Agent")
    debug(f"Model      : {MODEL_NAME}")
    debug(f"API Base   : {API_BASE_URL}")
    debug(f"Env Server : {ENV_BASE_URL}")

    wait_for_server()

    for task_id in TASKS:
        run_task(task_id)

if __name__ == "__main__":
    main()