Davidtran99 commited on
Commit
3251b14
·
1 Parent(s): 9246dca

Improve follow-up handling

Browse files
Files changed (1) hide show
  1. backend/hue_portal/chatbot/chatbot.py +142 -88
backend/hue_portal/chatbot/chatbot.py CHANGED
@@ -4,6 +4,8 @@ Chatbot wrapper that integrates core chatbot with router, LLM, and context manag
4
  import os
5
  import copy
6
  import logging
 
 
7
  from typing import Dict, Any, Optional
8
  from hue_portal.core.chatbot import Chatbot as CoreChatbot, get_chatbot as get_core_chatbot
9
  from hue_portal.chatbot.router import decide_route, IntentRoute, RouteDecision
@@ -20,6 +22,30 @@ EXACT_MATCH_CACHE = ExactMatchCache(
20
  ttl_seconds=int(os.environ.get("EXACT_MATCH_CACHE_TTL_SECONDS", "43200")),
21
  )
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  class Chatbot(CoreChatbot):
25
  """
@@ -29,6 +55,9 @@ class Chatbot(CoreChatbot):
29
  def __init__(self):
30
  super().__init__()
31
  self.llm_generator = None
 
 
 
32
  self._initialize_llm()
33
 
34
  def _initialize_llm(self):
@@ -113,104 +142,121 @@ class Chatbot(CoreChatbot):
113
  }
114
 
115
  elif route_decision.route == IntentRoute.SMALL_TALK:
116
- # Xử lý follow-up questions trong context
117
- follow_up_keywords = [" điều khoản", "liên quan", "khác", "nữa", "thêm", "tóm tắt", "tải file"]
 
 
 
 
 
 
 
 
 
 
 
 
118
  query_lower = query.lower()
119
  is_follow_up = any(kw in query_lower for kw in follow_up_keywords)
120
-
 
 
 
 
 
 
 
 
 
 
 
 
121
  response = None
122
-
123
- # Nếu là follow-up question, thử tìm context từ conversation trước
124
  if is_follow_up and session_id:
125
- try:
126
- recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
127
- # Tìm message bot cuối cùng results
128
- for msg in reversed(recent_messages):
129
- if msg.role == "bot" and msg.intent == "search_legal":
130
- # context về legal query trước đó, thử search lại với query mới
131
- enhanced_query = f"{query} {msg.content[:100]}"
132
- search_result = self.search_by_intent("search_legal", enhanced_query, limit=3)
133
- if search_result["count"] > 0:
134
- # Tìm thấy results, trả về
135
- top_result = search_result["results"][0]
136
- top_data = top_result.get("data", {})
137
- doc_code = top_data.get("document_code", "")
138
- doc_title = top_data.get("document_title", "văn bản pháp luật")
139
- section_code = top_data.get("section_code", "")
140
- section_title = top_data.get("section_title", "")
141
- content = top_data.get("content", "") or top_data.get("excerpt", "")
142
-
143
- if "tóm tắt" in query_lower:
144
- content_preview = content[:400] + "..." if len(content) > 400 else content
145
- message = (
146
- f"**Tóm tắt {section_code}**: {section_title or 'Nội dung chính'}\n\n"
147
- f"{content_preview}\n\n"
148
- f"Nguồn: {doc_title}" + (f" ({doc_code})" if doc_code else "")
149
- )
150
- elif "tải" in query_lower or "download" in query_lower:
151
- message = (
152
- f"Bạn có thể tải file gốc của {doc_title}" + (f" ({doc_code})" if doc_code else "") +
153
- f" từ link download trong kết quả tìm kiếm."
154
- )
155
- else:
156
- # Câu hỏi "có điều khoản liên quan nào khác không?"
157
- if search_result["count"] > 1:
158
- message = (
159
- f"Có, tôi tìm thấy {search_result['count']} điều khoản liên quan:\n\n"
160
- )
161
- for i, result in enumerate(search_result["results"][:3], 1):
162
- data = result.get("data", {})
163
- sec_code = data.get("section_code", "")
164
- sec_title = data.get("section_title", "")
165
- message += f"{i}. **{sec_code}**: {sec_title or 'Nội dung liên quan'}\n"
166
- message += f"\nNguồn: {doc_title}" + (f" ({doc_code})" if doc_code else "")
167
- else:
168
- message = (
169
- f"Tôi đã tìm thấy điều khoản liên quan:\n\n"
170
- f"**{section_code}**: {section_title or 'Nội dung liên quan'}\n\n"
171
- f"{content[:300]}...\n\n"
172
- f"Nguồn: {doc_title}" + (f" ({doc_code})" if doc_code else "")
173
- )
174
-
175
- response = {
176
- "message": message,
177
- "intent": "search_legal",
178
- "confidence": 0.85,
179
- "results": search_result["results"][:3],
180
- "count": search_result["count"],
181
- "routing": "follow_up"
182
- }
183
  break
184
- except Exception as e:
185
- logger.warning(f"[FOLLOW_UP] Failed to process follow-up: {e}")
186
-
187
- # Nếu không phải follow-up hoặc không tìm thấy context, trả về message thân thiện
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  if response is None:
189
- # Detect off-topic questions (nấu ăn, chả trứng, etc.)
190
- off_topic_keywords = ["nấu", "nau", "chả trứng", "cha trung", "món ăn", "mon an", "công thức", "cong thuc",
191
- "cách làm", "cach lam", "đổ chả", "do cha", "trứng", "trung"]
192
- is_off_topic = any(kw in query_lower for kw in off_topic_keywords)
193
-
194
- if is_off_topic:
195
- message = (
196
- "Xin lỗi, tôi là chatbot chuyên về tra cứu các văn bản quy định pháp luật "
197
- "về xử lí kỷ luật cán bộ đảng viên của Phòng Thanh Tra - Công An Thành Phố Huế.\n\n"
198
- "Tôi không thể trả lời các câu hỏi về nấu ăn, công thức nấu ăn hay các chủ đề khác ngoài phạm vi pháp luật.\n\n"
199
- "Bạn có muốn tra cứu thông tin về:\n"
200
- "- Các quy định về xử lí kỷ luật cán bộ đảng viên\n"
201
- "- Các điều khoản trong Thông tư 02 về xử lý điều lệnh trong CAND\n"
202
- "- Hoặc các văn bản pháp luật liên quan khác?"
203
- )
204
- else:
205
- message = "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. Bạn muốn tìm gì?"
206
-
207
  response = {
208
- "message": message,
209
  "intent": intent,
210
  "confidence": confidence,
211
  "results": [],
212
  "count": 0,
213
- "routing": "small_talk"
214
  }
215
 
216
  else: # IntentRoute.SEARCH
@@ -236,11 +282,19 @@ class Chatbot(CoreChatbot):
236
  "routing": "search"
237
  }
238
 
 
 
 
 
 
 
 
 
239
  # Add session_id
240
  if session_id:
241
  response["session_id"] = session_id
242
 
243
- # Save bot response to context
244
  if session_id:
245
  try:
246
  ConversationContext.add_message(
 
4
  import os
5
  import copy
6
  import logging
7
+ import json
8
+ import time
9
  from typing import Dict, Any, Optional
10
  from hue_portal.core.chatbot import Chatbot as CoreChatbot, get_chatbot as get_core_chatbot
11
  from hue_portal.chatbot.router import decide_route, IntentRoute, RouteDecision
 
22
  ttl_seconds=int(os.environ.get("EXACT_MATCH_CACHE_TTL_SECONDS", "43200")),
23
  )
24
 
25
+ DEBUG_LOG_PATH = "/Users/davidtran/Downloads/TryHarDemNayProject/.cursor/debug.log"
26
+ DEBUG_SESSION_ID = "debug-session"
27
+ DEBUG_RUN_ID = "pre-fix"
28
+
29
+ #region agent log
30
+ def _agent_debug_log(hypothesis_id: str, location: str, message: str, data: Dict[str, Any]) -> None:
31
+ """Append instrumentation logs to .cursor/debug.log in NDJSON format."""
32
+ try:
33
+ payload = {
34
+ "sessionId": DEBUG_SESSION_ID,
35
+ "runId": DEBUG_RUN_ID,
36
+ "hypothesisId": hypothesis_id,
37
+ "location": location,
38
+ "message": message,
39
+ "data": data,
40
+ "timestamp": int(time.time() * 1000),
41
+ }
42
+ with open(DEBUG_LOG_PATH, "a", encoding="utf-8") as log_file:
43
+ log_file.write(json.dumps(payload, ensure_ascii=False) + "\n")
44
+ except Exception:
45
+ # Silently ignore logging errors to avoid impacting runtime behavior.
46
+ pass
47
+ #endregion
48
+
49
 
50
  class Chatbot(CoreChatbot):
51
  """
 
55
  def __init__(self):
56
  super().__init__()
57
  self.llm_generator = None
58
+ # In-memory cache: nhớ câu trả lời legal gần nhất cho từng session
59
+ # để xử lý nhanh các câu hỏi follow-up như "tóm tắt", "có điều khoản liên quan không", ...
60
+ self._last_legal_answer_by_session: Dict[str, str] = {}
61
  self._initialize_llm()
62
 
63
  def _initialize_llm(self):
 
142
  }
143
 
144
  elif route_decision.route == IntentRoute.SMALL_TALK:
145
+ # Xử lý follow-up questions trong context cho các câu như:
146
+ # - " điều khoản liên quan nào khác không?"
147
+ # - "Tóm tắt nội dung chính của điều này?"
148
+ follow_up_keywords = [
149
+ "có điều khoản",
150
+ "liên quan",
151
+ "khác",
152
+ "nữa",
153
+ "thêm",
154
+ "tóm tắt",
155
+ "tải file",
156
+ "tải",
157
+ "download",
158
+ ]
159
  query_lower = query.lower()
160
  is_follow_up = any(kw in query_lower for kw in follow_up_keywords)
161
+ #region agent log
162
+ _agent_debug_log(
163
+ hypothesis_id="H1",
164
+ location="chatbot.py:120",
165
+ message="follow_up_detection",
166
+ data={
167
+ "query": query,
168
+ "is_follow_up": is_follow_up,
169
+ "session_id_present": bool(session_id),
170
+ },
171
+ )
172
+ #endregion
173
+
174
  response = None
175
+
176
+ # Nếu là follow-up question, ưu tiên dùng context legal gần nhất trong session
177
  if is_follow_up and session_id:
178
+ previous_answer = self._last_legal_answer_by_session.get(session_id, "")
179
+
180
+ # Nếu in-memory cache trống, thử fallback sang ConversationContext (DB)
181
+ if not previous_answer:
182
+ try:
183
+ recent_messages = ConversationContext.get_recent_messages(session_id, limit=5)
184
+ # Tìm message bot cuối cùng có intent search_legal
185
+ for msg in reversed(recent_messages):
186
+ if msg.role == "bot" and msg.intent == "search_legal":
187
+ previous_answer = msg.content or ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  break
189
+ except Exception as e:
190
+ logger.warning("[FOLLOW_UP] Failed to load context from DB: %s", e)
191
+
192
+ if previous_answer:
193
+ if "tóm tắt" in query_lower:
194
+ # Ưu tiên dùng LLM để tóm tắt lại câu trả lời trước đó
195
+ summary_message = None
196
+ if getattr(self, "llm_generator", None):
197
+ try:
198
+ prompt = (
199
+ "Bạn là chuyên gia pháp luật. Hãy tóm tắt ngắn gọn, rõ ràng nội dung chính của đoạn sau "
200
+ "(giữ nguyên tinh thần và các mức, tỷ lệ, hình thức kỷ luật nếu có):\n\n"
201
+ f"{previous_answer}"
202
+ )
203
+ summary_message = self.llm_generator.generate_answer(
204
+ prompt,
205
+ context=None,
206
+ documents=None,
207
+ )
208
+ except Exception as e:
209
+ logger.warning("[FOLLOW_UP] LLM summary failed: %s", e)
210
+
211
+ if summary_message:
212
+ message = summary_message
213
+ else:
214
+ # Fallback: cắt ngắn nội dung trước đó
215
+ content_preview = (
216
+ previous_answer[:400] + "..." if len(previous_answer) > 400 else previous_answer
217
+ )
218
+ message = "Tóm tắt nội dung chính của điều khoản trước đó:\n\n" f"{content_preview}"
219
+ elif "tải" in query_lower:
220
+ message = (
221
+ "Bạn có thể tải file gốc của văn bản tại mục Quản lý văn bản trên hệ thống "
222
+ "hoặc liên hệ cán bộ phụ trách để được cung cấp bản đầy đủ."
223
+ )
224
+ else:
225
+ message = (
226
+ "Trong câu trả lời trước, tôi đã trích dẫn điều khoản chính liên quan. "
227
+ "Nếu bạn cần điều khoản khác (ví dụ về thẩm quyền, trình tự, hồ sơ), "
228
+ "hãy nêu rõ nội dung muốn tìm để tôi trợ giúp nhanh nhất."
229
+ )
230
+
231
+ response = {
232
+ "message": message,
233
+ "intent": "search_legal",
234
+ "confidence": 0.85,
235
+ "results": [],
236
+ "count": 0,
237
+ "routing": "follow_up",
238
+ }
239
+
240
+ # Nếu không phải follow-up hoặc không tìm thấy context, trả về message thân thiện mặc định
241
  if response is None:
242
+ #region agent log
243
+ _agent_debug_log(
244
+ hypothesis_id="H1",
245
+ location="chatbot.py:187",
246
+ message="follow_up_fallback_small_talk",
247
+ data={
248
+ "is_follow_up": is_follow_up,
249
+ "session_id_present": bool(session_id),
250
+ },
251
+ )
252
+ #endregion
 
 
 
 
 
 
 
253
  response = {
254
+ "message": "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. Bạn muốn tìm gì?",
255
  "intent": intent,
256
  "confidence": confidence,
257
  "results": [],
258
  "count": 0,
259
+ "routing": "small_talk",
260
  }
261
 
262
  else: # IntentRoute.SEARCH
 
282
  "routing": "search"
283
  }
284
 
285
+ # Nếu là legal query, lưu lại câu trả lời gần nhất theo session để phục vụ follow-up nhanh
286
+ if session_id and intent == "search_legal":
287
+ try:
288
+ self._last_legal_answer_by_session[session_id] = response.get("message", "") or ""
289
+ except Exception:
290
+ # Không để việc cache in-memory làm hỏng flow chính
291
+ pass
292
+
293
  # Add session_id
294
  if session_id:
295
  response["session_id"] = session_id
296
 
297
+ # Save bot response to context (DB)
298
  if session_id:
299
  try:
300
  ConversationContext.add_message(