Spaces:
Sleeping
Sleeping
File size: 5,965 Bytes
b76f199 | 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 | """Coarse β fine retrieval: document β section β paragraph."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from app.config import settings
from app.models.schemas import SearchResult
from app.retrieval.retriever import retrieve_document_level_context, retrieve_for_report
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
def _exemplar_doc_ids() -> list[str]:
raw = (settings.rics_exemplar_document_ids or "").strip()
if not raw:
return []
return [x.strip() for x in raw.split(",") if x.strip()]
def _unique_by_doc(scored: list[SearchResult], limit: int) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for r in scored:
if r.doc_id in seen:
continue
seen.add(r.doc_id)
out.append(r.doc_id)
if len(out) >= limit:
break
return out
def retrieve_hierarchical_for_report(
*,
query: str,
tenant_id: str,
template_id: str,
skeleton_excerpt: str,
primary_document_id: str | None,
reference_document_ids: list[str] | None,
extra_doc_ids: list[str] | None = None,
product_label: str | None = None,
) -> tuple[list[SearchResult], list[SearchResult], list[SearchResult]]:
"""Return ``(document_hits, section_hits, paragraph_pool)`` for generation.
Paragraph hits should be reranked and truncated to ``rerank_top_n`` by the caller.
When the index has no ``hierarchy_level=document`` rows (legacy ingest), routing
falls back to prioritised flat retrieval for all three lists.
"""
from app.vectorstore.factory import get_vectorstore
vs = get_vectorstore()
refs = list(reference_document_ids or [])
exemplars = _exemplar_doc_ids()
secondary = list(dict.fromkeys(refs + exemplars))
extras = [x.strip() for x in (extra_doc_ids or []) if x and str(x).strip()]
sk = (skeleton_excerpt or "").strip().replace("\n", " ")[:500]
pl = (product_label or "RICS Home Survey Level 3 (Building Survey)").strip()
doc_broad_query = (
f"{pl} report {template_id}. Whole document scope and narrative. {sk} {query[:400]}"
)
k_doc = settings.hierarchical_k_document
k_sec = settings.hierarchical_k_section
k_pool = settings.hierarchical_k_paragraph_pool
# ββ Document-level ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
doc_hits = vs.search(
doc_broad_query,
tenant_id,
k=max(k_doc * 25, 40),
hierarchy_level="document",
)
top_doc_ids = _unique_by_doc(doc_hits, max(k_doc, 6))
# Legacy index: no document-level rows
if not doc_hits:
flat = retrieve_for_report(
query=query,
tenant_id=tenant_id,
primary_document_id=primary_document_id,
secondary_document_ids=secondary,
k=max(k_pool, 24),
)
doc_ctx = retrieve_document_level_context(
template_id=template_id,
skeleton_excerpt=skeleton_excerpt,
tenant_id=tenant_id,
primary_document_id=primary_document_id,
reference_document_ids=secondary,
product_label=product_label,
)
return doc_ctx, [], flat
# Preferred doc universe: routing βͺ primary βͺ references βͺ runtime overlays
universe: set[str] = set(top_doc_ids)
if primary_document_id:
universe.add(str(primary_document_id))
universe.update(secondary)
universe.update(extras)
allowed = frozenset(universe)
doc_filtered = [r for r in doc_hits if r.doc_id in allowed][:k_doc]
# Ensure document-tier rows exist for forced ids (e.g. runtime-edited sections) that did not rank globally.
if extras:
have_doc_tier = {r.doc_id for r in doc_filtered}
for eid in extras:
if eid in have_doc_tier:
continue
extra_rows = vs.search(
doc_broad_query,
tenant_id,
k=12,
hierarchy_level="document",
doc_id_in=frozenset({eid}),
)
if extra_rows:
doc_filtered.append(extra_rows[0])
have_doc_tier.add(eid)
doc_filtered = doc_filtered[:k_doc]
# ββ Section-level (within routed docs) βββββββββββββββββββββββββββββββββ
sec_hits = vs.search(
query,
tenant_id,
k=max(k_sec * 30, 60),
hierarchy_level="section",
doc_id_in=allowed,
)
sec_filtered = sec_hits[:k_sec]
# ββ Paragraph pool βββββββββββββββββββββββββββββββββββββββββββββββββββββ
para_hits = vs.search(
query,
tenant_id,
k=max(k_pool * 25, 80),
hierarchy_level="paragraph",
doc_id_in=allowed,
)
if len(para_hits) < max(8, k_pool // 2):
# Backfill from flat search (e.g. older paragraph rows without hierarchy tag)
extra = retrieve_for_report(
query=query,
tenant_id=tenant_id,
primary_document_id=primary_document_id,
secondary_document_ids=secondary,
k=k_pool,
)
seen = {r.chunk_id for r in para_hits}
for r in extra:
if r.chunk_id not in seen:
seen.add(r.chunk_id)
para_hits.append(r)
if len(para_hits) >= k_pool:
break
para_filtered = para_hits[:k_pool]
logger.debug(
"hierarchical RAG: doc=%d sec=%d para_pool=%d universe=%d",
len(doc_filtered),
len(sec_filtered),
len(para_filtered),
len(allowed),
)
return doc_filtered, sec_filtered, para_filtered
|