7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
746 lines
26 KiB
Python
746 lines
26 KiB
Python
"""
|
|
Tests for benchmarks/graders.py
|
|
|
|
Tests cover:
|
|
- extract_answer_from_response function
|
|
- grade_single_result with mocked LLM
|
|
- get_evaluation_llm configuration
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
|
|
class TestExtractAnswerFromResponse:
|
|
"""Tests for the extract_answer_from_response function."""
|
|
|
|
def test_browsecomp_extracts_exact_answer(self):
|
|
"""Test extraction of exact answer from BrowseComp response."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = """
|
|
Based on my research, I found the following information.
|
|
|
|
Exact Answer: 42
|
|
Confidence: 95%
|
|
"""
|
|
result = extract_answer_from_response(
|
|
response, dataset_type="browsecomp"
|
|
)
|
|
|
|
assert result["extracted_answer"] == "42"
|
|
assert result["confidence"] == "95"
|
|
|
|
def test_browsecomp_missing_answer_returns_none(self):
|
|
"""Test handling of missing answer in BrowseComp response."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "Some response without the expected format"
|
|
result = extract_answer_from_response(
|
|
response, dataset_type="browsecomp"
|
|
)
|
|
|
|
assert result["extracted_answer"] == "None"
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_browsecomp_missing_confidence_defaults(self):
|
|
"""Test that missing confidence defaults to 100."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "Exact Answer: Paris"
|
|
result = extract_answer_from_response(
|
|
response, dataset_type="browsecomp"
|
|
)
|
|
|
|
assert result["extracted_answer"] == "Paris"
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_simpleqa_returns_full_response(self):
|
|
"""Test that SimpleQA returns the full response as the answer."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "The capital of France is Paris."
|
|
result = extract_answer_from_response(response, dataset_type="simpleqa")
|
|
|
|
assert result["extracted_answer"] == response
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_removes_citations_from_response(self):
|
|
"""Test that citations are removed from the response."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "The answer is 42 [1] according to the source [2][3]."
|
|
result = extract_answer_from_response(response, dataset_type="simpleqa")
|
|
|
|
assert "[1]" not in result["extracted_answer"]
|
|
assert "[2]" not in result["extracted_answer"]
|
|
assert "[3]" not in result["extracted_answer"]
|
|
assert "42" in result["extracted_answer"]
|
|
|
|
def test_browsecomp_case_insensitive(self):
|
|
"""Test that dataset type matching is case insensitive."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "Exact Answer: test\nConfidence: 80%"
|
|
result = extract_answer_from_response(
|
|
response, dataset_type="BROWSECOMP"
|
|
)
|
|
|
|
assert result["extracted_answer"] == "test"
|
|
assert result["confidence"] == "80"
|
|
|
|
|
|
class TestGetEvaluationLLM:
|
|
"""Tests for the get_evaluation_llm function."""
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_uses_default_config(self, mock_get_llm):
|
|
"""Test that default config is used."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
get_evaluation_llm,
|
|
DEFAULT_EVALUATION_CONFIG,
|
|
)
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
|
|
get_evaluation_llm()
|
|
|
|
mock_get_llm.assert_called_once()
|
|
call_kwargs = mock_get_llm.call_args[1]
|
|
assert (
|
|
call_kwargs["model_name"] == DEFAULT_EVALUATION_CONFIG["model_name"]
|
|
)
|
|
assert (
|
|
call_kwargs["temperature"]
|
|
== DEFAULT_EVALUATION_CONFIG["temperature"]
|
|
)
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_custom_config_overrides_defaults(self, mock_get_llm):
|
|
"""Test that custom config overrides defaults."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
|
|
custom_config = {
|
|
"model_name": "custom-model",
|
|
"temperature": 0.5,
|
|
}
|
|
get_evaluation_llm(custom_config=custom_config)
|
|
|
|
call_kwargs = mock_get_llm.call_args[1]
|
|
assert call_kwargs["model_name"] == "custom-model"
|
|
assert call_kwargs["temperature"] == 0.5
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_filters_unsupported_params(self, mock_get_llm):
|
|
"""Test that unsupported parameters are filtered out."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
|
|
custom_config = {
|
|
"model_name": "test-model",
|
|
"unsupported_param": "value",
|
|
"max_tokens": 1000, # Not supported by LDR's get_llm
|
|
}
|
|
get_evaluation_llm(custom_config=custom_config)
|
|
|
|
call_kwargs = mock_get_llm.call_args[1]
|
|
assert "unsupported_param" not in call_kwargs
|
|
assert "max_tokens" not in call_kwargs
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_extracts_api_key_from_settings_snapshot(self, mock_get_llm):
|
|
"""Test that API key is extracted from settings snapshot."""
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
|
|
settings_snapshot = {
|
|
"llm.openai_endpoint.api_key": {"value": "test-api-key"}
|
|
}
|
|
get_evaluation_llm(settings_snapshot=settings_snapshot)
|
|
|
|
# Should not raise and should call get_llm
|
|
mock_get_llm.assert_called_once()
|
|
|
|
|
|
class TestGradeSingleResult:
|
|
"""Tests for the grade_single_result function."""
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grades_correctly(self, mock_get_eval_llm):
|
|
"""Test that grade_single_result grades correctly."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
# Mock the LLM response
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = """
|
|
Extracted Answer: Paris
|
|
Reasoning: The model correctly identified Paris as the capital of France.
|
|
Correct: yes
|
|
"""
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "What is the capital of France?",
|
|
"correct_answer": "Paris",
|
|
"response": "The capital of France is Paris.",
|
|
}
|
|
|
|
graded = grade_single_result(result_data, dataset_type="simpleqa")
|
|
|
|
assert graded["is_correct"] is True
|
|
assert "Paris" in graded["extracted_by_grader"]
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_handles_grading_error(self, mock_get_eval_llm):
|
|
"""Test that grade_single_result handles errors gracefully."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
# Mock the LLM to raise an error during invoke (inside the try block)
|
|
mock_llm = MagicMock()
|
|
mock_llm.invoke.side_effect = Exception("LLM invoke error")
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "test",
|
|
"correct_answer": "answer",
|
|
"response": "response",
|
|
}
|
|
|
|
graded = grade_single_result(result_data)
|
|
|
|
assert graded["is_correct"] is False
|
|
assert "grading_error" in graded
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_browsecomp_grading_format(self, mock_get_eval_llm):
|
|
"""Test BrowseComp-specific grading format extraction."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = """
|
|
extracted_final_answer: 42
|
|
reasoning: The model found the correct answer by analyzing the data.
|
|
correct: yes
|
|
confidence: 95
|
|
"""
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "What is the answer?",
|
|
"correct_answer": "42",
|
|
"response": "The answer is 42.",
|
|
}
|
|
|
|
graded = grade_single_result(result_data, dataset_type="browsecomp")
|
|
|
|
assert graded["is_correct"] is True
|
|
assert graded["extracted_by_grader"] == "42"
|
|
assert graded["graded_confidence"] == "95"
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grading_with_no_judgment(self, mock_get_eval_llm):
|
|
"""Test grading when LLM doesn't provide clear judgment."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = "Some response without proper format"
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "test",
|
|
"correct_answer": "answer",
|
|
"response": "response",
|
|
}
|
|
|
|
graded = grade_single_result(result_data, dataset_type="simpleqa")
|
|
|
|
# Should default to False when no clear judgment
|
|
assert graded["is_correct"] is False
|
|
assert graded["extracted_by_grader"] == "None"
|
|
|
|
|
|
class TestGradeResults:
|
|
"""Tests for grade_results function (batch grading)."""
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_results_processes_all_items(self, mock_get_eval_llm):
|
|
"""Test that grade_results processes all items in file."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import grade_results
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = """
|
|
Extracted Answer: test
|
|
Reasoning: Test reasoning
|
|
Correct: yes
|
|
"""
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create input file
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
for i in range(3):
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": f"Question {i}",
|
|
"correct_answer": f"Answer {i}",
|
|
"response": f"Response {i}",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
results = grade_results(input_file, output_file)
|
|
|
|
assert len(results) == 3
|
|
assert all(r["is_correct"] for r in results)
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_results_invokes_progress_callback(self, mock_get_eval_llm):
|
|
"""Test that progress callback is invoked during grading."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import grade_results
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = (
|
|
"Extracted Answer: test\nReasoning: test\nCorrect: yes"
|
|
)
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
callback_invocations = []
|
|
|
|
def progress_callback(idx, total, data):
|
|
callback_invocations.append(
|
|
{"idx": idx, "total": total, "data": data}
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
grade_results(
|
|
input_file, output_file, progress_callback=progress_callback
|
|
)
|
|
|
|
# Should have multiple invocations (grading and graded)
|
|
assert len(callback_invocations) >= 2
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_results_handles_errors_gracefully(self, mock_get_eval_llm):
|
|
"""Test that grade_results handles individual grading errors."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import grade_results
|
|
|
|
mock_llm = MagicMock()
|
|
# First call succeeds, second fails
|
|
mock_response = MagicMock()
|
|
mock_response.content = "Extracted Answer: test\nCorrect: yes"
|
|
mock_llm.invoke.side_effect = [
|
|
mock_response,
|
|
Exception("Grading error"),
|
|
]
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
for i in range(2):
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": f"Q{i}",
|
|
"correct_answer": f"A{i}",
|
|
"response": f"R{i}",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
results = grade_results(input_file, output_file)
|
|
|
|
# Should have both results (one success, one error)
|
|
assert len(results) == 2
|
|
# First should be correct
|
|
assert results[0]["is_correct"] is True
|
|
# Second should have error
|
|
assert "grading_error" in results[1]
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_results_writes_output_file(self, mock_get_eval_llm):
|
|
"""Test that grade_results writes to output file."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import grade_results
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = "Extracted Answer: test\nCorrect: yes"
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
f.write(
|
|
json.dumps(
|
|
{"problem": "Q", "correct_answer": "A", "response": "R"}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
grade_results(input_file, output_file)
|
|
|
|
# Output file should exist
|
|
with open(output_file, "r") as f:
|
|
lines = f.readlines()
|
|
|
|
assert len(lines) == 1
|
|
result = json.loads(lines[0])
|
|
assert "is_correct" in result
|
|
|
|
|
|
class TestHumanEvaluation:
|
|
"""Tests for human_evaluation function."""
|
|
|
|
def test_human_evaluation_noninteractive_mode(self):
|
|
"""Test human evaluation in non-interactive mode."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import human_evaluation
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "What is 2+2?",
|
|
"correct_answer": "4",
|
|
"response": "The answer is 4.",
|
|
"extracted_answer": "4",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
results = human_evaluation(
|
|
input_file, output_file, interactive=False
|
|
)
|
|
|
|
assert len(results) == 1
|
|
# Non-interactive defaults to is_correct=False
|
|
assert results[0]["is_correct"] is False
|
|
assert results[0]["human_evaluation"] is True
|
|
|
|
def test_human_evaluation_writes_output(self):
|
|
"""Test that human evaluation writes to output file."""
|
|
import tempfile
|
|
import json
|
|
from local_deep_research.benchmarks.graders import human_evaluation
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
input_file = f"{tmpdir}/input.jsonl"
|
|
with open(input_file, "w") as f:
|
|
f.write(
|
|
json.dumps(
|
|
{
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
output_file = f"{tmpdir}/output.jsonl"
|
|
|
|
human_evaluation(input_file, output_file, interactive=False)
|
|
|
|
with open(output_file, "r") as f:
|
|
lines = f.readlines()
|
|
|
|
assert len(lines) == 1
|
|
result = json.loads(lines[0])
|
|
assert "human_evaluation" in result
|
|
assert result["human_evaluation"] is True
|
|
|
|
|
|
class TestGradeSingleResultEdgeCases:
|
|
"""Edge case tests for grade_single_result."""
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_with_empty_response(self, mock_get_eval_llm):
|
|
"""Test grading with empty model response."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = ""
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "Question",
|
|
"correct_answer": "Answer",
|
|
"response": "",
|
|
}
|
|
|
|
graded = grade_single_result(result_data)
|
|
|
|
assert graded["is_correct"] is False
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_with_llm_no_invoke(self, mock_get_eval_llm):
|
|
"""Test grading when LLM doesn't have invoke method."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
# Create LLM without invoke method but with close for cleanup
|
|
mock_llm = MagicMock(spec=["close"])
|
|
mock_llm.__call__ = MagicMock(
|
|
return_value="Extracted Answer: test\nCorrect: yes"
|
|
)
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
|
|
graded = grade_single_result(result_data)
|
|
|
|
# Should still work via fallback
|
|
assert "is_correct" in graded
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_with_chat_messages_attribute(self, mock_get_eval_llm):
|
|
"""Test grading with LLM that has chat_messages attribute."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_llm.chat_messages = True # Has this attribute
|
|
mock_response = MagicMock()
|
|
mock_response.content = "Extracted Answer: test\nCorrect: yes"
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
|
|
graded = grade_single_result(result_data)
|
|
|
|
assert graded["is_correct"] is True
|
|
# Should have called invoke with HumanMessage
|
|
mock_llm.invoke.assert_called_once()
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_simpleqa_correct_no(self, mock_get_eval_llm):
|
|
"""Test SimpleQA grading with 'no' judgment."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = """
|
|
Extracted Answer: wrong answer
|
|
Reasoning: The model's answer is incorrect.
|
|
Correct: no
|
|
"""
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
result_data = {
|
|
"problem": "What is 2+2?",
|
|
"correct_answer": "4",
|
|
"response": "The answer is 5.",
|
|
}
|
|
|
|
graded = grade_single_result(result_data, dataset_type="simpleqa")
|
|
|
|
assert graded["is_correct"] is False
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_evaluation_llm")
|
|
def test_grade_preserves_settings_snapshot(self, mock_get_eval_llm):
|
|
"""Test that settings_snapshot is passed to get_evaluation_llm."""
|
|
from local_deep_research.benchmarks.graders import grade_single_result
|
|
|
|
mock_llm = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.content = "Extracted Answer: test\nCorrect: yes"
|
|
mock_llm.invoke.return_value = mock_response
|
|
mock_get_eval_llm.return_value = mock_llm
|
|
|
|
settings_snapshot = {"llm.api_key": "test-key"}
|
|
|
|
result_data = {
|
|
"problem": "Q",
|
|
"correct_answer": "A",
|
|
"response": "R",
|
|
}
|
|
|
|
grade_single_result(result_data, settings_snapshot=settings_snapshot)
|
|
|
|
# Verify settings_snapshot was passed
|
|
mock_get_eval_llm.assert_called_once()
|
|
call_args = mock_get_eval_llm.call_args
|
|
assert (
|
|
call_args[0][1] == settings_snapshot
|
|
or call_args[1].get("settings_snapshot") == settings_snapshot
|
|
)
|
|
|
|
|
|
class TestExtractAnswerEdgeCases:
|
|
"""Edge case tests for extract_answer_from_response."""
|
|
|
|
def test_extract_handles_multiline_answer(self):
|
|
"""Test extraction of multiline answers."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = """Based on my research:
|
|
|
|
Exact Answer: This is a
|
|
multiline answer
|
|
Confidence: 90%
|
|
"""
|
|
result = extract_answer_from_response(response, "browsecomp")
|
|
|
|
# Should capture first line after "Exact Answer:"
|
|
assert "This is a" in result["extracted_answer"]
|
|
|
|
def test_extract_handles_special_characters(self):
|
|
"""Test extraction handles special characters."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "The answer is: $100 (USD) [according to source]."
|
|
result = extract_answer_from_response(response, "simpleqa")
|
|
|
|
# Only numeric citations like [1], [2] are removed
|
|
# Text-based brackets like [according to source] are preserved
|
|
assert "$100" in result["extracted_answer"]
|
|
|
|
def test_extract_empty_response(self):
|
|
"""Test extraction with empty response."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
result = extract_answer_from_response("", "simpleqa")
|
|
|
|
assert result["extracted_answer"] == ""
|
|
assert result["confidence"] == "100"
|
|
|
|
def test_extract_browsecomp_no_exact_answer(self):
|
|
"""Test BrowseComp extraction without 'Exact Answer' marker."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "The value is 42."
|
|
result = extract_answer_from_response(response, "browsecomp")
|
|
|
|
assert result["extracted_answer"] == "None"
|
|
|
|
def test_extract_removes_multiple_citations(self):
|
|
"""Test that multiple citations are all removed."""
|
|
from local_deep_research.benchmarks.graders import (
|
|
extract_answer_from_response,
|
|
)
|
|
|
|
response = "First point [1], second point [2], third point [3][4][5]."
|
|
result = extract_answer_from_response(response, "simpleqa")
|
|
|
|
assert "[1]" not in result["extracted_answer"]
|
|
assert "[2]" not in result["extracted_answer"]
|
|
assert "[5]" not in result["extracted_answer"]
|
|
|
|
|
|
class TestSettingsSnapshotPropagation:
|
|
"""get_evaluation_llm must thread settings_snapshot into get_llm.
|
|
|
|
Regression (PR #4300 review): the snapshot was used only to read the
|
|
api_key and never passed to get_llm. Combined with the new snapshot-less
|
|
fail-closed LLM gate, a cloud grader (openai) was refused with
|
|
PolicyDeniedError because get_llm saw no snapshot to evaluate policy.
|
|
"""
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_snapshot_forwarded_to_get_llm(self, mock_get_llm):
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
snapshot = {"policy.egress_scope": "both", "llm.provider": "openai"}
|
|
|
|
get_evaluation_llm(settings_snapshot=snapshot)
|
|
|
|
call_kwargs = mock_get_llm.call_args[1]
|
|
assert "settings_snapshot" in call_kwargs, (
|
|
"get_evaluation_llm must forward settings_snapshot to get_llm"
|
|
)
|
|
assert call_kwargs["settings_snapshot"] is snapshot
|
|
|
|
@patch("local_deep_research.benchmarks.graders.get_llm")
|
|
def test_none_snapshot_still_forwarded(self, mock_get_llm):
|
|
from local_deep_research.benchmarks.graders import get_evaluation_llm
|
|
|
|
mock_get_llm.return_value = Mock()
|
|
get_evaluation_llm()
|
|
call_kwargs = mock_get_llm.call_args[1]
|
|
# Explicitly forwarded (as None) — get_llm's own gate then decides.
|
|
assert call_kwargs.get("settings_snapshot") is None
|