Spaces:
Sleeping
Sleeping
Commit ·
200954f
1
Parent(s): 0b28542
simplest version
Browse files- documents_prep.py +99 -47
- 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
|
| 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':
|
|
|
|
| 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"
|
| 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':
|
|
|
|
| 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 {
|
| 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 |
-
|
| 103 |
-
if not str(table_num).startswith('№'):
|
| 104 |
-
table_num = f"№{table_num}"
|
| 105 |
|
| 106 |
-
|
|
|
|
| 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"=
|
| 116 |
|
| 117 |
-
# Add
|
| 118 |
-
content += f"Это таблица {
|
|
|
|
| 119 |
if table_title:
|
| 120 |
-
content += f"{table_title}. "
|
|
|
|
| 121 |
if section:
|
| 122 |
-
content += f"
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
# Headers
|
| 126 |
if headers:
|
| 127 |
header_str = ' | '.join(str(h) for h in headers)
|
| 128 |
-
content += f"К
|
|
|
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
|
|
|
| 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)
|
| 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)
|
| 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
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
except Exception as e:
|
| 201 |
-
log_message(f"Error
|
| 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 |
-
#
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
log_message(f"Reranked to {len(reranked)} nodes")
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
prompt = f"""
|
|
|
|
|
|
|
| 60 |
{context}
|
| 61 |
|
| 62 |
-
В
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
#
|
| 89 |
filtered = [(n, s) for n, s in scored if s >= min_score]
|
| 90 |
|
| 91 |
if not filtered:
|
| 92 |
-
# Fallback: take top
|
| 93 |
-
cutoff = max(scores) * 0.
|
| 94 |
-
filtered = [(n, s) for n, s in scored if s >= cutoff]
|
| 95 |
|
| 96 |
-
#
|
|
|
|
|
|
|
|
|
|
| 97 |
selected = []
|
| 98 |
seen_docs = set()
|
|
|
|
|
|
|
| 99 |
|
| 100 |
for node, score in filtered:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
if len(selected) >= top_k:
|
| 102 |
break
|
| 103 |
-
|
| 104 |
-
|
| 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 |
|