mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-06 22:03:07 +00:00
refactor(server): Extract JSON extraction utility to shared module
Created libs/server/src/lib/json-extractor.ts with reusable JSON
extraction utilities for parsing AI responses:
- extractJson<T>(): Multi-strategy JSON extraction
- extractJsonWithKey<T>(): Extract with required key validation
- extractJsonWithArray<T>(): Extract with array property validation
Strategies (tried in order):
1. JSON in ```json code block
2. JSON in ``` code block
3. Find JSON object by matching braces (with optional required key)
4. Find any JSON object by matching braces
5. First { to last }
6. Parse entire response
Updated:
- generate-suggestions.ts: Use extractJsonWithArray('suggestions')
- validate-issue.ts: Use extractJson()
Both files now use the shared utility instead of local implementations,
following DRY principle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import type {
|
||||
} from '@automaker/types';
|
||||
import { isCursorModel } from '@automaker/types';
|
||||
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||
import { extractJson } from '../../../lib/json-extractor.js';
|
||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||
import {
|
||||
@@ -37,73 +38,6 @@ import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
|
||||
/** Valid Claude model values for validation */
|
||||
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
|
||||
/**
|
||||
* Extract JSON from a response that may contain markdown code blocks or other text.
|
||||
* Tries multiple extraction strategies in order of likelihood.
|
||||
*/
|
||||
function extractJsonFromResponse<T>(responseText: string, log: typeof logger): T | null {
|
||||
const strategies = [
|
||||
// Strategy 1: JSON in ```json code block
|
||||
() => {
|
||||
const match = responseText.match(/```json\s*([\s\S]*?)```/);
|
||||
if (match) {
|
||||
log.debug('Extracting JSON from ```json code block');
|
||||
return JSON.parse(match[1].trim()) as T;
|
||||
}
|
||||
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();
|
||||
// Only try if it looks like JSON (starts with { or [)
|
||||
if (content.startsWith('{') || content.startsWith('[')) {
|
||||
log.debug('Extracting JSON from ``` code block');
|
||||
return JSON.parse(content) as T;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// Strategy 3: Find JSON object directly in text (first { to last })
|
||||
() => {
|
||||
const firstBrace = responseText.indexOf('{');
|
||||
const lastBrace = responseText.lastIndexOf('}');
|
||||
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
||||
const jsonCandidate = responseText.slice(firstBrace, lastBrace + 1);
|
||||
log.debug('Extracting JSON object from raw text');
|
||||
return JSON.parse(jsonCandidate) as T;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// Strategy 4: Try parsing the entire response as JSON
|
||||
() => {
|
||||
const trimmed = responseText.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
log.debug('Parsing entire response as JSON');
|
||||
return JSON.parse(trimmed) as T;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
];
|
||||
|
||||
for (const strategy of strategies) {
|
||||
try {
|
||||
const result = strategy();
|
||||
if (result !== null) {
|
||||
log.debug('Successfully parsed JSON from Cursor response:', result);
|
||||
return result;
|
||||
}
|
||||
} catch {
|
||||
// Strategy failed, try next one
|
||||
}
|
||||
}
|
||||
|
||||
log.error('Failed to extract JSON from Cursor response after trying all strategies');
|
||||
log.debug('Raw response:', responseText.slice(0, 500) + (responseText.length > 500 ? '...' : ''));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request body for issue validation
|
||||
*/
|
||||
@@ -201,9 +135,9 @@ ${prompt}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON from the response text
|
||||
// Parse JSON from the response text using shared utility
|
||||
if (responseText) {
|
||||
validationResult = extractJsonFromResponse<IssueValidationResult>(responseText, logger);
|
||||
validationResult = extractJson<IssueValidationResult>(responseText, { logger });
|
||||
}
|
||||
} else {
|
||||
// Use Claude SDK for Claude models
|
||||
|
||||
Reference in New Issue
Block a user