CoolFace
Apppublic

The-Myth/DeepThinkers

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
validate.py383 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3validate.py — Pre-submission validation script for OpenEnv.4Checks all items from the pre-submission checklist:5  1. openenv.yaml exists and is well-formed6  2. Typed Pydantic models exist7  3. step() / reset() / state() work correctly8  4. 3+ tasks with graders, scores in [0.0, 1.0]9  5. Dockerfile exists and is parseable10  6. inference.py exists at root11  7. Environment variables documented12  8. Stdout log format compliance13"""14 15import json16import os17import sys18import time19import subprocess20import importlib21 22# ── Utilities ────────────────────────────────────────────────────────────────23 24PASS = 025FAIL = 026WARN = 027 28 29def check(name: str, condition: bool, got=None, expected=None, critical: bool = True):30    global PASS, FAIL, WARN31    if condition:32        print(f"  ✓  {name}")33        PASS += 134    elif critical:35        msg = f"  ✗  {name}"36        if got is not None:37            msg += f"  (got={got!r}"38            if expected is not None:39                msg += f", expected={expected!r}"40            msg += ")"41        print(msg)42        FAIL += 143    else:44        print(f"  ⚠  {name} (warning)")45        WARN += 146 47 48ROOT = os.path.dirname(os.path.abspath(__file__))49 50 51def main():52    global PASS, FAIL, WARN53 54    # ─────────────────────────────────────────────55    # 1. File structure56    # ─────────────────────────────────────────────57    print("\n=== 1. File Structure ===")58 59    required_files = [60        "openenv.yaml",61        "Dockerfile",62        "README.md",63        "requirements.txt",64        "inference.py",65        "server/app.py",66        "server/environment.py",67        "server/models.py",68        "server/graders.py",69        "server/__init__.py",70        "data/emails.py",71        "data/__init__.py",72    ]73 74    for f in required_files:75        path = os.path.join(ROOT, f)76        check(f"File exists: {f}", os.path.exists(path))77 78    # ─────────────────────────────────────────────79    # 2. openenv.yaml validation80    # ─────────────────────────────────────────────81    print("\n=== 2. openenv.yaml ===")82 83    yaml_path = os.path.join(ROOT, "openenv.yaml")84    try:85        # Use simple YAML parsing (no PyYAML dependency needed)86        with open(yaml_path, encoding="utf-8") as f:87            yaml_content = f.read()88        check("openenv.yaml is readable", True)89 90        required_fields = ["name:", "version:", "tasks:", "observation_space:", "action_space:", "reward:", "endpoint:"]91        for field in required_fields:92            check(f"openenv.yaml has '{field}'", field in yaml_content)93 94        # Check for openenv tag95        check("openenv.yaml has 'openenv' tag", "openenv" in yaml_content)96 97        # Count tasks98        task_count = yaml_content.count("- name:")99        check(f"openenv.yaml defines 3+ tasks", task_count >= 3, got=task_count, expected=">=3")100 101    except FileNotFoundError:102        check("openenv.yaml exists", False)103 104    # ─────────────────────────────────────────────105    # 3. Pydantic typed models106    # ─────────────────────────────────────────────107    print("\n=== 3. Typed Models ===")108 109    sys.path.insert(0, ROOT)110 111    try:112        from server.models import (113            EmailObservation, TriageAction, TriageReward,114            StepResult, EpisodeState,115            Priority, Category, Sentiment,116        )117        check("Import EmailObservation", True)118        check("Import TriageAction", True)119        check("Import TriageReward", True)120        check("Import StepResult", True)121        check("Import EpisodeState", True)122        check("Import Priority enum", True)123        check("Import Category enum", True)124        check("Import Sentiment enum", True)125 126        # Verify they're proper Pydantic models127        obs = EmailObservation(email_id="test", subject="Test", sender="a@b.com")128        check("EmailObservation instantiable", obs is not None)129        check("EmailObservation has model_dump", hasattr(obs, "model_dump"))130 131        action = TriageAction(priority="critical", category="engineering")132        check("TriageAction instantiable", action is not None)133        check("TriageAction has model_dump", hasattr(action, "model_dump"))134 135        reward = TriageReward(total=0.75)136        check("TriageReward instantiable", reward is not None)137 138    except Exception as e:139        check(f"Import models: {e}", False)140 141    # ─────────────────────────────────────────────142    # 4. Environment: reset() / step() / state()143    # ─────────────────────────────────────────────144    print("\n=== 4. Environment Interface ===")145 146    try:147        from server.environment import EmailTriageEnv, TASK_NAMES148        from server.graders import TASK_MAX_STEPS149 150        check("3+ task names defined", len(TASK_NAMES) >= 3, got=len(TASK_NAMES))151 152        for task in TASK_NAMES:153            env = EmailTriageEnv(task_name=task, seed=42)154 155            # reset()156            obs = env.reset()157            check(f"{task}: reset() returns observation", obs is not None)158            check(f"{task}: obs has email_id", hasattr(obs, "email_id") and obs.email_id)159            check(f"{task}: obs.step == 0", obs.step == 0)160 161            # state()162            state = env.state()163            check(f"{task}: state() returns EpisodeState", state is not None)164            check(f"{task}: state.done == False after reset", not state.done)165            check(f"{task}: state.task_name correct", state.task_name == task)166 167            # step()168            action = {169                "priority": "high",170                "category": "engineering",171                "route_to": "engineering-oncall",172                "action_items": ["review logs"],173                "sla_hours": 4,174                "sentiment": "urgent",175                "flags": ["escalate"],176                "reasoning": "Test action.",177            }178            result = env.step(action)179            check(f"{task}: step() returns result", result is not None)180            check(f"{task}: reward in [0,1]",181                  0.0 <= result.reward <= 1.0, got=result.reward)182            check(f"{task}: done is bool", isinstance(result.done, bool))183            check(f"{task}: result has observation", result.observation is not None)184 185            # state after step186            state2 = env.state()187            check(f"{task}: state.step==1 after step", state2.step == 1)188 189    except Exception as e:190        check(f"Environment interface: {e}", False)191 192    # ─────────────────────────────────────────────193    # 5. Graders: 3+ tasks, scores in [0.0, 1.0]194    # ─────────────────────────────────────────────195    print("\n=== 5. Task Graders ===")196 197    try:198        from server.graders import grade, TASK_GRADERS199        from data.emails import get_emails_for_task200 201        check("3+ graders defined", len(TASK_GRADERS) >= 3, got=len(TASK_GRADERS))202 203        for task in TASK_NAMES:204            emails = get_emails_for_task(task)205            check(f"{task}: has emails for grading", len(emails) >= 3, got=len(emails))206 207            for email in emails:208                gt = email["ground_truth"]209 210                # Perfect action211                perfect = {212                    "priority": gt.get("priority"),213                    "category": gt.get("category"),214                    "route_to": gt.get("route_to"),215                    "action_items": gt.get("action_items", []),216                    "sla_hours": gt.get("sla_hours"),217                    "sentiment": gt.get("sentiment"),218                    "flags": gt.get("flags", []),219                }220                scores = grade(task, perfect, gt)221                check(f"{task}/{email['email_id']}: perfect score in [0,1]",222                      0.0 <= scores["total"] <= 1.0, got=scores["total"])223                check(f"{task}/{email['email_id']}: perfect score >= 0.9",224                      scores["total"] >= 0.9, got=scores["total"])225 226                # Worst action227                worst = {228                    "priority": "low" if gt["priority"] == "critical" else "critical",229                    "category": "spam",230                    "route_to": "trash",231                    "action_items": [],232                    "sla_hours": 999,233                    "sentiment": "positive" if gt.get("sentiment") == "urgent" else "urgent",234                    "flags": [],235                }236                worst_scores = grade(task, worst, gt)237                check(f"{task}/{email['email_id']}: worst score in [0,1]",238                      0.0 <= worst_scores["total"] <= 1.0, got=worst_scores["total"])239 240    except Exception as e:241        check(f"Grader validation: {e}", False)242 243    # ─────────────────────────────────────────────244    # 6. Dockerfile validation245    # ─────────────────────────────────────────────246    print("\n=== 6. Dockerfile ===")247 248    dockerfile_path = os.path.join(ROOT, "Dockerfile")249    try:250        with open(dockerfile_path, encoding="utf-8") as f:251            dockerfile = f.read()252        check("Dockerfile is readable", True)253        check("Dockerfile has FROM", "FROM" in dockerfile)254        check("Dockerfile has EXPOSE 7860", "EXPOSE 7860" in dockerfile)255        check("Dockerfile has CMD", "CMD" in dockerfile)256        check("Dockerfile copies requirements", "requirements.txt" in dockerfile)257        check("Dockerfile has HEALTHCHECK", "HEALTHCHECK" in dockerfile)258    except FileNotFoundError:259        check("Dockerfile exists", False)260 261    # ─────────────────────────────────────────────262    # 7. inference.py validation263    # ─────────────────────────────────────────────264    print("\n=== 7. inference.py ===")265 266    inference_path = os.path.join(ROOT, "inference.py")267    try:268        with open(inference_path, encoding="utf-8") as f:269            inf_content = f.read()270        check("inference.py is readable", True)271        check("inference.py uses OpenAI client", "OpenAI" in inf_content or "openai" in inf_content)272        check("inference.py reads HF_TOKEN", "HF_TOKEN" in inf_content)273        check("inference.py reads API_BASE_URL", "API_BASE_URL" in inf_content)274        check("inference.py reads MODEL_NAME", "MODEL_NAME" in inf_content)275        check("inference.py has [START] log", "[START]" in inf_content)276        check("inference.py has [STEP] log", "[STEP]" in inf_content)277        check("inference.py has [END] log", "[END]" in inf_content)278        check("inference.py has main()", "def main(" in inf_content)279    except FileNotFoundError:280        check("inference.py exists at root", False)281 282    # ─────────────────────────────────────────────283    # 8. Stdout format compliance284    # ─────────────────────────────────────────────285    print("\n=== 8. Stdout Log Format ===")286 287    import io288    import contextlib289 290    def capture_print(fn, *args, **kwargs):291        buf = io.StringIO()292        with contextlib.redirect_stdout(buf):293            fn(*args, **kwargs)294        return buf.getvalue().strip()295 296    def log_start(task, env, model):297        print(f"[START] task={task} env={env} model={model}", flush=True)298 299    def log_step(step, action, reward, done, error):300        error_val = error if error else "null"301        done_val = str(done).lower()302        action_clean = str(action).replace("\n", " ")[:200]303        print(f"[STEP] step={step} action={action_clean} reward={reward:.2f} done={done_val} error={error_val}", flush=True)304 305    def log_end(success, steps, score, rewards):306        rewards_str = ",".join(f"{r:.2f}" for r in rewards)307        print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)308 309    start_line = capture_print(log_start, "priority-classification", "email-triage-env", "TestModel")310    check("START: begins with [START]", start_line.startswith("[START]"))311    check("START: has task=", "task=" in start_line)312    check("START: has env=", "env=" in start_line)313    check("START: has model=", "model=" in start_line)314 315    step_line = capture_print(log_step, 1, '{"priority":"high"}', 0.75, False, None)316    check("STEP: begins with [STEP]", step_line.startswith("[STEP]"))317    check("STEP: has step=", "step=" in step_line)318    check("STEP: has reward=0.75", "reward=0.75" in step_line)319    check("STEP: has done=false", "done=false" in step_line)320    check("STEP: has error=null", "error=null" in step_line)321 322    end_line = capture_print(log_end, True, 5, 0.623, [0.5, 0.7, 0.6, 0.55, 0.7])323    check("END: begins with [END]", end_line.startswith("[END]"))324    check("END: has success=true", "success=true" in end_line)325    check("END: has steps=5", "steps=5" in end_line)326    check("END: has score=0.623", "score=0.623" in end_line)327    check("END: has rewards list", "rewards=" in end_line)328    check("END: no newlines in line", "\n" not in end_line)329 330    # ─────────────────────────────────────────────331    # 9. README checks332    # ─────────────────────────────────────────────333    print("\n=== 9. README.md ===")334 335    readme_path = os.path.join(ROOT, "README.md")336    try:337        with open(readme_path, encoding="utf-8") as f:338            readme = f.read()339        check("README.md is readable", True)340        check("README has environment description", "environment" in readme.lower() or "description" in readme.lower())341        check("README has action space", "action" in readme.lower() and "space" in readme.lower())342        check("README has observation space", "observation" in readme.lower() and "space" in readme.lower())343        check("README has task descriptions", "task" in readme.lower())344        check("README has setup instructions", "setup" in readme.lower() or "usage" in readme.lower())345        check("README has baseline scores", "baseline" in readme.lower() and "score" in readme.lower())346 347        # HF Space frontmatter348        check("README has HF Space frontmatter", readme.startswith("---"))349        check("README has sdk: docker", "sdk: docker" in readme)350        check("README has app_port", "app_port:" in readme)351        check("README has openenv tag in frontmatter", "openenv" in readme.split("---")[1] if readme.startswith("---") and readme.count("---") >= 2 else False)352    except FileNotFoundError:353        check("README.md exists", False)354 355    # ─────────────────────────────────────────────356    # 10. Environment variable config357    # ─────────────────────────────────────────────358    print("\n=== 10. Environment Variables ===")359 360    check("API_BASE_URL documented", "API_BASE_URL" in readme if 'readme' in dir() else False)361    check("MODEL_NAME documented", "MODEL_NAME" in readme if 'readme' in dir() else False)362    check("HF_TOKEN documented", "HF_TOKEN" in readme if 'readme' in dir() else False)363 364    # ─────────────────────────────────────────────365    # Summary366    # ─────────────────────────────────────────────367    total = PASS + FAIL + WARN368    print(f"\n{'='*60}")369    print(f"VALIDATION RESULTS: {PASS}/{total} passed", end="")370    if WARN:371        print(f"  ({WARN} warnings)", end="")372    if FAIL:373        print(f"  ({FAIL} FAILED)")374        print(f"\n❌ VALIDATION FAILED — fix {FAIL} issue(s) before submitting.")375        sys.exit(1)376    else:377        print(f"\n\n✅ ALL CHECKS PASSED — ready to submit!")378        sys.exit(0)379 380 381if __name__ == "__main__":382    main()383