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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixed

- Reject ambiguous LLM-judge JSON with duplicate keys or unexpected top-level fields; require the exact mode-specific schema including advisory `accepted`.
- Require compiled Rust ownership for public `s_x2` and `person_fit`, including prior-mean S-X² dispatch, with fail-closed errors when the core is missing.
- Validate parallel-analysis integer controls and bound random-eigenvalue workspace before Rust dispatch.
- Cap LLM-judge response JSON nesting at 32 levels before parse to prevent recursive-object resource exhaustion.
Expand Down
35 changes: 30 additions & 5 deletions python/fast_mlsirm/llm_judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ class JudgeFormatError(ValueError):
"""Raised when a judge response is not a bounded, interpretable decision."""


class _DuplicateJsonKeyError(ValueError):
"""Internal signal for duplicate JSON object members."""


def _duplicate_free_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, member in pairs:
if key in value:
raise _DuplicateJsonKeyError(key)
value[key] = member
return value


def _category_count(value: Any) -> int:
if (
not isinstance(value, int)
Expand Down Expand Up @@ -296,15 +309,21 @@ def _validate_raw_json_depth(content: str) -> None:
depth -= 1


def _response_object(raw: str) -> dict[str, Any]:
def _response_object(raw: str, *, required_fields: set[str]) -> dict[str, Any]:
text = raw.strip()
_validate_raw_json_depth(text)
try:
value = json.loads(text)
value = json.loads(text, object_pairs_hook=_duplicate_free_object)
except _DuplicateJsonKeyError as exc:
raise JudgeFormatError("judge response contains duplicate JSON object keys") from exc
except json.JSONDecodeError as exc:
raise JudgeFormatError("judge response JSON is invalid") from exc
if not isinstance(value, dict):
raise JudgeFormatError("judge response must be a JSON object")
if set(value) != required_fields:
raise JudgeFormatError(
"judge response must contain exactly the required fields"
)
return value


Expand Down Expand Up @@ -428,10 +447,16 @@ def judge(
raw = _bounded_text(completion.get("answer"), "judge answer")
except ValueError as exc:
raise JudgeFormatError(str(exc)) from exc
parsed = _response_object(raw)
criterion_field = (
"criterion_categories" if category_count is not None else "criterion_scores"
)
parsed = _response_object(
raw,
required_fields={"score", "accepted", "rationale", criterion_field},
)
advisory_accepted = parsed.get("accepted")
if advisory_accepted is not None and not isinstance(advisory_accepted, bool):
raise JudgeFormatError("accepted must be a boolean when present")
if not isinstance(advisory_accepted, bool):
raise JudgeFormatError("accepted must be a boolean")
try:
rationale = _bounded_text(parsed.get("rationale"), "rationale")
except ValueError as exc:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_llm_judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ def test_judge_rejects_wrapped_or_fenced_json() -> None:
)



def test_judge_rejects_duplicate_and_unknown_top_level_fields() -> None:
duplicate = (
'{"score":0.8,"accepted":true,"rationale":"supported",'
'"criterion_scores":{"task_alignment":0.8,"factual_support":0.8},'
'"score":0.2}'
)
unknown = json.loads(_payload())
unknown["unexpected"] = "ignored fields are unsafe"
for answer in (duplicate, json.dumps(unknown)):
with pytest.raises(JudgeFormatError, match="exactly|duplicate"):
ContextualOrchestratorJudge(_FakeOrchestrator(answer)).judge(
task="task",
answer="answer",
criteria=CRITERIA,
)

Comment on lines +112 to +127

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target lines ---'
cat -n tests/test_llm_judge.py | sed -n '100,135p'

printf '%s\n' '--- Ruff configuration and references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  'RUF043|ruff|pytest-raises|exactly\|duplicate' \
  pyproject.toml setup.cfg tox.ini .ruff.toml ruff.toml tests 2>/dev/null || true

printf '%s\n' '--- available Ruff executable ---'
if command -v ruff >/dev/null 2>&1; then
  ruff --version
  ruff check tests/test_llm_judge.py --select RUF043
else
  echo 'ruff is not installed'
fi

printf '%s\n' '--- relevant dependency declarations ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  'ruff|pytest' . 2>/dev/null | head -80

Repository: ContextualWisdomLab/fast-mlsirm

Length of output: 8441


Use a raw regex literal for match.

Ruff RUF043 reports match="exactly|duplicate". Change it to match=r"exactly|duplicate".

🧰 Tools
🪛 ast-grep (0.45.1)

[info] 118-118: use jsonify instead of json.dumps for JSON output
Context: json.dumps(unknown)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 Ruff (0.16.1)

[warning] 120-120: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_llm_judge.py` around lines 111 - 126, Update the pytest.raises
call in test_judge_rejects_duplicate_and_unknown_top_level_fields to use a raw
regex string for its match argument, changing the existing "exactly|duplicate"
pattern to the raw-string form while preserving the test behavior.

Source: Linters/SAST tools


def test_judge_rejects_duplicate_nested_criterion_fields() -> None:
answer = (
'{"score":0.8,"accepted":true,"rationale":"supported",'
'"criterion_scores":{"task_alignment":0.8,"task_alignment":0.2,'
'"factual_support":0.8}}'
)
with pytest.raises(JudgeFormatError, match="duplicate"):
ContextualOrchestratorJudge(_FakeOrchestrator(answer)).judge(
task="task",
answer="answer",
criteria=CRITERIA,
)


def test_judge_result_projects_only_multiple_criteria_to_irt_items() -> None:
result = ContextualOrchestratorJudge(_FakeOrchestrator(_payload())).judge(
task="task",
Expand Down
Loading