From e4e7bb444af30ba1b8ba73679e91fa2e6d8cecca Mon Sep 17 00:00:00 2001 From: prog1764 Date: Tue, 4 Aug 2026 23:32:58 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B0=20dictio?= =?UTF-8?q?naries/prompts=20=E2=80=94=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BF=D1=80=D0=BE=D0=BC=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=98=D0=98-=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prompts/suggest_object_params.json: жёсткие правила подбора характеристик — выбирать ТОЛЬКО из предоставленных списков (сферы/типы/архитектуры/ типовые объекты), ничего не выдумывать; назначение строго по форме «Объект предназначен для … с целью …»; типовой объект — дословно из 360-р - prompts/suggest_threats.json, check_security.json, check_company.json, declension.json — промты для остальных ИИ-кнопок - verify.py: _load_prompt() + подключение промтов в suggest_object_params_ai, suggest_threats_ai, check_security_ai; ИИ получает полные списки (spheres/object_types/architectures) и выбирает из них - ui.py: окно рекомендаций ИИ — галочки у каждого поля, применяются только отмеченные (ИИ может ошибаться в архитектуре — оператор решает) --- .../dictionaries/prompts/check_company.json | 8 ++ .../dictionaries/prompts/check_security.json | 4 + dokogen/dictionaries/prompts/declension.json | 9 ++ .../prompts/suggest_object_params.json | 13 ++ .../dictionaries/prompts/suggest_threats.json | 9 ++ dokogen/ui.py | 47 ++++--- dokogen/verify.py | 116 +++++++++++------- 7 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 dokogen/dictionaries/prompts/check_company.json create mode 100644 dokogen/dictionaries/prompts/check_security.json create mode 100644 dokogen/dictionaries/prompts/declension.json create mode 100644 dokogen/dictionaries/prompts/suggest_object_params.json create mode 100644 dokogen/dictionaries/prompts/suggest_threats.json diff --git a/dokogen/dictionaries/prompts/check_company.json b/dokogen/dictionaries/prompts/check_company.json new file mode 100644 index 0000000..bacff46 --- /dev/null +++ b/dokogen/dictionaries/prompts/check_company.json @@ -0,0 +1,8 @@ +{ + "system": "Ты — юрист по защите персональных данных и критической информационной инфраструктуры (187-ФЗ). Проверь реквизиты организации.", + "rules": [ + "Не выдумывай реквизиты — исправляй/дополняй только если уверен.", + "В «полях» для каждого поля укажи статус: ok (заполнено корректно), пусто (не заполнено), ошибка (заполнено с ошибкой)." + ], + "response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [\"...\"], \"предложения\": {\"full_name\": \"...\", \"short_name\": \"...\", \"address\": \"...\", \"inn\": \"...\", \"kpp\": \"...\", \"ogrn\": \"...\", \"ogrn_date\": \"...\", \"chief_position\": \"...\", \"chief_fio\": \"...\"}, \"поля\": {\"full_name\": {\"статус\": \"ok|пусто|ошибка\", \"комментарий\": \"...\"}, ...}}" +} diff --git a/dokogen/dictionaries/prompts/check_security.json b/dokogen/dictionaries/prompts/check_security.json new file mode 100644 index 0000000..caf82c7 --- /dev/null +++ b/dokogen/dictionaries/prompts/check_security.json @@ -0,0 +1,4 @@ +{ + "system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). Проверь заполненные сведения об объекте КИИ и дай рекомендации.", + "response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [\"...\"], \"предложения\": {\"attacker_category\": \"...\", \"threats\": \"...\", \"incidents\": \"...\", \"criteria_values\": \"...\", \"criteria_justification\": \"...\", \"org_measures\": \"...\", \"tech_measures\": \"...\"}}. В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки." +} diff --git a/dokogen/dictionaries/prompts/declension.json b/dokogen/dictionaries/prompts/declension.json new file mode 100644 index 0000000..b603980 --- /dev/null +++ b/dokogen/dictionaries/prompts/declension.json @@ -0,0 +1,9 @@ +{ + "system": "Ты — лингвист, специалист по склонению русских наименований организаций (187-ФЗ). Просклоняй название организации по падежам.", + "rules": [ + "Склоняй только само название (включая организационно-правовую форму, если она склоняется по правилам русского языка).", + "Сохраняй кавычки, регистр и пунктуацию оригинала.", + "Не выдумывай падежи — используй стандартную парадигму: именительный, родительный, дательный, винительный, творительный, предложный." + ], + "response_format": "Верни ТОЛЬКО JSON: {\"nominative\": \"...\", \"genitive\": \"...\", \"dative\": \"...\", \"accusative\": \"...\", \"instrumental\": \"...\", \"prepositional\": \"...\"}" +} diff --git a/dokogen/dictionaries/prompts/suggest_object_params.json b/dokogen/dictionaries/prompts/suggest_object_params.json new file mode 100644 index 0000000..242a6c3 --- /dev/null +++ b/dokogen/dictionaries/prompts/suggest_object_params.json @@ -0,0 +1,13 @@ +{ + "system": "Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры (187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). Твоя задача — по описанию объекта КИИ ПОДОБРАТЬ его характеристики ТОЛЬКО из предоставленных списков, ничего не выдумывая.", + "rules": [ + "Выбирай значения ТОЛЬКО из предоставленных списков. Если подходящего значения в списке нет — верни пустую строку, не придумывай своё.", + "Поле «типовой_объект»: выбери полную строку ТОЛЬКО из предоставленного перечня типовых отраслевых объектов (Распоряжение ПРФ № 360-р), наиболее подходящую. Не перефразируй, не сокращай — копируй строку дословно. Если ничего не подходит — пустая строка.", + "Поле «сфера»: выбери ТОЛЬКО одну из предоставленных сфер. Не придумывай свои.", + "Поле «тип»: выбери ТОЛЬКО один из предоставленных типов объектов.", + "Поле «архитектура»: выбери ТОЛЬКО одну из предоставленных архитектур.", + "Поле «назначение»: заполни строго по форме: «Объект предназначен для <что делает> с целью <зачем>.». Не отклоняйся от этой формы.", + "Поле «обоснование»: 1-2 предложения, почему выбраны такие характеристики." + ], + "response_format": "Верни ТОЛЬКО JSON без пояснений: {\"назначение\": \"...\", \"сфера\": \"...\", \"тип\": \"...\", \"архитектура\": \"...\", \"типовой_объект\": \"...\", \"обоснование\": \"...\"}" +} diff --git a/dokogen/dictionaries/prompts/suggest_threats.json b/dokogen/dictionaries/prompts/suggest_threats.json new file mode 100644 index 0000000..994636c --- /dev/null +++ b/dokogen/dictionaries/prompts/suggest_threats.json @@ -0,0 +1,9 @@ +{ + "system": "Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации.", + "rules": [ + "Выбирай угрозы ТОЛЬКО из предоставленного перечня — не выдумывай и не перефразируй, используй полные строки (код + название) как они есть.", + "Верни от 10 до 40 угроз.", + "Учитывай характеристики объекта: тип (ИС/АСУ/ИТКС), наличие интернета, сферу, категорию нарушителя." + ], + "response_format": "Верни ТОЛЬКО JSON: {\"threats\": [\"УБИ.xxx Название угрозы\", ...]} — без пояснений и комментариев." +} diff --git a/dokogen/ui.py b/dokogen/ui.py index bfd7930..d3ec04e 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -1671,6 +1671,10 @@ class DokoGenApp: 'sphere': isys.sphere or '', 'object_type': isys.object_type or '', } + # Полные списки допустимых значений — ИИ выбирает ТОЛЬКО из них + spheres = list(self.is_sphere_combo['values']) if hasattr(self, 'is_sphere_combo') else [] + object_types = list(self.is_object_type_combo['values']) if hasattr(self, 'is_object_type_combo') else [] + architectures = list(self.is_architecture_combo['values']) if hasattr(self, 'is_architecture_combo') else [] self.log(f"🤖 Запрос к ИИ: подбор характеристик для «{isys.name}»") self._verify_log("Подбор характеристик через ИИ…") @@ -1679,7 +1683,8 @@ class DokoGenApp: 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) + settings.get('ai_base_url'), typical_objects=typical_list, + spheres=spheres, object_types=object_types, architectures=architectures) 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)) @@ -1697,28 +1702,35 @@ class DokoGenApp: dlg = tk.Toplevel(self.root) dlg.title("🤖 Рекомендации ИИ по характеристикам объекта") - dlg.geometry("860x560") + dlg.geometry("880x620") 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) + ttk.Label(frm, text=f"Объект: {obj_name} — рекомендации ИИ. Отметьте галочками, " + f"какие поля применить (ИИ может ошибаться — проверяйте).", + font=('Arial', 10, 'bold'), wraplength=840, justify=tk.LEFT).pack(anchor=tk.W) rows = [ - ('назначение', 'Назначение объекта'), + ('назначение', 'Назначение объекта (форма: «Объект предназначен для … с целью …»)'), ('сфера', 'Сфера деятельности'), ('тип', 'Тип объекта'), ('архитектура', 'Архитектура объекта'), ('типовой_объект', 'Типовой отраслевой объект КИИ'), ] entries = {} + checks = {} 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) + row = ttk.Frame(frm) + row.pack(fill=tk.X, pady=(6, 0)) + var = tk.BooleanVar(value=True) + cb = ttk.Checkbutton(row, text=label, variable=var) + cb.pack(anchor=tk.W) + e = tk.Text(frm, height=2 if key in ('типовой_объект', 'назначение') else 1, wrap=tk.WORD) e.pack(fill=tk.X) e.insert('1.0', res.get(key, '') or '') entries[key] = e + checks[key] = var ttk.Label(frm, text="Обоснование ИИ:").pack(anchor=tk.W, pady=(6, 0)) just = tk.Text(frm, height=2, wrap=tk.WORD) @@ -1727,20 +1739,23 @@ class DokoGenApp: 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=lambda: self._apply_ai_params(entries, dlg, checks)).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 _apply_ai_params(self, entries, dlg, checks=None): + """Применить рекомендации ИИ к полям объекта (только отмеченные галочками).""" 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['типовой_объект']) + def _wanted(key): + return checks is None or checks.get(key, tk.BooleanVar(value=True)).get() + + purpose = _get(entries['назначение']) if _wanted('назначение') else '' + sphere = _get(entries['сфера']) if _wanted('сфера') else '' + otype = _get(entries['тип']) if _wanted('тип') else '' + arch = _get(entries['архитектура']) if _wanted('архитектура') else '' + typical = _get(entries['типовой_объект']) if _wanted('типовой_объект') else '' if purpose and hasattr(self, 'is_purpose_entry'): self.is_purpose_entry.delete(0, tk.END) diff --git a/dokogen/verify.py b/dokogen/verify.py index 77338c4..cdee3df 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -14,6 +14,18 @@ """ import json + +def _load_prompt(name): + """Загружает промт из dictionaries/prompts/.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 @@ -526,20 +538,24 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N else: threat_list_text = "(перечень не предоставлен — используй знания об актуальном перечне БДУ ФСТЭК)" - system = ( + pr = _load_prompt('suggest_threats') + system = pr.get('system', ( "Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры " "(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из " - "предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации и верни " - "ТОЛЬКО JSON: " - '{"threats": ["УБИ.xxx Название угрозы", ...]} — от 10 до 40 угроз, ' - "без пояснений и комментариев. Не выдумывай угрозы, которых нет в перечне; " - "используй полные строки из перечня (код + название) как они есть." - ) + "предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации." + )) + 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" + 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: @@ -552,48 +568,66 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None, - typical_objects=None): + typical_objects=None, spheres=None, object_types=None, + architectures=None): """ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру, типовой отраслевой объект — как рекомендацию оператору. object_data — dict: name, description, purpose (что известно об объекте). - typical_objects — перечень типовых отраслевых объектов (из 360-р) для выбора. + typical_objects — перечень типовых отраслевых объектов (из 360-р) для выбора; + spheres / object_types / architectures — полные списки допустимых значений. + Промт берётся из dictionaries/prompts/suggest_object_params.json. Возвращает (dict, ошибка): dict с полями - purpose / sphere / object_type / architecture / typical_object / обоснование. + назначение / сфера / тип / архитектура / типовой_объект / обоснование. """ 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 = "(перечень не предоставлен)" - system = ( - "Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры " - "(187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). " - "По описанию объекта КИИ предложи его характеристики. Верни ТОЛЬКО JSON: " - '{"назначение": "короткое назначение объекта", ' - '"сфера": "одна из сфер: Здравоохранение, Наука, Транспорт, Связь, Энергетика, ' - 'Государственная регистрация прав, Банковская сфера и финансовый рынок, ' - 'Топливно-энергетический комплекс, Атомная энергетика, Оборонная промышленность, ' - 'Ракетно-космическая промышленность, Горнодобывающая промышленность, ' - 'Металлургическая промышленность, Химическая промышленность, Иная сфера деятельности", ' - '"тип": "Информационная система | Автоматизированные системы управления | ' - 'Информационно-телекоммуникационная сеть", ' - '"архитектура": "одно из: клиент-серверная система, автономная (standalone), ' - 'распределенная система, иное", ' - '"типовой_объект": "полная строка из предоставленного перечня, наиболее подходящая, ' - 'или пустая строка", ' - '"обоснование": "почему выбраны такие характеристики (1-2 предложения)"} ' - "Не выдумывай типовой объект — бери только из перечня." - ) + 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" - "Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р):\n" - f"{to_text}" + 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: @@ -615,18 +649,18 @@ def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=No """ if not api_key: return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" - system = ( + pr = _load_prompt('check_security') + system = pr.get('system', ( "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, " - "приказ ФСТЭК России от 25.12.2017 № 239 «Об утверждении Требований по обеспечению " - "безопасности значимых объектов КИИ» и приказ № 236). Проверь заполненные сведения " - "об объекте КИИ и дай рекомендации. Верни ТОЛЬКО JSON: " - '{"оценка": "ok|warning|error", ' - '"замечания": ["..."], ' + "приказ ФСТЭК России от 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: