217 lines
6.5 KiB
TypeScript
217 lines
6.5 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { artifactUrl, createStepGlbPreview, readArtifact } from "@/lib/preview-artifacts";
|
|
import { enginePath } from "@/lib/paths";
|
|
import { ensureTask, sanitizeFilename, upsertManifest } from "@/lib/task-store";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
export type CadGenerationResult = {
|
|
handled: true;
|
|
taskId: string;
|
|
artifactPath: string;
|
|
artifactUrl: string;
|
|
previewPath: string;
|
|
previewUrl: string;
|
|
viewerAssetPath: string;
|
|
viewerAssetUrl: string;
|
|
sourcePath: string;
|
|
summary: string;
|
|
};
|
|
|
|
function ensureRelativeFilename(filename: string, extension: string, fallback: string) {
|
|
const safeName = sanitizeFilename(filename || fallback);
|
|
return safeName.toLowerCase().endsWith(extension) ? safeName : `${safeName}${extension}`;
|
|
}
|
|
|
|
function pythonExecutable() {
|
|
return process.env.CAD_PYTHON
|
|
|| enginePath("text-to-cad", ".venv", "bin", "python");
|
|
}
|
|
|
|
function pipelineScript() {
|
|
return enginePath("designir-pipeline", "scripts", "designir_pipeline.py");
|
|
}
|
|
|
|
function routerScript() {
|
|
return enginePath("text-to-cad", "skills", "cad-router", "scripts", "route.py");
|
|
}
|
|
|
|
async function runJson(command: string, args: string[], cwd: string) {
|
|
const { stdout } = await execFileAsync(command, args, {
|
|
cwd,
|
|
timeout: 120_000,
|
|
maxBuffer: 1024 * 1024 * 8,
|
|
});
|
|
return JSON.parse(stdout) as Record<string, unknown>;
|
|
}
|
|
|
|
export async function executeDesignIR({
|
|
prompt,
|
|
summary,
|
|
designir,
|
|
designirFilename,
|
|
stepFilename,
|
|
selectedTaskId,
|
|
assumptions = [],
|
|
teacherTaskId,
|
|
teacherPath,
|
|
}: {
|
|
prompt: string;
|
|
summary: string;
|
|
designir: Record<string, unknown>;
|
|
designirFilename?: string;
|
|
stepFilename?: string;
|
|
selectedTaskId?: string;
|
|
assumptions?: string[];
|
|
teacherTaskId?: string;
|
|
teacherPath?: string;
|
|
}): Promise<CadGenerationResult> {
|
|
const task = await ensureTask(selectedTaskId || undefined);
|
|
const sourcePath = ensureRelativeFilename(
|
|
designirFilename || "model.designir.json",
|
|
".designir.json",
|
|
"model.designir.json",
|
|
);
|
|
const artifactPath = ensureRelativeFilename(stepFilename || "model.step", ".step", "model.step");
|
|
const sourceAbsolutePath = path.join(task.dir, sourcePath);
|
|
const artifactAbsolutePath = path.join(task.dir, artifactPath);
|
|
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(designir, null, 2)}\n`);
|
|
|
|
const route = await runJson(
|
|
pythonExecutable(),
|
|
[routerScript(), prompt],
|
|
task.dir,
|
|
);
|
|
const selectedBackend = String(route.selected_backend || "build123d");
|
|
const normalizedDesignIR = {
|
|
...designir,
|
|
backend_hint: selectedBackend,
|
|
};
|
|
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(normalizedDesignIR, null, 2)}\n`);
|
|
|
|
const antiCheat = await runJson(
|
|
pythonExecutable(),
|
|
[pipelineScript(), "anti-cheat", sourceAbsolutePath],
|
|
task.dir,
|
|
);
|
|
if (antiCheat.valid !== true) {
|
|
throw new Error(`DesignIR anti-cheat validation failed: ${JSON.stringify(antiCheat.violations || [])}`);
|
|
}
|
|
const rebuild = await runJson(
|
|
pythonExecutable(),
|
|
[
|
|
pipelineScript(),
|
|
"isolated-rebuild",
|
|
sourceAbsolutePath,
|
|
"--output",
|
|
artifactAbsolutePath,
|
|
"--backend",
|
|
selectedBackend,
|
|
],
|
|
task.dir,
|
|
);
|
|
if (rebuild.valid !== true) {
|
|
throw new Error("Isolated DesignIR rebuild failed.");
|
|
}
|
|
|
|
const validation: string[] = [
|
|
"CAD Router selected the backend before compilation.",
|
|
"DesignIR anti-cheat validation passed.",
|
|
"STEP rebuilt in an isolated workspace without teacher STEP access.",
|
|
];
|
|
const artifacts: Array<{ path: string; role: string; kind?: string }> = [
|
|
{ path: sourcePath, role: "editable_contract", kind: "designir-2.0" },
|
|
{ path: artifactPath, role: "primary", kind: "step" },
|
|
];
|
|
if (typeof rebuild.model_json === "string" && rebuild.model_json) {
|
|
const simpleCadModelPath = path.basename(rebuild.model_json);
|
|
artifacts.push({
|
|
path: simpleCadModelPath,
|
|
role: "backend_editable_graph",
|
|
kind: "simplecad-model-json",
|
|
});
|
|
}
|
|
|
|
if (teacherTaskId && teacherPath) {
|
|
const { absolutePath: teacherAbsolutePath } = await readArtifact(
|
|
teacherTaskId,
|
|
teacherPath,
|
|
);
|
|
const reportPath = "acceptance-report.json";
|
|
const reportAbsolutePath = path.join(task.dir, reportPath);
|
|
const acceptance = await runJson(
|
|
pythonExecutable(),
|
|
[
|
|
pipelineScript(),
|
|
"accept",
|
|
"--teacher",
|
|
teacherAbsolutePath,
|
|
"--designir",
|
|
sourceAbsolutePath,
|
|
"--rebuilt",
|
|
artifactAbsolutePath,
|
|
"--report",
|
|
reportAbsolutePath,
|
|
],
|
|
task.dir,
|
|
);
|
|
artifacts.push({ path: reportPath, role: "acceptance", kind: "json" });
|
|
validation.push(`Independent acceptance verdict: ${String(acceptance.verdict || "unknown")}.`);
|
|
}
|
|
|
|
const viewerAssetPath = await createStepGlbPreview({
|
|
taskId: task.taskId,
|
|
stepPath: artifactPath,
|
|
});
|
|
artifacts.push({ path: viewerAssetPath, role: "viewer", kind: "glb" });
|
|
const artifactVersion = Date.now().toString(36);
|
|
|
|
await upsertManifest(task.taskId, {
|
|
request: prompt,
|
|
route: {
|
|
selected_backend: selectedBackend,
|
|
runner_skill: String(route.runner_skill || "cad"),
|
|
project: String(route.project || "text-to-cad"),
|
|
fallback_order: Array.isArray(route.fallback_order) ? route.fallback_order : [],
|
|
workflow_profiles: Array.isArray(route.workflow_profiles) ? route.workflow_profiles : [],
|
|
},
|
|
source: {
|
|
path: sourcePath,
|
|
format: "designir-2.0",
|
|
backend: selectedBackend,
|
|
source_editability: "editable_source",
|
|
generated_by: "llm_designir_tool_call",
|
|
},
|
|
artifacts,
|
|
assumptions,
|
|
validation,
|
|
studio: {
|
|
latestArtifact: artifactPath,
|
|
latestPreview: artifactPath,
|
|
latestViewerAsset: viewerAssetPath,
|
|
latestVersion: artifactVersion,
|
|
},
|
|
});
|
|
|
|
const stepUrl = artifactUrl(task.taskId, artifactPath, artifactVersion);
|
|
return {
|
|
handled: true,
|
|
taskId: task.taskId,
|
|
artifactPath,
|
|
artifactUrl: stepUrl,
|
|
previewPath: artifactPath,
|
|
previewUrl: stepUrl,
|
|
viewerAssetPath,
|
|
viewerAssetUrl: artifactUrl(task.taskId, viewerAssetPath, artifactVersion),
|
|
sourcePath,
|
|
summary: summary || "CAD model generated from executable DesignIR 2.0.",
|
|
};
|
|
}
|
|
|
|
export function cadResultDirective(result: CadGenerationResult) {
|
|
return `::cad-result{base64="${Buffer.from(JSON.stringify(result), "utf8").toString("base64")}"}`;
|
|
}
|