|
| 1 | +"""Utility for formatting JSON decode errors with visual context.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import json |
| 6 | +from dataclasses import dataclass |
| 7 | +from typing import Any |
| 8 | + |
| 9 | + |
| 10 | +@dataclass |
| 11 | +class JsonErrorInfo: |
| 12 | + """Extracted information from a JSONDecodeError.""" |
| 13 | + |
| 14 | + msg: str |
| 15 | + pos: int |
| 16 | + lineno: int |
| 17 | + colno: int |
| 18 | + doc: str |
| 19 | + error_line: str |
| 20 | + lines: list[str] |
| 21 | + |
| 22 | + |
| 23 | +def extract_json_error_info(error: json.JSONDecodeError) -> JsonErrorInfo: |
| 24 | + """Extract structured information from a JSONDecodeError. |
| 25 | +
|
| 26 | + Args: |
| 27 | + error: The JSONDecodeError to extract from |
| 28 | +
|
| 29 | + Returns: |
| 30 | + JsonErrorInfo with extracted data |
| 31 | + """ |
| 32 | + doc = error.doc or '' |
| 33 | + lines = doc.splitlines() |
| 34 | + |
| 35 | + # Get the problematic line (lineno is 1-based) |
| 36 | + error_line = '' |
| 37 | + if error.lineno >= 1 and error.lineno <= len(lines): |
| 38 | + error_line = lines[error.lineno - 1] |
| 39 | + |
| 40 | + return JsonErrorInfo( |
| 41 | + msg=error.msg, |
| 42 | + pos=error.pos, |
| 43 | + lineno=error.lineno, |
| 44 | + colno=error.colno, |
| 45 | + doc=doc, |
| 46 | + error_line=error_line, |
| 47 | + lines=lines, |
| 48 | + ) |
| 49 | + |
| 50 | + |
| 51 | +def format_json_error_visual(error_info: JsonErrorInfo) -> str: |
| 52 | + """Format JsonErrorInfo with visual context similar to compiler errors. |
| 53 | +
|
| 54 | + Args: |
| 55 | + error_info: The extracted error information |
| 56 | +
|
| 57 | + Returns: |
| 58 | + A formatted string showing the error location with visual indicators |
| 59 | + """ |
| 60 | + if not error_info.doc: |
| 61 | + return f'{error_info.msg} at position {error_info.pos}' |
| 62 | + |
| 63 | + # If we don't have valid line/col info, fall back to basic error |
| 64 | + if error_info.lineno < 1 or error_info.lineno > len(error_info.lines): |
| 65 | + return f'{error_info.msg} at position {error_info.pos}' |
| 66 | + |
| 67 | + # Create the visual indicator |
| 68 | + # colno is 1-based, so we need colno-1 spaces before the caret |
| 69 | + caret_pos = max(0, error_info.colno - 1) |
| 70 | + visual_indicator = ' ' * caret_pos + '^' |
| 71 | + |
| 72 | + # Build the formatted error message |
| 73 | + parts = [ |
| 74 | + f'JSON parsing error, line {error_info.lineno}:', |
| 75 | + f' {error_info.error_line}', |
| 76 | + f' {visual_indicator}', |
| 77 | + f'JSONDecodeError: {error_info.msg}', |
| 78 | + ] |
| 79 | + |
| 80 | + return '\n'.join(parts) |
| 81 | + |
| 82 | + |
| 83 | +def format_json_decode_error(error: json.JSONDecodeError) -> str: |
| 84 | + """Format a JSONDecodeError with visual context similar to compiler errors. |
| 85 | +
|
| 86 | + Args: |
| 87 | + error: The JSONDecodeError to format |
| 88 | +
|
| 89 | + Returns: |
| 90 | + A formatted string showing the error location with visual indicators |
| 91 | + """ |
| 92 | + error_info = extract_json_error_info(error) |
| 93 | + return format_json_error_visual(error_info) |
| 94 | + |
| 95 | + |
| 96 | +def create_json_error_context(error: json.JSONDecodeError, model_name: str, chunk_count: int) -> dict[str, Any]: |
| 97 | + """Create structured context for JSON decode errors. |
| 98 | +
|
| 99 | + Args: |
| 100 | + error: The JSONDecodeError |
| 101 | + model_name: Name of the model that failed |
| 102 | + chunk_count: Number of chunks processed before failure |
| 103 | +
|
| 104 | + Returns: |
| 105 | + Dictionary with structured error context |
| 106 | + """ |
| 107 | + error_info = extract_json_error_info(error) |
| 108 | + formatted_error = format_json_error_visual(error_info) |
| 109 | + |
| 110 | + return { |
| 111 | + 'model_name': model_name, |
| 112 | + 'chunk_count': chunk_count, |
| 113 | + 'json_error_msg': error_info.msg, |
| 114 | + 'json_error_pos': error_info.pos, |
| 115 | + 'json_error_lineno': error_info.lineno, |
| 116 | + 'json_error_colno': error_info.colno, |
| 117 | + 'formatted_error': formatted_error, |
| 118 | + 'problematic_content_preview': error_info.doc[:500] + '...' |
| 119 | + if len(error_info.doc) > 500 |
| 120 | + else error_info.doc or 'N/A', |
| 121 | + } |
0 commit comments