quick fix timestamp
Browse files- app/sheets.py +134 -86
app/sheets.py
CHANGED
|
@@ -16,20 +16,22 @@ from .constants import SHEET_RANGE
|
|
| 16 |
|
| 17 |
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
|
| 18 |
|
| 19 |
-
def generate_conversation_id(user_id: str, page_id: str, timestamp:
|
| 20 |
"""Tạo ID hội thoại duy nhất."""
|
| 21 |
hash_input = f"{user_id}:{page_id}:{timestamp}"
|
| 22 |
return hashlib.sha256(hash_input.encode()).hexdigest()[:32]
|
| 23 |
|
| 24 |
-
|
|
|
|
| 25 |
"""
|
| 26 |
-
Hàm tiện ích để làm phẳng danh sách timestamp
|
| 27 |
-
|
| 28 |
"""
|
| 29 |
-
flat_list = []
|
| 30 |
if not isinstance(items, list):
|
|
|
|
| 31 |
return [items]
|
| 32 |
|
|
|
|
| 33 |
for item in items:
|
| 34 |
if isinstance(item, list):
|
| 35 |
flat_list.extend(_flatten_and_unique_timestamps(item))
|
|
@@ -70,118 +72,164 @@ class SheetsClient:
|
|
| 70 |
|
| 71 |
@timing_decorator_sync
|
| 72 |
def get_conversation_history(self, user_id: str, page_id: str) -> List[Dict[str, Any]]:
|
| 73 |
-
"""Lấy lịch sử hội thoại từ sheet
|
| 74 |
try:
|
| 75 |
if not self.service:
|
| 76 |
self.authenticate()
|
| 77 |
-
|
| 78 |
result = self.service.spreadsheets().values().get(
|
| 79 |
spreadsheetId=self.sheet_id,
|
| 80 |
-
range=
|
| 81 |
).execute()
|
| 82 |
values = result.get('values', [])
|
| 83 |
-
|
|
|
|
| 84 |
return []
|
| 85 |
-
|
| 86 |
header = values[0]
|
| 87 |
-
|
| 88 |
-
for row in values[1:]: # Bỏ qua dòng header
|
| 89 |
-
# Đảm bảo row có đủ cột
|
| 90 |
row_data = dict(zip(header, row + [""] * (len(header) - len(row))))
|
| 91 |
|
| 92 |
if row_data.get('recipient_id') == user_id and row_data.get('page_id') == page_id:
|
| 93 |
-
# Cải tiến: Xử lý an toàn việc đọc timestamp
|
| 94 |
try:
|
| 95 |
-
|
| 96 |
-
timestamps = _flatten_and_unique_timestamps(
|
| 97 |
except (json.JSONDecodeError, TypeError):
|
| 98 |
timestamps = []
|
| 99 |
-
|
| 100 |
row_data['timestamp'] = timestamps
|
| 101 |
row_data['originalattachments'] = json.loads(row_data.get('originalattachments', '[]'))
|
| 102 |
row_data['isdone'] = str(row_data.get('isdone', 'false')).lower() == 'true'
|
| 103 |
history.append(row_data)
|
| 104 |
return history
|
| 105 |
except Exception as e:
|
| 106 |
-
logger.error(f"
|
| 107 |
return []
|
| 108 |
|
| 109 |
@timing_decorator_sync
|
| 110 |
-
def log_conversation(
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
try:
|
| 117 |
if not self.service:
|
| 118 |
self.authenticate()
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
| 123 |
header = values[0] if values else []
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
if row and row[0] == conversation_id:
|
| 135 |
-
row_index_to_update = i
|
| 136 |
-
break
|
| 137 |
-
|
| 138 |
-
# Làm phẳng và đảm bảo timestamp là một list duy nhất
|
| 139 |
-
timestamps_list = _flatten_and_unique_timestamps(kwargs.get('timestamp', []))
|
| 140 |
-
kwargs['timestamp'] = timestamps_list # Cập nhật lại kwargs
|
| 141 |
-
|
| 142 |
-
# Chuẩn bị dữ liệu cho dòng mới/cập nhật
|
| 143 |
-
row_data = [str(kwargs.get(h, '')) for h in header]
|
| 144 |
-
# Chuyển đổi các trường đặc biệt sang JSON string
|
| 145 |
-
row_data[header.index('originalattachments')] = json.dumps(kwargs.get('originalattachments', []))
|
| 146 |
-
row_data[header.index('timestamp')] = json.dumps(timestamps_list)
|
| 147 |
-
row_data[header.index('isdone')] = str(kwargs.get('isdone', False)).lower()
|
| 148 |
-
|
| 149 |
-
if row_index_to_update != -1:
|
| 150 |
-
# --- CẬP NHẬT DÒNG HIỆN CÓ ---
|
| 151 |
-
logger.info(f"Đang cập nhật conversation: {conversation_id}")
|
| 152 |
-
range_to_update = f"A{row_index_to_update}"
|
| 153 |
-
body = {'values': [row_data]}
|
| 154 |
-
self.service.spreadsheets().values().update(
|
| 155 |
-
spreadsheetId=self.sheet_id,
|
| 156 |
-
range=range_to_update,
|
| 157 |
-
valueInputOption='RAW',
|
| 158 |
-
body=body
|
| 159 |
-
).execute()
|
| 160 |
else:
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
except Exception as e:
|
| 186 |
-
logger.error(f"
|
| 187 |
return None
|
|
|
|
| 16 |
|
| 17 |
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
|
| 18 |
|
| 19 |
+
def generate_conversation_id(user_id: str, page_id: str, timestamp: str) -> str:
|
| 20 |
"""Tạo ID hội thoại duy nhất."""
|
| 21 |
hash_input = f"{user_id}:{page_id}:{timestamp}"
|
| 22 |
return hashlib.sha256(hash_input.encode()).hexdigest()[:32]
|
| 23 |
|
| 24 |
+
# --- HÀM TIỆN ÍCH ĐƯỢC THÊM VÀO ĐỂ FIX LỖI ---
|
| 25 |
+
def _flatten_and_unique_timestamps(items: Any) -> List[Any]:
|
| 26 |
"""
|
| 27 |
+
Hàm tiện ích để làm phẳng danh sách timestamp (xử lý list lồng nhau)
|
| 28 |
+
và loại bỏ các giá trị trùng lặp, giữ nguyên thứ tự.
|
| 29 |
"""
|
|
|
|
| 30 |
if not isinstance(items, list):
|
| 31 |
+
# Nếu đầu vào không phải list, gói nó vào một list
|
| 32 |
return [items]
|
| 33 |
|
| 34 |
+
flat_list = []
|
| 35 |
for item in items:
|
| 36 |
if isinstance(item, list):
|
| 37 |
flat_list.extend(_flatten_and_unique_timestamps(item))
|
|
|
|
| 72 |
|
| 73 |
@timing_decorator_sync
|
| 74 |
def get_conversation_history(self, user_id: str, page_id: str) -> List[Dict[str, Any]]:
|
| 75 |
+
"""Lấy lịch sử hội thoại từ sheet."""
|
| 76 |
try:
|
| 77 |
if not self.service:
|
| 78 |
self.authenticate()
|
| 79 |
+
range_name = SHEET_RANGE
|
| 80 |
result = self.service.spreadsheets().values().get(
|
| 81 |
spreadsheetId=self.sheet_id,
|
| 82 |
+
range=range_name
|
| 83 |
).execute()
|
| 84 |
values = result.get('values', [])
|
| 85 |
+
history = []
|
| 86 |
+
if not values or len(values) < 1:
|
| 87 |
return []
|
| 88 |
+
|
| 89 |
header = values[0]
|
| 90 |
+
for row in values[1:]:
|
|
|
|
|
|
|
| 91 |
row_data = dict(zip(header, row + [""] * (len(header) - len(row))))
|
| 92 |
|
| 93 |
if row_data.get('recipient_id') == user_id and row_data.get('page_id') == page_id:
|
|
|
|
| 94 |
try:
|
| 95 |
+
# Dùng hàm tiện ích để đảm bảo timestamp đọc ra luôn phẳng
|
| 96 |
+
timestamps = _flatten_and_unique_timestamps(json.loads(row_data.get('timestamp', '[]')))
|
| 97 |
except (json.JSONDecodeError, TypeError):
|
| 98 |
timestamps = []
|
| 99 |
+
|
| 100 |
row_data['timestamp'] = timestamps
|
| 101 |
row_data['originalattachments'] = json.loads(row_data.get('originalattachments', '[]'))
|
| 102 |
row_data['isdone'] = str(row_data.get('isdone', 'false')).lower() == 'true'
|
| 103 |
history.append(row_data)
|
| 104 |
return history
|
| 105 |
except Exception as e:
|
| 106 |
+
logger.error(f"Error getting conversation history: {e}")
|
| 107 |
return []
|
| 108 |
|
| 109 |
@timing_decorator_sync
|
| 110 |
+
def log_conversation(
|
| 111 |
+
self,
|
| 112 |
+
conversation_id: str,
|
| 113 |
+
recipient_id: str,
|
| 114 |
+
page_id: str,
|
| 115 |
+
originaltext: str = "",
|
| 116 |
+
originalcommand: str = "",
|
| 117 |
+
originalcontent: str = "",
|
| 118 |
+
originalattachments: Optional[List[str]] = None,
|
| 119 |
+
originalvehicle: str = "",
|
| 120 |
+
originalaction: str = "",
|
| 121 |
+
originalpurpose: str = "",
|
| 122 |
+
originalquestion: str = "",
|
| 123 |
+
systemresponse: str = "",
|
| 124 |
+
timestamp: Optional[Any] = None, # Cho phép timestamp có thể là list
|
| 125 |
+
isdone: bool = False
|
| 126 |
+
) -> Optional[Dict[str, Any]]:
|
| 127 |
+
"""Ghi lại một hội thoại, giữ nguyên logic gốc của người dùng."""
|
| 128 |
try:
|
| 129 |
if not self.service:
|
| 130 |
self.authenticate()
|
| 131 |
|
| 132 |
+
result = self.service.spreadsheets().values().get(
|
| 133 |
+
spreadsheetId=self.sheet_id,
|
| 134 |
+
range=SHEET_RANGE
|
| 135 |
+
).execute()
|
| 136 |
+
values = result.get('values', [])
|
| 137 |
header = values[0] if values else []
|
| 138 |
+
|
| 139 |
+
# --- SỬA LỖI LỒNG TIMESTAMP TẠI ĐÂY ---
|
| 140 |
+
# 1. Luôn đảm bảo `timestamps_list` là một danh sách phẳng, không lồng nhau.
|
| 141 |
+
timestamps_list = _flatten_and_unique_timestamps(timestamp or [])
|
| 142 |
+
|
| 143 |
+
# 2. Xác định timestamp cho sự kiện hiện tại (dùng để kiểm tra trùng lặp).
|
| 144 |
+
# Lấy timestamp mới nhất (cuối cùng trong list). Nếu list rỗng thì tạo mới.
|
| 145 |
+
if not timestamps_list:
|
| 146 |
+
current_ts = datetime.now().isoformat()
|
| 147 |
+
timestamps_list.append(current_ts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
else:
|
| 149 |
+
current_ts = timestamps_list[-1]
|
| 150 |
+
# --- KẾT THÚC SỬA LỖI ---
|
| 151 |
+
|
| 152 |
+
# Logic kiểm tra trùng lặp của bạn được giữ nguyên
|
| 153 |
+
if header:
|
| 154 |
+
for row in values[1:]:
|
| 155 |
+
row_data = dict(zip(header, row + [""] * (len(header) - len(row))))
|
| 156 |
+
try:
|
| 157 |
+
row_timestamps = json.loads(row_data.get('timestamp', '[]'))
|
| 158 |
+
if not isinstance(row_timestamps, list):
|
| 159 |
+
row_timestamps = [row_timestamps]
|
| 160 |
+
except Exception:
|
| 161 |
+
row_timestamps = []
|
| 162 |
+
|
| 163 |
+
if str(current_ts) in [str(ts) for ts in row_timestamps] and \
|
| 164 |
+
row_data.get('recipient_id') == str(recipient_id) and \
|
| 165 |
+
row_data.get('page_id') == str(page_id):
|
| 166 |
+
logger.info(f"Found duplicate conversation for user {recipient_id}, page {page_id}, timestamp {current_ts}")
|
| 167 |
+
# Trả về dữ liệu tìm thấy, logic không đổi
|
| 168 |
+
return {
|
| 169 |
+
'conversation_id': row_data.get('conversation_id'),
|
| 170 |
+
'originalcommand': row_data.get('originalcommand'),
|
| 171 |
+
'originalcontent': row_data.get('originalcontent'),
|
| 172 |
+
'originalattachments': json.loads(row_data.get('originalattachments', '[]')),
|
| 173 |
+
'recipient_id': row_data.get('recipient_id'),
|
| 174 |
+
'page_id': row_data.get('page_id'),
|
| 175 |
+
'originaltext': row_data.get('originaltext'),
|
| 176 |
+
'originalvehicle': row_data.get('originalvehicle'),
|
| 177 |
+
'originalaction': row_data.get('originalaction'),
|
| 178 |
+
'originalpurpose': row_data.get('originalpurpose'),
|
| 179 |
+
'originalquestion': row_data.get('originalquestion'),
|
| 180 |
+
'systemresponse': row_data.get('systemresponse'),
|
| 181 |
+
'timestamp': row_timestamps,
|
| 182 |
+
'isdone': str(row_data.get('isdone', 'false')).lower() == 'true'
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
# Logic tạo dòng mới và append vào sheet được giữ nguyên
|
| 186 |
+
if not conversation_id:
|
| 187 |
+
conversation_id = generate_conversation_id(recipient_id, page_id, current_ts)
|
| 188 |
|
| 189 |
+
new_row = [
|
| 190 |
+
conversation_id,
|
| 191 |
+
originalcommand,
|
| 192 |
+
originalcontent,
|
| 193 |
+
json.dumps(originalattachments or []),
|
| 194 |
+
recipient_id,
|
| 195 |
+
page_id,
|
| 196 |
+
originaltext,
|
| 197 |
+
originalvehicle,
|
| 198 |
+
originalaction,
|
| 199 |
+
originalpurpose,
|
| 200 |
+
originalquestion,
|
| 201 |
+
systemresponse,
|
| 202 |
+
json.dumps(timestamps_list), # Lưu danh sách đã được làm phẳng
|
| 203 |
+
str(isdone).lower()
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
body = {'values': [new_row]}
|
| 207 |
+
self.service.spreadsheets().values().append(
|
| 208 |
+
spreadsheetId=self.sheet_id,
|
| 209 |
+
range=SHEET_RANGE,
|
| 210 |
+
valueInputOption='RAW',
|
| 211 |
+
body=body
|
| 212 |
+
).execute()
|
| 213 |
|
| 214 |
+
logger.info(f"Thêm mới conversation: {conversation_id}")
|
| 215 |
+
|
| 216 |
+
# Trả về dữ liệu đã được ghi, logic không đổi
|
| 217 |
+
return {
|
| 218 |
+
'conversation_id': conversation_id,
|
| 219 |
+
'originalcommand': originalcommand,
|
| 220 |
+
'originalcontent': originalcontent,
|
| 221 |
+
'originalattachments': originalattachments or [],
|
| 222 |
+
'recipient_id': recipient_id,
|
| 223 |
+
'page_id': page_id,
|
| 224 |
+
'originaltext': originaltext,
|
| 225 |
+
'originalvehicle': originalvehicle,
|
| 226 |
+
'originalaction': originalaction,
|
| 227 |
+
'originalpurpose': originalpurpose,
|
| 228 |
+
'originalquestion': originalquestion,
|
| 229 |
+
'systemresponse': systemresponse,
|
| 230 |
+
'timestamp': timestamps_list, # Trả về danh sách đã được làm phẳng
|
| 231 |
+
'isdone': isdone
|
| 232 |
+
}
|
| 233 |
except Exception as e:
|
| 234 |
+
logger.error(f"Error logging conversation: {e}", exc_info=True)
|
| 235 |
return None
|