anugrah55 commited on
Commit
56ed1f1
·
verified ·
1 Parent(s): 6d6d41d

Upload folder using huggingface_hub

Browse files
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: pink
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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