from __future__ import annotations
from pathlib import Path
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from openenv.core.env_server.http_server import create_app
from openenv_runtime.environment import OpenEnvOrigamiEnvironment
from openenv_runtime.models import OrigamiAction, OrigamiObservation
app = create_app(
env=lambda: OpenEnvOrigamiEnvironment(),
action_cls=OrigamiAction,
observation_cls=OrigamiObservation,
env_name="optigami",
)
# ---------------------------------------------------------------------------
# Demo routes required by the React frontend.
# These must be registered BEFORE the StaticFiles catch-all mount.
# ---------------------------------------------------------------------------
DEMO_COMPLETIONS: dict[str, str] = {
"half_horizontal": '[{"instruction": "Valley fold along horizontal center line", "from": [0, 0.5], "to": [1, 0.5], "assignment": "V"}]',
"half_vertical": '[{"instruction": "Mountain fold along vertical center line", "from": [0.5, 0], "to": [0.5, 1], "assignment": "M"}]',
"diagonal_main": '[{"instruction": "Valley fold along main diagonal", "from": [0, 0], "to": [1, 1], "assignment": "V"}]',
"diagonal_anti": '[{"instruction": "Mountain fold along anti-diagonal", "from": [1, 0], "to": [0, 1], "assignment": "M"}]',
"thirds_h": '[{"instruction": "Valley fold at one-third height", "from": [0, 0.333], "to": [1, 0.333], "assignment": "V"}, {"instruction": "Valley fold at two-thirds height", "from": [0, 0.667], "to": [1, 0.667], "assignment": "V"}]',
"thirds_v": '[{"instruction": "Mountain fold at one-third width", "from": [0.333, 0], "to": [0.333, 1], "assignment": "M"}, {"instruction": "Mountain fold at two-thirds width", "from": [0.667, 0], "to": [0.667, 1], "assignment": "M"}]',
"accordion_3h": '[{"instruction": "Valley fold at quarter height", "from": [0, 0.25], "to": [1, 0.25], "assignment": "V"}, {"instruction": "Mountain fold at half height", "from": [0, 0.5], "to": [1, 0.5], "assignment": "M"}, {"instruction": "Valley fold at three-quarter height", "from": [0, 0.75], "to": [1, 0.75], "assignment": "V"}]',
"accordion_4h": '[{"instruction": "Valley fold at 0.2", "from": [0, 0.2], "to": [1, 0.2], "assignment": "V"}, {"instruction": "Mountain fold at 0.4", "from": [0, 0.4], "to": [1, 0.4], "assignment": "M"}, {"instruction": "Valley fold at 0.6", "from": [0, 0.6], "to": [1, 0.6], "assignment": "V"}, {"instruction": "Mountain fold at 0.8", "from": [0, 0.8], "to": [1, 0.8], "assignment": "M"}]',
}
@app.get("/targets", include_in_schema=True)
def get_targets() -> dict:
"""Return available target names and metadata for the frontend."""
from env.environment import OrigamiEnvironment
env = OrigamiEnvironment()
result: dict[str, dict] = {}
for name in env.available_targets():
t = env._targets[name]
result[name] = {
"name": name,
"level": t.get("level", 1),
"description": t.get("description", ""),
"n_creases": sum(1 for a in t["edges_assignment"] if a in ("M", "V")),
}
return result
@app.get("/episode/run", include_in_schema=True)
def run_episode(target: str = "half_horizontal", completion: str = "") -> dict:
"""Run a fold-sequence episode and return step-by-step data."""
from env.environment import OrigamiEnvironment
from env.prompts import parse_fold_list, step_level_prompt
from env.rewards import compute_reward
env = OrigamiEnvironment(mode="step")
obs = env.reset(target_name=target)
if not completion:
return {"prompt": obs["prompt"], "steps": [], "target": env.target}
try:
folds = parse_fold_list(completion)
except ValueError as exc:
return {"error": str(exc), "steps": []}
steps: list[dict] = []
for i, fold in enumerate(folds):
result = env.paper.add_crease(fold["from"], fold["to"], fold["assignment"])
reward = compute_reward(env.paper, result, env.target)
paper_state = {
"vertices": {str(k): list(v) for k, v in env.paper.graph.vertices.items()},
"edges": [
{
"id": k,
"v1": list(env.paper.graph.vertices[v[0]]),
"v2": list(env.paper.graph.vertices[v[1]]),
"assignment": v[2],
}
for k, v in env.paper.graph.edges.items()
],
"anchor_points": [list(p) for p in env.paper.anchor_points()],
}
step_prompt = step_level_prompt(
target=env.target,
paper_state=env.paper,
step=i + 1,
max_steps=env.max_steps,
last_reward=reward,
)
steps.append(
{
"step": i + 1,
"fold": {
"from_point": fold["from"],
"to_point": fold["to"],
"assignment": fold["assignment"],
"instruction": fold.get("instruction", ""),
},
"paper_state": paper_state,
"anchor_points": [list(p) for p in env.paper.anchor_points()],
"reward": reward,
"done": reward.get("completion", 0) > 0,
"info": env._info(),
"prompt": step_prompt,
}
)
if reward.get("completion", 0) > 0:
break
return {
"target_name": target,
"target": env.target,
"steps": steps,
"final_reward": steps[-1]["reward"] if steps else {},
}
@app.get("/episode/demo", include_in_schema=True)
def demo_episode(target: str = "half_horizontal") -> dict:
"""Return a pre-solved demo episode for the given target."""
completion = DEMO_COMPLETIONS.get(target, DEMO_COMPLETIONS["half_horizontal"])
return run_episode(target=target, completion=completion)
# ---------------------------------------------------------------------------
# Static file serving — must come LAST so API routes take priority.
# ---------------------------------------------------------------------------
_BUILD_DIR = Path(__file__).resolve().parent.parent / "build"
if _BUILD_DIR.exists():
app.mount("/", StaticFiles(directory=str(_BUILD_DIR), html=True), name="renderer")
else:
@app.get("/", include_in_schema=False)
def missing_renderer_build() -> HTMLResponse:
return HTMLResponse(
"""
Renderer build not found
No build/ directory is present in the container.
OpenEnv API docs are available at /docs.
""",
status_code=200,
)