Skip to content

Commit 0fb1da3

Browse files
committed
Fix JSON parsing error in RPG orchestrator
- Add robust JSON extraction to handle malformed AI responses - Implement retry logic with exponential backoff for failed API calls - Improve prompts to ensure JSON-only responses from AI - Add detailed error analysis for debugging JSON parsing issues - Update .gitignore to exclude .grok directory containing user data Fixes the 'Unterminated string in JSON' error that was causing code generation to fail.
1 parent 772d196 commit 0fb1da3

File tree

2 files changed

+130
-41
lines changed

2 files changed

+130
-41
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ yarn-error.log*
99
# Environment variables and API keys
1010
.env
1111
.grok_code_key
12+
.grok/
1213
*.key
1314

1415
# OS generated files

lib/rpg/orchestrator.js

Lines changed: 129 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,21 @@ export class RPGOrchestrator {
114114
*/
115115
async generateRPGPlan(prompt, context = {}) {
116116
const existingFiles = context.existingFiles || 'none';
117+
const maxRetries = 3;
117118

118-
const planningPrompt = `
119-
You are an expert software architect using the RPG (Recursive Planning Graph) methodology for systematic code generation.
119+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
120+
try {
121+
logger.info(`RPG planning attempt ${attempt}/${maxRetries}`);
122+
123+
const planningPrompt = `You are an expert software architect using the RPG (Recursive Planning Graph) methodology for systematic code generation.
124+
125+
CRITICAL: Respond with ONLY a valid JSON object. Do NOT include any explanations, comments, or text before or after the JSON. The response must be parseable by JSON.parse().
120126
121127
For the user request: "${prompt}"
122128
123129
${existingFiles !== 'none' ? `Existing project files: ${JSON.stringify(existingFiles, null, 2)}` : ''}
124130
125-
Create a comprehensive RPG plan following this structure:
131+
Create a comprehensive RPG plan following this EXACT structure:
126132
127133
{
128134
"features": ["Feature 1", "Feature 2", "Feature 3"],
@@ -159,51 +165,86 @@ Guidelines:
159165
- Be specific about technologies and architecture patterns
160166
- Consider the existing codebase when planning modifications
161167
162-
Respond with valid JSON only.`;
168+
IMPORTANT: Your response must be ONLY the JSON object, nothing else. Start with { and end with }.${attempt > 1 ? `\n\nPrevious attempts failed due to JSON parsing errors. Please ensure your response is valid JSON.` : ''}`;
163169

164-
logger.debug('Making RPG planning request', {
165-
promptLength: planningPrompt.length,
166-
model: this.model,
167-
});
170+
logger.debug('Making RPG planning request', {
171+
promptLength: planningPrompt.length,
172+
model: this.model,
173+
attempt,
174+
});
168175

169-
const response = await this.client.chat.completions.create({
170-
model: this.model,
171-
messages: [
172-
{
173-
role: 'system',
174-
content:
175-
'You are a precise RPG planner. Respond with valid JSON only. No explanations or markdown.',
176-
},
177-
{ role: 'user', content: planningPrompt },
178-
],
179-
max_tokens: 4096,
180-
temperature: 0.3,
181-
});
176+
const response = await this.client.chat.completions.create({
177+
model: this.model,
178+
messages: [
179+
{
180+
role: 'system',
181+
content:
182+
'You are a precise RPG planner. You must respond with ONLY a valid JSON object. Do NOT include any explanations, comments, markdown, or additional text. The response must start with { and end with } and be parseable by JSON.parse(). Failure to follow this instruction will result in parsing errors.',
183+
},
184+
{ role: 'user', content: planningPrompt },
185+
],
186+
max_tokens: 4096,
187+
temperature: Math.max(0.1, 0.3 - (attempt - 1) * 0.1), // Decrease temperature on retries
188+
});
182189

183-
const rawResponse = response.choices[0].message.content.trim();
184-
logger.debug('Received RPG planning response', {
185-
responseLength: rawResponse.length,
186-
});
190+
const rawResponse = response.choices[0].message.content.trim();
191+
logger.debug('Received RPG planning response', {
192+
responseLength: rawResponse.length,
193+
attempt,
194+
});
187195

188-
try {
189-
const plan = JSON.parse(rawResponse);
196+
// Extract JSON from response - handle cases where AI returns extra text
197+
let jsonString = rawResponse;
190198

191-
// Validate plan structure
192-
this.validatePlanStructure(plan);
199+
// Try to find JSON object in the response
200+
const jsonStart = rawResponse.indexOf('{');
201+
const jsonEnd = rawResponse.lastIndexOf('}');
193202

194-
logger.info('Successfully parsed RPG plan', {
195-
features: plan.features?.length || 0,
196-
files: Object.keys(plan.files || {}).length,
197-
flows: plan.flows?.length || 0,
198-
deps: plan.deps?.length || 0,
199-
});
203+
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
204+
jsonString = rawResponse.substring(jsonStart, jsonEnd + 1);
205+
}
200206

201-
return { plan, rawResponse };
202-
} catch (parseError) {
203-
logger.error('Failed to parse RPG planning response', parseError, {
204-
rawResponse: rawResponse.substring(0, 500),
205-
});
206-
throw new Error(`Invalid RPG plan format: ${parseError.message}`);
207+
// Clean up common issues
208+
jsonString = jsonString
209+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Remove control characters
210+
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
211+
.trim();
212+
213+
const plan = JSON.parse(jsonString);
214+
215+
// Validate plan structure
216+
this.validatePlanStructure(plan);
217+
218+
logger.info('Successfully parsed RPG plan', {
219+
features: plan.features?.length || 0,
220+
files: Object.keys(plan.files || {}).length,
221+
flows: plan.flows?.length || 0,
222+
deps: plan.deps?.length || 0,
223+
attempt,
224+
});
225+
226+
return { plan, rawResponse };
227+
228+
} catch (parseError) {
229+
logger.error(`RPG planning attempt ${attempt} failed`, parseError, {
230+
errorMessage: parseError.message,
231+
attempt,
232+
});
233+
234+
// If this is the last attempt, provide detailed error information
235+
if (attempt === maxRetries) {
236+
const errorDetails = this.analyzeJsonError(
237+
parseError.response || 'No response available',
238+
parseError
239+
);
240+
throw new Error(`Invalid RPG plan format after ${maxRetries} attempts: ${parseError.message}. ${errorDetails}`);
241+
}
242+
243+
// Wait before retrying (exponential backoff)
244+
const waitTime = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
245+
logger.info(`Retrying RPG planning in ${waitTime}ms...`);
246+
await new Promise(resolve => setTimeout(resolve, waitTime));
247+
}
207248
}
208249
}
209250

@@ -480,6 +521,53 @@ Respond with valid JSON only.`;
480521
}
481522
}
482523

524+
/**
525+
* Analyze JSON parsing errors to provide helpful debugging information
526+
* @param {string} rawResponse - The raw AI response
527+
* @param {Error} parseError - The JSON parsing error
528+
* @returns {string} Error analysis details
529+
*/
530+
analyzeJsonError(rawResponse, parseError) {
531+
const errorMessage = parseError.message;
532+
let details = '';
533+
534+
// Check for common JSON issues
535+
if (errorMessage.includes('Unterminated string')) {
536+
details = 'The JSON contains an unterminated string literal. Check for missing quotes around string values.';
537+
} else if (errorMessage.includes('Unexpected token')) {
538+
details = 'The JSON contains unexpected characters. The response may include extra text before or after the JSON.';
539+
} else if (errorMessage.includes('Unexpected end of JSON input')) {
540+
details = 'The JSON appears to be truncated. The AI response may have been cut off.';
541+
} else if (errorMessage.includes('Expected property name')) {
542+
details = 'Missing quotes around property names in the JSON object.';
543+
}
544+
545+
// Check if response contains JSON markers
546+
const hasJsonStart = rawResponse.includes('{');
547+
const hasJsonEnd = rawResponse.includes('}');
548+
549+
if (!hasJsonStart || !hasJsonEnd) {
550+
details += ' No JSON object markers found in response.';
551+
} else {
552+
const jsonStart = rawResponse.indexOf('{');
553+
const jsonEnd = rawResponse.lastIndexOf('}');
554+
const jsonLength = jsonEnd - jsonStart + 1;
555+
details += ` Found JSON-like content (${jsonLength} chars) in response.`;
556+
}
557+
558+
// Show a snippet around the error position
559+
const positionMatch = errorMessage.match(/position (\d+)/);
560+
if (positionMatch) {
561+
const position = parseInt(positionMatch[1]);
562+
const start = Math.max(0, position - 50);
563+
const end = Math.min(rawResponse.length, position + 50);
564+
const snippet = rawResponse.substring(start, end);
565+
details += ` Snippet around error: "...${snippet}..."`;
566+
}
567+
568+
return details;
569+
}
570+
483571
// Helper methods
484572

485573
validatePlanStructure(plan) {

0 commit comments

Comments
 (0)