File size: 9,745 Bytes
a85d6bf
5884230
a85d6bf
5884230
 
451cdc6
 
 
 
 
 
 
 
 
 
 
 
 
 
0067c9d
6370d73
c697463
 
 
 
 
 
0067c9d
 
c697463
 
 
 
 
6370d73
5884230
6370d73
451cdc6
6370d73
 
 
 
 
 
 
 
 
 
f9e7c0c
451cdc6
6370d73
 
 
 
5884230
0067c9d
 
6370d73
 
5884230
6370d73
0067c9d
 
 
6370d73
 
 
 
 
451cdc6
6370d73
 
c697463
6370d73
 
 
5884230
451cdc6
5884230
90e6b4c
 
 
 
 
 
 
aa38fcf
 
451cdc6
 
 
aa38fcf
 
 
6370d73
 
 
 
 
 
aa38fcf
6370d73
 
aa38fcf
a85d6bf
451cdc6
a85d6bf
 
451cdc6
a85d6bf
 
6370d73
a85d6bf
 
 
 
 
 
 
aa38fcf
a85d6bf
6370d73
aa38fcf
a85d6bf
6370d73
 
a85d6bf
 
 
 
 
90e6b4c
a85d6bf
6370d73
a85d6bf
90e6b4c
451cdc6
aa38fcf
a85d6bf
90e6b4c
 
 
 
 
 
451cdc6
 
6370d73
90e6b4c
822ef8c
90e6b4c
a85d6bf
90e6b4c
 
 
822ef8c
f9e7c0c
 
451cdc6
6370d73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9e7c0c
451cdc6
f9e7c0c
 
 
451cdc6
 
f9e7c0c
 
451cdc6
f9e7c0c
 
451cdc6
 
f9e7c0c
 
 
451cdc6
f9e7c0c
 
 
451cdc6
f9e7c0c
 
451cdc6
 
f9e7c0c
 
 
 
 
451cdc6
 
 
 
 
 
f9e7c0c
451cdc6
 
f9e7c0c
 
 
451cdc6
 
f9e7c0c
 
451cdc6
f9e7c0c
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
from llama_index.core.text_splitter import SentenceSplitter
from llama_index.core import Document
from config import CHUNK_SIZE, CHUNK_OVERLAP
from my_logging import log_message

def normalize_table_number(table_num, section):
    """Normalize table numbers for consistent retrieval"""
    if not table_num or table_num == 'Неизвестно':
        return 'Неизвестно'
    
    # Clean up common prefixes
    tn = str(table_num).replace('Таблица', '').replace('№', '').strip()
    
    # Add section context for appendix tables
    if section and ('Приложение' in str(section) or 'приложение' in str(section).lower()):
        return f"№{tn} ({section})"
    
    return f"№{tn}"

def create_table_content(table_data):
    """Create formatted content optimized for semantic search"""
    doc_id = (
        table_data.get('document_id') or 
        table_data.get('document') or 
        table_data.get('Обозначение документа') or
        'Неизвестно'
    )
    table_num = table_data.get('table_number', 'Неизвестно')
    table_title = table_data.get('table_title', 'Неизвестно')
    section = (
        table_data.get('section') or 
        table_data.get('Раздел документа') or
        'Неизвестно'
    )
    sheet_name = table_data.get('sheet_name', '')
    
    # Enhanced table number with appendix context
    normalized_num = normalize_table_number(table_num, section)
    if 'Приложени' in str(section):
        # Extract appendix number
        import re
        appendix_match = re.search(r'Приложени[ея]\s*(\d+)', str(section))
        if appendix_match:
            appendix_num = appendix_match.group(1)
            normalized_num = f"{normalized_num} Приложения {appendix_num}"
    
    # Build searchable header
    content = f"Документ: {doc_id}\n"
    content += f"Раздел: {section}\n"
    content += f"Таблица: {normalized_num}\n"
    content += f"Название: {table_title}\n"
    if sheet_name:
        content += f"Лист: {sheet_name}\n"
    content += f"\n"
    
    headers = table_data.get('headers', [])
    if headers:
        header_str = ' | '.join(str(h) for h in headers)
        content += f"Колонки: {header_str}\n\n"
    
    # CRITICAL: Preserve searchable row identifiers
    if 'data' in table_data and isinstance(table_data['data'], list):
        for row_idx, row in enumerate(table_data['data'], start=1):
            if isinstance(row, dict):
                # Extract ALL key-value pairs naturally
                row_parts = []
                for k, v in row.items():
                    if v and str(v).strip() and str(v) != 'nan':
                        row_parts.append(f"{k}: {v}")
                
                if row_parts:
                    content += ' | '.join(row_parts) + "\n"
            elif isinstance(row, list):
                row_str = ' | '.join([str(v) for v in row if v and str(v).strip() and str(v) != 'nan'])
                if row_str:
                    content += row_str + "\n"
    
    return content, normalized_num


def chunk_table_document(doc, chunk_size=None, chunk_overlap=None):
    if chunk_size is None:
        chunk_size = CHUNK_SIZE
    if chunk_overlap is None:
        chunk_overlap = CHUNK_OVERLAP
    
    table_num = doc.metadata.get('table_number', 'unknown')
    doc_id = doc.metadata.get('document_id', 'unknown')
    section = doc.metadata.get('section', 'Неизвестно')
    
    full_table_id = f"{doc_id} | {section} | {table_num}"
    
    lines = doc.text.strip().split('\n')
    
    # Find where data rows start
    data_start_idx = 0
    for i, line in enumerate(lines):
        if line.startswith('Колонки:'):
            data_start_idx = i + 2  # Skip header and blank line
            break
    
    table_header = '\n'.join(lines[:data_start_idx])
    data_rows = lines[data_start_idx:]
    
    if not data_rows or len(doc.text) < chunk_size * 1.5:
        log_message(f"  📊 {full_table_id}: малая таблица, без разбиения")
        return [doc]
    
    log_message(f"  📋 {full_table_id}: {len(data_rows)} строк → chunking")
    
    header_size = len(table_header)
    available_size = chunk_size - header_size - 100
    
    text_chunks = []
    current_chunk_rows = []
    current_size = 0
    
    for row in data_rows:
        row_size = len(row) + 1
        
        if current_size + row_size > available_size and current_chunk_rows:
            chunk_text = table_header + '\n' + '\n'.join(current_chunk_rows)
            text_chunks.append(chunk_text)
            
            # Keep last 2 rows for overlap
            overlap_count = min(2, len(current_chunk_rows))
            current_chunk_rows = current_chunk_rows[-overlap_count:]
            current_size = sum(len(r) + 1 for r in current_chunk_rows)
        
        current_chunk_rows.append(row)
        current_size += row_size
    
    if current_chunk_rows:
        chunk_text = table_header + '\n' + '\n'.join(current_chunk_rows)
        text_chunks.append(chunk_text)
    
    log_message(f"  ✂️ {full_table_id}{len(text_chunks)} чанков")
    
    chunked_docs = []
    for i, chunk_text in enumerate(text_chunks):
        chunk_metadata = doc.metadata.copy()
        chunk_metadata.update({
            "chunk_id": i,
            "total_chunks": len(text_chunks),
            "chunk_size": len(chunk_text),
            "is_chunked": True,
            "full_table_id": full_table_id,
            "table_number_normalized": doc.metadata.get('table_number_normalized')
        })
        
        chunked_doc = Document(
            text=chunk_text,
            metadata=chunk_metadata
        )
        chunked_docs.append(chunked_doc)
    
    return chunked_docs


def table_to_document(table_data, document_id=None):
    """Convert table data to Document with complete metadata"""
    if not isinstance(table_data, dict):
        return []
    
    sheet_doc_id = (
        table_data.get('document_id') or 
        table_data.get('document') or 
        table_data.get('Обозначение документа')
    )
    
    doc_id = sheet_doc_id or document_id or 'Неизвестно'
    
    table_num = table_data.get('table_number', 'Неизвестно')
    table_title = table_data.get('table_title', 'Неизвестно')
    section = table_data.get('section', table_data.get('Раздел документа', 'Неизвестно'))
    sheet_name = table_data.get('sheet_name', '')
    
    table_rows = table_data.get('data', [])
    if not table_rows:
        log_message(f"⚠️ Таблица {table_num} ({doc_id}) пропущена: нет данных")
        return []
    
    content, normalized_num = create_table_content(table_data)
    content_size = len(content)
    
    base_doc = Document(
        text=content,
        metadata={
            "type": "table",
            "table_number": table_num,
            "table_number_normalized": normalized_num,
            "table_title": table_title,
            "document_id": doc_id,
            "section": section,
            "section_id": section,
            "sheet_name": sheet_name,
            "total_rows": len(table_rows),
            "content_size": content_size,
            "full_table_id": f"{doc_id} | {section} | {normalized_num}"
        }
    )
    
    if content_size > CHUNK_SIZE:
        log_message(f"📊 CHUNKING: {doc_id} | {normalized_num} | {content_size} > {CHUNK_SIZE}")
        return chunk_table_document(base_doc)
    else:
        log_message(f"✓ {doc_id} | {normalized_num} ({content_size} символов)")
        return [base_doc]
    
    
def table_to_document(table_data, document_id=None):
    """Convert table data to Document with proper metadata"""
    if not isinstance(table_data, dict):
        return []
    
    # FIXED: Extract sheet-level document_id first
    sheet_doc_id = (
        table_data.get('document_id') or 
        table_data.get('document') or 
        table_data.get('Обозначение документа')
    )
    
    # Use sheet doc_id if available, otherwise use passed document_id
    doc_id = sheet_doc_id or document_id or 'Неизвестно'
    
    table_num = table_data.get('table_number', 'Неизвестно')
    table_title = table_data.get('table_title', 'Неизвестно')
    section = table_data.get('section', table_data.get('Раздел документа', 'Неизвестно'))
    
    table_rows = table_data.get('data', [])
    if not table_rows:
        log_message(f"⚠️ Таблица {table_num} ({doc_id}) пропущена: нет данных")
        return []
    
    content, normalized_num = create_table_content(table_data)
    content_size = len(content)
    
    base_doc = Document(
        text=content,
        metadata={
            "type": "table",
            "table_number": table_num,
            "table_number_normalized": normalized_num,
            "table_title": table_title,
            "document_id": doc_id,
            "section": section,
            "section_id": section,
            "total_rows": len(table_rows),
            "content_size": content_size,
            "full_table_id": f"{doc_id} | {section} | {normalized_num}"
        }
    )
    
    if content_size > CHUNK_SIZE:
        log_message(f"📊 CHUNKING: {doc_id} | {normalized_num} | {content_size} > {CHUNK_SIZE}")
        return chunk_table_document(base_doc)
    else:
        log_message(f"✓ {doc_id} | {normalized_num} ({content_size} символов)")
        return [base_doc]