Skip to content

Commit e43e7ed

Browse files
Joseph Ibrahimclaude
andcommitted
fix: strip markdown code fences from Claude detector response
Claude sometimes wraps JSON in ```json ... ``` fences even when told to respond with raw JSON. Strip the fences before parsing. Found during Phase 6 live test (93 tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 542c565 commit e43e7ed

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

otto_v4/src/otto/detector.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@ async def detect_commitment(message: str, chat_name: str) -> Commitment | None:
6060
print(f"OTTO detector API error: {e}", file=sys.stderr)
6161
return None
6262

63-
raw_text = response.content[0].text
64-
print(f"OTTO detector raw: {raw_text}", file=sys.stderr)
63+
raw_text = response.content[0].text.strip()
64+
65+
# Claude sometimes wraps JSON in markdown code fences
66+
if raw_text.startswith("```"):
67+
lines = raw_text.split("\n")
68+
# Drop first line (```json) and last line (```)
69+
lines = [l for l in lines if not l.strip().startswith("```")]
70+
raw_text = "\n".join(lines).strip()
6571

6672
try:
6773
data = json.loads(raw_text)

otto_v4/tests/test_detector.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ async def test_null_deadline_stays_none():
136136
assert result.deadline is None
137137

138138

139+
@pytest.mark.asyncio
140+
async def test_markdown_fenced_json_stripped():
141+
"""Claude sometimes wraps JSON in ```json ... ``` code fences."""
142+
inner = json.dumps({
143+
"found": True,
144+
"commitment_text": "send the deck",
145+
"who_to": "Alice",
146+
"deadline": None,
147+
"deadline_source": "none",
148+
"confidence": 0.92,
149+
})
150+
fenced = f"```json\n{inner}\n```"
151+
with patch("otto.detector.anthropic.AsyncAnthropic") as mock_cls:
152+
mock_cls.return_value.messages.create = AsyncMock(
153+
return_value=_mock_response(fenced)
154+
)
155+
result = await detect_commitment("I'll send the deck", "Work")
156+
157+
assert result is not None
158+
assert result.commitment_text == "send the deck"
159+
160+
139161
@pytest.mark.asyncio
140162
async def test_invalid_json_returns_none():
141163
with patch("otto.detector.anthropic.AsyncAnthropic") as mock_cls:

0 commit comments

Comments
 (0)