add data viewer
Browse files- app/law_document_chunker.py +36 -14
- app/main.py +75 -8
app/law_document_chunker.py
CHANGED
|
@@ -183,12 +183,27 @@ class LawDocumentChunker:
|
|
| 183 |
"""Xử lý văn bản theo cấu trúc phân cấp."""
|
| 184 |
lines = content.split('\n')
|
| 185 |
chunks = []
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
| 188 |
|
| 189 |
current_chunk_content = ""
|
| 190 |
current_level = "CONTENT"
|
| 191 |
current_level_value = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
for line in lines:
|
| 194 |
level, level_value, level_content = self._detect_structure_level(line)
|
|
@@ -207,18 +222,11 @@ class LawDocumentChunker:
|
|
| 207 |
)
|
| 208 |
chunks.append(metadata)
|
| 209 |
|
| 210 |
-
#
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
elif level == "DIEU":
|
| 216 |
-
# Điều thuộc về cấp độ cao nhất hiện tại
|
| 217 |
-
current_parent = parent_stack[-1] if parent_stack else None
|
| 218 |
-
parent_stack.append(metadata.id)
|
| 219 |
-
elif level in ["KHOAN", "DIEM"]:
|
| 220 |
-
# Khoản/Điểm thuộc về Điều hiện tại
|
| 221 |
-
current_parent = parent_stack[-1] if parent_stack else None
|
| 222 |
|
| 223 |
# Bắt đầu chunk mới
|
| 224 |
current_chunk_content = line + "\n"
|
|
@@ -260,6 +268,20 @@ class LawDocumentChunker:
|
|
| 260 |
|
| 261 |
logger.info(f"[CHUNKER] Created {len(chunks)} chunks from document")
|
| 262 |
return chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
async def _create_embeddings_for_chunks(self, chunks: List[ChunkMetadata]) -> int:
|
| 265 |
"""Tạo embeddings cho các chunks và lưu ngay lập tức vào Supabase."""
|
|
|
|
| 183 |
"""Xử lý văn bản theo cấu trúc phân cấp."""
|
| 184 |
lines = content.split('\n')
|
| 185 |
chunks = []
|
| 186 |
+
|
| 187 |
+
# Stack để theo dõi các chunks theo thứ tự xuất hiện
|
| 188 |
+
# Mỗi item là (chunk_id, level, level_value)
|
| 189 |
+
chunk_stack = []
|
| 190 |
|
| 191 |
current_chunk_content = ""
|
| 192 |
current_level = "CONTENT"
|
| 193 |
current_level_value = None
|
| 194 |
+
current_parent = None
|
| 195 |
+
|
| 196 |
+
# Định nghĩa thứ tự ưu tiên của các level (số càng nhỏ càng cao)
|
| 197 |
+
level_priority = {
|
| 198 |
+
"PHAN": 1,
|
| 199 |
+
"PHU_LUC": 1,
|
| 200 |
+
"CHUONG": 2,
|
| 201 |
+
"MUC": 3,
|
| 202 |
+
"DIEU": 4,
|
| 203 |
+
"KHOAN": 5,
|
| 204 |
+
"DIEM": 6,
|
| 205 |
+
"CONTENT": 7
|
| 206 |
+
}
|
| 207 |
|
| 208 |
for line in lines:
|
| 209 |
level, level_value, level_content = self._detect_structure_level(line)
|
|
|
|
| 222 |
)
|
| 223 |
chunks.append(metadata)
|
| 224 |
|
| 225 |
+
# Thêm vào stack
|
| 226 |
+
chunk_stack.append((metadata.id, current_level, current_level_value))
|
| 227 |
+
|
| 228 |
+
# Tìm parent cho level mới
|
| 229 |
+
current_parent = self._find_parent_for_level(chunk_stack, level, level_priority)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
# Bắt đầu chunk mới
|
| 232 |
current_chunk_content = line + "\n"
|
|
|
|
| 268 |
|
| 269 |
logger.info(f"[CHUNKER] Created {len(chunks)} chunks from document")
|
| 270 |
return chunks
|
| 271 |
+
|
| 272 |
+
def _find_parent_for_level(self, chunk_stack: List[Tuple[str, str, Optional[str]]],
|
| 273 |
+
current_level: str, level_priority: Dict[str, int]) -> Optional[str]:
|
| 274 |
+
"""
|
| 275 |
+
Tìm parent gần nhất có level cao hơn (priority thấp hơn) cho level hiện tại.
|
| 276 |
+
"""
|
| 277 |
+
current_priority = level_priority.get(current_level, 999)
|
| 278 |
+
|
| 279 |
+
# Tìm từ cuối stack (gần nhất) đến đầu stack
|
| 280 |
+
for chunk_id, level, level_value in reversed(chunk_stack):
|
| 281 |
+
if level_priority.get(level, 999) < current_priority:
|
| 282 |
+
return chunk_id
|
| 283 |
+
|
| 284 |
+
return None
|
| 285 |
|
| 286 |
async def _create_embeddings_for_chunks(self, chunks: List[ChunkMetadata]) -> int:
|
| 287 |
"""Tạo embeddings cho các chunks và lưu ngay lập tức vào Supabase."""
|
app/main.py
CHANGED
|
@@ -2,7 +2,7 @@ from fastapi import FastAPI, Request, HTTPException, Depends
|
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from loguru import logger
|
| 4 |
import json
|
| 5 |
-
from typing import Dict, Any, List
|
| 6 |
import asyncio
|
| 7 |
from concurrent.futures import ThreadPoolExecutor
|
| 8 |
import os
|
|
@@ -698,7 +698,7 @@ async def update_all_documents():
|
|
| 698 |
@timing_decorator_async
|
| 699 |
async def view_all_document_chunks():
|
| 700 |
"""
|
| 701 |
-
API xem toàn bộ dữ liệu trong bảng document_chunks.
|
| 702 |
"""
|
| 703 |
try:
|
| 704 |
logger.info("[API] Starting view all document chunks")
|
|
@@ -710,7 +710,7 @@ async def view_all_document_chunks():
|
|
| 710 |
total_chunks = len(chunks_data)
|
| 711 |
unique_documents = len(set(chunk.get('vanbanid') for chunk in chunks_data if chunk.get('vanbanid')))
|
| 712 |
|
| 713 |
-
# Nhóm theo vanbanid
|
| 714 |
chunks_by_document = {}
|
| 715 |
for chunk in chunks_data:
|
| 716 |
vanbanid = chunk.get('vanbanid')
|
|
@@ -720,7 +720,7 @@ async def view_all_document_chunks():
|
|
| 720 |
|
| 721 |
# Thống kê chi tiết
|
| 722 |
document_stats = []
|
| 723 |
-
|
| 724 |
|
| 725 |
for vanbanid, chunks in chunks_by_document.items():
|
| 726 |
# Thống kê
|
|
@@ -730,12 +730,14 @@ async def view_all_document_chunks():
|
|
| 730 |
"document_title": chunks[0].get('document_title', 'Unknown') if chunks else 'Unknown'
|
| 731 |
})
|
| 732 |
|
| 733 |
-
#
|
| 734 |
-
|
|
|
|
|
|
|
| 735 |
"vanbanid": vanbanid,
|
| 736 |
"document_title": chunks[0].get('document_title', 'Unknown') if chunks else 'Unknown',
|
| 737 |
"chunk_count": len(chunks),
|
| 738 |
-
"chunks":
|
| 739 |
})
|
| 740 |
|
| 741 |
return {
|
|
@@ -746,13 +748,78 @@ async def view_all_document_chunks():
|
|
| 746 |
"unique_documents": unique_documents,
|
| 747 |
"document_stats": document_stats
|
| 748 |
},
|
| 749 |
-
"data":
|
| 750 |
}
|
| 751 |
|
| 752 |
except Exception as e:
|
| 753 |
logger.error(f"[API] Error in view_all_document_chunks: {e}")
|
| 754 |
raise HTTPException(status_code=500, detail=f"Lỗi: {str(e)}")
|
| 755 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
@app.get("/api/document-chunks/status")
|
| 757 |
@timing_decorator_async
|
| 758 |
async def get_document_chunks_status():
|
|
|
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from loguru import logger
|
| 4 |
import json
|
| 5 |
+
from typing import Dict, Any, List, Optional
|
| 6 |
import asyncio
|
| 7 |
from concurrent.futures import ThreadPoolExecutor
|
| 8 |
import os
|
|
|
|
| 698 |
@timing_decorator_async
|
| 699 |
async def view_all_document_chunks():
|
| 700 |
"""
|
| 701 |
+
API xem toàn bộ dữ liệu trong bảng document_chunks theo cấu trúc cây.
|
| 702 |
"""
|
| 703 |
try:
|
| 704 |
logger.info("[API] Starting view all document chunks")
|
|
|
|
| 710 |
total_chunks = len(chunks_data)
|
| 711 |
unique_documents = len(set(chunk.get('vanbanid') for chunk in chunks_data if chunk.get('vanbanid')))
|
| 712 |
|
| 713 |
+
# Nhóm theo vanbanid
|
| 714 |
chunks_by_document = {}
|
| 715 |
for chunk in chunks_data:
|
| 716 |
vanbanid = chunk.get('vanbanid')
|
|
|
|
| 720 |
|
| 721 |
# Thống kê chi tiết
|
| 722 |
document_stats = []
|
| 723 |
+
hierarchical_data = []
|
| 724 |
|
| 725 |
for vanbanid, chunks in chunks_by_document.items():
|
| 726 |
# Thống kê
|
|
|
|
| 730 |
"document_title": chunks[0].get('document_title', 'Unknown') if chunks else 'Unknown'
|
| 731 |
})
|
| 732 |
|
| 733 |
+
# Tạo cấu trúc cây cho từng văn bản
|
| 734 |
+
tree_structure = build_chunk_tree(chunks)
|
| 735 |
+
|
| 736 |
+
hierarchical_data.append({
|
| 737 |
"vanbanid": vanbanid,
|
| 738 |
"document_title": chunks[0].get('document_title', 'Unknown') if chunks else 'Unknown',
|
| 739 |
"chunk_count": len(chunks),
|
| 740 |
+
"chunks": tree_structure
|
| 741 |
})
|
| 742 |
|
| 743 |
return {
|
|
|
|
| 748 |
"unique_documents": unique_documents,
|
| 749 |
"document_stats": document_stats
|
| 750 |
},
|
| 751 |
+
"data": hierarchical_data
|
| 752 |
}
|
| 753 |
|
| 754 |
except Exception as e:
|
| 755 |
logger.error(f"[API] Error in view_all_document_chunks: {e}")
|
| 756 |
raise HTTPException(status_code=500, detail=f"Lỗi: {str(e)}")
|
| 757 |
|
| 758 |
+
def build_chunk_tree(chunks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 759 |
+
"""
|
| 760 |
+
Xây dựng cấu trúc cây từ danh sách chunks phẳng.
|
| 761 |
+
"""
|
| 762 |
+
# Tạo dictionary để truy cập nhanh
|
| 763 |
+
chunks_dict = {chunk['id']: chunk for chunk in chunks}
|
| 764 |
+
|
| 765 |
+
# Tạo cấu trúc cây
|
| 766 |
+
root_chunks = []
|
| 767 |
+
|
| 768 |
+
for chunk in chunks:
|
| 769 |
+
chunk_id = chunk['id']
|
| 770 |
+
parent_id = chunk.get('cha')
|
| 771 |
+
|
| 772 |
+
# Tạo node mới với cấu trúc cây
|
| 773 |
+
tree_node = {
|
| 774 |
+
"id": chunk_id,
|
| 775 |
+
"content": chunk.get('content', ''),
|
| 776 |
+
"vanbanid": chunk.get('vanbanid'),
|
| 777 |
+
"cha": parent_id,
|
| 778 |
+
"document_title": chunk.get('document_title', ''),
|
| 779 |
+
"article_number": chunk.get('article_number'),
|
| 780 |
+
"article_title": chunk.get('article_title', ''),
|
| 781 |
+
"clause_number": chunk.get('clause_number', ''),
|
| 782 |
+
"sub_clause_letter": chunk.get('sub_clause_letter', ''),
|
| 783 |
+
"context_summary": chunk.get('context_summary', ''),
|
| 784 |
+
"data": chunk, # Toàn bộ dữ liệu gốc
|
| 785 |
+
"children": []
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
if parent_id is None:
|
| 789 |
+
# Đây là root node
|
| 790 |
+
root_chunks.append(tree_node)
|
| 791 |
+
else:
|
| 792 |
+
# Tìm parent và thêm vào children
|
| 793 |
+
parent = chunks_dict.get(parent_id)
|
| 794 |
+
if parent:
|
| 795 |
+
# Tìm node parent trong cây
|
| 796 |
+
parent_node = find_node_in_tree(root_chunks, parent_id)
|
| 797 |
+
if parent_node:
|
| 798 |
+
parent_node["children"].append(tree_node)
|
| 799 |
+
else:
|
| 800 |
+
# Nếu không tìm thấy parent, coi như root
|
| 801 |
+
root_chunks.append(tree_node)
|
| 802 |
+
else:
|
| 803 |
+
# Parent không tồn tại, coi như root
|
| 804 |
+
root_chunks.append(tree_node)
|
| 805 |
+
|
| 806 |
+
return root_chunks
|
| 807 |
+
|
| 808 |
+
def find_node_in_tree(nodes: List[Dict[str, Any]], target_id: str) -> Optional[Dict[str, Any]]:
|
| 809 |
+
"""
|
| 810 |
+
Tìm node trong cây theo ID.
|
| 811 |
+
"""
|
| 812 |
+
for node in nodes:
|
| 813 |
+
if node["id"] == target_id:
|
| 814 |
+
return node
|
| 815 |
+
|
| 816 |
+
# Tìm trong children
|
| 817 |
+
found = find_node_in_tree(node.get("children", []), target_id)
|
| 818 |
+
if found:
|
| 819 |
+
return found
|
| 820 |
+
|
| 821 |
+
return None
|
| 822 |
+
|
| 823 |
@app.get("/api/document-chunks/status")
|
| 824 |
@timing_decorator_async
|
| 825 |
async def get_document_chunks_status():
|