study-partner / test_chat_json_repair.py
nz-nz's picture
Sync from GitHub via hub-sync
ae15cb7 verified
Raw
History Blame Contribute Delete
3.42 kB
"""
chat_json's repair pass must not contaminate the result.
Bug (observed live on MiniCPM-V-4.6): the grader's first reply didn't parse, so
the old repair injected a user turn — "That was not valid JSON. Reply again..."
— and the grader then GRADED THAT MESSAGE, returning a real-looking but
nonsensical grade (score 0, explanation "incorrect JSON syntax"). The repair now
folds a format reminder into the existing user turn instead, and drops the bad
reply. These check that contract without any model/GPU.
python3 -m pytest test_chat_json_repair.py
"""
import os
os.environ["RECALL_STUB"] = "1"
import llm
def _grading_messages():
return [
{"role": "system", "content": "You grade an answer. Return ONLY a JSON object."},
{"role": "user", "content":
"Question: How does friction generate heat?\n"
"Reference answer: Friction converts motion into heat.\n"
"Student answer: More friction means more heat\nGrade it."},
]
def test_repair_reasks_task_without_meta_turn(monkeypatch):
calls: list[list[dict]] = []
def fake_chat(messages, max_tokens=512):
calls.append([dict(m) for m in messages])
if len(calls) == 1:
return "Sure, here is my assessment of the answer." # no JSON
return '{"score": 3, "explanation": "Mostly right.", "missed_concept": ""}'
monkeypatch.setattr(llm, "chat", fake_chat)
data = llm.chat_json(_grading_messages(), max_tokens=512)
assert data == {"score": 3, "explanation": "Mostly right.", "missed_concept": ""}, data
# The second (repair) call must re-ask the SAME grading task...
second = calls[1]
text = " ".join(m["content"] for m in second if isinstance(m.get("content"), str))
assert "Student answer: More friction" in text, "lost the grading task"
# ...with a format reminder folded in...
assert "ONLY the raw JSON" in text
# ...and WITHOUT the contaminating meta phrasing or the echoed bad reply.
assert "not valid JSON" not in text
assert all(m.get("role") != "assistant" for m in second), "bad reply re-fed"
assert len(second) == len(_grading_messages()), "injected an extra turn"
print("ok repair re-asks the task, no contaminating meta turn")
def test_augment_handles_multimodal_user_content():
# Image-PDF path passes list content (text + PIL images). The reminder must
# ride along as a text part, not crash on the list.
msgs = [
{"role": "system", "content": "Generate quiz JSON."},
{"role": "user", "content": ["<image>", "Generate the JSON array."]},
]
out = llm._augment_last_user(msgs)
assert isinstance(out[-1]["content"], list)
assert any("ONLY the raw JSON" in p for p in out[-1]["content"] if isinstance(p, str))
# original is untouched
assert msgs[-1]["content"] == ["<image>", "Generate the JSON array."]
print("ok augment appends a text reminder to multimodal content")
def test_returns_none_when_every_attempt_fails(monkeypatch):
# Both passes unparseable -> None, so callers fall back honestly (never a
# garbage grade).
monkeypatch.setattr(llm, "chat", lambda messages, max_tokens=512: "no json here")
assert llm.chat_json(_grading_messages(), max_tokens=512) is None
print("ok None when no attempt yields JSON")
if __name__ == "__main__":
import pytest
raise SystemExit(pytest.main([__file__, "-q"]))