From 2f4a08b2e42e8c28d7dace24dbd880340a68bde3 Mon Sep 17 00:00:00 2001 From: DokoGen Date: Mon, 3 Aug 2026 12:59:51 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=9C=D0=BE=D1=80=D1=84=D0=B5=D1=80?= =?UTF-8?q?/DaData/DeepSeek=20(=D0=98=D0=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - правая панель на вкладке «Организация»: проверка склонений (Морфер 3.0 — бесплатно, без ключа; DeepSeek — ИИ) и реквизитов (DaData по ИНН; DeepSeek — ИИ-рецензия) - мини-табло результата: источник, дисклеймер для ИИ («сгенерировано ИИ — перепроверьте»), кнопки «Применить»/«Очистить» - меню «Файл → Настройки…»: вкладка API-ключей (DaData, DeepSeek, модель), сохраняются в api_settings.json рядом с программой - веб-проверки показывают источник (morpher.ru / dadata.ru) --- dokogen/api_settings.py | 59 +++++++++ dokogen/ui.py | 259 +++++++++++++++++++++++++++++++++++++- dokogen/verify.py | 266 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 580 insertions(+), 4 deletions(-) create mode 100644 dokogen/api_settings.py create mode 100644 dokogen/verify.py diff --git a/dokogen/api_settings.py b/dokogen/api_settings.py new file mode 100644 index 0000000..0f35feb --- /dev/null +++ b/dokogen/api_settings.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""DokoGen — настройки API для внешних сервисов проверки. + +Хранятся в JSON рядом с программой (api_settings.json). +Поля: +- dadata_token — API-ключ DaData (проверка организации по ИНН) +- deepseek_api_key — API-ключ DeepSeek (ИИ-проверки) +- deepseek_model — модель DeepSeek (по умолчанию deepseek-chat) +""" + +import os +import sys +import json + +DEFAULT_SETTINGS = { + 'dadata_token': '', + 'deepseek_api_key': '', + 'deepseek_model': 'deepseek-chat', +} + + +def default_api_settings_path(): + """Путь к файлу настроек: рядом с exe (frozen) или рядом с модулем.""" + if getattr(sys, 'frozen', False): + base = os.path.dirname(sys.executable) + else: + base = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(base, 'api_settings.json') + + +def load_api_settings(path=None): + """Загрузка настроек API из JSON-файла. Если файла нет — дефолт.""" + if path is None: + path = default_api_settings_path() + if not path or not os.path.exists(path): + return dict(DEFAULT_SETTINGS) + try: + with open(path, encoding='utf-8-sig') as f: + loaded = json.load(f) + merged = dict(DEFAULT_SETTINGS) + for k, v in loaded.items(): + if k in merged: + merged[k] = v + return merged + except Exception: + return dict(DEFAULT_SETTINGS) + + +def save_api_settings(settings, path=None): + """Сохранение настроек API. Возвращает True/False.""" + if path is None: + path = default_api_settings_path() + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w', encoding='utf-8') as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + return True + except Exception: + return False diff --git a/dokogen/ui.py b/dokogen/ui.py index b9d1758..c4b55c4 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -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) diff --git a/dokogen/verify.py b/dokogen/verify.py new file mode 100644 index 0000000..4673ee1 --- /dev/null +++ b/dokogen/verify.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""DokoGen — проверка данных через внешние сервисы. + +Провайдеры: +- Морфер 3.0 (morpher.ru) — склонение ФИО/названий по падежам. Бесплатно, без ключа. +- DaData (dadata.ru) — проверка организации по ИНН/ОГРН (наименование, адрес, КПП). + Нужен API-ключ (бесплатный тариф ~10 000 запросов/день). +- DeepSeek (deepseek.com) — ИИ-проверка: рецензия склонений и реквизитов. + Нужен API-ключ. Данные ИИ ВСЕГДА помечаются дисклеймером. + +Каждая функция возвращает (result, error): +- result: dict с полями {source, source_url, data, disclaimer, apply} +- error: строка ошибки или None +""" + +import json +import urllib.parse +import urllib.request +import urllib.error + +MORPHER_URL = "https://ws3.morpher.ru/russian/declension" +DADATA_URL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party" +DEEPSEEK_URL = "https://api.deepseek.com/chat/completions" + +CASES = ['И', 'Р', 'Д', 'В', 'Т', 'П'] +CASE_NAMES = { + 'И': 'Именительный', 'Р': 'Родительный', 'Д': 'Дательный', + 'В': 'Винительный', 'Т': 'Творительный', 'П': 'Предложный', +} + + +def _http_json(url, data=None, headers=None, timeout=30): + """HTTP-запрос, возвращает (json, error_str).""" + body = None + if data is not None: + body = json.dumps(data).encode('utf-8') + req = urllib.request.Request(url, data=body, headers=headers or {}) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read().decode('utf-8') + return json.loads(raw), None + except urllib.error.HTTPError as e: + try: + err_body = e.read().decode('utf-8') + except Exception: + err_body = '' + return None, f"HTTP {e.code}: {err_body[:300]}" + except Exception as e: + return None, str(e) + + +# ============================================================ +# МОРФЕР 3.0 — склонение (бесплатно, без ключа) +# ============================================================ +def check_declension_morpher(text): + """Склонение слова/ФИО/названия через Морфер 3.0. + + Возвращает result с полем data: {case_code: слово} и apply-инструкцией + для заполнения полей склонений в UI. + """ + if not text or not text.strip(): + return None, "Пустое значение для склонения" + url = MORPHER_URL + '?' + urllib.parse.urlencode({'s': text.strip(), 'format': 'json'}) + js, err = _http_json(url) + if err: + return None, err + if not isinstance(js, dict): + return None, "Неожиданный ответ Морфера" + + decl = {} + for c in CASES: + val = js.get(c) or js.get(c.lower()) or '' + if isinstance(val, str) and val: + decl[c] = val + + if not decl: + return None, "Морфер не смог просклонять (возможно, это не слово/ФИО)" + + return { + 'source': 'Морфер 3.0', + 'source_url': 'https://morpher.ru', + 'disclaimer': '', + 'kind': 'declension', + 'original': text.strip(), + 'data': decl, + 'apply': {'declension': decl}, + }, None + + +# ============================================================ +# DADATA — проверка организации по ИНН (нужен API-ключ) +# ============================================================ +def check_company_dadata(inn, token): + """Проверка организации по ИНН/ОГРН через DaData. + + Возвращает result с данными: полное/краткое наименование, юр. адрес, + КПП, ОГРН, дата ОГРН, руководитель. + """ + if not token: + return None, "Не задан API-ключ DaData (Файл → Настройки → API)" + inn = (inn or '').replace(' ', '') + if not inn or not inn.isdigit(): + return None, "Введите ИНН или ОГРН для проверки" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Token {token}', + } + js, err = _http_json(DADATA_URL, data={'query': inn}, headers=headers) + if err: + return None, err + suggestions = (js or {}).get('suggestions') or [] + if not suggestions: + return None, f"DaData: организация с ИНН/ОГРН {inn} не найдена" + s = suggestions[0] + d = s.get('data') or {} + name = d.get('name') or {} + address = d.get('address') or {} + management = d.get('management') or {} + ogrn_date = d.get('ogrn_date') or '' + + return { + 'source': 'DaData', + 'source_url': 'https://dadata.ru', + 'disclaimer': '', + 'kind': 'company', + 'query': inn, + 'data': { + 'full_name': name.get('full_with_opf') or s.get('value', ''), + 'short_name': name.get('short_with_opf') or name.get('short') or '', + 'address': address.get('value', ''), + 'kpp': d.get('kpp', ''), + 'ogrn': d.get('ogrn', ''), + 'ogrn_date': _fmt_date(ogrn_date), + 'chief_position': management.get('post', ''), + 'chief_fio': management.get('name', ''), + 'inn': d.get('inn', inn), + }, + 'apply': {'company': { + 'full_name': name.get('full_with_opf') or s.get('value', ''), + 'short_name': name.get('short_with_opf') or name.get('short') or '', + 'address': address.get('value', ''), + 'kpp': d.get('kpp', ''), + 'ogrn': d.get('ogrn', ''), + 'ogrn_date': _fmt_date(ogrn_date), + 'chief_position': management.get('post', ''), + 'chief_fio': management.get('name', ''), + }}, + }, None + + +def _fmt_date(date_str): + """YYYY-MM-DD → DD.MM.YYYY""" + if not date_str: + return '' + parts = str(date_str).split('-') + if len(parts) == 3 and all(p.isdigit() for p in parts): + return f"{parts[2]}.{parts[1]}.{parts[0]}" + return str(date_str) + + +# ============================================================ +# DEEPSEEK (ИИ) — рецензия склонений и реквизитов +# ============================================================ +def _deepseek_json(api_key, model, system, user): + if not api_key: + return None, "Не задан API-ключ DeepSeek (Файл → Настройки → API)" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', + } + payload = { + 'model': model or 'deepseek-chat', + 'messages': [ + {'role': 'system', 'content': system}, + {'role': 'user', 'content': user}, + ], + 'temperature': 0.2, + 'response_format': {'type': 'json_object'}, + } + js, err = _http_json(DEEPSEEK_URL, data=payload, headers=headers, timeout=60) + if err: + return None, err + try: + content = js['choices'][0]['message']['content'] + return json.loads(content), None + except Exception: + return None, "DeepSeek вернул неожиданный ответ" + + +AI_DISCLAIMER = "⚠ Сгенерировано ИИ (DeepSeek). Информацию рекомендуется перепроверить по официальным источникам." + + +def check_declension_ai(text, api_key, model='deepseek-chat'): + """ИИ-рецензия склонений: проверяет падежи и предлагает исправления.""" + if not text or not text.strip(): + return None, "Пустое значение для склонения" + system = ( + "Ты — эксперт по русскому языку. Склоняй слово/ФИО/название организации " + "по падежам. Верни ТОЛЬКО JSON: " + '{"И": "...", "Р": "...", "Д": "...", "В": "...", "Т": "...", "П": "...", "комментарий": "..."}' + ) + user = f"Просклоняй: {text.strip()}" + js, err = _deepseek_json(api_key, model, system, user) + if err: + return None, err + + decl = {} + for c in CASES: + val = js.get(c) or js.get(c.lower()) or '' + if isinstance(val, str) and val: + decl[c] = val + comment = js.get('комментарий', '') or '' + + if not decl: + return None, "ИИ не смог просклонять" + + return { + 'source': 'DeepSeek (ИИ)', + 'source_url': 'https://deepseek.com', + 'disclaimer': AI_DISCLAIMER, + 'kind': 'declension', + 'original': text.strip(), + 'data': decl, + 'comment': comment, + 'apply': {'declension': decl}, + }, None + + +def check_company_ai(company_data, api_key, model='deepseek-chat'): + """ИИ-рецензия реквизитов организации: проверяет заполненность и формат.""" + if not api_key: + return None, "Не задан API-ключ DeepSeek (Файл → Настройки → API)" + system = ( + "Ты — юрист по защите персональных данных (152-ФЗ). Проверь реквизиты организации: " + "полноту, формат ИНН (10/12 цифр), КПП (9 цифр), ОГРН (13 цифр), дату ОГРН, адрес. " + "Верни ТОЛЬКО JSON: " + '{"оценка": "ok|warning|error", "замечания": ["..."], ' + '"предложения": {"full_name": "...", "short_name": "...", "address": "...", ' + '"inn": "...", "kpp": "...", "ogrn": "...", "ogrn_date": "...", ' + '"chief_position": "...", "chief_fio": "..."}}' + "В предложениях указывай исправленные значения только если уверен, иначе пустые строки." + ) + user = json.dumps(company_data, ensure_ascii=False) + js, err = _deepseek_json(api_key, model, system, user) + if err: + return None, err + + notes = js.get('замечания') or [] + if isinstance(notes, str): + notes = [notes] + proposals = js.get('предложения') or {} + if not isinstance(proposals, dict): + proposals = {} + + return { + 'source': 'DeepSeek (ИИ)', + 'source_url': 'https://deepseek.com', + 'disclaimer': AI_DISCLAIMER, + 'kind': 'company', + 'data': { + 'rating': js.get('оценка', 'warning'), + 'notes': notes, + 'proposals': proposals, + }, + 'apply': {'company': {k: v for k, v in proposals.items() if v}}, + }, None