Spaces:
Sleeping
Sleeping
update prompt
Browse files- core/prompting.py +6 -8
- core/qa_pipeline.py +56 -60
core/prompting.py
CHANGED
|
@@ -85,7 +85,7 @@ Về vấn đề [Chủ đề], theo **Điều [Số]**, các trường hợp ng
|
|
| 85 |
# Lấy ví dụ phù hợp (Fallback về simple nếu không khớp)
|
| 86 |
example = examples.get(question_type, examples['simple'])
|
| 87 |
|
| 88 |
-
#
|
| 89 |
if topic:
|
| 90 |
topic_instr = (
|
| 91 |
f"\n\n **LƯU Ý ĐẶC BIỆT VỀ CHỦ ĐỀ MỞ RỘNG:**\n"
|
|
@@ -97,19 +97,17 @@ Về vấn đề [Chủ đề], theo **Điều [Số]**, các trường hợp ng
|
|
| 97 |
else:
|
| 98 |
topic_instr = ""
|
| 99 |
|
| 100 |
-
# [YEAR-AWARE CHANGE] Rang buoc cau tra loi theo nam hoc duoc hoi.
|
| 101 |
if year_scope:
|
| 102 |
year_instr = (
|
| 103 |
-
f"\n\n **RÀNG BUỘC NĂM HỌC (
|
| 104 |
-
f"- Người dùng đang hỏi
|
| 105 |
-
f"-
|
| 106 |
-
f"- Nếu
|
| 107 |
-
f"- Không kết luận 'không có dữ liệu' chỉ vì thiếu đúng nhãn năm nếu vẫn có quy định bao quát liên quan.\n"
|
| 108 |
)
|
| 109 |
else:
|
| 110 |
year_instr = ""
|
| 111 |
|
| 112 |
-
#
|
| 113 |
full_prompt = f"""{base_system}
|
| 114 |
----------------
|
| 115 |
{example}
|
|
|
|
| 85 |
# Lấy ví dụ phù hợp (Fallback về simple nếu không khớp)
|
| 86 |
example = examples.get(question_type, examples['simple'])
|
| 87 |
|
| 88 |
+
# TOPIC INSTRUCTION: Rào chắn ngữ cảnh (Context Guardrail)
|
| 89 |
if topic:
|
| 90 |
topic_instr = (
|
| 91 |
f"\n\n **LƯU Ý ĐẶC BIỆT VỀ CHỦ ĐỀ MỞ RỘNG:**\n"
|
|
|
|
| 97 |
else:
|
| 98 |
topic_instr = ""
|
| 99 |
|
|
|
|
| 100 |
if year_scope:
|
| 101 |
year_instr = (
|
| 102 |
+
f"\n\n **RÀNG BUỘC NĂM HỌC (LƯU Ý QUAN TRỌNG):**\n"
|
| 103 |
+
f"- Người dùng đang hỏi cho năm học: **{year_scope}**.\n"
|
| 104 |
+
f"- Nếu trong `TÀI LIỆU THAM KHẢO` có nội dung khớp với năm này, hãy dùng nó làm đáp án chính.\n"
|
| 105 |
+
f"- Nếu KHÔNG CÓ nội dung đúng năm, BẮT BUỘC SỬ DỤNG tài liệu có nhãn 'Áp dụng nhiều năm' hoặc quy chế gần nhất có trong context. Khi trả lời, hãy rào trước một câu thân thiện: *'Hệ thống hiện ghi nhận quy chế dùng chung/năm [Năm của tài liệu] quy định như sau...'*. TUYỆT ĐỐI KHÔNG TỪ CHỐI trả lời nếu vẫn có bản dùng chung.\n"
|
|
|
|
| 106 |
)
|
| 107 |
else:
|
| 108 |
year_instr = ""
|
| 109 |
|
| 110 |
+
# Gộp Prompt
|
| 111 |
full_prompt = f"""{base_system}
|
| 112 |
----------------
|
| 113 |
{example}
|
core/qa_pipeline.py
CHANGED
|
@@ -4,7 +4,7 @@ import logging
|
|
| 4 |
import groq
|
| 5 |
import google.generativeai as genai
|
| 6 |
import json
|
| 7 |
-
|
| 8 |
from .models import llm
|
| 9 |
from .config import TOP_K_RESULTS, FINAL_TOP_K
|
| 10 |
from .rerank import advanced_rerank
|
|
@@ -140,10 +140,14 @@ def sanitize_for_prompt(text: str) -> str:
|
|
| 140 |
return text.strip()
|
| 141 |
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
def _normalize_for_router(message: str) -> str:
|
| 144 |
-
compact =
|
| 145 |
-
compact = re.sub(r"\s
|
| 146 |
-
return compact
|
| 147 |
|
| 148 |
|
| 149 |
def _quick_non_domain_reply(message: str) -> Optional[str]:
|
|
@@ -278,34 +282,25 @@ def ask_ai_improved(message: str, history: List, hybrid_retriever) -> Generator[
|
|
| 278 |
yield full_response
|
| 279 |
|
| 280 |
def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Generator[str, None, None]:
|
|
|
|
| 281 |
if not message.strip():
|
| 282 |
-
yield "
|
| 283 |
return
|
| 284 |
|
|
|
|
| 285 |
quick_reply = _quick_non_domain_reply(message)
|
| 286 |
if quick_reply:
|
| 287 |
logger.info("Bỏ qua truy xuất tài liệu cho câu hỏi giao tiếp/ngoài phạm vi")
|
| 288 |
yield quick_reply
|
| 289 |
return
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
if not _was_recently_prompted_for_year(history):
|
| 294 |
-
logger.info("Yêu cầu người dùng bổ sung năm học trước khi truy vấn")
|
| 295 |
-
yield "Vui lòng nhập kèm năm học để tra cứu nhanh hơn (ví dụ: 2022-2023 hoặc 2023)."
|
| 296 |
-
return
|
| 297 |
-
|
| 298 |
-
logger.info("Người dùng chưa nhập năm sau khi đã được nhắc; fallback sang tìm kiếm toàn bộ")
|
| 299 |
-
|
| 300 |
-
logger.info(f" CÂU HỎI GỐC: {message}")
|
| 301 |
question = generate_standalone_query(message, history)
|
| 302 |
-
# [YEAR-AWARE CHANGE] Xac dinh pham vi nam ma nguoi dung yeu cau.
|
| 303 |
requested_year_range, mentioned_years = detect_requested_year(f"{message}\n{question}")
|
| 304 |
-
|
| 305 |
-
logger.info(f"Lọc theo năm học yêu cầu: {requested_year_range}")
|
| 306 |
-
elif mentioned_years:
|
| 307 |
-
logger.info(f"Lọc theo năm được nhắc tới: {sorted(mentioned_years)}")
|
| 308 |
|
|
|
|
| 309 |
processed_data = analyze_and_expand_query(question)
|
| 310 |
|
| 311 |
if processed_data.get("question_type") == "normal":
|
|
@@ -317,57 +312,55 @@ def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Genera
|
|
| 317 |
queries = processed_data['expanded_queries']
|
| 318 |
logger.info(f"Các truy vấn tìm kiếm: {queries}")
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
|
| 338 |
logger.info(f"Tìm thấy tổng {len(all_docs)} documents.")
|
|
|
|
|
|
|
| 339 |
if not all_docs:
|
| 340 |
-
yield "
|
| 341 |
return
|
| 342 |
|
| 343 |
-
#
|
| 344 |
-
year_scope = None
|
| 345 |
-
year_filter_requested = bool(requested_year_range or mentioned_years)
|
| 346 |
-
year_filtered_docs = filter_docs_by_year(all_docs, requested_year_range, mentioned_years)
|
| 347 |
-
|
| 348 |
-
if year_filter_requested:
|
| 349 |
-
if year_filtered_docs:
|
| 350 |
-
if len(year_filtered_docs) != len(all_docs):
|
| 351 |
-
logger.info(f"Đã lọc theo năm: còn {len(year_filtered_docs)}/{len(all_docs)} documents")
|
| 352 |
-
all_docs = year_filtered_docs
|
| 353 |
-
if requested_year_range:
|
| 354 |
-
year_scope = requested_year_range
|
| 355 |
-
elif mentioned_years:
|
| 356 |
-
year_scope = ", ".join(sorted(mentioned_years))
|
| 357 |
-
else:
|
| 358 |
-
logger.warning("Không tìm thấy tài liệu đúng năm yêu cầu, fallback sang tập tài liệu tổng quát")
|
| 359 |
-
|
| 360 |
final_docs = advanced_rerank(question, all_docs, top_k=FINAL_TOP_K)
|
| 361 |
|
|
|
|
| 362 |
context_parts = []
|
| 363 |
total_chars = 0
|
| 364 |
for doc in final_docs:
|
| 365 |
page = doc.metadata.get('page_number', 'N/A')
|
| 366 |
file_name = doc.metadata.get('source_file') or doc.metadata.get('source')
|
| 367 |
-
|
| 368 |
doc_year = infer_doc_academic_year(doc)
|
| 369 |
year_label = f"Năm {doc_year}" if doc_year != "ALL" else "Áp dụng nhiều năm"
|
| 370 |
source = f"[{year_label} | {os.path.basename(file_name)} | Trang {page}]" if file_name else f"[{year_label} | Trang {page}]"
|
|
|
|
| 371 |
block = f"{source}\n{doc.page_content}"
|
| 372 |
if total_chars + len(block) > MAX_CONTEXT_CHARS:
|
| 373 |
break
|
|
@@ -377,12 +370,14 @@ def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Genera
|
|
| 377 |
context = "\n\n---\n\n".join(context_parts)
|
| 378 |
topic_hint = processed_data.get('topic') or processed_data.get('root_question') or question
|
| 379 |
|
| 380 |
-
prompt
|
|
|
|
| 381 |
|
| 382 |
logger.info("Đang tạo câu trả lời cuối cùng ...")
|
| 383 |
|
| 384 |
success = False
|
| 385 |
-
|
|
|
|
| 386 |
for _ in range(len(api_manager.groq_keys)):
|
| 387 |
try:
|
| 388 |
client = api_manager.get_groq_client()
|
|
@@ -404,7 +399,7 @@ def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Genera
|
|
| 404 |
logger.error(f"Lỗi Groq: {e}")
|
| 405 |
break
|
| 406 |
|
| 407 |
-
#
|
| 408 |
if not success:
|
| 409 |
logger.warning("Chuyển sang Gemini ...")
|
| 410 |
for _ in range(max(1, len(api_manager.gemini_keys))):
|
|
@@ -421,5 +416,6 @@ def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Genera
|
|
| 421 |
api_manager.rotate_gemini()
|
| 422 |
logger.error(f"Lỗi Gemini: {e}")
|
| 423 |
|
|
|
|
| 424 |
if not success:
|
| 425 |
-
yield "Đã xảy ra lỗi hệ thống hoặc quá tải. Vui lòng thử lại sau giây lát!"
|
|
|
|
| 4 |
import groq
|
| 5 |
import google.generativeai as genai
|
| 6 |
import json
|
| 7 |
+
import unicodedata
|
| 8 |
from .models import llm
|
| 9 |
from .config import TOP_K_RESULTS, FINAL_TOP_K
|
| 10 |
from .rerank import advanced_rerank
|
|
|
|
| 140 |
return text.strip()
|
| 141 |
|
| 142 |
|
| 143 |
+
def remove_accents(input_str: str) -> str:
|
| 144 |
+
s1 = unicodedata.normalize('NFKD', input_str).encode('ASCII', 'ignore').decode('utf-8')
|
| 145 |
+
return s1.lower()
|
| 146 |
+
|
| 147 |
def _normalize_for_router(message: str) -> str:
|
| 148 |
+
compact = remove_accents(message or "")
|
| 149 |
+
compact = re.sub(r"[^\w\s]", " ", compact, flags=re.UNICODE)
|
| 150 |
+
return re.sub(r"\s+", " ", compact).strip()
|
| 151 |
|
| 152 |
|
| 153 |
def _quick_non_domain_reply(message: str) -> Optional[str]:
|
|
|
|
| 282 |
yield full_response
|
| 283 |
|
| 284 |
def ask_ai_stream_delta(message: str, history: List, hybrid_retriever) -> Generator[str, None, None]:
|
| 285 |
+
# Kiểm tra rỗng
|
| 286 |
if not message.strip():
|
| 287 |
+
yield "Bạn chưa nhập câu hỏi."
|
| 288 |
return
|
| 289 |
|
| 290 |
+
# Xử lý các câu giao tiếp/xã giao nhanh (đã được sửa lỗi dấu tiếng Việt)
|
| 291 |
quick_reply = _quick_non_domain_reply(message)
|
| 292 |
if quick_reply:
|
| 293 |
logger.info("Bỏ qua truy xuất tài liệu cho câu hỏi giao tiếp/ngoài phạm vi")
|
| 294 |
yield quick_reply
|
| 295 |
return
|
| 296 |
|
| 297 |
+
# Phân tích câu hỏi
|
| 298 |
+
logger.info(f"CÂU HỎI GỐC: {message}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
question = generate_standalone_query(message, history)
|
|
|
|
| 300 |
requested_year_range, mentioned_years = detect_requested_year(f"{message}\n{question}")
|
| 301 |
+
year_scope_hint = requested_year_range or (", ".join(sorted(mentioned_years)) if mentioned_years else None)
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
+
# Phân loại và mở rộng từ khóa
|
| 304 |
processed_data = analyze_and_expand_query(question)
|
| 305 |
|
| 306 |
if processed_data.get("question_type") == "normal":
|
|
|
|
| 312 |
queries = processed_data['expanded_queries']
|
| 313 |
logger.info(f"Các truy vấn tìm kiếm: {queries}")
|
| 314 |
|
| 315 |
+
def fetch_docs(year_hint):
|
| 316 |
+
docs_temp = []
|
| 317 |
+
seen_temp = set()
|
| 318 |
+
for query in queries:
|
| 319 |
+
current_alpha = 0.4 if "CNTT" in query.upper() else 0.5
|
| 320 |
+
retrieved = hybrid_retriever.search(
|
| 321 |
+
query,
|
| 322 |
+
k=TOP_K_RESULTS,
|
| 323 |
+
alpha=current_alpha,
|
| 324 |
+
year_scope=year_hint
|
| 325 |
+
)
|
| 326 |
+
for doc in retrieved:
|
| 327 |
+
content_hash = hashlib.sha256(doc.page_content.encode("utf-8")).hexdigest()
|
| 328 |
+
if content_hash not in seen_temp:
|
| 329 |
+
docs_temp.append(doc)
|
| 330 |
+
seen_temp.add(content_hash)
|
| 331 |
+
return docs_temp
|
| 332 |
+
# Tìm tài liệu
|
| 333 |
+
|
| 334 |
+
# Cố gắng tìm tài liệu khớp chính xác với năm học người dùng nhắc đến
|
| 335 |
+
all_docs = fetch_docs(year_scope_hint)
|
| 336 |
+
|
| 337 |
+
# Nếu lớp 1 tìm không ra hoặc người dùng hoàn toàn không nhập năm, hệ thống sẽ tự động hạ chuẩn, tìm trên toàn bộ cơ sở dữ liệu chung (ALL)
|
| 338 |
+
if not all_docs and year_scope_hint:
|
| 339 |
+
logger.info(f"Bộ l���c năm '{year_scope_hint}' quá gắt không ra kết quả. Tự động Fallback tìm trên bản chung...")
|
| 340 |
+
year_scope_hint = None # Reset lại biến hint để quét toàn bộ VectorDB
|
| 341 |
+
all_docs = fetch_docs(None)
|
| 342 |
|
| 343 |
logger.info(f"Tìm thấy tổng {len(all_docs)} documents.")
|
| 344 |
+
|
| 345 |
+
# Xử lý lịch sự nếu Vector DB thực sự "bó tay"
|
| 346 |
if not all_docs:
|
| 347 |
+
yield f"Dạ, hiện tại hệ thống không tìm thấy quy định nào liên quan đến vấn đề này. Bạn có thể dùng các từ khóa mang tính hành chính hơn được không ạ?"
|
| 348 |
return
|
| 349 |
|
| 350 |
+
# Rerank lại kết quả để chống ảo giác
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
final_docs = advanced_rerank(question, all_docs, top_k=FINAL_TOP_K)
|
| 352 |
|
| 353 |
+
# Gắn nhãn năm học vào Context cho LLM đọc
|
| 354 |
context_parts = []
|
| 355 |
total_chars = 0
|
| 356 |
for doc in final_docs:
|
| 357 |
page = doc.metadata.get('page_number', 'N/A')
|
| 358 |
file_name = doc.metadata.get('source_file') or doc.metadata.get('source')
|
| 359 |
+
|
| 360 |
doc_year = infer_doc_academic_year(doc)
|
| 361 |
year_label = f"Năm {doc_year}" if doc_year != "ALL" else "Áp dụng nhiều năm"
|
| 362 |
source = f"[{year_label} | {os.path.basename(file_name)} | Trang {page}]" if file_name else f"[{year_label} | Trang {page}]"
|
| 363 |
+
|
| 364 |
block = f"{source}\n{doc.page_content}"
|
| 365 |
if total_chars + len(block) > MAX_CONTEXT_CHARS:
|
| 366 |
break
|
|
|
|
| 370 |
context = "\n\n---\n\n".join(context_parts)
|
| 371 |
topic_hint = processed_data.get('topic') or processed_data.get('root_question') or question
|
| 372 |
|
| 373 |
+
# Truyền year_scope_hint vào prompt để LLM biết đường rào đón
|
| 374 |
+
prompt = create_advanced_prompt(question, context, question_type, topic_hint, year_scope=year_scope_hint)
|
| 375 |
|
| 376 |
logger.info("Đang tạo câu trả lời cuối cùng ...")
|
| 377 |
|
| 378 |
success = False
|
| 379 |
+
|
| 380 |
+
# Streaming qua Groq (Có xoay tua khi gặp lỗi 429)
|
| 381 |
for _ in range(len(api_manager.groq_keys)):
|
| 382 |
try:
|
| 383 |
client = api_manager.get_groq_client()
|
|
|
|
| 399 |
logger.error(f"Lỗi Groq: {e}")
|
| 400 |
break
|
| 401 |
|
| 402 |
+
# Streaming dự phòng qua Gemini
|
| 403 |
if not success:
|
| 404 |
logger.warning("Chuyển sang Gemini ...")
|
| 405 |
for _ in range(max(1, len(api_manager.gemini_keys))):
|
|
|
|
| 416 |
api_manager.rotate_gemini()
|
| 417 |
logger.error(f"Lỗi Gemini: {e}")
|
| 418 |
|
| 419 |
+
# Báo lỗi khi cả 2 API đều sập
|
| 420 |
if not success:
|
| 421 |
+
yield "Đã xảy ra lỗi hệ thống hoặc quá tải API. Vui lòng thử lại sau giây lát!"
|