CoolFace
Apppublic

RonyForAI/Mirage_DB_RL

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
inference.py358 linesDownload Raw Back to root
1"""2inference.py — Baseline inference script for the Mirage_RL OpenEnv environment.3Place this file in the root directory of the project (Mirage_RL/).4 5MANDATORY ENVIRONMENT VARIABLES:6    HF_TOKEN         Your Hugging Face / API key.7    API_BASE_URL     The API endpoint for the LLM.8                     Default: https://router.huggingface.co/v19    MODEL_NAME       The model identifier to use for inference.10                     Default: Qwen/Qwen2.5-72B-Instruct11 12STDOUT FORMAT (exact — one line per tag, no newlines within a line):13    [START] task=<task_name> env=<benchmark> model=<model_name>14    [STEP]  step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>15    [END]   success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>16 17Usage:18    HF_TOKEN=<key> API_BASE_URL=<url> MODEL_NAME=<model> python inference.py19"""20 21from __future__ import annotations22 23import os24import sys25import time26import textwrap27from typing import List, Optional28 29# ── Path setup: works when run locally or inside Docker ───────────────────────30_HERE = os.path.dirname(os.path.abspath(__file__))31if _HERE not in sys.path:32    sys.path.insert(0, _HERE)33 34# ── OpenAI client ─────────────────────────────────────────────────────────────35try:36    from openai import OpenAI37except ImportError:38    sys.exit("ERROR: 'openai' package not found. Run: pip install openai>=1.0.0")39 40# ── Environment + task imports ────────────────────────────────────────────────41try:42    from Mirage_RL.server.Mirage_RL_environment import QueryEnv43    from Mirage_RL.server.tasks import TASKS, grade44    from Mirage_RL.models import QueryAction45except ImportError:46    try:47        from server.Mirage_RL_environment import QueryEnv          # type: ignore48        from server.tasks import TASKS, grade                      # type: ignore49        from models import QueryAction                             # type: ignore50    except ImportError as exc:51        sys.exit(52            f"ERROR: Cannot import Mirage_RL modules.\n"53            f"Run inference.py from the Mirage_RL/ directory or install the package.\n"54            f"Details: {exc}"55        )56 57# ─────────────────────────────────────────────────────────────────────────────58# Configuration59# ─────────────────────────────────────────────────────────────────────────────60 61API_KEY      = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY", "")62API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"63MODEL_NAME   = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"64BENCHMARK    = "mirage_rl"65 66TASK_ORDER              = ["easy", "medium", "hard"]67MAX_STEPS               = 10      # hard upper bound per episode (tasks finish naturally)68MAX_RETRIES             = 369RETRY_DELAY             = 1.070SUCCESS_SCORE_THRESHOLD = 0.5     # score >= 0.5 counts as success71 72 73# ─────────────────────────────────────────────────────────────────────────────74# Mandatory log helpers  (exact format — do not modify field names or order)75# ─────────────────────────────────────────────────────────────────────────────76 77def log_start(task: str, env: str, model: str) -> None:78    print(f"[START] task={task} env={env} model={model}", flush=True)79 80 81def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:82    error_val = error if error else "null"83    done_val  = str(done).lower()84    print(85        f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",86        flush=True,87    )88 89 90def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:91    rewards_str = ",".join(f"{r:.2f}" for r in rewards)92    print(93        f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}",94        flush=True,95    )96 97 98# ─────────────────────────────────────────────────────────────────────────────99# Action string formatter  (used in [STEP] log)100# ─────────────────────────────────────────────────────────────────────────────101 102_JOIN_NAMES  = {0: "hash", 1: "nested_loop", 2: "merge_sort"}103_INDEX_NAMES = {0: "no_index", 1: "use_index"}104 105def format_action(action: QueryAction, tables: list) -> str:106    table_name = tables[action.next_table] if action.next_table < len(tables) else str(action.next_table)107    join_name  = _JOIN_NAMES.get(action.join_type, str(action.join_type))108    idx_name   = _INDEX_NAMES.get(action.use_index, str(action.use_index))109    return f"join({table_name},{join_name},{idx_name})"110 111 112# ─────────────────────────────────────────────────────────────────────────────113# Prompt builder114# ─────────────────────────────────────────────────────────────────────────────115 116SYSTEM_PROMPT = textwrap.dedent("""117    You are a database query optimizer. Your job is to build an efficient join plan118    by deciding, one table at a time, which table to join next and how.119 120    You will see table statistics including estimated row counts, join selectivities,121    and index availability for each table. These estimates may not be exact — real122    database planners always operate on statistics that can differ from ground truth.123 124    TWO things determine your score:125 126    1. JOIN METHOD quality (60% of score): which algorithm and index you choose.127       Different join algorithms have very different cost profiles.128       Using an index when one is available significantly reduces scan cost.129 130    2. JOIN ORDER quality (40% of score): which table you pick at each step.131       Joining a table with a large output early causes the intermediate result132       to explode — every subsequent join must then probe against that larger set.133       To minimise total cost: prefer joining tables with the smallest134       (estimated_rows × selectivity) output first, saving large tables for later.135 136    The observation shows 'intermediate_size': the estimated size of the accumulated137    intermediate result from all joins so far. Keep this number small by joining138    highly selective, small-output tables early in the sequence.139 140    Respond with ONLY a valid JSON object — no markdown, no explanation:141    {"next_table": <int>, "join_type": <0|1|2>, "use_index": <0|1>}142""").strip()143 144 145def build_user_prompt(obs, task_config, step: int) -> str:146    lines = []147    for i in range(len(obs.tables)):148        status   = "JOINED" if i in obs.chosen_order else "available"149        idx_info = "indexed" if obs.has_index[i] else "no_index"150        est_out  = obs.table_rows[i] * obs.selectivities[i]   # estimated output rows151        lines.append(152            f"  [{i}] {obs.tables[i]:15s}  est_rows={obs.table_rows[i]:>12,}  "153            f"sel={obs.selectivities[i]:.3f}  est_output={est_out:>12,.0f}  "154            f"{idx_info:10s}  [{status}]"155        )156 157    remaining_display = [f"[{i}]{obs.tables[i]}" for i in obs.remaining_tables]158    joined_display    = [obs.tables[i] for i in obs.chosen_order]159 160    return textwrap.dedent(f"""161        Step {step} of episode  |  Task: {task_config.name}  [{task_config.difficulty.upper()}]162 163        Query:164        {obs.query_context}165 166        Table statistics (est_rows = planner estimates, may differ from true cardinality):167        {chr(10).join(lines)}168 169        Join progress:170          Already joined       : {joined_display if joined_display else "(none — first step)"}171          Remaining            : {remaining_display}172          Accumulated cost     : {obs.current_cost:.2f}173          Intermediate size    : {obs.intermediate_size:,.0f}  ← keep this small; explodes if you join large-output tables early174 175        Decide the next join. Choose next_table from {obs.remaining_tables}.176        Reason about join ORDER (intermediate blowup) AND join METHOD (algorithm + index).177        Output the JSON.178    """).strip()179 180 181 182# ─────────────────────────────────────────────────────────────────────────────183# LLM call + greedy fallback184# ─────────────────────────────────────────────────────────────────────────────185 186import random as _random187 188def random_fallback(obs) -> dict:189    """190    Random fallback when LLM fails — picks a uniformly random remaining table,191    random join type, random index usage. This ensures LLM failures are penalised192    rather than silently rescued by an optimal greedy choice.193    """194    table  = _random.choice(obs.remaining_tables)195    j_type = _random.randint(0, 2)196    use_ix = _random.randint(0, 1)197    return {"next_table": table, "join_type": j_type, "use_index": use_ix}198 199 200def get_action(client: OpenAI, obs, task_config, step: int) -> tuple[dict, Optional[str]]:201    """202    Ask LLM for next action. Returns (action_dict, error_or_None).203    Falls back to greedy after MAX_RETRIES failures.204    """205    user_prompt = build_user_prompt(obs, task_config, step)206    last_error: Optional[str] = None207 208    for attempt in range(MAX_RETRIES):209        try:210            completion = client.chat.completions.create(211                model=MODEL_NAME,212                messages=[213                    {"role": "system", "content": SYSTEM_PROMPT},214                    {"role": "user",   "content": user_prompt},215                ],216                temperature=0.0,217                max_tokens=128,218                stream=False,219            )220            text = (completion.choices[0].message.content or "").strip()221 222            # Strip markdown fences if present223            if text.startswith("```"):224                parts = text.split("```")225                text = parts[1].lstrip("json").strip() if len(parts) > 1 else text226 227            import json228            action = json.loads(text)229 230            # Validate231            if action.get("next_table") not in obs.remaining_tables:232                raise ValueError(f"next_table={action.get('next_table')} not in {obs.remaining_tables}")233            action["join_type"] = int(action.get("join_type", 2))234            action["use_index"] = int(action.get("use_index", 1))235            if action["join_type"] not in (0, 1, 2):236                action["join_type"] = 2237            if action["use_index"] not in (0, 1):238                action["use_index"] = 1239 240            return action, None241 242        except Exception as exc:243            last_error = str(exc)244            if attempt < MAX_RETRIES - 1:245                time.sleep(RETRY_DELAY)246 247    # All retries exhausted → random fallback (penalises LLM failures)248    return random_fallback(obs), f"llm_failed:{last_error}"249 250 251# ─────────────────────────────────────────────────────────────────────────────252# Single task runner253# ─────────────────────────────────────────────────────────────────────────────254 255def run_task(task_id: str, client: OpenAI) -> float:256    """Run one task episode. Returns final score in [0.0, 1.0]."""257    task_config = TASKS[task_id]258    env = QueryEnv()259 260    rewards:     List[float]  = []261    steps_taken: int          = 0262    score:       float        = 0.0263    success:     bool         = False264 265    log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)266 267    try:268        obs = env.reset(task_id=task_id, seed=42)  # seed=42 → reproducible scenario + noise269 270        for step in range(1, MAX_STEPS + 1):271            if obs.done:272                break273 274            action_dict, error = get_action(client, obs, task_config, step)275 276            action = QueryAction(277                next_table=int(action_dict["next_table"]),278                join_type=int(action_dict["join_type"]),279                use_index=int(action_dict["use_index"]),280            )281 282            obs = env.step(action)283 284            reward       = obs.reward285            done         = obs.done286            steps_taken  = step287 288            rewards.append(reward)289 290            log_step(291                step=step,292                action=format_action(action, list(obs.tables)),293                reward=reward,294                done=done,295                error=error,296            )297 298            if done:299                break300 301        # Final grader score — the environment returns the episode score natively on done302        score   = rewards[-1] if rewards else 0.0303        score   = min(max(score, 0.0), 1.0)304        success = score >= SUCCESS_SCORE_THRESHOLD305 306    except Exception as exc:307        print(f"[DEBUG] Task {task_id} exception: {exc}", flush=True)308        score   = 0.0309        success = False310 311    finally:312        try:313            if hasattr(env, "close"):314                env.close()315        except Exception:316            pass317        log_end(success=success, steps=steps_taken, score=score, rewards=rewards)318 319    return score320 321 322# ─────────────────────────────────────────────────────────────────────────────323# Entry point324# ─────────────────────────────────────────────────────────────────────────────325 326def main() -> None:327    if not API_KEY:328        print(329            "ERROR: No API key found.\n"330            "Set HF_TOKEN (or API_KEY / OPENAI_API_KEY) before running.",331            file=sys.stderr,332        )333        sys.exit(1)334 335    client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)336 337    results: list[dict] = []338    for task_id in TASK_ORDER:339        score = run_task(task_id, client)340        results.append({"task_id": task_id, "score": score})341 342    # Human-readable summary (goes to stdout after all [END] lines)343    avg = sum(r["score"] for r in results) / len(results)344    sep = "=" * 52345    print(f"\n{sep}", flush=True)346    print("  MIRAGE_RL BASELINE RESULTS", flush=True)347    print(sep, flush=True)348    for r in results:349        bar = "█" * int(r["score"] * 20)350        print(f"  {r['task_id']:8s} | {r['score']:.2f} | {bar}", flush=True)351    print(sep, flush=True)352    print(f"  {'AVERAGE':8s} | {avg:.2f}", flush=True)353    print(sep, flush=True)354 355 356if __name__ == "__main__":357    main()358