Spaces:
Paused
Paused
| """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 ------ | |
| 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": {}}) | |