auralynq-rag / tests /test_self_consistency.py
asdfasdfqrqwer's picture
sync: bring the Space up to the current GitHub tree
656439d
Raw
History Blame Contribute Delete
4.93 kB
"""Self-consistency hallucination signal (Feature 06) β€” offline tests."""
from __future__ import annotations
import pytest
from auralynq.agent.runner import answer_question
from auralynq.agent.self_consistency import consistency_score, sample_answers
from auralynq.config import reload_settings
from auralynq.pipeline import build_index
# --------------------------------------------------------------- units ------
def test_consistency_score_identical_is_high():
assert consistency_score("Paris is the capital", ["Paris is the capital"] * 3) == 1.0
def test_consistency_score_divergent_is_low():
# Disjoint content tokens β†’ no agreement under resampling (hallucination sig).
score = consistency_score("alpha bravo charlie", ["xray yankee zulu", "delta echo foxtrot"])
assert score == 0.0
def test_consistency_score_partial_between():
score = consistency_score("paris capital france", ["paris capital spain"])
assert 0.0 < score < 1.0
def test_consistency_score_empty_samples_is_one():
assert consistency_score("anything", []) == 1.0
class _VaryLLM:
name = "vary"
def __init__(self, answers: list[str]) -> None:
self._answers = list(answers)
self._i = 0
def answer(self, question, contexts, **kw) -> str:
a = self._answers[self._i % len(self._answers)]
self._i += 1
return a
def test_sample_answers_count():
llm = _VaryLLM(["a", "b", "c"])
out = sample_answers(llm, "q", [], 3)
assert out == ["a", "b", "c"]
# ---------------------------------------------------- node integration ------
@pytest.fixture
def indexed(corpus_dir):
build_index(corpus_dir)
from auralynq.agent import runner
runner._CACHE.clear()
return corpus_dir
def test_signal_absent_by_default(indexed):
res = answer_question("What is the capital of France?")
assert "s_consistency" not in res.confidence_signals
def test_signal_present_when_enabled(indexed, monkeypatch):
monkeypatch.setenv("AURALYNQ_AGENT__SELF_CONSISTENCY_ENABLED", "1")
monkeypatch.setenv("AURALYNQ_AGENT__SELF_CONSISTENCY_SAMPLES", "3")
reload_settings()
from auralynq.agent import runner
runner._CACHE.clear()
res = answer_question("What is the capital of France?")
assert "s_consistency" in res.confidence_signals
s = res.confidence_signals["s_consistency"]
assert 0.0 <= s <= 1.0
# Deterministic extractive LLM β†’ resamples match the main answer β†’ high.
assert s == pytest.approx(1.0)
# --------------------------------------------------- abstention gate --------
def _state(consistency: float, answer: str = "an answer", with_ctx: bool = True):
from auralynq.agent.state import AgentState
from auralynq.ingest.models import Chunk, SourceType
from auralynq.retrieval.models import ScoredChunk
st = AgentState(question="q")
st.answer = answer
st.consistency = consistency
if with_ctx:
st.contexts = [
ScoredChunk(chunk=Chunk(id="c", doc_id="d", text="t", source_type=SourceType.text))
]
return st
def test_low_consistency_only_gates_when_enabled():
from auralynq.agent.runner import _low_consistency
from auralynq.config.settings import Settings
s = Settings()
s.agent.self_consistency_enabled = True
s.agent.self_consistency_min = 0.35
assert _low_consistency(_state(0.1), s) is True # unstable β†’ abstain
assert _low_consistency(_state(0.9), s) is False # stable β†’ keep
assert _low_consistency(_state(0.0, with_ctx=False), s) is False # no grounding β†’ other gate
s.agent.self_consistency_enabled = False
assert _low_consistency(_state(0.0), s) is False # off β†’ never abstains
def test_low_consistency_forces_abstention(indexed, monkeypatch):
# An impossible floor makes any (deterministic) consistency fail β†’ abstain.
monkeypatch.setenv("AURALYNQ_AGENT__SELF_CONSISTENCY_ENABLED", "1")
monkeypatch.setenv("AURALYNQ_AGENT__SELF_CONSISTENCY_SAMPLES", "2")
monkeypatch.setenv("AURALYNQ_AGENT__SELF_CONSISTENCY_MIN", "1.1")
reload_settings()
from auralynq.agent import runner
runner._CACHE.clear()
res = answer_question("What is the capital of France?")
assert res.status == "insufficient_evidence"
assert res.insufficient_evidence_reason is not None
assert "inconsistent" in res.insufficient_evidence_reason["summary"].lower()
def test_hallucination_rate_in_eval_gate():
from auralynq.eval.gate import eval_gate, extract_metrics
report = {"agentic": {"consistency": {"hallucination_rate": 0.8, "mean": 0.2}}}
assert extract_metrics(report)["hallucination_rate"] == 0.8
checks = eval_gate(report)["checks"]
assert any(c["metric"] == "hallucination_rate" and not c["passed"] for c in checks)
# Absent (default off) β†’ skipped, not failed.
assert "hallucination_rate" not in extract_metrics({"agentic": {}})