优化skill,添加特征树、编辑窗口
This commit is contained in:
@@ -11,22 +11,48 @@ import {
|
||||
type UserContent,
|
||||
} from "ai";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
editDesignIR3Parameter,
|
||||
executeDesignIR,
|
||||
editDesignIRParameter,
|
||||
executeBackendNativeGeneration,
|
||||
reconstructUploadedStep,
|
||||
routeNewCadRequest,
|
||||
type CadGenerationResult,
|
||||
type NewCadRouteDecision,
|
||||
} from "@/lib/cad-generator";
|
||||
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "@/lib/config";
|
||||
import {
|
||||
appendConversationMessage,
|
||||
conversationModelMessages,
|
||||
ensureConversation,
|
||||
safeConversationId,
|
||||
} from "@/lib/conversation-store";
|
||||
import { exportRobotDescription } from "@/lib/robot-export";
|
||||
import { readManifest, taskDir } from "@/lib/task-store";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function cadPythonExecutable() {
|
||||
const configured = String(process.env.CAD_PYTHON || "").trim();
|
||||
if (configured) return configured;
|
||||
const workspacePython = path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python");
|
||||
if (existsSync(workspacePython)) return workspacePython;
|
||||
if (commandExists("python")) return "python";
|
||||
if (commandExists("python3")) return "python3";
|
||||
return "python3";
|
||||
}
|
||||
|
||||
function commandExists(command: string) {
|
||||
return String(process.env.PATH || "")
|
||||
.split(path.delimiter)
|
||||
.some((directory) => existsSync(path.join(directory, command)));
|
||||
}
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
@@ -61,11 +87,14 @@ function writeTextPart(writer: { write: (part: any) => void }, text: string) {
|
||||
writer.write({ type: "text-end", id });
|
||||
}
|
||||
|
||||
function uiTextResponse(text: string) {
|
||||
function uiTextResponse(text: string, onEnd?: (message: UIMessage) => Promise<void>) {
|
||||
const stream = createUIMessageStream<UIMessage>({
|
||||
execute: ({ writer }) => {
|
||||
writeTextPart(writer, text);
|
||||
},
|
||||
onEnd: async ({ responseMessage, isAborted }) => {
|
||||
if (!isAborted) await onEnd?.(responseMessage);
|
||||
},
|
||||
});
|
||||
return createUIMessageStreamResponse({
|
||||
stream,
|
||||
@@ -352,6 +381,292 @@ function attachmentSummary(attachments: unknown[]) {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const SIMPLECADAPI_SKILL_ROOT = path.join(process.cwd(), "..", "SimpleCADAPI", "skills", "simplecadapi");
|
||||
const SIMPLECADAPI_REFERENCE_ROOT = path.join(SIMPLECADAPI_SKILL_ROOT, "references");
|
||||
const SIMPLECADAPI_REQUIRED_DOCS = ["skill", "api/README.md", "stdlib/README.md"];
|
||||
const TEXT_TO_CAD_SKILL_ROOT = path.join(process.cwd(), "..", "text-to-cad", "skills", "cad");
|
||||
const TEXT_TO_CAD_REQUIRED_DOCS = ["skill", "references/build123d-modeling.md", "references/step-generation.md"];
|
||||
|
||||
type SimpleCadApiDocumentationResult =
|
||||
| {
|
||||
ok: true;
|
||||
document: string;
|
||||
path: string;
|
||||
content: string;
|
||||
resolvedFrom?: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
document: string;
|
||||
error: string;
|
||||
suggestedDocuments: string[];
|
||||
};
|
||||
|
||||
const SIMPLECADAPI_DOC_DIRECTORIES = ["api", "stdlib", "core"] as const;
|
||||
|
||||
function normalizeSimpleCadApiDocument(document: string) {
|
||||
const requested = String(document || "").trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
||||
const normalized = path.posix.normalize(requested);
|
||||
if (!requested || normalized === "." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeTextToCadDocument(document: string) {
|
||||
const requested = String(document || "").trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
||||
const normalized = path.posix.normalize(requested);
|
||||
if (
|
||||
!requested
|
||||
|| requested.split("/").includes("..")
|
||||
|| normalized === "."
|
||||
|| normalized.startsWith("../")
|
||||
|| path.posix.isAbsolute(normalized)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function matchingSimpleCadApiDocuments(filename: string) {
|
||||
if (!filename.endsWith(".md")) return [];
|
||||
const matches = await Promise.all(SIMPLECADAPI_DOC_DIRECTORIES.map(async (directory) => {
|
||||
const root = path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", directory);
|
||||
try {
|
||||
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name === filename)
|
||||
.map((entry) => `${directory}/${entry.name}`);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
return matches.flat();
|
||||
}
|
||||
|
||||
export async function readSimpleCadApiDocumentation(document: string): Promise<SimpleCadApiDocumentationResult> {
|
||||
const normalized = normalizeSimpleCadApiDocument(document);
|
||||
if (!normalized) {
|
||||
return {
|
||||
ok: false,
|
||||
document: String(document || ""),
|
||||
error: "SimpleCADAPI document must be a listed base document or a relative Markdown page below references/docs/.",
|
||||
suggestedDocuments: [],
|
||||
};
|
||||
}
|
||||
const fixedDocuments: Record<string, string> = {
|
||||
skill: path.join(SIMPLECADAPI_SKILL_ROOT, "SKILL.md"),
|
||||
"api/README.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", "api", "README.md"),
|
||||
"stdlib/README.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "docs", "stdlib", "README.md"),
|
||||
"SDK_SURFACES.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "SDK_SURFACES.md"),
|
||||
"MODELING_WORKFLOWS.md": path.join(SIMPLECADAPI_REFERENCE_ROOT, "MODELING_WORKFLOWS.md"),
|
||||
};
|
||||
const fixedPath = fixedDocuments[normalized];
|
||||
const referenceDocsRoot = path.resolve(SIMPLECADAPI_REFERENCE_ROOT, "docs");
|
||||
let resolvedDocument = normalized;
|
||||
let target = fixedPath || path.resolve(referenceDocsRoot, normalized);
|
||||
const relativeTarget = path.relative(referenceDocsRoot, target);
|
||||
const allowedDirectPath = fixedPath || (
|
||||
normalized.endsWith(".md")
|
||||
&& !relativeTarget.startsWith("..")
|
||||
&& !path.isAbsolute(relativeTarget)
|
||||
);
|
||||
|
||||
if (!allowedDirectPath || !target.endsWith(".md") || !existsSync(target)) {
|
||||
const candidates = await matchingSimpleCadApiDocuments(path.posix.basename(normalized));
|
||||
if (candidates.length === 1) {
|
||||
resolvedDocument = candidates[0];
|
||||
target = path.join(referenceDocsRoot, resolvedDocument);
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
document: normalized,
|
||||
error: candidates.length > 1
|
||||
? "SimpleCADAPI document path is ambiguous. Use one of the suggested canonical paths."
|
||||
: "SimpleCADAPI document was not found. Read api/README.md or stdlib/README.md and use its linked canonical page path.",
|
||||
suggestedDocuments: candidates,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(target, "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
document: resolvedDocument,
|
||||
path: target,
|
||||
content,
|
||||
...(resolvedDocument !== normalized ? { resolvedFrom: normalized } : {}),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
document: normalized,
|
||||
error: error instanceof Error ? error.message : "SimpleCADAPI document could not be read.",
|
||||
suggestedDocuments: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type TextToCadDocumentationResult =
|
||||
| { ok: true; document: string; path: string; content: string }
|
||||
| { ok: false; document: string; error: string; suggestedDocuments: string[] };
|
||||
|
||||
export async function readTextToCadDocumentation(document: string): Promise<TextToCadDocumentationResult> {
|
||||
const normalized = normalizeTextToCadDocument(document);
|
||||
const fixedDocuments: Record<string, string> = {
|
||||
skill: path.join(TEXT_TO_CAD_SKILL_ROOT, "SKILL.md"),
|
||||
"references/build123d-modeling.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "build123d-modeling.md"),
|
||||
"references/step-generation.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "step-generation.md"),
|
||||
"references/inspection-and-validation.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "inspection-and-validation.md"),
|
||||
"references/positioning.md": path.join(TEXT_TO_CAD_SKILL_ROOT, "references", "positioning.md"),
|
||||
};
|
||||
const cadSkillxRoot = path.resolve(TEXT_TO_CAD_SKILL_ROOT, "references", "cad-skillx");
|
||||
const cadSkillxTarget = normalized
|
||||
? path.resolve(TEXT_TO_CAD_SKILL_ROOT, normalized)
|
||||
: "";
|
||||
const cadSkillxRelative = cadSkillxTarget ? path.relative(cadSkillxRoot, cadSkillxTarget) : "";
|
||||
const cadSkillxDocument = Boolean(
|
||||
normalized
|
||||
&& normalized.startsWith("references/cad-skillx/")
|
||||
&& normalized.endsWith(".md")
|
||||
&& cadSkillxRelative
|
||||
&& !cadSkillxRelative.startsWith("..")
|
||||
&& !path.isAbsolute(cadSkillxRelative),
|
||||
);
|
||||
const target = fixedDocuments[normalized || ""] || (cadSkillxDocument ? cadSkillxTarget : "");
|
||||
if (!normalized || !target) {
|
||||
return {
|
||||
ok: false,
|
||||
document: String(document || ""),
|
||||
error: "Use a listed text-to-cad skill document key or a Markdown file below references/cad-skillx/.",
|
||||
suggestedDocuments: [
|
||||
"skill",
|
||||
...Object.keys(fixedDocuments).filter((key) => key !== "skill"),
|
||||
"references/cad-skillx/planning/mounting-plate.planning.md",
|
||||
"references/cad-skillx/planning/flange.planning.md",
|
||||
"references/cad-skillx/planning/bearing-housing-or-seat.planning.md",
|
||||
],
|
||||
};
|
||||
}
|
||||
try {
|
||||
return {
|
||||
ok: true,
|
||||
document: normalized,
|
||||
path: target,
|
||||
content: await fs.readFile(target, "utf8"),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
document: normalized,
|
||||
error: error instanceof Error ? error.message : "text-to-cad document could not be read.",
|
||||
suggestedDocuments: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRequiredTextToCadDocumentation() {
|
||||
const results = await Promise.all(TEXT_TO_CAD_REQUIRED_DOCS.map(readTextToCadDocumentation));
|
||||
const unavailable = results.filter((result) => !result.ok);
|
||||
if (unavailable.length) {
|
||||
throw new Error(`Studio could not load required text-to-cad documentation: ${unavailable.map((result) => result.document).join(", ")}.`);
|
||||
}
|
||||
return results.filter((result): result is Extract<TextToCadDocumentationResult, { ok: true }> => result.ok);
|
||||
}
|
||||
|
||||
type Build123dApiSymbol = {
|
||||
symbol: string;
|
||||
ok: boolean;
|
||||
signature?: string;
|
||||
documentation?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
function parseJsonObjectFromStdout(stdout: string) {
|
||||
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 {
|
||||
// Ignore non-JSON diagnostics preceding a final JSON report.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function inspectBuild123dApi(symbols: string[]) {
|
||||
const requested = [...new Set(symbols.map((symbol) => String(symbol).trim()))]
|
||||
.filter((symbol) => /^[A-Za-z_]\w*$/.test(symbol))
|
||||
.slice(0, 32);
|
||||
if (!requested.length) {
|
||||
return { symbols: [] as Build123dApiSymbol[] };
|
||||
}
|
||||
const script = [
|
||||
"import inspect, json, sys",
|
||||
"import build123d as b123d",
|
||||
"result = []",
|
||||
"for name in sys.argv[1:] :",
|
||||
" try:",
|
||||
" value = getattr(b123d, name)",
|
||||
" signature = str(inspect.signature(value))",
|
||||
" documentation = inspect.getdoc(value) or ''",
|
||||
" result.append({'symbol': name, 'ok': True, 'signature': signature, 'documentation': documentation[:4000]})",
|
||||
" except Exception as error:",
|
||||
" result.append({'symbol': name, 'ok': False, 'error': str(error)})",
|
||||
"print(json.dumps({'symbols': result}, ensure_ascii=False))",
|
||||
].join("\n");
|
||||
const { stdout } = await execFileAsync(cadPythonExecutable(), ["-c", script, ...requested], {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const payload = parseJsonObjectFromStdout(stdout);
|
||||
const values = Array.isArray(payload?.symbols) ? payload.symbols : [];
|
||||
return {
|
||||
symbols: values.filter((value): value is Build123dApiSymbol => Boolean(value) && typeof value === "object"),
|
||||
};
|
||||
}
|
||||
|
||||
function importedPythonNames(source: string) {
|
||||
const build123dImports = new Set<string>();
|
||||
const externalImports = new Set<string>();
|
||||
for (const match of source.matchAll(/^\s*from\s+([A-Za-z_][\w.]*)\s+import\s+(.+)$/gm)) {
|
||||
const moduleName = match[1];
|
||||
const names = match[2]
|
||||
.split(",")
|
||||
.map((part) => part.trim().replace(/\s+#.*$/, ""))
|
||||
.filter(Boolean);
|
||||
for (const namePart of names) {
|
||||
const localName = (/\s+as\s+([A-Za-z_]\w*)$/.exec(namePart)?.[1] || namePart.split(/\s+as\s+/)[0]).trim();
|
||||
if (!/^[A-Za-z_]\w*$/.test(localName)) continue;
|
||||
if (moduleName === "build123d") {
|
||||
build123dImports.add(localName);
|
||||
} else {
|
||||
externalImports.add(localName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { build123dImports, externalImports };
|
||||
}
|
||||
|
||||
export function build123dCallCandidates(source: string) {
|
||||
const { build123dImports, externalImports } = importedPythonNames(source);
|
||||
const defined = new Set(
|
||||
[...source.matchAll(/^\s*def\s+([A-Za-z_]\w*)\s*\(/gm)].map((match) => match[1]),
|
||||
);
|
||||
const candidates = [...source.matchAll(/(?<![\w.])([A-Za-z_]\w*)\s*\(/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((name) => !defined.has(name))
|
||||
.filter((name) => !externalImports.has(name))
|
||||
.filter((name) => !build123dImports.size || build123dImports.has(name));
|
||||
const memberReferences = [...source.matchAll(/(?<![\w.])([A-Za-z_]\w*)\.[A-Za-z_]\w*/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((name) => build123dImports.has(name));
|
||||
return [...new Set([...candidates, ...memberReferences])].slice(0, 64);
|
||||
}
|
||||
|
||||
function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], taskContext = "") {
|
||||
return [
|
||||
"You are the CAD Agent Studio assistant.",
|
||||
@@ -359,7 +674,9 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
|
||||
"Strict honesty rule: never claim that a CAD file/model was generated unless the generate_cad tool has succeeded and returned artifact URLs.",
|
||||
"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.",
|
||||
"For every new text/image CAD model, call route_cad_request once before writing native source. CAD Router selects SimpleCADAPI only for catalogued standard primary parts; every other new part is text-to-cad/build123d. Use its returned sourceKind in generate_cad. Do not choose the backend yourself and do not read backend documentation for routing.",
|
||||
"When route_cad_request returns simplecadapi_python, first read the original SimpleCADAPI docs through read_simplecadapi_docs: skill, api/README.md, and stdlib/README.md. Then read the exact original API or stdlib Markdown page for every SimpleCADAPI function used in the source. Follow the documented signatures literally; do not invent namespaces or API names. Documentation paths come from the README links, not Python namespaces: for example simplecadapi.ql.value is documented as api/value.md, not ql/value.md. The server rejects source that calls a nonexistent SimpleCADAPI attribute.",
|
||||
"When route_cad_request returns build123d_python, Studio automatically loads the text-to-cad skill, build123d-modeling, and step-generation documents into the route result. Do not repeat those reads. If the request matches a CAD-SkillX entry listed there, use read_text_to_cad_docs only for the relevant Planning/Functional/Atomic reference. Before writing source, call read_build123d_api once with every build123d symbol you plan to use. It reads the active build123d runtime's actual signature and docstring. Follow those signatures exactly: do not guess argument order, namespaces, or keyword names. Use build123d enum members exactly, for example Align.CENTER and Mode.SUBTRACT, not strings such as \"CENTER\".",
|
||||
"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.",
|
||||
@@ -367,16 +684,26 @@ function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[], tas
|
||||
"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.",
|
||||
"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.",
|
||||
"Prefer the source of truth recorded in cad-task.json before regenerating STEP and viewer assets.",
|
||||
"If the requested change matches a listed editable parameter, call edit_designir3_parameter. The server edits the backend-bound parameter without LLM participation: build123d/native Python, SimpleCADAPI source/model graph, or SurfaceIR validated parameter as recorded.",
|
||||
"After uploaded-STEP reconstruction, list every perturbation-validated editable parameter by name, current value, unit, and validated range. Never report only the parameter count. Explain that unlisted inferred parameters remain in DesignIR but are not safely exposed until an executable binding and perturbation acceptance exist.",
|
||||
"When the user asks to download or convert the current model to URDF or MJCF, call export_robot_description. Do not fabricate XML. The tool performs lazy conversion from the current DesignIR and returns a portable ZIP containing the robot description and mesh.",
|
||||
"After export_robot_description succeeds, the UI starts the download and renders the exact package link. Say that the download has started; do not manually rewrite, shorten, or guess the returned URL.",
|
||||
"A STEP-derived DesignIR without authoritative link/joint/material evidence exports honestly as one fixed base_link with inertial data omitted. Never invent articulated joints, limits, actuators, density, mass, or inertia.",
|
||||
"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.",
|
||||
"For structural edits to generated text/image models, read the current source of truth and submit the edited backend-native source to generate_cad; preserve the current backend unless the user explicitly requests backend conversion.",
|
||||
"Do not author full DesignIR JSON for new text/image CAD generation. DesignIR 3.0 is the normalized contract layer generated after the backend has produced native source/graph and STEP.",
|
||||
"DesignIR records backend, source-of-truth paths, parameters, feature tree, validation, and edit bindings; it is not the default modeling language for new geometry.",
|
||||
"For generated backend-native models, never pretend backend_native_feature entries are replayable DesignIR compiler operations.",
|
||||
"CAD Router may use its standard-part keyword catalog only to select a backend. The server never infers geometry, dimensions, or features from keywords. generate_cad requires final backend-native source.",
|
||||
"Native source must be raw Python, not markdown. It must be a CLI program with argparse parse_args(), accept --step and --metadata, call export_step(..., args.step), and write backend-metadata.json to args.metadata; SimpleCADAPI source must also accept --model-json and write the requested model JSON file. A def gen_step() that only returns a part/object is an obsolete interface and will be rejected unless a __main__ CLI exports the files. stdout JSON is optional diagnostic output, not the source of truth.",
|
||||
"Do not submit API/signature/geometry probe scripts to generate_cad. nativeSource must be the final model generator for the user request, not a script that introspects the SDK, sweeps trial parameters, diagnoses topology, or intentionally raises an exception to report facts.",
|
||||
"Do not use generate_cad as an API discovery or exploration tool.",
|
||||
"If generate_cad returns ok=false and retryable=true, read the returned error/artifact diagnostics, revise the native source, and call generate_cad again in the same turn. Do not ask the user whether to retry unless the failure is caused by missing user requirements.",
|
||||
"Use generationMode=new_model for ordinary new CAD generation, structural_edit for modifying the current backend source, and backend_conversion only when the user explicitly asks to convert backend.",
|
||||
"Every generated backend-native model must expose editable parameters as a normal part of generation. Define important dimensions, counts, spacings, radii, chamfers/fillets, angles, and offsets as named numeric variables in the native source instead of burying them as magic numbers.",
|
||||
"For each generated model, include several useful editable parameters whenever the geometry has several meaningful dimensions. If the model is very simple, expose all meaningful dimensions. Do not leave backend-metadata.json parameters empty for a successfully generated model unless there are genuinely no numeric modeling choices.",
|
||||
"Editable parameters must be real source-bound variables used by the geometry, not decorative metadata. Native source must include a CAD_AGENT_PARAMETERS block, and backend-metadata.json parameters must include name, display_name, value, unit, editable, binding_kind, parameter_path, and regenerate_adapter for each exposed parameter.",
|
||||
"Parameter names shown to the user must be concise Chinese labels generated for the model, not raw English code identifiers. Keep code variable names stable and machine-friendly; keep display_name human-friendly and Chinese.",
|
||||
"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.",
|
||||
@@ -545,7 +872,7 @@ async function inspectStepAttachment(attachment: AttachmentRecord) {
|
||||
"print(json.dumps(payload, ensure_ascii=False))",
|
||||
].join("\n");
|
||||
const { stdout } = await execFileAsync(
|
||||
process.env.CAD_PYTHON || path.join(process.cwd(), "..", "text-to-cad", ".venv", "bin", "python"),
|
||||
cadPythonExecutable(),
|
||||
["-c", script, absolutePath],
|
||||
{
|
||||
timeout: 30_000,
|
||||
@@ -599,6 +926,7 @@ export async function streamAgentResponse({
|
||||
provider,
|
||||
model,
|
||||
selectedTaskId,
|
||||
conversationId,
|
||||
}: {
|
||||
messages: unknown[];
|
||||
attachments: unknown[];
|
||||
@@ -606,14 +934,54 @@ export async function streamAgentResponse({
|
||||
provider?: string;
|
||||
model?: string;
|
||||
selectedTaskId?: string;
|
||||
conversationId?: string;
|
||||
}) {
|
||||
const config = loadLlmConfig();
|
||||
const requestedConversationId = String(conversationId || "").trim();
|
||||
const normalizedConversationId = requestedConversationId ? safeConversationId(requestedConversationId) : "";
|
||||
const requestMessages = messages;
|
||||
const uploadedAttachments = normalizedAttachments(attachments);
|
||||
let chatMessages = normalizedMessages(requestMessages);
|
||||
if (normalizedConversationId) {
|
||||
const latestUserMessage = [...requestMessages].reverse().find((message) => {
|
||||
const record = message && typeof message === "object" ? message as Record<string, unknown> : null;
|
||||
return record?.role === "user";
|
||||
});
|
||||
if (!latestUserMessage) {
|
||||
throw new Error("A user message is required to continue a conversation.");
|
||||
}
|
||||
const conversation = await ensureConversation(normalizedConversationId, selectedTaskId || "");
|
||||
const persisted = await appendConversationMessage({
|
||||
conversationId: normalizedConversationId,
|
||||
message: latestUserMessage,
|
||||
currentTaskId: selectedTaskId || conversation.currentTaskId,
|
||||
attachments: uploadedAttachments,
|
||||
});
|
||||
chatMessages = conversationModelMessages(persisted.messages) as ChatMessage[];
|
||||
selectedTaskId = String(selectedTaskId || persisted.currentTaskId || "").trim() || undefined;
|
||||
}
|
||||
const persistAssistantMessage = async (message: UIMessage) => {
|
||||
if (!normalizedConversationId) return;
|
||||
await appendConversationMessage({
|
||||
conversationId: normalizedConversationId,
|
||||
message,
|
||||
currentTaskId: selectedTaskId || "",
|
||||
attachments: uploadedAttachments,
|
||||
});
|
||||
};
|
||||
let config: LlmConfig;
|
||||
try {
|
||||
config = loadLlmConfig();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "无法加载模型配置。";
|
||||
return uiTextResponse(`Agent 请求没有成功。\n\n原因:${message}`, persistAssistantMessage);
|
||||
}
|
||||
const selected = selectedModelId(provider, model, config);
|
||||
const apiKey = resolveProviderApiKey(selected.providerConfig);
|
||||
const chatMessages = normalizedMessages(messages);
|
||||
const uploadedAttachments = normalizedAttachments(attachments);
|
||||
if (!apiKey) {
|
||||
return uiTextResponse(localAssistantReply({ messages: chatMessages, attachments: uploadedAttachments, viewerContext, selectedTaskId }));
|
||||
return uiTextResponse(
|
||||
localAssistantReply({ messages: chatMessages, attachments: uploadedAttachments, viewerContext, selectedTaskId }),
|
||||
persistAssistantMessage,
|
||||
);
|
||||
}
|
||||
const languageModel = buildLanguageModel({
|
||||
apiKey,
|
||||
@@ -624,7 +992,7 @@ export async function streamAgentResponse({
|
||||
|
||||
let cadGeneration: CadGenerationResult | null = null;
|
||||
let streamWriter: { write: (part: any) => void } | null = null;
|
||||
const transientProgressSteps = new Set(["analyze_request", "agent_stream"]);
|
||||
const transientProgressSteps = new Set(["analyze_request"]);
|
||||
const writeCadProgress = (payload: CadProgressPayload) => {
|
||||
streamWriter?.write({
|
||||
type: "data-cad-progress",
|
||||
@@ -663,7 +1031,105 @@ export async function streamAgentResponse({
|
||||
writeCadError("agent_stream", message);
|
||||
};
|
||||
const taskContext = await buildTaskContext(selectedTaskId);
|
||||
const simpleCadApiDocsRead = new Set<string>();
|
||||
const textToCadDocsRead = new Set<string>();
|
||||
const build123dApiSymbolsRead = new Set<string>();
|
||||
const routeDecisions = new Map<string, NewCadRouteDecision>();
|
||||
const cadTools = {
|
||||
route_cad_request: tool({
|
||||
description: [
|
||||
"Route one new CAD request before authoring native source.",
|
||||
"The CAD Router uses its exact SimpleCADAPI standard-part catalog: catalogued primary parts use SimpleCADAPI; all other new parts use text-to-cad/build123d.",
|
||||
"Do not use this for an uploaded STEP or a same-backend structural edit.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
request: z.string().describe("The full new-model request to classify."),
|
||||
}),
|
||||
execute: async ({ request }) => {
|
||||
writeCadProgress({
|
||||
step: "route_request",
|
||||
label: "CAD Router 选路",
|
||||
status: "running",
|
||||
message: "正在按标准零件类型目录选择生成后端。",
|
||||
});
|
||||
const decision = await routeNewCadRequest(request || lastUserText(chatMessages));
|
||||
simpleCadApiDocsRead.clear();
|
||||
textToCadDocsRead.clear();
|
||||
build123dApiSymbolsRead.clear();
|
||||
routeDecisions.clear();
|
||||
const build123dBaseDocs = decision.selectedBackend === "build123d"
|
||||
? await loadRequiredTextToCadDocumentation()
|
||||
: [];
|
||||
for (const document of build123dBaseDocs) {
|
||||
textToCadDocsRead.add(document.document);
|
||||
}
|
||||
const routeToken = randomUUID();
|
||||
routeDecisions.set(routeToken, decision);
|
||||
writeCadProgress({
|
||||
step: "route_request",
|
||||
label: "CAD Router 选路",
|
||||
status: "success",
|
||||
message: `${decision.selectedBackend}: ${decision.rationale}`,
|
||||
});
|
||||
return {
|
||||
...decision,
|
||||
routeToken,
|
||||
...(build123dBaseDocs.length ? {
|
||||
backendContext: {
|
||||
source: "Studio-loaded text-to-cad/build123d base documentation",
|
||||
documents: build123dBaseDocs.map(({ document, content }) => ({ document, content })),
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
},
|
||||
}),
|
||||
read_simplecadapi_docs: tool({
|
||||
description: [
|
||||
"Read original SimpleCADAPI skill or API documentation before writing SimpleCADAPI source.",
|
||||
"Read skill, api/README.md, and stdlib/README.md first, then the exact API or stdlib Markdown page for every function used.",
|
||||
"Use the canonical paths linked from api/README.md or stdlib/README.md. Do not derive a documentation directory from a Python namespace; ql/value.md, for example, resolves to api/value.md when it is unique.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
document: z.string().describe("Original skill/doc key or Markdown page path."),
|
||||
}),
|
||||
execute: async ({ document }) => {
|
||||
const result = await readSimpleCadApiDocumentation(document);
|
||||
if (result.ok) simpleCadApiDocsRead.add(result.document);
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
read_text_to_cad_docs: tool({
|
||||
description: [
|
||||
"Read an additional original text-to-cad/build123d reference when the Studio-loaded base documentation identifies a need.",
|
||||
"The base skill, build123d modeling, and STEP generation references are already returned by route_cad_request for build123d routes.",
|
||||
"Use this for relevant Markdown files below references/cad-skillx/ or optional positioning/inspection references.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
document: z.string().describe("Listed text-to-cad skill document key or references/cad-skillx/... Markdown path."),
|
||||
}),
|
||||
execute: async ({ document }) => {
|
||||
const result = await readTextToCadDocumentation(document);
|
||||
if (result.ok) textToCadDocsRead.add(result.document);
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
read_build123d_api: tool({
|
||||
description: [
|
||||
"Read exact function/class signatures and docstrings from the active build123d runtime before writing source.",
|
||||
"Pass all planned build123d symbols in one call, for example BuildPart, Locations, Cylinder, Box, chamfer, and export_step.",
|
||||
"This is runtime documentation only; it does not execute or generate geometry.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
symbols: z.array(z.string()).min(1).max(32).describe("build123d top-level symbols used by the planned native source."),
|
||||
}),
|
||||
execute: async ({ symbols }) => {
|
||||
const result = await inspectBuild123dApi(symbols);
|
||||
for (const symbol of result.symbols) {
|
||||
if (symbol.ok) build123dApiSymbolsRead.add(symbol.symbol);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
inspect_uploaded_file: tool({
|
||||
description: [
|
||||
"Inspect a user-uploaded file before using it for CAD generation.",
|
||||
@@ -800,9 +1266,14 @@ export async function streamAgentResponse({
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
sourceUrl: cadGeneration.sourceUrl,
|
||||
parameters: cadGeneration.parameters,
|
||||
editableParameters: cadGeneration.editableParameters,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
featureTreePath: cadGeneration.featureTreePath,
|
||||
featureTreeUrl: cadGeneration.featureTreeUrl,
|
||||
parameterCatalogPath: cadGeneration.parameterCatalogPath,
|
||||
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
@@ -816,26 +1287,26 @@ export async function streamAgentResponse({
|
||||
}),
|
||||
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.",
|
||||
"Edit one listed editable CAD parameter on the current task without LLM participation.",
|
||||
"The requested value must be within the range shown in parameters.json.",
|
||||
"The server uses the recorded backend binding: native Python/source for generated models or SurfaceIR acceptance for uploaded STEP reconstructions.",
|
||||
].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."),
|
||||
parameter: z.string().describe("Editable parameter name from the current task."),
|
||||
value: z.number().describe("New value within the parameter's declared range."),
|
||||
}),
|
||||
execute: async ({ parameter, value }) => {
|
||||
if (!selectedTaskId) {
|
||||
throw new Error("No current DesignIR 3.0 task is selected.");
|
||||
throw new Error("No current CAD task is selected.");
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
step: "backend_parameter_edit",
|
||||
label: "修改后端绑定参数",
|
||||
status: "running",
|
||||
message: `${parameter} → ${value},正在执行独立参数验收。`,
|
||||
message: `${parameter} -> ${value},正在使用记录的无 LLM adapter 重新生成。`,
|
||||
});
|
||||
try {
|
||||
cadGeneration = await editDesignIR3Parameter({
|
||||
cadGeneration = await editDesignIRParameter({
|
||||
prompt: lastUserText(chatMessages),
|
||||
sourceTaskId: selectedTaskId,
|
||||
parameter,
|
||||
@@ -844,10 +1315,10 @@ export async function streamAgentResponse({
|
||||
} catch (error) {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
: "DesignIR 3.0 parameter edit failed.";
|
||||
: "CAD parameter edit failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
step: "backend_parameter_edit",
|
||||
label: "修改后端绑定参数",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
@@ -855,8 +1326,8 @@ export async function streamAgentResponse({
|
||||
throw error;
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "修改 DesignIR 3.0 参数",
|
||||
step: "backend_parameter_edit",
|
||||
label: "修改后端绑定参数",
|
||||
status: "success",
|
||||
message: cadGeneration.summary,
|
||||
});
|
||||
@@ -866,16 +1337,21 @@ export async function streamAgentResponse({
|
||||
status: "success",
|
||||
message: "参数修改后的模型已载入。",
|
||||
});
|
||||
completeAgentStream("DesignIR 3.0 参数修改和验收已完成。");
|
||||
completeAgentStream("CAD 参数修改和重新生成已完成。");
|
||||
streamWriter?.write({
|
||||
type: "data-cad-result",
|
||||
data: {
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
sourceUrl: cadGeneration.sourceUrl,
|
||||
parameters: cadGeneration.parameters,
|
||||
editableParameters: cadGeneration.editableParameters,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
featureTreePath: cadGeneration.featureTreePath,
|
||||
featureTreeUrl: cadGeneration.featureTreeUrl,
|
||||
parameterCatalogPath: cadGeneration.parameterCatalogPath,
|
||||
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
@@ -951,69 +1427,115 @@ export async function streamAgentResponse({
|
||||
}),
|
||||
generate_cad: tool({
|
||||
description: [
|
||||
"Generate a real CAD model from agent-authored semantic DesignIR 3.0.",
|
||||
"Execute backend-native Python source submitted by the agent to generate a real CAD model.",
|
||||
"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.",
|
||||
"The server does not infer geometry, choose templates, or generate fallback models.",
|
||||
"Do not call this for API discovery, topology diagnostics, radius sweeps, or smoke probes.",
|
||||
"stdout JSON is optional diagnostic output; generated files are the execution source of truth.",
|
||||
"If this returns ok=false and retryable=true, fix the nativeSource from the diagnostics and call generate_cad again immediately.",
|
||||
].join(" "),
|
||||
inputSchema: z.object({
|
||||
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 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."),
|
||||
routeToken: z.string().optional().describe("Opaque token returned by route_cad_request. Required for new_model and binds the request/backend."),
|
||||
sourceKind: z.enum(["build123d_python", "simplecadapi_python"]).describe("Native source kind returned by route_cad_request, or the existing source of truth for an edit."),
|
||||
generationMode: z.enum(["new_model", "structural_edit", "backend_conversion"]).default("new_model").describe("Whether this is a new model, a same-backend structural edit, or an explicit backend conversion."),
|
||||
nativeSource: z.string().describe("Raw backend-native Python source. No markdown fences. Must be the final CLI generator, not an API/topology/radius probe and not a gen_step-only return-object script. Must parse required CLI args, call export_step(..., args.step), and write STEP plus metadata. Editable parameter metadata must include Chinese user-facing names/display_name values."),
|
||||
summary: z.string().optional().describe("Concise user-facing summary of the requested CAD model."),
|
||||
targetName: z.string().optional().describe("Optional model/task artifact name stem, e.g. mounting_bracket."),
|
||||
assumptions: z.array(z.string()).default([]).describe("Assumptions made because the user did not specify every dimension."),
|
||||
}),
|
||||
execute: async ({
|
||||
routeToken,
|
||||
sourceKind,
|
||||
generationMode,
|
||||
nativeSource,
|
||||
summary,
|
||||
designirFilename,
|
||||
stepFilename,
|
||||
designir,
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
targetName,
|
||||
assumptions,
|
||||
}) => {
|
||||
const routeDecision = generationMode === "new_model"
|
||||
? routeDecisions.get(String(routeToken || ""))
|
||||
: undefined;
|
||||
if (generationMode === "new_model" && !routeDecision) {
|
||||
throw new Error("New CAD generation requires the routeToken returned by route_cad_request; do not rephrase or reroute the request in generate_cad.");
|
||||
}
|
||||
if (
|
||||
generationMode === "new_model"
|
||||
&& sourceKind === "simplecadapi_python"
|
||||
&& SIMPLECADAPI_REQUIRED_DOCS.some((document) => !simpleCadApiDocsRead.has(document))
|
||||
) {
|
||||
throw new Error(
|
||||
"SimpleCADAPI generation requires original docs: skill, api/README.md, and stdlib/README.md before native source is submitted.",
|
||||
);
|
||||
}
|
||||
if (generationMode === "new_model" && sourceKind === "build123d_python") {
|
||||
const apiSymbols = await inspectBuild123dApi(build123dCallCandidates(nativeSource));
|
||||
const missing = apiSymbols.symbols
|
||||
.filter((symbol) => symbol.ok && !build123dApiSymbolsRead.has(symbol.symbol))
|
||||
.map((symbol) => symbol.symbol);
|
||||
if (missing.length) {
|
||||
for (const symbol of missing) {
|
||||
build123dApiSymbolsRead.add(symbol);
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "build123d_api_preflight",
|
||||
label: "补全 build123d API",
|
||||
status: "success",
|
||||
message: `服务端已自动补读缺失 API:${missing.join(", ")}。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "generate_designir",
|
||||
label: "生成 DesignIR",
|
||||
status: "success",
|
||||
message: designirFilename,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "隔离重建 STEP",
|
||||
step: "backend_generate",
|
||||
label: "后端原生生成",
|
||||
status: "running",
|
||||
message: "正在进行防作弊检查、CAD Router 选路和无教师重建。",
|
||||
message: "正在执行 agent 提交的原生 source,server 不生成模板或替代模型。",
|
||||
});
|
||||
try {
|
||||
cadGeneration = await executeDesignIR({
|
||||
prompt: lastUserText(chatMessages),
|
||||
cadGeneration = await executeBackendNativeGeneration({
|
||||
request: routeDecision?.request || lastUserText(chatMessages),
|
||||
sourceKind,
|
||||
generationMode,
|
||||
routeDecision,
|
||||
nativeSource,
|
||||
summary,
|
||||
designirFilename,
|
||||
stepFilename,
|
||||
designir,
|
||||
teacherTaskId,
|
||||
teacherPath,
|
||||
targetName,
|
||||
assumptions,
|
||||
selectedTaskId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "CAD generation failed.";
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "隔离重建 STEP",
|
||||
step: "backend_generate",
|
||||
label: "后端原生生成",
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
failAgentStream(message);
|
||||
throw error;
|
||||
return {
|
||||
ok: false,
|
||||
retryable: true,
|
||||
stage: "backend_generate",
|
||||
error: message,
|
||||
instruction: "Revise nativeSource using this diagnostic and call generate_cad again in the same turn. Do not ask the user for permission to retry.",
|
||||
};
|
||||
}
|
||||
writeCadProgress({
|
||||
step: "execute_cad",
|
||||
label: "隔离重建 STEP",
|
||||
step: "backend_generate",
|
||||
label: "后端原生生成",
|
||||
status: "success",
|
||||
message: cadGeneration.artifactPath,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "normalize_designir",
|
||||
label: "归一化 DesignIR",
|
||||
status: "success",
|
||||
message: cadGeneration.sourcePath,
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "publish_artifacts",
|
||||
label: "发布产物",
|
||||
status: "success",
|
||||
message: "特征树、参数列表和预览已准备完成。",
|
||||
});
|
||||
writeCadProgress({
|
||||
step: "update_preview",
|
||||
label: "更新右侧预览",
|
||||
@@ -1027,9 +1549,14 @@ export async function streamAgentResponse({
|
||||
taskId: cadGeneration.taskId,
|
||||
sourcePath: cadGeneration.sourcePath,
|
||||
sourceUrl: cadGeneration.sourceUrl,
|
||||
parameters: cadGeneration.parameters,
|
||||
editableParameters: cadGeneration.editableParameters,
|
||||
artifactPath: cadGeneration.artifactPath,
|
||||
artifactUrl: cadGeneration.artifactUrl,
|
||||
featureTreePath: cadGeneration.featureTreePath,
|
||||
featureTreeUrl: cadGeneration.featureTreeUrl,
|
||||
parameterCatalogPath: cadGeneration.parameterCatalogPath,
|
||||
parameterCatalogUrl: cadGeneration.parameterCatalogUrl,
|
||||
previewPath: cadGeneration.previewPath,
|
||||
previewUrl: cadGeneration.previewUrl,
|
||||
viewerAssetPath: cadGeneration.viewerAssetPath,
|
||||
@@ -1075,10 +1602,11 @@ export async function streamAgentResponse({
|
||||
model: languageModel,
|
||||
system: buildSystemPrompt(viewerContext, uploadedAttachments, taskContext),
|
||||
tools: cadTools,
|
||||
stopWhen: isStepCount(3),
|
||||
stopWhen: isStepCount(16),
|
||||
messages: modelMessages,
|
||||
});
|
||||
writer.merge(result.toUIMessageStream({
|
||||
sendReasoning: false,
|
||||
onEnd: () => {
|
||||
completeAgentStream(cadGeneration ? "模型和 CAD 工具执行已完成。" : "模型回复已完成。");
|
||||
},
|
||||
@@ -1095,6 +1623,14 @@ export async function streamAgentResponse({
|
||||
}
|
||||
},
|
||||
onError: formatCurrentModelError,
|
||||
onEnd: async ({ responseMessage, isAborted }) => {
|
||||
if (isAborted) return;
|
||||
try {
|
||||
await persistAssistantMessage(responseMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to persist CAD conversation response", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
return createUIMessageStreamResponse({
|
||||
stream,
|
||||
|
||||
Reference in New Issue
Block a user