Spaces:
Runtime error
Runtime error
File size: 7,526 Bytes
aad7814 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """Shared test fixtures.
Tests run fully offline: the FAISS store is backed by a deterministic fake
embedder and no OpenAI key is configured, so mapping/grounding take their
deterministic fallback paths.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from docx import Document
from backend.config import settings
from backend.core import embeddings, notes_parser, pii_scrubber, rag_store
from backend.core.rics_canonical_l3 import build_section_anchor_text, iter_canonical_leaf_sections
_ANCHOR_OVERLAP_STOP = frozenset({
"about", "elements", "other", "property", "section", "inside", "outside",
"survey", "rics", "level", "typical", "matters", "engagement", "terms",
"looked", "overall", "fine", "noted", "specific", "defects", "general",
"inspection", "opinion", "declaration", "diagram", "recommendations",
"the", "and", "not", "with", "for", "are", "was", "from", "this", "that",
"have", "has", "were", "its", "any", "all", "may", "can", "your", "our",
})
class AnchorSemanticFakeEmbedder(embeddings.FakeEmbedder):
"""Deterministic embedder that approximates anchor similarity for offline tests."""
def __init__(self, dim: int = 384) -> None:
super().__init__(dim)
self._anchor_texts: dict[str, str] = {}
self._anchor_vecs: dict[str, list[float]] = {}
def register_anchors(self, anchors: dict[str, str]) -> None:
self._anchor_texts = dict(anchors)
self._anchor_vecs = {sid: super()._vec(text) for sid, text in anchors.items()}
@staticmethod
def _token_set(text: str) -> set[str]:
return {
w
for w in re.findall(r"[a-z0-9]{3,}", (text or "").lower())
if w not in _ANCHOR_OVERLAP_STOP
}
def _vec(self, text: str) -> list[float]:
base = super()._vec(text)
if not self._anchor_texts:
return base
words = self._token_set(text)
best_sid: str | None = None
best_overlap = 0
for sid, anchor in self._anchor_texts.items():
overlap = len(words & self._token_set(anchor))
if overlap > best_overlap:
best_overlap = overlap
best_sid = sid
if best_sid and best_overlap > 0:
anchor_vec = self._anchor_vecs[best_sid]
alpha = min(0.95, 0.55 + 0.12 * best_overlap)
blended = [alpha * a + (1.0 - alpha) * b for a, b in zip(anchor_vec, base)]
norm = sum(x * x for x in blended) ** 0.5 or 1.0
return [x / norm for x in blended]
return base
@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data"))
monkeypatch.setattr(settings, "openai_api_key", "")
monkeypatch.setattr(settings, "pii_use_spacy", False)
monkeypatch.setattr(settings, "reference_auto_ingest_enabled", False)
fake = AnchorSemanticFakeEmbedder()
anchors = {
sid: build_section_anchor_text(sid)
for sid, _, _, _ in iter_canonical_leaf_sections()
}
fake.register_anchors(anchors)
monkeypatch.setattr(embeddings, "get_embedder", lambda: fake)
monkeypatch.setattr(rag_store, "get_embedder", lambda: fake)
embeddings.reset_embedder()
rag_store.reset_rag_store()
pii_scrubber.reset_nlp_cache()
notes_parser.reset_section_anchors()
notes_parser.initialize_section_anchors()
yield
notes_parser.reset_section_anchors()
rag_store.reset_rag_store()
def _write_report_template(path: Path) -> Path:
"""Minimal report template: section structure only."""
doc = Document()
doc.add_paragraph("A About the inspection")
doc.add_paragraph("E1 Chimney Stacks")
doc.add_paragraph("E2 Roof Coverings")
doc.add_paragraph("G4 Central Heating")
doc.save(str(path))
return path
def _write_standard_paragraphs(path: Path, *, with_rating: bool = False) -> Path:
"""Standard paragraphs Word master: headings + boilerplate body."""
doc = Document()
doc.add_paragraph("Section E1 - Chimney Stacks")
body = "The chimney stacks were inspected from ground level using binoculars."
if with_rating:
body += " Condition Rating 2 where defects require attention."
doc.add_paragraph(body)
doc.add_paragraph("Section E2 - Roof Coverings")
doc.add_paragraph("The roof covering comprises slate laid to pitched timber rafters.")
doc.add_paragraph("Section G4 - Central Heating")
doc.add_paragraph("The property is served by a gas-fired central heating system.")
doc.save(str(path))
return path
@pytest.fixture
def report_template_docx(tmp_path) -> Path:
return _write_report_template(tmp_path / "report_template.docx")
@pytest.fixture
def standard_paragraphs_docx(tmp_path) -> Path:
return _write_standard_paragraphs(tmp_path / "standard_paragraphs.docx")
@pytest.fixture
def standard_paragraphs_rated_docx(tmp_path) -> Path:
return _write_standard_paragraphs(tmp_path / "standard_paragraphs_rated.docx", with_rating=True)
# Legacy alias used by template_discoverer unit tests.
@pytest.fixture
def master_docx(standard_paragraphs_docx) -> Path:
return standard_paragraphs_docx
@pytest.fixture
def master_docx_rated(standard_paragraphs_rated_docx) -> Path:
return standard_paragraphs_rated_docx
@pytest.fixture
def client(configured_master):
from backend.main import app
from fastapi.testclient import TestClient
with TestClient(app) as c:
yield c
def _write_past_report(path: Path) -> Path:
"""Simulated uploaded past report for REFERENCE-tier generation."""
doc = Document()
doc.add_paragraph("Section E1 - Chimney Stacks")
doc.add_paragraph(
"The chimney stacks were inspected from ground level using binoculars. "
"Inspection was limited to visible external surfaces at the time of survey."
)
doc.add_paragraph("Section E2 - Roof Coverings")
doc.add_paragraph(
"The roof coverings were inspected from ground level using binoculars and, "
"where accessible, from within the roof void. The main roof covering "
"comprises natural slate tiles laid to pitched timber rafters with underfelt "
"and counter battens."
)
doc.add_paragraph("Section G4 - Central Heating")
doc.add_paragraph(
"The property was served by a gas-fired central heating system with "
"radiators to most rooms."
)
doc.save(str(path))
return path
@pytest.fixture
def configured_master(monkeypatch, report_template_docx, standard_paragraphs_docx) -> Path:
"""Point config at a split report template + standard paragraphs bundle.
This fixture models an operator that explicitly configures AND enables a shared
master, so it opts into ``master_template_auto_ingest`` (default-off in prod).
"""
folder = report_template_docx.parent
_write_past_report(folder / "past_report_sample.docx")
monkeypatch.setattr(settings, "master_template_dir", str(folder))
monkeypatch.setattr(settings, "report_template_filename", report_template_docx.name)
monkeypatch.setattr(settings, "standard_paragraphs_filename", standard_paragraphs_docx.name)
monkeypatch.setattr(settings, "master_template_filename", standard_paragraphs_docx.name)
monkeypatch.setattr(settings, "master_template_auto_ingest", True)
monkeypatch.setattr(settings, "reference_auto_ingest_enabled", True)
return standard_paragraphs_docx
|