feat: проверка данных через Морфер/DaData/DeepSeek (ИИ)

- правая панель на вкладке «Организация»: проверка склонений
  (Морфер 3.0 — бесплатно, без ключа; DeepSeek — ИИ) и реквизитов
  (DaData по ИНН; DeepSeek — ИИ-рецензия)
- мини-табло результата: источник, дисклеймер для ИИ («сгенерировано
  ИИ — перепроверьте»), кнопки «Применить»/«Очистить»
- меню «Файл → Настройки…»: вкладка API-ключей (DaData, DeepSeek,
  модель), сохраняются в api_settings.json рядом с программой
- веб-проверки показывают источник (morpher.ru / dadata.ru)
This commit is contained in:
2026-08-03 12:59:51 +04:00
parent 3a4cd2fde4
commit 2f4a08b2e4
3 changed files with 580 additions and 4 deletions
+255 -4
View File
@@ -17,6 +17,12 @@ from . import (
get_short_fio, get_initials,
)
from .verify import (
check_declension_morpher, check_company_dadata,
check_declension_ai, check_company_ai,
)
from .api_settings import load_api_settings, save_api_settings
class DokoGenApp:
"""Главное окно приложения."""
@@ -82,6 +88,8 @@ class DokoGenApp:
self.recent_menu = tk.Menu(file_menu, tearoff=0)
file_menu.add_cascade(label="Последние файлы", menu=self.recent_menu)
file_menu.add_separator()
file_menu.add_command(label="Настройки…", command=self.open_api_settings)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self._on_close)
gen_menu = tk.Menu(menubar, tearoff=0)
@@ -149,8 +157,11 @@ class DokoGenApp:
main = ttk.Frame(scrollable, padding=15)
main.pack(fill=tk.BOTH, expand=True)
left = ttk.Frame(main)
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# --- Название организации ---
f1 = ttk.LabelFrame(main, text="Наименование организации", padding=10)
f1 = ttk.LabelFrame(left, text="Наименование организации", padding=10)
f1.pack(fill=tk.X, pady=5)
ttk.Label(f1, text="Полное название:").grid(row=0, column=0, sticky=tk.W, pady=2)
@@ -202,7 +213,7 @@ class DokoGenApp:
decl_frame.columnconfigure(1, weight=1)
# --- Адрес и реквизиты ---
f2 = ttk.LabelFrame(main, text="Адрес и реквизиты", padding=10)
f2 = ttk.LabelFrame(left, text="Адрес и реквизиты", padding=10)
f2.pack(fill=tk.X, pady=5)
addr_fields = [
@@ -227,7 +238,7 @@ class DokoGenApp:
f2.columnconfigure(1, weight=1)
# --- Должностные лица ---
f3 = ttk.LabelFrame(main, text="Должностные лица", padding=10)
f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10)
f3.pack(fill=tk.X, pady=5)
of_fields = [
@@ -246,7 +257,7 @@ class DokoGenApp:
f3.columnconfigure(1, weight=1)
# --- Документ ---
f4 = ttk.LabelFrame(main, text="Документ", padding=10)
f4 = ttk.LabelFrame(left, text="Документ", padding=10)
f4.pack(fill=tk.X, pady=5)
ttk.Label(f4, text="Номер договора:").grid(row=0, column=0, sticky=tk.W)
ttk.Entry(f4, textvariable=self.doc_number_var, width=25).grid(
@@ -255,6 +266,246 @@ class DokoGenApp:
ttk.Entry(f4, textvariable=self.doc_date_var, width=25).grid(
row=1, column=1, padx=10, sticky=tk.W)
# --- Правая панель: проверка данных ---
right = ttk.LabelFrame(main, text="Проверка данных", padding=10)
right.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0))
ttk.Label(right, text="Склонение названия:", font=('', 9, 'bold')).pack(anchor=tk.W)
ttk.Button(right, text="🌐 Склонение — Морфер 3.0",
command=self._check_declension_web).pack(fill=tk.X, pady=2)
ttk.Button(right, text="🤖 Склонение — ИИ (DeepSeek)",
command=self._check_declension_ai).pack(fill=tk.X, pady=2)
ttk.Label(right, text="Реквизиты организации:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(10, 0))
ttk.Button(right, text="🌐 Реквизиты — DaData",
command=self._check_company_web).pack(fill=tk.X, pady=2)
ttk.Button(right, text="🤖 Реквизиты — ИИ (DeepSeek)",
command=self._check_company_ai).pack(fill=tk.X, pady=2)
ttk.Label(right, text="Результат проверки:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(10, 0))
self._verify_text = scrolledtext.ScrolledText(
right, width=44, height=14, state=tk.DISABLED, wrap=tk.WORD, font=('', 9))
self._verify_text.pack(fill=tk.BOTH, expand=True, pady=4)
btn_row = ttk.Frame(right)
btn_row.pack(fill=tk.X)
ttk.Button(btn_row, text="✓ Применить", command=self._verify_apply).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_row, text="Очистить", command=self._verify_clear).pack(side=tk.LEFT, padx=2)
self._verify_result = None
# ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ----------
def open_api_settings(self):
"""Окно настроек API-ключей (меню «Файл → Настройки»)."""
win = tk.Toplevel(self.root)
win.title("Настройки")
win.geometry("540x460")
win.transient(self.root)
win.grab_set()
nb = ttk.Notebook(win)
nb.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Вкладка API-ключи
api_tab = ttk.Frame(nb, padding=10)
nb.add(api_tab, text="API-ключи")
settings = load_api_settings()
ttk.Label(api_tab, text="DaData — проверка организации по ИНН/ОГРН",
font=('', 9, 'bold')).pack(anchor=tk.W)
ttk.Label(api_tab, text="API-ключ (получить на https://dadata.ru):").pack(anchor=tk.W, pady=(6, 0))
dadata_var = tk.StringVar(value=settings.get('dadata_token', ''))
ttk.Entry(api_tab, textvariable=dadata_var, width=64).pack(fill=tk.X, pady=2)
ttk.Label(api_tab, text="DeepSeek — ИИ-проверка склонений и реквизитов",
font=('', 9, 'bold')).pack(anchor=tk.W, pady=(14, 0))
ttk.Label(api_tab, text="API-ключ (получить на https://platform.deepseek.com):").pack(anchor=tk.W, pady=(6, 0))
ds_var = tk.StringVar(value=settings.get('deepseek_api_key', ''))
ttk.Entry(api_tab, textvariable=ds_var, width=64).pack(fill=tk.X, pady=2)
ttk.Label(api_tab, text="Модель:").pack(anchor=tk.W, pady=(6, 0))
model_var = tk.StringVar(value=settings.get('deepseek_model', 'deepseek-chat'))
ttk.Entry(api_tab, textvariable=model_var, width=64).pack(fill=tk.X, pady=2)
ttk.Label(api_tab, text="Морфер 3.0 работает без ключа (бесплатный склонятор).",
foreground='gray').pack(anchor=tk.W, pady=(14, 0))
def _save():
save_api_settings({
'dadata_token': dadata_var.get().strip(),
'deepseek_api_key': ds_var.get().strip(),
'deepseek_model': model_var.get().strip() or 'deepseek-chat',
})
self.log("✅ Настройки API сохранены")
win.destroy()
ttk.Button(api_tab, text="💾 Сохранить", command=_save).pack(anchor=tk.W, pady=12)
# ---------- ПРОВЕРКИ ----------
_CASE_TO_CODE = {'И': 'nomn', 'Р': 'gent', 'Д': 'datv', 'В': 'accs', 'Т': 'ablt', 'П': 'loct'}
def _verify_log(self, text):
self._verify_text.config(state=tk.NORMAL)
self._verify_text.delete('1.0', tk.END)
self._verify_text.insert('1.0', text)
self._verify_text.config(state=tk.DISABLED)
def _verify_clear(self):
self._verify_result = None
self._verify_log('')
def _check_declension_web(self):
name = self.company_name_entry.get().strip()
if not name:
messagebox.showwarning("Проверка", "Сначала введите полное название организации")
return
self._verify_log("Запрос к Морфер 3.0…")
def _run():
result, err = check_declension_morpher(name)
self.root.after(0, lambda: self._verify_declension_done(result, err))
threading.Thread(target=_run, daemon=True).start()
def _check_declension_ai(self):
name = self.company_name_entry.get().strip()
if not name:
messagebox.showwarning("Проверка", "Сначала введите полное название организации")
return
settings = load_api_settings()
if not settings.get('deepseek_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»")
return
self._verify_log("Запрос к DeepSeek (ИИ)…")
def _run():
result, err = check_declension_ai(name, settings['deepseek_api_key'], settings.get('deepseek_model'))
self.root.after(0, lambda: self._verify_declension_done(result, err))
threading.Thread(target=_run, daemon=True).start()
def _verify_declension_done(self, result, err):
if err:
self._verify_log(f"{err}")
return
self._verify_result = result
lines = [f"Источник: {result['source']} ({result['source_url']})"]
if result.get('disclaimer'):
lines.append(result['disclaimer'])
for code, label in [('Р', 'Родительный'), ('Д', 'Дательный'), ('В', 'Винительный'),
('Т', 'Творительный'), ('П', 'Предложный')]:
if code in result['data']:
lines.append(f"{label}: {result['data'][code]}")
if result.get('comment'):
lines.append(f"Комментарий: {result['comment']}")
lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
self._verify_log('\n'.join(lines))
def _check_company_web(self):
inn_entry = self._company_entries.get('inn')
inn = inn_entry.get().strip() if inn_entry else ''
if not inn:
messagebox.showwarning("Проверка", "Сначала введите ИНН организации")
return
settings = load_api_settings()
if not settings.get('dadata_token'):
messagebox.showwarning("Настройки", "Не задан API-ключ DaData.\nМеню «Файл → Настройки»")
return
self._verify_log("Запрос к DaData…")
def _run():
result, err = check_company_dadata(inn, settings['dadata_token'])
self.root.after(0, lambda: self._verify_company_done(result, err))
threading.Thread(target=_run, daemon=True).start()
def _check_company_ai(self):
self._collect_company()
c = self.model
settings = load_api_settings()
if not settings.get('deepseek_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»")
return
company_data = {
'full_name': c.full_name, 'short_name': c.short_name,
'address': c.address, 'inn': c.inn, 'kpp': c.kpp, 'ogrn': c.ogrn,
'ogrn_date': c.ogrn_date,
'chief_position': c.chief_position, 'chief_fio': c.chief_fio,
}
self._verify_log("Запрос к DeepSeek (ИИ)…")
def _run():
result, err = check_company_ai(company_data, settings['deepseek_api_key'], settings.get('deepseek_model'))
self.root.after(0, lambda: self._verify_company_done(result, err))
threading.Thread(target=_run, daemon=True).start()
def _verify_company_done(self, result, err):
if err:
self._verify_log(f"{err}")
return
self._verify_result = result
lines = [f"Источник: {result['source']} ({result['source_url']})"]
if result.get('disclaimer'):
lines.append(result['disclaimer'])
d = result['data']
if 'full_name' in d:
# DaData — фактические данные
for key, label in [('full_name', 'Наименование'), ('short_name', 'Краткое'),
('address', 'Адрес'), ('inn', 'ИНН'), ('kpp', 'КПП'),
('ogrn', 'ОГРН'), ('ogrn_date', 'Дата ОГРН'),
('chief_position', 'Руководитель'), ('chief_fio', 'ФИО')]:
if d.get(key):
lines.append(f"{label}: {d[key]}")
else:
# ИИ-рецензия
lines.append(f"Оценка: {d.get('rating', '')}")
for n in (d.get('notes') or []):
lines.append(f"{n}")
props = d.get('proposals') or {}
if any(props.values()):
lines.append("Предложения:")
for k, v in props.items():
if v:
lines.append(f" {k}: {v}")
lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
self._verify_log('\n'.join(lines))
def _verify_apply(self):
result = self._verify_result
if not result:
return
apply_data = result.get('apply', {})
if 'declension' in apply_data:
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
for case, val in apply_data['declension'].items():
code = self._CASE_TO_CODE.get(case)
if code and code in self.decl_entries:
self.decl_entries[code].delete(0, tk.END)
self.decl_entries[code].insert(0, val)
setattr(self.model, f'company_name_{idx_map[code]}', val)
self.log("✅ Склонения применены")
if 'company' in apply_data:
comp = apply_data['company']
if comp.get('full_name'):
self.company_name_entry.delete(0, tk.END)
self.company_name_entry.insert(0, comp['full_name'])
if comp.get('short_name'):
self.short_name_entry.delete(0, tk.END)
self.short_name_entry.insert(0, comp['short_name'])
mapping = {'address': 'address', 'kpp': 'kpp', 'ogrn': 'ogrn',
'ogrn_date': 'ogrn_date', 'inn': 'inn'}
for data_key, ui_key in mapping.items():
if comp.get(data_key) and ui_key in self._company_entries:
self._company_entries[ui_key].delete(0, tk.END)
self._company_entries[ui_key].insert(0, comp[data_key])
if comp.get('chief_position') and 'chief_position' in self._official_entries:
self._official_entries['chief_position'].delete(0, tk.END)
self._official_entries['chief_position'].insert(0, comp['chief_position'])
if comp.get('chief_fio') and 'chief_fio' in self._official_entries:
self._official_entries['chief_fio'].delete(0, tk.END)
self._official_entries['chief_fio'].insert(0, comp['chief_fio'])
self.log("✅ Реквизиты применены")
# ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------
def _build_is_tab(self):
tab = ttk.Frame(self.notebook)