refactor message flow on purposes
Browse files- app/facebook.py +56 -16
- app/message_processor.py +54 -57
app/facebook.py
CHANGED
|
@@ -63,27 +63,67 @@ class FacebookClient:
|
|
| 63 |
|
| 64 |
return hmac.compare_digest(signature[7:], expected)
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
@timing_decorator_async
|
| 67 |
-
async def send_message(self, page_access_token: Optional[str] = None, recipient_id: Optional[str] = None, message: str = "") ->
|
| 68 |
page_access_token = page_access_token or self.page_token
|
| 69 |
recipient_id = recipient_id or self.sender_id
|
| 70 |
if not page_access_token or not recipient_id:
|
| 71 |
raise ValueError("FacebookClient: page_access_token and recipient_id must not be None when sending a message.")
|
| 72 |
-
#
|
| 73 |
-
response_to_send = message.replace('**', '*') if isinstance(message, str) else message
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
@timing_decorator_sync
|
| 89 |
def parse_message(self, body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
|
| 63 |
|
| 64 |
return hmac.compare_digest(signature[7:], expected)
|
| 65 |
|
| 66 |
+
def format_message(self, text: str) -> str:
|
| 67 |
+
# 1. Thay bullet markdown bằng ký hiệu khác
|
| 68 |
+
text = text.replace('\n* ', '\n- ')
|
| 69 |
+
text = text.replace('\n * ', '\n + ')
|
| 70 |
+
text = text.replace('\n* ', '\n- ')
|
| 71 |
+
text = text.replace('\n * ', '\n + ')
|
| 72 |
+
# 2. Chuyển **text** hoặc __text__ thành *text*
|
| 73 |
+
import re
|
| 74 |
+
text = re.sub(r'\*\*([^\*]+)\*\*', r'*\1*', text)
|
| 75 |
+
text = re.sub(r'__([^_]+)__', r'*\1*', text)
|
| 76 |
+
# 3. Loại bỏ các tiêu đề markdown kiểu #, ##, ###, ...
|
| 77 |
+
text = re.sub(r'^#+\s+', '', text, flags=re.MULTILINE)
|
| 78 |
+
# 4. Rút gọn nhiều dòng trống liên tiếp thành 1 dòng trống
|
| 79 |
+
text = re.sub(r'\n{3,}', '\n\n', text)
|
| 80 |
+
# 5. Loại bỏ các markdown không hỗ trợ khác nếu cần
|
| 81 |
+
return text
|
| 82 |
+
|
| 83 |
+
def split_message(self, text: str, max_length: int = 2000) -> list:
|
| 84 |
+
"""
|
| 85 |
+
Chia message thành các đoạn <= max_length ký tự, ưu tiên chia theo dòng.
|
| 86 |
+
"""
|
| 87 |
+
lines = text.split('\n')
|
| 88 |
+
messages = []
|
| 89 |
+
current = ""
|
| 90 |
+
for line in lines:
|
| 91 |
+
# +1 cho ký tự xuống dòng
|
| 92 |
+
if len(current) + len(line) + 1 > max_length:
|
| 93 |
+
messages.append(current.rstrip())
|
| 94 |
+
current = ""
|
| 95 |
+
current += (line + '\n')
|
| 96 |
+
if current.strip():
|
| 97 |
+
messages.append(current.rstrip())
|
| 98 |
+
return messages
|
| 99 |
+
|
| 100 |
@timing_decorator_async
|
| 101 |
+
async def send_message(self, page_access_token: Optional[str] = None, recipient_id: Optional[str] = None, message: str = "") -> dict:
|
| 102 |
page_access_token = page_access_token or self.page_token
|
| 103 |
recipient_id = recipient_id or self.sender_id
|
| 104 |
if not page_access_token or not recipient_id:
|
| 105 |
raise ValueError("FacebookClient: page_access_token and recipient_id must not be None when sending a message.")
|
| 106 |
+
# Format message
|
| 107 |
+
response_to_send = self.format_message(message.replace('**', '*')) if isinstance(message, str) else message
|
| 108 |
+
# Chia nhỏ nếu quá dài
|
| 109 |
+
messages = self.split_message(response_to_send)
|
| 110 |
+
results = []
|
| 111 |
+
for msg in messages:
|
| 112 |
+
if len(msg) > 2000:
|
| 113 |
+
msg = msg[:2000] # fallback cắt cứng
|
| 114 |
+
url = f"https://graph.facebook.com/v18.0/me/messages?access_token={page_access_token}"
|
| 115 |
+
payload = {
|
| 116 |
+
"recipient": {"id": recipient_id},
|
| 117 |
+
"message": {"text": msg}
|
| 118 |
+
}
|
| 119 |
+
try:
|
| 120 |
+
response = await self._client.post(url, json=payload)
|
| 121 |
+
response.raise_for_status()
|
| 122 |
+
results.append(response.json())
|
| 123 |
+
except httpx.HTTPError as e:
|
| 124 |
+
logger.error(f"Error sending message to Facebook: {e}")
|
| 125 |
+
raise HTTPException(status_code=500, detail="Failed to send message to Facebook")
|
| 126 |
+
return results[0] if results else {}
|
| 127 |
|
| 128 |
@timing_decorator_sync
|
| 129 |
def parse_message(self, body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
app/message_processor.py
CHANGED
|
@@ -196,52 +196,15 @@ class MessageProcessor:
|
|
| 196 |
response = None
|
| 197 |
if not command:
|
| 198 |
if muc_dich_to_use == "hỏi về mức phạt":
|
| 199 |
-
|
| 200 |
-
vehicle = conv.get('originalvehicle', '')
|
| 201 |
-
action = conv.get('originalaction', '')
|
| 202 |
-
keywords = [kw.strip() for kw in vehicle.split(',') if kw.strip()]
|
| 203 |
-
if keywords:
|
| 204 |
-
if action:
|
| 205 |
-
logger.info(f"[DEBUG] tạo embedding: {action}")
|
| 206 |
-
embedding = await self.channel.embedder.create_embedding(action)
|
| 207 |
-
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 208 |
-
matches = self.channel.supabase.match_documents(
|
| 209 |
-
embedding,
|
| 210 |
-
vehicle_keywords=keywords,
|
| 211 |
-
user_question=action
|
| 212 |
-
)
|
| 213 |
-
logger.info(f"[DEBUG] matches: {matches}")
|
| 214 |
-
if matches:
|
| 215 |
-
response = await self.format_search_results(message_text, matches, page_token, sender_id)
|
| 216 |
-
else:
|
| 217 |
-
response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
|
| 218 |
-
else:
|
| 219 |
-
logger.info(f"[DEBUG] Không có hành vi vi phạm: {message_text}")
|
| 220 |
-
response = "Xin lỗi, tôi không tìm thấy thông tin về hành vi vi phạm trong câu hỏi của bạn."
|
| 221 |
-
conv['isdone'] = True
|
| 222 |
-
else:
|
| 223 |
-
response = "Vui lòng cho biết loại phương tiện bạn cần tìm (xe máy, ô tô...)"
|
| 224 |
-
conv['isdone'] = False
|
| 225 |
elif muc_dich_to_use == "hỏi về quy tắc giao thông":
|
| 226 |
-
|
| 227 |
-
answer = await self.channel.llm.generate_text(message_text)
|
| 228 |
-
response = answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về quy tắc giao thông sẽ sớm có mặt."
|
| 229 |
-
conv['isdone'] = True
|
| 230 |
elif muc_dich_to_use == "hỏi về báo hiệu đường bộ":
|
| 231 |
-
|
| 232 |
-
answer = await self.channel.llm.generate_text(message_text)
|
| 233 |
-
response = answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về báo hiệu đường bộ sẽ sớm có mặt."
|
| 234 |
-
conv['isdone'] = True
|
| 235 |
elif muc_dich_to_use == "hỏi về quy trình xử lý vi phạm giao thông":
|
| 236 |
-
|
| 237 |
-
answer = await self.channel.llm.generate_text(message_text)
|
| 238 |
-
response = answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về quy trình xử lý vi phạm giao thông sẽ sớm có mặt."
|
| 239 |
-
conv['isdone'] = True
|
| 240 |
else:
|
| 241 |
-
|
| 242 |
-
answer = await self.channel.llm.generate_text(message_text)
|
| 243 |
-
response = answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng này sẽ sớm có mặt."
|
| 244 |
-
conv['isdone'] = True
|
| 245 |
else:
|
| 246 |
# Có command
|
| 247 |
if command == "xong":
|
|
@@ -256,11 +219,9 @@ class MessageProcessor:
|
|
| 256 |
conv['isdone'] = False
|
| 257 |
|
| 258 |
# 6. Gửi response và cập nhật final state
|
| 259 |
-
|
| 260 |
-
response_to_send = format_for_facebook(response.replace('**', '*')) if isinstance(response, str) else response
|
| 261 |
-
await self.facebook.send_message(message=response_to_send)
|
| 262 |
if hasattr(self.channel, 'sheets'):
|
| 263 |
-
await loop.run_in_executor(None, lambda:
|
| 264 |
return
|
| 265 |
|
| 266 |
def flatten_timestamp(self, ts):
|
|
@@ -454,14 +415,50 @@ class MessageProcessor:
|
|
| 454 |
logger.info(f"[MOCK] Creating Facebook post for sender_id={sender_id} with history={history}")
|
| 455 |
return "https://facebook.com/mock_post_url"
|
| 456 |
|
| 457 |
-
def
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
response = None
|
| 197 |
if not command:
|
| 198 |
if muc_dich_to_use == "hỏi về mức phạt":
|
| 199 |
+
response = await self.handle_muc_phat(conv, message_text, page_token, sender_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
elif muc_dich_to_use == "hỏi về quy tắc giao thông":
|
| 201 |
+
response = await self.handle_quy_tac(conv, message_text)
|
|
|
|
|
|
|
|
|
|
| 202 |
elif muc_dich_to_use == "hỏi về báo hiệu đường bộ":
|
| 203 |
+
response = await self.handle_bao_hieu(conv, message_text)
|
|
|
|
|
|
|
|
|
|
| 204 |
elif muc_dich_to_use == "hỏi về quy trình xử lý vi phạm giao thông":
|
| 205 |
+
response = await self.handle_quy_trinh(conv, message_text)
|
|
|
|
|
|
|
|
|
|
| 206 |
else:
|
| 207 |
+
response = await self.handle_khac(conv, message_text)
|
|
|
|
|
|
|
|
|
|
| 208 |
else:
|
| 209 |
# Có command
|
| 210 |
if command == "xong":
|
|
|
|
| 219 |
conv['isdone'] = False
|
| 220 |
|
| 221 |
# 6. Gửi response và cập nhật final state
|
| 222 |
+
await self.facebook.send_message(message=response)
|
|
|
|
|
|
|
| 223 |
if hasattr(self.channel, 'sheets'):
|
| 224 |
+
await loop.run_in_executor(None, lambda: sheets_client.log_conversation(**conv))
|
| 225 |
return
|
| 226 |
|
| 227 |
def flatten_timestamp(self, ts):
|
|
|
|
| 415 |
logger.info(f"[MOCK] Creating Facebook post for sender_id={sender_id} with history={history}")
|
| 416 |
return "https://facebook.com/mock_post_url"
|
| 417 |
|
| 418 |
+
async def handle_muc_phat(self, conv, message_text, page_token, sender_id):
|
| 419 |
+
vehicle = conv.get('originalvehicle', '')
|
| 420 |
+
action = conv.get('originalaction', '')
|
| 421 |
+
keywords = [kw.strip() for kw in vehicle.split(',') if kw.strip()]
|
| 422 |
+
if keywords:
|
| 423 |
+
if action:
|
| 424 |
+
logger.info(f"[DEBUG] tạo embedding: {action}")
|
| 425 |
+
embedding = await self.channel.embedder.create_embedding(action)
|
| 426 |
+
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 427 |
+
matches = self.channel.supabase.match_documents(
|
| 428 |
+
embedding,
|
| 429 |
+
vehicle_keywords=keywords,
|
| 430 |
+
user_question=action
|
| 431 |
+
)
|
| 432 |
+
logger.info(f"[DEBUG] matches: {matches}")
|
| 433 |
+
if matches:
|
| 434 |
+
response = await self.format_search_results(message_text, matches, page_token, sender_id)
|
| 435 |
+
else:
|
| 436 |
+
response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp."
|
| 437 |
+
else:
|
| 438 |
+
logger.info(f"[DEBUG] Không có hành vi vi phạm: {message_text}")
|
| 439 |
+
response = "Xin lỗi, tôi không tìm thấy thông tin về hành vi vi phạm trong câu hỏi của bạn."
|
| 440 |
+
conv['isdone'] = True
|
| 441 |
+
else:
|
| 442 |
+
response = "Vui lòng cho biết loại phương tiện bạn cần tìm (xe máy, ô tô...)"
|
| 443 |
+
conv['isdone'] = False
|
| 444 |
+
return response
|
| 445 |
+
|
| 446 |
+
async def handle_quy_tac(self, conv, message_text):
|
| 447 |
+
answer = await self.channel.llm.generate_text(message_text)
|
| 448 |
+
conv['isdone'] = True
|
| 449 |
+
return answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về quy tắc giao thông sẽ sớm có mặt."
|
| 450 |
+
|
| 451 |
+
async def handle_bao_hieu(self, conv, message_text):
|
| 452 |
+
answer = await self.channel.llm.generate_text(message_text)
|
| 453 |
+
conv['isdone'] = True
|
| 454 |
+
return answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về báo hiệu đường bộ sẽ sớm có mặt."
|
| 455 |
+
|
| 456 |
+
async def handle_quy_trinh(self, conv, message_text):
|
| 457 |
+
answer = await self.channel.llm.generate_text(message_text)
|
| 458 |
+
conv['isdone'] = True
|
| 459 |
+
return answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng trả lời về quy trình xử lý vi phạm giao thông sẽ sớm có mặt."
|
| 460 |
+
|
| 461 |
+
async def handle_khac(self, conv, message_text):
|
| 462 |
+
answer = await self.channel.llm.generate_text(message_text)
|
| 463 |
+
conv['isdone'] = True
|
| 464 |
+
return answer.strip() if answer and answer.strip() else "[Đang phát triển] Tính năng này sẽ sớm có mặt."
|