diff --git a/frontend/scene-generator/llm-client.js b/frontend/scene-generator/llm-client.js index 9018001..54fda63 100644 --- a/frontend/scene-generator/llm-client.js +++ b/frontend/scene-generator/llm-client.js @@ -747,6 +747,52 @@ async function analyzeScene(sourceDir, dirContents, config) { }; } +function validateExtractedSceneInfo(sceneIr) { + const issues = []; + + // Check: at least one apiEndpoint has contentType + const endpointsWithCt = (sceneIr.apiEndpoints || []).filter( + ep => ep && ep.contentType + ); + if ((sceneIr.apiEndpoints || []).length > 0 && endpointsWithCt.length === 0) { + issues.push("missing_contentType_on_endpoints"); + } + + // Check: at least one mode has responsePath (if modes exist) + if ((sceneIr.modes || []).length > 0) { + const modesWithPath = sceneIr.modes.filter(m => m.responsePath !== undefined && m.responsePath !== null); + if (modesWithPath.length === 0) { + issues.push("missing_responsePath_on_modes"); + } + } + + // Check: workflowArchetype is set + if (!sceneIr.workflowArchetype) { + issues.push("missing_workflowArchetype"); + } + + return issues; +} + +function mergeSceneIrFields(repaired, original) { + const merged = {}; + // Only fill fields that were empty/missing in original + const criticalFields = ['workflowArchetype', 'defaultMode', 'modeSwitchField']; + for (const field of criticalFields) { + if (!original[field] && repaired[field]) { + merged[field] = repaired[field]; + } + } + // For arrays, merge if original is empty + if ((!original.apiEndpoints || original.apiEndpoints.length === 0) && repaired.apiEndpoints) { + merged.apiEndpoints = repaired.apiEndpoints; + } + if ((!original.modes || original.modes.length === 0) && repaired.modes) { + merged.modes = repaired.modes; + } + return merged; +} + async function analyzeSceneDeep(sourceDir, dirContents, config) { const content = await requestChatCompletionWithRetry( [ @@ -807,6 +853,30 @@ async function analyzeSceneDeep(sourceDir, dirContents, config) { } } + // POST-EXTRACTION VALIDATION: one-shot retry for missing fields + const issues = validateExtractedSceneInfo(normalized); + if (issues.length > 0) { + const followUpPrompt = `The previous extraction has these issues:\n${issues.join('\n')}\nPlease re-analyze the source snippets and fill in the missing fields. Use defaults if truly unavailable.`; + + try { + const followUpContent = await requestChatCompletionWithRetry( + [ + { role: "system", content: DEEP_SYSTEM_PROMPT }, + { role: "user", content: followUpPrompt }, + ], + { ...config, maxTokens: 2400, timeoutMs: DEEP_REQUEST_TIMEOUT_MS, retryAttempts: 1 } + ); + + const repaired = normalizeSceneIr(await extractJsonFromResponseWithRepair(followUpContent, config)); + // Merge repaired fields into normalized (only fill empty fields) + const mergeResult = mergeSceneIrFields(repaired, normalized); + Object.assign(normalized, mergeResult); + } catch (error) { + // Silently continue with original extraction if retry fails + // The auto-wrap logic (above) will handle empty modes + } + } + return normalized; }