update content_summary in chunking
Browse files- app/config.py +1 -1
- app/law_document_chunker.py +110 -10
- app/llm.py +2 -0
- app/main.py +1 -3
- app/supabase_db.py +2 -3
app/config.py
CHANGED
|
@@ -40,7 +40,7 @@ class Settings(BaseSettings):
|
|
| 40 |
|
| 41 |
# LLM (chat/completion) provider/model
|
| 42 |
llm_provider: str = os.getenv("LLM_PROVIDER", "gemini") or ""
|
| 43 |
-
llm_model: str = os.getenv("LLM_MODEL", "gemini-
|
| 44 |
# Embedding provider/model
|
| 45 |
embedding_provider: str = os.getenv("EMBEDDING_PROVIDER", "gemini") or ""
|
| 46 |
embedding_model: str = os.getenv("EMBEDDING_MODEL", "models/embedding-001") or ""
|
|
|
|
| 40 |
|
| 41 |
# LLM (chat/completion) provider/model
|
| 42 |
llm_provider: str = os.getenv("LLM_PROVIDER", "gemini") or ""
|
| 43 |
+
llm_model: str = os.getenv("LLM_MODEL", "gemini-2.5-flash") or ""
|
| 44 |
# Embedding provider/model
|
| 45 |
embedding_provider: str = os.getenv("EMBEDDING_PROVIDER", "gemini") or ""
|
| 46 |
embedding_model: str = os.getenv("EMBEDDING_MODEL", "models/embedding-001") or ""
|
app/law_document_chunker.py
CHANGED
|
@@ -133,7 +133,7 @@ class LawDocumentChunker:
|
|
| 133 |
|
| 134 |
def _create_chunk_metadata(self, content: str, level: str, level_value: Optional[str],
|
| 135 |
parent_id: Optional[str], vanbanid: int,
|
| 136 |
-
document_title: str) -> ChunkMetadata:
|
| 137 |
"""Tạo metadata cho chunk."""
|
| 138 |
chunk_id = str(uuid.uuid4())
|
| 139 |
|
|
@@ -145,17 +145,112 @@ class LawDocumentChunker:
|
|
| 145 |
document_title=document_title
|
| 146 |
)
|
| 147 |
|
| 148 |
-
# Điền metadata
|
| 149 |
if level == "DIEU" and level_value:
|
| 150 |
metadata.article_number = int(level_value) if level_value.isdigit() else None
|
| 151 |
-
metadata.article_title = content.strip()
|
| 152 |
elif level == "KHOAN" and level_value:
|
| 153 |
metadata.clause_number = level_value
|
| 154 |
elif level == "DIEM" and level_value:
|
| 155 |
metadata.sub_clause_letter = level_value
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
return metadata
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
def _split_into_chunks(self, text: str, chunk_size: int, overlap: int) -> List[str]:
|
| 160 |
"""Chia text thành các chunk với overlap."""
|
| 161 |
chunks = []
|
|
@@ -228,7 +323,8 @@ class LawDocumentChunker:
|
|
| 228 |
current_level_value,
|
| 229 |
current_parent,
|
| 230 |
vanbanid,
|
| 231 |
-
document_title
|
|
|
|
| 232 |
)
|
| 233 |
chunks.append(metadata)
|
| 234 |
|
|
@@ -260,7 +356,8 @@ class LawDocumentChunker:
|
|
| 260 |
current_level_value,
|
| 261 |
current_parent,
|
| 262 |
vanbanid,
|
| 263 |
-
document_title
|
|
|
|
| 264 |
)
|
| 265 |
chunks.append(metadata)
|
| 266 |
|
|
@@ -278,7 +375,8 @@ class LawDocumentChunker:
|
|
| 278 |
current_level_value,
|
| 279 |
current_parent,
|
| 280 |
vanbanid,
|
| 281 |
-
document_title
|
|
|
|
| 282 |
)
|
| 283 |
chunks.append(metadata)
|
| 284 |
|
|
@@ -322,9 +420,11 @@ class LawDocumentChunker:
|
|
| 322 |
|
| 323 |
for i, chunk in enumerate(chunks, 1):
|
| 324 |
try:
|
| 325 |
-
# Tạo embedding
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
| 328 |
|
| 329 |
# Chuẩn bị data cho Supabase
|
| 330 |
chunk_dict = {
|
|
@@ -338,7 +438,7 @@ class LawDocumentChunker:
|
|
| 338 |
'article_title': chunk.article_title,
|
| 339 |
'clause_number': chunk.clause_number,
|
| 340 |
'sub_clause_letter': chunk.sub_clause_letter,
|
| 341 |
-
'context_summary':
|
| 342 |
}
|
| 343 |
|
| 344 |
# Lưu ngay lập tức vào Supabase
|
|
|
|
| 133 |
|
| 134 |
def _create_chunk_metadata(self, content: str, level: str, level_value: Optional[str],
|
| 135 |
parent_id: Optional[str], vanbanid: int,
|
| 136 |
+
document_title: str, chunk_stack: List[Tuple[str, str, Optional[str]]] = []) -> ChunkMetadata:
|
| 137 |
"""Tạo metadata cho chunk."""
|
| 138 |
chunk_id = str(uuid.uuid4())
|
| 139 |
|
|
|
|
| 145 |
document_title=document_title
|
| 146 |
)
|
| 147 |
|
| 148 |
+
# Điền metadata từ chunk hiện tại
|
| 149 |
if level == "DIEU" and level_value:
|
| 150 |
metadata.article_number = int(level_value) if level_value.isdigit() else None
|
| 151 |
+
metadata.article_title = content.split('\n')[0].strip() if content else ""
|
| 152 |
elif level == "KHOAN" and level_value:
|
| 153 |
metadata.clause_number = level_value
|
| 154 |
elif level == "DIEM" and level_value:
|
| 155 |
metadata.sub_clause_letter = level_value
|
| 156 |
|
| 157 |
+
# Điền metadata từ parent chunks nếu có
|
| 158 |
+
if chunk_stack and parent_id:
|
| 159 |
+
self._fill_metadata_from_parents(metadata, chunk_stack, parent_id)
|
| 160 |
+
|
| 161 |
return metadata
|
| 162 |
|
| 163 |
+
def _fill_metadata_from_parents(self, metadata: ChunkMetadata, chunk_stack: List[Tuple[str, str, Optional[str]]], parent_id: str):
|
| 164 |
+
"""
|
| 165 |
+
Điền metadata từ parent chunks (Điều, Khoản) nếu chunk hiện tại có cha hoặc ông là Điều/Khoản.
|
| 166 |
+
"""
|
| 167 |
+
# Tìm tất cả parent chunks
|
| 168 |
+
parent_chunks = []
|
| 169 |
+
current_parent = parent_id
|
| 170 |
+
|
| 171 |
+
# Tìm tất cả parents trong stack
|
| 172 |
+
for chunk_id, level, level_value in reversed(chunk_stack):
|
| 173 |
+
if chunk_id == current_parent:
|
| 174 |
+
parent_chunks.append((level, level_value))
|
| 175 |
+
# Tìm parent của parent này
|
| 176 |
+
for parent_chunk_id, parent_level, parent_level_value in reversed(chunk_stack):
|
| 177 |
+
if parent_chunk_id == chunk_id:
|
| 178 |
+
current_parent = parent_chunk_id
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
# Điền metadata từ parents
|
| 182 |
+
for level, level_value in parent_chunks:
|
| 183 |
+
if level == "DIEU" and level_value:
|
| 184 |
+
if not metadata.article_number: # Chỉ điền nếu chưa có
|
| 185 |
+
metadata.article_number = int(level_value) if level_value.isdigit() else None
|
| 186 |
+
if not metadata.article_title: # Chỉ điền nếu chưa có
|
| 187 |
+
# Lấy title từ content của parent chunk
|
| 188 |
+
for chunk_id, parent_level, parent_level_value in chunk_stack:
|
| 189 |
+
if chunk_id == parent_id and parent_level == "DIEU":
|
| 190 |
+
# Tìm content của parent chunk này
|
| 191 |
+
# (Cần truyền content của parent vào đây)
|
| 192 |
+
break
|
| 193 |
+
elif level == "KHOAN" and level_value:
|
| 194 |
+
if not metadata.clause_number: # Chỉ điền nếu chưa có
|
| 195 |
+
metadata.clause_number = level_value
|
| 196 |
+
elif level == "DIEM" and level_value:
|
| 197 |
+
if not metadata.sub_clause_letter: # Chỉ điền nếu chưa có
|
| 198 |
+
metadata.sub_clause_letter = level_value
|
| 199 |
+
|
| 200 |
+
async def _create_context_summary_with_llm(self, content: str, metadata: ChunkMetadata) -> str:
|
| 201 |
+
"""
|
| 202 |
+
Tạo context_summary bằng LLM theo format: "Structure: LEVEL | Chủ đề: Semantic: SUMMARY"
|
| 203 |
+
"""
|
| 204 |
+
try:
|
| 205 |
+
# Tạo LEVEL từ metadata
|
| 206 |
+
level_parts = []
|
| 207 |
+
if metadata.sub_clause_letter:
|
| 208 |
+
level_parts.append(f"Điểm {metadata.sub_clause_letter}")
|
| 209 |
+
if metadata.clause_number:
|
| 210 |
+
level_parts.append(f"Khoản {metadata.clause_number}")
|
| 211 |
+
if metadata.article_number:
|
| 212 |
+
level_parts.append(f"Điều {metadata.article_number}")
|
| 213 |
+
|
| 214 |
+
level = " ".join(reversed(level_parts)) if level_parts else "Nội dung"
|
| 215 |
+
|
| 216 |
+
# Gọi LLM để tóm tắt chủ đề
|
| 217 |
+
summary_prompt = f"""
|
| 218 |
+
Tóm tắt ngắn gọn chủ đề chính của đoạn văn bản sau trong 1-2 câu:
|
| 219 |
+
|
| 220 |
+
{content[:500]}...
|
| 221 |
+
|
| 222 |
+
Trả về chỉ nội dung tóm tắt, không có thêm text nào khác.
|
| 223 |
+
"""
|
| 224 |
+
|
| 225 |
+
# Sử dụng GeminiClient với RequestLimitManager
|
| 226 |
+
from .gemini_client import GeminiClient
|
| 227 |
+
|
| 228 |
+
gemini_client = GeminiClient()
|
| 229 |
+
summary_response = gemini_client.generate_text(
|
| 230 |
+
prompt=summary_prompt
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
summary = summary_response.strip() if summary_response else "Không có tóm tắt"
|
| 234 |
+
|
| 235 |
+
# Tạo context_summary theo format yêu cầu
|
| 236 |
+
context_summary = f"Structure: {level} | Chủ đề: Semantic: {summary}"
|
| 237 |
+
|
| 238 |
+
return context_summary
|
| 239 |
+
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logger.error(f"[CHUNKER] Error creating context_summary with LLM: {e}")
|
| 242 |
+
# Fallback nếu LLM lỗi
|
| 243 |
+
level_parts = []
|
| 244 |
+
if metadata.sub_clause_letter:
|
| 245 |
+
level_parts.append(f"Điểm {metadata.sub_clause_letter}")
|
| 246 |
+
if metadata.clause_number:
|
| 247 |
+
level_parts.append(f"Khoản {metadata.clause_number}")
|
| 248 |
+
if metadata.article_number:
|
| 249 |
+
level_parts.append(f"Điều {metadata.article_number}")
|
| 250 |
+
|
| 251 |
+
level = " ".join(reversed(level_parts)) if level_parts else "Nội dung"
|
| 252 |
+
return f"Structure: {level} | Chủ đề: Semantic: Không có tóm tắt"
|
| 253 |
+
|
| 254 |
def _split_into_chunks(self, text: str, chunk_size: int, overlap: int) -> List[str]:
|
| 255 |
"""Chia text thành các chunk với overlap."""
|
| 256 |
chunks = []
|
|
|
|
| 323 |
current_level_value,
|
| 324 |
current_parent,
|
| 325 |
vanbanid,
|
| 326 |
+
document_title,
|
| 327 |
+
chunk_stack
|
| 328 |
)
|
| 329 |
chunks.append(metadata)
|
| 330 |
|
|
|
|
| 356 |
current_level_value,
|
| 357 |
current_parent,
|
| 358 |
vanbanid,
|
| 359 |
+
document_title,
|
| 360 |
+
chunk_stack
|
| 361 |
)
|
| 362 |
chunks.append(metadata)
|
| 363 |
|
|
|
|
| 375 |
current_level_value,
|
| 376 |
current_parent,
|
| 377 |
vanbanid,
|
| 378 |
+
document_title,
|
| 379 |
+
chunk_stack
|
| 380 |
)
|
| 381 |
chunks.append(metadata)
|
| 382 |
|
|
|
|
| 420 |
|
| 421 |
for i, chunk in enumerate(chunks, 1):
|
| 422 |
try:
|
| 423 |
+
# Tạo embedding
|
| 424 |
+
embedding = await self.embedding_client.create_embedding(chunk.content)
|
| 425 |
+
|
| 426 |
+
# Tạo context_summary bằng LLM
|
| 427 |
+
context_summary = await self._create_context_summary_with_llm(chunk.content, chunk)
|
| 428 |
|
| 429 |
# Chuẩn bị data cho Supabase
|
| 430 |
chunk_dict = {
|
|
|
|
| 438 |
'article_title': chunk.article_title,
|
| 439 |
'clause_number': chunk.clause_number,
|
| 440 |
'sub_clause_letter': chunk.sub_clause_letter,
|
| 441 |
+
'context_summary': context_summary
|
| 442 |
}
|
| 443 |
|
| 444 |
# Lưu ngay lập tức vào Supabase
|
app/llm.py
CHANGED
|
@@ -88,7 +88,9 @@ class LLMClient:
|
|
| 88 |
|
| 89 |
def _setup_gemini(self, config: Dict[str, Any]):
|
| 90 |
"""Cấu hình cho Gemini."""
|
|
|
|
| 91 |
self.gemini_client = GeminiClient()
|
|
|
|
| 92 |
|
| 93 |
@timing_decorator_async
|
| 94 |
async def generate_text(
|
|
|
|
| 88 |
|
| 89 |
def _setup_gemini(self, config: Dict[str, Any]):
|
| 90 |
"""Cấu hình cho Gemini."""
|
| 91 |
+
# Sử dụng GeminiClient với RequestLimitManager
|
| 92 |
self.gemini_client = GeminiClient()
|
| 93 |
+
logger.info("[LLM] Initialized GeminiClient with RequestLimitManager")
|
| 94 |
|
| 95 |
@timing_decorator_async
|
| 96 |
async def generate_text(
|
app/main.py
CHANGED
|
@@ -369,9 +369,7 @@ async def process_business_logic(log_kwargs: Dict[str, Any], page_token: str) ->
|
|
| 369 |
# Có thông tin phương tiện
|
| 370 |
if action:
|
| 371 |
logger.info(f"[DEBUG] tạo embedding: {action}")
|
| 372 |
-
|
| 373 |
-
# embedding = await embedding_client.create_embedding(action)
|
| 374 |
-
embedding = [0.0] * 768 # Placeholder embedding cho test
|
| 375 |
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 376 |
matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
|
| 377 |
logger.info(f"[DEBUG] matches: {matches}")
|
|
|
|
| 369 |
# Có thông tin phương tiện
|
| 370 |
if action:
|
| 371 |
logger.info(f"[DEBUG] tạo embedding: {action}")
|
| 372 |
+
embedding = await embedding_client.create_embedding(action)
|
|
|
|
|
|
|
| 373 |
logger.info(f"[DEBUG] embedding: {embedding[:5]} ... (total {len(embedding)})")
|
| 374 |
matches = supabase_client.match_documents(embedding, vehicle_keywords=keywords)
|
| 375 |
logger.info(f"[DEBUG] matches: {matches}")
|
app/supabase_db.py
CHANGED
|
@@ -89,10 +89,9 @@ class SupabaseClient:
|
|
| 89 |
# Xử lý các giá trị null/empty cho integer fields
|
| 90 |
processed_data = chunk_data.copy()
|
| 91 |
|
| 92 |
-
#
|
| 93 |
if 'embedding' in processed_data:
|
| 94 |
-
|
| 95 |
-
del processed_data['embedding'] # Xóa embedding để test
|
| 96 |
|
| 97 |
# Xử lý article_number - chỉ gửi nếu có giá trị hợp lệ
|
| 98 |
if 'article_number' in processed_data:
|
|
|
|
| 89 |
# Xử lý các giá trị null/empty cho integer fields
|
| 90 |
processed_data = chunk_data.copy()
|
| 91 |
|
| 92 |
+
# Giữ lại embedding để lưu vào database
|
| 93 |
if 'embedding' in processed_data:
|
| 94 |
+
processed_data['embedding'] = processed_data['embedding']
|
|
|
|
| 95 |
|
| 96 |
# Xử lý article_number - chỉ gửi nếu có giá trị hợp lệ
|
| 97 |
if 'article_number' in processed_data:
|