Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion review_roadmap/agent/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,26 @@ def reflect_on_roadmap(state: ReviewState) -> Dict[str, Any]:

# Parse response (with fallback for non-JSON responses)
import json
import re

# Strip markdown code fences if present (LLM often wraps JSON in ```json ... ```)
content = response.content.strip()
# Try complete code fence first
code_fence_pattern = r'^```(?:json)?\s*\n?(.*?)\n?```$'
match = re.match(code_fence_pattern, content, re.DOTALL)
if match:
content = match.group(1).strip()
else:
# Handle truncated response or unclosed code fence
if content.startswith('```'):
# Remove opening fence (```json or ```)
content = re.sub(r'^```(?:json)?\s*\n?', '', content)
# Remove closing fence if present
content = re.sub(r'\n?```$', '', content)
content = content.strip()

try:
result = json.loads(response.content)
result = json.loads(content)
passed = result.get("passed", False)
feedback = result.get("feedback", "")
notes = result.get("notes", "")
Expand Down
56 changes: 56 additions & 0 deletions tests/test_agent_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,62 @@ def test_reflection_handles_non_json_response(
assert result["reflection_passed"] is True
assert result["reflection_iterations"] == 1

def test_reflection_handles_json_in_code_fence(
self, sample_review_state_with_roadmap: ReviewState
):
"""Test that reflection correctly parses JSON wrapped in markdown code fences."""
mock_response = MagicMock()
# This is the format the LLM often returns - JSON wrapped in code fences
mock_response.content = '```json\n{"passed": true, "notes": "Self-review: looks good"}\n```'

mock_chain = MagicMock()
mock_chain.invoke.return_value = mock_response

mock_llm = MagicMock()
mock_llm.__or__ = MagicMock(return_value=mock_chain)

with patch("review_roadmap.agent.nodes._get_llm_instance", return_value=mock_llm):
with patch("review_roadmap.agent.nodes.ChatPromptTemplate") as mock_template:
mock_prompt = MagicMock()
mock_prompt.__or__ = MagicMock(return_value=mock_chain)
mock_template.from_messages.return_value = mock_prompt

from review_roadmap.agent.nodes import reflect_on_roadmap

result = reflect_on_roadmap(sample_review_state_with_roadmap)

# Should correctly parse the JSON from within code fences
assert result["reflection_passed"] is True
assert result["reflection_iterations"] == 1

def test_reflection_handles_truncated_code_fence(
self, sample_review_state_with_roadmap: ReviewState
):
"""Test that reflection handles truncated code fences (missing closing ```)."""
mock_response = MagicMock()
# Truncated response - missing closing ```
mock_response.content = '```json\n{"passed": true, "notes": "Self-review: good"}'

mock_chain = MagicMock()
mock_chain.invoke.return_value = mock_response

mock_llm = MagicMock()
mock_llm.__or__ = MagicMock(return_value=mock_chain)

with patch("review_roadmap.agent.nodes._get_llm_instance", return_value=mock_llm):
with patch("review_roadmap.agent.nodes.ChatPromptTemplate") as mock_template:
mock_prompt = MagicMock()
mock_prompt.__or__ = MagicMock(return_value=mock_chain)
mock_template.from_messages.return_value = mock_prompt

from review_roadmap.agent.nodes import reflect_on_roadmap

result = reflect_on_roadmap(sample_review_state_with_roadmap)

# Should correctly parse the JSON even with truncated code fence
assert result["reflection_passed"] is True
assert result["reflection_iterations"] == 1

def test_reflection_increments_iteration_count(
self, sample_review_state_with_roadmap: ReviewState
):
Expand Down