Skip to content

Commit d8aaa89

Browse files
authored
fix: strip markdown code fences from reflection JSON response (#12)
The LLM often returns JSON wrapped in markdown code fences (```json ... ```) because the prompt shows the expected format that way. This caused JSON parsing to fail, triggering the fallback path. Changes: - Add regex-based code fence stripping before JSON parsing - Handle both complete and truncated code fences - Add tests for code fence parsing scenarios
1 parent 2e6a8e6 commit d8aaa89

File tree

2 files changed

+75
-1
lines changed

2 files changed

+75
-1
lines changed

review_roadmap/agent/nodes.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,26 @@ def reflect_on_roadmap(state: ReviewState) -> Dict[str, Any]:
472472

473473
# Parse response (with fallback for non-JSON responses)
474474
import json
475+
import re
476+
477+
# Strip markdown code fences if present (LLM often wraps JSON in ```json ... ```)
478+
content = response.content.strip()
479+
# Try complete code fence first
480+
code_fence_pattern = r'^```(?:json)?\s*\n?(.*?)\n?```$'
481+
match = re.match(code_fence_pattern, content, re.DOTALL)
482+
if match:
483+
content = match.group(1).strip()
484+
else:
485+
# Handle truncated response or unclosed code fence
486+
if content.startswith('```'):
487+
# Remove opening fence (```json or ```)
488+
content = re.sub(r'^```(?:json)?\s*\n?', '', content)
489+
# Remove closing fence if present
490+
content = re.sub(r'\n?```$', '', content)
491+
content = content.strip()
492+
475493
try:
476-
result = json.loads(response.content)
494+
result = json.loads(content)
477495
passed = result.get("passed", False)
478496
feedback = result.get("feedback", "")
479497
notes = result.get("notes", "")

tests/test_agent_nodes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,62 @@ def test_reflection_handles_non_json_response(
453453
assert result["reflection_passed"] is True
454454
assert result["reflection_iterations"] == 1
455455

456+
def test_reflection_handles_json_in_code_fence(
457+
self, sample_review_state_with_roadmap: ReviewState
458+
):
459+
"""Test that reflection correctly parses JSON wrapped in markdown code fences."""
460+
mock_response = MagicMock()
461+
# This is the format the LLM often returns - JSON wrapped in code fences
462+
mock_response.content = '```json\n{"passed": true, "notes": "Self-review: looks good"}\n```'
463+
464+
mock_chain = MagicMock()
465+
mock_chain.invoke.return_value = mock_response
466+
467+
mock_llm = MagicMock()
468+
mock_llm.__or__ = MagicMock(return_value=mock_chain)
469+
470+
with patch("review_roadmap.agent.nodes._get_llm_instance", return_value=mock_llm):
471+
with patch("review_roadmap.agent.nodes.ChatPromptTemplate") as mock_template:
472+
mock_prompt = MagicMock()
473+
mock_prompt.__or__ = MagicMock(return_value=mock_chain)
474+
mock_template.from_messages.return_value = mock_prompt
475+
476+
from review_roadmap.agent.nodes import reflect_on_roadmap
477+
478+
result = reflect_on_roadmap(sample_review_state_with_roadmap)
479+
480+
# Should correctly parse the JSON from within code fences
481+
assert result["reflection_passed"] is True
482+
assert result["reflection_iterations"] == 1
483+
484+
def test_reflection_handles_truncated_code_fence(
485+
self, sample_review_state_with_roadmap: ReviewState
486+
):
487+
"""Test that reflection handles truncated code fences (missing closing ```)."""
488+
mock_response = MagicMock()
489+
# Truncated response - missing closing ```
490+
mock_response.content = '```json\n{"passed": true, "notes": "Self-review: good"}'
491+
492+
mock_chain = MagicMock()
493+
mock_chain.invoke.return_value = mock_response
494+
495+
mock_llm = MagicMock()
496+
mock_llm.__or__ = MagicMock(return_value=mock_chain)
497+
498+
with patch("review_roadmap.agent.nodes._get_llm_instance", return_value=mock_llm):
499+
with patch("review_roadmap.agent.nodes.ChatPromptTemplate") as mock_template:
500+
mock_prompt = MagicMock()
501+
mock_prompt.__or__ = MagicMock(return_value=mock_chain)
502+
mock_template.from_messages.return_value = mock_prompt
503+
504+
from review_roadmap.agent.nodes import reflect_on_roadmap
505+
506+
result = reflect_on_roadmap(sample_review_state_with_roadmap)
507+
508+
# Should correctly parse the JSON even with truncated code fence
509+
assert result["reflection_passed"] is True
510+
assert result["reflection_iterations"] == 1
511+
456512
def test_reflection_increments_iteration_count(
457513
self, sample_review_state_with_roadmap: ReviewState
458514
):

0 commit comments

Comments
 (0)