@@ -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+
464519def _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