- правая панель на вкладке «Организация»: проверка склонений (Морфер 3.0 — бесплатно, без ключа; DeepSeek — ИИ) и реквизитов (DaData по ИНН; DeepSeek — ИИ-рецензия) - мини-табло результата: источник, дисклеймер для ИИ («сгенерировано ИИ — перепроверьте»), кнопки «Применить»/«Очистить» - меню «Файл → Настройки…»: вкладка API-ключей (DaData, DeepSeek, модель), сохраняются в api_settings.json рядом с программой - веб-проверки показывают источник (morpher.ru / dadata.ru)
267 lines
11 KiB
Python
267 lines
11 KiB
Python
# -*- 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
|