"""Startup ingest: report template (schema) + standard paragraphs (MASTER RAG). The operator bundle in ``Master Standard report and paragraphs/`` contains: - PDF report template -> section structure (schema.json) - Word standard paras -> approved wording (MASTER FAISS) No user upload is required before the first report. """ from __future__ import annotations import logging import shutil from backend.config import settings from backend.domain import template_discoverer from backend.domain.notes import parser as notes_parser from backend.domain.rics_level3_schema import PARENT_SECTION_COUNT from backend.ingest import pipeline as ingest from backend.pii import scrubber as pii_scrubber from backend.rag.index_guard import ensure_reference_indices_clean from backend.rag.store import get_rag_store from backend.rag.types import TIER_MASTER, TIER_REFERENCE from backend.storage import tenant_store from backend.utils.runtime_paths import ensure_data_drive_runtime_dirs logger = logging.getLogger(__name__) def _scrub_stale_faiss_write_artifacts() -> None: """Remove half-written FAISS/meta files left by interrupted persists.""" tenants_root = settings.data_dir_path / "tenants" if not tenants_root.is_dir(): return for pattern in ( "*.write.faiss", "*.write.json", "index.faiss.bad", "meta.json.bad", ): for path in tenants_root.rglob(pattern): try: path.unlink(missing_ok=True) except OSError as exc: logger.debug("Could not remove stale artifact %s: %s", path, exc) def _load_prebuilt() -> bool: """Copy prebuilt schema.json + MASTER FAISS into the tenant store, if set.""" schema_src = settings.master_template_prebuilt_schema faiss_src = settings.master_template_prebuilt_faiss if not (schema_src and faiss_src): return False schema_path = settings.resolve_path(schema_src) faiss_path = settings.resolve_path(faiss_src) if not (schema_path.is_file() and faiss_path.is_dir()): logger.warning( "Prebuilt artifacts configured but missing; falling back to discovery." ) return False tenant = settings.default_tenant_id shutil.copyfile(schema_path, tenant_store.schema_path(tenant)) dest = tenant_store.faiss_dir(tenant, TIER_MASTER) for item in faiss_path.iterdir(): shutil.copy2(item, dest / item.name) logger.info("Loaded prebuilt master artifacts for tenant=%s", tenant) return True def run_startup_ingest() -> dict: """Ingest the operator bundle. Returns a summary for the health endpoint.""" ensure_data_drive_runtime_dirs() _scrub_stale_faiss_write_artifacts() tenant = settings.default_tenant_id summary: dict = { "master_loaded": False, "sections": 0, "paragraph_chunks": 0, "report_template": settings.report_template_filename, "standard_paragraphs": settings.standard_paragraphs_filename, "reference_documents": 0, } if not settings.master_template_auto_ingest: logger.info( "master_template_auto_ingest disabled; reference-only generation. " "Purging any persisted MASTER tier for tenant=%s.", tenant, ) template_discoverer.ensure_canonical_schema(tenant) # Reference-only model: make removal real and idempotent so stale operator # boilerplate from a prior master-enabled run cannot linger in the index. get_rag_store().clear_tier(tenant, TIER_MASTER) index_guard = ensure_reference_indices_clean() summary["reference_index_guard"] = index_guard summary["master_loaded"] = True # schema (canonical) is present summary["sections"] = PARENT_SECTION_COUNT summary["section_anchor_vectors"] = notes_parser.initialize_section_anchors() if settings.reference_auto_ingest_enabled and not index_guard.get("rebuilt"): try: ref = ingest.auto_ingest_reference_dir(tenant) summary["reference_documents"] = ref["documents"] except Exception as exc: # noqa: BLE001 logger.warning("Reference auto-ingest failed: %s", exc) return summary loaded = False if _load_prebuilt(): loaded = True else: try: res = ingest.ingest_operator_bundle(tenant) summary["sections"] = res["sections"] summary["paragraph_chunks"] = res["paragraph_chunks"] loaded = True logger.info( "Operator bundle ingested: schema from %s (%d sections), " "paragraphs from %s (%d chunks)", res["report_template"], res["sections"], res["standard_paragraphs"], res["paragraph_chunks"], ) except pii_scrubber.PiiDetectedError as exc: logger.critical( "STANDARD PARAGRAPHS REJECTED — contains property-identifying PII: %s", exc, ) except FileNotFoundError as exc: logger.error("Operator bundle file missing: %s", exc) except Exception as exc: # noqa: BLE001 logger.exception("Operator bundle ingest failed: %s", exc) template_discoverer.ensure_canonical_schema(tenant) index_guard = ensure_reference_indices_clean() summary["reference_index_guard"] = index_guard summary["master_loaded"] = ( loaded or template_discoverer.load_schema(tenant) is not None ) summary["sections"] = PARENT_SECTION_COUNT if settings.reference_auto_ingest_enabled and not index_guard.get("rebuilt"): try: ref = ingest.auto_ingest_reference_dir(tenant) summary["reference_documents"] = ref["documents"] except Exception as exc: # noqa: BLE001 logger.warning("Reference auto-ingest failed: %s", exc) elif index_guard.get("rebuilt"): summary["reference_documents"] = sum( row.get("auto_ingest_docs", 0) + row.get("library_reingested", 0) for row in index_guard.get("rebuilt", []) ) anchor_count = notes_parser.initialize_section_anchors() summary["section_anchor_vectors"] = anchor_count store = get_rag_store() logger.info( "Startup complete: loaded=%s sections=%d paragraph_chunks=%d reference_chunks=%d anchors=%d", summary["master_loaded"], summary["sections"], store.count(tenant, TIER_MASTER), store.count(tenant, TIER_REFERENCE), anchor_count, ) return summary