# Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. """ LaTeX OCR Environment client. Gym-style client (reset/step over WebSocket) plus thin HTTP helpers for the Task API so a trainer can enumerate and select dataset tasks. Example: >>> with LatexOCREnv(base_url="http://localhost:8000") as env: ... print(env.list_splits()) # ["train", "test"] ... print(env.num_tasks("test")) # 7595 ... result = env.reset(split="test", index=0) ... img = result.observation.image_base64 ... # ... run a VLM to produce `latex` ... ... result = env.step(LatexOCRAction(latex=latex)) ... print(result.reward, result.observation.target_latex) """ from __future__ import annotations from typing import Any, Optional from urllib.parse import urljoin import requests try: from openenv.core.client_types import StepResult from openenv.core.env_client import EnvClient from openenv.core.env_server.types import State except ImportError: from core.client_types import StepResult from core.env_client import EnvClient from core.env_server.types import State from .models import LatexOCRAction, LatexOCRObservation ENV_NAME = "latex_ocr_env" class LatexOCREnv(EnvClient[LatexOCRAction, LatexOCRObservation, State]): """Client for the LaTeX OCR environment.""" def reset( self, split: str = "train", index: Optional[int] = None, seed: Optional[int] = None, **kwargs: Any, ) -> StepResult[LatexOCRObservation]: """Reset to a specific dataset task (or a random one if ``index`` is None).""" payload: dict[str, Any] = {"split": split} if index is not None: payload["index"] = index if seed is not None: payload["seed"] = seed payload.update(kwargs) return super().reset(**payload) # --- Gym-style (de)serialization required by EnvClient --- def _step_payload(self, action: LatexOCRAction) -> dict[str, Any]: return action.model_dump() def _parse_result(self, data: dict[str, Any]) -> StepResult[LatexOCRObservation]: obs_data = dict(data.get("observation", data)) # Core serialization lifts reward/done to the top level and strips them from # the observation payload; merge them back so observation.reward/.done match # StepResult (consistent with other env clients). reward = data.get("reward", obs_data.get("reward")) done = data.get("done", obs_data.get("done")) obs_data["reward"] = reward obs_data["done"] = done obs = LatexOCRObservation(**obs_data) base = dict(observation=obs, reward=reward, done=done) # Newer core's StepResult carries `info`; older core does not. try: return StepResult(**base, info=data.get("info", {})) except TypeError: return StepResult(**base) def _parse_state(self, data: dict[str, Any]) -> State: return State(**data) # ------------------------------------------------------------------ # # Task API (HTTP) # # ------------------------------------------------------------------ # def _http_base(self) -> str: # Newer core exposes ``_base_url`` (http); older core stores only # ``_ws_url`` (ws://host/ws). Derive an http base that works for both. base = getattr(self, "_base_url", None) if not base: ws = getattr(self, "_ws_url", None) if not ws: raise RuntimeError("Task API requires an HTTP base URL") base = ws[:-3] if ws.endswith("/ws") else ws base = base.replace("wss://", "https://").replace("ws://", "http://") return base if base.endswith("/") else base + "/" def list_splits(self) -> list[str]: resp = requests.get( urljoin(self._http_base(), f"{ENV_NAME}/splits"), timeout=30 ) resp.raise_for_status() return [s["name"] for s in resp.json()] def num_tasks(self, split: str) -> int: resp = requests.post( urljoin(self._http_base(), f"{ENV_NAME}/num_tasks"), json={"split": split}, timeout=60, ) resp.raise_for_status() return int(resp.json()["num_tasks"]) def get_task(self, split: str, index: int) -> dict[str, Any]: resp = requests.post( urljoin(self._http_base(), f"{ENV_NAME}/task"), json={"split": split, "index": index}, timeout=60, ) resp.raise_for_status() return resp.json()["task"] def get_task_range( self, split: str, start: int | None = None, stop: int | None = None ) -> list[dict[str, Any]]: resp = requests.post( urljoin(self._http_base(), f"{ENV_NAME}/task_range"), json={"split": split, "start": start, "stop": stop}, timeout=120, ) resp.raise_for_status() return resp.json()["tasks"]