/
Paff
/
declarant
Обзор
Документация
Войти
/
Paff
/
declarant
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
gui/verify_tab.py
386 строк
17 KB
1pash1985-gif
feat: AI Ensemble - DeepSeek + Kimi parallel parsing with debates
27 июл 2026, 11:14
27 июл 2026, 11:14
5ea7945
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- """Вкладка 'Верификация' — сводка + таблицы с редактированием.""" from __future__ import annotations from PyQt6.QtCore import Qt, pyqtSignal, QThread from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTableWidget, QTableWidgetItem, QTabWidget, QGroupBox, QHeaderView, QComboBox, QMessageBox, QSplitter, QMenu, QInputDialog, ) from PyQt6.QtGui import QColor, QAction from corrections import load_corrections, save_corrections from ai_agent import AIAgent, AISuggestion from gui.ai_review_dialog import AIReviewDialog class VerifyTab(QWidget): fill_requested = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self._scan_data: dict = {} self._ai_suggestions: list[AISuggestion] = [] self._setup_ui() def _setup_ui(self): layout = QVBoxLayout(self) # --- Summary panel --- self.grp_summary = QGroupBox('Сводка') h_sum = QHBoxLayout(self.grp_summary) self.lbl_files = QLabel('Файлов: —') self.lbl_items = QLabel('Позиций: —') self.lbl_stems = QLabel('Стеблей: —') self.lbl_errors = QLabel('Проблем: —') self.lbl_errors.setStyleSheet('color: red; font-weight: bold;') for lbl in (self.lbl_files, self.lbl_items, self.lbl_stems, self.lbl_errors): lbl.setStyleSheet(lbl.styleSheet() + '; font-size: 13px;') h_sum.addWidget(lbl) layout.addWidget(self.grp_summary) # --- Sub-tabs for tables --- splitter = QSplitter(Qt.Orientation.Vertical) self.sub_tabs = QTabWidget() # Table 1: Markings self.tbl_markings = QTableWidget() self.tbl_markings.setColumnCount(6) self.tbl_markings.setHorizontalHeaderLabels([ 'Файл', 'Маркинг', '→ Колонка', 'Статус', 'Позиций', 'AI подсказка' ]) self.tbl_markings.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.Stretch) self.tbl_markings.setEditTriggers( QTableWidget.EditTrigger.DoubleClicked) self.tbl_markings.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.tbl_markings.customContextMenuRequested.connect(self._markings_context_menu) self.sub_tabs.addTab(self.tbl_markings, 'Маркировки') # Table 2: Classification self.tbl_classify = QTableWidget() self.tbl_classify.setColumnCount(4) self.tbl_classify.setHorizontalHeaderLabels([ 'Сорт', '→ Семейство', 'Уверенность', 'AI подсказка' ]) self.tbl_classify.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.Stretch) self.tbl_classify.setEditTriggers( QTableWidget.EditTrigger.DoubleClicked) self.tbl_classify.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.tbl_classify.customContextMenuRequested.connect(self._classify_context_menu) self.sub_tabs.addTab(self.tbl_classify, 'Классификация') # Table 3: Summary per table self.tbl_totals = QTableWidget() self.tbl_totals.setColumnCount(4) self.tbl_totals.setHorizontalHeaderLabels([ 'Таблица', 'Будет заполнено', 'Не размещено', 'Причина' ]) self.tbl_totals.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.Stretch) self.sub_tabs.addTab(self.tbl_totals, 'Итоги') splitter.addWidget(self.sub_tabs) layout.addWidget(splitter, 1) # --- Bottom buttons --- h_btn = QHBoxLayout() self.btn_ai = QPushButton('🤖 Спросить AI') self.btn_ai.setFixedHeight(36) self.btn_ai.clicked.connect(self._ask_ai) self.btn_ai_review = QPushButton('🔍 AI Review') self.btn_ai_review.setFixedHeight(36) self.btn_ai_review.setToolTip('Сравнить ответы DeepSeek и Kimi') self.btn_ai_review.clicked.connect(self._open_ai_review) self.btn_save_rule = QPushButton('💾 Сохранить правило') self.btn_save_rule.setFixedHeight(36) self.btn_save_rule.clicked.connect(self._save_correction) self.btn_fill = QPushButton('✅ Всё верно — заполнить файлы') self.btn_fill.setFixedHeight(40) self.btn_fill.setStyleSheet('font-weight: bold; font-size: 14px;') self.btn_fill.clicked.connect(self._on_fill) h_btn.addWidget(self.btn_ai) h_btn.addWidget(self.btn_ai_review) h_btn.addWidget(self.btn_save_rule) h_btn.addStretch() h_btn.addWidget(self.btn_fill) layout.addLayout(h_btn) # --- Public API --- def load_scan_data(self, scan_data: dict): """Populate tables from scan results.""" self._scan_data = scan_data # Summary sheets = scan_data.get('sheets', []) markings = scan_data.get('markings_info', []) n_problems = sum(1 for m in markings if m['status'] == 'FAIL') n_problems += len(scan_data.get('unclassified', [])) self.lbl_files.setText(f"Файлов: {len(scan_data.get('files_info', []))}") self.lbl_items.setText(f"Позиций: {scan_data.get('total_items', 0)}") self.lbl_stems.setText(f"Стеблей: {scan_data.get('total_stems', 0)}") self.lbl_errors.setText(f"Проблем: {n_problems}") # Table 1: Markings self.tbl_markings.setRowCount(len(markings)) for i, m in enumerate(markings): self.tbl_markings.setItem(i, 0, QTableWidgetItem(m['file'])) self.tbl_markings.setItem(i, 1, QTableWidgetItem(m['marking'])) self.tbl_markings.setItem(i, 2, QTableWidgetItem(m['column'])) status_item = QTableWidgetItem(m['status']) if m['status'] == 'FAIL': status_item.setBackground(QColor(255, 200, 200)) elif m['status'] == 'PREFIX': status_item.setBackground(QColor(255, 255, 200)) elif m['status'] == 'LEARNED': status_item.setBackground(QColor(200, 220, 255)) else: status_item.setBackground(QColor(200, 255, 200)) self.tbl_markings.setItem(i, 3, status_item) self.tbl_markings.setItem(i, 4, QTableWidgetItem(str(m['items']))) self.tbl_markings.setItem(i, 5, QTableWidgetItem('')) # Table 2: Unclassified unclass = scan_data.get('unclassified', []) self.tbl_classify.setRowCount(len(unclass)) for i, var in enumerate(unclass): self.tbl_classify.setItem(i, 0, QTableWidgetItem(var)) self.tbl_classify.setItem(i, 1, QTableWidgetItem('???')) self.tbl_classify.setItem(i, 2, QTableWidgetItem('—')) self.tbl_classify.setItem(i, 3, QTableWidgetItem('')) # Table 3: Totals (placeholder — filled after dry-run) self.tbl_totals.setRowCount(3) nl_placed = sum(m['items'] for m in markings if m['status'] != 'FAIL') nl_failed = sum(m['items'] for m in markings if m['status'] == 'FAIL') self.tbl_totals.setItem(0, 0, QTableWidgetItem('Europa')) self.tbl_totals.setItem(0, 1, QTableWidgetItem(str(nl_placed))) self.tbl_totals.setItem(0, 2, QTableWidgetItem(str(nl_failed))) self.tbl_totals.setItem(0, 3, QTableWidgetItem('no_col' if nl_failed else '')) other_items = sum(len(s.items) for s in scan_data.get('other_sheets', [])) self.tbl_totals.setItem(1, 0, QTableWidgetItem('Import')) self.tbl_totals.setItem(1, 1, QTableWidgetItem(str(other_items))) self.tbl_totals.setItem(1, 2, QTableWidgetItem('—')) self.tbl_totals.setItem(1, 3, QTableWidgetItem('')) self.tbl_totals.setItem(2, 0, QTableWidgetItem('Florunner')) self.tbl_totals.setItem(2, 1, QTableWidgetItem('—')) self.tbl_totals.setItem(2, 2, QTableWidgetItem('—')) self.tbl_totals.setItem(2, 3, QTableWidgetItem('')) def get_verified_data(self) -> dict: """Return verified/corrected data from tables.""" # Read back any corrections made by user in marking table corrected_markings = {} for i in range(self.tbl_markings.rowCount()): marking = self.tbl_markings.item(i, 1) column = self.tbl_markings.item(i, 2) if marking and column and column.text(): corrected_markings[marking.text()] = column.text() corrected_families = {} for i in range(self.tbl_classify.rowCount()): var = self.tbl_classify.item(i, 0) fam = self.tbl_classify.item(i, 1) if var and fam and fam.text() and fam.text() != '???': corrected_families[var.text()] = fam.text() return { 'corrected_markings': corrected_markings, 'corrected_families': corrected_families, 'scan_data': self._scan_data, } # --- Buttons --- def _ask_ai(self): """Send problems to AI agent.""" from gui.settings_tab import get_ai_settings settings = get_ai_settings() if not settings.get('api_key'): QMessageBox.warning(self, 'AI', 'API ключ не настроен. Перейдите в Настройки.') return agent = AIAgent(provider='groq', api_key=settings['api_key']) if not agent.is_available: QMessageBox.warning(self, 'AI', 'AI-агент недоступен.') return # Collect unmapped markings unmapped = [] for i in range(self.tbl_markings.rowCount()): status = self.tbl_markings.item(i, 3) if status and status.text() == 'FAIL': marking = self.tbl_markings.item(i, 1) if marking: unmapped.append(marking.text()) europa_cols = self._scan_data.get('europa_cols', []) # Ask AI for marking suggestions if unmapped and europa_cols: suggestions = agent.verify_markings(unmapped, europa_cols) for sug in suggestions: # Find row and fill AI column for i in range(self.tbl_markings.rowCount()): m_item = self.tbl_markings.item(i, 1) if m_item and m_item.text() == sug.original: hint = f'{sug.suggestion} ({sug.confidence:.0%})' self.tbl_markings.setItem(i, 5, QTableWidgetItem(hint)) break # Ask AI for classification unclass = self._scan_data.get('unclassified', [])[:20] if unclass: suggestions = agent.classify_unknown(unclass) for sug in suggestions: for i in range(self.tbl_classify.rowCount()): v_item = self.tbl_classify.item(i, 0) if v_item and v_item.text() == sug.original: self.tbl_classify.setItem( i, 1, QTableWidgetItem(sug.suggestion)) self.tbl_classify.setItem( i, 2, QTableWidgetItem(f'{sug.confidence:.0%}')) self.tbl_classify.setItem( i, 3, QTableWidgetItem(sug.reason)) break QMessageBox.information(self, 'AI', 'AI-анализ завершён. Проверьте подсказки.') def _open_ai_review(self): """Open side-by-side DeepSeek vs Kimi review dialog.""" dlg = AIReviewDialog(parsed_files=[], parent=self) dlg.exec() def _save_correction(self): """Save currently selected row as a correction rule.""" corr = load_corrections() # Check which sub-tab is active idx = self.sub_tabs.currentIndex() if idx == 0: # Markings row = self.tbl_markings.currentRow() if row < 0: return marking = self.tbl_markings.item(row, 1) column = self.tbl_markings.item(row, 2) if marking and column and column.text(): corr.set_column_mapping(marking.text(), column.text()) # Also record in history corr.add_correction( source_file='', supplier='', correction_type='document', field_name='marking_column', original_value=marking.text(), corrected_value=column.text(), ) save_corrections(corr) QMessageBox.information( self, 'Правило', f'Сохранено: {marking.text()} → {column.text()}') elif idx == 1: # Classification row = self.tbl_classify.currentRow() if row < 0: return var = self.tbl_classify.item(row, 0) fam = self.tbl_classify.item(row, 1) if var and fam and fam.text() and fam.text() != '???': corr.set_family(var.text(), fam.text()) # Also record in history corr.add_correction( source_file='', supplier='', correction_type='recognition', field_name='flower_family', original_value=var.text(), corrected_value=fam.text(), ) save_corrections(corr) QMessageBox.information( self, 'Правило', f'Сохранено: {var.text()} → {fam.text()}') # --- Context menus --- def _markings_context_menu(self, pos): """Context menu for markings table.""" row = self.tbl_markings.rowAt(pos.y()) if row < 0: return menu = QMenu(self) act_save = menu.addAction('💾 Сохранить правило') act_alias = menu.addAction('🏷 Добавить алиас маркинга') act_ignore = menu.addAction('❌ Игнорировать маркинг') action = menu.exec(self.tbl_markings.viewport().mapToGlobal(pos)) marking_item = self.tbl_markings.item(row, 1) if not marking_item: return marking = marking_item.text() if action == act_save: self.tbl_markings.setCurrentCell(row, 0) self._save_correction() elif action == act_alias: alias, ok = QInputDialog.getText( self, 'Алиас', f'Заменить «{marking}» на:') if ok and alias.strip(): corr = load_corrections() corr.set_marking_alias(marking, alias.strip()) save_corrections(corr) QMessageBox.information(self, 'Алиас', f'Сохранено: {marking} → {alias.strip()}') elif action == act_ignore: corr = load_corrections() corr.set_marking_alias(marking, None) save_corrections(corr) QMessageBox.information(self, 'Игнор', f'Маркинг «{marking}» будет игнорироваться.') def _classify_context_menu(self, pos): """Context menu for classification table.""" row = self.tbl_classify.rowAt(pos.y()) if row < 0: return menu = QMenu(self) act_save = menu.addAction('💾 Сохранить правило') act_syn = menu.addAction('🌺 Добавить синоним цветка') action = menu.exec(self.tbl_classify.viewport().mapToGlobal(pos)) var_item = self.tbl_classify.item(row, 0) if not var_item: return variety = var_item.text() if action == act_save: self.tbl_classify.setCurrentCell(row, 0) self._save_correction() elif action == act_syn: full_name, ok = QInputDialog.getText( self, 'Синоним', f'Сокращение «{variety}» — полное название цветка:') if ok and full_name.strip(): corr = load_corrections() corr.set_flower_synonym(variety, full_name.strip()) corr.add_correction( source_file='', supplier='', correction_type='synonym', field_name='flower_synonym', original_value=variety, corrected_value=full_name.strip(), ) save_corrections(corr) QMessageBox.information(self, 'Синоним', f'Сохранено: {variety} → {full_name.strip()}') def _on_fill(self): """User confirmed — emit signal to fill files.""" reply = QMessageBox.question( self, 'Подтверждение', 'Заполнить выходные файлы? Существующие будут перезаписаны.', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: self.fill_requested.emit()