Files
Atomizer/mcp-server/atomizer-tools/src/tools/reporting.ts
Anto01 73a7b9d9f1 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>
2026-01-13 15:53:55 -05:00

300 lines
8.9 KiB
TypeScript

/**
* 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;
}