feat: подбор характеристик объекта КИИ — программа и ИИ

Вкладка «Основное», кнопки под полем типового объекта:
- ⚙️ «Подобрать типовой объект (программа)»: скоринг по совпадению
  значимых слов названия/назначения объекта с перечнем 392 типовых
  отраслевых объектов (Распоряжение 360-р), IDF-веса (редкие слова
  важнее общих) → окно с вариантами, оператор выбирает и применяет
  (заполняет сферу + типовой объект)
- 🤖 «Подобрать характеристики (ИИ)»: DeepSeek предлагает назначение,
  сферу, тип, архитектуру и типовой объект (из перечня 360-р) как
  рекомендацию → окно с полями, оператор подтверждает/правит

Примеры: СКУД → «ИС, АСУ, ИТКС, обеспечивающие контроль и управление
доступом на территорию»; 1С:УПП → «ИС для ведения бухгалтерского учета»
This commit is contained in:
2026-08-04 22:43:04 +04:00
parent b9968449ef
commit dadec02665
3 changed files with 389 additions and 0 deletions
+132
View File
@@ -342,3 +342,135 @@ def auto_fill_security(isys, threats=None):
res['tech_measures'] = '\n'.join(tech)
return res
# ---------------------------------------------------------------
# Подбор типового отраслевого объекта КИИ по характеристикам объекта
# ---------------------------------------------------------------
# Стоп-слова: не несут смысла при сопоставлении названия объекта и типового объекта
STOPWORDS = {
'информационные', 'информационно', 'телекоммуникационные', 'автоматизированные',
'системы', 'система', 'сети', 'сеть', 'обеспечивающие', 'обеспечивающих',
'предназначенные', 'предназначенных', 'управление', 'контроль', 'мониторинг',
'объекта', 'объектов', 'объект', 'объекты', 'кии', 'рф', 'российской',
'федерации', 'исполнительной', 'власти', 'органов', 'организаций', 'организации',
'подведомственных', 'функций', 'функции', 'осуществление', 'полномочий',
'обеспечение', 'процессов', 'процесса', 'процессы', 'данных', 'информации',
'сбор', 'хранение', 'обработка', 'представление', 'использование', 'передача',
'автоматизации', 'средств', 'программных', 'программно', 'аппаратных',
'технических', 'деятельность', 'деятельности', 'осуществляющих',
'размещенные', 'размещенных', 'расположенных', 'используемых', 'используемые',
'проведении', 'проведения', 'выполнении', 'выполнения', 'реализация', 'реализации',
'электронных', 'электронного', 'информационных', 'информационного',
# Общие словоформы, встречающиеся почти у всех типовых объектов:
'контроль', 'контроля', 'контролю', 'контролем', 'контроле',
'управление', 'управления', 'управлению', 'управлением', 'управлении',
'безопасность', 'безопасности', 'безопасностью', 'безопасном',
'обеспечение', 'обеспечения', 'обеспечению', 'обеспечением',
'предназначен', 'предназначена', 'предназначено', 'предназначены',
'предоставление', 'предоставления', 'предоставлению',
'взаимодействие', 'взаимодействия', 'взаимодействию',
'функционирование', 'функционирования', 'функционированию',
'деятельность', 'деятельности', 'деятельностью',
'автоматизирован', 'автоматизированной', 'автоматизированных',
'телекоммуникацион', 'телекоммуникационной', 'телекоммуникационных',
'информационн', 'информационной', 'информационных', 'информационное',
'осуществление', 'осуществления', 'осуществлению',
'для', 'систем', 'системы', 'система', 'ресурсов', 'ресурсы',
'целью', 'цели', 'помощи', 'помощь', 'также', 'или', 'иных', 'иное',
'при', 'на', 'по', 'в', 'с', 'и', 'не', 'то', 'числе',
}
def _tokenize(text):
"""Значимые слова текста (без стоп-слов)."""
import re as _re
words = _re.findall(r'[а-яёa-z0-9]+', (text or '').lower())
return [w for w in words if len(w) > 2 and w not in STOPWORDS]
def _load_all_typical(dict_dir=None):
"""Все типовые отраслевые объекты из typical_industry_objects.json (по сферам)."""
import os as _os
if dict_dir is None:
dict_dir = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries')
path = _os.path.join(dict_dir, 'typical_industry_objects.json')
try:
with open(path, encoding='utf-8') as f:
data = json.load(f)
except Exception:
return []
out = []
for sphere, items in data.items():
if sphere == 'Иная сфера деятельности':
continue
for it in items:
out.append({'sphere': sphere, 'num': it.get('num', ''),
'name': it.get('name', '')})
return out
_IDF_CACHE = None
def _idf_weights(cands):
"""IDF-веса слов: чем реже слово в перечне типовых объектов, тем больше вес.
Общие слова («информационные», «управление», «контроль») встречаются почти
у всех 392 объектов и почти ничего не значат; редкие («доступом», «СКУД»,
«сигнализации») — сильно различают объекты.
"""
global _IDF_CACHE
if _IDF_CACHE is not None:
return _IDF_CACHE
import math
df = {}
for c in cands:
for w in set(_tokenize(c['name'])):
df[w] = df.get(w, 0) + 1
n = max(len(cands), 1)
weights = {w: math.log((n + 1) / (df.get(w, 0) + 1)) + 1.0 for w in df}
_IDF_CACHE = weights
return weights
def suggest_typical_object_program(isys, limit=10, sphere=None):
"""Подбор типового отраслевого объекта КИИ по характеристикам объекта.
Логика: значимые слова из названия/назначения/описания объекта КИИ
сопоставляются со значимыми словами названий типовых объектов
(перечень Распоряжения ПРФ № 360-р, 392 объекта). Скоринг по IDF:
редкие слова (например, «доступом», «сигнализации») весят больше общих.
Пример: объект «СКУД» / «контроль и управление доступом» →
«ИС, АСУ, ИТКС, обеспечивающие контроль и управление доступом на территорию».
Возвращает список dict: {sphere, num, name, score} — по убыванию релевантности.
"""
text = ' '.join([
getattr(isys, 'name', '') or '',
getattr(isys, 'purpose', '') or '',
getattr(isys, 'description', '') or '',
])
obj_words = set(_tokenize(text))
if not obj_words:
return []
cands = _load_all_typical()
if sphere:
cands = [c for c in cands if c['sphere'] == sphere]
weights = _idf_weights(cands)
scored = []
for c in cands:
tw = set(_tokenize(c['name']))
common = tw & obj_words
if common:
score = sum(weights.get(w, 1.0) for w in common)
scored.append({'sphere': c['sphere'], 'num': c['num'],
'name': c['name'], 'score': round(score, 2),
'matched': sorted(common)})
scored.sort(key=lambda x: (-x['score'], x['name']))
return scored[:limit]
+200
View File
@@ -868,6 +868,16 @@ class DokoGenApp:
self._typical_objects_all = ['']
self.is_typical_object_combo['values'] = self._typical_objects_all
# Кнопки подбора характеристик: программа (типовой объект) и ИИ (всё)
suggest_row = ttk.Frame(main_frame)
suggest_row.grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=(4, 0))
ttk.Button(suggest_row, text="⚙️ Подобрать типовой объект (программа)",
command=self._suggest_typical_program_click).pack(side=tk.LEFT, padx=2)
ttk.Button(suggest_row, text="🤖 Подобрать характеристики (ИИ)",
command=self._suggest_object_params_ai_click).pack(side=tk.LEFT, padx=2)
ttk.Label(suggest_row, text="Подбор по названию/назначению объекта — как рекомендация, оператор подтверждает.",
foreground='gray').pack(side=tk.LEFT, padx=8)
main_frame.columnconfigure(1, weight=1)
# ===== Вкладка «Субъект» =====
@@ -1554,6 +1564,196 @@ class DokoGenApp:
def _set_is_edit_state(self, enabled):
state = 'normal' if enabled else 'disabled'
def _suggest_typical_program_click(self):
"""⚙️ Подобрать типовой отраслевой объект программой (по названию/назначению объекта)."""
if self._current_is_idx is None:
messagebox.showinfo("Подбор", "Сначала выберите объект КИИ в списке слева")
return
self._collect_is()
isys = self.model.information_systems[self._current_is_idx]
try:
from .threat_rules import suggest_typical_object_program
sphere = self.is_sphere_combo.get() or None
results = suggest_typical_object_program(isys, limit=12, sphere=sphere)
except Exception as e:
messagebox.showerror("Ошибка", str(e))
return
if not results:
messagebox.showinfo("Подбор",
"Не удалось подобрать типовой объект по названию/назначению.\n"
"Попробуйте кнопку «🤖 Подобрать характеристики (ИИ)».")
return
dlg = tk.Toplevel(self.root)
dlg.title("⚙️ Подбор типового отраслевого объекта")
dlg.geometry("860x520")
dlg.transient(self.root)
frm = ttk.Frame(dlg, padding=8)
frm.pack(fill=tk.BOTH, expand=True)
ttk.Label(frm, text=f"Объект: {isys.name}",
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
ttk.Label(frm, text="Наиболее подходящие типовые отраслевые объекты (по совпадению слов "
"с названием/назначением). Выберите и нажмите «Применить».",
foreground='gray', wraplength=820, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6))
lb = tk.Listbox(frm, height=16, exportselection=False)
lb.pack(fill=tk.BOTH, expand=True)
for r in results:
lb.insert(tk.END, f"[{r['score']}] ({r['sphere']}) {r['name']}")
btn_row = ttk.Frame(frm)
btn_row.pack(fill=tk.X, pady=(8, 0))
ttk.Button(btn_row, text="✔️ Применить выбранное",
command=lambda: self._apply_typical_selection(lb, results, dlg)).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2)
def _apply_typical_selection(self, lb, results, dlg):
sel = lb.curselection()
if not sel:
messagebox.showinfo("Подбор", "Выберите типовой объект из списка")
return
r = results[sel[0]]
# Устанавливаем сферу и типовой объект
if hasattr(self, 'is_sphere_combo'):
self.is_sphere_combo.set(r['sphere'])
self._update_typical_objects_by_sphere()
if hasattr(self, 'is_typical_object_combo'):
self.is_typical_object_combo.set(r['name'])
self._collect_is()
self.log(f"⚙️ Типовой отраслевой объект: {r['name'][:70]}")
dlg.destroy()
def _suggest_object_params_ai_click(self):
"""🤖 Подобрать характеристики объекта через ИИ (назначение, сфера, тип, архитектура, типовой объект)."""
if self._current_is_idx is None:
messagebox.showinfo("Подбор", "Сначала выберите объект КИИ в списке слева")
return
settings = load_api_settings()
if not settings.get('ai_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
return
self._collect_is()
isys = self.model.information_systems[self._current_is_idx]
# Перечень типовых объектов для выбора ИИ (всех сфер)
typical_list = []
if hasattr(self, '_typical_industry'):
for sph, items in self._typical_industry.items():
if sph == 'Иная сфера деятельности':
continue
for it in items:
typical_list.append(it.get('name', ''))
data = {
'name': isys.name or '',
'description': isys.description or '',
'purpose': isys.purpose or '',
'sphere': isys.sphere or '',
'object_type': isys.object_type or '',
}
self.log(f"🤖 Запрос к ИИ: подбор характеристик для «{isys.name}»")
self._verify_log("Подбор характеристик через ИИ…")
def _run():
try:
from .verify import suggest_object_params_ai
res, err = suggest_object_params_ai(
data, settings['ai_api_key'], settings.get('ai_model'),
settings.get('ai_base_url'), typical_objects=typical_list)
self.root.after(0, lambda: self._suggest_object_params_ai_done(res, err, isys.name))
except Exception as e:
self.root.after(0, lambda: self._suggest_object_params_ai_done(None, str(e), isys.name))
threading.Thread(target=_run, daemon=True).start()
def _suggest_object_params_ai_done(self, res, err, obj_name):
if err:
self._verify_log(f"{err}")
self.log(f"❌ Подбор характеристик ИИ: {err}")
return
if not res:
self._verify_log("❌ ИИ не вернул характеристики")
return
dlg = tk.Toplevel(self.root)
dlg.title("🤖 Рекомендации ИИ по характеристикам объекта")
dlg.geometry("860x560")
dlg.transient(self.root)
frm = ttk.Frame(dlg, padding=8)
frm.pack(fill=tk.BOTH, expand=True)
ttk.Label(frm, text=f"Объект: {obj_name} — рекомендации ИИ (подтвердите или скорректируйте)",
font=('Arial', 10, 'bold')).pack(anchor=tk.W)
rows = [
('назначение', 'Назначение объекта'),
('сфера', 'Сфера деятельности'),
('тип', 'Тип объекта'),
('архитектура', 'Архитектура объекта'),
('типовой_объект', 'Типовой отраслевой объект КИИ'),
]
entries = {}
for key, label in rows:
ttk.Label(frm, text=label + ":").pack(anchor=tk.W, pady=(6, 0))
e = tk.Text(frm, height=2 if key == 'типовой_объект' else 1, wrap=tk.WORD)
e.pack(fill=tk.X)
e.insert('1.0', res.get(key, '') or '')
entries[key] = e
ttk.Label(frm, text="Обоснование ИИ:").pack(anchor=tk.W, pady=(6, 0))
just = tk.Text(frm, height=2, wrap=tk.WORD)
just.pack(fill=tk.X)
just.insert('1.0', res.get('обоснование', '') or '')
btn_row = ttk.Frame(frm)
btn_row.pack(fill=tk.X, pady=(8, 0))
ttk.Button(btn_row, text="✔️ Применить",
command=lambda: self._apply_ai_params(entries, dlg)).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2)
def _apply_ai_params(self, entries, dlg):
"""Применить рекомендации ИИ к полям объекта."""
def _get(e):
return e.get('1.0', 'end-1c').strip()
purpose = _get(entries['назначение'])
sphere = _get(entries['сфера'])
otype = _get(entries['тип'])
arch = _get(entries['архитектура'])
typical = _get(entries['типовой_объект'])
if purpose and hasattr(self, 'is_purpose_entry'):
self.is_purpose_entry.delete(0, tk.END)
self.is_purpose_entry.insert(0, purpose)
# Сфера — проверим, есть ли в списке
if sphere and hasattr(self, 'is_sphere_combo'):
values = list(self.is_sphere_combo['values'])
exact = next((v for v in values if v.lower() == sphere.lower()), None)
if exact:
self.is_sphere_combo.set(exact)
self._update_typical_objects_by_sphere()
else:
self.is_sphere_combo.set(sphere)
if otype and hasattr(self, 'is_object_type_combo'):
values = list(self.is_object_type_combo['values'])
exact = next((v for v in values if v.lower() == otype.lower()), None)
self.is_object_type_combo.set(exact or otype)
if arch and hasattr(self, 'is_architecture_combo'):
values = list(self.is_architecture_combo['values'])
exact = next((v for v in values if v.lower() == arch.lower()), None)
self.is_architecture_combo.set(exact or arch)
if typical and hasattr(self, 'is_typical_object_combo'):
values = list(self.is_typical_object_combo['values'])
exact = next((v for v in values if v.lower() == typical.lower()), None)
self.is_typical_object_combo.set(exact or typical)
self._collect_is()
self.log("🤖 Рекомендации ИИ применены к характеристикам объекта")
dlg.destroy()
def _on_sphere_changed(self, event=None):
"""При смене сферы объекта — подставляем типовые отраслевые объекты этой сферы (Распоряжение ПРФ № 360-р)."""
self._update_typical_objects_by_sphere()
+57
View File
@@ -519,6 +519,63 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
return threats_out, None
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
typical_objects=None):
"""ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру,
типовой отраслевой объект как рекомендацию оператору.
object_data dict: name, description, purpose (что известно об объекте).
typical_objects перечень типовых отраслевых объектов (из 360-р) для выбора.
Возвращает (dict, ошибка): dict с полями
purpose / sphere / object_type / architecture / typical_object / обоснование.
"""
if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
if typical_objects:
to_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(typical_objects[:400]))
else:
to_text = "(перечень не предоставлен)"
system = (
"Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры "
"(187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). "
"По описанию объекта КИИ предложи его характеристики. Верни ТОЛЬКО JSON: "
'{"назначение": "короткое назначение объекта", '
'"сфера": "одна из сфер: Здравоохранение, Наука, Транспорт, Связь, Энергетика, '
'Государственная регистрация прав, Банковская сфера и финансовый рынок, '
'Топливно-энергетический комплекс, Атомная энергетика, Оборонная промышленность, '
'Ракетно-космическая промышленность, Горнодобывающая промышленность, '
'Металлургическая промышленность, Химическая промышленность, Иная сфера деятельности", '
'"тип": "Информационная система | Автоматизированные системы управления | '
'Информационно-телекоммуникационная сеть", '
'"архитектура": "одно из: клиент-серверная система, автономная (standalone), '
'распределенная система, иное", '
'"типовой_объект": "полная строка из предоставленного перечня, наиболее подходящая, '
'или пустая строка", '
'"обоснование": "почему выбраны такие характеристики (1-2 предложения)"} '
"Не выдумывай типовой объект — бери только из перечня."
)
user = (
"Информация об объекте КИИ:\n"
f"{json.dumps(object_data, ensure_ascii=False)}\n\n"
"Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р):\n"
f"{to_text}"
)
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 приказ).