Files
dokogen187/dokogen/verify.py
T

863 lines
39 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 запросов/день).
- ИИ (OpenAI-совместимый) — ИИ-проверка склонений и реквизитов (DeepSeek, ChatGPT и др.).
Нужен API-ключ и модель. Данные ИИ ВСЕГДА помечаются дисклеймером.
Каждая функция возвращает (result, error):
- result: dict с полями {source, source_url, data, disclaimer, apply}
- error: строка ошибки или None
"""
import json
def _load_prompt(name):
"""Загружает промт из dictionaries/prompts/<name>.json (system/rules/response_format)."""
import os as _os
p = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
'dictionaries', 'prompts', name + '.json')
try:
with open(p, encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
import re
from datetime import datetime, timedelta
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"
DEFAULT_AI_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)
# ============================================================
# НОРМАЛИЗАЦИЯ ТЕКСТА (КАПС → нормальный регистр, сокращения адреса)
# ============================================================
_CAPS_ABBR = {
'АРМ', 'СКУД', 'СЗИ', 'СКЗИ', 'СОВ', 'ЛВС', 'ИНН', 'КПП', 'ОГРН', 'ЦОД',
'МБУ', 'МКУ', 'ООО', 'СНИЛС', 'СБИС', 'ЕМСЭД', 'ФИО', 'ИСПДН', 'ККТ',
}
_ADDRESS_ABBR = {
'Г': 'г.', 'УЛ': 'ул.', 'Д': 'д.', 'КВ': 'кв.', 'ПР': 'пр.',
'ПЕР': 'пер.', 'ПЛ': 'пл.', 'ОБЛ': 'обл.', 'ПОС': 'пос.',
'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.',
'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.',
'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.',
'Г.О': 'г.о.', 'Г.О.': 'г.о.', 'ГО': 'г.о.', 'М.О': 'м.о.', 'М.О.': 'м.о.',
}
# Слова, которые в адресах пишутся со строчной буквы (кроме случаев,
# когда стоят первым словом после точки — «г. Электроугли» и т.п.)
_ADDRESS_LOWERCASE = {
'область', 'край', 'республика', 'округ', 'район', 'город', 'поселок',
'посёлок', 'деревня', 'село', 'станица', 'хутор', 'аул', 'улица',
'проспект', 'переулок', 'бульвар', 'шоссе', 'набережная', 'площадь',
'проезд', 'тупик', 'аллея', 'линия', 'дом', 'строение', 'корпус',
'квартира', 'офис', 'помещение', 'этаж', 'комната', 'участок',
}
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)
res = ''.join(out)
# КАПС → нормальный регистр по словам
res = _normalize_text(res)
# Адресные термины — со строчной буквы (сохраняя пунктуацию)
words = res.split(' ')
fixed = []
for w in words:
m = re.match(r'^([^\w]*)([\w\-]+)([^\w]*)$', w, re.UNICODE)
if m:
pre, core, post = m.group(1), m.group(2), m.group(3)
if core.lower() in _ADDRESS_LOWERCASE:
fixed.append(pre + core.lower() + post)
continue
fixed.append(w)
return ' '.join(fixed)
def resolve_okved(code, dadata_token=None):
"""Код ОКВЭД → «код - название».
1) Ищем в словаре dictionaries/okved.json (строки вида «27.32.2 Название»).
2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен).
3) Иначе возвращаем код как есть.
"""
code = (code or '').strip()
if not code:
return ''
# 1) Словарь
try:
import os, json
dict_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries', 'okved.json')
with open(dict_path, encoding='utf-8') as f:
items = json.load(f)
for item in items:
item = str(item).strip()
# совпадение по началу: «27.32.2» или «27.32.2 - ...» / «27.32.2 ...»
if item.split()[0].rstrip('.') == code.rstrip('.'):
# нормализуем: «код - название»
rest = item[len(item.split()[0]):].strip().lstrip('- ').strip()
if rest:
return f"{code} - {rest}"
return item
except Exception:
pass
# 2) DaData suggest okved2
if dadata_token:
try:
js, err = _http_json('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/okved2',
data={'query': code, 'count': 1},
headers={'Content-Type': 'application/json',
'Authorization': f'Token {dadata_token}'})
if not err and js:
sugg = (js.get('suggestions') or [])
if sugg and sugg[0].get('value'):
return sugg[0]['value']
except Exception:
pass
return code
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 — склонение (бесплатно, без ключа)
# ============================================================
def check_declension_morpher(text):
"""Склонение слова/ФИО/названия через Морфер 3.0."""
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, "Морфер не смог просклонять (возможно, это не слово/ФИО)"
# Морфер не возвращает именительный падеж (он равен исходному слову) —
# добавляем его сами, иначе поле «Именительный» остаётся пустым
if 'И' not in decl:
decl['И'] = text.strip()
return {
'source': 'Морфер 3.0',
'source_url': 'https://morpher.ru',
'disclaimer': '',
'kind': 'declension',
'original': text.strip(),
'data': decl,
'apply': {'declension': decl},
}, None
# ============================================================
# DADATA — проверка организации по ИНН (нужен API-ключ)
# ============================================================
def get_company_finance_dadata(inn, token):
"""Доходы организации по годам через DaData (findById/party → finance/finance_history).
Возвращает (dict, ошибка): dict вида
{'finance': {...}, 'history': [{'year': 2023, 'income': 123456, ...}, ...]}
finance — показатели за последний отчётный год;
history — история по годам (если доступна в тарифе DaData).
"""
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} не найдена"
d = suggestions[0].get('data') or {}
return {
'finance': d.get('finance') or {},
'history': d.get('finance_history') or [],
'inn': d.get('inn', inn),
'name': (d.get('name') or {}).get('short_with_opf') or suggestions[0].get('value', ''),
}, None
def check_company_dadata(inn, token):
"""Проверка организации по ИНН/ОГРН через DaData.
Данные нормализуются: регистр (КАПС → обычный), адрес (г., ул., д.),
дата ОГРН (в т.ч. из миллисекунд).
"""
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 {}
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', ''))
# DaData отдаёт индекс отдельным полем postal_code — добавляем его
# в начало адреса, если его там ещё нет
postal = (address.get('data') or {}).get('postal_code') or address.get('postal_code') or ''
postal = str(postal).strip()
if postal and postal.isdigit():
addr_first = (addr or '').split(',')[0].strip()
if not addr_first.startswith(postal):
addr = f"{postal}, {addr}" if addr else postal
chief_position = _normalize_text(management.get('post', ''))
chief_fio = _normalize_text(management.get('name', ''))
ogrn_date = _fmt_date(d.get('ogrn_date') or '')
# ОКВЭД: основной (okved) и дополнительные (okveds — список словарей с 'code')
okved = d.get('okved', '') or ''
okved_extra = []
for ok in (d.get('okveds') or []):
code = (ok or {}).get('code') or ''
if code:
okved_extra.append(code)
return {
'source': 'DaData',
'source_url': 'https://dadata.ru',
'disclaimer': '',
'kind': 'company',
'query': inn,
'data': {
'full_name': full_name,
'short_name': short_name,
'address': addr,
'kpp': d.get('kpp', ''),
'ogrn': d.get('ogrn', ''),
'ogrn_date': ogrn_date,
'chief_position': chief_position,
'chief_fio': chief_fio,
'inn': d.get('inn', inn),
'okved': okved,
'okved_extra': okved_extra,
},
'apply': {'company': {
'full_name': full_name,
'short_name': short_name,
'address': addr,
'kpp': d.get('kpp', ''),
'ogrn': d.get('ogrn', ''),
'ogrn_date': ogrn_date,
'chief_position': chief_position,
'chief_fio': chief_fio,
'okved': okved,
'okved_extra': okved_extra,
}},
}, None
# ============================================================
# ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.)
# ============================================================
AI_DISCLAIMER = ("⚠ Сгенерировано ИИ. Информацию рекомендуется перепроверить "
"по официальным источникам.")
def _ai_json(api_key, model, base_url, system, user, timeout=60):
"""Запрос к OpenAI-совместимому API (DeepSeek, ChatGPT и т.п.)."""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
url = (base_url or DEFAULT_AI_URL).rstrip('/')
if not url.endswith('/chat/completions'):
url += '/chat/completions'
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(url, data=payload, headers=headers, timeout=timeout)
if err:
return None, err
try:
content = js['choices'][0]['message']['content']
return json.loads(content), None
except Exception:
return None, "ИИ вернул неожиданный ответ"
def check_declension_ai(text, api_key, model='deepseek-chat', base_url=None):
"""ИИ-рецензия склонений: проверяет падежи и предлагает исправления."""
if not text or not text.strip():
return None, "Пустое значение для склонения"
system = (
"Ты — эксперт по русскому языку. Склоняй слово/ФИО/название организации "
"по падежам. Верни ТОЛЬКО JSON: "
'{"И": "...", "Р": "...", "Д": "...", "В": "...", "Т": "...", "П": "...", "комментарий": "..."}'
)
user = f"Просклоняй: {text.strip()}"
js, err = _ai_json(api_key, model, base_url, 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, "ИИ не смог просклонять"
# Именительный падеж равен исходному слову — подставляем, если ИИ его не вернул
if 'И' not in decl:
decl['И'] = text.strip()
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'declension',
'original': text.strip(),
'data': decl,
'comment': comment,
'apply': {'declension': decl},
}, None
# Поля организации для ИИ-проверки (для читаемого вывода)
COMPANY_FIELDS = [
('full_name', 'Полное наименование'),
('short_name', 'Краткое наименование'),
('address', 'Юридический адрес'),
('inn', 'ИНН'),
('kpp', 'КПП'),
('ogrn', 'ОГРН'),
('ogrn_date', 'Дата ОГРН'),
('chief_position', 'Руководитель (должность)'),
('chief_fio', 'Руководитель (ФИО)'),
]
# Поля для сравнения: (ключ, подпись, тип)
# type: text — регистр/пунктуация игнорируются; digits — любое отличие = расхождение
_COMPARE_FIELDS = [
('full_name', 'Наименование', 'text'),
('short_name', 'Краткое наименование', 'text'),
('address', 'Юридический адрес', 'text'),
('inn', 'ИНН', 'digits'),
('kpp', 'КПП', 'digits'),
('ogrn', 'ОГРН', 'digits'),
('ogrn_date', 'Дата ОГРН', 'text'),
('chief_position', 'Руководитель (должность)', 'text'),
('chief_fio', 'Руководитель (ФИО)', 'text'),
]
def _norm_compare(s):
"""Нормализация для сравнения: регистр, ё→е, пробелы/точки/дефисы/кавычки убираются."""
s = (s or '').lower().replace('ё', 'е')
s = re.sub(r'[\s.,;:()«»"\'\-—–]+', '', s)
return s
def _norm_digits(s):
"""Только цифры."""
return re.sub(r'\D', '', s or '')
def compare_company_values(old_data, new_data):
"""Сравнивает старые и новые значения реквизитов организации.
Возвращает список расхождений: [(подпись, было, стало), ...].
- text: разница в регистре/пунктуации (ИВАНОВ И.И. vs Иванов И.И.) НЕ считается;
- digits (ИНН/КПП/ОГРН): любое отличие цифр = расхождение.
"""
diffs = []
for key, label, ftype in _COMPARE_FIELDS:
old = (old_data or {}).get(key) or ''
new = (new_data or {}).get(key) or ''
if not old or not new:
continue
if ftype == 'digits':
if _norm_digits(old) != _norm_digits(new):
diffs.append((label, old, new))
else:
if _norm_compare(old) != _norm_compare(new):
diffs.append((label, old, new))
return diffs
def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=None,
threats=None):
"""ИИ предлагает актуальные угрозы БИ для объекта КИИ по его характеристикам.
security_data — dict: name, description, object_type, sphere, architecture,
attacker_category, has_internet (True/False), etc.
threats — актуальный перечень угроз БДУ ФСТЭК (список строк «УБИ.xxx …»).
Передаётся в промт целиком, чтобы ИИ выбирал из свежего списка,
а не из своего устаревшего.
Возвращает (список_угроз, ошибка).
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
if threats:
threat_list_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(threats))
else:
threat_list_text = "(перечень не предоставлен — используй знания об актуальном перечне БДУ ФСТЭК)"
pr = _load_prompt('suggest_threats')
system = pr.get('system', (
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
"(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из "
"предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации."
))
rules = pr.get('rules', [])
resp = pr.get('response_format',
'Верни ТОЛЬКО JSON: {"threats": ["УБИ.xxx Название угрозы", ...]} '
'— от 10 до 40 угроз, без пояснений.')
user = (
"Характеристики объекта КИИ:\n"
f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
"Актуальный перечень угроз БДУ ФСТЭК (выбирай ТОЛЬКО из него, "
"используй полные строки как они есть):\n"
f"{threat_list_text}\n\n"
+ ("Правила:\n" + "\n".join(f"{i+1}. {r}" for i, r in enumerate(rules)) + "\n\n" if rules else "")
+ resp
)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
threats_out = js.get('threats') or []
if isinstance(threats_out, str):
threats_out = [threats_out]
threats_out = [str(t).strip() for t in threats_out if str(t).strip()]
return threats_out, None
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
typical_objects=None, spheres=None, object_types=None,
architectures=None):
"""ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру,
типовой отраслевой объект — как рекомендацию оператору.
object_data — dict: name, description, purpose (что известно об объекте).
typical_objects — перечень типовых отраслевых объектов (из 360-р) для выбора;
spheres / object_types / architectures — полные списки допустимых значений.
Промт берётся из dictionaries/prompts/suggest_object_params.json.
Возвращает (dict, ошибка): dict с полями
назначение / сфера / тип / архитектура / типовой_объект / обоснование.
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
# Загрузка промта из папки dictionaries/prompts
import os as _os
prompt_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
'dictionaries', 'prompts', 'suggest_object_params.json')
try:
with open(prompt_path, encoding='utf-8') as _f:
prompt = json.load(_f)
except Exception:
prompt = {}
system = prompt.get('system', 'Ты — эксперт ФСТЭК по категорированию объектов КИИ.')
rules = prompt.get('rules', [])
response_format = prompt.get('response_format',
'Верни ТОЛЬКО JSON: {"назначение": "...", "сфера": "...", '
'"тип": "...", "архитектура": "...", "типовой_объект": "...", '
'"обоснование": "..."}')
# Полные списки допустимых значений
def _fmt_list(name, items):
if not items:
return f"{name}: (список не предоставлен)"
return f"{name}:\n" + "\n".join(f"- {i}" for i in items)
lists_text = "\n\n".join([
_fmt_list("Сферы деятельности (выбирай ТОЛЬКО из них)", spheres),
_fmt_list("Типы объектов (выбирай ТОЛЬКО из них)", object_types),
_fmt_list("Архитектуры (выбирай ТОЛЬКО из них)", architectures),
])
if typical_objects:
to_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(typical_objects[:400]))
else:
to_text = "(перечень не предоставлен)"
rules_text = "\n".join(f"{i+1}. {r}" for i, r in enumerate(rules))
user = (
"Информация об объекте КИИ:\n"
f"{json.dumps(object_data, ensure_ascii=False)}\n\n"
f"{lists_text}\n\n"
"Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р, "
"выбирай ТОЛЬКО из него, копируй строку дословно):\n"
f"{to_text}\n\n"
"Правила:\n"
f"{rules_text}\n\n"
f"{response_format}"
)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
return {
'назначение': str(js.get('назначение', '') or '').strip(),
'сфера': str(js.get('сфера', '') or '').strip(),
'тип': str(js.get('тип', '') or '').strip(),
'архитектура': str(js.get('архитектура', '') or '').strip(),
'типовой_объект': str(js.get('типовой_объект', '') or '').strip(),
'обоснование': str(js.get('обоснование', '') or '').strip(),
}, None
def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка заполненной вкладки «Безопасность» объекта КИИ (236 приказ).
Возвращает (результат, ошибка): результат — dict с 'оценка', 'замечания', 'предложения'.
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
pr = _load_prompt('check_security')
system = pr.get('system', (
"Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, "
"приказ ФСТЭК России от 25.12.2017 № 239 и приказ № 236). Проверь заполненные "
"сведения об объекте КИИ и дай рекомендации."
))
system = system + " " + pr.get('response_format',
'Верни ТОЛЬКО JSON: {"оценка": "ok|warning|error", "замечания": ["..."], '
'"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", '
'"criteria_values": "...", "criteria_justification": "...", '
'"org_measures": "...", "tech_measures": "..."}} '
'В «предложениях» укажи, что добавить/исправить по каждому разделу, иначе пустые строки.')
user = json.dumps(security_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, 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': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'security',
'data': {
'rating': js.get('оценка', 'warning'),
'notes': notes,
'proposals': proposals,
},
}, None
def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка реквизитов организации.
Возвращает оценку, замечания, предложения и полный «снимок» данных
(что ИИ увидел и как оценил каждое поле) — чтобы пользователь видел
не только «ОК», но и информацию по организации.
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
system = (
"Ты — юрист по защите персональных данных (187-ФЗ). Проверь реквизиты организации. "
"Верни ТОЛЬКО JSON: "
'{"оценка": "ok|warning|error", '
'"замечания": ["..."], '
'"предложения": {"full_name": "...", "short_name": "...", "address": "...", '
'"inn": "...", "kpp": "...", "ogrn": "...", "ogrn_date": "...", '
'"chief_position": "...", "chief_fio": "..."}, '
'"поля": {"full_name": {"статус": "ok|пусто|ошибка", "комментарий": "..."}, ...}} '
"В «предложениях» указывай исправленные значения только если уверен, иначе пустые строки. "
"В «полях» дай оценку каждого поля (ok/пусто/ошибка) и комментарий."
)
user = json.dumps(company_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, 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 = {}
fields = js.get('поля') or {}
if not isinstance(fields, dict):
fields = {}
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'company',
'data': {
'rating': js.get('оценка', 'warning'),
'notes': notes,
'proposals': proposals,
'fields': fields,
},
'apply': {'company': {k: v for k, v in proposals.items() if v}},
}, None
def suggest_defense_ai(defense_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-рекомендации по средствам защиты информации (СЗИ).
По категории значимости, угрозам и текущему списку СЗИ ИИ подсказывает,
чего не хватает (например, антивирус, СКЗИ, SIEM и т.п.).
Возвращает (результат, ошибка).
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
pr = _load_prompt('suggest_defense')
system = pr.get('system', (
"Ты — эксперт по безопасности КИИ (187-ФЗ, приказ ФСТЭК № 239). "
"По категории значимости объекта и актуальным угрозам определи, каких "
"средств защиты информации (СЗИ) не хватает, и дай рекомендации."
))
system = system + " " + pr.get('response_format',
'Верни ТОЛЬКО JSON: {"рекомендации": [{"сзи": "...", "зачем": "..."}], '
'"общие_советы": ["..."]}')
user = json.dumps(defense_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
return None, err
recs = js.get('рекомендации') or []
if not isinstance(recs, list):
recs = []
tips = js.get('общие_советы') or []
if isinstance(tips, str):
tips = [tips]
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'defense',
'data': {
'рекомендации': recs,
'общие_советы': tips,
},
}, None
def check_criteria_ai(criteria_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка категорий значимости по ПП РФ № 127.
По данным объекта КИИ (назначение, сфера, категории по показателям,
обоснования) ИИ проверяет корректность присвоенных категорий.
Возвращает (результат, ошибка).
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
pr = _load_prompt('check_criteria')
system = pr.get('system', (
"Ты — эксперт по категорированию объектов КИИ (187-ФЗ, ПП РФ № 127). "
"Проверь присвоенные категории значимости по показателям критериев."
))
system = system + " " + pr.get('response_format',
'Верни ТОЛЬКО JSON: {"оценка": "ok|warning|error", '
'"замечания": [{"показатель": "...", "категория": "...", "комментарий": "..."}], '
'"предложения": {"criteria_values": "...", "criteria_justification": "..."}, '
'"итоговая_категория": "I|II|III|—"}')
user = json.dumps(criteria_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, 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': 'ИИ',
'source_url': 'openai-совместимый API',
'disclaimer': AI_DISCLAIMER,
'kind': 'criteria',
'data': {
'rating': js.get('оценка', 'warning'),
'notes': notes,
'proposals': proposals,
'final_category': js.get('итоговая_категория', ''),
},
}, None
def check_object_partial(blocks, api_key, model='deepseek-chat', base_url=None, workdir=None):
"""ИИ-проверка объекта КИИ по частям.
blocks: список [{'name': 'Реквизиты организации', 'prompt': 'check_company', 'data': {...}}, ...]
Для каждого блока: данные пишутся во временный файл (вход), вызывается ИИ,
ответ пишется во временный файл (выход). Возвращает (results, errors):
results — [{name, ok, text, files: {in: путь, out: путь}}].
"""
import tempfile
import os
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
if workdir is None:
workdir = tempfile.mkdtemp(prefix='dokogen_ai_')
results = []
for i, blk in enumerate(blocks, 1):
name = blk.get('name', f'Блок {i}')
prompt_name = blk.get('prompt', 'check_security')
data = blk.get('data', {})
f_in = os.path.join(workdir, f'block_{i}_input.json')
f_out = os.path.join(workdir, f'block_{i}_output.json')
try:
with open(f_in, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception:
pass
pr = _load_prompt(prompt_name)
system = pr.get('system', 'Ты — эксперт по КИИ (187-ФЗ). Проверь данные и дай рекомендации.')
system = system + ' ' + pr.get('response_format', 'Верни ТОЛЬКО JSON с результатом проверки.')
user = json.dumps(data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user)
if err:
results.append({'name': name, 'ok': False, 'text': f'Ошибка: {err}', 'files': {'in': f_in, 'out': f_out}})
continue
try:
with open(f_out, 'w', encoding='utf-8') as f:
json.dump(js, f, ensure_ascii=False, indent=2)
except Exception:
pass
text = json.dumps(js, ensure_ascii=False, indent=2)
results.append({'name': name, 'ok': True, 'text': text, 'files': {'in': f_in, 'out': f_out}})
return results, None