bpHigh's picture
RailSimEnv v1: OpenEnv server, eval explorer, 3D replay and train graph
31252db verified
Raw History Blame Contribute Delete
4.57 kB
"""A client session on a RailSimEnv server (OpenEnv's MCP client over its WebSocket), for scripts and agents.
env = EnvSession("http://127.0.0.1:8000") # or LocalSession(pack): the same tools, in this process
meta = env.reset("gnlw-2025-03-13-0703-real-0") # metadata["instructions"] is the system prompt
text, done, reward, meta = env.call("submit_plan", {"plan": [...]})
"""
from __future__ import annotations
import json
from openenv.core.env_server.mcp_types import CallToolAction
from openenv.core.mcp_client import MCPToolClient
class EnvSession:
"""One WebSocket session (one episode at a time)."""
def __init__(self, base_url: str, timeout_s: float = 300.0):
self.client = MCPToolClient(base_url.rstrip("/"), message_timeout_s=timeout_s).sync()
self._tools = None
def reset(self, task_id: str | None = None, **kw) -> dict:
if task_id is not None:
kw["task_id"] = task_id
return dict(self.client.reset(**kw).observation.metadata or {})
@property
def tools(self):
if self._tools is None:
self._tools = self.client.list_tools()
return self._tools
def call(self, name: str, arguments: dict) -> tuple[str, bool, float | None, dict]:
"""(tool result text, done, reward, observation metadata)."""
step = self.client.step(CallToolAction(tool_name=name, arguments=arguments))
obs = step.observation
err = getattr(obs, "error", None)
res = getattr(obs, "result", None)
text = None
if isinstance(res, dict):
blocks = res.get("content") or []
text = "\n".join(b.get("text", "") for b in blocks if b.get("type") == "text") or None
if text is None and res.get("data") is not None:
text = res["data"] if isinstance(res["data"], str) else json.dumps(res["data"])
elif res is not None:
data = getattr(res, "data", None)
text = data if isinstance(data, str) else json.dumps(data) if data is not None else str(res)
if err:
msg = err.get("message") if isinstance(err, dict) else getattr(err, "message", str(err))
text = json.dumps({"error": str(msg)})
reward = step.reward if step.reward is not None else obs.reward
return text or "", bool(step.done or obs.done), reward, dict(obs.metadata or {})
def close(self):
try:
self.client.close()
except Exception:
pass
def _result_text(res) -> str | None:
if res is None:
return None
if isinstance(res, dict):
blocks = res.get("content") or []
text = "\n".join(b.get("text", "") for b in blocks if b.get("type") == "text")
if text:
return text
data = res.get("data")
return data if isinstance(data, str) else (json.dumps(data) if data is not None else None)
content = getattr(res, "content", None)
if content:
text = "\n".join(getattr(b, "text", "") for b in content)
if text:
return text
data = getattr(res, "data", None)
return data if isinstance(data, str) else (json.dumps(data) if data is not None else str(res))
class LocalSession:
"""The environment in this process (no server, no WebSocket): the same tools, results and rubric. For tests,
pilots and quick experiments."""
def __init__(self, pack=None):
from openenv.core.env_server.mcp_types import ListToolsAction
from .environment import RailOpsEnvironment
self.env = RailOpsEnvironment(pack)
self._list = ListToolsAction
self._tools = None
def reset(self, task_id: str | None = None, **kw) -> dict:
if task_id is not None:
kw["task_id"] = task_id
return dict(self.env.reset(**kw).metadata or {})
@property
def tools(self):
if self._tools is None:
self._tools = self.env.step(self._list()).tools
return self._tools
def call(self, name: str, arguments: dict) -> tuple[str, bool, float | None, dict]:
obs = self.env.step(CallToolAction(tool_name=name, arguments=arguments))
err = getattr(obs, "error", None)
text = _result_text(getattr(obs, "result", None))
if err:
msg = err.get("message") if isinstance(err, dict) else getattr(err, "message", str(err))
text = json.dumps({"error": str(msg)})
return text or "", bool(obs.done), obs.reward, dict(obs.metadata or {})
def close(self):
pass