Download tests/test_retriever.py from CamiloS1111/ragsolver: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/CamiloS1111/ragsolver/resolve/main/tests/test_retriever.py
- Command line
-
hf download hf://CamiloS1111/ragsolver/tests/test_retriever.py
-
curl -L -o test_retriever.py https://huggingface.co/CamiloS1111/ragsolver/resolve/main/tests/test_retriever.py
12.4 kB
| """Fase 3, tarea 6 — Retriever con LanceDB temporal y embedder/reranker falsos. | |
| Vectores de dimensión 4 elegidos a mano para controlar el orden denso; el | |
| reranker falso puntúa por longitud del texto. Así cada test aísla una pieza: | |
| filtros SQL, modo `error`, dedupe, figuras adjuntas, ablación, traza, plantillas. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import pytest | |
| import yaml | |
| from ragsolver.logging.jsonl_writer import read_events | |
| from ragsolver.rag import retrieve as R | |
| from ragsolver.rag.index import INFORMAL_FTS_COLUMNS, MATHLIB_FTS_COLUMNS, build_table | |
| from ragsolver.rag.retrieve import Retriever, lean_identifiers, sql_filter | |
| from ragsolver.rag.schema import RetrievalFilters, RetrievalRequest | |
| E1, E2, E3, E4 = [1.0, 0, 0, 0], [0, 1.0, 0, 0], [0, 0, 1.0, 0], [0, 0, 0, 1.0] | |
| MATHLIB = [ | |
| {"chunk_id": "mathlib:Nat.Prime.two_le:0", "doc_id": "mathlib", "corpus": "mathlib", "unit_type": "mathlib_decl", "decl_name": "Nat.Prime.two_le", "module_path": "Mathlib.Data.Nat.Prime.Defs", "kind": "theorem", "signature": "theorem Nat.Prime.two_le {p : ℕ} (hp : p.Prime) : 2 ≤ p", "informal_name": "prime is at least two", "informal_description": "A prime number is at least 2.", "docs_url": "https://x#Nat.Prime.two_le", "text": "prime is at least two", "embedding": E4}, | |
| {"chunk_id": "mathlib:Nat.Coprime:0", "doc_id": "mathlib", "corpus": "mathlib", "unit_type": "mathlib_decl", "decl_name": "Nat.Coprime", "module_path": "Mathlib.Data.Nat.GCD.Basic", "kind": "definition", "signature": "def Nat.Coprime (m n : ℕ) : Prop", "informal_name": "coprime naturals", "informal_description": "gcd m n = 1", "docs_url": "https://x#Nat.Coprime", "text": "coprime naturals gcd equals one", "embedding": E1}, | |
| {"chunk_id": "mathlib:Nat.gcd_comm:0", "doc_id": "mathlib", "corpus": "mathlib", "unit_type": "mathlib_decl", "decl_name": "Nat.gcd_comm", "module_path": "Mathlib.Data.Nat.GCD.Basic", "kind": "theorem", "signature": "theorem Nat.gcd_comm (m n : ℕ) : gcd m n = gcd n m", "informal_name": "gcd is commutative", "informal_description": "gcd commutes", "docs_url": "https://x#Nat.gcd_comm", "text": "gcd is commutative", "embedding": E2}, | |
| {"chunk_id": "mathlib:Int.gcd_comm:0", "doc_id": "mathlib", "corpus": "mathlib", "unit_type": "mathlib_decl", "decl_name": "Int.gcd_comm", "module_path": "Mathlib.Data.Int.GCD", "kind": "theorem", "signature": "theorem Int.gcd_comm (m n : ℤ) : gcd m n = gcd n m", "informal_name": "integer gcd is commutative", "informal_description": "gcd commutes for integers", "docs_url": "https://x#Int.gcd_comm", "text": "integer gcd is commutative", "embedding": E3}, | |
| ] | |
| INFORMAL = [ | |
| {"chunk_id": "informal:d1:problem_card:8", "doc_id": "d1", "corpus": "informal", "unit_type": "problem_card", "parent_id": None, "item_number": 8, "page_start": 10, "math_area": ["geometry"], "keywords": ["circle"], "theorems_used": [], "figure_ids": ["d1:fig:10:2"], "content_md": "8. tangent circles", "text": "problem about tangent circles and an inscribed angle", "embedding": E1}, | |
| {"chunk_id": "informal:d1:figure:10:2", "doc_id": "d1", "corpus": "informal", "unit_type": "figure", "parent_id": "informal:d1:problem_card:8", "item_number": 8, "page_start": 10, "math_area": ["geometry"], "keywords": ["circle"], "theorems_used": [], "figure_ids": ["d1:fig:10:2"], "content_md": "a circle tangent to a bigger circle", "text": "figure: a circle tangent to a bigger circle", "embedding": [0.9, 0.1, 0, 0]}, | |
| {"chunk_id": "informal:d1:problem_card:86", "doc_id": "d1", "corpus": "informal", "unit_type": "problem_card", "parent_id": None, "item_number": 86, "page_start": 22, "math_area": ["number_theory"], "keywords": ["divisibility"], "theorems_used": [], "figure_ids": [], "content_md": "86. divisible by 7", "text": "prove that a power sum is divisible by seven", "embedding": E2}, | |
| {"chunk_id": "informal:d2:problem_card:1", "doc_id": "d2", "corpus": "informal", "unit_type": "problem_card", "parent_id": None, "item_number": 1, "page_start": 1, "math_area": ["algebra"], "keywords": [], "theorems_used": [], "figure_ids": [], "content_md": "1. other doc", "text": "an algebra problem from another document", "embedding": E3}, | |
| ] | |
| FIGURES = [{"figure_id": "d1:fig:10:2", "image_path": "/tmp/p010_b2.png", "description": "two tangent circles", "page": 10, "bbox": [0.1, 0.1, 0.5, 0.5], "figure_type": "geometric_diagram", "labels": ["A", "B"], "detected_by": "layout"}] | |
| def retriever(tmp_path, monkeypatch): | |
| db = str(tmp_path / "db") | |
| build_table(db, "mathlib", MATHLIB, MATHLIB_FTS_COLUMNS) | |
| build_table(db, "informal", INFORMAL, INFORMAL_FTS_COLUMNS) | |
| build_table(db, "figures", FIGURES) | |
| cfg = yaml.safe_load(open("configs/retrieval.yaml", encoding="utf-8")) | |
| (tmp_path / "retrieval.yaml").write_text(yaml.safe_dump(cfg), encoding="utf-8") | |
| calls = {"instructions": [], "queries": []} | |
| def fake_embed(texts, model_name, batch_size=8, instruction=None): | |
| calls["instructions"].append(instruction) | |
| calls["queries"].append(list(texts)) | |
| table = {"gcd": E2, "coprime": E1, "prime": E4, "integer": E3, "circle": E1, "seven": E2, "algebra": E3} | |
| return [next((v for k, v in table.items() if k in t.lower()), [0.5, 0.5, 0.5, 0.5]) for t in texts] | |
| monkeypatch.setattr(R, "embed_queries", fake_embed) | |
| class FakeReranker: # más largo = mejor, para poder predecir el orden | |
| def predict(self, pairs): | |
| return [float(len(doc)) for _, doc in pairs] | |
| r = Retriever(db_path=db, config_path=str(tmp_path / "retrieval.yaml")) | |
| monkeypatch.setattr(Retriever, "_reranker", lambda self: FakeReranker()) | |
| r.trace_dir = str(tmp_path / "traces") | |
| r._calls = calls | |
| return r | |
| def _req(**kw): | |
| base = dict(queries=["gcd commutes"], corpora=["mathlib"], k=8, rerank=False, context={"problem_id": "p1", "subgoal_id": "s1", "attempt": 2, "agent": "test"}) | |
| base.update(kw) | |
| return RetrievalRequest(**base) | |
| def test_lean_identifiers_from_error_messages(): | |
| assert lean_identifiers("unknown identifier 'Nat.Prime.two_le'") == ["Nat.Prime.two_le"] | |
| assert lean_identifiers("unknown constant 'Foo.bar' and `Baz.qux'` here, also Nat.gcd_comm") == ["Foo.bar", "Baz.qux'", "Nat.gcd_comm"] | |
| assert lean_identifiers("no identifiers here") == [] | |
| def test_sql_filter_quotes_and_scopes_by_corpus(): | |
| f = RetrievalFilters(doc_ids=["d'1"], kinds=["theorem"], math_area=["geometry"], unit_types=["problem_card"]) | |
| assert sql_filter("mathlib", f) == "doc_id IN ('d''1') AND unit_type IN ('problem_card') AND kind IN ('theorem')" | |
| assert sql_filter("informal", f) == "doc_id IN ('d''1') AND unit_type IN ('problem_card') AND array_has_any(math_area, ['geometry'])" | |
| assert sql_filter("mathlib", RetrievalFilters()) is None | |
| def test_error_mode_pins_exact_identifier_at_top_even_against_dense_and_rerank(retriever): | |
| # el embedding falso de la consulta apunta a los gcd (E2) y el reranker prefiere textos largos: | |
| # ninguno favorece a Nat.Prime.two_le; el modo error lo fija igual en el top-1. | |
| res = retriever.search(_req(queries=["unknown identifier 'Nat.Prime.two_le' in goal gcd commutes"], mode="error", rerank=True)) | |
| assert res.hits[0].chunk_id == "mathlib:Nat.Prime.two_le:0" | |
| assert res.hits[0].citation.citation_id == "M1" | |
| assert res.hits[0].citation.locator["docs_url"].endswith("#Nat.Prime.two_le") | |
| assert len(res.hits) == 4 | |
| def test_filters_prefilter_kinds_doc_ids_and_math_area(retriever): | |
| res = retriever.search(_req(filters={"kinds": ["definition"]})) | |
| assert [h.chunk_id for h in res.hits] == ["mathlib:Nat.Coprime:0"] | |
| res = retriever.search(_req(queries=["algebra"], corpora=["informal"], filters={"doc_ids": ["d1"]})) | |
| assert all(h.metadata["doc_id"] == "d1" for h in res.hits) and res.hits | |
| res = retriever.search(_req(queries=["circle"], corpora=["informal"], filters={"math_area": ["number_theory"]})) | |
| assert [h.chunk_id for h in res.hits] == ["informal:d1:problem_card:86"] | |
| def test_figure_chunk_collapses_into_its_card_and_figures_are_attached(retriever): | |
| res = retriever.search(_req(queries=["circle"], corpora=["informal"])) | |
| ids = [h.chunk_id for h in res.hits] | |
| assert "informal:d1:problem_card:8" in ids and "informal:d1:figure:10:2" not in ids | |
| card = next(h for h in res.hits if h.chunk_id == "informal:d1:problem_card:8") | |
| assert card.figures == [{"figure_id": "d1:fig:10:2", "image_path": "/tmp/p010_b2.png", "description": "two tangent circles", "page": 10, "bbox": [0.1, 0.1, 0.5, 0.5], "figure_type": "geometric_diagram", "labels": ["A", "B"]}] | |
| assert card.citation.locator["figure_ids"] == ["d1:fig:10:2"] | |
| assert card.citation.citation_id == "I1" and card.citation.snippet == "8. tangent circles" | |
| res = retriever.search(_req(queries=["circle"], corpora=["informal"], include_figures=False)) | |
| assert all(h.figures == [] for h in res.hits) | |
| def test_rerank_reorders_and_scores_are_reported(retriever): | |
| res = retriever.search(_req(rerank=True)) | |
| texts = [h.text for h in res.hits] | |
| assert texts == sorted(texts, key=len, reverse=True) # reranker falso: más largo primero | |
| assert all(h.citation.scores.rerank is not None and h.citation.scores.fused is not None for h in res.hits) | |
| def test_multi_query_rrf_and_query_instruction_per_mode(retriever): | |
| res = retriever.search(_req(queries=["gcd commutes", "coprime"], mode="formal", k=2)) | |
| assert {h.chunk_id for h in res.hits} <= {"mathlib:Nat.gcd_comm:0", "mathlib:Nat.Coprime:0", "mathlib:Int.gcd_comm:0"} | |
| assert retriever._calls["instructions"][-1] == retriever.config["query_instructions"]["formal"] | |
| assert retriever._calls["queries"][-1] == ["gcd commutes", "coprime"] | |
| def test_max_per_module_limits_mathlib_hits(retriever): | |
| retriever.config["max_per_module"] = 1 | |
| res = retriever.search(_req(queries=["gcd commutes coprime"], k=8)) | |
| modules = [h.metadata["module_path"] for h in res.hits] | |
| assert len(modules) == len(set(modules)) | |
| def test_ablation_disabled_returns_empty_but_still_traces(retriever): | |
| retriever.config["ablation"]["enabled"] = False | |
| res = retriever.search(_req()) | |
| assert res.hits == [] and res.citations == [] | |
| events = [e for f in Path(retriever.trace_dir).glob("*.jsonl") for e in read_events(f.stem, retriever.trace_dir)] | |
| assert len(events) == 1 and events[0].hits == [] and events[0].llm_calls == 0 | |
| def test_trace_event_records_request_hits_and_versions(retriever): | |
| res = retriever.search(_req(k=2)) | |
| files = list(Path(retriever.trace_dir).glob("p1:*.jsonl")) | |
| assert len(files) == 1 | |
| ev = json.loads(files[0].read_text(encoding="utf-8").splitlines()[0]) | |
| assert ev["agent"] == "retriever" and ev["llm_calls"] == 0 and ev["subgoal_id"] == "s1" and ev["attempt"] == 2 | |
| assert [h["chunk_id"] for h in ev["hits"]] == [h.chunk_id for h in res.hits] | |
| assert ev["versions"]["mathlib_tag"] == retriever.versions["lean"]["mathlib_tag"] | |
| assert ev["request"]["queries"] == ["gcd commutes"] | |
| def test_trace_event_pins_the_mathlib_table_being_served(retriever): | |
| """MET-1: la traza registra qué tabla sirve `mathlib` (muestra vs índice completo).""" | |
| build_table(retriever.db_path, "mathlib_full", MATHLIB, MATHLIB_FTS_COLUMNS) | |
| retriever.tables["mathlib"] = "mathlib_full" | |
| retriever.search(_req(k=2)) | |
| f = next(Path(retriever.trace_dir).glob("p1:*.jsonl")) | |
| ev = json.loads(f.read_text(encoding="utf-8").splitlines()[-1]) | |
| assert ev["versions"]["mathlib_table"] == "mathlib_full" | |
| def test_run_id_in_context_groups_searches_into_one_trace_file(retriever): | |
| """TRZ-1: con `context.run_id`, los eventos de varias búsquedas del mismo par | |
| (config, problema) van al mismo archivo — antes cada búsqueda creaba el suyo, | |
| sin poder atribuirla a una corrida/config.""" | |
| ctx = {"problem_id": "p1", "subgoal_id": "s1", "attempt": 1, "agent": "test", "run_id": "eval-v1:full:p1"} | |
| retriever.search(_req(context=ctx)) | |
| retriever.search(_req(context={**ctx, "subgoal_id": "s2"})) | |
| run_files = list(Path(retriever.trace_dir).glob("eval-v1*.jsonl")) | |
| assert len(run_files) == 1 | |
| events = run_files[0].read_text(encoding="utf-8").splitlines() | |
| assert len(events) == 2 | |
| assert not list(Path(retriever.trace_dir).glob("p1:*.jsonl")) # nada quedó en el formato viejo (por-búsqueda) | |