diff --git a/lib/orchestration/workflow-advisor.ts b/lib/orchestration/workflow-advisor.ts index 8790947..f28d860 100644 --- a/lib/orchestration/workflow-advisor.ts +++ b/lib/orchestration/workflow-advisor.ts @@ -18,6 +18,7 @@ import type { WorkflowTool, WorkflowCategory, } from "./workflow-types" +import { TOOL_REGISTRY } from "./tool-registry" // --------------------------------------------------------------------------- // Tool recommendation knowledge base @@ -80,10 +81,10 @@ const CATEGORY_DEFAULTS: Record = { animation: { tool: "blender_agent", reasoning: - "Procedural animation (orbit, walk cycles, keyframe insertion) is fast and precise via Python. Manual keyframing for custom performances.", + "Animation work starts with structured animation-data inspection before deciding whether a focused fallback is needed for unsupported keyframe creation.", alternatives: ["manual"], estimatedDuration: "~15-30s", - blenderAction: "execute_code", + blenderAction: "inspect_animation_data", }, lighting: { tool: "blender_agent", @@ -154,10 +155,28 @@ interface LLMWorkflowResponse { requiresPreviousStep: boolean tips?: string neuralProvider?: string + blenderAction?: string }> overallTips: string[] } +const VALID_BLENDER_ACTIONS = new Set(TOOL_REGISTRY.map((tool) => tool.name)) + +function normalizeBlenderAction(category: WorkflowCategory, recommendedTool: WorkflowTool, action?: string): string | undefined { + if (recommendedTool !== "blender_agent") return undefined + + const categoryDefault = CATEGORY_DEFAULTS[category] ?? CATEGORY_DEFAULTS.other + const candidate = action?.trim() + if (candidate && VALID_BLENDER_ACTIONS.has(candidate)) { + if (candidate === "execute_code" && categoryDefault.blenderAction && categoryDefault.blenderAction !== "execute_code") { + return categoryDefault.blenderAction + } + return candidate + } + + return categoryDefault.blenderAction +} + const WORKFLOW_SYSTEM_PROMPT = `You are a 3D workflow advisor for ModelForge. Given a user's creation request, generate a step-by-step workflow where each step recommends the BEST tool. Available tools: @@ -177,6 +196,19 @@ Rules: 7. Set requiresPreviousStep=true when a step depends on the output of the prior step 8. Include practical tips for each step 9. neuralProvider is required when recommendedTool is "neural" +10. blenderAction is required when recommendedTool is "blender_agent"; choose the most specific direct tool when one exists. + +Preferred blenderAction examples by category: +- topology: inspect_retopology_readiness +- uv: prepare_uv_layout +- rigging: inspect_rigging_data or inspect_weight_paint_readiness +- animation: inspect_animation_data before any unsupported keyframe fallback +- lighting: setup_studio_scene, validate_studio_scene, add_light, or set_light_properties +- composition: organize_collection_hierarchy, set_object_transform, focus_viewport_on_objects, or get_viewport_screenshot +- materials/texturing: create_material_preset, inspect_material_node_graph, create_material, assign_material, list_materials, or set_texture +- export: prepare_uv_layout, validate_export_readiness, or export_object +- rendering: render_thumbnail_to_path, set_render_settings, validate_studio_scene, or render_image +- effects: execute_code only when no direct tool can express the requested effect Respond with ONLY a JSON object matching this schema: { @@ -192,7 +224,8 @@ Respond with ONLY a JSON object matching this schema: "estimatedDuration": "~Xs or ~Xmin", "requiresPreviousStep": true|false, "tips": "optional string", - "neuralProvider": "hunyuan-shape|hunyuan-paint|trellis|yvo3d — required when tool is neural" + "neuralProvider": "hunyuan-shape|hunyuan-paint|trellis|yvo3d — required when tool is neural", + "blenderAction": "direct Blender tool name — required when tool is blender_agent" } ], "overallTips": ["string"] @@ -275,6 +308,7 @@ function generateFallbackWorkflow( requiresPreviousStep: false, tips: "You can also upload a reference image for image-to-3D generation.", neuralProvider: "hunyuan-shape", + blenderAction: undefined, }) } @@ -289,6 +323,7 @@ function generateFallbackWorkflow( alternativeTools: ["manual"], estimatedDuration: CATEGORY_DEFAULTS.topology.estimatedDuration, requiresPreviousStep: true, + blenderAction: CATEGORY_DEFAULTS.topology.blenderAction, }, { title: "UV Unwrap", @@ -299,6 +334,7 @@ function generateFallbackWorkflow( alternativeTools: ["manual"], estimatedDuration: CATEGORY_DEFAULTS.uv.estimatedDuration, requiresPreviousStep: true, + blenderAction: CATEGORY_DEFAULTS.uv.blenderAction, }, { title: "Apply Textures", @@ -310,6 +346,7 @@ function generateFallbackWorkflow( estimatedDuration: CATEGORY_DEFAULTS.texturing.estimatedDuration, requiresPreviousStep: true, neuralProvider: "hunyuan-paint", + blenderAction: undefined, }, { title: "Export", @@ -320,6 +357,7 @@ function generateFallbackWorkflow( alternativeTools: ["manual"], estimatedDuration: CATEGORY_DEFAULTS.export.estimatedDuration, requiresPreviousStep: true, + blenderAction: CATEGORY_DEFAULTS.export.blenderAction, } ) } @@ -392,7 +430,7 @@ export async function generateWorkflowProposal( tips: step.tips, status: "pending", neuralProvider: step.neuralProvider ?? categoryDefault.neuralProvider, - blenderAction: categoryDefault.blenderAction, + blenderAction: normalizeBlenderAction(step.category, step.recommendedTool, step.blenderAction), } }) diff --git a/scripts/test/test-workflow-direct-tool-recommendations.ts b/scripts/test/test-workflow-direct-tool-recommendations.ts index 9e91d34..79b5eda 100644 --- a/scripts/test/test-workflow-direct-tool-recommendations.ts +++ b/scripts/test/test-workflow-direct-tool-recommendations.ts @@ -29,6 +29,10 @@ async function main() { assert.equal(exportStep?.blenderAction, "validate_export_readiness") const source = readFileSync("lib/orchestration/workflow-advisor.ts", "utf8") + assert.match(source, /blenderAction is required when recommendedTool is "blender_agent"/) + assert.match(source, /VALID_BLENDER_ACTIONS/) + assert.match(source, /normalizeBlenderAction/) + assert.match(source, /animation:[\s\S]*blenderAction: "inspect_animation_data"/) assert.match(source, /setup_studio_scene/) assert.match(source, /render_thumbnail_to_path/) assert.match(source, /inspect_rigging_data/)