dataforge-07 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataforge/__init__.py +204 -0
- dataforge/__main__.py +5 -0
- dataforge/agent/__init__.py +16 -0
- dataforge/agent/providers.py +259 -0
- dataforge/agent/scratchpad.py +183 -0
- dataforge/agent/tool_actions.py +343 -0
- dataforge/bench/__init__.py +31 -0
- dataforge/bench/core.py +426 -0
- dataforge/bench/groq_client.py +386 -0
- dataforge/bench/methods.py +443 -0
- dataforge/bench/report.py +309 -0
- dataforge/bench/runner.py +247 -0
- dataforge/causal/__init__.py +21 -0
- dataforge/causal/dag.py +174 -0
- dataforge/causal/pc.py +232 -0
- dataforge/causal/root_cause.py +193 -0
- dataforge/cli/__init__.py +50 -0
- dataforge/cli/audit.py +70 -0
- dataforge/cli/bench.py +154 -0
- dataforge/cli/common.py +267 -0
- dataforge/cli/constraints.py +407 -0
- dataforge/cli/profile.py +147 -0
- dataforge/cli/release.py +166 -0
- dataforge/cli/repair.py +407 -0
- dataforge/cli/revert.py +139 -0
- dataforge/cli/watch.py +144 -0
- dataforge/datasets/__init__.py +25 -0
- dataforge/datasets/embedded/hospital/clean.csv +11 -0
- dataforge/datasets/embedded/hospital/dirty.csv +11 -0
- dataforge/datasets/real_world.py +290 -0
- dataforge/datasets/registry.py +103 -0
- dataforge/detectors/__init__.py +80 -0
- dataforge/detectors/base.py +145 -0
- dataforge/detectors/decimal_shift.py +166 -0
- dataforge/detectors/fd_violation.py +157 -0
- dataforge/detectors/type_mismatch.py +173 -0
- dataforge/engine/__init__.py +39 -0
- dataforge/engine/repair.py +905 -0
- dataforge/env/__init__.py +22 -0
- dataforge/env/environment.py +883 -0
- dataforge/env/observation.py +61 -0
- dataforge/env/openenv_core.py +161 -0
- dataforge/env/reward.py +128 -0
- dataforge/env/server.py +176 -0
- dataforge/evaluation_contract.py +76 -0
- dataforge/fixtures/hospital_10rows.csv +11 -0
- dataforge/fixtures/hospital_schema.yaml +17 -0
- dataforge/http/__init__.py +1 -0
- dataforge/http/problem.py +103 -0
- dataforge/integrations/__init__.py +1 -0
- dataforge/integrations/dbt.py +164 -0
- dataforge/observability.py +76 -0
- dataforge/py.typed +1 -0
- dataforge/release/__init__.py +1 -0
- dataforge/release/doctor.py +367 -0
- dataforge/release/full_vision.py +702 -0
- dataforge/release/gate.py +861 -0
- dataforge/release/playground_check.py +411 -0
- dataforge/repair_contract.py +468 -0
- dataforge/repairers/__init__.py +88 -0
- dataforge/repairers/base.py +77 -0
- dataforge/repairers/decimal_shift.py +43 -0
- dataforge/repairers/fd_violation.py +225 -0
- dataforge/repairers/type_mismatch.py +73 -0
- dataforge/safety/__init__.py +5 -0
- dataforge/safety/adversarial/attack_01_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_02_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_03_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_04_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_05_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_06_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_07_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_08_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_09_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_10_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_11_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_12_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_13_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_14_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_15_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_16_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_17_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_18_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_19_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_20_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_21_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_22_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_23_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_24_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_25_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_26_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_27_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_28_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_29_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_30_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_31_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_32_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_33_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_34_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_35_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_36_row_delete.yaml +11 -0
- dataforge/safety/adversarial/attack_37_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_38_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_39_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_40_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_41_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_42_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_43_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_44_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_45_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_46_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_47_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_48_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_49_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_50_row_delete.yaml +7 -0
- dataforge/safety/constitution.py +307 -0
- dataforge/safety/constitutions/default.yaml +40 -0
- dataforge/safety/filter.py +134 -0
- dataforge/schema_inference.py +620 -0
- dataforge/stores/__init__.py +46 -0
- dataforge/stores/base.py +73 -0
- dataforge/stores/cloud.py +78 -0
- dataforge/stores/csv.py +94 -0
- dataforge/stores/duckdb.py +313 -0
- dataforge/stores/patch_plan.py +178 -0
- dataforge/stores/registry.py +82 -0
- dataforge/stores/repair.py +121 -0
- dataforge/stores/revert.py +22 -0
- dataforge/stores/sql.py +27 -0
- dataforge/table.py +228 -0
- dataforge/transactions/__init__.py +34 -0
- dataforge/transactions/files.py +96 -0
- dataforge/transactions/log.py +613 -0
- dataforge/transactions/revert.py +102 -0
- dataforge/transactions/txn.py +104 -0
- dataforge/ui/__init__.py +1 -0
- dataforge/ui/profile_view.py +136 -0
- dataforge/ui/repair_diff.py +91 -0
- dataforge/verifier/__init__.py +55 -0
- dataforge/verifier/constraint_ir.py +155 -0
- dataforge/verifier/explain.py +47 -0
- dataforge/verifier/gate.py +5 -0
- dataforge/verifier/schema.py +111 -0
- dataforge/verifier/smt.py +433 -0
- dataforge_07-0.1.0.dist-info/METADATA +436 -0
- dataforge_07-0.1.0.dist-info/RECORD +150 -0
- dataforge_07-0.1.0.dist-info/WHEEL +5 -0
- dataforge_07-0.1.0.dist-info/entry_points.txt +3 -0
- dataforge_07-0.1.0.dist-info/licenses/LICENSE +176 -0
- dataforge_07-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Observation builder for the DataForge RL environment.
|
|
2
|
+
|
|
3
|
+
Constructs agent-visible observations containing partial data views,
|
|
4
|
+
scratchpad summaries, tool results, and step budget information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
__all__ = ["DataForgeObservation", "ToolResult"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolResult(BaseModel):
|
|
17
|
+
"""Result of a single tool-use action.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
action_type: The action type that produced this result.
|
|
21
|
+
success: Whether the action succeeded.
|
|
22
|
+
data: Action-specific result data (rows, stats, matches, etc.).
|
|
23
|
+
error: Structured error information if the action failed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
action_type: str
|
|
27
|
+
success: bool = True
|
|
28
|
+
data: Any = None
|
|
29
|
+
error: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
model_config = {"frozen": True}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DataForgeObservation(BaseModel):
|
|
35
|
+
"""Agent-visible observation returned after each environment step.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
visible_rows: Dataset rows returned by INSPECT_ROWS or reset.
|
|
39
|
+
detector_hints: Optional hints from detectors (partial ground truth).
|
|
40
|
+
scratchpad_summary: Compact summary of the agent's scratchpad.
|
|
41
|
+
step_budget_remaining: Steps left before auto-finalize.
|
|
42
|
+
tool_usage_history: Last 5 tool results for context.
|
|
43
|
+
latest_result: Result of the most recent action.
|
|
44
|
+
done: Whether the episode has ended.
|
|
45
|
+
reward: Step reward.
|
|
46
|
+
cumulative_reward: Running total reward for the episode.
|
|
47
|
+
metadata: Additional key-value metadata.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
visible_rows: list[dict[str, Any]] | None = None
|
|
51
|
+
detector_hints: list[str] | None = None
|
|
52
|
+
scratchpad_summary: str = ""
|
|
53
|
+
step_budget_remaining: int = 0
|
|
54
|
+
tool_usage_history: list[ToolResult] = Field(default_factory=list)
|
|
55
|
+
latest_result: ToolResult | None = None
|
|
56
|
+
done: bool = False
|
|
57
|
+
reward: float = 0.0
|
|
58
|
+
cumulative_reward: float = 0.0
|
|
59
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
model_config = {"frozen": True}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""OpenEnv-core adapter for the DataForge RL environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from dataforge.env.environment import DataForgeEnv
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
|
|
13
|
+
class OpenEnvAction(BaseModel):
|
|
14
|
+
"""Typed stand-in for openenv-core's action model."""
|
|
15
|
+
|
|
16
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
class OpenEnvObservation(BaseModel):
|
|
19
|
+
"""Typed stand-in for openenv-core's observation model."""
|
|
20
|
+
|
|
21
|
+
done: bool = False
|
|
22
|
+
reward: float | None = None
|
|
23
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
class OpenEnvEnvironment:
|
|
26
|
+
"""Typed stand-in for openenv-core's environment base."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None: ...
|
|
29
|
+
|
|
30
|
+
def create_app(*args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
"""Typed stand-in for openenv-core's FastAPI app factory."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
from openenv.core.env_server import Action as OpenEnvAction
|
|
37
|
+
from openenv.core.env_server import Environment as OpenEnvEnvironment
|
|
38
|
+
from openenv.core.env_server import Observation as OpenEnvObservation
|
|
39
|
+
from openenv.core.env_server import create_app
|
|
40
|
+
except ImportError as exc: # pragma: no cover - exercised only without openenv extra
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"The OpenEnv adapter requires the openenv extra: pip install 'dataforge[openenv]'."
|
|
43
|
+
) from exc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DataForgeOpenEnvAction(OpenEnvAction):
|
|
47
|
+
"""OpenEnv action wrapper for DataForge's typed action payloads."""
|
|
48
|
+
|
|
49
|
+
action_type: str = Field(min_length=1)
|
|
50
|
+
row_indices: list[int] | None = None
|
|
51
|
+
column_names: list[str] | None = None
|
|
52
|
+
query: str | None = None
|
|
53
|
+
sql: str | None = None
|
|
54
|
+
test_type: str | None = None
|
|
55
|
+
test: str | None = None
|
|
56
|
+
column: str | None = None
|
|
57
|
+
threshold: float | None = None
|
|
58
|
+
pattern: str | None = None
|
|
59
|
+
regex: str | None = None
|
|
60
|
+
expect_match: bool | None = None
|
|
61
|
+
claim: str | None = None
|
|
62
|
+
affected_rows: list[int] | None = None
|
|
63
|
+
affected_columns: list[str] | None = None
|
|
64
|
+
root_cause_type: str | None = None
|
|
65
|
+
error_indices: list[int] | None = None
|
|
66
|
+
row: int | None = None
|
|
67
|
+
issue_type: str | None = None
|
|
68
|
+
new_value: str | None = None
|
|
69
|
+
proposed_value: str | None = None
|
|
70
|
+
justification: str | None = None
|
|
71
|
+
fix_type: str | None = None
|
|
72
|
+
|
|
73
|
+
def as_dataforge_payload(self) -> dict[str, Any]:
|
|
74
|
+
"""Return the action payload expected by ``DataForgeEnv.step``."""
|
|
75
|
+
payload: dict[str, Any] = self.model_dump(exclude_none=True)
|
|
76
|
+
payload.pop("metadata", None)
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DataForgeOpenEnvObservation(OpenEnvObservation):
|
|
81
|
+
"""OpenEnv observation model mirroring DataForge's native observation."""
|
|
82
|
+
|
|
83
|
+
visible_rows: list[dict[str, Any]] | None = None
|
|
84
|
+
detector_hints: list[str] | None = None
|
|
85
|
+
scratchpad_summary: str = ""
|
|
86
|
+
step_budget_remaining: int = 0
|
|
87
|
+
tool_usage_history: list[dict[str, Any]] = Field(default_factory=list)
|
|
88
|
+
latest_result: dict[str, Any] | None = None
|
|
89
|
+
cumulative_reward: float = 0.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _to_openenv_observation(payload: dict[str, Any]) -> DataForgeOpenEnvObservation:
|
|
93
|
+
"""Convert a native DataForge observation dictionary into OpenEnv shape."""
|
|
94
|
+
return DataForgeOpenEnvObservation(
|
|
95
|
+
visible_rows=payload.get("visible_rows"),
|
|
96
|
+
detector_hints=payload.get("detector_hints"),
|
|
97
|
+
scratchpad_summary=str(payload.get("scratchpad_summary", "")),
|
|
98
|
+
step_budget_remaining=int(payload.get("step_budget_remaining", 0)),
|
|
99
|
+
tool_usage_history=list(payload.get("tool_usage_history") or []),
|
|
100
|
+
latest_result=payload.get("latest_result"),
|
|
101
|
+
done=bool(payload.get("done", False)),
|
|
102
|
+
reward=payload.get("reward"),
|
|
103
|
+
cumulative_reward=float(payload.get("cumulative_reward", 0.0)),
|
|
104
|
+
metadata=dict(payload.get("metadata") or {}),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DataForgeOpenEnv(OpenEnvEnvironment):
|
|
109
|
+
"""OpenEnv-native environment wrapper."""
|
|
110
|
+
|
|
111
|
+
SUPPORTS_CONCURRENT_SESSIONS = True
|
|
112
|
+
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
super().__init__()
|
|
115
|
+
self._env = DataForgeEnv()
|
|
116
|
+
self._last_observation: DataForgeOpenEnvObservation | None = None
|
|
117
|
+
|
|
118
|
+
def reset(
|
|
119
|
+
self,
|
|
120
|
+
seed: int | None = None,
|
|
121
|
+
episode_id: str | None = None,
|
|
122
|
+
**kwargs: Any,
|
|
123
|
+
) -> DataForgeOpenEnvObservation:
|
|
124
|
+
"""Reset the wrapped DataForge environment."""
|
|
125
|
+
del episode_id, kwargs
|
|
126
|
+
result = self._env.reset(seed=seed)
|
|
127
|
+
observation = _to_openenv_observation(result.observation.model_dump(mode="json"))
|
|
128
|
+
self._last_observation = observation
|
|
129
|
+
return observation
|
|
130
|
+
|
|
131
|
+
def step(
|
|
132
|
+
self,
|
|
133
|
+
action: DataForgeOpenEnvAction,
|
|
134
|
+
timeout_s: float | None = None,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> DataForgeOpenEnvObservation:
|
|
137
|
+
"""Step the wrapped DataForge environment."""
|
|
138
|
+
del timeout_s, kwargs
|
|
139
|
+
result = self._env.step(action.as_dataforge_payload())
|
|
140
|
+
observation = _to_openenv_observation(result.observation.model_dump(mode="json"))
|
|
141
|
+
self._last_observation = observation
|
|
142
|
+
return observation
|
|
143
|
+
|
|
144
|
+
def state(self) -> DataForgeOpenEnvObservation:
|
|
145
|
+
"""Return the latest observation or reset lazily."""
|
|
146
|
+
if self._last_observation is None:
|
|
147
|
+
return self.reset()
|
|
148
|
+
return self._last_observation
|
|
149
|
+
|
|
150
|
+
def close(self) -> None:
|
|
151
|
+
"""Close the wrapped environment."""
|
|
152
|
+
self._env.close()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
app = create_app(
|
|
156
|
+
DataForgeOpenEnv,
|
|
157
|
+
DataForgeOpenEnvAction,
|
|
158
|
+
DataForgeOpenEnvObservation,
|
|
159
|
+
env_name="dataforge-env",
|
|
160
|
+
max_concurrent_envs=64,
|
|
161
|
+
)
|
dataforge/env/reward.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Reward engine for the DataForge RL environment.
|
|
2
|
+
|
|
3
|
+
All constants and formulas are derived bit-for-bit from REWARD_DESIGN.md.
|
|
4
|
+
|
|
5
|
+
Terminal score: detection_rate * 0.40 + fix_rate * 0.60 - false_positives * fp_rate
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DETECTION_WEIGHT",
|
|
14
|
+
"FALSE_POS_PENALTY_RATE",
|
|
15
|
+
"FIX_WEIGHT",
|
|
16
|
+
"LATE_STEP_THRESHOLD",
|
|
17
|
+
"P_FALSE_POS",
|
|
18
|
+
"P_INVALID",
|
|
19
|
+
"P_LATE_STEP",
|
|
20
|
+
"P_REINSPECT",
|
|
21
|
+
"P_WRONG_FIX",
|
|
22
|
+
"R_DIAGNOSE",
|
|
23
|
+
"R_EXPLORE",
|
|
24
|
+
"R_FIX",
|
|
25
|
+
"R_FIX_PARTIAL",
|
|
26
|
+
"R_JUSTIFY_BONUS",
|
|
27
|
+
"R_ROOT_CAUSE",
|
|
28
|
+
"R_TYPE_BONUS",
|
|
29
|
+
"SPAM_THRESHOLD",
|
|
30
|
+
"EpisodeMetrics",
|
|
31
|
+
"RewardEngine",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Positive rewards
|
|
35
|
+
R_DIAGNOSE: float = 0.10
|
|
36
|
+
R_TYPE_BONUS: float = 0.05
|
|
37
|
+
R_FIX: float = 0.15
|
|
38
|
+
R_FIX_PARTIAL: float = 0.075
|
|
39
|
+
R_JUSTIFY_BONUS: float = 0.05
|
|
40
|
+
R_EXPLORE: float = 0.01
|
|
41
|
+
R_ROOT_CAUSE: float = 0.10
|
|
42
|
+
|
|
43
|
+
# Negative penalties
|
|
44
|
+
P_FALSE_POS: float = -0.05
|
|
45
|
+
P_WRONG_FIX: float = -0.08
|
|
46
|
+
P_LATE_STEP: float = -0.02
|
|
47
|
+
P_INVALID: float = -0.01
|
|
48
|
+
P_REINSPECT: float = -0.01
|
|
49
|
+
|
|
50
|
+
# Thresholds
|
|
51
|
+
LATE_STEP_THRESHOLD: float = 0.80
|
|
52
|
+
DETECTION_WEIGHT: float = 0.40
|
|
53
|
+
FIX_WEIGHT: float = 0.60
|
|
54
|
+
FALSE_POS_PENALTY_RATE: float = 0.05
|
|
55
|
+
SPAM_THRESHOLD: float = 2.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class EpisodeMetrics:
|
|
60
|
+
"""Accumulated metrics for terminal score computation."""
|
|
61
|
+
|
|
62
|
+
found_issues: int = 0
|
|
63
|
+
total_issues: int = 0
|
|
64
|
+
fixed_issues: int = 0
|
|
65
|
+
fixable_issues: int = 0
|
|
66
|
+
false_positives: int = 0
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def total_diagnoses(self) -> int:
|
|
70
|
+
"""Total diagnosis attempts (correct + incorrect)."""
|
|
71
|
+
return self.found_issues + self.false_positives
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RewardEngine:
|
|
75
|
+
"""Computes dense per-step and terminal rewards."""
|
|
76
|
+
|
|
77
|
+
def compute_terminal_score(self, metrics: EpisodeMetrics) -> float:
|
|
78
|
+
"""Compute terminal score per REWARD_DESIGN.md formula."""
|
|
79
|
+
if metrics.total_issues == 0:
|
|
80
|
+
return 0.0
|
|
81
|
+
detection_rate = metrics.found_issues / metrics.total_issues
|
|
82
|
+
fix_rate = (
|
|
83
|
+
metrics.fixed_issues / metrics.fixable_issues if metrics.fixable_issues > 0 else 0.0
|
|
84
|
+
)
|
|
85
|
+
fp_rate = FALSE_POS_PENALTY_RATE
|
|
86
|
+
if (
|
|
87
|
+
metrics.total_issues > 0
|
|
88
|
+
and metrics.total_diagnoses > SPAM_THRESHOLD * metrics.total_issues
|
|
89
|
+
):
|
|
90
|
+
fp_rate *= 2.0
|
|
91
|
+
penalty = metrics.false_positives * fp_rate
|
|
92
|
+
raw = detection_rate * DETECTION_WEIGHT + fix_rate * FIX_WEIGHT - penalty
|
|
93
|
+
return round(max(0.0, min(1.0, raw)), 4)
|
|
94
|
+
|
|
95
|
+
def compute_late_penalty(self, step: int, max_steps: int) -> float:
|
|
96
|
+
"""Return P_LATE_STEP if past 80% budget, else 0.0."""
|
|
97
|
+
threshold = int(max_steps * LATE_STEP_THRESHOLD)
|
|
98
|
+
return P_LATE_STEP if step > threshold else 0.0
|
|
99
|
+
|
|
100
|
+
def compute_exploration_bonus(
|
|
101
|
+
self,
|
|
102
|
+
new_row_indices: set[int],
|
|
103
|
+
inspected_rows: set[int],
|
|
104
|
+
total_rows: int,
|
|
105
|
+
ground_truth_rows: set[int],
|
|
106
|
+
found_issue_rows: set[int],
|
|
107
|
+
) -> float:
|
|
108
|
+
"""Compute exploration bonus for newly-inspected rows."""
|
|
109
|
+
if not new_row_indices:
|
|
110
|
+
return P_REINSPECT
|
|
111
|
+
undiscovered = sum(
|
|
112
|
+
1 for r in new_row_indices if r in ground_truth_rows and r not in found_issue_rows
|
|
113
|
+
)
|
|
114
|
+
bonus = undiscovered * R_EXPLORE
|
|
115
|
+
if total_rows > 0:
|
|
116
|
+
all_inspected = inspected_rows | new_row_indices
|
|
117
|
+
coverage_ratio = len(all_inspected) / total_rows
|
|
118
|
+
bonus += len(new_row_indices) * R_EXPLORE * 0.5 * (1.0 - coverage_ratio)
|
|
119
|
+
return bonus
|
|
120
|
+
|
|
121
|
+
def diagnose_reward(self, type_match: bool) -> float:
|
|
122
|
+
"""Reward for correct diagnosis."""
|
|
123
|
+
return R_DIAGNOSE + (R_TYPE_BONUS if type_match else 0.0)
|
|
124
|
+
|
|
125
|
+
def fix_reward(self, exact: bool, has_justification: bool) -> float:
|
|
126
|
+
"""Reward for correct fix."""
|
|
127
|
+
reward = R_FIX if exact else R_FIX_PARTIAL
|
|
128
|
+
return reward + (R_JUSTIFY_BONUS if has_justification else 0.0)
|
dataforge/env/server.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""FastAPI server for the DataForge RL environment.
|
|
2
|
+
|
|
3
|
+
Provides OpenEnv-compatible HTTP endpoints:
|
|
4
|
+
POST /reset — Start a new episode
|
|
5
|
+
POST /step — Execute an action
|
|
6
|
+
GET /state — Return current state snapshot
|
|
7
|
+
POST /close — No-op shutdown
|
|
8
|
+
GET /health — Liveness check
|
|
9
|
+
GET /metadata — Environment metadata
|
|
10
|
+
GET /schema — Action/observation JSON schemas
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from threading import RLock
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
21
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
22
|
+
from pydantic import TypeAdapter
|
|
23
|
+
|
|
24
|
+
from dataforge import __version__
|
|
25
|
+
from dataforge.agent.tool_actions import Action
|
|
26
|
+
from dataforge.env.environment import DataForgeEnv, EnvState
|
|
27
|
+
from dataforge.env.observation import DataForgeObservation
|
|
28
|
+
from dataforge.http.problem import problem_exception_handler
|
|
29
|
+
from dataforge.observability import configure_fastapi_observability
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("dataforge.env.server")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_cors_origins() -> list[str]:
|
|
35
|
+
"""Build the explicit OpenEnv CORS allowlist from the environment."""
|
|
36
|
+
raw_origins = os.environ.get("DATAFORGE_OPENENV_ORIGINS", "")
|
|
37
|
+
return [origin.strip() for origin in raw_origins.split(",") if origin.strip()]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_cors_origin_regex() -> str | None:
|
|
41
|
+
"""Allow local browser development only when explicitly enabled."""
|
|
42
|
+
if os.environ.get("DATAFORGE_OPENENV_DEV") != "1":
|
|
43
|
+
return None
|
|
44
|
+
return r"^http://(?:localhost|127(?:\.\d{1,3}){3})(?::\d+)?$"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app = FastAPI(
|
|
48
|
+
title="DataForge Environment",
|
|
49
|
+
description="OpenEnv-compatible RL environment for data-quality repair.",
|
|
50
|
+
version=__version__,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
app.add_middleware(
|
|
54
|
+
CORSMiddleware,
|
|
55
|
+
allow_origins=_build_cors_origins(),
|
|
56
|
+
allow_origin_regex=_build_cors_origin_regex(),
|
|
57
|
+
allow_credentials=False,
|
|
58
|
+
allow_methods=["GET", "POST", "OPTIONS"],
|
|
59
|
+
allow_headers=["*"],
|
|
60
|
+
)
|
|
61
|
+
app.add_exception_handler(HTTPException, problem_exception_handler)
|
|
62
|
+
configure_fastapi_observability(app, service_name="dataforge-openenv")
|
|
63
|
+
|
|
64
|
+
_registry_lock = RLock()
|
|
65
|
+
_default_env = DataForgeEnv()
|
|
66
|
+
_sessions: dict[str, DataForgeEnv] = {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_env(episode_id: str | None) -> DataForgeEnv:
|
|
70
|
+
"""Resolve an environment by episode id, preserving legacy no-id behavior."""
|
|
71
|
+
if not episode_id:
|
|
72
|
+
return _default_env
|
|
73
|
+
with _registry_lock:
|
|
74
|
+
try:
|
|
75
|
+
return _sessions[episode_id]
|
|
76
|
+
except KeyError as exc:
|
|
77
|
+
raise HTTPException(
|
|
78
|
+
status_code=404,
|
|
79
|
+
detail={"error": "episode_not_found", "episode_id": episode_id},
|
|
80
|
+
) from exc
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _remember_env(env: DataForgeEnv, episode_id: str) -> None:
|
|
84
|
+
"""Register a session and update the legacy default environment."""
|
|
85
|
+
global _default_env
|
|
86
|
+
with _registry_lock:
|
|
87
|
+
_sessions[episode_id] = env
|
|
88
|
+
_default_env = env
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.post("/reset")
|
|
92
|
+
async def reset(seed: int | None = None) -> dict[str, Any]:
|
|
93
|
+
"""Reset the environment for a new episode."""
|
|
94
|
+
env = DataForgeEnv()
|
|
95
|
+
result = env.reset(seed=seed)
|
|
96
|
+
episode_id = str(result.info["episode_id"])
|
|
97
|
+
_remember_env(env, episode_id)
|
|
98
|
+
return result.model_dump(mode="json")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.post("/step")
|
|
102
|
+
async def step(action: dict[str, Any]) -> dict[str, Any]:
|
|
103
|
+
"""Execute one agent action."""
|
|
104
|
+
action_payload = dict(action)
|
|
105
|
+
raw_episode_id = action_payload.pop("episode_id", None)
|
|
106
|
+
episode_id = str(raw_episode_id) if raw_episode_id else None
|
|
107
|
+
result = _get_env(episode_id).step(action_payload)
|
|
108
|
+
return result.model_dump(mode="json")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.get("/state")
|
|
112
|
+
async def state(episode_id: str | None = None) -> dict[str, Any]:
|
|
113
|
+
"""Return current environment state snapshot."""
|
|
114
|
+
result = _get_env(episode_id).state()
|
|
115
|
+
return result.model_dump(mode="json")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.post("/close")
|
|
119
|
+
async def close(request: Request, episode_id: str | None = None) -> dict[str, Any]:
|
|
120
|
+
"""No-op close endpoint for OpenEnv compatibility."""
|
|
121
|
+
body_episode_id: str | None = None
|
|
122
|
+
if episode_id is None:
|
|
123
|
+
try:
|
|
124
|
+
payload = await request.json()
|
|
125
|
+
except Exception:
|
|
126
|
+
payload = None
|
|
127
|
+
if isinstance(payload, dict) and payload.get("episode_id"):
|
|
128
|
+
body_episode_id = str(payload["episode_id"])
|
|
129
|
+
|
|
130
|
+
target_episode_id = episode_id or body_episode_id
|
|
131
|
+
env = _get_env(target_episode_id)
|
|
132
|
+
env.close()
|
|
133
|
+
if target_episode_id:
|
|
134
|
+
with _registry_lock:
|
|
135
|
+
_sessions.pop(target_episode_id, None)
|
|
136
|
+
return {"status": "closed", "episode_id": target_episode_id}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.get("/health")
|
|
140
|
+
async def health() -> dict[str, Any]:
|
|
141
|
+
"""Liveness check."""
|
|
142
|
+
return {"status": "healthy", "environment": "dataforge-env"}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.get("/metadata")
|
|
146
|
+
async def metadata() -> dict[str, Any]:
|
|
147
|
+
"""Environment metadata for OpenEnv discovery."""
|
|
148
|
+
return {
|
|
149
|
+
"name": "dataforge-env",
|
|
150
|
+
"version": __version__,
|
|
151
|
+
"description": (
|
|
152
|
+
"DataForge RL Environment — agents learn to detect, diagnose, "
|
|
153
|
+
"and repair data-quality issues in tabular datasets."
|
|
154
|
+
),
|
|
155
|
+
"action_types": [
|
|
156
|
+
"INSPECT_ROWS",
|
|
157
|
+
"SQL_QUERY",
|
|
158
|
+
"STAT_TEST",
|
|
159
|
+
"PATTERN_MATCH",
|
|
160
|
+
"HYPOTHESIS",
|
|
161
|
+
"ROOT_CAUSE",
|
|
162
|
+
"DIAGNOSE",
|
|
163
|
+
"FIX",
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.get("/schema")
|
|
169
|
+
async def schema() -> dict[str, Any]:
|
|
170
|
+
"""Return JSON schemas for action and observation models."""
|
|
171
|
+
action_adapter: TypeAdapter[Action] = TypeAdapter(Action)
|
|
172
|
+
return {
|
|
173
|
+
"action": action_adapter.json_schema(),
|
|
174
|
+
"observation": DataForgeObservation.model_json_schema(),
|
|
175
|
+
"state": EnvState.model_json_schema(),
|
|
176
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Public evaluation evidence models for DataForge repair releases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
InferabilityLabel = Literal[
|
|
12
|
+
"deterministic_normalization",
|
|
13
|
+
"context_derivable",
|
|
14
|
+
"external_reference_required",
|
|
15
|
+
"not_inferable_from_prompt",
|
|
16
|
+
]
|
|
17
|
+
PROMOTION_SLICE: InferabilityLabel = "deterministic_normalization"
|
|
18
|
+
ABSTENTION_SLICES = frozenset({"external_reference_required", "not_inferable_from_prompt"})
|
|
19
|
+
AUXILIARY_SLICES = frozenset({"context_derivable"})
|
|
20
|
+
PromotionStatus = Literal[
|
|
21
|
+
"diagnostic_only",
|
|
22
|
+
"diagnostic_promoted",
|
|
23
|
+
"quality_improved_verified",
|
|
24
|
+
"public_quality_milestone",
|
|
25
|
+
"rejected",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EvaluationTaskV2(BaseModel):
|
|
30
|
+
"""One auditable, source-stable model grading task.
|
|
31
|
+
|
|
32
|
+
Ground truth is retained for local grading but excluded from normal JSON
|
|
33
|
+
serialization so prompts and public reports cannot accidentally leak labels.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
schema_version: Literal["evaluation_task_v2"] = "evaluation_task_v2"
|
|
37
|
+
task_id: str = Field(min_length=1)
|
|
38
|
+
prompt_hash: str = Field(min_length=64, max_length=64)
|
|
39
|
+
dataset_sha: str = Field(min_length=1)
|
|
40
|
+
split_id: str = Field(min_length=1)
|
|
41
|
+
inferability: InferabilityLabel
|
|
42
|
+
prompt: dict[str, Any]
|
|
43
|
+
allowed_columns: list[str] = Field(min_length=1)
|
|
44
|
+
valid_rows: list[int] = Field(min_length=1)
|
|
45
|
+
provenance: dict[str, Any]
|
|
46
|
+
hidden_ground_truth: list[dict[str, Any]] = Field(default_factory=list, exclude=True)
|
|
47
|
+
|
|
48
|
+
model_config = {"frozen": True}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReleaseEvidenceV2(BaseModel):
|
|
52
|
+
"""Serializable release-gate evidence for model and benchmark promotion."""
|
|
53
|
+
|
|
54
|
+
schema_version: Literal["release_evidence_v2"] = "release_evidence_v2"
|
|
55
|
+
model_repo: str = Field(min_length=1)
|
|
56
|
+
model_sha: str = Field(min_length=1)
|
|
57
|
+
dataset_repo: str = Field(min_length=1)
|
|
58
|
+
dataset_sha: str = Field(min_length=1)
|
|
59
|
+
strict_macro_f1: float = Field(ge=0.0, le=1.0)
|
|
60
|
+
canonicalized_macro_f1: float = Field(ge=0.0, le=1.0)
|
|
61
|
+
parse_success_rate: float = Field(ge=0.0, le=1.0)
|
|
62
|
+
schema_case_error_count: int = Field(ge=0)
|
|
63
|
+
promotion_slice: InferabilityLabel = PROMOTION_SLICE
|
|
64
|
+
slice_scores: dict[InferabilityLabel, dict[str, float | int]] = Field(default_factory=dict)
|
|
65
|
+
inferability_slice_scores: dict[InferabilityLabel, float] = Field(default_factory=dict)
|
|
66
|
+
package_versions: dict[str, str] = Field(default_factory=dict)
|
|
67
|
+
promotion_status: PromotionStatus
|
|
68
|
+
gate_failures: list[str] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
model_config = {"frozen": True}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def prompt_sha256(prompt: dict[str, Any]) -> str:
|
|
74
|
+
"""Hash a prompt payload with stable JSON serialization."""
|
|
75
|
+
encoded = json.dumps(prompt, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
76
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
provider_number,hospital_name,city,state,zip_code,phone_number,rating,mortality_rate,readmission_rate,er_wait_time
|
|
2
|
+
PRV001,General Hospital,Springfield,IL,62701,2175550101,4.2,0.023,0.145,28
|
|
3
|
+
PRV002,St. Mary Medical Center,Chicago,IL,60601,3125550202,3.8,0.031,0.162,35
|
|
4
|
+
PRV001,Springfield Medical,Springfield,IL,62701,2175550303,4.5,0.019,0.138,22
|
|
5
|
+
PRV003,Mercy Hospital,Peoria,IL,61602,3095550404,3.5,0.028,0.158,31
|
|
6
|
+
PRV004,Northwestern Memorial,Chicago,IL,60611,not available,4.1,0.025,0.149,26
|
|
7
|
+
PRV005,Rush University MC,Chicago,IL,60612,3125550606,45.0,0.022,0.141,29
|
|
8
|
+
PRV006,Advocate Christ,Oak Lawn,IL,60453,7085550707,3.9,0.027,0.155,33
|
|
9
|
+
PRV007,Loyola University MC,Maywood,IL,60153,7085550808,4.3,0.020,0.142,25
|
|
10
|
+
PRV008,Presence St. Joseph,Joliet,IL,60435,8155550909,4.0,0.026,0.151,30
|
|
11
|
+
PRV009,Edward Hospital,Naperville,IL,60540,6305551010,3.7,0.029,0.160,34
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Hospital dataset schema for DataForge profile command.
|
|
2
|
+
|
|
3
|
+
columns:
|
|
4
|
+
provider_number: str
|
|
5
|
+
hospital_name: str
|
|
6
|
+
city: str
|
|
7
|
+
state: str
|
|
8
|
+
zip_code: str
|
|
9
|
+
phone_number: str
|
|
10
|
+
rating: float
|
|
11
|
+
mortality_rate: float
|
|
12
|
+
readmission_rate: float
|
|
13
|
+
er_wait_time: int
|
|
14
|
+
|
|
15
|
+
functional_dependencies:
|
|
16
|
+
- determinant: [provider_number]
|
|
17
|
+
dependent: hospital_name
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP helpers shared by DataForge backend surfaces."""
|