diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 908ea724..22855a89 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -289,11 +289,10 @@ ${JSON.stringify(suggestionsSchema, null, 2)}`; })), }); } else { - // Fallback: try to parse from text (for backwards compatibility) + // Fallback: try to parse from text using multiple strategies logger.warn('No structured output received, attempting to parse from text'); - const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[0]); + const parsed = extractSuggestionsJson(responseText); + if (parsed && parsed.suggestions) { events.emit('suggestions:event', { type: 'suggestions_complete', suggestions: parsed.suggestions.map((s: Record, i: number) => ({ @@ -323,3 +322,99 @@ ${JSON.stringify(suggestionsSchema, null, 2)}`; }); } } + +/** + * Extract suggestions JSON from response text using multiple strategies. + * Handles various formats: markdown code blocks, raw JSON, etc. + */ +function extractSuggestionsJson( + responseText: string +): { suggestions: Array> } | null { + const strategies = [ + // Strategy 1: JSON in ```json code block + () => { + const match = responseText.match(/```json\s*([\s\S]*?)```/); + if (match) { + return JSON.parse(match[1].trim()); + } + return null; + }, + // Strategy 2: JSON in ``` code block (no language specified) + () => { + const match = responseText.match(/```\s*([\s\S]*?)```/); + if (match) { + const content = match[1].trim(); + if (content.startsWith('{') && content.includes('"suggestions"')) { + return JSON.parse(content); + } + } + return null; + }, + // Strategy 3: Find JSON object containing "suggestions" array + () => { + // Find the start of the JSON object + const startIdx = responseText.indexOf('{"suggestions"'); + if (startIdx === -1) return null; + + // Find matching closing brace by counting brackets + let depth = 0; + let endIdx = -1; + for (let i = startIdx; i < responseText.length; i++) { + if (responseText[i] === '{') depth++; + if (responseText[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i + 1; + break; + } + } + } + + if (endIdx > startIdx) { + return JSON.parse(responseText.slice(startIdx, endIdx)); + } + return null; + }, + // Strategy 4: Find any JSON object with suggestions + () => { + const startIdx = responseText.indexOf('{'); + if (startIdx === -1) return null; + + // Find matching closing brace + let depth = 0; + let endIdx = -1; + for (let i = startIdx; i < responseText.length; i++) { + if (responseText[i] === '{') depth++; + if (responseText[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i + 1; + break; + } + } + } + + if (endIdx > startIdx) { + const parsed = JSON.parse(responseText.slice(startIdx, endIdx)); + if (parsed.suggestions && Array.isArray(parsed.suggestions)) { + return parsed; + } + } + return null; + }, + ]; + + for (const strategy of strategies) { + try { + const result = strategy(); + if (result && result.suggestions && Array.isArray(result.suggestions)) { + logger.debug('Successfully extracted suggestions JSON'); + return result; + } + } catch { + // Strategy failed, try next + } + } + + return null; +}