CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
2d9cf0c
·
1 Parent(s): eb89dca

feat: add /api/plan/approve and /api/plan/reject endpoints

Browse files

Add two new API endpoints enabling the frontend planning review gate:
- POST /api/plan/approve: merges an (optionally edited) DesignPlan into
DesignState, sets phase="approved"
- POST /api/plan/reject: resets DesignState phase to "exploring" and
clears the plan field

Also adds PlanApproveRequest and PlanRejectRequest Pydantic models, and
three new test cases covering phase transitions and plan merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. server/routes.py +36 -0
  2. tests/test_api_routes.py +66 -0
server/routes.py CHANGED
@@ -8,6 +8,7 @@ from fastapi import APIRouter
8
  from fastapi.responses import JSONResponse
9
  from pydantic import BaseModel, Field
10
 
 
11
  from agents.orchestrator import get_orchestrator
12
  from agents.prompts import parse_mentions
13
  from agents.definitions import AGENTS
@@ -43,6 +44,15 @@ class ReportRequest(BaseModel):
43
  backend: str = "gemini"
44
 
45
 
 
 
 
 
 
 
 
 
 
46
  # ── Endpoints ──────────────────────────────────────────────────────────────
47
 
48
 
@@ -149,3 +159,29 @@ async def list_agents():
149
  for agent in AGENTS.values()
150
  ]
151
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  from fastapi.responses import JSONResponse
9
  from pydantic import BaseModel, Field
10
 
11
+ from agents.design_state import DesignState, DesignPlan
12
  from agents.orchestrator import get_orchestrator
13
  from agents.prompts import parse_mentions
14
  from agents.definitions import AGENTS
 
44
  backend: str = "gemini"
45
 
46
 
47
+ class PlanApproveRequest(BaseModel):
48
+ plan: dict = Field(...)
49
+ design_state: dict = Field(default_factory=dict)
50
+
51
+
52
+ class PlanRejectRequest(BaseModel):
53
+ design_state: dict = Field(default_factory=dict)
54
+
55
+
56
  # ── Endpoints ──────────────────────────────────────────────────────────────
57
 
58
 
 
159
  for agent in AGENTS.values()
160
  ]
161
  })
162
+
163
+
164
+ @router.post("/api/plan/approve")
165
+ async def plan_approve(body: PlanApproveRequest):
166
+ """Approve (possibly edited) design plan, merge into state."""
167
+ plan = DesignPlan(**body.plan)
168
+ state = DesignState(**body.design_state)
169
+ state.part_name = plan.part_name
170
+ state.description = plan.description
171
+ state.material = plan.material
172
+ state.dimensions = dict(plan.dimensions)
173
+ state.features = list(plan.features)
174
+ state.constraints = list(plan.constraints)
175
+ state.axis_recommendation = plan.axis_recommendation
176
+ state.phase = "approved"
177
+ state.plan = plan
178
+ return JSONResponse({"design_state": state.model_dump()})
179
+
180
+
181
+ @router.post("/api/plan/reject")
182
+ async def plan_reject(body: PlanRejectRequest):
183
+ """Reject plan, reset to exploring."""
184
+ state = DesignState(**body.design_state)
185
+ state.phase = "exploring"
186
+ state.plan = None
187
+ return JSONResponse({"design_state": state.model_dump()})
tests/test_api_routes.py CHANGED
@@ -125,3 +125,69 @@ class TestAgentsEndpoint:
125
  assert "role" in agent
126
  assert "color" in agent
127
  assert "avatar" in agent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  assert "role" in agent
126
  assert "color" in agent
127
  assert "avatar" in agent
128
+
129
+
130
+ class TestPlanApproveEndpoint:
131
+ def test_approve_sets_phase(self):
132
+ resp = client.post("/api/plan/approve", json={
133
+ "plan": {
134
+ "part_name": "bracket",
135
+ "description": "test",
136
+ "material": "aluminum 6061",
137
+ "dimensions": {"width": 60},
138
+ "features": ["4x M6 holes"],
139
+ "constraints": ["min wall 3mm"],
140
+ "axis_recommendation": "3-axis",
141
+ "machining_notes": [],
142
+ "confidence_score": 9.0,
143
+ },
144
+ "design_state": {
145
+ "part_name": "bracket",
146
+ "material": "steel",
147
+ "dimensions": {"width": 50},
148
+ "phase": "planning",
149
+ },
150
+ })
151
+ assert resp.status_code == 200
152
+ data = resp.json()
153
+ assert data["design_state"]["phase"] == "approved"
154
+ assert data["design_state"]["material"] == "aluminum 6061"
155
+ assert data["design_state"]["dimensions"]["width"] == 60
156
+
157
+ def test_approve_merges_plan_into_state(self):
158
+ resp = client.post("/api/plan/approve", json={
159
+ "plan": {
160
+ "part_name": "gear",
161
+ "description": "spur gear",
162
+ "material": "brass",
163
+ "dimensions": {"diameter": 40},
164
+ "features": [],
165
+ "constraints": [],
166
+ "axis_recommendation": "3-axis",
167
+ "machining_notes": ["No undercuts"],
168
+ "confidence_score": 8.0,
169
+ },
170
+ "design_state": {"phase": "planning"},
171
+ })
172
+ data = resp.json()
173
+ assert data["design_state"]["part_name"] == "gear"
174
+ assert data["design_state"]["plan"]["material"] == "brass"
175
+
176
+
177
+ class TestPlanRejectEndpoint:
178
+ def test_reject_resets_phase(self):
179
+ resp = client.post("/api/plan/reject", json={
180
+ "design_state": {
181
+ "phase": "planning",
182
+ "material": "aluminum",
183
+ "plan": {"part_name": "x", "description": "", "material": "aluminum",
184
+ "dimensions": {}, "features": [], "constraints": [],
185
+ "axis_recommendation": "", "machining_notes": [],
186
+ "confidence_score": 5.0},
187
+ },
188
+ })
189
+ assert resp.status_code == 200
190
+ data = resp.json()
191
+ assert data["design_state"]["phase"] == "exploring"
192
+ assert data["design_state"]["plan"] is None
193
+ assert data["design_state"]["material"] == "aluminum"