@@ -297,6 +297,93 @@ def _build_llm_prompt(self, requirement: str, user_id: Optional[str] = None) ->
297297
298298 return "\n " .join (prompt_parts )
299299
300+ def _attempt_json_repair (self , response : str ) -> Optional [str ]:
301+ """
302+ Attempt to repair truncated or malformed JSON
303+
304+ This method tries to fix common issues with LLM-generated JSON:
305+ 1. Truncated responses (incomplete objects/arrays)
306+ 2. Missing closing brackets
307+ 3. Trailing commas
308+
309+ Args:
310+ response: Potentially malformed JSON string
311+
312+ Returns:
313+ Repaired JSON string, or None if repair failed
314+ """
315+ try :
316+ # Strategy: Parse incrementally and keep only complete objects
317+ repaired = response .strip ()
318+
319+ # If it starts with '[', we expect a JSON array
320+ if repaired .startswith ("[" ):
321+ # Try to extract complete objects from the array
322+ # Find all complete objects (those with matching braces)
323+ complete_objects = []
324+ depth = 0
325+ in_string = False
326+ escape_next = False
327+ current_obj_start = None
328+
329+ for i , char in enumerate (repaired ):
330+ if escape_next :
331+ escape_next = False
332+ continue
333+
334+ if char == "\\ " :
335+ escape_next = True
336+ continue
337+
338+ if char == '"' and not escape_next :
339+ in_string = not in_string
340+ continue
341+
342+ if in_string :
343+ continue
344+
345+ if char == "{" :
346+ if depth == 0 :
347+ current_obj_start = i
348+ depth += 1
349+ elif char == "}" :
350+ depth -= 1
351+ if depth == 0 and current_obj_start is not None :
352+ # We have a complete object
353+ obj_str = repaired [current_obj_start : i + 1 ]
354+ try :
355+ json .loads (obj_str )
356+ complete_objects .append (obj_str )
357+ except json .JSONDecodeError :
358+ # Skip malformed objects
359+ pass
360+ current_obj_start = None
361+
362+ if complete_objects :
363+ # Build a valid JSON array from complete objects
364+ repaired = "[" + ", " .join (complete_objects ) + "]"
365+ json .loads (repaired ) # Validate
366+ return repaired
367+
368+ # Fallback: Try simple bracket matching
369+ open_braces = response .count ("{" )
370+ close_braces = response .count ("}" )
371+ open_brackets = response .count ("[" )
372+ close_brackets = response .count ("]" )
373+
374+ repaired = response
375+ if close_braces < open_braces :
376+ repaired += "}" * (open_braces - close_braces )
377+ if close_brackets < open_brackets :
378+ repaired += "]" * (open_brackets - close_brackets )
379+
380+ json .loads (repaired )
381+ return repaired
382+
383+ except Exception as e :
384+ logger .debug (f"JSON repair attempt failed: { e } " )
385+ return None
386+
300387 def _parse_llm_response (self , response : str ) -> List [Dict [str , Any ]]:
301388 """
302389 Parse LLM JSON response
@@ -327,9 +414,22 @@ def _parse_llm_response(self, response: str) -> List[Dict[str, Any]]:
327414 try :
328415 tasks = json .loads (response )
329416 except json .JSONDecodeError as e :
330- raise ValueError (
331- f"Failed to parse JSON from LLM response: { e } . Response: { response [:500 ]} "
332- )
417+ # Try to repair truncated JSON by attempting to parse partial content
418+ repaired_response = self ._attempt_json_repair (response )
419+ if repaired_response :
420+ try :
421+ tasks = json .loads (repaired_response )
422+ logger .warning (
423+ f"Repaired truncated JSON response. Original error: { e } "
424+ )
425+ except json .JSONDecodeError :
426+ raise ValueError (
427+ f"Failed to parse JSON from LLM response: { e } . Response: { response [:500 ]} "
428+ )
429+ else :
430+ raise ValueError (
431+ f"Failed to parse JSON from LLM response: { e } . Response: { response [:500 ]} "
432+ )
333433
334434 # Validate it's a list
335435 if not isinstance (tasks , list ):
0 commit comments