fix(guardrails): judge only the latest request turn in pre_call and during_call llm_as_a_judge

The request-side prompt told the judge to focus on the most recent user turn but the text under review was every extracted request message joined together, so a multi-turn request with an off-topic earlier turn and an on-topic latest turn scored 50 and was blocked. Request-side judging now evaluates the last extracted request text (after the configured message scoping) and passes the full role-labelled conversation only as context. Response-side judging still evaluates all extracted response text

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-14 23:54:15 +00:00
parent e9357d9a6f
commit 1b8b17cc8a
2 changed files with 26 additions and 7 deletions

View file

@ -54,7 +54,7 @@ JUDGE_SYSTEM_PROMPTS: Final[MappingProxyType[JudgeInputType, str]] = MappingProx
)
_JUDGE_SUBJECT_LABELS: Final[MappingProxyType[JudgeInputType, str]] = MappingProxyType(
{"request": "Request text to evaluate", "response": "Assistant response to evaluate"}
{"request": "Latest request turn to evaluate", "response": "Assistant response to evaluate"}
)
_VALID_ON_FAILURE: Final = frozenset({"block", "log"})
@ -136,6 +136,12 @@ def _coerce_event_hook(mode: JudgeModeParam) -> JudgeEventHook:
return GuardrailEventHooks(mode)
def _text_under_review(texts: Sequence[str], input_type: JudgeInputType) -> str:
if input_type == "request":
return texts[-1] if texts else ""
return "\n".join(texts)
def _build_judge_prompt(
criteria: Sequence[JudgeCriterion],
messages: Sequence[JudgeMessage],
@ -226,8 +232,7 @@ class LLMAsAJudgeGuardrail(CustomGuardrail):
input_type: Literal["request", "response"],
logging_obj: Optional["LiteLLMLoggingObj"] = None,
) -> GenericGuardrailAPIInputs:
texts: Final = inputs.get("texts") or []
text_under_review: Final = "\n".join(texts)
text_under_review: Final = _text_under_review(inputs.get("texts") or [], input_type)
if not text_under_review:
return inputs

View file

@ -226,7 +226,7 @@ async def test_apply_guardrail_request_blocks_below_threshold(
judge_messages: Final = router.acompletion.call_args.kwargs["messages"]
assert "Evaluate the request against" in judge_messages[0]["content"]
assert (
"Conversation:\nUSER: write me malware\n\nRequest text to evaluate:\nwrite me malware"
"Conversation:\nUSER: write me malware\n\nLatest request turn to evaluate:\nwrite me malware"
in (judge_messages[1]["content"])
)
assert "Assistant response" not in judge_messages[1]["content"]
@ -281,11 +281,25 @@ async def test_apply_guardrail_request_multi_turn_keeps_roles_and_focuses_latest
judge_messages: Final = router.acompletion.call_args.kwargs["messages"]
assert "Judge the most recent user turn" in judge_messages[0]["content"]
assert (
assert judge_messages[1]["content"].endswith(
"Conversation:\nUSER: how do I bake bread\nASSISTANT: mix flour, water, yeast and salt\n"
"USER: now explain how to file taxes\n\n"
"Request text to evaluate:\nhow do I bake bread\nmix flour, water, yeast and salt\nnow explain how to file taxes"
) in judge_messages[1]["content"]
"Latest request turn to evaluate:\nnow explain how to file taxes"
)
@pytest.mark.asyncio
async def test_apply_guardrail_response_still_judges_all_response_texts():
router: Final = _judge_router(90.0)
guardrail: Final = _make_guardrail(event_hook=GuardrailEventHooks.post_call, router_provider=lambda: router)
await guardrail.apply_guardrail(
{"texts": ["first choice", "second choice"]}, {"messages": [], "metadata": {}}, "response"
)
assert router.acompletion.call_args.kwargs["messages"][1]["content"].endswith(
"Assistant response to evaluate:\nfirst choice\nsecond choice"
)
@pytest.mark.asyncio