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 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 20:18:46 -05:00
parent 7919511bb2
commit 1ae35382da
8 changed files with 1051 additions and 11 deletions

View File

@@ -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[] = [

View File

@@ -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<string, unknown>;
}>;
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<string, unknown> {
// Map extractor IDs to physics names
const extractorPhysicsMap: Record<string, string> = {
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<string, unknown> = {
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),
},
],
};
},
},
];