davidtran999 commited on
Commit
d2c3c89
·
verified ·
1 Parent(s): 6260a86

Upload backend/hue_portal/chatbot/slow_path_handler.py with huggingface_hub

Browse files
backend/hue_portal/chatbot/slow_path_handler.py CHANGED
@@ -4,9 +4,12 @@ Slow Path Handler - Full RAG pipeline for complex queries.
4
  import os
5
  import time
6
  import logging
7
- from typing import Dict, Any, Optional, List
 
8
  import unicodedata
9
  import re
 
 
10
 
11
  from hue_portal.core.chatbot import get_chatbot, RESPONSE_TEMPLATES
12
  from hue_portal.core.models import (
@@ -18,12 +21,15 @@ from hue_portal.core.models import (
18
  LegalDocument,
19
  )
20
  from hue_portal.core.search_ml import search_with_ml
 
21
  # Lazy import reranker to avoid blocking startup (FlagEmbedding may download model)
22
  # from hue_portal.core.reranker import rerank_documents
23
  from hue_portal.chatbot.llm_integration import get_llm_generator
24
  from hue_portal.chatbot.structured_legal import format_structured_legal_answer
25
  from hue_portal.chatbot.context_manager import ConversationContext
26
  from hue_portal.chatbot.router import DOCUMENT_CODE_PATTERNS
 
 
27
 
28
  logger = logging.getLogger(__name__)
29
 
@@ -34,6 +40,15 @@ class SlowPathHandler:
34
  def __init__(self):
35
  self.chatbot = get_chatbot()
36
  self.llm_generator = get_llm_generator()
 
 
 
 
 
 
 
 
 
37
 
38
  def handle(
39
  self,
@@ -54,6 +69,7 @@ class SlowPathHandler:
54
  query: User query.
55
  intent: Detected intent.
56
  session_id: Optional session ID for context.
 
57
 
58
  Returns:
59
  Response dict with message, intent, results, etc.
@@ -62,7 +78,7 @@ class SlowPathHandler:
62
  selected_document_code_normalized = (
63
  selected_document_code.strip().upper() if selected_document_code else None
64
  )
65
-
66
  # Handle greetings
67
  if intent == "greeting":
68
  query_lower = query.lower().strip()
@@ -80,7 +96,7 @@ class SlowPathHandler:
80
  "count": 0,
81
  "_source": "slow_path"
82
  }
83
-
84
  # Wizard / option-first cho mọi câu hỏi pháp lý chung:
85
  # Nếu:
86
  # - intent là search_legal
@@ -101,68 +117,236 @@ class SlowPathHandler:
101
  and not selected_document_code_normalized
102
  and not has_explicit_code
103
  ):
104
- logger.info("[WIZARD] ✅ Wizard conditions met, returning options payload")
105
- canonical_candidates: List[Dict[str, Any]] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  try:
107
- canonical_docs = list(
108
- LegalDocument.objects.filter(
109
- code__in=["264-QD-TW", "QD-69-TW", "TT-02-CAND"]
110
- )
 
 
 
 
 
 
 
 
 
111
  )
112
- for doc in canonical_docs:
113
- summary = getattr(doc, "summary", "") or ""
114
- metadata = getattr(doc, "metadata", {}) or {}
115
- if not summary and isinstance(metadata, dict):
116
- summary = metadata.get("summary", "")
117
- canonical_candidates.append(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  {
119
- "code": doc.code,
120
- "title": getattr(doc, "title", "") or doc.code,
121
- "summary": summary,
122
- "doc_type": getattr(doc, "doc_type", "") or "",
123
- "section_title": "",
124
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  except Exception as exc:
127
- logger.warning(
128
- "[CLARIFICATION] Canonical documents lookup failed, using static list: %s",
129
  exc,
 
130
  )
131
-
132
- if not canonical_candidates:
133
- canonical_candidates = [
134
- {
135
- "code": "264-QD-TW",
136
- "title": "Quyết định 264-QĐ/TW về kỷ luật đảng viên",
137
- "summary": "",
138
- "doc_type": "",
139
- "section_title": "",
140
- },
141
- {
142
- "code": "QD-69-TW",
143
- "title": "Quy định 69-QĐ/TW về kỷ luật tổ chức đảng, đảng viên",
144
- "summary": "",
145
- "doc_type": "",
146
- "section_title": "",
147
- },
148
- {
149
- "code": "TT-02-CAND",
150
- "title": "Thông tư 02/2021/TT-BCA về điều lệnh CAND",
151
- "summary": "",
152
- "doc_type": "",
153
- "section_title": "",
154
- },
155
- ]
156
-
157
- clarification_payload = self._build_clarification_payload(
158
- query, canonical_candidates
159
- )
160
- if clarification_payload:
161
- clarification_payload.setdefault("intent", intent)
162
- clarification_payload.setdefault("_source", "clarification")
163
- clarification_payload.setdefault("routing", "clarification")
164
- clarification_payload.setdefault("confidence", 0.3)
165
- return clarification_payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  # Search based on intent - retrieve top-15 for reranking (balance speed and RAM)
168
  search_result = self._search_by_intent(
@@ -391,7 +575,7 @@ class SlowPathHandler:
391
  }
392
 
393
  return response
394
-
395
  def _maybe_request_clarification(
396
  self,
397
  query: str,
@@ -651,6 +835,193 @@ class SlowPathHandler:
651
  logger.warning("[CLARIFICATION] LLM suggestion failed: %s", exc)
652
  return None
653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  def _search_by_intent(
655
  self,
656
  intent: str,
@@ -738,13 +1109,12 @@ class SlowPathHandler:
738
  )
739
  else:
740
  logger.debug("[SEARCH] No document code detected for query: %s", query)
741
- # Retrieve top-15 for reranking (will be reduced to top-4 after rerank)
742
- search_results = search_with_ml(
 
743
  qs,
744
- keywords,
745
- text_fields,
746
  top_k=limit, # limit=15 for reranking, will be reduced to 4
747
- min_score=0.02, # Lower threshold for legal
748
  )
749
  results = self._format_legal_results(search_results, detected_code, query=query)
750
  logger.info(
 
4
  import os
5
  import time
6
  import logging
7
+ import hashlib
8
+ from typing import Dict, Any, Optional, List, Set
9
  import unicodedata
10
  import re
11
+ from concurrent.futures import ThreadPoolExecutor, Future
12
+ import threading
13
 
14
  from hue_portal.core.chatbot import get_chatbot, RESPONSE_TEMPLATES
15
  from hue_portal.core.models import (
 
21
  LegalDocument,
22
  )
23
  from hue_portal.core.search_ml import search_with_ml
24
+ from hue_portal.core.pure_semantic_search import pure_semantic_search
25
  # Lazy import reranker to avoid blocking startup (FlagEmbedding may download model)
26
  # from hue_portal.core.reranker import rerank_documents
27
  from hue_portal.chatbot.llm_integration import get_llm_generator
28
  from hue_portal.chatbot.structured_legal import format_structured_legal_answer
29
  from hue_portal.chatbot.context_manager import ConversationContext
30
  from hue_portal.chatbot.router import DOCUMENT_CODE_PATTERNS
31
+ from hue_portal.core.query_rewriter import get_query_rewriter
32
+ from hue_portal.core.pure_semantic_search import pure_semantic_search, parallel_vector_search
33
 
34
  logger = logging.getLogger(__name__)
35
 
 
40
  def __init__(self):
41
  self.chatbot = get_chatbot()
42
  self.llm_generator = get_llm_generator()
43
+ # Thread pool for parallel search (max 2 workers to avoid overwhelming DB)
44
+ self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="parallel_search")
45
+ # Cache for prefetched results by session_id (in-memory fallback)
46
+ self._prefetched_cache: Dict[str, Dict[str, Any]] = {}
47
+ self._cache_lock = threading.Lock()
48
+ # Redis cache for prefetch results
49
+ self.redis_cache = get_redis_cache()
50
+ # Prefetch cache TTL (30 minutes default)
51
+ self.prefetch_cache_ttl = int(os.environ.get("CACHE_PREFETCH_TTL", "1800"))
52
 
53
  def handle(
54
  self,
 
69
  query: User query.
70
  intent: Detected intent.
71
  session_id: Optional session ID for context.
72
+ selected_document_code: Selected document code from wizard.
73
 
74
  Returns:
75
  Response dict with message, intent, results, etc.
 
78
  selected_document_code_normalized = (
79
  selected_document_code.strip().upper() if selected_document_code else None
80
  )
81
+
82
  # Handle greetings
83
  if intent == "greeting":
84
  query_lower = query.lower().strip()
 
96
  "count": 0,
97
  "_source": "slow_path"
98
  }
99
+
100
  # Wizard / option-first cho mọi câu hỏi pháp lý chung:
101
  # Nếu:
102
  # - intent là search_legal
 
117
  and not selected_document_code_normalized
118
  and not has_explicit_code
119
  ):
120
+ logger.info("[QUERY_REWRITE] ✅ Wizard conditions met, using Query Rewrite Strategy")
121
+
122
+ # Query Rewrite Strategy: Rewrite query into 3-5 optimized legal queries
123
+ query_rewriter = get_query_rewriter(self.llm_generator)
124
+
125
+ # Get conversation context for query rewriting
126
+ context = None
127
+ if session_id:
128
+ try:
129
+ recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
130
+ context = [
131
+ {"role": msg.role, "content": msg.content}
132
+ for msg in recent_messages
133
+ ]
134
+ except Exception as exc:
135
+ logger.warning("[QUERY_REWRITE] Failed to load context: %s", exc)
136
+
137
+ # Rewrite query into 3-5 queries
138
+ rewritten_queries = query_rewriter.rewrite_query(
139
+ query,
140
+ context=context,
141
+ max_queries=5,
142
+ min_queries=3
143
+ )
144
+
145
+ if not rewritten_queries:
146
+ # Fallback to original query if rewrite fails
147
+ rewritten_queries = [query]
148
+
149
+ logger.info(
150
+ "[QUERY_REWRITE] Rewrote query into %d queries: %s",
151
+ len(rewritten_queries),
152
+ rewritten_queries[:3]
153
+ )
154
+
155
+ # Parallel vector search with multiple queries
156
  try:
157
+ from hue_portal.core.models import LegalSection
158
+
159
+ # Search all legal sections (no document filter yet)
160
+ qs = LegalSection.objects.all()
161
+ text_fields = ["section_title", "section_code", "content"]
162
+
163
+ # Use parallel vector search
164
+ search_results = parallel_vector_search(
165
+ rewritten_queries,
166
+ qs,
167
+ top_k_per_query=5,
168
+ final_top_k=7,
169
+ text_fields=text_fields
170
  )
171
+
172
+ # Extract unique document codes from results
173
+ doc_codes_seen: Set[str] = set()
174
+ document_options: List[Dict[str, Any]] = []
175
+
176
+ for section, score in search_results:
177
+ doc = getattr(section, "document", None)
178
+ if not doc:
179
+ continue
180
+
181
+ doc_code = getattr(doc, "code", "").upper()
182
+ if not doc_code or doc_code in doc_codes_seen:
183
+ continue
184
+
185
+ doc_codes_seen.add(doc_code)
186
+
187
+ # Get document metadata
188
+ doc_title = getattr(doc, "title", "") or doc_code
189
+ doc_summary = getattr(doc, "summary", "") or ""
190
+ if not doc_summary:
191
+ metadata = getattr(doc, "metadata", {}) or {}
192
+ if isinstance(metadata, dict):
193
+ doc_summary = metadata.get("summary", "")
194
+
195
+ document_options.append({
196
+ "code": doc_code,
197
+ "title": doc_title,
198
+ "summary": doc_summary,
199
+ "score": float(score),
200
+ "doc_type": getattr(doc, "doc_type", "") or "",
201
+ })
202
+
203
+ # Limit to top 5 documents
204
+ if len(document_options) >= 5:
205
+ break
206
+
207
+ # If no documents found, use canonical fallback
208
+ if not document_options:
209
+ logger.warning("[QUERY_REWRITE] No documents found, using canonical fallback")
210
+ canonical_candidates = [
211
  {
212
+ "code": "264-QD-TW",
213
+ "title": "Quyết định 264-QĐ/TW về kỷ luật đảng viên",
214
+ "summary": "",
215
+ "doc_type": "",
216
+ },
217
+ {
218
+ "code": "QD-69-TW",
219
+ "title": "Quy định 69-QĐ/TW về kỷ luật tổ chức đảng, đảng viên",
220
+ "summary": "",
221
+ "doc_type": "",
222
+ },
223
+ {
224
+ "code": "TT-02-CAND",
225
+ "title": "Thông tư 02/2021/TT-BCA về điều lệnh CAND",
226
+ "summary": "",
227
+ "doc_type": "",
228
+ },
229
+ ]
230
+ clarification_payload = self._build_clarification_payload(
231
+ query, canonical_candidates
232
  )
233
+ if clarification_payload:
234
+ clarification_payload.setdefault("intent", intent)
235
+ clarification_payload.setdefault("_source", "clarification")
236
+ clarification_payload.setdefault("routing", "clarification")
237
+ clarification_payload.setdefault("confidence", 0.3)
238
+ return clarification_payload
239
+
240
+ # Build options from search results
241
+ options = [
242
+ {
243
+ "code": opt["code"],
244
+ "title": opt["title"],
245
+ "reason": opt.get("summary") or f"Độ liên quan: {opt['score']:.2f}",
246
+ }
247
+ for opt in document_options
248
+ ]
249
+
250
+ # Add "Khác" option
251
+ if not any(opt.get("code") == "__other__" for opt in options):
252
+ options.append({
253
+ "code": "__other__",
254
+ "title": "Khác",
255
+ "reason": "Tôi muốn hỏi văn bản hoặc chủ đề pháp luật khác.",
256
+ })
257
+
258
+ message = (
259
+ "Tôi đã tìm thấy các văn bản pháp luật liên quan đến câu hỏi của bạn.\n\n"
260
+ "Bạn hãy chọn văn bản muốn tra cứu để tôi trả lời chi tiết hơn:"
261
+ )
262
+
263
+ logger.info(
264
+ "[QUERY_REWRITE] ✅ Found %d documents using Query Rewrite Strategy",
265
+ len(document_options)
266
+ )
267
+
268
+ return {
269
+ "type": "options",
270
+ "wizard_stage": "choose_document",
271
+ "message": message,
272
+ "options": options,
273
+ "clarification": {
274
+ "message": message,
275
+ "options": options,
276
+ },
277
+ "results": [],
278
+ "count": 0,
279
+ "intent": intent,
280
+ "_source": "query_rewrite",
281
+ "routing": "query_rewrite",
282
+ "confidence": 0.95, # High confidence with Query Rewrite Strategy
283
+ }
284
+
285
  except Exception as exc:
286
+ logger.error(
287
+ "[QUERY_REWRITE] Error in Query Rewrite Strategy: %s, falling back to LLM suggestions",
288
  exc,
289
+ exc_info=True
290
  )
291
+ # Fallback to original LLM-based clarification
292
+ canonical_candidates: List[Dict[str, Any]] = []
293
+ try:
294
+ canonical_docs = list(
295
+ LegalDocument.objects.filter(
296
+ code__in=["264-QD-TW", "QD-69-TW", "TT-02-CAND"]
297
+ )
298
+ )
299
+ for doc in canonical_docs:
300
+ summary = getattr(doc, "summary", "") or ""
301
+ metadata = getattr(doc, "metadata", {}) or {}
302
+ if not summary and isinstance(metadata, dict):
303
+ summary = metadata.get("summary", "")
304
+ canonical_candidates.append(
305
+ {
306
+ "code": doc.code,
307
+ "title": getattr(doc, "title", "") or doc.code,
308
+ "summary": summary,
309
+ "doc_type": getattr(doc, "doc_type", "") or "",
310
+ "section_title": "",
311
+ }
312
+ )
313
+ except Exception as e:
314
+ logger.warning("[CLARIFICATION] Canonical documents lookup failed: %s", e)
315
+
316
+ if not canonical_candidates:
317
+ canonical_candidates = [
318
+ {
319
+ "code": "264-QD-TW",
320
+ "title": "Quyết định 264-QĐ/TW về kỷ luật đảng viên",
321
+ "summary": "",
322
+ "doc_type": "",
323
+ "section_title": "",
324
+ },
325
+ {
326
+ "code": "QD-69-TW",
327
+ "title": "Quy định 69-QĐ/TW về kỷ luật tổ chức đảng, đảng viên",
328
+ "summary": "",
329
+ "doc_type": "",
330
+ "section_title": "",
331
+ },
332
+ {
333
+ "code": "TT-02-CAND",
334
+ "title": "Thông tư 02/2021/TT-BCA về điều lệnh CAND",
335
+ "summary": "",
336
+ "doc_type": "",
337
+ "section_title": "",
338
+ },
339
+ ]
340
+
341
+ clarification_payload = self._build_clarification_payload(
342
+ query, canonical_candidates
343
+ )
344
+ if clarification_payload:
345
+ clarification_payload.setdefault("intent", intent)
346
+ clarification_payload.setdefault("_source", "clarification_fallback")
347
+ clarification_payload.setdefault("routing", "clarification")
348
+ clarification_payload.setdefault("confidence", 0.3)
349
+ return clarification_payload
350
 
351
  # Search based on intent - retrieve top-15 for reranking (balance speed and RAM)
352
  search_result = self._search_by_intent(
 
575
  }
576
 
577
  return response
578
+
579
  def _maybe_request_clarification(
580
  self,
581
  query: str,
 
835
  logger.warning("[CLARIFICATION] LLM suggestion failed: %s", exc)
836
  return None
837
 
838
+ def _parallel_search_prepare(
839
+ self,
840
+ document_code: str,
841
+ keywords: List[str],
842
+ session_id: Optional[str] = None,
843
+ ) -> None:
844
+ """
845
+ Trigger parallel search in background when user selects a document option.
846
+ Stores results in cache for Stage 2 (choose topic).
847
+
848
+ Args:
849
+ document_code: Selected document code
850
+ keywords: Keywords extracted from query/options
851
+ session_id: Session ID for caching results
852
+ """
853
+ if not session_id:
854
+ return
855
+
856
+ def _search_task():
857
+ try:
858
+ logger.info(
859
+ "[PARALLEL_SEARCH] Starting background search for doc=%s, keywords=%s",
860
+ document_code,
861
+ keywords[:5],
862
+ )
863
+
864
+ # Check Redis cache first
865
+ cache_key = f"prefetch:{document_code.upper()}:{hashlib.sha256(' '.join(keywords).encode()).hexdigest()[:16]}"
866
+ cached_result = None
867
+ if self.redis_cache and self.redis_cache.is_available():
868
+ cached_result = self.redis_cache.get(cache_key)
869
+ if cached_result:
870
+ logger.info(
871
+ "[PARALLEL_SEARCH] ✅ Cache hit for doc=%s",
872
+ document_code
873
+ )
874
+ # Store in in-memory cache too
875
+ with self._cache_lock:
876
+ if session_id not in self._prefetched_cache:
877
+ self._prefetched_cache[session_id] = {}
878
+ self._prefetched_cache[session_id]["document_results"] = cached_result
879
+ return
880
+
881
+ # Search in the selected document
882
+ query_text = " ".join(keywords) if keywords else ""
883
+ search_result = self._search_by_intent(
884
+ intent="search_legal",
885
+ query=query_text,
886
+ limit=20, # Get more results for topic options
887
+ preferred_document_code=document_code.upper(),
888
+ )
889
+
890
+ # Prepare cache data
891
+ cache_data = {
892
+ "document_code": document_code,
893
+ "results": search_result.get("results", []),
894
+ "count": search_result.get("count", 0),
895
+ "timestamp": time.time(),
896
+ }
897
+
898
+ # Store in Redis cache
899
+ if self.redis_cache and self.redis_cache.is_available():
900
+ self.redis_cache.set(cache_key, cache_data, ttl_seconds=self.prefetch_cache_ttl)
901
+ logger.debug(
902
+ "[PARALLEL_SEARCH] Cached prefetch results (TTL: %ds)",
903
+ self.prefetch_cache_ttl
904
+ )
905
+
906
+ # Store in in-memory cache (fallback)
907
+ with self._cache_lock:
908
+ if session_id not in self._prefetched_cache:
909
+ self._prefetched_cache[session_id] = {}
910
+ self._prefetched_cache[session_id]["document_results"] = cache_data
911
+
912
+ logger.info(
913
+ "[PARALLEL_SEARCH] Completed background search for doc=%s, found %d results",
914
+ document_code,
915
+ search_result.get("count", 0),
916
+ )
917
+ except Exception as exc:
918
+ logger.warning("[PARALLEL_SEARCH] Background search failed: %s", exc)
919
+
920
+ # Submit to thread pool
921
+ self._executor.submit(_search_task)
922
+
923
+ def _parallel_search_topic(
924
+ self,
925
+ document_code: str,
926
+ topic_keywords: List[str],
927
+ session_id: Optional[str] = None,
928
+ ) -> None:
929
+ """
930
+ Trigger parallel search when user selects a topic option.
931
+ Stores results for final answer generation.
932
+
933
+ Args:
934
+ document_code: Selected document code
935
+ topic_keywords: Keywords from selected topic
936
+ session_id: Session ID for caching results
937
+ """
938
+ if not session_id:
939
+ return
940
+
941
+ def _search_task():
942
+ try:
943
+ logger.info(
944
+ "[PARALLEL_SEARCH] Starting topic search for doc=%s, keywords=%s",
945
+ document_code,
946
+ topic_keywords[:5],
947
+ )
948
+
949
+ # Search with topic keywords
950
+ query_text = " ".join(topic_keywords) if topic_keywords else ""
951
+ search_result = self._search_by_intent(
952
+ intent="search_legal",
953
+ query=query_text,
954
+ limit=10,
955
+ preferred_document_code=document_code.upper(),
956
+ )
957
+
958
+ # Store in cache
959
+ with self._cache_lock:
960
+ if session_id not in self._prefetched_cache:
961
+ self._prefetched_cache[session_id] = {}
962
+ self._prefetched_cache[session_id]["topic_results"] = {
963
+ "document_code": document_code,
964
+ "keywords": topic_keywords,
965
+ "results": search_result.get("results", []),
966
+ "count": search_result.get("count", 0),
967
+ "timestamp": time.time(),
968
+ }
969
+
970
+ logger.info(
971
+ "[PARALLEL_SEARCH] Completed topic search, found %d results",
972
+ search_result.get("count", 0),
973
+ )
974
+ except Exception as exc:
975
+ logger.warning("[PARALLEL_SEARCH] Topic search failed: %s", exc)
976
+
977
+ # Submit to thread pool
978
+ self._executor.submit(_search_task)
979
+
980
+ def _get_prefetched_results(
981
+ self,
982
+ session_id: Optional[str],
983
+ result_type: str = "document_results",
984
+ ) -> Optional[Dict[str, Any]]:
985
+ """
986
+ Get prefetched search results from cache.
987
+
988
+ Args:
989
+ session_id: Session ID
990
+ result_type: "document_results" or "topic_results"
991
+
992
+ Returns:
993
+ Cached results dict or None
994
+ """
995
+ if not session_id:
996
+ return None
997
+
998
+ with self._cache_lock:
999
+ cache_entry = self._prefetched_cache.get(session_id)
1000
+ if not cache_entry:
1001
+ return None
1002
+
1003
+ results = cache_entry.get(result_type)
1004
+ if not results:
1005
+ return None
1006
+
1007
+ # Check if results are still fresh (within 5 minutes)
1008
+ timestamp = results.get("timestamp", 0)
1009
+ if time.time() - timestamp > 300: # 5 minutes
1010
+ logger.debug("[PARALLEL_SEARCH] Prefetched results expired for session=%s", session_id)
1011
+ return None
1012
+
1013
+ return results
1014
+
1015
+ def _clear_prefetched_cache(self, session_id: Optional[str]) -> None:
1016
+ """Clear prefetched cache for a session."""
1017
+ if not session_id:
1018
+ return
1019
+
1020
+ with self._cache_lock:
1021
+ if session_id in self._prefetched_cache:
1022
+ del self._prefetched_cache[session_id]
1023
+ logger.debug("[PARALLEL_SEARCH] Cleared cache for session=%s", session_id)
1024
+
1025
  def _search_by_intent(
1026
  self,
1027
  intent: str,
 
1109
  )
1110
  else:
1111
  logger.debug("[SEARCH] No document code detected for query: %s", query)
1112
+ # Use pure semantic search (100% vector, no BM25)
1113
+ search_results = pure_semantic_search(
1114
+ [keywords],
1115
  qs,
 
 
1116
  top_k=limit, # limit=15 for reranking, will be reduced to 4
1117
+ text_fields=text_fields
1118
  )
1119
  results = self._format_legal_results(search_results, detected_code, query=query)
1120
  logger.info(