Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +81 -0
- README.md +50 -3
- __init__.py +16 -0
- client.py +49 -0
- inference.py +145 -0
- models.py +19 -0
- openenv.yaml +7 -0
- openenv_data_clean_env.egg-info/PKG-INFO +10 -0
- openenv_data_clean_env.egg-info/SOURCES.txt +15 -0
- openenv_data_clean_env.egg-info/dependency_links.txt +1 -0
- openenv_data_clean_env.egg-info/entry_points.txt +2 -0
- openenv_data_clean_env.egg-info/requires.txt +6 -0
- openenv_data_clean_env.egg-info/top_level.txt +1 -0
- pyproject.toml +27 -0
- server/__init__.py +11 -0
- server/app.py +82 -0
- server/data_clean_env_environment.py +181 -0
- server/requirements.txt +6 -0
- uv.lock +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=data_clean_env
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 81 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,57 @@
|
|
| 1 |
---
|
| 2 |
title: Data Clean Env
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Data Clean Env
|
| 3 |
+
emoji: 🧹
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
tags:
|
| 10 |
+
- openenv
|
| 11 |
+
base_path: /web
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Data Clean Environment for OpenEnv
|
| 15 |
+
|
| 16 |
+
## Overview and Motivation
|
| 17 |
+
Data cleaning is one of the most time-consuming real-world tasks for data scientists and analysts.
|
| 18 |
+
This OpenEnv simulates a data cleaning scenario where an AI agent must clean a dirty pandas DataFrame.
|
| 19 |
+
The agent interacts with the DataFrame using discrete operations (filling NaNs, dropping columns, etc.)
|
| 20 |
+
and receives a score based on how perfectly it cleans the data according to the task objective.
|
| 21 |
+
|
| 22 |
+
## Action Space
|
| 23 |
+
The environment expects a `DataCleanAction` which performs one atomic change to the dataframe:
|
| 24 |
+
- `fill_na`: Provide `column_name` and `value` to fill NaNs.
|
| 25 |
+
- `drop_na`: Provide `column_name` to drop rows with NaNs in that column.
|
| 26 |
+
- `drop_column`: Provide `column_name` to drop it.
|
| 27 |
+
- `rename_column`: Provide `column_name` and `value` (new name).
|
| 28 |
+
- `change_type`: Provide `column_name` and `value` ('int', 'float', 'str').
|
| 29 |
+
- `submit`: Commit the final dataframe for grading.
|
| 30 |
+
|
| 31 |
+
## Observation Space
|
| 32 |
+
The environment returns a `DataCleanObservation` detailing the current dataframe state:
|
| 33 |
+
- `df_schema`: The dictionary representation of column types.
|
| 34 |
+
- `missing_values`: A dictionary representation of NaN counts per column.
|
| 35 |
+
- `head`: The first 5 rows in string format.
|
| 36 |
+
- `feedback`: Text feedback of the last action.
|
| 37 |
+
- `last_error`: Text description of any error encountered.
|
| 38 |
+
|
| 39 |
+
## Tasks and Difficulty
|
| 40 |
+
- **easy_clean (Easy)**: Fill missing values in a single column ('age').
|
| 41 |
+
- **medium_clean (Medium)**: Handle multiple missing value types and drop an unnecessary column.
|
| 42 |
+
- **hard_clean (Hard)**: Handle missing values, rename columns, and change column data types.
|
| 43 |
+
|
| 44 |
+
## Setup and Usage
|
| 45 |
+
1. Build the Docker image:
|
| 46 |
+
`docker build -t openenv_data_clean:latest -f server/Dockerfile .`
|
| 47 |
+
2. Run the server locally:
|
| 48 |
+
`docker run -p 8000:8000 openenv_data_clean:latest`
|
| 49 |
+
3. Run inference baseline:
|
| 50 |
+
`export HF_TOKEN="your_token"`
|
| 51 |
+
`export IMAGE_NAME="openenv_data_clean:latest"`
|
| 52 |
+
`python inference.py`
|
| 53 |
+
|
| 54 |
+
## Baseline Scores
|
| 55 |
+
- easy_clean: 1.00
|
| 56 |
+
- medium_clean: 1.00
|
| 57 |
+
- hard_clean: 1.00
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Data Clean Env Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import DataCleanEnv
|
| 10 |
+
from .models import DataCleanAction, DataCleanObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"DataCleanAction",
|
| 14 |
+
"DataCleanObservation",
|
| 15 |
+
"DataCleanEnv",
|
| 16 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Optional
|
| 2 |
+
|
| 3 |
+
from openenv.core.client_types import StepResult
|
| 4 |
+
from openenv.core.env_server.types import State
|
| 5 |
+
from openenv.core import EnvClient
|
| 6 |
+
|
| 7 |
+
from .models import DataCleanAction, DataCleanObservation
|
| 8 |
+
from .server.data_clean_env_environment import DataCleanState
|
| 9 |
+
|
| 10 |
+
class DataCleanEnv(
|
| 11 |
+
EnvClient[DataCleanAction, DataCleanObservation]
|
| 12 |
+
):
|
| 13 |
+
def _step_payload(self, action: DataCleanAction) -> Dict:
|
| 14 |
+
return action.model_dump()
|
| 15 |
+
|
| 16 |
+
def _parse_result(self, payload: Dict) -> StepResult[DataCleanObservation]:
|
| 17 |
+
obs_data = payload.get("observation", {})
|
| 18 |
+
observation = DataCleanObservation(
|
| 19 |
+
df_schema=obs_data.get("df_schema", ""),
|
| 20 |
+
missing_values=obs_data.get("missing_values", ""),
|
| 21 |
+
head=obs_data.get("head", ""),
|
| 22 |
+
last_error=obs_data.get("last_error"),
|
| 23 |
+
feedback=obs_data.get("feedback"),
|
| 24 |
+
metadata=obs_data.get("metadata", {}),
|
| 25 |
+
done=payload.get("done", False),
|
| 26 |
+
reward=payload.get("reward", 0.0),
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
return StepResult(
|
| 30 |
+
observation=observation,
|
| 31 |
+
reward=payload.get("reward"),
|
| 32 |
+
done=payload.get("done", False),
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def _parse_state(self, payload: Dict) -> DataCleanState:
|
| 36 |
+
return DataCleanState(
|
| 37 |
+
episode_id=payload.get("episode_id", ""),
|
| 38 |
+
step_count=payload.get("step_count", 0),
|
| 39 |
+
current_df_json=payload.get("current_df_json", ""),
|
| 40 |
+
task_name=payload.get("task_name", ""),
|
| 41 |
+
target_df_json=payload.get("target_df_json", ""),
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
async def get_client(image_name: Optional[str] = None):
|
| 45 |
+
if image_name:
|
| 46 |
+
client = await DataCleanEnv.from_docker_image(image_name)
|
| 47 |
+
else:
|
| 48 |
+
client = DataCleanEnv(base_url="http://localhost:8000")
|
| 49 |
+
return client
|
inference.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import textwrap
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
|
| 9 |
+
from client import get_client
|
| 10 |
+
from models import DataCleanAction
|
| 11 |
+
|
| 12 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 13 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1-mini")
|
| 14 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 15 |
+
|
| 16 |
+
BENCHMARK = "data_clean_env"
|
| 17 |
+
MAX_STEPS = 10
|
| 18 |
+
TEMPERATURE = 0.7
|
| 19 |
+
|
| 20 |
+
SYSTEM_PROMPT = textwrap.dedent(
|
| 21 |
+
"""
|
| 22 |
+
You are an AI agent tasked with cleaning a pandas DataFrame.
|
| 23 |
+
You will be given the current DataFrame schema, missing values count per column, and the first 5 rows.
|
| 24 |
+
You must output a JSON string representing exactly one action to take.
|
| 25 |
+
|
| 26 |
+
Allowed actions:
|
| 27 |
+
{"action_type": "fill_na", "column_name": "col", "value": "0"}
|
| 28 |
+
{"action_type": "drop_na", "column_name": "col"}
|
| 29 |
+
{"action_type": "drop_column", "column_name": "col"}
|
| 30 |
+
{"action_type": "rename_column", "column_name": "old_col", "value": "new_col"}
|
| 31 |
+
{"action_type": "change_type", "column_name": "col", "value": "int"} (value can be int, float, or str)
|
| 32 |
+
{"action_type": "submit"}
|
| 33 |
+
|
| 34 |
+
Your goal:
|
| 35 |
+
- easy_clean: Fill missing values in 'age' with '0'.
|
| 36 |
+
- medium_clean: Drop rows with missing values in 'name' and 'age'. Drop column 'ignore_me'.
|
| 37 |
+
- hard_clean: Rename 'EmployeeID' to 'emp_id'. Drop 'Dept' column. Make 'Salary' valid (fill NaN with '0' and convert to float/int). Fill NaN in 'JoinDate' with '2000-01-01'.
|
| 38 |
+
|
| 39 |
+
When you are done cleaning according to the goal, output {"action_type": "submit"}.
|
| 40 |
+
Reply ONLY with valid JSON.
|
| 41 |
+
"""
|
| 42 |
+
).strip()
|
| 43 |
+
|
| 44 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 45 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 46 |
+
|
| 47 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 48 |
+
error_val = error if error else "null"
|
| 49 |
+
done_val = str(done).lower()
|
| 50 |
+
print(
|
| 51 |
+
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
|
| 52 |
+
flush=True,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 56 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 57 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 58 |
+
|
| 59 |
+
def get_model_action(client: OpenAI, obs_dict: dict) -> dict:
|
| 60 |
+
user_prompt = f"Observation:\n{json.dumps(obs_dict, indent=2)}\nWhat is your next action?"
|
| 61 |
+
try:
|
| 62 |
+
completion = client.chat.completions.create(
|
| 63 |
+
model=MODEL_NAME,
|
| 64 |
+
messages=[
|
| 65 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 66 |
+
{"role": "user", "content": user_prompt},
|
| 67 |
+
],
|
| 68 |
+
temperature=TEMPERATURE,
|
| 69 |
+
stream=False,
|
| 70 |
+
)
|
| 71 |
+
text = completion.choices[0].message.content.strip()
|
| 72 |
+
if text.startswith("```json"):
|
| 73 |
+
text = text[7:]
|
| 74 |
+
if text.endswith("```"):
|
| 75 |
+
text = text[:-3]
|
| 76 |
+
return json.loads(text.strip())
|
| 77 |
+
except Exception as exc:
|
| 78 |
+
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 79 |
+
return {"action_type": "submit"}
|
| 80 |
+
|
| 81 |
+
async def run_task(task_name: str, client: OpenAI, env_client) -> None:
|
| 82 |
+
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
result = await env_client.reset(task=task_name)
|
| 86 |
+
|
| 87 |
+
rewards = []
|
| 88 |
+
steps_taken = 0
|
| 89 |
+
score = 0.0
|
| 90 |
+
success = False
|
| 91 |
+
|
| 92 |
+
for step in range(1, MAX_STEPS + 1):
|
| 93 |
+
if result.done:
|
| 94 |
+
break
|
| 95 |
+
|
| 96 |
+
obs = result.observation
|
| 97 |
+
obs_dict = {
|
| 98 |
+
"schema": obs.df_schema,
|
| 99 |
+
"missing": obs.missing_values,
|
| 100 |
+
"head": obs.head,
|
| 101 |
+
"feedback": obs.feedback,
|
| 102 |
+
"error": obs.last_error
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
action_dict = get_model_action(client, obs_dict)
|
| 106 |
+
action_str = json.dumps(action_dict)
|
| 107 |
+
action = DataCleanAction(**action_dict)
|
| 108 |
+
|
| 109 |
+
result = await env_client.step(action)
|
| 110 |
+
reward = result.reward or 0.0
|
| 111 |
+
done = result.done
|
| 112 |
+
error = result.observation.last_error
|
| 113 |
+
|
| 114 |
+
rewards.append(reward)
|
| 115 |
+
steps_taken = step
|
| 116 |
+
|
| 117 |
+
if action.action_type == "submit":
|
| 118 |
+
score = reward # grader sets final reward to score
|
| 119 |
+
|
| 120 |
+
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
|
| 121 |
+
|
| 122 |
+
if done:
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
success = score >= 0.5
|
| 126 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 127 |
+
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"[DEBUG] Error running task {task_name}: {e}", flush=True)
|
| 130 |
+
|
| 131 |
+
async def main() -> None:
|
| 132 |
+
if HF_TOKEN is None:
|
| 133 |
+
raise ValueError("HF_TOKEN environment variable is required")
|
| 134 |
+
|
| 135 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
| 136 |
+
image_name = os.getenv("LOCAL_IMAGE_NAME") or os.getenv("IMAGE_NAME")
|
| 137 |
+
env_client = await get_client(image_name)
|
| 138 |
+
|
| 139 |
+
for task in ["easy_clean", "medium_clean", "hard_clean"]:
|
| 140 |
+
await run_task(task, client, env_client)
|
| 141 |
+
|
| 142 |
+
await env_client.close()
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
asyncio.run(main())
|
models.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Literal, Optional, List
|
| 2 |
+
from pydantic import Field
|
| 3 |
+
from openenv.core.env_server.types import Action, Observation
|
| 4 |
+
|
| 5 |
+
class DataCleanAction(Action):
|
| 6 |
+
"""Action for the Data Clean Env environment to manipulate the dataframe."""
|
| 7 |
+
action_type: Literal["fill_na", "drop_na", "rename_column", "drop_column", "change_type", "submit"] = Field(
|
| 8 |
+
..., description="The type of action to perform."
|
| 9 |
+
)
|
| 10 |
+
column_name: Optional[str] = Field(None, description="The target column name.")
|
| 11 |
+
value: Optional[str] = Field(None, description="The value to use (for fill_na), new name (for rename_column), or new type (for change_type like 'int', 'float', 'str').")
|
| 12 |
+
|
| 13 |
+
class DataCleanObservation(Observation):
|
| 14 |
+
"""Observation from the Data Clean Env environment showing the dataframe state."""
|
| 15 |
+
df_schema: str = Field(default="", description="The schema of the dataframe.")
|
| 16 |
+
missing_values: str = Field(default="", description="A string detailing missing values per column.")
|
| 17 |
+
head: str = Field(default="", description="The first 5 rows of the dataframe.")
|
| 18 |
+
last_error: Optional[str] = Field(default=None, description="Any error from the last action.")
|
| 19 |
+
feedback: Optional[str] = Field(default=None, description="Feedback about the last action.")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: data_clean_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
|
openenv_data_clean_env.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: openenv-data_clean_env
|
| 3 |
+
Version: 0.1.0
|
| 4 |
+
Summary: Data Clean Env environment for OpenEnv
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
Requires-Dist: openenv-core[core]>=0.2.0
|
| 7 |
+
Requires-Dist: pandas>=2.0.0
|
| 8 |
+
Provides-Extra: dev
|
| 9 |
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 10 |
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
openenv_data_clean_env.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
README.md
|
| 2 |
+
pyproject.toml
|
| 3 |
+
./__init__.py
|
| 4 |
+
./client.py
|
| 5 |
+
./inference.py
|
| 6 |
+
./models.py
|
| 7 |
+
openenv_data_clean_env.egg-info/PKG-INFO
|
| 8 |
+
openenv_data_clean_env.egg-info/SOURCES.txt
|
| 9 |
+
openenv_data_clean_env.egg-info/dependency_links.txt
|
| 10 |
+
openenv_data_clean_env.egg-info/entry_points.txt
|
| 11 |
+
openenv_data_clean_env.egg-info/requires.txt
|
| 12 |
+
openenv_data_clean_env.egg-info/top_level.txt
|
| 13 |
+
server/__init__.py
|
| 14 |
+
server/app.py
|
| 15 |
+
server/data_clean_env_environment.py
|
openenv_data_clean_env.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
openenv_data_clean_env.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
server = data_clean_env.server.app:main
|
openenv_data_clean_env.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.0
|
| 2 |
+
pandas>=2.0.0
|
| 3 |
+
|
| 4 |
+
[dev]
|
| 5 |
+
pytest>=8.0.0
|
| 6 |
+
pytest-cov>=4.0.0
|
openenv_data_clean_env.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
data_clean_env
|
pyproject.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-data_clean_env"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Data Clean Env environment for OpenEnv"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"openenv-core[core]>=0.2.0",
|
| 12 |
+
"pandas>=2.0.0",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
[project.optional-dependencies]
|
| 16 |
+
dev = [
|
| 17 |
+
"pytest>=8.0.0",
|
| 18 |
+
"pytest-cov>=4.0.0",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[project.scripts]
|
| 22 |
+
server = "data_clean_env.server.app:main"
|
| 23 |
+
|
| 24 |
+
[tool.setuptools]
|
| 25 |
+
include-package-data = true
|
| 26 |
+
packages = ["data_clean_env", "data_clean_env.server"]
|
| 27 |
+
package-dir = { "data_clean_env" = ".", "data_clean_env.server" = "server" }
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Data Clean Env environment server components."""
|
| 8 |
+
|
| 9 |
+
from .data_clean_env_environment import DataCleanEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["DataCleanEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
FastAPI application for the Data Clean Env Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the DataCleanEnvironment
|
| 11 |
+
over HTTP and WebSocket endpoints, compatible with EnvClient.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
- POST /reset: Reset the environment
|
| 15 |
+
- POST /step: Execute an action
|
| 16 |
+
- GET /state: Get current environment state
|
| 17 |
+
- GET /schema: Get action/observation schemas
|
| 18 |
+
- WS /ws: WebSocket endpoint for persistent sessions
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# Development (with auto-reload):
|
| 22 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 23 |
+
|
| 24 |
+
# Production:
|
| 25 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 26 |
+
|
| 27 |
+
# Or run directly:
|
| 28 |
+
python -m server.app
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
except Exception as e: # pragma: no cover
|
| 34 |
+
raise ImportError(
|
| 35 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 36 |
+
) from e
|
| 37 |
+
|
| 38 |
+
# Import from local models.py (PYTHONPATH includes /app/env in Docker)
|
| 39 |
+
from models import DataCleanAction, DataCleanObservation
|
| 40 |
+
from .data_clean_env_environment import DataCleanEnvironment
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# Create the app with web interface and README integration
|
| 44 |
+
app = create_app(
|
| 45 |
+
DataCleanEnvironment,
|
| 46 |
+
DataCleanAction,
|
| 47 |
+
DataCleanObservation,
|
| 48 |
+
env_name="data_clean_env",
|
| 49 |
+
max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 54 |
+
"""
|
| 55 |
+
Entry point for direct execution via uv run or python -m.
|
| 56 |
+
|
| 57 |
+
This function enables running the server without Docker:
|
| 58 |
+
uv run --project . server
|
| 59 |
+
uv run --project . server --port 8001
|
| 60 |
+
python -m data_clean_env.server.app
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
host: Host address to bind to (default: "0.0.0.0")
|
| 64 |
+
port: Port number to listen on (default: 8000)
|
| 65 |
+
|
| 66 |
+
For production deployments, consider using uvicorn directly with
|
| 67 |
+
multiple workers:
|
| 68 |
+
uvicorn data_clean_env.server.app:app --workers 4
|
| 69 |
+
"""
|
| 70 |
+
import uvicorn
|
| 71 |
+
|
| 72 |
+
uvicorn.run(app, host=host, port=port)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
# main() is callable here
|
| 77 |
+
import argparse
|
| 78 |
+
|
| 79 |
+
parser = argparse.ArgumentParser()
|
| 80 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 81 |
+
args = parser.parse_args()
|
| 82 |
+
main(port=args.port)
|
server/data_clean_env_environment.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from uuid import uuid4
|
| 3 |
+
from typing import Dict, Any, Optional
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from openenv.core.env_server.interfaces import Environment
|
| 8 |
+
from openenv.core.env_server.types import State
|
| 9 |
+
|
| 10 |
+
from models import DataCleanAction, DataCleanObservation
|
| 11 |
+
|
| 12 |
+
class DataCleanState(State):
|
| 13 |
+
current_df_json: str
|
| 14 |
+
task_name: str
|
| 15 |
+
target_df_json: str
|
| 16 |
+
|
| 17 |
+
class DataCleanEnvironment(Environment):
|
| 18 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self._state = DataCleanState(episode_id=str(uuid4()), step_count=0, current_df_json="", task_name="", target_df_json="")
|
| 22 |
+
self._df: pd.DataFrame = pd.DataFrame()
|
| 23 |
+
self._target_df: pd.DataFrame = pd.DataFrame()
|
| 24 |
+
|
| 25 |
+
def _get_obs(self, feedback: Optional[str] = None, error: Optional[str] = None, done: bool = False, reward: float = 0.0) -> DataCleanObservation:
|
| 26 |
+
schema = str(self._df.dtypes.to_dict())
|
| 27 |
+
missing = str(self._df.isna().sum().to_dict())
|
| 28 |
+
head = self._df.head().to_string()
|
| 29 |
+
return DataCleanObservation(
|
| 30 |
+
df_schema=schema,
|
| 31 |
+
missing_values=missing,
|
| 32 |
+
head=head,
|
| 33 |
+
last_error=error,
|
| 34 |
+
feedback=feedback,
|
| 35 |
+
done=done,
|
| 36 |
+
reward=reward,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def reset(self, seed: Optional[int] = None, episode_id: Optional[str] = None, task: str = "easy_clean", **kwargs: Any) -> DataCleanObservation:
|
| 40 |
+
self._state = DataCleanState(episode_id=str(uuid4()), step_count=0, current_df_json="", task_name=task, target_df_json="")
|
| 41 |
+
|
| 42 |
+
if task == "easy_clean":
|
| 43 |
+
self._df = pd.DataFrame({"id": [1, 2, 3], "age": [25.0, np.nan, 30.0]})
|
| 44 |
+
self._target_df = pd.DataFrame({"id": [1, 2, 3], "age": [25.0, 0.0, 30.0]})
|
| 45 |
+
elif task == "medium_clean":
|
| 46 |
+
self._df = pd.DataFrame({
|
| 47 |
+
"name": ["Alice", "Bob", "Charlie", None],
|
| 48 |
+
"age": [25.0, np.nan, 30.0, 22.0],
|
| 49 |
+
"ignore_me": [1, 2, 3, 4]
|
| 50 |
+
})
|
| 51 |
+
self._target_df = pd.DataFrame({
|
| 52 |
+
"name": ["Alice", "Bob", "Charlie"],
|
| 53 |
+
"age": [25.0, np.nan, 30.0],
|
| 54 |
+
}).dropna(subset=["name", "age"])
|
| 55 |
+
self._target_df = self._target_df.reset_index(drop=True)
|
| 56 |
+
elif task == "hard_clean":
|
| 57 |
+
self._df = pd.DataFrame({
|
| 58 |
+
"EmployeeID": ["E1", "E2", "E3"],
|
| 59 |
+
"Dept": ["IT", "HR", "IT"],
|
| 60 |
+
"Salary": ["5000", np.nan, "6000"],
|
| 61 |
+
"JoinDate": [np.nan, "2020-01-01", "2021-01-01"]
|
| 62 |
+
})
|
| 63 |
+
self._target_df = pd.DataFrame({
|
| 64 |
+
"emp_id": ["E1", "E2", "E3"],
|
| 65 |
+
"Salary": [5000.0, 0.0, 6000.0],
|
| 66 |
+
"JoinDate": ["2000-01-01", "2020-01-01", "2021-01-01"]
|
| 67 |
+
})
|
| 68 |
+
else:
|
| 69 |
+
self._df = pd.DataFrame({"col": [1, 2]})
|
| 70 |
+
self._target_df = pd.DataFrame({"col": [1, 2]})
|
| 71 |
+
|
| 72 |
+
self._state.current_df_json = self._df.to_json()
|
| 73 |
+
self._state.target_df_json = self._target_df.to_json()
|
| 74 |
+
|
| 75 |
+
return self._get_obs(feedback=f"Started task {task}.")
|
| 76 |
+
|
| 77 |
+
def step(self, action: DataCleanAction) -> DataCleanObservation: # type: ignore[override]
|
| 78 |
+
self._state.step_count += 1
|
| 79 |
+
reward = 0.0
|
| 80 |
+
error = None
|
| 81 |
+
feedback = None
|
| 82 |
+
done = False
|
| 83 |
+
|
| 84 |
+
if action.action_type == "submit":
|
| 85 |
+
done = True
|
| 86 |
+
score = self._grade()
|
| 87 |
+
reward = score # Final reward based on grader
|
| 88 |
+
feedback = f"Submitted. Final score: {score}"
|
| 89 |
+
return self._get_obs(feedback=feedback, done=done, reward=reward)
|
| 90 |
+
|
| 91 |
+
col = action.column_name
|
| 92 |
+
val = action.value
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
if col and col not in self._df.columns:
|
| 96 |
+
raise ValueError(f"Column '{col}' not found.")
|
| 97 |
+
|
| 98 |
+
if action.action_type == "fill_na":
|
| 99 |
+
if not col or val is None: raise ValueError("fill_na requires column_name and value.")
|
| 100 |
+
# Basic inference of type
|
| 101 |
+
try:
|
| 102 |
+
typed_val = float(val) if '.' in val else int(val)
|
| 103 |
+
except ValueError:
|
| 104 |
+
typed_val = val
|
| 105 |
+
self._df[col] = self._df[col].fillna(typed_val)
|
| 106 |
+
feedback = f"Filled NaNs in {col} with {val}."
|
| 107 |
+
reward = 0.1
|
| 108 |
+
|
| 109 |
+
elif action.action_type == "drop_na":
|
| 110 |
+
if not col: raise ValueError("drop_na requires column_name.")
|
| 111 |
+
self._df = self._df.dropna(subset=[col])
|
| 112 |
+
self._df = self._df.reset_index(drop=True)
|
| 113 |
+
feedback = f"Dropped rows with NaNs in {col}."
|
| 114 |
+
reward = 0.1
|
| 115 |
+
|
| 116 |
+
elif action.action_type == "drop_column":
|
| 117 |
+
if not col: raise ValueError("drop_column requires column_name.")
|
| 118 |
+
self._df = self._df.drop(columns=[col])
|
| 119 |
+
feedback = f"Dropped column {col}."
|
| 120 |
+
reward = 0.1
|
| 121 |
+
|
| 122 |
+
elif action.action_type == "rename_column":
|
| 123 |
+
if not col or not val: raise ValueError("rename_column requires column_name and value.")
|
| 124 |
+
self._df = self._df.rename(columns={col: val})
|
| 125 |
+
feedback = f"Renamed column {col} to {val}."
|
| 126 |
+
reward = 0.1
|
| 127 |
+
|
| 128 |
+
elif action.action_type == "change_type":
|
| 129 |
+
if not col or not val: raise ValueError("change_type requires column_name and value.")
|
| 130 |
+
if val == "int": self._df[col] = self._df[col].astype(int)
|
| 131 |
+
elif val == "float": self._df[col] = self._df[col].astype(float)
|
| 132 |
+
elif val == "str": self._df[col] = self._df[col].astype(str)
|
| 133 |
+
else: raise ValueError("Type must be 'int', 'float', or 'str'.")
|
| 134 |
+
feedback = f"Changed type of {col} to {val}."
|
| 135 |
+
reward = 0.1
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
error = str(e)
|
| 139 |
+
reward = -0.05
|
| 140 |
+
|
| 141 |
+
self._state.current_df_json = self._df.to_json()
|
| 142 |
+
return self._get_obs(feedback=feedback, error=error, done=done, reward=reward)
|
| 143 |
+
|
| 144 |
+
def _grade(self) -> float:
|
| 145 |
+
task = self._state.task_name
|
| 146 |
+
score = 0.0
|
| 147 |
+
|
| 148 |
+
if task == "easy_clean":
|
| 149 |
+
if "age" in self._df.columns and self._df["age"].isna().sum() == 0:
|
| 150 |
+
if (self._df["age"] == self._target_df["age"]).all():
|
| 151 |
+
score = 1.0
|
| 152 |
+
|
| 153 |
+
elif task == "medium_clean":
|
| 154 |
+
max_score = 3.0
|
| 155 |
+
current_score = 0.0
|
| 156 |
+
if "name" in self._df.columns and self._df["name"].isna().sum() == 0:
|
| 157 |
+
current_score += 1.0
|
| 158 |
+
if "age" in self._df.columns and self._df["age"].isna().sum() == 0:
|
| 159 |
+
current_score += 1.0
|
| 160 |
+
if "ignore_me" not in self._df.columns:
|
| 161 |
+
current_score += 1.0
|
| 162 |
+
score = current_score / max_score
|
| 163 |
+
|
| 164 |
+
elif task == "hard_clean":
|
| 165 |
+
max_score = 4.0
|
| 166 |
+
current_score = 0.0
|
| 167 |
+
if "emp_id" in self._df.columns:
|
| 168 |
+
current_score += 1.0
|
| 169 |
+
if "Dept" not in self._df.columns:
|
| 170 |
+
current_score += 1.0
|
| 171 |
+
if "Salary" in self._df.columns and self._df["Salary"].isna().sum() == 0 and pd.api.types.is_numeric_dtype(self._df["Salary"]):
|
| 172 |
+
current_score += 1.0
|
| 173 |
+
if "JoinDate" in self._df.columns and self._df["JoinDate"].isna().sum() == 0:
|
| 174 |
+
current_score += 1.0
|
| 175 |
+
score = current_score / max_score
|
| 176 |
+
|
| 177 |
+
return score
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def state(self) -> State:
|
| 181 |
+
return self._state
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|