remove validateAndRepairJson function and tests
This commit is contained in:
parent
263ff8adcc
commit
0aed4e06a2
@ -2,9 +2,6 @@
|
|||||||
* Shared OpenRouter API helper for structured JSON responses.
|
* Shared OpenRouter API helper for structured JSON responses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getSetting } from '../repositories/settings.js';
|
|
||||||
|
|
||||||
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
export interface JsonSchemaDefinition {
|
export interface JsonSchemaDefinition {
|
||||||
@ -170,118 +167,3 @@ export function parseJsonContent<T>(content: string, jobId?: string): T {
|
|||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate JSON against a Zod schema and repair with AI if invalid.
|
|
||||||
*
|
|
||||||
* @param data - The JSON object to validate
|
|
||||||
* @param schema - Zod schema to validate against
|
|
||||||
* @param context - Optional context for logging (e.g., job ID)
|
|
||||||
* @returns The validated (and possibly repaired) data
|
|
||||||
*/
|
|
||||||
export async function validateAndRepairJson<T>(
|
|
||||||
data: unknown,
|
|
||||||
schema: z.ZodSchema<T>,
|
|
||||||
context?: string
|
|
||||||
): Promise<{ success: true; data: T; repaired: boolean } | { success: false; error: string }> {
|
|
||||||
const label = context ?? 'unknown';
|
|
||||||
|
|
||||||
// First attempt: validate as-is
|
|
||||||
const result = schema.safeParse(data);
|
|
||||||
if (result.success) {
|
|
||||||
return { success: true, data: result.data, repaired: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation failed - attempt AI repair
|
|
||||||
console.warn(`⚠️ [${label}] Schema validation failed, attempting AI repair...`);
|
|
||||||
|
|
||||||
const errors = result.error.issues.map((issue: z.ZodIssue) => ({
|
|
||||||
path: issue.path.join('.'),
|
|
||||||
message: issue.message,
|
|
||||||
code: issue.code,
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.warn(` Validation errors:`, errors.slice(0, 5)); // Log first 5 errors
|
|
||||||
|
|
||||||
// Check if API key is available
|
|
||||||
if (!process.env.OPENROUTER_API_KEY) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Schema validation failed and no API key for repair: ${errors.map((e: { path: string; message: string }) => `${e.path}: ${e.message}`).join('; ')}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const [overrideModel] = await Promise.all([getSetting('model')]);
|
|
||||||
const model = overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
|
|
||||||
|
|
||||||
const repairPrompt = buildRepairPrompt(data, errors);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(OPENROUTER_API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'HTTP-Referer': 'JobOps',
|
|
||||||
'X-Title': 'JobOpsSchemaRepair',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
messages: [{ role: 'user', content: repairPrompt }],
|
|
||||||
stream: false,
|
|
||||||
plugins: [{ id: 'response-healing' }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text().catch(() => 'No error body');
|
|
||||||
return { success: false, error: `AI repair request failed: ${response.status} - ${errorBody}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
|
||||||
const content = responseData.choices?.[0]?.message?.content;
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return { success: false, error: 'AI repair returned no content' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the repaired JSON
|
|
||||||
const repaired = parseJsonContent<unknown>(content, label);
|
|
||||||
|
|
||||||
// Validate the repaired version
|
|
||||||
const repairedResult = schema.safeParse(repaired);
|
|
||||||
if (repairedResult.success) {
|
|
||||||
console.log(`✅ [${label}] AI successfully repaired the JSON`);
|
|
||||||
return { success: true, data: repairedResult.data, repaired: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Still invalid after repair
|
|
||||||
const newErrors = repairedResult.error.issues.slice(0, 3).map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
||||||
return { success: false, error: `AI repair did not fix all issues: ${newErrors}` };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
return { success: false, error: `AI repair failed: ${message}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRepairPrompt(data: unknown, errors: Array<{ path: string; message: string; code: string }>): string {
|
|
||||||
const errorList = errors.slice(0, 10).map(e => `- Path "${e.path}": ${e.message}`).join('\n');
|
|
||||||
|
|
||||||
return `You are fixing a JSON object that failed schema validation.
|
|
||||||
|
|
||||||
VALIDATION ERRORS:
|
|
||||||
${errorList}
|
|
||||||
|
|
||||||
ORIGINAL JSON (may be truncated):
|
|
||||||
${JSON.stringify(data, null, 2).slice(0, 15000)}
|
|
||||||
|
|
||||||
INSTRUCTIONS:
|
|
||||||
1. Fix ONLY the validation errors listed above
|
|
||||||
2. Do NOT remove or modify data that isn't causing errors
|
|
||||||
3. For missing required fields, add them with sensible defaults (empty strings, empty arrays, etc.)
|
|
||||||
4. For type mismatches, convert to the correct type
|
|
||||||
5. Preserve all existing valid data
|
|
||||||
|
|
||||||
Return ONLY the fixed JSON object, no explanation.`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -61,14 +61,6 @@ vi.mock('./resumeProjects.js', () => ({
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./openrouter.js', () => ({
|
|
||||||
validateAndRepairJson: vi.fn().mockImplementation(async (data: unknown) => ({
|
|
||||||
success: true,
|
|
||||||
data,
|
|
||||||
repaired: false
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('child_process', () => ({
|
vi.mock('child_process', () => ({
|
||||||
spawn: vi.fn().mockImplementation(() => ({
|
spawn: vi.fn().mockImplementation(() => ({
|
||||||
stdout: { on: vi.fn() },
|
stdout: { on: vi.fn() },
|
||||||
|
|||||||
@ -73,13 +73,6 @@ vi.mock('./resumeProjects.js', () => ({
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock validateAndRepairJson to always return success (bypass validation in tests)
|
|
||||||
vi.mock('./openrouter.js', () => ({
|
|
||||||
validateAndRepairJson: vi.fn().mockImplementation((data: unknown) =>
|
|
||||||
Promise.resolve({ success: true, data, repaired: false })
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('child_process', () => ({
|
vi.mock('child_process', () => ({
|
||||||
spawn: vi.fn().mockImplementation(() => ({
|
spawn: vi.fn().mockImplementation(() => ({
|
||||||
stdout: { on: vi.fn() },
|
stdout: { on: vi.fn() },
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import { pickProjectIdsForJob } from './projectSelection.js';
|
|||||||
import { extractProjectsFromProfile, resolveResumeProjectsSettings } from './resumeProjects.js';
|
import { extractProjectsFromProfile, resolveResumeProjectsSettings } from './resumeProjects.js';
|
||||||
import { getDataDir } from '../config/dataDir.js';
|
import { getDataDir } from '../config/dataDir.js';
|
||||||
import { getProfile } from './profile.js';
|
import { getProfile } from './profile.js';
|
||||||
import { validateAndRepairJson } from './openrouter.js';
|
|
||||||
import { resumeDataSchema } from '../../shared/rxresume-schema.js';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@ -168,22 +166,9 @@ export async function generatePdf(
|
|||||||
console.warn(` ⚠️ Project visibility step failed for job ${jobId}:`, err);
|
console.warn(` ⚠️ Project visibility step failed for job ${jobId}:`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and repair the resume JSON before PDF generation
|
|
||||||
const validationResult = await validateAndRepairJson(baseResume, resumeDataSchema, `pdf-${jobId}`);
|
|
||||||
if (!validationResult.success) {
|
|
||||||
console.error(`❌ [Job ${jobId}] Resume validation failed: ${validationResult.error}`);
|
|
||||||
return { success: false, error: `Resume validation failed: ${validationResult.error}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationResult.repaired) {
|
|
||||||
console.log(`🔧 [Job ${jobId}] Resume JSON was repaired by AI`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatedResume = validationResult.data;
|
|
||||||
|
|
||||||
// Write modified resume to temp file
|
// Write modified resume to temp file
|
||||||
const tempResumePath = join(RESUME_GEN_DIR, `temp_resume_${jobId}.json`);
|
const tempResumePath = join(RESUME_GEN_DIR, `temp_resume_${jobId}.json`);
|
||||||
await writeFile(tempResumePath, JSON.stringify(validatedResume, null, 2));
|
await writeFile(tempResumePath, JSON.stringify(baseResume, null, 2));
|
||||||
|
|
||||||
// Generate PDF using Python script - output directly to our data folder
|
// Generate PDF using Python script - output directly to our data folder
|
||||||
const outputFilename = `resume_${jobId}.pdf`;
|
const outputFilename = `resume_${jobId}.pdf`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user