Spaces:
Sleeping
Sleeping
| """ | |
| FastAPI server for the Data Cleaning OpenEnv environment. | |
| Endpoints | |
| --------- | |
| GET / — Redirects to interactive API docs | |
| POST /reset — Start a new episode | |
| POST /step — Apply a cleaning action | |
| GET /state — Inspect current state (read-only) | |
| GET /health — Liveness probe | |
| GET /info — Environment metadata + available actions | |
| GET /tasks — List all tasks with metadata | |
| POST /grade — Call grader directly (useful for judges) | |
| GET /actions — List all supported actions with parameter docs | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from typing import Any, Dict | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from environment.env import DataCleaningEnv, TASK_CONFIG | |
| from environment.actions import SUPPORTED_ACTIONS | |
| from environment.models import ( | |
| DataCleaningAction, | |
| ResetRequest, | |
| ResetResult, | |
| StateResult, | |
| StepRequest, | |
| StepResult, | |
| ) | |
| # ============================================================================ | |
| # App setup | |
| # ============================================================================ | |
| app = FastAPI( | |
| title="Data Cleaning OpenEnv", | |
| description=( | |
| "A real-world AI environment for training agents to clean and preprocess " | |
| "tabular data. Implements the full OpenEnv spec: typed observations, actions, " | |
| "rewards, and episode management across 3 difficulty levels.\n\n" | |
| "**Tasks**: `csv-doctor` (Easy) → `schema-enforcer` (Medium) → `pipeline-debugger` (Hard)\n\n" | |
| "**Reward**: Shaped at every step — `score_delta + step_cost + destructive_penalty`" | |
| ), | |
| version="1.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Global environment instance (single-session) | |
| _env = DataCleaningEnv() | |
| # ============================================================================ | |
| # Action documentation | |
| # ============================================================================ | |
| ACTION_DOCS: Dict[str, Dict[str, Any]] = { | |
| "fill_missing": { | |
| "description": "Fill NaN values in a column", | |
| "parameters": { | |
| "column": "str — target column name", | |
| "strategy": "str — mean | median | mode | constant | forward_fill | backward_fill | drop | unknown", | |
| "fill_value": "Any (optional) — used when strategy='constant'", | |
| }, | |
| "example": {"column": "age", "strategy": "median"}, | |
| }, | |
| "drop_duplicates": { | |
| "description": "Remove duplicate rows", | |
| "parameters": { | |
| "subset": "List[str] (optional) — columns to consider for dedup", | |
| "keep": "str (optional) — first | last | False", | |
| }, | |
| "example": {"subset": ["customer_id", "product", "price"]}, | |
| }, | |
| "cast_column": { | |
| "description": "Change a column's data type", | |
| "parameters": { | |
| "column": "str — target column", | |
| "dtype": "str — int | float | string | datetime | bool | category", | |
| }, | |
| "example": {"column": "age", "dtype": "int"}, | |
| }, | |
| "normalize_format": { | |
| "description": "Standardise values to a canonical format", | |
| "parameters": { | |
| "column": "str — target column", | |
| "format_type": "str — phone | email | date | text_case | strip_currency | zip_code", | |
| "output_format": "str (optional) — for date: strftime format; for text_case: lower|upper|title", | |
| }, | |
| "examples": [ | |
| {"column": "phone", "format_type": "phone"}, | |
| {"column": "salary", "format_type": "strip_currency"}, | |
| {"column": "birth_date", "format_type": "date", "output_format": "%Y-%m-%d"}, | |
| ], | |
| }, | |
| "standardize_text": { | |
| "description": "Apply one or more text normalisation operations", | |
| "parameters": { | |
| "column": "str — target column", | |
| "operations": "List[str] — strip | lower | upper | title | remove_extra_spaces", | |
| }, | |
| "example": {"column": "name", "operations": ["title"]}, | |
| }, | |
| "clip_outliers": { | |
| "description": "Clip statistical outliers in a numeric column", | |
| "parameters": { | |
| "column": "str — target column", | |
| "method": "str — iqr | zscore | drop", | |
| "threshold": "float (optional) — IQR multiplier or Z-score threshold (default 1.5)", | |
| }, | |
| "example": {"column": "price", "method": "iqr", "threshold": 1.5}, | |
| }, | |
| "fix_referential_integrity": { | |
| "description": "Remove or flag rows with foreign-key violations", | |
| "parameters": { | |
| "child_column": "str — FK column in main table", | |
| "parent_table": "str — auxiliary table name (e.g. 'customers')", | |
| "parent_column": "str — PK column in parent table", | |
| "action": "str — drop | flag", | |
| }, | |
| "example": { | |
| "child_column": "customer_id", | |
| "parent_table": "customers", | |
| "parent_column": "customer_id", | |
| "action": "drop", | |
| }, | |
| }, | |
| "merge_tables": { | |
| "description": "Merge an auxiliary table into the main dataset", | |
| "parameters": { | |
| "right_table": "str — auxiliary table name", | |
| "left_on": "str — join key in main table", | |
| "right_on": "str — join key in auxiliary table", | |
| "how": "str — left | inner | outer | right", | |
| "columns": "List[str] (optional) — columns to bring in from right table", | |
| }, | |
| "example": { | |
| "right_table": "customers", | |
| "left_on": "customer_id", | |
| "right_on": "customer_id", | |
| "how": "left", | |
| "columns": ["segment"], | |
| }, | |
| }, | |
| "apply_regex": { | |
| "description": "Apply a regex substitution across a column", | |
| "parameters": { | |
| "column": "str — target column", | |
| "pattern": "str — regex pattern", | |
| "replacement": "str — replacement string", | |
| }, | |
| "example": {"column": "salary", "pattern": r"[\$,]", "replacement": ""}, | |
| }, | |
| "drop_column": { | |
| "description": "Remove a column entirely", | |
| "parameters": {"column": "str — column to drop"}, | |
| "example": {"column": "unnamed_col"}, | |
| }, | |
| "drop_rows_by_condition": { | |
| "description": "Drop rows matching a condition", | |
| "parameters": { | |
| "column": "str — target column", | |
| "operator": "str — == | != | > | < | >= | <= | isnull | notnull | contains", | |
| "value": "Any — comparison value", | |
| }, | |
| "example": {"column": "age", "operator": "<", "value": 0}, | |
| }, | |
| "rename_column": { | |
| "description": "Rename a column", | |
| "parameters": { | |
| "old_name": "str — current column name", | |
| "new_name": "str — desired column name", | |
| }, | |
| "example": {"old_name": "Salary $", "new_name": "salary"}, | |
| }, | |
| } | |
| from fastapi.responses import RedirectResponse | |
| async def root(): | |
| """Redirect root to interactive API documentation.""" | |
| return RedirectResponse(url="/docs") | |
| async def health() -> Dict[str, str]: | |
| """Liveness probe — returns 200 OK if the server is running.""" | |
| return {"status": "ok", "version": "1.0.0"} | |
| async def list_tasks() -> Dict[str, Any]: | |
| """List all available tasks with difficulty, max_steps, and reward threshold.""" | |
| return { | |
| "tasks": [ | |
| { | |
| "name": name, | |
| "difficulty": {"csv-doctor": "easy", "schema-enforcer": "medium", "pipeline-debugger": "hard"}[name], | |
| "max_steps": cfg["max_steps"], | |
| "reward_threshold": {"csv-doctor": 0.75, "schema-enforcer": 0.70, "pipeline-debugger": 0.60}[name], | |
| "description": cfg["description"][:200] + "…", | |
| } | |
| for name, cfg in TASK_CONFIG.items() | |
| ] | |
| } | |
| async def list_actions() -> Dict[str, Any]: | |
| """List all supported actions with parameter documentation and examples.""" | |
| return {"actions": ACTION_DOCS, "total": len(ACTION_DOCS)} | |
| async def info() -> Dict[str, Any]: | |
| """Return environment metadata.""" | |
| return { | |
| "name": "data-cleaning-env", | |
| "version": "1.0.0", | |
| "description": "Real-world AI Data Cleaning & Preprocessing OpenEnv environment", | |
| "tasks": list(TASK_CONFIG.keys()), | |
| "supported_actions": SUPPORTED_ACTIONS, | |
| "reward_range": [0.0, 1.0], | |
| "reward_description": "Shaped per-step: score_delta + step_cost(-0.005) + destructive_penalty(-0.10 if >30% rows dropped)", | |
| "docs_url": "/docs", | |
| } | |
| async def reset(request: ResetRequest = ResetRequest()) -> ResetResult: | |
| """ | |
| Start a new episode. | |
| - **task_name**: `csv-doctor` | `schema-enforcer` | `pipeline-debugger` | |
| - **seed**: integer for reproducibility (default 42) | |
| """ | |
| try: | |
| return _env.reset(task_name=request.task_name, seed=request.seed) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| except Exception as exc: | |
| raise HTTPException(status_code=500, detail=f"Reset failed: {exc}") from exc | |
| async def step(request: StepRequest) -> StepResult: | |
| """ | |
| Apply a data-cleaning action. | |
| **Example body:** | |
| ```json | |
| { | |
| "action": { | |
| "action_type": "fill_missing", | |
| "parameters": {"column": "age", "strategy": "median"} | |
| } | |
| } | |
| ``` | |
| See `GET /actions` for all action types and their parameters. | |
| """ | |
| try: | |
| return _env.step(request.action) | |
| except Exception as exc: | |
| raise HTTPException(status_code=500, detail=f"Step failed: {exc}") from exc | |
| async def state() -> StateResult: | |
| """Return a lightweight read-only snapshot of the current episode state.""" | |
| try: | |
| return _env.state() | |
| except Exception as exc: | |
| raise HTTPException(status_code=500, detail=f"State failed: {exc}") from exc | |
| async def grade() -> Dict[str, Any]: | |
| """ | |
| Run the grader on the current dataset state and return the full score breakdown. | |
| This is a **read-only** operation — it does not advance the episode. | |
| Useful for judges and for inspecting per-dimension quality scores. | |
| """ | |
| try: | |
| from environment.graders.graders import grade_easy, grade_medium, grade_hard | |
| import pandas as pd | |
| task = _env._task_name # type: ignore[attr-defined] | |
| df = _env._df # type: ignore[attr-defined] | |
| if task == "csv-doctor": | |
| score, breakdown = grade_easy(df) | |
| elif task == "schema-enforcer": | |
| score, breakdown = grade_medium(df) | |
| elif task == "pipeline-debugger": | |
| customers = _env._aux_dfs.get("customers", pd.DataFrame()) # type: ignore[attr-defined] | |
| orig = _env._original_orders or df # type: ignore[attr-defined] | |
| score, breakdown = grade_hard(df.copy(), customers.copy(), orig.copy()) | |
| else: | |
| raise HTTPException(status_code=400, detail=f"Unknown task: {task}") | |
| return { | |
| "task": task, | |
| "score": round(float(score), 4), | |
| "breakdown": {k: round(float(v), 4) for k, v in breakdown.items()}, | |
| "step_count": _env._step_count, # type: ignore[attr-defined] | |
| "rows": len(df), | |
| "columns": list(df.columns), | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as exc: | |
| raise HTTPException(status_code=500, detail=f"Grade failed: {exc}") from exc | |
| # ============================================================================ | |
| # Entry point | |
| # ============================================================================ | |
| def main() -> None: | |
| """Entry point for the command-line script.""" | |
| import uvicorn | |
| port = int(os.getenv("PORT", "7860")) | |
| uvicorn.run("server.app:app", host="0.0.0.0", port=port, reload=False) | |
| if __name__ == "__main__": | |
| main() | |