Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions lib/orchestration/workflow-advisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
WorkflowTool,
WorkflowCategory,
} from "./workflow-types"
import { TOOL_REGISTRY } from "./tool-registry"

// ---------------------------------------------------------------------------
// Tool recommendation knowledge base
Expand Down Expand Up @@ -80,10 +81,10 @@ const CATEGORY_DEFAULTS: Record<WorkflowCategory, ToolRecommendation> = {
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",
Expand Down Expand Up @@ -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:
Expand All @@ -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:
{
Expand All @@ -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"]
Expand Down Expand Up @@ -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,
})
}

Expand All @@ -289,6 +323,7 @@ function generateFallbackWorkflow(
alternativeTools: ["manual"],
estimatedDuration: CATEGORY_DEFAULTS.topology.estimatedDuration,
requiresPreviousStep: true,
blenderAction: CATEGORY_DEFAULTS.topology.blenderAction,
},
{
title: "UV Unwrap",
Expand All @@ -299,6 +334,7 @@ function generateFallbackWorkflow(
alternativeTools: ["manual"],
estimatedDuration: CATEGORY_DEFAULTS.uv.estimatedDuration,
requiresPreviousStep: true,
blenderAction: CATEGORY_DEFAULTS.uv.blenderAction,
},
{
title: "Apply Textures",
Expand All @@ -310,6 +346,7 @@ function generateFallbackWorkflow(
estimatedDuration: CATEGORY_DEFAULTS.texturing.estimatedDuration,
requiresPreviousStep: true,
neuralProvider: "hunyuan-paint",
blenderAction: undefined,
},
{
title: "Export",
Expand All @@ -320,6 +357,7 @@ function generateFallbackWorkflow(
alternativeTools: ["manual"],
estimatedDuration: CATEGORY_DEFAULTS.export.estimatedDuration,
requiresPreviousStep: true,
blenderAction: CATEGORY_DEFAULTS.export.blenderAction,
}
)
}
Expand Down Expand Up @@ -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),
}
})

Expand Down
4 changes: 4 additions & 0 deletions scripts/test/test-workflow-direct-tool-recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
Loading