optimize timing
Browse files- app/constants.py +1 -1
- app/message_processor.py +74 -106
- app/reranker.py +3 -0
app/constants.py
CHANGED
|
@@ -192,4 +192,4 @@ FOUND_REGULATIONS_MESSAGES = [
|
|
| 192 |
]
|
| 193 |
|
| 194 |
SHEET_RANGE = 'chat!A2:N'
|
| 195 |
-
VERSION_NUMBER =
|
|
|
|
| 192 |
]
|
| 193 |
|
| 194 |
SHEET_RANGE = 'chat!A2:N'
|
| 195 |
+
VERSION_NUMBER = 123456793
|
app/message_processor.py
CHANGED
|
@@ -30,6 +30,8 @@ class MessageProcessor:
|
|
| 30 |
if field not in message_data:
|
| 31 |
logger.error(f"[ERROR] Missing field {field} in message_data: {message_data}")
|
| 32 |
return
|
|
|
|
|
|
|
| 33 |
sender_id = message_data["sender_id"]
|
| 34 |
page_id = message_data["page_id"]
|
| 35 |
message_text = message_data["text"]
|
|
@@ -41,38 +43,24 @@ class MessageProcessor:
|
|
| 41 |
logger.info(f"[DEBUG] Không có message_text và attachments, không xử lý...")
|
| 42 |
return
|
| 43 |
|
| 44 |
-
loop = asyncio.get_event_loop()
|
| 45 |
sheets_client = self.channel.get_sheets_client()
|
| 46 |
history = await loop.run_in_executor(
|
| 47 |
None, lambda: sheets_client.get_conversation_history(sender_id, page_id)
|
| 48 |
)
|
| 49 |
logger.info(f"[DEBUG] history: ... {history[-3:]}")
|
| 50 |
|
| 51 |
-
# --- SỬA LỖI LOGIC CHỐNG TRÙNG LẶP TẠI ĐÂY ---
|
| 52 |
-
# Kiểm tra xem timestamp của sự kiện webhook này đã tồn tại trong lịch sử chưa
|
| 53 |
for row in history:
|
| 54 |
-
# Chuyển đổi an toàn sang string để so sánh
|
| 55 |
sheet_timestamps = [str(ts) for ts in row.get('timestamp', [])]
|
| 56 |
if str(timestamp) in sheet_timestamps:
|
| 57 |
logger.warning(f"Webhook lặp lại cho sự kiện đã tồn tại (timestamp: {timestamp}). Bỏ qua.")
|
| 58 |
-
return
|
| 59 |
|
| 60 |
-
# --- LUỒNG XỬ LÝ GỐC CỦA BẠN ĐƯỢC GIỮ NGUYÊN ---
|
| 61 |
log_kwargs = {
|
| 62 |
-
'conversation_id': None,
|
| 63 |
-
'
|
| 64 |
-
'
|
| 65 |
-
'
|
| 66 |
-
'
|
| 67 |
-
'originalcontent': '',
|
| 68 |
-
'originalattachments': attachments,
|
| 69 |
-
'originalvehicle': '',
|
| 70 |
-
'originalaction': '',
|
| 71 |
-
'originalpurpose': '',
|
| 72 |
-
'originalquestion': '',
|
| 73 |
-
'systemresponse': '',
|
| 74 |
-
'timestamp': [timestamp],
|
| 75 |
-
'isdone': False
|
| 76 |
}
|
| 77 |
|
| 78 |
logger.info(f"[DEBUG] Message cơ bản: {log_kwargs}")
|
|
@@ -81,13 +69,11 @@ class MessageProcessor:
|
|
| 81 |
logger.error("Không thể tạo conversation mới!")
|
| 82 |
return
|
| 83 |
logger.info(f"[DEBUG] Message history sau lần ghi đầu: {conv}")
|
| 84 |
-
|
| 85 |
-
# Thêm timestamp mới nếu chưa có (logic này có thể không cần thiết nữa nhưng giữ lại để không thay đổi luồng)
|
| 86 |
conv['timestamp'] = self.flatten_timestamp(conv['timestamp'])
|
| 87 |
if timestamp not in conv['timestamp']:
|
| 88 |
conv['timestamp'].append(timestamp)
|
| 89 |
|
| 90 |
-
# Lần gọi thứ 2 để cập nhật thêm thông tin ban đầu (nếu có)
|
| 91 |
conv_after_update1 = await loop.run_in_executor(None, lambda: sheets_client.log_conversation(**conv))
|
| 92 |
if conv_after_update1:
|
| 93 |
conv = conv_after_update1
|
|
@@ -98,7 +84,8 @@ class MessageProcessor:
|
|
| 98 |
return
|
| 99 |
|
| 100 |
try:
|
| 101 |
-
|
|
|
|
| 102 |
except Exception as e:
|
| 103 |
if "expired" in str(e).lower():
|
| 104 |
logger.warning("[FACEBOOK] Token expired, invalidate and refresh")
|
|
@@ -131,127 +118,111 @@ class MessageProcessor:
|
|
| 131 |
else:
|
| 132 |
keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
|
| 133 |
cau_hoi = message_text
|
| 134 |
-
for kw in keywords:
|
| 135 |
-
cau_hoi = cau_hoi.replace(kw, "")
|
| 136 |
cau_hoi = cau_hoi.strip()
|
| 137 |
|
| 138 |
logger.info(f"[DEBUG] Phương tiện: {keywords} - Hành vi: {tu_khoa} - Mục đích: {muc_dich} - Câu hỏi: {cau_hoi}")
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
conv['originalaction'] = tu_khoa
|
| 145 |
-
conv['originalpurpose'] = muc_dich
|
| 146 |
-
conv['originalquestion'] = cau_hoi or ""
|
| 147 |
|
| 148 |
muc_dich_to_use = muc_dich or conv.get('originalpurpose')
|
| 149 |
logger.info(f"[DEBUG] Định hướng mục đích xử lý: {muc_dich_to_use}")
|
| 150 |
-
|
| 151 |
conversation_context = self.get_llm_history(history)
|
| 152 |
|
| 153 |
response = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
if not command:
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
elif muc_dich_to_use == "hỏi về quy tắc giao thông":
|
| 158 |
-
response = await self.handle_quy_tac(conv, conversation_context, page_token, sender_id)
|
| 159 |
-
elif muc_dich_to_use == "hỏi về báo hiệu đường bộ":
|
| 160 |
-
response = await self.handle_bao_hieu(conv, conversation_context, page_token, sender_id)
|
| 161 |
-
elif muc_dich_to_use == "hỏi về quy trình xử lý vi phạm giao thông":
|
| 162 |
-
response = await self.handle_quy_trinh(conv, conversation_context, page_token, sender_id)
|
| 163 |
-
elif muc_dich_to_use == "thông tin cá nhân của AI":
|
| 164 |
-
response = await self.handle_ca_nhan(conv, conversation_context, page_token, sender_id)
|
| 165 |
-
else:
|
| 166 |
-
response = await self.handle_khac(conv, conversation_context, page_token, sender_id)
|
| 167 |
else:
|
| 168 |
if command == "xong":
|
| 169 |
post_url = await self.create_facebook_post(page_token, conv['recipient_id'], [conv])
|
| 170 |
-
if post_url
|
| 171 |
-
response = f"Bài viết đã được tạo thành công! Bạn có thể xem tại: {post_url}"
|
| 172 |
-
else:
|
| 173 |
-
response = "Đã xảy ra lỗi khi tạo bài viết. Vui lòng thử lại sau."
|
| 174 |
conv['isdone'] = True
|
| 175 |
else:
|
| 176 |
response = "Vui lòng cung cấp thêm thông tin và gõ lệnh \\xong khi hoàn tất."
|
| 177 |
conv['isdone'] = False
|
| 178 |
|
| 179 |
-
|
|
|
|
| 180 |
|
| 181 |
conv['systemresponse'] = response
|
| 182 |
|
| 183 |
logger.info(f"Chuẩn bị ghi/cập nhật dữ liệu cuối cùng vào sheet: {conv}")
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
| 185 |
return
|
| 186 |
|
|
|
|
| 187 |
def get_latest_timestamp(self,ts_value):
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
|
| 199 |
def get_llm_history(self, history: List):
|
| 200 |
sorted_history = sorted(history, key=lambda row: self.get_latest_timestamp(row.get('timestamp', 0)))
|
| 201 |
-
|
| 202 |
total_chars = 0
|
| 203 |
MAX_CONTEXT_CHARS = 20_000
|
| 204 |
conversation_context = []
|
| 205 |
for row in reversed(sorted_history):
|
| 206 |
temp_blocks = []
|
| 207 |
-
|
| 208 |
-
# --- SỬA LỖI THỨ TỰ LỊCH SỬ TẠI ĐÂY ---
|
| 209 |
-
# Đảm bảo lượt nói của user luôn được thêm vào trước.
|
| 210 |
if row.get('originaltext'):
|
| 211 |
temp_blocks.append({"role": "user", "content": row['originaltext']})
|
| 212 |
-
# Lượt nói của trợ lý ảo được thêm vào sau.
|
| 213 |
if row.get('systemresponse'):
|
| 214 |
temp_blocks.append({"role": "assistant", "content": row['systemresponse']})
|
| 215 |
-
|
| 216 |
temp_total = sum(len(block['content']) for block in temp_blocks)
|
| 217 |
if total_chars + temp_total > MAX_CONTEXT_CHARS: continue
|
| 218 |
-
|
| 219 |
-
# Thêm cặp hỏi-đáp vào đầu danh sách context, duy trì thứ tự thời gian
|
| 220 |
conversation_context = temp_blocks + conversation_context
|
| 221 |
total_chars += temp_total
|
| 222 |
return conversation_context
|
| 223 |
|
| 224 |
def flatten_timestamp(self, ts):
|
| 225 |
flat = []
|
| 226 |
-
if not isinstance(ts, list):
|
| 227 |
-
ts = [ts]
|
| 228 |
for t in ts:
|
| 229 |
-
if isinstance(t, list):
|
| 230 |
-
|
| 231 |
-
else:
|
| 232 |
-
flat.append(t)
|
| 233 |
return flat
|
| 234 |
|
| 235 |
def normalize_vehicle_keyword(self, keyword: str) -> str:
|
| 236 |
from app.constants import VEHICLE_KEYWORDS
|
| 237 |
import difflib
|
| 238 |
-
if not keyword:
|
| 239 |
-
return ""
|
| 240 |
matches = difflib.get_close_matches(keyword.lower(), [k.lower() for k in VEHICLE_KEYWORDS], n=1, cutoff=0.6)
|
| 241 |
if matches:
|
| 242 |
for k in VEHICLE_KEYWORDS:
|
| 243 |
-
if k.lower() == matches[0]:
|
| 244 |
-
return k
|
| 245 |
return keyword
|
| 246 |
-
|
| 247 |
async def format_search_results(self, conversation_context: str, question: str, matches: List[Dict[str, Any]], page_token: str, sender_id: str) -> str:
|
| 248 |
if not matches:
|
| 249 |
return "Không tìm thấy kết quả phù hợp."
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
| 251 |
try:
|
| 252 |
reranked = await self.channel.reranker.rerank(question, matches, top_k=10)
|
| 253 |
-
if reranked:
|
| 254 |
-
matches = reranked
|
| 255 |
except Exception as e:
|
| 256 |
logger.error(f"[RERANK] Lỗi khi rerank: {e}")
|
| 257 |
|
|
@@ -265,16 +236,11 @@ class MessageProcessor:
|
|
| 265 |
fullContent = (match.get('fullcontent') or '').strip()
|
| 266 |
full_result_text += f"{fullContent}"
|
| 267 |
hpbsnoidung = arr_to_str(match.get('hpbsnoidung'), sep="; ")
|
| 268 |
-
if hpbsnoidung:
|
| 269 |
-
full_result_text += f"\n- Hình phạt bổ sung: {hpbsnoidung}"
|
| 270 |
bpkpnoidung = arr_to_str(match.get('bpkpnoidung'), sep="; ")
|
| 271 |
-
if bpkpnoidung:
|
| 272 |
-
|
| 273 |
-
impounding = match.get('impounding')
|
| 274 |
-
if impounding:
|
| 275 |
-
full_result_text += f"\n- Tạm giữ phương tiện: 07 ngày"
|
| 276 |
|
| 277 |
-
# --- PROMPT MỚI ĐƯỢC CẬP NHẬT TẠI ĐÂY ---
|
| 278 |
prompt = (
|
| 279 |
"Bạn là một trợ lý pháp lý AI chuyên nghiệp. Nhiệm vụ của bạn là tổng hợp thông tin từ hai nguồn: **Lịch sử trò chuyện** và **Các đoạn luật liên quan** để đưa ra một câu trả lời duy nhất, liền mạch và tự nhiên cho người dùng.\n\n"
|
| 280 |
"**QUY TẮC BẮT BUỘC:**\n"
|
|
@@ -288,7 +254,9 @@ class MessageProcessor:
|
|
| 288 |
"### Trả lời:"
|
| 289 |
)
|
| 290 |
|
| 291 |
-
|
|
|
|
|
|
|
| 292 |
try:
|
| 293 |
answer = await self.channel.llm.generate_text(prompt)
|
| 294 |
if answer and answer.strip():
|
|
@@ -299,13 +267,10 @@ class MessageProcessor:
|
|
| 299 |
except Exception as e:
|
| 300 |
logger.error(f"LLM không sẵn sàng: {e}\n{traceback.format_exc()}")
|
| 301 |
|
| 302 |
-
# Fallback
|
| 303 |
-
return "Dựa trên thông tin bạn cung cấp, tôi đã tìm thấy một số quy định liên quan. Tuy nhiên, tôi đang gặp chút khó khăn trong việc tóm tắt. Bạn vui lòng tham khảo nội dung chi tiết trong các văn bản luật nhé."
|
| 304 |
|
| 305 |
async def create_facebook_post(self, page_token: str, sender_id: str, history: List[Dict[str, Any]]) -> str:
|
| 306 |
logger.info(f"[MOCK] Creating Facebook post for sender_id={sender_id} with history={history}")
|
| 307 |
-
# In a real scenario, you would use the Facebook Graph API to create a post.
|
| 308 |
-
# This is a mock implementation.
|
| 309 |
return "https://facebook.com/mock_post_url"
|
| 310 |
|
| 311 |
async def handle_muc_phat(self, conv, conversation_context, page_token, sender_id):
|
|
@@ -322,12 +287,19 @@ class MessageProcessor:
|
|
| 322 |
embedding = await self.channel.embedder.create_embedding(search_query)
|
| 323 |
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 324 |
|
|
|
|
| 325 |
match_count = get_settings().match_count
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
)
|
|
|
|
| 331 |
logger.info(f"[DEBUG] matches: {matches[:2]}...{matches[-2:]}")
|
| 332 |
if matches:
|
| 333 |
response = await self.format_search_results(conversation_context, question or action, matches, page_token, sender_id)
|
|
@@ -341,7 +313,6 @@ class MessageProcessor:
|
|
| 341 |
return response
|
| 342 |
|
| 343 |
async def _handle_general_question(self, conversation_context: str, message_text: str, topic: str) -> str:
|
| 344 |
-
"""Hàm chung để xử lý các câu hỏi kiến thức chung."""
|
| 345 |
prompt = (
|
| 346 |
"Bạn là một trợ lý AI am hiểu về luật giao thông Việt Nam. "
|
| 347 |
"Dựa vào lịch sử trò chuyện và kiến thức của bạn, hãy trả lời câu hỏi của người dùng một cách rõ ràng, ngắn gọn và chính xác.\n"
|
|
@@ -365,17 +336,14 @@ class MessageProcessor:
|
|
| 365 |
|
| 366 |
async def handle_quy_tac(self, conv, conversation_context, page_token, sender_id):
|
| 367 |
conv['isdone'] = True
|
| 368 |
-
# return await self._handle_general_question(conversation_context, conv['originaltext'], "quy tắc giao thông")
|
| 369 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 370 |
|
| 371 |
async def handle_bao_hieu(self, conv, conversation_context, page_token, sender_id):
|
| 372 |
conv['isdone'] = True
|
| 373 |
-
# return await self._handle_general_question(conversation_context, conv['originaltext'], "báo hiệu đường bộ")
|
| 374 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 375 |
|
| 376 |
async def handle_quy_trinh(self, conv, conversation_context, page_token, sender_id):
|
| 377 |
conv['isdone'] = True
|
| 378 |
-
# return await self._handle_general_question(conversation_context, conv['originaltext'], "quy trình xử lý vi phạm giao thông")
|
| 379 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 380 |
|
| 381 |
async def handle_ca_nhan(self, conv, conversation_context, page_token, sender_id):
|
|
@@ -394,4 +362,4 @@ class MessageProcessor:
|
|
| 394 |
return answer.strip() if answer and answer.strip() else "Chào bạn, mình là WeThoong AI đây!"
|
| 395 |
except Exception as e:
|
| 396 |
logger.error(f"Lỗi khi xử lý câu hỏi cá nhân: {e}")
|
| 397 |
-
return "Chào bạn, mình là WeThoong AI, trợ lý giao thông thông minh của bạn!"
|
|
|
|
| 30 |
if field not in message_data:
|
| 31 |
logger.error(f"[ERROR] Missing field {field} in message_data: {message_data}")
|
| 32 |
return
|
| 33 |
+
|
| 34 |
+
loop = asyncio.get_event_loop()
|
| 35 |
sender_id = message_data["sender_id"]
|
| 36 |
page_id = message_data["page_id"]
|
| 37 |
message_text = message_data["text"]
|
|
|
|
| 43 |
logger.info(f"[DEBUG] Không có message_text và attachments, không xử lý...")
|
| 44 |
return
|
| 45 |
|
|
|
|
| 46 |
sheets_client = self.channel.get_sheets_client()
|
| 47 |
history = await loop.run_in_executor(
|
| 48 |
None, lambda: sheets_client.get_conversation_history(sender_id, page_id)
|
| 49 |
)
|
| 50 |
logger.info(f"[DEBUG] history: ... {history[-3:]}")
|
| 51 |
|
|
|
|
|
|
|
| 52 |
for row in history:
|
|
|
|
| 53 |
sheet_timestamps = [str(ts) for ts in row.get('timestamp', [])]
|
| 54 |
if str(timestamp) in sheet_timestamps:
|
| 55 |
logger.warning(f"Webhook lặp lại cho sự kiện đã tồn tại (timestamp: {timestamp}). Bỏ qua.")
|
| 56 |
+
return
|
| 57 |
|
|
|
|
| 58 |
log_kwargs = {
|
| 59 |
+
'conversation_id': None, 'recipient_id': sender_id, 'page_id': page_id,
|
| 60 |
+
'originaltext': message_text, 'originalcommand': '', 'originalcontent': '',
|
| 61 |
+
'originalattachments': attachments, 'originalvehicle': '', 'originalaction': '',
|
| 62 |
+
'originalpurpose': '', 'originalquestion': '', 'systemresponse': '',
|
| 63 |
+
'timestamp': [timestamp], 'isdone': False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
logger.info(f"[DEBUG] Message cơ bản: {log_kwargs}")
|
|
|
|
| 69 |
logger.error("Không thể tạo conversation mới!")
|
| 70 |
return
|
| 71 |
logger.info(f"[DEBUG] Message history sau lần ghi đầu: {conv}")
|
| 72 |
+
|
|
|
|
| 73 |
conv['timestamp'] = self.flatten_timestamp(conv['timestamp'])
|
| 74 |
if timestamp not in conv['timestamp']:
|
| 75 |
conv['timestamp'].append(timestamp)
|
| 76 |
|
|
|
|
| 77 |
conv_after_update1 = await loop.run_in_executor(None, lambda: sheets_client.log_conversation(**conv))
|
| 78 |
if conv_after_update1:
|
| 79 |
conv = conv_after_update1
|
|
|
|
| 84 |
return
|
| 85 |
|
| 86 |
try:
|
| 87 |
+
# TỐI ƯU 1: Gửi tin nhắn trạng thái và không cần chờ đợi
|
| 88 |
+
asyncio.create_task(self.facebook.send_message(message=get_random_message(PROCESSING_STATUS_MESSAGES)))
|
| 89 |
except Exception as e:
|
| 90 |
if "expired" in str(e).lower():
|
| 91 |
logger.warning("[FACEBOOK] Token expired, invalidate and refresh")
|
|
|
|
| 118 |
else:
|
| 119 |
keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
|
| 120 |
cau_hoi = message_text
|
| 121 |
+
for kw in keywords: cau_hoi = cau_hoi.replace(kw, "")
|
|
|
|
| 122 |
cau_hoi = cau_hoi.strip()
|
| 123 |
|
| 124 |
logger.info(f"[DEBUG] Phương tiện: {keywords} - Hành vi: {tu_khoa} - Mục đích: {muc_dich} - Câu hỏi: {cau_hoi}")
|
| 125 |
|
| 126 |
+
conv.update({
|
| 127 |
+
'originalcommand': command, 'originalcontent': remaining_text, 'originalvehicle': ','.join(keywords),
|
| 128 |
+
'originalaction': tu_khoa, 'originalpurpose': muc_dich, 'originalquestion': cau_hoi or ""
|
| 129 |
+
})
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
muc_dich_to_use = muc_dich or conv.get('originalpurpose')
|
| 132 |
logger.info(f"[DEBUG] Định hướng mục đích xử lý: {muc_dich_to_use}")
|
|
|
|
| 133 |
conversation_context = self.get_llm_history(history)
|
| 134 |
|
| 135 |
response = None
|
| 136 |
+
handlers = {
|
| 137 |
+
"hỏi về mức phạt": self.handle_muc_phat,
|
| 138 |
+
"hỏi về quy tắc giao thông": self.handle_quy_tac,
|
| 139 |
+
"hỏi về báo hiệu đường bộ": self.handle_bao_hieu,
|
| 140 |
+
"hỏi về quy trình xử lý vi phạm giao thông": self.handle_quy_trinh,
|
| 141 |
+
"thông tin cá nhân của AI": self.handle_ca_nhan
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
if not command:
|
| 145 |
+
handler = handlers.get(muc_dich_to_use, self.handle_khac)
|
| 146 |
+
response = await handler(conv, conversation_context, page_token, sender_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
else:
|
| 148 |
if command == "xong":
|
| 149 |
post_url = await self.create_facebook_post(page_token, conv['recipient_id'], [conv])
|
| 150 |
+
response = f"Bài viết đã được tạo thành công! Bạn có thể xem tại: {post_url}" if post_url else "Đã xảy ra lỗi khi tạo bài viết."
|
|
|
|
|
|
|
|
|
|
| 151 |
conv['isdone'] = True
|
| 152 |
else:
|
| 153 |
response = "Vui lòng cung cấp thêm thông tin và gõ lệnh \\xong khi hoàn tất."
|
| 154 |
conv['isdone'] = False
|
| 155 |
|
| 156 |
+
# TỐI ƯU 2: Gửi câu trả lời cuối cùng và không cần chờ
|
| 157 |
+
asyncio.create_task(self.facebook.send_message(message=response))
|
| 158 |
|
| 159 |
conv['systemresponse'] = response
|
| 160 |
|
| 161 |
logger.info(f"Chuẩn bị ghi/cập nhật dữ liệu cuối cùng vào sheet: {conv}")
|
| 162 |
+
# TỐI ƯU 3: Ghi log cuối cùng và không cần chờ, cho phép webhook trả về ngay
|
| 163 |
+
asyncio.create_task(
|
| 164 |
+
loop.run_in_executor(None, lambda: sheets_client.log_conversation(**conv))
|
| 165 |
+
)
|
| 166 |
return
|
| 167 |
|
| 168 |
+
# ... (các hàm get_latest_timestamp, get_llm_history, flatten_timestamp, normalize_vehicle_keyword không đổi) ...
|
| 169 |
def get_latest_timestamp(self,ts_value):
|
| 170 |
+
if isinstance(ts_value, (int, float)): return int(ts_value)
|
| 171 |
+
if isinstance(ts_value, str):
|
| 172 |
+
try: return int(json.loads(ts_value))
|
| 173 |
+
except:
|
| 174 |
+
try: return int(ts_value)
|
| 175 |
+
except: return 0
|
| 176 |
+
if isinstance(ts_value, list):
|
| 177 |
+
if not ts_value: return 0
|
| 178 |
+
return max([self.get_latest_timestamp(item) for item in ts_value]) if ts_value else 0
|
| 179 |
+
return 0
|
| 180 |
|
| 181 |
def get_llm_history(self, history: List):
|
| 182 |
sorted_history = sorted(history, key=lambda row: self.get_latest_timestamp(row.get('timestamp', 0)))
|
|
|
|
| 183 |
total_chars = 0
|
| 184 |
MAX_CONTEXT_CHARS = 20_000
|
| 185 |
conversation_context = []
|
| 186 |
for row in reversed(sorted_history):
|
| 187 |
temp_blocks = []
|
|
|
|
|
|
|
|
|
|
| 188 |
if row.get('originaltext'):
|
| 189 |
temp_blocks.append({"role": "user", "content": row['originaltext']})
|
|
|
|
| 190 |
if row.get('systemresponse'):
|
| 191 |
temp_blocks.append({"role": "assistant", "content": row['systemresponse']})
|
|
|
|
| 192 |
temp_total = sum(len(block['content']) for block in temp_blocks)
|
| 193 |
if total_chars + temp_total > MAX_CONTEXT_CHARS: continue
|
|
|
|
|
|
|
| 194 |
conversation_context = temp_blocks + conversation_context
|
| 195 |
total_chars += temp_total
|
| 196 |
return conversation_context
|
| 197 |
|
| 198 |
def flatten_timestamp(self, ts):
|
| 199 |
flat = []
|
| 200 |
+
if not isinstance(ts, list): ts = [ts]
|
|
|
|
| 201 |
for t in ts:
|
| 202 |
+
if isinstance(t, list): flat.extend(self.flatten_timestamp(t))
|
| 203 |
+
else: flat.append(t)
|
|
|
|
|
|
|
| 204 |
return flat
|
| 205 |
|
| 206 |
def normalize_vehicle_keyword(self, keyword: str) -> str:
|
| 207 |
from app.constants import VEHICLE_KEYWORDS
|
| 208 |
import difflib
|
| 209 |
+
if not keyword: return ""
|
|
|
|
| 210 |
matches = difflib.get_close_matches(keyword.lower(), [k.lower() for k in VEHICLE_KEYWORDS], n=1, cutoff=0.6)
|
| 211 |
if matches:
|
| 212 |
for k in VEHICLE_KEYWORDS:
|
| 213 |
+
if k.lower() == matches[0]: return k
|
|
|
|
| 214 |
return keyword
|
| 215 |
+
|
| 216 |
async def format_search_results(self, conversation_context: str, question: str, matches: List[Dict[str, Any]], page_token: str, sender_id: str) -> str:
|
| 217 |
if not matches:
|
| 218 |
return "Không tìm thấy kết quả phù hợp."
|
| 219 |
+
|
| 220 |
+
# TỐI ƯU: Gửi tin nhắn trạng thái và không chờ
|
| 221 |
+
asyncio.create_task(self.facebook.send_message(message=get_random_message(FOUND_REGULATIONS_MESSAGES)))
|
| 222 |
+
|
| 223 |
try:
|
| 224 |
reranked = await self.channel.reranker.rerank(question, matches, top_k=10)
|
| 225 |
+
if reranked: matches = reranked
|
|
|
|
| 226 |
except Exception as e:
|
| 227 |
logger.error(f"[RERANK] Lỗi khi rerank: {e}")
|
| 228 |
|
|
|
|
| 236 |
fullContent = (match.get('fullcontent') or '').strip()
|
| 237 |
full_result_text += f"{fullContent}"
|
| 238 |
hpbsnoidung = arr_to_str(match.get('hpbsnoidung'), sep="; ")
|
| 239 |
+
if hpbsnoidung: full_result_text += f"\n- Hình phạt bổ sung: {hpbsnoidung}"
|
|
|
|
| 240 |
bpkpnoidung = arr_to_str(match.get('bpkpnoidung'), sep="; ")
|
| 241 |
+
if bpkpnoidung: full_result_text += f"\n- Biện pháp khắc phục: {bpkpnoidung}"
|
| 242 |
+
if match.get('impounding'): full_result_text += f"\n- Tạm giữ phương tiện: 07 ngày"
|
|
|
|
|
|
|
|
|
|
| 243 |
|
|
|
|
| 244 |
prompt = (
|
| 245 |
"Bạn là một trợ lý pháp lý AI chuyên nghiệp. Nhiệm vụ của bạn là tổng hợp thông tin từ hai nguồn: **Lịch sử trò chuyện** và **Các đoạn luật liên quan** để đưa ra một câu trả lời duy nhất, liền mạch và tự nhiên cho người dùng.\n\n"
|
| 246 |
"**QUY TẮC BẮT BUỘC:**\n"
|
|
|
|
| 254 |
"### Trả lời:"
|
| 255 |
)
|
| 256 |
|
| 257 |
+
# TỐI ƯU: Gửi tin nhắn trạng thái và không chờ
|
| 258 |
+
asyncio.create_task(self.facebook.send_message(message=f"{get_random_message(SUMMARY_STATUS_MESSAGES)}"))
|
| 259 |
+
|
| 260 |
try:
|
| 261 |
answer = await self.channel.llm.generate_text(prompt)
|
| 262 |
if answer and answer.strip():
|
|
|
|
| 267 |
except Exception as e:
|
| 268 |
logger.error(f"LLM không sẵn sàng: {e}\n{traceback.format_exc()}")
|
| 269 |
|
| 270 |
+
return "Dựa trên thông tin bạn cung cấp..." # Fallback
|
|
|
|
| 271 |
|
| 272 |
async def create_facebook_post(self, page_token: str, sender_id: str, history: List[Dict[str, Any]]) -> str:
|
| 273 |
logger.info(f"[MOCK] Creating Facebook post for sender_id={sender_id} with history={history}")
|
|
|
|
|
|
|
| 274 |
return "https://facebook.com/mock_post_url"
|
| 275 |
|
| 276 |
async def handle_muc_phat(self, conv, conversation_context, page_token, sender_id):
|
|
|
|
| 287 |
embedding = await self.channel.embedder.create_embedding(search_query)
|
| 288 |
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 289 |
|
| 290 |
+
loop = asyncio.get_event_loop()
|
| 291 |
match_count = get_settings().match_count
|
| 292 |
+
|
| 293 |
+
# TỐI ƯU 4: Chạy tác vụ I/O đồng bộ trong executor để không chặn event loop
|
| 294 |
+
matches = await loop.run_in_executor(
|
| 295 |
+
None,
|
| 296 |
+
lambda: self.channel.supabase.match_documents(
|
| 297 |
+
embedding,
|
| 298 |
+
match_count=match_count,
|
| 299 |
+
user_question=search_query
|
| 300 |
+
)
|
| 301 |
)
|
| 302 |
+
|
| 303 |
logger.info(f"[DEBUG] matches: {matches[:2]}...{matches[-2:]}")
|
| 304 |
if matches:
|
| 305 |
response = await self.format_search_results(conversation_context, question or action, matches, page_token, sender_id)
|
|
|
|
| 313 |
return response
|
| 314 |
|
| 315 |
async def _handle_general_question(self, conversation_context: str, message_text: str, topic: str) -> str:
|
|
|
|
| 316 |
prompt = (
|
| 317 |
"Bạn là một trợ lý AI am hiểu về luật giao thông Việt Nam. "
|
| 318 |
"Dựa vào lịch sử trò chuyện và kiến thức của bạn, hãy trả lời câu hỏi của người dùng một cách rõ ràng, ngắn gọn và chính xác.\n"
|
|
|
|
| 336 |
|
| 337 |
async def handle_quy_tac(self, conv, conversation_context, page_token, sender_id):
|
| 338 |
conv['isdone'] = True
|
|
|
|
| 339 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 340 |
|
| 341 |
async def handle_bao_hieu(self, conv, conversation_context, page_token, sender_id):
|
| 342 |
conv['isdone'] = True
|
|
|
|
| 343 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 344 |
|
| 345 |
async def handle_quy_trinh(self, conv, conversation_context, page_token, sender_id):
|
| 346 |
conv['isdone'] = True
|
|
|
|
| 347 |
return await self.handle_muc_phat(conv, conversation_context, page_token, sender_id)
|
| 348 |
|
| 349 |
async def handle_ca_nhan(self, conv, conversation_context, page_token, sender_id):
|
|
|
|
| 362 |
return answer.strip() if answer and answer.strip() else "Chào bạn, mình là WeThoong AI đây!"
|
| 363 |
except Exception as e:
|
| 364 |
logger.error(f"Lỗi khi xử lý câu hỏi cá nhân: {e}")
|
| 365 |
+
return "Chào bạn, mình là WeThoong AI, trợ lý giao thông thông minh của bạn!"
|
app/reranker.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
from typing import List, Dict
|
|
|
|
|
|
|
| 2 |
from .config import get_settings
|
| 3 |
from .gemini_client import GeminiClient
|
| 4 |
from loguru import logger
|
|
@@ -203,6 +205,7 @@ class Reranker:
|
|
| 203 |
doc['rerank_score'] = 0
|
| 204 |
return doc
|
| 205 |
|
|
|
|
| 206 |
async def rerank(self, query: str, docs: List[Dict], top_k: int = 5) -> List[Dict]:
|
| 207 |
"""
|
| 208 |
Rerank docs theo độ liên quan với query, trả về top_k docs.
|
|
|
|
| 1 |
from typing import List, Dict
|
| 2 |
+
|
| 3 |
+
from app.utils import timing_decorator_async
|
| 4 |
from .config import get_settings
|
| 5 |
from .gemini_client import GeminiClient
|
| 6 |
from loguru import logger
|
|
|
|
| 205 |
doc['rerank_score'] = 0
|
| 206 |
return doc
|
| 207 |
|
| 208 |
+
@timing_decorator_async
|
| 209 |
async def rerank(self, query: str, docs: List[Dict], top_k: int = 5) -> List[Dict]:
|
| 210 |
"""
|
| 211 |
Rerank docs theo độ liên quan với query, trả về top_k docs.
|