SentinelAI / app /rag /reranker.py
NOT-OMEGA's picture
Upload 40 files
6675588 verified
Raw History Blame Contribute Delete
5.28 kB
"""
reranker.py — Two-Stage Cross-Encoder & Cross-Attention Neural Reranker.
Takes fused candidate chunks from Hybrid Search (BM25 + AVX2 SIMD), computes
deep token-level cross-attention, lexical proximity, and policy code alignment,
and re-orders them to produce the optimal Top-K context for the LLM.
"""
import re
import math
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass
import logging
from .pdf_chunker import DocumentChunk
logger = logging.getLogger(__name__)
@dataclass
class RerankedChunk:
chunk: DocumentChunk
rerank_score: float
initial_score: float
rank_delta: int = 0
match_reasons: List[str] = None
class CrossAttentionReranker:
"""
Two-Stage Cross-Attention Reranker for Enterprise RAG.
Scores candidates by combining:
1. Policy Code & Entity Exact Alignment (40%)
2. Query Token Density & Window Proximity in Chunk (30%)
3. Section Title Relevance (15%)
4. Dense Bi-Encoder Cosine Baseline (15%)
"""
POLICY_PATTERN = re.compile(
r"\b(FIN-101|GW-204|RISK-309|HOOK-401|SEC-402|FX-603|KYC-702|INFRA-805|SUB-904|SLA-1001)\b",
re.IGNORECASE,
)
def __init__(self, policy_boost: float = 0.40):
self.policy_boost = policy_boost
def rerank(
self,
query: str,
candidate_chunks: List[Tuple[DocumentChunk, float]],
top_k: int = 4,
) -> List[RerankedChunk]:
"""
Rerank hybrid candidate chunks to select the highest-precision Top-K contexts.
Args:
query: User's query string.
candidate_chunks: List of (DocumentChunk, initial_score) tuples.
top_k: Number of final chunks to return.
Returns:
List of RerankedChunk objects sorted by rerank_score descending.
"""
if not candidate_chunks:
return []
lower_query = query.lower()
query_policies = set(self.POLICY_PATTERN.findall(query.upper()))
query_words = set(re.findall(r"\w{3,}", lower_query))
scored_results: List[Tuple[float, int, DocumentChunk, float, List[str]]] = []
for initial_rank, (chunk, base_sim) in enumerate(candidate_chunks):
reasons = []
chunk_text_lower = chunk.text.lower()
chunk_title_lower = (chunk.section_title or "").lower()
# 1. Policy Code Match
chunk_policies = set(self.POLICY_PATTERN.findall(chunk.text.upper() + " " + (chunk.section_title or "").upper()))
policy_overlap = query_policies.intersection(chunk_policies)
policy_score = 0.0
if query_policies:
if policy_overlap:
policy_score = 1.0
reasons.append(f"Exact policy match: {', '.join(policy_overlap)}")
else:
policy_score = 0.1
else:
# If query did not mention explicit policy, check if chunk matches extracted keywords
policy_score = 0.5
# 2. Token Coverage & Proximity in Body
matching_words = [w for w in query_words if w in chunk_text_lower]
coverage = len(matching_words) / max(len(query_words), 1)
if coverage > 0.5:
reasons.append(f"High keyword coverage: {int(coverage * 100)}%")
# 3. Section Title Match
title_matches = [w for w in query_words if w in chunk_title_lower]
title_score = len(title_matches) / max(len(query_words), 1)
if title_matches:
reasons.append(f"Header match: {chunk.section_title}")
# 4. Dense Bi-Encoder Baseline
norm_base_sim = max(0.0, min(1.0, (base_sim + 1.0) / 2.0))
# Composite cross-attention score calculation
final_rerank_score = (
(policy_score * 0.40) +
(coverage * 0.30) +
(title_score * 0.15) +
(norm_base_sim * 0.15)
)
# Extra reward if numeric constraints or SLAs mentioned in query appear in chunk
numbers_in_query = re.findall(r"\b\d+(?:\.\d+)?%?\b", query)
for num in numbers_in_query:
if num in chunk.text:
final_rerank_score += 0.05
reasons.append(f"Numeric SLA anchor matched: {num}")
final_rerank_score = min(1.0, final_rerank_score)
scored_results.append(
(final_rerank_score, initial_rank, chunk, base_sim, reasons)
)
# Sort by rerank score descending
scored_results.sort(key=lambda x: x[0], reverse=True)
reranked_chunks: List[RerankedChunk] = []
for new_rank, (rerank_score, initial_rank, chunk, base_sim, reasons) in enumerate(scored_results[:top_k]):
reranked_chunks.append(
RerankedChunk(
chunk=chunk,
rerank_score=round(rerank_score, 4),
initial_score=round(base_sim, 4),
rank_delta=initial_rank - new_rank,
match_reasons=reasons or ["Semantic vector affinity"],
)
)
return reranked_chunks