auralynq-rag / tests /test_visual_grounding.py
MHamdan's picture
Deploy Auralynq RAG (Llama-3.3-70B via HF Inference Providers)
8c1b9fe verified
Raw
History Blame Contribute Delete
12.1 kB
"""Tests for visual grounding infrastructure: resolver, ingest metadata, endpoints."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client():
from auralynq.serving.app import create_app
return TestClient(create_app())
# ---------------------------------------------------------------------------
# Grounding resolver unit tests
# ---------------------------------------------------------------------------
def test_resolver_returns_empty_for_no_citations():
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
results = resolver.resolve(answer_id="test", answer="Hello", citations=[])
assert results == []
def test_resolver_page_level_grounding():
"""Citation with page number but no bbox → page-level grounding."""
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
citations = [
{
"marker": 1,
"chunk_id": "chunk_001",
"doc_id": "doc_001",
"source": "test.pdf",
"page": 3,
"score": 0.8,
"visual_grounding": {
"grounding_version": 1,
"page": 3,
"has_bbox": False,
"bbox": None,
"normalized_bbox": None,
},
}
]
results = resolver.resolve("test_id", "Some answer [1].", citations)
assert len(results) == 1
r = results[0]
assert r.page == 3
assert r.doc_id == "doc_001"
assert r.visual_grounding_available is True
assert r.grounding_stage == "page"
assert len(r.highlights) == 1
ev = r.highlights[0]
assert ev.support_type == "page"
assert ev.normalized_bbox is None
assert ev.reindex_required is False
def test_resolver_span_level_grounding():
"""Citation with bbox → span-level grounding."""
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
citations = [
{
"marker": 1,
"chunk_id": "chunk_002",
"doc_id": "doc_002",
"source": "paper.pdf",
"page": 1,
"score": 0.9,
"visual_grounding": {
"grounding_version": 1,
"page": 1,
"has_bbox": True,
"bbox": [72.0, 100.0, 400.0, 120.0],
"normalized_bbox": [0.1, 0.13, 0.55, 0.16],
"block_type": "paragraph",
},
}
]
results = resolver.resolve("span_test", "Answer with span [1].", citations)
assert len(results) == 1
r = results[0]
assert r.grounding_stage == "span"
ev = r.highlights[0]
assert ev.support_type == "span"
assert ev.normalized_bbox == [0.1, 0.13, 0.55, 0.16]
assert ev.bbox == [72.0, 100.0, 400.0, 120.0]
def test_resolver_reindex_required_for_old_doc():
"""Citation with grounding_version=0 → reindex_required=True."""
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
citations = [
{
"marker": 1,
"chunk_id": "old_chunk",
"doc_id": "old_doc",
"source": "legacy.pdf",
"page": None,
"score": 0.5,
"visual_grounding": {
"grounding_version": 0,
"page": None,
"has_bbox": False,
},
}
]
results = resolver.resolve("old_test", "Answer.", citations)
assert len(results) == 1
ev = results[0].highlights[0]
assert ev.reindex_required is True
assert ev.support_type == "unavailable"
def test_resolver_to_api_response():
"""to_api_response produces correct structure."""
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
citations = [
{
"marker": 1,
"chunk_id": "c1",
"doc_id": "d1",
"source": "doc.pdf",
"page": 2,
"score": 0.7,
"visual_grounding": {
"grounding_version": 1,
"page": 2,
"has_bbox": True,
"bbox": [0, 0, 100, 20],
"normalized_bbox": [0.0, 0.0, 0.14, 0.025],
},
}
]
results = resolver.resolve("api_test", "The answer is X [1].", citations)
api_resp = resolver.to_api_response(results)
assert "highlights" in api_resp
assert "claim_grounding" in api_resp
assert "warnings" in api_resp
assert isinstance(api_resp["visual_grounding_available"], bool)
assert api_resp["visual_grounding_available"] is True
assert api_resp["grounding_stage"] == "span"
assert len(api_resp["highlights"]) == 1
h = api_resp["highlights"][0]
assert h["page_image_url"] == "/api/documents/d1/pages/2/image"
def test_resolver_page_image_url_format():
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
citations = [
{
"marker": 2,
"chunk_id": "c2",
"doc_id": "abc123",
"source": "file.pdf",
"page": 5,
"score": 0.6,
"visual_grounding": {"grounding_version": 1, "page": 5, "has_bbox": False},
}
]
results = resolver.resolve("url_test", "Answer.", citations)
assert results[0].page_image_url == "/api/documents/abc123/pages/5/image"
def test_resolver_claim_grounding_unsupported_without_citations():
from auralynq.grounding.resolver import GroundingResolver
resolver = GroundingResolver()
# Long answer sentences with no citations matching
citations = [
{
"marker": 1,
"chunk_id": "c3",
"doc_id": "d3",
"source": "x.pdf",
"page": 1,
"score": 0.5,
"visual_grounding": {"grounding_version": 1, "page": 1, "has_bbox": False},
}
]
results = resolver.resolve(
"claim_test", "This is a long answer sentence. [1] Another unrelated claim here.", citations
)
# Claim grounding should exist
all_claims = [cg for r in results for cg in r.claim_grounding]
assert len(all_claims) >= 1
# ---------------------------------------------------------------------------
# Ingest model tests
# ---------------------------------------------------------------------------
def test_visual_grounding_version_constant():
from auralynq.ingest.models import VISUAL_GROUNDING_VERSION
assert VISUAL_GROUNDING_VERSION == 1
def test_document_has_visual_grounding_property():
from auralynq.ingest.models import Document, SourceType
doc = Document(
id="d1",
source="test.pdf",
source_type=SourceType.pdf,
visual_grounding_version=1,
)
assert doc.has_visual_grounding is True
doc_old = Document(
id="d2",
source="old.pdf",
source_type=SourceType.pdf,
visual_grounding_version=0,
)
assert doc_old.has_visual_grounding is False
def test_layout_block_model():
from auralynq.ingest.models import LayoutBlock
blk = LayoutBlock(
page=1,
bbox=[72.0, 100.0, 400.0, 120.0],
normalized_bbox=[0.1, 0.13, 0.55, 0.16],
text="Sample text",
block_type="paragraph",
reading_order=3,
page_width=720.0,
page_height=960.0,
)
assert blk.page == 1
assert blk.block_type == "paragraph"
assert blk.normalized_bbox[0] == pytest.approx(0.1)
# ---------------------------------------------------------------------------
# Settings tests
# ---------------------------------------------------------------------------
def test_visual_grounding_settings_defaults():
from auralynq.config.settings import VisualGroundingSettings
vgs = VisualGroundingSettings()
assert vgs.enabled is True
assert vgs.page_rendering_enabled is True
assert vgs.render_dpi == 144
assert vgs.max_cached_pages == 500
assert vgs.visual_retrieval_enabled is False
assert vgs.metadata_version == 1
def test_settings_has_visual():
import os
os.environ["AURALYNQ_DOTENV_DISABLED"] = "1"
from auralynq.config.settings import Settings
s = Settings(_env_file=None) # type: ignore
assert hasattr(s, "visual")
assert s.visual.enabled is True
# ---------------------------------------------------------------------------
# API endpoint tests
# ---------------------------------------------------------------------------
def test_visual_grounding_settings_endpoint(client):
resp = client.get("/visual-grounding/settings")
assert resp.status_code == 200
data = resp.json()
assert "enabled" in data
assert "page_rendering_enabled" in data
assert "render_dpi" in data
assert "metadata_version" in data
def test_document_pages_invalid_doc_id_returns_result(client):
"""Valid request format but unknown doc_id → empty result (not error)."""
resp = client.get("/documents/abc123def456/pages")
# Should return 200 with empty page list (doc not found = empty metadata)
assert resp.status_code == 200
data = resp.json()
assert "doc_id" in data
assert "pages" in data
def test_document_page_image_invalid_doc_id_rejected(client):
"""Path traversal attempt should be rejected."""
resp = client.get("/documents/../etc/passwd/pages/1/image")
# Either 400, 404, or the path is blocked by FastAPI routing
assert resp.status_code in (400, 404, 422)
def test_document_page_image_bad_doc_id_rejected(client):
"""Doc ID with invalid chars should return 400."""
resp = client.get("/documents/../../etc/pages/1/image")
assert resp.status_code in (400, 404, 422)
def test_document_grounding_status_endpoint(client):
resp = client.get("/documents/abc123def456/grounding-status")
assert resp.status_code == 200
data = resp.json()
assert "doc_id" in data
assert "reindex_required" in data
assert "grounding_available" in data
# ---------------------------------------------------------------------------
# Strategy registry: new experimental strategies
# ---------------------------------------------------------------------------
def test_self_rag_strategy_available():
from auralynq.rag import get_registry
registry = get_registry()
strategies = registry.list_all()
sr = next(s for s in strategies if s["id"] == "self_rag")
assert sr["available"] is True
assert sr["status"] == "experimental"
def test_crag_strategy_available():
from auralynq.rag import get_registry
registry = get_registry()
strategies = registry.list_all()
crag = next(s for s in strategies if s["id"] == "crag")
assert crag["available"] is True
assert crag["status"] == "experimental"
def test_adaptive_strategy_available():
from auralynq.rag import get_registry
registry = get_registry()
strategies = registry.list_all()
adaptive = next(s for s in strategies if s["id"] == "adaptive")
assert adaptive["available"] is True
assert adaptive["status"] == "experimental"
def test_lightrag_local_unavailable():
from auralynq.rag import get_registry
registry = get_registry()
strategies = registry.list_all()
lr = next(s for s in strategies if s["id"] == "lightrag_local")
assert lr["available"] is False
assert lr["unavailable_reason"] is not None
def test_raptor_unavailable():
from auralynq.rag import get_registry
registry = get_registry()
strategies = registry.list_all()
raptor = next(s for s in strategies if s["id"] == "raptor")
assert raptor["available"] is False
assert "hierarchical" in raptor["unavailable_reason"].lower()
def test_all_strategies_have_status_field():
from auralynq.rag import get_registry
registry = get_registry()
for s in registry.list_all():
assert s["status"] in ("available", "experimental", "planned"), f"{s['id']} has bad status"