From 1ae35382dabd6ed5a9010ab115e89a6b4f43409c Mon Sep 17 00:00:00 2001 From: Anto01 Date: Wed, 14 Jan 2026 20:18:46 -0500 Subject: [PATCH] feat: Phase 2 - LLM Integration for Canvas - Add canvas.ts MCP tool with validate_canvas_intent, execute_canvas_intent, interpret_canvas_intent - Add useCanvasChat.ts bridge hook connecting canvas to chat system - Update context_builder.py with canvas tool instructions - Add ExecuteDialog for study name input - Add ChatPanel for canvas-integrated Claude responses - Connect AtomizerCanvas to Claude via useCanvasChat Canvas workflow now: 1. Build graph visually 2. Click Validate/Analyze/Execute 3. Claude processes intent via MCP tools 4. Response shown in integrated chat panel Co-Authored-By: Claude Opus 4.5 --- .../backend/api/services/context_builder.py | 8 + .../src/components/canvas/AtomizerCanvas.tsx | 109 +++- .../frontend/src/components/canvas/index.ts | 2 + .../components/canvas/panels/ChatPanel.tsx | 48 ++ .../canvas/panels/ExecuteDialog.tsx | 131 ++++ .../frontend/src/hooks/useCanvasChat.ts | 184 ++++++ mcp-server/atomizer-tools/src/index.ts | 2 + mcp-server/atomizer-tools/src/tools/canvas.ts | 578 ++++++++++++++++++ 8 files changed, 1051 insertions(+), 11 deletions(-) create mode 100644 atomizer-dashboard/frontend/src/components/canvas/panels/ChatPanel.tsx create mode 100644 atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx create mode 100644 atomizer-dashboard/frontend/src/hooks/useCanvasChat.ts create mode 100644 mcp-server/atomizer-tools/src/tools/canvas.ts diff --git a/atomizer-dashboard/backend/api/services/context_builder.py b/atomizer-dashboard/backend/api/services/context_builder.py index 555c68ed..105f09ea 100644 --- a/atomizer-dashboard/backend/api/services/context_builder.py +++ b/atomizer-dashboard/backend/api/services/context_builder.py @@ -235,4 +235,12 @@ Available tools: - `get_trial_data`, `analyze_convergence`, `compare_trials`, `get_best_design` - `generate_report`, `export_data` - `explain_physics`, `recommend_method`, `query_extractors` + +**Canvas Tools (for visual workflow builder):** +- `validate_canvas_intent` - Validate a canvas-generated optimization intent +- `execute_canvas_intent` - Create a study from a canvas intent +- `interpret_canvas_intent` - Analyze intent and provide recommendations + +When you receive a message containing "INTENT:" followed by JSON, this is from the Canvas UI. +Parse the intent and use the appropriate canvas tool to process it. """ diff --git a/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx index c1a8c59e..310c3d0f 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, DragEvent } from 'react'; +import { useCallback, useRef, useState, DragEvent } from 'react'; import ReactFlow, { Background, Controls, @@ -12,12 +12,17 @@ import { nodeTypes } from './nodes'; import { NodePalette } from './palette/NodePalette'; import { NodeConfigPanel } from './panels/NodeConfigPanel'; import { ValidationPanel } from './panels/ValidationPanel'; +import { ExecuteDialog } from './panels/ExecuteDialog'; import { useCanvasStore } from '../../hooks/useCanvasStore'; +import { useCanvasChat } from '../../hooks/useCanvasChat'; import { NodeType } from '../../lib/canvas/schema'; +import { ChatPanel } from './panels/ChatPanel'; function CanvasFlow() { const reactFlowWrapper = useRef(null); const reactFlowInstance = useRef(null); + const [showExecuteDialog, setShowExecuteDialog] = useState(false); + const [showChat, setShowChat] = useState(false); const { nodes, @@ -33,6 +38,18 @@ function CanvasFlow() { toIntent, } = useCanvasStore(); + const { + messages, + isThinking, + isExecuting, + isConnected, + executeIntent, + validateIntent, + analyzeIntent, + } = useCanvasChat({ + onError: (error) => console.error('Canvas chat error:', error), + }); + const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; @@ -67,17 +84,39 @@ function CanvasFlow() { selectNode(null); }, [selectNode]); - const handleExecute = () => { + const handleValidate = () => { + const result = validate(); + if (result.valid) { + // Also send to Claude for intelligent feedback + const intent = toIntent(); + validateIntent(intent); + setShowChat(true); + } + }; + + const handleAnalyze = () => { const result = validate(); if (result.valid) { const intent = toIntent(); - // Send to chat - console.log('Executing intent:', JSON.stringify(intent, null, 2)); - // TODO: Connect to useChat hook - alert('Intent generated! Check console for JSON output.\n\nIn Phase 2, this will be sent to Claude.'); + analyzeIntent(intent); + setShowChat(true); } }; + const handleExecuteClick = () => { + const result = validate(); + if (result.valid) { + setShowExecuteDialog(true); + } + }; + + const handleExecute = async (studyName: string, autoRun: boolean) => { + const intent = toIntent(); + await executeIntent(intent, studyName, autoRun); + setShowExecuteDialog(false); + setShowChat(true); + }; + return (
{/* Left: Node Palette */} @@ -104,16 +143,38 @@ function CanvasFlow() { - {/* Execute Button */} + {/* Action Buttons */}
+ + +
+ +
+ ) : selectedNode ? ( + + ) : null} + + {/* Execute Dialog */} + setShowExecuteDialog(false)} + onExecute={handleExecute} + isExecuting={isExecuting} + /> ); } diff --git a/atomizer-dashboard/frontend/src/components/canvas/index.ts b/atomizer-dashboard/frontend/src/components/canvas/index.ts index 4a14d2f2..753a0264 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/index.ts +++ b/atomizer-dashboard/frontend/src/components/canvas/index.ts @@ -2,4 +2,6 @@ export { AtomizerCanvas } from './AtomizerCanvas'; export { NodePalette } from './palette/NodePalette'; export { NodeConfigPanel } from './panels/NodeConfigPanel'; export { ValidationPanel } from './panels/ValidationPanel'; +export { ExecuteDialog } from './panels/ExecuteDialog'; +export { ChatPanel } from './panels/ChatPanel'; export * from './nodes'; diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/ChatPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/ChatPanel.tsx new file mode 100644 index 00000000..1afcff85 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/ChatPanel.tsx @@ -0,0 +1,48 @@ +/** + * Chat Panel for Canvas - Displays messages from Claude + */ + +import { useRef, useEffect } from 'react'; +import { Message, ChatMessage } from '../../chat/ChatMessage'; +import { ThinkingIndicator } from '../../chat/ThinkingIndicator'; + +interface ChatPanelProps { + messages: Message[]; + isThinking: boolean; +} + +export function ChatPanel({ messages, isThinking }: ChatPanelProps) { + const messagesEndRef = useRef(null); + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, isThinking]); + + return ( +
+ {/* Welcome message if no messages */} + {messages.length === 0 && !isThinking && ( +
+
+ 🧠 +
+

+ Use Validate, Analyze, or Execute to interact with Claude about your optimization workflow. +

+
+ )} + + {/* Messages */} + {messages.map((message) => ( + + ))} + + {/* Thinking indicator */} + {isThinking && } + + {/* Scroll anchor */} +
+
+ ); +} diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx new file mode 100644 index 00000000..08ceb382 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx @@ -0,0 +1,131 @@ +/** + * Execute Dialog - Prompts for study name before executing canvas intent + */ + +import { useState } from 'react'; + +interface ExecuteDialogProps { + isOpen: boolean; + onClose: () => void; + onExecute: (studyName: string, autoRun: boolean) => void; + isExecuting: boolean; +} + +export function ExecuteDialog({ + isOpen, + onClose, + onExecute, + isExecuting, +}: ExecuteDialogProps) { + const [studyName, setStudyName] = useState(''); + const [autoRun, setAutoRun] = useState(false); + const [error, setError] = useState(null); + + if (!isOpen) return null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Validate study name + const trimmed = studyName.trim(); + if (!trimmed) { + setError('Study name is required'); + return; + } + + // Check for valid snake_case + if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) { + setError('Study name must be snake_case (lowercase letters, numbers, underscores)'); + return; + } + + setError(null); + onExecute(trimmed, autoRun); + }; + + const handleClose = () => { + setStudyName(''); + setAutoRun(false); + setError(null); + onClose(); + }; + + return ( +
+
+

+ Execute with Claude +

+ +
+
+ + setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))} + placeholder="my_optimization_study" + className="w-full px-3 py-2 border rounded-lg font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={isExecuting} + autoFocus + /> + {error && ( +

{error}

+ )} +

+ Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization) +

+
+ +
+ +
+ +
+ + +
+
+
+
+ ); +} diff --git a/atomizer-dashboard/frontend/src/hooks/useCanvasChat.ts b/atomizer-dashboard/frontend/src/hooks/useCanvasChat.ts new file mode 100644 index 00000000..3587b22d --- /dev/null +++ b/atomizer-dashboard/frontend/src/hooks/useCanvasChat.ts @@ -0,0 +1,184 @@ +/** + * Canvas-Chat Bridge Hook + * + * Bridges the Canvas UI with the Chat system, allowing canvas intents + * to be sent to Claude for intelligent execution. + */ + +import { useCallback, useState } from 'react'; +import { useChat, ChatMode } from './useChat'; +import { OptimizationIntent, formatIntentForChat } from '../lib/canvas/intent'; + +interface UseCanvasChatOptions { + mode?: ChatMode; + onError?: (error: string) => void; +} + +interface CanvasChatState { + isExecuting: boolean; + lastIntent: OptimizationIntent | null; + executionResult: ExecutionResult | null; +} + +interface ExecutionResult { + success: boolean; + action: string; + studyName?: string; + path?: string; + error?: string; + message?: string; +} + +export function useCanvasChat({ + mode = 'user', + onError, +}: UseCanvasChatOptions = {}) { + const chat = useChat({ mode, onError }); + + const [state, setState] = useState({ + isExecuting: false, + lastIntent: null, + executionResult: null, + }); + + /** + * Submit an intent for validation only (no execution) + */ + const validateIntent = useCallback( + async (intent: OptimizationIntent): Promise => { + setState((prev) => ({ + ...prev, + isExecuting: true, + lastIntent: intent, + executionResult: null, + })); + + // Format intent for chat and ask Claude to validate + const message = `Please validate this canvas optimization intent: + +${formatIntentForChat(intent)} + +Use the validate_canvas_intent tool to check for errors and provide feedback.`; + + await chat.sendMessage(message); + + setState((prev) => ({ + ...prev, + isExecuting: false, + })); + }, + [chat] + ); + + /** + * Execute an intent (create study and optionally run) + */ + const executeIntent = useCallback( + async ( + intent: OptimizationIntent, + studyName: string, + autoRun: boolean = false + ): Promise => { + setState((prev) => ({ + ...prev, + isExecuting: true, + lastIntent: intent, + executionResult: null, + })); + + // Format intent for chat and ask Claude to execute + const message = `Please execute this canvas optimization intent to create study "${studyName}"${autoRun ? ' and start the optimization' : ''}: + +${formatIntentForChat(intent)} + +Use the execute_canvas_intent tool with: +- study_name: "${studyName}" +- auto_run: ${autoRun} + +After execution, provide a summary of what was created.`; + + await chat.sendMessage(message); + + setState((prev) => ({ + ...prev, + isExecuting: false, + })); + }, + [chat] + ); + + /** + * Get recommendations for an intent without executing + */ + const analyzeIntent = useCallback( + async (intent: OptimizationIntent): Promise => { + setState((prev) => ({ + ...prev, + isExecuting: true, + lastIntent: intent, + })); + + const message = `Please analyze this canvas optimization intent and provide recommendations: + +${formatIntentForChat(intent)} + +Use the interpret_canvas_intent tool to: +1. Analyze the problem characteristics +2. Suggest the best optimization method +3. Recommend trial budget +4. Identify any potential issues + +Provide your recommendations in a clear, actionable format.`; + + await chat.sendMessage(message); + + setState((prev) => ({ + ...prev, + isExecuting: false, + })); + }, + [chat] + ); + + /** + * Send a free-form message about the current canvas state + */ + const askAboutCanvas = useCallback( + async (intent: OptimizationIntent, question: string): Promise => { + const message = `Given this canvas optimization intent: + +${formatIntentForChat(intent)} + +${question}`; + + await chat.sendMessage(message); + }, + [chat] + ); + + return { + // Chat state + messages: chat.messages, + isThinking: chat.isThinking || state.isExecuting, + isConnected: chat.isConnected, + error: chat.error, + sessionId: chat.sessionId, + mode: chat.mode, + + // Canvas-specific state + isExecuting: state.isExecuting, + lastIntent: state.lastIntent, + executionResult: state.executionResult, + + // Actions + validateIntent, + executeIntent, + analyzeIntent, + askAboutCanvas, + + // Base chat actions + sendMessage: chat.sendMessage, + clearMessages: chat.clearMessages, + switchMode: chat.switchMode, + }; +} diff --git a/mcp-server/atomizer-tools/src/index.ts b/mcp-server/atomizer-tools/src/index.ts index 4359c023..ba7bb68a 100644 --- a/mcp-server/atomizer-tools/src/index.ts +++ b/mcp-server/atomizer-tools/src/index.ts @@ -21,6 +21,7 @@ import { optimizationTools } from "./tools/optimization.js"; import { analysisTools } from "./tools/analysis.js"; import { reportingTools } from "./tools/reporting.js"; import { physicsTools } from "./tools/physics.js"; +import { canvasTools } from "./tools/canvas.js"; import { adminTools } from "./tools/admin.js"; import { ATOMIZER_MODE } from "./utils/paths.js"; @@ -50,6 +51,7 @@ const userTools: AtomizerTool[] = [ ...analysisTools, ...reportingTools, ...physicsTools, + ...canvasTools, ]; const powerTools: AtomizerTool[] = [ diff --git a/mcp-server/atomizer-tools/src/tools/canvas.ts b/mcp-server/atomizer-tools/src/tools/canvas.ts new file mode 100644 index 00000000..5ac66ef6 --- /dev/null +++ b/mcp-server/atomizer-tools/src/tools/canvas.ts @@ -0,0 +1,578 @@ +/** + * Canvas Intent Processing Tools + * + * Tools for processing optimization workflow intents from the Canvas UI. + * The canvas serializes node graphs to Intent JSON, which Claude interprets + * using protocols and LAC to execute the optimization. + */ + +import { execSync } from "child_process"; +import { AtomizerTool } from "../index.js"; +import { PYTHON_PATH, STUDIES_DIR } from "../utils/paths.js"; + +// Intent type definitions matching frontend schema +interface CanvasIntent { + version: string; + source: "canvas"; + timestamp: string; + model: { + path?: string; + type?: string; + }; + solver: { + type?: string; + }; + design_variables: Array<{ + name: string; + min: number; + max: number; + unit?: string; + }>; + extractors: Array<{ + id: string; + name: string; + config?: Record; + }>; + objectives: Array<{ + name: string; + direction: "minimize" | "maximize"; + weight: number; + extractor: string; + }>; + constraints: Array<{ + name: string; + operator: string; + value: number; + extractor: string; + }>; + optimization: { + method?: string; + max_trials?: number; + }; + surrogate?: { + enabled: boolean; + type?: string; + min_trials?: number; + }; +} + +interface ValidationError { + field: string; + message: string; + severity: "error" | "warning"; +} + +/** + * Validate a canvas intent and return detailed feedback + */ +function validateIntent(intent: CanvasIntent): ValidationError[] { + const errors: ValidationError[] = []; + + // Model validation + if (!intent.model?.path) { + errors.push({ + field: "model.path", + message: "Model file path is required", + severity: "error", + }); + } + + // Solver validation + if (!intent.solver?.type) { + errors.push({ + field: "solver.type", + message: "Solver type is required (e.g., SOL101)", + severity: "error", + }); + } + + // Design variables validation + if (!intent.design_variables || intent.design_variables.length === 0) { + errors.push({ + field: "design_variables", + message: "At least one design variable is required", + severity: "error", + }); + } else { + intent.design_variables.forEach((dv, i) => { + if (!dv.name) { + errors.push({ + field: `design_variables[${i}].name`, + message: "Design variable name is required", + severity: "error", + }); + } + if (dv.min >= dv.max) { + errors.push({ + field: `design_variables[${i}]`, + message: `Invalid bounds: min (${dv.min}) must be less than max (${dv.max})`, + severity: "error", + }); + } + }); + } + + // Objectives validation + if (!intent.objectives || intent.objectives.length === 0) { + errors.push({ + field: "objectives", + message: "At least one objective is required", + severity: "error", + }); + } else { + intent.objectives.forEach((obj, i) => { + if (!obj.name) { + errors.push({ + field: `objectives[${i}].name`, + message: "Objective name is required", + severity: "error", + }); + } + if (!obj.extractor) { + errors.push({ + field: `objectives[${i}].extractor`, + message: "Objective must be connected to an extractor", + severity: "error", + }); + } + }); + } + + // Extractors validation + if (!intent.extractors || intent.extractors.length === 0) { + errors.push({ + field: "extractors", + message: "At least one physics extractor is required", + severity: "error", + }); + } + + // Optimization settings + if (!intent.optimization?.method) { + errors.push({ + field: "optimization.method", + message: "Optimization method not specified, will default to TPE", + severity: "warning", + }); + } + + if (!intent.optimization?.max_trials) { + errors.push({ + field: "optimization.max_trials", + message: "Max trials not specified, will default to 100", + severity: "warning", + }); + } + + // Multi-objective check + if (intent.objectives && intent.objectives.length > 1) { + if (intent.optimization?.method && intent.optimization.method !== "NSGA-II") { + errors.push({ + field: "optimization.method", + message: `Multiple objectives detected. Consider using NSGA-II instead of ${intent.optimization.method}`, + severity: "warning", + }); + } + } + + return errors; +} + +/** + * Convert canvas intent to optimization_config.json format + */ +function intentToConfig(intent: CanvasIntent, studyName: string): Record { + // Map extractor IDs to physics names + const extractorPhysicsMap: Record = { + E1: "displacement", + E2: "frequency", + E3: "stress", + E4: "mass_bdf", + E5: "mass_cad", + E8: "zernike_op2", + E9: "zernike_csv", + E10: "zernike_rms", + }; + + return { + study_name: studyName, + model: { + path: intent.model.path, + type: intent.model.type || "sim", + }, + solver: { + type: "nastran", + solution: parseInt(intent.solver.type?.replace("SOL", "") || "101"), + }, + design_variables: intent.design_variables.map((dv) => ({ + name: dv.name, + expression_name: dv.name, + lower: dv.min, + upper: dv.max, + type: "continuous", + })), + objectives: intent.objectives.map((obj) => ({ + name: obj.name, + direction: obj.direction, + weight: obj.weight || 1.0, + extractor: obj.extractor, + physics: extractorPhysicsMap[obj.extractor] || "custom", + })), + constraints: intent.constraints.map((c) => ({ + name: c.name, + type: c.operator === "<=" || c.operator === "<" ? "upper" : "lower", + value: c.value, + extractor: c.extractor, + })), + method: intent.optimization.method || "TPE", + max_trials: intent.optimization.max_trials || 100, + surrogate: intent.surrogate?.enabled + ? { + type: intent.surrogate.type || "MLP", + min_trials: intent.surrogate.min_trials || 20, + } + : null, + }; +} + +export const canvasTools: AtomizerTool[] = [ + { + definition: { + name: "validate_canvas_intent", + description: + "Validate a canvas-generated optimization intent. Returns validation errors and warnings without creating a study.", + inputSchema: { + type: "object" as const, + properties: { + intent: { + type: "object", + description: "The optimization intent JSON from the canvas", + }, + }, + required: ["intent"], + }, + }, + handler: async (args) => { + const intent = args.intent as CanvasIntent; + + if (!intent) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + valid: false, + errors: [{ field: "intent", message: "Intent is required", severity: "error" }], + }), + }, + ], + isError: true, + }; + } + + const errors = validateIntent(intent); + const hasErrors = errors.some((e) => e.severity === "error"); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + valid: !hasErrors, + errors: errors.filter((e) => e.severity === "error"), + warnings: errors.filter((e) => e.severity === "warning"), + summary: hasErrors + ? `Found ${errors.filter((e) => e.severity === "error").length} error(s) that must be fixed` + : `Intent is valid with ${errors.filter((e) => e.severity === "warning").length} warning(s)`, + }, + null, + 2 + ), + }, + ], + }; + }, + }, + + { + definition: { + name: "execute_canvas_intent", + description: + "Execute a canvas-generated optimization intent. Creates a study from the intent and optionally starts the optimization.", + inputSchema: { + type: "object" as const, + properties: { + intent: { + type: "object", + description: "The optimization intent JSON from the canvas", + }, + study_name: { + type: "string", + description: "Name for the study (snake_case)", + }, + auto_run: { + type: "boolean", + description: "Whether to automatically start the optimization after creating the study", + }, + }, + required: ["intent", "study_name"], + }, + }, + handler: async (args) => { + const intent = args.intent as CanvasIntent; + const studyName = args.study_name as string; + const autoRun = args.auto_run as boolean || false; + + // First validate + const errors = validateIntent(intent); + const hasErrors = errors.some((e) => e.severity === "error"); + + if (hasErrors) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + action: "validation_failed", + errors: errors.filter((e) => e.severity === "error"), + message: "Cannot execute intent - validation errors must be fixed first", + }, null, 2), + }, + ], + isError: true, + }; + } + + // Convert intent to config + const config = intentToConfig(intent, studyName); + const configJson = JSON.stringify(config).replace(/"/g, '\\"'); + + // Python script to create study from config + const script = ` +import sys +import json +sys.path.insert(0, r"C:/Users/antoi/Atomizer") +from pathlib import Path +from optimization_engine.study.creator import StudyCreator + +config = json.loads("""${configJson}""") +study_name = "${studyName}" + +try: + creator = StudyCreator() + result = creator.create_from_config(study_name, config) + print(json.dumps({"success": True, "study_name": study_name, "path": str(result)})) +except Exception as e: + print(json.dumps({"success": False, "error": str(e)})) + sys.exit(1) +`; + + try { + const output = execSync(`"${PYTHON_PATH}" -c "${script}"`, { + encoding: "utf-8", + cwd: STUDIES_DIR, + timeout: 60000, + }); + + const result = JSON.parse(output.trim()); + + if (!result.success) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + action: "creation_failed", + error: result.error, + }, null, 2), + }, + ], + isError: true, + }; + } + + // If auto_run, start the optimization + if (autoRun) { + const runScript = ` +import sys +sys.path.insert(0, r"C:/Users/antoi/Atomizer") +from optimization_engine.core.runner import OptimizationRunner + +try: + runner = OptimizationRunner("${studyName}") + runner.start_async() + print("STARTED") +except Exception as e: + print(f"RUN_ERROR: {e}") +`; + try { + const runOutput = execSync(`"${PYTHON_PATH}" -c "${runScript}"`, { + encoding: "utf-8", + cwd: STUDIES_DIR, + timeout: 30000, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + action: "created_and_started", + study_name: studyName, + path: result.path, + message: `Study "${studyName}" created and optimization started!`, + config_summary: { + design_variables: intent.design_variables.length, + objectives: intent.objectives.length, + constraints: intent.constraints.length, + method: intent.optimization.method || "TPE", + max_trials: intent.optimization.max_trials || 100, + }, + }, null, 2), + }, + ], + }; + } catch (runError) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + action: "created_but_run_failed", + study_name: studyName, + path: result.path, + run_error: runError instanceof Error ? runError.message : String(runError), + message: `Study created but failed to start optimization. You can start it manually.`, + }, null, 2), + }, + ], + }; + } + } + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: true, + action: "created", + study_name: studyName, + path: result.path, + message: `Study "${studyName}" created successfully! Use run_optimization to start.`, + config_summary: { + design_variables: intent.design_variables.length, + objectives: intent.objectives.length, + constraints: intent.constraints.length, + method: intent.optimization.method || "TPE", + max_trials: intent.optimization.max_trials || 100, + }, + }, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + action: "error", + error: message, + }, null, 2), + }, + ], + isError: true, + }; + } + }, + }, + + { + definition: { + name: "interpret_canvas_intent", + description: + "Interpret a canvas intent and provide recommendations. Does not create anything - just analyzes and suggests improvements.", + inputSchema: { + type: "object" as const, + properties: { + intent: { + type: "object", + description: "The optimization intent JSON from the canvas", + }, + }, + required: ["intent"], + }, + }, + handler: async (args) => { + const intent = args.intent as CanvasIntent; + + const analysis: Record = { + source: intent.source, + timestamp: intent.timestamp, + }; + + // Analyze problem characteristics + const numObjectives = intent.objectives?.length || 0; + const numDesignVars = intent.design_variables?.length || 0; + const numConstraints = intent.constraints?.length || 0; + + analysis.problem_type = numObjectives > 1 ? "multi-objective" : "single-objective"; + analysis.complexity = numDesignVars > 5 ? "high" : numDesignVars > 2 ? "medium" : "low"; + + // Method recommendation based on problem characteristics + const recommendations: string[] = []; + + if (numObjectives > 1 && intent.optimization?.method !== "NSGA-II") { + recommendations.push( + `Consider using NSGA-II for multi-objective optimization (${numObjectives} objectives detected)` + ); + } + + if (numDesignVars > 10 && intent.optimization?.method === "CMA-ES") { + recommendations.push( + "CMA-ES may struggle with high-dimensional problems. Consider TPE or GP-BO." + ); + } + + if ((intent.optimization?.max_trials || 100) < 50 && numDesignVars > 5) { + recommendations.push( + `Trial budget (${intent.optimization?.max_trials || 100}) may be insufficient for ${numDesignVars} design variables. Consider 100+ trials.` + ); + } + + if (!intent.surrogate?.enabled && (intent.optimization?.max_trials || 100) > 100) { + recommendations.push( + "Consider enabling neural surrogate for faster optimization with high trial counts." + ); + } + + analysis.recommendations = recommendations; + analysis.suggested_method = + numObjectives > 1 + ? "NSGA-II" + : numDesignVars > 10 + ? "TPE" + : "TPE"; // Default to TPE for most cases + + analysis.suggested_trials = + numDesignVars <= 3 ? 50 : numDesignVars <= 6 ? 100 : numDesignVars <= 10 ? 200 : 500; + + return { + content: [ + { + type: "text", + text: JSON.stringify(analysis, null, 2), + }, + ], + }; + }, + }, +];