diff --git a/dokogen/api_settings.py b/dokogen/api_settings.py index 0f35feb..cb5eac0 100644 --- a/dokogen/api_settings.py +++ b/dokogen/api_settings.py @@ -3,9 +3,10 @@ Хранятся в JSON рядом с программой (api_settings.json). Поля: -- dadata_token — API-ключ DaData (проверка организации по ИНН) -- deepseek_api_key — API-ключ DeepSeek (ИИ-проверки) -- deepseek_model — модель DeepSeek (по умолчанию deepseek-chat) +- dadata_token — API-ключ DaData (проверка организации по ИНН) +- ai_api_key — API-ключ ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.) +- ai_model — модель ИИ (deepseek-chat, gpt-4o-mini, gpt-4o и т.п.) +- ai_base_url — адрес API ИИ (по умолчанию DeepSeek) """ import os @@ -14,8 +15,15 @@ import json DEFAULT_SETTINGS = { 'dadata_token': '', - 'deepseek_api_key': '', - 'deepseek_model': 'deepseek-chat', + 'ai_api_key': '', + 'ai_model': 'deepseek-chat', + 'ai_base_url': 'https://api.deepseek.com', +} + +# Старые ключи (для совместимости со старым файлом api_settings.json) +_LEGACY_KEYS = { + 'deepseek_api_key': 'ai_api_key', + 'deepseek_model': 'ai_model', } @@ -39,7 +47,10 @@ def load_api_settings(path=None): loaded = json.load(f) merged = dict(DEFAULT_SETTINGS) for k, v in loaded.items(): - if k in merged: + # переносим старые ключи в новые + if k in _LEGACY_KEYS and not loaded.get(_LEGACY_KEYS[k]): + merged[_LEGACY_KEYS[k]] = v + elif k in merged: merged[k] = v return merged except Exception: diff --git a/dokogen/ui.py b/dokogen/ui.py index a22dfb7..a8bca00 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -20,6 +20,7 @@ from . import ( from .verify import ( check_declension_morpher, check_company_dadata, check_declension_ai, check_company_ai, + COMPANY_FIELDS, ) from .api_settings import load_api_settings, save_api_settings @@ -273,13 +274,13 @@ class DokoGenApp: 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)", + ttk.Button(right, text="🤖 Склонение — ИИ", 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)", + ttk.Button(right, text="🤖 Реквизиты — ИИ", 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)) @@ -317,14 +318,17 @@ class DokoGenApp: 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 — ИИ-проверка склонений и реквизитов", + ttk.Label(api_tab, text="ИИ-проверка (OpenAI-совместимый API: DeepSeek, ChatGPT и др.)", 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.Label(api_tab, text="API-ключ:").pack(anchor=tk.W, pady=(6, 0)) + ds_var = tk.StringVar(value=settings.get('ai_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.Label(api_tab, text="Модель (например: deepseek-chat, gpt-4o-mini, gpt-4o):").pack(anchor=tk.W, pady=(6, 0)) + model_var = tk.StringVar(value=settings.get('ai_model', 'deepseek-chat')) ttk.Entry(api_tab, textvariable=model_var, width=64).pack(fill=tk.X, pady=2) + ttk.Label(api_tab, text="Базовый URL API (для DeepSeek можно оставить пустым):").pack(anchor=tk.W, pady=(6, 0)) + url_var = tk.StringVar(value=settings.get('ai_base_url', '')) + ttk.Entry(api_tab, textvariable=url_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)) @@ -332,8 +336,9 @@ class DokoGenApp: 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', + 'ai_api_key': ds_var.get().strip(), + 'ai_model': model_var.get().strip() or 'deepseek-chat', + 'ai_base_url': url_var.get().strip(), }) self.log("✅ Настройки API сохранены") win.destroy() @@ -355,49 +360,91 @@ class DokoGenApp: def _check_declension_web(self): name = self.company_name_entry.get().strip() - if not name: - messagebox.showwarning("Проверка", "Сначала введите полное название организации") + short = self.short_name_entry.get().strip() + if not name and not short: + 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)) + # Склоняем и полное, и краткое название (если заполнено) + results = [] + if name: + r1, e1 = check_declension_morpher(name) + if e1: + self.root.after(0, lambda: self._verify_log(f"❌ {e1}")) + return + results.append(('declension', r1)) + if short: + r2, e2 = check_declension_morpher(short) + if e2: + self.root.after(0, lambda: self._verify_log(f"❌ Краткое: {e2}")) + return + results.append(('short_declension', r2)) + self.root.after(0, lambda: self._verify_declension_done(results, None)) threading.Thread(target=_run, daemon=True).start() def _check_declension_ai(self): name = self.company_name_entry.get().strip() - if not name: - messagebox.showwarning("Проверка", "Сначала введите полное название организации") + short = self.short_name_entry.get().strip() + if not name and not short: + messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации") return settings = load_api_settings() - if not settings.get('deepseek_api_key'): - messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»") + if not settings.get('ai_api_key'): + messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»") return - self._verify_log("Запрос к DeepSeek (ИИ)…") + self._verify_log("Запрос к ИИ…") 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)) + results = [] + if name: + r1, e1 = check_declension_ai(name, settings['ai_api_key'], + settings.get('ai_model'), settings.get('ai_base_url')) + if e1: + self.root.after(0, lambda: self._verify_log(f"❌ {e1}")) + return + results.append(('declension', r1)) + if short: + r2, e2 = check_declension_ai(short, settings['ai_api_key'], + settings.get('ai_model'), settings.get('ai_base_url')) + if e2: + self.root.after(0, lambda: self._verify_log(f"❌ Краткое: {e2}")) + return + results.append(('short_declension', r2)) + self.root.after(0, lambda: self._verify_declension_done(results, None)) threading.Thread(target=_run, daemon=True).start() - def _verify_declension_done(self, result, err): + def _verify_declension_done(self, results, 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']}") + # Собираем единый результат для применения + combined = { + 'source': results[0][1]['source'] if results else 'ИИ', + 'source_url': results[0][1]['source_url'] if results else '', + 'disclaimer': results[0][1].get('disclaimer', '') if results else '', + 'kind': 'declension', + 'data': {}, + 'apply': {}, + } + lines = [f"Источник: {combined['source']} ({combined['source_url']})"] + if combined['disclaimer']: + lines.append(combined['disclaimer']) + for key, res in results: + label = "Полное название" if key == 'declension' else "Краткое название" + lines.append(f"\n{label}:") + for code, name_case in [('Р', 'Родительный'), ('Д', 'Дательный'), ('В', 'Винительный'), + ('Т', 'Творительный'), ('П', 'Предложный')]: + if code in res['data']: + lines.append(f" {name_case}: {res['data'][code]}") + if res.get('comment'): + lines.append(f" Комментарий: {res['comment']}") + combined['apply'][key] = res['apply']['declension'] lines.append("\nНажмите «✓ Применить», чтобы подставить склонения") + self._verify_result = combined self._verify_log('\n'.join(lines)) def _check_company_web(self): @@ -422,8 +469,8 @@ class DokoGenApp: self._collect_company() c = self.model settings = load_api_settings() - if not settings.get('deepseek_api_key'): - messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»") + if not settings.get('ai_api_key'): + messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»") return company_data = { 'full_name': c.full_name, 'short_name': c.short_name, @@ -431,10 +478,11 @@ class DokoGenApp: 'ogrn_date': c.ogrn_date, 'chief_position': c.chief_position, 'chief_fio': c.chief_fio, } - self._verify_log("Запрос к DeepSeek (ИИ)…") + self._verify_log("Запрос к ИИ…") def _run(): - result, err = check_company_ai(company_data, settings['deepseek_api_key'], settings.get('deepseek_model')) + result, err = check_company_ai(company_data, settings['ai_api_key'], + settings.get('ai_model'), settings.get('ai_base_url')) self.root.after(0, lambda: self._verify_company_done(result, err)) threading.Thread(target=_run, daemon=True).start() @@ -457,16 +505,26 @@ class DokoGenApp: if d.get(key): lines.append(f"{label}: {d[key]}") else: - # ИИ-рецензия + # ИИ-рецензия: показываем снимок полей (что ИИ увидел), замечания и предложения lines.append(f"Оценка: {d.get('rating', '')}") + fields = d.get('fields') or {} + if fields: + lines.append("\nПроверенные поля:") + for key, label in COMPANY_FIELDS: + info = fields.get(key) + if isinstance(info, dict): + status = info.get('статус', '') + comment = info.get('комментарий', '') + lines.append(f" {label}: {status}" + (f" — {comment}" if comment else "")) 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(): + lines.append("\nПредложения ИИ:") + for key, label in COMPANY_FIELDS: + v = props.get(key) if v: - lines.append(f" {k}: {v}") + lines.append(f" {label}: {v}") lines.append("\nНажмите «✓ Применить», чтобы подставить данные") self._verify_log('\n'.join(lines)) @@ -475,15 +533,23 @@ class DokoGenApp: if not result: return apply_data = result.get('apply', {}) + idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6} 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("✅ Склонения применены") + self.log("✅ Склонения полного названия применены") + if 'short_declension' in apply_data: + for case, val in apply_data['short_declension'].items(): + code = self._CASE_TO_CODE.get(case) + if code and code in self.short_decl_entries: + self.short_decl_entries[code].delete(0, tk.END) + self.short_decl_entries[code].insert(0, val) + setattr(self.model, f'short_name_{idx_map[code]}', val) + self.log("✅ Склонения краткого названия применены") if 'company' in apply_data: comp = apply_data['company'] if comp.get('full_name'): diff --git a/dokogen/verify.py b/dokogen/verify.py index 4673ee1..7af0ed6 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -5,8 +5,8 @@ - Морфер 3.0 (morpher.ru) — склонение ФИО/названий по падежам. Бесплатно, без ключа. - DaData (dadata.ru) — проверка организации по ИНН/ОГРН (наименование, адрес, КПП). Нужен API-ключ (бесплатный тариф ~10 000 запросов/день). -- DeepSeek (deepseek.com) — ИИ-проверка: рецензия склонений и реквизитов. - Нужен API-ключ. Данные ИИ ВСЕГДА помечаются дисклеймером. +- ИИ (OpenAI-совместимый) — ИИ-проверка склонений и реквизитов (DeepSeek, ChatGPT и др.). + Нужен API-ключ и модель. Данные ИИ ВСЕГДА помечаются дисклеймером. Каждая функция возвращает (result, error): - result: dict с полями {source, source_url, data, disclaimer, apply} @@ -14,13 +14,15 @@ """ import json +import re +from datetime import datetime, timedelta 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" +DEFAULT_AI_URL = "https://api.deepseek.com/chat/completions" CASES = ['И', 'Р', 'Д', 'В', 'Т', 'П'] CASE_NAMES = { @@ -49,15 +51,96 @@ def _http_json(url, data=None, headers=None, timeout=30): return None, str(e) +# ============================================================ +# НОРМАЛИЗАЦИЯ ТЕКСТА (КАПС → нормальный регистр, сокращения адреса) +# ============================================================ +_CAPS_ABBR = { + 'АРМ', 'СКУД', 'СЗИ', 'СКЗИ', 'СОВ', 'ЛВС', 'ИНН', 'КПП', 'ОГРН', 'ЦОД', + 'МБУ', 'МКУ', 'ООО', 'СНИЛС', 'СБИС', 'ЕМСЭД', 'ФИО', 'ИСПДН', 'ККТ', +} + +_ADDRESS_ABBR = { + 'Г': 'г.', 'УЛ': 'ул.', 'Д': 'д.', 'КВ': 'кв.', 'ПР': 'пр.', + 'ПЕР': 'пер.', 'ПЛ': 'пл.', 'ОБЛ': 'обл.', 'ПОС': 'пос.', + 'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.', + 'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.', + 'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.', +} + + +def _normalize_text(text): + """КАПС → нормальный регистр по словам (сохраняя аббревиатуры и числа).""" + if not text: + return '' + words = str(text).split() + out = [] + for w in words: + pre = '' + post = '' + core = w + while core and not core[0].isalnum(): + pre += core[0] + core = core[1:] + while core and not core[-1].isalnum(): + post = core[-1] + post + core = core[:-1] + if not core: + out.append(w) + continue + if any('а' <= c <= 'я' or c == 'ё' for c in core): + out.append(w) + continue + if not any('А' <= c <= 'Я' or c == 'Ё' for c in core): + out.append(w) + continue + letters = [c for c in core if c.isalpha()] + if len(letters) <= 2: + out.append(w) + continue + if core.upper() in _CAPS_ABBR: + out.append(w) + continue + out.append(pre + core[0].upper() + core[1:].lower() + post) + return ' '.join(out) + + +def normalize_address(text): + """«Г Крымск» → «г. Крымск», «Р-Н» → «р-н.» и т.п.""" + if not text: + return '' + parts = re.split(r'(\s+|[,\;])', text) + out = [] + for p in parts: + if p.strip() and p.strip().upper() in _ADDRESS_ABBR: + out.append(_ADDRESS_ABBR[p.strip().upper()]) + else: + out.append(p) + return ''.join(out) + + +def _fmt_date(value): + """Дата: '2005-02-09' → '09.02.2005'; миллисекунды 1107907200000 → дата.""" + if not value: + return '' + # миллисекунды с эпохи (DaData иногда отдаёт так) + if isinstance(value, (int, float)) and value > 10 ** 10: + try: + dt = datetime(1970, 1, 1) + timedelta(milliseconds=value) + return dt.strftime('%d.%m.%Y') + except Exception: + pass + s = str(value).strip() + parts = s.split('-') + if len(parts) == 3 and all(p.isdigit() for p in parts): + return f"{parts[2]}.{parts[1]}.{parts[0]}" + return s + + # ============================================================ # МОРФЕР 3.0 — склонение (бесплатно, без ключа) # ============================================================ def check_declension_morpher(text): - """Склонение слова/ФИО/названия через Морфер 3.0. - - Возвращает result с полем data: {case_code: слово} и apply-инструкцией - для заполнения полей склонений в UI. - """ + """Склонение слова/ФИО/названия через Морфер 3.0.""" if not text or not text.strip(): return None, "Пустое значение для склонения" url = MORPHER_URL + '?' + urllib.parse.urlencode({'s': text.strip(), 'format': 'json'}) @@ -93,8 +176,8 @@ def check_declension_morpher(text): def check_company_dadata(inn, token): """Проверка организации по ИНН/ОГРН через DaData. - Возвращает result с данными: полное/краткое наименование, юр. адрес, - КПП, ОГРН, дата ОГРН, руководитель. + Данные нормализуются: регистр (КАПС → обычный), адрес (г., ул., д.), + дата ОГРН (в т.ч. из миллисекунд). """ if not token: return None, "Не задан API-ключ DaData (Файл → Настройки → API)" @@ -116,7 +199,13 @@ def check_company_dadata(inn, token): name = d.get('name') or {} address = d.get('address') or {} management = d.get('management') or {} - ogrn_date = d.get('ogrn_date') or '' + + full_name = _normalize_text(name.get('full_with_opf') or s.get('value', '')) + short_name = _normalize_text(name.get('short_with_opf') or name.get('short') or '') + addr = normalize_address(address.get('value', '')) + chief_position = _normalize_text(management.get('post', '')) + chief_fio = _normalize_text(management.get('name', '')) + ogrn_date = _fmt_date(d.get('ogrn_date') or '') return { 'source': 'DaData', @@ -125,45 +214,43 @@ def check_company_dadata(inn, token): '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', ''), + 'full_name': full_name, + 'short_name': short_name, + 'address': addr, 'kpp': d.get('kpp', ''), 'ogrn': d.get('ogrn', ''), - 'ogrn_date': _fmt_date(ogrn_date), - 'chief_position': management.get('post', ''), - 'chief_fio': management.get('name', ''), + 'ogrn_date': ogrn_date, + 'chief_position': chief_position, + 'chief_fio': chief_fio, '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', ''), + 'full_name': full_name, + 'short_name': short_name, + 'address': addr, 'kpp': d.get('kpp', ''), 'ogrn': d.get('ogrn', ''), - 'ogrn_date': _fmt_date(ogrn_date), - 'chief_position': management.get('post', ''), - 'chief_fio': management.get('name', ''), + 'ogrn_date': ogrn_date, + 'chief_position': chief_position, + 'chief_fio': chief_fio, }}, }, 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) +# ============================================================ +# ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.) +# ============================================================ +AI_DISCLAIMER = ("⚠ Сгенерировано ИИ. Информацию рекомендуется перепроверить " + "по официальным источникам.") -# ============================================================ -# DEEPSEEK (ИИ) — рецензия склонений и реквизитов -# ============================================================ -def _deepseek_json(api_key, model, system, user): +def _ai_json(api_key, model, base_url, system, user, timeout=60): + """Запрос к OpenAI-совместимому API (DeepSeek, ChatGPT и т.п.).""" if not api_key: - return None, "Не задан API-ключ DeepSeek (Файл → Настройки → API)" + return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" + url = (base_url or DEFAULT_AI_URL).rstrip('/') + if not url.endswith('/chat/completions'): + url += '/chat/completions' headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}', @@ -177,20 +264,17 @@ def _deepseek_json(api_key, model, system, user): 'temperature': 0.2, 'response_format': {'type': 'json_object'}, } - js, err = _http_json(DEEPSEEK_URL, data=payload, headers=headers, timeout=60) + js, err = _http_json(url, data=payload, headers=headers, timeout=timeout) if err: return None, err try: content = js['choices'][0]['message']['content'] return json.loads(content), None except Exception: - return None, "DeepSeek вернул неожиданный ответ" + return None, "ИИ вернул неожиданный ответ" -AI_DISCLAIMER = "⚠ Сгенерировано ИИ (DeepSeek). Информацию рекомендуется перепроверить по официальным источникам." - - -def check_declension_ai(text, api_key, model='deepseek-chat'): +def check_declension_ai(text, api_key, model='deepseek-chat', base_url=None): """ИИ-рецензия склонений: проверяет падежи и предлагает исправления.""" if not text or not text.strip(): return None, "Пустое значение для склонения" @@ -200,7 +284,7 @@ def check_declension_ai(text, api_key, model='deepseek-chat'): '{"И": "...", "Р": "...", "Д": "...", "В": "...", "Т": "...", "П": "...", "комментарий": "..."}' ) user = f"Просклоняй: {text.strip()}" - js, err = _deepseek_json(api_key, model, system, user) + js, err = _ai_json(api_key, model, base_url, system, user) if err: return None, err @@ -215,8 +299,8 @@ def check_declension_ai(text, api_key, model='deepseek-chat'): return None, "ИИ не смог просклонять" return { - 'source': 'DeepSeek (ИИ)', - 'source_url': 'https://deepseek.com', + 'source': 'ИИ', + 'source_url': 'openai-совместимый API', 'disclaimer': AI_DISCLAIMER, 'kind': 'declension', 'original': text.strip(), @@ -226,22 +310,43 @@ def check_declension_ai(text, api_key, model='deepseek-chat'): }, None -def check_company_ai(company_data, api_key, model='deepseek-chat'): - """ИИ-рецензия реквизитов организации: проверяет заполненность и формат.""" +# Поля организации для ИИ-проверки (для читаемого вывода) +COMPANY_FIELDS = [ + ('full_name', 'Полное наименование'), + ('short_name', 'Краткое наименование'), + ('address', 'Юридический адрес'), + ('inn', 'ИНН'), + ('kpp', 'КПП'), + ('ogrn', 'ОГРН'), + ('ogrn_date', 'Дата ОГРН'), + ('chief_position', 'Руководитель (должность)'), + ('chief_fio', 'Руководитель (ФИО)'), +] + + +def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None): + """ИИ-проверка реквизитов организации. + + Возвращает оценку, замечания, предложения и полный «снимок» данных + (что ИИ увидел и как оценил каждое поле) — чтобы пользователь видел + не только «ОК», но и информацию по организации. + """ if not api_key: - return None, "Не задан API-ключ DeepSeek (Файл → Настройки → API)" + return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" system = ( - "Ты — юрист по защите персональных данных (152-ФЗ). Проверь реквизиты организации: " - "полноту, формат ИНН (10/12 цифр), КПП (9 цифр), ОГРН (13 цифр), дату ОГРН, адрес. " + "Ты — юрист по защите персональных данных (152-ФЗ). Проверь реквизиты организации. " "Верни ТОЛЬКО JSON: " - '{"оценка": "ok|warning|error", "замечания": ["..."], ' + '{"оценка": "ok|warning|error", ' + '"замечания": ["..."], ' '"предложения": {"full_name": "...", "short_name": "...", "address": "...", ' '"inn": "...", "kpp": "...", "ogrn": "...", "ogrn_date": "...", ' - '"chief_position": "...", "chief_fio": "..."}}' - "В предложениях указывай исправленные значения только если уверен, иначе пустые строки." + '"chief_position": "...", "chief_fio": "..."}, ' + '"поля": {"full_name": {"статус": "ok|пусто|ошибка", "комментарий": "..."}, ...}} ' + "В «предложениях» указывай исправленные значения только если уверен, иначе пустые строки. " + "В «полях» дай оценку каждого поля (ok/пусто/ошибка) и комментарий." ) user = json.dumps(company_data, ensure_ascii=False) - js, err = _deepseek_json(api_key, model, system, user) + js, err = _ai_json(api_key, model, base_url, system, user) if err: return None, err @@ -251,16 +356,20 @@ def check_company_ai(company_data, api_key, model='deepseek-chat'): proposals = js.get('предложения') or {} if not isinstance(proposals, dict): proposals = {} + fields = js.get('поля') or {} + if not isinstance(fields, dict): + fields = {} return { - 'source': 'DeepSeek (ИИ)', - 'source_url': 'https://deepseek.com', + 'source': 'ИИ', + 'source_url': 'openai-совместимый API', 'disclaimer': AI_DISCLAIMER, 'kind': 'company', 'data': { 'rating': js.get('оценка', 'warning'), 'notes': notes, 'proposals': proposals, + 'fields': fields, }, 'apply': {'company': {k: v for k, v in proposals.items() if v}}, }, None