Skip to content

Commit a53cc91

Browse files
MARSclaude
andcommitted
feat(rag): multi-index routing via corpus_filter (corpus/ace/jim)
_execute_rag_query now routes to the correct TF-IDF sub-index based on corpus_filter values: 'documents'/'corpus' -> collection (one entry per document) 'findings'/'ace' -> ace_collection (one per anomaly finding) 'patterns'/'jim' -> jim_collection (cross-jurisdiction patterns) No filter -> all three indices are queried and sources merged by score. Added module-level _rag_cache to avoid reloading numpy/vocab on every request. _resolve_target_indices() maps filter terms to (index, vocab) pairs with deduplication and fallback to all indices for unknown terms. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 44a48e3 commit a53cc91

1 file changed

Lines changed: 108 additions & 23 deletions

File tree

  • src/oraculus_di_auditor/interface

src/oraculus_di_auditor/interface/api.py

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -461,45 +461,130 @@ def _load_jurisdiction_config_at_startup() -> Any:
461461
return None
462462

463463

464+
# Maps corpus_filter terms to (index_name, vocab_filename) pairs.
465+
# The index builder (scripts/build_rag_index.py) creates all three collections.
466+
_INDEX_MAP: dict[str, tuple[str, str]] = {
467+
"documents": ("collection", "data/vectors/collection_vocab.pkl"),
468+
"corpus": ("collection", "data/vectors/collection_vocab.pkl"),
469+
"findings": ("ace_collection", "data/vectors/ace_collection_vocab.pkl"),
470+
"ace": ("ace_collection", "data/vectors/ace_collection_vocab.pkl"),
471+
"patterns": ("jim_collection", "data/vectors/jim_collection_vocab.pkl"),
472+
"jim": ("jim_collection", "data/vectors/jim_collection_vocab.pkl"),
473+
}
474+
475+
_ALL_INDICES: list[tuple[str, str]] = [
476+
("collection", "data/vectors/collection_vocab.pkl"),
477+
("ace_collection", "data/vectors/ace_collection_vocab.pkl"),
478+
("jim_collection", "data/vectors/jim_collection_vocab.pkl"),
479+
]
480+
481+
# Module-level OracRAG cache keyed by index_name — avoids reloading TF-IDF
482+
# vocabulary and numpy arrays on every request.
483+
_rag_cache: dict[str, Any] = {}
484+
485+
486+
def _get_rag_instance(index_name: str, vocab_path: str) -> Any:
487+
"""Return a cached OracRAG instance for *index_name*, loading it on first use."""
488+
if index_name not in _rag_cache:
489+
from oraculus_di_auditor.rag import OracRAG
490+
491+
instance = OracRAG()
492+
try:
493+
instance.load_index(index_name=index_name, vocab_path=vocab_path)
494+
except FileNotFoundError:
495+
return None
496+
_rag_cache[index_name] = instance
497+
return _rag_cache[index_name]
498+
499+
500+
def _resolve_target_indices(
501+
corpus_filter: list[str] | None,
502+
) -> list[tuple[str, str]]:
503+
"""Map corpus_filter values to (index_name, vocab_path) pairs.
504+
505+
Unknown filter terms are ignored. No filter → all three indices.
506+
"""
507+
if not corpus_filter:
508+
return _ALL_INDICES
509+
seen: set[str] = set()
510+
targets: list[tuple[str, str]] = []
511+
for term in corpus_filter:
512+
pair = _INDEX_MAP.get(term.lower())
513+
if pair and pair[0] not in seen:
514+
targets.append(pair)
515+
seen.add(pair[0])
516+
return targets or _ALL_INDICES
517+
518+
464519
def _execute_rag_query(
465520
request: RAGQueryRequest,
466521
) -> RAGQueryResponse:
467-
"""Execute a RAG query, returning a populated response or an error response.
522+
"""Execute a RAG query across one or more vector indices.
468523
469-
Args:
470-
request: RAG query request containing query text, top_k, and optional filter
471-
472-
Returns:
473-
RAGQueryResponse with answer, sources, confidence, and optional error
524+
corpus_filter routes the query to specific sub-indices:
525+
"documents"/"corpus" → collection (one entry per document)
526+
"findings"/"ace" → ace_collection (one entry per anomaly)
527+
"patterns"/"jim" → jim_collection (cross-jurisdiction patterns)
528+
No filter → all three indices are queried and sources are merged.
474529
"""
475530
_logger = logging.getLogger(__name__)
476531
try:
477-
from oraculus_di_auditor.rag import OracRAG
532+
targets = _resolve_target_indices(request.corpus_filter)
478533

479-
orac_rag = OracRAG()
480-
# TODO: Cache loaded index in production (e.g., using functools.lru_cache
481-
# or global singleton) to avoid reloading on every request
482-
orac_rag.load_index(index_name="collection")
534+
all_sources: list[dict] = []
535+
primary_answer = ""
536+
primary_confidence = 0.0
483537

484-
if request.corpus_filter:
485-
result = orac_rag.query_with_filter(
486-
question=request.query,
487-
corpus_ids=request.corpus_filter,
488-
top_k=request.top_k,
489-
)
490-
else:
491-
result = orac_rag.query(
538+
for index_name, vocab_path in targets:
539+
rag = _get_rag_instance(index_name, vocab_path)
540+
if rag is None:
541+
_logger.debug("Index '%s' not built yet — skipping", index_name)
542+
continue
543+
544+
result = rag.query(
492545
question=request.query,
493546
top_k=request.top_k,
494547
include_sources=True,
495548
)
496549

550+
sources = result.get("sources", [])
551+
for src in sources:
552+
src.setdefault("index", index_name)
553+
all_sources.extend(sources)
554+
555+
conf = result.get("confidence") or 0.0
556+
if conf > primary_confidence:
557+
primary_confidence = conf
558+
primary_answer = result.get("answer", "")
559+
560+
if not all_sources and not primary_answer:
561+
return RAGQueryResponse(
562+
answer="No indexed data found. Run scripts/build_rag_index.py first.",
563+
sources=[],
564+
confidence=0.0,
565+
)
566+
567+
# Deduplicate and sort sources by score descending
568+
seen_ids: set[str] = set()
569+
merged: list[dict] = []
570+
for src in sorted(all_sources, key=lambda s: s.get("score", 0), reverse=True):
571+
sid = src.get("id") or src.get("source_id") or str(src)
572+
if sid not in seen_ids:
573+
seen_ids.add(sid)
574+
merged.append(src)
575+
merged = merged[: request.top_k]
576+
497577
_logger.debug(
498-
"RAG query complete: confidence=%.2f, sources=%d",
499-
result.get("confidence", 0),
500-
len(result.get("sources", [])),
578+
"RAG query complete: indices=%s confidence=%.2f sources=%d",
579+
[t[0] for t in targets],
580+
primary_confidence,
581+
len(merged),
582+
)
583+
return RAGQueryResponse(
584+
answer=primary_answer,
585+
sources=merged,
586+
confidence=round(primary_confidence, 3),
501587
)
502-
return RAGQueryResponse(**result)
503588

504589
except ImportError as e:
505590
_logger.error("RAG not available: %s", e)

0 commit comments

Comments
 (0)