update reranker
Browse files- app/facebook.py +122 -69
- app/gemini_client.py +1 -1
- app/llm.py +32 -22
- app/message_processor.py +1 -1
- 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__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
| 74 |
-
text = text.replace(
|
| 75 |
-
text = text.replace(
|
| 76 |
-
text = text.replace(
|
| 77 |
# 2. Chuyển **text** hoặc __text__ thành *text*
|
| 78 |
import re
|
| 79 |
-
|
| 80 |
-
text = re.sub(r
|
|
|
|
| 81 |
# 3. Loại bỏ các tiêu đề markdown kiểu #, ##, ###, ...
|
| 82 |
-
text = re.sub(r
|
| 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
|
| 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(
|
| 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 +=
|
| 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.
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 158 |
return {"error": str(e)}
|
| 159 |
|
| 160 |
-
def _send_message_sync(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 183 |
if attempt == max_retries - 1: # Lần cuối
|
| 184 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
| 185 |
time.sleep(retry_delay)
|
| 186 |
retry_delay *= 2 # Exponential backoff
|
| 187 |
except Exception as e:
|
| 188 |
-
logger.error(
|
|
|
|
|
|
|
| 189 |
if attempt == max_retries - 1: # Lần cuối
|
| 190 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 208 |
if attempt == max_retries - 1: # Lần cuối
|
| 209 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 210 |
time.sleep(retry_delay)
|
| 211 |
retry_delay *= 2 # Exponential backoff
|
| 212 |
except Exception as e:
|
| 213 |
-
logger.error(
|
|
|
|
|
|
|
| 214 |
if attempt == max_retries - 1: # Lần cuối
|
| 215 |
-
raise HTTPException(
|
|
|
|
|
|
|
| 216 |
time.sleep(retry_delay)
|
| 217 |
retry_delay *= 2 # Exponential backoff
|
| 218 |
|
| 219 |
@timing_decorator_async
|
| 220 |
-
async def send_message(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 226 |
return {}
|
| 227 |
|
| 228 |
if not page_access_token or not recipient_id:
|
| 229 |
-
logger.error(
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
| 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.
|
| 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ý
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
-
|
| 473 |
-
|
| 474 |
-
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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": ["
|
| 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
|
| 509 |
-
"
|
| 510 |
"biển báo hết tốc độ tối đa cho phép",
|
| 511 |
"biển báo DP.134",
|
| 512 |
-
"
|
| 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 và 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 mô 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 ô tô 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 |
+
- Ví 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 |
+
- Ví dụ: hỏi về "hiệu lực biển báo khi qua ngã tư" -> 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.
|
| 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
|
| 166 |
scores_text = str(response).strip()
|
| 167 |
-
|
| 168 |
-
score_strings =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|