Files
dokogen152/dokogen/verify.py
T
prog1764 2f4a08b2e4 feat: проверка данных через Морфер/DaData/DeepSeek (ИИ)
- правая панель на вкладке «Организация»: проверка склонений
  (Морфер 3.0 — бесплатно, без ключа; DeepSeek — ИИ) и реквизитов
  (DaData по ИНН; DeepSeek — ИИ-рецензия)
- мини-табло результата: источник, дисклеймер для ИИ («сгенерировано
  ИИ — перепроверьте»), кнопки «Применить»/«Очистить»
- меню «Файл → Настройки…»: вкладка API-ключей (DaData, DeepSeek,
  модель), сохраняются в api_settings.json рядом с программой
- веб-проверки показывают источник (morpher.ru / dadata.ru)
2026-08-03 12:59:51 +04:00

267 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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