ragsolver / tests /test_enrich.py
CamiloS1111's picture
Upload folder using huggingface_hub
a3c590d verified
Raw History Blame Contribute Delete
4.67 kB
"""Lógica del enriquecedor que no depende de la red: cadena de modelos por cuota
diaria, caché con modelo, y guardas de aceptación de la QA de fórmulas."""
from __future__ import annotations
import json
import pytest
from ragsolver.rag import enrich
from ragsolver.rag.enrich import DailyQuotaExhausted, Enricher
@pytest.fixture
def enricher(tmp_path, monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
e = Enricher(tmp_path / "enrich")
e.llm.models = ["model-a", "model-b", "model-c"]
e.llm._model_idx = 0
return e
def _fake(enricher, monkeypatch, fn):
"""Reemplaza la llamada HTTP: fn(model, prompt, schema, images, system, max_tokens) -> texto."""
from ragsolver.llm import LLMResponse
monkeypatch.setattr(enricher.llm, "_generate_with", lambda model, prompt, schema, images, system, max_tokens: LLMResponse(fn(model, prompt, schema), model))
def test_daily_quota_moves_to_next_model_and_records_it(enricher, monkeypatch):
seen = []
def fake(model, prompt, schema):
seen.append(model)
if model == "model-a":
raise DailyQuotaExhausted(model)
return json.dumps({"first_line": "x = 1"})
_fake(enricher, monkeypatch, fake)
assert enricher.split_solution(55, "texto", "p54", "p55") == "x = 1"
assert seen == ["model-a", "model-b"]
assert enricher.exhausted == ["model-a"] and enricher.model == "model-b"
# la caché guarda el modelo que produjo el resultado y se reutiliza sin llamar
rec = json.loads((enricher.cache_dir / "split.jsonl").read_text().splitlines()[0])
assert rec["model"] == "model-b"
assert enricher.split_solution(55, "texto", "p54", "p55") == "x = 1" and seen == ["model-a", "model-b"]
def test_all_models_exhausted_raises(enricher, monkeypatch):
_fake(enricher, monkeypatch, lambda m, p, s: (_ for _ in ()).throw(DailyQuotaExhausted(m)))
with pytest.raises(DailyQuotaExhausted):
enricher.transcribe_region(_png(enricher.cache_dir))
assert enricher.exhausted == ["model-a", "model-b"]
def test_qa_formulas_guards(enricher, monkeypatch):
"""Casos reales del PDF de ejemplo (docs/DECISIONS.md #19): qué acepta y qué no."""
cases = [ # (original, propuesta, inline, se_acepta)
("6 0 ^ { \\circ } . ~ { \\dot { \\omega } } \\mathrm { C u a l }", "60^{\\circ}", True, True), # basura absorbida
("^ { 1 , 4 , 9 , 1 6 , \\ldots \\ldots }", "1,4,9,16,......", True, True), # superíndice espurio
("x ^ { 2 } y ^ { 5 } z ^ { \\hat { 1 0 } } ?", "x ^ { 2 } y ^ { 5 } z ^ { 1 0 }", True, True), # acento espurio
("r ^ {2} + (r _ {l}) ^ {2}", "r ^ {2} + (r _ {1}) ^ {2}", False, True), # l -> 1: arreglo local
("= \\left( = a \\cdot 10^{3} + b", "abcd = \\left( = a \\cdot 10^{3} + b", False, True), # display puede agregar
("a = 9 ,", "a = 9,,", True, False), # sólo puntuación
("A B = 3", "AB = 3 \\text{ cm}", True, False), # inline absorbe texto vecino
("C _ { 1 0 0 } ^ { 2 0 0 } ~ \\mathrm { e s } ~ 6 1", "C _ { 1 0 0 } ^ { 2 0 0 }", True, False), # pierde la respuesta
("a ^ { 2 } + b ^ { 2 } \\mathrm { ~ y ~ } a + b ,", "a ^ { 2 } + b ^ { 2 }", True, False), # pierde matemática
("9 ( b c d a )", "( b c d a )", False, False), # borrar dígitos: conservador
("n \\in \\mathbb { Z }", "n ∈ ℤ", True, False), # unicode en vez de LaTeX
("\\underset { n } { ( 9 9 9 ) ^ { 2 } }", "\\raw { ( 9 9 9 ) ^ { 2 } }", True, False), # LaTeX destrozado
("S = n + \\dots + k", "S = n + ... + k", False, False), # cosmético
]
_fake(enricher, monkeypatch, lambda m, p, s: json.dumps({"items": [{"i": i, "latex": c[1], "changed": True} for i, c in enumerate(cases)]}))
out = enricher.qa_formulas(_png(enricher.cache_dir), [c[0] for c in cases], inline=[c[2] for c in cases])
assert [o["changed"] for o in out] == [c[3] for c in cases]
assert out[6]["rejected"] == "AB = 3 \\text{ cm}"
assert "rejected" not in out[5] # cosmético: ni cambio ni rechazo registrado
def test_is_daily_quota_parses_google_error():
class R:
def json(self):
return {"error": {"details": [{"violations": [{"quotaId": "GenerateRequestsPerDayPerProjectPerModel-FreeTier"}]}]}}
class R2:
def json(self):
return {"error": {"details": [{"violations": [{"quotaId": "GenerateRequestsPerMinutePerProjectPerModel"}]}]}}
from ragsolver import llm
assert llm._is_daily_quota(R()) is True
assert llm._is_daily_quota(R2()) is False
def _png(d):
p = d / "x.png"
p.write_bytes(b"\x89PNG\r\n\x1a\n")
return p