Spaces:
Sleeping
Sleeping
File size: 12,068 Bytes
0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 7b2be87 0d7c387 4e45b0f 8a33787 4e45b0f 0d7c387 4e45b0f 8a33787 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 7b2be87 2f52c9b 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 4e45b0f 0d7c387 2f52c9b 0d7c387 4e45b0f 2f52c9b 0d7c387 4e45b0f a6128af 0d7c387 4e45b0f 0d7c387 7b2be87 0d7c387 2f52c9b 0d7c387 7b2be87 0d7c387 2f52c9b 0d7c387 a6128af 2f52c9b 0d7c387 4e45b0f 0d7c387 2f52c9b 0d7c387 8a33787 0d7c387 12d8c76 0d7c387 4e45b0f | 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 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | """
Inference Script — CSV Cleaner Environment
=============================================
Baseline agent using OpenAI client to clean CSV datasets across 3 tasks.
MANDATORY ENV VARS:
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.
IMAGE_NAME Docker image name (if using from_docker_image)
STDOUT FORMAT:
[START] task=<task_name> env=<benchmark> model=<model_name>
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
"""
import asyncio
import json
import os
import textwrap
from typing import Any, Dict, List, Optional
from openai import OpenAI
from openenv.core.env_server.mcp_types import CallToolAction
from csv_cleaner_env import CsvCleanerEnv
IMAGE_NAME = os.getenv("IMAGE_NAME")
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") # default allowed
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
API_KEY = os.environ.get("API_KEY", os.environ.get("HF_TOKEN", "dummy_key"))
BENCHMARK = os.getenv("CSV_CLEANER_BENCHMARK", "csv_cleaner_env")
TEMPERATURE = 0.3
MAX_TOKENS = 300
# Debug print to confirm env vars are loaded
print(f"[CONFIG] API_BASE_URL={API_BASE_URL} MODEL={MODEL_NAME} API_KEY={'SET' if API_KEY != 'dummy_key' else 'NOT SET'}", flush=True)
# Task configurations
TASKS = [
{"name": "fix_column_types", "max_steps": 10},
{"name": "clean_missing_duplicates", "max_steps": 15},
{"name": "full_pipeline", "max_steps": 20},
]
SYSTEM_PROMPT = textwrap.dedent("""
You are a data cleaning agent. You interact with a CSV dataset through structured tool calls.
Available tools:
- get_dataset_info(): See current columns, types, null counts, samples
- rename_column(old_name, new_name): Rename a column
- cast_column(column, dtype): Cast column to int/float/str/datetime
- fill_missing(column, strategy, value): Fill nulls. strategy: mean/median/mode/constant/zero
- drop_missing(column): Drop rows with nulls (empty string for all columns)
- drop_duplicates(columns): Remove duplicates (empty string for all columns)
- filter_rows(column, operator, value): Filter rows. operator: ==/!=/>/</contains
- strip_whitespace(column): Strip whitespace from string column
- replace_values(column, old_value, new_value): Replace values in column
You must respond with EXACTLY ONE tool call per turn as a JSON object:
{"tool": "<tool_name>", "args": {"param1": "value1", ...}}
Read the task description carefully and execute the cleaning steps one at a time.
Start by calling get_dataset_info to understand the current state, then fix issues.
""").strip()
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:
error_val = error if error else "null"
done_val = str(done).lower()
print(
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
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)
print(
f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}",
flush=True,
)
def parse_tool_call(text: str) -> Optional[Dict[str, Any]]:
"""Extract JSON tool call from model response."""
text = text.strip()
for start_char in ["{"]:
start = text.find(start_char)
if start == -1:
continue
depth = 0
for i in range(start, len(text)):
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
if depth == 0:
try:
return json.loads(text[start : i + 1])
except json.JSONDecodeError:
continue
return None
def normalize_tool_call(tool_call: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
"""Normalize model output into a safe tool name + args payload."""
tool_name = tool_call.get("tool", "get_dataset_info")
tool_args = tool_call.get("args", {})
if not isinstance(tool_args, dict):
tool_args = {}
# Model outputs sometimes include nulls for string fields; FastMCP rejects None for str args.
normalized_args: Dict[str, Any] = {}
for key, value in tool_args.items():
normalized_args[key] = "" if value is None else value
return tool_name, normalized_args
def parse_dataset_snapshot(result_str: str) -> Optional[Dict[str, Any]]:
"""Parse dataset info payload when a tool returns JSON snapshot text."""
try:
payload = json.loads(result_str)
except Exception:
return None
if isinstance(payload, dict) and "columns" in payload:
return payload
return None
def get_model_response(
client: OpenAI,
task_desc: str,
dataset_info: str,
last_result: str,
step: int,
history: List[str],
) -> Optional[Dict[str, Any]]:
"""Get next tool call from the model."""
history_block = "\n".join(history[-6:]) if history else "None"
user_prompt = textwrap.dedent(f"""
Task: {task_desc}
Current Step: {step}
Last Action Result: {last_result}
Current Dataset State:
{dataset_info}
Previous Actions:
{history_block}
Respond with your next tool call as JSON: {{"tool": "tool_name", "args": {{...}}}}
""").strip()
try:
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
stream=False,
)
text = (completion.choices[0].message.content or "").strip()
return parse_tool_call(text)
except Exception as exc:
error_text = str(exc)
print(f"[DEBUG] Model request failed: {error_text}", flush=True)
# Stop the task early when provider quota is exhausted instead of
# repeatedly falling back to no-op tool calls.
if "Error code: 402" in error_text or "depleted your monthly included credits" in error_text:
return {"tool": "__quota_exhausted__", "args": {}}
return None
async def run_task(client: OpenAI, env: CsvCleanerEnv, task_config: Dict) -> None:
"""Run a single task and log stdout."""
task_name = task_config["name"]
max_steps = task_config["max_steps"]
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
rewards: List[float] = []
steps_taken: int = 0
score: float = 0.0
success: bool = False
done: bool = False
try:
result = await env.reset(task=task_name)
obs_metadata = getattr(result.observation, "metadata", getattr(result, "metadata", {})) or {}
task_desc = obs_metadata.get("task_description", task_name)
dataset_info = json.dumps(obs_metadata.get("columns", []), indent=2)
last_result = obs_metadata.get("last_action_result", "Ready")
history: List[str] = []
for step in range(1, max_steps + 1):
if result.done:
break
# First step: always get dataset info
if step == 1:
tool_call = {"tool": "get_dataset_info", "args": {}}
else:
tool_call = get_model_response(
client, task_desc, dataset_info, last_result, step, history
)
if tool_call is None:
tool_call = {"tool": "get_dataset_info", "args": {}}
tool_name, tool_args = normalize_tool_call(tool_call)
if tool_name == "__quota_exhausted__":
print("[DEBUG] Stopping task early due to provider quota exhaustion", flush=True)
break
try:
action = CallToolAction(tool_name=tool_name, arguments=tool_args)
result = await env.step(action)
obs = result.observation
obs_error = getattr(obs, "error", None)
if obs_error is not None:
result_str = f"Error: {getattr(obs_error, 'message', str(obs_error))}"
else:
obs_result = getattr(obs, "result", None)
if hasattr(obs_result, "data"):
obs_result = obs_result.data
elif isinstance(obs_result, dict) and "data" in obs_result:
obs_result = obs_result["data"]
result_str = str(obs_result) if obs_result is not None else ""
except Exception as e:
result_str = f"Error: {e}"
reward = result.reward if hasattr(result, "reward") and result.reward else 0.0
done = result.done if hasattr(result, "done") else False
obs_metadata = getattr(result.observation, "metadata", getattr(result, "metadata", {})) or {}
snapshot = parse_dataset_snapshot(result_str)
state_payload: Dict[str, Any] = {}
if isinstance(obs_metadata, dict):
state_payload.update(obs_metadata)
if isinstance(snapshot, dict):
state_payload.update(snapshot)
if state_payload:
progress = state_payload.get("progress")
if isinstance(progress, (int, float)):
score = float(progress)
columns = state_payload.get("columns")
if isinstance(columns, list):
dataset_info = json.dumps(columns, indent=2)
task_desc = state_payload.get("task_description", task_desc)
last_result = state_payload.get("last_action_result", result_str)
else:
last_result = result_str
rewards.append(reward)
steps_taken = step
action_str = f"{tool_name}({json.dumps(tool_args)})"
log_step(step=step, action=action_str, reward=reward, done=done, error=None)
history.append(f"Step {step}: {action_str} -> {last_result[:100]}")
if done:
break
if score == 0.0 and rewards:
# Fallback when progress metadata is unavailable from the client payload.
score = min(1.0, max(0.0, sum(rewards)))
score = min(max(score, 0.0), 1.0)
# Environment marks done=True on success or step-limit. If it ended before max steps,
# that's a reliable success signal even when explicit progress metadata is absent.
success = (done and steps_taken < max_steps) or score >= 0.95
except Exception as e:
print(f"[DEBUG] Task error: {e}", flush=True)
finally:
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
async def main() -> None:
# Use API_KEY as the API key — injected by the hackathon validator
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
if IMAGE_NAME:
env = await CsvCleanerEnv.from_docker_image(IMAGE_NAME)
else:
# Since uvicorn is already running on port 8000 inside the HF Space container, connect locally
env = CsvCleanerEnv(base_url="http://localhost:8000")
await env.connect()
try:
for task_config in TASKS:
await run_task(client, env, task_config)
finally:
try:
await env.close()
except Exception as e:
print(f"[DEBUG] env.close() error: {e}", flush=True)
if __name__ == "__main__":
asyncio.run(main()) |