# -*- 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