Commit ·
c6fefb4
1
Parent(s): 68aadac
fix: Robust JSON parsing and field mapping for report generation
Browse filesFrontend improvements:
- Added comprehensive JSON parsing with multiple fallback strategies
- Handle both object and string responses from backend
- Added field-level validation to ensure each report field gets distinct data
- Warning if colposcopicFindings contains JSON (indicates parsing failure)
- Detailed console logging for debugging field population
Backend improvements:
- Validate AI response is valid JSON before returning
- Strip markdown code fences in backend (before sending to frontend)
- Better error messages for JSON parsing failures
- Log parsed JSON keys to verify structure
This ensures report data populates correctly into individual form fields.
- backend/app.py +24 -5
- src/pages/ReportPage.tsx +66 -38
backend/app.py
CHANGED
|
@@ -13,6 +13,7 @@ import uvicorn
|
|
| 13 |
import traceback
|
| 14 |
import json
|
| 15 |
from typing import List, Dict, Optional
|
|
|
|
| 16 |
|
| 17 |
# Load .env file for local development.
|
| 18 |
# Search from this file's directory upward so it works whether the server
|
|
@@ -394,11 +395,29 @@ Do NOT include any other keys. Do NOT wrap in markdown. Return raw JSON only."""
|
|
| 394 |
if not response_text:
|
| 395 |
raise Exception("All model attempts failed. Please check API key and model availability.")
|
| 396 |
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
except Exception as e:
|
| 404 |
error_msg = str(e)
|
|
|
|
| 13 |
import traceback
|
| 14 |
import json
|
| 15 |
from typing import List, Dict, Optional
|
| 16 |
+
import re
|
| 17 |
|
| 18 |
# Load .env file for local development.
|
| 19 |
# Search from this file's directory upward so it works whether the server
|
|
|
|
| 395 |
if not response_text:
|
| 396 |
raise Exception("All model attempts failed. Please check API key and model availability.")
|
| 397 |
|
| 398 |
+
# Ensure response_text is valid JSON before returning
|
| 399 |
+
try:
|
| 400 |
+
# Strip markdown if present
|
| 401 |
+
cleaned_text = response_text.strip()
|
| 402 |
+
if cleaned_text.startswith('```'):
|
| 403 |
+
cleaned_text = re.sub(r'^```[a-z]*\n?', '', cleaned_text, flags=re.IGNORECASE)
|
| 404 |
+
cleaned_text = re.sub(r'\n?```\s*$', '', cleaned_text)
|
| 405 |
+
cleaned_text = cleaned_text.strip()
|
| 406 |
+
|
| 407 |
+
# Parse to verify it's valid JSON
|
| 408 |
+
parsed_json = json.loads(cleaned_text)
|
| 409 |
+
print(f"✅ Report is valid JSON with keys: {list(parsed_json.keys())}")
|
| 410 |
+
|
| 411 |
+
# Return as JSON object (not string) so it's properly encoded by FastAPI
|
| 412 |
+
return JSONResponse({
|
| 413 |
+
"status": "success",
|
| 414 |
+
"report": cleaned_text, # Return the cleaned JSON string
|
| 415 |
+
"model": used_model
|
| 416 |
+
})
|
| 417 |
+
except json.JSONDecodeError as je:
|
| 418 |
+
print(f"⚠️ Response is not valid JSON: {je}")
|
| 419 |
+
print(f"Response text: {response_text[:500]}")
|
| 420 |
+
raise Exception(f"Gemini returned invalid JSON: {str(je)}")
|
| 421 |
|
| 422 |
except Exception as e:
|
| 423 |
error_msg = str(e)
|
src/pages/ReportPage.tsx
CHANGED
|
@@ -206,38 +206,61 @@ export function ReportPage({
|
|
| 206 |
throw new Error('Invalid response from backend');
|
| 207 |
}
|
| 208 |
|
| 209 |
-
//
|
| 210 |
-
let reportText: string = data.report.trim();
|
| 211 |
-
console.log('[ReportPage] Raw AI response:', reportText.substring(0, 200));
|
| 212 |
-
|
| 213 |
-
if (reportText.startsWith('```')) {
|
| 214 |
-
reportText = reportText.replace(/^```[a-z]*\n?/i, '').replace(/```\s*$/, '').trim();
|
| 215 |
-
console.log('[ReportPage] Stripped markdown, new length:', reportText.length);
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
// Parse structured JSON returned by Gemini
|
| 219 |
let parsed: Record<string, string> = {};
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}
|
| 236 |
-
} else {
|
| 237 |
-
throw new Error('No JSON object found in AI response');
|
| 238 |
}
|
| 239 |
}
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
// Validate that all required keys are present
|
| 242 |
const requiredKeys = ['examQuality', 'transformationZone', 'acetowL', 'nativeFindings',
|
| 243 |
'aceticFindings', 'biopsySites', 'biopsyNotes', 'colposcopicFindings',
|
|
@@ -262,19 +285,24 @@ export function ReportPage({
|
|
| 262 |
biopsyNotes: String(parsed.biopsyNotes || '').trim(),
|
| 263 |
};
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
followUp: updatedFormData.followUp.substring(0, 50),
|
| 274 |
-
biopsySites: updatedFormData.biopsySites.substring(0, 30),
|
| 275 |
-
biopsyNotes: updatedFormData.biopsyNotes.substring(0, 50),
|
| 276 |
});
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
setFormData(prev => ({
|
| 279 |
...prev,
|
| 280 |
...updatedFormData
|
|
@@ -284,7 +312,7 @@ export function ReportPage({
|
|
| 284 |
if (missingKeys.length === 0) {
|
| 285 |
setAiError(null);
|
| 286 |
} else {
|
| 287 |
-
setAiError(`✓ Report generated!
|
| 288 |
}
|
| 289 |
|
| 290 |
} catch (err: any) {
|
|
|
|
| 206 |
throw new Error('Invalid response from backend');
|
| 207 |
}
|
| 208 |
|
| 209 |
+
// Get the report text - it should be a JSON string
|
| 210 |
+
let reportText: string = String(data.report).trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
let parsed: Record<string, string> = {};
|
| 212 |
+
|
| 213 |
+
console.log('[ReportPage] Raw response type:', typeof data.report);
|
| 214 |
+
console.log('[ReportPage] Raw response preview:', reportText.substring(0, 300));
|
| 215 |
+
|
| 216 |
+
// If response is already an object (shouldn't happen but handle it)
|
| 217 |
+
if (typeof data.report === 'object' && data.report !== null) {
|
| 218 |
+
console.log('[ReportPage] Response is already an object, using directly');
|
| 219 |
+
parsed = data.report;
|
| 220 |
+
} else {
|
| 221 |
+
// Response is a string - need to parse it
|
| 222 |
+
reportText = String(data.report).trim();
|
| 223 |
+
|
| 224 |
+
// Remove markdown code fence wrappers if present
|
| 225 |
+
if (reportText.startsWith('```')) {
|
| 226 |
+
reportText = reportText.replace(/^```(?:json)?\n/i, '').replace(/\n```\s*$/i, '').trim();
|
| 227 |
+
console.log('[ReportPage] Removed markdown fence');
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Clean up any escape sequences
|
| 231 |
+
reportText = reportText.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
| 232 |
|
| 233 |
+
console.log('[ReportPage] Cleaned text preview:', reportText.substring(0, 200));
|
| 234 |
+
|
| 235 |
+
// Try direct JSON parse first
|
| 236 |
+
try {
|
| 237 |
+
parsed = JSON.parse(reportText);
|
| 238 |
+
console.log('[ReportPage] ✅ Direct parse successful, keys:', Object.keys(parsed));
|
| 239 |
+
} catch (directErr) {
|
| 240 |
+
console.log('[ReportPage] Direct parse failed, attempting JSON extraction...');
|
| 241 |
+
|
| 242 |
+
// Try to find and extract the JSON object from the string
|
| 243 |
+
const jsonMatch = reportText.match(/\{[\s\S]*\}/);
|
| 244 |
+
if (jsonMatch) {
|
| 245 |
+
try {
|
| 246 |
+
parsed = JSON.parse(jsonMatch[0]);
|
| 247 |
+
console.log('[ReportPage] ✅ Extracted JSON successful, keys:', Object.keys(parsed));
|
| 248 |
+
} catch (extractErr) {
|
| 249 |
+
console.error('[ReportPage] JSON extraction failed:', extractErr);
|
| 250 |
+
throw new Error('Could not parse AI response as valid JSON');
|
| 251 |
+
}
|
| 252 |
+
} else {
|
| 253 |
+
throw new Error('No JSON object found in response');
|
| 254 |
}
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
}
|
| 257 |
|
| 258 |
+
// At this point, parsed should be an object with the report fields
|
| 259 |
+
// Validate structure
|
| 260 |
+
if (!parsed || typeof parsed !== 'object') {
|
| 261 |
+
throw new Error('Parsed response is not a valid object');
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
// Validate that all required keys are present
|
| 265 |
const requiredKeys = ['examQuality', 'transformationZone', 'acetowL', 'nativeFindings',
|
| 266 |
'aceticFindings', 'biopsySites', 'biopsyNotes', 'colposcopicFindings',
|
|
|
|
| 285 |
biopsyNotes: String(parsed.biopsyNotes || '').trim(),
|
| 286 |
};
|
| 287 |
|
| 288 |
+
// Verify each field is populated correctly
|
| 289 |
+
console.log('[ReportPage] Field validation:');
|
| 290 |
+
Object.entries(updatedFormData).forEach(([key, value]) => {
|
| 291 |
+
if (value.length > 0) {
|
| 292 |
+
console.log(` ✅ ${key}: ${value.substring(0, 60)}...`);
|
| 293 |
+
} else {
|
| 294 |
+
console.warn(` ❌ ${key}: EMPTY!`);
|
| 295 |
+
}
|
|
|
|
|
|
|
|
|
|
| 296 |
});
|
| 297 |
|
| 298 |
+
// Additional safety check - if colposcopicFindings contains JSON structure, something went wrong
|
| 299 |
+
if (updatedFormData.colposcopicFindings.includes('{') && updatedFormData.colposcopicFindings.includes('"')) {
|
| 300 |
+
console.error('[ReportPage] ⚠️ WARNING: colposcopicFindings contains JSON! Parsing may have failed.');
|
| 301 |
+
console.error('Value:', updatedFormData.colposcopicFindings.substring(0, 200));
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
console.log('[ReportPage] AI generated fields:', updatedFormData);
|
| 305 |
+
|
| 306 |
setFormData(prev => ({
|
| 307 |
...prev,
|
| 308 |
...updatedFormData
|
|
|
|
| 312 |
if (missingKeys.length === 0) {
|
| 313 |
setAiError(null);
|
| 314 |
} else {
|
| 315 |
+
setAiError(`✓ Report generated! Some fields were missing: ${missingKeys.join(', ')}`);
|
| 316 |
}
|
| 317 |
|
| 318 |
} catch (err: any) {
|