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
+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()