update analyze flow
Browse files- app/constants.py +1 -1
- app/llm.py +16 -11
- app/message_processor.py +53 -29
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 = 123456799
|
app/llm.py
CHANGED
|
@@ -370,17 +370,16 @@ class LLMClient:
|
|
| 370 |
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. Chỉ trả về đối tượng JSON, không thêm bất kỳ giải thích nào.
|
| 371 |
|
| 372 |
Định dạng JSON bắt buộc:
|
| 373 |
-
|
| 374 |
{{
|
| 375 |
"muc_dich": "...",
|
| 376 |
"phuong_tien": "...",
|
| 377 |
-
"tu_khoa":
|
| 378 |
"cau_hoi": "..."
|
| 379 |
}}
|
| 380 |
|
| 381 |
Hướng dẫn chi tiết cho từng trường:
|
| 382 |
|
| 383 |
-
**muc_dich**: Phải là một trong các giá trị sau
|
| 384 |
- "hỏi về mức phạt"
|
| 385 |
- "hỏi về quy tắc giao thông"
|
| 386 |
- "hỏi về báo hiệu đường bộ"
|
|
@@ -388,23 +387,29 @@ class LLMClient:
|
|
| 388 |
- "thông tin cá nhân của AI"
|
| 389 |
- "khác"
|
| 390 |
|
| 391 |
-
**Phải dựa vào câu hỏi mới nhất để xác định.**
|
| 392 |
-
|
| 393 |
**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 "".
|
| 394 |
|
| 395 |
-
**tu_khoa**:
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
**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ý.
|
| 398 |
|
| 399 |
VÍ DỤ MẪU:
|
| 400 |
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
Kết quả JSON mong muốn:
|
| 403 |
{{
|
| 404 |
"muc_dich": "hỏi về mức phạt",
|
| 405 |
-
"phuong_tien": "
|
| 406 |
-
"tu_khoa": "
|
| 407 |
-
"cau_hoi": "Mức xử phạt cho hành vi
|
| 408 |
}}
|
| 409 |
|
| 410 |
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.
|
|
@@ -412,7 +417,7 @@ class LLMClient:
|
|
| 412 |
Lịch sử trò chuyện:
|
| 413 |
"{conversation_context}"
|
| 414 |
|
| 415 |
-
Câu hỏi:
|
| 416 |
"{text}"
|
| 417 |
""".strip()
|
| 418 |
|
|
|
|
| 370 |
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. Chỉ trả về đối tượng JSON, không thêm bất kỳ giải thích nào.
|
| 371 |
|
| 372 |
Định dạng JSON bắt buộc:
|
|
|
|
| 373 |
{{
|
| 374 |
"muc_dich": "...",
|
| 375 |
"phuong_tien": "...",
|
| 376 |
+
"tu_khoa": [...],
|
| 377 |
"cau_hoi": "..."
|
| 378 |
}}
|
| 379 |
|
| 380 |
Hướng dẫn chi tiết cho từng trường:
|
| 381 |
|
| 382 |
+
**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**:
|
| 383 |
- "hỏi về mức phạt"
|
| 384 |
- "hỏi về quy tắc giao thông"
|
| 385 |
- "hỏi về báo hiệu đường bộ"
|
|
|
|
| 387 |
- "thông tin cá nhân của AI"
|
| 388 |
- "khác"
|
| 389 |
|
|
|
|
|
|
|
| 390 |
**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 "".
|
| 391 |
|
| 392 |
+
**tu_khoa**: **MỘT DANH SÁCH (LIST) các thuật ngữ pháp lý** ngắn gọn, chính xác nhất để tìm kiếm trong cơ sở dữ liệu luật.
|
| 393 |
+
- **Chuyển đổi ngôn ngữ**: 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 (ví dụ: "Không chấp hành hiệu lệnh của đèn tín hiệu giao thông").
|
| 394 |
+
- **Trích xuất nhiều từ khóa**: Nếu câu hỏi phức tạp, hãy trích xuất nhiều từ khóa liên quan. 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 mà trong máu hoặc hơi thở có nồng độ cồn"].
|
| 395 |
+
- **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à..."), hãy tạo ra một bộ từ khóa **MỚI** và **KHÁC** với các từ khóa đã dùng trong lượt hỏi trước (được ghi trong `(từ khóa đã dùng: ...)` của lịch sử) để tìm kiếm thông tin chính xác hơn.
|
| 396 |
|
| 397 |
**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ý.
|
| 398 |
|
| 399 |
VÍ DỤ MẪU:
|
| 400 |
|
| 401 |
+
Lịch sử trò chuyện:
|
| 402 |
+
"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)
|
| 403 |
+
Trợ lý: Mức phạt cho hành vi đi vào khu vực cấm là..."
|
| 404 |
+
|
| 405 |
+
Câu hỏi mới nhất: "không phải, ý tôi là đi vào đường cao tốc cơ"
|
| 406 |
+
|
| 407 |
Kết quả JSON mong muốn:
|
| 408 |
{{
|
| 409 |
"muc_dich": "hỏi về mức phạt",
|
| 410 |
+
"phuong_tien": "Xe máy",
|
| 411 |
+
"tu_khoa": ["Điều khiển xe đi vào đường cao tốc"],
|
| 412 |
+
"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?"
|
| 413 |
}}
|
| 414 |
|
| 415 |
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.
|
|
|
|
| 417 |
Lịch sử trò chuyện:
|
| 418 |
"{conversation_context}"
|
| 419 |
|
| 420 |
+
Câu hỏi mới nhất:
|
| 421 |
"{text}"
|
| 422 |
""".strip()
|
| 423 |
|
app/message_processor.py
CHANGED
|
@@ -102,29 +102,44 @@ class MessageProcessor:
|
|
| 102 |
logger.info(f"[LLM][RAW] Kết quả trả về từ analyze: {llm_analysis}")
|
| 103 |
|
| 104 |
muc_dich = None
|
| 105 |
-
|
| 106 |
cau_hoi = None
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
elif isinstance(llm_analysis,
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
else:
|
|
|
|
| 118 |
keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
|
| 119 |
cau_hoi = message_text
|
| 120 |
for kw in keywords: cau_hoi = cau_hoi.replace(kw, "")
|
| 121 |
cau_hoi = cau_hoi.strip()
|
| 122 |
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
conv.update({
|
| 126 |
'originalcommand': command, 'originalcontent': remaining_text, 'originalvehicle': ','.join(keywords),
|
| 127 |
-
'originalaction':
|
| 128 |
})
|
| 129 |
|
| 130 |
muc_dich_to_use = muc_dich or conv.get('originalpurpose')
|
|
@@ -174,22 +189,29 @@ class MessageProcessor:
|
|
| 174 |
return max([self.get_latest_timestamp(item) for item in ts_value]) if ts_value else 0
|
| 175 |
return 0
|
| 176 |
|
| 177 |
-
def get_llm_history(self, history: List):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
sorted_history = sorted(history, key=lambda row: self.get_latest_timestamp(row.get('timestamp', 0)))
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
if
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
def flatten_timestamp(self, ts):
|
| 195 |
flat = []
|
|
@@ -272,6 +294,7 @@ class MessageProcessor:
|
|
| 272 |
vehicle = conv.get('originalvehicle', '')
|
| 273 |
action = conv.get('originalaction', '')
|
| 274 |
question = conv.get('originalquestion', '')
|
|
|
|
| 275 |
|
| 276 |
if not action and not question:
|
| 277 |
return "Để tra cứu mức phạt, bạn vui lòng cung cấp hành vi vi phạm nhé."
|
|
@@ -285,6 +308,7 @@ class MessageProcessor:
|
|
| 285 |
loop = asyncio.get_event_loop()
|
| 286 |
match_count = get_settings().match_count
|
| 287 |
|
|
|
|
| 288 |
matches = await loop.run_in_executor(
|
| 289 |
None,
|
| 290 |
lambda: self.channel.supabase.match_documents(
|
|
@@ -300,7 +324,7 @@ class MessageProcessor:
|
|
| 300 |
else:
|
| 301 |
response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp với hành vi bạn mô tả."
|
| 302 |
except Exception as e:
|
| 303 |
-
logger.error(f"Lỗi khi tra cứu mức phạt: {e}")
|
| 304 |
response = "Đã có lỗi xảy ra trong quá trình tra cứu. Vui lòng thử lại sau."
|
| 305 |
|
| 306 |
conv['isdone'] = True
|
|
|
|
| 102 |
logger.info(f"[LLM][RAW] Kết quả trả về từ analyze: {llm_analysis}")
|
| 103 |
|
| 104 |
muc_dich = None
|
| 105 |
+
tu_khoa_list = [] # Sửa: đổi tên thành tu_khoa_list và khởi tạo là list rỗng
|
| 106 |
cau_hoi = None
|
| 107 |
+
|
| 108 |
+
# Sửa: Đơn giản hóa logic, vì LLM giờ luôn trả về 1 dict
|
| 109 |
+
analysis_data = None
|
| 110 |
+
if isinstance(llm_analysis, list) and llm_analysis:
|
| 111 |
+
analysis_data = llm_analysis[0]
|
| 112 |
+
elif isinstance(llm_analysis, dict):
|
| 113 |
+
analysis_data = llm_analysis
|
| 114 |
+
|
| 115 |
+
if analysis_data:
|
| 116 |
+
# Lấy phương tiện và chuẩn hóa
|
| 117 |
+
phuong_tien = self.normalize_vehicle_keyword(analysis_data.get('phuong_tien', ''))
|
| 118 |
+
keywords = [phuong_tien] if phuong_tien else []
|
| 119 |
+
|
| 120 |
+
muc_dich = analysis_data.get('muc_dich')
|
| 121 |
+
|
| 122 |
+
# Lấy danh sách từ khóa, đảm bảo nó là list
|
| 123 |
+
raw_tu_khoa = analysis_data.get('tu_khoa', [])
|
| 124 |
+
if isinstance(raw_tu_khoa, list):
|
| 125 |
+
tu_khoa_list = raw_tu_khoa
|
| 126 |
+
elif isinstance(raw_tu_khoa, str) and raw_tu_khoa:
|
| 127 |
+
tu_khoa_list = [raw_tu_khoa] # Chuyển string thành list 1 phần tử
|
| 128 |
+
|
| 129 |
+
cau_hoi = analysis_data.get('cau_hoi')
|
| 130 |
else:
|
| 131 |
+
# Fallback logic cũ nếu LLM không phân tích được
|
| 132 |
keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
|
| 133 |
cau_hoi = message_text
|
| 134 |
for kw in keywords: cau_hoi = cau_hoi.replace(kw, "")
|
| 135 |
cau_hoi = cau_hoi.strip()
|
| 136 |
|
| 137 |
+
# Sửa: Log danh sách từ khóa
|
| 138 |
+
logger.info(f"[DEBUG] Phương tiện: {keywords} - Từ khóa pháp lý: {tu_khoa_list} - Mục đích: {muc_dich} - Câu hỏi: {cau_hoi}")
|
| 139 |
|
| 140 |
conv.update({
|
| 141 |
'originalcommand': command, 'originalcontent': remaining_text, 'originalvehicle': ','.join(keywords),
|
| 142 |
+
'originalaction': ' '.join(tu_khoa_list), 'originalpurpose': muc_dich, 'originalquestion': cau_hoi or ""
|
| 143 |
})
|
| 144 |
|
| 145 |
muc_dich_to_use = muc_dich or conv.get('originalpurpose')
|
|
|
|
| 189 |
return max([self.get_latest_timestamp(item) for item in ts_value]) if ts_value else 0
|
| 190 |
return 0
|
| 191 |
|
| 192 |
+
def get_llm_history(self, history: List[Dict[str, Any]]) -> str:
|
| 193 |
+
"""
|
| 194 |
+
Định dạng lịch sử hội thoại thành một chuỗi văn bản duy nhất,
|
| 195 |
+
bao gồm cả các từ khóa đã sử dụng để cung cấp ngữ cảnh cho LLM.
|
| 196 |
+
"""
|
| 197 |
sorted_history = sorted(history, key=lambda row: self.get_latest_timestamp(row.get('timestamp', 0)))
|
| 198 |
+
|
| 199 |
+
# Lấy 5 lượt hội thoại gần nhất để tránh context quá dài
|
| 200 |
+
recent_history = sorted_history[-5:]
|
| 201 |
+
|
| 202 |
+
context_lines = []
|
| 203 |
+
for row in recent_history:
|
| 204 |
+
user_text = row.get('originaltext', '').strip()
|
| 205 |
+
assistant_text = row.get('systemresponse', '').strip()
|
| 206 |
+
keywords_used = row.get('originalaction', '').strip()
|
| 207 |
+
|
| 208 |
+
if user_text:
|
| 209 |
+
context_lines.append(f"Người dùng: {user_text} (từ khóa đã dùng: {keywords_used})")
|
| 210 |
+
|
| 211 |
+
if assistant_text:
|
| 212 |
+
context_lines.append(f"Trợ lý: {assistant_text}")
|
| 213 |
+
|
| 214 |
+
return "\n".join(context_lines)
|
| 215 |
|
| 216 |
def flatten_timestamp(self, ts):
|
| 217 |
flat = []
|
|
|
|
| 294 |
vehicle = conv.get('originalvehicle', '')
|
| 295 |
action = conv.get('originalaction', '')
|
| 296 |
question = conv.get('originalquestion', '')
|
| 297 |
+
|
| 298 |
|
| 299 |
if not action and not question:
|
| 300 |
return "Để tra cứu mức phạt, bạn vui lòng cung cấp hành vi vi phạm nhé."
|
|
|
|
| 308 |
loop = asyncio.get_event_loop()
|
| 309 |
match_count = get_settings().match_count
|
| 310 |
|
| 311 |
+
|
| 312 |
matches = await loop.run_in_executor(
|
| 313 |
None,
|
| 314 |
lambda: self.channel.supabase.match_documents(
|
|
|
|
| 324 |
else:
|
| 325 |
response = "Xin lỗi, tôi không tìm thấy thông tin phù hợp với hành vi bạn mô tả."
|
| 326 |
except Exception as e:
|
| 327 |
+
logger.error(f"Lỗi khi tra cứu mức phạt: {e}\n{traceback.format_exc()}")
|
| 328 |
response = "Đã có lỗi xảy ra trong quá trình tra cứu. Vui lòng thử lại sau."
|
| 329 |
|
| 330 |
conv['isdone'] = True
|