MrSimple07 commited on
Commit
200954f
·
1 Parent(s): 0b28542

simplest version

Browse files
Files changed (2) hide show
  1. documents_prep.py +99 -47
  2. utils.py +82 -28
documents_prep.py CHANGED
@@ -32,17 +32,20 @@ def chunk_text_documents(documents):
32
 
33
 
34
  def chunk_table_by_rows(table_data, doc_id, max_rows=30):
35
- """Split large tables into row blocks"""
36
  headers = table_data.get('headers', [])
37
  rows = table_data.get('data', [])
38
  table_num = table_data.get('table_number', 'unknown')
39
  table_title = table_data.get('table_title', '')
40
  section = table_data.get('section', '')
41
 
 
 
 
42
  if not rows:
43
  return []
44
 
45
- # Ensure table_data has document_id for format_table_content
46
  if 'document_id' not in table_data:
47
  table_data['document_id'] = doc_id
48
 
@@ -54,10 +57,12 @@ def chunk_table_by_rows(table_data, doc_id, max_rows=30):
54
  metadata={
55
  'type': 'table',
56
  'document_id': doc_id,
57
- 'table_number': table_num,
 
58
  'table_title': table_title,
59
  'section': section,
60
- 'total_rows': len(rows)
 
61
  }
62
  )]
63
 
@@ -69,7 +74,7 @@ def chunk_table_by_rows(table_data, doc_id, max_rows=30):
69
  table_data,
70
  headers,
71
  chunk_rows,
72
- chunk_info=f"Строки {i+1}-{i+len(chunk_rows)} из {len(rows)}"
73
  )
74
 
75
  chunks.append(Document(
@@ -77,75 +82,83 @@ def chunk_table_by_rows(table_data, doc_id, max_rows=30):
77
  metadata={
78
  'type': 'table',
79
  'document_id': doc_id,
80
- 'table_number': table_num,
 
81
  'table_title': table_title,
82
  'section': section,
83
  'chunk_id': i // max_rows,
84
  'row_start': i,
85
  'row_end': i + len(chunk_rows),
86
- 'total_rows': len(rows)
 
 
87
  }
88
  ))
89
 
90
- log_message(f" 📊 Table {table_num} ({doc_id}): {len(rows)} rows → {len(chunks)} chunks")
91
  return chunks
92
 
93
 
94
  def format_table_content(table_data, headers, rows, chunk_info=""):
95
- """Format table for semantic search"""
96
  doc_id = table_data.get('document_id', table_data.get('document', 'unknown'))
97
  table_num = table_data.get('table_number', 'unknown')
98
  table_title = table_data.get('table_title', '')
99
  section = table_data.get('section', '')
100
 
101
- # Normalize table number
102
- if table_num and table_num != 'unknown':
103
- if not str(table_num).startswith('№'):
104
- table_num = f"№{table_num}"
105
 
106
- content = f"=== ТАБЛИЦА ===\n"
 
107
  content += f"Документ: {doc_id}\n"
108
- content += f"Таблица: {table_num}\n"
109
  if table_title:
110
- content += f"Название: {table_title}\n"
111
  if section:
112
  content += f"Раздел: {section}\n"
113
  if chunk_info:
114
  content += f"{chunk_info}\n"
115
- content += f"================\n\n"
116
 
117
- # Add searchable description
118
- content += f"Это таблица {table_num} из документа {doc_id}. "
 
119
  if table_title:
120
- content += f"{table_title}. "
 
121
  if section:
122
- content += f"Находится в разделе: {section}. "
123
- content += f"\n\n"
 
 
 
124
 
125
- # Headers
126
  if headers:
127
  header_str = ' | '.join(str(h) for h in headers)
128
- content += f"Колонки: {header_str}\n\n"
 
129
 
130
- # Rows
131
- for row in rows:
 
132
  if isinstance(row, dict):
133
  parts = [f"{k}: {v}" for k, v in row.items()
134
- if v and str(v).strip() and str(v) != 'nan']
135
  if parts:
136
- content += ' | '.join(parts) + "\n"
137
  elif isinstance(row, list):
138
- parts = [str(v) for v in row if v and str(v).strip() and str(v) != 'nan']
139
  if parts:
140
- content += ' | '.join(parts) + "\n"
141
 
142
  return content
143
 
144
-
145
  def load_json_documents(repo_id, hf_token, json_dir):
146
  """Load text sections from JSON (including ZIPs)"""
147
  import zipfile
148
  import tempfile
 
149
 
150
  log_message("Loading JSON documents...")
151
 
@@ -160,6 +173,7 @@ def load_json_documents(repo_id, hf_token, json_dir):
160
  # Load direct JSON files
161
  for file_path in json_files:
162
  try:
 
163
  local_path = hf_hub_download(
164
  repo_id=repo_id,
165
  filename=file_path,
@@ -169,13 +183,15 @@ def load_json_documents(repo_id, hf_token, json_dir):
169
 
170
  docs = extract_sections_from_json(local_path)
171
  documents.extend(docs)
 
172
 
173
  except Exception as e:
174
- log_message(f"Error loading {file_path}: {e}")
175
 
176
  # Extract and load ZIP files
177
  for zip_path in zip_files:
178
  try:
 
179
  local_zip = hf_hub_download(
180
  repo_id=repo_id,
181
  filename=zip_path,
@@ -184,23 +200,59 @@ def load_json_documents(repo_id, hf_token, json_dir):
184
  )
185
 
186
  with zipfile.ZipFile(local_zip, 'r') as zf:
187
- for json_file in zf.namelist():
188
- if json_file.endswith('.json') and not json_file.startswith('__MACOSX'):
189
- with zf.open(json_file) as f:
190
- with tempfile.NamedTemporaryFile(delete=False, suffix='.json') as tmp:
191
- tmp.write(f.read())
192
- tmp_path = tmp.name
193
-
194
- docs = extract_sections_from_json(tmp_path)
195
- documents.extend(docs)
196
-
197
- import os
198
- os.unlink(tmp_path)
199
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  except Exception as e:
201
- log_message(f"Error loading ZIP {zip_path}: {e}")
202
 
203
- log_message(f"✓ Loaded {len(documents)} text sections")
204
  return documents
205
 
206
  def extract_sections_from_json(json_path):
 
32
 
33
 
34
  def chunk_table_by_rows(table_data, doc_id, max_rows=30):
35
+ """Split large tables into row blocks - OPTIMIZED"""
36
  headers = table_data.get('headers', [])
37
  rows = table_data.get('data', [])
38
  table_num = table_data.get('table_number', 'unknown')
39
  table_title = table_data.get('table_title', '')
40
  section = table_data.get('section', '')
41
 
42
+ # Normalize table number
43
+ table_num_clean = str(table_num).replace('№', '').strip()
44
+
45
  if not rows:
46
  return []
47
 
48
+ # Ensure document_id is set
49
  if 'document_id' not in table_data:
50
  table_data['document_id'] = doc_id
51
 
 
57
  metadata={
58
  'type': 'table',
59
  'document_id': doc_id,
60
+ 'table_number': table_num_clean,
61
+ 'table_number_original': table_num,
62
  'table_title': table_title,
63
  'section': section,
64
+ 'total_rows': len(rows),
65
+ 'is_complete_table': True
66
  }
67
  )]
68
 
 
74
  table_data,
75
  headers,
76
  chunk_rows,
77
+ chunk_info=f"Часть таблицы: строки {i+1}-{i+len(chunk_rows)} из {len(rows)} всего"
78
  )
79
 
80
  chunks.append(Document(
 
82
  metadata={
83
  'type': 'table',
84
  'document_id': doc_id,
85
+ 'table_number': table_num_clean,
86
+ 'table_number_original': table_num,
87
  'table_title': table_title,
88
  'section': section,
89
  'chunk_id': i // max_rows,
90
  'row_start': i,
91
  'row_end': i + len(chunk_rows),
92
+ 'total_rows': len(rows),
93
+ 'total_chunks': (len(rows) + max_rows - 1) // max_rows,
94
+ 'is_complete_table': False
95
  }
96
  ))
97
 
98
+ log_message(f" 📊 Table {table_num_clean} ({doc_id}): {len(rows)} rows → {len(chunks)} chunks")
99
  return chunks
100
 
101
 
102
  def format_table_content(table_data, headers, rows, chunk_info=""):
103
+ """Format table for semantic search - OPTIMIZED"""
104
  doc_id = table_data.get('document_id', table_data.get('document', 'unknown'))
105
  table_num = table_data.get('table_number', 'unknown')
106
  table_title = table_data.get('table_title', '')
107
  section = table_data.get('section', '')
108
 
109
+ # Normalize table number - remove № prefix for consistent searching
110
+ table_num_clean = str(table_num).replace('№', '').strip()
 
 
111
 
112
+ # Create highly searchable header
113
+ content = f"ТАБЛИЦА {table_num_clean}\n"
114
  content += f"Документ: {doc_id}\n"
 
115
  if table_title:
116
+ content += f"Название таблицы: {table_title}\n"
117
  if section:
118
  content += f"Раздел: {section}\n"
119
  if chunk_info:
120
  content += f"{chunk_info}\n"
121
+ content += f"{'='*60}\n\n"
122
 
123
+ # Add multiple search-friendly descriptions
124
+ content += f"Это таблица {table_num_clean} из документа {doc_id}. "
125
+ content += f"Таблица номер {table_num_clean}. "
126
  if table_title:
127
+ content += f"Таблица называется: {table_title}. "
128
+ content += f"Содержание таблицы: {table_title}. "
129
  if section:
130
+ content += f"Таблица находится в разделе {section}. "
131
+
132
+ # Add searchable patterns
133
+ content += f"Если вы ищете таблицу {table_num_clean} в {doc_id}, это она. "
134
+ content += f"\n\nСОДЕРЖАНИЕ ТАБЛИЦЫ:\n"
135
 
136
+ # Headers with emphasis
137
  if headers:
138
  header_str = ' | '.join(str(h) for h in headers)
139
+ content += f"\nКОЛОНКИ: {header_str}\n"
140
+ content += f"Заголовки столбцов: {header_str}\n\n"
141
 
142
+ # Row data with better formatting
143
+ content += "ДАННЫЕ:\n"
144
+ for idx, row in enumerate(rows, 1):
145
  if isinstance(row, dict):
146
  parts = [f"{k}: {v}" for k, v in row.items()
147
+ if v and str(v).strip() and str(v).lower() != 'nan']
148
  if parts:
149
+ content += f"Строка {idx}: {' | '.join(parts)}\n"
150
  elif isinstance(row, list):
151
+ parts = [str(v) for v in row if v and str(v).strip() and str(v).lower() != 'nan']
152
  if parts:
153
+ content += f"Строка {idx}: {' | '.join(parts)}\n"
154
 
155
  return content
156
 
 
157
  def load_json_documents(repo_id, hf_token, json_dir):
158
  """Load text sections from JSON (including ZIPs)"""
159
  import zipfile
160
  import tempfile
161
+ import os
162
 
163
  log_message("Loading JSON documents...")
164
 
 
173
  # Load direct JSON files
174
  for file_path in json_files:
175
  try:
176
+ log_message(f" Loading: {file_path}")
177
  local_path = hf_hub_download(
178
  repo_id=repo_id,
179
  filename=file_path,
 
183
 
184
  docs = extract_sections_from_json(local_path)
185
  documents.extend(docs)
186
+ log_message(f" ✓ Extracted {len(docs)} sections")
187
 
188
  except Exception as e:
189
+ log_message(f"Error loading {file_path}: {e}")
190
 
191
  # Extract and load ZIP files
192
  for zip_path in zip_files:
193
  try:
194
+ log_message(f" Processing ZIP: {zip_path}")
195
  local_zip = hf_hub_download(
196
  repo_id=repo_id,
197
  filename=zip_path,
 
200
  )
201
 
202
  with zipfile.ZipFile(local_zip, 'r') as zf:
203
+ json_files_in_zip = [f for f in zf.namelist()
204
+ if f.endswith('.json')
205
+ and not f.startswith('__MACOSX')
206
+ and not f.startswith('.')]
207
+
208
+ log_message(f" Found {len(json_files_in_zip)} JSON files in ZIP")
209
+
210
+ for json_file in json_files_in_zip:
211
+ try:
212
+ log_message(f" Processing: {json_file}")
213
+
214
+ # Read file content
215
+ file_content = zf.read(json_file)
216
+
217
+ # Try to detect encoding
218
+ try:
219
+ # First try UTF-8
220
+ text_content = file_content.decode('utf-8')
221
+ except UnicodeDecodeError:
222
+ try:
223
+ # Try UTF-8 with BOM
224
+ text_content = file_content.decode('utf-8-sig')
225
+ except UnicodeDecodeError:
226
+ try:
227
+ # Try Windows-1251 (common for Cyrillic)
228
+ text_content = file_content.decode('windows-1251')
229
+ except UnicodeDecodeError:
230
+ log_message(f" ✗ Skipping: Cannot decode {json_file}")
231
+ continue
232
+
233
+ # Write to temp file
234
+ with tempfile.NamedTemporaryFile(mode='w', delete=False,
235
+ suffix='.json', encoding='utf-8') as tmp:
236
+ tmp.write(text_content)
237
+ tmp_path = tmp.name
238
+
239
+ # Extract sections
240
+ docs = extract_sections_from_json(tmp_path)
241
+ documents.extend(docs)
242
+ log_message(f" ✓ Extracted {len(docs)} sections")
243
+
244
+ # Clean up temp file
245
+ os.unlink(tmp_path)
246
+
247
+ except json.JSONDecodeError as e:
248
+ log_message(f" ✗ Invalid JSON in {json_file}: {e}")
249
+ except Exception as e:
250
+ log_message(f" ✗ Error processing {json_file}: {e}")
251
+
252
  except Exception as e:
253
+ log_message(f"Error with ZIP {zip_path}: {e}")
254
 
255
+ log_message(f"✓ Loaded {len(documents)} text sections total")
256
  return documents
257
 
258
  def extract_sections_from_json(json_path):
utils.py CHANGED
@@ -37,44 +37,86 @@ def format_sources(nodes):
37
  return "\n".join(set(sources))
38
 
39
  def answer_question(question, query_engine, reranker):
40
- """Answer question using RAG"""
41
  try:
42
  log_message(f"Query: {question}")
43
 
44
- # Retrieve
45
  retrieved = query_engine.retriever.retrieve(question)
46
  log_message(f"Retrieved {len(retrieved)} nodes")
47
 
48
- # Rerank
49
- reranked = rerank_nodes(question, retrieved, reranker, top_k=15)
 
 
 
 
 
 
 
50
  log_message(f"Reranked to {len(reranked)} nodes")
51
 
52
- # Format context
53
- context = "\n\n".join([
54
- f"[{n.metadata.get('document_id', 'unknown')}]\n{n.text}"
55
- for n in reranked
56
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- # Generate answer
59
- prompt = f"""Контекст из базы данных:
 
 
60
  {context}
61
 
62
- Вопрос: {question}
 
 
 
 
 
 
 
63
 
64
- Ответь на вопрос используя ТОЛЬКО информацию из контекста. Цитируй источники."""
65
 
66
  response = query_engine.query(prompt)
67
-
68
  sources = format_sources(reranked)
69
 
70
  return response.response, sources
71
 
72
  except Exception as e:
73
  log_message(f"Error: {e}")
 
 
74
  return f"Ошибка: {e}", ""
75
 
76
- def rerank_nodes(query, nodes, reranker, top_k=15, min_score=0.5):
77
- """Rerank nodes with diversity"""
78
  if not nodes:
79
  return []
80
 
@@ -85,28 +127,40 @@ def rerank_nodes(query, nodes, reranker, top_k=15, min_score=0.5):
85
  # Sort by score
86
  scored = sorted(zip(nodes, scores), key=lambda x: x[1], reverse=True)
87
 
88
- # Filter by threshold
89
  filtered = [(n, s) for n, s in scored if s >= min_score]
90
 
91
  if not filtered:
92
- # Fallback: take top 30% if nothing passes threshold
93
- cutoff = max(scores) * 0.6
94
- filtered = [(n, s) for n, s in scored if s >= cutoff]
95
 
96
- # Diversity selection
 
 
 
97
  selected = []
98
  seen_docs = set()
 
 
99
 
100
  for node, score in filtered:
 
 
 
 
 
 
 
 
 
 
 
 
101
  if len(selected) >= top_k:
102
  break
103
-
104
- doc_id = node.metadata.get('document_id', 'unknown')
105
-
106
- # Prioritize diverse documents
107
- if doc_id not in seen_docs or len(selected) < 5:
108
- selected.append(node)
109
- seen_docs.add(doc_id)
110
 
111
  log_message(f"Reranked: {len(filtered)} → {len(selected)} (from {len(seen_docs)} docs)")
112
 
 
37
  return "\n".join(set(sources))
38
 
39
  def answer_question(question, query_engine, reranker):
40
+ """Answer question using RAG - OPTIMIZED"""
41
  try:
42
  log_message(f"Query: {question}")
43
 
44
+ # Retrieve more nodes initially
45
  retrieved = query_engine.retriever.retrieve(question)
46
  log_message(f"Retrieved {len(retrieved)} nodes")
47
 
48
+ # Log what was retrieved for debugging
49
+ doc_ids = [n.metadata.get('document_id', 'unknown') for n in retrieved]
50
+ table_nums = [n.metadata.get('table_number', '') for n in retrieved if n.metadata.get('type') == 'table']
51
+ log_message(f"Retrieved from documents: {set(doc_ids)}")
52
+ if table_nums:
53
+ log_message(f"Retrieved tables: {set(table_nums)}")
54
+
55
+ # Rerank with more nodes
56
+ reranked = rerank_nodes(question, retrieved, reranker, top_k=20)
57
  log_message(f"Reranked to {len(reranked)} nodes")
58
 
59
+ # Log what survived reranking
60
+ doc_ids_reranked = [n.metadata.get('document_id', 'unknown') for n in reranked]
61
+ table_nums_reranked = [n.metadata.get('table_number', '') for n in reranked if n.metadata.get('type') == 'table']
62
+ log_message(f"After reranking - documents: {set(doc_ids_reranked)}")
63
+ if table_nums_reranked:
64
+ log_message(f"After reranking - tables: {set(table_nums_reranked)}")
65
+
66
+ # Format context with clear source attribution
67
+ context_parts = []
68
+ for n in reranked:
69
+ meta = n.metadata
70
+ doc_id = meta.get('document_id', 'unknown')
71
+ doc_type = meta.get('type', 'text')
72
+
73
+ if doc_type == 'table':
74
+ table_num = meta.get('table_number', 'unknown')
75
+ title = meta.get('table_title', '')
76
+ source_label = f"[{doc_id} - Таблица {table_num}]"
77
+ if title:
78
+ source_label += f" {title}"
79
+ elif doc_type == 'image':
80
+ img_num = meta.get('image_number', 'unknown')
81
+ source_label = f"[{doc_id} - Рисунок {img_num}]"
82
+ else:
83
+ section = meta.get('section_id', '')
84
+ source_label = f"[{doc_id} - Раздел {section}]"
85
+
86
+ context_parts.append(f"{source_label}\n{n.text}")
87
+
88
+ context = "\n\n" + "="*60 + "\n\n".join(context_parts)
89
 
90
+ # Improved prompt
91
+ prompt = f"""Ты эксперт по технической документации. Используй ТОЛЬКО предоставленный контекст для ответа.
92
+
93
+ КОНТЕКСТ ИЗ БАЗЫ ДАННЫХ:
94
  {context}
95
 
96
+ ВОПРОС: {question}
97
+
98
+ ИНСТРУКЦИИ:
99
+ 1. Отвечай ТОЛЬКО на основе предоставленного контекста
100
+ 2. Если спрашивают о конкретной таблице - найди её в контексте и передай её содержание
101
+ 3. ОБЯЗАТЕЛЬНО укажи источник: документ, номер таблицы/раздела
102
+ 4. Если нужной информации нет в контексте - четко скажи об этом
103
+ 5. Будь точным и конкретным
104
 
105
+ ОТВЕТ:"""
106
 
107
  response = query_engine.query(prompt)
 
108
  sources = format_sources(reranked)
109
 
110
  return response.response, sources
111
 
112
  except Exception as e:
113
  log_message(f"Error: {e}")
114
+ import traceback
115
+ log_message(traceback.format_exc())
116
  return f"Ошибка: {e}", ""
117
 
118
+ def rerank_nodes(query, nodes, reranker, top_k=20, min_score=0.3):
119
+ """Rerank nodes with diversity - MORE LENIENT"""
120
  if not nodes:
121
  return []
122
 
 
127
  # Sort by score
128
  scored = sorted(zip(nodes, scores), key=lambda x: x[1], reverse=True)
129
 
130
+ # More lenient threshold
131
  filtered = [(n, s) for n, s in scored if s >= min_score]
132
 
133
  if not filtered:
134
+ # Fallback: take top 50% if nothing passes threshold
135
+ cutoff = max(scores) * 0.5
136
+ filtered = [(n, s) for n, s in scored if s >= cutoff][:top_k]
137
 
138
+ # Log top scores for debugging
139
+ log_message(f"Top 5 reranking scores: {[f'{s:.3f}' for _, s in scored[:5]]}")
140
+
141
+ # Diversity selection - but prioritize tables if query mentions them
142
  selected = []
143
  seen_docs = set()
144
+ table_nodes = []
145
+ other_nodes = []
146
 
147
  for node, score in filtered:
148
+ if node.metadata.get('type') == 'table':
149
+ table_nodes.append((node, score))
150
+ else:
151
+ other_nodes.append((node, score))
152
+
153
+ # If query mentions "таблица", prioritize table nodes
154
+ if 'таблиц' in query.lower():
155
+ combined = table_nodes + other_nodes
156
+ else:
157
+ combined = filtered
158
+
159
+ for node, score in combined[:top_k]:
160
  if len(selected) >= top_k:
161
  break
162
+ selected.append(node)
163
+ seen_docs.add(node.metadata.get('document_id', 'unknown'))
 
 
 
 
 
164
 
165
  log_message(f"Reranked: {len(filtered)} → {len(selected)} (from {len(seen_docs)} docs)")
166