feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -34,10 +34,20 @@ Open `http://127.0.0.1:53821`.
|
||||
|
||||
Uploads and generated tasks are stored under `data/tasks/<taskId>`.
|
||||
|
||||
The model authors DesignIR 2.0, CAD Router selects text-to-cad or SimpleCADAPI,
|
||||
and the DesignIR pipeline performs a teacher-free isolated STEP rebuild.
|
||||
Uploaded STEP is teacher and acceptance truth; it is never imported by the
|
||||
reconstruction compiler.
|
||||
For new text/image models, the model authors semantic DesignIR 3.0 and CAD
|
||||
Router selects text-to-cad or SimpleCADAPI. The generated STEP is materialized
|
||||
as a non-authoritative SurfaceIR snapshot while the semantic layer remains the
|
||||
edit source. For uploaded STEP, chat calls the deterministic
|
||||
`reconstruct_uploaded_step` tool, which executes CAD Router's DesignIR 3.0 /
|
||||
SurfaceIR adapter, independently rebuilds STEP, runs geometry acceptance, and
|
||||
publishes only perturbation-validated editable parameters. Uploaded STEP is
|
||||
teacher and acceptance truth; it is never imported by the reconstruction
|
||||
compiler.
|
||||
|
||||
Follow-up chat edits on a reconstructed task use
|
||||
`edit_designir3_parameter`. The tool accepts only a parameter and value already
|
||||
covered by the published validation range, reruns teacher-side semantic edit
|
||||
acceptance, and publishes a new DesignIR 3.0 plus STEP revision.
|
||||
|
||||
CAD Agent Studio does not start the standalone CAD Viewer service. Generated
|
||||
models keep DesignIR as the editable contract and STEP as the geometry artifact.
|
||||
|
||||
@@ -4,7 +4,13 @@ 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";
|
||||
import {
|
||||
ensureTask,
|
||||
readManifest,
|
||||
sanitizeFilename,
|
||||
taskDir,
|
||||
upsertManifest,
|
||||
} from "@/lib/task-store";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -21,6 +27,13 @@ export type CadGenerationResult = {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
type UploadedStepReconstructionInput = {
|
||||
prompt: string;
|
||||
teacherTaskId: string;
|
||||
teacherPath: string;
|
||||
teacherName?: string;
|
||||
};
|
||||
|
||||
function ensureRelativeFilename(filename: string, extension: string, fallback: string) {
|
||||
const safeName = sanitizeFilename(filename || fallback);
|
||||
return safeName.toLowerCase().endsWith(extension) ? safeName : `${safeName}${extension}`;
|
||||
@@ -39,13 +52,33 @@ function routerScript() {
|
||||
return enginePath("text-to-cad", "skills", "cad-router", "scripts", "route.py");
|
||||
}
|
||||
|
||||
async function runJson(command: string, args: string[], cwd: string) {
|
||||
function surfaceirScript() {
|
||||
return enginePath("designir-pipeline", "scripts", "surfaceir_pipeline.py");
|
||||
}
|
||||
|
||||
async function runJson(
|
||||
command: string,
|
||||
args: string[],
|
||||
cwd: string,
|
||||
timeout = 120_000,
|
||||
) {
|
||||
const { stdout } = await execFileAsync(command, args, {
|
||||
cwd,
|
||||
timeout: 120_000,
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024 * 8,
|
||||
});
|
||||
return JSON.parse(stdout) as Record<string, unknown>;
|
||||
const lines = stdout.split(/\r?\n/);
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
if (!lines[index]?.trim().startsWith("{")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(lines.slice(index).join("\n")) as Record<string, unknown>;
|
||||
} catch {
|
||||
// An OCCT log line may contain a brace; continue to the enclosing object.
|
||||
}
|
||||
}
|
||||
throw new Error("CAD worker did not return a valid JSON result.");
|
||||
}
|
||||
|
||||
export async function executeDesignIR({
|
||||
@@ -91,6 +124,20 @@ export async function executeDesignIR({
|
||||
backend_hint: selectedBackend,
|
||||
};
|
||||
await fs.writeFile(sourceAbsolutePath, `${JSON.stringify(normalizedDesignIR, null, 2)}\n`);
|
||||
const migration = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
pipelineScript(),
|
||||
"migrate",
|
||||
sourceAbsolutePath,
|
||||
"--output",
|
||||
sourceAbsolutePath,
|
||||
],
|
||||
task.dir,
|
||||
);
|
||||
if (migration.valid !== true || migration.schema_version !== "3.0") {
|
||||
throw new Error("DesignIR 3.0 normalization failed.");
|
||||
}
|
||||
|
||||
const antiCheat = await runJson(
|
||||
pythonExecutable(),
|
||||
@@ -112,18 +159,37 @@ export async function executeDesignIR({
|
||||
selectedBackend,
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
if (rebuild.valid !== true) {
|
||||
throw new Error("Isolated DesignIR rebuild failed.");
|
||||
}
|
||||
const materialized = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
pipelineScript(),
|
||||
"materialize-hybrid",
|
||||
sourceAbsolutePath,
|
||||
"--step",
|
||||
artifactAbsolutePath,
|
||||
"--output",
|
||||
sourceAbsolutePath,
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
if (materialized.valid !== true) {
|
||||
throw new Error("DesignIR 3.0 SurfaceIR materialization failed.");
|
||||
}
|
||||
|
||||
const validation: string[] = [
|
||||
"CAD Router selected the backend before compilation.",
|
||||
"DesignIR anti-cheat validation passed.",
|
||||
"DesignIR 3.0 semantic-layer anti-cheat validation passed.",
|
||||
"STEP rebuilt in an isolated workspace without teacher STEP access.",
|
||||
"Generated STEP was materialized as a non-authoritative SurfaceIR snapshot.",
|
||||
];
|
||||
const artifacts: Array<{ path: string; role: string; kind?: string }> = [
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-2.0" },
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: artifactPath, role: "primary", kind: "step" },
|
||||
];
|
||||
if (typeof rebuild.model_json === "string" && rebuild.model_json) {
|
||||
@@ -180,10 +246,10 @@ export async function executeDesignIR({
|
||||
},
|
||||
source: {
|
||||
path: sourcePath,
|
||||
format: "designir-2.0",
|
||||
format: "designir-3.0",
|
||||
backend: selectedBackend,
|
||||
source_editability: "editable_source",
|
||||
generated_by: "llm_designir_tool_call",
|
||||
generated_by: "llm_semantic_designir3_tool_call",
|
||||
},
|
||||
artifacts,
|
||||
assumptions,
|
||||
@@ -207,7 +273,351 @@ export async function executeDesignIR({
|
||||
viewerAssetPath,
|
||||
viewerAssetUrl: artifactUrl(task.taskId, viewerAssetPath, artifactVersion),
|
||||
sourcePath,
|
||||
summary: summary || "CAD model generated from executable DesignIR 2.0.",
|
||||
summary: summary || "CAD model generated from semantic DesignIR 3.0.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function reconstructUploadedStep({
|
||||
prompt,
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
teacherName,
|
||||
}: UploadedStepReconstructionInput): Promise<CadGenerationResult> {
|
||||
const { absolutePath: teacherAbsolutePath } = await readArtifact(
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
);
|
||||
const task = await ensureTask();
|
||||
const route = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
routerScript(),
|
||||
prompt || "重建上传的 STEP,并返回可修改参数",
|
||||
"--edit-source",
|
||||
teacherAbsolutePath,
|
||||
"--task-dir",
|
||||
task.dir,
|
||||
"--execute",
|
||||
"--maximum-parameters",
|
||||
"8",
|
||||
"--timeout-seconds",
|
||||
"180",
|
||||
],
|
||||
task.dir,
|
||||
900_000,
|
||||
);
|
||||
const execution = (
|
||||
route.execution && typeof route.execution === "object"
|
||||
? route.execution
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (
|
||||
execution.status !== "accepted"
|
||||
&& execution.status !== "accepted_empty_source"
|
||||
) {
|
||||
throw new Error(
|
||||
`DesignIR 3.0 reconstruction was not accepted: ${JSON.stringify(execution)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relativeArtifact = (value: unknown) => {
|
||||
const absolute = path.resolve(String(value || ""));
|
||||
const relative = path.relative(task.dir, absolute);
|
||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
throw new Error(`CAD Router returned an artifact outside the task: ${absolute}`);
|
||||
}
|
||||
return relative;
|
||||
};
|
||||
const sourcePath = relativeArtifact(execution.designir);
|
||||
const artifactPath = relativeArtifact(execution.rebuilt_step);
|
||||
const parametersPath = relativeArtifact(execution.parameters);
|
||||
const geometryReportPath = relativeArtifact(execution.geometry_report);
|
||||
const editReportPath = relativeArtifact(execution.edit_report);
|
||||
const parameterCatalog = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, parametersPath), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
const existingManifest = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, "cad-task.json"), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const viewerAssetPath = await createStepGlbPreview({
|
||||
taskId: task.taskId,
|
||||
stepPath: artifactPath,
|
||||
});
|
||||
const artifactVersion = Date.now().toString(36);
|
||||
const experience = (
|
||||
execution.experience && typeof execution.experience === "object"
|
||||
? execution.experience
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
await upsertManifest(task.taskId, {
|
||||
request: prompt,
|
||||
route: {
|
||||
selected_backend: String(route.selected_backend || "build123d"),
|
||||
runner_skill: "cad-router",
|
||||
project: "text-to-cad",
|
||||
execution_adapter: "DesignIR 3.0 / SurfaceIR",
|
||||
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-3.0",
|
||||
backend: "surfaceir_occt",
|
||||
source_editability: "validated_parameter_source",
|
||||
generated_by: "cad-router_uploaded_step_execute",
|
||||
teacher_embedded: false,
|
||||
},
|
||||
artifacts: [
|
||||
{ path: sourcePath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: artifactPath, role: "primary", kind: "step" },
|
||||
{ path: parametersPath, role: "validated_parameters", kind: "json" },
|
||||
{ path: geometryReportPath, role: "geometry_acceptance", kind: "json" },
|
||||
{ path: editReportPath, role: "parameter_acceptance", kind: "json" },
|
||||
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
|
||||
],
|
||||
parameters: parameterCatalog,
|
||||
experience: {
|
||||
context_kind: "promoted_surfaceir_runtime",
|
||||
...experience,
|
||||
},
|
||||
assumptions: [],
|
||||
validation: [
|
||||
"Uploaded STEP was used only as extraction and acceptance truth.",
|
||||
"DesignIR 3.0 anti-cheat validation passed.",
|
||||
"STEP was independently rebuilt without passing the teacher path to the rebuild worker.",
|
||||
"Independent geometry acceptance passed.",
|
||||
`${Number(execution.validated_parameter_count || 0)} editable parameters passed isolated perturbation acceptance.`,
|
||||
],
|
||||
studio: {
|
||||
latestArtifact: artifactPath,
|
||||
latestPreview: artifactPath,
|
||||
latestViewerAsset: viewerAssetPath,
|
||||
latestVersion: artifactVersion,
|
||||
uploadReconstruction: {
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
teacherName: teacherName || path.basename(teacherPath),
|
||||
sourceEmbedded: false,
|
||||
},
|
||||
editCommandTemplate: existingManifest.edit_command_template || [],
|
||||
},
|
||||
});
|
||||
|
||||
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: [
|
||||
`${teacherName || path.basename(teacherPath)} 已通过 DesignIR 3.0 / SurfaceIR 独立重建。`,
|
||||
`几何验收通过,已验证可修改参数 ${Number(execution.validated_parameter_count || 0)} 个。`,
|
||||
].join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
export async function editDesignIR3Parameter({
|
||||
prompt,
|
||||
sourceTaskId,
|
||||
parameter,
|
||||
value,
|
||||
}: {
|
||||
prompt: string;
|
||||
sourceTaskId: string;
|
||||
parameter: string;
|
||||
value: number;
|
||||
}): Promise<CadGenerationResult> {
|
||||
const sourceManifest = await readManifest(sourceTaskId);
|
||||
if (!sourceManifest || sourceManifest.source?.format !== "designir-3.0") {
|
||||
throw new Error("Current task is not an editable DesignIR 3.0 reconstruction.");
|
||||
}
|
||||
const sourcePath = String(sourceManifest.source.path || "");
|
||||
const sourceAbsolutePath = path.join(taskDir(sourceTaskId), sourcePath);
|
||||
const sourceDesignIR = JSON.parse(
|
||||
await fs.readFile(sourceAbsolutePath, "utf8"),
|
||||
) as Record<string, any>;
|
||||
const parameterRecord = sourceDesignIR.semantic_layer?.parameters?.[parameter];
|
||||
if (
|
||||
!parameterRecord
|
||||
|| parameterRecord.editable !== true
|
||||
|| parameterRecord.edit_state !== "validated_executable_binding"
|
||||
) {
|
||||
throw new Error(
|
||||
`${parameter} is not a perturbation-validated DesignIR 3.0 parameter.`,
|
||||
);
|
||||
}
|
||||
const range = parameterRecord.validated_range || {};
|
||||
const minimum = Number(range.minimum_tested_inclusive);
|
||||
const maximum = Number(range.maximum_tested_inclusive);
|
||||
if (
|
||||
!Number.isFinite(value)
|
||||
|| !Number.isFinite(minimum)
|
||||
|| !Number.isFinite(maximum)
|
||||
|| value < minimum
|
||||
|| value > maximum
|
||||
) {
|
||||
throw new Error(
|
||||
`${parameter} must remain within the validated range [${minimum}, ${maximum}].`,
|
||||
);
|
||||
}
|
||||
const uploadReconstruction = (
|
||||
sourceManifest.studio?.uploadReconstruction
|
||||
&& typeof sourceManifest.studio.uploadReconstruction === "object"
|
||||
? sourceManifest.studio.uploadReconstruction
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const teacherTaskId = String(uploadReconstruction.teacherTaskId || "");
|
||||
const teacherPath = String(uploadReconstruction.teacherPath || "");
|
||||
if (!teacherTaskId || !teacherPath) {
|
||||
throw new Error(
|
||||
"The DesignIR 3.0 task has no teacher reference for independent edit acceptance.",
|
||||
);
|
||||
}
|
||||
const { absolutePath: teacherAbsolutePath } = await readArtifact(
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
);
|
||||
const task = await ensureTask();
|
||||
const acceptanceDir = path.join(task.dir, "validation", "parameter-edit");
|
||||
const acceptance = await runJson(
|
||||
pythonExecutable(),
|
||||
[
|
||||
surfaceirScript(),
|
||||
"validate-edit",
|
||||
teacherAbsolutePath,
|
||||
sourceAbsolutePath,
|
||||
"--parameter",
|
||||
parameter,
|
||||
"--value",
|
||||
String(value),
|
||||
"--output-dir",
|
||||
acceptanceDir,
|
||||
],
|
||||
task.dir,
|
||||
300_000,
|
||||
);
|
||||
if (acceptance.accepted !== true) {
|
||||
throw new Error(
|
||||
`DesignIR 3.0 parameter edit failed independent acceptance: ${String(acceptance.state || "rejected")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const generatedStem = `${path.parse(sourcePath).name}.edited`;
|
||||
const generatedDesignIR = path.join(
|
||||
acceptanceDir,
|
||||
`${generatedStem}.json`,
|
||||
);
|
||||
const generatedStep = path.join(
|
||||
acceptanceDir,
|
||||
`${generatedStem}.step`,
|
||||
);
|
||||
const safeParameter = sanitizeFilename(parameter);
|
||||
const outputStem = `${path.parse(sourcePath).name}.${safeParameter}`;
|
||||
const outputDesignIRPath = `${outputStem}.json`;
|
||||
const outputStepPath = `${outputStem}.step`;
|
||||
await fs.copyFile(
|
||||
generatedDesignIR,
|
||||
path.join(task.dir, outputDesignIRPath),
|
||||
);
|
||||
await fs.copyFile(generatedStep, path.join(task.dir, outputStepPath));
|
||||
const editedDesignIR = JSON.parse(
|
||||
await fs.readFile(path.join(task.dir, outputDesignIRPath), "utf8"),
|
||||
) as Record<string, any>;
|
||||
const updatedParameter = editedDesignIR.semantic_layer.parameters[parameter];
|
||||
const parameterCatalog = {
|
||||
...(sourceManifest.parameters || {}),
|
||||
parameters: Array.isArray(sourceManifest.parameters?.parameters)
|
||||
? sourceManifest.parameters.parameters.map((item: any) => (
|
||||
item?.name === parameter
|
||||
? { ...item, value: updatedParameter.value }
|
||||
: item
|
||||
))
|
||||
: [],
|
||||
};
|
||||
const viewerAssetPath = await createStepGlbPreview({
|
||||
taskId: task.taskId,
|
||||
stepPath: outputStepPath,
|
||||
});
|
||||
const artifactVersion = Date.now().toString(36);
|
||||
await upsertManifest(task.taskId, {
|
||||
request: prompt,
|
||||
route: {
|
||||
selected_backend: "build123d",
|
||||
runner_skill: "cad-router",
|
||||
project: "text-to-cad",
|
||||
execution_adapter: "DesignIR 3.0 / SurfaceIR parameter edit",
|
||||
fallback_order: ["simplecadapi"],
|
||||
workflow_profiles: [],
|
||||
},
|
||||
source: {
|
||||
path: outputDesignIRPath,
|
||||
format: "designir-3.0",
|
||||
backend: "surfaceir_occt",
|
||||
source_editability: "validated_parameter_source",
|
||||
generated_by: "cad-router_designir3_parameter_edit",
|
||||
parentTaskId: sourceTaskId,
|
||||
teacher_embedded: false,
|
||||
},
|
||||
artifacts: [
|
||||
{ path: outputDesignIRPath, role: "editable_contract", kind: "designir-3.0" },
|
||||
{ path: outputStepPath, role: "primary", kind: "step" },
|
||||
{
|
||||
path: path.relative(
|
||||
task.dir,
|
||||
path.join(acceptanceDir, "acceptance-report.json"),
|
||||
),
|
||||
role: "parameter_acceptance",
|
||||
kind: "json",
|
||||
},
|
||||
{ path: viewerAssetPath, role: "viewer", kind: "glb" },
|
||||
],
|
||||
parameters: parameterCatalog,
|
||||
experience: sourceManifest.experience || {},
|
||||
assumptions: [],
|
||||
validation: [
|
||||
`Parameter ${parameter} changed to ${value} within its validated range.`,
|
||||
"The edited STEP passed independent teacher-side semantic edit acceptance.",
|
||||
"The output remains independently rebuildable from DesignIR 3.0.",
|
||||
],
|
||||
studio: {
|
||||
latestArtifact: outputStepPath,
|
||||
latestPreview: outputStepPath,
|
||||
latestViewerAsset: viewerAssetPath,
|
||||
latestVersion: artifactVersion,
|
||||
uploadReconstruction,
|
||||
parentTaskId: sourceTaskId,
|
||||
},
|
||||
});
|
||||
const stepUrl = artifactUrl(task.taskId, outputStepPath, artifactVersion);
|
||||
return {
|
||||
handled: true,
|
||||
taskId: task.taskId,
|
||||
artifactPath: outputStepPath,
|
||||
artifactUrl: stepUrl,
|
||||
previewPath: outputStepPath,
|
||||
previewUrl: stepUrl,
|
||||
viewerAssetPath,
|
||||
viewerAssetUrl: artifactUrl(
|
||||
task.taskId,
|
||||
viewerAssetPath,
|
||||
artifactVersion,
|
||||
),
|
||||
sourcePath: outputDesignIRPath,
|
||||
summary: `${parameter} 已修改为 ${value},并通过独立参数验收。`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { z } from "zod/v4";
|
||||
import { executeDesignIR, type CadGenerationResult } from "@/lib/cad-generator";
|
||||
import {
|
||||
editDesignIR3Parameter,
|
||||
executeDesignIR,
|
||||
reconstructUploadedStep,
|
||||
type CadGenerationResult,
|
||||
} from "@/lib/cad-generator";
|
||||
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "@/lib/config";
|
||||
import { readManifest, taskDir } from "@/lib/task-store";
|
||||
|
||||
@@ -244,7 +249,7 @@ async function buildAttachmentUserContent(lastUserContent: string, attachments:
|
||||
"\nUploaded files available to this turn:",
|
||||
...attachedFileLines,
|
||||
"Images are attached as model-visible image parts when the selected provider/model supports vision.",
|
||||
"For STEP/STP or binary files, call inspect_uploaded_file with id/taskId/path before using their contents.",
|
||||
"For STEP/STP reconstruction, call reconstruct_uploaded_step with id/taskId/path. Do not ask the model to author DesignIR from STEP text.",
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
@@ -307,7 +312,7 @@ function attachmentSummary(attachments: unknown[]) {
|
||||
return [
|
||||
"Current uploaded attachment JSON available to the agent:",
|
||||
compactJson(attachments, 14000),
|
||||
"If a STEP/STP attachment is present, inspect its deterministic evidence, then author DesignIR without reading or importing the teacher STEP.",
|
||||
"If a STEP/STP attachment is present, call reconstruct_uploaded_step so CAD Router executes the deterministic DesignIR 3.0 / SurfaceIR pipeline.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -319,15 +324,20 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
|
||||
"Never return only code when the user asks to generate CAD. Use the generate_cad tool instead.",
|
||||
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
|
||||
"The generate_cad tool validates executable DesignIR, selects a backend through CAD Router, and rebuilds STEP in a teacher-free sandbox.",
|
||||
"When STEP files are attached, the source is teacher and acceptance truth only. Reconstruction consumes deterministic evidence, never source B-Rep.",
|
||||
"When STEP files are attached, the source is teacher and acceptance truth only. Use reconstruct_uploaded_step; never import the teacher as model geometry.",
|
||||
"All uploaded files are available to you. Images are attached directly to the latest user message when the selected provider/model supports vision. STEP/STP and binary files are available through inspect_uploaded_file.",
|
||||
"When the user asks to model from an uploaded image, inspect the image directly from the image part. If the selected provider/model cannot process images, say that clearly and ask the user to switch to a vision-capable OpenAI-compatible model.",
|
||||
"When the user asks to use an uploaded STEP/STP or other non-image file, call inspect_uploaded_file before making claims about its geometry or contents.",
|
||||
"When the user asks to reconstruct an uploaded STEP/STP, call reconstruct_uploaded_step directly. Use inspect_uploaded_file only for read-only questions that do not request reconstruction.",
|
||||
"When viewer context is present, use its cad-edit-intent.v1 or cad-ai-geometry-selection.v1 payload as precise geometry context.",
|
||||
"If viewer context selection.scope is selected_reference_only, modify only that selected topology/feature. Do not change a global pattern parameter or all repeated/symmetric features unless the user explicitly asks for all of them.",
|
||||
"For selected holes, use the selected reference center/surface/bbox/adjacent selectors to identify the individual feature. If the editable source models a repeated pattern with one shared diameter variable, split out or override the selected instance instead of changing the shared variable.",
|
||||
"Prefer editing the DesignIR recorded in cad-task.json before regenerating STEP and viewer assets.",
|
||||
"For CAD generation, author complete DesignIR 2.0 with parameters, datums, features, constraints, edit interface, and validation perturbations.",
|
||||
"If the current task is an uploaded-STEP DesignIR 3.0 surface_parametric reconstruction and the requested change matches a validated parameter, call edit_designir3_parameter.",
|
||||
"If the current task is a text/image DesignIR 3.0 fully_semantic_parametric or hybrid_semantic_surface_parametric model, modify its authoritative semantic_layer and call generate_cad again; preserve unrelated parameters, constraints, and features.",
|
||||
"For every new text/image CAD generation, author complete DesignIR 3.0 with designir_kind=independent_parametric_cad, reconstruction_mode=fully_semantic_parametric, authoring_mode=semantic_feature_program, and semantic_layer containing reconstruction_status, coordinate_system, datums, parameters, expressions, sketches, constraints, features, patterns, attachments, and construction_stages.",
|
||||
"For semantic DesignIR 3.0, edit_interface must contain semantic_parameters, surface_parameter_groups=[], modification_levels=[semantic_feature], and preserved_interfaces. validation_contract must contain source_independence=true, geometry_checks, edit_checks, invariants, perturbations, and thresholds.",
|
||||
"Use semantic parameter names that describe design intent, such as flange_thickness, shelf_depth, hole_spacing, rib_count, bore_diameter, and bolt_circle_diameter. Every editable parameter must directly drive at least one feature and have a perturbation test.",
|
||||
"Uploaded STEP reconstruction also uses DesignIR 3.0, but in deterministic surface_parametric mode. Do not substitute one mode for the other.",
|
||||
"Never embed STEP/B-Rep/mesh data or source topology references in DesignIR.",
|
||||
"If CAD generation is requested and the generate_cad tool is available, call generate_cad.",
|
||||
"For modifications, use the current editable source of truth when provided. Preserve unrelated geometry and change only the selected/requested feature.",
|
||||
@@ -494,24 +504,20 @@ async function inspectStepAttachment(attachment: AttachmentRecord) {
|
||||
"}",
|
||||
"print(json.dumps(payload, ensure_ascii=False))",
|
||||
].join("\n");
|
||||
const [{ stdout }, excerpt] = await Promise.all([
|
||||
execFileAsync("python3", ["-c", script, absolutePath], {
|
||||
const { stdout } = await execFileAsync(
|
||||
process.env.CAD_PYTHON || path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python"),
|
||||
["-c", script, absolutePath],
|
||||
{
|
||||
timeout: 30_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
}),
|
||||
readTextExcerpt(attachment, 24_000).catch(() => null),
|
||||
]);
|
||||
},
|
||||
);
|
||||
const geometry = JSON.parse(stdout.trim().split(/\r?\n/).pop() || "{}");
|
||||
return {
|
||||
attachment,
|
||||
inspectionKind: "step_geometry",
|
||||
geometry,
|
||||
stepTextExcerpt: excerpt ? {
|
||||
text: excerpt.text,
|
||||
truncated: excerpt.truncated,
|
||||
bytesRead: excerpt.bytesRead,
|
||||
totalBytes: excerpt.totalBytes,
|
||||
} : null,
|
||||
sourcePolicy: "Teacher geometry was measured read-only; raw STEP text is not exposed to the reconstruction model.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -687,9 +693,159 @@ export async function streamAgentResponse({
|
||||
}
|
||||
},
|
||||
}),
|
||||
reconstruct_uploaded_step: tool({
|
||||
description: [
|
||||
"Reconstruct one uploaded STEP/STP through CAD Router's executable DesignIR 3.0 / SurfaceIR pipeline.",
|
||||
"Use this instead of generate_cad for an uploaded STEP.",
|
||||
"It independently rebuilds STEP, runs geometry acceptance, validates editable parameters, and publishes the preview.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
id: z.string().optional().describe("Attachment id from the uploaded file list."),
|
||||
taskId: z.string().optional().describe("Attachment taskId."),
|
||||
path: z.string().optional().describe("STEP path inside the upload task."),
|
||||
}),
|
||||
execute: async ({ id, taskId, path: attachmentPath }) => {
|
||||
const attachment = findAttachment(uploadedAttachments, {
|
||||
id,
|
||||
taskId,
|
||||
path: attachmentPath,
|
||||
});
|
||||
if (!attachment || !isStepAttachment(attachment)) {
|
||||
throw new Error(
|
||||
"A STEP/STP attachment is required. Use id/taskId/path from the uploaded attachment list.",
|
||||
);
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "running",
|
||||
message: "正在提取 SurfaceIR、独立重建、执行几何和参数验收。",
|
||||
});
|
||||
try {
|
||||
cadGeneration = await reconstructUploadedStep({
|
||||
prompt: lastUserText(chatMessages),
|
||||
teacherTaskId: String(attachment.taskId),
|
||||
teacherPath: String(attachment.path),
|
||||
teacherName: attachment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: "Uploaded STEP reconstruction failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
failAgentStream(message);
|
||||
throw error;
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "DesignIR 3.0 独立重建",
|
||||
status: "success",
|
||||
message: `${cadGeneration.artifactPath},参数验收已完成。`,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "update_preview",
|
||||
label: "更新右侧预览",
|
||||
status: "success",
|
||||
message: "独立重建模型已载入。",
|
||||
});
|
||||
completeAgentStream("DesignIR 3.0 重建和验收已完成。");
|
||||
streamWriter?.write({
|
||||
type: "data-cad-result",
|
||||
data: {
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
viewerAssetUrl: cadGeneration.viewerAssetUrl,
|
||||
summary: cadGeneration.summary,
|
||||
},
|
||||
id: `cad_result_${cadGeneration.taskId}`,
|
||||
});
|
||||
return cadGeneration;
|
||||
},
|
||||
}),
|
||||
edit_designir3_parameter: tool({
|
||||
description: [
|
||||
"Edit one perturbation-validated parameter on the current DesignIR 3.0 reconstruction.",
|
||||
"The requested value must be within the validated range shown in current cad-task.json.",
|
||||
"The server reruns independent teacher-side semantic edit acceptance before publishing.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
parameter: z.string().describe("Validated parameter name from the current task."),
|
||||
value: z.number().describe("New value within the parameter's validated range."),
|
||||
}),
|
||||
execute: async ({ parameter, value }) => {
|
||||
if (!selectedTaskId) {
|
||||
throw new Error("No current DesignIR 3.0 task is selected.");
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "running",
|
||||
message: `${parameter} → ${value},正在执行独立参数验收。`,
|
||||
});
|
||||
try {
|
||||
cadGeneration = await editDesignIR3Parameter({
|
||||
prompt: lastUserText(chatMessages),
|
||||
sourceTaskId: selectedTaskId,
|
||||
parameter,
|
||||
value,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: "DesignIR 3.0 parameter edit failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
failAgentStream(message);
|
||||
throw error;
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
status: "success",
|
||||
message: cadGeneration.summary,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "update_preview",
|
||||
label: "更新右侧预览",
|
||||
status: "success",
|
||||
message: "参数修改后的模型已载入。",
|
||||
});
|
||||
completeAgentStream("DesignIR 3.0 参数修改和验收已完成。");
|
||||
streamWriter?.write({
|
||||
type: "data-cad-result",
|
||||
data: {
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
viewerAssetUrl: cadGeneration.viewerAssetUrl,
|
||||
summary: cadGeneration.summary,
|
||||
},
|
||||
id: `cad_result_${cadGeneration.taskId}`,
|
||||
});
|
||||
return cadGeneration;
|
||||
},
|
||||
}),
|
||||
generate_cad: tool({
|
||||
description: [
|
||||
"Generate a real CAD model from agent-authored executable DesignIR 2.0.",
|
||||
"Generate a real CAD model from agent-authored semantic DesignIR 3.0.",
|
||||
"Call this only when the user wants CAD generation or a model modification.",
|
||||
"CAD Router selects the backend and the server rebuilds STEP without teacher geometry.",
|
||||
].join(" "),
|
||||
@@ -697,7 +853,7 @@ export async function streamAgentResponse({
|
||||
summary: z.string().describe("Concise user-facing summary of the generated CAD model."),
|
||||
designirFilename: z.string().describe("DesignIR filename, e.g. model.designir.json."),
|
||||
stepFilename: z.string().describe("STEP output filename, e.g. model.step."),
|
||||
designir: z.record(z.string(), z.unknown()).describe("Complete executable DesignIR 2.0 JSON object."),
|
||||
designir: z.record(z.string(), z.unknown()).describe("Complete semantic DesignIR 3.0 JSON object in fully_semantic_parametric mode."),
|
||||
teacherTaskId: z.string().optional().describe("Uploaded teacher STEP task id for independent acceptance only."),
|
||||
teacherPath: z.string().optional().describe("Uploaded teacher STEP path for independent acceptance only."),
|
||||
assumptions: z.array(z.string()).default([]).describe("Assumptions made because the user did not specify every dimension."),
|
||||
|
||||
Reference in New Issue
Block a user