Files
siemens_ragas/tests/test_offline_eval.py

311 lines
12 KiB
Python
Raw Normal View History

2026-06-12 14:02:15 +08:00
import os
import unittest
from pathlib import Path
from unittest import mock
import pandas as pd
from pydantic_settings import SettingsConfigDict
from rag_eval.config.loader import load_scenario
from rag_eval.datasets.normalizers import normalize_records
from rag_eval.execution.evaluator import Evaluator
from rag_eval.metrics.pipeline import MetricPipeline
from rag_eval.reporting.summary import build_summary_markdown
from rag_eval.reporting.writers import write_run_artifacts
from rag_eval.settings import EvaluationSettings
from rag_eval.shared.models import EvaluationResult
class EnvOnlySettings(EvaluationSettings):
model_config = SettingsConfigDict(env_file=None, extra="ignore")
class FakeMetric:
def __init__(self, value: float):
self.value = value
async def ascore(self, **kwargs):
class Result:
def __init__(self, value: float):
self.value = value
return Result(self.value)
class SlowMetric:
async def ascore(self, **kwargs):
await __import__("asyncio").sleep(0.05)
return type("Result", (), {"value": 1.0})()
class OpenAIConfigTests(unittest.TestCase):
def test_openai_client_kwargs_without_base_url(self) -> None:
with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=True):
settings = EnvOnlySettings()
self.assertEqual(
settings.openai_client_kwargs,
{"api_key": "test-key", "base_url": "http://6.86.80.4:30080/v1", "timeout": 30.0},
)
def test_openai_client_kwargs_with_base_url(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPENAI_API_KEY": "test-key",
"OPENAI_BASE_URL": "https://proxy.example/v1",
},
clear=True,
):
settings = EnvOnlySettings()
self.assertEqual(
settings.openai_client_kwargs,
{"api_key": "test-key", "base_url": "https://proxy.example/v1", "timeout": 30.0},
)
def test_settings_defaults(self) -> None:
with mock.patch.dict(os.environ, {}, clear=True):
settings = EnvOnlySettings()
self.assertEqual(settings.openai_base_url, "http://6.86.80.4:30080/v1")
self.assertEqual(settings.ragas_judge_model, "deepseek-v4-flash")
self.assertEqual(settings.ragas_embedding_model, "text-embedding-v3")
self.assertEqual(settings.openai_timeout_seconds, 30.0)
self.assertEqual(settings.ragas_metric_timeout_seconds, 45.0)
self.assertEqual(settings.batch_size, 8)
class ScenarioAndDatasetTests(unittest.TestCase):
def test_load_scenario_resolves_relative_paths(self) -> None:
scenario = load_scenario("scenarios/offline/sample-offline.yaml")
self.assertEqual(scenario.mode, "offline")
self.assertTrue(scenario.dataset.path.name.endswith(".csv"))
self.assertTrue(scenario.output_dir.name == "sample-offline-baseline")
def test_scenario_snapshot_serializes_path_static_kwargs(self) -> None:
scenario = load_scenario("scenarios/online/sample-pdf-question-bank-online.yaml")
snapshot = scenario.snapshot()
self.assertIsInstance(snapshot["app_adapter"]["static_kwargs"]["source_chunks_path"], str)
self.assertTrue(
snapshot["app_adapter"]["static_kwargs"]["source_chunks_path"].endswith("source_chunks.jsonl")
)
def test_load_sample_pdf_offline_smoke_scenario(self) -> None:
scenario = load_scenario("scenarios/offline/sample-pdf-offline-smoke.yaml")
self.assertEqual(scenario.mode, "offline")
self.assertEqual(scenario.dataset.path.name, "sample_pdf_offline_smoke.csv")
self.assertEqual(scenario.output_dir.name, "sample-pdf-offline-smoke")
def test_normalize_records_splits_valid_and_invalid(self) -> None:
records = [
{
"question": "Q1",
"contexts": '["C1"]',
"answer": "A1",
"ground_truth": "G1",
},
{
"question": "",
"contexts": "",
"answer": "",
"ground_truth": "",
},
]
valid, invalid = normalize_records(records)
self.assertEqual(len(valid), 1)
self.assertEqual(len(invalid), 1)
self.assertEqual(valid[0].contexts, ["C1"])
def test_normalize_sample_pdf_offline_smoke_row(self) -> None:
frame = pd.read_csv("datasets/normalized/sample_pdf_offline_smoke.csv")
valid, invalid = normalize_records(frame.to_dict(orient="records"))
self.assertEqual(len(invalid), 0)
self.assertEqual(len(valid), 3)
self.assertTrue(valid[0].answer)
self.assertTrue(valid[0].ground_truth)
self.assertTrue(valid[0].contexts)
class EvaluatorAndReportingTests(unittest.TestCase):
def test_metric_pipeline_scores_sample(self) -> None:
pipeline = MetricPipeline(
metrics={
"faithfulness": FakeMetric(0.1),
"answer_relevancy": FakeMetric(0.2),
"context_recall": FakeMetric(0.3),
"context_precision": FakeMetric(0.4),
}
)
valid, _ = normalize_records(
[
{
"question": "What is RAG?",
"contexts": ["RAG combines retrieval and generation."],
"answer": "RAG combines retrieval and generation.",
"ground_truth": "RAG combines retrieval and generation.",
}
]
)
score = __import__("asyncio").run(pipeline.score_sample(valid[0]))
self.assertEqual(score.metrics["faithfulness"], 0.1)
self.assertEqual(score.metrics["context_precision"], 0.4)
def test_metric_pipeline_captures_metric_timeout_without_aborting(self) -> None:
pipeline = MetricPipeline(
metrics={
"faithfulness": SlowMetric(),
"answer_relevancy": FakeMetric(0.2),
},
metric_timeout_seconds=0.01,
)
valid, _ = normalize_records(
[
{
"question": "What is RAG?",
"contexts": ["RAG combines retrieval and generation."],
"answer": "RAG combines retrieval and generation.",
"ground_truth": "RAG combines retrieval and generation.",
}
]
)
score = __import__("asyncio").run(pipeline.score_sample(valid[0]))
self.assertEqual(score.metrics["faithfulness"], 1.0)
self.assertEqual(score.metrics["answer_relevancy"], 0.2)
self.assertEqual(score.error, "")
def test_evaluator_and_reporting_write_run_assets(self) -> None:
temp_root = Path("tests/.tmp/run-assets")
temp_root.mkdir(parents=True, exist_ok=True)
for child in temp_root.iterdir():
if child.is_dir():
import shutil
shutil.rmtree(child)
else:
child.unlink()
output_root = temp_root
try:
scenario = load_scenario("scenarios/offline/sample-offline.yaml")
scenario.output_dir = output_root
pipeline = MetricPipeline(
metrics={
"faithfulness": FakeMetric(0.1),
"answer_relevancy": FakeMetric(0.2),
"context_recall": FakeMetric(0.3),
"context_precision": FakeMetric(0.4),
}
)
evaluator = Evaluator(scenario=scenario, metric_pipeline=pipeline)
result = evaluator.evaluate()
write_run_artifacts(result)
run_dir = output_root / result.run_id
self.assertTrue((run_dir / "scenario.snapshot.yaml").exists())
self.assertTrue((run_dir / "scores.csv").exists())
self.assertTrue((run_dir / "invalid.csv").exists())
self.assertTrue((run_dir / "summary.md").exists())
self.assertTrue((run_dir / "metadata.json").exists())
scores = pd.read_csv(run_dir / "scores.csv")
self.assertEqual(len(scores), 3)
self.assertIn("faithfulness", scores.columns)
finally:
import shutil
shutil.rmtree(temp_root, ignore_errors=True)
def test_summary_markdown_lists_all_scored_samples_and_errors(self) -> None:
scenario = load_scenario("scenarios/offline/sample-offline.yaml")
valid, invalid = normalize_records(
[
{
"sample_id": "sample-1",
"question": "Q1",
"contexts": ["C1"],
"answer": "A1",
"ground_truth": "G1",
},
{
"sample_id": "sample-2",
"question": "Q2",
"contexts": ["C2"],
"answer": "A2",
"ground_truth": "G2",
},
{
"sample_id": "sample-3",
"question": "Q3",
"contexts": ["C3"],
"answer": "A3",
"ground_truth": "G3",
},
{
"sample_id": "sample-4",
"question": "Q4",
"contexts": ["C4"],
"answer": "A4",
"ground_truth": "G4",
},
]
)
summary = build_summary_markdown(
EvaluationResult(
scenario=scenario,
run_id="test-run",
started_at="2026-06-10T00:00:00+00:00",
finished_at="2026-06-10T00:01:00+00:00",
valid_samples=valid,
invalid_samples=invalid,
score_rows=[
{
"sample_id": "sample-1",
"faithfulness": 1.0,
"answer_relevancy": 0.9,
"context_recall": 1.0,
"context_precision": 0.8,
"error": "",
},
{
"sample_id": "sample-2",
"faithfulness": 0.8,
"answer_relevancy": 0.7,
"context_recall": 0.9,
"context_precision": 0.6,
"error": "faithfulness: timeout",
},
{
"sample_id": "sample-3",
"faithfulness": 0.7,
"answer_relevancy": 0.6,
"context_recall": 0.8,
"context_precision": 0.5,
"error": "",
},
{
"sample_id": "sample-4",
"faithfulness": 0.6,
"answer_relevancy": 0.5,
"context_recall": 0.7,
"context_precision": 0.4,
"error": "context_precision: failed",
},
],
)
)
self.assertIn("## Per-sample Scores", summary)
self.assertIn("sample-1", summary)
self.assertIn("sample-2", summary)
self.assertIn("sample-3", summary)
self.assertIn("sample-4", summary)
self.assertIn("faithfulness", summary)
self.assertIn("answer_relevancy", summary)
self.assertIn("context_recall", summary)
self.assertIn("context_precision", summary)
self.assertIn("error", summary)
self.assertIn("faithfulness: timeout", summary)
self.assertIn("context_precision: failed", summary)
if __name__ == "__main__":
unittest.main()