Spaces:
Paused
Paused
| from __future__ import annotations | |
| from auralynq.config.settings import Settings | |
| from auralynq.ingest.models import Chunk, SourceType | |
| from auralynq.retrieval.pathrag.builder import build_from_chunks | |
| from auralynq.wiki.generator import synthesize_wiki | |
| from auralynq.wiki.store import WikiStore, slug | |
| class _StubLLM: | |
| """Deterministic stand-in for a provider — no network, no model.""" | |
| name = "stub" | |
| def __init__(self) -> None: | |
| self.calls: list[str] = [] | |
| def generate(self, prompt: str, *, system=None, temperature=None, max_tokens=None) -> str: | |
| self.calls.append(prompt) | |
| return "One-sentence definition.\n\n## Summary\nThe sources describe it [1].\n" | |
| def _chunks() -> list[Chunk]: | |
| return [ | |
| Chunk( | |
| id="c0", | |
| doc_id="d", | |
| ordinal=0, | |
| source="geo.txt", | |
| source_type=SourceType.text, | |
| text="Paris is the capital of France. France is in Europe.", | |
| ), | |
| Chunk( | |
| id="c1", | |
| doc_id="d", | |
| ordinal=1, | |
| source="geo2.txt", | |
| source_type=SourceType.text, | |
| text="Paris is the capital of France and a large city.", | |
| ), | |
| ] | |
| def test_slug_is_filesystem_safe_and_stable(): | |
| assert slug("GPT-4 / Turbo!") == "gpt_4_turbo" | |
| assert slug(" Hello World ") == "hello_world" | |
| long = "x" * 200 | |
| assert len(slug(long)) <= 64 and slug(long) == slug(long) | |
| def test_wiki_store_roundtrip(tmp_path): | |
| store = WikiStore(tmp_path / "wiki_pages") | |
| meta = store.write_page( | |
| "paris", | |
| title="Paris", | |
| body="Capital of France [1].", | |
| sources=["geo.txt", "geo2.txt"], | |
| mentions=3, | |
| ) | |
| assert meta.path == "entity_paris.md" | |
| page = store.read_page("paris") | |
| assert page is not None | |
| assert 'title: "Paris"' in page["markdown"] | |
| assert "Capital of France [1]." in page["markdown"] | |
| assert page["mentions"] == 3 | |
| listed = store.list_pages() | |
| assert any(p["id"] == "paris" for p in listed) | |
| assert store.count() == 1 | |
| store.append_log("test", n=1) | |
| assert (tmp_path / "wiki_pages" / "_log.jsonl").exists() | |
| def test_read_missing_page_returns_none(tmp_path): | |
| assert WikiStore(tmp_path / "w").read_page("nope") is None | |
| def test_synthesize_wiki_from_kg(tmp_path): | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| s.wiki.min_mentions = 1 | |
| kg = build_from_chunks(_chunks()) | |
| llm = _StubLLM() | |
| written = synthesize_wiki(kg, _chunks(), llm=llm, settings=s) | |
| assert written >= 1 | |
| assert llm.calls, "LLM was invoked for synthesis" | |
| store = WikiStore(s.wiki_dir) | |
| assert store.count() == written | |
| paris = store.read_page("paris") | |
| assert paris is not None | |
| # Frontmatter carries the source provenance from the entity's chunks. | |
| assert "geo.txt" in paris["markdown"] | |
| assert "## Summary" in paris["markdown"] | |
| def test_wiki_disabled_by_default(): | |
| # Assert the declared code default (immune to a local .env that enables it). | |
| from auralynq.config.settings import WikiSettings | |
| assert WikiSettings.model_fields["enabled"].default is False | |
| def test_min_mentions_gate(tmp_path): | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| s.wiki.min_mentions = 999 # gate everything out | |
| kg = build_from_chunks(_chunks()) | |
| assert synthesize_wiki(kg, _chunks(), llm=_StubLLM(), settings=s) == 0 | |
| def test_wiki_retriever_matches_entity(tmp_path): | |
| from auralynq.wiki.retriever import WikiRetriever | |
| store = WikiStore(tmp_path / "w") | |
| store.write_page( | |
| "auralynq", | |
| title="Auralynq", | |
| body="## Summary\nAuralynq fuses dense and sparse vectors [1][2].", | |
| sources=["a.md"], | |
| mentions=3, | |
| ) | |
| store.write_page("france", title="France", body="Country in Europe [1].", mentions=2) | |
| res = WikiRetriever(tmp_path / "w").retrieve("Tell me about Auralynq", k=2) | |
| assert res.chunks, "matched the Auralynq page" | |
| top = res.chunks[0] | |
| assert top.method == "wiki" | |
| assert top.chunk.source == "Auralynq (wiki)" | |
| # Internal [n] markers are stripped so they don't clash with answer citations. | |
| assert "[1]" not in top.chunk.text and "[2]" not in top.chunk.text | |
| assert "fuses dense and sparse" in top.chunk.text | |
| def test_wiki_retriever_no_match_is_empty(tmp_path): | |
| from auralynq.wiki.retriever import WikiRetriever | |
| store = WikiStore(tmp_path / "w") | |
| store.write_page("france", title="France", body="A country.", mentions=2) | |
| res = WikiRetriever(tmp_path / "w").retrieve("quantum chromodynamics gluons", k=2) | |
| assert res.chunks == [] | |
| # ---------------------------------------------------------- Phase 3 -------- | |
| def test_detect_contradictions_parses_json(): | |
| from auralynq.wiki.contradiction import detect_contradictions | |
| class J: | |
| def generate(self, p, *, system=None, temperature=None, max_tokens=None): | |
| return 'noise [{"old_claim":"X is blue","new_claim":"X is red","why":"color"}] trailing' | |
| out = detect_contradictions("X", "X is blue.", "X is red.", J()) | |
| assert len(out) == 1 | |
| assert out[0].old_claim == "X is blue" and out[0].new_claim == "X is red" | |
| assert out[0].entity == "X" | |
| def test_detect_contradictions_none_and_missing_side(): | |
| from auralynq.wiki.contradiction import detect_contradictions | |
| class E: | |
| def generate(self, p, *, system=None, temperature=None, max_tokens=None): | |
| return "[]" | |
| assert detect_contradictions("X", "a", "b", E()) == [] | |
| assert detect_contradictions("X", "", "b", E()) == [] # missing side → skip | |
| class _DualStub: | |
| """Returns markdown for synthesis prompts, a JSON contradiction for the | |
| contradiction-detection prompt (distinguished by its system prompt).""" | |
| def generate(self, p, *, system=None, temperature=None, max_tokens=None): | |
| if system and "CONTRADICTIONS" in system: | |
| return '[{"old_claim":"Paris is small","new_claim":"Paris is large","why":"size"}]' | |
| return "Paris is a city [1]." | |
| def test_contradiction_flagged_on_page_update(tmp_path): | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| s.wiki.min_mentions = 1 | |
| llm = _DualStub() | |
| synthesize_wiki(build_from_chunks(_chunks()), _chunks(), llm=llm, settings=s) # first pass | |
| # Second pass adds a NEW source about Paris → contradiction check fires only | |
| # when new evidence appears (re-synthesizing the same sources must NOT flag). | |
| extra = [ | |
| *_chunks(), | |
| Chunk( | |
| id="c9", | |
| doc_id="d2", | |
| ordinal=0, | |
| source="new.txt", | |
| source_type=SourceType.text, | |
| text="Paris is a major capital city in France.", | |
| ), | |
| ] | |
| synthesize_wiki(build_from_chunks(extra), extra, llm=llm, settings=s) | |
| store = WikiStore(s.wiki_dir) | |
| paris = store.read_page("paris") | |
| assert "Contradictions flagged" in paris["markdown"] | |
| report = store.lint() | |
| assert report["contradiction_count"] >= 1 | |
| assert any(c["entity"] == "paris" for c in report["contradictions"]) | |
| assert (tmp_path / "storage" / "wiki_pages" / "_log.jsonl").exists() | |
| def test_no_contradiction_when_sources_unchanged(tmp_path): | |
| # Re-synthesizing the exact same sources must NOT flag contradictions (only | |
| # new evidence can contradict) — guards against reword false-positives. | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| s.wiki.min_mentions = 1 | |
| llm = _DualStub() | |
| synthesize_wiki(build_from_chunks(_chunks()), _chunks(), llm=llm, settings=s) | |
| synthesize_wiki(build_from_chunks(_chunks()), _chunks(), llm=llm, settings=s) # same sources | |
| report = WikiStore(s.wiki_dir).lint() | |
| assert report["contradiction_count"] == 0 | |
| def test_lint_orphans(tmp_path): | |
| store = WikiStore(tmp_path / "w") | |
| store.write_page("a", title="A", body="Links to [[B]].", mentions=1) | |
| store.write_page("b", title="B", body="No links here.", mentions=1) | |
| report = store.lint() | |
| assert "a" in report["orphan_pages"] # nobody links to A | |
| assert "b" not in report["orphan_pages"] # A links to B | |
| # --------------------------------------------------------- Phase 3b ------- | |
| def test_file_answer_creates_answer_page(tmp_path): | |
| from auralynq.wiki.generator import file_answer | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| ok = file_answer( | |
| "What is Auralynq?", | |
| "Auralynq is a local-first RAG platform [1].", | |
| [{"source": "a.md", "locator": "p.1"}], | |
| 0.8, | |
| settings=s, | |
| ) | |
| assert ok | |
| store = WikiStore(s.wiki_dir) | |
| answers = [p for p in store.list_pages() if p["type"] == "answer"] | |
| assert len(answers) == 1 | |
| page = store.read_page(answers[0]["id"]) | |
| assert "Auralynq is a local-first RAG platform" in page["markdown"] | |
| assert "## Sources" in page["markdown"] | |
| assert "a.md" in page["markdown"] | |
| def test_file_answer_gates(tmp_path): | |
| from auralynq.wiki.generator import file_answer | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| assert file_answer("q", "ans", [{"source": "a"}], 0.1, settings=s) is False # low conf | |
| assert file_answer("q", "ans", [], 0.9, settings=s) is False # no citations | |
| assert file_answer("q", "", [{"source": "a"}], 0.9, settings=s) is False # empty answer | |
| s.wiki.file_answers = False | |
| assert file_answer("q", "ans", [{"source": "a"}], 0.9, settings=s) is False # disabled | |
| def test_file_answer_is_idempotent_by_question(tmp_path): | |
| from auralynq.wiki.generator import file_answer | |
| s = Settings(data_dir=tmp_path) | |
| s.wiki.enabled = True | |
| file_answer("Same question?", "First answer [1].", [{"source": "a"}], 0.9, settings=s) | |
| file_answer("Same question?", "Updated answer [1].", [{"source": "a"}], 0.9, settings=s) | |
| store = WikiStore(s.wiki_dir) | |
| answers = [p for p in store.list_pages() if p["type"] == "answer"] | |
| assert len(answers) == 1 # same question → same page, updated in place | |
| def test_contradiction_has_flagged_at(): | |
| from auralynq.wiki.contradiction import Contradiction | |
| c = Contradiction("e", "old", "new") | |
| assert len(c.flagged_at) >= 10 # ISO timestamp | |
| assert "flagged_at" in c.to_dict() | |