anugrah55 commited on
Commit
7b49766
·
verified ·
1 Parent(s): d442355

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  app_port: 8000
 
9
  tags:
10
  - openenv
11
  ---
@@ -98,7 +99,7 @@ Our reward function is designed for efficient RL convergence:
98
  - ✅ **Typed Models**: Fully Pydantic-powered `Observation` and `Action`.
99
  - ✅ **API Standard**: Implements `step()`, `reset()`, and `state()`.
100
  - ✅ **Strict Logs**: Emits `[START]`, `[STEP]`, and `[END]` traces exactly as required.
101
- - ✅ **Robustness**: Handles network timeouts and invalid JSON gracefully.
102
 
103
  ---
104
  Built with ❤️ for the Meta & Hugging Face OpenEnv Hackathon.
 
6
  sdk: docker
7
  pinned: false
8
  app_port: 8000
9
+ base_path: /web
10
  tags:
11
  - openenv
12
  ---
 
99
  - ✅ **Typed Models**: Fully Pydantic-powered `Observation` and `Action`.
100
  - ✅ **API Standard**: Implements `step()`, `reset()`, and `state()`.
101
  - ✅ **Strict Logs**: Emits `[START]`, `[STEP]`, and `[END]` traces exactly as required.
102
+ - ✅ **Robustness**: Handles network timeouts and invalid JSON carefully.
103
 
104
  ---
105
  Built with ❤️ for the Meta & Hugging Face OpenEnv Hackathon.
client.py CHANGED
@@ -1,19 +1,22 @@
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, DataCleanState]
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", ""),
@@ -28,11 +31,11 @@ class DataCleanEnv(
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),
@@ -41,7 +44,7 @@ class DataCleanEnv(
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:
 
1
+ from typing import Any, Dict, Optional
2
 
3
  from openenv.core.client_types import StepResult
 
4
  from openenv.core import EnvClient
5
 
6
+ try:
7
+ from .models import DataCleanAction, DataCleanObservation
8
+ from .server.data_clean_env_environment import DataCleanState
9
+ except ImportError:
10
+ from models import DataCleanAction, DataCleanObservation
11
+ from server.data_clean_env_environment import DataCleanState
12
 
13
  class DataCleanEnv(
14
  EnvClient[DataCleanAction, DataCleanObservation, DataCleanState]
15
  ):
16
+ def _step_payload(self, action: DataCleanAction) -> Dict[str, Any]:
17
  return action.model_dump()
18
 
19
+ def _parse_result(self, payload: Dict[str, Any]) -> StepResult[DataCleanObservation]:
20
  obs_data = payload.get("observation", {})
21
  observation = DataCleanObservation(
22
  df_schema=obs_data.get("df_schema", ""),
 
31
 
32
  return StepResult(
33
  observation=observation,
34
+ reward=payload.get("reward", 0.0),
35
  done=payload.get("done", False),
36
  )
37
 
38
+ def _parse_state(self, payload: Dict[str, Any]) -> DataCleanState:
39
  return DataCleanState(
40
  episode_id=payload.get("episode_id", ""),
41
  step_count=payload.get("step_count", 0),
 
44
  target_df_json=payload.get("target_df_json", ""),
45
  )
46
 
47
+ async def get_client(image_name: Optional[str] = None) -> DataCleanEnv:
48
  if image_name:
49
  client = await DataCleanEnv.from_docker_image(image_name)
50
  else:
inference.py CHANGED
@@ -131,14 +131,18 @@ async def run_task(task_name: str, client: OpenAI, env_client) -> None:
131
  log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
132
 
133
  async def main() -> None:
 
 
 
134
  if HF_TOKEN is None:
135
- raise ValueError("HF_TOKEN environment variable is required")
 
 
 
 
136
 
137
  client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
138
  image_name = os.getenv("LOCAL_IMAGE_NAME") or os.getenv("IMAGE_NAME")
139
-
140
- task_name_env = os.getenv("DATA_CLEAN_ENV_TASK")
141
- tasks_to_run = [task_name_env] if task_name_env else ["easy_clean", "medium_clean", "hard_clean"]
142
 
143
  try:
144
  env_client = await get_client(image_name)
 
131
  log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
132
 
133
  async def main() -> None:
134
+ task_name_env = os.getenv("DATA_CLEAN_ENV_TASK")
135
+ tasks_to_run = [task_name_env] if task_name_env else ["easy_clean", "medium_clean", "hard_clean"]
136
+
137
  if HF_TOKEN is None:
138
+ print("[DEBUG] HF_TOKEN environment variable is required", flush=True)
139
+ for task in tasks_to_run:
140
+ log_start(task=task, env=BENCHMARK, model=MODEL_NAME)
141
+ log_end(success=False, steps=0, score=0.0, rewards=[])
142
+ return
143
 
144
  client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
145
  image_name = os.getenv("LOCAL_IMAGE_NAME") or os.getenv("IMAGE_NAME")
 
 
 
146
 
147
  try:
148
  env_client = await get_client(image_name)
local_smoke.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ from models import DataCleanAction
4
+ from client import get_client
5
+
6
+
7
+ async def test_env():
8
+ # Test without docker first
9
+ client = await get_client(None)
10
+
11
+ try:
12
+ print("Resetting...")
13
+ result = await client.reset(task="easy_clean")
14
+ print("Reset result:", result)
15
+
16
+ print("Sending action...")
17
+ action = DataCleanAction(action_type="fill_na", column_name="age", value="0")
18
+ result = await client.step(action)
19
+ print("Step result:", result)
20
+
21
+ print("Submitting...")
22
+ action = DataCleanAction(action_type="submit")
23
+ result = await client.step(action)
24
+ print("Submit result:", result)
25
+ print("Success!")
26
+ finally:
27
+ await client.close()
28
+
29
+ if __name__ == "__main__":
30
+ asyncio.run(test_env())
server/app.py CHANGED
@@ -35,9 +35,12 @@ except Exception as e: # pragma: no cover
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
@@ -50,7 +53,7 @@ app = create_app(
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
 
 
35
  "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
  ) from e
37
 
38
+ try:
39
+ from ..models import DataCleanAction, DataCleanObservation
40
+ from .data_clean_env_environment import DataCleanEnvironment
41
+ except ImportError:
42
+ from models import DataCleanAction, DataCleanObservation
43
+ from .data_clean_env_environment import DataCleanEnvironment
44
 
45
 
46
  # Create the app with web interface and README integration
 
53
  )
54
 
55
 
56
+ def main(host: str = "0.0.0.0", port: int = 8000) -> None:
57
  """
58
  Entry point for direct execution via uv run or python -m.
59
 
server/data_clean_env_environment.py CHANGED
@@ -1,13 +1,15 @@
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
@@ -21,6 +23,29 @@ class DataCleanEnvironment(Environment):
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())
@@ -146,39 +171,50 @@ class DataCleanEnvironment(Environment):
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
- try:
151
- if len(self._df["age"]) == len(self._target_df["age"]) and (self._df["age"] == self._target_df["age"]).all():
152
- score = 1.0
153
- except Exception:
154
- pass
155
 
156
  elif task == "medium_clean":
157
- max_score = 3.0
158
  current_score = 0.0
159
- if "name" in self._df.columns and self._df["name"].isna().sum() == 0:
 
160
  current_score += 1.0
161
- if "age" in self._df.columns and self._df["age"].isna().sum() == 0:
 
 
162
  current_score += 1.0
163
- if "ignore_me" not in self._df.columns:
164
  current_score += 1.0
 
165
  score = current_score / max_score
166
 
167
  elif task == "hard_clean":
168
  max_score = 4.0
169
  current_score = 0.0
170
- if "emp_id" in self._df.columns:
 
171
  current_score += 1.0
172
- if "Dept" not in self._df.columns:
173
  current_score += 1.0
174
- if "Salary" in self._df.columns and self._df["Salary"].isna().sum() == 0 and pd.api.types.is_numeric_dtype(self._df["Salary"]):
175
  current_score += 1.0
176
- if "JoinDate" in self._df.columns and self._df["JoinDate"].isna().sum() == 0:
177
  current_score += 1.0
 
178
  score = current_score / max_score
179
 
180
  return max(0.01, min(0.99, float(score)))
181
 
182
  @property
183
- def state(self) -> State:
184
  return self._state
 
 
1
  from uuid import uuid4
2
+ from typing import Any, Optional
3
  import pandas as pd
4
  import numpy as np
5
 
6
  from openenv.core.env_server.interfaces import Environment
7
  from openenv.core.env_server.types import State
8
 
9
+ try:
10
+ from ..models import DataCleanAction, DataCleanObservation
11
+ except ImportError:
12
+ from models import DataCleanAction, DataCleanObservation
13
 
14
  class DataCleanState(State):
15
  current_df_json: str
 
23
  self._state = DataCleanState(episode_id=str(uuid4()), step_count=0, current_df_json="", task_name="", target_df_json="")
24
  self._df: pd.DataFrame = pd.DataFrame()
25
  self._target_df: pd.DataFrame = pd.DataFrame()
26
+
27
+ def _columns_match_target(self) -> bool:
28
+ return list(self._df.columns) == list(self._target_df.columns)
29
+
30
+ def _series_matches_target(self, column_name: str) -> bool:
31
+ if column_name not in self._df.columns or column_name not in self._target_df.columns:
32
+ return False
33
+
34
+ left = self._df[column_name].reset_index(drop=True)
35
+ right = self._target_df[column_name].reset_index(drop=True)
36
+ return left.equals(right)
37
+
38
+ def _numeric_series_matches_target(self, column_name: str) -> bool:
39
+ if column_name not in self._df.columns or column_name not in self._target_df.columns:
40
+ return False
41
+
42
+ try:
43
+ left = pd.to_numeric(self._df[column_name]).reset_index(drop=True)
44
+ right = pd.to_numeric(self._target_df[column_name]).reset_index(drop=True)
45
+ except Exception:
46
+ return False
47
+
48
+ return left.equals(right)
49
 
50
  def _get_obs(self, feedback: Optional[str] = None, error: Optional[str] = None, done: bool = False, reward: float = 0.0) -> DataCleanObservation:
51
  schema = str(self._df.dtypes.to_dict())
 
171
  score = 0.0
172
 
173
  if task == "easy_clean":
174
+ max_score = 3.0
175
+ current_score = 0.0
176
+
177
+ if self._columns_match_target():
178
+ current_score += 1.0
179
  if "age" in self._df.columns and self._df["age"].isna().sum() == 0:
180
+ current_score += 1.0
181
+ if self._series_matches_target("age"):
182
+ current_score += 1.0
183
+
184
+ score = current_score / max_score
185
 
186
  elif task == "medium_clean":
187
+ max_score = 4.0
188
  current_score = 0.0
189
+
190
+ if self._columns_match_target():
191
  current_score += 1.0
192
+ if len(self._df) == len(self._target_df):
193
+ current_score += 1.0
194
+ if self._series_matches_target("name"):
195
  current_score += 1.0
196
+ if self._numeric_series_matches_target("age"):
197
  current_score += 1.0
198
+
199
  score = current_score / max_score
200
 
201
  elif task == "hard_clean":
202
  max_score = 4.0
203
  current_score = 0.0
204
+
205
+ if self._columns_match_target():
206
  current_score += 1.0
207
+ if self._series_matches_target("emp_id"):
208
  current_score += 1.0
209
+ if self._numeric_series_matches_target("Salary"):
210
  current_score += 1.0
211
+ if self._series_matches_target("JoinDate"):
212
  current_score += 1.0
213
+
214
  score = current_score / max_score
215
 
216
  return max(0.01, min(0.99, float(score)))
217
 
218
  @property
219
+ def state(self) -> DataCleanState:
220
  return self._state
tests/conftest.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
tests/test_environment.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+
3
+ from models import DataCleanAction
4
+ from server.app import app
5
+ from server.data_clean_env_environment import DataCleanEnvironment
6
+
7
+
8
+ def test_easy_clean_solution_scores_expected_value() -> None:
9
+ env = DataCleanEnvironment()
10
+ env.reset(task="easy_clean")
11
+ env.step(DataCleanAction(action_type="fill_na", column_name="age", value="0"))
12
+
13
+ result = env.step(DataCleanAction(action_type="submit"))
14
+
15
+ assert result.done is True
16
+ assert result.reward == 0.99
17
+
18
+
19
+ def test_medium_clean_wrong_solution_is_not_near_perfect() -> None:
20
+ env = DataCleanEnvironment()
21
+ env.reset(task="medium_clean")
22
+ env.step(DataCleanAction(action_type="fill_na", column_name="age", value="0"))
23
+ env.step(DataCleanAction(action_type="drop_na", column_name="name"))
24
+ env.step(DataCleanAction(action_type="drop_column", column_name="ignore_me"))
25
+
26
+ result = env.step(DataCleanAction(action_type="submit"))
27
+
28
+ assert result.reward < 0.99
29
+
30
+
31
+ def test_hard_clean_wrong_join_date_is_not_near_perfect() -> None:
32
+ env = DataCleanEnvironment()
33
+ env.reset(task="hard_clean")
34
+ env.step(DataCleanAction(action_type="rename_column", column_name="EmployeeID", value="emp_id"))
35
+ env.step(DataCleanAction(action_type="drop_column", column_name="Dept"))
36
+ env.step(DataCleanAction(action_type="fill_na", column_name="Salary", value="0"))
37
+ env.step(DataCleanAction(action_type="change_type", column_name="Salary", value="float"))
38
+ env.step(DataCleanAction(action_type="fill_na", column_name="JoinDate", value="wrong-date"))
39
+
40
+ result = env.step(DataCleanAction(action_type="submit"))
41
+
42
+ assert result.reward < 0.99
43
+
44
+
45
+ def test_state_endpoint_keeps_core_state_fields() -> None:
46
+ client = TestClient(app)
47
+ client.post("/reset", json={"task": "easy_clean"})
48
+
49
+ response = client.get("/state")
50
+
51
+ assert response.status_code == 200
52
+ payload = response.json()
53
+ assert "episode_id" in payload
54
+ assert "step_count" in payload