Report-Genius / backend /startup.py
StormShadow308's picture
Deploy RICS v2 backend (CPU embedder + reranker, baked jina models)
a671976
Raw
History Blame Contribute Delete
6.66 kB
"""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