VietCat commited on
Commit
fd5dbb4
·
1 Parent(s): 96d59ac

update reranker

Browse files
Files changed (5) hide show
  1. app/facebook.py +122 -69
  2. app/gemini_client.py +1 -1
  3. app/llm.py +32 -22
  4. app/message_processor.py +1 -1
  5. app/reranker.py +36 -3
app/facebook.py CHANGED
@@ -13,8 +13,15 @@ from .config import Settings, get_settings
13
 
14
  from .utils import timing_decorator_async, timing_decorator_sync, _safe_truncate
15
 
 
16
  class FacebookClient:
17
- def __init__(self, app_secret: str, page_id: Optional[str] = None, page_token: Optional[str] = None, sender_id: Optional[str] = None):
 
 
 
 
 
 
18
  """
19
  Khởi tạo FacebookClient với app_secret.
20
  Input: app_secret (str) - Facebook App Secret.
@@ -26,7 +33,12 @@ class FacebookClient:
26
  self.page_token = page_token
27
  self.sender_id = sender_id
28
 
29
- def update_context(self, page_id: Optional[str] = None, page_token: Optional[str] = None, sender_id: Optional[str] = None):
 
 
 
 
 
30
  """
31
  Cập nhật các thông tin context (page_id, page_token, sender_id) của client.
32
  Input: page_id (str), page_token (str), sender_id (str)
@@ -40,7 +52,9 @@ class FacebookClient:
40
  self.sender_id = sender_id
41
 
42
  @timing_decorator_async
43
- async def verify_webhook(self, token: str, challenge: str, verify_token: str) -> int:
 
 
44
  """
45
  Xác thực webhook Facebook bằng verify_token và trả về challenge.
46
  Input: token (str), challenge (str), verify_token (str)
@@ -61,27 +75,26 @@ class FacebookClient:
61
  return False
62
 
63
  expected = hmac.new(
64
- self.app_secret.encode(),
65
- payload,
66
- hashlib.sha256
67
  ).hexdigest()
68
-
69
  return hmac.compare_digest(signature[7:], expected)
70
 
71
  def format_message(self, text: str) -> str:
72
  # 1. Thay bullet markdown bằng ký hiệu khác
73
- text = text.replace('\n* ', '\n- ')
74
- text = text.replace('\n * ', '\n + ')
75
- text = text.replace('\n* ', '\n- ')
76
- text = text.replace('\n * ', '\n + ')
77
  # 2. Chuyển **text** hoặc __text__ thành *text*
78
  import re
79
- text = re.sub(r'\*\*([^\*]+)\*\*', r'*\1*', text)
80
- text = re.sub(r'__([^_]+)__', r'*\1*', text)
 
81
  # 3. Loại bỏ các tiêu đề markdown kiểu #, ##, ###, ...
82
- text = re.sub(r'^#+\s+', '', text, flags=re.MULTILINE)
83
  # 4. Rút gọn nhiều dòng trống liên tiếp thành 1 dòng trống
84
- text = re.sub(r'\n{3,}', '\n\n', text)
85
  # 5. Loại bỏ các markdown không hỗ trợ khác nếu cần
86
  return text
87
 
@@ -89,7 +102,7 @@ class FacebookClient:
89
  """
90
  Chia message thành các đoạn <= max_length ký tự, ưu tiên chia theo dòng.
91
  """
92
- lines = text.split('\n')
93
  messages = []
94
  current = ""
95
  for line in lines:
@@ -97,16 +110,13 @@ class FacebookClient:
97
  if len(current) + len(line) + 1 > max_length:
98
  messages.append(current.rstrip())
99
  current = ""
100
- current += (line + '\n')
101
  if current.strip():
102
  messages.append(current.rstrip())
103
  return messages
104
 
105
  def send_message_forwarder(
106
- self,
107
- access_token: str,
108
- recipient_id: str,
109
- message: str
110
  ) -> dict:
111
  """
112
  Gửi tin nhắn đến Facebook Messenger qua API được triển khai.
@@ -126,17 +136,21 @@ class FacebookClient:
126
  payload = {
127
  "recipient_id": recipient_id,
128
  "access_token": access_token,
129
- "message": message
130
  }
131
-
132
  # Ghi lại toàn bộ payload để gỡ lỗi.
133
  # CẢNH BÁO: Việc này sẽ ghi lại cả PAGE_ACCESS_TOKEN. Chỉ nên dùng trong môi trường dev hoặc khi cần gỡ lỗi.
134
- logger.info(f"[FACEBOOK_FORWARDER] Forwarding message to {url}. Full payload: {json.dumps(payload, ensure_ascii=False)}")
 
 
135
 
136
  try:
137
  response = requests.post(url, json=payload, timeout=10)
138
  response.raise_for_status() # Sẽ raise HTTPError cho các status 4xx/5xx
139
- logger.info(f"[FACEBOOK_FORWARDER] Forwarder API returned status {response.status_code}.")
 
 
140
  return response.json()
141
  except requests.HTTPError as e:
142
  # Lỗi HTTP (4xx, 5xx), log chi tiết hơn để gỡ lỗi phía forwarder
@@ -154,40 +168,53 @@ class FacebookClient:
154
  return {"error": str(e), "details": error_content}
155
  except requests.RequestException as e:
156
  # Các lỗi request khác (timeout, connection error)
157
- logger.error(f"[FACEBOOK_FORWARDER] Request Error calling forwarder API: {e}")
 
 
158
  return {"error": str(e)}
159
 
160
- def _send_message_sync(self, page_access_token: str, recipient_id: str, message: str) -> dict:
 
 
161
  """
162
  Gửi tin nhắn sử dụng facebook-sdk với request method trực tiếp.
163
  """
164
  max_retries = 3
165
  retry_delay = 1 # giây
166
-
167
  for attempt in range(max_retries):
168
  try:
169
  graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")
170
-
171
  # Sử dụng request method trực tiếp cho Messenger API với timeout
172
  result = graph.request(
173
  path="me/messages",
174
  post_args={
175
  "recipient": {"id": recipient_id},
176
- "message": {"text": message}
177
  },
178
- timeout=30 # Thêm timeout 30 giây
179
  )
180
  return result
181
  except facebook.GraphAPIError as e:
182
- logger.error(f"Facebook GraphAPI Error (attempt {attempt + 1}/{max_retries}): {e}")
 
 
183
  if attempt == max_retries - 1: # Lần cuối
184
- raise HTTPException(status_code=500, detail=f"Failed to send message to Facebook: {e}")
 
 
 
185
  time.sleep(retry_delay)
186
  retry_delay *= 2 # Exponential backoff
187
  except Exception as e:
188
- logger.error(f"Unexpected error sending message to Facebook (attempt {attempt + 1}/{max_retries}): {e}")
 
 
189
  if attempt == max_retries - 1: # Lần cuối
190
- raise HTTPException(status_code=500, detail="Failed to send message to Facebook")
 
 
191
  time.sleep(retry_delay)
192
  retry_delay *= 2 # Exponential backoff
193
 
@@ -197,85 +224,111 @@ class FacebookClient:
197
  """
198
  max_retries = 3
199
  retry_delay = 1 # giây
200
-
201
  for attempt in range(max_retries):
202
  try:
203
  graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")
204
  result = graph.get_object(page_id)
205
  return result
206
  except facebook.GraphAPIError as e:
207
- logger.error(f"Facebook GraphAPI Error getting page info (attempt {attempt + 1}/{max_retries}): {e}")
 
 
208
  if attempt == max_retries - 1: # Lần cuối
209
- raise HTTPException(status_code=500, detail=f"Failed to get page info: {e}")
 
 
210
  time.sleep(retry_delay)
211
  retry_delay *= 2 # Exponential backoff
212
  except Exception as e:
213
- logger.error(f"Unexpected error getting page info (attempt {attempt + 1}/{max_retries}): {e}")
 
 
214
  if attempt == max_retries - 1: # Lần cuối
215
- raise HTTPException(status_code=500, detail="Failed to get page info")
 
 
216
  time.sleep(retry_delay)
217
  retry_delay *= 2 # Exponential backoff
218
 
219
  @timing_decorator_async
220
- async def send_message(self, page_access_token: Optional[str] = None, recipient_id: Optional[str] = None, message: str = "") -> dict:
 
 
 
 
 
221
  page_access_token = page_access_token or self.page_token
222
  recipient_id = recipient_id or self.sender_id
223
 
224
  if not message or not str(message).strip():
225
- logger.warning(f"[FACEBOOK_SEND] Attempted to send an empty or whitespace-only message to recipient {recipient_id}. Aborting.")
 
 
226
  return {}
227
 
228
  if not page_access_token or not recipient_id:
229
- logger.error(f"[FACEBOOK_SEND] Missing page_access_token or recipient_id. Cannot send message.")
230
- raise ValueError("FacebookClient: page_access_token and recipient_id must not be None when sending a message.")
231
-
232
- logger.info(f"[FACEBOOK_SEND] Preparing to send message to recipient {recipient_id}. Full message (truncated): '{_safe_truncate(str(message))}'")
 
 
 
 
 
 
233
 
234
  # Format message
235
- response_to_send = self.format_message(str(message).replace('**', '*'))
236
-
237
  # Chia nhỏ nếu quá dài
238
  messages = self.split_message(response_to_send)
239
  results = []
240
-
241
  for i, msg_part in enumerate(messages, 1):
242
  if len(msg_part) > 2000:
243
  msg_part = msg_part[:2000] # fallback cắt cứng
244
-
245
- logger.info(f"[FACEBOOK_SEND] Sending part {i}/{len(messages)} to recipient {recipient_id}.")
 
 
246
  try:
247
  # Wrap sync HTTP call in thread executor để giữ async
248
  loop = asyncio.get_event_loop()
249
  result = await loop.run_in_executor(
250
- None,
251
- self.send_message_forwarder,
252
- page_access_token,
253
- recipient_id,
254
- msg_part
255
  )
256
  results.append(result)
257
  except Exception as e:
258
- logger.error(f"[FACEBOOK_SEND] Failed to send part {i}/{len(messages)} to {recipient_id}. Error: {e}")
 
 
259
  results.append({"error": str(e), "part": i})
260
-
261
  return results[0] if results else {}
262
 
263
  @timing_decorator_async
264
- async def get_page_info(self, page_access_token: Optional[str] = None, page_id: Optional[str] = None) -> dict:
 
 
265
  """
266
  Lấy thông tin page sử dụng Facebook SDK (async).
267
  """
268
  page_access_token = page_access_token or self.page_token
269
  page_id = page_id or self.page_id
270
  if not page_access_token or not page_id:
271
- raise ValueError("FacebookClient: page_access_token and page_id must not be None when getting page info.")
272
-
 
 
273
  loop = asyncio.get_event_loop()
274
  result = await loop.run_in_executor(
275
- None,
276
- self._get_page_info_sync,
277
- page_access_token,
278
- page_id
279
  )
280
  return result
281
 
@@ -289,17 +342,17 @@ class FacebookClient:
289
  try:
290
  entry = body["entry"][0]
291
  messaging = entry["messaging"][0]
292
-
293
  sender_id = messaging["sender"]["id"]
294
  recipient_id = messaging["recipient"]["id"]
295
  timestamp = messaging["timestamp"]
296
-
297
  message_data = {
298
  "sender_id": sender_id,
299
  "page_id": recipient_id,
300
  "timestamp": timestamp,
301
  "text": None,
302
- "attachments": []
303
  }
304
 
305
  if "message" in messaging:
@@ -312,4 +365,4 @@ class FacebookClient:
312
  return message_data
313
  except (KeyError, IndexError) as e:
314
  logger.error(f"Error parsing Facebook message: {e}\n\n{body}")
315
- return None
 
13
 
14
  from .utils import timing_decorator_async, timing_decorator_sync, _safe_truncate
15
 
16
+
17
  class FacebookClient:
18
+ def __init__(
19
+ self,
20
+ app_secret: str,
21
+ page_id: Optional[str] = None,
22
+ page_token: Optional[str] = None,
23
+ sender_id: Optional[str] = None,
24
+ ):
25
  """
26
  Khởi tạo FacebookClient với app_secret.
27
  Input: app_secret (str) - Facebook App Secret.
 
33
  self.page_token = page_token
34
  self.sender_id = sender_id
35
 
36
+ def update_context(
37
+ self,
38
+ page_id: Optional[str] = None,
39
+ page_token: Optional[str] = None,
40
+ sender_id: Optional[str] = None,
41
+ ):
42
  """
43
  Cập nhật các thông tin context (page_id, page_token, sender_id) của client.
44
  Input: page_id (str), page_token (str), sender_id (str)
 
52
  self.sender_id = sender_id
53
 
54
  @timing_decorator_async
55
+ async def verify_webhook(
56
+ self, token: str, challenge: str, verify_token: str
57
+ ) -> int:
58
  """
59
  Xác thực webhook Facebook bằng verify_token và trả về challenge.
60
  Input: token (str), challenge (str), verify_token (str)
 
75
  return False
76
 
77
  expected = hmac.new(
78
+ self.app_secret.encode(), payload, hashlib.sha256
 
 
79
  ).hexdigest()
80
+
81
  return hmac.compare_digest(signature[7:], expected)
82
 
83
  def format_message(self, text: str) -> str:
84
  # 1. Thay bullet markdown bằng ký hiệu khác
85
+ text = text.replace("\n* ", "\n- ")
86
+ text = text.replace("\n * ", "\n + ")
87
+ text = text.replace("\n* ", "\n- ")
88
+ text = text.replace("\n * ", "\n + ")
89
  # 2. Chuyển **text** hoặc __text__ thành *text*
90
  import re
91
+
92
+ text = re.sub(r"\*\*([^\*]+)\*\*", r"*\1*", text)
93
+ text = re.sub(r"__([^_]+)__", r"*\1*", text)
94
  # 3. Loại bỏ các tiêu đề markdown kiểu #, ##, ###, ...
95
+ text = re.sub(r"^#+\s+", "", text, flags=re.MULTILINE)
96
  # 4. Rút gọn nhiều dòng trống liên tiếp thành 1 dòng trống
97
+ text = re.sub(r"\n{3,}", "\n\n", text)
98
  # 5. Loại bỏ các markdown không hỗ trợ khác nếu cần
99
  return text
100
 
 
102
  """
103
  Chia message thành các đoạn <= max_length ký tự, ưu tiên chia theo dòng.
104
  """
105
+ lines = text.split("\n")
106
  messages = []
107
  current = ""
108
  for line in lines:
 
110
  if len(current) + len(line) + 1 > max_length:
111
  messages.append(current.rstrip())
112
  current = ""
113
+ current += line + "\n"
114
  if current.strip():
115
  messages.append(current.rstrip())
116
  return messages
117
 
118
  def send_message_forwarder(
119
+ self, access_token: str, recipient_id: str, message: str
 
 
 
120
  ) -> dict:
121
  """
122
  Gửi tin nhắn đến Facebook Messenger qua API được triển khai.
 
136
  payload = {
137
  "recipient_id": recipient_id,
138
  "access_token": access_token,
139
+ "message": message,
140
  }
141
+
142
  # Ghi lại toàn bộ payload để gỡ lỗi.
143
  # CẢNH BÁO: Việc này sẽ ghi lại cả PAGE_ACCESS_TOKEN. Chỉ nên dùng trong môi trường dev hoặc khi cần gỡ lỗi.
144
+ logger.debug(
145
+ f"[FACEBOOK_FORWARDER] Forwarding message to {url}. Full payload: {json.dumps(payload, ensure_ascii=False)}"
146
+ )
147
 
148
  try:
149
  response = requests.post(url, json=payload, timeout=10)
150
  response.raise_for_status() # Sẽ raise HTTPError cho các status 4xx/5xx
151
+ logger.info(
152
+ f"[FACEBOOK_FORWARDER] Forwarder API returned status {response.status_code}."
153
+ )
154
  return response.json()
155
  except requests.HTTPError as e:
156
  # Lỗi HTTP (4xx, 5xx), log chi tiết hơn để gỡ lỗi phía forwarder
 
168
  return {"error": str(e), "details": error_content}
169
  except requests.RequestException as e:
170
  # Các lỗi request khác (timeout, connection error)
171
+ logger.error(
172
+ f"[FACEBOOK_FORWARDER] Request Error calling forwarder API: {e}"
173
+ )
174
  return {"error": str(e)}
175
 
176
+ def _send_message_sync(
177
+ self, page_access_token: str, recipient_id: str, message: str
178
+ ) -> dict:
179
  """
180
  Gửi tin nhắn sử dụng facebook-sdk với request method trực tiếp.
181
  """
182
  max_retries = 3
183
  retry_delay = 1 # giây
184
+
185
  for attempt in range(max_retries):
186
  try:
187
  graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")
188
+
189
  # Sử dụng request method trực tiếp cho Messenger API với timeout
190
  result = graph.request(
191
  path="me/messages",
192
  post_args={
193
  "recipient": {"id": recipient_id},
194
+ "message": {"text": message},
195
  },
196
+ timeout=30, # Thêm timeout 30 giây
197
  )
198
  return result
199
  except facebook.GraphAPIError as e:
200
+ logger.error(
201
+ f"Facebook GraphAPI Error (attempt {attempt + 1}/{max_retries}): {e}"
202
+ )
203
  if attempt == max_retries - 1: # Lần cuối
204
+ raise HTTPException(
205
+ status_code=500,
206
+ detail=f"Failed to send message to Facebook: {e}",
207
+ )
208
  time.sleep(retry_delay)
209
  retry_delay *= 2 # Exponential backoff
210
  except Exception as e:
211
+ logger.error(
212
+ f"Unexpected error sending message to Facebook (attempt {attempt + 1}/{max_retries}): {e}"
213
+ )
214
  if attempt == max_retries - 1: # Lần cuối
215
+ raise HTTPException(
216
+ status_code=500, detail="Failed to send message to Facebook"
217
+ )
218
  time.sleep(retry_delay)
219
  retry_delay *= 2 # Exponential backoff
220
 
 
224
  """
225
  max_retries = 3
226
  retry_delay = 1 # giây
227
+
228
  for attempt in range(max_retries):
229
  try:
230
  graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")
231
  result = graph.get_object(page_id)
232
  return result
233
  except facebook.GraphAPIError as e:
234
+ logger.error(
235
+ f"Facebook GraphAPI Error getting page info (attempt {attempt + 1}/{max_retries}): {e}"
236
+ )
237
  if attempt == max_retries - 1: # Lần cuối
238
+ raise HTTPException(
239
+ status_code=500, detail=f"Failed to get page info: {e}"
240
+ )
241
  time.sleep(retry_delay)
242
  retry_delay *= 2 # Exponential backoff
243
  except Exception as e:
244
+ logger.error(
245
+ f"Unexpected error getting page info (attempt {attempt + 1}/{max_retries}): {e}"
246
+ )
247
  if attempt == max_retries - 1: # Lần cuối
248
+ raise HTTPException(
249
+ status_code=500, detail="Failed to get page info"
250
+ )
251
  time.sleep(retry_delay)
252
  retry_delay *= 2 # Exponential backoff
253
 
254
  @timing_decorator_async
255
+ async def send_message(
256
+ self,
257
+ page_access_token: Optional[str] = None,
258
+ recipient_id: Optional[str] = None,
259
+ message: str = "",
260
+ ) -> dict:
261
  page_access_token = page_access_token or self.page_token
262
  recipient_id = recipient_id or self.sender_id
263
 
264
  if not message or not str(message).strip():
265
+ logger.warning(
266
+ f"[FACEBOOK_SEND] Attempted to send an empty or whitespace-only message to recipient {recipient_id}. Aborting."
267
+ )
268
  return {}
269
 
270
  if not page_access_token or not recipient_id:
271
+ logger.error(
272
+ f"[FACEBOOK_SEND] Missing page_access_token or recipient_id. Cannot send message."
273
+ )
274
+ raise ValueError(
275
+ "FacebookClient: page_access_token and recipient_id must not be None when sending a message."
276
+ )
277
+
278
+ logger.info(
279
+ f"[FACEBOOK_SEND] Preparing to send message to recipient {recipient_id}. Full message (truncated): '{_safe_truncate(str(message))}'"
280
+ )
281
 
282
  # Format message
283
+ response_to_send = self.format_message(str(message).replace("**", "*"))
284
+
285
  # Chia nhỏ nếu quá dài
286
  messages = self.split_message(response_to_send)
287
  results = []
288
+
289
  for i, msg_part in enumerate(messages, 1):
290
  if len(msg_part) > 2000:
291
  msg_part = msg_part[:2000] # fallback cắt cứng
292
+
293
+ logger.info(
294
+ f"[FACEBOOK_SEND] Sending part {i}/{len(messages)} to recipient {recipient_id}."
295
+ )
296
  try:
297
  # Wrap sync HTTP call in thread executor để giữ async
298
  loop = asyncio.get_event_loop()
299
  result = await loop.run_in_executor(
300
+ None,
301
+ self.send_message_forwarder,
302
+ page_access_token,
303
+ recipient_id,
304
+ msg_part,
305
  )
306
  results.append(result)
307
  except Exception as e:
308
+ logger.error(
309
+ f"[FACEBOOK_SEND] Failed to send part {i}/{len(messages)} to {recipient_id}. Error: {e}"
310
+ )
311
  results.append({"error": str(e), "part": i})
312
+
313
  return results[0] if results else {}
314
 
315
  @timing_decorator_async
316
+ async def get_page_info(
317
+ self, page_access_token: Optional[str] = None, page_id: Optional[str] = None
318
+ ) -> dict:
319
  """
320
  Lấy thông tin page sử dụng Facebook SDK (async).
321
  """
322
  page_access_token = page_access_token or self.page_token
323
  page_id = page_id or self.page_id
324
  if not page_access_token or not page_id:
325
+ raise ValueError(
326
+ "FacebookClient: page_access_token and page_id must not be None when getting page info."
327
+ )
328
+
329
  loop = asyncio.get_event_loop()
330
  result = await loop.run_in_executor(
331
+ None, self._get_page_info_sync, page_access_token, page_id
 
 
 
332
  )
333
  return result
334
 
 
342
  try:
343
  entry = body["entry"][0]
344
  messaging = entry["messaging"][0]
345
+
346
  sender_id = messaging["sender"]["id"]
347
  recipient_id = messaging["recipient"]["id"]
348
  timestamp = messaging["timestamp"]
349
+
350
  message_data = {
351
  "sender_id": sender_id,
352
  "page_id": recipient_id,
353
  "timestamp": timestamp,
354
  "text": None,
355
+ "attachments": [],
356
  }
357
 
358
  if "message" in messaging:
 
365
  return message_data
366
  except (KeyError, IndexError) as e:
367
  logger.error(f"Error parsing Facebook message: {e}\n\n{body}")
368
+ return None
app/gemini_client.py CHANGED
@@ -167,7 +167,7 @@ class GeminiClient:
167
  )
168
 
169
  try:
170
- logger.info(
171
  f"[GEMINI][TEXT_RESPONSE] {_safe_truncate(response.text)}"
172
  )
173
  return response.text
 
167
  )
168
 
169
  try:
170
+ logger.debug(
171
  f"[GEMINI][TEXT_RESPONSE] {_safe_truncate(response.text)}"
172
  )
173
  return response.text
app/llm.py CHANGED
@@ -447,16 +447,18 @@ class LLMClient:
447
  Bạn là một chuyên gia phân tích ngôn ngữ tự nhiên (NLP) chuyên xử lý các câu hỏi về luật giao thông Việt Nam. Nhiệm vụ của bạn là đọc kỹ **lịch sử trò chuyện** và **câu hỏi mới nhất** của người dùng để trích xuất thông tin vào một cấu trúc JSON duy nhất. **Luôn chỉ trả về đối tượng JSON hợp lệ**, không thêm bất kỳ giải thích nào.
448
 
449
  Định dạng JSON bắt buộc:
450
- {{
 
451
  "muc_dich": "...",
452
  "phuong_tien": "...",
453
  "tu_khoa": [],
454
  "cau_hoi": "..."
455
- }}
 
456
 
457
  Hướng dẫn chi tiết cho từng trường:
458
 
459
- **muc_dich**: Phải là một trong các giá trị sau, dựa vào **câu hỏi mới nhất**:
460
  - "hỏi về mức phạt"
461
  - "hỏi về quy tắc giao thông"
462
  - "hỏi về báo hiệu đường bộ"
@@ -464,35 +466,41 @@ class LLMClient:
464
  - "thông tin cá nhân của AI"
465
  - "khác"
466
 
467
- **phuong_tien**: Tên phương tiện được đề cập trong câu hỏi mới hoặc trong lịch sử gần nhất. Nếu không có, để chuỗi rỗng "".
468
 
469
- **tu_khoa**: **MỘT DANH SÁCH (LIST) các thuật ngữ pháp lý các khái niệm liên quan** để tìm kiếm hiệu quả nhất trong sở dữ liệu luật.
470
- - **Quy tắc 1 (Chuyển đổi & Trực tiếp)**: Chuyển đổi ngôn ngữ đời thường của người dùng (ví dụ: "vượt đèn đỏ") thành thuật ngữ pháp lý chính xác ("Không chấp hành hiệu lệnh của đèn tín hiệu giao thông"). Trích xuất các hành vi, đối tượng, địa điểm được đề cập trực tiếp.
471
- - **Quy tắc 2 (Suy luận & Mở rộng)**: **Đây quy tắc quan trọng nhất.** Dựa vào câu hỏi, hãy suy luận ra các từ khóa tìm kiếm tiềm năng khác có thể chứa câu trả lời, ngay cả khi chúng không được nhắc đến trực tiếp.
472
- - **Suy luận theo loại**: Nếu hỏi về một biển báo cụ thể (ví dụ: "biển hạn chế tốc độ tối đa"), hãy suy luận ra loại chung của ("biển báo cấm").
473
- - **Suy luận theo hiệu**: Nếu biết hiệu của một đối tượng pháp lý (ví dụ: biển báo P.127), hãy thêm từ khóa về mã hiệu đó.
474
- - **Suy luận theo khái niệm**: Nếu câu hỏi về một tình huống (ví dụ: "hiệu lực biển báo khi qua ngã tư"), hãy tạo từ khóa về khái niệm đó ("hiệu lực của biển báo tại nơi đường giao nhau").
475
- - **Quy tắc 3 (Đa dạng hóa)**: Nếu câu hỏi phức tạp, hãy kết hợp các quy tắc trên để trích xuất một bộ từ khóa đa dạng và toàn diện. Ví dụ: "vượt đèn đỏ khi đang say rượu" -> ["Không chấp hành hiệu lệnh của đèn tín hiệu giao thông", "Điều khiển xe trên đường trong máu hoặc hơi thở có nồng độ cồn"].
476
- - **Quy tắc 4 (Xử lý ngữ cảnh không hài lòng)**: Đọc kỹ lịch sử. Nếu người dùng hỏi lại hoặc thể hiện không hài lòng (ví dụ: "không phải", "ý tôi là..."), và trong lịch sử có ghi chú (từ khóa đã dùng: ...), TUYỆT ĐỐI KHÔNG SỬ DỤNG LẠI các từ khóa đó. Hãy tạo ra một bộ từ khóa **HOÀN TOÀN MỚI** dựa trên các quy tắc trên để tìm kiếm thông tin chính xác hơn.
 
 
 
 
477
 
478
- **cau_hoi**: Diễn đạt lại câu hỏi mới nhất của người dùng thành một câu hỏi hoàn chỉnh, kết hợp ngữ cảnh từ lịch sử nếu cần, sử dụng đúng thuật ngữ pháp lý.
479
 
480
  VÍ DỤ MẪU:
481
 
482
  **VÍ DỤ 1 (Xử lý ngữ cảnh):**
483
  Lịch sử trò chuyện:
484
- "Người dùng: xe máy đi vào đường cấm thì sao? (từ khóa đã dùng: đi vào khu vực cấm)
485
  Trợ lý: Mức phạt cho hành vi đi vào khu vực cấm là..."
486
 
487
  Câu hỏi mới nhất: "không phải, ý tôi là đi vào đường cao tốc cơ"
488
 
489
  Kết quả JSON mong muốn:
490
- {{
 
491
  "muc_dich": "hỏi về mức phạt",
492
  "phuong_tien": "Xe máy",
493
- "tu_khoa": ["Điều khiển xe đi vào đường cao tốc"],
494
  "cau_hoi": "Mức xử phạt cho hành vi xe máy đi vào đường cao tốc là bao nhiêu?"
495
- }}
 
496
 
497
  **VÍ DỤ 2 (Suy luận từ khóa):**
498
  Lịch sử trò chuyện:
@@ -501,18 +509,20 @@ class LLMClient:
501
  Câu hỏi mới nhất: "qua ngã 3, ngã 4 thì biển báo hạn chế tốc độ tối đa (nền trắng, viền đỏ) có hết hiệu lực không hay chỉ khi gặp biển báo 'Hết tốc độ tối đa cho phép' thì mới hết hiệu lực?"
502
 
503
  Kết quả JSON mong muốn:
504
- {{
 
505
  "muc_dich": "hỏi về quy tắc giao thông",
506
  "phuong_tien": "",
507
  "tu_khoa": [
508
- "hiệu lực của biển báo cấm",
509
- "hiệu lực của biển báo P.127",
510
  "biển báo hết tốc độ tối đa cho phép",
511
  "biển báo DP.134",
512
- "hiệu lực của biển báo tại nơi đường giao nhau"
513
  ],
514
  "cau_hoi": "Hiệu lực của biển báo hạn chế tốc độ tối đa (P.127) khi đi qua nơi đường giao nhau (ngã ba, ngã tư) như thế nào và khi nào thì hết hiệu lực?"
515
- }}
 
516
 
517
  Bây giờ, hãy phân tích lịch sử và câu hỏi sau và chỉ trả về đối tượng JSON.
518
 
 
447
  Bạn là một chuyên gia phân tích ngôn ngữ tự nhiên (NLP) chuyên xử lý các câu hỏi về luật giao thông Việt Nam. Nhiệm vụ của bạn là đọc kỹ **lịch sử trò chuyện** và **câu hỏi mới nhất** của người dùng để trích xuất thông tin vào một cấu trúc JSON duy nhất. **Luôn chỉ trả về đối tượng JSON hợp lệ**, không thêm bất kỳ giải thích nào.
448
 
449
  Định dạng JSON bắt buộc:
450
+ ```json
451
+ {
452
  "muc_dich": "...",
453
  "phuong_tien": "...",
454
  "tu_khoa": [],
455
  "cau_hoi": "..."
456
+ }
457
+ ```
458
 
459
  Hướng dẫn chi tiết cho từng trường:
460
 
461
+ - **muc_dich**: Phải là một trong các giá trị sau, dựa vào **câu hỏi mới nhất**:
462
  - "hỏi về mức phạt"
463
  - "hỏi về quy tắc giao thông"
464
  - "hỏi về báo hiệu đường bộ"
 
466
  - "thông tin cá nhân của AI"
467
  - "khác"
468
 
469
+ - **phuong_tien**: Tên phương tiện được đề cập trong câu hỏi mới hoặc trong lịch sử gần nhất. Nếu không có, để chuỗi rỗng "".
470
 
471
+ - **tu_khoa**: **MỘT DANH SÁCH (LIST) các CỤM TỪ KHÓA NGẮN GỌN** là thuật ngữ pháp lý hoặc khái niệm cốt lõi để tìm kiếm trong văn bản luật.
472
+ - **QUY TẮC 1 (Trích xuất & Chuẩn hóa)**: Xác định các hành vi vi phạm chính chuyển đổi chúng thành cụm từ khóa pháp lý ngắn gọn. **KHÔNG** dùng cả câu tả đầy đủ hành vi.
473
+ - Tốt: "vượt đèn đỏ" -> ["không chấp hành hiệu lệnh đèn tín hiệu giao thông"]
474
+ - Xấu: "vượt đèn đỏ" -> ["Điều khiển xe ô không chấp hành hiệu lệnh của đèn tín hiệu giao thông"]
475
+ - **QUY TẮC 2 (Suy luận & Mở rộng)**: Dựa vào câu hỏi, suy luận các từ khóa liên quan.
476
+ - dụ: hỏi về "biển hạn chế tốc độ tối đa" -> suy luận thêm ["biển báo cấm", "biển báo P.127"].
477
+ - dụ: hỏi về "hiệu lực biển báo khi qua ngã " -> suy luận thêm ["hiệu lực của biển báo", "nơi đường giao nhau"].
478
+ - **QUY TẮC 3 (Xử lý ngữ cảnh không hài lòng)**: Đọc kỹ lịch sử. Nếu người dùng hỏi lại hoặc thể hiện không hài lòng (ví dụ: "không phải", "ý tôi là..."), và trong lịch sử có ghi chú `(từ khóa đã dùng: ...)` thì **TUYỆT ĐỐI KHÔNG SỬ DỤNG LẠI** các từ khóa đó. Hãy tạo ra một bộ từ khóa **HOÀN TOÀN MỚI** để tìm kiếm chính xác hơn.
479
+ - **QUY TẮC 4 (CẤM)**: Danh sách `tu_khoa` **CHỈ** chứa các thuật ngữ pháp lý hoặc khái niệm. **KHÔNG** được chứa:
480
+ - Từ ngữ đời thường (ví dụ: "vượt đèn đỏ", "say rượu").
481
+ - Các câu hỏi hoặc cụm từ chứa ý định hỏi (ví dụ: "mức phạt bao nhiêu", "phạt tiền").
482
+ - Các câu diễn giải dài dòng.
483
 
484
+ - **cau_hoi**: Diễn đạt lại câu hỏi mới nhất của người dùng thành một câu hỏi hoàn chỉnh, kết hợp ngữ cảnh từ lịch sử nếu cần, sử dụng đúng thuật ngữ pháp lý.
485
 
486
  VÍ DỤ MẪU:
487
 
488
  **VÍ DỤ 1 (Xử lý ngữ cảnh):**
489
  Lịch sử trò chuyện:
490
+ "##Người dùng##: xe máy đi vào đường cấm thì sao? (từ khóa đã dùng: đi vào khu vực cấm)
491
  Trợ lý: Mức phạt cho hành vi đi vào khu vực cấm là..."
492
 
493
  Câu hỏi mới nhất: "không phải, ý tôi là đi vào đường cao tốc cơ"
494
 
495
  Kết quả JSON mong muốn:
496
+ ```json
497
+ {
498
  "muc_dich": "hỏi về mức phạt",
499
  "phuong_tien": "Xe máy",
500
+ "tu_khoa": ["đi vào đường cao tốc", "xe máy đi vào đường cao tốc"],
501
  "cau_hoi": "Mức xử phạt cho hành vi xe máy đi vào đường cao tốc là bao nhiêu?"
502
+ }
503
+ ```
504
 
505
  **VÍ DỤ 2 (Suy luận từ khóa):**
506
  Lịch sử trò chuyện:
 
509
  Câu hỏi mới nhất: "qua ngã 3, ngã 4 thì biển báo hạn chế tốc độ tối đa (nền trắng, viền đỏ) có hết hiệu lực không hay chỉ khi gặp biển báo 'Hết tốc độ tối đa cho phép' thì mới hết hiệu lực?"
510
 
511
  Kết quả JSON mong muốn:
512
+ ```json
513
+ {
514
  "muc_dich": "hỏi về quy tắc giao thông",
515
  "phuong_tien": "",
516
  "tu_khoa": [
517
+ "hiệu lực biển báo cấm",
518
+ "biển báo P.127",
519
  "biển báo hết tốc độ tối đa cho phép",
520
  "biển báo DP.134",
521
+ "nơi đường giao nhau"
522
  ],
523
  "cau_hoi": "Hiệu lực của biển báo hạn chế tốc độ tối đa (P.127) khi đi qua nơi đường giao nhau (ngã ba, ngã tư) như thế nào và khi nào thì hết hiệu lực?"
524
+ }
525
+ ```
526
 
527
  Bây giờ, hãy phân tích lịch sử và câu hỏi sau và chỉ trả về đối tượng JSON.
528
 
app/message_processor.py CHANGED
@@ -61,7 +61,7 @@ class MessageProcessor:
61
  history = await loop.run_in_executor(
62
  None, lambda: sheets_client.get_conversation_history(sender_id, page_id)
63
  )
64
- logger.info(f"[DEBUG] history: ... {history[-3:]}")
65
 
66
  for row in history:
67
  sheet_timestamps = [str(ts) for ts in row.get("timestamp", [])]
 
61
  history = await loop.run_in_executor(
62
  None, lambda: sheets_client.get_conversation_history(sender_id, page_id)
63
  )
64
+ logger.debug(f"[DEBUG] history: ... {history[-3:]}")
65
 
66
  for row in history:
67
  sheet_timestamps = [str(ts) for ts in row.get("timestamp", [])]
app/reranker.py CHANGED
@@ -162,10 +162,43 @@ class Reranker:
162
  )
163
  logger.info(f"[RERANK] Got batch scores from Gemini: {response}")
164
 
165
- # Cải thiện parsing scores bằng regex để chỉ lấy các số hợp lệ
166
  scores_text = str(response).strip()
167
- # Tìm tất cả các chuỗi số (integer hoặc float) trong văn bản trả về
168
- score_strings = re.findall(r"\b\d+(?:\.\d+)?\b", scores_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  scores = []
171
  for s in score_strings:
 
162
  )
163
  logger.info(f"[RERANK] Got batch scores from Gemini: {response}")
164
 
165
+ # --- START: Cải thiện logic trích xuất điểm ---
166
  scores_text = str(response).strip()
167
+ scores_line = ""
168
+ score_strings = []
169
+
170
+ # Ưu tiên tìm dòng có "Kết quả:" hoặc các từ khóa tương tự
171
+ match = re.search(
172
+ r"(?i)(?:Kết quả:|Scores:|Scores\s*:|Trả về:)\s*([0-9.,\s]+)$",
173
+ scores_text,
174
+ re.MULTILINE,
175
+ )
176
+ if match:
177
+ scores_line = match.group(1)
178
+ logger.debug(
179
+ f"[RERANK] Found scores line using keyword: '{scores_line}'"
180
+ )
181
+ else:
182
+ # Fallback: tìm dòng cuối cùng chỉ chứa số, dấu phẩy, và khoảng trắng
183
+ lines = scores_text.split("\n")
184
+ for line in reversed(lines):
185
+ line = line.strip()
186
+ if line and re.match(r"^[0-9.,\s]+$", line):
187
+ scores_line = line
188
+ logger.debug(
189
+ f"[RERANK] Found scores line using fallback pattern: '{scores_line}'"
190
+ )
191
+ break
192
+
193
+ if scores_line:
194
+ # Trích xuất tất cả các số từ dòng đã tìm thấy
195
+ score_strings = re.findall(r"\b\d+(?:\.\d+)?\b", scores_line)
196
+ else:
197
+ logger.warning(
198
+ "[RERANK] Could not find a dedicated score line. Falling back to parsing all numbers from response."
199
+ )
200
+ score_strings = re.findall(r"\b\d+(?:\.\d+)?\b", scores_text)
201
+ # --- END: Cải thiện logic trích xuất điểm ---
202
 
203
  scores = []
204
  for s in score_strings: