davidtran999 commited on
Commit
df3858b
·
verified ·
1 Parent(s): 1504d2c

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

Browse files
Files changed (1) hide show
  1. backend/hue_portal/chatbot/chatbot.py +327 -113
backend/hue_portal/chatbot/chatbot.py CHANGED
@@ -136,7 +136,7 @@ class Chatbot(CoreChatbot):
136
  # tránh trả lại các câu trả lời cũ không có options.
137
  cached_response = None
138
  if intent != "search_legal":
139
- cached_response = EXACT_MATCH_CACHE.get(query, intent)
140
  if cached_response:
141
  cached_response["_cache"] = "exact_match"
142
  cached_response["_source"] = cached_response.get("_source", "cache")
@@ -162,128 +162,212 @@ class Chatbot(CoreChatbot):
162
  return cached_response
163
 
164
  # Wizard / option-first ngay tại chatbot layer:
165
- # Nếu câu hỏi search_legal chung, chưa chọn văn bản, không có mã văn bản trong câu hỏi
166
- # => trả về danh sách văn bản để người dùng chọn, không sinh câu trả lời chi tiết.
167
- # ⚠️ QUAN TRỌNG: Wizard check PHẢI ở TRƯỚC nhánh "if intent == search_legal" để được trigger.
 
 
 
168
  has_doc_code_in_query = self._query_has_document_code(query)
169
- print(f"[WIZARD] Chatbot layer check - intent={intent}, selected_doc_code={selected_doc_code}, has_doc_code_in_query={has_doc_code_in_query}, query='{query[:50]}'")
170
- # Logic wizard:
171
- # - Nếu user đã chọn văn bản (selected_doc_code có giá trị) không bật wizard, đi thẳng vào slow_path để trả lời
172
- # - Nếu user chưa chọn và không có mã trong query → bật wizard để user chọn
173
- # - Nếu trong query không bật wizard, đi thẳng vào slow_path
 
 
 
174
  if intent == "search_legal" and not selected_doc_code and not has_doc_code_in_query:
175
- print("[WIZARD] ✅ Chatbot layer wizard triggered, using AI to generate options")
176
- # Load canonical documents từ DB
177
- canonical_candidates = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  try:
179
- canonical_docs = list(
180
- LegalDocument.objects.filter(
181
- code__in=["264-QD-TW", "QD-69-TW", "TT-02-CAND"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  )
184
- for doc in canonical_docs:
185
- summary = getattr(doc, "summary", "") or ""
186
- metadata = getattr(doc, "metadata", {}) or {}
187
- if not summary and isinstance(metadata, dict):
188
- summary = metadata.get("summary", "")
189
- canonical_candidates.append(
190
- {
191
- "code": doc.code,
192
- "title": getattr(doc, "title", "") or doc.code,
193
- "summary": summary,
194
- "doc_type": getattr(doc, "doc_type", "") or "",
195
- "section_title": "",
196
- }
197
- )
198
- except Exception as exc:
199
- logger.warning("[WIZARD] Failed to load canonical documents: %s", exc)
200
 
201
- # Fallback nếu không load được từ DB
202
- if not canonical_candidates:
203
- canonical_candidates = [
204
- {
205
- "code": "264-QD-TW",
206
- "title": "Quyết định 264-QĐ/TW về kỷ luật đảng viên",
207
- "summary": "Quy định chung về xử lý kỷ luật đối với đảng viên vi phạm.",
208
- "doc_type": "",
209
- "section_title": "",
210
- },
211
- {
212
- "code": "QD-69-TW",
213
- "title": "Quy định 69-QĐ/TW về kỷ luật tổ chức đảng, đảng viên",
214
- "summary": "Quy định chi tiết về các hành vi vi phạm và hình thức kỷ luật.",
215
- "doc_type": "",
216
- "section_title": "",
217
- },
218
- {
219
- "code": "TT-02-CAND",
220
- "title": "Thông tư 02/2021/TT-BCA về điều lệnh CAND",
221
- "summary": "Quy định về điều lệnh, lễ tiết, tác phong trong CAND.",
222
- "doc_type": "",
223
- "section_title": "",
224
- },
225
- ]
226
 
227
- # Dùng LLM để đề xuất options dựa trên câu hỏi
228
- clarification_options = []
229
- intro_message = (
230
- "Tôi tìm thấy một số nhóm văn bản có thể liên quan đến câu hỏi của bạn.\n\n"
231
- "Bạn hãy chọn văn bản muốn tra cứu trước, sau đó tôi sẽ trả lời chi tiết hơn:"
232
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  if self.llm_generator:
235
  try:
236
- llm_payload = self.llm_generator.suggest_clarification_topics(
237
- query,
238
- canonical_candidates,
 
 
 
239
  max_options=3,
240
  )
241
  if llm_payload:
242
  intro_message = llm_payload.get("message") or intro_message
243
- raw_options = llm_payload.get("options")
244
- if isinstance(raw_options, list) and len(raw_options) > 0:
245
- clarification_options = [
246
- {
247
- "code": (opt.get("code") or candidate.get("code", "")).upper(),
248
- "title": opt.get("title") or opt.get("document_title") or candidate.get("title", ""),
249
- "reason": opt.get("reason")
250
- or opt.get("summary")
251
- or candidate.get("summary")
252
- or candidate.get("section_title")
253
- or "",
254
- }
255
- for opt, candidate in zip(
256
- raw_options,
257
- canonical_candidates[: len(raw_options)],
258
- )
259
- if (opt.get("code") or candidate.get("code"))
260
- and (opt.get("title") or opt.get("document_title") or candidate.get("title"))
261
- ]
262
- print(f"[WIZARD] ✅ LLM generated {len(clarification_options)} options")
263
  except Exception as exc:
264
- logger.warning("[WIZARD] LLM suggestion failed: %s, using fallback", exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- # Fallback nếu LLM không trả về options hợp lệ
267
- if not clarification_options:
268
- clarification_options = [
269
  {
270
- "code": candidate["code"].upper(),
271
- "title": candidate["title"],
272
- "reason": candidate.get("summary") or candidate.get("section_title") or "",
 
273
  }
274
- for candidate in canonical_candidates[:3]
275
  ]
276
- print("[WIZARD] Using fallback options (LLM unavailable or failed)")
277
 
278
- # Thêm option "Khác" nếu chưa
279
- if not any(opt.get("code") == "__other__" for opt in clarification_options):
280
- clarification_options.append(
281
- {
282
- "code": "__other__",
283
- "title": "Khác",
284
- "reason": "Tôi muốn hỏi văn bản hoặc chủ đề pháp luật khác.",
285
- }
286
  )
 
287
  response = {
288
  "message": intro_message,
289
  "intent": intent,
@@ -292,12 +376,12 @@ class Chatbot(CoreChatbot):
292
  "count": 0,
293
  "routing": "legal_wizard",
294
  "type": "options",
295
- "wizard_stage": "choose_document",
296
  "clarification": {
297
  "message": intro_message,
298
- "options": clarification_options,
299
  },
300
- "options": clarification_options,
301
  }
302
  if session_id:
303
  response["session_id"] = session_id
@@ -308,10 +392,140 @@ class Chatbot(CoreChatbot):
308
  content=intro_message,
309
  intent=intent,
310
  )
 
 
 
 
 
 
311
  except Exception as e:
312
- print(f"⚠️ Failed to save wizard bot message: {e}")
313
  return response
314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  # Always send legal intent through Slow Path RAG
316
  if intent == "search_legal":
317
  response = self._run_slow_path_legal(
@@ -491,14 +705,14 @@ class Chatbot(CoreChatbot):
491
  "Tôi có thể giúp bạn tra cứu các văn bản quy định pháp luật về xử lí kỷ luật cán bộ đảng viên. "
492
  "Bạn muốn tìm gì?"
493
  )
494
- response = {
495
- "message": message,
496
- "intent": intent,
497
- "confidence": confidence,
498
- "results": [],
499
- "count": 0,
500
  "routing": "small_talk",
501
- }
502
 
503
  else: # IntentRoute.SEARCH
504
  # Use core chatbot search for other intents
 
136
  # tránh trả lại các câu trả lời cũ không có options.
137
  cached_response = None
138
  if intent != "search_legal":
139
+ cached_response = EXACT_MATCH_CACHE.get(query, intent)
140
  if cached_response:
141
  cached_response["_cache"] = "exact_match"
142
  cached_response["_source"] = cached_response.get("_source", "cache")
 
162
  return cached_response
163
 
164
  # Wizard / option-first ngay tại chatbot layer:
165
+ # Multi-stage wizard flow:
166
+ # Stage 1: Choose document (if no document selected)
167
+ # Stage 2: Choose topic/section (if document selected but no topic)
168
+ # Stage 3: Choose detail (if topic selected, ask for more details)
169
+ # Final: Answer (when user says "Không" or after detail selection)
170
+
171
  has_doc_code_in_query = self._query_has_document_code(query)
172
+ wizard_stage = session_metadata.get("wizard_stage") if session_metadata else None
173
+ selected_topic = session_metadata.get("selected_topic") if session_metadata else None
174
+ wizard_depth = session_metadata.get("wizard_depth", 0) if session_metadata else 0
175
+
176
+ print(f"[WIZARD] Chatbot layer check - intent={intent}, wizard_stage={wizard_stage}, selected_doc_code={selected_doc_code}, selected_topic={selected_topic}, has_doc_code_in_query={has_doc_code_in_query}, query='{query[:50]}'")
177
+
178
+ # Stage 1: Choose document (if no document selected and no code in query)
179
+ # Use Query Rewrite Strategy from slow_path_handler instead of old LLM suggestions
180
  if intent == "search_legal" and not selected_doc_code and not has_doc_code_in_query:
181
+ print("[WIZARD] ✅ Stage 1: Using Query Rewrite Strategy from slow_path_handler")
182
+ # Delegate to slow_path_handler which has Query Rewrite Strategy
183
+ slow_handler = SlowPathHandler()
184
+ response = slow_handler.handle(
185
+ query=query,
186
+ intent=intent,
187
+ session_id=session_id,
188
+ selected_document_code=None, # No document selected yet
189
+ )
190
+
191
+ # Ensure response has wizard metadata
192
+ if response:
193
+ response.setdefault("wizard_stage", "choose_document")
194
+ response.setdefault("routing", "legal_wizard")
195
+ response.setdefault("type", "options")
196
+
197
+ # Update session metadata
198
+ if session_id:
199
+ try:
200
+ ConversationContext.update_session_metadata(
201
+ session_id,
202
+ {
203
+ "wizard_stage": "choose_document",
204
+ "wizard_depth": 1,
205
+ }
206
+ )
207
+ except Exception as e:
208
+ logger.warning("[WIZARD] Failed to update session metadata: %s", e)
209
+
210
+ # Save bot message to context
211
+ if session_id:
212
+ try:
213
+ bot_message = response.get("message") or response.get("clarification", {}).get("message", "")
214
+ ConversationContext.add_message(
215
+ session_id=session_id,
216
+ role="bot",
217
+ content=bot_message,
218
+ intent=intent,
219
+ )
220
+ except Exception as e:
221
+ print(f"⚠️ Failed to save wizard bot message: {e}")
222
+
223
+ return response if response else {
224
+ "message": "Xin lỗi, có lỗi xảy ra khi tìm kiếm văn bản.",
225
+ "intent": intent,
226
+ "results": [],
227
+ "count": 0,
228
+ }
229
+
230
+ # Stage 2: Choose topic/section (if document selected but no topic yet)
231
+ # Skip if wizard_stage is already "answer" (user wants final answer)
232
+ if intent == "search_legal" and selected_doc_code and not selected_topic and not has_doc_code_in_query and wizard_stage != "answer":
233
+ print("[WIZARD] ✅ Stage 2 triggered: Choose topic/section")
234
+
235
+ # Get document title
236
+ document_title = selected_doc_code
237
  try:
238
+ doc = LegalDocument.objects.filter(code=selected_doc_code).first()
239
+ if doc:
240
+ document_title = getattr(doc, "title", "") or selected_doc_code
241
+ except Exception:
242
+ pass
243
+
244
+ # Extract keywords from query for parallel search
245
+ search_keywords_from_query = []
246
+ if self.llm_generator:
247
+ try:
248
+ conversation_context = None
249
+ if session_id:
250
+ try:
251
+ recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
252
+ conversation_context = [
253
+ {"role": msg.role, "content": msg.content}
254
+ for msg in recent_messages
255
+ ]
256
+ except Exception:
257
+ pass
258
+
259
+ search_keywords_from_query = self.llm_generator.extract_search_keywords(
260
+ query=query,
261
+ selected_options=None, # No options selected yet
262
+ conversation_context=conversation_context,
263
  )
264
+ print(f"[WIZARD] Extracted keywords: {search_keywords_from_query[:5]}")
265
+ except Exception as exc:
266
+ logger.warning("[WIZARD] Keyword extraction failed: %s", exc)
267
+
268
+ # Fallback to simple keyword extraction
269
+ if not search_keywords_from_query:
270
+ search_keywords_from_query = self.chatbot.extract_keywords(query)
271
+
272
+ # Trigger parallel search for document (if not already done)
273
+ slow_handler = SlowPathHandler()
274
+ prefetched_results = slow_handler._get_prefetched_results(session_id, "document_results")
275
+
276
+ if not prefetched_results:
277
+ # Trigger parallel search now
278
+ slow_handler._parallel_search_prepare(
279
+ document_code=selected_doc_code,
280
+ keywords=search_keywords_from_query,
281
+ session_id=session_id,
282
  )
283
+ logger.info("[WIZARD] Triggered parallel search for document")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
+ # Get prefetched search results from parallel search (if available)
286
+ prefetched_results = slow_handler._get_prefetched_results(session_id, "document_results")
287
+ search_results = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
+ if prefetched_results:
290
+ search_results = prefetched_results.get("results", [])
291
+ logger.info("[WIZARD] Using prefetched results: %d sections", len(search_results))
292
+ else:
293
+ # Fallback: search synchronously if prefetch not ready
294
+ search_result = slow_handler._search_by_intent(
295
+ intent="search_legal",
296
+ query=query,
297
+ limit=20,
298
+ preferred_document_code=selected_doc_code.upper(),
299
+ )
300
+ search_results = search_result.get("results", [])
301
+ logger.info("[WIZARD] Fallback search: %d sections", len(search_results))
302
+
303
+ # Extract keywords for topic options
304
+ conversation_context = None
305
+ if session_id:
306
+ try:
307
+ recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
308
+ conversation_context = [
309
+ {"role": msg.role, "content": msg.content}
310
+ for msg in recent_messages
311
+ ]
312
+ except Exception:
313
+ pass
314
+
315
+ # Use LLM to generate topic options
316
+ topic_options = []
317
+ intro_message = f"Bạn muốn tìm điều khoản/chủ đề nào cụ thể trong {document_title}?"
318
+ search_keywords = []
319
 
320
  if self.llm_generator:
321
  try:
322
+ llm_payload = self.llm_generator.suggest_topic_options(
323
+ query=query,
324
+ document_code=selected_doc_code,
325
+ document_title=document_title,
326
+ search_results=search_results[:10], # Top 10 for options
327
+ conversation_context=conversation_context,
328
  max_options=3,
329
  )
330
  if llm_payload:
331
  intro_message = llm_payload.get("message") or intro_message
332
+ topic_options = llm_payload.get("options", [])
333
+ search_keywords = llm_payload.get("search_keywords", [])
334
+ print(f"[WIZARD] LLM generated {len(topic_options)} topic options")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  except Exception as exc:
336
+ logger.warning("[WIZARD] LLM topic suggestion failed: %s", exc)
337
+
338
+ # Fallback: build options from search results
339
+ if not topic_options and search_results:
340
+ for result in search_results[:3]:
341
+ data = result.get("data", {})
342
+ section_title = data.get("section_title") or data.get("title") or ""
343
+ article = data.get("article") or data.get("article_number") or ""
344
+ if section_title or article:
345
+ topic_options.append({
346
+ "title": section_title or article,
347
+ "article": article,
348
+ "reason": data.get("excerpt", "")[:100] or "",
349
+ "keywords": [],
350
+ })
351
 
352
+ # If still no options, create generic ones
353
+ if not topic_options:
354
+ topic_options = [
355
  {
356
+ "title": "Các điều khoản liên quan",
357
+ "article": "",
358
+ "reason": "Tìm kiếm các điều khoản liên quan đến câu hỏi của bạn",
359
+ "keywords": [],
360
  }
 
361
  ]
 
362
 
363
+ # Trigger parallel search for selected keywords
364
+ if search_keywords:
365
+ slow_handler._parallel_search_topic(
366
+ document_code=selected_doc_code,
367
+ topic_keywords=search_keywords,
368
+ session_id=session_id,
 
 
369
  )
370
+
371
  response = {
372
  "message": intro_message,
373
  "intent": intent,
 
376
  "count": 0,
377
  "routing": "legal_wizard",
378
  "type": "options",
379
+ "wizard_stage": "choose_topic",
380
  "clarification": {
381
  "message": intro_message,
382
+ "options": topic_options,
383
  },
384
+ "options": topic_options,
385
  }
386
  if session_id:
387
  response["session_id"] = session_id
 
392
  content=intro_message,
393
  intent=intent,
394
  )
395
+ ConversationContext.update_session_metadata(
396
+ session_id,
397
+ {
398
+ "wizard_stage": "choose_topic",
399
+ },
400
+ )
401
  except Exception as e:
402
+ print(f"⚠️ Failed to save Stage 2 bot message: {e}")
403
  return response
404
 
405
+ # Stage 3: Choose detail (if topic selected, ask if user wants more details)
406
+ # Skip if wizard_stage is already "answer" (user wants final answer)
407
+ if intent == "search_legal" and selected_doc_code and selected_topic and wizard_stage != "answer":
408
+ # Check if user is asking for more details or saying "Không"
409
+ query_lower = query.lower()
410
+ wants_more = any(kw in query_lower for kw in ["có", "cần", "muốn", "thêm", "chi tiết", "nữa"])
411
+ says_no = any(kw in query_lower for kw in ["không", "khong", "thôi", "đủ", "xong"])
412
+
413
+ if says_no or wizard_depth >= 2:
414
+ # User doesn't want more details or already asked twice - proceed to final answer
415
+ print("[WIZARD] ✅ User wants final answer, proceeding to slow_path")
416
+ # Clear wizard stage to allow normal answer flow
417
+ if session_id:
418
+ try:
419
+ ConversationContext.update_session_metadata(
420
+ session_id,
421
+ {
422
+ "wizard_stage": "answer",
423
+ },
424
+ )
425
+ except Exception:
426
+ pass
427
+ elif wants_more or wizard_depth == 0:
428
+ # User wants more details - generate detail options
429
+ print("[WIZARD] ✅ Stage 3 triggered: Choose detail")
430
+
431
+ # Get conversation context
432
+ conversation_context = None
433
+ if session_id:
434
+ try:
435
+ recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
436
+ conversation_context = [
437
+ {"role": msg.role, "content": msg.content}
438
+ for msg in recent_messages
439
+ ]
440
+ except Exception:
441
+ pass
442
+
443
+ # Use LLM to generate detail options
444
+ detail_options = []
445
+ intro_message = "Bạn muốn chi tiết gì cho chủ đề này nữa không?"
446
+ search_keywords = []
447
+
448
+ if self.llm_generator:
449
+ try:
450
+ llm_payload = self.llm_generator.suggest_detail_options(
451
+ query=query,
452
+ selected_document_code=selected_doc_code,
453
+ selected_topic=selected_topic,
454
+ conversation_context=conversation_context,
455
+ max_options=3,
456
+ )
457
+ if llm_payload:
458
+ intro_message = llm_payload.get("message") or intro_message
459
+ detail_options = llm_payload.get("options", [])
460
+ search_keywords = llm_payload.get("search_keywords", [])
461
+ print(f"[WIZARD] ✅ LLM generated {len(detail_options)} detail options")
462
+ except Exception as exc:
463
+ logger.warning("[WIZARD] LLM detail suggestion failed: %s", exc)
464
+
465
+ # Fallback options
466
+ if not detail_options:
467
+ detail_options = [
468
+ {
469
+ "title": "Thẩm quyền xử lý",
470
+ "reason": "Tìm hiểu về thẩm quyền xử lý kỷ luật",
471
+ "keywords": ["thẩm quyền", "xử lý"],
472
+ },
473
+ {
474
+ "title": "Trình tự, thủ tục",
475
+ "reason": "Tìm hiểu về trình tự, thủ tục xử lý",
476
+ "keywords": ["trình tự", "thủ tục"],
477
+ },
478
+ {
479
+ "title": "Hình thức kỷ luật",
480
+ "reason": "Tìm hiểu về các hình thức kỷ luật",
481
+ "keywords": ["hình thức", "kỷ luật"],
482
+ },
483
+ ]
484
+
485
+ # Trigger parallel search for detail keywords
486
+ if search_keywords and session_id:
487
+ slow_handler = SlowPathHandler()
488
+ slow_handler._parallel_search_topic(
489
+ document_code=selected_doc_code,
490
+ topic_keywords=search_keywords,
491
+ session_id=session_id,
492
+ )
493
+
494
+ response = {
495
+ "message": intro_message,
496
+ "intent": intent,
497
+ "confidence": confidence,
498
+ "results": [],
499
+ "count": 0,
500
+ "routing": "legal_wizard",
501
+ "type": "options",
502
+ "wizard_stage": "choose_detail",
503
+ "clarification": {
504
+ "message": intro_message,
505
+ "options": detail_options,
506
+ },
507
+ "options": detail_options,
508
+ }
509
+ if session_id:
510
+ response["session_id"] = session_id
511
+ try:
512
+ ConversationContext.add_message(
513
+ session_id=session_id,
514
+ role="bot",
515
+ content=intro_message,
516
+ intent=intent,
517
+ )
518
+ ConversationContext.update_session_metadata(
519
+ session_id,
520
+ {
521
+ "wizard_stage": "choose_detail",
522
+ "wizard_depth": wizard_depth + 1,
523
+ },
524
+ )
525
+ except Exception as e:
526
+ print(f"⚠️ Failed to save Stage 3 bot message: {e}")
527
+ return response
528
+
529
  # Always send legal intent through Slow Path RAG
530
  if intent == "search_legal":
531
  response = self._run_slow_path_legal(
 
705
  "Tôi có thể giúp bạn tra cứu các văn bản quy định pháp luật về xử lí kỷ luật cán bộ đảng viên. "
706
  "Bạn muốn tìm gì?"
707
  )
708
+ response = {
709
+ "message": message,
710
+ "intent": intent,
711
+ "confidence": confidence,
712
+ "results": [],
713
+ "count": 0,
714
  "routing": "small_talk",
715
+ }
716
 
717
  else: # IntentRoute.SEARCH
718
  # Use core chatbot search for other intents