feat: проверка данных через Морфер/DaData/DeepSeek (ИИ)
- правая панель на вкладке «Организация»: проверка склонений (Морфер 3.0 — бесплатно, без ключа; DeepSeek — ИИ) и реквизитов (DaData по ИНН; DeepSeek — ИИ-рецензия) - мини-табло результата: источник, дисклеймер для ИИ («сгенерировано ИИ — перепроверьте»), кнопки «Применить»/«Очистить» - меню «Файл → Настройки…»: вкладка API-ключей (DaData, DeepSeek, модель), сохраняются в api_settings.json рядом с программой - веб-проверки показывают источник (morpher.ru / dadata.ru)
This commit is contained in:
@@ -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
|
||||
+255
-4
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user