feat(llm-client): add post-extraction validation with one-shot retry

After LLM returns scene IR, validate that critical fields (contentType,
responsePath, workflowArchetype) are present. If missing, send one
follow-up prompt to fill gaps. Merges repaired fields without overwriting
valid data from the first extraction.

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
木炎
2026-04-17 18:45:15 +08:00
parent 0fcdfb1787
commit 56ae03f3f9

View File

@@ -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;
}