feat: Add dashboard chat integration and MCP server
Major changes: - Dashboard: WebSocket-based chat with session management - Dashboard: New chat components (ChatPane, ChatInput, ModeToggle) - Dashboard: Enhanced UI with parallel coordinates chart - MCP Server: New atomizer-tools server for Claude integration - Extractors: Enhanced Zernike OPD extractor - Reports: Improved report generator New studies (configs and scripts only): - M1 Mirror: Cost reduction campaign studies - Simple Beam, Simple Bracket, UAV Arm studies Note: Large iteration data (2_iterations/, best_design_archive/) excluded via .gitignore - kept on local Gitea only. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
299
mcp-server/atomizer-tools/src/tools/reporting.ts
Normal file
299
mcp-server/atomizer-tools/src/tools/reporting.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Reporting Tools
|
||||
*
|
||||
* Tools for generating reports and exporting data.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { resolve } from "path";
|
||||
import Database from "better-sqlite3";
|
||||
import { AtomizerTool } from "../index.js";
|
||||
import { PYTHON_PATH, ATOMIZER_ROOT, getStudyDir, getStudyDbPath } from "../utils/paths.js";
|
||||
|
||||
export const reportingTools: AtomizerTool[] = [
|
||||
{
|
||||
definition: {
|
||||
name: "generate_report",
|
||||
description:
|
||||
"Generate a markdown report for an optimization study, including configuration summary, results analysis, and recommendations.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
study_name: {
|
||||
type: "string",
|
||||
description: "Name of the study to report on",
|
||||
},
|
||||
report_type: {
|
||||
type: "string",
|
||||
enum: ["summary", "detailed", "executive"],
|
||||
description:
|
||||
"Type of report: summary (quick overview), detailed (full analysis), executive (high-level for stakeholders)",
|
||||
},
|
||||
include_plots: {
|
||||
type: "boolean",
|
||||
description: "Whether to generate convergence plots (requires matplotlib)",
|
||||
},
|
||||
},
|
||||
required: ["study_name"],
|
||||
},
|
||||
},
|
||||
handler: async (args) => {
|
||||
const studyName = args.study_name as string;
|
||||
const reportType = (args.report_type as string) || "summary";
|
||||
const includePlots = args.include_plots as boolean;
|
||||
const studyDir = getStudyDir(studyName);
|
||||
|
||||
// Use Python report generator
|
||||
const script = `
|
||||
import sys
|
||||
sys.path.insert(0, r"${ATOMIZER_ROOT}")
|
||||
from optimization_engine.reporting.markdown_report import generate_study_report
|
||||
|
||||
try:
|
||||
report = generate_study_report(
|
||||
study_name="${studyName}",
|
||||
report_type="${reportType}",
|
||||
include_plots=${includePlots ? "True" : "False"}
|
||||
)
|
||||
print(report)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
`;
|
||||
|
||||
try {
|
||||
const output = execSync(`"${PYTHON_PATH}" -c "${script}"`, {
|
||||
encoding: "utf-8",
|
||||
cwd: studyDir,
|
||||
timeout: 120000, // 2 minute timeout for plots
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large reports
|
||||
});
|
||||
|
||||
// Save report to file
|
||||
const reportPath = resolve(studyDir, "3_results", `STUDY_REPORT_${reportType}.md`);
|
||||
try {
|
||||
await writeFile(reportPath, output);
|
||||
} catch {
|
||||
// Ignore save errors - still return the report
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: output,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
// If Python report generator fails, generate a basic report from DB
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Fall back to basic report
|
||||
try {
|
||||
const basicReport = await generateBasicReport(studyName, reportType);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: basicReport,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (fallbackError) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error generating report: ${message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
definition: {
|
||||
name: "export_data",
|
||||
description:
|
||||
"Export optimization results to CSV or JSON format for external analysis.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
study_name: {
|
||||
type: "string",
|
||||
description: "Name of the study to export",
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
enum: ["csv", "json"],
|
||||
description: "Export format",
|
||||
},
|
||||
include_failed: {
|
||||
type: "boolean",
|
||||
description: "Whether to include failed trials (default: false)",
|
||||
},
|
||||
},
|
||||
required: ["study_name", "format"],
|
||||
},
|
||||
},
|
||||
handler: async (args) => {
|
||||
const studyName = args.study_name as string;
|
||||
const format = args.format as string;
|
||||
const includeFailed = args.include_failed as boolean;
|
||||
const studyDir = getStudyDir(studyName);
|
||||
const dbPath = getStudyDbPath(studyName);
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
// Get all trial data
|
||||
const stateFilter = includeFailed
|
||||
? ""
|
||||
: "WHERE t.state = 'COMPLETE'";
|
||||
|
||||
const trials = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT t.number, t.state, t.datetime_start, t.datetime_complete
|
||||
FROM trials t
|
||||
${stateFilter}
|
||||
ORDER BY t.number ASC
|
||||
`
|
||||
)
|
||||
.all() as {
|
||||
number: number;
|
||||
state: string;
|
||||
datetime_start: string;
|
||||
datetime_complete: string | null;
|
||||
}[];
|
||||
|
||||
// Get params and values for each trial
|
||||
const data = trials.map((trial) => {
|
||||
const trialId = db
|
||||
.prepare("SELECT trial_id FROM trials WHERE number = ?")
|
||||
.get(trial.number) as { trial_id: number };
|
||||
|
||||
const params = db
|
||||
.prepare("SELECT param_name, param_value FROM trial_params WHERE trial_id = ?")
|
||||
.all(trialId.trial_id) as { param_name: string; param_value: string }[];
|
||||
|
||||
const values = db
|
||||
.prepare("SELECT objective_id, value FROM trial_values WHERE trial_id = ?")
|
||||
.all(trialId.trial_id) as { objective_id: number; value: number }[];
|
||||
|
||||
return {
|
||||
trial_number: trial.number,
|
||||
state: trial.state,
|
||||
datetime_start: trial.datetime_start,
|
||||
datetime_complete: trial.datetime_complete,
|
||||
...Object.fromEntries(
|
||||
params.map((p) => [p.param_name, parseFloat(p.param_value) || p.param_value])
|
||||
),
|
||||
...Object.fromEntries(values.map((v) => [`objective_${v.objective_id}`, v.value])),
|
||||
};
|
||||
});
|
||||
|
||||
db.close();
|
||||
|
||||
let output: string;
|
||||
let filename: string;
|
||||
|
||||
if (format === "csv") {
|
||||
// Generate CSV
|
||||
if (data.length === 0) {
|
||||
output = "No data to export";
|
||||
} else {
|
||||
const headers = Object.keys(data[0]);
|
||||
const rows = data.map((row) => headers.map((h) => row[h as keyof typeof row] ?? "").join(","));
|
||||
output = [headers.join(","), ...rows].join("\n");
|
||||
}
|
||||
filename = `${studyName}_export.csv`;
|
||||
} else {
|
||||
// Generate JSON
|
||||
output = JSON.stringify(data, null, 2);
|
||||
filename = `${studyName}_export.json`;
|
||||
}
|
||||
|
||||
// Save to file
|
||||
const exportPath = resolve(studyDir, "3_results", filename);
|
||||
await writeFile(exportPath, output);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Exported ${data.length} trials to ${exportPath}\n\nPreview (first 5 rows):\n${format === "csv" ? output.split("\n").slice(0, 6).join("\n") : JSON.stringify(data.slice(0, 5), null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error exporting data: ${message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function for basic report generation
|
||||
async function generateBasicReport(
|
||||
studyName: string,
|
||||
reportType: string
|
||||
): Promise<string> {
|
||||
const dbPath = getStudyDbPath(studyName);
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
// Get trial counts
|
||||
const total = (db.prepare("SELECT COUNT(*) as c FROM trials").get() as { c: number }).c;
|
||||
const completed = (
|
||||
db.prepare("SELECT COUNT(*) as c FROM trials WHERE state = 'COMPLETE'").get() as { c: number }
|
||||
).c;
|
||||
const failed = (
|
||||
db.prepare("SELECT COUNT(*) as c FROM trials WHERE state = 'FAIL'").get() as { c: number }
|
||||
).c;
|
||||
|
||||
// Get best result
|
||||
const best = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT t.number, tv.value
|
||||
FROM trials t
|
||||
JOIN trial_values tv ON t.trial_id = tv.trial_id
|
||||
WHERE t.state = 'COMPLETE'
|
||||
ORDER BY tv.value ASC
|
||||
LIMIT 1
|
||||
`
|
||||
)
|
||||
.get() as { number: number; value: number } | undefined;
|
||||
|
||||
db.close();
|
||||
|
||||
let report = `# Optimization Report: ${studyName}\n\n`;
|
||||
report += `**Generated:** ${new Date().toISOString()}\n\n`;
|
||||
report += `## Summary\n\n`;
|
||||
report += `- Total trials: ${total}\n`;
|
||||
report += `- Completed: ${completed}\n`;
|
||||
report += `- Failed: ${failed}\n`;
|
||||
|
||||
if (best) {
|
||||
report += `\n## Best Result\n\n`;
|
||||
report += `- Trial #${best.number}\n`;
|
||||
report += `- Objective value: ${best.value.toFixed(6)}\n`;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
Reference in New Issue
Block a user