fix: проверка информации — 4 замечания

1) ИИ универсален: вместо DeepSeek — «ИИ» (OpenAI-совместимый:
   DeepSeek, ChatGPT и др.). В настройках: API-ключ, модель, базовый URL
2) склонение проверяет И полное, И краткое название (Морфер и ИИ)
3) ИИ-проверка реквизитов показывает снимок полей (статус каждого),
   замечания и предложения — а не просто «ОК»
4) DaData: регистр нормализуется (КАПС → обычный), адрес (г., ул., д.),
   дата ОГРН из миллисекунд → 09.02.2005
- совместимость со старым api_settings.json (deepseek_* → ai_*)
This commit is contained in:
2026-08-03 13:29:09 +04:00
parent 9d2690e3e0
commit 0b540f093b
3 changed files with 289 additions and 103 deletions
+16 -5
View File
@@ -4,8 +4,9 @@
Хранятся в JSON рядом с программой (api_settings.json). Хранятся в JSON рядом с программой (api_settings.json).
Поля: Поля:
- dadata_token — API-ключ DaData (проверка организации по ИНН) - dadata_token — API-ключ DaData (проверка организации по ИНН)
- deepseek_api_key — API-ключ DeepSeek (ИИ-проверки) - ai_api_key — API-ключ ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.)
- deepseek_model — модель DeepSeek (по умолчанию deepseek-chat) - ai_model — модель ИИ (deepseek-chat, gpt-4o-mini, gpt-4o и т.п.)
- ai_base_url — адрес API ИИ (по умолчанию DeepSeek)
""" """
import os import os
@@ -14,8 +15,15 @@ import json
DEFAULT_SETTINGS = { DEFAULT_SETTINGS = {
'dadata_token': '', 'dadata_token': '',
'deepseek_api_key': '', 'ai_api_key': '',
'deepseek_model': 'deepseek-chat', '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) loaded = json.load(f)
merged = dict(DEFAULT_SETTINGS) merged = dict(DEFAULT_SETTINGS)
for k, v in loaded.items(): 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 merged[k] = v
return merged return merged
except Exception: except Exception:
+106 -40
View File
@@ -20,6 +20,7 @@ from . import (
from .verify import ( from .verify import (
check_declension_morpher, check_company_dadata, check_declension_morpher, check_company_dadata,
check_declension_ai, check_company_ai, check_declension_ai, check_company_ai,
COMPANY_FIELDS,
) )
from .api_settings import load_api_settings, save_api_settings 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.Label(right, text="Склонение названия:", font=('', 9, 'bold')).pack(anchor=tk.W)
ttk.Button(right, text="🌐 Склонение — Морфер 3.0", ttk.Button(right, text="🌐 Склонение — Морфер 3.0",
command=self._check_declension_web).pack(fill=tk.X, pady=2) 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) 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.Label(right, text="Реквизиты организации:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(10, 0))
ttk.Button(right, text="🌐 Реквизиты — DaData", ttk.Button(right, text="🌐 Реквизиты — DaData",
command=self._check_company_web).pack(fill=tk.X, pady=2) 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) 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)) 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', '')) 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.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)) 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)) ttk.Label(api_tab, text="API-ключ:").pack(anchor=tk.W, pady=(6, 0))
ds_var = tk.StringVar(value=settings.get('deepseek_api_key', '')) 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.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)) 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('deepseek_model', 'deepseek-chat')) 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.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 работает без ключа (бесплатный склонятор).", ttk.Label(api_tab, text="Морфер 3.0 работает без ключа (бесплатный склонятор).",
foreground='gray').pack(anchor=tk.W, pady=(14, 0)) foreground='gray').pack(anchor=tk.W, pady=(14, 0))
@@ -332,8 +336,9 @@ class DokoGenApp:
def _save(): def _save():
save_api_settings({ save_api_settings({
'dadata_token': dadata_var.get().strip(), 'dadata_token': dadata_var.get().strip(),
'deepseek_api_key': ds_var.get().strip(), 'ai_api_key': ds_var.get().strip(),
'deepseek_model': model_var.get().strip() or 'deepseek-chat', 'ai_model': model_var.get().strip() or 'deepseek-chat',
'ai_base_url': url_var.get().strip(),
}) })
self.log("✅ Настройки API сохранены") self.log("✅ Настройки API сохранены")
win.destroy() win.destroy()
@@ -355,49 +360,91 @@ class DokoGenApp:
def _check_declension_web(self): def _check_declension_web(self):
name = self.company_name_entry.get().strip() name = self.company_name_entry.get().strip()
if not name: short = self.short_name_entry.get().strip()
messagebox.showwarning("Проверка", "Сначала введите полное название организации") if not name and not short:
messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
return return
self._verify_log("Запрос к Морфер 3.0…") self._verify_log("Запрос к Морфер 3.0…")
def _run(): 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() threading.Thread(target=_run, daemon=True).start()
def _check_declension_ai(self): def _check_declension_ai(self):
name = self.company_name_entry.get().strip() name = self.company_name_entry.get().strip()
if not name: short = self.short_name_entry.get().strip()
messagebox.showwarning("Проверка", "Сначала введите полное название организации") if not name and not short:
messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
return return
settings = load_api_settings() settings = load_api_settings()
if not settings.get('deepseek_api_key'): if not settings.get('ai_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»") messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
return return
self._verify_log("Запрос к DeepSeek (ИИ)") self._verify_log("Запрос к ИИ…")
def _run(): def _run():
result, err = check_declension_ai(name, settings['deepseek_api_key'], settings.get('deepseek_model')) results = []
self.root.after(0, lambda: self._verify_declension_done(result, err)) 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() threading.Thread(target=_run, daemon=True).start()
def _verify_declension_done(self, result, err): def _verify_declension_done(self, results, err):
if err: if err:
self._verify_log(f"{err}") self._verify_log(f"{err}")
return return
self._verify_result = result # Собираем единый результат для применения
lines = [f"Источник: {result['source']} ({result['source_url']})"] combined = {
if result.get('disclaimer'): 'source': results[0][1]['source'] if results else 'ИИ',
lines.append(result['disclaimer']) 'source_url': results[0][1]['source_url'] if results else '',
for code, label in [('Р', 'Родительный'), ('Д', 'Дательный'), ('В', 'Винительный'), '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 result['data']: if code in res['data']:
lines.append(f"{label}: {result['data'][code]}") lines.append(f" {name_case}: {res['data'][code]}")
if result.get('comment'): if res.get('comment'):
lines.append(f"Комментарий: {result['comment']}") lines.append(f" Комментарий: {res['comment']}")
combined['apply'][key] = res['apply']['declension']
lines.append("\nНажмите «✓ Применить», чтобы подставить склонения") lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
self._verify_result = combined
self._verify_log('\n'.join(lines)) self._verify_log('\n'.join(lines))
def _check_company_web(self): def _check_company_web(self):
@@ -422,8 +469,8 @@ class DokoGenApp:
self._collect_company() self._collect_company()
c = self.model c = self.model
settings = load_api_settings() settings = load_api_settings()
if not settings.get('deepseek_api_key'): if not settings.get('ai_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ DeepSeek.\nМеню «Файл → Настройки»") messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
return return
company_data = { company_data = {
'full_name': c.full_name, 'short_name': c.short_name, 'full_name': c.full_name, 'short_name': c.short_name,
@@ -431,10 +478,11 @@ class DokoGenApp:
'ogrn_date': c.ogrn_date, 'ogrn_date': c.ogrn_date,
'chief_position': c.chief_position, 'chief_fio': c.chief_fio, 'chief_position': c.chief_position, 'chief_fio': c.chief_fio,
} }
self._verify_log("Запрос к DeepSeek (ИИ)") self._verify_log("Запрос к ИИ…")
def _run(): 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)) self.root.after(0, lambda: self._verify_company_done(result, err))
threading.Thread(target=_run, daemon=True).start() threading.Thread(target=_run, daemon=True).start()
@@ -457,16 +505,26 @@ class DokoGenApp:
if d.get(key): if d.get(key):
lines.append(f"{label}: {d[key]}") lines.append(f"{label}: {d[key]}")
else: else:
# ИИ-рецензия # ИИ-рецензия: показываем снимок полей (что ИИ увидел), замечания и предложения
lines.append(f"Оценка: {d.get('rating', '')}") 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 []): for n in (d.get('notes') or []):
lines.append(f"{n}") lines.append(f"{n}")
props = d.get('proposals') or {} props = d.get('proposals') or {}
if any(props.values()): if any(props.values()):
lines.append("Предложения:") lines.append("\nПредложения ИИ:")
for k, v in props.items(): for key, label in COMPANY_FIELDS:
v = props.get(key)
if v: if v:
lines.append(f" {k}: {v}") lines.append(f" {label}: {v}")
lines.append("\nНажмите «✓ Применить», чтобы подставить данные") lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
self._verify_log('\n'.join(lines)) self._verify_log('\n'.join(lines))
@@ -475,15 +533,23 @@ class DokoGenApp:
if not result: if not result:
return return
apply_data = result.get('apply', {}) apply_data = result.get('apply', {})
if 'declension' in apply_data:
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6} idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
if 'declension' in apply_data:
for case, val in apply_data['declension'].items(): for case, val in apply_data['declension'].items():
code = self._CASE_TO_CODE.get(case) code = self._CASE_TO_CODE.get(case)
if code and code in self.decl_entries: if code and code in self.decl_entries:
self.decl_entries[code].delete(0, tk.END) self.decl_entries[code].delete(0, tk.END)
self.decl_entries[code].insert(0, val) self.decl_entries[code].insert(0, val)
setattr(self.model, f'company_name_{idx_map[code]}', 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: if 'company' in apply_data:
comp = apply_data['company'] comp = apply_data['company']
if comp.get('full_name'): if comp.get('full_name'):
+165 -56
View File
@@ -5,8 +5,8 @@
- Морфер 3.0 (morpher.ru) склонение ФИО/названий по падежам. Бесплатно, без ключа. - Морфер 3.0 (morpher.ru) склонение ФИО/названий по падежам. Бесплатно, без ключа.
- DaData (dadata.ru) проверка организации по ИНН/ОГРН (наименование, адрес, КПП). - DaData (dadata.ru) проверка организации по ИНН/ОГРН (наименование, адрес, КПП).
Нужен API-ключ (бесплатный тариф ~10 000 запросов/день). Нужен API-ключ (бесплатный тариф ~10 000 запросов/день).
- DeepSeek (deepseek.com) ИИ-проверка: рецензия склонений и реквизитов. - ИИ (OpenAI-совместимый) ИИ-проверка склонений и реквизитов (DeepSeek, ChatGPT и др.).
Нужен API-ключ. Данные ИИ ВСЕГДА помечаются дисклеймером. Нужен API-ключ и модель. Данные ИИ ВСЕГДА помечаются дисклеймером.
Каждая функция возвращает (result, error): Каждая функция возвращает (result, error):
- result: dict с полями {source, source_url, data, disclaimer, apply} - result: dict с полями {source, source_url, data, disclaimer, apply}
@@ -14,13 +14,15 @@
""" """
import json import json
import re
from datetime import datetime, timedelta
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import urllib.error import urllib.error
MORPHER_URL = "https://ws3.morpher.ru/russian/declension" MORPHER_URL = "https://ws3.morpher.ru/russian/declension"
DADATA_URL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party" 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 = ['И', 'Р', 'Д', 'В', 'Т', 'П'] CASES = ['И', 'Р', 'Д', 'В', 'Т', 'П']
CASE_NAMES = { CASE_NAMES = {
@@ -49,15 +51,96 @@ def _http_json(url, data=None, headers=None, timeout=30):
return None, str(e) 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 — склонение (бесплатно, без ключа) # МОРФЕР 3.0 — склонение (бесплатно, без ключа)
# ============================================================ # ============================================================
def check_declension_morpher(text): def check_declension_morpher(text):
"""Склонение слова/ФИО/названия через Морфер 3.0. """Склонение слова/ФИО/названия через Морфер 3.0."""
Возвращает result с полем data: {case_code: слово} и apply-инструкцией
для заполнения полей склонений в UI.
"""
if not text or not text.strip(): if not text or not text.strip():
return None, "Пустое значение для склонения" return None, "Пустое значение для склонения"
url = MORPHER_URL + '?' + urllib.parse.urlencode({'s': text.strip(), 'format': 'json'}) 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): def check_company_dadata(inn, token):
"""Проверка организации по ИНН/ОГРН через DaData. """Проверка организации по ИНН/ОГРН через DaData.
Возвращает result с данными: полное/краткое наименование, юр. адрес, Данные нормализуются: регистр (КАПС обычный), адрес (г., ул., д.),
КПП, ОГРН, дата ОГРН, руководитель. дата ОГРН (в т.ч. из миллисекунд).
""" """
if not token: if not token:
return None, "Не задан API-ключ DaData (Файл → Настройки → API)" return None, "Не задан API-ключ DaData (Файл → Настройки → API)"
@@ -116,7 +199,13 @@ def check_company_dadata(inn, token):
name = d.get('name') or {} name = d.get('name') or {}
address = d.get('address') or {} address = d.get('address') or {}
management = d.get('management') 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 { return {
'source': 'DaData', 'source': 'DaData',
@@ -125,45 +214,43 @@ def check_company_dadata(inn, token):
'kind': 'company', 'kind': 'company',
'query': inn, 'query': inn,
'data': { 'data': {
'full_name': name.get('full_with_opf') or s.get('value', ''), 'full_name': full_name,
'short_name': name.get('short_with_opf') or name.get('short') or '', 'short_name': short_name,
'address': address.get('value', ''), 'address': addr,
'kpp': d.get('kpp', ''), 'kpp': d.get('kpp', ''),
'ogrn': d.get('ogrn', ''), 'ogrn': d.get('ogrn', ''),
'ogrn_date': _fmt_date(ogrn_date), 'ogrn_date': ogrn_date,
'chief_position': management.get('post', ''), 'chief_position': chief_position,
'chief_fio': management.get('name', ''), 'chief_fio': chief_fio,
'inn': d.get('inn', inn), 'inn': d.get('inn', inn),
}, },
'apply': {'company': { 'apply': {'company': {
'full_name': name.get('full_with_opf') or s.get('value', ''), 'full_name': full_name,
'short_name': name.get('short_with_opf') or name.get('short') or '', 'short_name': short_name,
'address': address.get('value', ''), 'address': addr,
'kpp': d.get('kpp', ''), 'kpp': d.get('kpp', ''),
'ogrn': d.get('ogrn', ''), 'ogrn': d.get('ogrn', ''),
'ogrn_date': _fmt_date(ogrn_date), 'ogrn_date': ogrn_date,
'chief_position': management.get('post', ''), 'chief_position': chief_position,
'chief_fio': management.get('name', ''), 'chief_fio': chief_fio,
}}, }},
}, None }, None
def _fmt_date(date_str): # ============================================================
"""YYYY-MM-DD → DD.MM.YYYY""" # ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.)
if not date_str: # ============================================================
return '' AI_DISCLAIMER = ("⚠ Сгенерировано ИИ. Информацию рекомендуется перепроверить "
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)
# ============================================================ def _ai_json(api_key, model, base_url, system, user, timeout=60):
# DEEPSEEK (ИИ) — рецензия склонений и реквизитов """Запрос к OpenAI-совместимому API (DeepSeek, ChatGPT и т.п.)."""
# ============================================================
def _deepseek_json(api_key, model, system, user):
if not api_key: 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 = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}', 'Authorization': f'Bearer {api_key}',
@@ -177,20 +264,17 @@ def _deepseek_json(api_key, model, system, user):
'temperature': 0.2, 'temperature': 0.2,
'response_format': {'type': 'json_object'}, '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: if err:
return None, err return None, err
try: try:
content = js['choices'][0]['message']['content'] content = js['choices'][0]['message']['content']
return json.loads(content), None return json.loads(content), None
except Exception: except Exception:
return None, "DeepSeek вернул неожиданный ответ" return None, "ИИ вернул неожиданный ответ"
AI_DISCLAIMER = "⚠ Сгенерировано ИИ (DeepSeek). Информацию рекомендуется перепроверить по официальным источникам." def check_declension_ai(text, api_key, model='deepseek-chat', base_url=None):
def check_declension_ai(text, api_key, model='deepseek-chat'):
"""ИИ-рецензия склонений: проверяет падежи и предлагает исправления.""" """ИИ-рецензия склонений: проверяет падежи и предлагает исправления."""
if not text or not text.strip(): if not text or not text.strip():
return None, "Пустое значение для склонения" return None, "Пустое значение для склонения"
@@ -200,7 +284,7 @@ def check_declension_ai(text, api_key, model='deepseek-chat'):
'{"И": "...", "Р": "...", "Д": "...", "В": "...", "Т": "...", "П": "...", "комментарий": "..."}' '{"И": "...", "Р": "...", "Д": "...", "В": "...", "Т": "...", "П": "...", "комментарий": "..."}'
) )
user = f"Просклоняй: {text.strip()}" 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: if err:
return None, err return None, err
@@ -215,8 +299,8 @@ def check_declension_ai(text, api_key, model='deepseek-chat'):
return None, "ИИ не смог просклонять" return None, "ИИ не смог просклонять"
return { return {
'source': 'DeepSeek (ИИ)', 'source': 'ИИ',
'source_url': 'https://deepseek.com', 'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER, 'disclaimer': AI_DISCLAIMER,
'kind': 'declension', 'kind': 'declension',
'original': text.strip(), 'original': text.strip(),
@@ -226,22 +310,43 @@ def check_declension_ai(text, api_key, model='deepseek-chat'):
}, None }, 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: if not api_key:
return None, "Не задан API-ключ DeepSeek (Файл → Настройки → API)" return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
system = ( system = (
"Ты — юрист по защите персональных данных (152-ФЗ). Проверь реквизиты организации: " "Ты — юрист по защите персональных данных (152-ФЗ). Проверь реквизиты организации. "
"полноту, формат ИНН (10/12 цифр), КПП (9 цифр), ОГРН (13 цифр), дату ОГРН, адрес. "
"Верни ТОЛЬКО JSON: " "Верни ТОЛЬКО JSON: "
'{"оценка": "ok|warning|error", "замечания": ["..."], ' '{"оценка": "ok|warning|error", '
'"замечания": ["..."], '
'"предложения": {"full_name": "...", "short_name": "...", "address": "...", ' '"предложения": {"full_name": "...", "short_name": "...", "address": "...", '
'"inn": "...", "kpp": "...", "ogrn": "...", "ogrn_date": "...", ' '"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) 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: if err:
return None, err return None, err
@@ -251,16 +356,20 @@ def check_company_ai(company_data, api_key, model='deepseek-chat'):
proposals = js.get('предложения') or {} proposals = js.get('предложения') or {}
if not isinstance(proposals, dict): if not isinstance(proposals, dict):
proposals = {} proposals = {}
fields = js.get('поля') or {}
if not isinstance(fields, dict):
fields = {}
return { return {
'source': 'DeepSeek (ИИ)', 'source': 'ИИ',
'source_url': 'https://deepseek.com', 'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER, 'disclaimer': AI_DISCLAIMER,
'kind': 'company', 'kind': 'company',
'data': { 'data': {
'rating': js.get('оценка', 'warning'), 'rating': js.get('оценка', 'warning'),
'notes': notes, 'notes': notes,
'proposals': proposals, 'proposals': proposals,
'fields': fields,
}, },
'apply': {'company': {k: v for k, v in proposals.items() if v}}, 'apply': {'company': {k: v for k, v in proposals.items() if v}},
}, None }, None