""" Parallel Baseline Script ======================== Runs all 4 tasks SIMULTANEOUSLY using concurrent.futures.ThreadPoolExecutor. Each task gets its own thread + its own session_id — fully isolated. Wall-clock time = time of slowest task (not sum of all tasks). On a 4-task run: ~3x faster than sequential. Usage: # Terminal 1: uvicorn server.app:app --host 0.0.0.0 --port 7860 # Terminal 2: $env:OPENAI_API_KEY = "gsk_..." $env:OPENAI_BASE_URL = "https://api.groq.com/openai/v1" $env:BASELINE_MODEL = "llama-3.3-70b-versatile" python baseline.py """ import os, json, time, sys import requests from openai import OpenAI from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Dict, Tuple # ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860") # API_KEY = os.environ["API_KEY"] # API_BASE_URL = os.environ["API_BASE_URL"] # MODEL_NAME = os.environ["MODEL_NAME"] ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860") API_KEY = os.environ.get("API_KEY") or os.environ.get("HF_TOKEN") or os.environ.get("HFTOKEN") API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1") MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini") # Each task runs in its own thread with its own OpenAI client (thread-safe) def _make_client(): return OpenAI(api_key=API_KEY, base_url=API_BASE_URL) TASK_MAX_STEPS = { "task1": 10, "task2": 20, "task3": 30, "task4_data_drift": 40, } SYSTEM_PROMPT = """You are an expert data cleaning agent. Respond ONLY with valid JSON — no prose, no markdown. Operations: fill_nulls: {"operation":"fill_nulls","column":"","strategy":"mean|median|mode|constant|forward_fill|backward_fill","table_name":""} cast_column: {"operation":"cast_column","column":"","dtype":"int|float|str|datetime","table_name":""} remove_duplicates: {"operation":"remove_duplicates","table_name":""} normalize_values: {"operation":"normalize_values","column":"","method":"upper|lower|regex","table_name":""} filter_outliers: {"operation":"filter_outliers","column":"","method":"iqr|zscore","threshold":1.5,"table_name":""} merge_tables: {"operation":"merge_tables","left_table":"orders","right_table":"customers","on":"customer_id","output_table":"merged"} add_derived_column: {"operation":"add_derived_column","column_name":"order_year","source_column":"order_date","transform":"year_from_date","table_name":"merged"} submit: {"operation":"submit"} Task strategies: task1: fill_nulls(age,median,main)→cast_column(age,int,main)→fill_nulls(salary,mean,main)→submit task2: remove_duplicates(main)→normalize_values(country,upper,main)→cast_column(order_date,datetime,main)→fill_nulls(amount,mean,main)→submit task3: merge_tables(orders,customers,customer_id)→fill_nulls(age,median,merged)→cast_column(age,int,merged)→filter_outliers(amount,iqr,1.5,merged)→add_derived_column(order_year,order_date,year_from_date,merged)→submit 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)→[repeat after each drift injection]→submit IMPORTANT for task4: New dirty rows are injected every 5 steps automatically. After each drift injection (shown in message), re-run your cleaning ops on 'stream'. """ def _build_prompt(obs: dict, task_id: str) -> str: # For task4 show drift-specific info drift_note = "" if task_id == "task4_data_drift": drift_note = f"\nDRIFT TABLE ROW COUNT: {obs.get('row_count',{}).get('stream','?')}" drift_note += f"\n[Watch message for drift injections — re-clean after each one]" return ( f"Task: {obs['task_id']}\n" f"Step: {obs['step_count']}/{obs['max_steps']}\n" f"Score: {obs['partial_score']:.4f}\n" f"Last message: {obs['message']}\n" f"Schema errors: {obs['schema_errors'][:6]}\n" f"Column dtypes: {json.dumps(obs['column_dtypes'])}\n" f"Null counts: {json.dumps(obs['null_counts'])}\n" f"Duplicate counts: {obs['duplicate_count']}\n" f"Row counts: {obs['row_count']}\n" f"Available ops: {obs['available_operations']}" f"{drift_note}\n\nNext action JSON:" ) def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]: """ Run one full episode for task_id. Returns (task_id, final_score, elapsed_seconds). Each call is self-contained — uses its own session_id and OpenAI client. """ session_id = f"baseline_{task_id}" client = _make_client() t0 = time.time() max_steps = TASK_MAX_STEPS[task_id] # Reset resp = requests.post( f"{ENV_URL}/reset", json={"task_id": task_id, "seed": seed, "session_id": session_id}, timeout=20, ) resp.raise_for_status() obs = resp.json() done = obs.get("done", False) print(f" [{task_id}] started | score={obs['partial_score']:.3f} | " f"tables={list(obs['column_dtypes'].keys())}") for step_num in range(max_steps): if done: break prompt = _build_prompt(obs, task_id) try: response = client.chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}, ], temperature=0.0, max_tokens=300, ) raw = response.choices[0].message.content.strip() if "```" in raw: raw = raw.split("```")[1] if raw.startswith("json"): raw = raw[4:] action = json.loads(raw) except Exception as e: print(f" [{task_id}] LLM error step {step_num+1}: {e} — submitting") action = {"operation": "submit"} step_resp = requests.post( f"{ENV_URL}/step?session_id={session_id}", json=action, timeout=20, ) step_resp.raise_for_status() data = step_resp.json() obs = data["observation"] done = data["done"] print(f" [{task_id}] step {step_num+1:2d} | op={action.get('operation'):22s} | " f"reward={data['reward']:+.4f} | score={obs['partial_score']:.4f}") time.sleep(0.5) # light rate-limit buffer elapsed = round(time.time() - t0, 2) score = float(obs.get("partial_score", 0.0)) print(f" [{task_id}] DONE | score={score:.4f} | {elapsed}s") return task_id, score, elapsed def run_baseline_parallel(seed: int = 42) -> Dict[str, float]: """ Run all tasks IN PARALLEL using ThreadPoolExecutor. Wall-clock time = slowest task, not sum of all tasks. """ if not API_KEY: print("ERROR: OPENAI_API_KEY not set"); sys.exit(1) try: h = requests.get(f"{ENV_URL}/health", timeout=5) print(f"Server: {h.json()}") except Exception as e: print(f"ERROR: Cannot reach {ENV_URL}: {e}"); sys.exit(1) tasks = list(TASK_MAX_STEPS.keys()) print(f"\n{'='*60}") print(f" DataClean Parallel Baseline") print(f" model={MODEL} | seed={seed} | tasks={tasks}") print(f" Running {len(tasks)} tasks SIMULTANEOUSLY (ThreadPoolExecutor)") print(f"{'='*60}\n") t_total = time.time() scores: Dict[str, float] = {} elapsed: Dict[str, float] = {} # All tasks run in parallel — each thread is fully independent with ThreadPoolExecutor(max_workers=len(tasks)) as pool: futures = { pool.submit(run_episode, task_id, seed): task_id for task_id in tasks } for future in as_completed(futures): task_id = futures[future] try: tid, score, secs = future.result() scores[tid] = round(score, 4) elapsed[tid] = secs except Exception as exc: print(f" [{task_id}] FAILED: {exc}") scores[task_id] = 0.001 elapsed[task_id] = -1.0 wall_time = round(time.time() - t_total, 2) mean = round(sum(scores.values()) / len(scores), 4) print(f"\n{'='*60}") print(f" RESULTS (wall time: {wall_time}s)") print(f"{'='*60}") for k, v in scores.items(): bar = "#" * int(v * 25) diff = elapsed.get(k, 0) print(f" {k:<25} {v:.4f} {bar:<25} ({diff}s)") print(f" {'mean':<25} {mean:.4f}") print(f"{'='*60}\n") print(json.dumps({**scores, "mean": mean, "wall_time_seconds": wall_time}, indent=2)) return scores if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--seed", type=int, default=42) parser.add_argument("--url", default="http://localhost:7860") args = parser.parse_args() ENV_URL = args.url run_baseline_parallel(seed=args.seed)