diff --git a/dokogen/ui.py b/dokogen/ui.py index f6dae1e..c678bcb 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -1066,6 +1066,8 @@ class DokoGenApp: command=self._auto_fill_security_click).pack(side=tk.LEFT, padx=2) ttk.Button(sec_main_btns, text="🤖 Проверить безопасность через ИИ", command=self._check_security_ai).pack(side=tk.LEFT, padx=2) + ttk.Button(sec_main_btns, text="🧩 Проверить объект по частям (ИИ)", + command=self._check_object_partial_click).pack(side=tk.LEFT, padx=2) ttk.Label(sec_main_btns, text="Подбор по данным объекта — как рекомендация, оператор подтверждает.", foreground='gray').pack(side=tk.LEFT, padx=8) @@ -3507,6 +3509,83 @@ class DokoGenApp: self._verify_log("🤖 Проверка категорий ИИ завершена") self.log("🤖 Проверка категорий ИИ завершена") + def _check_object_partial_click(self): + """🧩 ИИ-проверка всего объекта по частям: каждый блок — отдельный запрос, + данные и ответы ИИ пишутся во временную папку (dokogen_ai_*).""" + 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_company() + self._collect_is() + c = self.model + isys = self.model.information_systems[self._current_is_idx] + + blocks = [ + {'name': 'Реквизиты организации', 'prompt': 'check_company', + 'data': {'full_name': c.full_name, 'short_name': c.short_name, 'address': c.address, + 'inn': c.inn, 'kpp': c.kpp, 'ogrn': c.ogrn, 'ogrn_date': c.ogrn_date, + 'chief_position': c.chief_position, 'chief_fio': c.chief_fio}}, + {'name': 'Характеристики объекта КИИ', 'prompt': 'suggest_object_params', + 'data': {'name': isys.name, 'description': isys.description, 'purpose': isys.purpose, + 'sphere': isys.sphere, 'object_type': isys.object_type, + 'architecture': isys.architecture, 'typical_object': isys.typical_object, + 'net_category': isys.net_category, 'net_purpose': isys.net_purpose}}, + {'name': 'Безопасность (нарушитель, угрозы, меры)', 'prompt': 'check_security', + 'data': {'name': isys.name, 'description': isys.description, 'category': isys.category, + 'sphere': isys.sphere, 'attacker_category': isys.attacker_category, + 'attacker_level': isys.attacker_level, 'attacker_types': isys.attacker_types, + 'threats': isys.threats, 'incidents': isys.incidents, + 'org_measures': isys.org_measures, 'tech_measures': isys.tech_measures}}, + {'name': 'Категории значимости (ПП 127)', 'prompt': 'check_criteria', + 'data': {'name': isys.name, 'description': isys.description, 'category': isys.category, + 'sphere': isys.sphere, 'object_type': isys.object_type, + 'criteria_values': isys.criteria_values, + 'criteria_justification': isys.criteria_justification}}, + {'name': 'Средства защиты (СЗИ)', 'prompt': 'suggest_defense', + 'data': {'name': isys.name, 'category': isys.category, 'sphere': isys.sphere, + 'threats': isys.threats, + 'defense_tools': list(self.defense_tools_listbox.get(0, tk.END)) or []}}, + ] + self._verify_log(f"🧩 ИИ-проверка объекта «{isys.name}» по частям ({len(blocks)} блоков)…") + self.log(f"🧩 ИИ-проверка объекта «{isys.name}» по частям: {len(blocks)} блоков") + + def _run(): + try: + from .verify import check_object_partial + res, err = check_object_partial( + blocks, settings['ai_api_key'], settings.get('ai_model'), + settings.get('ai_base_url')) + self.root.after(0, lambda: self._check_object_partial_done(res, err, isys.name)) + except Exception as e: + self.root.after(0, lambda: self._check_object_partial_done(None, str(e), isys.name)) + + threading.Thread(target=_run, daemon=True).start() + + def _check_object_partial_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 + lines = [f"🧩 ИИ-проверка объекта «{obj_name}» по частям", ""] + for r in res: + status = "✅" if r.get('ok') else "❌" + lines.append(f"{status} {r['name']}") + if r.get('files', {}).get('out'): + lines.append(f" ответ ИИ: {r['files']['out']}") + lines.append("") + lines.append("Полные ответы ИИ по каждому блоку — во временных файлах выше.") + lines.append("Рекомендации применяются по кнопкам разделов (по галочкам).") + self._show_security_ai_result(lines) + self._verify_log("🧩 Проверка объекта по частям завершена") + self.log("🧩 Проверка объекта по частям завершена") + def _select_from_defense_list(self): """Выбор СЗИ из JSON словаря.""" import json diff --git a/dokogen/verify.py b/dokogen/verify.py index 8231d74..9c7bc8f 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -816,3 +816,47 @@ def check_criteria_ai(criteria_data, api_key, model='deepseek-chat', base_url=No '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