File size: 12,625 Bytes
300df0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import asyncio
import json
import os
import pandas as pd
from openai import AsyncOpenAI
from tqdm.asyncio import tqdm
from src.config import OPENAI_API_KEY

MODEL = "gpt-5.4-nano"
PROCESSED_CHECKPOINT_PATH = "data/extracted_relations_batched_processed_uids.json"

# Prompt template for batch processing
SYSTEM_PROMPT = """Bạn là chuyên gia phân tích văn bản pháp luật Việt Nam. Nhiệm vụ của bạn là trích xuất quan hệ pháp luật từ DANH SÁCH các đoạn văn bản được cung cấp.

Bạn phải phân loại mỗi quan hệ vào đúng 1 trong 3 nhóm:

1. internal
- Dẫn chiếu đến điểm, khoản, điều hoặc toàn bộ văn bản trong CÙNG văn bản nguồn.
- Dấu hiệu thường gặp: "Điều này", "Khoản này", "Điểm này", "Luật này", "Bộ luật này", "Nghị định này", "Thông tư này", "Điều 1 của Luật này", "điểm a khoản 1 Điều 37a của Luật này".
- target_doc phải là một trong các cụm nội bộ xuất hiện hoặc được suy ra: "luật này", "bộ luật này", "nghị định này", "thông tư này", "văn bản này".
- target_article là số/ký hiệu điều, ví dụ "1", "37a". Nếu chỉ nói "Điều này" và source_uid có dạng doc_X_dieu_37a... thì target_article là "37a".
- target_clause là số khoản, ví dụ "1", "2". Nếu không có khoản thì null.
- target_diem là chữ điểm, ví dụ "a", "b", "đ". Nếu không có điểm thì null.
- Không bao giờ ghi "luật này", "nghị định này", "thông tư này" vào target_clause hoặc target_diem.

2. external
- Dẫn chiếu đến văn bản KHÁC.
- target_doc có thể là số hiệu hoặc tên văn bản, ví dụ "12/2022/NĐ-CP", "Bộ luật Lao động", "Luật Xử lý vi phạm hành chính", "Nghị định 46/2016/NĐ-CP".
- Nếu câu chỉ nêu tên văn bản mà không có số hiệu, vẫn phải trích xuất. Ví dụ "theo Bộ luật Lao động" => target_doc là "Bộ luật Lao động".
- Nếu có điều/khoản/điểm của văn bản ngoài thì điền target_article, target_clause, target_diem; nếu không có thì để null.

3. modify
- Quan hệ mà đoạn nguồn CHỦ ĐỘNG sửa đổi, bổ sung, thay thế, bãi bỏ, đình chỉ, ngưng hiệu lực hoặc làm hết hiệu lực một phần/toàn bộ văn bản khác.
- relationship_type luôn là "modify".
- modify_action phải là một trong:
  "sua_doi" cho sửa đổi;
  "bo_sung" cho bổ sung;
  "thay_the" cho thay thế;
  "bai_bo" cho bãi bỏ;
  "dinh_chi" cho đình chỉ;
  "ngung_hieu_luc" cho ngưng hiệu lực;
  "het_hieu_luc" cho hết hiệu lực.
- Chỉ lấy quan hệ chủ động. Ví dụ: "Sửa đổi Điều 5 của Nghị định 12/2022/NĐ-CP" thì lấy. "Điều 5 được sửa đổi bởi Nghị định 99/2024/NĐ-CP" thì không lấy nếu đoạn nguồn chỉ đang mô tả văn bản bị sửa.
- Với internal và external, modify_action luôn là null.

QUY TẮC TÁCH NHIỀU ĐỐI TƯỢNG:
- Nếu một câu dẫn chiếu đến nhiều đối tượng, phải tạo nhiều object riêng biệt.
- Ví dụ "khoản 1, khoản 2 Điều 10" => 2 object: khoản 1 Điều 10 và khoản 2 Điều 10.
- Ví dụ "các điểm a, b, c, đ, e, h, i, k, l, m và n khoản 1 Điều 37a của Luật này" => tạo từng object riêng:
  điểm a khoản 1 Điều 37a;
  điểm b khoản 1 Điều 37a;
  điểm c khoản 1 Điều 37a;
  điểm đ khoản 1 Điều 37a;
  tiếp tục cho từng điểm còn lại.
- Khi nhiều điểm/khoản dùng chung điều hoặc văn bản phía sau, phải copy phần chung đó vào từng object.

QUY TẮC SUY LUẬN TỪ source_uid:
- source_uid có thể chứa vị trí nguồn, ví dụ "doc_123_dieu_37a_khoan_1_diem_a".
- Nếu văn bản nói "Điều này", dùng điều trong source_uid làm target_article.
- Nếu văn bản nói "Khoản này", dùng khoản trong source_uid làm target_clause và điều trong source_uid làm target_article.
- Nếu văn bản nói "Điểm này", dùng điểm/khoản/điều trong source_uid làm target_diem, target_clause, target_article.
- Nếu không thể suy luận chắc chắn thì để field tương ứng là null, không bịa.

Mỗi object trong mảng JSON phải có đúng cấu trúc:
{
  "source_uid": "ID của đoạn văn bản chứa quan hệ này",
  "target_doc": "tên/số hiệu văn bản hoặc 'luật này'/'nghị định này'/...",
  "target_article": "số điều hoặc null",
  "target_clause": "số khoản hoặc null",
  "target_diem": "tên điểm hoặc null",
  "relationship_type": "internal" | "external" | "modify",
  "modify_action": "sua_doi" | "bo_sung" | "thay_the" | "bai_bo" | "dinh_chi" | "ngung_hieu_luc" | "het_hieu_luc" | null
}

QUY TẮC BATCH:
- Tôi cung cấp danh sách đoạn văn bản, mỗi đoạn có một source_uid.
- Trả về một MẢNG JSON duy nhất chứa tất cả quan hệ tìm thấy trong tất cả đoạn.
- Mỗi object phải dùng đúng source_uid của đoạn chứa quan hệ.
- Nếu không tìm thấy quan hệ nào, trả về [].
- Chỉ trả về JSON array hợp lệ, không giải thích, không markdown, không code fence."""

async def process_batch(client, batch_segments):
    """Xử lý một batch các đoạn văn bản."""
    # Ghép các đoạn văn bản lại thành 1 prompt
    formatted_texts = []
    for i, seg in enumerate(batch_segments):
        formatted_texts.append(f"[{i+1}] ID: {seg['uid']}\nNội dung: {seg['text']}")
    
    combined_text = "\n\n".join(formatted_texts)
    
    for attempt in range(3):
        try:
            response = await client.chat.completions.create(
                model=MODEL,
                messages=[
                    {
                        "role": "system",
                        "content": "Bạn là chuyên gia pháp luật. Trả về duy nhất 1 JSON array chứa các quan hệ trích xuất được. Mỗi quan hệ phải có source_uid chính xác.",
                    },
                    {
                        "role": "user",
                        "content": f"{SYSTEM_PROMPT}\n\nDANH SÁCH VĂN BẢN:\n{combined_text}",
                    },
                ],
                temperature=0.0,
                timeout=60.0,
            )

            content = response.choices[0].message.content.strip()
            usage = response.usage
            
            # Log tokens
            in_tokens = usage.prompt_tokens if usage else 0
            out_tokens = usage.completion_tokens if usage else 0
            # print(f"  [Batch] In: {in_tokens}, Out: {out_tokens}")
            
            # Xử lý markdown code blocks
            if content.startswith("```"):
                content = content.split("```")[1]
                if content.startswith("json"):
                    content = content[4:].strip()
            
            result = json.loads(content)
            
            if not isinstance(result, list):
                if isinstance(result, dict):
                    for key in ['results', 'relationships', 'data']:
                        if key in result and isinstance(result[key], list):
                            result = result[key]
                            break
                    else:
                        result = [result]
                else:
                    result = []
            
            return result, in_tokens, out_tokens
        except Exception as e:
            if attempt == 2:
                print(f"Error processing batch: {e}")
            await asyncio.sleep(2 * (attempt + 1))
    return [], 0, 0

def create_batches_by_chars(segments, max_chars=4000):
    """Chia segments thành các batch dựa trên số lượng ký tự."""
    batches = []
    current_batch = []
    current_chars = 0
    
    for seg in segments:
        text_len = len(seg['text'])
        if current_chars + text_len > max_chars and current_batch:
            batches.append(current_batch)
            current_batch = []
            current_chars = 0
        
        current_batch.append(seg)
        current_chars += text_len
        
    if current_batch:
        batches.append(current_batch)
        
    return batches

async def main():
    # Load data
    input_path = 'data/legal_segments_for_colab.parquet'
    if not os.path.exists(input_path):
        input_path = '../../data/legal_segments_for_colab.parquet'
    
    if not os.path.exists(input_path):
        print(f"Error: File {input_path} not found.")
        return

    df = pd.read_parquet(input_path)
    all_segments = df.to_dict('records')
    
    # --- Checkpoint / Resume Logic ---
    output_dir = 'data' if os.path.exists('data') else '../../data'
    output_path = os.path.join(output_dir, 'extracted_relations_batched.json')
    
    all_results = []
    processed_uids = set()
    
    if os.path.exists(output_path):
        try:
            with open(output_path, 'r', encoding='utf-8') as f:
                all_results = json.load(f)
                processed_uids = {res.get('source_uid') for res in all_results if res.get('source_uid')}
            print(f"Loaded {len(all_results)} existing relations from {len(processed_uids)} processed segments.")
        except Exception as e:
            print(f"Warning: Could not load existing results: {e}")
            all_results = []

    if os.path.exists(PROCESSED_CHECKPOINT_PATH):
        try:
            with open(PROCESSED_CHECKPOINT_PATH, "r", encoding="utf-8") as f:
                processed_checkpoint = json.load(f)
                if isinstance(processed_checkpoint, list):
                    processed_uids.update(str(uid) for uid in processed_checkpoint if uid)
                    print(f"Loaded checkpoint for {len(processed_checkpoint)} processed segments.")
        except Exception as e:
            print(f"Warning: Could not load processed checkpoint: {e}")
    
    # Lọc những segments chưa được xử lý
    segments_to_process = [s for s in all_segments if s['uid'] not in processed_uids]
    
    # GIỚI HẠN XỬ LÝ CHO MỖI LẦN CHẠY (Ví dụ chia làm 3 lần cho 7000 segment)
    CHUNK_SIZE = 2000
    
    if not segments_to_process:
        print("✅ All segments already processed!")
        return

    segments_current_run = segments_to_process[:CHUNK_SIZE]
    print(f"Processing chunk: {len(segments_current_run)} segments (Remaining total: {len(segments_to_process)})")
    
    # Chia batch theo ký tự
    batches = create_batches_by_chars(segments_current_run, max_chars=5000)
    print(f"Current run: {len(segments_current_run)} segments in {len(batches)} batches.")

    total_in_tokens = 0
    total_out_tokens = 0
    new_results = []
    newly_processed_uids = []
    
    client = AsyncOpenAI(api_key=OPENAI_API_KEY)
    async with client:
        MAX_CONCURRENT = 3
        sem = asyncio.Semaphore(MAX_CONCURRENT)

        async def batch_task(batch):
            async with sem:
                res, in_t, out_t = await process_batch(client, batch)
                batch_uids = [seg["uid"] for seg in batch if seg.get("uid")]
                return res, in_t, out_t, batch_uids

        tasks = [batch_task(b) for b in batches]
        
        for f in tqdm(asyncio.as_completed(tasks), total=len(tasks)):
            batch_res, in_t, out_t, batch_uids = await f
            new_results.extend(batch_res)
            total_in_tokens += in_t
            total_out_tokens += out_t
            newly_processed_uids.extend(batch_uids)

    # Gộp kết quả mới vào kết quả cũ
    all_results.extend(new_results)
    
    # Lưu lại
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(all_results, f, ensure_ascii=False, indent=2)

    # Lưu checkpoint riêng để các segment không sinh ra relation cũng không bị chạy lại
    processed_uids.update(newly_processed_uids)
    with open(PROCESSED_CHECKPOINT_PATH, "w", encoding="utf-8") as f:
        json.dump(sorted(processed_uids), f, ensure_ascii=False, indent=2)
    
    print(f"\n✅ Chunk complete!")
    print(f"New relationships found: {len(new_results)}")
    print(f"Total input tokens: {total_in_tokens}")
    print(f"Total output tokens: {total_out_tokens}")
    print(f"Final total relationships in file: {len(all_results)}")
    print(f"Results saved to: {output_path}")

if __name__ == "__main__":
    asyncio.run(main())