adjust log
Browse files- app/gemini_client.py +3 -2
- app/llm.py +10 -17
- app/message_processor.py +6 -6
- app/reranker.py +1 -1
- app/sheets.py +3 -3
- app/supabase_db.py +1 -1
- app/utils.py +44 -1
app/gemini_client.py
CHANGED
|
@@ -4,6 +4,7 @@ from google.generativeai.generative_models import GenerativeModel
|
|
| 4 |
from loguru import logger
|
| 5 |
from .request_limit_manager import RequestLimitManager
|
| 6 |
from typing import List, Optional
|
|
|
|
| 7 |
|
| 8 |
class GeminiClient:
|
| 9 |
def __init__(self):
|
|
@@ -59,10 +60,10 @@ class GeminiClient:
|
|
| 59 |
logger.info(f"[GEMINI][USAGE] Prompt Token Count: {response.usage_metadata.prompt_token_count} - Candidate Token Count: {response.usage_metadata.candidates_token_count} - Total Token Count: {response.usage_metadata.total_token_count}")
|
| 60 |
|
| 61 |
if hasattr(response, 'text'):
|
| 62 |
-
logger.info(f"[GEMINI][TEXT_RESPONSE] {response.text}")
|
| 63 |
return response.text
|
| 64 |
elif hasattr(response, 'candidates') and response.candidates:
|
| 65 |
-
logger.info(f"[GEMINI][CANDIDATES_RESPONSE] {response.candidates[0].content.parts[0].text}")
|
| 66 |
return response.candidates[0].content.parts[0].text
|
| 67 |
|
| 68 |
logger.info(f"[GEMINI][RAW_RESPONSE] {response}")
|
|
|
|
| 4 |
from loguru import logger
|
| 5 |
from .request_limit_manager import RequestLimitManager
|
| 6 |
from typing import List, Optional
|
| 7 |
+
from utils import _safe_truncate
|
| 8 |
|
| 9 |
class GeminiClient:
|
| 10 |
def __init__(self):
|
|
|
|
| 60 |
logger.info(f"[GEMINI][USAGE] Prompt Token Count: {response.usage_metadata.prompt_token_count} - Candidate Token Count: {response.usage_metadata.candidates_token_count} - Total Token Count: {response.usage_metadata.total_token_count}")
|
| 61 |
|
| 62 |
if hasattr(response, 'text'):
|
| 63 |
+
logger.info(f"[GEMINI][TEXT_RESPONSE] {_safe_truncate(response.text)}")
|
| 64 |
return response.text
|
| 65 |
elif hasattr(response, 'candidates') and response.candidates:
|
| 66 |
+
logger.info(f"[GEMINI][CANDIDATES_RESPONSE] {_safe_truncate(response.candidates[0].content.parts[0].text)}")
|
| 67 |
return response.candidates[0].content.parts[0].text
|
| 68 |
|
| 69 |
logger.info(f"[GEMINI][RAW_RESPONSE] {response}")
|
app/llm.py
CHANGED
|
@@ -15,16 +15,9 @@ from .utils import (
|
|
| 15 |
timing_decorator_async,
|
| 16 |
timing_decorator_sync, # kept for compatibility even if unused here
|
| 17 |
call_endpoint_with_retry,
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
def _safe_truncate(s: str, n: int = 1000) -> str:
|
| 22 |
-
"""Truncate long strings for logging purposes."""
|
| 23 |
-
if not isinstance(s, str):
|
| 24 |
-
s = str(s)
|
| 25 |
-
return s if len(s) <= n else s[:n] + "... [truncated]"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
def _parse_json_from_text(text: str) -> Optional[Union[List[Dict[str, Any]], Dict[str, Any]]]:
|
| 29 |
"""Best-effort JSON extractor from LLM free-form responses.
|
| 30 |
|
|
@@ -136,7 +129,7 @@ class LLMClient:
|
|
| 136 |
Tạo text từ prompt sử dụng LLM.
|
| 137 |
"""
|
| 138 |
logger.info(
|
| 139 |
-
f"[LLM] generate_text - provider: {self.provider}\n\t prompt: {_safe_truncate(prompt
|
| 140 |
)
|
| 141 |
try:
|
| 142 |
if self.provider == "openai":
|
|
@@ -154,7 +147,7 @@ class LLMClient:
|
|
| 154 |
else:
|
| 155 |
raise ValueError(f"Unsupported provider: {self.provider}")
|
| 156 |
|
| 157 |
-
logger.info(f"[LLM] generate_text - provider: {self.provider}\n\t result: {_safe_truncate(result
|
| 158 |
return result
|
| 159 |
except Exception as e:
|
| 160 |
logger.exception(f"[LLM] Error generating text with {self.provider}: {e}")
|
|
@@ -226,7 +219,7 @@ class LLMClient:
|
|
| 226 |
self._client, endpoint, payload, 3, 500, headers=headers
|
| 227 |
)
|
| 228 |
logger.info(
|
| 229 |
-
f"[LLM] generate_text - provider: {self.provider}\n\t response: {_safe_truncate(str(response)
|
| 230 |
)
|
| 231 |
try:
|
| 232 |
logger.info(
|
|
@@ -323,7 +316,7 @@ class LLMClient:
|
|
| 323 |
return {
|
| 324 |
"category": "unknown",
|
| 325 |
"confidence": 0.0,
|
| 326 |
-
"reasoning": f"Cannot parse JSON from response: {_safe_truncate(response
|
| 327 |
}
|
| 328 |
|
| 329 |
@timing_decorator_async
|
|
@@ -353,7 +346,7 @@ class LLMClient:
|
|
| 353 |
|
| 354 |
try:
|
| 355 |
logger.info(
|
| 356 |
-
f"[LLM][RAW_RESPONSE][extract_entities] {_safe_truncate(response
|
| 357 |
)
|
| 358 |
parsed = _parse_json_from_text(response or "")
|
| 359 |
if isinstance(parsed, list):
|
|
@@ -380,7 +373,7 @@ class LLMClient:
|
|
| 380 |
{{
|
| 381 |
"muc_dich": "...",
|
| 382 |
"phuong_tien": "...",
|
| 383 |
-
"
|
| 384 |
"cau_hoi": "..."
|
| 385 |
}}
|
| 386 |
|
|
@@ -398,7 +391,7 @@ class LLMClient:
|
|
| 398 |
|
| 399 |
**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 "".
|
| 400 |
|
| 401 |
-
**
|
| 402 |
|
| 403 |
**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ý.
|
| 404 |
|
|
@@ -409,7 +402,7 @@ class LLMClient:
|
|
| 409 |
{{
|
| 410 |
"muc_dich": "hỏi về mức phạt",
|
| 411 |
"phuong_tien": "Ô tô",
|
| 412 |
-
"
|
| 413 |
"cau_hoi": "Mức xử phạt cho hành vi ô tô không chấp hành hiệu lệnh của đèn tín hiệu giao thông là bao nhiêu?"
|
| 414 |
}}
|
| 415 |
|
|
@@ -423,7 +416,7 @@ class LLMClient:
|
|
| 423 |
""".strip()
|
| 424 |
|
| 425 |
response = await self.generate_text(prompt, **kwargs)
|
| 426 |
-
logger.info(f"[LLM][RAW][analyze] Kết quả trả về từ generate_text: {_safe_truncate(response
|
| 427 |
|
| 428 |
try:
|
| 429 |
parsed = _parse_json_from_text(response or "")
|
|
|
|
| 15 |
timing_decorator_async,
|
| 16 |
timing_decorator_sync, # kept for compatibility even if unused here
|
| 17 |
call_endpoint_with_retry,
|
| 18 |
+
_safe_truncate
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def _parse_json_from_text(text: str) -> Optional[Union[List[Dict[str, Any]], Dict[str, Any]]]:
|
| 22 |
"""Best-effort JSON extractor from LLM free-form responses.
|
| 23 |
|
|
|
|
| 129 |
Tạo text từ prompt sử dụng LLM.
|
| 130 |
"""
|
| 131 |
logger.info(
|
| 132 |
+
f"[LLM] generate_text - provider: {self.provider}\n\t prompt: {_safe_truncate(prompt)}"
|
| 133 |
)
|
| 134 |
try:
|
| 135 |
if self.provider == "openai":
|
|
|
|
| 147 |
else:
|
| 148 |
raise ValueError(f"Unsupported provider: {self.provider}")
|
| 149 |
|
| 150 |
+
logger.info(f"[LLM] generate_text - provider: {self.provider}\n\t result: {_safe_truncate(result)}")
|
| 151 |
return result
|
| 152 |
except Exception as e:
|
| 153 |
logger.exception(f"[LLM] Error generating text with {self.provider}: {e}")
|
|
|
|
| 219 |
self._client, endpoint, payload, 3, 500, headers=headers
|
| 220 |
)
|
| 221 |
logger.info(
|
| 222 |
+
f"[LLM] generate_text - provider: {self.provider}\n\t response: {_safe_truncate(str(response))}"
|
| 223 |
)
|
| 224 |
try:
|
| 225 |
logger.info(
|
|
|
|
| 316 |
return {
|
| 317 |
"category": "unknown",
|
| 318 |
"confidence": 0.0,
|
| 319 |
+
"reasoning": f"Cannot parse JSON from response: {_safe_truncate(response)}",
|
| 320 |
}
|
| 321 |
|
| 322 |
@timing_decorator_async
|
|
|
|
| 346 |
|
| 347 |
try:
|
| 348 |
logger.info(
|
| 349 |
+
f"[LLM][RAW_RESPONSE][extract_entities] {_safe_truncate(response)}"
|
| 350 |
)
|
| 351 |
parsed = _parse_json_from_text(response or "")
|
| 352 |
if isinstance(parsed, list):
|
|
|
|
| 373 |
{{
|
| 374 |
"muc_dich": "...",
|
| 375 |
"phuong_tien": "...",
|
| 376 |
+
"tu_khoa": "...",
|
| 377 |
"cau_hoi": "..."
|
| 378 |
}}
|
| 379 |
|
|
|
|
| 391 |
|
| 392 |
**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 "".
|
| 393 |
|
| 394 |
+
**tu_khoa**: Là cụm từ hoặc từ khóa ngắn gọn và phù hợp nhất để **tìm kiếm nội dung liên quan đến câu hỏi**. Có thể là tên hành vi vi phạm, thuật ngữ pháp lý, hoặc khái niệm về quy tắc/báo hiệu/vi phạm. Nếu không có thông tin rõ ràng, để chuỗi rỗng "".
|
| 395 |
|
| 396 |
**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ý.
|
| 397 |
|
|
|
|
| 402 |
{{
|
| 403 |
"muc_dich": "hỏi về mức phạt",
|
| 404 |
"phuong_tien": "Ô tô",
|
| 405 |
+
"tu_khoa": "Không chấp hành hiệu lệnh của đèn tín hiệu giao thông",
|
| 406 |
"cau_hoi": "Mức xử phạt cho hành vi ô tô không chấp hành hiệu lệnh của đèn tín hiệu giao thông là bao nhiêu?"
|
| 407 |
}}
|
| 408 |
|
|
|
|
| 416 |
""".strip()
|
| 417 |
|
| 418 |
response = await self.generate_text(prompt, **kwargs)
|
| 419 |
+
logger.info(f"[LLM][RAW][analyze] Kết quả trả về từ generate_text: {_safe_truncate(response)}")
|
| 420 |
|
| 421 |
try:
|
| 422 |
parsed = _parse_json_from_text(response or "")
|
app/message_processor.py
CHANGED
|
@@ -116,17 +116,17 @@ class MessageProcessor:
|
|
| 116 |
logger.info(f"[LLM][RAW] Kết quả trả về từ analyze: {llm_analysis}")
|
| 117 |
|
| 118 |
muc_dich = None
|
| 119 |
-
|
| 120 |
cau_hoi = None
|
| 121 |
if isinstance(llm_analysis, dict):
|
| 122 |
keywords = [self.normalize_vehicle_keyword(llm_analysis.get('phuong_tien', ''))]
|
| 123 |
muc_dich = llm_analysis.get('muc_dich')
|
| 124 |
-
|
| 125 |
cau_hoi = llm_analysis.get('cau_hoi')
|
| 126 |
elif isinstance(llm_analysis, list) and len(llm_analysis) > 0:
|
| 127 |
keywords = [self.normalize_vehicle_keyword(llm_analysis[0].get('phuong_tien', ''))]
|
| 128 |
muc_dich = llm_analysis[0].get('muc_dich')
|
| 129 |
-
|
| 130 |
cau_hoi = llm_analysis[0].get('cau_hoi')
|
| 131 |
else:
|
| 132 |
keywords = extract_keywords(message_text, VEHICLE_KEYWORDS)
|
|
@@ -135,13 +135,13 @@ class MessageProcessor:
|
|
| 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: {
|
| 139 |
|
| 140 |
# Hợp nhất dữ liệu đã phân tích vào `conv`
|
| 141 |
conv['originalcommand'] = command
|
| 142 |
conv['originalcontent'] = remaining_text
|
| 143 |
conv['originalvehicle'] = ','.join(keywords)
|
| 144 |
-
conv['originalaction'] =
|
| 145 |
conv['originalpurpose'] = muc_dich
|
| 146 |
conv['originalquestion'] = cau_hoi or ""
|
| 147 |
|
|
@@ -328,7 +328,7 @@ class MessageProcessor:
|
|
| 328 |
match_count=match_count,
|
| 329 |
user_question=search_query
|
| 330 |
)
|
| 331 |
-
logger.info(f"[DEBUG] matches: {matches}")
|
| 332 |
if matches:
|
| 333 |
response = await self.format_search_results(conversation_context, question or action, matches, page_token, sender_id)
|
| 334 |
else:
|
|
|
|
| 116 |
logger.info(f"[LLM][RAW] Kết quả trả về từ analyze: {llm_analysis}")
|
| 117 |
|
| 118 |
muc_dich = None
|
| 119 |
+
tu_khoa = None
|
| 120 |
cau_hoi = None
|
| 121 |
if isinstance(llm_analysis, dict):
|
| 122 |
keywords = [self.normalize_vehicle_keyword(llm_analysis.get('phuong_tien', ''))]
|
| 123 |
muc_dich = llm_analysis.get('muc_dich')
|
| 124 |
+
tu_khoa = llm_analysis.get('tu_khoa')
|
| 125 |
cau_hoi = llm_analysis.get('cau_hoi')
|
| 126 |
elif isinstance(llm_analysis, list) and len(llm_analysis) > 0:
|
| 127 |
keywords = [self.normalize_vehicle_keyword(llm_analysis[0].get('phuong_tien', ''))]
|
| 128 |
muc_dich = llm_analysis[0].get('muc_dich')
|
| 129 |
+
tu_khoa = llm_analysis[0].get('tu_khoa')
|
| 130 |
cau_hoi = llm_analysis[0].get('cau_hoi')
|
| 131 |
else:
|
| 132 |
keywords = extract_keywords(message_text, VEHICLE_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 |
# Hợp nhất dữ liệu đã phân tích vào `conv`
|
| 141 |
conv['originalcommand'] = command
|
| 142 |
conv['originalcontent'] = remaining_text
|
| 143 |
conv['originalvehicle'] = ','.join(keywords)
|
| 144 |
+
conv['originalaction'] = tu_khoa
|
| 145 |
conv['originalpurpose'] = muc_dich
|
| 146 |
conv['originalquestion'] = cau_hoi or ""
|
| 147 |
|
|
|
|
| 328 |
match_count=match_count,
|
| 329 |
user_question=search_query
|
| 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)
|
| 334 |
else:
|
app/reranker.py
CHANGED
|
@@ -249,5 +249,5 @@ class Reranker:
|
|
| 249 |
# Cache kết quả với system mới
|
| 250 |
self._set_cached_result(cache_key, scored)
|
| 251 |
|
| 252 |
-
logger.info(f"[RERANK] Top reranked docs: {result}")
|
| 253 |
return result
|
|
|
|
| 249 |
# Cache kết quả với system mới
|
| 250 |
self._set_cached_result(cache_key, scored)
|
| 251 |
|
| 252 |
+
logger.info(f"[RERANK] Top reranked docs: {result[:2]}...{result[-2:]}")
|
| 253 |
return result
|
app/sheets.py
CHANGED
|
@@ -198,7 +198,7 @@ class SheetsClient:
|
|
| 198 |
if len(row) > id_col_idx:
|
| 199 |
sheet_conv_id = str(row[id_col_idx]).strip()
|
| 200 |
is_match = sheet_conv_id == target_conv_id
|
| 201 |
-
logger.trace(f"Dòng {i}: So sánh ID: '{sheet_conv_id}' == '{target_conv_id}' -> {is_match}")
|
| 202 |
if is_match:
|
| 203 |
found_row_index = i
|
| 204 |
found_row_data = dict(zip(header, row))
|
|
@@ -215,13 +215,13 @@ class SheetsClient:
|
|
| 215 |
sheet_page_id = str(row[page_col_idx]).strip()
|
| 216 |
|
| 217 |
id_match = (sheet_recipient_id == recipient_id) and (sheet_page_id == page_id)
|
| 218 |
-
logger.trace(f"Dòng {i}: So sánh (user, page): ('{sheet_recipient_id}' == '{recipient_id}') AND ('{sheet_page_id}' == '{page_id}') -> {id_match}")
|
| 219 |
|
| 220 |
if id_match:
|
| 221 |
try:
|
| 222 |
sheet_timestamps = [str(ts).strip() for ts in _flatten_and_unique_timestamps(json.loads(row[timestamp_col_idx]))]
|
| 223 |
ts_match = event_timestamp and event_timestamp in sheet_timestamps
|
| 224 |
-
logger.trace(f"Dòng {i}: So sánh timestamp: '{event_timestamp}' in {sheet_timestamps} -> {ts_match}")
|
| 225 |
if ts_match:
|
| 226 |
found_row_index = i
|
| 227 |
found_row_data = dict(zip(header, row))
|
|
|
|
| 198 |
if len(row) > id_col_idx:
|
| 199 |
sheet_conv_id = str(row[id_col_idx]).strip()
|
| 200 |
is_match = sheet_conv_id == target_conv_id
|
| 201 |
+
# logger.trace(f"Dòng {i}: So sánh ID: '{sheet_conv_id}' == '{target_conv_id}' -> {is_match}")
|
| 202 |
if is_match:
|
| 203 |
found_row_index = i
|
| 204 |
found_row_data = dict(zip(header, row))
|
|
|
|
| 215 |
sheet_page_id = str(row[page_col_idx]).strip()
|
| 216 |
|
| 217 |
id_match = (sheet_recipient_id == recipient_id) and (sheet_page_id == page_id)
|
| 218 |
+
# logger.trace(f"Dòng {i}: So sánh (user, page): ('{sheet_recipient_id}' == '{recipient_id}') AND ('{sheet_page_id}' == '{page_id}') -> {id_match}")
|
| 219 |
|
| 220 |
if id_match:
|
| 221 |
try:
|
| 222 |
sheet_timestamps = [str(ts).strip() for ts in _flatten_and_unique_timestamps(json.loads(row[timestamp_col_idx]))]
|
| 223 |
ts_match = event_timestamp and event_timestamp in sheet_timestamps
|
| 224 |
+
# logger.trace(f"Dòng {i}: So sánh timestamp: '{event_timestamp}' in {sheet_timestamps} -> {ts_match}")
|
| 225 |
if ts_match:
|
| 226 |
found_row_index = i
|
| 227 |
found_row_data = dict(zip(header, row))
|
app/supabase_db.py
CHANGED
|
@@ -71,7 +71,7 @@ class SupabaseClient:
|
|
| 71 |
words = cleaned_text.split()
|
| 72 |
or_query_tsquery = " ".join([word for word in words if word not in VIETNAMESE_STOP_WORDS])
|
| 73 |
logger.info(f"[DEBUG][RPC]: or_query_tsquery: {or_query_tsquery}")
|
| 74 |
-
logger.info(f"[DEBUG][RPC]: embedding: {embedding}")
|
| 75 |
|
| 76 |
try:
|
| 77 |
payload = {
|
|
|
|
| 71 |
words = cleaned_text.split()
|
| 72 |
or_query_tsquery = " ".join([word for word in words if word not in VIETNAMESE_STOP_WORDS])
|
| 73 |
logger.info(f"[DEBUG][RPC]: or_query_tsquery: {or_query_tsquery}")
|
| 74 |
+
logger.info(f"[DEBUG][RPC]: embedding: {embedding[:5]}...{embedding[-5:]}")
|
| 75 |
|
| 76 |
try:
|
| 77 |
payload = {
|
app/utils.py
CHANGED
|
@@ -157,4 +157,47 @@ def get_random_message(message_list: List[str]) -> str:
|
|
| 157 |
if not message_list:
|
| 158 |
return "Đang xử lý..."
|
| 159 |
|
| 160 |
-
return random.choice(message_list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
if not message_list:
|
| 158 |
return "Đang xử lý..."
|
| 159 |
|
| 160 |
+
return random.choice(message_list)
|
| 161 |
+
|
| 162 |
+
def _safe_truncate(
|
| 163 |
+
s: str,
|
| 164 |
+
nguong_a: int = 100,
|
| 165 |
+
do_dai_x: int = 50,
|
| 166 |
+
do_dai_y: int = 100
|
| 167 |
+
) -> str:
|
| 168 |
+
"""
|
| 169 |
+
Cắt chuỗi một cách thông minh dựa trên độ dài của nó.
|
| 170 |
+
|
| 171 |
+
- Nếu độ dài chuỗi < nguong_a: chỉ hiển thị `do_dai_x` ký tự đầu tiên.
|
| 172 |
+
- Nếu độ dài chuỗi >= nguong_a: hiển thị `do_dai_y` ký tự đầu và `do_dai_y` ký tự cuối.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
s: Chuỗi đầu vào cần xử lý.
|
| 176 |
+
nguong_a (A): Ngưỡng độ dài để quyết định logic cắt chuỗi.
|
| 177 |
+
do_dai_x (X): Số ký tự đầu tiên cần hiển thị cho chuỗi ngắn.
|
| 178 |
+
do_dai_y (Y): Số ký tự đầu/cuối cần hiển thị cho chuỗi dài.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
Chuỗi đã được cắt ngắn theo quy tắc.
|
| 182 |
+
"""
|
| 183 |
+
if not isinstance(s, str):
|
| 184 |
+
s = str(s)
|
| 185 |
+
|
| 186 |
+
s_len = len(s)
|
| 187 |
+
|
| 188 |
+
# --- Trường hợp 1: Độ dài chuỗi NGẮN HƠN ngưỡng A ---
|
| 189 |
+
if s_len < nguong_a:
|
| 190 |
+
# Nếu chuỗi đã ngắn hơn hoặc bằng X, trả về nguyên bản
|
| 191 |
+
if s_len <= do_dai_x:
|
| 192 |
+
return s
|
| 193 |
+
# Nếu không, cắt lấy X ký tự đầu
|
| 194 |
+
return f"{s[:do_dai_x]}... [đã cắt]"
|
| 195 |
+
|
| 196 |
+
# --- Trường hợp 2: Độ dài chuỗi DÀI HƠN hoặc BẰNG ngưỡng A ---
|
| 197 |
+
else:
|
| 198 |
+
# Nếu việc lấy Y ký tự đầu và Y cuối sẽ bao trọn cả chuỗi (2*Y >= s_len)
|
| 199 |
+
# thì không cần cắt để tránh hiển thị trùng lặp.
|
| 200 |
+
if s_len <= do_dai_y * 2:
|
| 201 |
+
return s
|
| 202 |
+
# Ngược lại, lấy Y ký tự đầu và Y ký tự cuối
|
| 203 |
+
return f"{s[:do_dai_y]}... [đã cắt] ...{s[-do_dai_y:]}"
|