File size: 10,246 Bytes
5716a3a
51264be
 
5716a3a
 
 
 
 
 
 
 
 
 
 
 
0a57921
5716a3a
 
2184846
5716a3a
 
0a9f157
2184846
 
 
 
5716a3a
 
51264be
 
 
 
 
 
5716a3a
 
 
0a9f157
 
 
 
5716a3a
 
 
51264be
 
 
 
 
 
 
 
5716a3a
 
0a9f157
5716a3a
 
 
 
 
 
 
51264be
 
0a9f157
5716a3a
 
 
 
 
 
51264be
0a9f157
 
 
 
51264be
 
5716a3a
 
 
 
 
 
51264be
 
 
5716a3a
2184846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a9f157
2184846
 
0a9f157
 
 
2184846
 
 
0a9f157
 
 
 
 
 
2184846
 
 
5716a3a
51264be
 
5716a3a
0a9f157
5716a3a
51264be
 
 
 
5716a3a
 
 
 
 
51264be
 
5716a3a
 
 
 
 
 
51264be
5716a3a
 
 
 
 
 
2184846
5716a3a
 
2184846
5716a3a
51264be
0a9f157
 
 
 
5716a3a
0a9f157
 
5716a3a
0a9f157
 
 
 
 
51264be
 
 
 
 
5716a3a
0a9f157
 
 
 
 
 
 
 
5716a3a
 
0a9f157
5716a3a
574f8e7
5716a3a
0a9f157
 
 
574f8e7
51264be
574f8e7
 
51264be
 
0a9f157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5716a3a
 
0a9f157
 
5716a3a
 
0a9f157
574f8e7
 
 
51264be
 
0a9f157
5716a3a
 
51264be
0a9f157
 
 
 
5716a3a
0a9f157
 
 
 
 
574f8e7
51264be
0a9f157
 
 
51264be
 
574f8e7
0a9f157
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
293
294
295
296
297
298
299
300
"""
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()
    # Collapse newlines so the entire step fits on one line (spec requirement)
    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 errors: FORM->FROM, WEHRE->WHERE, GRUP->GROUP, HAVNG->HAVING,
    ORDR->ORDER, INNE->INNER, LFT->LEFT, BETWEN->BETWEEN, DSC->DESC, SELCT->SELECT.
    """
).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",
    "DPT_ID": "DEPT_ID",
}


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"}
        Your 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. This is required
    by the hackathon spec to avoid disqualification.
    """
    # Initialise all accumulators BEFORE the try so finally can always read them
    rewards: List[float] = []
    history: List[str] = []
    steps_taken = 0
    score = 0.0
    success = False

    # Build LLM client (best-effort; None means heuristic-only mode)
    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()
            obs = reset_resp.json()
        except Exception as exc:
            print(f"[DEBUG] Reset failed: {exc}", flush=True)
            # Do NOT return here — fall through to finally so [END] is always logged
            reset_failed = True

        if not reset_failed:
            # --- step loop -------------------------------------------------
            for step in range(1, MAX_STEPS + 1):
                # Get action (never raises — heuristic is the ultimate fallback)
                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", ""))

                # Submit action to environment
                try:
                    step_resp = await http.post(
                        "/step", json={"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)
                    # Treat as a 0-reward terminal step so episode ends cleanly
                    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

            # Score = average reward across all steps, clamped to [0, 1]
            if rewards:
                score = min(max(sum(rewards) / len(rewards), 0.0), 1.0)
            success = score >= SUCCESS_SCORE_THRESHOLD

    except Exception as exc:
        # Catch-all for any unexpected error in the episode body
        print(f"[DEBUG] Unhandled episode error: {exc}", flush=True)

    finally:
        # Always close the HTTP client
        if http is not None:
            try:
                await http.aclose()
            except Exception as exc:
                print(f"[DEBUG] HTTP close error: {exc}", flush=True)
        # [END] MUST always be emitted — even after reset failure or exception
        log_end(success, steps_taken, score, rewards)


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

async def main() -> None:
    """
    Run tasks according to SQL_ENV_TASK.
    If SQL_ENV_TASK is a single valid difficulty, run only that task.
    Otherwise run all three in sequence so all 3 tasks produce scores.
    """
    try:
        difficulties = (
            (TASK_NAME,)
            if TASK_NAME in {"easy", "medium", "hard"}
            else ("easy", "medium", "hard")
        )
        for difficulty in difficulties:
            await run_task(difficulty)
            print("", flush=True)  # blank line separator between tasks
    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)