File size: 9,909 Bytes
5716a3a
51264be
 
5716a3a
 
 
 
 
 
 
 
 
 
 
 
0a57921
5716a3a
 
2184846
5716a3a
 
0a9f157
2184846
 
 
 
5716a3a
 
2194233
 
 
 
 
 
5716a3a
 
 
0a9f157
 
 
 
5716a3a
 
 
51264be
 
 
 
 
 
 
 
5716a3a
 
 
 
 
 
 
 
 
51264be
 
0a9f157
5716a3a
 
 
 
 
 
51264be
0a9f157
 
 
 
51264be
 
5716a3a
 
 
 
2194233
 
 
 
 
 
51264be
 
 
2184846
2194233
 
 
 
 
 
 
 
2184846
2194233
 
 
2184846
 
 
 
0a9f157
2184846
 
0a9f157
 
 
2184846
 
 
0a9f157
2194233
 
 
0a9f157
 
 
 
2184846
 
 
5716a3a
51264be
 
5716a3a
0a9f157
5716a3a
2194233
 
 
 
 
5716a3a
 
 
 
 
51264be
 
5716a3a
 
 
 
 
 
2194233
5716a3a
 
 
 
 
 
2184846
5716a3a
 
2184846
5716a3a
51264be
0a9f157
 
 
 
5716a3a
0a9f157
 
5716a3a
0a9f157
2194233
0a9f157
51264be
2194233
51264be
2194233
 
5716a3a
0a9f157
 
 
 
 
 
 
5716a3a
 
0a9f157
5716a3a
574f8e7
5716a3a
2194233
0a9f157
 
574f8e7
2194233
 
 
574f8e7
bb74cb1
2194233
bb74cb1
51264be
 
0a9f157
 
 
2194233
0a9f157
 
 
 
 
2194233
 
 
0a9f157
 
2194233
 
 
 
 
0a9f157
 
 
 
 
 
 
 
 
2194233
0a9f157
2194233
 
 
0a9f157
 
 
 
 
 
 
 
 
 
 
 
 
5d880f3
0a9f157
5716a3a
 
0a9f157
5716a3a
 
574f8e7
 
 
51264be
 
5716a3a
 
51264be
0a9f157
 
 
 
5716a3a
2194233
574f8e7
cd6351a
574f8e7
cd6351a
51264be
0a9f157
51264be
5716a3a
 
574f8e7
 
0a9f157
 
51264be
 
 
 
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
"""
inference.py - SQL Correction Environment Baseline Script
=========================================================
MANDATORY - Place this file in the ROOT of the project.

Required environment variables:
  API_BASE_URL   The API endpoint for the LLM
  MODEL_NAME     The model identifier to use for inference
  HF_TOKEN       Your Hugging Face / API key
  ENV_URL        URL of the running environment (default: http://localhost:7860)
  SQL_ENV_TASK   Task difficulty: easy | medium | hard (default: easy)
"""

import asyncio
import os
import sys
import textwrap
from typing import List, Optional
import re

import httpx

try:
    from openai import OpenAI
except Exception:
    OpenAI = None

API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
MODEL_NAME   = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
API_KEY      = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "dummy")
TASK_NAME    = os.getenv("SQL_ENV_TASK", "easy")
BENCHMARK    = "sql-correction-env"
ENV_URL      = os.getenv("ENV_URL", "http://localhost:7860")
MAX_STEPS    = 8
SUCCESS_SCORE_THRESHOLD = 0.5


# ---------------------------------------------------------------------------
# Logging helpers β€” must match the spec format exactly
# ---------------------------------------------------------------------------

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:
    err = error if error else "null"
    done_val = str(done).lower()
    action_clean = action.replace("\n", " ").replace("\r", "").strip()
    print(
        f"[STEP] step={step} action={action_clean} "
        f"reward={reward:.2f} done={done_val} error={err}",
        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) if rewards else ""
    print(
        f"[END] success={str(success).lower()} steps={steps} "
        f"score={score:.3f} rewards={rewards_str}",
        flush=True,
    )


# ---------------------------------------------------------------------------
# LLM / heuristic helpers
# ---------------------------------------------------------------------------

SYSTEM_PROMPT = textwrap.dedent(
    """
    You are an expert SQL debugger.
    You will be shown a broken SQL query that contains typos or keyword errors.
    Fix ALL errors and return ONLY the corrected SQL query.
    No explanation, no markdown, no code blocks, no backticks.
    Common keyword typos to watch for:
      FORM->FROM, WEHRE->WHERE, WHER->WHERE,
      GRUP->GROUP, HAVNG->HAVING, ORDR->ORDER,
      INNE->INNER, LFT->LEFT, BETWEN->BETWEEN,
      DSC->DESC, SELCT->SELECT, LIMT->LIMIT.
    Also watch for column name errors described in the schema context.
    """
).strip()

SQL_REPLACEMENTS = {
    "FORM":   "FROM",
    "WEHRE":  "WHERE",
    "WHER":   "WHERE",
    "GRUP":   "GROUP",
    "HAVNG":  "HAVING",
    "ORDR":   "ORDER",
    "INNE":   "INNER",
    "LFT":    "LEFT",
    "BETWEN": "BETWEEN",
    "DSC":    "DESC",
    "SELCT":  "SELECT",
    "LIMT":   "LIMIT",
}


def heuristic_correct_sql(query: str) -> str:
    """Deterministic fallback when the LLM is unavailable."""
    corrected = query
    for broken, fixed in SQL_REPLACEMENTS.items():
        corrected = re.sub(
            rf"\b{re.escape(broken)}\b", fixed, corrected, flags=re.IGNORECASE
        )
    return corrected.strip()


def get_model_action(
    client: Optional["OpenAI"],
    obs: dict,
    history: List[str],
) -> str:
    """Return a corrected SQL string. Falls back to heuristic on any failure."""
    heuristic = heuristic_correct_sql(obs.get("broken_query", ""))

    if client is None:
        return heuristic

    history_block = "\n".join(history[-4:]) if history else "None"
    user_prompt = textwrap.dedent(
        f"""
        Broken SQL query:
        {obs.get("broken_query", "")}

        Schema context:    {obs.get("schema_context") or "Not provided"}
        Error hint:        {obs.get("error_hint") or "None"}
        Steps remaining:   {obs.get("steps_remaining", "?")}
        Previous attempt:  {obs.get("previous_attempt") or "None"}
        Feedback:          {obs.get("feedback") or "None"}

        Recent history:
        {history_block}

        Return ONLY the corrected SQL query.
        """
    ).strip()

    try:
        completion = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user",   "content": user_prompt},
            ],
            temperature=0.2,
            max_tokens=300,
            stream=False,
        )
        text = (completion.choices[0].message.content or "").strip()
        return text if text else heuristic
    except Exception as exc:
        print(f"[DEBUG] LLM call failed: {exc}", flush=True)
        return heuristic


# ---------------------------------------------------------------------------
# Episode runner
# ---------------------------------------------------------------------------

async def run_task(task_name: str) -> None:
    """
    Run one full episode for `task_name`.

    The [END] log line is ALWAYS emitted via the finally block, even if an
    exception occurs mid-episode or the reset call fails.
    """
    rewards: List[float] = []
    history: List[str]   = []
    steps_taken = 0
    score       = 0.0
    success     = False

    client = None
    if OpenAI is not None and API_KEY not in {"", "dummy"}:
        try:
            client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
        except Exception as exc:
            print(f"[DEBUG] OpenAI client init failed: {exc}", flush=True)

    log_start(task_name, BENCHMARK, MODEL_NAME)

    http: Optional[httpx.AsyncClient] = None
    try:
        http = httpx.AsyncClient(base_url=ENV_URL, timeout=60.0)

        # ── reset ──────────────────────────────────────────────────────────
        reset_failed = False
        obs: dict = {}
        try:
            reset_resp = await http.post(
                "/reset", json={"difficulty": task_name}
            )
            reset_resp.raise_for_status()
            reset_data = reset_resp.json()
            # The openenv wrapper may nest the observation under "observation"
            obs = reset_data.get("observation", reset_data)
        except Exception as exc:
            print(f"[DEBUG] Reset failed: {exc}", flush=True)
            reset_failed = True

        if not reset_failed:
            # ── step loop ──────────────────────────────────────────────────
            for step in range(1, MAX_STEPS + 1):
                try:
                    action_str = get_model_action(client, obs, history)
                except Exception as exc:
                    print(f"[DEBUG] Model action failed: {exc}", flush=True)
                    action_str = heuristic_correct_sql(
                        obs.get("broken_query", "")
                    )

                try:
                    # Action must be wrapped under {"action": {...}}
                    step_resp = await http.post(
                        "/step",
                        json={"action": {"corrected_query": action_str}},
                    )
                    step_resp.raise_for_status()
                    result = step_resp.json()
                except Exception as exc:
                    print(f"[DEBUG] Step {step} request failed: {exc}", flush=True)
                    rewards.append(0.0)
                    steps_taken = step
                    log_step(step, action_str, 0.0, True, str(exc))
                    break

                obs    = result.get("observation", obs)
                reward = float(result.get("reward", 0.0))
                done   = bool(result.get("done", False))
                info   = result.get("info")
                error  = info.get("error") if isinstance(info, dict) else None

                rewards.append(reward)
                steps_taken = step
                history.append(
                    f"Step {step}: attempt={action_str!r} reward={reward:+.2f}"
                )

                log_step(step, action_str, reward, done, error)

                if done:
                    break

            if rewards:
                score   = min(max(sum(rewards) / len(rewards), 0.01), 0.99)
            success = score >= SUCCESS_SCORE_THRESHOLD

    except Exception as exc:
        print(f"[DEBUG] Unhandled episode error: {exc}", flush=True)

    finally:
        if http is not None:
            try:
                await http.aclose()
            except Exception as exc:
                print(f"[DEBUG] HTTP close error: {exc}", flush=True)
        log_end(success, steps_taken, score, rewards)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

async def main() -> None:
    """Run all three difficulties in sequence so validator sees 3 [END] lines."""
    try:
        for difficulty in ("easy", "medium", "hard"):
            await run_task(difficulty)
            print("", flush=True)
    except Exception as exc:
        print(f"[DEBUG] Main loop error: {exc}", flush=True)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass
    except Exception as exc:
        print(f"[DEBUG] Fatal error: {exc}", flush=True)
    finally:
        sys.exit(0)