Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- baseline.py +0 -4
- client.py +9 -4
- inference.py +36 -24
- models.py +0 -2
- server/app.py +25 -21
- server/data_cleaning_env_environment.py +49 -42
baseline.py
CHANGED
|
@@ -62,7 +62,6 @@ RULE_POLICIES = {
|
|
| 62 |
],
|
| 63 |
}
|
| 64 |
|
| 65 |
-
|
| 66 |
def run_rule_baseline(base_url: str) -> dict[str, float]:
|
| 67 |
"""Run deterministic rule-based baseline — no LLM required."""
|
| 68 |
scores = {}
|
|
@@ -91,7 +90,6 @@ def run_rule_baseline(base_url: str) -> dict[str, float]:
|
|
| 91 |
|
| 92 |
return scores
|
| 93 |
|
| 94 |
-
|
| 95 |
# ---------------------------------------------------------------------------
|
| 96 |
# LLM agent (uses OpenAI API)
|
| 97 |
# ---------------------------------------------------------------------------
|
|
@@ -116,7 +114,6 @@ or with an optional column:
|
|
| 116 |
|
| 117 |
No explanation. JSON only."""
|
| 118 |
|
| 119 |
-
|
| 120 |
def run_llm_baseline(base_url: str, api_key: str, max_steps: int = 10) -> dict[str, float]:
|
| 121 |
"""Run an LLM agent (GPT-4o-mini) against the environment."""
|
| 122 |
client = OpenAI(api_key=api_key)
|
|
@@ -184,7 +181,6 @@ def run_llm_baseline(base_url: str, api_key: str, max_steps: int = 10) -> dict[s
|
|
| 184 |
|
| 185 |
return scores
|
| 186 |
|
| 187 |
-
|
| 188 |
# ---------------------------------------------------------------------------
|
| 189 |
# Main
|
| 190 |
# ---------------------------------------------------------------------------
|
|
|
|
| 62 |
],
|
| 63 |
}
|
| 64 |
|
|
|
|
| 65 |
def run_rule_baseline(base_url: str) -> dict[str, float]:
|
| 66 |
"""Run deterministic rule-based baseline — no LLM required."""
|
| 67 |
scores = {}
|
|
|
|
| 90 |
|
| 91 |
return scores
|
| 92 |
|
|
|
|
| 93 |
# ---------------------------------------------------------------------------
|
| 94 |
# LLM agent (uses OpenAI API)
|
| 95 |
# ---------------------------------------------------------------------------
|
|
|
|
| 114 |
|
| 115 |
No explanation. JSON only."""
|
| 116 |
|
|
|
|
| 117 |
def run_llm_baseline(base_url: str, api_key: str, max_steps: int = 10) -> dict[str, float]:
|
| 118 |
"""Run an LLM agent (GPT-4o-mini) against the environment."""
|
| 119 |
client = OpenAI(api_key=api_key)
|
|
|
|
| 181 |
|
| 182 |
return scores
|
| 183 |
|
|
|
|
| 184 |
# ---------------------------------------------------------------------------
|
| 185 |
# Main
|
| 186 |
# ---------------------------------------------------------------------------
|
client.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""Data Cleaning Env Environment Client."""
|
| 2 |
|
| 3 |
-
from typing import Dict
|
| 4 |
|
| 5 |
from openenv.core import EnvClient
|
| 6 |
from openenv.core.client_types import StepResult
|
|
@@ -8,7 +8,6 @@ from openenv.core.env_server.types import State
|
|
| 8 |
|
| 9 |
from .models import DataCleaningAction, DataCleaningObservation
|
| 10 |
|
| 11 |
-
|
| 12 |
class DataCleaningEnv(
|
| 13 |
EnvClient[DataCleaningAction, DataCleaningObservation, State]
|
| 14 |
):
|
|
@@ -34,19 +33,25 @@ class DataCleaningEnv(
|
|
| 34 |
def _parse_result(self, payload: Dict) -> StepResult[DataCleaningObservation]:
|
| 35 |
"""Parse server response into StepResult[DataCleaningObservation]."""
|
| 36 |
obs_data = payload.get("observation", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
observation = DataCleaningObservation(
|
| 39 |
current_text=obs_data.get("current_text", ""),
|
| 40 |
is_normalized=obs_data.get("is_normalized", False),
|
| 41 |
html_found=obs_data.get("html_found", False),
|
| 42 |
remaining_typos=obs_data.get("remaining_typos", 0),
|
|
|
|
|
|
|
| 43 |
metadata=obs_data.get("metadata", {}),
|
| 44 |
)
|
| 45 |
|
| 46 |
return StepResult(
|
| 47 |
observation=observation,
|
| 48 |
-
reward=
|
| 49 |
-
done=
|
| 50 |
)
|
| 51 |
|
| 52 |
def _parse_state(self, payload: Dict) -> State:
|
|
|
|
| 1 |
"""Data Cleaning Env Environment Client."""
|
| 2 |
|
| 3 |
+
from typing import Dict, Optional
|
| 4 |
|
| 5 |
from openenv.core import EnvClient
|
| 6 |
from openenv.core.client_types import StepResult
|
|
|
|
| 8 |
|
| 9 |
from .models import DataCleaningAction, DataCleaningObservation
|
| 10 |
|
|
|
|
| 11 |
class DataCleaningEnv(
|
| 12 |
EnvClient[DataCleaningAction, DataCleaningObservation, State]
|
| 13 |
):
|
|
|
|
| 33 |
def _parse_result(self, payload: Dict) -> StepResult[DataCleaningObservation]:
|
| 34 |
"""Parse server response into StepResult[DataCleaningObservation]."""
|
| 35 |
obs_data = payload.get("observation", {})
|
| 36 |
+
|
| 37 |
+
# done and reward live at top-level in the server response
|
| 38 |
+
done = payload.get("done", obs_data.get("done", False))
|
| 39 |
+
reward = payload.get("reward", obs_data.get("reward"))
|
| 40 |
|
| 41 |
observation = DataCleaningObservation(
|
| 42 |
current_text=obs_data.get("current_text", ""),
|
| 43 |
is_normalized=obs_data.get("is_normalized", False),
|
| 44 |
html_found=obs_data.get("html_found", False),
|
| 45 |
remaining_typos=obs_data.get("remaining_typos", 0),
|
| 46 |
+
done=done,
|
| 47 |
+
reward=reward,
|
| 48 |
metadata=obs_data.get("metadata", {}),
|
| 49 |
)
|
| 50 |
|
| 51 |
return StepResult(
|
| 52 |
observation=observation,
|
| 53 |
+
reward=reward,
|
| 54 |
+
done=done,
|
| 55 |
)
|
| 56 |
|
| 57 |
def _parse_state(self, payload: Dict) -> State:
|
inference.py
CHANGED
|
@@ -61,7 +61,7 @@ RULE_POLICIES = {
|
|
| 61 |
],
|
| 62 |
}
|
| 63 |
|
| 64 |
-
# ── System prompt
|
| 65 |
SYSTEM_PROMPT = """\
|
| 66 |
You are a data cleaning agent. Pick ONE operation per turn.
|
| 67 |
|
|
@@ -71,14 +71,14 @@ OUTPUT RULE: Respond with ONLY a JSON object. No explanation. No markdown. No ot
|
|
| 71 |
Format: {"operation": "operation_name"}
|
| 72 |
|
| 73 |
SELECTION RULES — apply in order based on PROBLEMS DETECTED:
|
| 74 |
-
1. If missing values > 0 and quantity column
|
| 75 |
-
2. If missing values > 0 and numeric columns affected
|
| 76 |
-
3. If missing values > 0 and text columns affected
|
| 77 |
-
4. If has_duplicates is true
|
| 78 |
-
5. If has_outliers is true
|
| 79 |
-
6. If non-numeric values
|
| 80 |
-
7. If text columns have inconsistent casing/whitespace
|
| 81 |
-
8. If
|
| 82 |
9. Pick the first operation from AVAILABLE that makes sense.
|
| 83 |
|
| 84 |
You MUST pick from the AVAILABLE list only — operations not listed are already done.
|
|
@@ -136,20 +136,25 @@ def _sanitize(text: str) -> str:
|
|
| 136 |
# ── Pick next unused op from a policy list ─────────────────────────────────
|
| 137 |
|
| 138 |
def _next_unused(policy: List[str], applied: List[str]) -> Optional[str]:
|
| 139 |
-
"""Return the first op in policy not already applied, or None."""
|
| 140 |
applied_set = set(applied)
|
| 141 |
for op in policy:
|
| 142 |
if op not in applied_set:
|
| 143 |
return op
|
| 144 |
return None
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
def parse_llm_response(raw: str, task: str, applied: List[str]) -> dict:
|
| 149 |
-
"""
|
| 150 |
-
3-layer fallback parser. Hard-enforces that the chosen operation
|
| 151 |
-
has not already been applied.
|
| 152 |
-
"""
|
| 153 |
if not raw:
|
| 154 |
return _fallback(task, applied)
|
| 155 |
|
|
@@ -158,7 +163,6 @@ def parse_llm_response(raw: str, task: str, applied: List[str]) -> dict:
|
|
| 158 |
|
| 159 |
candidate = None
|
| 160 |
|
| 161 |
-
# Layer 1: direct JSON parse
|
| 162 |
try:
|
| 163 |
result = json.loads(text)
|
| 164 |
if "operation" in result and result["operation"] in VALID_OPS:
|
|
@@ -166,7 +170,6 @@ def parse_llm_response(raw: str, task: str, applied: List[str]) -> dict:
|
|
| 166 |
except Exception:
|
| 167 |
pass
|
| 168 |
|
| 169 |
-
# Layer 2: find first {...} in string
|
| 170 |
if not candidate:
|
| 171 |
match = re.search(r"\{[^{}]*\}", text, re.DOTALL)
|
| 172 |
if match:
|
|
@@ -177,7 +180,6 @@ def parse_llm_response(raw: str, task: str, applied: List[str]) -> dict:
|
|
| 177 |
except Exception:
|
| 178 |
pass
|
| 179 |
|
| 180 |
-
# Layer 3: op name mentioned anywhere in raw text
|
| 181 |
if not candidate:
|
| 182 |
for op in VALID_OPS:
|
| 183 |
if op in raw:
|
|
@@ -215,10 +217,6 @@ def _fallback(task: str, applied: List[str]) -> dict:
|
|
| 215 |
# ── LLM call ───────────────────────────────────────────────────────────────
|
| 216 |
|
| 217 |
def get_llm_action(client: OpenAI, obs: dict, task: str, applied: List[str]) -> dict:
|
| 218 |
-
"""
|
| 219 |
-
Build prompt from observed facts only (no hint field).
|
| 220 |
-
Inject available ops explicitly so model cannot repeat a done op.
|
| 221 |
-
"""
|
| 222 |
metadata = obs.get("metadata", {})
|
| 223 |
quality = metadata.get("quality_score", "?")
|
| 224 |
missing = metadata.get("missing_count", 0)
|
|
@@ -254,7 +252,7 @@ def get_llm_action(client: OpenAI, obs: dict, task: str, applied: List[str]) ->
|
|
| 254 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 255 |
{"role": "user", "content": user_msg},
|
| 256 |
],
|
| 257 |
-
temperature=0.
|
| 258 |
max_tokens=50,
|
| 259 |
)
|
| 260 |
raw = (completion.choices[0].message.content or "").strip()
|
|
@@ -265,7 +263,21 @@ def get_llm_action(client: OpenAI, obs: dict, task: str, applied: List[str]) ->
|
|
| 265 |
print(f"[DEBUG] LLM call failed: {exc}", flush=True)
|
| 266 |
return _fallback(task, applied)
|
| 267 |
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
def run_episode(base_url: str, task: str, mode: str, client=None) -> None:
|
| 271 |
import requests
|
|
|
|
| 61 |
],
|
| 62 |
}
|
| 63 |
|
| 64 |
+
# ── System prompt ─────────────────
|
| 65 |
SYSTEM_PROMPT = """\
|
| 66 |
You are a data cleaning agent. Pick ONE operation per turn.
|
| 67 |
|
|
|
|
| 71 |
Format: {"operation": "operation_name"}
|
| 72 |
|
| 73 |
SELECTION RULES — apply in order based on PROBLEMS DETECTED:
|
| 74 |
+
1. If missing values > 0 and quantity column affected -> fill_quantity_mean
|
| 75 |
+
2. If missing values > 0 and numeric columns affected -> impute_mean
|
| 76 |
+
3. If missing values > 0 and text columns affected -> impute_mode
|
| 77 |
+
4. If has_duplicates is true -> remove_duplicates
|
| 78 |
+
5. If has_outliers is true -> remove_outliers
|
| 79 |
+
6. If non-numeric values in numeric columns -> fix_type_errors
|
| 80 |
+
7. If text columns have inconsistent casing/whitespace -> normalize_text
|
| 81 |
+
8. If rows still have missing values -> drop_missing_rows
|
| 82 |
9. Pick the first operation from AVAILABLE that makes sense.
|
| 83 |
|
| 84 |
You MUST pick from the AVAILABLE list only — operations not listed are already done.
|
|
|
|
| 136 |
# ── Pick next unused op from a policy list ─────────────────────────────────
|
| 137 |
|
| 138 |
def _next_unused(policy: List[str], applied: List[str]) -> Optional[str]:
|
|
|
|
| 139 |
applied_set = set(applied)
|
| 140 |
for op in policy:
|
| 141 |
if op not in applied_set:
|
| 142 |
return op
|
| 143 |
return None
|
| 144 |
|
| 145 |
+
def _fallback(task: str, applied: List[str]) -> dict:
|
| 146 |
+
policy = RULE_POLICIES.get(task, RULE_POLICIES["easy"])
|
| 147 |
+
op = _next_unused(policy, applied)
|
| 148 |
+
if op:
|
| 149 |
+
return {"operation": op}
|
| 150 |
+
op = _next_unused(VALID_OPS, applied)
|
| 151 |
+
if op:
|
| 152 |
+
print(f"[DEBUG] Policy exhausted, global fallback: {op}", flush=True)
|
| 153 |
+
return {"operation": op}
|
| 154 |
+
print("[DEBUG] All ops exhausted — repeating first policy op.", flush=True)
|
| 155 |
+
return {"operation": policy[0]}
|
| 156 |
|
| 157 |
def parse_llm_response(raw: str, task: str, applied: List[str]) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
if not raw:
|
| 159 |
return _fallback(task, applied)
|
| 160 |
|
|
|
|
| 163 |
|
| 164 |
candidate = None
|
| 165 |
|
|
|
|
| 166 |
try:
|
| 167 |
result = json.loads(text)
|
| 168 |
if "operation" in result and result["operation"] in VALID_OPS:
|
|
|
|
| 170 |
except Exception:
|
| 171 |
pass
|
| 172 |
|
|
|
|
| 173 |
if not candidate:
|
| 174 |
match = re.search(r"\{[^{}]*\}", text, re.DOTALL)
|
| 175 |
if match:
|
|
|
|
| 180 |
except Exception:
|
| 181 |
pass
|
| 182 |
|
|
|
|
| 183 |
if not candidate:
|
| 184 |
for op in VALID_OPS:
|
| 185 |
if op in raw:
|
|
|
|
| 217 |
# ── LLM call ───────────────────────────────────────────────────────────────
|
| 218 |
|
| 219 |
def get_llm_action(client: OpenAI, obs: dict, task: str, applied: List[str]) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
metadata = obs.get("metadata", {})
|
| 221 |
quality = metadata.get("quality_score", "?")
|
| 222 |
missing = metadata.get("missing_count", 0)
|
|
|
|
| 252 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 253 |
{"role": "user", "content": user_msg},
|
| 254 |
],
|
| 255 |
+
temperature=0.3,
|
| 256 |
max_tokens=50,
|
| 257 |
)
|
| 258 |
raw = (completion.choices[0].message.content or "").strip()
|
|
|
|
| 263 |
print(f"[DEBUG] LLM call failed: {exc}", flush=True)
|
| 264 |
return _fallback(task, applied)
|
| 265 |
|
| 266 |
+
def _extract_reward(result: dict, obs: dict) -> float:
|
| 267 |
+
# Top-level first (standard)
|
| 268 |
+
r = result.get("reward")
|
| 269 |
+
if r is not None:
|
| 270 |
+
return float(r)
|
| 271 |
+
# Inside observation object
|
| 272 |
+
r = obs.get("reward")
|
| 273 |
+
if r is not None:
|
| 274 |
+
return float(r)
|
| 275 |
+
# Inside metadata as quality delta (last resort)
|
| 276 |
+
meta = obs.get("metadata", {})
|
| 277 |
+
r = meta.get("last_reward")
|
| 278 |
+
if r is not None:
|
| 279 |
+
return float(r)
|
| 280 |
+
return 0.0
|
| 281 |
|
| 282 |
def run_episode(base_url: str, task: str, mode: str, client=None) -> None:
|
| 283 |
import requests
|
models.py
CHANGED
|
@@ -2,7 +2,6 @@ from typing import Any, Dict, Optional
|
|
| 2 |
from openenv.core.env_server.types import Action, Observation
|
| 3 |
from pydantic import Field
|
| 4 |
|
| 5 |
-
|
| 6 |
class DataCleaningAction(Action):
|
| 7 |
"""Actions the agent can take to clean a dirty dataset."""
|
| 8 |
|
|
@@ -20,7 +19,6 @@ class DataCleaningAction(Action):
|
|
| 20 |
description="Target column (optional). If omitted the op applies to all relevant columns.",
|
| 21 |
)
|
| 22 |
|
| 23 |
-
|
| 24 |
class DataCleaningObservation(Observation):
|
| 25 |
"""The dataset state observed after each cleaning step."""
|
| 26 |
|
|
|
|
| 2 |
from openenv.core.env_server.types import Action, Observation
|
| 3 |
from pydantic import Field
|
| 4 |
|
|
|
|
| 5 |
class DataCleaningAction(Action):
|
| 6 |
"""Actions the agent can take to clean a dirty dataset."""
|
| 7 |
|
|
|
|
| 19 |
description="Target column (optional). If omitted the op applies to all relevant columns.",
|
| 20 |
)
|
| 21 |
|
|
|
|
| 22 |
class DataCleaningObservation(Observation):
|
| 23 |
"""The dataset state observed after each cleaning step."""
|
| 24 |
|
server/app.py
CHANGED
|
@@ -18,12 +18,14 @@ Additional hackathon-required endpoints:
|
|
| 18 |
try:
|
| 19 |
from openenv.core.env_server.http_server import create_app
|
| 20 |
except Exception as e:
|
| 21 |
-
raise ImportError(
|
| 22 |
-
"openenv is required. Install dependencies with: uv sync"
|
| 23 |
-
) from e
|
| 24 |
|
| 25 |
-
|
| 26 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
from fastapi import HTTPException
|
| 29 |
from fastapi.responses import JSONResponse
|
|
@@ -39,17 +41,25 @@ app = create_app(
|
|
| 39 |
max_concurrent_envs=1,
|
| 40 |
)
|
| 41 |
|
| 42 |
-
# ---------------------------------------------------------------------------
|
| 43 |
-
# Shared environment instance for grader/baseline
|
| 44 |
-
# ---------------------------------------------------------------------------
|
| 45 |
-
_env: DataCleaningEnvironment | None = None
|
| 46 |
-
|
| 47 |
def _get_env() -> DataCleaningEnvironment:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
# GET /health
|
| 55 |
# ---------------------------------------------------------------------------
|
|
@@ -57,7 +67,6 @@ def _get_env() -> DataCleaningEnvironment:
|
|
| 57 |
def health():
|
| 58 |
"""Simple health check endpoint."""
|
| 59 |
return {"status": "ok"}
|
| 60 |
-
|
| 61 |
# ---------------------------------------------------------------------------
|
| 62 |
# GET /tasks
|
| 63 |
# ---------------------------------------------------------------------------
|
|
@@ -86,8 +95,6 @@ def run_grader():
|
|
| 86 |
|
| 87 |
if isinstance(result, dict) and "score" in result:
|
| 88 |
return JSONResponse(content=result)
|
| 89 |
-
|
| 90 |
-
# fallback safety
|
| 91 |
return JSONResponse(content={"score": float(result)})
|
| 92 |
|
| 93 |
except Exception as e:
|
|
@@ -98,9 +105,6 @@ def run_grader():
|
|
| 98 |
# ---------------------------------------------------------------------------
|
| 99 |
@app.post("/baseline")
|
| 100 |
def run_baseline():
|
| 101 |
-
"""
|
| 102 |
-
Run deterministic baseline agent across all tasks.
|
| 103 |
-
"""
|
| 104 |
try:
|
| 105 |
result = _get_env().run_baseline()
|
| 106 |
return JSONResponse(content=result)
|
|
|
|
| 18 |
try:
|
| 19 |
from openenv.core.env_server.http_server import create_app
|
| 20 |
except Exception as e:
|
| 21 |
+
raise ImportError("openenv is required. Install with: uv sync") from e
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
try:
|
| 24 |
+
from ..models import DataCleaningAction, DataCleaningObservation
|
| 25 |
+
from .data_cleaning_env_environment import DataCleaningEnvironment
|
| 26 |
+
except ModuleNotFoundError:
|
| 27 |
+
from models import DataCleaningAction, DataCleaningObservation
|
| 28 |
+
from server.data_cleaning_env_environment import DataCleaningEnvironment
|
| 29 |
|
| 30 |
from fastapi import HTTPException
|
| 31 |
from fastapi.responses import JSONResponse
|
|
|
|
| 41 |
max_concurrent_envs=1,
|
| 42 |
)
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def _get_env() -> DataCleaningEnvironment:
|
| 45 |
+
"""
|
| 46 |
+
openenv stores the managed env at app.state.env or app.state.server.env.
|
| 47 |
+
Fall back to a fresh instance only if neither attribute exists.
|
| 48 |
+
"""
|
| 49 |
+
state = getattr(app, "state", None)
|
| 50 |
+
if state:
|
| 51 |
+
env = getattr(state, "env", None)
|
| 52 |
+
if env:
|
| 53 |
+
return env
|
| 54 |
+
server = getattr(state, "server", None)
|
| 55 |
+
if server:
|
| 56 |
+
env = getattr(server, "env", None)
|
| 57 |
+
if env:
|
| 58 |
+
return env
|
| 59 |
+
print("[WARN] Could not find openenv internal env — grader uses fresh instance", flush=True)
|
| 60 |
+
e = DataCleaningEnvironment()
|
| 61 |
+
e.reset(task="easy")
|
| 62 |
+
return e
|
| 63 |
# ---------------------------------------------------------------------------
|
| 64 |
# GET /health
|
| 65 |
# ---------------------------------------------------------------------------
|
|
|
|
| 67 |
def health():
|
| 68 |
"""Simple health check endpoint."""
|
| 69 |
return {"status": "ok"}
|
|
|
|
| 70 |
# ---------------------------------------------------------------------------
|
| 71 |
# GET /tasks
|
| 72 |
# ---------------------------------------------------------------------------
|
|
|
|
| 95 |
|
| 96 |
if isinstance(result, dict) and "score" in result:
|
| 97 |
return JSONResponse(content=result)
|
|
|
|
|
|
|
| 98 |
return JSONResponse(content={"score": float(result)})
|
| 99 |
|
| 100 |
except Exception as e:
|
|
|
|
| 105 |
# ---------------------------------------------------------------------------
|
| 106 |
@app.post("/baseline")
|
| 107 |
def run_baseline():
|
|
|
|
|
|
|
|
|
|
| 108 |
try:
|
| 109 |
result = _get_env().run_baseline()
|
| 110 |
return JSONResponse(content=result)
|
server/data_cleaning_env_environment.py
CHANGED
|
@@ -112,12 +112,14 @@ def _grade_medium(rows: list[dict]) -> float:
|
|
| 112 |
dupes += 1
|
| 113 |
seen.add(key)
|
| 114 |
dedup_score = 1.0 if dupes == 0 else max(0.0, 1.0 - dupes * 0.5)
|
|
|
|
| 115 |
# Type-fix check: all 'age' values must be int or float
|
| 116 |
type_errors = sum(1 for r in rows if not isinstance(r.get("age"), (int, float)))
|
| 117 |
type_score = 1.0 if type_errors == 0 else max(0.0, 1.0 - type_errors * 0.5)
|
| 118 |
|
| 119 |
return round((dedup_score + type_score) / 2, 4)
|
| 120 |
|
|
|
|
| 121 |
def _grade_hard(rows: list[dict]) -> float:
|
| 122 |
"""Score 0-1 across 4 sub-criteria."""
|
| 123 |
if not rows:
|
|
@@ -161,6 +163,7 @@ DATASETS = {
|
|
| 161 |
"hard": _make_hard_dataset,
|
| 162 |
}
|
| 163 |
|
|
|
|
| 164 |
# ---------------------------------------------------------------------------
|
| 165 |
# Environment
|
| 166 |
# ---------------------------------------------------------------------------
|
|
@@ -202,7 +205,9 @@ class DataCleaningEnvironment(Environment):
|
|
| 202 |
Each task presents a different level of difficulty and requires
|
| 203 |
different cleaning operations.
|
| 204 |
"""
|
|
|
|
| 205 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
|
|
|
| 206 |
# -----------------------------------------------------------------------
|
| 207 |
# Lifecycle
|
| 208 |
# -----------------------------------------------------------------------
|
|
@@ -274,7 +279,7 @@ class DataCleaningEnvironment(Environment):
|
|
| 274 |
reward = -0.05
|
| 275 |
self._last_reward = reward
|
| 276 |
obs = self._make_observation(reward=reward, done=False)
|
| 277 |
-
obs.metadata["error"] = f"Unknown operation '{op}'. Valid: {sorted(
|
| 278 |
return obs
|
| 279 |
|
| 280 |
before_score = GRADERS[self._task](self._rows)
|
|
@@ -283,9 +288,8 @@ class DataCleaningEnvironment(Environment):
|
|
| 283 |
|
| 284 |
# Reward = improvement in quality score (partial progress signal)
|
| 285 |
improvement = after_score - before_score
|
| 286 |
-
# 0.01
|
| 287 |
if improvement > 0:
|
| 288 |
-
reward = round(improvement + 0.02, 4) # bonus for improvement
|
| 289 |
elif improvement < 0:
|
| 290 |
reward = round(improvement - 0.02, 4) # penalty for harming dataset
|
| 291 |
else:
|
|
@@ -301,7 +305,6 @@ class DataCleaningEnvironment(Environment):
|
|
| 301 |
max_steps = MAX_STEPS[self._task]
|
| 302 |
done = (after_score >= 1.0) or (self._step_count >= max_steps)
|
| 303 |
self._done = done
|
| 304 |
-
|
| 305 |
return self._make_observation(reward=reward, done=done)
|
| 306 |
|
| 307 |
# -----------------------------------------------------------------------
|
|
@@ -392,7 +395,7 @@ class DataCleaningEnvironment(Environment):
|
|
| 392 |
def run_baseline(self) -> dict:
|
| 393 |
"""
|
| 394 |
Run a deterministic rule-based baseline agent on all 3 tasks.
|
| 395 |
-
Returns scores dict compatible with the
|
| 396 |
"""
|
| 397 |
results = {}
|
| 398 |
for task_id in ["easy", "medium", "hard"]:
|
|
@@ -431,6 +434,9 @@ class DataCleaningEnvironment(Environment):
|
|
| 431 |
"quality_score": score,
|
| 432 |
"valid_operations": sorted(valid_ops),
|
| 433 |
"ops_already_applied": list(self._applied_ops),
|
|
|
|
|
|
|
|
|
|
| 434 |
},
|
| 435 |
)
|
| 436 |
|
|
@@ -540,8 +546,8 @@ _BASELINE_POLICIES: dict[str, list[str]] = {
|
|
| 540 |
"easy": ["impute_mean", "impute_mode", "drop_missing_rows"],
|
| 541 |
"medium": ["remove_duplicates", "fix_type_errors", "drop_missing_rows"],
|
| 542 |
"hard": [
|
| 543 |
-
"drop_missing_rows",
|
| 544 |
"fill_quantity_mean",
|
|
|
|
| 545 |
"remove_duplicates",
|
| 546 |
"fix_type_errors",
|
| 547 |
"remove_outliers",
|
|
@@ -553,42 +559,43 @@ _BASELINE_POLICIES: dict[str, list[str]] = {
|
|
| 553 |
# Recommendation helper (guides the LLM toward correct next op)
|
| 554 |
# ---------------------------------------------------------------------------
|
| 555 |
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
#
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
|
|
|
| 592 |
|
| 593 |
# ---------------------------------------------------------------------------
|
| 594 |
# Column utility helpers
|
|
|
|
| 112 |
dupes += 1
|
| 113 |
seen.add(key)
|
| 114 |
dedup_score = 1.0 if dupes == 0 else max(0.0, 1.0 - dupes * 0.5)
|
| 115 |
+
|
| 116 |
# Type-fix check: all 'age' values must be int or float
|
| 117 |
type_errors = sum(1 for r in rows if not isinstance(r.get("age"), (int, float)))
|
| 118 |
type_score = 1.0 if type_errors == 0 else max(0.0, 1.0 - type_errors * 0.5)
|
| 119 |
|
| 120 |
return round((dedup_score + type_score) / 2, 4)
|
| 121 |
|
| 122 |
+
|
| 123 |
def _grade_hard(rows: list[dict]) -> float:
|
| 124 |
"""Score 0-1 across 4 sub-criteria."""
|
| 125 |
if not rows:
|
|
|
|
| 163 |
"hard": _make_hard_dataset,
|
| 164 |
}
|
| 165 |
|
| 166 |
+
|
| 167 |
# ---------------------------------------------------------------------------
|
| 168 |
# Environment
|
| 169 |
# ---------------------------------------------------------------------------
|
|
|
|
| 205 |
Each task presents a different level of difficulty and requires
|
| 206 |
different cleaning operations.
|
| 207 |
"""
|
| 208 |
+
|
| 209 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 210 |
+
|
| 211 |
# -----------------------------------------------------------------------
|
| 212 |
# Lifecycle
|
| 213 |
# -----------------------------------------------------------------------
|
|
|
|
| 279 |
reward = -0.05
|
| 280 |
self._last_reward = reward
|
| 281 |
obs = self._make_observation(reward=reward, done=False)
|
| 282 |
+
obs.metadata["error"] = f"Unknown operation '{op}'. Valid: {sorted(VALID_OPERATIONS)}"
|
| 283 |
return obs
|
| 284 |
|
| 285 |
before_score = GRADERS[self._task](self._rows)
|
|
|
|
| 288 |
|
| 289 |
# Reward = improvement in quality score (partial progress signal)
|
| 290 |
improvement = after_score - before_score
|
|
|
|
| 291 |
if improvement > 0:
|
| 292 |
+
reward = round(improvement + 0.02, 4) # bonus for any improvement
|
| 293 |
elif improvement < 0:
|
| 294 |
reward = round(improvement - 0.02, 4) # penalty for harming dataset
|
| 295 |
else:
|
|
|
|
| 305 |
max_steps = MAX_STEPS[self._task]
|
| 306 |
done = (after_score >= 1.0) or (self._step_count >= max_steps)
|
| 307 |
self._done = done
|
|
|
|
| 308 |
return self._make_observation(reward=reward, done=done)
|
| 309 |
|
| 310 |
# -----------------------------------------------------------------------
|
|
|
|
| 395 |
def run_baseline(self) -> dict:
|
| 396 |
"""
|
| 397 |
Run a deterministic rule-based baseline agent on all 3 tasks.
|
| 398 |
+
Returns scores dict compatible with the /baseline endpoint.
|
| 399 |
"""
|
| 400 |
results = {}
|
| 401 |
for task_id in ["easy", "medium", "hard"]:
|
|
|
|
| 434 |
"quality_score": score,
|
| 435 |
"valid_operations": sorted(valid_ops),
|
| 436 |
"ops_already_applied": list(self._applied_ops),
|
| 437 |
+
"recommended_next": _recommend_next(
|
| 438 |
+
self._task, missing_count, has_dupes, has_outliers, self._applied_ops
|
| 439 |
+
),
|
| 440 |
},
|
| 441 |
)
|
| 442 |
|
|
|
|
| 546 |
"easy": ["impute_mean", "impute_mode", "drop_missing_rows"],
|
| 547 |
"medium": ["remove_duplicates", "fix_type_errors", "drop_missing_rows"],
|
| 548 |
"hard": [
|
|
|
|
| 549 |
"fill_quantity_mean",
|
| 550 |
+
"drop_missing_rows",
|
| 551 |
"remove_duplicates",
|
| 552 |
"fix_type_errors",
|
| 553 |
"remove_outliers",
|
|
|
|
| 559 |
# Recommendation helper (guides the LLM toward correct next op)
|
| 560 |
# ---------------------------------------------------------------------------
|
| 561 |
|
| 562 |
+
def _recommend_next(
|
| 563 |
+
task: str,
|
| 564 |
+
missing_count: int,
|
| 565 |
+
has_dupes: bool,
|
| 566 |
+
has_outliers: bool,
|
| 567 |
+
applied_ops: list,
|
| 568 |
+
) -> str:
|
| 569 |
+
"""Return a plain-English hint for the LLM about the best next operation."""
|
| 570 |
+
applied = set(applied_ops)
|
| 571 |
+
|
| 572 |
+
if task == "easy":
|
| 573 |
+
if missing_count > 0:
|
| 574 |
+
return "There are missing values. Use impute_mean (numeric) or impute_mode (text)."
|
| 575 |
+
return "No issues remain. Episode should be complete."
|
| 576 |
+
|
| 577 |
+
if task == "medium":
|
| 578 |
+
if has_dupes and "remove_duplicates" not in applied:
|
| 579 |
+
return "Duplicate rows exist. Use remove_duplicates."
|
| 580 |
+
if "fix_type_errors" not in applied:
|
| 581 |
+
return "Non-numeric values in numeric columns. Use fix_type_errors."
|
| 582 |
+
if missing_count > 0 and "drop_missing_rows" not in applied:
|
| 583 |
+
return "Some values still missing after type fix. Use drop_missing_rows."
|
| 584 |
+
return "No issues remain. Episode should be complete."
|
| 585 |
+
|
| 586 |
+
# hard
|
| 587 |
+
if missing_count > 0 and "fill_quantity_mean" not in applied:
|
| 588 |
+
return "Missing quantity values. Use fill_quantity_mean first."
|
| 589 |
+
if missing_count > 0 and "drop_missing_rows" not in applied:
|
| 590 |
+
return "Missing product/category values. Use drop_missing_rows."
|
| 591 |
+
if has_outliers and "remove_outliers" not in applied:
|
| 592 |
+
return "Price outliers detected (price<=0 or price>=500). Use remove_outliers."
|
| 593 |
+
if "normalize_text" not in applied:
|
| 594 |
+
return "String columns have inconsistent casing/whitespace. Use normalize_text."
|
| 595 |
+
if has_dupes and "remove_duplicates" not in applied:
|
| 596 |
+
return "Duplicate rows remain. Use remove_duplicates."
|
| 597 |
+
return "All issues fixed. Episode should be complete."
|
| 598 |
+
|
| 599 |
|
| 600 |
# ---------------------------------------------------------------------------
|
| 601 |
# Column utility helpers
|