Skip to content

Commit 980943c

Browse files
committed
Merge branch 'main' of github.com:aipartnerup/apflow
2 parents 89e0bd4 + c23fc81 commit 980943c

File tree

3 files changed

+104
-5
lines changed

3 files changed

+104
-5
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525

2626
# Core dependencies (pure orchestration framework - NO CrewAI)
2727
dependencies = [
28-
"pydantic>=2.0.0",
28+
"pydantic[email]>=2.0.0",
2929
"pydantic-settings>=2.0.0",
3030
# Default storage support (embedded DuckDB)
3131
"sqlalchemy>=2.0.0",

src/apflow/extensions/generate/generate_executor.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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):

tests/extensions/generate/test_generate_executor.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def test_parse_llm_response_invalid_json(self):
4949
executor = GenerateExecutor()
5050
with pytest.raises(ValueError, match="Failed to parse JSON"):
5151
executor._parse_llm_response("invalid json")
52-
5352
def test_validate_tasks_array_empty(self):
5453
"""Test validation of empty array"""
5554
executor = GenerateExecutor()

0 commit comments

Comments
 (0)