Spaces:
Sleeping
Sleeping
| """ | |
| SciAgent RL Environment | |
| ======================= | |
| Trains LLMs to conduct hypothesis-driven scientific reasoning. | |
| Observation: dataset (two groups) + research question | |
| Action: JSON with hypothesis, statistical_test, reasoning, conclusion | |
| Reward: Composite 0-1 score (structural + statistical correctness) | |
| """ | |
| import json | |
| import random | |
| import numpy as np | |
| from scipy import stats | |
| # ββ Built-in datasets ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DATASETS = [ | |
| { | |
| "name": "exam_scores", | |
| "data": { | |
| "group_a": [72, 85, 90, 68, 75, 88, 92, 70], | |
| "group_b": [65, 70, 78, 62, 69, 74, 80, 67], | |
| }, | |
| "question": "Does group A score significantly higher than group B?", | |
| }, | |
| { | |
| "name": "drug_trial", | |
| "data": { | |
| "treatment": [8.2, 7.5, 9.1, 6.8, 8.8, 7.2, 9.5, 8.0], | |
| "placebo": [7.1, 6.8, 7.5, 6.2, 7.8, 6.5, 7.0, 6.9], | |
| }, | |
| "question": "Does the drug reduce symptoms significantly compared to placebo?", | |
| }, | |
| { | |
| "name": "website_ab", | |
| "data": { | |
| "variant_a": [3.2, 4.1, 2.8, 3.9, 4.5, 3.0, 4.2, 3.7], | |
| "variant_b": [2.1, 2.8, 1.9, 2.5, 3.1, 2.3, 2.7, 2.4], | |
| }, | |
| "question": "Does variant A produce a significantly higher conversion rate?", | |
| }, | |
| { | |
| "name": "temperature", | |
| "data": { | |
| "city_x": [22, 24, 23, 25, 21, 26, 24, 23], | |
| "city_y": [18, 20, 19, 21, 17, 22, 20, 19], | |
| }, | |
| "question": "Is city X significantly warmer than city Y on average?", | |
| }, | |
| { | |
| "name": "crop_yield", | |
| "data": { | |
| "fertilizer_a": [45.2, 48.1, 44.7, 50.3, 47.8, 46.5, 49.1, 45.9], | |
| "fertilizer_b": [40.1, 42.5, 39.8, 43.7, 41.2, 40.9, 42.1, 41.5], | |
| }, | |
| "question": "Does fertilizer A produce significantly higher crop yield than fertilizer B?", | |
| }, | |
| ] | |
| # ββ Environment βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class SciAgentEnv: | |
| """ | |
| Gym-compatible environment for scientific hypothesis testing. | |
| Usage | |
| ----- | |
| env = SciAgentEnv() | |
| obs, _ = env.reset() | |
| action = json.dumps({ | |
| "hypothesis": "Group A scores higher than Group B", | |
| "statistical_test": "Welch t-test", | |
| "reasoning": "Two independent groups, unknown variance", | |
| "conclusion": True | |
| }) | |
| obs, reward, done, truncated, info = env.step(action) | |
| """ | |
| metadata = {"render_modes": ["human"]} | |
| def __init__(self, seed: int = 42, max_steps: int = 3): | |
| self.seed_val = seed | |
| self.max_steps = max_steps | |
| self.rng = random.Random(seed) | |
| self.dataset = None | |
| self.step_count = 0 | |
| self.history = [] | |
| # ββ Gym API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def reset(self, seed=None): | |
| if seed is not None: | |
| self.rng = random.Random(seed) | |
| self.dataset = self.rng.choice(DATASETS) | |
| self.step_count = 0 | |
| self.history = [] | |
| return self.state(), {} | |
| def step(self, action_str: str): | |
| self.step_count += 1 | |
| action = self._parse_action(action_str) | |
| if action is None: | |
| obs = self.state() | |
| done = self.step_count >= self.max_steps | |
| return obs, 0.0, done, False, {"error": "invalid_json", "step": self.step_count} | |
| reward = self._programmatic_reward(action) | |
| self.history.append({"step": self.step_count, "action": action, "reward": reward}) | |
| done = (self.step_count >= self.max_steps) or (action.get("conclusion") is not None) | |
| return self.state(), reward, done, False, {"reward": reward, "step": self.step_count} | |
| def state(self) -> dict: | |
| return { | |
| "question": self.dataset["question"], | |
| "data": self.dataset["data"], | |
| "step": self.step_count, | |
| "history": self.history, | |
| } | |
| def render(self, mode="human"): | |
| s = self.state() | |
| print(f"\n=== SciAgentEnv | Step {s['step']} ===") | |
| print(f"Question : {s['question']}") | |
| print(f"Data : {s['data']}") | |
| if s["history"]: | |
| last = s["history"][-1] | |
| print(f"Last reward: {last['reward']:.3f}") | |
| # ββ Internal helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_action(self, s: str): | |
| try: | |
| cleaned = s.strip() | |
| for fence in ["```json", "```"]: | |
| cleaned = cleaned.lstrip(fence).rstrip(fence).strip() | |
| return json.loads(cleaned) | |
| except Exception: | |
| return None | |
| def _programmatic_reward(self, action: dict) -> float: | |
| score = 0.0 | |
| has_hyp = bool(action.get("hypothesis", "").strip()) | |
| has_test = bool(action.get("statistical_test", "").strip()) | |
| has_conc = action.get("conclusion") is not None | |
| score += 0.2 * has_hyp | |
| score += 0.2 * has_test | |
| score += 0.2 * has_conc | |
| # Appropriate test family? | |
| test_str = (action.get("statistical_test") or "").lower() | |
| appropriate = any( | |
| t in test_str | |
| for t in ["t-test", "t_test", "ttest", "welch", "student"] | |
| ) | |
| score += 0.2 * appropriate | |
| # Statistically correct conclusion? | |
| if has_conc: | |
| d = self.dataset["data"] | |
| keys = list(d.keys()) | |
| try: | |
| _, pval = stats.ttest_ind(d[keys[0]], d[keys[1]], equal_var=False) | |
| true_sig = pval < 0.05 | |
| claimed_text = str(action.get("conclusion", "")).lower() | |
| claimed_sig = any( | |
| w in claimed_text | |
| for w in ["true", "yes", "significant", "confirmed", "supports", "1"] | |
| ) | |
| if true_sig == claimed_sig: | |
| score += 0.2 | |
| except Exception: | |
| pass | |
| return min(round(score, 4), 1.0) | |
| def _ground_truth(self) -> dict: | |
| """Return the correct answer for the current dataset.""" | |
| d = self.dataset["data"] | |
| keys = list(d.keys()) | |
| t, p = stats.ttest_ind(d[keys[0]], d[keys[1]], equal_var=False) | |
| return {"t_statistic": round(t, 4), "p_value": round(p, 4), "significant": p < 0.05} |