7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
546 lines
19 KiB
Python
546 lines
19 KiB
Python
"""Extra coverage tests for benchmarks/graders.py targeting uncovered branches.
|
|
|
|
Targets:
|
|
- LLM invoke __call__ fallback (line 203)
|
|
- BrowseComp extraction edge cases (missing fields, multiline, malformed)
|
|
- SimpleQA "no" judgment
|
|
- Batch _grade_results_inner per-item exception
|
|
- grade_single_result chat_messages path
|
|
- extract_answer_from_response: SimpleQA path, BrowseComp with/without fields
|
|
- grade_single_result: exception path (grading error)
|
|
- _grade_results_inner: browsecomp dataset, progress callback, with existing output file
|
|
- get_evaluation_llm: custom config override, openai_endpoint API key from snapshot
|
|
"""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
MODULE = "local_deep_research.benchmarks.graders"
|
|
|
|
|
|
def _make_llm_with_invoke(response_text):
|
|
"""Create a mock LLM with invoke() returning an object with .content."""
|
|
llm = MagicMock()
|
|
result = MagicMock()
|
|
result.content = response_text
|
|
llm.invoke.return_value = result
|
|
# Ensure callable interface also exists
|
|
llm.return_value = response_text
|
|
return llm
|
|
|
|
|
|
def _make_llm_callable_only(response_text):
|
|
"""Create a mock LLM without invoke attribute — uses __call__ fallback."""
|
|
llm = MagicMock(spec=[]) # No attributes
|
|
llm.return_value = response_text
|
|
return llm
|
|
|
|
|
|
def _make_llm_with_chat_messages(response_text):
|
|
"""Create a mock LLM with both invoke and chat_messages."""
|
|
llm = MagicMock()
|
|
llm.chat_messages = True
|
|
result = MagicMock()
|
|
result.content = response_text
|
|
llm.invoke.return_value = result
|
|
return llm
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LLM invoke __call__ fallback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLLMCallFallback:
|
|
def test_callable_only_llm(self):
|
|
"""LLM without invoke uses __call__ fallback in grade_single_result."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
llm = _make_llm_callable_only(
|
|
"Extracted Answer: Paris\nReasoning: Capital of France\nCorrect: yes"
|
|
)
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "Capital of France?",
|
|
"correct_answer": "Paris",
|
|
"response": "Paris is the capital.",
|
|
},
|
|
dataset_type="simpleqa",
|
|
)
|
|
|
|
assert result["is_correct"] is True
|
|
assert result["extracted_by_grader"] == "Paris"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BrowseComp extraction edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowseCompExtraction:
|
|
def test_missing_reasoning_field(self):
|
|
"""Response without reasoning → still extracts answer and correct."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
response = "extracted_final_answer: The Moon\ncorrect: yes"
|
|
llm = _make_llm_with_invoke(response)
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "What orbits Earth?",
|
|
"correct_answer": "The Moon",
|
|
"response": "The Moon orbits Earth",
|
|
},
|
|
dataset_type="browsecomp",
|
|
)
|
|
|
|
assert result["is_correct"] is True
|
|
assert result["extracted_by_grader"] == "The Moon"
|
|
|
|
def test_multiline_reasoning(self):
|
|
"""Response with multiline reasoning (re.DOTALL) → extracts correctly."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
response = (
|
|
"extracted_final_answer: 42\n"
|
|
"reasoning: The answer is 42\nbecause of the ultimate question\n\n"
|
|
"correct: yes\nconfidence: 95"
|
|
)
|
|
llm = _make_llm_with_invoke(response)
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "Answer to life?",
|
|
"correct_answer": "42",
|
|
"response": "42",
|
|
},
|
|
dataset_type="browsecomp",
|
|
)
|
|
|
|
assert result["is_correct"] is True
|
|
assert result["graded_confidence"] == "95"
|
|
|
|
def test_completely_malformed_response(self):
|
|
"""Malformed response → returns defaults (None/False)."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
llm = _make_llm_with_invoke("This is totally garbled output xyz")
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "Test?",
|
|
"correct_answer": "A",
|
|
"response": "B",
|
|
},
|
|
dataset_type="browsecomp",
|
|
)
|
|
|
|
assert result["is_correct"] is False
|
|
assert result["extracted_by_grader"] == "None"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SimpleQA "no" judgment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSimpleQANoJudgment:
|
|
def test_correct_no(self):
|
|
"""SimpleQA with 'Correct: no' → is_correct=False."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
response = (
|
|
"Extracted Answer: Wrong\nReasoning: Completely wrong\nCorrect: no"
|
|
)
|
|
llm = _make_llm_with_invoke(response)
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "What is 2+2?",
|
|
"correct_answer": "4",
|
|
"response": "5",
|
|
},
|
|
dataset_type="simpleqa",
|
|
)
|
|
|
|
assert result["is_correct"] is False
|
|
assert result["extracted_by_grader"] == "Wrong"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Batch _grade_results_inner per-item exception
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBatchGradePerItemException:
|
|
def test_one_item_throws_others_still_graded(self):
|
|
"""One result throwing during grading → error recorded, others graded."""
|
|
from local_deep_research.benchmarks.graders import _grade_results_inner
|
|
|
|
# Create results file with 2 entries
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".jsonl", delete=False
|
|
) as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q1",
|
|
"correct_answer": "A1",
|
|
"response": "R1",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q2",
|
|
"correct_answer": "A2",
|
|
"response": "R2",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
results_file = f.name
|
|
|
|
output_file = results_file + ".graded"
|
|
|
|
# LLM that fails on first call, succeeds on second
|
|
llm = MagicMock()
|
|
call_count = 0
|
|
|
|
def invoke_side_effect(prompt):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
raise RuntimeError("LLM failed")
|
|
result = MagicMock()
|
|
result.content = (
|
|
"Extracted Answer: A2\nReasoning: Correct\nCorrect: yes"
|
|
)
|
|
return result
|
|
|
|
llm.invoke = invoke_side_effect
|
|
|
|
try:
|
|
results = _grade_results_inner(
|
|
llm, results_file, output_file, "simpleqa", None
|
|
)
|
|
|
|
assert len(results) == 2
|
|
assert "grading_error" in results[0]
|
|
assert results[1].get("is_correct") is True
|
|
finally:
|
|
Path(results_file).unlink(missing_ok=True)
|
|
Path(output_file).unlink(missing_ok=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# grade_single_result chat_messages path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestChatMessagesPath:
|
|
def test_chat_messages_format(self):
|
|
"""LLM with chat_messages attribute → uses HumanMessage format."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
response_text = (
|
|
"Extracted Answer: Paris\nReasoning: Capital\nCorrect: yes"
|
|
)
|
|
llm = _make_llm_with_chat_messages(response_text)
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "Capital of France?",
|
|
"correct_answer": "Paris",
|
|
"response": "Paris",
|
|
},
|
|
dataset_type="simpleqa",
|
|
)
|
|
|
|
assert result["is_correct"] is True
|
|
# Verify HumanMessage was used (invoke called with list)
|
|
call_args = llm.invoke.call_args[0][0]
|
|
assert isinstance(call_args, list)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_answer_from_response
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtractAnswerFromResponse:
|
|
def test_simpleqa_strips_citations(self):
|
|
"""SimpleQA: citations [1] [2] removed, whole response returned."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
result = extract_answer_from_response(
|
|
"Paris is the capital [1] of France [2].", "simpleqa"
|
|
)
|
|
assert result["extracted_answer"] == "Paris is the capital of France ."
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_browsecomp_extracts_answer_and_confidence(self):
|
|
"""BrowseComp: extracts Exact Answer and Confidence."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
result = extract_answer_from_response(
|
|
"Exact Answer: The Moon\nConfidence: 95%", "browsecomp"
|
|
)
|
|
assert result["extracted_answer"] == "The Moon"
|
|
assert result["confidence"] == "95"
|
|
|
|
def test_browsecomp_no_match(self):
|
|
"""BrowseComp: no matching fields → defaults."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
result = extract_answer_from_response("Random text here", "browsecomp")
|
|
assert result["extracted_answer"] == "None"
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_browsecomp_no_confidence(self):
|
|
"""BrowseComp: answer found but no confidence → default 100."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
result = extract_answer_from_response(
|
|
"Exact Answer: Jupiter", "browsecomp"
|
|
)
|
|
assert result["extracted_answer"] == "Jupiter"
|
|
assert result["confidence"] == "100"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# grade_single_result — exception path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGradeSingleResultException:
|
|
def test_llm_exception_returns_error(self):
|
|
"""LLM raises exception → returns grading_error dict."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
llm = MagicMock()
|
|
llm.invoke.side_effect = RuntimeError("API down")
|
|
|
|
with patch(f"{MODULE}.get_evaluation_llm", return_value=llm):
|
|
result = grade_single_result(
|
|
{
|
|
"problem": "Q?",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
},
|
|
dataset_type="simpleqa",
|
|
)
|
|
|
|
assert "grading_error" in result
|
|
assert result["is_correct"] is False
|
|
assert result["graded_confidence"] == "0"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _grade_results_inner — browsecomp + progress callback + existing output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGradeResultsInnerBrowsecomp:
|
|
def test_browsecomp_batch(self):
|
|
"""Batch grading with browsecomp dataset type."""
|
|
from local_deep_research.benchmarks.graders import _grade_results_inner
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".jsonl", delete=False
|
|
) as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q1",
|
|
"correct_answer": "A1",
|
|
"response": "R1",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
results_file = f.name
|
|
|
|
output_file = results_file + ".graded"
|
|
|
|
llm = MagicMock()
|
|
result_obj = MagicMock()
|
|
result_obj.content = (
|
|
"extracted_final_answer: A1\n"
|
|
"reasoning: Looks correct\n\n"
|
|
"correct: yes\nconfidence: 90"
|
|
)
|
|
llm.invoke.return_value = result_obj
|
|
|
|
try:
|
|
results = _grade_results_inner(
|
|
llm, results_file, output_file, "browsecomp", None
|
|
)
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["is_correct"] is True
|
|
assert results[0]["graded_confidence"] == "90"
|
|
finally:
|
|
Path(results_file).unlink(missing_ok=True)
|
|
Path(output_file).unlink(missing_ok=True)
|
|
|
|
def test_with_progress_callback(self):
|
|
"""Progress callback called for each result."""
|
|
from local_deep_research.benchmarks.graders import _grade_results_inner
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".jsonl", delete=False
|
|
) as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q1",
|
|
"correct_answer": "A1",
|
|
"response": "R1",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
results_file = f.name
|
|
|
|
output_file = results_file + ".graded"
|
|
|
|
llm = MagicMock()
|
|
result_obj = MagicMock()
|
|
result_obj.content = (
|
|
"Extracted Answer: A1\nReasoning: Good\nCorrect: yes"
|
|
)
|
|
llm.invoke.return_value = result_obj
|
|
|
|
callback_calls = []
|
|
|
|
def progress_cb(idx, total, meta):
|
|
callback_calls.append((idx, total, meta["status"]))
|
|
|
|
try:
|
|
_grade_results_inner(
|
|
llm, results_file, output_file, "simpleqa", progress_cb
|
|
)
|
|
|
|
# Should be called twice per item: "grading" and "graded"
|
|
assert len(callback_calls) == 2
|
|
assert callback_calls[0][2] == "grading"
|
|
assert callback_calls[1][2] == "graded"
|
|
finally:
|
|
Path(results_file).unlink(missing_ok=True)
|
|
Path(output_file).unlink(missing_ok=True)
|
|
|
|
def test_existing_output_file_removed(self):
|
|
"""Existing output file gets removed before grading starts."""
|
|
from local_deep_research.benchmarks.graders import _grade_results_inner
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".jsonl", delete=False
|
|
) as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
results_file = f.name
|
|
|
|
output_file = results_file + ".graded"
|
|
# Create pre-existing output file
|
|
Path(output_file).write_text("old data\n")
|
|
|
|
llm = MagicMock()
|
|
result_obj = MagicMock()
|
|
result_obj.content = (
|
|
"Extracted Answer: A\nReasoning: Correct\nCorrect: yes"
|
|
)
|
|
llm.invoke.return_value = result_obj
|
|
|
|
try:
|
|
_grade_results_inner(
|
|
llm, results_file, output_file, "simpleqa", None
|
|
)
|
|
# Output file should only contain the new result, not "old data"
|
|
content = Path(output_file).read_text()
|
|
assert "old data" not in content
|
|
finally:
|
|
Path(results_file).unlink(missing_ok=True)
|
|
Path(output_file).unlink(missing_ok=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_evaluation_llm
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetEvaluationLLM:
|
|
def test_custom_config_override(self):
|
|
"""Custom config merges with defaults."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
with patch(f"{MODULE}.get_llm") as mock_get_llm:
|
|
mock_get_llm.return_value = MagicMock()
|
|
get_evaluation_llm(custom_config={"model_name": "custom-model"})
|
|
|
|
call_kwargs = mock_get_llm.call_args.kwargs
|
|
assert call_kwargs["model_name"] == "custom-model"
|
|
|
|
def test_api_key_from_settings_snapshot(self):
|
|
"""API key extracted from settings snapshot dict format."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
snapshot = {"llm.openai_endpoint.api_key": {"value": "sk-test-key-123"}}
|
|
|
|
with patch(f"{MODULE}.get_llm") as mock_get_llm:
|
|
mock_get_llm.return_value = MagicMock()
|
|
get_evaluation_llm(settings_snapshot=snapshot)
|
|
|
|
# Should not raise or warn about missing API key
|
|
mock_get_llm.assert_called_once()
|
|
|
|
def test_api_key_from_settings_snapshot_plain_string(self):
|
|
"""API key as plain string (not dict) from snapshot."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
snapshot = {"llm.openai_endpoint.api_key": "sk-plain-key"}
|
|
|
|
with patch(f"{MODULE}.get_llm") as mock_get_llm:
|
|
mock_get_llm.return_value = MagicMock()
|
|
get_evaluation_llm(settings_snapshot=snapshot)
|
|
|
|
mock_get_llm.assert_called_once()
|
|
|
|
def test_no_snapshot_no_api_key(self):
|
|
"""No snapshot, no API key → warns but proceeds."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
with patch(f"{MODULE}.get_llm") as mock_get_llm:
|
|
mock_get_llm.return_value = MagicMock()
|
|
get_evaluation_llm()
|
|
|
|
mock_get_llm.assert_called_once()
|