864 lines
29 KiB
Markdown
864 lines
29 KiB
Markdown
|
|
# SaaS-Level Atomizer Roadmap (Revised)
|
||
|
|
|
||
|
|
## Executive Summary
|
||
|
|
|
||
|
|
This roadmap transforms Atomizer into a **SaaS-grade, LLM-assisted structural optimization platform** with the core innovation being **side-by-side Claude + Canvas** integration where:
|
||
|
|
|
||
|
|
1. **Claude talks → Canvas updates in real-time** (user sees nodes appear/change)
|
||
|
|
2. **User tweaks Canvas → Claude sees changes** (bi-directional sync)
|
||
|
|
3. **Full Claude Code-level power** through the dashboard chat
|
||
|
|
4. **Interview-driven study creation** entirely through conversation
|
||
|
|
|
||
|
|
**Vision**: An engineer opens Atomizer, describes their optimization goal, watches the canvas build itself, makes quick tweaks, and starts optimization—all through natural conversation with full visual feedback.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## The Core Innovation: Unified Claude + Canvas
|
||
|
|
|
||
|
|
The power of Atomizer is the **side-by-side experience**:
|
||
|
|
|
||
|
|
```
|
||
|
|
┌───────────────────────────────────────────────────────────────────┐
|
||
|
|
│ ATOMIZER DASHBOARD │
|
||
|
|
├────────────────────────────┬──────────────────────────────────────┤
|
||
|
|
│ CHAT PANEL │ CANVAS │
|
||
|
|
│ (Atomizer Assistant) │ (SpecRenderer) │
|
||
|
|
│ │ │
|
||
|
|
│ "Create a bracket │ [DV: thickness] │
|
||
|
|
│ optimization with │ │ │
|
||
|
|
│ mass and stiffness" │ ▼ │
|
||
|
|
│ │ │ [Model Node] │
|
||
|
|
│ ▼ │ │ │
|
||
|
|
│ 🔧 Adding thickness │ ▼ │
|
||
|
|
│ 🔧 Adding mass ext ◄───┼──►[Ext: mass]──>[Obj: min mass] │
|
||
|
|
│ 🔧 Adding stiffness ◄───┼──►[Ext: disp]──>[Obj: min disp] │
|
||
|
|
│ ✓ Study configured! │ │
|
||
|
|
│ │ (nodes appear in real-time) │
|
||
|
|
│ User clicks a node ───────┼──► Claude sees the edit │
|
||
|
|
│ │ │
|
||
|
|
└────────────────────────────┴──────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Current State vs Target
|
||
|
|
|
||
|
|
### What We Have
|
||
|
|
|
||
|
|
| Component | Status | Notes |
|
||
|
|
|-----------|--------|-------|
|
||
|
|
| Power Mode WebSocket | ✅ Implemented | `/ws/power` endpoint with write tools |
|
||
|
|
| Write Tools | ✅ Implemented | add_design_variable, add_extractor, etc. |
|
||
|
|
| spec_modified Events | ✅ Implemented | Frontend receives notifications |
|
||
|
|
| Canvas Auto-reload | ✅ Implemented | Triggers on spec_modified |
|
||
|
|
| Streaming Responses | ❌ Missing | Currently waits for full response |
|
||
|
|
| Canvas State → Claude | ❌ Missing | Claude doesn't see current canvas |
|
||
|
|
| Interview Engine | ❌ Missing | No guided creation |
|
||
|
|
| Unified WebSocket | ❌ Missing | Separate connections for chat/spec |
|
||
|
|
|
||
|
|
### Target Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────┐
|
||
|
|
│ Unified WebSocket │
|
||
|
|
│ /api/atomizer/ws │
|
||
|
|
└─────────┬───────────┘
|
||
|
|
│
|
||
|
|
Bi-directional Sync
|
||
|
|
│
|
||
|
|
┌────────────────────┼────────────────────┐
|
||
|
|
│ │ │
|
||
|
|
▼ ▼ ▼
|
||
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
|
|
│ Chat Panel │ │ Canvas │ │ Spec Store │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ Send messages │ │ User edits → │ │ Single source │
|
||
|
|
│ Stream text │ │ Notify Claude │ │ of truth │
|
||
|
|
│ See tool calls │ │ Receive updates │ │ Validates all │
|
||
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 1: Unified WebSocket Hub (1-2 weeks)
|
||
|
|
|
||
|
|
### Goal: Single connection for chat + canvas + spec sync
|
||
|
|
|
||
|
|
### 1.1 Backend: Unified WebSocket Endpoint
|
||
|
|
|
||
|
|
```python
|
||
|
|
# atomizer-dashboard/backend/api/routes/atomizer_ws.py
|
||
|
|
|
||
|
|
@router.websocket("/api/atomizer/ws")
|
||
|
|
async def atomizer_websocket(websocket: WebSocket):
|
||
|
|
"""
|
||
|
|
Unified WebSocket for Atomizer Dashboard.
|
||
|
|
|
||
|
|
Handles:
|
||
|
|
- Chat messages with streaming responses
|
||
|
|
- Spec modifications with real-time canvas updates
|
||
|
|
- Canvas edit notifications from user
|
||
|
|
- Study context switching
|
||
|
|
"""
|
||
|
|
await websocket.accept()
|
||
|
|
|
||
|
|
agent = AtomizerClaudeAgent()
|
||
|
|
conversation: List[Dict] = []
|
||
|
|
current_spec: Optional[Dict] = None
|
||
|
|
|
||
|
|
try:
|
||
|
|
while True:
|
||
|
|
data = await websocket.receive_json()
|
||
|
|
|
||
|
|
if data["type"] == "message":
|
||
|
|
# Chat message - stream response
|
||
|
|
async for event in agent.chat_stream(
|
||
|
|
message=data["content"],
|
||
|
|
conversation=conversation,
|
||
|
|
canvas_state=current_spec # Claude sees current canvas!
|
||
|
|
):
|
||
|
|
await websocket.send_json(event)
|
||
|
|
|
||
|
|
# If spec changed, update our local copy
|
||
|
|
if event.get("type") == "spec_updated":
|
||
|
|
current_spec = event["spec"]
|
||
|
|
|
||
|
|
elif data["type"] == "canvas_edit":
|
||
|
|
# User made a manual edit - update spec and tell Claude
|
||
|
|
current_spec = apply_patch(current_spec, data["patch"])
|
||
|
|
# Next message to Claude will include updated spec
|
||
|
|
|
||
|
|
elif data["type"] == "set_study":
|
||
|
|
# Switch study context
|
||
|
|
agent.set_study(data["study_id"])
|
||
|
|
current_spec = agent.load_spec()
|
||
|
|
await websocket.send_json({
|
||
|
|
"type": "spec_updated",
|
||
|
|
"spec": current_spec
|
||
|
|
})
|
||
|
|
|
||
|
|
except WebSocketDisconnect:
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 Enhanced Claude Agent with Streaming
|
||
|
|
|
||
|
|
```python
|
||
|
|
class AtomizerClaudeAgent:
|
||
|
|
"""Full-power Claude agent with Claude Code-like capabilities"""
|
||
|
|
|
||
|
|
async def chat_stream(
|
||
|
|
self,
|
||
|
|
message: str,
|
||
|
|
conversation: List[Dict],
|
||
|
|
canvas_state: Optional[Dict] = None
|
||
|
|
) -> AsyncGenerator[Dict, None]:
|
||
|
|
"""Stream responses with tool calls"""
|
||
|
|
|
||
|
|
# Build system prompt with current canvas state
|
||
|
|
system = self._build_system_prompt()
|
||
|
|
if canvas_state:
|
||
|
|
system += self._format_canvas_context(canvas_state)
|
||
|
|
|
||
|
|
messages = conversation + [{"role": "user", "content": message}]
|
||
|
|
|
||
|
|
# Use streaming API
|
||
|
|
with self.client.messages.stream(
|
||
|
|
model="claude-sonnet-4-20250514",
|
||
|
|
max_tokens=8192,
|
||
|
|
system=system,
|
||
|
|
messages=messages,
|
||
|
|
tools=self.tools
|
||
|
|
) as stream:
|
||
|
|
current_text = ""
|
||
|
|
|
||
|
|
for event in stream:
|
||
|
|
if event.type == "content_block_delta":
|
||
|
|
if hasattr(event.delta, "text"):
|
||
|
|
current_text += event.delta.text
|
||
|
|
yield {"type": "text", "content": event.delta.text, "done": False}
|
||
|
|
|
||
|
|
# Get final message for tool calls
|
||
|
|
response = stream.get_final_message()
|
||
|
|
|
||
|
|
# Process tool calls
|
||
|
|
for block in response.content:
|
||
|
|
if block.type == "tool_use":
|
||
|
|
yield {"type": "tool_start", "tool": block.name, "input": block.input}
|
||
|
|
|
||
|
|
result = await self._execute_tool(block.name, block.input)
|
||
|
|
|
||
|
|
yield {"type": "tool_result", "tool": block.name, "result": result["preview"]}
|
||
|
|
|
||
|
|
if result.get("spec_changed"):
|
||
|
|
yield {"type": "spec_updated", "spec": self.spec_store.get()}
|
||
|
|
|
||
|
|
yield {"type": "done"}
|
||
|
|
|
||
|
|
def _format_canvas_context(self, spec: Dict) -> str:
|
||
|
|
"""Format current canvas state for Claude's context"""
|
||
|
|
lines = ["\n## Current Canvas State\n"]
|
||
|
|
|
||
|
|
if spec.get("design_variables"):
|
||
|
|
lines.append(f"**Design Variables ({len(spec['design_variables'])}):**")
|
||
|
|
for dv in spec["design_variables"]:
|
||
|
|
lines.append(f" - {dv['name']}: [{dv['bounds']['min']}, {dv['bounds']['max']}]")
|
||
|
|
|
||
|
|
if spec.get("extractors"):
|
||
|
|
lines.append(f"\n**Extractors ({len(spec['extractors'])}):**")
|
||
|
|
for ext in spec["extractors"]:
|
||
|
|
lines.append(f" - {ext['name']} ({ext['type']})")
|
||
|
|
|
||
|
|
if spec.get("objectives"):
|
||
|
|
lines.append(f"\n**Objectives ({len(spec['objectives'])}):**")
|
||
|
|
for obj in spec["objectives"]:
|
||
|
|
lines.append(f" - {obj['name']} ({obj['direction']})")
|
||
|
|
|
||
|
|
if spec.get("constraints"):
|
||
|
|
lines.append(f"\n**Constraints ({len(spec['constraints'])}):**")
|
||
|
|
for con in spec["constraints"]:
|
||
|
|
lines.append(f" - {con['name']} {con['operator']} {con['threshold']}")
|
||
|
|
|
||
|
|
lines.append("\nThe user can see this canvas. When you modify it, they see changes in real-time.")
|
||
|
|
|
||
|
|
return "\n".join(lines)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 Frontend: Unified Hook
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// hooks/useAtomizerSocket.ts
|
||
|
|
export function useAtomizerSocket(studyId?: string) {
|
||
|
|
const [spec, setSpec] = useState<AtomizerSpec | null>(null);
|
||
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
||
|
|
const [isThinking, setIsThinking] = useState(false);
|
||
|
|
const [streamingText, setStreamingText] = useState("");
|
||
|
|
const [activeTool, setActiveTool] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const ws = useRef<WebSocket | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const url = `ws://localhost:8001/api/atomizer/ws`;
|
||
|
|
ws.current = new WebSocket(url);
|
||
|
|
|
||
|
|
ws.current.onopen = () => {
|
||
|
|
if (studyId) {
|
||
|
|
ws.current?.send(JSON.stringify({ type: "set_study", study_id: studyId }));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.current.onmessage = (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
|
||
|
|
switch (data.type) {
|
||
|
|
case "text":
|
||
|
|
setStreamingText(prev => prev + data.content);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "tool_start":
|
||
|
|
setActiveTool(data.tool);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "tool_result":
|
||
|
|
setActiveTool(null);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "spec_updated":
|
||
|
|
setSpec(data.spec); // Canvas updates automatically!
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "done":
|
||
|
|
// Finalize message
|
||
|
|
setMessages(prev => [...prev, {
|
||
|
|
role: "assistant",
|
||
|
|
content: streamingText
|
||
|
|
}]);
|
||
|
|
setStreamingText("");
|
||
|
|
setIsThinking(false);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return () => ws.current?.close();
|
||
|
|
}, [studyId]);
|
||
|
|
|
||
|
|
const sendMessage = useCallback((content: string) => {
|
||
|
|
setIsThinking(true);
|
||
|
|
setMessages(prev => [...prev, { role: "user", content }]);
|
||
|
|
ws.current?.send(JSON.stringify({ type: "message", content }));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const notifyCanvasEdit = useCallback((path: string, value: any) => {
|
||
|
|
ws.current?.send(JSON.stringify({
|
||
|
|
type: "canvas_edit",
|
||
|
|
patch: { path, value }
|
||
|
|
}));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return {
|
||
|
|
spec,
|
||
|
|
messages,
|
||
|
|
streamingText,
|
||
|
|
isThinking,
|
||
|
|
activeTool,
|
||
|
|
sendMessage,
|
||
|
|
notifyCanvasEdit
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 2: Full Tool Set (1-2 weeks)
|
||
|
|
|
||
|
|
### Goal: Claude Code-level power through dashboard
|
||
|
|
|
||
|
|
### 2.1 Tool Categories
|
||
|
|
|
||
|
|
```python
|
||
|
|
ATOMIZER_TOOLS = {
|
||
|
|
# === SPEC MODIFICATION (Already Implemented) ===
|
||
|
|
"add_design_variable": "Add a design variable to the optimization",
|
||
|
|
"add_extractor": "Add a physics extractor (mass, stress, displacement, custom)",
|
||
|
|
"add_objective": "Add an optimization objective",
|
||
|
|
"add_constraint": "Add a constraint",
|
||
|
|
"update_spec_field": "Update any field in the spec by JSON path",
|
||
|
|
"remove_node": "Remove a node from the spec",
|
||
|
|
|
||
|
|
# === READ/QUERY ===
|
||
|
|
"read_study_config": "Read the full atomizer_spec.json",
|
||
|
|
"query_trials": "Query optimization trial data",
|
||
|
|
"list_studies": "List all available studies",
|
||
|
|
"get_optimization_status": "Check if optimization is running",
|
||
|
|
|
||
|
|
# === STUDY MANAGEMENT (New) ===
|
||
|
|
"create_study": "Create a new study directory with atomizer_spec.json",
|
||
|
|
"clone_study": "Clone an existing study as a starting point",
|
||
|
|
"validate_spec": "Validate the current spec for errors",
|
||
|
|
|
||
|
|
# === NX INTEGRATION (New) ===
|
||
|
|
"introspect_model": "Analyze NX model for expressions and features",
|
||
|
|
"suggest_design_vars": "AI-suggest design variables from model",
|
||
|
|
"list_model_expressions": "List all expressions in the NX model",
|
||
|
|
|
||
|
|
# === OPTIMIZATION CONTROL (New) ===
|
||
|
|
"start_optimization": "Start the optimization run",
|
||
|
|
"stop_optimization": "Stop a running optimization",
|
||
|
|
|
||
|
|
# === INTERVIEW (New) ===
|
||
|
|
"start_interview": "Begin guided study creation interview",
|
||
|
|
"get_interview_progress": "Get current interview state",
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 Create Study Tool
|
||
|
|
|
||
|
|
```python
|
||
|
|
async def _tool_create_study(self, params: Dict) -> Dict:
|
||
|
|
"""Create a new study with atomizer_spec.json"""
|
||
|
|
study_name = params["name"]
|
||
|
|
study_dir = STUDIES_DIR / study_name
|
||
|
|
|
||
|
|
# Create directory structure
|
||
|
|
study_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
(study_dir / "1_setup").mkdir(exist_ok=True)
|
||
|
|
(study_dir / "2_iterations").mkdir(exist_ok=True)
|
||
|
|
(study_dir / "3_results").mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
# Create initial spec
|
||
|
|
spec = {
|
||
|
|
"meta": {
|
||
|
|
"version": "2.0",
|
||
|
|
"study_name": study_name,
|
||
|
|
"created_at": datetime.now().isoformat(),
|
||
|
|
"created_by": "claude_agent"
|
||
|
|
},
|
||
|
|
"model": {
|
||
|
|
"sim": {"path": params.get("model_path", ""), "solver": "nastran"}
|
||
|
|
},
|
||
|
|
"design_variables": [],
|
||
|
|
"extractors": [],
|
||
|
|
"objectives": [],
|
||
|
|
"constraints": [],
|
||
|
|
"optimization": {
|
||
|
|
"algorithm": {"type": "TPE"},
|
||
|
|
"budget": {"max_trials": 100}
|
||
|
|
},
|
||
|
|
"canvas": {"edges": [], "layout_version": "2.0"}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Save spec
|
||
|
|
spec_path = study_dir / "atomizer_spec.json"
|
||
|
|
with open(spec_path, "w") as f:
|
||
|
|
json.dump(spec, f, indent=2)
|
||
|
|
|
||
|
|
# Update agent's study context
|
||
|
|
self.study_id = study_name
|
||
|
|
self.study_dir = study_dir
|
||
|
|
|
||
|
|
return {
|
||
|
|
"preview": f"✓ Created study '{study_name}' at {study_dir}",
|
||
|
|
"spec_changed": True,
|
||
|
|
"study_id": study_name
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 NX Introspection Tool
|
||
|
|
|
||
|
|
```python
|
||
|
|
async def _tool_introspect_model(self, params: Dict) -> Dict:
|
||
|
|
"""Analyze NX model for design variable candidates"""
|
||
|
|
model_path = params.get("model_path") or self._find_model_path()
|
||
|
|
|
||
|
|
if not model_path or not Path(model_path).exists():
|
||
|
|
return {"preview": "✗ Model file not found", "spec_changed": False}
|
||
|
|
|
||
|
|
# Use NX session to get expressions
|
||
|
|
expressions = await self._get_nx_expressions(model_path)
|
||
|
|
|
||
|
|
# Classify expressions as potential DVs
|
||
|
|
candidates = []
|
||
|
|
for expr in expressions:
|
||
|
|
score = self._score_dv_candidate(expr)
|
||
|
|
if score > 0.5:
|
||
|
|
candidates.append({
|
||
|
|
"name": expr["name"],
|
||
|
|
"value": expr["value"],
|
||
|
|
"formula": expr.get("formula", ""),
|
||
|
|
"score": score,
|
||
|
|
"suggested_bounds": self._suggest_bounds(expr)
|
||
|
|
})
|
||
|
|
|
||
|
|
# Sort by score
|
||
|
|
candidates.sort(key=lambda x: x["score"], reverse=True)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"preview": f"Found {len(expressions)} expressions, {len(candidates)} are DV candidates",
|
||
|
|
"expressions": expressions,
|
||
|
|
"candidates": candidates[:10], # Top 10
|
||
|
|
"spec_changed": False
|
||
|
|
}
|
||
|
|
|
||
|
|
def _score_dv_candidate(self, expr: Dict) -> float:
|
||
|
|
"""Score expression as design variable candidate"""
|
||
|
|
score = 0.0
|
||
|
|
name = expr["name"].lower()
|
||
|
|
|
||
|
|
# Geometric parameters score high
|
||
|
|
if any(kw in name for kw in ["thickness", "width", "height", "radius", "diameter", "depth"]):
|
||
|
|
score += 0.4
|
||
|
|
|
||
|
|
# Numeric with reasonable value
|
||
|
|
if isinstance(expr["value"], (int, float)) and expr["value"] > 0:
|
||
|
|
score += 0.2
|
||
|
|
|
||
|
|
# Not a formula (pure number)
|
||
|
|
if not expr.get("formula") or expr["formula"] == str(expr["value"]):
|
||
|
|
score += 0.2
|
||
|
|
|
||
|
|
# Common design parameter names
|
||
|
|
if any(kw in name for kw in ["rib", "web", "flange", "support", "angle"]):
|
||
|
|
score += 0.2
|
||
|
|
|
||
|
|
return min(score, 1.0)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 3: Interview Engine (1-2 weeks)
|
||
|
|
|
||
|
|
### Goal: Guided study creation through conversation
|
||
|
|
|
||
|
|
### 3.1 Interview Engine
|
||
|
|
|
||
|
|
```python
|
||
|
|
class InterviewEngine:
|
||
|
|
"""Guided study creation through conversation"""
|
||
|
|
|
||
|
|
PHASES = [
|
||
|
|
("welcome", "What kind of optimization do you want to set up?"),
|
||
|
|
("model", "What's the path to your NX simulation file (.sim)?"),
|
||
|
|
("objectives", "What do you want to optimize? (e.g., minimize mass, minimize displacement)"),
|
||
|
|
("design_vars", "Which parameters should vary? I can suggest some from your model."),
|
||
|
|
("constraints", "Any constraints to respect? (e.g., max stress ≤ 200 MPa)"),
|
||
|
|
("method", "I recommend {method} for this. Sound good?"),
|
||
|
|
("review", "Here's your configuration. Ready to create the study?"),
|
||
|
|
]
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.phase_index = 0
|
||
|
|
self.collected = {}
|
||
|
|
self.spec_builder = SpecBuilder()
|
||
|
|
|
||
|
|
def get_current_question(self) -> str:
|
||
|
|
phase, question = self.PHASES[self.phase_index]
|
||
|
|
|
||
|
|
# Dynamic question customization
|
||
|
|
if phase == "method":
|
||
|
|
method = self._recommend_method()
|
||
|
|
question = question.format(method=method)
|
||
|
|
elif phase == "design_vars" and self.collected.get("model_expressions"):
|
||
|
|
candidates = self.collected["model_expressions"][:5]
|
||
|
|
question += f"\n\nI found these candidates: {', '.join(c['name'] for c in candidates)}"
|
||
|
|
|
||
|
|
return question
|
||
|
|
|
||
|
|
def process_answer(self, answer: str) -> Dict:
|
||
|
|
"""Process user's answer and advance interview"""
|
||
|
|
phase, _ = self.PHASES[self.phase_index]
|
||
|
|
|
||
|
|
# Extract structured data based on phase
|
||
|
|
extracted = self._extract_for_phase(phase, answer)
|
||
|
|
self.collected[phase] = extracted
|
||
|
|
|
||
|
|
# Build spec incrementally
|
||
|
|
spec_changes = self.spec_builder.apply(phase, extracted)
|
||
|
|
|
||
|
|
# Advance
|
||
|
|
self.phase_index += 1
|
||
|
|
complete = self.phase_index >= len(self.PHASES)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"phase_completed": phase,
|
||
|
|
"extracted": extracted,
|
||
|
|
"spec_changes": spec_changes,
|
||
|
|
"next_question": None if complete else self.get_current_question(),
|
||
|
|
"complete": complete,
|
||
|
|
"spec": self.spec_builder.get_spec() if complete else None
|
||
|
|
}
|
||
|
|
|
||
|
|
def _extract_for_phase(self, phase: str, answer: str) -> Dict:
|
||
|
|
"""Extract structured data from natural language answer"""
|
||
|
|
if phase == "model":
|
||
|
|
# Extract file path
|
||
|
|
return {"path": self._extract_path(answer)}
|
||
|
|
|
||
|
|
elif phase == "objectives":
|
||
|
|
# Extract objectives
|
||
|
|
objectives = []
|
||
|
|
if "mass" in answer.lower() or "weight" in answer.lower():
|
||
|
|
direction = "minimize" if "minimize" in answer.lower() or "reduce" in answer.lower() else "minimize"
|
||
|
|
objectives.append({"name": "mass", "direction": direction})
|
||
|
|
if "displacement" in answer.lower() or "stiff" in answer.lower():
|
||
|
|
objectives.append({"name": "max_displacement", "direction": "minimize"})
|
||
|
|
if "stress" in answer.lower():
|
||
|
|
objectives.append({"name": "max_stress", "direction": "minimize"})
|
||
|
|
if "wfe" in answer.lower() or "wavefront" in answer.lower():
|
||
|
|
objectives.append({"name": "wfe", "direction": "minimize"})
|
||
|
|
return {"objectives": objectives}
|
||
|
|
|
||
|
|
elif phase == "constraints":
|
||
|
|
# Extract constraints
|
||
|
|
constraints = []
|
||
|
|
import re
|
||
|
|
# Pattern: "stress < 200 MPa" or "max stress <= 200"
|
||
|
|
stress_match = re.search(r'stress[^0-9]*([<>=]+)\s*(\d+)', answer.lower())
|
||
|
|
if stress_match:
|
||
|
|
constraints.append({
|
||
|
|
"name": "max_stress",
|
||
|
|
"operator": stress_match.group(1),
|
||
|
|
"threshold": float(stress_match.group(2))
|
||
|
|
})
|
||
|
|
return {"constraints": constraints}
|
||
|
|
|
||
|
|
return {"raw": answer}
|
||
|
|
|
||
|
|
def _recommend_method(self) -> str:
|
||
|
|
"""Recommend optimization method based on collected info"""
|
||
|
|
objectives = self.collected.get("objectives", {}).get("objectives", [])
|
||
|
|
if len(objectives) > 1:
|
||
|
|
return "NSGA-II (multi-objective)"
|
||
|
|
return "TPE (Bayesian optimization)"
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Interview Tool Integration
|
||
|
|
|
||
|
|
```python
|
||
|
|
async def _tool_start_interview(self, params: Dict) -> Dict:
|
||
|
|
"""Start guided study creation"""
|
||
|
|
self.interview = InterviewEngine()
|
||
|
|
question = self.interview.get_current_question()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"preview": f"Starting interview.\n\n{question}",
|
||
|
|
"interview_started": True,
|
||
|
|
"spec_changed": False
|
||
|
|
}
|
||
|
|
|
||
|
|
async def _tool_interview_answer(self, params: Dict) -> Dict:
|
||
|
|
"""Process interview answer"""
|
||
|
|
if not self.interview:
|
||
|
|
return {"preview": "No interview in progress", "spec_changed": False}
|
||
|
|
|
||
|
|
result = self.interview.process_answer(params["answer"])
|
||
|
|
|
||
|
|
response = f"Got it: {result['phase_completed']}\n\n"
|
||
|
|
|
||
|
|
if result["spec_changes"]:
|
||
|
|
response += "Updated configuration:\n"
|
||
|
|
for change in result["spec_changes"]:
|
||
|
|
response += f" ✓ {change}\n"
|
||
|
|
|
||
|
|
if result["next_question"]:
|
||
|
|
response += f"\n{result['next_question']}"
|
||
|
|
elif result["complete"]:
|
||
|
|
response += "\n✓ Interview complete! Creating study..."
|
||
|
|
# Auto-create the study
|
||
|
|
self.spec_store.set(result["spec"])
|
||
|
|
|
||
|
|
return {
|
||
|
|
"preview": response,
|
||
|
|
"spec_changed": result["complete"],
|
||
|
|
"complete": result["complete"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 4: Visual Polish (1 week)
|
||
|
|
|
||
|
|
### Goal: Beautiful, responsive canvas updates
|
||
|
|
|
||
|
|
### 4.1 Tool Call Visualization
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// components/chat/ToolCallIndicator.tsx
|
||
|
|
function ToolCallIndicator({ tool, status }: { tool: string; status: 'running' | 'complete' }) {
|
||
|
|
const icons: Record<string, JSX.Element> = {
|
||
|
|
add_design_variable: <Variable className="w-4 h-4" />,
|
||
|
|
add_extractor: <Cpu className="w-4 h-4" />,
|
||
|
|
add_objective: <Target className="w-4 h-4" />,
|
||
|
|
add_constraint: <Lock className="w-4 h-4" />,
|
||
|
|
create_study: <FolderPlus className="w-4 h-4" />,
|
||
|
|
introspect_model: <Search className="w-4 h-4" />,
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${
|
||
|
|
status === 'running'
|
||
|
|
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
||
|
|
: 'bg-green-500/10 text-green-400 border border-green-500/20'
|
||
|
|
}`}>
|
||
|
|
{status === 'running' ? (
|
||
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Check className="w-4 h-4" />
|
||
|
|
)}
|
||
|
|
{icons[tool] || <Wrench className="w-4 h-4" />}
|
||
|
|
<span className="text-sm font-medium">
|
||
|
|
{formatToolName(tool)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 Canvas Node Animation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// components/canvas/AnimatedNode.tsx
|
||
|
|
function AnimatedNode({ data, isNew, isHighlighted }) {
|
||
|
|
return (
|
||
|
|
<motion.div
|
||
|
|
initial={isNew ? { scale: 0, opacity: 0 } : false}
|
||
|
|
animate={{
|
||
|
|
scale: 1,
|
||
|
|
opacity: 1,
|
||
|
|
boxShadow: isHighlighted
|
||
|
|
? '0 0 0 3px rgba(245, 158, 11, 0.5)'
|
||
|
|
: 'none'
|
||
|
|
}}
|
||
|
|
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||
|
|
className="node-container"
|
||
|
|
>
|
||
|
|
{/* Node content */}
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 Connection Line Animation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Draw animated line when Claude adds an edge
|
||
|
|
function AnimatedEdge({ source, target, isNew }) {
|
||
|
|
return (
|
||
|
|
<motion.path
|
||
|
|
d={getBezierPath({ source, target })}
|
||
|
|
initial={isNew ? { pathLength: 0, opacity: 0 } : false}
|
||
|
|
animate={{ pathLength: 1, opacity: 1 }}
|
||
|
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth={2}
|
||
|
|
fill="none"
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## User Experience Flows
|
||
|
|
|
||
|
|
### Flow 1: Quick Creation (Experienced User)
|
||
|
|
|
||
|
|
```
|
||
|
|
User: "Create bracket optimization, minimize mass, thickness 2-10mm, max stress 200 MPa"
|
||
|
|
|
||
|
|
Claude: [Parses complete intent]
|
||
|
|
🔧 Creating study "bracket_optimization"
|
||
|
|
🔧 Adding design variable: thickness [2mm - 10mm]
|
||
|
|
🔧 Adding extractor: mass
|
||
|
|
🔧 Adding extractor: max_stress
|
||
|
|
🔧 Adding objective: minimize mass
|
||
|
|
🔧 Adding constraint: stress ≤ 200 MPa
|
||
|
|
|
||
|
|
✓ Created! Canvas shows your setup. Click any node to adjust.
|
||
|
|
|
||
|
|
[Canvas animates: DV appears → Model → Extractors → Objectives/Constraints]
|
||
|
|
[Total time: ~5 seconds]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Flow 2: Guided Interview (New User)
|
||
|
|
|
||
|
|
```
|
||
|
|
User: "Help me set up an optimization"
|
||
|
|
|
||
|
|
Claude: What kind of optimization do you want to set up?
|
||
|
|
(e.g., bracket stiffness, mirror WFE, beam stress)
|
||
|
|
|
||
|
|
User: "Mirror surface quality optimization"
|
||
|
|
|
||
|
|
Claude: Got it - mirror optimization!
|
||
|
|
|
||
|
|
What's the path to your NX simulation file?
|
||
|
|
(You can drag & drop or paste the path)
|
||
|
|
|
||
|
|
User: "studies/M1_Mirror/model.sim"
|
||
|
|
|
||
|
|
Claude: 🔧 Loading model...
|
||
|
|
|
||
|
|
Found your model with 15 expressions. These look like good design variables:
|
||
|
|
- rib_thickness (8mm)
|
||
|
|
- mirror_depth (50mm)
|
||
|
|
- support_angle (45°)
|
||
|
|
|
||
|
|
What are you trying to optimize?
|
||
|
|
|
||
|
|
[Canvas: Model node appears]
|
||
|
|
|
||
|
|
User: "Minimize wavefront error and mass"
|
||
|
|
|
||
|
|
Claude: 🔧 Adding Zernike WFE extractor
|
||
|
|
🔧 Adding mass extractor
|
||
|
|
🔧 Adding objectives (multi-objective)
|
||
|
|
|
||
|
|
Multi-objective optimization! I recommend NSGA-II.
|
||
|
|
|
||
|
|
Which parameters should vary?
|
||
|
|
|
||
|
|
[Canvas: Extractors and objectives appear, connected to model]
|
||
|
|
|
||
|
|
User: "Use rib_thickness and mirror_depth, 50% range on each"
|
||
|
|
|
||
|
|
Claude: 🔧 Adding rib_thickness [4mm - 12mm]
|
||
|
|
🔧 Adding mirror_depth [25mm - 75mm]
|
||
|
|
|
||
|
|
Any constraints?
|
||
|
|
|
||
|
|
[Canvas: DVs appear, connected to model]
|
||
|
|
|
||
|
|
User: "Keep stress under 150 MPa"
|
||
|
|
|
||
|
|
Claude: 🔧 Adding stress extractor
|
||
|
|
🔧 Adding constraint: stress ≤ 150 MPa
|
||
|
|
|
||
|
|
✓ Configuration complete!
|
||
|
|
|
||
|
|
Your setup:
|
||
|
|
- 2 design variables
|
||
|
|
- 3 extractors (WFE, mass, stress)
|
||
|
|
- 2 objectives (multi-objective)
|
||
|
|
- 1 constraint
|
||
|
|
|
||
|
|
Ready to run? Say "start" or adjust the canvas first.
|
||
|
|
|
||
|
|
[Canvas: Full optimization flow visible]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Flow 3: User Edits, Claude Adapts
|
||
|
|
|
||
|
|
```
|
||
|
|
[User clicks rib_thickness node, changes max from 12mm to 20mm]
|
||
|
|
|
||
|
|
User: "Is this a good range?"
|
||
|
|
|
||
|
|
Claude: I see you expanded rib_thickness to [4mm - 20mm].
|
||
|
|
|
||
|
|
That's a wide range! Considerations:
|
||
|
|
- Thicker ribs = more mass, more stiffness
|
||
|
|
- 20mm might exceed typical manufacturing limits
|
||
|
|
- Wider range = more trials to explore
|
||
|
|
|
||
|
|
If you're exploring, this is fine. For production,
|
||
|
|
I'd suggest [4mm - 15mm].
|
||
|
|
|
||
|
|
Want me to adjust, or keep your change?
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Priority
|
||
|
|
|
||
|
|
### Week 1-2: Unified WebSocket
|
||
|
|
- [ ] Create `/api/atomizer/ws` endpoint
|
||
|
|
- [ ] Implement streaming in `AtomizerClaudeAgent`
|
||
|
|
- [ ] Create `useAtomizerSocket` hook
|
||
|
|
- [ ] Wire canvas to receive spec updates
|
||
|
|
- [ ] Add canvas edit notifications
|
||
|
|
|
||
|
|
### Week 3-4: Tools & Interview
|
||
|
|
- [ ] Add `create_study` tool
|
||
|
|
- [ ] Add `introspect_model` tool
|
||
|
|
- [ ] Implement `InterviewEngine`
|
||
|
|
- [ ] Add interview tools
|
||
|
|
- [ ] Test guided creation flow
|
||
|
|
|
||
|
|
### Week 5: Polish
|
||
|
|
- [ ] Tool call indicators in chat
|
||
|
|
- [ ] Node appear/highlight animations
|
||
|
|
- [ ] Edge draw animations
|
||
|
|
- [ ] Error recovery & reconnection
|
||
|
|
- [ ] Performance optimization
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Success Metrics
|
||
|
|
|
||
|
|
| Metric | Target |
|
||
|
|
|--------|--------|
|
||
|
|
| Study creation time (experienced) | < 30 seconds |
|
||
|
|
| Study creation time (interview) | < 3 minutes |
|
||
|
|
| Canvas update latency | < 200ms |
|
||
|
|
| User edit → Claude context | < 100ms |
|
||
|
|
| Interview completion rate | > 90% |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Files to Modify
|
||
|
|
|
||
|
|
### Backend
|
||
|
|
- `atomizer-dashboard/backend/api/routes/atomizer_ws.py` (new)
|
||
|
|
- `atomizer-dashboard/backend/api/services/claude_agent.py` (enhance)
|
||
|
|
- `atomizer-dashboard/backend/api/services/interview_engine.py` (new)
|
||
|
|
|
||
|
|
### Frontend
|
||
|
|
- `atomizer-dashboard/frontend/src/hooks/useAtomizerSocket.ts` (new)
|
||
|
|
- `atomizer-dashboard/frontend/src/pages/CanvasView.tsx` (update)
|
||
|
|
- `atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx` (update)
|
||
|
|
- `atomizer-dashboard/frontend/src/components/chat/ToolCallIndicator.tsx` (new)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*This architecture makes Atomizer uniquely powerful: natural language + visual feedback + full control, all in one seamless experience.*
|