ianalin123 commited on
Commit
c46fef8
·
1 Parent(s): 0bcd0b1

refactor(server): migrate demo routes to server/ task+env API

Browse files

Replace env/ environment with server/origami_environment.OrigamiEnvironment.
Switch /targets to server/tasks.available_task_names with difficulty/material
metadata. Replace /episode/run with /episode/demo using new DEMO_SEQUENCES
format (type/line/angle) and new OrigamiAction model. Remove old parse_fold_list
completion string approach.

Files changed (1) hide show
  1. openenv_server/app.py +78 -85
openenv_server/app.py CHANGED
@@ -19,123 +19,116 @@ app = create_app(
19
 
20
 
21
  # ---------------------------------------------------------------------------
22
- # Demo routes required by the React frontend.
23
- # These must be registered BEFORE the StaticFiles catch-all mount.
24
  # ---------------------------------------------------------------------------
25
 
26
- DEMO_COMPLETIONS: dict[str, str] = {
27
- "half_horizontal": '<folds>[{"instruction": "Valley fold along horizontal center line", "from": [0, 0.5], "to": [1, 0.5], "assignment": "V"}]</folds>',
28
- "half_vertical": '<folds>[{"instruction": "Mountain fold along vertical center line", "from": [0.5, 0], "to": [0.5, 1], "assignment": "M"}]</folds>',
29
- "diagonal_main": '<folds>[{"instruction": "Valley fold along main diagonal", "from": [0, 0], "to": [1, 1], "assignment": "V"}]</folds>',
30
- "diagonal_anti": '<folds>[{"instruction": "Mountain fold along anti-diagonal", "from": [1, 0], "to": [0, 1], "assignment": "M"}]</folds>',
31
- "thirds_h": '<folds>[{"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"}]</folds>',
32
- "thirds_v": '<folds>[{"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"}]</folds>',
33
- "accordion_3h": '<folds>[{"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"}]</folds>',
34
- "accordion_4h": '<folds>[{"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"}]</folds>',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
 
 
 
 
 
38
  @app.get("/targets", include_in_schema=True)
39
  def get_targets() -> dict:
40
- """Return available target names and metadata for the frontend."""
41
- from env.environment import OrigamiEnvironment
42
 
43
- env = OrigamiEnvironment()
44
  result: dict[str, dict] = {}
45
- for name in env.available_targets():
46
- t = env._targets[name]
47
  result[name] = {
48
  "name": name,
49
- "level": t.get("level", 1),
50
  "description": t.get("description", ""),
51
- "n_creases": sum(1 for a in t["edges_assignment"] if a in ("M", "V")),
 
 
52
  }
53
  return result
54
 
55
 
56
- @app.get("/episode/run", include_in_schema=True)
57
- def run_episode(target: str = "half_horizontal", completion: str = "") -> dict:
58
- """Run a fold-sequence episode and return step-by-step data."""
59
- from env.environment import OrigamiEnvironment
60
- from env.prompts import parse_fold_list, step_level_prompt
61
- from env.rewards import compute_reward
62
-
63
- env = OrigamiEnvironment(mode="step")
64
- obs = env.reset(target_name=target)
65
 
66
- if not completion:
67
- return {"prompt": obs["prompt"], "steps": [], "target": env.target}
68
 
69
- try:
70
- folds = parse_fold_list(completion)
71
- except ValueError as exc:
72
- return {"error": str(exc), "steps": []}
73
 
74
  steps: list[dict] = []
75
- for i, fold in enumerate(folds):
76
- result = env.paper.add_crease(fold["from"], fold["to"], fold["assignment"])
77
- reward = compute_reward(env.paper, result, env.target)
78
-
79
- paper_state = {
80
- "vertices": {str(k): list(v) for k, v in env.paper.graph.vertices.items()},
81
- "edges": [
82
- {
83
- "id": k,
84
- "v1": list(env.paper.graph.vertices[v[0]]),
85
- "v2": list(env.paper.graph.vertices[v[1]]),
86
- "assignment": v[2],
87
- }
88
- for k, v in env.paper.graph.edges.items()
89
- ],
90
- "anchor_points": [list(p) for p in env.paper.anchor_points()],
91
- }
92
 
93
- step_prompt = step_level_prompt(
94
- target=env.target,
95
- paper_state=env.paper,
96
- step=i + 1,
97
- max_steps=env.max_steps,
98
- last_reward=reward,
99
- )
100
 
101
- steps.append(
102
- {
103
- "step": i + 1,
104
- "fold": {
105
- "from_point": fold["from"],
106
- "to_point": fold["to"],
107
- "assignment": fold["assignment"],
108
- "instruction": fold.get("instruction", ""),
109
- },
110
- "paper_state": paper_state,
111
- "anchor_points": [list(p) for p in env.paper.anchor_points()],
112
- "reward": reward,
113
- "done": reward.get("completion", 0) > 0,
114
- "info": env._info(),
115
- "prompt": step_prompt,
116
- }
117
  )
118
 
119
- if reward.get("completion", 0) > 0:
 
 
 
 
 
 
 
 
 
 
120
  break
121
 
 
 
122
  return {
123
- "target_name": target,
124
- "target": env.target,
125
  "steps": steps,
126
- "final_reward": steps[-1]["reward"] if steps else {},
127
  }
128
 
129
 
130
- @app.get("/episode/demo", include_in_schema=True)
131
- def demo_episode(target: str = "half_horizontal") -> dict:
132
- """Return a pre-solved demo episode for the given target."""
133
- completion = DEMO_COMPLETIONS.get(target, DEMO_COMPLETIONS["half_horizontal"])
134
- return run_episode(target=target, completion=completion)
135
-
136
-
137
  # ---------------------------------------------------------------------------
138
- # Static file serving — must come LAST so API routes take priority.
139
  # ---------------------------------------------------------------------------
140
 
141
  _BUILD_DIR = Path(__file__).resolve().parent.parent / "build"
 
19
 
20
 
21
  # ---------------------------------------------------------------------------
22
+ # Demo fold sequences new format: type, line {start, end}, angle
 
23
  # ---------------------------------------------------------------------------
24
 
25
+ DEMO_SEQUENCES: dict[str, list[dict]] = {
26
+ "half_fold": [
27
+ {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
28
+ ],
29
+ "quarter_fold": [
30
+ {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
31
+ {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
32
+ ],
33
+ "letter_fold": [
34
+ {"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
35
+ {"type": "mountain", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
36
+ ],
37
+ "map_fold": [
38
+ {"type": "valley", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
39
+ {"type": "mountain", "line": {"start": [0.5, 0.0], "end": [0.5, 1.0]}, "angle": 180.0},
40
+ ],
41
+ "solar_panel": [
42
+ {"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 180.0},
43
+ {"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 180.0},
44
+ {"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 180.0},
45
+ ],
46
+ "shelter_wall": [
47
+ {"type": "valley", "line": {"start": [0.0, 0.333], "end": [1.0, 0.333]}, "angle": 180.0},
48
+ {"type": "valley", "line": {"start": [0.0, 0.667], "end": [1.0, 0.667]}, "angle": 180.0},
49
+ ],
50
+ "stent": [
51
+ {"type": "valley", "line": {"start": [0.0, 0.25], "end": [1.0, 0.25]}, "angle": 90.0},
52
+ {"type": "mountain", "line": {"start": [0.0, 0.5], "end": [1.0, 0.5]}, "angle": 90.0},
53
+ {"type": "valley", "line": {"start": [0.0, 0.75], "end": [1.0, 0.75]}, "angle": 90.0},
54
+ {"type": "stop", "line": {"start": [0.0, 0.0], "end": [1.0, 1.0]}, "angle": 0.0},
55
+ ],
56
  }
57
 
58
 
59
+ # ---------------------------------------------------------------------------
60
+ # API routes — must be registered BEFORE the StaticFiles catch-all mount
61
+ # ---------------------------------------------------------------------------
62
+
63
  @app.get("/targets", include_in_schema=True)
64
  def get_targets() -> dict:
65
+ """Return available task names and metadata for the frontend."""
66
+ from server.tasks import get_task_by_name, available_task_names
67
 
 
68
  result: dict[str, dict] = {}
69
+ for name in available_task_names():
70
+ t = get_task_by_name(name)
71
  result[name] = {
72
  "name": name,
73
+ "level": t.get("difficulty", 1),
74
  "description": t.get("description", ""),
75
+ "n_creases": t.get("max_folds", 3),
76
+ "difficulty": t.get("difficulty", 1),
77
+ "material": t.get("material", "paper"),
78
  }
79
  return result
80
 
81
 
82
+ @app.get("/episode/demo", include_in_schema=True)
83
+ def demo_episode(target: str = "half_fold") -> dict:
84
+ """Return a pre-solved demo episode for the given task."""
85
+ from server.origami_environment import OrigamiEnvironment
86
+ from server.models import OrigamiAction as NewOrigamiAction
87
+ from server.tasks import get_task_by_name
 
 
 
88
 
89
+ # Fall back to half_fold if target not found
90
+ folds = DEMO_SEQUENCES.get(target, DEMO_SEQUENCES["half_fold"])
91
 
92
+ env = OrigamiEnvironment()
93
+ obs = env.reset(task_name=target)
 
 
94
 
95
  steps: list[dict] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ for i, fold_dict in enumerate(folds):
98
+ if fold_dict.get("type") == "stop":
99
+ break
 
 
 
 
100
 
101
+ action = NewOrigamiAction(
102
+ fold_type=fold_dict["type"],
103
+ fold_line=fold_dict["line"],
104
+ fold_angle=float(fold_dict.get("angle", 180.0)),
 
 
 
 
 
 
 
 
 
 
 
 
105
  )
106
 
107
+ obs = env.step(action)
108
+
109
+ steps.append({
110
+ "step": i + 1,
111
+ "fold": fold_dict,
112
+ "paper_state": obs.paper_state,
113
+ "metrics": obs.metrics,
114
+ "done": obs.done,
115
+ })
116
+
117
+ if obs.done:
118
  break
119
 
120
+ task_def = get_task_by_name(target) if target else {}
121
+
122
  return {
123
+ "task_name": target,
124
+ "task": task_def,
125
  "steps": steps,
126
+ "final_metrics": obs.metrics if steps else {},
127
  }
128
 
129
 
 
 
 
 
 
 
 
130
  # ---------------------------------------------------------------------------
131
+ # Static file serving — must come LAST so API routes take priority
132
  # ---------------------------------------------------------------------------
133
 
134
  _BUILD_DIR = Path(__file__).resolve().parent.parent / "build"