sql-agent / inference.py
Abhishek Tiwari
fix: strictly output [START], [STEP], [END] to stdout for phase 2 validator parsing
ebcd8b6
Raw
History Blame
6.4 kB
import os
import re
import json
import requests
from openai import OpenAI
import sys
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
ENV_URL = os.getenv("ENV_URL", "http://localhost:7860")
ENV_NAME = "sql_debugger"
MAX_STEPS = 6
TEMPERATURE = 0.1
MAX_TOKENS = 500
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
SYSTEM_PROMPT = """You are an expert SQL developer working with a SQLite database.
Your job depends on the task type:
- write_query: Write a correct SQL SELECT query from scratch
- fix_query: You are shown broken SQL. Fix it so it runs correctly
- optimize_query: You are shown slow SQL. Rewrite it using CTEs (WITH clause)
or window functions (ROW_NUMBER, RANK) instead of correlated subqueries
RESPONSE FORMAT — always respond with ONLY this JSON:
{"action_type": "write_query", "sql": "SELECT ...", "explanation": "brief reason"}
RULES:
- Only SELECT statements are allowed
- No DROP, DELETE, INSERT, UPDATE, CREATE, ALTER, TRUNCATE
- For fix_query: keep the same intent, just fix the bugs
- For optimize_query: use WITH clause or window functions
- Respond ONLY with the JSON object, no other text"""
def build_prompt(obs: dict, step: int, history: list) -> str:
parts = []
parts.append(f"=== TASK ===\n{obs.get('task_description', '')}")
schema = obs.get('schema_info', '')
if schema:
parts.append(f"=== DATABASE SCHEMA ===\n{chr(10).join(schema.split(chr(10))[:30])}")
data = obs.get('sample_data', '')
if data:
parts.append(f"=== SAMPLE DATA ===\n{chr(10).join(data.split(chr(10))[:20])}")
hints = obs.get('expected_description', '')
if hints:
parts.append(f"=== HINTS ===\n{hints}")
last_sql = obs.get('last_sql')
if last_sql:
parts.append(f"=== YOUR PREVIOUS SQL ===\n{last_sql}")
last_result = obs.get('last_result')
if last_result:
parts.append(f"=== RESULT OF PREVIOUS SQL ===\n{last_result}")
last_error = obs.get('last_error')
if last_error:
parts.append(f"=== ERROR ===\n{last_error}")
feedback = obs.get('feedback', '')
if feedback and step > 1:
parts.append(f"=== GRADER FEEDBACK ===\n{feedback}")
if history:
hist_str = "\n".join(history[-3:])
parts.append(f"=== RECENT HISTORY ===\n{hist_str}")
parts.append("Respond with ONLY a JSON action object.")
return "\n\n".join(parts)
def parse_action(response_text: str, task_type: str) -> dict:
text = response_text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r'\{[^{}]+\}', text)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
pass
sql_match = re.search(r'(?:SELECT|WITH).+', text, re.DOTALL | re.IGNORECASE)
if sql_match:
sql = sql_match.group(0).strip()
if sql.endswith("```"): sql = sql[:-3].strip()
return {"action_type": task_type, "sql": sql, "explanation": "Regex parsed"}
return {"action_type": task_type, "sql": "SELECT 1", "explanation": "parse failed"}
def run_episode(task_id: str):
try:
res = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id, "difficulty": None})
res.raise_for_status()
obs_obj = res.json()
obs = obs_obj["observation"]
except Exception as e:
# Failsafe reset
obs = {"task_type": "write_query"}
print(f"[START] task={task_id} env={ENV_NAME} model={MODEL_NAME}", flush=True)
best_reward = 0.0
history = []
step = 0
done = False
for step in range(1, MAX_STEPS + 1):
if obs.get("done", False):
break
prompt = build_prompt(obs, step, history)
try:
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS
)
response_text = completion.choices[0].message.content or ""
except Exception as e:
response_text = ""
action = parse_action(response_text, obs.get("task_type", "write_query"))
# Safe JSON stringification for the action output in the log
action_str = json.dumps(action)
try:
step_res = requests.post(f"{ENV_URL}/step", json=action)
step_res.raise_for_status()
step_data = step_res.json()
except Exception as e:
# Fallback if step errors
step_data = {"reward": 0.05, "observation": {"done": True}, "done": True, "error": str(e)}
reward = step_data.get("reward", 0.05)
done = step_data.get("done", False)
err = step_data.get("error")
# Format the numbers cleanly
reward_formatted = f"{reward:.2f}"
best_reward = max(best_reward, reward)
obs = step_data.get("observation", {})
# Convert flags to lowercase 'true' or 'false' for validator tracking
done_str = "true" if done else "false"
err_str = "null" if not err else f'"{str(err)}"'
print(f"[STEP] step={step} action={action_str} reward={reward_formatted} done={done_str} error={err_str}", flush=True)
history.append(f"Step {step}: reward={reward_formatted}")
if done:
break
# Track final outcome: successes usually represent the max reward crossing some threshold
success_str = "true" if best_reward > 0.8 else "false"
best_reward_formatted = f"{best_reward:.2f}"
print(f"[END] success={success_str} steps={step} rewards={best_reward_formatted}", flush=True)
def main():
# Only process the tasks quietly!
task_ids = ["easy_01", "easy_02", "medium_01", "medium_02", "hard_01", "hard_02"]
for tid in task_ids:
run_episode(tid)
if __name__ == "__main__":
main()