Spaces:
Sleeping
Sleeping
Mishika Sardana commited on
Refactor inference.py for improved validation and error handling
Browse filesRefactor inference.py to improve task validation and error handling. Implemented score clamping and server readiness checks, ensuring consistent output format.
- inference.py +107 -282
inference.py
CHANGED
|
@@ -1,122 +1,26 @@
|
|
| 1 |
"""
|
| 2 |
DataClean OpenEnv β inference.py
|
| 3 |
Required output format: [START] / [STEP] / [END] structured blocks.
|
| 4 |
-
|
| 5 |
-
FIX SUMMARY (Phase 2 Task Validation failure):
|
| 6 |
-
ROOT CAUSE: sys.exit(1) when HF_TOKEN is missing causes NO [END] lines to be
|
| 7 |
-
printed. The validator then defaults missing task scores to 0.0, which is NOT
|
| 8 |
-
strictly in (0, 1) β Task Validation fails even though Output Parsing passes.
|
| 9 |
-
|
| 10 |
-
FIX 1: Removed sys.exit(1). When HF_TOKEN/LLM is unavailable, inference.py
|
| 11 |
-
falls back to a deterministic rule-based agent that achieves 0.93+ without
|
| 12 |
-
any external API. [END] lines are ALWAYS printed for all 4 tasks.
|
| 13 |
-
|
| 14 |
-
FIX 2: Triple-layer score clamping: server clamps, inference clamps, log_end
|
| 15 |
-
clamps β ensuring score is never exactly 0.0 or 1.0 at any output point.
|
| 16 |
-
|
| 17 |
-
FIX 3: Added server readiness wait loop β if the Space just woke up, requests
|
| 18 |
-
retry for up to 60s instead of failing instantly with a connection error.
|
| 19 |
-
|
| 20 |
-
Env vars:
|
| 21 |
-
ENV_URL β environment base URL (default: http://localhost:7860)
|
| 22 |
-
HF_TOKEN β LLM API key (optional; falls back to rule-based if missing)
|
| 23 |
-
API_BASE_URL β LLM base URL (default: https://api.openai.com/v1)
|
| 24 |
-
MODEL_NAME β LLM model name (default: gpt-4o-mini)
|
| 25 |
"""
|
| 26 |
import os, json, time, sys
|
| 27 |
import requests
|
|
|
|
| 28 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 29 |
-
from typing import Dict,
|
| 30 |
|
| 31 |
-
ENV_URL = os.environ.get("ENV_URL",
|
| 32 |
-
API_KEY = os.environ.get("HF_TOKEN",
|
| 33 |
-
BASE_URL = os.environ.get("API_BASE_URL",
|
| 34 |
-
MODEL = os.environ.get("MODEL_NAME",
|
| 35 |
|
| 36 |
TASK_MAX_STEPS = {
|
| 37 |
-
"task1":
|
| 38 |
-
"task2":
|
| 39 |
-
"task3":
|
| 40 |
"task4_data_drift": 40,
|
| 41 |
}
|
| 42 |
|
| 43 |
-
# ββ Deterministic rule-based strategies (no LLM needed) ββββββββββββββββββββββ
|
| 44 |
-
# Each strategy is a list of actions to execute in order.
|
| 45 |
-
# The agent follows these regardless of LLM availability.
|
| 46 |
-
# These are the exact sequences that achieve 0.93+ baseline scores.
|
| 47 |
-
|
| 48 |
-
RULE_STRATEGIES: Dict[str, list] = {
|
| 49 |
-
"task1": [
|
| 50 |
-
{"operation": "fill_nulls", "column": "age", "strategy": "median", "table_name": "main"},
|
| 51 |
-
{"operation": "cast_column", "column": "age", "dtype": "int", "table_name": "main"},
|
| 52 |
-
{"operation": "fill_nulls", "column": "salary", "strategy": "mean", "table_name": "main"},
|
| 53 |
-
{"operation": "submit"},
|
| 54 |
-
],
|
| 55 |
-
"task2": [
|
| 56 |
-
{"operation": "remove_duplicates", "table_name": "main"},
|
| 57 |
-
{"operation": "normalize_values", "column": "country", "method": "upper", "table_name": "main"},
|
| 58 |
-
{"operation": "cast_column", "column": "order_date", "dtype": "datetime", "table_name": "main"},
|
| 59 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "main"},
|
| 60 |
-
{"operation": "submit"},
|
| 61 |
-
],
|
| 62 |
-
"task3": [
|
| 63 |
-
{"operation": "merge_tables", "left_table": "orders", "right_table": "customers",
|
| 64 |
-
"on": "customer_id", "output_table": "merged"},
|
| 65 |
-
{"operation": "fill_nulls", "column": "age", "strategy": "median", "table_name": "merged"},
|
| 66 |
-
{"operation": "cast_column", "column": "age", "dtype": "int", "table_name": "merged"},
|
| 67 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5,
|
| 68 |
-
"table_name": "merged"},
|
| 69 |
-
{"operation": "add_derived_column", "column_name": "order_year", "source_column": "order_date",
|
| 70 |
-
"transform": "year_from_date", "table_name": "merged"},
|
| 71 |
-
{"operation": "submit"},
|
| 72 |
-
],
|
| 73 |
-
"task4_data_drift": [
|
| 74 |
-
# Cleaning cycle β repeated after every drift injection (every 5 steps)
|
| 75 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 76 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 77 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 78 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 79 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 80 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 81 |
-
# Drift injected at step 5, 10, 15, 20, 25, 30, 35 β re-clean each time
|
| 82 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 83 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 84 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 85 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 86 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 87 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 88 |
-
# Cycle 3
|
| 89 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 90 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 91 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 92 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 93 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 94 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 95 |
-
# Cycle 4
|
| 96 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 97 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 98 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 99 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 100 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 101 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 102 |
-
# Cycle 5
|
| 103 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 104 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 105 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 106 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 107 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 108 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 109 |
-
# Cycle 6
|
| 110 |
-
{"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
|
| 111 |
-
{"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
|
| 112 |
-
{"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
|
| 113 |
-
{"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
|
| 114 |
-
{"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
|
| 115 |
-
{"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
|
| 116 |
-
{"operation": "submit"},
|
| 117 |
-
],
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
SYSTEM_PROMPT = """You are an expert data cleaning agent. Respond ONLY with valid JSON β no prose, no markdown.
|
| 121 |
|
| 122 |
Operations:
|
|
@@ -136,146 +40,73 @@ Task strategies:
|
|
| 136 |
task4_data_drift: filter_outliers(amount,iqr,1.5,stream)->fill_nulls(amount,mean,stream)->cast_column(amount,float,stream)->fill_nulls(category,mode,stream)->fill_nulls(region,mode,stream)->cast_column(event_ts,datetime,stream)->submit
|
| 137 |
"""
|
| 138 |
|
| 139 |
-
# ββ LLM client (optional) βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
|
| 141 |
-
|
| 142 |
|
| 143 |
-
def
|
| 144 |
-
"""
|
| 145 |
-
global _llm_available
|
| 146 |
-
if _llm_available is not None:
|
| 147 |
-
return _llm_available
|
| 148 |
-
if not API_KEY:
|
| 149 |
-
print("[INFO] HF_TOKEN not set β using rule-based fallback agent", flush=True)
|
| 150 |
-
_llm_available = False
|
| 151 |
-
return False
|
| 152 |
try:
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
messages=[{"role": "user", "content": "ping"}],
|
| 158 |
-
max_tokens=1,
|
| 159 |
-
timeout=10,
|
| 160 |
-
)
|
| 161 |
-
_llm_available = True
|
| 162 |
-
print(f"[INFO] LLM available: {MODEL} @ {BASE_URL}", flush=True)
|
| 163 |
-
except Exception as e:
|
| 164 |
-
print(f"[INFO] LLM unavailable ({e}) β using rule-based fallback agent", flush=True)
|
| 165 |
-
_llm_available = False
|
| 166 |
-
return _llm_available
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
def _llm_action(obs: dict, task_id: str) -> Optional[dict]:
|
| 170 |
-
"""Get action from LLM. Returns None if LLM fails."""
|
| 171 |
-
if not _test_llm():
|
| 172 |
-
return None
|
| 173 |
-
try:
|
| 174 |
-
from openai import OpenAI
|
| 175 |
-
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
|
| 176 |
-
drift_note = ""
|
| 177 |
-
if task_id == "task4_data_drift":
|
| 178 |
-
drift_note = f"\nSTREAM ROW COUNT: {obs.get('row_count',{}).get('stream','?')}"
|
| 179 |
-
prompt = (
|
| 180 |
-
f"Task: {obs['task_id']}\n"
|
| 181 |
-
f"Step: {obs['step_count']}/{obs['max_steps']}\n"
|
| 182 |
-
f"Score: {obs['partial_score']:.4f}\n"
|
| 183 |
-
f"Last message: {obs['message']}\n"
|
| 184 |
-
f"Schema errors: {obs['schema_errors'][:5]}\n"
|
| 185 |
-
f"Column dtypes: {json.dumps(obs['column_dtypes'])}\n"
|
| 186 |
-
f"Null counts: {json.dumps(obs['null_counts'])}\n"
|
| 187 |
-
f"Duplicate counts: {obs['duplicate_count']}\n"
|
| 188 |
-
f"Row counts: {obs['row_count']}\n"
|
| 189 |
-
f"Available ops: {obs['available_operations']}"
|
| 190 |
-
f"{drift_note}\n\nNext action JSON:"
|
| 191 |
-
)
|
| 192 |
-
response = client.chat.completions.create(
|
| 193 |
-
model=MODEL,
|
| 194 |
-
messages=[
|
| 195 |
-
{"role": "system", "content": SYSTEM_PROMPT},
|
| 196 |
-
{"role": "user", "content": prompt},
|
| 197 |
-
],
|
| 198 |
-
temperature=0.0,
|
| 199 |
-
max_tokens=300,
|
| 200 |
-
)
|
| 201 |
-
raw = response.choices[0].message.content.strip()
|
| 202 |
-
if "```" in raw:
|
| 203 |
-
raw = raw.split("```")[1]
|
| 204 |
-
if raw.startswith("json"):
|
| 205 |
-
raw = raw[4:]
|
| 206 |
-
return json.loads(raw)
|
| 207 |
except Exception:
|
| 208 |
-
return
|
| 209 |
|
| 210 |
|
| 211 |
-
# ββ Required structured output βββββββββββββββββββββββββββββββββββββββ
|
| 212 |
|
| 213 |
def log_start(task_id: str):
|
| 214 |
print(f"[START] task={task_id}", flush=True)
|
| 215 |
|
| 216 |
-
|
| 217 |
def log_step(step: int, action: str, reward: float, done: bool, error=None):
|
| 218 |
error_val = error if error else "null"
|
| 219 |
-
print(
|
| 220 |
-
f"[STEP] step={step} action={action} reward={reward:.4f} "
|
| 221 |
-
f"done={str(done).lower()} error={error_val}",
|
| 222 |
-
flush=True,
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
|
| 226 |
def log_end(task_id: str, score: float, steps: int, success: bool):
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
)
|
| 234 |
|
| 235 |
|
| 236 |
-
# ββ Server readiness check ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
-
|
| 238 |
-
def _wait_for_server(timeout: int = 90) -> bool:
|
| 239 |
-
"""
|
| 240 |
-
Poll /health until the server responds or timeout is reached.
|
| 241 |
-
Handles the case where the HF Space is still waking up.
|
| 242 |
-
"""
|
| 243 |
-
deadline = time.time() + timeout
|
| 244 |
-
while time.time() < deadline:
|
| 245 |
-
try:
|
| 246 |
-
r = requests.get(f"{ENV_URL}/health", timeout=10)
|
| 247 |
-
if r.status_code == 200:
|
| 248 |
-
data = r.json()
|
| 249 |
-
# Make sure it's actually our FastAPI JSON, not HF HTML
|
| 250 |
-
if isinstance(data, dict) and "status" in data:
|
| 251 |
-
print(f"[INFO] Server ready: {data}", flush=True)
|
| 252 |
-
return True
|
| 253 |
-
except Exception:
|
| 254 |
-
pass
|
| 255 |
-
time.sleep(5)
|
| 256 |
-
return False
|
| 257 |
-
|
| 258 |
-
|
| 259 |
# ββ Episode runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 260 |
|
| 261 |
def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
|
| 262 |
-
|
| 263 |
-
Run one full episode for task_id.
|
| 264 |
-
Uses LLM if available, falls back to deterministic rule-based agent.
|
| 265 |
-
ALWAYS prints [START], [STEP]s, and [END] β never exits early.
|
| 266 |
-
Returns (task_id, final_score, elapsed_seconds).
|
| 267 |
-
"""
|
| 268 |
session_id = f"inference_{task_id}_{seed}"
|
|
|
|
| 269 |
t0 = time.time()
|
| 270 |
max_steps = TASK_MAX_STEPS[task_id]
|
| 271 |
step_num = 0
|
| 272 |
-
score = 0.
|
| 273 |
|
| 274 |
log_start(task_id)
|
| 275 |
|
| 276 |
-
#
|
| 277 |
-
obs = None
|
| 278 |
-
done = False
|
| 279 |
try:
|
| 280 |
resp = requests.post(
|
| 281 |
f"{ENV_URL}/reset",
|
|
@@ -285,41 +116,45 @@ def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
|
|
| 285 |
resp.raise_for_status()
|
| 286 |
obs = resp.json()
|
| 287 |
done = obs.get("done", False)
|
| 288 |
-
raw_score = float(obs.get("partial_score", 0.0010))
|
| 289 |
-
score = round(min(0.9990, max(0.0010, raw_score)), 4)
|
| 290 |
except Exception as e:
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
return task_id, score, round(time.time() - t0, 2)
|
| 294 |
-
|
| 295 |
-
# ββ Rule-based strategy index βββββββββββββββββββββββββββββββββββββββββββββ
|
| 296 |
-
rule_actions = RULE_STRATEGIES.get(task_id, [{"operation": "submit"}])
|
| 297 |
-
rule_idx = 0
|
| 298 |
|
| 299 |
-
#
|
| 300 |
for step_num in range(1, max_steps + 1):
|
| 301 |
if done:
|
| 302 |
break
|
| 303 |
|
| 304 |
-
#
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
| 306 |
action_str = "submit"
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
reward = 0.0
|
| 324 |
try:
|
| 325 |
step_resp = requests.post(
|
|
@@ -332,50 +167,45 @@ def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
|
|
| 332 |
obs = data["observation"]
|
| 333 |
done = data["done"]
|
| 334 |
reward = float(data.get("reward", 0.0))
|
| 335 |
-
|
| 336 |
-
|
|
|
|
| 337 |
except Exception as e:
|
| 338 |
error_msg = str(e)[:80]
|
| 339 |
done = True
|
| 340 |
|
| 341 |
log_step(step_num, action_str, reward, done, error_msg)
|
| 342 |
-
time.sleep(0.
|
| 343 |
|
| 344 |
-
#
|
|
|
|
| 345 |
if step_num == 0:
|
| 346 |
step_num = 1
|
| 347 |
|
| 348 |
-
|
| 349 |
-
score = round(min(0.9990, max(0.0010, score)), 4)
|
| 350 |
success = score >= 0.5
|
| 351 |
log_end(task_id, score, step_num, success)
|
| 352 |
return task_id, score, round(time.time() - t0, 2)
|
| 353 |
|
| 354 |
|
| 355 |
-
# ββ Main βββββββββββββββββββββββββββββββββββββββββ
|
| 356 |
|
| 357 |
def main():
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
print(json.dumps({**scores, "mean": mean}, indent=2), flush=True)
|
| 369 |
-
return
|
| 370 |
-
|
| 371 |
-
# Test LLM once (caches result globally)
|
| 372 |
-
_test_llm()
|
| 373 |
|
| 374 |
tasks = list(TASK_MAX_STEPS.keys())
|
| 375 |
scores: Dict[str, float] = {}
|
| 376 |
elapsed: Dict[str, float] = {}
|
| 377 |
|
| 378 |
-
# All 4 tasks run in parallel β ThreadPoolExecutor, each with own session_id
|
| 379 |
with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
|
| 380 |
futures = {
|
| 381 |
pool.submit(run_episode, task_id, 42): task_id
|
|
@@ -385,22 +215,17 @@ def main():
|
|
| 385 |
task_id = futures[future]
|
| 386 |
try:
|
| 387 |
tid, score, secs = future.result()
|
| 388 |
-
scores[tid] =
|
| 389 |
elapsed[tid] = secs
|
| 390 |
except Exception as exc:
|
| 391 |
-
|
| 392 |
-
# Even on exception, print [END] with safe score
|
| 393 |
-
log_end(task_id, 0.0010, 0, False)
|
| 394 |
-
scores[task_id] = 0.0010
|
| 395 |
elapsed[task_id] = -1.0
|
|
|
|
| 396 |
|
| 397 |
-
mean = round(
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
summary = {**scores, "mean": mean, "elapsed_seconds": elapsed}
|
| 402 |
-
print(json.dumps(summary, indent=2), flush=True)
|
| 403 |
|
| 404 |
|
| 405 |
if __name__ == "__main__":
|
| 406 |
-
main()
|
|
|
|
| 1 |
"""
|
| 2 |
DataClean OpenEnv β inference.py
|
| 3 |
Required output format: [START] / [STEP] / [END] structured blocks.
|
| 4 |
+
Uses: HF_TOKEN, API_BASE_URL, MODEL_NAME env vars.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
import os, json, time, sys
|
| 7 |
import requests
|
| 8 |
+
from openai import OpenAI
|
| 9 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 10 |
+
from typing import Dict, Tuple
|
| 11 |
|
| 12 |
+
ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860")
|
| 13 |
+
API_KEY = os.environ.get("HF_TOKEN", "")
|
| 14 |
+
BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
|
| 15 |
+
MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini")
|
| 16 |
|
| 17 |
TASK_MAX_STEPS = {
|
| 18 |
+
"task1": 10,
|
| 19 |
+
"task2": 20,
|
| 20 |
+
"task3": 30,
|
| 21 |
"task4_data_drift": 40,
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
SYSTEM_PROMPT = """You are an expert data cleaning agent. Respond ONLY with valid JSON β no prose, no markdown.
|
| 25 |
|
| 26 |
Operations:
|
|
|
|
| 40 |
task4_data_drift: filter_outliers(amount,iqr,1.5,stream)->fill_nulls(amount,mean,stream)->cast_column(amount,float,stream)->fill_nulls(category,mode,stream)->fill_nulls(region,mode,stream)->cast_column(event_ts,datetime,stream)->submit
|
| 41 |
"""
|
| 42 |
|
|
|
|
| 43 |
|
| 44 |
+
# ββ Score contract helper βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
|
| 46 |
+
def _clamp_score(v) -> float:
|
| 47 |
+
"""Enforce 0.001β0.999 at the inference layer as a last-resort safety net."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
+
f = float(v)
|
| 50 |
+
if not (f == f): # NaN check
|
| 51 |
+
f = 0.001
|
| 52 |
+
return round(min(0.999, max(0.001, f)), 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
except Exception:
|
| 54 |
+
return 0.001
|
| 55 |
|
| 56 |
|
| 57 |
+
# ββ Required structured output helpers βββββββββββββββββββββββββββββββββββββββ
|
| 58 |
|
| 59 |
def log_start(task_id: str):
|
| 60 |
print(f"[START] task={task_id}", flush=True)
|
| 61 |
|
|
|
|
| 62 |
def log_step(step: int, action: str, reward: float, done: bool, error=None):
|
| 63 |
error_val = error if error else "null"
|
| 64 |
+
print(f"[STEP] step={step} action={action} reward={reward:.4f} done={str(done).lower()} error={error_val}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
def log_end(task_id: str, score: float, steps: int, success: bool):
|
| 67 |
+
clipped = _clamp_score(score)
|
| 68 |
+
print(f"[END] task={task_id} score={clipped:.4f} steps={steps} success={str(success).lower()}", flush=True)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ββ LLM client βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
|
| 73 |
+
def _make_client():
|
| 74 |
+
return OpenAI(api_key=API_KEY, base_url=BASE_URL)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _build_prompt(obs: dict, task_id: str) -> str:
|
| 78 |
+
drift_note = ""
|
| 79 |
+
if task_id == "task4_data_drift":
|
| 80 |
+
drift_note = f"\nSTREAM ROW COUNT: {obs.get('row_count',{}).get('stream','?')}"
|
| 81 |
+
return (
|
| 82 |
+
f"Task: {obs['task_id']}\n"
|
| 83 |
+
f"Step: {obs['step_count']}/{obs['max_steps']}\n"
|
| 84 |
+
f"Score: {obs['partial_score']:.4f}\n"
|
| 85 |
+
f"Last message: {obs['message']}\n"
|
| 86 |
+
f"Schema errors: {obs['schema_errors'][:5]}\n"
|
| 87 |
+
f"Column dtypes: {json.dumps(obs['column_dtypes'])}\n"
|
| 88 |
+
f"Null counts: {json.dumps(obs['null_counts'])}\n"
|
| 89 |
+
f"Duplicate counts: {obs['duplicate_count']}\n"
|
| 90 |
+
f"Row counts: {obs['row_count']}\n"
|
| 91 |
+
f"Available ops: {obs['available_operations']}"
|
| 92 |
+
f"{drift_note}\n\nNext action JSON:"
|
| 93 |
)
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# ββ Episode runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 97 |
|
| 98 |
def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
|
| 99 |
+
# FIX: unique session_id per task+seed prevents collision in parallel runs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
session_id = f"inference_{task_id}_{seed}"
|
| 101 |
+
client = _make_client()
|
| 102 |
t0 = time.time()
|
| 103 |
max_steps = TASK_MAX_STEPS[task_id]
|
| 104 |
step_num = 0
|
| 105 |
+
score = 0.001
|
| 106 |
|
| 107 |
log_start(task_id)
|
| 108 |
|
| 109 |
+
# Reset
|
|
|
|
|
|
|
| 110 |
try:
|
| 111 |
resp = requests.post(
|
| 112 |
f"{ENV_URL}/reset",
|
|
|
|
| 116 |
resp.raise_for_status()
|
| 117 |
obs = resp.json()
|
| 118 |
done = obs.get("done", False)
|
|
|
|
|
|
|
| 119 |
except Exception as e:
|
| 120 |
+
log_end(task_id, 0.001, 0, False)
|
| 121 |
+
return task_id, 0.001, 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
# Episode loop
|
| 124 |
for step_num in range(1, max_steps + 1):
|
| 125 |
if done:
|
| 126 |
break
|
| 127 |
|
| 128 |
+
# FIX: initialise error_msg BEFORE the LLM block so a stale error
|
| 129 |
+
# from a previous LLM failure is never logged against a clean step.
|
| 130 |
+
error_msg = None
|
| 131 |
+
|
| 132 |
+
# Get action from LLM
|
| 133 |
action_str = "submit"
|
| 134 |
+
try:
|
| 135 |
+
prompt = _build_prompt(obs, task_id)
|
| 136 |
+
response = client.chat.completions.create(
|
| 137 |
+
model=MODEL,
|
| 138 |
+
messages=[
|
| 139 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 140 |
+
{"role": "user", "content": prompt},
|
| 141 |
+
],
|
| 142 |
+
temperature=0.0,
|
| 143 |
+
max_tokens=300,
|
| 144 |
+
)
|
| 145 |
+
raw = response.choices[0].message.content.strip()
|
| 146 |
+
if "```" in raw:
|
| 147 |
+
raw = raw.split("```")[1]
|
| 148 |
+
if raw.startswith("json"):
|
| 149 |
+
raw = raw[4:]
|
| 150 |
+
action = json.loads(raw)
|
| 151 |
+
action_str = action.get("operation", "submit")
|
| 152 |
+
except Exception as e:
|
| 153 |
+
action = {"operation": "submit"}
|
| 154 |
+
action_str = "submit"
|
| 155 |
+
error_msg = str(e)[:80]
|
| 156 |
+
|
| 157 |
+
# Execute step
|
| 158 |
reward = 0.0
|
| 159 |
try:
|
| 160 |
step_resp = requests.post(
|
|
|
|
| 167 |
obs = data["observation"]
|
| 168 |
done = data["done"]
|
| 169 |
reward = float(data.get("reward", 0.0))
|
| 170 |
+
# FIX: clamp at read time β server could theoretically send 0.0 or
|
| 171 |
+
# 1.0 if it's running old code; _clamp_score enforces the contract.
|
| 172 |
+
score = _clamp_score(obs.get("partial_score", 0.001))
|
| 173 |
except Exception as e:
|
| 174 |
error_msg = str(e)[:80]
|
| 175 |
done = True
|
| 176 |
|
| 177 |
log_step(step_num, action_str, reward, done, error_msg)
|
| 178 |
+
time.sleep(0.3)
|
| 179 |
|
| 180 |
+
# FIX: if done=True on first check, step_num stays at its range() init
|
| 181 |
+
# value; clamp to 1 so log_end never reports 0 steps for a started episode.
|
| 182 |
if step_num == 0:
|
| 183 |
step_num = 1
|
| 184 |
|
| 185 |
+
score = _clamp_score(score)
|
|
|
|
| 186 |
success = score >= 0.5
|
| 187 |
log_end(task_id, score, step_num, success)
|
| 188 |
return task_id, score, round(time.time() - t0, 2)
|
| 189 |
|
| 190 |
|
| 191 |
+
# ββ Main β parallel across all tasks βββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
|
| 193 |
def main():
|
| 194 |
+
if not API_KEY:
|
| 195 |
+
print("[ERROR] HF_TOKEN not set", flush=True)
|
| 196 |
+
sys.exit(1)
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
h = requests.get(f"{ENV_URL}/health", timeout=15)
|
| 200 |
+
print(f"[INFO] Server: {h.json()}", flush=True)
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"[ERROR] Cannot reach server at {ENV_URL}: {e}", flush=True)
|
| 203 |
+
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
tasks = list(TASK_MAX_STEPS.keys())
|
| 206 |
scores: Dict[str, float] = {}
|
| 207 |
elapsed: Dict[str, float] = {}
|
| 208 |
|
|
|
|
| 209 |
with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
|
| 210 |
futures = {
|
| 211 |
pool.submit(run_episode, task_id, 42): task_id
|
|
|
|
| 215 |
task_id = futures[future]
|
| 216 |
try:
|
| 217 |
tid, score, secs = future.result()
|
| 218 |
+
scores[tid] = _clamp_score(score) # FIX: clamp final scores too
|
| 219 |
elapsed[tid] = secs
|
| 220 |
except Exception as exc:
|
| 221 |
+
scores[task_id] = 0.001
|
|
|
|
|
|
|
|
|
|
| 222 |
elapsed[task_id] = -1.0
|
| 223 |
+
log_end(task_id, 0.001, 0, False)
|
| 224 |
|
| 225 |
+
mean = round(sum(scores.values()) / len(scores), 4) if scores else 0.001
|
| 226 |
+
# FIX: include elapsed_seconds in summary for diagnostics
|
| 227 |
+
print(json.dumps({**scores, "mean": mean, "elapsed_seconds": elapsed}, indent=2), flush=True)
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
|
| 230 |
if __name__ == "__main__":
|
| 231 |
+
main()
|