feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -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