@in-the-loop-labs/pair-review 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/src/ai/claude-provider.js +12 -7
- package/src/ai/codex-provider.js +9 -7
- package/src/ai/cursor-agent-provider.js +9 -6
- package/src/ai/gemini-provider.js +9 -7
- package/src/ai/opencode-provider.js +9 -7
- package/src/utils/json-extractor.js +129 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -396,24 +396,28 @@ class ClaudeProvider extends AIProvider {
|
|
|
396
396
|
} else {
|
|
397
397
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
398
398
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
399
|
-
|
|
399
|
+
// Pass extracted text content to LLM fallback (not raw JSONL stdout).
|
|
400
|
+
// The text content is the actual LLM response text extracted from JSONL
|
|
401
|
+
// events and is much smaller and more relevant than the full JSONL stream.
|
|
402
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
403
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
400
404
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
401
405
|
|
|
402
406
|
// Use async IIFE to handle the async LLM extraction
|
|
403
407
|
(async () => {
|
|
404
408
|
try {
|
|
405
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
409
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
406
410
|
if (llmExtracted.success) {
|
|
407
411
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
408
412
|
settle(resolve, llmExtracted.data);
|
|
409
413
|
} else {
|
|
410
414
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
411
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
412
|
-
settle(resolve, { raw:
|
|
415
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
416
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
413
417
|
}
|
|
414
418
|
} catch (llmError) {
|
|
415
419
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
416
|
-
settle(resolve, { raw:
|
|
420
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
417
421
|
}
|
|
418
422
|
})();
|
|
419
423
|
}
|
|
@@ -731,9 +735,10 @@ class ClaudeProvider extends AIProvider {
|
|
|
731
735
|
return extracted;
|
|
732
736
|
}
|
|
733
737
|
|
|
734
|
-
// If no JSON found, return the
|
|
738
|
+
// If no JSON found, return with textContent so the caller can
|
|
739
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
735
740
|
logger.warn(`${levelPrefix} Text content is not JSON, treating as raw text`);
|
|
736
|
-
return { success: false, error: 'Text content is not valid JSON' };
|
|
741
|
+
return { success: false, error: 'Text content is not valid JSON', textContent };
|
|
737
742
|
}
|
|
738
743
|
|
|
739
744
|
// No text content found - don't fall back to raw stdout extraction
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -271,24 +271,25 @@ class CodexProvider extends AIProvider {
|
|
|
271
271
|
} else {
|
|
272
272
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
273
273
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
274
|
-
|
|
274
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
275
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
275
276
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
276
277
|
|
|
277
278
|
// Use async IIFE to handle the async LLM extraction
|
|
278
279
|
(async () => {
|
|
279
280
|
try {
|
|
280
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
281
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
281
282
|
if (llmExtracted.success) {
|
|
282
283
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
283
284
|
settle(resolve, llmExtracted.data);
|
|
284
285
|
} else {
|
|
285
286
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
286
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
287
|
-
settle(resolve, { raw:
|
|
287
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
288
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
288
289
|
}
|
|
289
290
|
} catch (llmError) {
|
|
290
291
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
291
|
-
settle(resolve, { raw:
|
|
292
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
292
293
|
}
|
|
293
294
|
})();
|
|
294
295
|
}
|
|
@@ -373,9 +374,10 @@ class CodexProvider extends AIProvider {
|
|
|
373
374
|
return extracted;
|
|
374
375
|
}
|
|
375
376
|
|
|
376
|
-
// If no JSON found, return the
|
|
377
|
+
// If no JSON found, return with textContent so the caller can
|
|
378
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
377
379
|
logger.warn(`${levelPrefix} Agent message is not JSON, treating as raw text`);
|
|
378
|
-
return { success: false, error: 'Agent message is not valid JSON' };
|
|
380
|
+
return { success: false, error: 'Agent message is not valid JSON', textContent: agentMessageText };
|
|
379
381
|
}
|
|
380
382
|
|
|
381
383
|
// No agent message found, try extracting JSON directly from stdout
|
|
@@ -314,7 +314,8 @@ class CursorAgentProvider extends AIProvider {
|
|
|
314
314
|
} else {
|
|
315
315
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
316
316
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
317
|
-
|
|
317
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
318
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
318
319
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
319
320
|
|
|
320
321
|
// Use async IIFE to handle the async LLM extraction
|
|
@@ -324,18 +325,18 @@ class CursorAgentProvider extends AIProvider {
|
|
|
324
325
|
// orphan processes if timeout fired between close-handler entry
|
|
325
326
|
// and reaching this point.
|
|
326
327
|
if (settled) return;
|
|
327
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
328
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
328
329
|
if (llmExtracted.success) {
|
|
329
330
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
330
331
|
settle(resolve, llmExtracted.data);
|
|
331
332
|
} else {
|
|
332
333
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
333
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
334
|
-
settle(resolve, { raw:
|
|
334
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
335
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
335
336
|
}
|
|
336
337
|
} catch (llmError) {
|
|
337
338
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
338
|
-
settle(resolve, { raw:
|
|
339
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
339
340
|
}
|
|
340
341
|
})();
|
|
341
342
|
}
|
|
@@ -466,7 +467,9 @@ class CursorAgentProvider extends AIProvider {
|
|
|
466
467
|
return extracted;
|
|
467
468
|
}
|
|
468
469
|
|
|
469
|
-
|
|
470
|
+
// Include textContent so the caller can pass it to LLM extraction fallback
|
|
471
|
+
const textContent = assistantText || resultText || null;
|
|
472
|
+
return { success: false, error: 'No valid JSON found in assistant or result text', textContent };
|
|
470
473
|
|
|
471
474
|
} catch (parseError) {
|
|
472
475
|
// stdout might not be valid JSONL at all, try extracting JSON from it
|
|
@@ -320,24 +320,25 @@ class GeminiProvider extends AIProvider {
|
|
|
320
320
|
} else {
|
|
321
321
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
322
322
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
323
|
-
|
|
323
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
324
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
324
325
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
325
326
|
|
|
326
327
|
// Use async IIFE to handle the async LLM extraction
|
|
327
328
|
(async () => {
|
|
328
329
|
try {
|
|
329
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
330
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
330
331
|
if (llmExtracted.success) {
|
|
331
332
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
332
333
|
settle(resolve, llmExtracted.data);
|
|
333
334
|
} else {
|
|
334
335
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
335
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
336
|
-
settle(resolve, { raw:
|
|
336
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
337
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
337
338
|
}
|
|
338
339
|
} catch (llmError) {
|
|
339
340
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
340
|
-
settle(resolve, { raw:
|
|
341
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
341
342
|
}
|
|
342
343
|
})();
|
|
343
344
|
}
|
|
@@ -424,9 +425,10 @@ class GeminiProvider extends AIProvider {
|
|
|
424
425
|
return extracted;
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
// If no JSON found, return the
|
|
428
|
+
// If no JSON found, return with textContent so the caller can
|
|
429
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
428
430
|
logger.warn(`${levelPrefix} Assistant message is not JSON, treating as raw text`);
|
|
429
|
-
return { success: false, error: 'Assistant message is not valid JSON' };
|
|
431
|
+
return { success: false, error: 'Assistant message is not valid JSON', textContent: assistantText };
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
// No assistant message found, try extracting JSON directly from stdout
|
|
@@ -255,24 +255,25 @@ class OpenCodeProvider extends AIProvider {
|
|
|
255
255
|
} else {
|
|
256
256
|
// Regex extraction failed, try LLM-based extraction as fallback
|
|
257
257
|
logger.warn(`${levelPrefix} Regex extraction failed: ${parsed.error}`);
|
|
258
|
-
|
|
258
|
+
const llmFallbackInput = parsed.textContent || stdout;
|
|
259
|
+
logger.info(`${levelPrefix} LLM fallback input length: ${llmFallbackInput.length} characters (${parsed.textContent ? 'text content' : 'raw stdout'})`);
|
|
259
260
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction fallback...`);
|
|
260
261
|
|
|
261
262
|
// Use async IIFE to handle the async LLM extraction
|
|
262
263
|
(async () => {
|
|
263
264
|
try {
|
|
264
|
-
const llmExtracted = await this.extractJSONWithLLM(
|
|
265
|
+
const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
|
|
265
266
|
if (llmExtracted.success) {
|
|
266
267
|
logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
|
|
267
268
|
settle(resolve, llmExtracted.data);
|
|
268
269
|
} else {
|
|
269
270
|
logger.warn(`${levelPrefix} LLM extraction fallback also failed: ${llmExtracted.error}`);
|
|
270
|
-
logger.info(`${levelPrefix} Raw response preview: ${
|
|
271
|
-
settle(resolve, { raw:
|
|
271
|
+
logger.info(`${levelPrefix} Raw response preview: ${llmFallbackInput.substring(0, 500)}...`);
|
|
272
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
272
273
|
}
|
|
273
274
|
} catch (llmError) {
|
|
274
275
|
logger.warn(`${levelPrefix} LLM extraction fallback error: ${llmError.message}`);
|
|
275
|
-
settle(resolve, { raw:
|
|
276
|
+
settle(resolve, { raw: llmFallbackInput, parsed: false });
|
|
276
277
|
}
|
|
277
278
|
})();
|
|
278
279
|
}
|
|
@@ -495,9 +496,10 @@ class OpenCodeProvider extends AIProvider {
|
|
|
495
496
|
return extracted;
|
|
496
497
|
}
|
|
497
498
|
|
|
498
|
-
// If no JSON found, return the
|
|
499
|
+
// If no JSON found, return with textContent so the caller can
|
|
500
|
+
// pass it (not raw JSONL stdout) to the LLM extraction fallback
|
|
499
501
|
logger.warn(`${levelPrefix} Text content is not JSON, treating as raw text`);
|
|
500
|
-
return { success: false, error: 'Text content is not valid JSON' };
|
|
502
|
+
return { success: false, error: 'Text content is not valid JSON', textContent };
|
|
501
503
|
}
|
|
502
504
|
|
|
503
505
|
// No text content found, try extracting JSON directly from stdout
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
const logger = require('./logger');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Extract JSON from text responses using multiple strategies
|
|
6
|
-
* This is a shared utility to ensure consistent JSON extraction across the application
|
|
7
|
-
*
|
|
5
|
+
* Extract JSON from text responses using multiple strategies.
|
|
6
|
+
* This is a shared utility to ensure consistent JSON extraction across the application.
|
|
7
|
+
*
|
|
8
|
+
* Strategies are tried in order:
|
|
9
|
+
* 1. Markdown code blocks (```json ... ```)
|
|
10
|
+
* 2. Direct JSON.parse of the trimmed response
|
|
11
|
+
* 3. First { to last } substring
|
|
12
|
+
* 4. Known JSON key anchors (e.g. {"level", {"suggestions")
|
|
13
|
+
* 5. Forward scan: try JSON.parse from every top-level { in the text
|
|
14
|
+
* 6. Bracket-matched substring from the first {
|
|
15
|
+
*
|
|
16
|
+
* @param {string} response - Raw response text (may include preamble/postamble prose)
|
|
8
17
|
* @param {string|number} level - Level identifier for logging (e.g., 1, 2, 3, 'orchestration', 'unknown')
|
|
9
18
|
* @returns {Object} Extraction result with success flag and data/error
|
|
10
19
|
*/
|
|
@@ -35,51 +44,132 @@ function extractJSON(response, level = 'unknown') {
|
|
|
35
44
|
}
|
|
36
45
|
throw new Error('No JSON code block found');
|
|
37
46
|
},
|
|
38
|
-
|
|
39
|
-
// Strategy 2:
|
|
47
|
+
|
|
48
|
+
// Strategy 2: Try the entire response as JSON (fast path for clean responses)
|
|
49
|
+
() => {
|
|
50
|
+
return JSON.parse(response.trim());
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Strategy 3: Look for JSON between first { and last }
|
|
54
|
+
// Works when the response is just JSON or has minimal wrapping
|
|
40
55
|
() => {
|
|
41
56
|
const firstBrace = response.indexOf('{');
|
|
42
57
|
const lastBrace = response.lastIndexOf('}');
|
|
43
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace
|
|
58
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
44
59
|
return JSON.parse(response.substring(firstBrace, lastBrace + 1));
|
|
45
60
|
}
|
|
46
61
|
throw new Error('No valid JSON braces found');
|
|
47
62
|
},
|
|
48
|
-
|
|
49
|
-
// Strategy
|
|
63
|
+
|
|
64
|
+
// Strategy 4: Anchor-based extraction — look for known JSON key patterns
|
|
65
|
+
// that mark the start of our expected response structures.
|
|
66
|
+
// This handles the common case where preamble text contains { characters
|
|
67
|
+
// (e.g. LLM discussing code: "the function handleEvent(event) { ... }")
|
|
68
|
+
// which would cause Strategy 3 to grab the wrong first brace.
|
|
50
69
|
() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// Look for patterns that start our expected JSON structures
|
|
71
|
+
const anchors = [
|
|
72
|
+
/\{"level"\s*:/,
|
|
73
|
+
/\{"suggestions"\s*:/,
|
|
74
|
+
/\{"fileLevelSuggestions"\s*:/,
|
|
75
|
+
/\{"summary"\s*:/,
|
|
76
|
+
/\{"overview"\s*:/,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const anchor of anchors) {
|
|
80
|
+
const match = response.match(anchor);
|
|
81
|
+
if (match) {
|
|
82
|
+
const startIdx = match.index;
|
|
83
|
+
// Find the matching closing brace from the end
|
|
84
|
+
const lastBrace = response.lastIndexOf('}');
|
|
85
|
+
if (lastBrace > startIdx) {
|
|
86
|
+
const candidate = response.substring(startIdx, lastBrace + 1);
|
|
87
|
+
return JSON.parse(candidate);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error('No known JSON anchor found');
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Strategy 5: Forward scan — try JSON.parse starting from each { in the text.
|
|
95
|
+
// Handles arbitrary preamble text with braces by trying every { as a potential
|
|
96
|
+
// JSON start. Stops at the first successful parse.
|
|
97
|
+
() => {
|
|
98
|
+
let searchFrom = 0;
|
|
99
|
+
// Limit attempts to avoid excessive parsing on very large non-JSON text
|
|
100
|
+
const maxAttempts = 20;
|
|
101
|
+
let attempts = 0;
|
|
102
|
+
const lastBrace = response.lastIndexOf('}');
|
|
103
|
+
|
|
104
|
+
while (searchFrom < response.length && attempts < maxAttempts) {
|
|
105
|
+
const braceIdx = response.indexOf('{', searchFrom);
|
|
106
|
+
if (braceIdx === -1) break;
|
|
107
|
+
|
|
108
|
+
attempts++;
|
|
109
|
+
try {
|
|
110
|
+
// Try parsing from this brace to the end of the response.
|
|
111
|
+
// JSON.parse is lenient about trailing content only if we trim to the
|
|
112
|
+
// right boundary, so use lastIndexOf('}') from the end.
|
|
113
|
+
if (lastBrace > braceIdx) {
|
|
114
|
+
const candidate = response.substring(braceIdx, lastBrace + 1);
|
|
115
|
+
const parsed = JSON.parse(candidate);
|
|
116
|
+
if (parsed && typeof parsed === 'object') {
|
|
117
|
+
return parsed;
|
|
66
118
|
}
|
|
67
119
|
}
|
|
120
|
+
} catch {
|
|
121
|
+
// This { wasn't the start of valid JSON, try the next one
|
|
68
122
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
searchFrom = braceIdx + 1;
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Forward scan found no valid JSON');
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Strategy 6: Bracket-matched substring from the first {.
|
|
129
|
+
// Counts balanced braces (ignoring those inside JSON strings) to find
|
|
130
|
+
// the end of the first top-level object. No iteration cap — the loop
|
|
131
|
+
// runs for the full length of the matched region.
|
|
132
|
+
() => {
|
|
133
|
+
const firstBrace = response.indexOf('{');
|
|
134
|
+
if (firstBrace === -1) throw new Error('No opening brace found');
|
|
135
|
+
|
|
136
|
+
let braceCount = 0;
|
|
137
|
+
let inString = false;
|
|
138
|
+
let escaped = false;
|
|
139
|
+
|
|
140
|
+
for (let i = firstBrace; i < response.length; i++) {
|
|
141
|
+
const ch = response[i];
|
|
142
|
+
|
|
143
|
+
if (escaped) {
|
|
144
|
+
escaped = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ch === '\\' && inString) {
|
|
149
|
+
escaped = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ch === '"') {
|
|
154
|
+
inString = !inString;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (inString) continue;
|
|
159
|
+
|
|
160
|
+
if (ch === '{') braceCount++;
|
|
161
|
+
else if (ch === '}') {
|
|
162
|
+
braceCount--;
|
|
163
|
+
if (braceCount === 0) {
|
|
164
|
+
return JSON.parse(response.substring(firstBrace, i + 1));
|
|
165
|
+
}
|
|
72
166
|
}
|
|
73
167
|
}
|
|
74
168
|
throw new Error('No balanced JSON structure found');
|
|
75
169
|
},
|
|
76
|
-
|
|
77
|
-
// Strategy 4: Try the entire response as JSON (for simple cases)
|
|
78
|
-
() => {
|
|
79
|
-
return JSON.parse(response.trim());
|
|
80
|
-
}
|
|
81
170
|
];
|
|
82
171
|
|
|
172
|
+
const strategyErrors = [];
|
|
83
173
|
for (let i = 0; i < strategies.length; i++) {
|
|
84
174
|
try {
|
|
85
175
|
const data = strategies[i]();
|
|
@@ -88,17 +178,17 @@ function extractJSON(response, level = 'unknown') {
|
|
|
88
178
|
return { success: true, data };
|
|
89
179
|
}
|
|
90
180
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
if (i === strategies.length - 1) {
|
|
93
|
-
// Last strategy failed, log the error
|
|
94
|
-
logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
|
|
95
|
-
logger.warn(`${levelPrefix} Response preview: ${response.substring(0, 200)}...`);
|
|
96
|
-
}
|
|
181
|
+
strategyErrors.push(`S${i + 1}: ${error.message}`);
|
|
97
182
|
}
|
|
98
183
|
}
|
|
99
184
|
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
// All strategies failed — log details for debugging
|
|
186
|
+
logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
|
|
187
|
+
logger.warn(`${levelPrefix} Strategy errors: ${strategyErrors.join('; ')}`);
|
|
188
|
+
logger.warn(`${levelPrefix} Response length: ${response.length} chars, preview: ${response.substring(0, 200)}...`);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
102
192
|
error: 'Failed to extract JSON from response',
|
|
103
193
|
response: response.substring(0, 500) // Include preview for debugging
|
|
104
194
|
};
|