File size: 6,098 Bytes
7560863
ca526ce
 
 
7560863
 
 
ca526ce
 
 
7560863
 
ca526ce
fa83aac
7560863
fa83aac
ca526ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7560863
ca526ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7560863
ca526ce
 
 
7560863
ca526ce
 
2c2a501
 
 
 
 
 
ca526ce
 
2c2a501
ca526ce
2c2a501
 
ca526ce
 
 
 
0b21fa5
ca526ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa83aac
 
7560863
8be3fe6
 
 
ca526ce
 
8be3fe6
 
 
 
 
 
 
 
 
 
7560863
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""
MetaOCT: OpenEnv-Compliant REST API Server
Implements POST /reset, POST /step, GET /validate per OpenEnv specification.
Hackathon automated checker compatible.
"""

import json
import os
import asyncio
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from dotenv import load_dotenv
from env import MetaOCTEnv, Action

load_dotenv()

# Global environment instance and state
_env = None
_env_lock = threading.Lock()
_loop = None

def get_loop():
    global _loop
    if _loop is None or _loop.is_closed():
        _loop = asyncio.new_event_loop()
    return _loop

def run_async(coro):
    loop = get_loop()
    return loop.run_until_complete(coro)

def get_env(difficulty="easy"):
    global _env
    if _env is None:
        _env = MetaOCTEnv(difficulty=difficulty)
    return _env

class OpenEnvHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args): pass  # Suppress access logs

    def send_json(self, code, data):
        body = json.dumps(data).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)

    def read_body(self):
        length = int(self.headers.get("Content-Length", 0))
        if length:
            return json.loads(self.rfile.read(length))
        return {}

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        self.end_headers()

    def do_GET(self):
        if self.path == "/validate" or self.path == "/openenv/validate" or self.path == "/openenv/info":
            task_list = [
                {"id": "easy", "name": "Easy POMDP Diagnosis", "difficulty": "easy", "budget": 1000, "description": "Basic POMDP traversal", "grader": "tasks:grade_easy"},
                {"id": "medium", "name": "Medium Accuracy POMDP", "difficulty": "medium", "budget": 400, "description": "Precision optimization", "grader": "tasks:grade_medium"},
                {"id": "hard", "name": "Hard Zero-Shot POMDP", "difficulty": "hard", "budget": 200, "description": "Extreme resource constraints", "grader": "tasks:grade_hard"}
            ]
            self.send_json(200, {
                "status": "ok",
                "name": "MetaOCT",
                "version": "1.0.0",
                "description": "Multi-step POMDP clinical OCT diagnostic environment",
                "tasks": task_list,
                "action_space": ["request_oct_scan", "enhance_contrast", "measure_fluid_thickness", "submit_diagnosis"],
                "observation_space": ["acquired_scans", "available_budget", "tool_outputs", "step_count"]
            })
        elif self.path == "/" or self.path == "/health":
            self.send_json(200, {"status": "ok", "message": "MetaOCT API Server is running. Use /validate, /reset, /step endpoints."})

    def do_POST(self):
        try:
            body = self.read_body()

            if self.path in ["/reset", "/openenv/reset"]:
                difficulty = body.get("task", body.get("difficulty", "easy"))
                with _env_lock:
                    global _env
                    _env = MetaOCTEnv(difficulty=difficulty)
                    obs = run_async(_env.reset())
                
                self.send_json(200, {
                    "observation": {
                        "acquired_scans": obs.acquired_scans,
                        "available_budget": obs.available_budget,
                        "tool_outputs": obs.tool_outputs,
                        "step_count": obs.step_count
                    },
                    "info": {"difficulty": difficulty, "task": difficulty}
                })

            elif self.path in ["/step", "/openenv/step"]:
                action_data = body.get("action", body)
                tool_name = action_data.get("tool_name", action_data.get("name", "submit_diagnosis"))
                parameters = action_data.get("parameters", action_data.get("args", {}))
                action = Action(tool_name=tool_name, parameters=parameters)
                
                with _env_lock:
                    env = get_env()
                    result = run_async(env.step(action))
                
                obs = result.observation
                obs_dict = {}
                if obs:
                    obs_dict = {
                        "acquired_scans": obs.acquired_scans,
                        "available_budget": obs.available_budget,
                        "tool_outputs": obs.tool_outputs,
                        "step_count": obs.step_count
                    }
                
                self.send_json(200, {
                    "observation": obs_dict,
                    "reward": result.reward,
                    "done": result.done,
                    "info": result.info or {}
                })

            else:
                self.send_json(404, {"error": "Unknown endpoint"})

        except Exception as e:
            print(f"[ERROR] {self.path}: {e}", flush=True)
            self.send_json(500, {"error": str(e)})


if __name__ == "__main__":
    port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
    
    # Start HTTP server IMMEDIATELY so checker can connect
    server = HTTPServer(("0.0.0.0", port), OpenEnvHandler)
    print(f"[INFO] MetaOCT OpenEnv API running on port {port}", flush=True)
    print(f"[INFO] Endpoints: POST /reset, POST /step, GET /validate", flush=True)
    
    # Pre-warm environment in background thread
    def prewarm():
        print("[INFO] Pre-warming MetaOCT environment...", flush=True)
        get_env("easy")
        print("[INFO] Environment ready.", flush=True)
    
    t = threading.Thread(target=prewarm, daemon=True)
    t.start()
    
    server.serve_forever()